Skip to content

fix: keep a character Markdown does not trim at the end of a line - #343

Merged
Azganoth merged 1 commit into
mainfrom
bug/untrimmed-line-final-whitespace
Aug 31, 2026
Merged

fix: keep a character Markdown does not trim at the end of a line#343
Azganoth merged 1 commit into
mainfrom
bug/untrimmed-line-final-whitespace

Conversation

@Azganoth

Copy link
Copy Markdown
Owner

Summary

A no-break space closing a line was deleted from the file on the first save. Unlike the whitespace #278 and #339 leave out, nothing about the character is trimmed on read: it renders, and it survives a reload wherever it is written, so removing it lost text rather than settling the file.

TRAILING_WHITESPACE_PATTERN decides two things — what a trimmed position leaves out of the file, and what state.safe never sees. Matching \s selected every Unicode space separator rather than the space and tab CommonMark trims at a line edge, so closesTrimmedContent dropped characters alongside the ones it was meant to. The pattern is now /[\t ]+$/u, the class the leading edge already uses and the one thematicBreakMarkdown.ts already spells the same way.

The pattern predates #278, where it only ever selected characters that were appended raw a moment later, so being wider than the trimmed set changed nothing. #278 gave the match its second meaning and the extra characters became losses at that point.

Narrowing it means the character reaches state.safe with the rest of the value, which is what its escapes have to be decided against rather than only what survives. A backslash before one is no longer escaped: the character stands between it and the line ending, so it no longer spells a hard break. The same holds for a *, _, or ~ run, a bracket, a bare autolink, an email address, and a www address, none of which change beyond keeping the character.

Related Issue

Closes #341

Verification

markdownCompatibility.test.ts gains rows in Line-final whitespace, beside the ones asserting what that position leaves out. A no-break space is kept where it closes a paragraph, a heading, a list item, a blockquote, and a table cell, an em space and an ideographic space are kept closing a paragraph, and the reloaded document is asserted to hold the character rather than only the bytes.

Three rows put the two classes against each other on one line, since that is what separates a trim from a deletion: a space the character follows is kept because the character ends the line, and a space or a tab following the character is still left out while the character stays.

Two rows assert the escapes the character now takes part in: a backslash it separates from the line ending is written bare, and a bare autolink before it stays bare.

corpus/commonmark/text-and-breaks.md gains Trailing whitespace a parse keeps, the contrast to the trimmed-whitespace section above it, with each invisible character named in prose as that file already does for its trailing spaces. This is what makes the class a standing guard: corpusRoundTrip.test.ts compares the document either side of a save, and reverting the one-line change makes preserves the document across a save for commonmark/text-and-breaks.md fail. Byte convergence alone cannot see the defect, because the old behavior was stable from the first save and only lossy.

pnpm check:frontend passes. The backend is untouched, so pnpm check:backend was not run.

Not verified: the manual pass over corpus/ in the running application, which covers rendering, interaction, and navigator behavior beyond the automated round trip.

Notes

A character written as   was never affected and is unchanged. It is written from the source stored on its mark and never reaches the trim, which is why the character-reference coverage showed no sign of this.

The corpus fixture is not named in the issue's completion criteria. It is added because the guard was already the right home for this class and only lacked a case, at the cost of putting invisible characters into a fixture corpusRoundTrip.test.ts and sourceProjectionEscapeCorpus.test.ts both read.

This is the third pass over the same line. #278 leaves out whitespace closing a line, #339 does the same for whitespace opening one, and the two together are what turned a pattern that was merely wider than it needed to be into one that deleted text.

`TRAILING_WHITESPACE_PATTERN` decides two things: what a trimmed position
leaves out of the file, and what `state.safe` never sees. Matching `\s`
selected every Unicode space separator, so a no-break space closing a line
was deleted rather than trimmed. It predates #278, where the match only ever
chose characters that were appended raw a moment later and widening it past
the trimmed set changed nothing; #278 gave the match its second meaning and
the extra characters became losses at that point.

The character now reaches `state.safe` with the rest of the value, which is
what its escapes have to be decided against rather than only what survives.
A backslash before one is no longer escaped, because the character stands
between it and the line ending, so it no longer spells a hard break.
@Azganoth Azganoth added the Bug Something isn't working label Aug 31, 2026
@Azganoth Azganoth self-assigned this Aug 31, 2026
@Azganoth
Azganoth merged commit addcd49 into main Aug 31, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/untrimmed-line-final-whitespace branch August 31, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Whitespace Markdown does not trim is deleted at the end of a line

1 participant