diff --git a/app/package.json b/app/package.json
index e940e5cc8..c24b218d3 100644
--- a/app/package.json
+++ b/app/package.json
@@ -93,6 +93,7 @@
"buffer": "^6.0.3",
"cmdk": "^1.1.1",
"debug": "^4.4.3",
+ "katex": "^0.16.47",
"lottie-react": "^2.4.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
@@ -106,6 +107,8 @@
"react-redux": "^9.2.0",
"react-router-dom": "^7.13.0",
"redux-persist": "^6.0.0",
+ "rehype-katex": "^7.0.1",
+ "remark-math": "^6.0.0",
"remotion": "4.0.454",
"socket.io-client": "^4.8.3",
"tauri-plugin-ptt-api": "workspace:*",
@@ -114,8 +117,8 @@
"zod": "4.3.6"
},
"devDependencies": {
- "@playwright/test": "^1.56.1",
"@eslint/js": "^9.39.2",
+ "@playwright/test": "^1.56.1",
"@sentry/vite-plugin": "^2.22.6",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
diff --git a/app/src/main.tsx b/app/src/main.tsx
index a083193ca..f1179f4a5 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -1,5 +1,6 @@
// IMPORTANT: Polyfills must be imported FIRST
import { getCurrentWindow } from '@tauri-apps/api/window';
+import 'katex/dist/katex.min.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
diff --git a/app/src/pages/conversations/components/AgentMessageBubble.test.tsx b/app/src/pages/conversations/components/AgentMessageBubble.test.tsx
index d67e583f4..3496251be 100644
--- a/app/src/pages/conversations/components/AgentMessageBubble.test.tsx
+++ b/app/src/pages/conversations/components/AgentMessageBubble.test.tsx
@@ -76,3 +76,43 @@ describe('AgentMessageBubble markdown links', () => {
expect(mocks.openWorkspacePath).not.toHaveBeenCalled();
});
});
+
+describe('BubbleMarkdown math rendering', () => {
+ test('renders \\[ ... \\] block math (raw delimiters consumed, math visible)', () => {
+ const { container } = render();
+ const text = container.textContent ?? '';
+ expect(text).not.toContain('\\[');
+ expect(text).not.toContain('\\]');
+ expect(text).toContain('x');
+ expect(text).toContain('y');
+ expect(text).toContain('z');
+ });
+
+ test('renders inline \\( ... \\) math (raw delimiters consumed, math visible)', () => {
+ const { container } = render();
+ const text = container.textContent ?? '';
+ expect(text).not.toContain('\\(');
+ expect(text).not.toContain('\\)');
+ expect(text).toContain('value');
+ expect(text).toContain('here');
+ expect(text).toContain('a');
+ expect(text).toContain('b');
+ });
+
+ test('renders bare bracket vmatrix block (math rendered, not raw text)', () => {
+ const { container } = render(
+
+ );
+ const text = container.textContent ?? '';
+ // KaTeX renders visible glyphs (∣ for vmatrix bars) — confirm rendering happened.
+ expect(text).toContain('∣');
+ expect(text).toContain('1');
+ expect(text).toContain('4');
+ });
+
+ test('does NOT treat currency mentions as math', () => {
+ const { container } = render();
+ expect(container.textContent).toContain('$10');
+ expect(container.textContent).toContain('$20');
+ });
+});
diff --git a/app/src/pages/conversations/components/AgentMessageBubble.tsx b/app/src/pages/conversations/components/AgentMessageBubble.tsx
index 4d08a6892..4074bca60 100644
--- a/app/src/pages/conversations/components/AgentMessageBubble.tsx
+++ b/app/src/pages/conversations/components/AgentMessageBubble.tsx
@@ -1,8 +1,11 @@
import type { ReactNode } from 'react';
import Markdown, { defaultUrlTransform } from 'react-markdown';
+import rehypeKatex from 'rehype-katex';
+import remarkMath from 'remark-math';
import { OPENHUMAN_LINK_EVENT } from '../../../components/OpenhumanLinkModal';
import { parseMarkdownTable } from '../../../utils/agentMessageBubbles';
+import { hasLatexContent, normalizeLatexDelimiters } from '../../../utils/latex';
import { openUrl } from '../../../utils/openUrl';
import { openWorkspacePath } from '../../../utils/tauriCommands/workspacePaths';
import { parseWorkspaceHref } from '../../../utils/workspaceLinks';
@@ -13,6 +16,10 @@ import {
parseBubbleSegments,
} from '../utils/format';
+const MATH_REMARK_PLUGINS = [remarkMath];
+const MATH_REHYPE_PLUGINS = [rehypeKatex];
+const EMPTY_PLUGINS: [] = [];
+
/**
* Pill rendered below an agent bubble for each
* `label` tag the agent
@@ -80,23 +87,36 @@ export function BubbleMarkdown({
? 'prose-invert prose-p:text-white prose-li:text-white prose-a:text-white prose-code:text-white prose-strong:text-white prose-headings:text-white [&_li::marker]:text-white/85'
: 'dark:prose-invert prose-a:text-primary-500 prose-code:text-primary-700 dark:prose-code:text-primary-300 prose-headings:text-sm [&_li::marker]:text-stone-700 dark:[&_li::marker]:text-neutral-300';
+ const hasMath = hasLatexContent(content);
+ const rendered = hasMath ? normalizeLatexDelimiters(content) : content;
+
return (
-
- {content}
+
+ {rendered}
);
}
export function TableCellMarkdown({ content }: { content: string }) {
+ const hasMath = hasLatexContent(content);
+ const rendered = hasMath ? normalizeLatexDelimiters(content) : content;
return (
-
- {content}
+
+ {rendered}
);
diff --git a/app/src/utils/__tests__/latex.test.ts b/app/src/utils/__tests__/latex.test.ts
new file mode 100644
index 000000000..c4f3897b2
--- /dev/null
+++ b/app/src/utils/__tests__/latex.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from 'vitest';
+
+import { hasLatexContent, normalizeLatexDelimiters } from '../latex';
+
+describe('normalizeLatexDelimiters', () => {
+ it('converts \\[ ... \\] to $$ ... $$', () => {
+ expect(normalizeLatexDelimiters('\\[ x^2 + y^2 = z^2 \\]')).toContain('$$ x^2 + y^2 = z^2 $$');
+ });
+
+ it('converts \\( ... \\) to $ ... $', () => {
+ expect(normalizeLatexDelimiters('inline \\(a+b\\) here')).toBe('inline $a+b$ here');
+ });
+
+ it('converts bare bracketed LaTeX-only block to $$', () => {
+ const input =
+ '直接套公式:\n\n[ V_3 = (x_2 - x_1)(x_3 - x_1)(x_3 - x_2) = 1 \\times 3 \\times 2 = 6 ]';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('$$');
+ expect(out).toContain('\\times');
+ expect(out).not.toMatch(/\[ V_3/);
+ });
+
+ it('preserves markdown link syntax', () => {
+ const input = 'see [link](https://example.com) and more';
+ expect(normalizeLatexDelimiters(input)).toBe(input);
+ });
+
+ it('returns input unchanged when no LaTeX present', () => {
+ const input = 'plain text with no math';
+ expect(normalizeLatexDelimiters(input)).toBe(input);
+ });
+
+ it('handles vmatrix blocks', () => {
+ const input = '[ \\begin{vmatrix} 1 & 2 \\\\ 3 & 4 \\end{vmatrix} = -2 ]';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('$$');
+ expect(out).toContain('\\begin{vmatrix}');
+ });
+
+ it('preserves \\[...\\] inside inline code spans', () => {
+ const input = 'use `\\[x^2\\]` for display math and \\[a+b\\] renders';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('`\\[x^2\\]`');
+ expect(out).toContain('$$a+b$$');
+ });
+
+ it('preserves \\(...\\) inside inline code spans', () => {
+ const input = 'use `\\(x\\)` for inline math and \\(a+b\\) renders';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('`\\(x\\)`');
+ expect(out).toContain('$a+b$');
+ });
+
+ it('preserves LaTeX delimiters inside fenced code blocks', () => {
+ const input = '```\n\\[x^2\\]\n\\(y\\)\n```\n\nthen \\(a+b\\) here';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('```\n\\[x^2\\]\n\\(y\\)\n```');
+ expect(out).toContain('$a+b$');
+ });
+
+ it('does not corrupt math bodies containing digits when code blocks present', () => {
+ const input = 'use `x` and \\[a^2 + 7\\] and `y` and \\(b_3\\)';
+ const out = normalizeLatexDelimiters(input);
+ expect(out).toContain('`x`');
+ expect(out).toContain('`y`');
+ expect(out).toContain('$$a^2 + 7$$');
+ expect(out).toContain('$b_3$');
+ expect(out).not.toContain('undefined');
+ });
+});
+
+describe('hasLatexContent', () => {
+ it('detects backslash math commands', () => {
+ expect(hasLatexContent('use \\frac{1}{2} here')).toBe(true);
+ expect(hasLatexContent('\\begin{vmatrix} 1 \\end{vmatrix}')).toBe(true);
+ expect(hasLatexContent('\\[ a \\]')).toBe(true);
+ expect(hasLatexContent('\\(a\\)')).toBe(true);
+ expect(hasLatexContent('$$x$$')).toBe(true);
+ });
+
+ it('rejects currency mentions', () => {
+ expect(hasLatexContent('total is $10 and $20')).toBe(false);
+ expect(hasLatexContent('$100')).toBe(false);
+ });
+
+ it('rejects plain text and markdown', () => {
+ expect(hasLatexContent('see [link](https://example.com)')).toBe(false);
+ expect(hasLatexContent('hello world')).toBe(false);
+ expect(hasLatexContent('')).toBe(false);
+ });
+});
diff --git a/app/src/utils/latex.ts b/app/src/utils/latex.ts
new file mode 100644
index 000000000..fb13053db
--- /dev/null
+++ b/app/src/utils/latex.ts
@@ -0,0 +1,68 @@
+/**
+ * Normalize LaTeX math delimiters emitted by upstream LLMs into the
+ * `$...$` / `$$...$$` form that `remark-math` understands.
+ *
+ * Models frequently emit `\[ ... \]` (display) and `\( ... \)` (inline)
+ * or even bare `[ ... ]` blocks containing `\begin{vmatrix}`, `\cdot`,
+ * `x_1`, etc. Without this normalization those land in chat as raw
+ * source instead of rendered math.
+ */
+
+const DISPLAY_BACKSLASH = /\\\[([\s\S]+?)\\\]/g;
+const INLINE_BACKSLASH = /\\\(([\s\S]+?)\\\)/g;
+
+// Bare `[ ... ]` block that contains a LaTeX-only signal (\begin, \cdot,
+// \times, etc.) and lives on its own line. Conservative: avoids matching
+// markdown link/image syntax (`[text](url)`, ``).
+const DISPLAY_BARE_BRACKETS =
+ /(^|\n)[ \t]*\[[ \t]*((?:[^[\]\n]|\n(?!\n))*?\\(?:begin|end|frac|sqrt|cdot|times|sum|int|prod|lim|left|right|vmatrix|pmatrix|bmatrix|matrix|mathrm|mathbf|mathbb|alpha|beta|gamma|delta|theta|pi|sigma|infty)[^[\]]*?)[ \t]*\][ \t]*(?=\n|$)/g;
+
+// Match fenced code blocks (```...```) and inline code spans (`...`) so
+// they can be masked out before delimiter normalization. Without this,
+// content like "use `\[x^2\]` for display math" would get its inline
+// code corrupted into `$$x^2$$`.
+const CODE_BLOCKS = /```[\s\S]*?```|`[^`\n]+`/g;
+
+// Sentinel uses Unicode Private Use Area (U+E000 / U+E001):
+// - non-control (ESLint `no-control-regex` does not fire)
+// - reserved by Unicode for private use, will not appear in real chat text
+// - wrapped on both sides so the restore regex matches *only* placeholders
+// and never stray digits inside math bodies (e.g. `$$a^2$$`).
+const PLACEHOLDER_OPEN = '';
+const PLACEHOLDER_CLOSE = '';
+const PLACEHOLDER = /(\d+)/g;
+
+export function normalizeLatexDelimiters(input: string): string {
+ if (!input || (!input.includes('\\') && !input.includes('['))) return input;
+
+ const codeSegments: string[] = [];
+ let out = input.replace(CODE_BLOCKS, match => {
+ codeSegments.push(match);
+ return `${PLACEHOLDER_OPEN}${codeSegments.length - 1}${PLACEHOLDER_CLOSE}`;
+ });
+
+ out = out.replace(DISPLAY_BACKSLASH, (_m, body) => `\n\n$$${body}$$\n\n`);
+ out = out.replace(INLINE_BACKSLASH, (_m, body) => `$${body}$`);
+ out = out.replace(DISPLAY_BARE_BRACKETS, (_m, lead, body) => `${lead}\n$$${body}$$\n`);
+
+ if (codeSegments.length > 0) {
+ out = out.replace(PLACEHOLDER, (_m, i) => codeSegments[Number(i)] ?? '');
+ }
+ return out;
+}
+
+/**
+ * Heuristic: does this string likely contain LaTeX math?
+ *
+ * We use this to gate `remark-math` + `rehype-katex` so plain chat
+ * messages (e.g. "$10 vs $20", "[link](url)") are never reinterpreted as
+ * math. Only content the LLM clearly intended as math turns the plugins
+ * on.
+ */
+const LATEX_SIGNATURE =
+ /\\(?:begin|end|frac|sqrt|cdot|times|sum|int|prod|lim|left|right|vmatrix|pmatrix|bmatrix|matrix|mathrm|mathbf|mathbb|alpha|beta|gamma|delta|theta|pi|sigma|infty)\b|\\\[|\\\(|\$\$/;
+
+export function hasLatexContent(input: string): boolean {
+ if (!input) return false;
+ return LATEX_SIGNATURE.test(input);
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5378f991e..8c6ce20c7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,6 +96,9 @@ importers:
debug:
specifier: ^4.4.3
version: 4.4.3(supports-color@8.1.1)
+ katex:
+ specifier: ^0.16.47
+ version: 0.16.47
lottie-react:
specifier: ^2.4.1
version: 2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -135,6 +138,12 @@ importers:
redux-persist:
specifier: ^6.0.0
version: 6.0.0(react@19.2.5)(redux@5.0.1)
+ rehype-katex:
+ specifier: ^7.0.1
+ version: 7.0.1
+ remark-math:
+ specifier: ^6.0.0
+ version: 6.0.0
remotion:
specifier: 4.0.454
version: 4.0.454(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -1937,6 +1946,9 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+ '@types/katex@0.16.8':
+ resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==}
+
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2641,6 +2653,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
@@ -3423,12 +3439,36 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
+ hast-util-from-dom@5.0.1:
+ resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
+
+ hast-util-from-html-isomorphic@2.0.0:
+ resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
+
+ hast-util-from-html@2.0.3:
+ resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+ hast-util-from-parse5@8.0.3:
+ resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+ hast-util-to-text@4.0.2:
+ resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
+
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -3847,6 +3887,10 @@ packages:
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+ katex@0.16.47:
+ resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
+ hasBin: true
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4051,6 +4095,9 @@ packages:
mdast-util-from-markdown@2.0.3:
resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+ mdast-util-math@3.0.0:
+ resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
+
mdast-util-mdx-expression@2.0.1:
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
@@ -4085,6 +4132,9 @@ packages:
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+ micromark-extension-math@3.1.0:
+ resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
+
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
@@ -4787,6 +4837,12 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ rehype-katex@7.0.1:
+ resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
+
+ remark-math@6.0.0:
+ resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
+
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
@@ -5338,12 +5394,18 @@ packages:
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+ unist-util-find-after@5.0.0:
+ resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
+
unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+ unist-util-remove-position@5.0.0:
+ resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
+
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
@@ -5410,6 +5472,9 @@ packages:
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+ vfile-location@5.0.3:
+ resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -5524,6 +5589,9 @@ packages:
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
+ web-namespaces@2.0.1:
+ resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
webdriver@9.27.0:
resolution: {integrity: sha512-w07ThZND48SIr0b4S7eFougYUyclmoUwdmju8yXvEJiXYjDjeYUpl8wZrYPEYRBylxpSx+sBHfEUBrPQkcTTRQ==}
engines: {node: '>=18.20.0'}
@@ -7117,6 +7185,8 @@ snapshots:
'@types/json5@0.0.29': {}
+ '@types/katex@0.16.8': {}
+
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -8047,6 +8117,8 @@ snapshots:
commander@4.1.1: {}
+ commander@8.3.0: {}
+
commander@9.5.0: {}
compress-commons@6.0.2:
@@ -9045,6 +9117,47 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-from-dom@5.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hastscript: 9.0.1
+ web-namespaces: 2.0.1
+
+ hast-util-from-html-isomorphic@2.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-from-dom: 5.0.1
+ hast-util-from-html: 2.0.3
+ unist-util-remove-position: 5.0.0
+
+ hast-util-from-html@2.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ devlop: 1.1.0
+ hast-util-from-parse5: 8.0.3
+ parse5: 7.3.0
+ vfile: 6.0.3
+ vfile-message: 4.0.3
+
+ hast-util-from-parse5@8.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ hastscript: 9.0.1
+ property-information: 7.1.0
+ vfile: 6.0.3
+ vfile-location: 5.0.3
+ web-namespaces: 2.0.1
+
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.8
@@ -9065,10 +9178,25 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ hast-util-to-text@4.0.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ hast-util-is-element: 3.0.0
+ unist-util-find-after: 5.0.0
+
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+
he@1.2.0: {}
hermes-estree@0.25.1: {}
@@ -9506,6 +9634,10 @@ snapshots:
readable-stream: 2.3.8
setimmediate: 1.0.5
+ katex@0.16.47:
+ dependencies:
+ commander: 8.3.0
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -9706,6 +9838,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-math@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ longest-streak: 3.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ unist-util-remove-position: 5.0.0
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-mdx-expression@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -9803,6 +9947,16 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
+ micromark-extension-math@3.1.0:
+ dependencies:
+ '@types/katex': 0.16.8
+ devlop: 1.1.0
+ katex: 0.16.47
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@@ -10683,6 +10837,25 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ rehype-katex@7.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/katex': 0.16.8
+ hast-util-from-html-isomorphic: 2.0.0
+ hast-util-to-text: 4.0.2
+ katex: 0.16.47
+ unist-util-visit-parents: 6.0.2
+ vfile: 6.0.3
+
+ remark-math@6.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-math: 3.0.0
+ micromark-extension-math: 3.1.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -11386,6 +11559,11 @@ snapshots:
trough: 2.2.0
vfile: 6.0.3
+ unist-util-find-after@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
unist-util-is@6.0.1:
dependencies:
'@types/unist': 3.0.3
@@ -11394,6 +11572,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ unist-util-remove-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-visit: 5.1.0
+
unist-util-stringify-position@4.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -11469,6 +11652,11 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
+ vfile-location@5.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile: 6.0.3
+
vfile-message@4.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -11553,6 +11741,8 @@ snapshots:
defaults: 1.0.4
optional: true
+ web-namespaces@2.0.1: {}
+
webdriver@9.27.0:
dependencies:
'@types/node': 20.19.39