vantage-md 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/dist/react.js ADDED
@@ -0,0 +1,826 @@
1
+ import { memo, useState, useRef, useMemo, useEffect, useCallback } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import remarkMath from 'remark-math';
5
+ import rehypeRaw from 'rehype-raw';
6
+ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
7
+ import rehypeHighlight from 'rehype-highlight';
8
+ import rehypeKatex from 'rehype-katex';
9
+ import rehypeSlug from 'rehype-slug';
10
+ import YAML from 'yaml';
11
+ import { parse } from 'smol-toml';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
13
+ import { unified } from 'unified';
14
+ import remarkParse from 'remark-parse';
15
+ import remarkRehype from 'remark-rehype';
16
+ import rehypeStringify from 'rehype-stringify';
17
+
18
+ // src/MarkdownViewer.tsx
19
+
20
+ // src/rehypeSourceLines.ts
21
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
22
+ "p",
23
+ "h1",
24
+ "h2",
25
+ "h3",
26
+ "h4",
27
+ "h5",
28
+ "h6",
29
+ "li",
30
+ "blockquote",
31
+ "pre",
32
+ "table",
33
+ "tr",
34
+ "ul",
35
+ "ol",
36
+ "hr",
37
+ "div"
38
+ ]);
39
+ function visit(node) {
40
+ if ("children" in node) {
41
+ for (const child of node.children) {
42
+ if (child.type === "element") {
43
+ if (BLOCK_TAGS.has(child.tagName) && child.position?.start?.line) {
44
+ child.properties = child.properties || {};
45
+ child.properties["dataSourceLine"] = child.position.start.line;
46
+ }
47
+ visit(child);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ var rehypeSourceLines = () => {
53
+ return (tree) => {
54
+ visit(tree);
55
+ };
56
+ };
57
+ var rehypeSourceLines_default = rehypeSourceLines;
58
+ var sanitizeSchema = {
59
+ ...defaultSchema,
60
+ tagNames: [
61
+ ...defaultSchema.tagNames || [],
62
+ // KaTeX MathML elements
63
+ "math",
64
+ "semantics",
65
+ "mrow",
66
+ "mi",
67
+ "mo",
68
+ "mn",
69
+ "msup",
70
+ "msub",
71
+ "mfrac",
72
+ "mover",
73
+ "munder",
74
+ "msqrt",
75
+ "mroot",
76
+ "mtable",
77
+ "mtr",
78
+ "mtd",
79
+ "mtext",
80
+ "mspace",
81
+ "annotation",
82
+ // Other
83
+ "figure",
84
+ "figcaption",
85
+ "summary",
86
+ "details"
87
+ ],
88
+ attributes: {
89
+ ...defaultSchema.attributes,
90
+ "*": [
91
+ ...defaultSchema.attributes?.["*"] || [],
92
+ "className",
93
+ "style",
94
+ "dataSourceLine"
95
+ ],
96
+ code: [...defaultSchema.attributes?.code || [], "className"],
97
+ span: [...defaultSchema.attributes?.span || [], "className", "style"],
98
+ div: [...defaultSchema.attributes?.div || [], "className", "style"],
99
+ a: [...defaultSchema.attributes?.a || [], "id", "className"],
100
+ math: ["xmlns"],
101
+ annotation: ["encoding"],
102
+ img: [...defaultSchema.attributes?.img || [], "loading"],
103
+ td: [...defaultSchema.attributes?.td || [], "style"],
104
+ th: [...defaultSchema.attributes?.th || [], "style"]
105
+ }
106
+ };
107
+ function parseFrontmatter(content) {
108
+ if (content.startsWith("+++")) {
109
+ return parseFrontmatterWithDelimiter(content, "+++", "toml");
110
+ }
111
+ if (content.startsWith("---")) {
112
+ return parseFrontmatterWithDelimiter(content, "---", "yaml");
113
+ }
114
+ return { frontmatter: {}, body: content, format: "none" };
115
+ }
116
+ function parseFrontmatterWithDelimiter(content, delimiter, format) {
117
+ const searchStart = delimiter.length;
118
+ const endIndex = content.indexOf(`
119
+ ${delimiter}`, searchStart);
120
+ if (endIndex === -1) {
121
+ return { frontmatter: {}, body: content, format: "none" };
122
+ }
123
+ const raw = content.slice(searchStart + 1, endIndex).trim();
124
+ const bodyStart = endIndex + 1 + delimiter.length;
125
+ const body = content.slice(bodyStart).replace(/^\n/, "");
126
+ try {
127
+ const frontmatter = format === "toml" ? parse(raw) : YAML.parse(raw);
128
+ return { frontmatter: frontmatter || {}, body, format };
129
+ } catch {
130
+ return { frontmatter: {}, body: content, format: "none" };
131
+ }
132
+ }
133
+
134
+ // src/mermaidCache.ts
135
+ var svgCache = /* @__PURE__ */ new Map();
136
+
137
+ // src/mermaidLoader.ts
138
+ var mermaidInstance = null;
139
+ var mermaidLoading = null;
140
+ var isDark = () => typeof document !== "undefined" && document.documentElement.classList.contains("dark");
141
+ async function getMermaid() {
142
+ if (mermaidInstance) return mermaidInstance;
143
+ if (!mermaidLoading) {
144
+ mermaidLoading = import('mermaid').then((mod) => {
145
+ const m = mod.default;
146
+ m.initialize({
147
+ startOnLoad: false,
148
+ theme: isDark() ? "dark" : "default",
149
+ securityLevel: "strict",
150
+ suppressErrorRendering: true
151
+ });
152
+ mermaidInstance = m;
153
+ return m;
154
+ });
155
+ }
156
+ return mermaidLoading;
157
+ }
158
+ var AlertTriangleIcon = () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "w-4 h-4 shrink-0", children: [
159
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
160
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
161
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
162
+ ] });
163
+ var ChevronDownIcon = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "w-3 h-3", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) });
164
+ var ChevronUpIcon = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "w-3 h-3", children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) });
165
+ var MaximizeIcon = () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "w-4 h-4 text-gray-600", children: [
166
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
167
+ /* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
168
+ /* @__PURE__ */ jsx("line", { x1: "21", x2: "14", y1: "3", y2: "10" }),
169
+ /* @__PURE__ */ jsx("line", { x1: "3", x2: "10", y1: "21", y2: "14" })
170
+ ] });
171
+ var CloseIcon = () => /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "w-5 h-5", children: [
172
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
173
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
174
+ ] });
175
+ function extractErrorMessage(err) {
176
+ if (err instanceof Error) {
177
+ const msg = err.message;
178
+ const parseMatch = msg.match(
179
+ /(?:Parse error|Syntax error|Error).*?(?:line \d+.*)/i
180
+ );
181
+ if (parseMatch) return parseMatch[0];
182
+ return msg.split("\n")[0].slice(0, 200);
183
+ }
184
+ if (typeof err === "string") return err.split("\n")[0].slice(0, 200);
185
+ return "Unknown error";
186
+ }
187
+ function DiagramModal({
188
+ isOpen,
189
+ onClose,
190
+ children
191
+ }) {
192
+ useEffect(() => {
193
+ if (!isOpen) return;
194
+ const handler = (e) => {
195
+ if (e.key === "Escape") onClose();
196
+ };
197
+ document.addEventListener("keydown", handler);
198
+ document.body.style.overflow = "hidden";
199
+ return () => {
200
+ document.removeEventListener("keydown", handler);
201
+ document.body.style.overflow = "unset";
202
+ };
203
+ }, [isOpen, onClose]);
204
+ if (!isOpen) return null;
205
+ return /* @__PURE__ */ jsx(
206
+ "div",
207
+ {
208
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4",
209
+ onClick: onClose,
210
+ children: /* @__PURE__ */ jsxs(
211
+ "div",
212
+ {
213
+ className: "bg-white rounded-lg shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col",
214
+ role: "dialog",
215
+ "aria-modal": "true",
216
+ onClick: (e) => e.stopPropagation(),
217
+ children: [
218
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b", children: [
219
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Mermaid Diagram" }),
220
+ /* @__PURE__ */ jsx(
221
+ "button",
222
+ {
223
+ onClick: onClose,
224
+ className: "p-1 hover:bg-gray-100 rounded-full transition-colors",
225
+ "aria-label": "Close modal",
226
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
227
+ }
228
+ )
229
+ ] }),
230
+ /* @__PURE__ */ jsx("div", { className: "p-4 overflow-auto flex-1", children })
231
+ ]
232
+ }
233
+ )
234
+ }
235
+ );
236
+ }
237
+ var MermaidDiagramInner = ({ code }) => {
238
+ const hasCached = svgCache.has(code);
239
+ const [svg, setSvg] = useState(() => svgCache.get(code) || "");
240
+ const [errorMessage, setErrorMessage] = useState(null);
241
+ const [showSource, setShowSource] = useState(false);
242
+ const [isModalOpen, setIsModalOpen] = useState(false);
243
+ const [isLoading, setIsLoading] = useState(!hasCached);
244
+ const [minHeight, setMinHeight] = useState("auto");
245
+ const containerRef = useRef(null);
246
+ const lastHeightRef = useRef(null);
247
+ const stableId = useMemo(() => {
248
+ let hash = 0;
249
+ for (let i = 0; i < code.length; i++) {
250
+ const char = code.charCodeAt(i);
251
+ hash = (hash << 5) - hash + char;
252
+ hash = hash & hash;
253
+ }
254
+ return `mermaid-${Math.abs(hash).toString(36)}`;
255
+ }, [code]);
256
+ useEffect(() => {
257
+ if (hasCached) return;
258
+ let mounted = true;
259
+ const renderDiagram = async () => {
260
+ try {
261
+ if (containerRef.current) {
262
+ const height = containerRef.current.offsetHeight;
263
+ lastHeightRef.current = height;
264
+ setMinHeight(`${height}px`);
265
+ }
266
+ const m = await getMermaid();
267
+ const id = `${stableId}-${Date.now()}`;
268
+ const { svg: renderedSvg } = await m.render(id, code);
269
+ if (mounted) {
270
+ svgCache.set(code, renderedSvg);
271
+ setSvg(renderedSvg);
272
+ setErrorMessage(null);
273
+ setIsLoading(false);
274
+ }
275
+ } catch (err) {
276
+ console.error("Mermaid render error:", err);
277
+ if (mounted) {
278
+ setErrorMessage(extractErrorMessage(err));
279
+ setIsLoading(false);
280
+ }
281
+ }
282
+ };
283
+ renderDiagram();
284
+ return () => {
285
+ mounted = false;
286
+ };
287
+ }, [code, stableId, hasCached]);
288
+ if (errorMessage) {
289
+ return /* @__PURE__ */ jsxs(
290
+ "div",
291
+ {
292
+ "data-testid": "mermaid-container",
293
+ className: "my-4 rounded-md border border-yellow-300/40 bg-yellow-50/50 dark:border-yellow-700/40 dark:bg-yellow-950/20 overflow-hidden",
294
+ children: [
295
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-2.5 text-sm text-yellow-800 dark:text-yellow-200", children: [
296
+ /* @__PURE__ */ jsx(AlertTriangleIcon, {}),
297
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Diagram syntax error" }),
298
+ /* @__PURE__ */ jsxs("span", { className: "text-yellow-700/70 dark:text-yellow-300/60", children: [
299
+ "\u2014 ",
300
+ errorMessage
301
+ ] })
302
+ ] }),
303
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-yellow-300/30 dark:border-yellow-700/30", children: [
304
+ /* @__PURE__ */ jsxs(
305
+ "button",
306
+ {
307
+ onClick: () => setShowSource(!showSource),
308
+ className: "flex items-center gap-1.5 px-4 py-1.5 text-xs text-yellow-700/60 dark:text-yellow-400/50 hover:text-yellow-800 dark:hover:text-yellow-300 transition-colors w-full",
309
+ children: [
310
+ showSource ? /* @__PURE__ */ jsx(ChevronUpIcon, {}) : /* @__PURE__ */ jsx(ChevronDownIcon, {}),
311
+ showSource ? "Hide source" : "Show source"
312
+ ]
313
+ }
314
+ ),
315
+ showSource && /* @__PURE__ */ jsx("pre", { className: "px-4 pb-3 text-xs font-mono text-yellow-800/70 dark:text-yellow-200/60 overflow-auto whitespace-pre-wrap", children: code })
316
+ ] })
317
+ ]
318
+ }
319
+ );
320
+ }
321
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
322
+ /* @__PURE__ */ jsxs(
323
+ "div",
324
+ {
325
+ ref: containerRef,
326
+ "data-testid": "mermaid-container",
327
+ className: "relative group inline-block max-w-full",
328
+ style: { minHeight: isLoading ? minHeight : "auto" },
329
+ children: [
330
+ /* @__PURE__ */ jsx(
331
+ "div",
332
+ {
333
+ className: `mermaid flex justify-center my-4 overflow-x-auto transition-opacity duration-150 ${isLoading ? "opacity-50" : "opacity-100"}`,
334
+ dangerouslySetInnerHTML: { __html: svg }
335
+ }
336
+ ),
337
+ svg && /* @__PURE__ */ jsx(
338
+ "button",
339
+ {
340
+ onClick: () => setIsModalOpen(true),
341
+ className: "absolute top-2 right-2 p-2 bg-white/90 shadow-sm border rounded-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50",
342
+ "aria-label": "Maximize diagram",
343
+ children: /* @__PURE__ */ jsx(MaximizeIcon, {})
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx(
350
+ DiagramModal,
351
+ {
352
+ isOpen: isModalOpen,
353
+ onClose: () => setIsModalOpen(false),
354
+ children: /* @__PURE__ */ jsx(
355
+ "div",
356
+ {
357
+ className: "flex justify-center items-center min-h-[50vh]",
358
+ dangerouslySetInnerHTML: { __html: svg }
359
+ }
360
+ )
361
+ }
362
+ )
363
+ ] });
364
+ };
365
+ var MermaidDiagram = memo(
366
+ MermaidDiagramInner,
367
+ (prevProps, nextProps) => prevProps.code === nextProps.code
368
+ );
369
+ var FileTextIcon = () => /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-slate-400", children: [
370
+ /* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
371
+ /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" }),
372
+ /* @__PURE__ */ jsx("path", { d: "M10 9H8" }),
373
+ /* @__PURE__ */ jsx("path", { d: "M16 13H8" }),
374
+ /* @__PURE__ */ jsx("path", { d: "M16 17H8" })
375
+ ] });
376
+ function isStringArray(value) {
377
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
378
+ }
379
+ function isPlainObject(value) {
380
+ return typeof value === "object" && value !== null && !Array.isArray(value);
381
+ }
382
+ function formatValue(value) {
383
+ if (value instanceof Date) {
384
+ return value.toISOString().split("T")[0];
385
+ }
386
+ if (Array.isArray(value)) {
387
+ return value.map(String).join(", ");
388
+ }
389
+ if (typeof value === "boolean") {
390
+ return value ? "true" : "false";
391
+ }
392
+ if (isPlainObject(value)) {
393
+ return JSON.stringify(value, null, 2);
394
+ }
395
+ return String(value);
396
+ }
397
+ function TagList({ items }) {
398
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: items.map((item) => /* @__PURE__ */ jsx(
399
+ "span",
400
+ {
401
+ className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
402
+ children: item
403
+ },
404
+ item
405
+ )) });
406
+ }
407
+ function ValueCell({ value }) {
408
+ if (isStringArray(value) && value.length > 0) {
409
+ return /* @__PURE__ */ jsx(TagList, { items: value });
410
+ }
411
+ if (isPlainObject(value)) {
412
+ return /* @__PURE__ */ jsx("pre", { className: "bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md px-3 py-2 text-xs font-mono overflow-x-auto text-slate-600 dark:text-slate-300", children: formatValue(value) });
413
+ }
414
+ return /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatValue(value) });
415
+ }
416
+ function flattenEntries(entries) {
417
+ const result = [];
418
+ for (const [key, value] of entries) {
419
+ if (key === "taxonomies" && isPlainObject(value)) {
420
+ for (const [taxKey, taxVal] of Object.entries(value)) {
421
+ result.push([taxKey, taxVal]);
422
+ }
423
+ } else if (key === "extra" && isPlainObject(value)) {
424
+ for (const [extraKey, extraVal] of Object.entries(value)) {
425
+ result.push([extraKey, extraVal]);
426
+ }
427
+ } else {
428
+ result.push([key, value]);
429
+ }
430
+ }
431
+ return result;
432
+ }
433
+ var FrontmatterDisplayInner = ({
434
+ frontmatter
435
+ }) => {
436
+ const raw = Object.entries(frontmatter);
437
+ if (raw.length === 0) return null;
438
+ const entries = flattenEntries(raw);
439
+ return /* @__PURE__ */ jsxs("div", { className: "mb-8 rounded-lg overflow-hidden bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-800/80 border border-slate-200 dark:border-slate-700 shadow-sm", children: [
440
+ /* @__PURE__ */ jsxs("div", { className: "px-4 py-2.5 bg-white/60 dark:bg-slate-900/40 border-b border-slate-200 dark:border-slate-700 flex items-center gap-2", children: [
441
+ /* @__PURE__ */ jsx(FileTextIcon, {}),
442
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider", children: "Metadata" })
443
+ ] }),
444
+ /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx("table", { className: "w-full text-sm", children: /* @__PURE__ */ jsx("tbody", { children: entries.map(([key, value]) => /* @__PURE__ */ jsxs(
445
+ "tr",
446
+ {
447
+ className: "border-b border-slate-200/60 dark:border-slate-700/60 last:border-0",
448
+ children: [
449
+ /* @__PURE__ */ jsx("td", { className: "py-2 pr-4 font-medium text-slate-500 dark:text-slate-400 whitespace-nowrap align-top w-1/4 min-w-[100px]", children: key }),
450
+ /* @__PURE__ */ jsx("td", { className: "py-2 text-slate-800 dark:text-slate-200 align-top", children: /* @__PURE__ */ jsx(ValueCell, { value }) })
451
+ ]
452
+ },
453
+ key
454
+ )) }) }) })
455
+ ] });
456
+ };
457
+ var FrontmatterDisplay = memo(FrontmatterDisplayInner);
458
+
459
+ // src/scrollToLineAnchor.ts
460
+ var HIGHLIGHT_CLASS = "line-anchor-highlight";
461
+ function parseLineAnchor(hash) {
462
+ if (!hash) return null;
463
+ const frag = hash.startsWith("#") ? hash.slice(1) : hash;
464
+ const match = frag.match(/^L(\d+)(?:-L?(\d+))?$/);
465
+ if (!match) return null;
466
+ const start = parseInt(match[1], 10);
467
+ const end = match[2] ? parseInt(match[2], 10) : start;
468
+ return { start: Math.min(start, end), end: Math.max(start, end) };
469
+ }
470
+ function clearLineAnchorHighlights(container) {
471
+ container.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((node) => {
472
+ node.classList.remove(HIGHLIGHT_CLASS);
473
+ });
474
+ }
475
+ function scrollToLineAnchor(container, hash) {
476
+ clearLineAnchorHighlights(container);
477
+ const range = parseLineAnchor(hash);
478
+ if (!range) return null;
479
+ const blocks = container.querySelectorAll("[data-source-line]");
480
+ let firstMatch = null;
481
+ for (const block of blocks) {
482
+ const line = parseInt(
483
+ block.dataset.sourceLine || "0",
484
+ 10
485
+ );
486
+ if (line >= range.start && line <= range.end) {
487
+ block.classList.add(HIGHLIGHT_CLASS);
488
+ if (!firstMatch) firstMatch = block;
489
+ }
490
+ }
491
+ if (!firstMatch) {
492
+ let closest = null;
493
+ let closestLine = 0;
494
+ for (const block of blocks) {
495
+ const line = parseInt(
496
+ block.dataset.sourceLine || "0",
497
+ 10
498
+ );
499
+ if (line <= range.start && line > closestLine) {
500
+ closestLine = line;
501
+ closest = block;
502
+ }
503
+ }
504
+ if (closest) {
505
+ closest.classList.add(HIGHLIGHT_CLASS);
506
+ firstMatch = closest;
507
+ }
508
+ }
509
+ if (firstMatch) {
510
+ requestAnimationFrame(() => {
511
+ const scrollParent = findScrollParent(container);
512
+ if (scrollParent) {
513
+ const offset = firstMatch.getBoundingClientRect().top - scrollParent.getBoundingClientRect().top + scrollParent.scrollTop;
514
+ scrollParent.scrollTo({ top: offset - 32, behavior: "smooth" });
515
+ } else {
516
+ firstMatch.scrollIntoView({ behavior: "smooth", block: "start" });
517
+ }
518
+ });
519
+ }
520
+ return () => clearLineAnchorHighlights(container);
521
+ }
522
+ function findScrollParent(el) {
523
+ let node = el;
524
+ while (node) {
525
+ const overflow = getComputedStyle(node).overflowY;
526
+ if (overflow === "auto" || overflow === "scroll") return node;
527
+ node = node.parentElement;
528
+ }
529
+ return null;
530
+ }
531
+
532
+ // src/useLineAnchor.ts
533
+ var HIGHLIGHT_CLASS2 = "line-anchor-highlight";
534
+ function useLineAnchor(containerRef, hash) {
535
+ const clearHighlights = useCallback(() => {
536
+ const el = containerRef.current;
537
+ if (!el) return;
538
+ clearLineAnchorHighlights(el);
539
+ }, [containerRef]);
540
+ useEffect(() => {
541
+ const el = containerRef.current;
542
+ if (!el) return;
543
+ clearHighlights();
544
+ const range = parseLineAnchor(hash);
545
+ if (!range) return;
546
+ const blocks = el.querySelectorAll("[data-source-line]");
547
+ let firstMatch = null;
548
+ for (const block of blocks) {
549
+ const line = parseInt(
550
+ block.dataset.sourceLine || "0",
551
+ 10
552
+ );
553
+ if (line >= range.start && line <= range.end) {
554
+ block.classList.add(HIGHLIGHT_CLASS2);
555
+ if (!firstMatch) firstMatch = block;
556
+ }
557
+ }
558
+ if (!firstMatch) {
559
+ let closest = null;
560
+ let closestLine = 0;
561
+ for (const block of blocks) {
562
+ const line = parseInt(
563
+ block.dataset.sourceLine || "0",
564
+ 10
565
+ );
566
+ if (line <= range.start && line > closestLine) {
567
+ closestLine = line;
568
+ closest = block;
569
+ }
570
+ }
571
+ if (closest) {
572
+ closest.classList.add(HIGHLIGHT_CLASS2);
573
+ firstMatch = closest;
574
+ }
575
+ }
576
+ if (firstMatch) {
577
+ let scrollParent = el;
578
+ while (scrollParent) {
579
+ const overflow = getComputedStyle(scrollParent).overflowY;
580
+ if (overflow === "auto" || overflow === "scroll") break;
581
+ scrollParent = scrollParent.parentElement;
582
+ }
583
+ if (scrollParent) {
584
+ const target = firstMatch;
585
+ requestAnimationFrame(() => {
586
+ const offset = target.getBoundingClientRect().top - scrollParent.getBoundingClientRect().top + scrollParent.scrollTop;
587
+ scrollParent.scrollTo({ top: offset - 32, behavior: "smooth" });
588
+ });
589
+ }
590
+ }
591
+ }, [hash, containerRef, clearHighlights]);
592
+ useEffect(() => {
593
+ const handler = (e) => {
594
+ if (e.key === "Escape") {
595
+ clearHighlights();
596
+ if (window.location.hash) {
597
+ history.replaceState(null, "", window.location.pathname);
598
+ }
599
+ }
600
+ };
601
+ document.addEventListener("keydown", handler);
602
+ return () => document.removeEventListener("keydown", handler);
603
+ }, [clearHighlights]);
604
+ useEffect(() => {
605
+ const el = containerRef.current;
606
+ if (!el) return;
607
+ const handler = (e) => {
608
+ const target = e.target;
609
+ if (target.closest(`.${HIGHLIGHT_CLASS2}`)) {
610
+ clearHighlights();
611
+ if (window.location.hash) {
612
+ history.replaceState(null, "", window.location.pathname);
613
+ }
614
+ }
615
+ };
616
+ el.addEventListener("click", handler);
617
+ return () => el.removeEventListener("click", handler);
618
+ }, [containerRef, clearHighlights]);
619
+ return { clearHighlights };
620
+ }
621
+ var MarkdownViewerInner = ({
622
+ content,
623
+ currentPath = "",
624
+ hash = "",
625
+ baseUrl = "/",
626
+ imageApiBase,
627
+ className,
628
+ onNavigate
629
+ }) => {
630
+ const containerRef = useRef(null);
631
+ useLineAnchor(containerRef, hash);
632
+ const { frontmatter, body } = useMemo(() => {
633
+ return parseFrontmatter(content);
634
+ }, [content]);
635
+ const handleLinkClick = useCallback(
636
+ (e, href) => {
637
+ if (href.startsWith("#")) {
638
+ e.preventDefault();
639
+ const id = href.slice(1);
640
+ const el = document.getElementById(id);
641
+ if (el) {
642
+ const scrollContainer = el.closest("[data-content-scroll]") || el.closest(".overflow-y-auto");
643
+ if (scrollContainer) {
644
+ const offset = el.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top + scrollContainer.scrollTop;
645
+ scrollContainer.scrollTo({ top: offset - 16 });
646
+ } else {
647
+ el.scrollIntoView();
648
+ }
649
+ }
650
+ return;
651
+ }
652
+ if (href.startsWith("http") || href.startsWith("mailto:")) return;
653
+ if (onNavigate) {
654
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
655
+ e.preventDefault();
656
+ const [pathPart, hashPart] = href.split("#");
657
+ const parts = currentPath.split("/");
658
+ parts.pop();
659
+ const dir = parts.join("/");
660
+ const cleanHref = pathPart.replace(/^\.\//, "");
661
+ const resolvedPath = dir ? `${dir}/${cleanHref}` : cleanHref;
662
+ onNavigate(resolvedPath + (hashPart ? `#${hashPart}` : ""));
663
+ }
664
+ },
665
+ [currentPath, onNavigate]
666
+ );
667
+ const resolveHref = useCallback(
668
+ (href) => {
669
+ if (!href) return "";
670
+ if (href.startsWith("http") || href.startsWith("mailto:") || href.startsWith("#") || href.startsWith("/")) {
671
+ return href;
672
+ }
673
+ const [pathPart, hashPart] = href.split("#");
674
+ const parts = currentPath.split("/");
675
+ parts.pop();
676
+ const dir = parts.join("/");
677
+ const cleanHref = pathPart.replace(/^\.\//, "");
678
+ const resolvedPath = dir ? `${dir}/${cleanHref}` : cleanHref;
679
+ return `${baseUrl}${resolvedPath}${hashPart ? `#${hashPart}` : ""}`;
680
+ },
681
+ [currentPath, baseUrl]
682
+ );
683
+ const transformImageUri = useCallback(
684
+ (uri, key) => {
685
+ if (key === "href") return uri;
686
+ if (uri.startsWith("http") || uri.startsWith("data:")) return uri;
687
+ const parts = currentPath.split("/");
688
+ parts.pop();
689
+ const dir = parts.join("/");
690
+ const resolvedPath = dir ? `${dir}/${uri}` : uri;
691
+ if (imageApiBase) {
692
+ return `${imageApiBase}/content?path=${encodeURIComponent(resolvedPath)}`;
693
+ }
694
+ return `${baseUrl}${resolvedPath}`;
695
+ },
696
+ [currentPath, baseUrl, imageApiBase]
697
+ );
698
+ const markdownComponents = useMemo(
699
+ () => ({
700
+ a({
701
+ href,
702
+ children,
703
+ ...props
704
+ }) {
705
+ const resolvedHref = resolveHref(href);
706
+ return /* @__PURE__ */ jsx(
707
+ "a",
708
+ {
709
+ href: resolvedHref,
710
+ onClick: (e) => href && handleLinkClick(e, href),
711
+ ...props,
712
+ children
713
+ }
714
+ );
715
+ },
716
+ code(props) {
717
+ const { children, className: codeClassName, ...rest } = props;
718
+ const match = /language-(\w+)/.exec(codeClassName || "");
719
+ if (match && match[1] === "mermaid") {
720
+ return /* @__PURE__ */ jsx(MermaidDiagram, { code: String(children).replace(/\n$/, "") });
721
+ }
722
+ return /* @__PURE__ */ jsx("code", { className: codeClassName, ...rest, children });
723
+ }
724
+ }),
725
+ [handleLinkClick, resolveHref]
726
+ );
727
+ const proseClasses = [
728
+ "prose prose-slate dark:prose-invert max-w-none",
729
+ "prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-slate-900 dark:prose-headings:text-slate-100",
730
+ "prose-h1:text-[2em] prose-h1:mb-3 prose-h1:pb-[0.3em] prose-h1:border-b prose-h1:border-slate-200 dark:prose-h1:border-slate-700",
731
+ "prose-h2:text-[1.5em] prose-h2:mt-6 prose-h2:mb-3 prose-h2:pb-[0.3em] prose-h2:border-b prose-h2:border-slate-200 dark:prose-h2:border-slate-700",
732
+ "prose-h3:text-[1.25em] prose-h3:mt-6 prose-h3:mb-2",
733
+ "prose-h4:text-[1em] prose-h4:mt-6 prose-h4:mb-2",
734
+ "prose-p:text-slate-700 dark:prose-p:text-slate-300 prose-p:leading-[1.5] prose-p:my-[16px]",
735
+ "prose-ul:my-[16px] prose-ul:list-disc prose-li:my-0.5 prose-li:marker:text-slate-900 dark:prose-li:marker:text-slate-300",
736
+ "prose-ol:my-[16px] prose-li:marker:text-slate-900 dark:prose-li:marker:text-slate-300",
737
+ "prose-pre:bg-slate-50 dark:prose-pre:bg-slate-800 prose-pre:border prose-pre:border-slate-200 dark:prose-pre:border-slate-700 prose-pre:p-4 prose-pre:rounded-md prose-pre:text-[85%] prose-pre:leading-[1.45]",
738
+ "prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline",
739
+ "prose-img:rounded-lg prose-img:my-4",
740
+ "prose-blockquote:border-l-[0.25em] prose-blockquote:border-slate-300 dark:prose-blockquote:border-slate-600 prose-blockquote:pl-4 prose-blockquote:text-slate-600 dark:prose-blockquote:text-slate-400 prose-blockquote:italic",
741
+ "prose-code:before:content-none prose-code:after:content-none",
742
+ "prose-code:bg-slate-100 dark:prose-code:bg-slate-800 prose-code:px-[0.4em] prose-code:py-[0.2em] prose-code:rounded-md prose-code:text-slate-800 dark:prose-code:text-slate-200 prose-code:font-mono prose-code:text-[85%] prose-code:font-normal prose-code:border prose-code:border-slate-200/50 dark:prose-code:border-slate-700/50",
743
+ "prose-table:text-sm",
744
+ "prose-th:px-3 prose-th:py-1.5 prose-th:border prose-th:border-slate-200 dark:prose-th:border-slate-700",
745
+ "prose-td:px-3 prose-td:py-1.5 prose-td:border prose-td:border-slate-200 dark:prose-td:border-slate-700"
746
+ ].join(" ");
747
+ return /* @__PURE__ */ jsxs(
748
+ "div",
749
+ {
750
+ ref: containerRef,
751
+ className: className ? `${proseClasses} ${className}` : proseClasses,
752
+ children: [
753
+ /* @__PURE__ */ jsx(FrontmatterDisplay, { frontmatter }),
754
+ /* @__PURE__ */ jsx(
755
+ ReactMarkdown,
756
+ {
757
+ remarkPlugins: [
758
+ [remarkGfm, { singleTilde: false }],
759
+ [remarkMath, { singleDollarTextMath: false }]
760
+ ],
761
+ rehypePlugins: [
762
+ rehypeRaw,
763
+ rehypeSourceLines_default,
764
+ [rehypeSanitize, sanitizeSchema],
765
+ rehypeSlug,
766
+ rehypeHighlight,
767
+ rehypeKatex
768
+ ],
769
+ urlTransform: transformImageUri,
770
+ components: markdownComponents,
771
+ children: body
772
+ }
773
+ )
774
+ ]
775
+ }
776
+ );
777
+ };
778
+ var MarkdownViewer = memo(
779
+ MarkdownViewerInner,
780
+ (prevProps, nextProps) => prevProps.content === nextProps.content && prevProps.currentPath === nextProps.currentPath && prevProps.hash === nextProps.hash && prevProps.className === nextProps.className
781
+ );
782
+ async function renderMarkdown(content, options = {}) {
783
+ const {
784
+ gfm = true,
785
+ math = true,
786
+ highlight = true,
787
+ sourceLines = true,
788
+ sanitize = true,
789
+ frontmatter: parseFm = true
790
+ } = options;
791
+ let parsed;
792
+ if (parseFm) {
793
+ parsed = parseFrontmatter(content);
794
+ } else {
795
+ parsed = { frontmatter: {}, body: content, format: "none" };
796
+ }
797
+ const remarkPlugins = [];
798
+ const rehypePlugins = [];
799
+ if (gfm) remarkPlugins.push([remarkGfm, { singleTilde: false }]);
800
+ if (math) remarkPlugins.push([remarkMath, { singleDollarTextMath: false }]);
801
+ rehypePlugins.push([rehypeRaw]);
802
+ if (sourceLines) rehypePlugins.push([rehypeSourceLines_default]);
803
+ if (sanitize) rehypePlugins.push([rehypeSanitize, sanitizeSchema]);
804
+ rehypePlugins.push([rehypeSlug]);
805
+ if (highlight) rehypePlugins.push([rehypeHighlight]);
806
+ if (math) rehypePlugins.push([rehypeKatex]);
807
+ let processor = unified().use(remarkParse);
808
+ for (const [plugin, ...args] of remarkPlugins) {
809
+ processor = processor.use(plugin, ...args);
810
+ }
811
+ processor = processor.use(remarkRehype, { allowDangerousHtml: true });
812
+ for (const [plugin, ...args] of rehypePlugins) {
813
+ processor = processor.use(plugin, ...args);
814
+ }
815
+ processor = processor.use(rehypeStringify);
816
+ const result = await processor.process(parsed.body);
817
+ return {
818
+ html: String(result),
819
+ frontmatter: parsed.frontmatter,
820
+ body: parsed.body
821
+ };
822
+ }
823
+
824
+ export { FrontmatterDisplay, MarkdownViewer, MermaidDiagram, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines_default as rehypeSourceLines, renderMarkdown, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
825
+ //# sourceMappingURL=react.js.map
826
+ //# sourceMappingURL=react.js.map