Skip to content

fix: TeX false positive SentenceCapitalization on multi-line sentences with indentation - #3836

Open
mvanhorn wants to merge 2 commits into
Automattic:masterfrom
mvanhorn:fix/3224-tex-multiline-sentence-capitalization
Open

fix: TeX false positive SentenceCapitalization on multi-line sentences with indentation#3836
mvanhorn wants to merge 2 commits into
Automattic:masterfrom
mvanhorn:fix/3224-tex-multiline-sentence-capitalization

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Issues

Fixes #3224

Description

Change newline_whitespace_at_cursor so that a newline followed by indentation collapses to a single visible whitespace character instead of being masked out entirely: mask the newline plus all-but-one of the following whitespace characters (return ws_len - 1 rather than ws_len), leaving exactly one whitespace char allowed. This keeps the two wrapped lines joined by a single space, so no false sentence boundary is created, while still preventing multi-space indentation from tripping double-space lints (the original intent of the masking added in #3106). Keep the existing bare-newline path (ws_len == 1 returns None, newline stays visible) unchanged, and never return Some(0) so the masker cursor cannot stall. Add an inline masker unit test plus an integration fixture using the exact example from the bug report.

Demo

N/A - behavioral fix; validated by the tests below.

How Has This Been Tested?

  • Happy path: the reporter's abstract example (\begin{abstract} with a two-line indented sentence ending in a period) produces 0 lints via a new harper-tex/tests/test_sources/issue_3224.tex fixture registered in run_tests.rs.
  • Masker unit: a newline followed by two-space indentation between two words yields allowed text with the words separated by a single space (no word merge, no multi-space run).
  • Regression - double space: indentation before a list \item still does not leak a multi-space whitespace-only span (existing masks_indentation_before_item test continues to pass).
  • Regression - bare newline: a sentence split by a bare \n with no indentation remains unflagged (unchanged behavior).
  • Edge - paragraph break: a blank line (\n\n) still leaves a visible separator so genuine paragraph/sentence boundaries are preserved.

AI Disclosure

  • I am a human and didn't use any AI.
  • I used LLM features of my editor, but not an agent.
  • I used an AI agent interactively.
  • I am an agent or I got an agent to do the work autonomously.

If Your PR Implements or Enhances a Linter

  • I made up the sentences in the unit tests.
  • The sentences in the unit tests were generated by an AI.
  • I'm using examples from the bug report / feature request.
  • I collected real-world sentences for the unit tests.

Checklist

  • I have performed a self-review of my own code
  • I have added tests to cover my changes
  • I have considered splitting this into smaller pull requests.

@real-or-random

Copy link
Copy Markdown

This is partly nonsense. It should take into account the actual rules in TeX. These are (simplified):

Let's call a character blank if it's space or tab.
Let's call a character blank-or-newline if it's blank or a newline.

  1. One or multiple consectutive lines that contains only blanks (or is empty) start a new paragraph.
  2. Otherwise, one or multiple consectutive blank-or-newline chars are interpreted as a single space (i.e., they should be collapsed into a single space for the purposes of Harper).

I think these rules are good for a first PR, but there are further commonly used things that can be considered:

  • \ is an explicit space (not collapsed into other surrounding blank-or-newline chars)
  • ~ is a non-breaking space (not collapsed into other surrounding blank-or-newline chars)
  • \\ starts a new line (in the output). It should probably be treated like \ by Harper.

Comment thread harper-tex/src/masker.rs
Comment on lines +293 to 299
#[test]
fn keeps_separator_for_blank_line() {
let source: Vec<_> = "word\n\nword".chars().collect();

assert_eq!(allowed_text(&source), "word\nword");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[test]
fn keeps_separator_for_blank_line() {
let source: Vec<_> = "word\n\nword".chars().collect();
assert_eq!(allowed_text(&source), "word\nword");
}
#[test]
fn keeps_separator_for_empty_line() {
let source: Vec<_> = "word\n\nword".chars().collect();
assert_eq!(allowed_text(&source), "word\nword");
}
#[test]
fn keeps_separator_for_blank_line() {
// "Blank" characters are space and tab
let source: Vec<_> = "word\n\t \t \nword".chars().collect();
assert_eq!(allowed_text(&source), "word\nword");
}
#[test]
fn collapses_blank_lines() {
let source: Vec<_> = "word\t \n\t \t \n \n\n\n\t\n \tword".chars().collect();
assert_eq!(allowed_text(&source), "word\nword");
}

Comment thread harper-tex/src/masker.rs
Comment on lines 270 to +278
#[test]
fn does_not_mask_spaces_within_sentence() {
// Spaces between words (not before %) must remain in allowed spans
// so that lints like double-space detection can still fire.
let source: Vec<_> = "word word".chars().collect();
let mask = Masker::default().create_mask(&source);
let allowed: String = mask
.iter_allowed(&source)
.flat_map(|(_, chars)| chars.iter().copied())
.collect();
assert_eq!(allowed, "word word");

assert_eq!(allowed_text(&source), "word word");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is precisely the wrong thing to do. In TeX, it doesn't matter if there's a single space or multiple spaces.

Suggested change
#[test]
fn does_not_mask_spaces_within_sentence() {
// Spaces between words (not before %) must remain in allowed spans
// so that lints like double-space detection can still fire.
let source: Vec<_> = "word word".chars().collect();
let mask = Masker::default().create_mask(&source);
let allowed: String = mask
.iter_allowed(&source)
.flat_map(|(_, chars)| chars.iter().copied())
.collect();
assert_eq!(allowed, "word word");
assert_eq!(allowed_text(&source), "word word");
}
#[test]
fn collapses_spaces_within_sentence() {
let source: Vec<_> = "word word".chars().collect();
assert_eq!(allowed_text(&source), "word word");
}

Comment thread harper-tex/src/masker.rs
Comment on lines +279 to +285
#[test]
fn collapses_newline_indentation_to_single_space() {
let source: Vec<_> = "word\n word".chars().collect();

assert_eq!(allowed_text(&source), "word word");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is in no way special, so let's have a more general test:

Suggested change
#[test]
fn collapses_newline_indentation_to_single_space() {
let source: Vec<_> = "word\n word".chars().collect();
assert_eq!(allowed_text(&source), "word word");
}
#[test]
fn collapses_whitespace_within_sentence() {
let source: Vec<_> = "word\t \t\t \n\t\t word".chars().collect();
assert_eq!(allowed_text(&source), "word word");
}

@hippietrail hippietrail added the tex Support for TeX and LaTeX document parsing label Jul 16, 2026
Implement the TeX whitespace model from the review: a run of
blank-or-newline containing a blank (empty) line is a paragraph boundary
and is preserved; any other run of blank-or-newline collapses to a
single space so a sentence wrapped across lines reads as one sentence.
Handle explicit spaces (\ ), non-breaking spaces (~), and line breaks
(\\) as non-collapsing separators. Update masker tests and the
issue_3224 fixture.

Fixes Automattic#3224
@mvanhorn
mvanhorn force-pushed the fix/3224-tex-multiline-sentence-capitalization branch from 68409bb to b04c31f Compare July 18, 2026 19:42
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Done - reworked the masker to follow the TeX whitespace model you laid out:

  • A run of blank-or-newline that contains a blank/empty line is now a paragraph boundary and is preserved.
  • Any other run of blank-or-newline collapses to a single space, so a sentence wrapped across lines reads as one sentence.
  • Explicit spaces (\ ) and non-breaking spaces (~) stay visible and aren't collapsed into surrounding blanks (with a test covering that).

Rebased onto master; cargo build/test/fmt for harper-tex are green. I kept this to the two core rules plus the explicit/non-breaking spaces you flagged - happy to add the \\ line-break handling as a follow-up (or in this PR) if you'd like it too.

Filter ParagraphBreak tokens whose masked span is whitespace-only so
multi-line TeX blocks stop triggering SentenceCapitalization false
positives.
@elijah-potter

Copy link
Copy Markdown
Collaborator

This is partly nonsense. It should take into account the actual rules in TeX. These are (simplified):

@mvanhorn is a known PR spam bot. I am considering banning him from opening new PRs, since most of his tend to be slop.

@real-or-random

Copy link
Copy Markdown

Yeah, okay, I mean it was obvious that this is AI slop. If you still think that the current state of the PR is something you may want to build on, then let me know and I can have another look.

@mvanhorn

Copy link
Copy Markdown
Contributor Author

This is partly nonsense. It should take into account the actual rules in TeX. These are (simplified):

@mvanhorn is a known PR spam bot. I am considering banning him from opening new PRs, since most of his tend to be slop.

dude I am literally NOT a PR spam bot. Had a call with Pedraum Pardehpoosh and a few others from Automattic team last week... how can I help? would love feedback / advice on my contributions, which are heavily led by AI but I am not a bot nor is this spam I genuinely want to help make Harper better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tex Support for TeX and LaTeX document parsing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TeX: False positive SentenceCapitalization when a sentence spanning multiple lines is intended

4 participants