fix: escape plain text in renderLatex; add latex utility tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LaurensBarteldsandClaude Sonnet 4.6 committed 2026-04-21 21:35:43 +02:00
1 parent 95de63bd98
commit b0efb524b1
2 files changed
+89 -7

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest'
import { escapeHtml, renderLatex, highlightCode } from './latex'
describe('escapeHtml', () => {
it('escapes all 5 HTML entities', () => {
expect(escapeHtml('&')).toBe('&amp;')
expect(escapeHtml('<')).toBe('&lt;')
expect(escapeHtml('>')).toBe('&gt;')
expect(escapeHtml('"')).toBe('&quot;')
expect(escapeHtml("'")).toBe('&#39;')
})
it('escapes multiple entities in a single string', () => {
expect(escapeHtml('<b class="x">it\'s &amp;</b>')).toBe(
'&lt;b class=&quot;x&quot;&gt;it&#39;s &amp;amp;&lt;/b&gt;'
)
})
})
describe('renderLatex', () => {
it('returns empty string for empty input', () => {
expect(renderLatex('')).toBe('')
})
it('HTML-escapes plain text with dangerous characters', () => {
const result = renderLatex('<img src=x onerror=alert(1)>')
expect(result).not.toContain('<img')
expect(result).toContain('&lt;img')
expect(result).toContain('&gt;')
})
it('renders inline math $x^2$ and output contains katex', () => {
const result = renderLatex('$x^2$')
expect(result.toLowerCase()).toContain('katex')
})
it('renders display math $$x^2$$ and wraps in katex-display-wrap span', () => {
const result = renderLatex('$$x^2$$')
expect(result).toContain('katex-display-wrap')
expect(result.toLowerCase()).toContain('katex')
})
it('HTML-escapes plain text portions alongside math', () => {
const result = renderLatex('<b>bold</b> and $x^2$')
// Plain text portion must be escaped
expect(result).toContain('&lt;b&gt;bold&lt;/b&gt;')
// Math portion must still be rendered
expect(result.toLowerCase()).toContain('katex')
})
})
describe('highlightCode', () => {
it('wraps keywords in cm-keyword spans', () => {
const result = highlightCode('const x = 1', 'js')
expect(result).toContain('<span class="cm-keyword">const</span>')
})
it('wraps comments in cm-comment spans', () => {
const result = highlightCode('// a comment', 'js')
expect(result).toContain('cm-comment')
})
it('wraps string literals in cm-string spans', () => {
const result = highlightCode('"hello"', 'js')
expect(result).toContain('cm-string')
})
it('wraps numbers in cm-number spans', () => {
const result = highlightCode('return 42', 'js')
expect(result).toContain('<span class="cm-number">42</span>')
})
})
+17 -7
View File
@@ -11,23 +11,33 @@ export const escapeHtml = (s: string): string =>
export const renderLatex = (text: string): string => {
if (!text) return ''
let rest = String(text)
// display math: $$...$$
const mathPlaceholders: string[] = []
// Replace math regions with null-byte placeholders before HTML-escaping
rest = rest.replace(/\$\$([\s\S]+?)\$\$/g, (_, tex: string) => {
const idx = mathPlaceholders.length
try {
return `<span class="katex-display-wrap">${katex.renderToString(tex, { displayMode: true, throwOnError: false })}</span>`
mathPlaceholders.push(`<span class="katex-display-wrap">${katex.renderToString(tex, { displayMode: true, throwOnError: false })}</span>`)
} catch {
return `<code>${escapeHtml(tex)}</code>`
mathPlaceholders.push(`<code>${escapeHtml(tex)}</code>`)
}
return `\x00MATH${idx}\x00`
})
// inline math: $...$
rest = rest.replace(/\$([^$\n]+?)\$/g, (_, tex: string) => {
const idx = mathPlaceholders.length
try {
return katex.renderToString(tex, { displayMode: false, throwOnError: false })
mathPlaceholders.push(katex.renderToString(tex, { displayMode: false, throwOnError: false }))
} catch {
return `<code>${escapeHtml(tex)}</code>`
mathPlaceholders.push(`<code>${escapeHtml(tex)}</code>`)
}
return `\x00MATH${idx}\x00`
})
return rest
// Escape plain text (null bytes survive escapeHtml since it only targets HTML chars)
rest = escapeHtml(rest)
// Restore rendered math
return rest.replace(/\x00MATH(\d+)\x00/g, (_, i) => mathPlaceholders[Number(i)])
}
export const highlightCode = (code: string, _lang: string): string => {