Skip to content

fix: preserve authored line breaks and represent blank paragraphs with blank lines - #198

Merged
Azganoth merged 3 commits into
mainfrom
bug/authored-line-breaks
Aug 12, 2026
Merged

fix: preserve authored line breaks and represent blank paragraphs with blank lines#198
Azganoth merged 3 commits into
mainfrom
bug/authored-line-breaks

Conversation

@Azganoth

@Azganoth Azganoth commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

An authored <br> never reached the document, because the commonmark preset deletes every <br>-shaped html node on parse to support its own empty-paragraph round trip. The two halves are coupled — an empty paragraph serializes to <br /> only while that plugin is registered — so this replaces the mechanism instead of narrowing the match.

  • Raw HTML is document content unconditionally. remarkPreserveEmptyLinePlugin is removed before the editor is created.
  • A blank paragraph is carried by the blank-line run the serializer already writes for it, restored on parse from mdast positions. Unlike <br />, blank lines render as nothing in other Markdown readers, so a Leafdown save no longer changes how a document renders elsewhere.
  • Block-position raw HTML inside a footnote definition is wrapped in a paragraph. The preset wraps only root, blockquote, and listItem, and an unwrapped inline node in block position takes the whole definition out of the document.

The file is the only state, so a\n\n<br />\n\nb is byte-identical whether the author typed the tag or the editor saved a blank paragraph. No parse-time rule can separate them, which is why the representation changes rather than the match.

Related Issue

Closes #193
Closes #196

Verification

Focused tests prove the round trip in both directions. blockStructure.test.ts covers the transformer as a pure tree function: the blank-line-to-paragraph mapping across 1, 2, 3, 5, and 7 blank lines, nested containers, absent positions, containers that hold no block content, and the footnote wrap. markdownCompatibility.test.tsx covers 16 authored line-break round trips across every spelling and position — inline, alone in a paragraph, heading, list item, table cell, blockquote, footnote definition, uppercase, code span, fenced block, and raw HTML block — plus an edit cycle, blank-paragraph preservation including inside a blockquote and a footnote definition, blank paragraphs made in the editor surviving a write and reopen for one through three, and an assertion that no <br is written to represent one.

Round-tripped corpus/commonmark/html.md through the editor: the two new sections return byte-identical. Three deviations in that file predate this change and were left alone — an inserted blank line before a block-tag interruption, a type-seven tag folded into the paragraph it cannot interrupt, and escapes added to malformed tag-like text.

Not verified: behavior in the WebView. Nothing here depends on layout or hit-testing, but a blank paragraph and a literal <br> have not been looked at in the running application.

Notes

The override is recorded under the existing Accept Milkdown GFM preset behavior decision rather than as a new decision, since that entry already reserves the case of the specification overriding a preset default. The Specification states the behavior only; the two-blank-lines convention is an implementation detail and stays out of it.

Trailing blank paragraphs are deliberately excluded. Save output trims trailing blank lines by specification, so they cannot survive regardless of representation, and a preserve-trailing-blank-lines option is tracked on the Output formatting preferences project draft.

The upstream defects behind this are unreported so far: the unscoped <br> match, and the missing footnoteDefinition in the preset's wrap list. Both are unchanged in 7.22.0. Only the second still affects Leafdown after this change, since the plugin holding the first is no longer loaded.

The commonmark preset couples the two: an empty paragraph serializes to
`<br />` only while a parse-time plugin is registered, and that plugin deletes
every `<br>` it finds, so an authored one never reaches the document. Removing
the plugin settles both halves at once. Raw HTML stays document content, and a
blank paragraph is carried by the blank-line run the serializer already writes
for it, which renders as nothing in other readers where `<br />` renders a
visible line break.

The preset wraps block-position raw HTML for root, blockquote, and list items
but not footnote definitions, where an unwrapped inline node takes the whole
definition out of the document.
The override belongs under the existing GFM preset decision rather than as a
new one, which already reserves the case of the specification overriding a
preset default.
@Azganoth Azganoth added the Bug Something isn't working label Aug 12, 2026
@Azganoth Azganoth self-assigned this Aug 12, 2026
@Azganoth
Azganoth merged commit 706ccc4 into main Aug 12, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/authored-line-breaks branch August 12, 2026 03:59
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.

Raw HTML in block position inside a footnote definition drops the whole definition Authored <br> is deleted on load and lost on save

1 participant