CP6.13: craft-libs#487 review — a trailing combining mark stays on its word; Italian and Portuguese look a hyphenated word up whole before its parts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-11-02 00:27:37 +00:00
1 parent be52680c9c
commit 64bd43db1d
3 files changed
+32 -3

No files matched your search

+10 -1
View File
@@ -95,8 +95,9 @@ fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool {
if TRUNCATIONS.contains(&lower.as_str()) || left_to_grammar(&lower) {
return true;
}
// The whole word first, as Hunspell does: a hyphenated entry or user word.
if w.contains('-') {
return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p));
return known(d, w) || w.split('-').all(|p| p.chars().count() <= 1 || known(d, p));
}
if let Some((head, tail)) = w.split_once(['\'', '’'])
&& PROCLITICS.contains(&head.to_lowercase().as_str())
@@ -139,6 +140,14 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// craft-libs#487 review: a hyphenated dictionary entry is a word even when its parts aren't.
#[test]
fn a_hyphenated_entry_is_looked_up_whole() {
let d = AffixDict::from_hunspell("SET UTF-8\n", "1\ntic-tac\n", &[]).unwrap();
assert!(is_correct_with(&d, "tic-tac"));
assert!(!is_correct_with(&d, "tic-tacc"));
}
#[test]
fn without_a_dictionary_nothing_is_flagged() {
let none = AffixDict::default();
+10 -1
View File
@@ -71,8 +71,9 @@ fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool {
let Some(w) = crate::spelling::word_to_check(d, word, opts) else {
return true;
};
// The whole word first, as Hunspell does: a hyphenated entry or user word.
if w.contains('-') {
return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p));
return known(d, w) || w.split('-').all(|p| p.chars().count() <= 1 || known(d, p));
}
known(d, w)
}
@@ -96,6 +97,14 @@ mod tests {
assert!(!d.rep().is_empty() && !d.map().is_empty() && !d.try_chars().is_empty());
}
/// craft-libs#487 review: a hyphenated dictionary entry is a word even when its parts aren't.
#[test]
fn a_hyphenated_entry_is_looked_up_whole() {
let d = AffixDict::from_hunspell("SET UTF-8\n", "1\ntic-tac\n", &[]).unwrap();
assert!(is_correct_with(&d, "tic-tac"));
assert!(!is_correct_with(&d, "tic-tacc"));
}
#[test]
fn without_a_dictionary_nothing_is_flagged() {
let none = AffixDict::default();
+12 -1
View File
@@ -24,7 +24,9 @@ pub(crate) fn word_to_check_in<'a>(d: &AffixDict, word: &'a str, opts: &Options)
if d.is_empty() {
return None;
}
let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’');
// A trailing combining mark with no precomposed form (`x\u{338}`) is part of the word.
let edge = |c: char| !c.is_alphanumeric() && c != '\'' && c != '’';
let w = word.trim_start_matches(edge).trim_end_matches(|c| edge(c) && !unicode_normalization::char::is_combining_mark(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) {
@@ -56,4 +58,13 @@ mod tests {
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");
}
/// craft-libs#487 review: a trailing combining mark stays on the word it marks.
#[test]
fn trailing_combining_marks_are_kept() {
let d = AffixDict::from_hunspell("", "1\nkot\n", &[]).unwrap();
let defaults = Options::default();
assert_eq!(word_to_check(&d, "kox\u{338}", &defaults), Some("kox\u{338}"));
assert_eq!(word_to_check(&d, "(kox\u{338}).", &defaults), Some("kox\u{338}"));
}
}