| author | |
| committer | |
| log | 2980a853e567d97b9e27a9409ecd98a60c4ae42a |
| tree | be5e58388f1946a1aaa2e456e3f04e8ef9b44ce5 |
| parent | 56de1465fa81eadc3c09140f7fff1579299e3bad |
- Replace `rehype-highlight`/`highlight.js` with `@pierre/diffs` highlighter in `ChatMarkdown`
- Add LRU caching and in-flight dedupe for highlighted code, with streaming-aware behavior
- Extract shared diff theme resolution utility and add unit tests for new LRU cache10 files changed, 343 insertions(+), 36 deletions(-)
apps/web/package.json-2| ... | ... | @@ -22,12 +22,10 @@ |
| 22 | 22 | "@xterm/xterm": "^6.0.0", |
| 23 | 23 | "class-variance-authority": "^0.7.1", |
| 24 | 24 | "effect": "catalog:", |
| 25 | "highlight.js": "^11.11.1", | |
| 26 | 25 | "lucide-react": "^0.564.0", |
| 27 | 26 | "react": "^19.0.0", |
| 28 | 27 | "react-dom": "^19.0.0", |
| 29 | 28 | "react-markdown": "^10.1.0", |
| 30 | "rehype-highlight": "^7.0.2", | |
| 31 | 29 | "remark-gfm": "^4.0.1", |
| 32 | 30 | "tailwind-merge": "^3.4.0" |
| 33 | 31 | }, |
apps/web/src/components/ChatMarkdown.tsx+218-9| ... | ... | @@ -1,18 +1,215 @@ |
| 1 | import { memo, useMemo } from "react"; | |
| 1 | import { getSharedHighlighter } from "@pierre/diffs"; | |
| 2 | import { | |
| 3 | Children, | |
| 4 | isValidElement, | |
| 5 | memo, | |
| 6 | useEffect, | |
| 7 | useMemo, | |
| 8 | useState, | |
| 9 | type ReactNode, | |
| 10 | } from "react"; | |
| 2 | 11 | import type { Components } from "react-markdown"; |
| 3 | 12 | import ReactMarkdown from "react-markdown"; |
| 4 | import rehypeHighlight from "rehype-highlight"; | |
| 5 | 13 | import remarkGfm from "remark-gfm"; |
| 14 | import { LRUCache } from "../lib/lruCache"; | |
| 6 | 15 | import { readNativeApi } from "../nativeApi"; |
| 16 | import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffThemes"; | |
| 17 | import { useTheme } from "../hooks/useTheme"; | |
| 7 | 18 | import { resolveMarkdownFileLinkTarget } from "../markdown-links"; |
| 8 | 19 | import { preferredTerminalEditor } from "../terminal-links"; |
| 9 | 20 | |
| 10 | 21 | interface ChatMarkdownProps { |
| 11 | 22 | text: string; |
| 12 | 23 | cwd: string | undefined; |
| 24 | isStreaming?: boolean; | |
| 13 | 25 | } |
| 14 | 26 | |
| 15 | function ChatMarkdown({ text, cwd }: ChatMarkdownProps) { | |
| 27 | const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/; | |
| 28 | const MAX_HIGHLIGHT_CACHE_ENTRIES = 500; | |
| 29 | const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024; | |
| 30 | const highlightedCodeCache = new LRUCache<string>( | |
| 31 | MAX_HIGHLIGHT_CACHE_ENTRIES, | |
| 32 | MAX_HIGHLIGHT_CACHE_MEMORY_BYTES, | |
| 33 | ); | |
| 34 | const inFlightHighlights = new Map<string, Promise<string>>(); | |
| 35 | ||
| 36 | function hashString(input: string): string { | |
| 37 | // FNV-1a 32-bit hash. | |
| 38 | let hash = 0x811c9dc5; | |
| 39 | for (let index = 0; index < input.length; index += 1) { | |
| 40 | hash ^= input.charCodeAt(index); | |
| 41 | hash = Math.imul(hash, 0x01000193); | |
| 42 | } | |
| 43 | return (hash >>> 0).toString(36); | |
| 44 | } | |
| 45 | ||
| 46 | function extractFenceLanguage(className: string | undefined): string { | |
| 47 | const match = className?.match(CODE_FENCE_LANGUAGE_REGEX); | |
| 48 | return match?.[1] ?? "text"; | |
| 49 | } | |
| 50 | ||
| 51 | function nodeToPlainText(node: ReactNode): string { | |
| 52 | if (typeof node === "string" || typeof node === "number") { | |
| 53 | return String(node); | |
| 54 | } | |
| 55 | if (Array.isArray(node)) { | |
| 56 | return node.map((child) => nodeToPlainText(child)).join(""); | |
| 57 | } | |
| 58 | if (isValidElement<{ children?: ReactNode }>(node)) { | |
| 59 | return nodeToPlainText(node.props.children); | |
| 60 | } | |
| 61 | return ""; | |
| 62 | } | |
| 63 | ||
| 64 | function extractCodeBlock( | |
| 65 | children: ReactNode, | |
| 66 | ): { className: string | undefined; code: string } | null { | |
| 67 | const childNodes = Children.toArray(children); | |
| 68 | if (childNodes.length !== 1) { | |
| 69 | return null; | |
| 70 | } | |
| 71 | ||
| 72 | const onlyChild = childNodes[0]; | |
| 73 | if ( | |
| 74 | !isValidElement<{ className?: string; children?: ReactNode }>(onlyChild) || | |
| 75 | onlyChild.type !== "code" | |
| 76 | ) { | |
| 77 | return null; | |
| 78 | } | |
| 79 | ||
| 80 | return { | |
| 81 | className: onlyChild.props.className, | |
| 82 | code: nodeToPlainText(onlyChild.props.children), | |
| 83 | }; | |
| 84 | } | |
| 85 | ||
| 86 | async function highlightCodeBlock(code: string, language: string, themeName: DiffThemeName) { | |
| 87 | try { | |
| 88 | const highlighter = await getSharedHighlighter({ | |
| 89 | themes: [themeName], | |
| 90 | langs: [language], | |
| 91 | }); | |
| 92 | return highlighter.codeToHtml(code, { lang: language, theme: themeName }); | |
| 93 | } catch { | |
| 94 | const highlighter = await getSharedHighlighter({ | |
| 95 | themes: [themeName], | |
| 96 | langs: ["text"], | |
| 97 | }); | |
| 98 | return highlighter.codeToHtml(code, { lang: "text", theme: themeName }); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | function createHighlightCacheKey(code: string, language: string, themeName: DiffThemeName): string { | |
| 103 | return `${hashString(code)}:${code.length}:${language}:${themeName}`; | |
| 104 | } | |
| 105 | ||
| 106 | function estimateHighlightedSize(html: string, code: string): number { | |
| 107 | return Math.max(html.length * 2, code.length * 3); | |
| 108 | } | |
| 109 | ||
| 110 | function getCachedOrCreateHighlight({ | |
| 111 | cacheKey, | |
| 112 | code, | |
| 113 | language, | |
| 114 | themeName, | |
| 115 | useCache, | |
| 116 | }: { | |
| 117 | cacheKey: string; | |
| 118 | code: string; | |
| 119 | language: string; | |
| 120 | themeName: DiffThemeName; | |
| 121 | useCache: boolean; | |
| 122 | }): Promise<string> { | |
| 123 | if (useCache) { | |
| 124 | const cached = highlightedCodeCache.get(cacheKey); | |
| 125 | if (cached) { | |
| 126 | return Promise.resolve(cached); | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | const existingInFlight = inFlightHighlights.get(cacheKey); | |
| 131 | if (existingInFlight) { | |
| 132 | return existingInFlight; | |
| 133 | } | |
| 134 | ||
| 135 | const request = highlightCodeBlock(code, language, themeName) | |
| 136 | .then((html) => { | |
| 137 | if (useCache) { | |
| 138 | highlightedCodeCache.set(cacheKey, html, estimateHighlightedSize(html, code)); | |
| 139 | } | |
| 140 | return html; | |
| 141 | }) | |
| 142 | .finally(() => { | |
| 143 | inFlightHighlights.delete(cacheKey); | |
| 144 | }); | |
| 145 | ||
| 146 | inFlightHighlights.set(cacheKey, request); | |
| 147 | return request; | |
| 148 | } | |
| 149 | ||
| 150 | interface ShikiCodeBlockProps { | |
| 151 | className: string | undefined; | |
| 152 | code: string; | |
| 153 | themeName: DiffThemeName; | |
| 154 | isStreaming: boolean; | |
| 155 | fallback: ReactNode; | |
| 156 | } | |
| 157 | ||
| 158 | function ShikiCodeBlock({ className, code, themeName, isStreaming, fallback }: ShikiCodeBlockProps) { | |
| 159 | const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null); | |
| 160 | ||
| 161 | useEffect(() => { | |
| 162 | let cancelled = false; | |
| 163 | const language = extractFenceLanguage(className); | |
| 164 | const cacheKey = createHighlightCacheKey(code, language, themeName); | |
| 165 | ||
| 166 | if (!isStreaming) { | |
| 167 | const cached = highlightedCodeCache.get(cacheKey); | |
| 168 | if (cached) { | |
| 169 | setHighlightedHtml(cached); | |
| 170 | return () => { | |
| 171 | cancelled = true; | |
| 172 | }; | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | setHighlightedHtml(null); | |
| 177 | void getCachedOrCreateHighlight({ | |
| 178 | cacheKey, | |
| 179 | code, | |
| 180 | language, | |
| 181 | themeName, | |
| 182 | useCache: !isStreaming, | |
| 183 | }) | |
| 184 | .then((html) => { | |
| 185 | if (cancelled) { | |
| 186 | return; | |
| 187 | } | |
| 188 | setHighlightedHtml(html); | |
| 189 | }) | |
| 190 | .catch(() => { | |
| 191 | if (!cancelled) { | |
| 192 | setHighlightedHtml(null); | |
| 193 | } | |
| 194 | }); | |
| 195 | ||
| 196 | return () => { | |
| 197 | cancelled = true; | |
| 198 | }; | |
| 199 | }, [className, code, themeName, isStreaming]); | |
| 200 | ||
| 201 | if (!highlightedHtml) { | |
| 202 | return fallback; | |
| 203 | } | |
| 204 | ||
| 205 | return ( | |
| 206 | <div className="chat-markdown-shiki" dangerouslySetInnerHTML={{ __html: highlightedHtml }} /> | |
| 207 | ); | |
| 208 | } | |
| 209 | ||
| 210 | function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) { | |
| 211 | const { resolvedTheme } = useTheme(); | |
| 212 | const diffThemeName = resolveDiffThemeName(resolvedTheme); | |
| 16 | 213 | const markdownComponents = useMemo<Components>( |
| 17 | 214 | () => ({ |
| 18 | 215 | a({ node: _node, href, ...props }) { |
| ... | ... | @@ -38,17 +235,29 @@ function ChatMarkdown({ text, cwd }: ChatMarkdownProps) { |
| 38 | 235 | /> |
| 39 | 236 | ); |
| 40 | 237 | }, |
| 238 | pre({ node: _node, children, ...props }) { | |
| 239 | const codeBlock = extractCodeBlock(children); | |
| 240 | if (!codeBlock) { | |
| 241 | return <pre {...props}>{children}</pre>; | |
| 242 | } | |
| 243 | ||
| 244 | return ( | |
| 245 | <ShikiCodeBlock | |
| 246 | className={codeBlock.className} | |
| 247 | code={codeBlock.code} | |
| 248 | themeName={diffThemeName} | |
| 249 | isStreaming={isStreaming} | |
| 250 | fallback={<pre {...props}>{children}</pre>} | |
| 251 | /> | |
| 252 | ); | |
| 253 | }, | |
| 41 | 254 | }), |
| 42 | [cwd], | |
| 255 | [cwd, diffThemeName, isStreaming], | |
| 43 | 256 | ); |
| 44 | 257 | |
| 45 | 258 | return ( |
| 46 | 259 | <div className="chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80"> |
| 47 | <ReactMarkdown | |
| 48 | remarkPlugins={[remarkGfm]} | |
| 49 | rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]} | |
| 50 | components={markdownComponents} | |
| 51 | > | |
| 260 | <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}> | |
| 52 | 261 | {text} |
| 53 | 262 | </ReactMarkdown> |
| 54 | 263 | </div> |
apps/web/src/components/ChatView.tsx+5-1| ... | ... | @@ -2767,7 +2767,11 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2767 | 2767 | </div> |
| 2768 | 2768 | )} |
| 2769 | 2769 | <div className="min-w-0 px-1 py-0.5"> |
| 2770 | <ChatMarkdown text={messageText} cwd={markdownCwd} /> | |
| 2770 | <ChatMarkdown | |
| 2771 | text={messageText} | |
| 2772 | cwd={markdownCwd} | |
| 2773 | isStreaming={Boolean(row.message.streaming)} | |
| 2774 | /> | |
| 2771 | 2775 | {(() => { |
| 2772 | 2776 | const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id); |
| 2773 | 2777 | if (!turnSummary) return null; |
apps/web/src/components/DiffPanel.tsx+1-4| ... | ... | @@ -11,6 +11,7 @@ import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch" |
| 11 | 11 | import { isElectron } from "../env"; |
| 12 | 12 | import { useTheme } from "../hooks/useTheme"; |
| 13 | 13 | import { buildPatchCacheKey } from "../lib/diffRendering"; |
| 14 | import { resolveDiffThemeName } from "../lib/diffThemes"; | |
| 14 | 15 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 15 | 16 | import { useStore } from "../store"; |
| 16 | 17 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| ... | ... | @@ -18,10 +19,6 @@ import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| 18 | 19 | type DiffRenderMode = "stacked" | "split"; |
| 19 | 20 | type DiffThemeType = "light" | "dark"; |
| 20 | 21 | |
| 21 | function resolveDiffThemeName(theme: "light" | "dark") { | |
| 22 | return theme === "dark" ? "pierre-dark" : "pierre-light"; | |
| 23 | } | |
| 24 | ||
| 25 | 22 | type RenderablePatch = |
| 26 | 23 | | { |
| 27 | 24 | kind: "files"; |
apps/web/src/components/DiffWorkerPoolProvider.tsx+2-5| ... | ... | @@ -2,12 +2,9 @@ import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; |
| 2 | 2 | import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; |
| 3 | 3 | import { useEffect, useMemo, type ReactNode } from "react"; |
| 4 | 4 | import { useTheme } from "../hooks/useTheme"; |
| 5 | import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffThemes"; | |
| 5 | 6 | |
| 6 | function resolveDiffThemeName(theme: "light" | "dark") { | |
| 7 | return theme === "dark" ? "pierre-dark" : "pierre-light"; | |
| 8 | } | |
| 9 | ||
| 10 | function DiffWorkerThemeSync({ themeName }: { themeName: "pierre-light" | "pierre-dark" }) { | |
| 7 | function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) { | |
| 11 | 8 | const workerPool = useWorkerPool(); |
| 12 | 9 | |
| 13 | 10 | useEffect(() => { |
apps/web/src/lib/diffThemes.ts created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | export const DIFF_THEME_NAMES = { | |
| 2 | light: "pierre-light", | |
| 3 | dark: "pierre-dark", | |
| 4 | } as const; | |
| 5 | ||
| 6 | export type DiffThemeName = (typeof DIFF_THEME_NAMES)[keyof typeof DIFF_THEME_NAMES]; | |
| 7 | ||
| 8 | export function resolveDiffThemeName(theme: "light" | "dark"): DiffThemeName { | |
| 9 | return theme === "dark" ? DIFF_THEME_NAMES.dark : DIFF_THEME_NAMES.light; | |
| 10 | } |
apps/web/src/lib/lruCache.test.ts created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | import { describe, expect, it } from "vitest"; | |
| 2 | import { LRUCache } from "./lruCache"; | |
| 3 | ||
| 4 | describe("LRUCache", () => { | |
| 5 | it("returns null for missing keys", () => { | |
| 6 | const cache = new LRUCache<string>(2, 100); | |
| 7 | expect(cache.get("missing")).toBeNull(); | |
| 8 | }); | |
| 9 | ||
| 10 | it("evicts oldest by max entries", () => { | |
| 11 | const cache = new LRUCache<string>(2, 1_000); | |
| 12 | cache.set("a", "A", 10); | |
| 13 | cache.set("b", "B", 10); | |
| 14 | cache.set("c", "C", 10); | |
| 15 | ||
| 16 | expect(cache.get("a")).toBeNull(); | |
| 17 | expect(cache.get("b")).toBe("B"); | |
| 18 | expect(cache.get("c")).toBe("C"); | |
| 19 | }); | |
| 20 | ||
| 21 | it("promotes on get and evicts least recently used", () => { | |
| 22 | const cache = new LRUCache<string>(2, 1_000); | |
| 23 | cache.set("a", "A", 10); | |
| 24 | cache.set("b", "B", 10); | |
| 25 | expect(cache.get("a")).toBe("A"); | |
| 26 | ||
| 27 | cache.set("c", "C", 10); | |
| 28 | expect(cache.get("a")).toBe("A"); | |
| 29 | expect(cache.get("b")).toBeNull(); | |
| 30 | expect(cache.get("c")).toBe("C"); | |
| 31 | }); | |
| 32 | ||
| 33 | it("evicts by memory budget", () => { | |
| 34 | const cache = new LRUCache<string>(10, 25); | |
| 35 | cache.set("a", "A", 10); | |
| 36 | cache.set("b", "B", 10); | |
| 37 | cache.set("c", "C", 10); | |
| 38 | ||
| 39 | expect(cache.get("a")).toBeNull(); | |
| 40 | expect(cache.get("b")).toBe("B"); | |
| 41 | expect(cache.get("c")).toBe("C"); | |
| 42 | }); | |
| 43 | }); |
apps/web/src/lib/lruCache.ts created+64| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | interface CacheEntry<T> { | |
| 2 | value: T; | |
| 3 | approximateSize: number; | |
| 4 | } | |
| 5 | ||
| 6 | export class LRUCache<T> { | |
| 7 | private cache = new Map<string, CacheEntry<T>>(); | |
| 8 | private totalSize = 0; | |
| 9 | ||
| 10 | constructor( | |
| 11 | private readonly maxEntries: number, | |
| 12 | private readonly maxMemoryBytes: number, | |
| 13 | ) {} | |
| 14 | ||
| 15 | get(key: string): T | null { | |
| 16 | const entry = this.cache.get(key); | |
| 17 | if (!entry) { | |
| 18 | return null; | |
| 19 | } | |
| 20 | ||
| 21 | this.promote(key, entry); | |
| 22 | return entry.value; | |
| 23 | } | |
| 24 | ||
| 25 | set(key: string, value: T, approximateSize: number): void { | |
| 26 | const existing = this.cache.get(key); | |
| 27 | if (existing) { | |
| 28 | this.totalSize -= existing.approximateSize; | |
| 29 | this.cache.delete(key); | |
| 30 | } | |
| 31 | ||
| 32 | this.evictIfNeeded(approximateSize); | |
| 33 | this.cache.set(key, { value, approximateSize }); | |
| 34 | this.totalSize += approximateSize; | |
| 35 | } | |
| 36 | ||
| 37 | clear(): void { | |
| 38 | this.cache.clear(); | |
| 39 | this.totalSize = 0; | |
| 40 | } | |
| 41 | ||
| 42 | private promote(key: string, entry: CacheEntry<T>): void { | |
| 43 | this.cache.delete(key); | |
| 44 | this.cache.set(key, entry); | |
| 45 | } | |
| 46 | ||
| 47 | private evictIfNeeded(incomingSize: number): void { | |
| 48 | while ( | |
| 49 | (this.cache.size >= this.maxEntries || this.totalSize + incomingSize > this.maxMemoryBytes) && | |
| 50 | this.cache.size > 0 | |
| 51 | ) { | |
| 52 | const oldestKey = this.cache.keys().next().value; | |
| 53 | if (oldestKey === undefined) { | |
| 54 | break; | |
| 55 | } | |
| 56 | ||
| 57 | const oldestEntry = this.cache.get(oldestKey); | |
| 58 | if (oldestEntry) { | |
| 59 | this.totalSize -= oldestEntry.approximateSize; | |
| 60 | } | |
| 61 | this.cache.delete(oldestKey); | |
| 62 | } | |
| 63 | } | |
| 64 | } |
apps/web/src/main.tsx-1| ... | ... | @@ -6,7 +6,6 @@ import { createHashHistory, createRouter, createBrowserHistory } from "@tanstack |
| 6 | 6 | import { StoreProvider } from "./store"; |
| 7 | 7 | |
| 8 | 8 | import "@xterm/xterm/css/xterm.css"; |
| 9 | import "highlight.js/styles/github-dark.css"; | |
| 10 | 9 | import "./index.css"; |
| 11 | 10 | |
| 12 | 11 | import { APP_DISPLAY_NAME } from "./branding"; |
bun.lock-14| ... | ... | @@ -71,12 +71,10 @@ |
| 71 | 71 | "@xterm/xterm": "^6.0.0", |
| 72 | 72 | "class-variance-authority": "^0.7.1", |
| 73 | 73 | "effect": "catalog:", |
| 74 | "highlight.js": "^11.11.1", | |
| 75 | 74 | "lucide-react": "^0.564.0", |
| 76 | 75 | "react": "^19.0.0", |
| 77 | 76 | "react-dom": "^19.0.0", |
| 78 | 77 | "react-markdown": "^10.1.0", |
| 79 | "rehype-highlight": "^7.0.2", | |
| 80 | 78 | "remark-gfm": "^4.0.1", |
| 81 | 79 | "tailwind-merge": "^3.4.0", |
| 82 | 80 | }, |
| ... | ... | @@ -863,20 +861,14 @@ |
| 863 | 861 | |
| 864 | 862 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], |
| 865 | 863 | |
| 866 | "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], | |
| 867 | ||
| 868 | 864 | "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], |
| 869 | 865 | |
| 870 | 866 | "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], |
| 871 | 867 | |
| 872 | "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], | |
| 873 | ||
| 874 | 868 | "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], |
| 875 | 869 | |
| 876 | 870 | "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], |
| 877 | 871 | |
| 878 | "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], | |
| 879 | ||
| 880 | 872 | "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], |
| 881 | 873 | |
| 882 | 874 | "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], |
| ... | ... | @@ -983,8 +975,6 @@ |
| 983 | 975 | |
| 984 | 976 | "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], |
| 985 | 977 | |
| 986 | "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], | |
| 987 | ||
| 988 | 978 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 989 | 979 | |
| 990 | 980 | "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], |
| ... | ... | @@ -1197,8 +1187,6 @@ |
| 1197 | 1187 | |
| 1198 | 1188 | "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], |
| 1199 | 1189 | |
| 1200 | "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], | |
| 1201 | ||
| 1202 | 1190 | "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], |
| 1203 | 1191 | |
| 1204 | 1192 | "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], |
| ... | ... | @@ -1361,8 +1349,6 @@ |
| 1361 | 1349 | |
| 1362 | 1350 | "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], |
| 1363 | 1351 | |
| 1364 | "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], | |
| 1365 | ||
| 1366 | 1352 | "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], |
| 1367 | 1353 | |
| 1368 | 1354 | "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], |