Skip to content

A raw link destination escapes parentheses that are already balanced #258

Description

@Azganoth

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

  1. Open a document containing [Balanced](garden(section(one)).md).
  2. Save it.
  3. 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

  • A balanced pair of parentheses in a raw destination saves without an escape.
  • An unbalanced parenthesis keeps its escape, and a destination that would need one either keeps it or switches to the angle-bracket form.
  • Regression coverage asserts both directions.

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

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