Summary
Saving escapes every parenthesis in a raw link destination, including balanced pairs that CommonMark allows unescaped. The destination is written by the link handler rather than by the text handler #252 made precise, so the same over-escaping survives there.
Steps to reproduce
- Open a document containing
[Balanced](garden(section(one)).md).
- Save it.
- Read the file.
Expected behavior
[Balanced](garden(section(one)).md). A raw destination may hold balanced parentheses, so no escape is needed.
An unbalanced parenthesis still needs its escape, because it would end the destination early.
Actual behavior
Measured against 9626471d by driving the editor mount used by the plugin tests. Both rows are stable across a second save:
| Document |
Saved |
[Balanced](garden(section(one)).md) |
[Balanced](garden\(section\(one\)\).md) |
[Inline script scheme](javascript:alert(1)) |
[Inline script scheme](javascript:alert\(1\)) |
Related context
Done when
Notes, logs, screenshots
Diagnosis
The escapes come from {character: '(', inConstruct: 'destinationRaw'} and {character: ')', inConstruct: 'destinationRaw'} in mdast-util-to-markdown's core unsafe list, applied while the link handler serializes the destination. serializeMarkdownText in src/features/editor/utils/markdownText.ts never sees it, and its relaxBracketEscapes pass is scoped to phrasing parents specifically to stay out of this construct.
Implementation direction
Whether a destination needs escaping is a property of the whole destination rather than of one character, so the natural seam is a link and image handler override alongside the existing text one in createMilkdownEditor.ts, not the character-level pass in markdownText.ts.
mdast-util-to-markdown already chooses between the raw and angle-bracket destination forms, so switching form is an available answer for a destination that cannot be written raw, and may be simpler than escaping selectively.
Out of scope
Summary
Saving escapes every parenthesis in a raw link destination, including balanced pairs that CommonMark allows unescaped. The destination is written by the link handler rather than by the text handler #252 made precise, so the same over-escaping survives there.
Steps to reproduce
[Balanced](garden(section(one)).md).Expected behavior
[Balanced](garden(section(one)).md). A raw destination may hold balanced parentheses, so no escape is needed.An unbalanced parenthesis still needs its escape, because it would end the destination early.
Actual behavior
Measured against
9626471dby driving the editor mount used by the plugin tests. Both rows are stable across a second save:[Balanced](garden(section(one)).md)[Balanced](garden\(section\(one\)\).md)[Inline script scheme](javascript:alert(1))[Inline script scheme](javascript:alert\(1\))Related context
Outcomesections of Determine whether the Markdown corpus can drive automated round-trip tests #135 and How a Markdown escape should be represented in the editor #245, andcorpus/commonmark/links-and-images.mdunderBalanced and unbalanced destination parentheses.Done when
Notes, logs, screenshots
Diagnosis
The escapes come from
{character: '(', inConstruct: 'destinationRaw'}and{character: ')', inConstruct: 'destinationRaw'}inmdast-util-to-markdown's core unsafe list, applied while the link handler serializes the destination.serializeMarkdownTextinsrc/features/editor/utils/markdownText.tsnever sees it, and itsrelaxBracketEscapespass is scoped to phrasing parents specifically to stay out of this construct.Implementation direction
Whether a destination needs escaping is a property of the whole destination rather than of one character, so the natural seam is a
linkandimagehandler override alongside the existingtextone increateMilkdownEditor.ts, not the character-level pass inmarkdownText.ts.mdast-util-to-markdownalready chooses between the raw and angle-bracket destination forms, so switching form is an available answer for a destination that cannot be written raw, and may be simpler than escaping selectively.Out of scope
\(written after a]in ordinary text, which The serializer escapes characters that cannot be syntax where they sit #252 already fixed.