mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(chat): render agent-bubble LaTeX with KaTeX and safe math detection (#2697)
This commit is contained in:
+4
-1
@@ -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,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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Generated
+190
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user