Skip to content

fix(editor): don't escape intraword underscores in rich markdown - #22123

Closed
nvimq wants to merge 1 commit into
stablyai:mainfrom
nvimq:fix/markdown-escape-intraword-underscore
Closed

nvimq wants to merge 1 commit into
stablyai:mainfrom
nvimq:fix/markdown-escape-intraword-underscore

Conversation

@nvimq

@nvimq nvimq commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

ELI5

When you edit a rich-markdown block that contains a word with an underscore in it (like user_name_field), saving turns it into user\_name\_field. The visible backslashes then just sit there in the text.

Problem

escapeMarkdownSyntax in @tiptap/markdown backslash-escapes every _ character unconditionally whenever a block gets re-serialized after an edit:

return text.replace(/([\\`*_[\]~])/g, '\\$1')

Any identifier, filename, or variable name containing an underscore gets corrupted with visible backslashes the moment the containing block is edited and saved.

Solution

Patched @tiptap/markdown@3.31.3 (via pnpm patch, following the project's existing config/patches/*.patch convention) so an underscore is only escaped when it is not flanked by word characters on both sides — matching CommonMark's rule that intraword underscores are literal, not emphasis delimiters:

return text.replace(/[\\`*[\]~]|_/g, (match, offset, str) => {
  if (match !== '_') return `\\${match}`
  const before = str[offset - 1]
  const after = str[offset + 1]
  const intraword = before !== undefined && after !== undefined && /\w/.test(before) && /\w/.test(after)
  return intraword ? match : `\\${match}`
})

Leading/trailing/whitespace-flanked underscores (the ones that actually could be emphasis delimiters, e.g. _word, word_, a _ b) still get escaped exactly as before.

Why this scope only

The linked issue reports five broken characters: [, ], _, ~, and backtick/*. This PR fixes only _.

  • Underscore is safe to fix with a purely local, per-character rule: CommonMark's intraword-underscore exclusion is a single-position rule that never depends on anything else in the string.
  • Tilde looked like the same kind of fix at first (only escape a tilde adjacent to another tilde), but that's wrong: the actual parser (marked, GFM mode) pairs up strikethrough delimiters using left/right-flanking rules that can span arbitrary distance within a text run, not just adjacent ~~. A round-trip test caught this — ~5 dollars, see v2~beta got silently reparsed as strikethrough even though neither tilde was doubled. Getting this right requires reimplementing marked's delimiter-matching pass, which is too risky to hand-roll in a vendor patch. Left unchanged (still always escaped, same as before — no regression, no fix).
  • Backslash, backtick, asterisk, brackets are unchanged. Bracket handling in particular already behaves specially outside of escapeMarkdownSyntax in ways I didn't fully trace, and touching it without full understanding risks new corruption.

Testing

New test file src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts, run through a real Editor instance (same pattern as the existing rich-markdown-literal-serialization.test.ts), including a round-trip check (serialize → reparse → compare JSON) proving no corruption on save:

pnpm exec vitest run src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts
✓ 8 tests passed

Also re-ran the existing rich-markdown-literal-serialization.test.ts and rich-markdown-normalize.test.ts (8 tests) to confirm no regression from the patch.

Review

  • config/patches/@tiptap__markdown@3.31.3.patch — the actual fix, applied to src/MarkdownManager.ts, dist/index.js, and dist/index.cjs identically (all three are part of the published package and get patched in sync).
  • pnpm-workspace.yaml — registers the new patch under patchedDependencies, same convention as the existing @xterm/*/lint-staged/etc. patches.
  • src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts — new, focused test file.

Checklist

  • Reproduced the bug (intraword underscore getting escaped on edit)
  • Fixed root cause in the vendored dependency via pnpm patch
  • Added regression tests exercising the real editor/parser, not just the regex in isolation
  • Verified no regression in adjacent existing tests
  • Scoped honestly — did not claim to fix bracket/tilde/backtick/asterisk cases from the linked issue

Fixes #22014 (partial — see "Why this scope only" above).

@tiptap/markdown's escapeMarkdownSyntax backslash-escapes every `_`
character unconditionally when re-serializing an edited block. This
turns any identifier containing underscores (e.g. `user_name_field`)
into `user\_name\_field` on save, which then renders literally with
visible backslashes.

Patch @tiptap/markdown@3.31.3 (via pnpm patch) so an underscore is
only escaped when it is NOT flanked by word characters on both sides,
matching CommonMark's intraword-underscore-is-literal rule. Emphasis-
style underscores (leading/trailing/whitespace-flanked) keep escaping
exactly as before.

Scope: this only fixes underscores. Backslash, backtick, asterisk,
and brackets are unchanged. Tilde is also left unchanged: GFM
strikethrough delimiters in the actual parser (marked) can pair up
across arbitrary distance within a text run based on flanking rules,
not just adjacent `~~`, so a safe per-character fix for tilde isn't
possible without a full delimiter-matching pass - out of scope here.

Fixes stablyai#22014 (partially; see scope note above).
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The TipTap markdown patch now preserves underscores between word characters and escapes underscores in other positions. The patched dependency is registered in the pnpm workspace. New tests cover serialization, markdown round trips, boundary cases, punctuation, and preservation of existing escaping for other markdown syntax characters.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 2d218

Identifiers containing non-ASCII letters can still be rewritten with escaped underscores when rich Markdown is serialized. Extend the intraword check before merging to preserve the intended behavior consistently.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug, solution, scope, linked issue, implementation, and testing. However, it omits the required Visual Proof section and does not complete the required template sections a… Add the required template sections, including Visual Proof with screenshots or an explicit N/A and reason, Agent skill upstream boundary, Notes, and completed Checklist items. Keep the linked issue in the Linked Issue section and document p…
Linked Issues check ⚠️ Warning Issue #22014 describes document-wide escaping of [, ], _, ~, `, and *, including corruption of alert and callout markers. This PR fixes only intraword underscore escaping. The PR summa… Extend the markdown serialization fix for the remaining syntax characters and the document-wide corruption behavior described in #22014. Add regression tests for alert or callout markers and the other affected characters, including content …
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files. (2 skipped: 2 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preventing escapes for intraword underscores in rich Markdown.
Out of Scope Changes check ✅ Passed The patch registration, published package updates, and editor-level regression tests directly support the underscore serialization fix described in the PR and the intraword underscore portion of issue…
Full details: Description check

Explanation

The description explains the bug, solution, scope, linked issue, implementation, and testing. However, it omits the required Visual Proof section and does not complete the required template sections and checklist items.

Resolution

Add the required template sections, including Visual Proof with screenshots or an explicit N/A and reason, Agent skill upstream boundary, Notes, and completed Checklist items. Keep the linked issue in the Linked Issue section and document platform testing and command results.

Full details: Linked Issues check

Explanation

Issue #22014 describes document-wide escaping of [, ], _, ~, `, and *, including corruption of alert and callout markers. This PR fixes only intraword underscore escaping. The PR summary states that brackets, tildes, backticks, asterisks, and backslashes remain unresolved. The tests cover the underscore behavior, but they do not establish a fix for the remaining issue behavior or for rewriting untouched blocks.

Resolution

Extend the markdown serialization fix for the remaining syntax characters and the document-wide corruption behavior described in #22014. Add regression tests for alert or callout markers and the other affected characters, including content in untouched blocks.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: e53afed9-4d76-48b4-9434-0d41afe0b92e

📥 Commits

Reviewing files that changed from the base of the PR and between 197550c and 2d21867.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • config/patches/@tiptap__markdown@3.31.3.patch
  • pnpm-workspace.yaml
  • src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment on lines +12 to +15
+ const before = str[offset - 1];
+ const after = str[offset + 1];
+ const intraword = before !== void 0 && after !== void 0 && /\w/.test(before) && /\w/.test(after);
+ return intraword ? match : `\\${match}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,90p' config/patches/@tiptap__markdown@3.31.3.patch
sed -n '1,120p' src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts
rg -n "escapeMarkdownSyntax|intraword|Unicode|non-ASCII|user_name" config/patches src/renderer/src/components/editor

Repository: stablyai/orca

Length of output: 45546


🌐 Web query:

CommonMark specification underscores intraword Unicode alphanumeric characters delimiter run

💡 Result:

<source_evidence>

<title>Can&`#39`;t mention to users that has underscores around the name · Issue `#496` · commonmark/commonmark-spec</title> GitHub issue 496 in commonmark/CommonMark (link omitted to avoid creating a cross-reference) # Issue: commonmark/commonmark-spec `#496` - Repository: commonmark/commonmark-spec | CommonMark spec, with reference implementations in C and JavaScript | 5K stars | Python ## Can&`#39`;t mention to users that has underscores around the name - Author: [`@gfx`](https://github.com/gfx) - State: closed (completed) - Created: 2017-08-29T10:26:35Z - Updated: 2019-03-23T00:19:52Z - Closed: 2019-03-23T00:19:52Z - Closed by: [`@jgm`](https://github.com/jgm) For a name `@__gfx__` (it&`#39`;s my twitter account): Input: ``` `@__gfx__` ``` Output (GFM is a CommonMark): `@__gfx__` I think it&`#39`;s a bug. --- ### Timeline **`@vyp`** commented · Aug 29, 2017 at 10:35am > This isn&`#39`;t in the spec. AFAIK GFM is a superset of CommonMark, so this is a GFM issue, not a CommonMark one. **`@gfx`** commented · Aug 29, 2017 at 11:03am · Author · edited > No, it&`#39`;s a CommonMark&`#39`;s issue because GitHub can&`#39`;t fix it until CommonMark leaves a `@__xxx__` style token as is. **`@vyp`** commented · Aug 29, 2017 at 11:29am · edited > It follows the spec as far as I can tell: > > http://spec.commonmark.org/0.28/#example-370 > http://spec.commonmark.org/0.28/#ascii-punctuation-character > > Edit: But yeah my bad you&`#39`;re right, it isn&`#39`;t a GFM issue. **`@gfx`** commented · Aug 29, 2017 at 11:41am · Author > Yep, so I think it&`#39`;s a spec issue that should be fixed. **`@mity`** commented · Aug 29, 2017 at 11:42am · edited > > No, it&`#39`;s a CommonMark&`#39`;s issue because GitHub can&`#39`;t fix it until CommonMark leaves a `@__xxx__` style token as is. > > So CommonMark specification should ask implementations to guess what extensions are written on top of it and behave appropriately for whatever the extensions do? Sorry, but it does not make much sense to me unless you know how to implement the Oracle of Delphi. **`@vyp`** commented · Aug 29, 2017 at 11:49am · edited > `@mity` actually `@gfx` was just using the github comment as an example of using commonmark. The issue is commonmark converting `@__gfx__` to `@ gfx ` (as the spec says), instead of keeping it the same. **`@mity`** commented · Aug 29, 2017 at 11:53am > `@vyp` So CommonMark specification should treat just this one particular extension from all the infinite set of potentially useful extensions? > > Github defines and implements the extension. Ask Github to take `@` with higher priority then the `_` in their extension implementation, so the CommonMark&`#39`;s meaning of `_` is suppressed. **`@vyp`** commented · Aug 29, 2017 at 11:57am > So this isn&`#39`;t about the `@`-mention GFM extension. `@gfx` wants the spec to change because they want to be able to correctly display their twitter account when using commonmark. **`@mity`** commented · Aug 29, 2017 at 12:04pm > Sorry, overlooked that. > > But still, the special meaning of `_` can be suppressed by escaping: `@\_\_foo\_\_` -> @\_\_foo\_\_ **`@jgm`** commented · Aug 29, 2017 at 4:25pm > So, we have the idea that `__` does not cause emphasis when > it&`#39`;s used inside a word (intra-word). The question is how > to define precisely what that means. Intuitively, one might > think, the `__` in `@__foo__` is intra-word. But it isn&`#39`;t > intra-word the way we&`#39`;re defining that in the spec. > > > A double `__` can open strong emphasis iff it is part of a > > left-flanking delimiter run and either (a) not part of a > > right-flanking delimiter run or (b) part of a > > right-flanking delimiter run preceded by punctuation. > > In this case, the `__` is a left-flanking and right-flanking > delimiter run, but since it is preceded by puncutation, > it is allowed to open strong emphasis. > > We certainly want some punctuation to appear like a word > boundary: consider, for example, a quotation mark or a comma > or parenthesis: > > "__bold__, __italic__, and __small caps__," she said, > should all be supported (__underline__ …[truncated] <title>Inconsistent em/strong combination parsing at start/center/end of the word.</title> GitHub issue 590 in commonmark/commonmark-spec (link omitted to avoid creating a cross-reference) # Inconsistent em/strong combination parsing at start/center/end of the word. - State: open - Author: AlexanderSergan - Created: 2019-06-20T12:28:06Z - Updated: 2019-06-21T18:50:32Z - Repository: commonmark/commonmark-spec - Number: `#590` --- The issue is that commonmark parse differently em/strong combination in case of using different mark notation: `***italicbold*** -->` ***italicbold*** `_**italicbold**_ -->` _**italicbold**_ `**_italicbold_** -->` **_italicbold_** `***italic***bold -->` ***italic***bold `_**italic**_bold -->` _**italic**_bold `**_italic_**bold -->` **_italic_**bold Commonmark dingus demo with various examples **Is this a desired behavior or a bug?** If yes, I&`#39`;d like to add this examples to tests, so other markup engines (e.g. markdown-it) can fix their implementation to be consistent with commonmark. Here is a discussion about it with markdown-it creator If no, we can figure out how to solve it. Thanks. ## Timeline **Crissov** commented on 2019-06-20T17:26:36Z: > This is the result of intended and desired behavior. Commonmark restricts intraword emphasis to asterisks which are less problematic than underscores, since the latter are too frequently encountered as a replacement for spaces, e.g. in file names. > > Babelmark **AlexanderSergan** commented on 2019-06-21T08:00:17Z: > Thanks for such clear answer, `@Crissov`! > What do you think about adding this cases to tests? Will it make this logic more obvious? > > Motivation of this issue is to add a little bit more consistensy to different parser impementations. > When some cases are not covered by tests, technically implementations must not handle this cases, and if yes - they have strict instructions. > Thanks. - Crissov mentioned - Crissov subscribed - Referenced by PR `#562`: Feature/fixtures for em strong **jgm** commented on 2019-06-21T15:22:06Z: > In > > > `_**italic**_bold` > > the second `_` cannot close emphasis. Why? > > > A single _ character can close emphasis iff it is part of a right-flanking delimiter run and either (a) not part of a left-flanking delimiter run or (b) part of a left-flanking delimiter run followed by punctuation. > > The second `_` is not right flanking, because it is preceded by punctuation and followed by a letter: > > > A right-flanking delimiter run is a delimiter run that is (1) not preceded by Unicode whitespace, and either (2a) not preceded by a punctuation character, or (2b) preceded by a punctuation character and followed by Unicode whitespace or a punctuation character. For purposes of this definition, the beginning and the end of the line count as Unicode whitespace. > > So that explains why we don&`#39`;t get emphasis in this case. It actually has nothing to do with the restriction of intraword emphasis with `_`; indeed, you get symmetrical behavior the other way round: > > > `*__italic__*bold` > > In the other case, > > > `**_italic_**bold` > > the second `**` fails to close emphasis for exactly the same reason as above. > > Hope that clears things up. It may indeed be a good idea to add these two cases to the spec examples. > **Crissov** commented on 2019-06-21T18:50:32Z: > > It may indeed be a good idea to add these two cases to the spec examples. > > Since some implementations that aim for CM conformance yield diverging results, this is probably a good idea indeed. - Referenced by PR `#2624`: feat(markdown): replace unified.js with custom markdown parser/serializer <title>CommonMark Spec</title> https://spec.commonmark.org/spec A character is a Unicode code point. Although some code points (for example, combining accents) do not correspond to characters in an intuitive sense, all code points count as characters for purposes of this spec. ... `Zs` general category, or a tab ... U+0009`), ... 00A`), form ... 00C`), or ... 000D ... An ASCII control character is a character between`U+0000–1F`(both including) or`U+007F`. ... An ASCII punctuation character is`!`,`"`,`#`,`$`,`%`,`&`,`&`#39`;`,`(`,`)`,`*`,`+`,`,`,`-`,`.`,`/`(U+0021–2F),`:`,`;`,`<`,`=`,`>`,`?`,`@`(U+003A–0040),`[`,`\`,`]`,`^`,`_`,```(U+005B–0060),`{`,`|`,`}`, or`~`(U+007B–007E). ... A Unicode punctuation character is a character in the Unicode`P`(puncuation) or`S`(symbol) general categories. ... For security reasons, the Unicode character`U+0000` must be replaced with the REPLACEMENT CHARACTER (`U+FFFD`). ... ## 2.5Entity and numeric character references ... Valid HTML entity references and numeric character references can be used in place of the corresponding Unicode character, with the following exceptions: ... Entity and character references cannot stand in place of special characters that define structural elements in CommonMark. For example, although`*` can be used in place of a literal`*` character,`*` cannot replace`*` in emphasis delimiters, bullet list markers, or thematic breaks. ... Decimal numeric character references consist of`&#`+ a string of 1–7 arabic digits +`;`. A numeric character reference is parsed as the corresponding Unicode character. Invalid Unicode code points will be replaced by the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, the code point`U+0000` will also be replaced by`U+FFFD`. ... numeric character references ... either`X ... + a string ... a hexadecimal numeral instead ... Entity and numeric ... code spans or code blocks ... URLs, link titles, and fenced code block info strings ... setext heading consists of one ... a blank line ... setext heading underline <title>CommonMark Spec</title> https://spec.commonmark.org/0.31.2/ A character is a Unicode code point. Although some code points (for example, combining accents) do not correspond to characters in an intuitive sense, all code points count as characters for purposes of this spec. ... This spec does ... specify an encoding; it thinks ... lines as composed ... characters rather than bytes. ... be limited to ... A line is a sequence of zero or more characters other than line feed (`U+000A`) or carriage return (`U+000D`), followed by a line ending or by the end of file. ... A line containing no characters, or a ... containing only spaces ... U+002 ... a blank line ... is a character ... `Zs` general category, or a tab (`U ... 0009`), line feed ... 000A`), form feed ... 0C`), or carriage return ... An ASCII control character is a character between `U+0000–1F` (both including) or `U+007F`. ... An ASCII punctuation character is `!`, `"`, `#`, `$`, `%`, `&`, `&`#39`;`, `(`, `)`, `*`, `+`, `,`, `-`, `.`, `/` (U+0021–2F), `:`, `;`, `<`, `=`, `>`, `?`, `@` (U+003A–0040), `[`, `\`, `]`, `^`, `_`, ``` (U+005B–0060), `{`, `|`, `}`, or `~` (U+007B–007E). ... A Unicode punctuation character is a character in the Unicode `P` (puncuation) or `S` (symbol) general categories. ... ## 2.3 Insecure characters For security reasons, the Unicode character `U+0000` must be replaced with the REPLACEMENT CHARACTER (`U+FFFD`). ... ## 2.5 Entity and numeric character references Valid HTML entity references and numeric character references can be used in place of the corresponding Unicode character, with the following exceptions: ... Entity and character ... cannot stand in place of special characters that define structural elements in ... , although `*` can be used in place of ... , `*` ... replace `*` in emphasis ... bullet list markers, or thematic ... Decimal numeric character references consist of `&#` + a string of 1–7 arabic digits + `;`. A numeric character reference is parsed as the corresponding Unicode character. Invalid Unicode code points will be replaced by the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, the code point `U+0000` will also be replaced by `U+FFFD`. Example 26 Try It ```markdown # Ӓ Ϡ � ``` ... Hexadecimal numeric character references consist of `&#` + either `X` or `x` + a string of 1-6 hexadecimal digits + `;`. They too are parsed as the corresponding Unicode character (this time specified with a hexadecimal numeral instead of decimal). ... link titles, ... A setext heading consists of one ... a blank line ... An indented code block is composed of one or more indented chunks separated by blank lines. An indented chunk is a sequence of non-blank lines, each preceded by four or more spaces of indentation. The contents of the code block are the ... lines, including trailing line endings, minus four spaces of indentation. An indented code block has no info ... consists of a <title>Redirecting...</title> https://spec.commonmark.org/current/ Redirecting... # Redirecting... Click here if you are not redirected. Redirecting... # Redirecting... [Click here if you are not redirected.](//spec.commonmark.org/0.31.2/)

Citations:


🏁 Script executed:

#!/bin/bash
set -o pipefail
printf '%s\n' '--- patch matches ---'
rg -n -C 10 'const intraword|before !== void 0|str\[offset' config/patches/@tiptap__markdown@3.31.3.patch
printf '%s\n' '--- patch header and nearby changed hunks ---'
rg -n -C 4 'escapeMarkdownSyntax|intraword|function .*escape|Markdown' config/patches/@tiptap__markdown@3.31.3.patch | head -n 220
printf '%s\n' '--- focused test matches ---'
rg -n -C 8 'escapeMarkdownSyntax|intraword|underscore|non-ASCII|Unicode|user_name|markdown' src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts
printf '%s\n' '--- test file size ---'
wc -l src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts

Repository: stablyai/orca

Length of output: 8426


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions

Length of output: 10797


Make the intraword check Unicode- and code-point-aware.

All three patched artifacts use UTF-16 indexing and ASCII-only /\w/. Therefore пристаням_стремятся becomes пристаням\_стремятся, and astral letters also fail because their surrogate code units are not word characters. CommonMark treats an underscore between Unicode letters as literal. Use code-point-aware neighbor lookup and Unicode property checks, such as /[\p{L}\p{N}_]/u, in all three artifacts. Add regression coverage for Cyrillic and astral-letter identifiers.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The intraword rule uses \w, which matches _. Underscores adjacent to another underscore are left unescaped even though CommonMark treats _ as punctuation, so a text node like __init__ now round-trips into an added italic mark. Details inline.

Reviewed changes — vendor patch to @tiptap/markdown so intraword underscores survive re-serialization, plus its registration, lockfile hash, and regression tests.

  • Escaping rule — escapeMarkdownSyntax keeps the old character set and now only leaves _ literal when both neighbours match \w.
  • Packaging — patch registered in pnpm-workspace.yaml; pnpm-lock.yaml records its patch hash and a patched snapshot. The patch applies cleanly on install.
  • Tests — new rich-markdown-escape-syntax.test.ts drives a real Editor and a serialize → reparse round trip; I confirmed it fails (3 tests) when the dist function is reverted, so it is real coverage.

ℹ️ Nitpicks

  • pnpm-lock.yaml carries unrelated churn: express-rate-limit: 8.5.2(express@5.2.1) → 8.5.2(express@5.2.1(supports-color@7.2.0)). Re-running install from a clean base would drop it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

+ if (match !== "_") return `\\${match}`;
+ const before = str[offset - 1];
+ const after = str[offset + 1];
+ const intraword = before !== void 0 && after !== void 0 && /\w/.test(before) && /\w/.test(after);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\w includes _, so an underscore next to another underscore is classified as intraword and left unescaped — but CommonMark treats _ as punctuation, so such a run can still open or close emphasis. A text node __init__ serializes to \__init_\_, which reparses as literal _ + italic init + _, adding an italic mark the original text did not have. Reverting this patch makes __init__ round-trip literally, so this is a new regression; use a neighbour class that excludes _ (e.g. [A-Za-z0-9]) in all three copies of the function.

Technical details
# intraword check treats `_` as a word character

## Affected sites
- `config/patches/@tiptap__markdown@3.31.3.patch` line 14 (`dist/index.cjs`), line 33 (`dist/index.js`), line 54 (`src/MarkdownManager.ts`) — `\w` is used for both the `before` and `after` neighbour tests.
- `src/renderer/src/components/editor/rich-markdown-escape-syntax.test.ts` — no case covers a doubled underscore at a text boundary, so the suite stays green over the regression.

## Evidence
- `__init__` → `editor.getMarkdown()` = `\__init_\_`; reopening yields `text "_"`, `italic "init"`, `text "_"` (an added mark).
- `__a__`, `__1__`, `a. __b__c` behave the same; the pre-patch regex round-trips all of them (verified against a real `Editor`).
- `\w` matches ASCII letters, digits and `_`. CommonMark classifies `_` as punctuation (Pc), so an underscore run is not necessarily intraword.

## Required outcome
- Underscores adjacent to `_` stay escaped, while `user_name_field` / `a_2_b` / `_word` / `word_` keep their current behaviour.

## Suggested approach
- Use `/[A-Za-z0-9]/` (or a Unicode letter/number class) for the neighbour tests in all three copies. Verified locally: `__init__` → `\_\_init_\_` (round-trips) and `user_name_field` stays literal.
- Add a doubled-underscore regression test (`__init__`, `__a__`).

@nwparker

Copy link
Copy Markdown
Contributor

Closing as superseded by merged PR #23280. The merged source-fidelity change includes the same rich-markdown escape-fidelity test area and resolves linked issue #22014.

@nwparker nwparker closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Rich markdown editor escapes brackets document-wide on any edit, corrupting GitHub alerts and callouts

2 participants