From 3f18a23236430852ef9b05668ada98009890da00 Mon Sep 17 00:00:00 2001 From: nlimpid Date: Fri, 4 Sep 2026 14:00:30 +0800 Subject: [PATCH] fix(mdx): stop double-escaping angle brackets inside code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preflight rewrites in astro.config.ts run over the raw mdx source with code blocks included. Rule 5 turned `` and `` into `<…>`, the downstream pipeline escaped that a second time, and 419 pages ended up rendering the entity text: every C++ include and every curl auth header was broken for anyone copying it off the page. Stash the code regions — fenced blocks, inline spans, and the CliCommand payload built in rule 1.5 — before rules 2-10 run, and restore them once the rewrites are done. The placeholder uses U+E000 rather than NUL so oxlint's no-control-regex stays quiet. Verified against the dev server: the three affected shapes now render as `#include `, `Bearer ` and ``; all 78 absolute-slug English pages still return 200; CliCommand cards, REST brace placeholders and directive callouts are unchanged. --- astro.config.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/astro.config.ts b/astro.config.ts index 4deb109e..5c45e8bc 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -77,6 +77,21 @@ export default defineConfig({ /\r?\n([\s\S]*?)\r?\n[ \t]*<\/CliCommand>/g, (_m, inner: string) => ``, ) + // 1.6) Stash every code region: fenced blocks, inline spans, and the + // CliCommand payload built just above. Their contents are literal + // text that shiki escapes on its own, so the rewrites below must + // not run over them — rule 5 turning `` into `<…>` + // gets escaped a second time downstream and the page renders the + // entity text instead of the angle brackets. Fences are taken + // before inline spans so a fence's own backticks are never read as + // a span, and the CliCommand payload before both since it is a + // JSON string that may itself contain backticks. + const codeRegions: string[] = [] + const stashCode = (m: string) => `\uE000CODE${codeRegions.push(m) - 1}\uE000` + out = out + .replace(//g, stashCode) + .replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*$/gm, stashCode) + .replace(/`[^`\n]+`/g, stashCode) // 2) Convert vitepress heading anchor syntax `## Foo {#bar}` → // `

Foo

`. // MDX parses `{#bar}` as a JSX expression (acorn) before remark plugins run, @@ -147,6 +162,10 @@ export default defineConfig({ /^(:{3,})(success|warning|tip|info|danger|note|caution)([ \t]+)([^\n[\]{]+?)\s*$/gm, (_match, colons, name, _ws, title) => `${colons}${name}[${title.trim()}]`, ) + // Put back the code regions stashed in 1.6, now that every rewrite + // above has run. Indices come from our own placeholders, so the + // fallback is unreachable; it only keeps the lookup total. + out = out.replace(/\uE000CODE(\d+)\uE000/g, (_m, i) => codeRegions[Number(i)] ?? '') return out === code ? null : { code: out, map: null } }, },