Public Access
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:
1 parent
95de63bd98
commit
b0efb524b1
2 files changed
+89
-7
No files matched your search
@@ -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('&')
|
||||
expect(escapeHtml('<')).toBe('<')
|
||||
expect(escapeHtml('>')).toBe('>')
|
||||
expect(escapeHtml('"')).toBe('"')
|
||||
expect(escapeHtml("'")).toBe(''')
|
||||
})
|
||||
|
||||
it('escapes multiple entities in a single string', () => {
|
||||
expect(escapeHtml('<b class="x">it\'s &</b>')).toBe(
|
||||
'<b class="x">it's &amp;</b>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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('<img')
|
||||
expect(result).toContain('>')
|
||||
})
|
||||
|
||||
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('<b>bold</b>')
|
||||
// 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>')
|
||||
})
|
||||
})
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user