@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.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/dist/ErrorBoundary.d.ts +25 -0
- package/dist/ErrorBoundary.d.ts.map +1 -0
- package/dist/ErrorBoundary.js +29 -0
- package/dist/ErrorBoundary.js.map +1 -0
- package/dist/MarkdownViewer.d.ts +41 -0
- package/dist/MarkdownViewer.d.ts.map +1 -0
- package/dist/MarkdownViewer.js +69 -0
- package/dist/MarkdownViewer.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +59 -0
- package/dist/lib/cache-manager.d.ts.map +1 -0
- package/dist/lib/cache-manager.js +160 -0
- package/dist/lib/cache-manager.js.map +1 -0
- package/dist/lib/cursor-controller.d.ts +13 -0
- package/dist/lib/cursor-controller.d.ts.map +1 -0
- package/dist/lib/cursor-controller.js +93 -0
- package/dist/lib/cursor-controller.js.map +1 -0
- package/dist/lib/defaults/code-block.d.ts +71 -0
- package/dist/lib/defaults/code-block.d.ts.map +1 -0
- package/dist/lib/defaults/code-block.js +104 -0
- package/dist/lib/defaults/code-block.js.map +1 -0
- package/dist/lib/defaults/image.d.ts +41 -0
- package/dist/lib/defaults/image.d.ts.map +1 -0
- package/dist/lib/defaults/image.js +45 -0
- package/dist/lib/defaults/image.js.map +1 -0
- package/dist/lib/defaults/link.d.ts +45 -0
- package/dist/lib/defaults/link.d.ts.map +1 -0
- package/dist/lib/defaults/link.js +76 -0
- package/dist/lib/defaults/link.js.map +1 -0
- package/dist/lib/defaults/math.d.ts +51 -0
- package/dist/lib/defaults/math.d.ts.map +1 -0
- package/dist/lib/defaults/math.js +119 -0
- package/dist/lib/defaults/math.js.map +1 -0
- package/dist/lib/defaults/table.d.ts +18 -0
- package/dist/lib/defaults/table.d.ts.map +1 -0
- package/dist/lib/defaults/table.js +19 -0
- package/dist/lib/defaults/table.js.map +1 -0
- package/dist/lib/highlighter.d.ts +81 -0
- package/dist/lib/highlighter.d.ts.map +1 -0
- package/dist/lib/highlighter.js +421 -0
- package/dist/lib/highlighter.js.map +1 -0
- package/dist/lib/hook-utils.d.ts +32 -0
- package/dist/lib/hook-utils.d.ts.map +1 -0
- package/dist/lib/hook-utils.js +42 -0
- package/dist/lib/hook-utils.js.map +1 -0
- package/dist/lib/html.d.ts +2 -0
- package/dist/lib/html.d.ts.map +1 -0
- package/dist/lib/html.js +12 -0
- package/dist/lib/html.js.map +1 -0
- package/dist/lib/morph.d.ts +57 -0
- package/dist/lib/morph.d.ts.map +1 -0
- package/dist/lib/morph.js +204 -0
- package/dist/lib/morph.js.map +1 -0
- package/dist/lib/parser.d.ts +32 -0
- package/dist/lib/parser.d.ts.map +1 -0
- package/dist/lib/parser.js +645 -0
- package/dist/lib/parser.js.map +1 -0
- package/dist/lib/wasm-init.d.ts +33 -0
- package/dist/lib/wasm-init.d.ts.map +1 -0
- package/dist/lib/wasm-init.js +69 -0
- package/dist/lib/wasm-init.js.map +1 -0
- package/dist/styles/alerts.css +294 -0
- package/dist/styles/dotted.svg +3 -0
- package/dist/styles/hljs.css +332 -0
- package/dist/styles/index.css +17 -0
- package/dist/styles/katex.css +74 -0
- package/dist/styles/viewer.css +975 -0
- package/dist/types/hooks.d.ts +207 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/hooks.js +7 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/useMarkdownViewer.d.ts +18 -0
- package/dist/useMarkdownViewer.d.ts.map +1 -0
- package/dist/useMarkdownViewer.js +403 -0
- package/dist/useMarkdownViewer.js.map +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +18 -0
- package/dist/utils.js.map +1 -0
- 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 @@
|
|
|
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
|