Commit 590d71a3 authored by Kantz's avatar Kantz
Browse files

Revert "conversion from mathjax to mathlive"

This reverts commit a94d3cb3.
parent a94d3cb3
......@@ -5,6 +5,22 @@
<link rel="icon" type="image/png" href="/SuMINT-Logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Math Tutor</title>
<script>
window.MathJax = {
tex: {
inlineMath: [["$", "$"], ["\\(", "\\)"]],
displayMath: [["$$", "$$"], ["\\[", "\\]"]],
},
options: {
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
},
};
</script>
<script
id="mathjax-script"
async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
></script>
</head>
<body>
<div id="root"></div>
......
......@@ -12,7 +12,6 @@
},
"dependencies": {
"iconoir-react": "^7.11.0",
"mathlive": "^0.110.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
......
......@@ -11,9 +11,6 @@ importers:
iconoir-react:
specifier: ^7.11.0
version: 7.11.0(react@19.2.3)
mathlive:
specifier: ^0.110.0
version: 0.110.0
react:
specifier: ^19.2.0
version: 19.2.3
......@@ -66,9 +63,6 @@ importers:
packages:
'@arnog/colors@0.5.0':
resolution: {integrity: sha512-NB7yqrO7qvInsqJdqz6C6mDU+2oiKmbBbk3czMS0E7KkGxLRy538tEHCsB5TKqmaxxXpi6/U120TnGl8NsDePg==}
'@babel/code-frame@7.28.6':
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
engines: {node: '>=6.9.0'}
......@@ -152,10 +146,6 @@ packages:
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
engines: {node: '>=6.9.0'}
'@cortex-js/compute-engine@0.58.0':
resolution: {integrity: sha512-N0+vXjVZfKwJS7ZIGvq41mojI55m5TrdDXveAXzNjzjZ9iNiNrdEzXL2EmrcP+9GCWCwTonrEeZWAIG78f3INg==}
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
......@@ -280,28 +270,24 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
......@@ -536,10 +522,6 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
complex-esm@2.1.1-esm1:
resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
engines: {node: '>=16.14.2', npm: '>=8.5.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
......@@ -566,9 +548,6 @@ packages:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
......@@ -848,28 +827,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
......@@ -900,9 +875,6 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
mathlive@0.110.0:
resolution: {integrity: sha512-UOpJsQ6h1eeN0xZULGTl1MwUB/lZLCDuzKeHvKEh7Zra8U/rtgDNrssj5PZdJtBTIV48AfEhtv6rv47AeMOxJA==}
mdast-util-from-markdown@2.0.2:
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
......@@ -1299,8 +1271,6 @@ packages:
snapshots:
'@arnog/colors@0.5.0': {}
'@babel/code-frame@7.28.6':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
......@@ -1413,12 +1383,6 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@cortex-js/compute-engine@0.58.0':
dependencies:
'@arnog/colors': 0.5.0
complex-esm: 2.1.1-esm1
decimal.js: 10.6.0
'@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
......@@ -1808,8 +1772,6 @@ snapshots:
comma-separated-tokens@2.0.3: {}
complex-esm@2.1.1-esm1: {}
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
......@@ -1828,8 +1790,6 @@ snapshots:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
......@@ -2132,10 +2092,6 @@ snapshots:
dependencies:
yallist: 3.1.1
mathlive@0.110.0:
dependencies:
'@cortex-js/compute-engine': 0.58.0
mdast-util-from-markdown@2.0.2:
dependencies:
'@types/mdast': 4.0.4
......
......@@ -2,8 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n";
import { escapeMarkdownInMath } from "../../utils/mathText";
import { renderMath } from "../../utils/mathRender";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type MessageBubbleProps = {
role: "user" | "assistant";
......@@ -13,6 +12,14 @@ type MessageBubbleProps = {
docSlugIndex?: Record<string, RetrievedDoc>;
};
declare global {
interface Window {
MathJax?: {
typesetPromise?: (elements?: Element[]) => Promise<void>;
};
}
}
export default function MessageBubble({
role,
text,
......@@ -29,15 +36,15 @@ export default function MessageBubble({
if (role !== "assistant" || parts.length < 2) {
return {
thinkingText: "",
answerText: escapeMarkdownInMath(text),
answerText: escapeAsterisksInsideMath(text),
};
}
const rawThinking = parts.slice(0, -1).join("</think>").trim();
const rawAnswer = parts[parts.length - 1].trim();
return {
thinkingText: escapeMarkdownInMath(rawThinking),
answerText: escapeMarkdownInMath(rawAnswer),
thinkingText: escapeAsterisksInsideMath(rawThinking),
answerText: escapeAsterisksInsideMath(rawAnswer),
};
}, [role, text]);
......@@ -122,7 +129,22 @@ export default function MessageBubble({
return;
}
renderMath(bubbleRef.current);
const typeset = () => {
if (window.MathJax?.typesetPromise && bubbleRef.current) {
window.MathJax.typesetPromise([bubbleRef.current]).catch(() => undefined);
}
};
if (window.MathJax?.typesetPromise) {
typeset();
return;
}
const script = document.getElementById("mathjax-script");
if (script) {
script.addEventListener("load", typeset);
return () => script.removeEventListener("load", typeset);
}
}, [answerText, isRawView, isThinkingExpanded, thinkingText]);
return (
......
import "mathlive";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { EditPencil, Send, Upload } from "iconoir-react";
import type { MathfieldElement } from "mathlive";
import { t } from "../../i18n";
type MessageInputProps = {
......@@ -30,12 +28,9 @@ export default function MessageInput({
const canSend = value.trim().length > 0;
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const mathfieldRef = useRef<MathfieldElement | null>(null);
const wasSendingRef = useRef(isSending);
const shouldRestoreFocusRef = useRef(false);
const sendButtonStartedFromTextareaRef = useRef(false);
const [isMathEditorOpen, setIsMathEditorOpen] = useState(false);
const [mathValue, setMathValue] = useState("");
useEffect(() => {
if (wasSendingRef.current && !isSending) {
......@@ -48,23 +43,6 @@ export default function MessageInput({
wasSendingRef.current = isSending;
}, [isSending]);
useEffect(() => {
const mathfield = mathfieldRef.current;
if (!mathfield) {
return;
}
mathfield.smartFence = true;
mathfield.mathVirtualKeyboardPolicy = "manual";
const handleInput = () => setMathValue(mathfield.value);
mathfield.addEventListener("input", handleInput);
return () => {
mathfield.removeEventListener("input", handleInput);
};
}, [isMathEditorOpen]);
const handleUploadClick = () => {
uploadInputRef.current?.click();
};
......@@ -85,38 +63,6 @@ export default function MessageInput({
onSend();
};
const insertAtCursor = (snippet: string) => {
const textarea = textareaRef.current;
if (!textarea) {
onChange(value ? `${value} ${snippet}` : snippet);
return;
}
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const nextValue = value.slice(0, start) + snippet + value.slice(end);
onChange(nextValue);
requestAnimationFrame(() => {
const nextCursor = start + snippet.length;
textarea.focus({ preventScroll: true });
textarea.setSelectionRange(nextCursor, nextCursor);
});
};
const handleInsertMath = () => {
const latex = mathValue.trim();
if (!latex) {
return;
}
insertAtCursor(`$${latex}$`);
setMathValue("");
if (mathfieldRef.current) {
mathfieldRef.current.value = "";
}
};
return (
<div className="composer">
<div className="composer-row">
......@@ -147,16 +93,6 @@ export default function MessageInput({
className="canvas-upload-input"
onChange={handleUploadChange}
/>
<button
className="btn"
type="button"
onClick={() => setIsMathEditorOpen((prev) => !prev)}
disabled={isSending}
aria-pressed={isMathEditorOpen}
title="Formula editor"
>
f (x)
</button>
{onTip ? (
<button
className="btn"
......@@ -240,27 +176,6 @@ export default function MessageInput({
}
}}
/>
{isMathEditorOpen ? (
<div className="composer-math">
<math-field
ref={mathfieldRef}
className="composer-mathfield"
onKeyDown={(event) => {
if (isSending) {
event.preventDefault();
return;
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
handleInsertMath();
}
}}
>
{mathValue}
</math-field>
</div>
) : null}
</div>
);
}
import { useEffect, useMemo, useRef } from "react";
import ReactMarkdown from "react-markdown";
import { t } from "../../i18n";
import { escapeMarkdownInMath } from "../../utils/mathText";
import { renderMath } from "../../utils/mathRender";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
declare global {
interface Window {
MathJax?: {
typesetPromise?: (elements?: Element[]) => Promise<void>;
};
}
}
type DocCardProps = {
title: string;
......@@ -25,12 +32,27 @@ export default function DocCard({
}: DocCardProps) {
const contentRef = useRef<HTMLDivElement | null>(null);
const renderedSnippet = useMemo(
() => escapeMarkdownInMath(snippetMarkdown),
() => escapeAsterisksInsideMath(snippetMarkdown),
[snippetMarkdown]
);
useEffect(() => {
renderMath(contentRef.current);
const typeset = () => {
if (window.MathJax?.typesetPromise && contentRef.current) {
window.MathJax.typesetPromise([contentRef.current]).catch(() => undefined);
}
};
if (window.MathJax?.typesetPromise) {
typeset();
return;
}
const script = document.getElementById("mathjax-script");
if (script) {
script.addEventListener("load", typeset);
return () => script.removeEventListener("load", typeset);
}
}, [snippetMarkdown]);
return (
......
import { useEffect, useMemo, useRef } from "react";
import ReactMarkdown from "react-markdown";
import { t } from "../../i18n";
import { escapeMarkdownInMath } from "../../utils/mathText";
import { renderMath } from "../../utils/mathRender";
import { escapeAsterisksInsideMath } from "../../utils/mathMarkdown";
type SocraticPanelProps = {
selectedTopicLabel?: string;
......@@ -18,7 +17,7 @@ export default function SocraticPanel({
const contentRef = useRef<HTMLDivElement | null>(null);
const summaryMarkdown = selectedTopicSummary?.trim() || "";
const renderedSummary = useMemo(
() => escapeMarkdownInMath(summaryMarkdown),
() => escapeAsterisksInsideMath(summaryMarkdown),
[summaryMarkdown]
);
......@@ -26,7 +25,11 @@ export default function SocraticPanel({
if (!contentRef.current) {
return;
}
renderMath(contentRef.current);
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([contentRef.current]).catch(() => undefined);
}, [selectedTopicSummary]);
return (
......
import { useEffect, useRef } from "react";
import type { TaskImage } from "../../api/taskApi";
import { t } from "../../i18n";
import { renderMath } from "../../utils/mathRender";
import TaskContent from "./TaskContent";
type SelectOption = {
......@@ -52,7 +51,11 @@ export default function TaskPanel({
if (!selectedTaskText || !taskDisplayRef.current) {
return;
}
renderMath(taskDisplayRef.current);
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([taskDisplayRef.current]).catch(() => undefined);
}, [selectedTaskText]);
return (
......
......@@ -1257,17 +1257,19 @@ export default function ChatPage() {
document.getElementById("meta").textContent = path;
document.getElementById("content").innerHTML = marked.parse(markdown);
</script>
<script type="module">
import { renderMathInElement } from "https://esm.run/mathlive";
renderMathInElement(document.getElementById("content"), {
TeX: {
delimiters: {
inline: [["$", "$"], ["\\\\(", "\\\\)"]],
display: [["$$", "$$"], ["\\\\[", "\\\\]"]],
},
},
});
<script>
window.MathJax = {
tex: {
inlineMath: [["$", "$"], ["\\\\(", "\\\\)"]],
displayMath: [["$$", "$$"], ["\\\\[", "\\\\]"]],
}
};
</script>
<script
id="mathjax-script"
async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
></script>
</body>
</html>`;
......
......@@ -9,7 +9,6 @@ import {
getSelectionRouteForOrchestrator,
type OrchestratorName,
} from "../utils/orchestrator";
import { renderMath } from "../utils/mathRender";
import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
......@@ -92,7 +91,11 @@ export default function SelectionPage({ mode }: SelectionPageProps) {
if (!isTaskMode || !selectedTask?.fullText || !taskDisplayRef.current) {
return;
}
renderMath(taskDisplayRef.current);
const mathjax = window.MathJax;
if (!mathjax?.typesetPromise) {
return;
}
mathjax.typesetPromise([taskDisplayRef.current]).catch(() => undefined);
}, [isTaskMode, selectedTask?.fullText]);
const handleStart = async () => {
......
......@@ -585,30 +585,6 @@ body {
cursor: not-allowed;
}
.composer-math {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px solid #d8d1c4;
border-radius: 12px;
background: #fffaf0;
}
.composer-mathfield {
width: 100%;
min-height: 54px;
border: 1px solid #d8d1c4;
border-radius: 10px;
background: #fff;
padding: 10px 12px;
--hue: 32;
}
.composer-mathfield::part(virtual-keyboard-toggle) {
display: none;
}
.btn {
border: 1px solid #b9b2a4;
background: #ffffff;
......
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import type { MathfieldElement } from "mathlive";
export {};
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"math-field": DetailedHTMLProps<
HTMLAttributes<MathfieldElement>,
MathfieldElement
>;
}
}
}
const MATH_SEGMENT_PATTERN =
/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$(?:\\.|[^$\\\n])+\$)/g;
// wandlet * in \* um sodass * in der MArkdown darstellung nich zum zeichen
// für kursiv oder fett gedruckt wird.
const escapeAsterisks = (value: string) => value.replace(/\*/g, "\\*");
// Wandelt einfache Bruchschreibweisen in LaTeX-Brüche um.
// Beispiele:
// - (x+1)/(x-1) -> \frac{x+1}{x-1}
// - a/b -> \frac{a}{b}
//
// Diese Ersetzung läuft nur innerhalb erkannter Math-Segmente, damit
// normale Markdown-Links, URLs oder Fließtext unangetastet bleiben.
const convertSimpleFractions = (value: string) => {
let output = value;
// Vorrang für explizit geklammerte Ausdrücke:
// (x+1)/(x-1) => \frac{x+1}{x-1}
output = output.replace(
/\(([^()]+)\)\s*\/\s*\(([^()]+)\)/g,
(_match, numerator: string, denominator: string) =>
`\\frac{${numerator}}{${denominator}}`
);
// Einseitig geklammerte Ausdrücke:
// (a+b)/c => \frac{a+b}{c}
// a/(b+c) => \frac{a}{b+c}
// Diese Regel kommt vor der ganz einfachen a/b-Regel, damit die Klammern
// erhalten bleiben und nicht versehentlich nur Teilstücke ersetzt werden.
output = output.replace(
/\(([^()]+)\)\s*\/\s*([A-Za-z0-9]+)(?![\w{])/g,
(_match, numerator: string, denominator: string) =>
`\\frac{${numerator}}{${denominator}}`
);
output = output.replace(
/([A-Za-z0-9]+)\s*\/\s*\(([^()]+)\)(?![\w{])/g,
(_match, numerator: string, denominator: string) =>
`\\frac{${numerator}}{${denominator}}`
);
// Danach sehr einfache Kurzschreibweise:
// a/b => \frac{a}{b}
// Hinweis: Das ist bewusst konservativ gehalten, damit keine komplexen
// Ausdrücke oder schon vorhandene LaTeX-Befehle umgeschrieben werden.
output = output.replace(
/(^|[^\\\w}])([A-Za-z0-9]+)\s*\/\s*([A-Za-z0-9]+)(?![\w{])/g,
(_match, prefix: string, numerator: string, denominator: string) =>
`${prefix}\\frac{${numerator}}{${denominator}}`
);
return output;
};
// Formatiert Potenzen so, dass zusammengesetzte Exponenten in Klammern
// vollständig in LaTeX-Klammern gesetzt werden.
// Beispiel:
// - x^(a+b) -> x^{a+b}
const normalizePowers = (value: string) =>
value.replace(
/([A-Za-z0-9\\]+)\s*\^\s*\(([^()]+)\)/g,
(_match, base: string, exponent: string) => `${base}^{${exponent}}`
);
// Wandelt sqrt(...) in LaTeX-Wurzelnotation um.
// Beispiel:
// - sqrt(x) -> \sqrt{x}
const normalizeSquareRoots = (value: string) =>
value.replace(/\bsqrt\s*\(([^()]+)\)/g, (_match, inner: string) => `\\sqrt{${inner}}`);
// Wandelt exp(x) in e^x um.
// Beispiel:
// - exp(x) -> e^x
const normalizeExp = (value: string) =>
value.replace(/\bexp\s*\(([^()]+)\)/g, (_match, inner: string) => `e^${inner}`);
// Wandelt log(x) in eine lesbare Logarithmus-Notation um.
// Beispiel:
// - log(x) -> \log x
const normalizeLog = (value: string) =>
value.replace(/\blog\s*\(([^()]+)\)/g, (_match, inner: string) => `\\log ${inner}`);
// Wandelt pi in das griechische LaTeX-Symbol um.
// Beispiel:
// - pi -> \pi
const normalizePi = (value: string) =>
value.replace(/\bpi\b/g, "\\pi");
// Wandelt sin/cos/tan-Aufrufe in LaTeX-Makros um.
// Beispiele:
// - sin(a) -> \sin(a)
// - cos(a) -> \cos(a)
// - tan(a) -> \tan(a)
const normalizeTrigFunctions = (value: string) =>
value.replace(/\b(sin|cos|tan)\s*\(([^()]+)\)/g, (_match, fn: string, inner: string) => `\\${fn}(${inner})`);
// Wandelt inf/minf in Infinities um.
// Beispiele:
// - inf -> \infty
// - minf -> - \infty
const normalizeInfinityTokens = (value: string) =>
value
.replace(/\bminf\b/g, "- \\infty")
.replace(/\binf\b/g, "\\infty");
// Macht bereits geschriebene Mengenklammern in Math-Segmenten sichtbar.
// Beispiele:
// - \{1, 2, 3\} -> \\{1, 2, 3\\}
// - \{x \in \mathbb{R} \mid x > 0\} -> \\{x \in \mathbb{R} \mid x > 0\\}
//
// Wichtig:
// - Rohe {a,b,c} werden absichtlich NICHT umgeschrieben, damit z. B. x_{a,b}
// nicht fälschlich als Menge interpretiert wird.
// - Nur bereits escapte Klammern werden angehoben, damit MathJax sie sichtbar
// rendert statt sie als Gruppierungszeichen zu schlucken.
const normalizeSetBraces = (value: string) =>
value
.replace(/\\\{/g, "\\\\{")
.replace(/\\\}/g, "\\\\}");
export const escapeAsterisksInsideMath = (input: string): string => {
return input.replace(MATH_SEGMENT_PATTERN, (segment) => {
if (segment.startsWith("$$") && segment.endsWith("$$")) {
const inner = segment.slice(2, -2);
return `$$${normalizeSetBraces(normalizeInfinityTokens(normalizeTrigFunctions(normalizePi(normalizeLog(normalizeExp(normalizeSquareRoots(normalizePowers(convertSimpleFractions(escapeAsterisks(inner))))))))))}$$`;
}
if (segment.startsWith("\\[") && segment.endsWith("\\]")) {
const inner = segment.slice(2, -2);
return `\\[${normalizeSetBraces(normalizeInfinityTokens(normalizeTrigFunctions(normalizePi(normalizeLog(normalizeExp(normalizeSquareRoots(normalizePowers(convertSimpleFractions(escapeAsterisks(inner))))))))))}\\]`;
}
if (segment.startsWith("\\(") && segment.endsWith("\\)")) {
const inner = segment.slice(2, -2);
return `\\(${normalizeSetBraces(normalizeInfinityTokens(normalizeTrigFunctions(normalizePi(normalizeLog(normalizeExp(normalizeSquareRoots(normalizePowers(convertSimpleFractions(escapeAsterisks(inner))))))))))}\\)`;
}
if (segment.startsWith("$") && segment.endsWith("$")) {
const inner = segment.slice(1, -1);
return `$${normalizeSetBraces(normalizeInfinityTokens(normalizeTrigFunctions(normalizePi(normalizeLog(normalizeExp(normalizeSquareRoots(normalizePowers(convertSimpleFractions(escapeAsterisks(inner))))))))))}$`;
}
return segment;
});
};
import { renderMathInElement, type StaticRenderOptions } from "mathlive";
const mathRenderOptions: StaticRenderOptions = {
TeX: {
delimiters: {
inline: [
["$", "$"],
["\\(", "\\)"],
],
display: [
["$$", "$$"],
["\\[", "\\]"],
],
},
},
};
export const renderMath = (element: HTMLElement | null) => {
if (!element) {
return;
}
renderMathInElement(element, mathRenderOptions);
};
const MATH_SEGMENT_PATTERN =
/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$(?:\\.|[^$\\\n])+\$)/g;
const escapeAsterisks = (value: string) => value.replace(/\*/g, "\\*");
export const escapeMarkdownInMath = (input: string): string =>
input.replace(MATH_SEGMENT_PATTERN, (segment) => {
if (segment.startsWith("$$") && segment.endsWith("$$")) {
return `$$${escapeAsterisks(segment.slice(2, -2))}$$`;
}
if (segment.startsWith("\\[") && segment.endsWith("\\]")) {
return `\\[${escapeAsterisks(segment.slice(2, -2))}\\]`;
}
if (segment.startsWith("\\(") && segment.endsWith("\\)")) {
return `\\(${escapeAsterisks(segment.slice(2, -2))}\\)`;
}
if (segment.startsWith("$") && segment.endsWith("$")) {
return `$${escapeAsterisks(segment.slice(1, -1))}$`;
}
return segment;
});
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment