From 95de63bd981033c07a82e038cde6fd44e18d50ff Mon Sep 17 00:00:00 2001 From: LaurensBartelds Date: Tue, 21 Apr 2026 21:29:26 +0200 Subject: [PATCH] feat: add LaTeX rendering utilities and Tex component --- frontend/src/components/Tex.tsx | 14 +++++++++++ frontend/src/utils/latex.ts | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 frontend/src/components/Tex.tsx create mode 100644 frontend/src/utils/latex.ts diff --git a/frontend/src/components/Tex.tsx b/frontend/src/components/Tex.tsx new file mode 100644 index 0000000..736f71d --- /dev/null +++ b/frontend/src/components/Tex.tsx @@ -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('
') + return +} diff --git a/frontend/src/utils/latex.ts b/frontend/src/utils/latex.ts new file mode 100644 index 0000000..a047d86 --- /dev/null +++ b/frontend/src/utils/latex.ts @@ -0,0 +1,42 @@ +import katex from 'katex' + +export const escapeHtml = (s: string): string => + String(s) + .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 `${katex.renderToString(tex, { displayMode: true, throwOnError: false })}` + } catch { + return `${escapeHtml(tex)}` + } + }) + // inline math: $...$ + rest = rest.replace(/\$([^$\n]+?)\$/g, (_, tex: string) => { + try { + return katex.renderToString(tex, { displayMode: false, throwOnError: false }) + } catch { + return `${escapeHtml(tex)}` + } + }) + 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, '$1') + .replace(/("[^&]*?"|'[^&]*?'|`[^`]*?`)/g, '$1') + .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, '$1') + .replace(/\b(\d+(?:\.\d+)?)\b/g, '$1') + .replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/g, '$1') +}