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)`, `![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); +} 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