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();