diff --git a/crates/proof/src/cs/mod.rs b/crates/proof/src/cs/mod.rs index 1d72dc91..9f8bbd0a 100644 --- a/crates/proof/src/cs/mod.rs +++ b/crates/proof/src/cs/mod.rs @@ -38,11 +38,6 @@ pub fn has_dictionary() -> bool { DICT.has_dictionary() } -/// Is `c` a letter of the Latin script (the only one the Czech dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in Czech? Numbers, single letters, short words in capitals /// (acronyms: `ČR`, `EU`), words with digits and words in other scripts are accepted, and so /// is every word when no Czech dictionary is installed. @@ -64,21 +59,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Czech dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; if w.contains('-') { return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); } diff --git a/crates/proof/src/es/mod.rs b/crates/proof/src/es/mod.rs index e16e9d6b..5263bd1d 100644 --- a/crates/proof/src/es/mod.rs +++ b/crates/proof/src/es/mod.rs @@ -77,11 +77,6 @@ pub fn has_dictionary(lang: ProofLang) -> bool { slot(lang).has_dictionary() } -/// Is `c` a letter of the Latin script (the only one the Spanish dictionaries cover)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in `lang`'s Spanish? Numbers, single letters, short words in /// capitals (acronyms: `ONU`, `CDMX`), words with digits and words in other scripts are accepted, /// and so is every word when no dictionary is installed for the variety. Hyphenated compounds @@ -105,21 +100,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str, lang: ProofLang) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, lang: ProofLang, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Spanish dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; if w.contains('-') { return known(d, w, lang) || w.split('-').all(|p| p.chars().count() <= 1 || known(d, p, lang)); } diff --git a/crates/proof/src/hr/mod.rs b/crates/proof/src/hr/mod.rs index 036b7235..53edf067 100644 --- a/crates/proof/src/hr/mod.rs +++ b/crates/proof/src/hr/mod.rs @@ -40,11 +40,6 @@ pub fn has_dictionary() -> bool { DICT.has_dictionary() } -/// Is `c` a letter of the Latin script (the only one the Croatian dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in Croatian? Numbers, single letters, short words in capitals /// (acronyms: `HRT`, `RH`), words with digits and words in other scripts are accepted, and so /// is every word when no Croatian dictionary is installed. @@ -66,21 +61,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Croatian dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; if w.contains('-') { return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); } diff --git a/crates/proof/src/id/mod.rs b/crates/proof/src/id/mod.rs index f1e44df4..04248d63 100644 --- a/crates/proof/src/id/mod.rs +++ b/crates/proof/src/id/mod.rs @@ -45,11 +45,6 @@ pub fn has_dictionary() -> bool { /// (`se-Indonesia`, `KTP-nya`, `Jakarta-lah`); they aren't dictionary words on their own. const CLITICS: &[&str] = &["se", "nya", "ku", "mu", "lah", "kah", "pun"]; -/// Is `c` a letter of the Latin script (the only one the Indonesian dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in Indonesian? Numbers, single letters, short words in capitals /// (acronyms: `DPR`, `KTP`), words with digits and words in other scripts are accepted, and so /// is every word when no Indonesian dictionary is installed. A hyphenated word is correct when @@ -73,21 +68,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Indonesian dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; if w.contains('-') { return known(d, w) || w.split('-').all(|p| { diff --git a/crates/proof/src/it/mod.rs b/crates/proof/src/it/mod.rs index 1e606ad7..ee5aaaee 100644 --- a/crates/proof/src/it/mod.rs +++ b/crates/proof/src/it/mod.rs @@ -49,11 +49,6 @@ pub fn has_dictionary() -> bool { DICT.has_dictionary() } -/// Is `c` a letter of the Latin script (the only one the Italian dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Proclitics (and verb clippings) that elide before a vowel or `h`: the word after the /// apostrophe is what gets checked (`l'amico`, `un'idea`, `c'è`, `m'hai`, `dov'è`, `anch'io`). const PROCLITICS: &[&str] = &[ @@ -93,21 +88,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Italian dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; let lower = w.to_lowercase(); if TRUNCATIONS.contains(&lower.as_str()) || left_to_grammar(&lower) { return true; diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index 0c51e236..365229d7 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -67,6 +67,7 @@ pub mod pl; pub mod pt; pub mod ru; mod slot; +mod spelling; pub mod tr; pub mod vi; diff --git a/crates/proof/src/pl/mod.rs b/crates/proof/src/pl/mod.rs index 30d6d966..7cde9d0f 100644 --- a/crates/proof/src/pl/mod.rs +++ b/crates/proof/src/pl/mod.rs @@ -52,11 +52,6 @@ pub fn has_dictionary() -> bool { /// (`Kennedy'ego`, `Wilde'a`, `Shakespeare'owi`). const APOSTROPHE_ENDINGS: &[&str] = &["a", "ach", "ami", "e", "ego", "em", "emu", "i", "m", "om", "owi", "owie", "u", "y", "ów", "ą", "ę"]; -/// Is `c` a letter of the Latin script (the only one the Polish dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in Polish? Numbers, single letters, short words in capitals /// (acronyms: `PKO`, `ZUS`), words with digits and words in other scripts are accepted, and so /// is every word when no Polish dictionary is installed. @@ -78,21 +73,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Polish dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; let w = w.replace('’', "'"); if w.contains('-') { return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); diff --git a/crates/proof/src/pt/mod.rs b/crates/proof/src/pt/mod.rs index 0110a8c1..ea91dbdb 100644 --- a/crates/proof/src/pt/mod.rs +++ b/crates/proof/src/pt/mod.rs @@ -47,11 +47,6 @@ pub fn has_dictionary() -> bool { DICT.has_dictionary() } -/// Is `c` a letter of the Latin script (the only one the Portuguese dictionary covers)? -fn is_latin_letter(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - /// Is `word` spelled correctly in Portuguese? Numbers, single letters, short words in capitals /// (acronyms: `USP`, `PT`), words with digits and words in other scripts are accepted, and so /// is every word when no Portuguese dictionary is installed. @@ -73,21 +68,9 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { /// [`is_correct_with_options`] against a given dictionary. fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Portuguese dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + let Some(w) = crate::spelling::word_to_check(d, word, opts) else { return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return true; - } - if !w.chars().any(is_latin_letter) { - return true; // other scripts: no dictionary - } + }; if w.contains('-') { return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); } diff --git a/crates/proof/src/spelling.rs b/crates/proof/src/spelling.rs new file mode 100644 index 00000000..ab5e520e --- /dev/null +++ b/crates/proof/src/spelling.rs @@ -0,0 +1,52 @@ +//! The start of every Latin-script dictionary language's spelling check (Czech, Croatian, +//! Indonesian, Italian, Polish, Portuguese, Spanish): the words that need no lookup, under the +//! File › Options › Proofing choices. Each language goes on from there with its own rules. + +use crate::Options; +use crate::affix::AffixDict; + +/// Is `c` a letter of the Latin script (the only one these dictionaries cover)? +fn latin(c: char) -> bool { + c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) +} + +/// `word` trimmed of quotes and apostrophes, when `d` must say whether it is right; `None` when it +/// is right without a lookup: there is no dictionary (no spelling marks), the word is a single +/// letter or has no letters, it has digits or is a short word in capitals (an acronym) and the +/// options skip those, or it is in another script. +pub(crate) fn word_to_check<'a>(d: &AffixDict, word: &'a str, opts: &Options) -> Option<&'a str> { + if d.is_empty() { + return None; + } + let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); + let w = w.trim_matches(['\'', '’']); + let n = w.chars().count(); + if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { + return None; + } + if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { + return None; + } + w.chars().any(latin).then_some(w) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn words_needing_no_lookup() { + let d = AffixDict::from_hunspell("", "1\nkot\n", &[]).unwrap(); + let defaults = Options::default(); + assert_eq!(word_to_check(&AffixDict::default(), "kot", &defaults), None, "no dictionary"); + for w in ["", "a", "2026", "PKO", "B2B", "日本語", "--"] { + assert_eq!(word_to_check(&d, w, &defaults), None, "{w}"); + } + assert_eq!(word_to_check(&d, "„kot”", &defaults), Some("kot")); + assert_eq!(word_to_check(&d, "'kot'", &defaults), Some("kot")); + let strict = Options { ignore_uppercase: false, ignore_numbers: false, ..defaults }; + assert_eq!(word_to_check(&d, "PKO", &strict), Some("PKO")); + assert_eq!(word_to_check(&d, "B2B", &strict), Some("B2B")); + assert_eq!(word_to_check(&d, "PRZEDSIĘBIORSTWO", &defaults), Some("PRZEDSIĘBIORSTWO"), "only short capitals are acronyms"); + } +}