From 51090dd6e5e6ea1a27fc11f3525323839ae4ff28 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 01:47:38 -0700 Subject: [PATCH] Proofing: web addresses after a no-break space are not marked as misspelled The spelling check found the start of the white-space-delimited token around a word one byte after the last white-space character. After a no-break space (two bytes in UTF-8) or an ideographic space (three bytes) that offset is inside the character, so the token could not be read and was never classified as a web or e-mail address. With Ignore web and file addresses on, the words of such an address were still marked as misspelled. The token now starts after the whole white-space character. A proofing test covers a web address, a www. address and an e-mail address after the two spaces, with the option on and off. --- crates/proof/src/lib.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index de596782..65190cab 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -693,7 +693,9 @@ fn spelling_issues(text: &str, lang: impl Fn(usize) -> ProofLang, opts: &Options /// The whitespace-delimited token enclosing the word at `a..b` of `text`: its byte range, and /// whether it looks like a web or e-mail address. fn enclosing_token(text: &str, a: usize, b: usize) -> (usize, usize, bool) { - let ts = text.get(..a).and_then(|t| t.rfind(char::is_whitespace)).map(|i| i + 1).unwrap_or(0); + // The token starts after the last white-space character, whatever its width (a no-break + // space is two bytes and an ideographic space is three). + let ts = text.get(..a).and_then(|t| t.char_indices().rev().find(|(_, c)| c.is_whitespace())).map(|(i, c)| i + c.len_utf8()).unwrap_or(0); let te = text.get(b..).and_then(|t| t.find(char::is_whitespace)).map(|i| b + i).unwrap_or(text.len()); let token = text.get(ts..te).unwrap_or(""); (ts, te, token.contains("://") || token.contains('@') || token.starts_with("www.")) @@ -1431,6 +1433,18 @@ mod tests { assert_eq!(bad.last(), Some(&"wugwug"), "{bad:?}"); } + #[test] + fn address_after_a_wide_space_is_an_internet_token() { + // A web or e-mail address after a no-break space or an ideographic space is skipped, + // as it is after a plain space. + let t = "see\u{a0}https://xyzq.example/blorp and\u{3000}www.qwirpy.example or\u{a0}zxq@mail.example wugwug"; + let bad: Vec<&str> = check_spelling(t).iter().map(|i| &t[i.start..i.end]).collect(); + assert_eq!(bad, vec!["wugwug"]); + let opts = Options { ignore_internet: false, ..Options::default() }; + let bad: Vec<&str> = check_spelling_with(t, &opts).iter().map(|i| &t[i.start..i.end]).collect(); + assert!(["xyzq", "blorp", "qwirpy", "zxq", "wugwug"].iter().all(|w| bad.contains(w)), "{bad:?}"); + } + #[test] fn spelling_scales_over_one_long_token() { // #1335: thousands of punctuation-separated words inside a single