Summary
Saving rewrites a fenced code block into one fence form. A tilde fence becomes a backtick fence, the space between the opening fence and its info string is dropped, a fence indented up to three spaces is moved to column zero, and a fence left unclosed at end of file gains a closing fence.
The tilde-to-backtick rewrite is the damaging one. A tilde fence may hold a backtick in its info string and a backtick fence may not, so changing the fence forces the info string to change too: ~~~ language with a backtick saves as a backtick fence whose info string carries `. The block still reads back as the same code, but the info string another tool reads is no longer the one the author wrote.
Steps to reproduce
- Open a document containing a
~~~ fenced block.
- Save it without editing.
- Read the file.
Expected behavior
The block keeps the fence character, fence length, indentation, and info string spacing it was authored with, and a fence the author left unclosed at end of file stays unclosed.
Actual behavior
Measured against 83e12b19 by driving the editor mount used by the plugin tests.
| Document |
Saved |
~~~ / tilde fence / ~~~ |
``` / tilde fence / ``` |
``` language+escaped |
```language+escaped |
~~~ language + backtick + with-backtick |
```language`with-backtick |
``` indented three spaces |
``` at column zero |
A ``` fence unclosed at end of file |
a closing ``` is appended |
The unclosed-fence case is the whole of corpus/isolated/end-of-file/unclosed-code-fence.md; the other three files in corpus/isolated/end-of-file/ already round-trip byte-identically, so this is the one difference keeping that directory from qualifying as a group.
Related context
Done when
Notes, logs, screenshots
The unclosed fence is worth separating in the fix even though it is filed here: the other end-of-file files pass already, so closing this one case is what lets that whole directory move to a byte-identity assertion.
Implementation direction
remarkStringifyOptionsCtx in src/features/editor/utils/createMilkdownEditor.ts takes a per-type handler, and the fence's authored text is the source slice at node.position, stamped at parse the way src/features/editor/plugins/characterReference.ts stamps inline forms.
The fence length is not free to preserve blindly: a fence must stay longer than any run of the same character inside its content, so a recorded length has to be treated as a minimum the serializer may still raise.
Out of scope
- Indented code blocks rewritten as fenced, which is its own class.
- Code spans, which are inline and come back unchanged.
Summary
Saving rewrites a fenced code block into one fence form. A tilde fence becomes a backtick fence, the space between the opening fence and its info string is dropped, a fence indented up to three spaces is moved to column zero, and a fence left unclosed at end of file gains a closing fence.
The tilde-to-backtick rewrite is the damaging one. A tilde fence may hold a backtick in its info string and a backtick fence may not, so changing the fence forces the info string to change too:
~~~ languagewith a backtick saves as a backtick fence whose info string carries`. The block still reads back as the same code, but the info string another tool reads is no longer the one the author wrote.Steps to reproduce
~~~fenced block.Expected behavior
The block keeps the fence character, fence length, indentation, and info string spacing it was authored with, and a fence the author left unclosed at end of file stays unclosed.
Actual behavior
Measured against
83e12b19by driving the editor mount used by the plugin tests.~~~/tilde fence/~~~```/tilde fence/`````` language+escaped```language+escaped~~~ language+ backtick +with-backtick```language`with-backtick```indented three spaces```at column zero```fence unclosed at end of file```is appendedThe unclosed-fence case is the whole of
corpus/isolated/end-of-file/unclosed-code-fence.md; the other three files incorpus/isolated/end-of-file/already round-trip byte-identically, so this is the one difference keeping that directory from qualifying as a group.Related context
Scope;corpus/commonmark/code.md, whose headings state the fence rules being measured.Done when
corpus/commonmark/code.mdandcorpus/isolated/end-of-file/unclosed-code-fence.mdlose these differences, or the reason they cannot is recorded.Notes, logs, screenshots
The unclosed fence is worth separating in the fix even though it is filed here: the other end-of-file files pass already, so closing this one case is what lets that whole directory move to a byte-identity assertion.
Implementation direction
remarkStringifyOptionsCtxinsrc/features/editor/utils/createMilkdownEditor.tstakes a per-type handler, and the fence's authored text is the source slice atnode.position, stamped at parse the waysrc/features/editor/plugins/characterReference.tsstamps inline forms.The fence length is not free to preserve blindly: a fence must stay longer than any run of the same character inside its content, so a recorded length has to be treated as a minimum the serializer may still raise.
Out of scope