diff --git a/apps/mobile/modules/t3-markdown-text/package.json b/apps/mobile/modules/t3-markdown-text/package.json index 376befbeb152..ec5ced0e4ee3 100644 --- a/apps/mobile/modules/t3-markdown-text/package.json +++ b/apps/mobile/modules/t3-markdown-text/package.json @@ -34,11 +34,14 @@ "expo": "*", "expo-asset": "*", "expo-clipboard": "*", + "expo-file-system": "*", "expo-haptics": "*", "expo-symbols": "*", + "mathjax-full": "*", "react": "*", "react-native": "*", - "react-native-nitro-markdown": "*" + "react-native-nitro-markdown": "*", + "react-native-webview": "*" }, "codegenConfig": { "name": "T3MarkdownTextSpec", diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx index 50a381bae288..0d372a5ffedf 100644 --- a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx @@ -1,5 +1,8 @@ import { createContext, useCallback, useContext, useMemo } from "react"; import { decodeComposerContextFragment } from "@t3tools/shared/composerContextClipboard"; +import { shouldTypesetNativeMath } from "./nativeMarkdownMath"; +import { NativeMathText } from "./NativeMathText"; +import { nativeMarkdownRunStyle } from "./nativeMarkdownRunStyle"; import { findNodeHandle, Image, @@ -7,7 +10,6 @@ import { Platform, StyleSheet, Text as RNText, - type TextStyle, useColorScheme, View, } from "react-native"; @@ -49,7 +51,6 @@ const SKILL_ICON_PLACEHOLDER = "\uFFFC"; // height. Any other single character keeps the run's attributes; the native side swaps it // for the chip attachment either way. const IOS_CHIP_PLACEHOLDER = "\u200B"; -const PARAGRAPH_STYLE_ENCODING_OFFSET = 1000; const MONO_FONT_FAMILY = Platform.select({ ios: "ui-monospace", android: "monospace", @@ -87,111 +88,6 @@ function runKeySignature(run: NativeMarkdownTextRun): string { ].join(":"); } -const DEFAULT_BODY_FONT_SIZE = 15; -const DEFAULT_HEADING_FONT_SIZES = [22, 19, 17, 16, 15, 15] as const; - -function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel: number): number { - const index = Math.max(0, Math.min(5, headingLevel - 1)); - const configured = textStyle.headingFontSizes?.[index]; - if (typeof configured === "number" && Number.isFinite(configured)) { - return configured; - } - - const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE; - return Math.max(12, Math.round((DEFAULT_HEADING_FONT_SIZES[index] ?? 15) * scale)); -} - -function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle { - const isFile = run.fileIcon != null; - const isSkill = run.skillName != null; - const headingLevel = Math.max(1, Math.min(6, run.headingLevel ?? 1)); - const headingFontSize = resolveHeadingFontSize(textStyle, headingLevel); - const isHeading = run.role === "heading"; - const isCodeBlock = run.role === "code-block" || run.role === "code-language"; - const hasParagraphStyle = run.headIndent !== undefined; - const textDecorationLine = run.strikethrough - ? "line-through" - : run.href && !isFile - ? "underline" - : "none"; - - return { - color: isFile - ? textStyle.fileTextColor - : isSkill - ? textStyle.skillTextColor - : run.href - ? textStyle.linkColor - : isHeading - ? textStyle.strongColor - : run.role === "quote-marker" - ? textStyle.quoteMarkerColor - : run.role === "divider" - ? textStyle.dividerColor - : run.role === "code-language" - ? textStyle.mutedColor - : run.role === "list-marker" - ? textStyle.mutedColor - : isCodeBlock - ? textStyle.codeColor - : run.code - ? textStyle.inlineCodeColor - : run.bold - ? textStyle.strongColor - : textStyle.color, - fontFamily: - isFile || isSkill - ? textStyle.boldFontFamily - : run.code || isCodeBlock - ? MONO_FONT_FAMILY - : isHeading - ? textStyle.headingFontFamily - : run.bold - ? textStyle.boldFontFamily - : textStyle.fontFamily, - fontSize: - run.role === "spacer" - ? (run.spacing ?? 10) - : run.role === "list-break" - ? textStyle.fontSize - : isHeading - ? headingFontSize - : run.role === "code-language" - ? Math.max(10, Math.round(textStyle.fontSize * 0.73)) - : run.code || isCodeBlock - ? Math.max(12, textStyle.fontSize - 2) - : textStyle.fontSize, - lineHeight: - run.role === "spacer" - ? (run.spacing ?? 10) - : run.role === "list-break" - ? textStyle.lineHeight + (run.spacing ?? 0) - : isHeading - ? Math.max(headingFontSize + 6, textStyle.lineHeight + 2) - : isCodeBlock - ? Math.max(16, textStyle.lineHeight - 2) - : textStyle.lineHeight, - fontStyle: run.italic ? "italic" : "normal", - fontWeight: isHeading || run.bold || isFile || isSkill ? "700" : "400", - textDecorationLine, - backgroundColor: isCodeBlock - ? textStyle.codeBlockBackgroundColor - : parseComposerContextHref(run.href ?? "") - ? textStyle.codeBackgroundColor - : undefined, - ...(hasParagraphStyle - ? { - shadowColor: "transparent", - shadowOffset: { - width: run.firstLineHeadIndent ?? 0, - height: run.headIndent, - }, - shadowRadius: PARAGRAPH_STYLE_ENCODING_OFFSET + (run.paragraphSpacing ?? 0), - } - : {}), - }; -} - export function NativeMarkdownSelectableText(props: { readonly runs: ReadonlyArray; readonly textStyle: NativeMarkdownTextStyle; @@ -326,7 +222,7 @@ export function NativeMarkdownSelectableText(props: { props.textStyle.contextChipBorderColor, ].join(":"); - return ( + const nativeText = ( ); + return shouldTypesetNativeMath(props.runs) ? ( + + ) : ( + nativeText + ); } diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMathText.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMathText.tsx new file mode 100644 index 000000000000..b369d1fb0e4c --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/NativeMathText.tsx @@ -0,0 +1,187 @@ +import { memo, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { Alert, Linking, useWindowDimensions } from "react-native"; +import { setStringAsync } from "expo-clipboard"; +import { WebView } from "react-native-webview"; +import type { NativeMarkdownTextRun } from "./nativeMarkdownText"; +import type { + MarkdownFileContextMenu, + NativeMarkdownTextStyle, +} from "./SelectableMarkdownText.types"; +import { loadNativeMathIcons, nativeMathIconKey } from "./nativeMathAssets"; +import { NATIVE_MATH_DOCUMENT } from "./nativeMathDocument"; + +const documentSource = { html: NATIVE_MATH_DOCUMENT }; +let rendererPromise: Promise | undefined; +function loadRenderer() { + return (rendererPromise ??= import("./nativeMathHtml").catch((error) => { + rendererPromise = undefined; + throw error; + })); +} + +/** One view per math-containing text chunk preserves inline layout and selection on both OSes. */ +export const NativeMathText = memo(function NativeMathText(props: { + readonly runs: ReadonlyArray; + readonly fallback: ReactNode; + readonly textStyle: NativeMarkdownTextStyle; + readonly onLinkPress?: (href: string) => void; + readonly fileContextMenu?: (href: string) => MarkdownFileContextMenu | undefined; + readonly onFileContextMenuAction?: (href: string, actionId: string) => void; +}) { + const webView = useRef(null); + const ready = useRef(false); + const [height, setHeight] = useState(props.textStyle.lineHeight); + const [failed, setFailed] = useState(false); + const [renderer, setRenderer] = useState(); + const [icons, setIcons] = useState>({}); + useEffect(() => { + let active = true; + void loadNativeMathIcons(props.runs).then((loaded) => { + if (active) + setIcons((current) => + Object.keys(current).length === Object.keys(loaded).length && + Object.entries(loaded).every(([key, value]) => current[key] === value) + ? current + : loaded, + ); + }); + return () => { + active = false; + }; + }, [props.runs]); + const { fontScale } = useWindowDimensions(); + useEffect(() => { + let active = true; + void loadRenderer().then( + (module) => { + if (active) setRenderer(module); + }, + () => { + if (active) setFailed(true); + }, + ); + return () => { + active = false; + }; + }, []); + const fileContextMenu = props.fileContextMenu; + const update = useMemo(() => { + if (!renderer) return null; + const style = { + ...props.textStyle, + fontSize: props.textStyle.fontSize * fontScale, + lineHeight: props.textStyle.lineHeight * fontScale, + headingFontSizes: props.textStyle.headingFontSizes?.map((size) => size * fontScale), + }; + const prefixedLinks = new Set(); + return { + runs: props.runs.map((run) => { + const showExternalIcon = !run.href || !prefixedLinks.has(run.href); + if (run.externalHost && run.href) prefixedLinks.add(run.href); + return renderer.nativeMathRunHtml( + run, + style, + run.fileIcon && run.href ? fileContextMenu?.(run.href) : undefined, + icons[nativeMathIconKey(run) ?? ""], + showExternalIcon, + ); + }), + color: style.color, + fontSize: style.fontSize, + lineHeight: style.lineHeight, + }; + }, [renderer, props.runs, props.textStyle, fileContextMenu, fontScale, icons]); + const revision = useRef(0); + const latest = useRef(update); + useEffect(() => { + latest.current = update; + if (ready.current && update) { + revision.current += 1; + webView.current?.injectJavaScript( + `window.updateMath(${JSON.stringify({ ...update, revision: revision.current })});true;`, + ); + } + }, [update]); + + if (failed || !renderer) return props.fallback; + return ( + request.url === "about:blank"} + scrollEnabled={false} + bounces={false} + showsVerticalScrollIndicator={false} + dataDetectorTypes="none" + allowFileAccess={false} + setSupportMultipleWindows={false} + style={{ height, backgroundColor: "transparent", flex: 0 }} + onError={() => setFailed(true)} + onContentProcessDidTerminate={() => setFailed(true)} + onRenderProcessGone={() => setFailed(true)} + onMessage={(event) => { + let message: unknown; + try { + message = JSON.parse(event.nativeEvent.data); + } catch { + return; + } + if (!message || typeof message !== "object" || !("type" in message)) return; + if (message.type === "ready") { + ready.current = true; + if (latest.current) { + revision.current += 1; + webView.current?.injectJavaScript( + `window.updateMath(${JSON.stringify({ ...latest.current, revision: revision.current })});true;`, + ); + } + return; + } + if (!("revision" in message) || message.revision !== revision.current) return; + if ( + message.type === "height" && + "height" in message && + typeof message.height === "number" && + Number.isFinite(message.height) && + message.height > 0 + ) + setHeight(message.height); + if (message.type === "copy" && "text" in message && typeof message.text === "string") { + const text = message.text; + const copyRevision = revision.current; + void setStringAsync(text).then( + () => { + if (revision.current === copyRevision) + webView.current?.injectJavaScript( + `window.mathCopyResult(${JSON.stringify(text)});true;`, + ); + }, + () => Alert.alert("Could not copy text"), + ); + } + if ( + message.type === "file-action" && + "href" in message && + typeof message.href === "string" && + "action" in message && + typeof message.action === "string" + ) { + const href = message.href; + const actionId = message.action; + if ( + props.runs.some((run) => run.fileIcon && run.href === href) && + props + .fileContextMenu?.(href) + ?.actions.some((action) => action.id === actionId && !action.disabled) + ) + props.onFileContextMenuAction?.(href, actionId); + } + if (message.type === "link" && "href" in message && typeof message.href === "string") { + if (props.onLinkPress) props.onLinkPress(message.href); + else void Linking.openURL(message.href); + } + }} + /> + ); +}); diff --git a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx index 8e59256b12b8..8053088c140e 100644 --- a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx @@ -1,3 +1,4 @@ +import { parseNativeMarkdownMath } from "./nativeMarkdownMath"; import { useMemo } from "react"; import { View } from "react-native"; import { parseMarkdownWithOptions } from "react-native-nitro-markdown/headless"; @@ -51,11 +52,9 @@ export function SelectableMarkdownText({ marginBottom = 0, }: SelectableMarkdownTextProps) { const chunks = useMemo(() => { - const parsedDocument = parseMarkdownWithOptions(markdown, { - gfm: true, - html: true, - math: false, - }); + const parsedDocument = parseNativeMarkdownMath(markdown, (source) => + parseMarkdownWithOptions(source, { gfm: true, html: true, math: false }), + ); const document = preserveSoftBreaks ? nativeMarkdownWithPreservedSoftBreaks(parsedDocument) : parsedDocument; diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownMath.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownMath.ts new file mode 100644 index 000000000000..a0f3662da3c1 --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownMath.ts @@ -0,0 +1,61 @@ +import { markdownMathRanges } from "@t3tools/client-runtime/markdown-math"; +import { parseComposerContextHref } from "@t3tools/shared/composerContextReferences"; +import type { NativeMarkdownTextRun } from "./nativeMarkdownText"; +import type { MarkdownNode } from "react-native-nitro-markdown/headless"; + +/** Shield math from the native parser, then restore typed nodes using the shared grammar. */ +export function parseNativeMarkdownMath( + source: string, + parse: (markdown: string) => MarkdownNode, +): MarkdownNode { + const ranges = markdownMathRanges(source); + if (ranges.length === 0) return parse(source); + let prefix = ":t3-math-"; + while (source.includes(prefix)) prefix += "-"; + const mathByMarker = new Map(ranges.map((math, index) => [`${prefix}${index}:`, math])); + // Text markers cannot merge with neighboring Markdown code-span delimiters. + let cursor = 0; + let protectedSource = ""; + for (const [marker, math] of mathByMarker) { + protectedSource += source.slice(cursor, math.start) + marker; + cursor = math.end; + } + protectedSource += source.slice(cursor); + const markerPattern = new RegExp(`${prefix}\\d+:`, "g"); + function restore(node: MarkdownNode): MarkdownNode { + if (!node.children) return node; + return { + ...node, + children: node.children.flatMap((child) => { + if (child.type !== "text" || !child.content) return [restore(child)]; + const restored: MarkdownNode[] = []; + let offset = 0; + for (const match of child.content.matchAll(markerPattern)) { + const math = mathByMarker.get(match[0]); + if (!math) continue; + if (match.index > offset) + restored.push({ ...child, content: child.content.slice(offset, match.index) }); + restored.push({ + type: math.math ? (math.math.display ? "math_block" : "math_inline") : "text", + content: math.source, + beg: math.start, + end: math.end, + }); + offset = match.index + match[0].length; + } + if (offset < child.content.length) + restored.push({ ...child, content: child.content.slice(offset) }); + return restored; + }), + }; + } + return restore(parse(protectedSource)); +} + +/** Rich context selection needs the native clipboard bridge; keep its TeX readable there. */ +export function shouldTypesetNativeMath(runs: ReadonlyArray): boolean { + return ( + runs.some((run) => run.mathSource !== undefined) && + !runs.some((run) => parseComposerContextHref(run.href ?? "") !== null) + ); +} diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownRunStyle.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownRunStyle.ts new file mode 100644 index 000000000000..06d994ef2bf8 --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownRunStyle.ts @@ -0,0 +1,114 @@ +import type { TextStyle } from "react-native"; +import { parseComposerContextHref } from "@t3tools/shared/composerContextReferences"; +import type { NativeMarkdownTextRun } from "./nativeMarkdownText"; +import type { NativeMarkdownTextStyle } from "./SelectableMarkdownText.types"; + +const PARAGRAPH_STYLE_ENCODING_OFFSET = 1000; +const DEFAULT_BODY_FONT_SIZE = 15; +const DEFAULT_HEADING_FONT_SIZES = [22, 19, 17, 16, 15, 15] as const; + +function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel: number): number { + const index = Math.max(0, Math.min(5, headingLevel - 1)); + const configured = textStyle.headingFontSizes?.[index]; + if (typeof configured === "number" && Number.isFinite(configured)) { + return configured; + } + + const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE; + return Math.max(12, Math.round((DEFAULT_HEADING_FONT_SIZES[index] ?? 15) * scale)); +} + +export function nativeMarkdownRunStyle( + run: NativeMarkdownTextRun, + textStyle: NativeMarkdownTextStyle, + monoFontFamily: string, +) { + const isFile = run.fileIcon != null; + const isSkill = run.skillName != null; + const headingLevel = Math.max(1, Math.min(6, run.headingLevel ?? 1)); + const headingFontSize = resolveHeadingFontSize(textStyle, headingLevel); + const isHeading = run.role === "heading"; + const isCodeBlock = run.role === "code-block" || run.role === "code-language"; + const hasParagraphStyle = run.headIndent !== undefined; + const textDecorationLine = run.strikethrough + ? "line-through" + : run.href && !isFile + ? "underline" + : "none"; + + return { + color: isFile + ? textStyle.fileTextColor + : isSkill + ? textStyle.skillTextColor + : run.href + ? textStyle.linkColor + : isHeading + ? textStyle.strongColor + : run.role === "quote-marker" + ? textStyle.quoteMarkerColor + : run.role === "divider" + ? textStyle.dividerColor + : run.role === "code-language" + ? textStyle.mutedColor + : run.role === "list-marker" + ? textStyle.mutedColor + : isCodeBlock + ? textStyle.codeColor + : run.code + ? textStyle.inlineCodeColor + : run.bold + ? textStyle.strongColor + : textStyle.color, + fontFamily: + isFile || isSkill + ? textStyle.boldFontFamily + : run.code || isCodeBlock + ? monoFontFamily + : isHeading + ? textStyle.headingFontFamily + : run.bold + ? textStyle.boldFontFamily + : textStyle.fontFamily, + fontSize: + run.role === "spacer" + ? (run.spacing ?? 10) + : run.role === "list-break" + ? textStyle.fontSize + : isHeading + ? headingFontSize + : run.role === "code-language" + ? Math.max(10, Math.round(textStyle.fontSize * 0.73)) + : run.code || isCodeBlock + ? Math.max(12, textStyle.fontSize - 2) + : textStyle.fontSize, + lineHeight: + run.role === "spacer" + ? (run.spacing ?? 10) + : run.role === "list-break" + ? textStyle.lineHeight + (run.spacing ?? 0) + : isHeading + ? Math.max(headingFontSize + 6, textStyle.lineHeight + 2) + : isCodeBlock + ? Math.max(16, textStyle.lineHeight - 2) + : textStyle.lineHeight, + fontStyle: run.italic ? "italic" : "normal", + fontWeight: isHeading || run.bold || isFile || isSkill ? "700" : "400", + textDecorationLine, + backgroundColor: isCodeBlock + ? textStyle.codeBlockBackgroundColor + : parseComposerContextHref(run.href ?? "") + ? textStyle.codeBackgroundColor + : undefined, + ...(hasParagraphStyle + ? { + shadowColor: "transparent", + shadowOffset: { + width: run.firstLineHeadIndent ?? 0, + height: run.headIndent, + }, + shadowRadius: PARAGRAPH_STYLE_ENCODING_OFFSET + (run.paragraphSpacing ?? 0), + } + : {}), + } satisfies TextStyle; +} diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts index ec8cf74fee2b..e6cd8e814afd 100644 --- a/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts @@ -131,6 +131,7 @@ import { export interface NativeMarkdownTextRun { readonly text: string; + readonly mathSource?: string; readonly bold?: boolean; readonly italic?: boolean; readonly strikethrough?: boolean; @@ -259,6 +260,8 @@ function inlineHtmlText(value: string): string { function sameRunStyle(left: NativeMarkdownTextRun, right: NativeMarkdownTextRun): boolean { return ( + left.mathSource === undefined && + right.mathSource === undefined && left.bold === right.bold && left.italic === right.italic && left.strikethrough === right.strikethrough && @@ -282,6 +285,7 @@ function appendRun( runs: NativeMarkdownTextRun[], text: string, context: RunContext, + mathSource?: string, ): NativeMarkdownTextRun[] { if (text.length === 0) { return runs; @@ -289,6 +293,7 @@ function appendRun( const run: NativeMarkdownTextRun = { text, + ...(mathSource ? { mathSource } : {}), ...(context.bold ? { bold: true } : {}), ...(context.italic ? { italic: true } : {}), ...(context.strikethrough ? { strikethrough: true } : {}), @@ -344,7 +349,7 @@ function decorateSkillRuns( const decorated: NativeMarkdownTextRun[] = []; for (const run of runs) { - if (run.code || run.href || run.fileIcon || run.role === "code-block") { + if (run.mathSource || run.code || run.href || run.fileIcon || run.role === "code-block") { decorated.push(run); continue; } @@ -384,7 +389,8 @@ function decorateSkillRuns( function decorateMentionRuns(runs: ReadonlyArray) { return runs.flatMap((run) => { - if (run.code || run.href || run.skillName || run.role === "code-block") return [run]; + if (run.mathSource || run.code || run.href || run.skillName || run.role === "code-block") + return [run]; const decorated: NativeMarkdownTextRun[] = []; let cursor = 0; for (const token of collectComposerInlineTokens(`${run.text} `)) { @@ -436,8 +442,10 @@ function appendNode( context: RunContext, ): NativeMarkdownTextRun[] { switch (node.type) { - case "text": case "math_inline": + case "math_block": + return appendRun(runs, nodeTextContent(node), context, nodeTextContent(node)); + case "text": return appendRun(runs, textNodeContent(nodeTextContent(node)), context); case "html_inline": return appendRun(runs, inlineHtmlText(nodeTextContent(node)), context); @@ -839,7 +847,7 @@ function appendDocumentBlock( }); return appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "body", depth }); case "math_block": - appendRun(runs, nodeTextContent(node), { ...EMPTY_CONTEXT, role: "body", depth }); + appendNode(runs, node, { ...EMPTY_CONTEXT, role: "body", depth }); return appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "body", depth }); default: appendInlineChildren(runs, node, { ...EMPTY_CONTEXT, role: "body", depth }); @@ -847,7 +855,18 @@ function appendDocumentBlock( } } +function containsMath(node: MarkdownNode): boolean { + return ( + node.type === "math_inline" || + node.type === "math_block" || + (node.children ?? []).some(containsMath) + ); +} + function containsRichBlock(node: MarkdownNode): boolean { + // NativeList already owns nested indentation and hanging markers. Keep that + // layout when a list item needs a math text view. + if (node.type === "list" && containsMath(node)) return true; if ( node.type === "code_block" || node.type === "blockquote" || diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMathAssets.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMathAssets.ts new file mode 100644 index 000000000000..f53459cc44f2 --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMathAssets.ts @@ -0,0 +1,49 @@ +import { Asset } from "expo-asset"; +import { File } from "expo-file-system"; +import { markdownFileIconSource } from "./markdownFileIcons"; +import { markdownLinkIconSource } from "./markdownLinkIcons"; +import { resolveMarkdownLinkIcon } from "./markdownLinks"; +import type { NativeMarkdownTextRun } from "./nativeMarkdownText"; + +// These keys come only from the finite set of bundled Markdown icon assets. +const icons = new Map>(); + +export function nativeMathIconKey(run: NativeMarkdownTextRun): string | undefined { + if (run.fileIcon) return `file:${run.fileIcon}`; + const linkIcon = run.externalHost && resolveMarkdownLinkIcon(run.externalHost); + return linkIcon ? `link:${linkIcon}` : undefined; +} + +/** Embed bundled icons so the WebView never needs file access or remote image permissions. */ +export async function loadNativeMathIcons(runs: ReadonlyArray) { + const entries = new Map>(); + for (const run of runs) { + const key = nativeMathIconKey(run); + if (!key || entries.has(key)) continue; + let icon = icons.get(key); + if (!icon) { + const linkIcon = run.externalHost && resolveMarkdownLinkIcon(run.externalHost); + const source = run.fileIcon + ? markdownFileIconSource(run.fileIcon) + : linkIcon + ? markdownLinkIconSource(linkIcon) + : undefined; + if (typeof source !== "number") continue; + icon = Asset.fromModule(source) + .downloadAsync() + .then(async (asset) => { + if (!asset.localUri) return ""; + return `data:image/png;base64,${await new File(asset.localUri).base64()}`; + }) + .catch(() => { + icons.delete(key); + return ""; + }); + icons.set(key, icon); + } + entries.set(key, icon); + } + return Object.fromEntries( + await Promise.all([...entries].map(async ([key, icon]) => [key, await icon])), + ); +} diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMathDocument.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMathDocument.ts new file mode 100644 index 000000000000..e8776157ad2a --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMathDocument.ts @@ -0,0 +1,34 @@ +// This document is installed once. Updates reconcile runs so streaming does not +// reload the WebView or recreate completed equations and open source disclosures. +export const NATIVE_MATH_DOCUMENT = `
`; diff --git a/apps/mobile/modules/t3-markdown-text/src/nativeMathHtml.ts b/apps/mobile/modules/t3-markdown-text/src/nativeMathHtml.ts new file mode 100644 index 000000000000..853d7d99f9a8 --- /dev/null +++ b/apps/mobile/modules/t3-markdown-text/src/nativeMathHtml.ts @@ -0,0 +1,109 @@ +import { nativeMarkdownRunStyle } from "./nativeMarkdownRunStyle"; +import { markdownMath } from "@t3tools/client-runtime/markdown-math"; +import { mathjax } from "mathjax-full/js/mathjax.js"; +import { TeX } from "mathjax-full/js/input/tex.js"; +import { SVG } from "mathjax-full/js/output/svg.js"; +import { liteAdaptor } from "mathjax-full/js/adaptors/liteAdaptor.js"; +import { AssistiveMmlHandler } from "mathjax-full/js/a11y/assistive-mml.js"; +import { RegisterHTMLHandler } from "mathjax-full/js/handlers/html.js"; +import "mathjax-full/js/input/tex/ams/AmsConfiguration.js"; +import type { NativeMarkdownTextRun } from "./nativeMarkdownText"; +import type { + MarkdownFileContextMenu, + NativeMarkdownTextStyle, +} from "./SelectableMarkdownText.types"; + +const adaptor = liteAdaptor(); +AssistiveMmlHandler(RegisterHTMLHandler(adaptor)); +const tex = new TeX({ packages: ["base", "ams"], maxBuffer: 16_384, maxMacros: 1000 }); +const renderer = mathjax.document("", { InputJax: tex, OutputJax: new SVG({ fontCache: "none" }) }); +const cache = new Map(); +let cacheSize = 0; + +function escapeHtml(text: string): string { + return text.replace( + /[&<>"']/g, + (character) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!, + ); +} + +/** Self-contained SVG needs no font downloads, browser typesetter or network access. */ +export function nativeMathSvg(source: string): string | null { + if (cache.has(source)) { + const cached = cache.get(source) ?? null; + cache.delete(source); + cache.set(source, cached); + return cached; + } + const math = markdownMath(source); + if (!math) return null; + let svg: string | null = null; + try { + renderer.reset(); + tex.reset(); + const node = renderer.convert(math.tex, { display: math.display }); + const output = adaptor.outerHTML(node); + if (!output.includes('data-mml-node="merror"')) svg = output; + } catch { + // Leave unsupported or malformed math as its original source. + } + const size = source.length + (svg?.length ?? 0); + if (size <= 1_000_000) { + while (cache.size >= 128 || cacheSize + size > 1_000_000) { + const oldest = cache.entries().next().value; + if (!oldest) break; + cacheSize -= oldest[0].length + (oldest[1]?.length ?? 0); + cache.delete(oldest[0]); + } + cache.set(source, svg); + cacheSize += size; + } + return svg; +} + +/** Render typed native text runs, never raw Markdown HTML, inside the math text view. */ +export function nativeMathRunHtml( + run: NativeMarkdownTextRun, + style: NativeMarkdownTextStyle, + menu?: MarkdownFileContextMenu, + iconUri?: string, + showExternalIcon = true, +): string { + const resolved = nativeMarkdownRunStyle(run, style, "monospace"); + const css = `color:${resolved.color};font-size:${resolved.fontSize}px;line-height:${resolved.lineHeight}px;font-weight:${resolved.fontWeight};font-style:${resolved.fontStyle};font-family:${resolved.fontFamily};text-decoration:${resolved.textDecorationLine}`; + if (run.role === "spacer") + return `${escapeHtml(run.text)}`; + + let content = escapeHtml(run.text); + if (run.mathSource) { + const math = markdownMath(run.mathSource); + const source = escapeHtml(run.mathSource); + const svg = nativeMathSvg(run.mathSource); + content = `${svg ?? source}`; + if (math?.display) + content = `${content}`; + } else if (run.skillName && run.skillLabel) { + // Match the native skill label while retaining the token for selection copy. + content = `${escapeHtml(run.skillLabel)}`; + } + if (iconUri && (run.fileIcon || showExternalIcon)) { + const icon = + run.externalHost && !run.fileIcon + ? `` + : ``; + content = icon + content; + } else if ((run.externalHost && showExternalIcon) || run.fileIcon) { + content = + `` + content; + } + if (run.href) { + const href = escapeHtml(run.href); + const actions = + run.fileIcon && menu + ? `` + : ""; + return `${content}${actions}`; + } + return `${content}`; +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0fd35faf7528..31284b0804b4 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -107,6 +107,7 @@ "expo-video": "~57.0.3", "expo-web-browser": "~57.0.2", "expo-widgets": "~57.0.15", + "mathjax-full": "^3.2.2", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.3", @@ -129,9 +130,11 @@ "devDependencies": { "@effect/vitest": "catalog:", "@pierre/trees": "1.0.0-beta.4", + "@types/jsdom": "^30.0.0", "@types/react": "~19.2.0", "@types/react-dom": "~19.2.3", "babel-preset-expo": "~57.0.9", + "jsdom": "^30.0.1", "tailwindcss": "^4.0.0", "typescript": "catalog:" }, diff --git a/apps/mobile/src/lib/nativeMarkdownMath.test.ts b/apps/mobile/src/lib/nativeMarkdownMath.test.ts new file mode 100644 index 000000000000..b57b1bb986ae --- /dev/null +++ b/apps/mobile/src/lib/nativeMarkdownMath.test.ts @@ -0,0 +1,153 @@ +import { JSDOM } from "jsdom"; +import { describe, expect, it } from "vite-plus/test"; +import { + parseNativeMarkdownMath, + shouldTypesetNativeMath, +} from "../../modules/t3-markdown-text/src/nativeMarkdownMath"; +import { + nativeMathSvg, + nativeMathRunHtml, +} from "../../modules/t3-markdown-text/src/nativeMathHtml"; +import { + nativeMarkdownDocumentChunks, + nativeMarkdownDocumentRuns, +} from "../../modules/t3-markdown-text/src/nativeMarkdownText"; + +const textStyle = { + color: "#fff", + strongColor: "#fff", + mutedColor: "#aaa", + linkColor: "#acf", + inlineCodeColor: "#fff", + codeColor: "#fff", + codeBackgroundColor: "#111", + codeBlockBackgroundColor: "#111", + fileTextColor: "#fff", + skillTextColor: "#fff", + quoteMarkerColor: "#888", + dividerColor: "#444", + fontSize: 15, + lineHeight: 22, + fontFamily: "system-ui", + headingFontFamily: "system-ui", + boldFontFamily: "system-ui", +}; + +describe("native math", () => { + it("keeps file mentions inside an equation in one math run", () => { + const source = String.raw`$x \text{ @foo.ts }$`; + const runs = nativeMarkdownDocumentRuns({ + type: "document", + children: [{ type: "paragraph", children: [{ type: "math_inline", content: source }] }], + }); + expect(runs).toHaveLength(1); + expect(runs[0]).toMatchObject({ text: source, mathSource: source }); + expect(runs[0]?.href).toBeUndefined(); + }); + it("keeps rich context selection native while typesetting other math chunks", () => { + const math = { text: "$x$", mathSource: "$x$" }; + expect(shouldTypesetNativeMath([math])).toBe(true); + expect(shouldTypesetNativeMath([math, { text: "docs", href: "https://example.com" }])).toBe( + true, + ); + expect( + shouldTypesetNativeMath([ + math, + { text: "Screenshot", href: "t3-context://v1/image/screenshot" }, + ]), + ).toBe(false); + expect(shouldTypesetNativeMath([{ text: "plain" }])).toBe(false); + }); + it("uses shared math recognition and keeps code and currency literal", () => { + let input = ""; + const source = String.raw`Use \(x_i\), keep ` + "`$code$` and pay $20 or $30."; + const tree = parseNativeMarkdownMath(source, (markdown) => { + input = markdown; + return { type: "paragraph", children: [{ type: "text", content: ":t3-math-0:" }] }; + }); + expect(input).toContain(":t3-math-0:"); + expect(input).toContain("`$code$`"); + expect(input).toContain("$20 or $30"); + expect(nativeMarkdownDocumentRuns(tree)).toEqual([ + { text: String.raw`\(x_i\)`, mathSource: String.raw`\(x_i\)`, role: "body" }, + ]); + }); + + it.each([ + String.raw`$x_i^2+\alpha$`, + String.raw`\(\frac{a}{b}\)`, + String.raw`$$\begin{pmatrix}1&2\\3&4\end{pmatrix}$$`, + String.raw`\[\begin{aligned}x&=1\\y&=2\end{aligned}\]`, + ])("produces a self-contained equation for %s", (source) => { + const svg = nativeMathSvg(source); + expect(svg).toContain(" { + expect(nativeMathSvg(String.raw`$\frac{$`)).toBeNull(); + nativeMathSvg(String.raw`$\gdef\privateMacro{x}\privateMacro$`); + expect(nativeMathSvg(String.raw`$\privateMacro$`)).toBeNull(); + }); + + it("keeps original TeX in the selection-copy contract", () => { + const source = String.raw`\[x < y\]`; + const html = nativeMathRunHtml({ text: source, mathSource: source }, textStyle); + expect(html).toContain('data-source="\\[x < y\\]"'); + expect(html).toContain('data-copy="\\[x < y\\]"'); + }); +}); + +it("preserves heading context on inline math and uses native layout for math lists", () => { + const math = { type: "math_inline", content: "$x$" } as const; + const heading = nativeMarkdownDocumentRuns({ type: "heading", level: 2, children: [math] }); + expect(heading[0]).toMatchObject({ mathSource: "$x$", role: "heading", headingLevel: 2 }); + const chunks = nativeMarkdownDocumentChunks({ + type: "document", + children: [{ type: "list", children: [{ type: "list_item", children: [math] }] }], + }); + expect(chunks[0]?.kind).toBe("rich"); +}); + +it("keeps heading size, skill labels, file icons and links in math text", () => { + const icon = "data:image/png;base64,iVBORw0KGgo="; + const html = [ + nativeMathRunHtml( + { text: "$x$", mathSource: "$x$", role: "heading", headingLevel: 2 }, + { ...textStyle, headingFontFamily: "serif" }, + ), + nativeMathRunHtml({ text: "$deploy", skillName: "deploy", skillLabel: "Deploy" }, textStyle), + nativeMathRunHtml( + { text: "app.ts", href: "file:///app.ts", fileIcon: "typescript" }, + textStyle, + { title: "app.ts", actions: [{ id: "open", title: "Open" }] }, + icon, + ), + nativeMathRunHtml({ text: "Email", href: "mailto:hi@example.com" }, textStyle), + nativeMathRunHtml({ text: "Docs", href: "https://example.com" }, textStyle, { + title: "File", + actions: [{ id: "open", title: "Open" }], + }), + ].join(""); + const dom = new JSDOM(html); + try { + const { document } = dom.window; + expect(document.querySelector(".equation")?.parentElement?.style.fontSize).toBe( + "19px", + ); + expect(document.querySelector(".equation")?.parentElement?.style.fontFamily).toBe( + "serif", + ); + expect(document.querySelector("[data-copy-source]")?.textContent).toBe("Deploy"); + expect(document.querySelector("img")?.getAttribute("src")).toBe(icon); + expect(document.querySelectorAll("[data-menu]")).toHaveLength(1); + expect(document.querySelector("[data-menu]")?.getAttribute("data-href")).toBe("file:///app.ts"); + expect(document.querySelector('a[href="https://example.com"]')?.textContent).toBe("Docs"); + expect(document.querySelector('a[href="mailto:hi@example.com"]')?.textContent).toBe("Email"); + } finally { + dom.window.close(); + } +}); diff --git a/apps/mobile/src/lib/nativeMathDocument.test.ts b/apps/mobile/src/lib/nativeMathDocument.test.ts new file mode 100644 index 000000000000..8ffd7b60c7c8 --- /dev/null +++ b/apps/mobile/src/lib/nativeMathDocument.test.ts @@ -0,0 +1,134 @@ +import { JSDOM } from "jsdom"; +import { describe, expect, it } from "vite-plus/test"; +import { NATIVE_MATH_DOCUMENT } from "../../modules/t3-markdown-text/src/nativeMathDocument"; + +function fixture() { + const messages: unknown[] = []; + const dom = new JSDOM(NATIVE_MATH_DOCUMENT, { + runScripts: "dangerously", + beforeParse(window) { + Object.defineProperty(window, "ReactNativeWebView", { + value: { postMessage: (message: string) => messages.push(JSON.parse(message)) }, + }); + Object.defineProperty(window, "ResizeObserver", { + value: class { + observe() {} + }, + }); + }, + }); + const update = (runs: string[], revision: number) => + dom.window.eval( + `window.updateMath(${JSON.stringify({ runs, revision, color: "white", fontSize: 15, lineHeight: 22 })})`, + ); + return { dom, messages, update }; +} + +describe("native math text bridge", () => { + it("copies a visible source disclosure without duplicating it in a whole equation selection", () => { + const { dom, update, messages } = fixture(); + try { + update( + [ + 'x$x$', + ], + 1, + ); + const { document } = dom.window; + const source = document.querySelector(".source")!; + const selection = dom.window.getSelection()!; + const range = document.createRange(); + range.selectNode(source); + selection.addRange(range); + document.dispatchEvent(new dom.window.Event("copy", { cancelable: true })); + expect(messages.at(-1)).toEqual({ type: "copy", revision: 1, text: "$x$" }); + selection.removeAllRanges(); + range.selectNode(document.querySelector(".display")!); + selection.addRange(range); + document.dispatchEvent(new dom.window.Event("copy", { cancelable: true })); + expect(messages.at(-1)).toEqual({ type: "copy", revision: 1, text: "$x$" }); + } finally { + dom.window.close(); + } + }); + it("keeps a completed equation and its open source while adjacent text streams", () => { + const { dom, update, messages } = fixture(); + try { + const equation = + 'x'; + update([equation, "Starting"], 1); + const original = dom.window.document.querySelector(".equation"); + dom.window.document.querySelector("button")!.click(); + update([equation, "Starting a longer answer"], 2); + expect(dom.window.document.querySelector(".equation")).toBe(original); + expect(dom.window.document.querySelector(".source")!.hidden).toBe(false); + expect(messages).toContainEqual({ type: "ready", revision: 0 }); + expect(messages).toContainEqual({ type: "height", revision: 2, height: 0 }); + } finally { + dom.window.close(); + } + }); + + it("lets file menus close without taking an action", () => { + const { dom, update, messages } = fixture(); + try { + update( + [ + ``, + ], + 1, + ); + const { document } = dom.window; + const button = document.querySelector("button")!; + button.click(); + expect(document.querySelector('[role="menu"]')).not.toBeNull(); + button.click(); + expect(document.querySelector('[role="menu"]')).toBeNull(); + button.click(); + document.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key: "Escape" })); + expect(document.querySelector('[role="menu"]')).toBeNull(); + button.click(); + document.body.click(); + expect(document.querySelector('[role="menu"]')).toBeNull(); + expect(messages).not.toContainEqual(expect.objectContaining({ type: "file-action" })); + } finally { + dom.window.close(); + } + }); + + it("copies a selected skill label as its original token", () => { + const { dom, update, messages } = fixture(); + try { + update(['Deploy'], 1); + const range = dom.window.document.createRange(); + range.selectNodeContents(dom.window.document.querySelector("[data-copy-source]")!.lastChild!); + dom.window.getSelection()!.addRange(range); + dom.window.document.dispatchEvent(new dom.window.Event("copy", { cancelable: true })); + expect(messages).toContainEqual({ type: "copy", revision: 1, text: "$deploy" }); + } finally { + dom.window.close(); + } + }); + + it("copies a selected equation as TeX and forwards links to the app", () => { + const { dom, update, messages } = fixture(); + try { + update( + [ + 'Before x after reference', + ], + 1, + ); + const { document } = dom.window; + const range = document.createRange(); + range.selectNodeContents(document.querySelector(".equation span")!); + dom.window.getSelection()!.addRange(range); + document.dispatchEvent(new dom.window.Event("copy", { cancelable: true })); + expect(messages).toContainEqual({ type: "copy", revision: 1, text: String.raw`\(x\)` }); + document.querySelector("a")!.click(); + expect(messages).toContainEqual({ type: "link", revision: 1, href: "https://example.com" }); + } finally { + dom.window.close(); + } + }); +}); diff --git a/apps/mobile/src/lib/wideMarkdownBlocks.test.ts b/apps/mobile/src/lib/wideMarkdownBlocks.test.ts index 4f745aed8851..ecd99083afa2 100644 --- a/apps/mobile/src/lib/wideMarkdownBlocks.test.ts +++ b/apps/mobile/src/lib/wideMarkdownBlocks.test.ts @@ -3,6 +3,12 @@ import { describe, expect, it } from "vite-plus/test"; import { hasWideMarkdownBlock } from "./wideMarkdownBlocks"; describe("hasWideMarkdownBlock", () => { + it("gives math a definite width without expanding currency or literal code", () => { + expect(hasWideMarkdownBlock("An equation $x_i$ here")).toBe(true); + expect(hasWideMarkdownBlock(String.raw`\[x+y\]`)).toBe(true); + expect(hasWideMarkdownBlock("Costs $20 or $30")).toBe(false); + expect(hasWideMarkdownBlock("`$x$`")).toBe(false); + }); it("ignores prose, inline code, and emphasis", () => { expect(hasWideMarkdownBlock("just a message")).toBe(false); expect(hasWideMarkdownBlock("I found it in `secteurs_intervention` earlier")).toBe(false); diff --git a/apps/mobile/src/lib/wideMarkdownBlocks.ts b/apps/mobile/src/lib/wideMarkdownBlocks.ts index 054c60be087e..1eca81cf86d1 100644 --- a/apps/mobile/src/lib/wideMarkdownBlocks.ts +++ b/apps/mobile/src/lib/wideMarkdownBlocks.ts @@ -1,6 +1,8 @@ +import { markdownMathRanges } from "@t3tools/client-runtime/markdown-math"; + /** * Detects markdown that the renderer draws as a block requiring a definite - * user-bubble width: fenced and indented code blocks, GFM tables, ordered + * user-bubble width: math-containing text, fenced and indented code blocks, GFM tables, ordered * lists, and blockquotes when requested by the caller. * * Fenced code blocks and tables report an intrinsic width equal to their @@ -121,6 +123,7 @@ export function hasWideMarkdownBlock( if (options.includeOrderedLists !== false && hasOrderedListItem(text)) { return true; } + if (markdownMathRanges(text).some((range) => range.math !== null)) return true; if (!text.includes("|")) { return false; } diff --git a/apps/web/package.json b/apps/web/package.json index 16579e730f1b..5bca776ae0a9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,6 +40,7 @@ "jose": "catalog:", "jsonc-parser": "3.3.1", "jszip": "3.10.1", + "katex": "^0.16.47", "lexical": "^0.41.0", "lucide-react": "^0.564.0", "react": "19.2.6", @@ -61,6 +62,7 @@ "@types/babel__core": "^7.20.5", "@types/compression": "^1.8.1", "@types/culori": "^4.0.1", + "@types/jsdom": "^30.0.0", "@types/mdast": "^4.0.4", "@types/react": "~19.2.14", "@types/react-dom": "~19.2.3", @@ -69,6 +71,7 @@ "@vitejs/plugin-react": "^6.0.0", "babel-plugin-react-compiler": "1.0.0", "compression": "^1.8.1", + "jsdom": "^30.0.1", "react-test-renderer": "19.2.6", "tailwindcss": "^4.0.0", "unified": "^11.0.5", diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 3aeaaa5b8443..d61b3bb55d9e 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -1,4 +1,6 @@ import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; +import { remarkMath } from "@t3tools/client-runtime/markdown-math"; +import { MarkdownMath } from "./MarkdownMath"; import { useAtomValue } from "@effect/atom-react"; import { COMPOSER_CONTEXT_CLIPBOARD_MIME, @@ -462,6 +464,7 @@ const CHAT_MARKDOWN_SANITIZE_SCHEMA = { attributes: { ...defaultSchema.attributes, "*": (defaultSchema.attributes?.["*"] ?? []).filter((attribute) => attribute !== "title"), + span: [...(defaultSchema.attributes?.span ?? []), "dataMathSource"], code: [...(defaultSchema.attributes?.code ?? []), "dataCodeMeta", "dataInlineCode"], blockquote: [...(defaultSchema.attributes?.blockquote ?? []), "dataAlert"], div: [...(defaultSchema.attributes?.div ?? []), ...CODEX_ARTIFACT_TEMPLATE_HAST_PROPERTIES], @@ -482,6 +485,7 @@ const CHAT_MARKDOWN_SANITIZE_SCHEMA = { const CHAT_MARKDOWN_REMARK_PLUGINS = [ remarkGfm, + remarkMath, remarkGithubAlerts, remarkNormalizeListItemIndentation, remarkCodexDirectives, @@ -491,6 +495,7 @@ const CHAT_MARKDOWN_REMARK_PLUGINS = [ const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [ remarkGfm, + remarkMath, remarkGithubAlerts, remarkNormalizeListItemIndentation, remarkCodexDirectives, @@ -2717,6 +2722,19 @@ const CHAT_MARKDOWN_COMPONENTS = { h4: markdownHeadingRenderer(4), h5: markdownHeadingRenderer(5), h6: markdownHeadingRenderer(6), + span: ({ node, children, ...props }) => { + const { text } = use(ChatMarkdownRendererContext); + const source = node?.properties.dataMathSource; + const offset = node?.position?.start.offset; + // Raw HTML can carry the same attribute. Only a parser-created span starts + // at a math delimiter in the original Markdown, rather than at an HTML tag. + const isMath = + typeof source === "string" && + offset !== undefined && + (source.startsWith("$") || source.startsWith("\\(") || source.startsWith("\\[")) && + text.startsWith(source.slice(0, 2), offset); + return isMath ? : {children}; + }, div: function MarkdownDiv({ node, children, ...props }) { const { onUseArtifactTemplate } = use(ChatMarkdownRendererContext); const artifactTemplate = artifactTemplateFromHastProperties(node?.properties); diff --git a/apps/web/src/components/MarkdownMath.tsx b/apps/web/src/components/MarkdownMath.tsx new file mode 100644 index 000000000000..d71f336c3c1c --- /dev/null +++ b/apps/web/src/components/MarkdownMath.tsx @@ -0,0 +1,78 @@ +import { lazy, memo, Suspense, useState } from "react"; +import { markdownMath } from "@t3tools/client-runtime/markdown-math"; +import { RenderErrorBoundary } from "./RenderErrorBoundary"; +import { Button } from "./ui/button"; +import { toastManager } from "./ui/toast"; + +// The renderer and its fonts are loaded only when a message contains math. +const MathTypeset = lazy(() => import("./MathTypeset")); + +export const MarkdownMath = memo(function MarkdownMath({ source }: { source: string }) { + const math = markdownMath(source); + const [showSource, setShowSource] = useState(false); + if (!math) return <>{source}; + const fallback = {source}; + return ( + + {math.display ? ( + + + + + ) : null} + + + + + + + + {showSource && math.display ? ( + + {source} + + ) : null} + + ); +}); diff --git a/apps/web/src/components/MathTypeset.tsx b/apps/web/src/components/MathTypeset.tsx new file mode 100644 index 000000000000..2698dde28f5f --- /dev/null +++ b/apps/web/src/components/MathTypeset.tsx @@ -0,0 +1,12 @@ +import { memo } from "react"; +import { renderMathHtml } from "../mathRendering"; +import "katex/dist/katex.min.css"; + +export default memo(function MathTypeset({ source }: { source: string }) { + const html = renderMathHtml(source); + return html === null ? ( + {source} + ) : ( + + ); +}); diff --git a/apps/web/src/components/cloud/CloudEnvironmentConnectList.test.tsx b/apps/web/src/components/cloud/CloudEnvironmentConnectList.test.tsx index 6474c2208c78..e2653a639561 100644 --- a/apps/web/src/components/cloud/CloudEnvironmentConnectList.test.tsx +++ b/apps/web/src/components/cloud/CloudEnvironmentConnectList.test.tsx @@ -122,7 +122,10 @@ async function advance(milliseconds: number) { } beforeEach(() => { - vi.useFakeTimers(); + // React act uses setImmediate to settle work; only application clocks are fake. + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"], + }); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); page = Object.assign(new EventTarget(), { visibilityState: "visible" as const }); browserWindow = new EventTarget(); diff --git a/apps/web/src/components/diffs/DiffFileTree.test.tsx b/apps/web/src/components/diffs/DiffFileTree.test.tsx index 3a97c60254ab..e04a83ef74ca 100644 --- a/apps/web/src/components/diffs/DiffFileTree.test.tsx +++ b/apps/web/src/components/diffs/DiffFileTree.test.tsx @@ -99,7 +99,10 @@ describe("diff tree file activation", () => { beforeEach(() => { targets.length = 0; - vi.useFakeTimers(); + // React act uses setImmediate to settle work; only application clocks are fake. + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"], + }); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.stubGlobal("HTMLElement", TreeRow); }); diff --git a/apps/web/src/components/files/useFileSaveCoordinator.test.tsx b/apps/web/src/components/files/useFileSaveCoordinator.test.tsx index 603f97cf1f26..bec2199d0374 100644 --- a/apps/web/src/components/files/useFileSaveCoordinator.test.tsx +++ b/apps/web/src/components/files/useFileSaveCoordinator.test.tsx @@ -50,7 +50,10 @@ function changeHandler(): (contents: string) => void { beforeEach(() => { renderer = null; - vi.useFakeTimers(); + // React act uses setImmediate to settle work; only application clocks are fake. + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"], + }); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); writeFile.mockReset().mockResolvedValue(AsyncResult.success(undefined)); confirmFile.mockReset(); diff --git a/apps/web/src/components/preview/PreviewView.test.tsx b/apps/web/src/components/preview/PreviewView.test.tsx index ea28a93235ee..bdf4af7f5b5a 100644 --- a/apps/web/src/components/preview/PreviewView.test.tsx +++ b/apps/web/src/components/preview/PreviewView.test.tsx @@ -355,7 +355,10 @@ describe("PreviewView navigation", () => { }); it("does not rerender while loading time passes", async () => { - vi.useFakeTimers(); + // React act uses setImmediate to settle work; only application clocks are fake. + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"], + }); mocks.loading = true; const document = installTestDom(); const { createRoot } = await import("react-dom/client"); diff --git a/apps/web/src/components/sidebar/SidebarCompletedTime.test.tsx b/apps/web/src/components/sidebar/SidebarCompletedTime.test.tsx index 7327af79e794..04e296a0dcab 100644 --- a/apps/web/src/components/sidebar/SidebarCompletedTime.test.tsx +++ b/apps/web/src/components/sidebar/SidebarCompletedTime.test.tsx @@ -7,7 +7,10 @@ import { SidebarCompletedTime } from "./SidebarCompletedTime"; let renderer: ReactTestRenderer | undefined; beforeEach(() => { - vi.useFakeTimers(); + // React act uses setImmediate to settle work; only application clocks are fake. + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"], + }); vi.setSystemTime(new Date("2026-09-07T01:01:00Z")); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.stubGlobal("window", { diff --git a/apps/web/src/index.css b/apps/web/src/index.css index a7f6d91f2e40..7e6913984409 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -2104,3 +2104,13 @@ code { visibility: visible; } } + +/* KaTeX owns this generated markup; wide equations scroll inside their viewport. */ +.markdown-math .katex-display { + margin: 0; +} +.markdown-math .katex-display > .katex { + display: block; + width: max-content; + min-width: 100%; +} diff --git a/apps/web/src/markdown-clipboard.ts b/apps/web/src/markdown-clipboard.ts index 4a96c8b31d13..2ec17e0d5b2e 100644 --- a/apps/web/src/markdown-clipboard.ts +++ b/apps/web/src/markdown-clipboard.ts @@ -384,8 +384,18 @@ export function chatMarkdownClipboardPayload( const texts: string[] = []; const htmls: string[] = []; for (let index = 0; index < selection.rangeCount; index += 1) { - const range = selection.getRangeAt(index); + const range = selection.getRangeAt(index).cloneRange(); if (range.collapsed) continue; + // A fraction may put each endpoint several spans inside the rendered math. + // Copy the equation as one source token, never as flattened glyphs. + const mathAt = (node: Node) => + (node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement)?.closest( + "[data-markdown-math]", + ); + const startMath = mathAt(range.startContainer); + const endMath = mathAt(range.endContainer); + if (startMath) range.setStartBefore(startMath); + if (endMath) range.setEndAfter(endMath); const container = document.createElement("div"); container.appendChild(range.cloneContents()); const ancestor = range.commonAncestorContainer; diff --git a/apps/web/src/mathClipboard.test.ts b/apps/web/src/mathClipboard.test.ts new file mode 100644 index 000000000000..39fa9936ce1d --- /dev/null +++ b/apps/web/src/mathClipboard.test.ts @@ -0,0 +1,51 @@ +import { JSDOM } from "jsdom"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import { chatMarkdownClipboardPayload } from "./markdown-clipboard"; +import { renderMathHtml } from "./mathRendering"; + +const windows: JSDOM[] = []; +afterEach(() => { + for (const dom of windows.splice(0)) dom.window.close(); + vi.unstubAllGlobals(); +}); + +function fixture() { + const dom = new JSDOM("

Before after.

"); + windows.push(dom); + const { document } = dom.window; + vi.stubGlobal("document", document); + vi.stubGlobal("Node", dom.window.Node); + const math = document.querySelector("[data-markdown-math]")!; + math.setAttribute("data-markdown-copy", String.raw`\(\frac{x}{y}\)`); + math.innerHTML = renderMathHtml(String.raw`\(\frac{x}{y}\)`) ?? ""; + return { document, math, selection: dom.window.getSelection()! }; +} + +describe("copying rendered equations", () => { + it("copies the original TeX when selection is inside a fraction", () => { + const { document, math, selection } = fixture(); + const numerator = [...math.querySelectorAll(".mord")].find((node) => node.textContent === "x")!; + const range = document.createRange(); + range.selectNodeContents(numerator); + selection.addRange(range); + expect(chatMarkdownClipboardPayload(selection)?.text).toBe(String.raw`\(\frac{x}{y}\)`); + }); + + it("preserves surrounding prose without duplicating accessible math", () => { + const { document, selection } = fixture(); + const range = document.createRange(); + range.selectNodeContents(document.querySelector("p")!); + selection.addRange(range); + expect(chatMarkdownClipboardPayload(selection)?.text).toBe( + String.raw`Before \(\frac{x}{y}\) after.`, + ); + }); + + it("does not copy a collapsed caret inside an equation", () => { + const { document, math, selection } = fixture(); + const range = document.createRange(); + range.setStart(math, 0); + selection.addRange(range); + expect(chatMarkdownClipboardPayload(selection)).toBeNull(); + }); +}); diff --git a/apps/web/src/mathRendering.test.ts b/apps/web/src/mathRendering.test.ts new file mode 100644 index 000000000000..c247f1c5f942 --- /dev/null +++ b/apps/web/src/mathRendering.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vite-plus/test"; +import { renderMathHtml } from "./mathRendering"; + +describe("math rendering", () => { + it.each([ + String.raw`$x_i^2 + \alpha$`, + String.raw`\(\frac{a}{b}\)`, + String.raw`\[\sum_{i=1}^n x_i\]`, + String.raw`$$\begin{pmatrix}1 & 2 \\ 3 & 4\end{pmatrix}$$`, + String.raw`$$\begin{aligned}x &= 1 \\ y &= 2\end{aligned}$$`, + ])("renders accessible math for %s", (source) => { + const html = renderMathHtml(source); + expect(html).toContain(' { + expect(renderMathHtml(source)).toBeNull(); + expect(renderMathHtml("$x$")).toContain(" { + const html = renderMathHtml(String.raw`$\href{javascript:alert(1)}{click}$`); + expect(html ?? "").not.toContain("href="); + expect(renderMathHtml(String.raw`$\htmlClass{bad}{x}$`)).toBeNull(); + }); + + it("does not share macro definitions between equations", () => { + renderMathHtml(String.raw`$\gdef\privateMacro{x}\privateMacro$`); + expect(renderMathHtml(String.raw`$\privateMacro$`)).toBeNull(); + }); +}); diff --git a/apps/web/src/mathRendering.ts b/apps/web/src/mathRendering.ts new file mode 100644 index 000000000000..72a4c70ef73c --- /dev/null +++ b/apps/web/src/mathRendering.ts @@ -0,0 +1,29 @@ +import katex from "katex"; +import { markdownMath } from "@t3tools/client-runtime/markdown-math"; +import { LRUCache } from "./lib/lruCache"; + +const cache = new LRUCache<{ html: string | null }>(128, 2 * 1024 * 1024); + +/** Only generated KaTeX markup reaches innerHTML. Authored HTML never enters this path. */ +export function renderMathHtml(source: string): string | null { + const cached = cache.get(source); + if (cached) return cached.html; + const math = markdownMath(source); + if (!math) return null; + let html: string | null = null; + try { + html = katex.renderToString(math.tex, { + displayMode: math.display, + output: "htmlAndMathml", + throwOnError: true, + strict: "error", + trust: false, + maxExpand: 1000, + maxSize: 20, + }); + } catch { + // Malformed and unsupported expressions remain readable and copyable TeX. + } + cache.set(source, { html }, (source.length + (html?.length ?? 0)) * 2); + return html; +} diff --git a/docs/user/composer.md b/docs/user/composer.md index 4da452215893..d8b854585ca1 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -210,3 +210,12 @@ automatically. HTML previews cannot access your T3 Code session. On mobile, select a PDF attachment or link to open it. iOS uses the native viewer; Android opens a compatible installed file viewer. + +## Math in messages + +Inline math uses `$...$` or `\(...\)`. Display equations use `$$...$$` or `\[...\]`. +Web, desktop, iOS, and Android render supported TeX, including fractions, matrices, and aligned equations. Wide equations scroll horizontally. + +Use **Copy TeX** on a display equation to reuse its original source. Selecting math or copying the whole response also preserves the TeX. Code stays literal, and invalid or unfinished expressions remain readable as source. + +On mobile, math in a text block containing attached context stays as TeX so copying preserves that context. diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index b4a3d5da118d..abcf80cf7355 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -254,6 +254,10 @@ "./file-preview": { "types": "./src/filePreview.ts", "default": "./src/filePreview.ts" + }, + "./markdown-math": { + "types": "./src/markdownMath.ts", + "default": "./src/markdownMath.ts" } }, "scripts": { @@ -267,11 +271,13 @@ "mdast-util-directive": "^3.1.0", "micromark-extension-directive": "^4.0.0", "micromark-util-character": "^2.1.1", + "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "unified": "^11.0.5" }, "devDependencies": { "@effect/vitest": "catalog:", + "@types/mdast": "^4.0.4", "micromark-util-types": "^2.0.2", "vite-plus": "catalog:" } diff --git a/packages/client-runtime/src/markdownMath.test.ts b/packages/client-runtime/src/markdownMath.test.ts new file mode 100644 index 000000000000..d2ccbb5b88ed --- /dev/null +++ b/packages/client-runtime/src/markdownMath.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vite-plus/test"; +import remarkParse from "remark-parse"; +import { unified } from "unified"; +import { markdownMathRanges, remarkMath } from "./markdownMath.ts"; + +const parser = unified().use(remarkParse).use(remarkMath); + +describe("Markdown math", () => { + it.each([ + ["$x_i^2$", "x_i^2", false], + ["$$x_i^2$$", "x_i^2", true], + [String.raw`\(\frac{a}{b}\)`, String.raw`\frac{a}{b}`, false], + [String.raw`\[\sum_i x_i\]`, String.raw`\sum_i x_i`, true], + ["$$\nx_i + y\n$$", "x_i + y", true], + [ + "\\[\n\\begin{aligned}\nx &= 1 \\\\\ny &= 2\n\\end{aligned}\n\\]", + "\\begin{aligned}\nx &= 1 \\\\\ny &= 2\n\\end{aligned}", + true, + ], + ])("recognizes %s", (source, tex, display) => { + expect(markdownMathRanges(source)).toEqual([ + { source, math: { source, tex, display }, start: 0, end: source.length }, + ]); + }); + + it.each([ + "Costs $20 today and $30 tomorrow.", + "$20.00, $30.00, and $40.00", + "`$x$` and `\\(x\\)`", + "```tex\n$$x$$\n```", + " \\[x\\]", + String.raw`\$x\$ and \\(x\\)`, + "[link](https://example.com/$x$)", + "https://example.com/$x$", + "www.example.com/$x$", + "$$\nunfinished\n\nNext paragraph $5", + "$ incomplete$ and $incomplete $", + "$$$x$$$", + ])("leaves literal content unchanged: %s", (source) => { + expect(markdownMathRanges(source)).toEqual([]); + }); + + it("keeps exact source offsets alongside emoji, lists and emphasis", () => { + const source = "🙂 **Fit** $x_i$\n\n- [ ] Verify \\(y\\)"; + const ranges = markdownMathRanges(source); + expect(ranges.map((range) => source.slice(range.start, range.end))).toEqual([ + "$x_i$", + "\\(y\\)", + ]); + expect(ranges.map((range) => range.math?.tex)).toEqual(["x_i", "y"]); + }); + + it("preserves incomplete backslash openers during streaming", () => { + const paragraph = parser.parse(String.raw`Use \(x`).children[0]; + expect( + paragraph?.type === "paragraph" && + paragraph.children.map((node) => ("value" in node ? node.value : "")).join(""), + ).toBe(String.raw`Use \(x`); + }); + + it("does not interpret escaped dollar signs inside an expression as its end", () => { + expect(markdownMathRanges(String.raw`$x + \$5$`)[0]?.math?.tex).toBe(String.raw`x + \$5`); + }); +}); + +it("ends unfinished backslash math at a paragraph boundary and preserves following Markdown", () => { + const tree = parser.parse("\\(unfinished\n\n## Heading\n\n[link](https://example.com)"); + expect(tree.children[1]).toMatchObject({ type: "heading", depth: 2 }); + expect(tree.children[2]).toMatchObject({ + type: "paragraph", + children: [{ type: "link", url: "https://example.com" }], + }); +}); diff --git a/packages/client-runtime/src/markdownMath.ts b/packages/client-runtime/src/markdownMath.ts new file mode 100644 index 000000000000..952d3435a21b --- /dev/null +++ b/packages/client-runtime/src/markdownMath.ts @@ -0,0 +1,228 @@ +import type { Literal, Root } from "mdast"; +import { markdownLineEnding, markdownSpace } from "micromark-util-character"; +import type { Code, Construct, State, Tokenizer } from "micromark-util-types"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import { unified, type Processor } from "unified"; + +export interface MarkdownMath { + readonly source: string; + readonly tex: string; + readonly display: boolean; +} + +interface MathNode extends Literal { + type: "inlineMath"; + data: { + math: MarkdownMath | null; + hName: "span"; + hProperties: { dataMathSource: string }; + }; +} + +declare module "mdast" { + interface RootContentMap { + inlineMath: MathNode; + } + interface PhrasingContentMap { + inlineMath: MathNode; + } +} + +declare module "micromark-util-types" { + interface TokenTypeMap { + t3Math: "t3Math"; + t3MathData: "t3MathData"; + } +} + +// Bound both incomplete-delimiter lookahead and work handed to a TeX renderer. +const MAX_MATH_LENGTH = 16_384; + +export function markdownMath(source: string): MarkdownMath | null { + const opener = source.startsWith("$$") ? "$$" : source.slice(0, 2); + const delimiter = opener === "\\(" || opener === "\\[" || opener === "$$" ? opener : "$"; + const close = delimiter === "\\(" ? "\\)" : delimiter === "\\[" ? "\\]" : delimiter; + if ( + !source.startsWith(delimiter) || + !source.endsWith(close) || + source.length <= delimiter.length + close.length || + source.length > MAX_MATH_LENGTH + ) + return null; + const tex = source.slice(delimiter.length, -close.length).trim(); + return tex ? { source, tex, display: delimiter === "$$" || delimiter === "\\[" } : null; +} + +const tokenizeMath: Tokenizer = function (effects, ok, nok) { + let opener: "$" | "$$" | "\\(" | "\\[" = "$"; + let count = 0; + let previous: Code = null; + let closingCount = 0; + let hasContent = false; + let lineStart = false; + let dataOpen = false; + const closeData = () => { + if (dataOpen) effects.exit("t3MathData"); + dataOpen = false; + }; + const consume = (code: Code) => { + if (!dataOpen) effects.enter("t3MathData"); + dataOpen = true; + effects.consume(code); + count += 1; + previous = code; + }; + return start; + + function unfinished(code: Code): State | undefined { + if (opener === "$" || opener === "$$") return nok(code); + closeData(); + effects.exit("t3Math"); + return ok(code); + } + function start(code: Code): State | undefined { + effects.enter("t3Math"); + consume(code); + return code === 92 ? backslashOpen : dollarOpen; + } + function backslashOpen(code: Code): State | undefined { + if (code !== 40 && code !== 91) return nok(code); + opener = code === 40 ? "\\(" : "\\["; + consume(code); + return body; + } + function dollarOpen(code: Code): State | undefined { + if (code === 36) { + opener = "$$"; + consume(code); + return displayStart; + } + if (code === null || markdownLineEnding(code) || markdownSpace(code)) return nok(code); + return body(code); + } + function displayStart(code: Code): State | undefined { + return code === 36 ? nok(code) : body(code); + } + function body(code: Code): State | undefined { + if (code === null || count >= MAX_MATH_LENGTH) return unfinished(code); + if (markdownLineEnding(code)) { + // A blank line ends a candidate. Never swallow later paragraphs while streaming. + if (lineStart || opener === "$") return unfinished(code); + lineStart = true; + closeData(); + effects.enter("lineEnding"); + effects.consume(code); + effects.exit("lineEnding"); + count += 1; + previous = code; + return body; + } + if (!markdownSpace(code)) lineStart = false; + if (code === 92) { + consume(code); + return escaped; + } + if (code === 36 && (opener === "$" || opener === "$$")) { + if ( + !hasContent || + (opener === "$" && (markdownSpace(previous) || markdownLineEnding(previous))) + ) + return nok(code); + closingCount = 1; + consume(code); + return closeDollar; + } + if (!markdownSpace(code)) hasContent = true; + consume(code); + return body; + } + function escaped(code: Code): State | undefined { + if ((opener === "\\(" && code === 41) || (opener === "\\[" && code === 93)) { + consume(code); + closeData(); + effects.exit("t3Math"); + return ok; + } + if (code === null || markdownLineEnding(code)) return body(code); + hasContent = true; + consume(code); + return body; + } + function closeDollar(code: Code): State | undefined { + if (code === 36) { + closingCount += 1; + consume(code); + return closeDollar; + } + if (closingCount !== opener.length) return nok(code); + // Pandoc-style dollar boundaries keep "$20 and $30" as ordinary prose. + if (opener === "$" && code !== null && code >= 48 && code <= 57) return nok(code); + closeData(); + effects.exit("t3Math"); + return ok(code); + } +}; + +/** Recognize math in Markdown's text grammar, so code, links and escapes keep their semantics. */ +function attachMath(this: Processor) { + const data = this.data(); + const construct: Construct = { tokenize: tokenizeMath }; + (data.micromarkExtensions ??= []).push({ + text: { 36: construct, 92: construct }, + }); + (data.fromMarkdownExtensions ??= []).push({ + enter: { + t3Math(token) { + const source = this.sliceSerialize(token); + const math = markdownMath(source); + this.enter( + { + type: "inlineMath", + value: math?.tex ?? source, + data: { + math, + hName: "span", + hProperties: { dataMathSource: source }, + }, + }, + token, + ); + }, + }, + exit: { + t3Math(token) { + this.exit(token); + }, + }, + }); +} + +export const remarkMath = attachMath; + +const parser = unified().use(remarkParse).use(remarkGfm).use(remarkMath).freeze(); + +/** Source ranges let native Markdown use the same grammar without a second delimiter scanner. */ +export function markdownMathRanges(source: string) { + const matches: Array<{ source: string; math: MarkdownMath | null; start: number; end: number }> = + []; + if (!source.includes("$") && !source.includes("\\(") && !source.includes("\\[")) return matches; + const tree = parser.parse(source); + function visit(node: Root | Root["children"][number] | MathNode): void { + if (node.type === "inlineMath") { + const start = node.position?.start.offset; + const end = node.position?.end.offset; + if (start !== undefined && end !== undefined) + matches.push({ + source: node.data.hProperties.dataMathSource, + math: node.data.math, + start, + end, + }); + } else if ("children" in node) { + for (const child of node.children) visit(child); + } + } + visit(tree); + return matches; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8dbaf396924..bd9a89aea6cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,7 +125,7 @@ importers: version: 7.0.2 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/desktop: dependencies: @@ -204,7 +204,7 @@ importers: version: 4.3.3 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/marketing: dependencies: @@ -292,7 +292,7 @@ importers: version: link:../../packages/contracts '@t3tools/mobile-markdown-text': specifier: file:./modules/t3-markdown-text - version: file:apps/mobile/modules/t3-markdown-text(6f0a94b6f541bbd4be3f343e27991996) + version: file:apps/mobile/modules/t3-markdown-text(850a76760aa5580964122081bc0f7daa) '@t3tools/mobile-review-diff-native': specifier: file:./modules/t3-review-diff version: file:apps/mobile/modules/t3-review-diff @@ -416,6 +416,9 @@ importers: expo-widgets: specifier: ~57.0.15 version: 57.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + mathjax-full: + specifier: ^3.2.2 + version: 3.2.2 react: specifier: 19.2.3 version: 19.2.3 @@ -477,6 +480,9 @@ importers: '@pierre/trees': specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/jsdom': + specifier: ^30.0.0 + version: 30.0.0 '@types/react': specifier: ~19.2.0 version: 19.2.16 @@ -486,6 +492,9 @@ importers: babel-preset-expo: specifier: ~57.0.9 version: 57.0.9(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@57.0.15)(expo@57.0.18)(react-refresh@0.14.2) + jsdom: + specifier: ^30.0.1 + version: 30.0.1(@noble/hashes@2.2.0) tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -573,7 +582,7 @@ importers: version: link:../../packages/effect-codex-app-server vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/web: dependencies: @@ -661,6 +670,9 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 + katex: + specifier: ^0.16.47 + version: 0.16.47 lexical: specifier: ^0.41.0 version: 0.41.0 @@ -719,6 +731,9 @@ importers: '@types/culori': specifier: ^4.0.1 version: 4.0.1 + '@types/jsdom': + specifier: ^30.0.0 + version: 30.0.0 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -743,6 +758,9 @@ importers: compression: specifier: ^1.8.1 version: 1.8.1 + jsdom: + specifier: ^30.0.1 + version: 30.0.1(@noble/hashes@1.8.0) react-test-renderer: specifier: 19.2.6 version: 19.2.6(react@19.2.6) @@ -757,7 +775,7 @@ importers: version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) infra/relay: dependencies: @@ -812,7 +830,7 @@ importers: version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) oxlint-plugin-t3code: dependencies: @@ -831,7 +849,7 @@ importers: version: 4.0.0-rc.112(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/client-runtime: dependencies: @@ -853,6 +871,9 @@ importers: micromark-util-character: specifier: ^2.1.1 version: 2.1.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 remark-parse: specifier: ^11.0.0 version: 11.0.0 @@ -863,12 +884,15 @@ importers: '@effect/vitest': specifier: 4.0.0-rc.112 version: 4.0.0-rc.112(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 micromark-util-types: specifier: ^2.0.2 version: 2.0.2 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/contracts: dependencies: @@ -881,7 +905,7 @@ importers: version: 4.0.0-rc.112(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-acp: dependencies: @@ -903,7 +927,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-codex-app-server: dependencies: @@ -925,7 +949,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/shared: dependencies: @@ -959,7 +983,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/ssh: dependencies: @@ -984,7 +1008,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/tailscale: dependencies: @@ -1003,7 +1027,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) scripts: dependencies: @@ -1046,7 +1070,7 @@ importers: version: typescript@6.0.3 vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages: @@ -1108,6 +1132,14 @@ packages: zod: optional: true + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} + '@astrojs/check@0.9.9': resolution: {integrity: sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==} hasBin: true @@ -1727,6 +1759,10 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bruits/satteri-darwin-arm64@0.9.3': resolution: {integrity: sha512-dRUZZrdwh1asfTOyM1nDNmzolhnHtlIFpqYrl1Tdd3YVcaebKmrfJgGL7NAoGPjbEwYmZxaugrxA0uzw83c0dw==} cpu: [arm64] @@ -2002,6 +2038,42 @@ packages: resolution: {integrity: sha512-WTYHwlBhImGmmMt81crxaRvNdZXlBNemFyrP5GGHj2WeDTJMBrHgHTaladTypXHXrviFFBBGROl5moijRQ4ODA==} engines: {node: '>=18'} + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.2': + resolution: {integrity: sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.13': + resolution: {integrity: sha512-i9ZylF5QNhmNfPA9l0vHAWK4kPrbIp6g9lKgaiIFsIBz2F/WNB7OLrzlNNcCOm+h42bkaSD2v1PG+IBPHhc3ZA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@distilled.cloud/aws@1.0.0-rc.8': resolution: {integrity: sha512-eL4tRACU4TSykk/6+ItwUTZs93r3tRlrBn36ohqaUxqYYOfmssYlt/SLCzos4nlLgCxb+1T6BAstgYC848v1EQ==} peerDependencies: @@ -2604,6 +2676,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} @@ -5118,11 +5199,14 @@ packages: expo: '*' expo-asset: '*' expo-clipboard: '*' + expo-file-system: '*' expo-haptics: '*' expo-symbols: '*' + mathjax-full: '*' react: '*' react-native: '*' react-native-nitro-markdown: '*' + react-native-webview: '*' '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': resolution: {directory: apps/mobile/modules/t3-review-diff, type: directory} @@ -5415,6 +5499,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jsdom@30.0.0': + resolution: {integrity: sha512-uAHGxujGE0cDaKGdK28zgDotFtNA7MKq5DXl8LrfdxdCI8VHcg15oJz+amHTChPNI5JpgEPQWc2xFdrw3em/nQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -5465,6 +5552,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -6353,6 +6443,9 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -6660,6 +6753,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -6675,6 +6772,10 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + 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} @@ -6805,6 +6906,10 @@ packages: resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dbus-next@0.10.2: resolution: {integrity: sha512-kLNQoadPstLgKKGIXKrnRsMgtAK/o+ix3ZmcfTfvBHzghiO9yHXpoKImGnB50EXwnfSFaSAullW/7UrSkAISSQ==} @@ -6837,6 +6942,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -7200,6 +7308,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -7279,6 +7391,10 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} @@ -8026,6 +8142,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -8211,6 +8331,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -8312,6 +8435,15 @@ packages: jsc-safe-url@0.2.4: resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8365,6 +8497,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==} @@ -8547,6 +8683,10 @@ packages: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -8592,6 +8732,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + deprecated: Version 4 replaces this package with the scoped package @mathjax/src + mdast-util-definitions@6.0.0: resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} @@ -8792,6 +8936,9 @@ packages: engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -8964,6 +9111,9 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -9338,6 +9488,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10099,6 +10252,10 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -10306,6 +10463,10 @@ packages: sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + speech-rule-engine@4.1.4: + resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==} + hasBin: true + split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} @@ -10465,6 +10626,9 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -10580,8 +10744,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} tr46@0.0.3: @@ -10591,6 +10755,10 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -10666,6 +10834,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@8.10.2: + resolution: {integrity: sha512-7/+aSjzkUoLc92hV22bTW4aGanXf800zbwguhcICs0OAoCF9wDOE4wkopQ+SqfhXZm8mCK8gHpdTs7pZUWzK3w==} + undici@6.28.0: resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} engines: {node: '>=18.17'} @@ -11090,6 +11261,10 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -11116,12 +11291,20 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url-minimum@0.1.2: resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==} @@ -11129,6 +11312,14 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.1: + resolution: {integrity: sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==} + engines: {node: ^22.14.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -11156,6 +11347,9 @@ packages: engines: {node: '>=8'} hasBin: true + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + widest-line@6.0.0: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} @@ -11220,6 +11414,10 @@ packages: resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} engines: {node: '>=10.0.0'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml-naming@0.1.0: resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} engines: {node: '>=16.0.0'} @@ -11240,6 +11438,9 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11419,6 +11620,21 @@ snapshots: optionalDependencies: zod: 4.4.3 + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.2.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + '@astrojs/check@0.9.9(prettier@3.8.3)(typescript@6.0.3)': dependencies: '@astrojs/language-server': 2.16.10(prettier@3.8.3)(typescript@6.0.3) @@ -12302,6 +12518,10 @@ snapshots: '@blazediff/core@1.9.1': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@bruits/satteri-darwin-arm64@0.9.3': optional: true @@ -12560,6 +12780,30 @@ snapshots: '@crowecawcaw/xa11y-win32-arm64-msvc': 0.13.0 '@crowecawcaw/xa11y-win32-x64-msvc': 0.13.0 + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.2.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.13(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@distilled.cloud/aws@1.0.0-rc.8(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -13100,6 +13344,14 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@exodus/bytes@1.15.1(@noble/hashes@1.8.0)': + optionalDependencies: + '@noble/hashes': 1.8.0 + + '@exodus/bytes@1.15.1(@noble/hashes@2.2.0)': + optionalDependencies: + '@noble/hashes': 2.2.0 + '@exodus/schemasafe@1.3.0': {} '@expo-google-fonts/dm-sans@0.4.2': {} @@ -15602,18 +15854,21 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(6f0a94b6f541bbd4be3f343e27991996)': + '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(850a76760aa5580964122081bc0f7daa)': dependencies: '@t3tools/client-runtime': link:packages/client-runtime '@t3tools/shared': link:packages/shared expo: 57.0.18(fc5a731e35a0144aab60c7305f29cbed) expo-asset: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@7.0.2) expo-clipboard: 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-file-system: 57.0.6(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-haptics: 57.0.2(expo@57.0.18) expo-symbols: 57.0.2(expo-font@57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + mathjax-full: 3.2.2 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-webview: 13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {} @@ -15925,6 +16180,13 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jsdom@30.0.0': + dependencies: + '@types/node': 24.12.4 + '@types/tough-cookie': 4.0.5 + parse5: 8.0.1 + undici-types: 8.10.2 + '@types/json-schema@7.0.15': {} '@types/keyv@3.1.4': @@ -15981,6 +16243,8 @@ snapshots: '@types/statuses@2.0.6': optional: true + '@types/tough-cookie@4.0.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -16095,7 +16359,7 @@ snapshots: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11) - vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) transitivePeerDependencies: - bufferutil - msw @@ -16111,7 +16375,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -16888,6 +17152,10 @@ snapshots: before-after-hook@4.0.0: {} + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + big-integer@1.6.52: {} bippy@0.5.41(react@19.2.6): @@ -17211,6 +17479,8 @@ snapshots: commander@12.1.0: {} + commander@13.1.0: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -17219,6 +17489,8 @@ snapshots: commander@7.2.0: {} + commander@8.3.0: {} + commander@9.5.0: optional: true @@ -17352,6 +17624,20 @@ snapshots: culori@4.0.2: {} + data-urls@7.0.0(@noble/hashes@1.8.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + + data-urls@7.0.0(@noble/hashes@2.2.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - '@noble/hashes' + dbus-next@0.10.2(patch_hash=cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4): dependencies: '@nornagon/put': 0.0.8 @@ -17378,6 +17664,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -17650,6 +17938,8 @@ snapshots: entities@6.0.1: {} + entities@8.1.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -17756,6 +18046,8 @@ snapshots: escape-string-regexp@5.0.0: {} + esm@3.2.25: {} + estree-util-is-identifier-name@3.0.0: {} estree-walker@2.0.2: {} @@ -18829,6 +19121,18 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - '@noble/hashes' + html-escaper@3.0.3: {} html-url-attributes@3.0.1: {} @@ -19011,6 +19315,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-unicode-supported@2.1.0: {} @@ -19096,6 +19402,58 @@ snapshots: jsc-safe-url@0.2.4: {} + jsdom@30.0.1(@noble/hashes@1.8.0): + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.13(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@1.8.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.1(@noble/hashes@1.8.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsdom@30.0.1(@noble/hashes@2.2.0): + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.13(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.2.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.1(@noble/hashes@2.2.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -19144,6 +19502,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 @@ -19303,6 +19665,8 @@ snapshots: lru-cache@11.5.1: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -19346,6 +19710,13 @@ snapshots: math-intrinsics@1.1.0: {} + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.1.4 + mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 @@ -19891,6 +20262,8 @@ snapshots: - supports-color - utf-8-validate + mhchemparser@4.2.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -20151,6 +20524,8 @@ snapshots: dependencies: minipass: 7.1.3 + mj-context-menu@0.6.1: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -20212,7 +20587,7 @@ snapshots: rettime: 0.10.1 statuses: 2.0.2 strict-event-emitter: 0.5.1 - tough-cookie: 6.0.1 + tough-cookie: 6.0.2 type-fest: 5.7.0 until-async: 3.0.2 yargs: 17.7.2 @@ -20488,7 +20863,7 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 - oxfmt@0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + oxfmt@0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -20511,7 +20886,32 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.64.0 '@oxfmt/binding-win32-ia32-msvc': 0.64.0 '@oxfmt/binding-win32-x64-msvc': 0.64.0 - vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + oxfmt@0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.64.0 + '@oxfmt/binding-android-arm64': 0.64.0 + '@oxfmt/binding-darwin-arm64': 0.64.0 + '@oxfmt/binding-darwin-x64': 0.64.0 + '@oxfmt/binding-freebsd-x64': 0.64.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.64.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.64.0 + '@oxfmt/binding-linux-arm64-gnu': 0.64.0 + '@oxfmt/binding-linux-arm64-musl': 0.64.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.64.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.64.0 + '@oxfmt/binding-linux-riscv64-musl': 0.64.0 + '@oxfmt/binding-linux-s390x-gnu': 0.64.0 + '@oxfmt/binding-linux-x64-gnu': 0.64.0 + '@oxfmt/binding-linux-x64-musl': 0.64.0 + '@oxfmt/binding-openharmony-arm64': 0.64.0 + '@oxfmt/binding-win32-arm64-msvc': 0.64.0 + '@oxfmt/binding-win32-ia32-msvc': 0.64.0 + '@oxfmt/binding-win32-x64-msvc': 0.64.0 + vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) oxlint-tsgolint@7.0.2001: optionalDependencies: @@ -20522,7 +20922,31 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.79.0 + '@oxlint/binding-android-arm64': 1.79.0 + '@oxlint/binding-darwin-arm64': 1.79.0 + '@oxlint/binding-darwin-x64': 1.79.0 + '@oxlint/binding-freebsd-x64': 1.79.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.79.0 + '@oxlint/binding-linux-arm-musleabihf': 1.79.0 + '@oxlint/binding-linux-arm64-gnu': 1.79.0 + '@oxlint/binding-linux-arm64-musl': 1.79.0 + '@oxlint/binding-linux-ppc64-gnu': 1.79.0 + '@oxlint/binding-linux-riscv64-gnu': 1.79.0 + '@oxlint/binding-linux-riscv64-musl': 1.79.0 + '@oxlint/binding-linux-s390x-gnu': 1.79.0 + '@oxlint/binding-linux-x64-gnu': 1.79.0 + '@oxlint/binding-linux-x64-musl': 1.79.0 + '@oxlint/binding-openharmony-arm64': 1.79.0 + '@oxlint/binding-win32-arm64-msvc': 1.79.0 + '@oxlint/binding-win32-ia32-msvc': 1.79.0 + '@oxlint/binding-win32-x64-msvc': 1.79.0 + oxlint-tsgolint: 7.0.2001 + vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.79.0 '@oxlint/binding-android-arm64': 1.79.0 @@ -20544,7 +20968,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.79.0 '@oxlint/binding-win32-x64-msvc': 1.79.0 oxlint-tsgolint: 7.0.2001 - vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) p-cancelable@2.1.1: {} @@ -20607,6 +21031,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.1: + dependencies: + entities: 8.1.0 + parseurl@1.3.3: {} patch-console@2.0.0: {} @@ -20625,7 +21053,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 minipass: 7.1.3 path-to-regexp@6.1.0: {} @@ -20841,8 +21269,7 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - punycode@2.3.1: - optional: true + punycode@2.3.1: {} pure-rand@8.4.0: {} @@ -21586,6 +22013,10 @@ snapshots: sax@1.6.0: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} schema-utils@4.3.3: @@ -21880,6 +22311,12 @@ snapshots: memory-pager: 1.5.0 optional: true + speech-rule-engine@4.1.4: + dependencies: + '@xmldom/xmldom': 0.9.10 + commander: 13.1.0 + wicked-good-xpath: 1.3.0 + split-on-first@1.1.0: {} split2@4.2.0: {} @@ -22045,6 +22482,8 @@ snapshots: transitivePeerDependencies: - encoding + symbol-tree@3.2.4: {} + tabbable@6.4.0: {} tagged-tag@1.0.0: {} @@ -22116,13 +22555,11 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@7.4.2: - optional: true + tldts-core@7.4.2: {} tldts@7.4.2: dependencies: tldts-core: 7.4.2 - optional: true tmp-promise@3.0.3: dependencies: @@ -22142,10 +22579,9 @@ snapshots: totalist@3.0.1: {} - tough-cookie@6.0.1: + tough-cookie@6.0.2: dependencies: tldts: 7.4.2 - optional: true tr46@0.0.3: {} @@ -22154,6 +22590,10 @@ snapshots: punycode: 2.3.1 optional: true + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -22228,6 +22668,8 @@ snapshots: undici-types@7.16.0: {} + undici-types@8.10.2: {} + undici@6.28.0: {} undici@7.27.1: {} @@ -22459,7 +22901,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): + vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.146.0 '@oxlint/plugins': 1.79.0 @@ -22473,11 +22915,69 @@ snapshots: '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0) - oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) oxlint-tsgolint: 7.0.2001 vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' - vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.3.0 + '@voidzero-dev/vite-plus-darwin-x64': 0.3.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.3.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.3.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.3.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.3.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.3.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.3.0 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - yaml + + vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.146.0 + '@oxlint/plugins': 1.79.0 + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11) + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11) + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0) + oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2001 + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.3.0 '@voidzero-dev/vite-plus-darwin-x64': 0.3.0 @@ -22521,7 +23021,36 @@ snapshots: optionalDependencies: vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest@4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)): + vitest@4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.4 + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11) + jsdom: 30.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - msw + + vitest@4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)): dependencies: '@vitest/expect': 4.1.11 '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) @@ -22546,6 +23075,7 @@ snapshots: optionalDependencies: '@types/node': 24.12.4 '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@4.1.11) + jsdom: 30.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw @@ -22648,6 +23178,10 @@ snapshots: vscode-uri@3.1.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} walker@1.0.8: @@ -22675,10 +23209,14 @@ snapshots: webidl-conversions@7.0.0: optional: true + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} whatwg-fetch@3.6.20: {} + whatwg-mimetype@5.0.0: {} + whatwg-url-minimum@0.1.2: {} whatwg-url@14.2.0: @@ -22687,6 +23225,38 @@ snapshots: webidl-conversions: 7.0.0 optional: true + whatwg-url@16.0.1(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@16.0.1(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.1(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.1(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -22711,6 +23281,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wicked-good-xpath@1.3.0: {} + widest-line@6.0.0: dependencies: string-width: 8.2.1 @@ -22764,6 +23336,8 @@ snapshots: simple-plist: 1.3.1 uuid: 7.0.3 + xml-name-validator@5.0.0: {} + xml-naming@0.1.0: {} xml2js@0.4.23: @@ -22780,6 +23354,8 @@ snapshots: xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} + xtend@4.0.2: {} xxhash-wasm@1.1.0: {} diff --git a/third-party-licenses.config.json b/third-party-licenses.config.json index 01955554adf6..ea3bc07cfedb 100644 --- a/third-party-licenses.config.json +++ b/third-party-licenses.config.json @@ -152,6 +152,13 @@ } ], "packageOverrides": [ + { + "name": "mj-context-menu", + "sourceUrl": "https://github.com/zorkow/context-menu", + "generatedNotice": { + "licenseId": "Apache-2.0" + } + }, { "license": "MIT", "name": "@react-grab/cli",