diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts
index 62bb6c9d7e..d1d248bbfd 100644
--- a/packages/ui/src/__tests__/markdown-body.test.ts
+++ b/packages/ui/src/__tests__/markdown-body.test.ts
@@ -162,6 +162,33 @@ it('renders display math while leaving ordinary currency alone', () => {
assert.doesNotMatch(markup, /class="maka-math maka-math-inline"/);
});
+it('keeps escaped brackets literal inside Markdown link labels', () => {
+ const markup = renderToStaticMarkup(createElement(LocaleProvider, {
+ locale: 'en',
+ children: createElement(MarkdownBody, {
+ text: '[\\[DISCUSS\\] Clarify](https://example.com)',
+ }),
+ }));
+
+ assert.match(markup, /]*href="https:\/\/example\.com"/);
+ assert.match(markup, /\[DISCUSS\] Clarify/);
+ assert.doesNotMatch(markup, /class="maka-math/);
+ assert.doesNotMatch(markup, /class="katex/);
+});
+
+it('still renders explicit inline math inside Markdown link labels', () => {
+ const markup = renderToStaticMarkup(createElement(LocaleProvider, {
+ locale: 'en',
+ children: createElement(MarkdownBody, {
+ text: '[Value \\(x + 1\\)](https://example.com)',
+ }),
+ }));
+
+ assert.match(markup, /]*href="https:\/\/example\.com"/);
+ assert.match(markup, /class="maka-math maka-math-inline"/);
+ assert.match(markup, /class="katex"/);
+});
+
it('does not treat shell variables, currency, or inline code as dollar-delimited math', () => {
const markup = renderToStaticMarkup(createElement(MarkdownBody, {
text: [
diff --git a/packages/ui/src/markdown-math.tsx b/packages/ui/src/markdown-math.tsx
index 74f5e9f19c..01c894d14e 100644
--- a/packages/ui/src/markdown-math.tsx
+++ b/packages/ui/src/markdown-math.tsx
@@ -90,7 +90,12 @@ export const MARKDOWN_MATH_PLUGINS = [{
},
}] satisfies MarkdownInlinePlugin[];
-function protectMarkdownMath(source: string, startsAtLineStart = true): {
+function protectMarkdownMath(
+ source: string,
+ startsAtLineStart = true,
+ protectEscapedBrackets = false,
+ detectLinkLabels = true,
+): {
text: string;
safeSourceEnd: number;
safeTextEnd: number;
@@ -122,6 +127,38 @@ function protectMarkdownMath(source: string, startsAtLineStart = true): {
continue;
}
+ // Hide escaped label brackets from both the math delimiter scan and the
+ // Markdown bracket matcher, then restore them through the literal plugin.
+ if (
+ protectEscapedBrackets
+ && source[index] === '\\'
+ && (source[index + 1] === '[' || source[index + 1] === ']')
+ ) {
+ text += transportToken(source[index + 1] ?? '', '2');
+ index += 2;
+ atLineStart = false;
+ markSafe();
+ continue;
+ }
+
+ const linkLabel = detectLinkLabels
+ ? readMarkdownLinkLabel(source, index)
+ : undefined;
+ if (linkLabel?.kind === 'pending') {
+ text += source.slice(index);
+ break;
+ }
+ if (linkLabel?.kind === 'match') {
+ const labelSource = source.slice(index + 1, linkLabel.end - 1);
+ const protectedLabel = protectMarkdownMath(labelSource, false, true, false);
+ text += `[${protectedLabel.text}]`;
+ index = linkLabel.end;
+ atLineStart = false;
+ if (protectedLabel.safeSourceEnd === labelSource.length) markSafe();
+ else canMarkSafe = false;
+ continue;
+ }
+
const literalToken = readLiteralToken(source, index);
if (literalToken?.kind === 'pending') {
text += source.slice(index);
@@ -189,6 +226,49 @@ function protectMarkdownMath(source: string, startsAtLineStart = true): {
return { text, safeSourceEnd, safeTextEnd };
}
+function readMarkdownLinkLabel(
+ source: string,
+ index: number,
+):
+ | { kind: 'match'; end: number }
+ | { kind: 'pending' }
+ | undefined {
+ if (source[index] !== '[') return undefined;
+
+ let depth = 1;
+ let cursor = index + 1;
+ while (cursor < source.length) {
+ if (source[cursor] === '\\') {
+ cursor += Math.min(2, source.length - cursor);
+ continue;
+ }
+ if (source[cursor] === '`') {
+ let runEnd = cursor + 1;
+ while (source[runEnd] === '`') runEnd++;
+ const run = source.slice(cursor, runEnd);
+ const close = source.indexOf(run, runEnd);
+ if (close < 0) return { kind: 'pending' };
+ cursor = close + run.length;
+ continue;
+ }
+ if (source[cursor] === '[') {
+ depth++;
+ cursor++;
+ continue;
+ }
+ if (source[cursor] === ']') {
+ depth--;
+ cursor++;
+ if (depth > 0) continue;
+ return source[cursor] === '(' || source[cursor] === '['
+ ? { kind: 'match', end: cursor }
+ : undefined;
+ }
+ cursor++;
+ }
+ return { kind: 'pending' };
+}
+
function readFence(
source: string,
index: number,