Skip to content

A code fence's authored form is rewritten on save #320

Description

@Azganoth

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

  1. Open a document containing a ~~~ fenced block.
  2. Save it without editing.
  3. 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

  • A fenced block writes the fence character and length it was authored with.
  • The info string is written with the spacing it was authored with, and a backtick in a tilde fence's info string stays a literal backtick.
  • A fence authored with leading indentation keeps it.
  • A fence left unclosed at end of file is written back unclosed.
  • A block made in the editor still writes a defined default fence.
  • corpus/commonmark/code.md and corpus/isolated/end-of-file/unclosed-code-fence.md lose 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

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.

Metadata

Metadata

Assignees

Labels

BugSomething isn't working

Projects

  • Status
    Ready

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions