fix: leave whitespace that ends a line out of the saved file - #340
Merged
Conversation
Encoding it as ` ` would preserve it, at the cost of returning the entity noise #242 removed to every file whose line happens to end in whitespace. A parse strips the character on read and a block renders it as nothing, so neither direction preserves anything an author can observe. An authored ` ` at a trimmed position is dropped on save as a result. It is lost today as well, one save later: Milkdown hoists the space out of the mark that records the reference before serialization sees it, so the authored form is not reachable from the stored source.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A space typed at the end of a line was written to the file and then discarded by the next parse, so the document stopped converging on its own serialization: the first save kept the space, the reload dropped it, and the second save differed from the first.
serializeMarkdownTextsplit trailing whitespace off the value, escaped the rest throughstate.safe, and appended the whitespace verbatim.mdast-util-to-markdownencodes a space only where a line ending is adjacent to it, so that append changed the written bytes in exactly one situation — whitespace that ends a line — and returned whatstate.safewould have written anyway everywhere else. The special case had no effect other than producing the byte the parser discards.The whitespace is now written only where something on the same line follows it.
closesTrimmedContentasks whether the text node is the last child of a paragraph, a heading, or a table cell, which are the parents a parse trims, andaftercarries what will actually be written so the escape passes read the real line ending rather than a space that never arrives. Only the last child can hold such whitespace:state.safeencodes whitespace a line ending follows, and Milkdown hoists whitespace out of a mark before the mark is written.The report named a paragraph. The same line reaches every parent a parse trims — heading, list item, blockquote, table cell — and any edit that leaves whitespace there, a deletion as much as a keystroke, a tab as much as a space. Two spaces ending a paragraph go with them: they spell a hard break only while another line follows them inside the paragraph, and at the end of the last line there is nothing to break.
Not writing the character is the direction, rather than encoding it as
 . Both reverse the assertion that guarded #242's intent, sinceplain tail \nholds either way; writing nothing keeps that intent, while encoding contradicts it and returns entity noise to every file whose line happens to end in whitespace. CommonMark strips the character on read and a block renders it as nothing, so neither direction preserves anything an author can observe.Related Issue
Closes #278
Verification
markdownCompatibility.test.tsgainsLine-final whitespace, which drives the editor mount through edit, save, reload, save, and asserts each stage rather than the bytes alone. Eight shapes that diverged now converge: a paragraph, a paragraph already holding a space, emphasis closing a paragraph, two spaces, a tab, a heading, a list item, and a blockquote. Alongside them, a table cell, whitespace reached by deleting a character rather than typing one, and the reloaded paragraph matching the text the editor showed before the space was typed.Three rows assert what must not change, since each is whitespace a parse keeps: a space that later text on the same line follows, a space in front of a hard break, and whitespace inside fenced code.
Existing expectations that encode the behavior this replaces were updated rather than worked around:
markdownCompatibility.test.tsassertedplain tail \nunder the name "writes an ordinary trailing space as itself". That byte is the defect, so the row now assertsplain tail\nunder a name that says what it proves. The three typed-link fixtures beside it drop the same space; what they exist to prove, that typed source commits as the link it describes once a space follows it, is unchanged and still asserted.sourceProjectionTypedLink.test.tsexpected\[test taillink](./test.html) \nafter a word is dragged out of the end of the paragraph. The drag leaves a line-final space, so it is no longer written. The escaped literal the row exists for is unchanged, and\[test link](./test.html)without a trailing space already has a row asserting that it reloads as literal text.The corpus round-trip guard cannot see any of this and is unchanged. It runs file, save, file, save, and a file it has opened once no longer holds line-final whitespace, so its baseline is already stable. Only an edit puts the whitespace there, which is why the new coverage edits a mounted document instead of adding a fixture.
pnpm check:frontendpasses. The backend is untouched, sopnpm check:backendwas 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
An authored
 at a trimmed position is now dropped on save. It was lost before this change as well, one save later, and it is not reachable from the stored source: parsing it produces a text node carrying aleafdownCharacterReferencemark, and Milkdown'sSerializerStatehoists the ASCII space out of that mark into a plain sibling before the mark is written, so the handler never sees the reference. and*survive under the identical mark because the hoist tests for the literal space, which is why the character-reference preservation shows no sign of this anywhere else.The opening edge of the same line is filed as #339 and deliberately left alone here. A typed leading space is written as
 correctly on the first save, reloads with the space intact, is written raw on the second, and is gone on the third open. #278 never named that edge, and its answer is not quite the same question: the closing edge never round-tripped at all, while this one survives a single save.The issue body was corrected while diagnosing this. Two of its statements did not survive measurement: two trailing spaces ending a paragraph fail exactly like one rather than round-tripping as a hard break, and authored whitespace is not always unaffected, since an authored
 is lost. Both are recorded there with the measurements.