feat: add LaTeX rendering utilities and Tex component

This commit is contained in:
LaurensBartelds committed 2026-04-21 21:29:26 +02:00
1 parent 611277ee8a
commit 95de63bd98
2 files changed
+56

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
import { renderLatex } from '../utils/latex'
interface TexProps {
children?: string | null
tag?: keyof JSX.IntrinsicElements
className?: string
style?: React.CSSProperties
}
// Safe: renderLatex only produces KaTeX-controlled HTML + HTML-escaped plain text.
export default function Tex({ children, tag: Tag = 'span', className, style }: TexProps) {
const html = String(children ?? '').split('\n').map(renderLatex).join('<br/>')
return <Tag className={className} style={style} dangerouslySetInnerHTML={{ __html: html }} />
}
+42
View File
@@ -0,0 +1,42 @@
import katex from 'katex'
export const escapeHtml = (s: string): string =>
String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
export const renderLatex = (text: string): string => {
if (!text) return ''
let rest = String(text)
// display math: $$...$$
rest = rest.replace(/\$\$([\s\S]+?)\$\$/g, (_, tex: string) => {
try {
return `<span class="katex-display-wrap">${katex.renderToString(tex, { displayMode: true, throwOnError: false })}</span>`
} catch {
return `<code>${escapeHtml(tex)}</code>`
}
})
// inline math: $...$
rest = rest.replace(/\$([^$\n]+?)\$/g, (_, tex: string) => {
try {
return katex.renderToString(tex, { displayMode: false, throwOnError: false })
} catch {
return `<code>${escapeHtml(tex)}</code>`
}
})
return rest
}
export const highlightCode = (code: string, _lang: string): string => {
// escapeHtml runs first — no raw user content reaches the span injection
const escaped = escapeHtml(code)
return escaped
.replace(/(\/\/[^\n]*|#[^\n]*)/g, '<span class="cm-comment">$1</span>')
.replace(/(&quot;[^&]*?&quot;|&#39;[^&]*?&#39;|`[^`]*?`)/g, '<span class="cm-string">$1</span>')
.replace(/\b(function|return|if|else|const|let|var|for|while|import|export|from|class|new|this|def|print|in|not|and|or|True|False|None|null|undefined|true|false)\b/g, '<span class="cm-keyword">$1</span>')
.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="cm-number">$1</span>')
.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/g, '<span class="cm-fn">$1</span>')
}