Merge PR #1509: Spelling: "we", "if", "email" and contractions with a typographer's apostrophe aren't misspelled

This commit is contained in:
aiongg
2026-10-30 19:46:49 +00:00
2 changed files with 131 additions and 5 deletions
+1
View File
@@ -469,6 +469,7 @@ Newest first.
- **2026-10-21** · Insert Pages… opens InDesign's dialog: Pages, Insert (After Page, Before Page, At Start of Document, At End of Document) with the page by name, and Parent ([None] or a parent), set to the page in view and its parent. It was a form asking for a page index and a parent id, and pages could not go before the first page; `layout.pages.insert` takes `before` (0: at the start). Found in the Learn Skills Daily course, chapter 25. No readiness percentage change.
- **2026-10-18** · Table of Contents offers More Options / Fewer Options, preserving page-number, separator, sorting and level settings while their controls are hidden. No readiness percentage change.
- **2026-10-18** · Table of Contents reserves the title field's width so Contents is readable next to its style picker; the grid previously squeezed it to a few characters. No readiness percentage change.
- **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-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
+130 -5
View File
@@ -81,13 +81,91 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
/// 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();