authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 01:06:06-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-28 15:17:03-08:00
log2980a853e567d97b9e27a9409ecd98a60c4ae42a
treebe5e58388f1946a1aaa2e456e3f04e8ef9b44ce5
parent56de1465fa81eadc3c09140f7fff1579299e3bad

Use shared Shiki highlighting for chat markdown code blocks

- 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 cache

10 files changed, 343 insertions(+), 36 deletions(-)

apps/web/package.json-2
...@@ -22,12 +22,10 @@...@@ -22,12 +22,10 @@
22 "@xterm/xterm": "^6.0.0",22 "@xterm/xterm": "^6.0.0",
23 "class-variance-authority": "^0.7.1",23 "class-variance-authority": "^0.7.1",
24 "effect": "catalog:",24 "effect": "catalog:",
25 "highlight.js": "^11.11.1",
26 "lucide-react": "^0.564.0",25 "lucide-react": "^0.564.0",
27 "react": "^19.0.0",26 "react": "^19.0.0",
28 "react-dom": "^19.0.0",27 "react-dom": "^19.0.0",
29 "react-markdown": "^10.1.0",28 "react-markdown": "^10.1.0",
30 "rehype-highlight": "^7.0.2",
31 "remark-gfm": "^4.0.1",29 "remark-gfm": "^4.0.1",
32 "tailwind-merge": "^3.4.0"30 "tailwind-merge": "^3.4.0"
33 },31 },
apps/web/src/components/ChatMarkdown.tsx+218-9
...@@ -1,18 +1,215 @@...@@ -1,18 +1,215 @@
1import { memo, useMemo } from "react";1import { getSharedHighlighter } from "@pierre/diffs";
2import {
3 Children,
4 isValidElement,
5 memo,
6 useEffect,
7 useMemo,
8 useState,
9 type ReactNode,
10} from "react";
2import type { Components } from "react-markdown";11import type { Components } from "react-markdown";
3import ReactMarkdown from "react-markdown";12import ReactMarkdown from "react-markdown";
4import rehypeHighlight from "rehype-highlight";
5import remarkGfm from "remark-gfm";13import remarkGfm from "remark-gfm";
14import { LRUCache } from "../lib/lruCache";
6import { readNativeApi } from "../nativeApi";15import { readNativeApi } from "../nativeApi";
16import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffThemes";
17import { useTheme } from "../hooks/useTheme";
7import { resolveMarkdownFileLinkTarget } from "../markdown-links";18import { resolveMarkdownFileLinkTarget } from "../markdown-links";
8import { preferredTerminalEditor } from "../terminal-links";19import { preferredTerminalEditor } from "../terminal-links";
920
10interface ChatMarkdownProps {21interface ChatMarkdownProps {
11 text: string;22 text: string;
12 cwd: string | undefined;23 cwd: string | undefined;
24 isStreaming?: boolean;
13}25}
1426
15function ChatMarkdown({ text, cwd }: ChatMarkdownProps) {27const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
28const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
29const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;
30const highlightedCodeCache = new LRUCache<string>(
31 MAX_HIGHLIGHT_CACHE_ENTRIES,
32 MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,
33);
34const inFlightHighlights = new Map<string, Promise<string>>();
35
36function 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
46function extractFenceLanguage(className: string | undefined): string {
47 const match = className?.match(CODE_FENCE_LANGUAGE_REGEX);
48 return match?.[1] ?? "text";
49}
50
51function 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
64function 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
86async 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
102function createHighlightCacheKey(code: string, language: string, themeName: DiffThemeName): string {
103 return `${hashString(code)}:${code.length}:${language}:${themeName}`;
104}
105
106function estimateHighlightedSize(html: string, code: string): number {
107 return Math.max(html.length * 2, code.length * 3);
108}
109
110function 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
150interface ShikiCodeBlockProps {
151 className: string | undefined;
152 code: string;
153 themeName: DiffThemeName;
154 isStreaming: boolean;
155 fallback: ReactNode;
156}
157
158function 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
210function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
211 const { resolvedTheme } = useTheme();
212 const diffThemeName = resolveDiffThemeName(resolvedTheme);
16 const markdownComponents = useMemo<Components>(213 const markdownComponents = useMemo<Components>(
17 () => ({214 () => ({
18 a({ node: _node, href, ...props }) {215 a({ node: _node, href, ...props }) {
...@@ -38,17 +235,29 @@ function ChatMarkdown({ text, cwd }: ChatMarkdownProps) {...@@ -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 );
44257
45 return (258 return (
46 <div className="chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80">259 <div className="chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80">
47 <ReactMarkdown260 <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
48 remarkPlugins={[remarkGfm]}
49 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}
50 components={markdownComponents}
51 >
52 {text}261 {text}
53 </ReactMarkdown>262 </ReactMarkdown>
54 </div>263 </div>
apps/web/src/components/ChatView.tsx+5-1
...@@ -2767,7 +2767,11 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2767,7 +2767,11 @@ const MessagesTimeline = memo(function MessagesTimeline({
2767 </div>2767 </div>
2768 )}2768 )}
2769 <div className="min-w-0 px-1 py-0.5">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 const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id);2776 const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id);
2773 if (!turnSummary) return null;2777 if (!turnSummary) return null;
apps/web/src/components/DiffPanel.tsx+1-4
...@@ -11,6 +11,7 @@ import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"...@@ -11,6 +11,7 @@ import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"
11import { isElectron } from "../env";11import { isElectron } from "../env";
12import { useTheme } from "../hooks/useTheme";12import { useTheme } from "../hooks/useTheme";
13import { buildPatchCacheKey } from "../lib/diffRendering";13import { buildPatchCacheKey } from "../lib/diffRendering";
14import { resolveDiffThemeName } from "../lib/diffThemes";
14import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";15import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
15import { useStore } from "../store";16import { useStore } from "../store";
16import { ToggleGroup, Toggle } from "./ui/toggle-group";17import { ToggleGroup, Toggle } from "./ui/toggle-group";
...@@ -18,10 +19,6 @@ import { ToggleGroup, Toggle } from "./ui/toggle-group";...@@ -18,10 +19,6 @@ import { ToggleGroup, Toggle } from "./ui/toggle-group";
18type DiffRenderMode = "stacked" | "split";19type DiffRenderMode = "stacked" | "split";
19type DiffThemeType = "light" | "dark";20type DiffThemeType = "light" | "dark";
2021
21function resolveDiffThemeName(theme: "light" | "dark") {
22 return theme === "dark" ? "pierre-dark" : "pierre-light";
23}
24
25type RenderablePatch =22type RenderablePatch =
26 | {23 | {
27 kind: "files";24 kind: "files";
apps/web/src/components/DiffWorkerPoolProvider.tsx+2-5
...@@ -2,12 +2,9 @@ import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react";...@@ -2,12 +2,9 @@ import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react";
2import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";2import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";
3import { useEffect, useMemo, type ReactNode } from "react";3import { useEffect, useMemo, type ReactNode } from "react";
4import { useTheme } from "../hooks/useTheme";4import { useTheme } from "../hooks/useTheme";
5import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffThemes";
56
6function resolveDiffThemeName(theme: "light" | "dark") {7function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) {
7 return theme === "dark" ? "pierre-dark" : "pierre-light";
8}
9
10function DiffWorkerThemeSync({ themeName }: { themeName: "pierre-light" | "pierre-dark" }) {
11 const workerPool = useWorkerPool();8 const workerPool = useWorkerPool();
129
13 useEffect(() => {10 useEffect(() => {
apps/web/src/lib/diffThemes.ts created+10
...@@ -0,0 +1,10 @@
1export const DIFF_THEME_NAMES = {
2 light: "pierre-light",
3 dark: "pierre-dark",
4} as const;
5
6export type DiffThemeName = (typeof DIFF_THEME_NAMES)[keyof typeof DIFF_THEME_NAMES];
7
8export 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 @@
1import { describe, expect, it } from "vitest";
2import { LRUCache } from "./lruCache";
3
4describe("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 @@
1interface CacheEntry<T> {
2 value: T;
3 approximateSize: number;
4}
5
6export 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,7 +6,6 @@ import { createHashHistory, createRouter, createBrowserHistory } from "@tanstack
6import { StoreProvider } from "./store";6import { StoreProvider } from "./store";
77
8import "@xterm/xterm/css/xterm.css";8import "@xterm/xterm/css/xterm.css";
9import "highlight.js/styles/github-dark.css";
10import "./index.css";9import "./index.css";
1110
12import { APP_DISPLAY_NAME } from "./branding";11import { APP_DISPLAY_NAME } from "./branding";
bun.lock-14
...@@ -71,12 +71,10 @@...@@ -71,12 +71,10 @@
71 "@xterm/xterm": "^6.0.0",71 "@xterm/xterm": "^6.0.0",
72 "class-variance-authority": "^0.7.1",72 "class-variance-authority": "^0.7.1",
73 "effect": "catalog:",73 "effect": "catalog:",
74 "highlight.js": "^11.11.1",
75 "lucide-react": "^0.564.0",74 "lucide-react": "^0.564.0",
76 "react": "^19.0.0",75 "react": "^19.0.0",
77 "react-dom": "^19.0.0",76 "react-dom": "^19.0.0",
78 "react-markdown": "^10.1.0",77 "react-markdown": "^10.1.0",
79 "rehype-highlight": "^7.0.2",
80 "remark-gfm": "^4.0.1",78 "remark-gfm": "^4.0.1",
81 "tailwind-merge": "^3.4.0",79 "tailwind-merge": "^3.4.0",
82 },80 },
...@@ -863,20 +861,14 @@...@@ -863,20 +861,14 @@
863861
864 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],862 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
865863
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 "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=="],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=="],
869865
870 "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=="],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=="],
871867
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 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],868 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
875869
876 "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],870 "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
877871
878 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
879
880 "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],872 "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
881873
882 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],874 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
...@@ -983,8 +975,6 @@...@@ -983,8 +975,6 @@
983975
984 "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],976 "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
985977
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 "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],978 "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
989979
990 "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],980 "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
...@@ -1197,8 +1187,6 @@...@@ -1197,8 +1187,6 @@
11971187
1198 "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],1188 "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
11991189
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 "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=="],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=="],
12031191
1204 "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=="],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,8 +1349,6 @@
13611349
1362 "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=="],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=="],
13631351
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 "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],1352 "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
13671353
1368 "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],1354 "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],