Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/ui/src/__tests__/markdown-body.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<a\b[^>]*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, /<a\b[^>]*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: [
Expand Down
82 changes: 81 additions & 1 deletion packages/ui/src/markdown-math.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down