Skip to content

Fix backticks escaped inside code blocks - #359

Open
Pranjal-SB wants to merge 1 commit into
kepano:mainfrom
Pranjal-SB:fix/code-block-backtick-escape
Open

Fix backticks escaped inside code blocks#359
Pranjal-SB wants to merge 1 commit into
kepano:mainfrom
Pranjal-SB:fix/code-block-backtick-escape

Conversation

@Pranjal-SB

Copy link
Copy Markdown

Fixes #353

What

preformattedCode escaped every backtick in a code block. Backslash escapes are literal inside a fence, so the \ reached the output and the code came out invalid.

Why

Any code containing a backtick was corrupted. JS template literals are the common case — res.write(`...`), fetch(`...`) — but it applies to shell command substitution and any language that uses the character.

src/markdown.ts:646:

const cleanCode = code
    .trim()
    .replace(/`/g, '\`');

return `\n\`\`\`${language}\n${cleanCode}\n\`\`\`\n`;

Turndown's own fencedCodeBlock rule already handles this correctly, by growing the fence past the longest backtick run in the code. This custom rule exists to read data-lang/data-language, which turndown's does not, and reimplemented the body handling with an escape instead. This restores the fence-growing behaviour and drops the escape.

Only a line-leading run of three or more backticks can close a fence, so backticks appearing mid-line need no treatment at all — which is why the escape was never necessary.

How

const cleanCode = code.trim();
const fenceSize = (cleanCode.match(/^`{3,}/gm) || [])
    .reduce((size, run) => Math.max(size, run.length + 1), 3);
const fence = '`'.repeat(fenceSize);

return `\n${fence}${language}\n${cleanCode}\n${fence}\n`;

String.matchAll is ES2020 and tsconfig.json targets es2019, so this uses match(/…/gm).

One expected fixture changed

tests/expected/general--obsidian.md-blog-verify-obsidian-sync-encryption.md had the bug recorded in it — line 41 was:

console.log(\`The salt of your vault ${vault.name} is: …\`);

That is the same defect from obsidian.md's own blog. Corrected to bare backticks. It is the only expected file affected; the other 200+ are byte-identical.

Verification

Four new tests in tests/markdown.test.ts under fenced code blocks. Confirmed failing before the fix:

1. should not escape backticks inside a code block
   expected '```javascript\nres.write(\`data: ${pa…' to contain 'res.write(`data: ${payload}`);'
2. should lengthen the fence when the code contains a fence line
   expected '```markdown\n\`\`\`js\nconst a = 1;\n…' to contain '````markdown\n```js\nconst a = 1;\n``…'
3. should size the fence past the longest fence line in the code
   expected '```\na\n\`\`\`\`\`\nb\n```' to contain '``````\na\n`````\nb\n``````'

Full suite after the fix:

$ TZ=UTC npx vitest run
PASS (446) FAIL (1)
1. Performance parse time per fixture (total including DOM parsing)

The perf failure is the 5s default timeout on my machine, not this change — it fails identically on a clean checkout of main.

npm run build and npm run size both pass:

✓ dist/index.js  88.4 KB gzip / 92 KB budget  [ok]
✓ dist/index.full.js  204.2 KB gzip / 215 KB budget  [ok]
All bundles within budget.

And against the URL from the issue, via npx tsx src/cli.ts parse … --markdown:

before

  res.write(\`event: endpoint
data: /messages?sessionId=${sessionId}

\`);

after

  res.write(`event: endpoint
data: /messages?sessionId=${sessionId}

`);

Not addressed here: the same article's code blocks without a language identifier, which #353 calls out as a separate request.

The preformattedCode rule escaped every backtick in a code block. Backslash
escapes are literal inside a fence, so `\`` reached the output verbatim and
any code containing a backtick came out invalid — JS template literals and
fetch(`...`) calls being the common case.

Turndown's own fencedCodeBlock rule already handles this by growing the fence
past the longest backtick run in the code. The custom rule exists to read
data-lang/data-language, and reimplemented the body handling with an escape
instead. This restores the fence-growing behaviour.

Only a line-leading run of three or more backticks can close a fence, so
backticks appearing mid-line need no treatment at all.

tests/expected/general--obsidian.md-blog-verify-obsidian-sync-encryption.md
had the bug baked into it — its console.log template literal was recorded
with escaped delimiters. Updated to the corrected output.

Fixes kepano#353
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.

JavaScript template literal delimiters are escaped when converting WeChat articles

1 participant