craft-proof — a run of spaces is one double-space issue over the whole run in every language, through one shared space_runs helper (#496 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-11-02 23:23:15 +00:00
1 parent 7b64b78cb0
commit cb82e91428
9 files changed
+70 -56

No files matched your search

+2 -5
View File
@@ -62,11 +62,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Mezera před interpunkcí".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Dvojitá mezera".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Dvojitá mezera".into(), vec![" ".into()]));
}
}
+2 -15
View File
@@ -75,21 +75,8 @@ fn spaces(text: &str, lang: ProofLang, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Espacio antes de un signo de puntuación".into(), vec![p.to_string()], lang));
}
}
// Each run of two or more spaces once, over the whole run.
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes.get(i) != Some(&b' ') {
i += 1;
continue;
}
let start = i;
while bytes.get(i) == Some(&b' ') {
i += 1;
}
if i - start >= 2 {
v.push(issue(start, i, "Espacio doble".into(), vec![" ".into()], lang));
}
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Espacio doble".into(), vec![" ".into()], lang));
}
}
+2 -5
View File
@@ -105,11 +105,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Razmak ispred interpunkcijskog znaka".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Dvostruki razmak".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Dvostruki razmak".into(), vec![" ".into()]));
}
}
+2 -5
View File
@@ -56,11 +56,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Spasi sebelum tanda baca".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Spasi ganda".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Spasi ganda".into(), vec![" ".into()]));
}
}
+2 -5
View File
@@ -211,11 +211,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Spazio prima della punteggiatura".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Spazio doppio".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Spazio doppio".into(), vec![" ".into()]));
}
}
+54 -6
View File
@@ -152,6 +152,28 @@ fn dictionary_key(w: &str) -> String {
composed(&w.to_lowercase()).into_owned()
}
/// Each run of two or more spaces in `text`, once, over the whole run (a fix replaces it with one
/// space). A run of three is one issue over three, not a pair and a leftover.
pub(crate) fn space_runs(text: &str) -> Vec<Range<usize>> {
let bytes = text.as_bytes();
let mut runs = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes.get(i) != Some(&b' ') {
i += 1;
continue;
}
let start = i;
while bytes.get(i) == Some(&b' ') {
i += 1;
}
if i - start >= 2 {
runs.push(start..i);
}
}
runs
}
/// Add a word to the user dictionary ("Add to Dictionary"). It applies to every language.
pub fn add_word(w: &str) {
if user_words().write().unwrap_or_else(std::sync::PoisonError::into_inner).insert(dictionary_key(w)) {
@@ -1098,13 +1120,10 @@ fn grammar_en(text: &str, capital_first: bool) -> Vec<Issue> {
lang: ProofLang::En,
});
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
for run in space_runs(text) {
v.push(Issue {
start: i,
end: i + 2,
start: run.start,
end: run.end,
kind: IssueKind::Grammar,
message: "Extra space".into(),
suggestions: vec![" ".into()],
@@ -2591,6 +2610,35 @@ mod tests {
assert!(suggest_in("billabon", 5, au).iter().any(|s| s == "billabong"), "{:?}", suggest_in("billabon", 5, au));
}
/// Review follow-up: a run of spaces is one issue over the whole run, in every language with a
/// double-space rule (a run of three used to leave two after the fix, four three).
#[test]
fn a_run_of_spaces_is_one_issue_in_every_language() {
assert_eq!(space_runs("a b c d "), [3..5, 6..9, 10..14]);
assert!(space_runs("a b c").is_empty() && space_runs("").is_empty() && space_runs(" ").is_empty());
let langs = [
ProofLang::En,
ProofLang::EnGb,
ProofLang::Es,
ProofLang::It,
ProofLang::Pt,
ProofLang::Cs,
ProofLang::Hr,
ProofLang::Id,
ProofLang::Pl,
ProofLang::Ru,
];
for lang in langs {
for n in 2..=5 {
let text = format!("Aaa{}bbb", " ".repeat(n));
let hits: Vec<_> =
check_grammar_in(&text, lang).into_iter().filter(|i| i.start <= 3 && i.end > 3 && i.suggestions == [" "]).collect();
assert_eq!(hits.len(), 1, "{lang:?} {n}: {hits:?}");
assert_eq!((hits[0].start, hits[0].end), (3, 3 + n), "{lang:?} {n}");
}
}
}
/// English spelling reads SCOWL 2026 through the affix engine: one dictionary per English,
/// its notice embedded, and its ordinals (`COMPOUNDRULE`) understood.
#[cfg_attr(not(craft_proof_data), ignore = "needs the Language data (CRAFT_I18N_DIR)")]
+2 -5
View File
@@ -304,11 +304,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Zbędna spacja przed znakiem interpunkcyjnym".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Podwójna spacja".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Podwójna spacja".into(), vec![" ".into()]));
}
}
+2 -5
View File
@@ -129,11 +129,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Espaço antes da pontuação".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Espaço duplo".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Espaço duplo".into(), vec![" ".into()]));
}
}
+2 -5
View File
@@ -503,11 +503,8 @@ fn spaces(text: &str, v: &mut Vec<Issue>) {
v.push(issue(i, i + 1 + p.len_utf8(), "Лишний пробел перед знаком препинания".into(), vec![p.to_string()]));
}
}
for (i, _) in text.match_indices(" ") {
if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) {
continue;
}
v.push(issue(i, i + 2, "Двойной пробел".into(), vec![" ".into()]));
for run in crate::space_runs(text) {
v.push(issue(run.start, run.end, "Двойной пробел".into(), vec![" ".into()]));
}
}