Skip to content
Open
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
73 changes: 73 additions & 0 deletions packages/ui/src/context/marked-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,79 @@ test("renders inline and block math", async () => {
expect(await parser.parse("$$\nx^2\n$$\n")).toContain('<span class="katex-display">')
})

test("renders single-dollar inline math", async () => {
const html = await parser.parse("Given $f(0)=f(0)+f(0)$ it follows that $f(0)=0$, so $f(-2)=-f(2)=4$")
expect(html).toContain('<span class="katex">')
expect(html).not.toContain("$")
})

test("renders symbol-heavy inline math", async () => {
const html = await parser.parse("The solution set is $(-\\infty,-3)\\cup(1,+\\infty)$ where $x_1<x_2$ and $x \\in \\mathbb{R}$")
expect(html).toContain('<span class="katex">')
expect(html).not.toContain("$")
})

test("renders CJK text inside \\text{}", async () => {
const html = await parser.parse("Given $\\text{速度} \\times \\text{时间} = \\text{路程}$, solve for speed")
expect(html).toContain('<span class="katex">')
expect(html).not.toContain("$")
})

test("renders single-line dollar-block math", async () => {
const html = await parser.parse("Derivation:\n\n$$f(-x^2)>f(2x)+f(-3)=f(2x-3)$$\n")
expect(html).toContain('<span class="katex-display">')
expect(html).not.toContain("$$")
})

test("renders block math glued to preceding text", async () => {
const html = await parser.parse("so the inequality becomes\n$$\nx^2+2x-3>0\\;\\Rightarrow\\;(x+3)(x-1)>0\n$$\n")
expect(html).toContain('<span class="katex-display">')
expect(html).not.toContain("$$")
})

test("renders double-dollar display math inside a paragraph", async () => {
const html = await parser.parse("Conclusion $$x<-3$$ or $$x>1$$ holds")
expect(html).toContain('<span class="katex-display">')
expect(html).not.toContain("$$")
})

test("leaves money amounts as text", async () => {
const html = await parser.parse("It costs $5, originally $10, ranging from $1,000 to $2,000.")
expect(html).not.toContain("katex")
expect(html).toContain("$5")
expect(html).toContain("$10")
})

test("leaves unclosed math delimiters as text without throwing", async () => {
const html = await parser.parse("Unclosed $f(0)=f(0) keeps streaming\n\nand $$\nx=1")
expect(html).not.toContain("katex")
expect(html).toContain("$f(0)")
})

test("renders invalid latex as inline error instead of throwing", async () => {
const html = await parser.parse("$\\notacommand{x}$")
expect(html).toContain('<span class="katex">')
expect(html).toContain("#cc0000")
})

test("keeps dollar delimiters literal inside code", async () => {
const inline = await parser.parse("Use `$f(0)$` as a placeholder")
expect(inline).toContain("<code>$f(0)$</code>")
expect(inline).not.toContain("katex")
expect(await parser.parse("```text\n$<a>$ and $b$\n```\n")).toBe(
'<pre data-language="text">$<a>$ and $b$</pre>\n',
)
})

test("plain markdown is unaffected by the math extension", async () => {
const html = await parser.parse("# Heading\n\n| a | b |\n| - | - |\n| 1 | 2 |\n\n- list **bold** *italic*\n")
expect(html).toContain("<h1>Heading</h1>")
expect(html).toContain("<table>")
expect(html).toContain("<strong>bold</strong>")
expect(html).toContain("<em>italic</em>")
expect(html).not.toContain("katex")
})

test("uses the configured code highlighter", async () => {
expect(await parser.parse("```ts\nconst value = 1\n```\n")).toBe('<pre data-language="ts">const value = 1</pre>\n')
})
Expand Down
32 changes: 24 additions & 8 deletions packages/ui/src/context/marked-parser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,49 @@ export function createMarkdownParser(highlight: (code: string, language: string)
}

const inlineMathRegex = /^\\\(((?:\\.|[^\\\n])*?)\\\)/
const blockMathRegex = /^\$\$\n([\s\S]+?)\n\$\$(?:\n|$)/
const dollarMathRegex = /^(\$+)(?!\s)((?:\\.|[^\n$])+?)\1(?!\d)/
const blockMathRegex = /^\$\$([\s\S]+?)\$\$(?:\n|$)/

const katexExtension: MarkedExtension = {
extensions: [
{
name: "inlineKatex",
level: "inline",
start(src) {
const index = src.indexOf("\\(")
if (index === -1) return
return index
const paren = src.indexOf("\\(")
const dollar = src.indexOf("$")
if (paren === -1) return dollar === -1 ? undefined : dollar
if (dollar === -1) return paren
return Math.min(paren, dollar)
},
tokenizer(src) {
const match = src.match(inlineMathRegex)
if (!match) return
if (match)
return {
type: "inlineKatex",
raw: match[0],
text: match[1].trim(),
displayMode: false,
}
const dollar = src.match(dollarMathRegex)
if (!dollar || /\s$/.test(dollar[2]!)) return
return {
type: "inlineKatex",
raw: match[0],
text: match[1].trim(),
displayMode: false,
raw: dollar[0],
text: dollar[2]!.trim(),
displayMode: dollar[1]!.length === 2,
}
},
renderer: renderKatexToken,
},
{
name: "blockKatex",
level: "block",
start(src) {
const index = src.indexOf("\n$$")
if (index === -1) return
return index
},
tokenizer(src) {
const match = src.match(blockMathRegex)
if (!match) return
Expand Down
Loading