Public Access
feat: add LaTeX rendering utilities and Tex component
This commit is contained in:
1 parent
611277ee8a
commit
95de63bd98
2 files changed
+56
No files matched your search
@@ -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 }} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import katex from 'katex'
|
||||||
|
|
||||||
|
export const escapeHtml = (s: string): string =>
|
||||||
|
String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
|
||||||
|
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(/("[^&]*?"|'[^&]*?'|`[^`]*?`)/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>')
|
||||||
|
}
|
||||||
Reference in new issue
Block a user