@typefm/react-markdown-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/dist/ErrorBoundary.d.ts +25 -0
  4. package/dist/ErrorBoundary.d.ts.map +1 -0
  5. package/dist/ErrorBoundary.js +29 -0
  6. package/dist/ErrorBoundary.js.map +1 -0
  7. package/dist/MarkdownViewer.d.ts +41 -0
  8. package/dist/MarkdownViewer.d.ts.map +1 -0
  9. package/dist/MarkdownViewer.js +69 -0
  10. package/dist/MarkdownViewer.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +21 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lib/cache-manager.d.ts +59 -0
  16. package/dist/lib/cache-manager.d.ts.map +1 -0
  17. package/dist/lib/cache-manager.js +160 -0
  18. package/dist/lib/cache-manager.js.map +1 -0
  19. package/dist/lib/cursor-controller.d.ts +13 -0
  20. package/dist/lib/cursor-controller.d.ts.map +1 -0
  21. package/dist/lib/cursor-controller.js +93 -0
  22. package/dist/lib/cursor-controller.js.map +1 -0
  23. package/dist/lib/defaults/code-block.d.ts +71 -0
  24. package/dist/lib/defaults/code-block.d.ts.map +1 -0
  25. package/dist/lib/defaults/code-block.js +104 -0
  26. package/dist/lib/defaults/code-block.js.map +1 -0
  27. package/dist/lib/defaults/image.d.ts +41 -0
  28. package/dist/lib/defaults/image.d.ts.map +1 -0
  29. package/dist/lib/defaults/image.js +45 -0
  30. package/dist/lib/defaults/image.js.map +1 -0
  31. package/dist/lib/defaults/link.d.ts +45 -0
  32. package/dist/lib/defaults/link.d.ts.map +1 -0
  33. package/dist/lib/defaults/link.js +76 -0
  34. package/dist/lib/defaults/link.js.map +1 -0
  35. package/dist/lib/defaults/math.d.ts +51 -0
  36. package/dist/lib/defaults/math.d.ts.map +1 -0
  37. package/dist/lib/defaults/math.js +119 -0
  38. package/dist/lib/defaults/math.js.map +1 -0
  39. package/dist/lib/defaults/table.d.ts +18 -0
  40. package/dist/lib/defaults/table.d.ts.map +1 -0
  41. package/dist/lib/defaults/table.js +19 -0
  42. package/dist/lib/defaults/table.js.map +1 -0
  43. package/dist/lib/highlighter.d.ts +81 -0
  44. package/dist/lib/highlighter.d.ts.map +1 -0
  45. package/dist/lib/highlighter.js +421 -0
  46. package/dist/lib/highlighter.js.map +1 -0
  47. package/dist/lib/hook-utils.d.ts +32 -0
  48. package/dist/lib/hook-utils.d.ts.map +1 -0
  49. package/dist/lib/hook-utils.js +42 -0
  50. package/dist/lib/hook-utils.js.map +1 -0
  51. package/dist/lib/html.d.ts +2 -0
  52. package/dist/lib/html.d.ts.map +1 -0
  53. package/dist/lib/html.js +12 -0
  54. package/dist/lib/html.js.map +1 -0
  55. package/dist/lib/morph.d.ts +57 -0
  56. package/dist/lib/morph.d.ts.map +1 -0
  57. package/dist/lib/morph.js +204 -0
  58. package/dist/lib/morph.js.map +1 -0
  59. package/dist/lib/parser.d.ts +32 -0
  60. package/dist/lib/parser.d.ts.map +1 -0
  61. package/dist/lib/parser.js +645 -0
  62. package/dist/lib/parser.js.map +1 -0
  63. package/dist/lib/wasm-init.d.ts +33 -0
  64. package/dist/lib/wasm-init.d.ts.map +1 -0
  65. package/dist/lib/wasm-init.js +69 -0
  66. package/dist/lib/wasm-init.js.map +1 -0
  67. package/dist/styles/alerts.css +294 -0
  68. package/dist/styles/dotted.svg +3 -0
  69. package/dist/styles/hljs.css +332 -0
  70. package/dist/styles/index.css +17 -0
  71. package/dist/styles/katex.css +74 -0
  72. package/dist/styles/viewer.css +975 -0
  73. package/dist/types/hooks.d.ts +207 -0
  74. package/dist/types/hooks.d.ts.map +1 -0
  75. package/dist/types/hooks.js +7 -0
  76. package/dist/types/hooks.js.map +1 -0
  77. package/dist/useMarkdownViewer.d.ts +18 -0
  78. package/dist/useMarkdownViewer.d.ts.map +1 -0
  79. package/dist/useMarkdownViewer.js +403 -0
  80. package/dist/useMarkdownViewer.js.map +1 -0
  81. package/dist/utils.d.ts +20 -0
  82. package/dist/utils.d.ts.map +1 -0
  83. package/dist/utils.js +18 -0
  84. package/dist/utils.js.map +1 -0
  85. package/package.json +78 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Hook type definitions for customizing markdown rendering.
3
+ *
4
+ * @module types/hooks
5
+ */
6
+ /**
7
+ * Hook return type — string HTML or null to use default processor.
8
+ *
9
+ * - `string` → HTML to insert (direct, fast)
10
+ * - `null` → Use the default processor
11
+ *
12
+ * @example
13
+ * // Direct HTML string (recommended)
14
+ * onCodeBlock: ({ code }) => `<pre>${escapeHtml(code)}</pre>`
15
+ *
16
+ * // Use default processor
17
+ * onCodeBlock: () => null
18
+ */
19
+ export type HookResult = string | null;
20
+ /**
21
+ * Data passed to code block hook.
22
+ */
23
+ export interface CodeBlockData {
24
+ /** Raw code content (HTML-decoded) */
25
+ code: string;
26
+ /** Language identifier (normalized, e.g., 'javascript' not 'js') */
27
+ language?: string;
28
+ }
29
+ /**
30
+ * Data passed to inline code hook.
31
+ */
32
+ export interface InlineCodeData {
33
+ /** Raw code content */
34
+ code: string;
35
+ }
36
+ /**
37
+ * Data passed to math hook.
38
+ */
39
+ export interface MathData {
40
+ /** TeX source (HTML-decoded) */
41
+ tex: string;
42
+ /** true for $$...$$ display blocks, false for $...$ inline */
43
+ displayMode: boolean;
44
+ }
45
+ /**
46
+ * Data passed to table hook.
47
+ */
48
+ export interface TableData {
49
+ /** Raw table HTML from parser (sanitized) */
50
+ html: string;
51
+ /** Parsed header cell contents (convenience, extracted from html) */
52
+ headers?: string[];
53
+ /** Parsed row data - array of rows, each row is array of cell contents */
54
+ rows?: string[][];
55
+ }
56
+ /**
57
+ * Data passed to link hook.
58
+ */
59
+ export interface LinkData {
60
+ /** href attribute value */
61
+ href: string;
62
+ /** Link text content */
63
+ text: string;
64
+ /** title attribute value (optional) */
65
+ title?: string;
66
+ }
67
+ /**
68
+ * Data passed to image hook.
69
+ */
70
+ export interface ImageData {
71
+ /** Image source URL */
72
+ src: string;
73
+ /** Alt text */
74
+ alt: string;
75
+ /** Title attribute (optional) */
76
+ title?: string;
77
+ }
78
+ /**
79
+ * Data passed to heading hook.
80
+ */
81
+ export interface HeadingData {
82
+ /** Heading level (1-6) */
83
+ level: 1 | 2 | 3 | 4 | 5 | 6;
84
+ /** Text content of the heading (HTML stripped) */
85
+ text: string;
86
+ /** Generated ID for anchor links (slugified text) */
87
+ id: string;
88
+ /** Raw HTML content (may contain inline formatting) */
89
+ html: string;
90
+ }
91
+ /**
92
+ * Data passed to blockquote hook.
93
+ */
94
+ export interface BlockquoteData {
95
+ /** Inner HTML content of the blockquote */
96
+ content: string;
97
+ }
98
+ /**
99
+ * Alert type for GitHub-style alerts.
100
+ */
101
+ export type AlertType = "note" | "tip" | "important" | "warning" | "caution";
102
+ /**
103
+ * Data passed to alert hook.
104
+ */
105
+ export interface AlertData {
106
+ /** Alert type */
107
+ type: AlertType;
108
+ /** Alert title (e.g., "Note", "Warning") */
109
+ title: string;
110
+ /** Inner HTML content of the alert */
111
+ content: string;
112
+ }
113
+ /**
114
+ * Data passed to list hook.
115
+ */
116
+ export interface ListData {
117
+ /** List type */
118
+ type: "ordered" | "unordered";
119
+ /** Raw HTML of the list */
120
+ html: string;
121
+ /** List items (text content, HTML stripped) */
122
+ items: string[];
123
+ }
124
+ /**
125
+ * Data passed to horizontal rule hook.
126
+ */
127
+ export interface HorizontalRuleData {
128
+ }
129
+ /**
130
+ * Data passed to footnote reference hook.
131
+ */
132
+ export interface FootnoteRefData {
133
+ /** Footnote identifier */
134
+ id: string;
135
+ /** Footnote number (1-based index) */
136
+ index: number;
137
+ }
138
+ /**
139
+ * Data passed to footnote definition hook.
140
+ */
141
+ export interface FootnoteDefData {
142
+ /** Footnote identifier */
143
+ id: string;
144
+ /** Footnote number (1-based index) */
145
+ index: number;
146
+ /** Inner HTML content of the footnote */
147
+ content: string;
148
+ }
149
+ /**
150
+ * Hooks for customizing markdown rendering.
151
+ *
152
+ * Each hook receives element data and can return:
153
+ * - `string` → HTML to insert (fast, recommended)
154
+ * - `null` → Use the default processor
155
+ *
156
+ * @example
157
+ * const hooks: RenderHooks = {
158
+ * // Custom code block
159
+ * onCodeBlock: ({ code, language }) =>
160
+ * `<pre data-lang="${language}"><code>${escapeHtml(code)}</code></pre>`,
161
+ *
162
+ * // Selective override - mermaid gets custom, others use default
163
+ * onCodeBlock: ({ code, language }) => {
164
+ * if (language === 'mermaid') {
165
+ * return `<div class="mermaid">${escapeHtml(code)}</div>`;
166
+ * }
167
+ * return null; // Use default for other languages
168
+ * },
169
+ *
170
+ * // Final transformation (string only)
171
+ * onRender: (html) => html.replace(/TODO/g, '<mark>TODO</mark>'),
172
+ * };
173
+ */
174
+ export interface RenderHooks {
175
+ /** Transform fenced code blocks (```language ... ```). */
176
+ onCodeBlock?: (data: CodeBlockData) => HookResult;
177
+ /** Transform inline code spans (`code`). */
178
+ onInlineCode?: (data: InlineCodeData) => HookResult;
179
+ /** Transform math blocks (KaTeX). */
180
+ onMath?: (data: MathData) => HookResult;
181
+ /** Transform tables. */
182
+ onTable?: (data: TableData) => HookResult;
183
+ /** Transform links. */
184
+ onLink?: (data: LinkData) => HookResult;
185
+ /** Transform images. */
186
+ onImage?: (data: ImageData) => HookResult;
187
+ /** Transform headings (h1-h6). */
188
+ onHeading?: (data: HeadingData) => HookResult;
189
+ /** Transform blockquotes. */
190
+ onBlockquote?: (data: BlockquoteData) => HookResult;
191
+ /** Transform GitHub-style alerts ([!NOTE], [!WARNING], etc.). */
192
+ onAlert?: (data: AlertData) => HookResult;
193
+ /** Transform lists (ordered and unordered). */
194
+ onList?: (data: ListData) => HookResult;
195
+ /** Transform horizontal rules. */
196
+ onHorizontalRule?: (data: HorizontalRuleData) => HookResult;
197
+ /** Transform footnote references (the [^1] in text). */
198
+ onFootnoteRef?: (data: FootnoteRefData) => HookResult;
199
+ /** Transform footnote definitions (at the bottom). */
200
+ onFootnoteDef?: (data: FootnoteDefData) => HookResult;
201
+ /**
202
+ * Final transformation on the complete HTML output.
203
+ * String-only — runs after all other processing.
204
+ */
205
+ onRender?: (html: string) => string;
206
+ }
207
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/types/hooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hook type definitions for customizing markdown rendering.
3
+ *
4
+ * @module types/hooks
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/types/hooks.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,18 @@
1
+ import type { RenderHooks } from "./types/hooks";
2
+ export interface UseMarkdownViewerOptions {
3
+ text: string;
4
+ isStreaming: boolean;
5
+ throttleMs: number;
6
+ onStreamingEnd?: () => void;
7
+ /** Hooks for customizing markdown rendering */
8
+ hooks?: RenderHooks;
9
+ }
10
+ export interface UseMarkdownViewerReturn {
11
+ containerRef: React.RefObject<HTMLDivElement | null>;
12
+ syncMorphEnabled: boolean;
13
+ getRenderedContent: () => string;
14
+ handleClick: (event: React.MouseEvent) => void;
15
+ reset: () => void;
16
+ }
17
+ export declare function useMarkdownViewer({ text, isStreaming, throttleMs, onStreamingEnd, hooks, }: UseMarkdownViewerOptions): UseMarkdownViewerReturn;
18
+ //# sourceMappingURL=useMarkdownViewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMarkdownViewer.d.ts","sourceRoot":"","sources":["../src/useMarkdownViewer.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAiBjD,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,+CAA+C;IAC/C,KAAK,CAAC,EAAE,WAAW,CAAC;CACpB;AAWD,MAAM,WAAW,uBAAuB;IACvC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,MAAM,MAAM,CAAC;IACjC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC/C,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EACjC,IAAI,EACJ,WAAW,EACX,UAAU,EACV,cAAc,EACd,KAAK,GACL,EAAE,wBAAwB,GAAG,uBAAuB,CA6cpD"}
@@ -0,0 +1,403 @@
1
+ import { useRef, useState, useCallback, useEffect, } from "react";
2
+ import { CURSOR_MARKER, CURSOR_HTML, renderMarkdown, preloadKaTeX, isKaTeXReady, onLanguageLoaded, offLanguageLoaded, getNotificationGeneration, } from "./lib/parser";
3
+ import { cacheManager } from "./lib/cache-manager";
4
+ import { morphContentSync, morphContentOptimized, getMorphStats, resetMorphCache, } from "./lib/morph";
5
+ import { createCursorController, } from "./lib/cursor-controller";
6
+ import { isWasmReady, initMarkdownViewer, onWasmReady, } from "./lib/wasm-init";
7
+ export function useMarkdownViewer({ text, isStreaming, throttleMs, onStreamingEnd, hooks, }) {
8
+ // Refs for DOM and mutable state (avoid re-renders)
9
+ const containerRef = useRef(null);
10
+ const cursorControllerRef = useRef(null);
11
+ // Store callback in ref to avoid effect re-runs
12
+ const onStreamingEndRef = useRef(onStreamingEnd);
13
+ onStreamingEndRef.current = onStreamingEnd;
14
+ // Store hooks in ref to avoid re-renders when hooks object changes
15
+ const hooksRef = useRef(hooks);
16
+ hooksRef.current = hooks;
17
+ // Throttling state (mutable refs to avoid re-renders)
18
+ const throttledTextRef = useRef("");
19
+ const lastThrottleTimeRef = useRef(0);
20
+ const rafScheduledRef = useRef(false);
21
+ const rafIdRef = useRef(null);
22
+ const copyTimeoutIdsRef = useRef(new Set());
23
+ // Adaptive throttling state
24
+ const adaptiveThrottleMsRef = useRef(0);
25
+ const lastMorphDurationRef = useRef(0);
26
+ // Streaming state
27
+ const hasStreamedRef = useRef(false);
28
+ const wasStreamingRef = useRef(false);
29
+ const streamingStatsRef = useRef({
30
+ morphCount: 0,
31
+ morphTotalMs: 0,
32
+ morphMinMs: Infinity,
33
+ morphMaxMs: 0,
34
+ throttleMaxMs: 0,
35
+ startTime: 0,
36
+ });
37
+ // Memoization cache
38
+ const lastSourceRef = useRef("");
39
+ const lastStrategyRef = useRef(false);
40
+ const lastStreamingRef = useRef(false);
41
+ const lastResultRef = useRef("");
42
+ const lastLoggedContentLengthRef = useRef(0);
43
+ // Force update trigger (for KaTeX loading / WASM ready)
44
+ const [updateCounter, setUpdateCounter] = useState(0);
45
+ // WASM readiness state
46
+ const [wasmReady, setWasmReady] = useState(isWasmReady());
47
+ // Computed: use sync morph strategy when streaming or has streamed
48
+ const syncMorphEnabled = isStreaming || hasStreamedRef.current;
49
+ // Get effective throttle (adaptive)
50
+ const getEffectiveThrottleMs = useCallback(() => {
51
+ return adaptiveThrottleMsRef.current || throttleMs;
52
+ }, [throttleMs]);
53
+ // Track last logged throttle to avoid spam
54
+ const lastLoggedThrottleRef = useRef(0);
55
+ const morphCountRef = useRef(0);
56
+ // Adjust adaptive throttle based on morph performance
57
+ const adjustAdaptiveThrottle = useCallback(() => {
58
+ const morphTime = lastMorphDurationRef.current;
59
+ const targetMorphBudget = 0.25;
60
+ const idealThrottle = morphTime / targetMorphBudget;
61
+ const smoothingFactor = 0.3;
62
+ const currentThrottle = adaptiveThrottleMsRef.current || throttleMs;
63
+ const newThrottle = currentThrottle + (idealThrottle - currentThrottle) * smoothingFactor;
64
+ const minThrottle = throttleMs;
65
+ const maxThrottle = Math.max(throttleMs * 4, 200);
66
+ const clampedThrottle = Math.max(minThrottle, Math.min(maxThrottle, newThrottle));
67
+ adaptiveThrottleMsRef.current = clampedThrottle;
68
+ if (process.env.NODE_ENV !== "production") {
69
+ morphCountRef.current++;
70
+ const rounded = Math.round(clampedThrottle);
71
+ const shouldLog = morphCountRef.current % 10 === 0 ||
72
+ Math.abs(rounded - lastLoggedThrottleRef.current) >= 3;
73
+ if (shouldLog && morphTime >= 1) {
74
+ lastLoggedThrottleRef.current = rounded;
75
+ const direction = rounded > throttleMs
76
+ ? "📈"
77
+ : rounded < throttleMs
78
+ ? "📉"
79
+ : "➡️";
80
+ const status = rounded === throttleMs ? "(at minimum)" : "";
81
+ console.log(`${direction} Throttle: ${rounded}ms ${status} | morph: ${morphTime.toFixed(1)}ms | ideal: ${idealThrottle.toFixed(0)}ms`);
82
+ }
83
+ }
84
+ }, [throttleMs]);
85
+ // Track morph stats
86
+ const trackMorphStats = useCallback(() => {
87
+ const morphTime = lastMorphDurationRef.current;
88
+ if (morphTime < 2)
89
+ return;
90
+ const stats = streamingStatsRef.current;
91
+ stats.morphCount++;
92
+ stats.morphTotalMs += morphTime;
93
+ stats.morphMinMs = Math.min(stats.morphMinMs, morphTime);
94
+ stats.morphMaxMs = Math.max(stats.morphMaxMs, morphTime);
95
+ stats.throttleMaxMs = Math.max(stats.throttleMaxMs, adaptiveThrottleMsRef.current);
96
+ }, []);
97
+ // Reset streaming stats
98
+ const resetStreamingStats = useCallback(() => {
99
+ streamingStatsRef.current = {
100
+ morphCount: 0,
101
+ morphTotalMs: 0,
102
+ morphMinMs: Infinity,
103
+ morphMaxMs: 0,
104
+ throttleMaxMs: 0,
105
+ startTime: performance.now(),
106
+ };
107
+ }, []);
108
+ // Track text length in ref for logging
109
+ const textLengthRef = useRef(0);
110
+ textLengthRef.current = text.length;
111
+ // Log streaming stats (dev only)
112
+ const logStreamingStats = useCallback(() => {
113
+ if (process.env.NODE_ENV === "production")
114
+ return;
115
+ const stats = streamingStatsRef.current;
116
+ if (stats.morphCount === 0)
117
+ return;
118
+ const duration = performance.now() - stats.startTime;
119
+ const avgMorph = stats.morphTotalMs / stats.morphCount;
120
+ console.log(`📊 Streaming complete:\n` +
121
+ ` Duration: ${(duration / 1000).toFixed(2)}s\n` +
122
+ ` Morphs: ${stats.morphCount} (avg ${avgMorph.toFixed(1)}ms, min ${stats.morphMinMs.toFixed(1)}ms, max ${stats.morphMaxMs.toFixed(1)}ms)\n` +
123
+ ` Throttle: base ${throttleMs}ms → max ${stats.throttleMaxMs.toFixed(1)}ms\n` +
124
+ ` Content: ${(textLengthRef.current / 1024).toFixed(1)}KB`);
125
+ }, [throttleMs]);
126
+ // Render markdown with memoization
127
+ const getRenderedContent = useCallback(() => {
128
+ // Guard: WASM must be ready
129
+ if (!wasmReady)
130
+ return "";
131
+ const baseText = isStreaming ? throttledTextRef.current : text;
132
+ const source = isStreaming ? baseText + CURSOR_MARKER : baseText;
133
+ if (!source)
134
+ return "";
135
+ if (baseText === lastSourceRef.current &&
136
+ syncMorphEnabled === lastStrategyRef.current &&
137
+ isStreaming === lastStreamingRef.current) {
138
+ return lastResultRef.current;
139
+ }
140
+ lastSourceRef.current = baseText;
141
+ lastStrategyRef.current = syncMorphEnabled;
142
+ lastStreamingRef.current = isStreaming;
143
+ let html = renderMarkdown(source, syncMorphEnabled, undefined, hooksRef.current);
144
+ // Streaming guard: ensure cursor is visible when streaming
145
+ if (isStreaming) {
146
+ const cursorIndex = html.indexOf(CURSOR_HTML);
147
+ if (cursorIndex === -1) {
148
+ html += CURSOR_HTML;
149
+ }
150
+ else {
151
+ const beforeCursor = html.slice(0, cursorIndex);
152
+ const lastOpenBracket = beforeCursor.lastIndexOf("<");
153
+ const lastCloseBracket = beforeCursor.lastIndexOf(">");
154
+ if (lastOpenBracket > lastCloseBracket) {
155
+ html = html.replace(CURSOR_HTML, "");
156
+ if (html.includes("<code")) {
157
+ html = html.replace(/(<code[^>]*>)/, "$1" + CURSOR_HTML);
158
+ }
159
+ else {
160
+ html += CURSOR_HTML;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ lastResultRef.current = html;
166
+ return lastResultRef.current;
167
+ }, [text, isStreaming, syncMorphEnabled, wasmReady]);
168
+ // Ensure KaTeX is loaded
169
+ const ensureKaTeXLoaded = useCallback(() => {
170
+ if (isKaTeXReady())
171
+ return;
172
+ preloadKaTeX().then(() => {
173
+ lastSourceRef.current = "";
174
+ lastResultRef.current = "";
175
+ cacheManager.renderCacheSync.clear();
176
+ cacheManager.renderCacheAsync.clear();
177
+ setUpdateCounter((n) => n + 1);
178
+ });
179
+ }, []);
180
+ // Apply DOM morphing
181
+ const applyMorph = useCallback(() => {
182
+ const container = containerRef.current;
183
+ if (!container || !syncMorphEnabled)
184
+ return;
185
+ const content = getRenderedContent();
186
+ if (isStreaming) {
187
+ const startTime = performance.now();
188
+ morphContentOptimized(container, content);
189
+ lastMorphDurationRef.current = performance.now() - startTime;
190
+ adjustAdaptiveThrottle();
191
+ trackMorphStats();
192
+ if (process.env.NODE_ENV !== "production") {
193
+ const stats = getMorphStats(container);
194
+ const contentLength = throttledTextRef.current.length;
195
+ if ((stats.added > 0 || stats.removed > 0) &&
196
+ contentLength !== lastLoggedContentLengthRef.current) {
197
+ lastLoggedContentLengthRef.current = contentLength;
198
+ const parts = [];
199
+ if (stats.added > 0)
200
+ parts.push(`+${stats.added} added`);
201
+ if (stats.removed > 0)
202
+ parts.push(`-${stats.removed} removed`);
203
+ console.log(`🔄 Morph: ${parts.join(", ")} (${stats.skipped} unchanged)`);
204
+ }
205
+ }
206
+ if (!cursorControllerRef.current) {
207
+ cursorControllerRef.current = createCursorController();
208
+ }
209
+ cursorControllerRef.current.update(container);
210
+ }
211
+ else {
212
+ morphContentSync(container, content);
213
+ }
214
+ }, [
215
+ isStreaming,
216
+ syncMorphEnabled,
217
+ getRenderedContent,
218
+ adjustAdaptiveThrottle,
219
+ trackMorphStats,
220
+ ]);
221
+ // Update throttled text
222
+ const updateThrottledText = useCallback(() => {
223
+ if (!isStreaming) {
224
+ throttledTextRef.current = text;
225
+ rafScheduledRef.current = false;
226
+ if (rafIdRef.current !== null) {
227
+ cancelAnimationFrame(rafIdRef.current);
228
+ rafIdRef.current = null;
229
+ }
230
+ return;
231
+ }
232
+ const now = Date.now();
233
+ const effectiveThrottle = getEffectiveThrottleMs();
234
+ if (now - lastThrottleTimeRef.current >= effectiveThrottle) {
235
+ throttledTextRef.current = text;
236
+ lastThrottleTimeRef.current = now;
237
+ applyMorph();
238
+ }
239
+ else if (!rafScheduledRef.current) {
240
+ rafScheduledRef.current = true;
241
+ rafIdRef.current = requestAnimationFrame(() => {
242
+ if (rafScheduledRef.current && isStreaming) {
243
+ throttledTextRef.current = text;
244
+ lastThrottleTimeRef.current = Date.now();
245
+ applyMorph();
246
+ }
247
+ rafScheduledRef.current = false;
248
+ rafIdRef.current = null;
249
+ });
250
+ }
251
+ }, [text, isStreaming, getEffectiveThrottleMs, applyMorph]);
252
+ // Handle copy button clicks
253
+ const handleClick = useCallback((event) => {
254
+ const target = event.target;
255
+ const copyBtn = target.closest(".copy-btn");
256
+ if (!copyBtn)
257
+ return;
258
+ const wrapper = copyBtn.closest(".code-block-wrapper");
259
+ const codeElement = wrapper?.querySelector("pre code");
260
+ if (!codeElement)
261
+ return;
262
+ const code = codeElement.textContent ?? "";
263
+ navigator.clipboard
264
+ .writeText(code)
265
+ .then(() => {
266
+ copyBtn.classList.add("copied");
267
+ const timeoutId = window.setTimeout(() => {
268
+ copyBtn.classList.remove("copied");
269
+ copyTimeoutIdsRef.current.delete(timeoutId);
270
+ }, 2000);
271
+ copyTimeoutIdsRef.current.add(timeoutId);
272
+ })
273
+ .catch((err) => {
274
+ console.error("Failed to copy code:", err);
275
+ });
276
+ }, []);
277
+ // Reset component state
278
+ const reset = useCallback(() => {
279
+ hasStreamedRef.current = false;
280
+ throttledTextRef.current = "";
281
+ lastSourceRef.current = "";
282
+ lastStrategyRef.current = false;
283
+ lastStreamingRef.current = false;
284
+ lastResultRef.current = "";
285
+ adaptiveThrottleMsRef.current = 0;
286
+ lastMorphDurationRef.current = 0;
287
+ lastLoggedContentLengthRef.current = 0;
288
+ lastLoggedThrottleRef.current = 0;
289
+ morphCountRef.current = 0;
290
+ resetStreamingStats();
291
+ resetMorphCache(containerRef.current ?? undefined);
292
+ cursorControllerRef.current?.reset();
293
+ }, [resetStreamingStats]);
294
+ // Effect: Initialize WASM if not ready
295
+ useEffect(() => {
296
+ if (wasmReady)
297
+ return;
298
+ initMarkdownViewer();
299
+ return onWasmReady(() => setWasmReady(true));
300
+ }, [wasmReady]);
301
+ // Effect: Handle streaming state changes
302
+ useEffect(() => {
303
+ if (isStreaming && !wasStreamingRef.current) {
304
+ hasStreamedRef.current = true;
305
+ resetStreamingStats();
306
+ containerRef.current?.focus();
307
+ }
308
+ else if (!isStreaming && wasStreamingRef.current) {
309
+ logStreamingStats();
310
+ onStreamingEndRef.current?.();
311
+ }
312
+ wasStreamingRef.current = isStreaming;
313
+ }, [isStreaming, resetStreamingStats, logStreamingStats]);
314
+ // Effect: Ensure KaTeX is loaded when text changes.
315
+ // Also handles the case where KaTeX loaded between render and effect
316
+ // (Vite dev mode resolves dynamic imports before useEffect runs).
317
+ const katexWasReady = useRef(isKaTeXReady());
318
+ useEffect(() => {
319
+ if (!text)
320
+ return;
321
+ if (!isKaTeXReady()) {
322
+ ensureKaTeXLoaded();
323
+ }
324
+ else if (!katexWasReady.current) {
325
+ // KaTeX loaded between render and effect — trigger re-render
326
+ katexWasReady.current = true;
327
+ lastSourceRef.current = "";
328
+ lastResultRef.current = "";
329
+ cacheManager.renderCacheSync.clear();
330
+ cacheManager.renderCacheAsync.clear();
331
+ setUpdateCounter((n) => n + 1);
332
+ }
333
+ }, [text, ensureKaTeXLoaded]);
334
+ // Stable ref for applyMorph to avoid re-subscribing language listener
335
+ const applyMorphRef = useRef(applyMorph);
336
+ applyMorphRef.current = applyMorph;
337
+ // Stable ref for syncMorphEnabled to avoid re-subscribing language listener
338
+ const syncMorphEnabledRef = useRef(syncMorphEnabled);
339
+ syncMorphEnabledRef.current = syncMorphEnabled;
340
+ // Capture notification generation before render so the effect can detect
341
+ // if languages loaded between render and effect mount (race condition
342
+ // in Vite dev mode where dynamic imports resolve before useEffect runs).
343
+ const preRenderGeneration = useRef(getNotificationGeneration());
344
+ preRenderGeneration.current = getNotificationGeneration();
345
+ // Effect: Re-highlight when dynamically loaded languages become available
346
+ useEffect(() => {
347
+ const triggerUpdate = () => {
348
+ lastSourceRef.current = "";
349
+ lastResultRef.current = "";
350
+ cacheManager.renderCacheSync.clear();
351
+ cacheManager.renderCacheAsync.clear();
352
+ cacheManager.highlightCache.clear();
353
+ if (syncMorphEnabledRef.current) {
354
+ applyMorphRef.current();
355
+ }
356
+ else {
357
+ setUpdateCounter((n) => n + 1);
358
+ }
359
+ };
360
+ const handleLanguageLoaded = (_language) => {
361
+ triggerUpdate();
362
+ };
363
+ onLanguageLoaded(handleLanguageLoaded);
364
+ // Detect missed notifications: if languages loaded between render
365
+ // and this effect (useEffect runs after paint), trigger update now.
366
+ if (getNotificationGeneration() !== preRenderGeneration.current) {
367
+ triggerUpdate();
368
+ }
369
+ return () => offLanguageLoaded(handleLanguageLoaded);
370
+ }, []);
371
+ // Effect: Update throttled text when text or streaming state changes
372
+ useEffect(() => {
373
+ updateThrottledText();
374
+ }, [text, isStreaming, updateThrottledText]);
375
+ // Effect: Apply morph after render for sync strategy
376
+ useEffect(() => {
377
+ if (syncMorphEnabled && !isStreaming) {
378
+ applyMorph();
379
+ }
380
+ }, [syncMorphEnabled, isStreaming, applyMorph, updateCounter]);
381
+ // Effect: Cleanup on unmount
382
+ useEffect(() => {
383
+ return () => {
384
+ if (rafIdRef.current !== null) {
385
+ cancelAnimationFrame(rafIdRef.current);
386
+ rafIdRef.current = null;
387
+ }
388
+ for (const timeoutId of copyTimeoutIdsRef.current) {
389
+ clearTimeout(timeoutId);
390
+ }
391
+ copyTimeoutIdsRef.current.clear();
392
+ cursorControllerRef.current?.destroy();
393
+ };
394
+ }, []);
395
+ return {
396
+ containerRef,
397
+ syncMorphEnabled,
398
+ getRenderedContent,
399
+ handleClick,
400
+ reset,
401
+ };
402
+ }
403
+ //# sourceMappingURL=useMarkdownViewer.js.map