Skip to content

fix: keep a thematic break in the run it was authored with - #338

Merged
Azganoth merged 1 commit into
mainfrom
bug/thematic-break-authored-run
Aug 31, 2026
Merged

fix: keep a thematic break in the run it was authored with#338
Azganoth merged 1 commit into
mainfrom
bug/thematic-break-authored-run

Conversation

@Azganoth

Copy link
Copy Markdown
Owner

Summary

A thematic break is now written with the run it was authored with. hrSchema carried no attributes, so ***, ---, _ _ _, and every other accepted spelling parsed into the same node and came back as ***, rewriting every break in a file on its first save.

A break holds no children, so the characters it was written with are the slice of the file it was built from and nothing else. leafdownThematicBreak stamps that slice onto the mdast node the way leafdownCharacterReference recovers a reference, the hr schema carries it as an attribute, and a thematicBreak stringify handler writes it. Indentation stands outside the slice and the whitespace closing the line is trimmed off it, so what is kept is the characters and the spacing between them. Tabs survive: position offsets index the original source rather than the tab stops the parser expands them to, so this is a serializer difference and not the read-time one #319 asked to have settled.

The rendered separator carries the run in data-marker, so a break keeps its spelling through a copy and a paste as an autolink already does.

Two positions read a preserved run back as something other than a break, and both give way to the default rather than write a line the next open would disagree with:

  • mdast-util-to-markdown joins a tight list item's children with a single newline, so a run of hyphens written after a paragraph there underlines it into a setext heading. The join is read off state.join by the resolution between uses itself, so the answer cannot drift from the blank line the serializer actually emits.
  • A run opening a list item stands on the bullet's line, where a bullet and a run spelled with the same character read as one longer break with no list around it. The serializer already moves the bullet off the rule character it was configured with, but that character cannot answer for a run the node carries, so the run gives way rather than the bullet. * ---\n Paragraph was otherwise saving as - ---\n Paragraph, which reopens with the list gone.

A break the editor inserts still writes ***, which is also both fallbacks: it is the one spelling no position reads as anything else.

Related Issue

Closes #319

Verification

markdownCompatibility.test.ts gains Thematic break form, which pins the written bytes for each accepted spelling across a save: seven runs including _ _ _, - - -, ----------, and a tab-separated *\t*\t*; three rows where indentation or trailing whitespace is dropped to the run it spells; three containers that keep the run, including a tight list item; and one row per fallback. The setext row sets the marker attribute on a break already sitting in a tight item, which is the arrangement no source can produce and an edit can.

nativeClipboard.test.ts gains two rows carrying --- and _ _ _ through a copy and a paste, beside the autolink rows that prove the same thing for a mark.

blocks.test.ts asserts the run an inserted rule writes, since a break read from a file now writes its own and one the editor creates needs a defined one.

markdownCompatibility.test.ts expected *** from the --- in its documented CommonMark fixture. That expectation encodes the behavior this replaces and was updated rather than worked around; the comment above it no longer lists thematic breaks among the markers the serializer normalizes.

Measured by diffing corpus/commonmark/blocks.md against its first save: the three breaks under Asterisks, hyphens, and underscores form thematic breaks and the one under An asterisk thematic break interrupts a paragraph instead no longer differ. The differences that remain in that file are setext headings, ATX closing sequences, and bullet markers, which belong to other classes. No corpus fixture is added: the round-trip assertions compare a save against the save before it and converge either way, while Thematic break form compares the written bytes, which is what distinguishes the two behaviors.

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

docs/reference.md lists a deferred Horizontal rule marker setting whose default is ---, while the implemented fallback is ***. The two are not the same thing — the setting names what an author picks for a break Leafdown must choose a run for, and the fallback is the run no line can misread — but the disagreement is worth settling when the setting lands. Left alone here rather than changed, since the default of a product setting is not this issue's to move.

A run opening a list item whose bullet shares its character still normalizes: * --- saves as - ***. Preserving it means deciding the bullet from the runs the items carry rather than from the configured rule, which is bullet scope, and a list mixing a *** opener with a --- one would still need the fallback for one of them.

A --- under paragraph text with no blank line between them is a setext underline and belongs to the heading class, as does the -------------------------------- in blocks.md. Neither is touched.

@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 775e439 into main Aug 31, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/thematic-break-authored-run branch August 31, 2026 11:17
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.

Every thematic break is rewritten as three asterisks on save

1 participant