feat(chat): render agent-bubble LaTeX with KaTeX and safe math detection (#2697)

This commit is contained in:
YellowSnnowmann
2026-05-27 18:36:31 -07:00
committed by GitHub
parent d8696c1c1f
commit 129abfe8a4
7 changed files with 418 additions and 5 deletions
+4 -1
View File
@@ -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",
+1
View File
@@ -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';
@@ -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(<BubbleMarkdown content={'\\[ x^2 + y^2 = z^2 \\]'} />);
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(<BubbleMarkdown content={'value \\(a+b\\) here'} />);
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(
<BubbleMarkdown content={'[ \\begin{vmatrix} 1 & 2 \\\\ 3 & 4 \\end{vmatrix} = -2 ]'} />
);
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(<BubbleMarkdown content={'total is $10 versus $20'} />);
expect(container.textContent).toContain('$10');
expect(container.textContent).toContain('$20');
});
});
@@ -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
* `<openhuman-link path="...">label</openhuman-link>` 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 (
<div
className={`text-sm prose prose-sm max-w-none prose-p:my-1 prose-pre:my-2 prose-pre:rounded-lg prose-code:text-xs prose-headings:font-semibold prose-ul:my-0 prose-ol:my-0 prose-li:my-0 ${proseTone} ${
tone === 'user' ? 'prose-pre:bg-white/10' : 'prose-pre:bg-stone-300/50'
} [&_ul]:my-0 [&_ol]:my-0 [&_ul]:pl-0 [&_ol]:pl-0 [&_ul]:list-inside [&_ol]:list-inside [&_li]:my-0 [&_li]:pl-0 [&_li_p]:inline [&_li_p]:m-0`}>
<Markdown urlTransform={transformMarkdownUrl} components={{ a: MarkdownAnchor }}>
{content}
<Markdown
urlTransform={transformMarkdownUrl}
components={{ a: MarkdownAnchor }}
remarkPlugins={hasMath ? MATH_REMARK_PLUGINS : EMPTY_PLUGINS}
rehypePlugins={hasMath ? MATH_REHYPE_PLUGINS : EMPTY_PLUGINS}>
{rendered}
</Markdown>
</div>
);
}
export function TableCellMarkdown({ content }: { content: string }) {
const hasMath = hasLatexContent(content);
const rendered = hasMath ? normalizeLatexDelimiters(content) : content;
return (
<div className="prose prose-sm dark:prose-invert max-w-none text-sm text-stone-700 dark:text-neutral-200 prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-code:text-xs prose-code:text-primary-700 dark:prose-code:text-primary-300 prose-a:text-primary-500 prose-strong:text-stone-900 dark:prose-strong:text-neutral-100 prose-headings:text-sm prose-headings:font-semibold [&_li::marker]:text-stone-700 dark:[&_li::marker]:text-neutral-300 [&_ul]:my-0 [&_ol]:my-0 [&_ul]:pl-0 [&_ol]:pl-0 [&_ul]:list-inside [&_ol]:list-inside [&_li]:pl-0 [&_li_p]:inline [&_li_p]:m-0">
<Markdown urlTransform={transformMarkdownUrl} components={{ a: MarkdownAnchor }}>
{content}
<Markdown
urlTransform={transformMarkdownUrl}
components={{ a: MarkdownAnchor }}
remarkPlugins={hasMath ? MATH_REMARK_PLUGINS : EMPTY_PLUGINS}
rehypePlugins={hasMath ? MATH_REHYPE_PLUGINS : EMPTY_PLUGINS}>
{rendered}
</Markdown>
</div>
);
+91
View File
@@ -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);
});
});
+68
View File
@@ -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)`, `![alt](src)`).
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);
}
+190
View File
@@ -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