Summary
Leafdown records the form a block was authored in and writes it back, but the formatting commands have no rule for that form. Two commands that make the same change to a heading disagree about whether it survives: Increase heading level carries the node's attributes through and keeps the form, while Heading 1 through Heading 6 rebuild the block from the level alone and drop it.
The file then gains the unrequested rewrite the preservation program exists to prevent, arriving one gesture later than a save would have delivered it. Neither behavior is a decision about form; the difference is which ProseMirror helper each command reached for.
Steps to reproduce
- Open a document containing
Setext one on one line and === on the next.
- Run
Heading 2.
- Save the document and read the file.
Expected behavior
Setext one over ---. That is the form the heading was authored in, at the level the command asked for, and it is what Increase heading level already writes for the same change.
Whatever rule is chosen, the two paths to one level change agree.
Actual behavior
## Setext one. The authored form is gone.
Measured by driving the editor mount on main at 71897f80, the merge of #358, which is where a heading first carries a form:
| Document |
Command |
Saved |
Setext one / === |
Increase heading level |
Setext one / --- |
Setext one / === |
Heading 2 |
## Setext one |
# Closed atx # |
Heading 3 |
### Closed atx |
adjustSelectedHeadingLevels dispatches setNodeMarkup(pos, undefined, { ...node.attrs, level }, node.marks), which carries every attribute the node holds. toggleHeading reaches setSelectionTextBlockType(view, "heading", { level }), which builds the block from the named attributes alone and leaves every other attribute at its schema default.
Related context
Done when
Notes, logs, screenshots
The list commands were probed alongside the headings and the result was inconclusive: the caret landed inside the existing list and toggled it off rather than acting on the trailing paragraph. Whether format.unorderedList and format.orderedList drop a list's recorded marker is unmeasured and part of the audit above.
Typora was measured on the same question for comparison. It preserves a construct's authored form through an edit to that construct, including a level-three heading closed by a single hash and a setext heading whose underline is far shorter than its text. Its three markdown preferences do not reformat an existing construct.
Implementation direction
setNodeMarkup with the node's own attributes spread is already the preserving form, so the smaller half of the work is making the toggle path match it. The rule is the larger half, because a level change and a paragraph-to-heading conversion are not obviously the same question: the first has a form to keep and the second has none.
Out of scope
- What form a construct with no authored form takes, which is its own issue.
- Undo and redo, which restore the attributes the history holds.
- Whether a formatting preference should exist at all.
Summary
Leafdown records the form a block was authored in and writes it back, but the formatting commands have no rule for that form. Two commands that make the same change to a heading disagree about whether it survives:
Increase heading levelcarries the node's attributes through and keeps the form, whileHeading 1throughHeading 6rebuild the block from the level alone and drop it.The file then gains the unrequested rewrite the preservation program exists to prevent, arriving one gesture later than a save would have delivered it. Neither behavior is a decision about form; the difference is which ProseMirror helper each command reached for.
Steps to reproduce
Setext oneon one line and===on the next.Heading 2.Expected behavior
Setext oneover---. That is the form the heading was authored in, at the level the command asked for, and it is whatIncrease heading levelalready writes for the same change.Whatever rule is chosen, the two paths to one level change agree.
Actual behavior
## Setext one. The authored form is gone.Measured by driving the editor mount on
mainat71897f80, the merge of #358, which is where a heading first carries a form:Setext one/===Increase heading levelSetext one/---Setext one/===Heading 2## Setext one# Closed atx #Heading 3### Closed atxadjustSelectedHeadingLevelsdispatchessetNodeMarkup(pos, undefined, { ...node.attrs, level }, node.marks), which carries every attribute the node holds.toggleHeadingreachessetSelectionTextBlockType(view, "heading", { level }), which builds the block from the named attributes alone and leaves every other attribute at its schema default.Related context
Scopeputs editing-surface behavior outside it, which is why this is a standalone issue rather than a sub-issue.Preserve the form a file was written inindocs/decisions.md, which ranks content, closure, and form but says nothing about what an editing command does to a recorded form.Done when
Heading 1throughHeading 6agree withIncrease heading levelandDecrease heading levelon a heading's form.Notes, logs, screenshots
The list commands were probed alongside the headings and the result was inconclusive: the caret landed inside the existing list and toggled it off rather than acting on the trailing paragraph. Whether
format.unorderedListandformat.orderedListdrop a list's recorded marker is unmeasured and part of the audit above.Typora was measured on the same question for comparison. It preserves a construct's authored form through an edit to that construct, including a level-three heading closed by a single hash and a setext heading whose underline is far shorter than its text. Its three markdown preferences do not reformat an existing construct.
Implementation direction
setNodeMarkupwith the node's own attributes spread is already the preserving form, so the smaller half of the work is making the toggle path match it. The rule is the larger half, because a level change and a paragraph-to-heading conversion are not obviously the same question: the first has a form to keep and the second has none.Out of scope