From c0e38160d6f187becb656543a2bbde9b89bbf650 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:50:19 +0200 Subject: [PATCH] Spelling: "we", "if", "email" and curly-apostrophe contractions are known MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spell checker uses the Moby Hyphenator II word list, which lacks some very common short words (we, us, if, Mr, pm, vs) and most computer and web vocabulary (email, online, website, database, desktop, download...). Contractions typed with a typographer's apostrophe (don’t, we’re), which Use Typographer's Quotes produces by default, were looked up with the curly apostrophe and flagged too. Dynamic Spelling underlined most words of an ordinary sentence. `known` now reads U+2019 as a straight apostrophe (also in the user dictionary) and accepts a short sorted list of such common words, with the existing inflection rules applied to them. Found in the Learn Skills Daily course, chapter 26. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/spelling.rs | 135 ++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..d4cf65258 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-21** · Spelling (Check Spelling and Dynamic Spelling) no longer flags everyday words the bundled hyphenation word list lacks: "we", "us", "if", titles such as "Mr", and words such as email, online, website, database, desktop and download, with their plurals and other forms. Contractions typed with a typographer's apostrophe (don’t, we’re), which Use Typographer's Quotes produces by default, count like their straight-apostrophe forms. Dynamic Spelling used to underline most words of an ordinary sentence. Found in the Learn Skills Daily course, chapter 26. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/engine/src/cmd/spelling.rs b/crates/engine/src/cmd/spelling.rs index 914165387..c799f9b1f 100644 --- a/crates/engine/src/cmd/spelling.rs +++ b/crates/engine/src/cmd/spelling.rs @@ -81,13 +81,91 @@ pub fn specs() -> Vec { ] } +/// Common words the hyphenation word list (from the early 1990s) lacks: everyday short words +/// such as "we", "us" and "if", titles, and words that came with computers and the web. Sorted, +/// lowercase, base forms (the inflections below reach "emails", "downloaded", "bloggers"). +const COMMON: &[&str] = &[ + "ah", + "bitmap", + "blog", + "bluetooth", + "chatbot", + "checkbox", + "cybersecurity", + "database", + "dataset", + "desktop", + "download", + "dr", + "dropdown", + "ebook", + "ecommerce", + "eh", + "email", + "emoji", + "firewall", + "gigabyte", + "hashtag", + "hm", + "hmm", + "homepage", + "hyperlink", + "if", + "inbox", + "internet", + "jr", + "kilobyte", + "laptop", + "livestream", + "login", + "logout", + "malware", + "megabyte", + "metadata", + "mr", + "offline", + "ok", + "online", + "ow", + "pm", + "podcast", + "popup", + "printout", + "screenshot", + "selfie", + "smartphone", + "smartwatch", + "spreadsheet", + "sr", + "startup", + "terabyte", + "touchscreen", + "um", + "upload", + "us", + "username", + "voicemail", + "vs", + "we", + "webinar", + "webmaster", + "webpage", + "website", + "wifi", + "workflow", + "yay", +]; + /// Is `w` (lowercase) a known word, allowing common inflections of dictionary stems? pub fn known(dict: &Dictionary, user: &[String], w: &str) -> bool { - if w.len() <= 1 || dict.get(w).is_some() || user.iter().any(|u| u == w) { + // A typographer's apostrophe (don’t, we’re) is the word list's straight one. + let w = &w.replace('’', "'"); + let has = |w: &str| dict.get(w).is_some() || COMMON.binary_search(&w).is_ok(); + if w.len() <= 1 || has(w) || user.iter().any(|u| u.replace('’', "'") == *w) { return true; } - let w = w.trim_end_matches("'s").trim_end_matches("’s"); - if dict.get(w).is_some() { + let w = w.trim_end_matches("'s"); + if has(w) { return true; } let stems: [(&str, &[&str]); 9] = [ @@ -105,14 +183,14 @@ pub fn known(dict: &Dictionary, user: &[String], w: &str) -> bool { if let Some(base) = w.strip_suffix(suf) { for add in adds { let cand = format!("{base}{add}"); - if cand.len() > 1 && dict.get(&cand).is_some() { + if cand.len() > 1 && has(&cand) { return true; } // Doubled consonant: "stopped" → "stop". if let Some(c) = base.chars().last() && base.len() > 2 && base.ends_with(&format!("{c}{c}")) - && dict.get(&base[..base.len() - c.len_utf8()]).is_some() + && has(&base[..base.len() - c.len_utf8()]) { return true; } @@ -253,6 +331,53 @@ mod tests { assert_eq!(bad, vec![6..10]); } + /// Everyday words the hyphenation list lacks aren't misspelled ("we", "us", "if", "email"…), + /// with their inflections, and neither are contractions typed with a typographer's + /// apostrophe, which Use Typographer's Quotes turns ' into. + #[test] + fn common_words_and_curly_apostrophes_are_known() { + let d = Dictionary::en_us(); + if d.is_empty() { + return; // data not bundled in this build + } + assert!(COMMON.is_sorted(), "COMMON is binary-searched"); + for w in [ + "we", + "us", + "if", + "ok", + "mr", + "email", + "emails", + "emailed", + "online", + "website", + "websites", + "downloaded", + "database", + "databases", + "blogger", + "don’t", + "can’t", + "i’m", + "we’re", + "they’ve", + "it’s", + "don't", + ] { + assert!(known(d, &[], w), "{w}"); + } + for w in ["dont", "wee’re", "emial"] { + assert!(!known(d, &[], w), "{w}"); + } + // A user word added with a typographer's apostrophe matches either form. + assert!(known(d, &["zorbo’s".into()], "zorbo's") && known(d, &["zorbo's".into()], "zorbo’s")); + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + s.execute("frame.create", &json!({"rect": [36, 36, 300, 200], "content": "text", "text": "We don’t email us if we’re online."})).unwrap(); + assert_eq!(s.execute("spelling.check", &json!({})).unwrap(), json!([])); + } + #[test] fn check_finds_misspellings() { let mut s = Session::new();