From 00405dba63598afdb0f3afdac5d42962133e2f27 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. --- ROADMAP.md | 1 + crates/proof/src/lib.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 802e12c64..d930fb4ee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,6 +139,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-10-22: Mail merge (#3028): Preview Results has First Record and Last Record buttons and a box with the previewed record number; type a number and press Enter to go there (past either end stops at the first or last record). Finish & Merge is now a menu. Edit Individual Documents… and Print Documents… ask for all records, the current record, or a From and To range by record number, and Print Documents opens Print for the merged document. Scripts get `mailings.first`, `mailings.last`, `mailings.goToRecord`, `mailings.printDocuments` and a `records` choice on `mailings.finish`; a range with no records in it is refused instead of leaving an empty document. - 2026-10-22: Re-routing a group connector keeps its free end at its displayed rotated and flipped position, including after nonuniform group resizing, while the glued end follows its existing connection-site path. Missing target shapes and invalid connection sites keep their transformed endpoints too. Fallback routes that cannot fit the renderer's 4000 pt child-size cap leave the original connector unchanged. Model and engine regressions cover starts, ends, unit/uniform/nonuniform scale, settled reroutes and safe handling of degenerate dimensions and display-size limits; no new native Word comparison. - 2026-10-23: Lists: an empty paragraph whose mark is hidden takes no room and draws no list number, in the body and in table cells, as in Word; the number is still counted, so the next item keeps its place (#3982). +- 2026-10-24: Spelling: with Ignore web and file addresses on, a web or e-mail address after a no-break space or an ideographic space is skipped, as it is after a plain space. Its words were marked as misspelled. - 2026-10-23: Font dialog: "NaN" or "inf" typed in the Size box previews the text at 12 pt. "NaN" reads as a number, and a font size that is not a number stopped the app while drawing the preview (#3663). - 2026-10-23: Tables: a cell span read from a `.json` document (any 32-bit number) no longer overflows when a column is inserted through the cell or the cell is merged: the span is counted from its clamped value and stays within 63 columns. It panicked in debug builds and wrapped to a small span in release (#3637). - 2026-10-22: Headers and footers: while editing the header, a single click in the footer edits the footer with the caret where clicked (and the reverse, and another page's own header or footer). The click went to the header being edited and the view scrolled back up to it, so a double-click on the footer rarely landed unless both were on screen. diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index de5967829..65190cab4 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