@owomark/react 0.1.5 → 0.1.7

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/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  // src/OwoMarkEditor.tsx
2
2
  import {
3
- useRef as useRef3,
4
- useEffect as useEffect2,
3
+ useRef as useRef4,
4
+ useEffect as useEffect3,
5
5
  useImperativeHandle,
6
6
  forwardRef,
7
- useCallback
7
+ useCallback as useCallback2
8
8
  } from "react";
9
9
  import { getThemeClassName } from "@owomark/view";
10
10
 
11
11
  // src/editor/EditorBlock.tsx
12
12
  import { memo as memo3, useMemo } from "react";
13
- import { BLOCK_TYPE_TO_CLASS, BQ_STEP, buildBlockquoteBarsBoxShadow } from "@owomark/core";
13
+ import { BLOCK_TYPE_TO_CLASS, BQ_STEP, buildBlockquoteBarsBoxShadow } from "@owomark/core/browser";
14
14
 
15
15
  // src/editor/EditorDecorator.tsx
16
16
  import { memo as memo2 } from "react";
17
- import { TOKEN_TO_CLASS } from "@owomark/core";
17
+ import { TOKEN_TO_CLASS } from "@owomark/core/browser";
18
18
 
19
19
  // src/editor/EditorLeaf.tsx
20
20
  import { memo } from "react";
@@ -58,6 +58,9 @@ var EditorBlock = memo3(
58
58
  {
59
59
  "data-owo-block": blockIndex,
60
60
  "data-block-id": block.id,
61
+ "data-source-key": block.sourceKey,
62
+ "data-source-line-start": block.startLine,
63
+ "data-source-line-end": block.endLine,
61
64
  className,
62
65
  style: bqStyle,
63
66
  ...headingLevel ? { "data-owo-heading": headingLevel } : {},
@@ -224,33 +227,135 @@ function groupByCategory(commands) {
224
227
  return groups;
225
228
  }
226
229
 
227
- // src/useOwoMarkCore.ts
230
+ // src/toolbar/Toolbar.tsx
228
231
  import {
229
- useRef as useRef2,
232
+ useMemo as useMemo2,
230
233
  useState as useState2,
234
+ useRef as useRef2,
235
+ useEffect as useEffect2,
236
+ useCallback,
237
+ memo as memo5
238
+ } from "react";
239
+ import {
240
+ editorEntryRegistry
241
+ } from "@owomark/core";
242
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
243
+ var EMPTY_IDS = [];
244
+ var COLLAPSE_BREAKPOINT = 420;
245
+ var Toolbar = memo5(function Toolbar2({ core, toolbarConfig }) {
246
+ const commandIds = toolbarConfig.componentCommands ?? EMPTY_IDS;
247
+ const cacheKey = commandIds.join(",");
248
+ const commands = useMemo2(() => {
249
+ const registry = core.getCommandRegistry();
250
+ const result = [];
251
+ for (const id of commandIds) {
252
+ const descriptor = editorEntryRegistry.list().find((entry) => entry.commandId === id);
253
+ if (!descriptor || descriptor.entryKind !== "component") {
254
+ if (process.env.NODE_ENV !== "production") {
255
+ console.warn(`[OwoMark Toolbar] Unknown component command id: "${id}", skipping.`);
256
+ }
257
+ continue;
258
+ }
259
+ const cmd = registry.get(id);
260
+ if (cmd && cmd.toolbarEligible) {
261
+ result.push(cmd);
262
+ }
263
+ }
264
+ return result;
265
+ }, [core, cacheKey]);
266
+ const containerRef = useRef2(null);
267
+ const [collapsed, setCollapsed] = useState2(false);
268
+ const [popoverOpen, setPopoverOpen] = useState2(false);
269
+ const popoverRef = useRef2(null);
270
+ useEffect2(() => {
271
+ const el = containerRef.current;
272
+ if (!el || typeof ResizeObserver === "undefined") return;
273
+ const ro = new ResizeObserver((entries) => {
274
+ for (const entry of entries) {
275
+ setCollapsed(entry.contentRect.width < COLLAPSE_BREAKPOINT);
276
+ }
277
+ });
278
+ ro.observe(el);
279
+ return () => ro.disconnect();
280
+ }, []);
281
+ useEffect2(() => {
282
+ if (!popoverOpen) return;
283
+ function handlePointerDown(e) {
284
+ if (popoverRef.current && !popoverRef.current.contains(e.target)) {
285
+ setPopoverOpen(false);
286
+ }
287
+ }
288
+ document.addEventListener("pointerdown", handlePointerDown, true);
289
+ return () => document.removeEventListener("pointerdown", handlePointerDown, true);
290
+ }, [popoverOpen]);
291
+ const handleCommandClick = useCallback((e, cmdId) => {
292
+ e.preventDefault();
293
+ core.executeCommandById(cmdId);
294
+ setPopoverOpen(false);
295
+ }, [core]);
296
+ if (commands.length === 0) return null;
297
+ const buttons = commands.map((cmd) => /* @__PURE__ */ jsxs2(
298
+ "button",
299
+ {
300
+ type: "button",
301
+ className: "owo-toolbar__component-button",
302
+ onPointerDown: (e) => handleCommandClick(e, cmd.id),
303
+ children: [
304
+ cmd.icon && /* @__PURE__ */ jsx5("span", { className: "owo-toolbar__component-icon", children: cmd.icon }),
305
+ /* @__PURE__ */ jsx5("span", { className: "owo-toolbar__component-label", children: cmd.label })
306
+ ]
307
+ },
308
+ cmd.id
309
+ ));
310
+ return /* @__PURE__ */ jsx5("div", { ref: containerRef, className: "owo-toolbar__components-group", role: "toolbar", "aria-label": "Components", children: collapsed ? /* @__PURE__ */ jsxs2("div", { style: { position: "relative" }, children: [
311
+ /* @__PURE__ */ jsxs2(
312
+ "button",
313
+ {
314
+ type: "button",
315
+ className: "owo-toolbar__collapse-trigger",
316
+ onPointerDown: (e) => {
317
+ e.preventDefault();
318
+ setPopoverOpen((v) => !v);
319
+ },
320
+ children: [
321
+ /* @__PURE__ */ jsx5("span", { className: "owo-toolbar__component-label", children: "Components" }),
322
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", children: popoverOpen ? "\u25B2" : "\u25BC" })
323
+ ]
324
+ }
325
+ ),
326
+ popoverOpen && /* @__PURE__ */ jsx5("div", { ref: popoverRef, className: "owo-toolbar__popover", children: buttons })
327
+ ] }) : buttons });
328
+ });
329
+
330
+ // src/useOwoMarkCore.ts
331
+ import {
332
+ useRef as useRef3,
333
+ useState as useState3,
231
334
  useLayoutEffect as useLayoutEffect2
232
335
  } from "react";
233
336
  import {
234
337
  createOwoMarkCore,
235
- readSelection,
236
- restoreSelection,
237
- invalidateBlockCache,
238
338
  virtualToLinear
239
339
  } from "@owomark/core";
340
+ import {
341
+ readSelection,
342
+ restoreSelection,
343
+ invalidateBlockCache
344
+ } from "@owomark/core/browser";
240
345
  function useOwoMarkCore(options) {
241
346
  const { initialMarkdown, readOnly = false } = options;
242
- const containerRef = useRef2(null);
243
- const coreRef = useRef2(null);
244
- const composingRef = useRef2(false);
245
- const pendingSelectionRef = useRef2(null);
246
- const onCompositionStateChangeRef = useRef2(options.onCompositionStateChange);
347
+ const containerRef = useRef3(null);
348
+ const coreRef = useRef3(null);
349
+ const composingRef = useRef3(false);
350
+ const pendingSelectionRef = useRef3(null);
351
+ const onCompositionStateChangeRef = useRef3(options.onCompositionStateChange);
247
352
  onCompositionStateChangeRef.current = options.onCompositionStateChange;
248
353
  if (!coreRef.current) {
249
354
  coreRef.current = createOwoMarkCore({ initialMarkdown });
250
355
  }
251
356
  const core = coreRef.current;
252
- const [doc, setDoc] = useState2(() => core.getDocument());
253
- const [slashState, setSlashState] = useState2(() => core.getSlashState());
357
+ const [doc, setDoc] = useState3(() => core.getDocument());
358
+ const [slashState, setSlashState] = useState3(() => core.getSlashState());
254
359
  useLayoutEffect2(() => {
255
360
  const el = containerRef.current;
256
361
  if (!el) return;
@@ -407,13 +512,25 @@ function useOwoMarkCore(options) {
407
512
  }
408
513
 
409
514
  // src/config.ts
515
+ var DEFAULT_TOOLBAR_COMPONENT_COMMANDS = [
516
+ "component-note",
517
+ "component-callout",
518
+ "component-code-demo",
519
+ "component-details",
520
+ "component-steps",
521
+ "component-tabs"
522
+ ];
410
523
  var DEFAULT_EDITOR_CONFIG = {
411
524
  indentMode: "auto",
412
525
  enableSideAnnotation: true,
413
526
  enableMath: true,
527
+ enableComponents: true,
414
528
  enableMarkdownSandbox: true,
415
529
  markdownSandbox: {
416
530
  outerFenceTicks: 4
531
+ },
532
+ toolbarConfig: {
533
+ componentCommands: DEFAULT_TOOLBAR_COMPONENT_COMMANDS
417
534
  }
418
535
  };
419
536
  function resolveEditorConfig(config) {
@@ -423,6 +540,10 @@ function resolveEditorConfig(config) {
423
540
  markdownSandbox: {
424
541
  ...DEFAULT_EDITOR_CONFIG.markdownSandbox,
425
542
  ...config?.markdownSandbox
543
+ },
544
+ toolbarConfig: {
545
+ ...DEFAULT_EDITOR_CONFIG.toolbarConfig,
546
+ ...config?.toolbarConfig
426
547
  }
427
548
  };
428
549
  }
@@ -430,6 +551,7 @@ function deriveProcessorOptions(config) {
430
551
  return {
431
552
  enableSideAnnotation: config.enableSideAnnotation,
432
553
  enableMath: config.enableMath,
554
+ enableComponents: config.enableComponents,
433
555
  enableMarkdownSandbox: config.enableMarkdownSandbox,
434
556
  markdownSandbox: config.markdownSandbox
435
557
  };
@@ -437,7 +559,7 @@ function deriveProcessorOptions(config) {
437
559
 
438
560
  // src/OwoMarkEditor.tsx
439
561
  import "@owomark/view/style.css";
440
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
562
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
441
563
  var OwoMarkEditor = forwardRef(
442
564
  function OwoMarkEditor2(props, ref) {
443
565
  const {
@@ -455,6 +577,7 @@ var OwoMarkEditor = forwardRef(
455
577
  commandsRef,
456
578
  coreRef: externalCoreRef,
457
579
  controller,
580
+ scrollController,
458
581
  indentMode: indentModeProp = "auto",
459
582
  ariaLabel,
460
583
  config: configProp
@@ -464,21 +587,24 @@ var OwoMarkEditor = forwardRef(
464
587
  const theme = themeProp;
465
588
  const enableSideAnnotation = resolved.enableSideAnnotation;
466
589
  const enableMath = resolved.enableMath;
590
+ const enableComponents = resolved.enableComponents;
591
+ const toolbarConfig = resolved.toolbarConfig;
467
592
  const isControlled = value !== void 0;
468
- const lastExternalValue = useRef3(value ?? defaultValue ?? "");
469
- const suppressOnChange = useRef3(false);
593
+ const lastExternalValue = useRef4(value ?? defaultValue ?? "");
594
+ const suppressOnChange = useRef4(false);
595
+ const boundSurfaceRef = useRef4(null);
470
596
  const { core, doc, slashState, containerRef } = useOwoMarkCore({
471
597
  initialMarkdown: lastExternalValue.current,
472
598
  readOnly,
473
599
  onCompositionStateChange
474
600
  });
475
- const handleSlashSelect = useCallback((index) => {
601
+ const handleSlashSelect = useCallback2((index) => {
476
602
  core.executeSlashCommand(index);
477
603
  }, [core]);
478
- const handleSlashDismiss = useCallback(() => {
604
+ const handleSlashDismiss = useCallback2(() => {
479
605
  core.dismissSlashMenu();
480
606
  }, [core]);
481
- useEffect2(() => {
607
+ useEffect3(() => {
482
608
  if (typeof externalCoreRef === "function") externalCoreRef(core);
483
609
  else if (externalCoreRef && typeof externalCoreRef === "object") {
484
610
  externalCoreRef.current = core;
@@ -490,7 +616,7 @@ var OwoMarkEditor = forwardRef(
490
616
  }
491
617
  };
492
618
  }, [core, externalCoreRef]);
493
- useEffect2(() => {
619
+ useEffect3(() => {
494
620
  const unsub = core.onChange((markdown) => {
495
621
  if (suppressOnChange.current) return;
496
622
  lastExternalValue.current = markdown;
@@ -498,15 +624,45 @@ var OwoMarkEditor = forwardRef(
498
624
  });
499
625
  return unsub;
500
626
  }, [core, onChange]);
501
- useEffect2(() => {
627
+ useEffect3(() => {
502
628
  if (!onSelectionChange) return;
503
629
  return core.onSelectionChange(onSelectionChange);
504
630
  }, [core, onSelectionChange]);
505
- useEffect2(() => {
631
+ useEffect3(() => {
506
632
  if (!controller) return;
507
633
  return controller.connectEditor(core);
508
634
  }, [core, controller]);
509
- useEffect2(() => {
635
+ useEffect3(() => {
636
+ const root = containerRef.current;
637
+ if (!root || !scrollController) return;
638
+ const measure = () => {
639
+ scrollController.measureSurface("editor");
640
+ };
641
+ const resizeObserver = new ResizeObserver(measure);
642
+ resizeObserver.observe(root);
643
+ const mutationObserver = new MutationObserver(measure);
644
+ mutationObserver.observe(root, {
645
+ childList: true,
646
+ subtree: true,
647
+ characterData: true
648
+ });
649
+ const frame = requestAnimationFrame(measure);
650
+ let disposed = false;
651
+ if (typeof document !== "undefined" && document.fonts) {
652
+ document.fonts.ready.then(() => {
653
+ if (!disposed) {
654
+ measure();
655
+ }
656
+ });
657
+ }
658
+ return () => {
659
+ disposed = true;
660
+ cancelAnimationFrame(frame);
661
+ resizeObserver.disconnect();
662
+ mutationObserver.disconnect();
663
+ };
664
+ }, [doc, scrollController, containerRef]);
665
+ useEffect3(() => {
510
666
  if (!isControlled) return;
511
667
  if (value === lastExternalValue.current) return;
512
668
  suppressOnChange.current = true;
@@ -514,21 +670,24 @@ var OwoMarkEditor = forwardRef(
514
670
  lastExternalValue.current = value;
515
671
  suppressOnChange.current = false;
516
672
  }, [core, value, isControlled]);
517
- useEffect2(() => {
673
+ useEffect3(() => {
518
674
  core.setIndentMode(indentMode);
519
675
  }, [core, indentMode]);
520
- useEffect2(() => {
676
+ useEffect3(() => {
521
677
  core.setEnableSideAnnotation(enableSideAnnotation);
522
678
  }, [core, enableSideAnnotation]);
523
- useEffect2(() => {
679
+ useEffect3(() => {
524
680
  core.setEnableMath(enableMath);
525
681
  }, [core, enableMath]);
526
- useEffect2(() => {
682
+ useEffect3(() => {
683
+ core.setEnableComponents(enableComponents);
684
+ }, [core, enableComponents]);
685
+ useEffect3(() => {
527
686
  if (containerRef.current && ariaLabel) {
528
687
  containerRef.current.setAttribute("aria-label", ariaLabel);
529
688
  }
530
689
  }, [ariaLabel, containerRef]);
531
- useEffect2(() => {
690
+ useEffect3(() => {
532
691
  if (containerRef.current) {
533
692
  containerRef.current.contentEditable = readOnly ? "false" : "true";
534
693
  if (readOnly) {
@@ -558,13 +717,23 @@ var OwoMarkEditor = forwardRef(
558
717
  themeClassName,
559
718
  className
560
719
  ].filter(Boolean).join(" ");
561
- const setRefs = useCallback((el) => {
720
+ const setRefs = useCallback2((el) => {
721
+ if (scrollController && boundSurfaceRef.current !== el) {
722
+ if (boundSurfaceRef.current) {
723
+ scrollController.unbindSurface("editor", boundSurfaceRef.current);
724
+ }
725
+ if (el) {
726
+ scrollController.bindSurface("editor", el);
727
+ }
728
+ boundSurfaceRef.current = el;
729
+ }
562
730
  containerRef.current = el;
563
731
  if (typeof ref === "function") ref(el);
564
732
  else if (ref) ref.current = el;
565
- }, [ref, containerRef]);
566
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
567
- /* @__PURE__ */ jsx5(
733
+ }, [ref, containerRef, scrollController]);
734
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
735
+ /* @__PURE__ */ jsx6(Toolbar, { core, toolbarConfig }),
736
+ /* @__PURE__ */ jsx6(
568
737
  "div",
569
738
  {
570
739
  ref: setRefs,
@@ -572,10 +741,10 @@ var OwoMarkEditor = forwardRef(
572
741
  "data-placeholder": placeholder,
573
742
  onScroll,
574
743
  suppressContentEditableWarning: true,
575
- children: doc.blocks.map((block, i) => /* @__PURE__ */ jsx5(EditorBlock, { block, blockIndex: i }, block.id))
744
+ children: doc.blocks.map((block, i) => /* @__PURE__ */ jsx6(EditorBlock, { block, blockIndex: i }, block.id))
576
745
  }
577
746
  ),
578
- /* @__PURE__ */ jsx5(
747
+ /* @__PURE__ */ jsx6(
579
748
  SlashMenu,
580
749
  {
581
750
  state: slashState,
@@ -588,144 +757,1508 @@ var OwoMarkEditor = forwardRef(
588
757
  );
589
758
 
590
759
  // src/OwoMarkPreview.tsx
591
- import { useRef as useRef4, useEffect as useEffect3 } from "react";
760
+ import { forwardRef as forwardRef3, useEffect as useEffect8, useRef as useRef11 } from "react";
592
761
  import {
593
762
  createOwoMarkPreviewEngine
594
763
  } from "@owomark/view";
595
- import { jsx as jsx6 } from "react/jsx-runtime";
596
- function isController(store) {
597
- return typeof store.updateVisibleBlockIds === "function";
764
+
765
+ // src/MdxPreview.tsx
766
+ import {
767
+ Component,
768
+ forwardRef as forwardRef2,
769
+ useEffect as useEffect7,
770
+ useMemo as useMemo6,
771
+ useRef as useRef10,
772
+ useState as useState7
773
+ } from "react";
774
+ import {
775
+ DEFAULT_MDX_COMPONENTS
776
+ } from "@owomark/processor";
777
+
778
+ // src/MdxSkeleton.tsx
779
+ import { useMemo as useMemo3, useRef as useRef5 } from "react";
780
+ import { createSkeletonHtml, ensureSkeletonStyles } from "@owomark/view";
781
+ import { jsx as jsx7 } from "react/jsx-runtime";
782
+ function MdxSkeleton({ height, lines, className, style }) {
783
+ const injected = useRef5(false);
784
+ if (!injected.current && typeof document !== "undefined") {
785
+ ensureSkeletonStyles(document);
786
+ injected.current = true;
787
+ }
788
+ const html = useMemo3(
789
+ () => createSkeletonHtml({ height, lines: lines ?? (height == null ? 3 : void 0) }),
790
+ [height, lines]
791
+ );
792
+ return /* @__PURE__ */ jsx7(
793
+ "div",
794
+ {
795
+ className: className ?? void 0,
796
+ style,
797
+ dangerouslySetInnerHTML: { __html: html }
798
+ }
799
+ );
598
800
  }
599
- var OwoMarkPreview = function OwoMarkPreview2(props) {
600
- const {
601
- state,
602
- className,
603
- strategy,
604
- themeKey,
605
- registry,
606
- viewportFirst,
607
- ariaLabel,
608
- renderBlock,
609
- onContentUpdate
610
- } = props;
611
- const onContentUpdateRef = useRef4(onContentUpdate);
612
- onContentUpdateRef.current = onContentUpdate;
613
- const renderBlockRef = useRef4(renderBlock);
614
- renderBlockRef.current = renderBlock;
615
- const hasRenderBlock = !!renderBlock;
616
- const containerRef = useRef4(null);
617
- const engineRef = useRef4(null);
618
- useEffect3(() => {
619
- if (!containerRef.current) return;
620
- const engineOptions = {
621
- strategy,
622
- themeKey,
623
- registry,
624
- viewportFirst,
625
- renderBlock: hasRenderBlock ? (block, ctx) => renderBlockRef.current(block, ctx) : void 0,
626
- onContentUpdate: () => onContentUpdateRef.current?.()
801
+
802
+ // src/MdxComponentShell.tsx
803
+ import {
804
+ useRef as useRef6,
805
+ useState as useState4,
806
+ useEffect as useEffect4,
807
+ useLayoutEffect as useLayoutEffect3,
808
+ useCallback as useCallback3
809
+ } from "react";
810
+ import { jsx as jsx8 } from "react/jsx-runtime";
811
+ var componentHeightCache = /* @__PURE__ */ new Map();
812
+ var DEFAULT_ESTIMATED_HEIGHT = 80;
813
+ function shallowPropsFingerprint(props) {
814
+ const keys = Object.keys(props).sort();
815
+ const parts = [];
816
+ for (const key of keys) {
817
+ const val = props[key];
818
+ if (typeof val === "function" || typeof val === "object") continue;
819
+ parts.push(`${key}=${String(val)}`);
820
+ }
821
+ return parts.join("&");
822
+ }
823
+ function MdxComponentShell({ name, propsFingerprint, children }) {
824
+ const containerRef = useRef6(null);
825
+ const [status, setStatus] = useState4("skeleton");
826
+ const cacheKey = propsFingerprint ? `${name}:${propsFingerprint}` : name;
827
+ const estimatedHeight = componentHeightCache.get(cacheKey) ?? DEFAULT_ESTIMATED_HEIGHT;
828
+ useEffect4(() => {
829
+ const id = requestAnimationFrame(() => setStatus("mounting"));
830
+ return () => cancelAnimationFrame(id);
831
+ }, []);
832
+ useLayoutEffect3(() => {
833
+ if (status !== "mounting") return;
834
+ const id = requestAnimationFrame(() => setStatus("ready"));
835
+ return () => cancelAnimationFrame(id);
836
+ }, [status]);
837
+ const measureHeight = useCallback3(() => {
838
+ const el = containerRef.current;
839
+ if (!el) return;
840
+ const h = el.getBoundingClientRect().height;
841
+ if (h > 0) {
842
+ componentHeightCache.set(cacheKey, h);
843
+ }
844
+ }, [cacheKey]);
845
+ useEffect4(() => {
846
+ const el = containerRef.current;
847
+ if (!el || status !== "ready") return;
848
+ measureHeight();
849
+ const ro = new ResizeObserver(() => measureHeight());
850
+ ro.observe(el);
851
+ return () => ro.disconnect();
852
+ }, [status, measureHeight]);
853
+ if (status === "skeleton") {
854
+ return /* @__PURE__ */ jsx8(MdxSkeleton, { height: estimatedHeight });
855
+ }
856
+ return /* @__PURE__ */ jsx8("div", { ref: containerRef, children });
857
+ }
858
+ function wrapWithShell(name, Component2) {
859
+ function ShellWrapped(props) {
860
+ const fingerprint = shallowPropsFingerprint(props);
861
+ return /* @__PURE__ */ jsx8(MdxComponentShell, { name, propsFingerprint: fingerprint, children: /* @__PURE__ */ jsx8(Component2, { ...props }) });
862
+ }
863
+ ShellWrapped.displayName = `MdxShell(${name})`;
864
+ return ShellWrapped;
865
+ }
866
+ function clearComponentHeightCache() {
867
+ componentHeightCache.clear();
868
+ }
869
+
870
+ // src/mdx-error.ts
871
+ function parseMdxErrorDetails(error) {
872
+ if (error && typeof error === "object") {
873
+ const value = error;
874
+ if (typeof value.line === "number") {
875
+ return {
876
+ message: value.message ?? String(error),
877
+ line: value.line,
878
+ column: typeof value.column === "number" ? value.column : void 0
879
+ };
880
+ }
881
+ const text = value.message ?? String(error);
882
+ const match = text.match(/\((\d+):(\d+)/);
883
+ if (match) {
884
+ return {
885
+ message: text,
886
+ line: Number(match[1]),
887
+ column: Number(match[2])
888
+ };
889
+ }
890
+ return { message: text };
891
+ }
892
+ return { message: String(error) };
893
+ }
894
+ function parseCompileError(error, kind) {
895
+ return {
896
+ ...parseMdxErrorDetails(error),
897
+ kind
898
+ };
899
+ }
900
+
901
+ // src/useMdxPreviewCompilation.ts
902
+ import { useEffect as useEffect6, useMemo as useMemo5, useRef as useRef8, useState as useState6 } from "react";
903
+ import { deriveRenderKey as deriveRenderKey2 } from "@owomark/core";
904
+
905
+ // src/mdx-runtime.ts
906
+ import { compile, run } from "@mdx-js/mdx";
907
+ import * as jsxRuntime from "react/jsx-runtime";
908
+ import {
909
+ getOwoMarkPlugins,
910
+ allBuiltinDescriptors,
911
+ inspectMdxSource,
912
+ remapMdxErrorDetails
913
+ } from "@owomark/processor";
914
+ var workerInstance = null;
915
+ var workerRefCount = 0;
916
+ var requestCounter = 0;
917
+ var WORKER_COMPILE_TIMEOUT_MS = 5e3;
918
+ var MAX_CRASHES = 3;
919
+ var CRASH_COOLDOWN_MS = 5e3;
920
+ var CRASH_WINDOW_MS = 3e4;
921
+ var permanentlyFailed = false;
922
+ var crashTimestamps = [];
923
+ var pendingRequests = /* @__PURE__ */ new Map();
924
+ function recordCrash() {
925
+ const now = Date.now();
926
+ crashTimestamps.push(now);
927
+ while (crashTimestamps.length > 0 && now - crashTimestamps[0] > CRASH_WINDOW_MS) {
928
+ crashTimestamps.shift();
929
+ }
930
+ if (crashTimestamps.length >= MAX_CRASHES) {
931
+ permanentlyFailed = true;
932
+ }
933
+ }
934
+ function canCreateWorker() {
935
+ if (permanentlyFailed) return false;
936
+ if (crashTimestamps.length === 0) return true;
937
+ return Date.now() - crashTimestamps[crashTimestamps.length - 1] >= CRASH_COOLDOWN_MS;
938
+ }
939
+ function tryCreateWorker() {
940
+ if (workerInstance) return workerInstance;
941
+ if (!canCreateWorker()) return null;
942
+ try {
943
+ if (typeof Worker === "undefined") {
944
+ permanentlyFailed = true;
945
+ return null;
946
+ }
947
+ const worker = new Worker(
948
+ new URL("./mdx.worker.js", import.meta.url),
949
+ { type: "module" }
950
+ );
951
+ worker.onmessage = (e) => {
952
+ const response = e.data;
953
+ const pending = pendingRequests.get(response.id);
954
+ if (!pending) return;
955
+ pendingRequests.delete(response.id);
956
+ pending.cleanup?.();
957
+ if (response.ok) {
958
+ pending.resolve(response.code);
959
+ } else {
960
+ pending.reject({ ...response.error, kind: "compile" });
961
+ }
627
962
  };
628
- const engine = createOwoMarkPreviewEngine(engineOptions);
629
- engine.mount(containerRef.current);
630
- engineRef.current = engine;
631
- const currentState = state.getState();
632
- void engine.update(currentState);
633
- return () => {
634
- engine.destroy();
635
- engineRef.current = null;
963
+ worker.onerror = () => {
964
+ recordCrash();
965
+ worker.terminate();
966
+ workerInstance = null;
967
+ for (const [id, pending] of pendingRequests) {
968
+ pendingRequests.delete(id);
969
+ pending.cleanup?.();
970
+ pending.reject({ message: "MDX compile worker crashed", kind: "compile" });
971
+ }
636
972
  };
637
- }, [strategy, themeKey, registry, viewportFirst, hasRenderBlock]);
638
- useEffect3(() => {
639
- const unsub = state.subscribe((newState) => {
640
- const engine = engineRef.current;
641
- if (engine) {
642
- void engine.update(newState);
973
+ workerInstance = worker;
974
+ return worker;
975
+ } catch {
976
+ recordCrash();
977
+ return null;
978
+ }
979
+ }
980
+ function acquireMdxWorker() {
981
+ workerRefCount++;
982
+ }
983
+ function releaseMdxWorker() {
984
+ if (--workerRefCount <= 0) {
985
+ workerRefCount = 0;
986
+ if (workerInstance) {
987
+ workerInstance.terminate();
988
+ workerInstance = null;
989
+ }
990
+ for (const [, pending] of pendingRequests) {
991
+ pending.reject({ message: "Worker released", kind: "compile" });
992
+ }
993
+ pendingRequests.clear();
994
+ }
995
+ }
996
+ function compileInWorker(markdown, options, inspection) {
997
+ const maybeWorker = tryCreateWorker();
998
+ if (!maybeWorker) {
999
+ return {
1000
+ promise: Promise.reject(new Error("Worker unavailable")),
1001
+ cancel: () => {
1002
+ }
1003
+ };
1004
+ }
1005
+ const workerRef = maybeWorker;
1006
+ const id = ++requestCounter;
1007
+ let cancelled = false;
1008
+ let timeoutId = null;
1009
+ const cleanup = () => {
1010
+ if (timeoutId !== null) {
1011
+ clearTimeout(timeoutId);
1012
+ timeoutId = null;
1013
+ }
1014
+ };
1015
+ const promise = new Promise((resolve, reject) => {
1016
+ pendingRequests.set(id, { resolve, reject, cleanup });
1017
+ workerRef.postMessage({
1018
+ type: "compile",
1019
+ id,
1020
+ markdown,
1021
+ options: {
1022
+ enableMath: options.enableMath,
1023
+ enableSideAnnotation: options.enableSideAnnotation,
1024
+ enableCodeHighlight: options.enableCodeHighlight,
1025
+ sourceAnchors: options.sourceAnchors,
1026
+ extraRemarkDescriptors: options.extraRemarkDescriptors,
1027
+ extraRehypeDescriptors: options.extraRehypeDescriptors,
1028
+ preparedSource: inspection?.compileSource,
1029
+ sourceMap: inspection?.sourceMap
643
1030
  }
644
1031
  });
645
- return unsub;
646
- }, [state]);
647
- useEffect3(() => {
648
- const container = containerRef.current;
649
- if (!viewportFirst || !container || !isController(state)) return;
650
- const controller = state;
651
- const observer = new IntersectionObserver(
652
- (entries) => {
653
- const visible = /* @__PURE__ */ new Set();
654
- for (const id of controller.getState().visibleBlockIds) {
655
- visible.add(id);
656
- }
657
- for (const entry of entries) {
658
- const blockId = entry.target.getAttribute("data-block-id");
659
- if (!blockId) continue;
660
- if (entry.isIntersecting) {
661
- visible.add(blockId);
662
- } else {
663
- visible.delete(blockId);
664
- }
1032
+ timeoutId = setTimeout(() => {
1033
+ const pending = pendingRequests.get(id);
1034
+ if (!pending) return;
1035
+ pendingRequests.delete(id);
1036
+ cleanup();
1037
+ try {
1038
+ workerRef.postMessage({ type: "cancel", id });
1039
+ } catch {
1040
+ }
1041
+ pending.reject({ message: "MDX compile worker timed out", kind: "compile" });
1042
+ }, WORKER_COMPILE_TIMEOUT_MS);
1043
+ });
1044
+ function cancel() {
1045
+ if (cancelled) return;
1046
+ cancelled = true;
1047
+ const pending = pendingRequests.get(id);
1048
+ if (pending) {
1049
+ pendingRequests.delete(id);
1050
+ pending.cleanup?.();
1051
+ pending.reject({ message: "Compilation cancelled", kind: "compile" });
1052
+ }
1053
+ workerRef.postMessage({ type: "cancel", id });
1054
+ }
1055
+ return { promise, cancel };
1056
+ }
1057
+ async function compileOnMainThread(markdown, options, inspection) {
1058
+ const resolvedInspection = inspection ?? inspectMdxSource(markdown, {
1059
+ enableMath: options.enableMath,
1060
+ enableSideAnnotation: options.enableSideAnnotation,
1061
+ extraRemarkDescriptors: options.extraRemarkDescriptors,
1062
+ extraRemarkPlugins: options.extraRemarkPlugins
1063
+ });
1064
+ const { remarkPlugins, rehypePlugins } = getOwoMarkPlugins({
1065
+ mode: "production",
1066
+ enableMath: options.enableMath,
1067
+ enableSideAnnotation: options.enableSideAnnotation,
1068
+ enableCodeHighlight: options.enableCodeHighlight,
1069
+ sourceAnchors: options.sourceAnchors,
1070
+ extraRemarkDescriptors: options.extraRemarkDescriptors,
1071
+ extraRehypeDescriptors: options.extraRehypeDescriptors,
1072
+ extraRemarkPlugins: options.extraRemarkPlugins,
1073
+ extraRehypePlugins: options.extraRehypePlugins,
1074
+ mdxSourceMap: resolvedInspection.sourceMap
1075
+ });
1076
+ try {
1077
+ const result = await compile(resolvedInspection.compileSource, {
1078
+ outputFormat: "function-body",
1079
+ remarkPlugins,
1080
+ rehypePlugins
1081
+ });
1082
+ return String(result);
1083
+ } catch (error) {
1084
+ throw remapMdxErrorDetails(error, resolvedInspection.sourceMap);
1085
+ }
1086
+ }
1087
+ function requiresMainThread(options) {
1088
+ if (options.preferWorker !== true) return true;
1089
+ if (options.extraRemarkPlugins?.length || options.extraRehypePlugins?.length) return true;
1090
+ if (options.extraRemarkDescriptors?.length && !allBuiltinDescriptors(options.extraRemarkDescriptors)) return true;
1091
+ if (options.extraRehypeDescriptors?.length && !allBuiltinDescriptors(options.extraRehypeDescriptors)) return true;
1092
+ return false;
1093
+ }
1094
+ function compileMdx(markdown, options, inspection) {
1095
+ if (requiresMainThread(options)) {
1096
+ return {
1097
+ promise: compileOnMainThread(markdown, options, inspection),
1098
+ cancel: () => {
1099
+ }
1100
+ };
1101
+ }
1102
+ const workerCompile = compileInWorker(markdown, options, inspection);
1103
+ let settled = false;
1104
+ const promise = workerCompile.promise.catch(() => {
1105
+ if (settled) throw new Error("Compilation cancelled");
1106
+ return compileOnMainThread(markdown, options, inspection);
1107
+ });
1108
+ return {
1109
+ promise,
1110
+ cancel: () => {
1111
+ settled = true;
1112
+ workerCompile.cancel();
1113
+ }
1114
+ };
1115
+ }
1116
+ async function runMdxCode(code) {
1117
+ const module = await run(code, {
1118
+ ...jsxRuntime,
1119
+ baseUrl: import.meta.url
1120
+ });
1121
+ return {
1122
+ Content: module.default
1123
+ };
1124
+ }
1125
+
1126
+ // src/mdx-preview-utils.tsx
1127
+ import { renderBlockDefault } from "@owomark/view";
1128
+
1129
+ // src/AsyncCodeBlock.tsx
1130
+ import {
1131
+ Children,
1132
+ isValidElement,
1133
+ useEffect as useEffect5,
1134
+ useMemo as useMemo4,
1135
+ useRef as useRef7,
1136
+ useState as useState5
1137
+ } from "react";
1138
+
1139
+ // src/highlight-cache-key.ts
1140
+ import { deriveRenderKey } from "@owomark/core";
1141
+
1142
+ // src/highlight-types.ts
1143
+ var HIGHLIGHT_WORKER_TIMEOUT_MS = 3500;
1144
+ var HIGHLIGHT_COMPONENT_DEBOUNCE_MS = 120;
1145
+ var HIGHLIGHT_CACHE_MAX_ENTRIES = 200;
1146
+ var DEFAULT_HIGHLIGHT_THEME = "vitesse-light";
1147
+ var HIGHLIGHTER_VERSION = "owomark-shiki-js-v1";
1148
+
1149
+ // src/highlight-cache-key.ts
1150
+ function buildHighlightCacheKey(input) {
1151
+ return deriveRenderKey(
1152
+ [
1153
+ input.code,
1154
+ input.language ?? "",
1155
+ input.themeKey,
1156
+ input.meta ?? "",
1157
+ HIGHLIGHTER_VERSION
1158
+ ].join("\u241F"),
1159
+ "code-fence",
1160
+ ""
1161
+ );
1162
+ }
1163
+
1164
+ // src/highlight-worker-manager.ts
1165
+ var HighlightWorkerManager = class {
1166
+ timeoutMs;
1167
+ cacheMaxEntries;
1168
+ workerFactory;
1169
+ worker = null;
1170
+ workerFailureCount = 0;
1171
+ requestCounter = 0;
1172
+ cache = /* @__PURE__ */ new Map();
1173
+ inflightByCacheKey = /* @__PURE__ */ new Map();
1174
+ inflightByTaskId = /* @__PURE__ */ new Map();
1175
+ constructor(options = {}) {
1176
+ this.timeoutMs = options.timeoutMs ?? HIGHLIGHT_WORKER_TIMEOUT_MS;
1177
+ this.cacheMaxEntries = options.cacheMaxEntries ?? HIGHLIGHT_CACHE_MAX_ENTRIES;
1178
+ this.workerFactory = options.workerFactory;
1179
+ }
1180
+ highlight(input) {
1181
+ const request = {
1182
+ ...input,
1183
+ requestId: `highlight-${++this.requestCounter}`,
1184
+ themeKey: input.themeKey?.trim() || DEFAULT_HIGHLIGHT_THEME
1185
+ };
1186
+ const cached = this.getCached(request.cacheKey);
1187
+ if (cached) {
1188
+ return {
1189
+ request,
1190
+ promise: Promise.resolve({
1191
+ ...request,
1192
+ ok: true,
1193
+ html: cached
1194
+ }),
1195
+ cancel: () => {
665
1196
  }
666
- controller.updateVisibleBlockIds([...visible]);
667
- },
668
- { root: container, rootMargin: "200px 0px" }
669
- );
670
- const wrappers = container.querySelectorAll("[data-block-id]");
671
- for (const wrapper of wrappers) {
672
- observer.observe(wrapper);
673
- }
674
- const mutation = new MutationObserver((mutations) => {
675
- for (const m of mutations) {
676
- for (const node of m.addedNodes) {
677
- if (node instanceof HTMLElement && node.hasAttribute("data-block-id")) {
678
- observer.observe(node);
679
- }
1197
+ };
1198
+ }
1199
+ let task = this.inflightByCacheKey.get(request.cacheKey);
1200
+ if (!task) {
1201
+ task = this.startTask(request);
1202
+ }
1203
+ const promise = new Promise((resolve, reject) => {
1204
+ task.consumers.set(request.requestId, {
1205
+ request,
1206
+ resolve,
1207
+ reject
1208
+ });
1209
+ });
1210
+ return {
1211
+ request,
1212
+ promise,
1213
+ cancel: () => this.cancelRequest(request.requestId)
1214
+ };
1215
+ }
1216
+ resetForTesting() {
1217
+ this.worker?.terminate();
1218
+ this.worker = null;
1219
+ this.workerFailureCount = 0;
1220
+ this.requestCounter = 0;
1221
+ this.cache.clear();
1222
+ this.inflightByCacheKey.clear();
1223
+ this.inflightByTaskId.clear();
1224
+ }
1225
+ ensureWorker() {
1226
+ if (this.worker) {
1227
+ return this.worker;
1228
+ }
1229
+ if (!this.workerFactory && typeof Worker === "undefined") {
1230
+ return null;
1231
+ }
1232
+ try {
1233
+ const worker = this.workerFactory ? this.workerFactory() : new Worker(
1234
+ new URL("./highlight.worker.js", import.meta.url),
1235
+ { type: "module" }
1236
+ );
1237
+ if (!worker) {
1238
+ return null;
1239
+ }
1240
+ worker.onmessage = (event) => {
1241
+ this.handleWorkerMessage(event.data);
1242
+ };
1243
+ worker.onerror = () => {
1244
+ this.workerFailureCount += 1;
1245
+ this.worker?.terminate();
1246
+ this.worker = null;
1247
+ const tasks = [...this.inflightByTaskId.values()];
1248
+ for (const task of tasks) {
1249
+ this.finishTaskWithError(task, "Code highlight worker crashed");
680
1250
  }
1251
+ };
1252
+ this.worker = worker;
1253
+ return worker;
1254
+ } catch {
1255
+ this.workerFailureCount += 1;
1256
+ return null;
1257
+ }
1258
+ }
1259
+ startTask(request) {
1260
+ const task = {
1261
+ taskId: request.requestId,
1262
+ cacheKey: request.cacheKey,
1263
+ request,
1264
+ consumers: /* @__PURE__ */ new Map(),
1265
+ timeoutId: null
1266
+ };
1267
+ this.inflightByCacheKey.set(task.cacheKey, task);
1268
+ this.inflightByTaskId.set(task.taskId, task);
1269
+ const worker = this.ensureWorker();
1270
+ if (!worker) {
1271
+ queueMicrotask(() => {
1272
+ this.finishTaskWithError(task, "Code highlight worker unavailable");
1273
+ });
1274
+ return task;
1275
+ }
1276
+ task.timeoutId = setTimeout(() => {
1277
+ this.cancelTask(task, "Code highlight timed out");
1278
+ }, this.timeoutMs);
1279
+ worker.postMessage({
1280
+ type: "highlight",
1281
+ ...request
1282
+ });
1283
+ return task;
1284
+ }
1285
+ handleWorkerMessage(message) {
1286
+ const task = this.inflightByTaskId.get(message.taskId);
1287
+ if (!task) {
1288
+ return;
1289
+ }
1290
+ this.clearTaskTimeout(task);
1291
+ this.inflightByTaskId.delete(task.taskId);
1292
+ this.inflightByCacheKey.delete(task.cacheKey);
1293
+ if (message.ok) {
1294
+ this.setCached(task.cacheKey, message.html);
1295
+ }
1296
+ for (const consumer of task.consumers.values()) {
1297
+ consumer.resolve(message.ok ? {
1298
+ ...consumer.request,
1299
+ ok: true,
1300
+ html: message.html
1301
+ } : {
1302
+ ...consumer.request,
1303
+ ok: false,
1304
+ error: message.error
1305
+ });
1306
+ }
1307
+ }
1308
+ cancelRequest(requestId) {
1309
+ for (const task of this.inflightByTaskId.values()) {
1310
+ if (!task.consumers.has(requestId)) {
1311
+ continue;
681
1312
  }
1313
+ const consumer = task.consumers.get(requestId);
1314
+ task.consumers.delete(requestId);
1315
+ consumer.reject(new Error("Highlight request cancelled"));
1316
+ if (task.consumers.size === 0) {
1317
+ this.cancelTask(task, "Highlight request cancelled");
1318
+ }
1319
+ return;
1320
+ }
1321
+ }
1322
+ cancelTask(task, error) {
1323
+ this.worker?.postMessage({
1324
+ type: "cancel",
1325
+ taskId: task.taskId
1326
+ });
1327
+ this.finishTaskWithError(task, error);
1328
+ }
1329
+ finishTaskWithError(task, error) {
1330
+ this.clearTaskTimeout(task);
1331
+ this.inflightByTaskId.delete(task.taskId);
1332
+ this.inflightByCacheKey.delete(task.cacheKey);
1333
+ for (const consumer of task.consumers.values()) {
1334
+ consumer.resolve({
1335
+ ...consumer.request,
1336
+ ok: false,
1337
+ error
1338
+ });
1339
+ }
1340
+ task.consumers.clear();
1341
+ }
1342
+ clearTaskTimeout(task) {
1343
+ if (task.timeoutId !== null) {
1344
+ clearTimeout(task.timeoutId);
1345
+ task.timeoutId = null;
1346
+ }
1347
+ }
1348
+ getCached(cacheKey) {
1349
+ const cached = this.cache.get(cacheKey);
1350
+ if (!cached) {
1351
+ return null;
1352
+ }
1353
+ cached.touchedAt = Date.now();
1354
+ this.cache.delete(cacheKey);
1355
+ this.cache.set(cacheKey, cached);
1356
+ return cached.html;
1357
+ }
1358
+ setCached(cacheKey, html) {
1359
+ this.cache.delete(cacheKey);
1360
+ this.cache.set(cacheKey, {
1361
+ html,
1362
+ touchedAt: Date.now()
682
1363
  });
683
- mutation.observe(container, { childList: true, subtree: true });
1364
+ while (this.cache.size > this.cacheMaxEntries) {
1365
+ const oldestKey = this.cache.keys().next().value;
1366
+ if (!oldestKey) {
1367
+ break;
1368
+ }
1369
+ this.cache.delete(oldestKey);
1370
+ }
1371
+ }
1372
+ };
1373
+ var singletonManager = null;
1374
+ function getHighlightWorkerManager() {
1375
+ if (!singletonManager) {
1376
+ singletonManager = new HighlightWorkerManager();
1377
+ }
1378
+ return singletonManager;
1379
+ }
1380
+
1381
+ // src/AsyncCodeBlock.tsx
1382
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
1383
+ var nextBlockInstanceId = 1;
1384
+ function pickProps(source, omitKeys) {
1385
+ if (!source) {
1386
+ return {};
1387
+ }
1388
+ const next = {};
1389
+ for (const [key, value] of Object.entries(source)) {
1390
+ if (omitKeys.includes(key)) {
1391
+ continue;
1392
+ }
1393
+ next[key] = value;
1394
+ }
1395
+ return next;
1396
+ }
1397
+ function renderPlainCode(props, status) {
1398
+ const preProps = pickProps(props.preProps, ["children"]);
1399
+ const codeProps = pickProps(props.codeProps, ["children", "className"]);
1400
+ return /* @__PURE__ */ jsx9(
1401
+ "figure",
1402
+ {
1403
+ "data-rehype-pretty-code-figure": "",
1404
+ "data-owo-code-block-state": status,
1405
+ className: "owo-async-code-block",
1406
+ children: /* @__PURE__ */ jsx9(
1407
+ "pre",
1408
+ {
1409
+ ...preProps,
1410
+ className: ["shiki", props.themeKey, props.className].filter(Boolean).join(" "),
1411
+ "data-language": props.language ?? "text",
1412
+ "data-theme": props.themeKey,
1413
+ children: /* @__PURE__ */ jsx9(
1414
+ "code",
1415
+ {
1416
+ ...codeProps,
1417
+ "data-language": props.language ?? "text",
1418
+ "data-theme": props.themeKey,
1419
+ style: { display: "grid" },
1420
+ children: props.code.split("\n").map((line, index, allLines) => /* @__PURE__ */ jsxs4("span", { "data-line": "", children: [
1421
+ line,
1422
+ index < allLines.length - 1 ? "\n" : ""
1423
+ ] }, `${index}:${line}`))
1424
+ }
1425
+ )
1426
+ }
1427
+ )
1428
+ }
1429
+ );
1430
+ }
1431
+ function AsyncCodeBlock(props) {
1432
+ const {
1433
+ code,
1434
+ language,
1435
+ meta,
1436
+ themeKey,
1437
+ documentVersion,
1438
+ manager: managerProp
1439
+ } = props;
1440
+ const manager = managerProp ?? getHighlightWorkerManager();
1441
+ const blockInstanceIdRef = useRef7(`async-code-block-${nextBlockInstanceId++}`);
1442
+ const activeRequestRef = useRef7(null);
1443
+ const [state, setState] = useState5({ status: "plain", html: null });
1444
+ const cacheKey = useMemo4(() => buildHighlightCacheKey({
1445
+ code,
1446
+ language,
1447
+ themeKey,
1448
+ meta
1449
+ }), [code, language, meta, themeKey]);
1450
+ useEffect5(() => {
1451
+ setState((current) => current.status === "highlighted" && current.html ? current : { status: "loading", html: null });
1452
+ let activeCancel = null;
1453
+ const timer = setTimeout(() => {
1454
+ const task = manager.highlight({
1455
+ documentVersion,
1456
+ blockInstanceId: blockInstanceIdRef.current,
1457
+ cacheKey,
1458
+ code,
1459
+ language,
1460
+ themeKey,
1461
+ meta
1462
+ });
1463
+ activeRequestRef.current = task.request.requestId;
1464
+ activeCancel = task.cancel;
1465
+ void task.promise.then((response) => {
1466
+ if (activeRequestRef.current !== response.requestId) {
1467
+ return;
1468
+ }
1469
+ if (response.documentVersion !== documentVersion) {
1470
+ return;
1471
+ }
1472
+ if (response.cacheKey !== cacheKey) {
1473
+ return;
1474
+ }
1475
+ if (response.ok) {
1476
+ setState({ status: "highlighted", html: response.html });
1477
+ return;
1478
+ }
1479
+ setState({ status: "plain", html: null });
1480
+ }).catch(() => {
1481
+ if (activeRequestRef.current === task.request.requestId) {
1482
+ setState({ status: "plain", html: null });
1483
+ }
1484
+ });
1485
+ }, HIGHLIGHT_COMPONENT_DEBOUNCE_MS);
684
1486
  return () => {
685
- observer.disconnect();
686
- mutation.disconnect();
1487
+ clearTimeout(timer);
1488
+ activeCancel?.();
1489
+ activeRequestRef.current = null;
687
1490
  };
688
- }, [viewportFirst, state]);
689
- return /* @__PURE__ */ jsx6(
690
- "div",
1491
+ }, [cacheKey, code, documentVersion, language, manager, meta, themeKey]);
1492
+ if (state.status === "highlighted") {
1493
+ return /* @__PURE__ */ jsx9(
1494
+ "figure",
1495
+ {
1496
+ "data-rehype-pretty-code-figure": "",
1497
+ "data-owo-code-block-state": "highlighted",
1498
+ className: "owo-async-code-block",
1499
+ dangerouslySetInnerHTML: { __html: state.html }
1500
+ }
1501
+ );
1502
+ }
1503
+ return renderPlainCode(props, state.status);
1504
+ }
1505
+ function readCodeText(children) {
1506
+ if (typeof children === "string") {
1507
+ return children;
1508
+ }
1509
+ if (Array.isArray(children)) {
1510
+ return children.map(readCodeText).join("");
1511
+ }
1512
+ if (isValidElement(children)) {
1513
+ return readCodeText(children.props.children);
1514
+ }
1515
+ return "";
1516
+ }
1517
+ function getLanguageFromClassName(className) {
1518
+ if (!className) {
1519
+ return null;
1520
+ }
1521
+ const match = className.match(/language-([A-Za-z0-9_-]+)/);
1522
+ return match?.[1] ?? null;
1523
+ }
1524
+ function MdxAsyncPre(props) {
1525
+ const {
1526
+ themeKey,
1527
+ documentVersion,
1528
+ children,
1529
+ ...nativePreProps
1530
+ } = props;
1531
+ if (Children.count(children) !== 1) {
1532
+ return /* @__PURE__ */ jsx9("pre", { ...nativePreProps, children });
1533
+ }
1534
+ const child = Children.only(children);
1535
+ if (!isValidElement(child)) {
1536
+ return /* @__PURE__ */ jsx9("pre", { ...nativePreProps, children });
1537
+ }
1538
+ const language = getLanguageFromClassName(child.props.className);
1539
+ const meta = typeof child.props["data-meta"] === "string" ? child.props["data-meta"] : null;
1540
+ const code = readCodeText(child.props.children);
1541
+ return /* @__PURE__ */ jsx9(
1542
+ AsyncCodeBlock,
691
1543
  {
692
- ref: containerRef,
693
- className,
694
- role: "document",
695
- "aria-label": ariaLabel ?? "Preview",
696
- "aria-live": "polite"
1544
+ code,
1545
+ language,
1546
+ meta,
1547
+ themeKey,
1548
+ documentVersion,
1549
+ className: child.props.className,
1550
+ codeProps: child.props,
1551
+ preProps: nativePreProps
697
1552
  }
698
1553
  );
699
- };
1554
+ }
1555
+ function MdxInlineCode(props) {
1556
+ return /* @__PURE__ */ jsx9("code", { ...props });
1557
+ }
1558
+
1559
+ // src/mdx-preview-utils.tsx
1560
+ import { jsx as jsx10 } from "react/jsx-runtime";
1561
+ var objectIdentityMap = /* @__PURE__ */ new WeakMap();
1562
+ var nextObjectIdentity = 1;
1563
+ function getObjectIdentity(value) {
1564
+ if (!value) return "0";
1565
+ const existing = objectIdentityMap.get(value);
1566
+ if (existing) return String(existing);
1567
+ const id = nextObjectIdentity++;
1568
+ objectIdentityMap.set(value, id);
1569
+ return String(id);
1570
+ }
1571
+ function buildRuntimeComponents(mdxAnalysis, baseComponents, runtimeContext) {
1572
+ const components = {
1573
+ ...baseComponents,
1574
+ pre: function RuntimeAsyncPre(props) {
1575
+ return /* @__PURE__ */ jsx10(
1576
+ MdxAsyncPre,
1577
+ {
1578
+ ...props,
1579
+ themeKey: runtimeContext.themeKey,
1580
+ documentVersion: runtimeContext.documentVersion
1581
+ }
1582
+ );
1583
+ },
1584
+ code: MdxInlineCode
1585
+ };
1586
+ function registerMissingComponentPath(componentName) {
1587
+ const parts = componentName.split(".");
1588
+ let cursor = components;
1589
+ for (let index = 0; index < parts.length; index += 1) {
1590
+ const part = parts[index];
1591
+ const isLeaf = index === parts.length - 1;
1592
+ const existing = cursor[part];
1593
+ if (isLeaf) {
1594
+ if (existing === void 0) {
1595
+ cursor[part] = function MissingComponent(props) {
1596
+ const { children, ...restProps } = props;
1597
+ return /* @__PURE__ */ jsx10("div", { "data-mdx-missing": componentName, ...restProps, children });
1598
+ };
1599
+ }
1600
+ return;
1601
+ }
1602
+ if (existing == null) {
1603
+ const nested = {};
1604
+ cursor[part] = nested;
1605
+ cursor = nested;
1606
+ continue;
1607
+ }
1608
+ if (typeof existing === "function") {
1609
+ return;
1610
+ }
1611
+ cursor = existing;
1612
+ }
1613
+ }
1614
+ for (const name of mdxAnalysis.componentNames) {
1615
+ registerMissingComponentPath(name);
1616
+ }
1617
+ return components;
1618
+ }
1619
+ async function renderMarkdownBlocksCached(blocks, themeKey, cache, renderBlock) {
1620
+ const nextCache = /* @__PURE__ */ new Map();
1621
+ const results = await Promise.all(blocks.map(async (block) => {
1622
+ const cached = cache.get(block.renderKey);
1623
+ if (cached !== void 0) {
1624
+ nextCache.set(block.renderKey, cached);
1625
+ return { blockId: block.blockId, html: cached };
1626
+ }
1627
+ const context = {
1628
+ version: 0,
1629
+ themeKey,
1630
+ sourceLineOffset: block.startLine - 1
1631
+ };
1632
+ const html = renderBlock ? await renderBlock(block, context) : await renderBlockDefault(block);
1633
+ nextCache.set(block.renderKey, html);
1634
+ return { blockId: block.blockId, html };
1635
+ }));
1636
+ cache.clear();
1637
+ for (const [k, v] of nextCache) cache.set(k, v);
1638
+ return results;
1639
+ }
1640
+
1641
+ // src/useMdxPreviewCompilation.ts
1642
+ import { inspectMdxSource as inspectMdxSource2 } from "@owomark/processor";
1643
+ function useMdxPreviewCompilation({
1644
+ snapshot,
1645
+ themeKey,
1646
+ renderBlock,
1647
+ mdx,
1648
+ wrappedComponents
1649
+ }) {
1650
+ const requestIdRef = useRef8(0);
1651
+ const cancelRef = useRef8(null);
1652
+ const blockCacheRef = useRef8(/* @__PURE__ */ new Map());
1653
+ const [mdxDisplay, setMdxDisplay] = useState6({
1654
+ code: null,
1655
+ Content: null,
1656
+ runtimeComponents: null,
1657
+ compileKey: null,
1658
+ renderKey: null,
1659
+ pending: false,
1660
+ error: null
1661
+ });
1662
+ const [markdownHtml, setMarkdownHtml] = useState6(null);
1663
+ useEffect6(() => {
1664
+ acquireMdxWorker();
1665
+ return () => {
1666
+ cancelRef.current?.();
1667
+ cancelRef.current = null;
1668
+ releaseMdxWorker();
1669
+ };
1670
+ }, []);
1671
+ const mdxInspection = useMemo5(
1672
+ () => inspectMdxSource2(snapshot.markdown, {
1673
+ enableMath: mdx?.enableMath,
1674
+ enableSideAnnotation: mdx?.enableSideAnnotation,
1675
+ extraRemarkDescriptors: mdx?.extraRemarkDescriptors,
1676
+ extraRemarkPlugins: mdx?.extraRemarkPlugins
1677
+ }),
1678
+ [
1679
+ mdx?.enableMath,
1680
+ mdx?.enableSideAnnotation,
1681
+ mdx?.extraRemarkDescriptors,
1682
+ mdx?.extraRemarkPlugins,
1683
+ snapshot.markdown
1684
+ ]
1685
+ );
1686
+ const hasMdx = mdx?.forceFullDocumentCompile === true || mdxInspection.hasMdxSyntax;
1687
+ const optionsFingerprint = useMemo5(() => {
1688
+ const b = (v) => v === true ? "1" : v === false ? "0" : "_";
1689
+ const ds = (arr) => arr?.length ? arr.map((d) => d.options ? `${d.name}(${JSON.stringify(d.options)})` : d.name).join(",") : "";
1690
+ return [
1691
+ b(mdx?.enableMath),
1692
+ b(mdx?.enableSideAnnotation),
1693
+ b(mdx?.sourceAnchors),
1694
+ ds(mdx?.extraRemarkDescriptors),
1695
+ ds(mdx?.extraRehypeDescriptors),
1696
+ `rp${getObjectIdentity(mdx?.extraRemarkPlugins)}`,
1697
+ `hp${getObjectIdentity(mdx?.extraRehypePlugins)}`
1698
+ ].join("|");
1699
+ }, [
1700
+ mdx?.enableMath,
1701
+ mdx?.enableSideAnnotation,
1702
+ mdx?.sourceAnchors,
1703
+ mdx?.extraRemarkDescriptors,
1704
+ mdx?.extraRehypeDescriptors,
1705
+ mdx?.extraRemarkPlugins,
1706
+ mdx?.extraRehypePlugins
1707
+ ]);
1708
+ const compileKey = useMemo5(
1709
+ () => deriveRenderKey2(`${snapshot.markdown}:${optionsFingerprint}`, "custom", ""),
1710
+ [snapshot.markdown, optionsFingerprint]
1711
+ );
1712
+ const componentFingerprint = useMemo5(
1713
+ () => `cp${getObjectIdentity(mdx?.components)}:theme:${themeKey}`,
1714
+ [mdx?.components, themeKey]
1715
+ );
1716
+ const renderKey = useMemo5(
1717
+ () => `${compileKey}:${componentFingerprint}`,
1718
+ [compileKey, componentFingerprint]
1719
+ );
1720
+ useEffect6(() => {
1721
+ if (!snapshot.markdown.trim()) {
1722
+ cancelRef.current?.();
1723
+ cancelRef.current = null;
1724
+ setMarkdownHtml(null);
1725
+ setMdxDisplay({
1726
+ code: null,
1727
+ Content: null,
1728
+ runtimeComponents: null,
1729
+ compileKey: null,
1730
+ renderKey: null,
1731
+ pending: false,
1732
+ error: null
1733
+ });
1734
+ return;
1735
+ }
1736
+ const requestId = ++requestIdRef.current;
1737
+ if (!hasMdx) {
1738
+ cancelRef.current?.();
1739
+ cancelRef.current = null;
1740
+ void renderMarkdownBlocksCached(snapshot.previewBlocks, themeKey, blockCacheRef.current, renderBlock).then((blocks) => {
1741
+ if (requestIdRef.current !== requestId) return;
1742
+ setMarkdownHtml(blocks);
1743
+ setMdxDisplay({
1744
+ code: null,
1745
+ Content: null,
1746
+ runtimeComponents: null,
1747
+ compileKey: null,
1748
+ renderKey: null,
1749
+ pending: false,
1750
+ error: null
1751
+ });
1752
+ });
1753
+ return;
1754
+ }
1755
+ if (mdxDisplay.compileKey === compileKey && mdxDisplay.code) {
1756
+ if (mdxDisplay.renderKey !== renderKey) {
1757
+ const rerunId = ++requestIdRef.current;
1758
+ const cachedCode = mdxDisplay.code;
1759
+ setMdxDisplay((prev) => ({ ...prev, pending: true, error: null }));
1760
+ void (async () => {
1761
+ try {
1762
+ const { Content } = await runMdxCode(cachedCode);
1763
+ if (requestIdRef.current !== rerunId) return;
1764
+ setMdxDisplay((prev) => {
1765
+ if (prev.compileKey !== compileKey || prev.code !== cachedCode) return prev;
1766
+ return {
1767
+ ...prev,
1768
+ Content,
1769
+ runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents, {
1770
+ themeKey,
1771
+ documentVersion: snapshot.contentVersion
1772
+ }),
1773
+ renderKey,
1774
+ pending: false,
1775
+ error: null
1776
+ };
1777
+ });
1778
+ } catch (error) {
1779
+ if (requestIdRef.current !== rerunId) return;
1780
+ setMdxDisplay((prev) => ({
1781
+ ...prev,
1782
+ pending: false,
1783
+ error: parseCompileError(error, "runtime")
1784
+ }));
1785
+ }
1786
+ })();
1787
+ }
1788
+ return;
1789
+ }
1790
+ setMdxDisplay((prev) => ({ ...prev, pending: true, error: null }));
1791
+ setMarkdownHtml(null);
1792
+ cancelRef.current?.();
1793
+ cancelRef.current = null;
1794
+ const timer = setTimeout(() => {
1795
+ const { promise, cancel } = compileMdx(snapshot.markdown, {
1796
+ enableMath: mdx?.enableMath,
1797
+ enableSideAnnotation: mdx?.enableSideAnnotation,
1798
+ enableCodeHighlight: false,
1799
+ sourceAnchors: mdx?.sourceAnchors,
1800
+ extraRemarkDescriptors: mdx?.extraRemarkDescriptors,
1801
+ extraRehypeDescriptors: mdx?.extraRehypeDescriptors,
1802
+ extraRemarkPlugins: mdx?.extraRemarkPlugins,
1803
+ extraRehypePlugins: mdx?.extraRehypePlugins
1804
+ }, mdxInspection);
1805
+ cancelRef.current = cancel;
1806
+ void (async () => {
1807
+ try {
1808
+ const code = await promise;
1809
+ if (requestIdRef.current !== requestId) return;
1810
+ const { Content } = await runMdxCode(code);
1811
+ if (requestIdRef.current !== requestId) return;
1812
+ setMdxDisplay({
1813
+ code,
1814
+ Content,
1815
+ runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents, {
1816
+ themeKey,
1817
+ documentVersion: snapshot.contentVersion
1818
+ }),
1819
+ compileKey,
1820
+ renderKey,
1821
+ pending: false,
1822
+ error: null
1823
+ });
1824
+ } catch (error) {
1825
+ if (requestIdRef.current !== requestId) return;
1826
+ setMdxDisplay((prev) => ({
1827
+ ...prev,
1828
+ pending: false,
1829
+ error: parseCompileError(error, "compile")
1830
+ }));
1831
+ }
1832
+ })();
1833
+ }, 200);
1834
+ return () => {
1835
+ clearTimeout(timer);
1836
+ cancelRef.current?.();
1837
+ cancelRef.current = null;
1838
+ };
1839
+ }, [
1840
+ compileKey,
1841
+ hasMdx,
1842
+ mdx?.enableMath,
1843
+ mdx?.enableSideAnnotation,
1844
+ mdx?.forceFullDocumentCompile,
1845
+ mdx?.extraRehypeDescriptors,
1846
+ mdx?.extraRehypePlugins,
1847
+ mdx?.extraRemarkDescriptors,
1848
+ mdx?.extraRemarkPlugins,
1849
+ mdxInspection,
1850
+ mdx?.sourceAnchors,
1851
+ mdxDisplay.compileKey,
1852
+ mdxDisplay.Content,
1853
+ mdxDisplay.renderKey,
1854
+ renderBlock,
1855
+ renderKey,
1856
+ snapshot.markdown,
1857
+ snapshot.contentVersion,
1858
+ snapshot.previewBlocks,
1859
+ themeKey,
1860
+ wrappedComponents
1861
+ ]);
1862
+ return {
1863
+ hasMdx,
1864
+ markdownHtml,
1865
+ mdxDisplay
1866
+ };
1867
+ }
700
1868
 
701
1869
  // src/useOwoMarkSharedState.ts
702
- import { useRef as useRef5, useSyncExternalStore, useCallback as useCallback2 } from "react";
1870
+ import { useRef as useRef9, useSyncExternalStore, useCallback as useCallback4 } from "react";
703
1871
  import {
704
1872
  createSharedStateStore
705
1873
  } from "@owomark/core";
706
1874
  function useOwoMarkSharedState(options) {
707
- const controllerRef = useRef5(null);
1875
+ const controllerRef = useRef9(null);
708
1876
  if (!controllerRef.current) {
709
1877
  controllerRef.current = createSharedStateStore(options);
710
1878
  }
711
1879
  return controllerRef.current;
712
1880
  }
713
1881
  function useSharedStateSnapshot(controller) {
714
- const subscribe = useCallback2(
1882
+ const subscribe = useCallback4(
715
1883
  (onStoreChange) => controller.subscribe(onStoreChange),
716
1884
  [controller]
717
1885
  );
718
- const getSnapshot = useCallback2(() => controller.getState(), [controller]);
1886
+ const getSnapshot = useCallback4(() => controller.getState(), [controller]);
1887
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1888
+ }
1889
+ function useSharedStateSelector(store, selector, isEqual = Object.is) {
1890
+ const selectedRef = useRef9(selector(store.getState()));
1891
+ const selected = selector(store.getState());
1892
+ if (!isEqual(selectedRef.current, selected)) {
1893
+ selectedRef.current = selected;
1894
+ }
1895
+ const subscribe = useCallback4((onStoreChange) => store.subscribeWithSelector(selector, (nextSelected) => {
1896
+ if (isEqual(selectedRef.current, nextSelected)) return;
1897
+ selectedRef.current = nextSelected;
1898
+ onStoreChange();
1899
+ }, isEqual), [isEqual, selector, store]);
1900
+ const getSnapshot = useCallback4(() => selectedRef.current, []);
719
1901
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
720
1902
  }
721
1903
 
1904
+ // src/MdxPreview.tsx
1905
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1906
+ function locationText(error) {
1907
+ if (error.line == null) return null;
1908
+ if (error.column == null) return `\u884C ${error.line}`;
1909
+ return `\u884C ${error.line}:${error.column}`;
1910
+ }
1911
+ function ErrorNotice({ error }) {
1912
+ const location = locationText(error);
1913
+ const label = error.kind === "compile" ? "MDX Compile" : "MDX Runtime";
1914
+ return /* @__PURE__ */ jsx11("div", { className: "sticky top-0 z-10 mb-4 rounded-md border border-[var(--owo-cmp-danger-border)] bg-[var(--owo-cmp-danger-bg)] px-4 py-3 text-sm text-[var(--owo-cmp-danger-text)]", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-2", children: [
1915
+ /* @__PURE__ */ jsx11("span", { className: "mt-0.5 shrink-0 font-mono text-[11px] font-bold uppercase", children: label }),
1916
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
1917
+ location && /* @__PURE__ */ jsx11("span", { className: "mr-2 rounded bg-[var(--owo-cmp-danger-border)] px-1.5 py-0.5 font-mono text-xs", children: location }),
1918
+ /* @__PURE__ */ jsx11("span", { className: "break-words", children: error.message })
1919
+ ] })
1920
+ ] }) });
1921
+ }
1922
+ var RuntimeErrorBoundary = class extends Component {
1923
+ state = { error: null };
1924
+ static getDerivedStateFromError(error) {
1925
+ return { error: parseCompileError(error, "runtime") };
1926
+ }
1927
+ componentDidCatch(error) {
1928
+ this.props.onError(parseCompileError(error, "runtime"));
1929
+ }
1930
+ render() {
1931
+ if (this.state.error) {
1932
+ return /* @__PURE__ */ jsx11(ErrorNotice, { error: this.state.error });
1933
+ }
1934
+ return this.props.children;
1935
+ }
1936
+ };
1937
+ function useMdxPreviewContentSnapshot(store, markdownOverride) {
1938
+ return useSharedStateSelector(
1939
+ store,
1940
+ (snapshot) => ({
1941
+ markdown: markdownOverride ?? snapshot.markdown,
1942
+ previewBlocks: snapshot.previewBlocks,
1943
+ contentVersion: snapshot.contentVersion
1944
+ }),
1945
+ (prev, next) => prev.markdown === next.markdown && prev.previewBlocks === next.previewBlocks && prev.contentVersion === next.contentVersion
1946
+ );
1947
+ }
1948
+ var MdxPreview = forwardRef2(function MdxPreview2(props, ref) {
1949
+ const {
1950
+ state,
1951
+ markdown,
1952
+ className,
1953
+ themeKey = "",
1954
+ ariaLabel,
1955
+ onScroll,
1956
+ scrollController,
1957
+ scrollSync = "bidirectional",
1958
+ renderBlock,
1959
+ mdx,
1960
+ onContentUpdate
1961
+ } = props;
1962
+ const snapshot = useMdxPreviewContentSnapshot(state, markdown);
1963
+ const rootRef = useRef10(null);
1964
+ const boundSurfaceRef = useRef10(null);
1965
+ const [runtimeError, setRuntimeError] = useState7(null);
1966
+ const wrappedComponents = useMemo6(() => {
1967
+ const wrapComponentMap = (componentMap) => {
1968
+ const wrapped = {};
1969
+ for (const [name, component] of Object.entries(componentMap)) {
1970
+ if (typeof component === "function") {
1971
+ wrapped[name] = wrapWithShell(name, component);
1972
+ continue;
1973
+ }
1974
+ wrapped[name] = wrapComponentMap(component);
1975
+ }
1976
+ return wrapped;
1977
+ };
1978
+ const merged = {
1979
+ ...DEFAULT_MDX_COMPONENTS,
1980
+ ...mdx?.components ?? {}
1981
+ };
1982
+ return wrapComponentMap(merged);
1983
+ }, [mdx?.components]);
1984
+ const { markdownHtml, mdxDisplay } = useMdxPreviewCompilation({
1985
+ snapshot,
1986
+ themeKey,
1987
+ renderBlock,
1988
+ mdx,
1989
+ wrappedComponents
1990
+ });
1991
+ useEffect7(() => {
1992
+ setRuntimeError(null);
1993
+ }, [mdxDisplay.renderKey]);
1994
+ useEffect7(() => {
1995
+ const root = rootRef.current;
1996
+ if (!root || !onContentUpdate && (!scrollController || scrollSync === "off")) return;
1997
+ const notify = () => {
1998
+ onContentUpdate?.();
1999
+ if (scrollController && scrollSync !== "off") {
2000
+ scrollController.measureSurface("preview");
2001
+ }
2002
+ };
2003
+ const observer = new ResizeObserver(() => notify());
2004
+ observer.observe(root);
2005
+ return () => observer.disconnect();
2006
+ }, [onContentUpdate, scrollController, scrollSync]);
2007
+ useEffect7(() => {
2008
+ if (!onContentUpdate && (!scrollController || scrollSync === "off")) return;
2009
+ const id = requestAnimationFrame(() => {
2010
+ onContentUpdate?.();
2011
+ if (scrollController && scrollSync !== "off") {
2012
+ scrollController.measureSurface("preview");
2013
+ }
2014
+ });
2015
+ return () => cancelAnimationFrame(id);
2016
+ }, [mdxDisplay, markdownHtml, runtimeError, onContentUpdate, scrollController, scrollSync]);
2017
+ const hasContent = mdxDisplay.Content !== null || markdownHtml !== null;
2018
+ return /* @__PURE__ */ jsxs5(
2019
+ "div",
2020
+ {
2021
+ ref: (node) => {
2022
+ if (scrollController && scrollSync !== "off" && boundSurfaceRef.current !== node) {
2023
+ if (boundSurfaceRef.current) {
2024
+ scrollController.unbindSurface("preview", boundSurfaceRef.current);
2025
+ }
2026
+ if (node) {
2027
+ scrollController.bindSurface("preview", node);
2028
+ }
2029
+ boundSurfaceRef.current = node;
2030
+ } else if ((!scrollController || scrollSync === "off") && boundSurfaceRef.current) {
2031
+ scrollController?.unbindSurface("preview", boundSurfaceRef.current);
2032
+ boundSurfaceRef.current = null;
2033
+ }
2034
+ rootRef.current = node;
2035
+ if (typeof ref === "function") ref(node);
2036
+ else if (ref) ref.current = node;
2037
+ },
2038
+ className,
2039
+ role: "document",
2040
+ "aria-label": ariaLabel ?? "Preview",
2041
+ "aria-live": "polite",
2042
+ onScroll,
2043
+ children: [
2044
+ mdxDisplay.pending && hasContent && /* @__PURE__ */ jsx11("div", { className: "pointer-events-none sticky top-0 z-10 mb-3 h-0.5 w-full animate-pulse rounded-full bg-p400" }),
2045
+ mdxDisplay.error && /* @__PURE__ */ jsx11(ErrorNotice, { error: mdxDisplay.error }),
2046
+ runtimeError && /* @__PURE__ */ jsx11(ErrorNotice, { error: runtimeError }),
2047
+ !hasContent && mdxDisplay.pending && /* @__PURE__ */ jsx11(MdxSkeleton, { lines: 8 }),
2048
+ markdownHtml !== null && markdownHtml.map(({ blockId, html }) => /* @__PURE__ */ jsx11("div", { dangerouslySetInnerHTML: { __html: html } }, blockId)),
2049
+ mdxDisplay.Content && mdxDisplay.runtimeComponents && /* @__PURE__ */ jsx11(RuntimeErrorBoundary, { onError: setRuntimeError, children: /* @__PURE__ */ jsx11(mdxDisplay.Content, { components: mdxDisplay.runtimeComponents }) }, mdxDisplay.renderKey)
2050
+ ]
2051
+ }
2052
+ );
2053
+ });
2054
+
2055
+ // src/OwoMarkPreview.tsx
2056
+ import { jsx as jsx12 } from "react/jsx-runtime";
2057
+ function isController(store) {
2058
+ return typeof store.updateVisibleBlockIds === "function";
2059
+ }
2060
+ function usePreviewSurfaceAdapter(rootRef, scrollController, scrollSync = "bidirectional", observeDeps = []) {
2061
+ useEffect8(() => {
2062
+ const root = rootRef.current;
2063
+ if (!root || !scrollController || scrollSync === "off") return;
2064
+ const measure = () => {
2065
+ scrollController.measureSurface("preview");
2066
+ };
2067
+ const resizeObserver = new ResizeObserver(() => measure());
2068
+ resizeObserver.observe(root);
2069
+ const observeChildren = () => {
2070
+ resizeObserver.disconnect();
2071
+ resizeObserver.observe(root);
2072
+ Array.from(root.children).forEach((child) => resizeObserver.observe(child));
2073
+ };
2074
+ observeChildren();
2075
+ const mutationObserver = new MutationObserver(() => {
2076
+ observeChildren();
2077
+ measure();
2078
+ });
2079
+ mutationObserver.observe(root, { childList: true, subtree: true, characterData: true });
2080
+ const onLoad = (event) => {
2081
+ if (event.target?.tagName === "IMG") {
2082
+ measure();
2083
+ }
2084
+ };
2085
+ root.addEventListener("load", onLoad, true);
2086
+ const frame = requestAnimationFrame(measure);
2087
+ let disposed = false;
2088
+ if (typeof document !== "undefined" && document.fonts) {
2089
+ document.fonts.ready.then(() => {
2090
+ if (!disposed) measure();
2091
+ });
2092
+ }
2093
+ return () => {
2094
+ disposed = true;
2095
+ cancelAnimationFrame(frame);
2096
+ resizeObserver.disconnect();
2097
+ mutationObserver.disconnect();
2098
+ root.removeEventListener("load", onLoad, true);
2099
+ };
2100
+ }, [rootRef, scrollController, scrollSync, ...observeDeps]);
2101
+ }
2102
+ var OwoMarkPreview = forwardRef3(
2103
+ function OwoMarkPreview2(props, ref) {
2104
+ const {
2105
+ state,
2106
+ markdown,
2107
+ className,
2108
+ strategy,
2109
+ themeKey,
2110
+ registry,
2111
+ viewportFirst,
2112
+ ariaLabel,
2113
+ scrollController,
2114
+ scrollSync = "bidirectional",
2115
+ onScroll,
2116
+ renderBlock,
2117
+ mdx,
2118
+ onContentUpdate
2119
+ } = props;
2120
+ if (strategy === "mdx") {
2121
+ return /* @__PURE__ */ jsx12(
2122
+ MdxPreview,
2123
+ {
2124
+ ref,
2125
+ state,
2126
+ markdown,
2127
+ className,
2128
+ themeKey,
2129
+ ariaLabel,
2130
+ onScroll,
2131
+ scrollController,
2132
+ scrollSync,
2133
+ onContentUpdate,
2134
+ renderBlock,
2135
+ mdx
2136
+ }
2137
+ );
2138
+ }
2139
+ const renderBlockRef = useRef11(renderBlock);
2140
+ renderBlockRef.current = renderBlock;
2141
+ const hasRenderBlock = !!renderBlock;
2142
+ const containerRef = useRef11(null);
2143
+ const boundSurfaceRef = useRef11(null);
2144
+ const engineRef = useRef11(null);
2145
+ usePreviewSurfaceAdapter(containerRef, scrollController, scrollSync, [
2146
+ strategy,
2147
+ themeKey,
2148
+ registry,
2149
+ viewportFirst,
2150
+ hasRenderBlock
2151
+ ]);
2152
+ useEffect8(() => {
2153
+ if (!containerRef.current) return;
2154
+ const engineOptions = {
2155
+ strategy,
2156
+ themeKey,
2157
+ registry,
2158
+ viewportFirst,
2159
+ renderBlock: hasRenderBlock ? (block, ctx) => renderBlockRef.current(block, ctx) : void 0,
2160
+ onContentUpdate
2161
+ };
2162
+ const engine = createOwoMarkPreviewEngine(engineOptions);
2163
+ engine.mount(containerRef.current);
2164
+ engineRef.current = engine;
2165
+ void engine.update(state.getState());
2166
+ return () => {
2167
+ engine.destroy();
2168
+ engineRef.current = null;
2169
+ };
2170
+ }, [strategy, themeKey, registry, viewportFirst, hasRenderBlock]);
2171
+ useEffect8(() => {
2172
+ const unsub = state.subscribe((newState) => {
2173
+ const engine = engineRef.current;
2174
+ if (engine) {
2175
+ engine.update(newState);
2176
+ requestAnimationFrame(() => {
2177
+ if (scrollController && scrollSync !== "off") {
2178
+ scrollController.measureSurface("preview");
2179
+ }
2180
+ onContentUpdate?.();
2181
+ });
2182
+ }
2183
+ });
2184
+ return unsub;
2185
+ }, [state, scrollController, scrollSync, onContentUpdate]);
2186
+ useEffect8(() => {
2187
+ const container = containerRef.current;
2188
+ if (!viewportFirst || !container || !isController(state)) return;
2189
+ const controller = state;
2190
+ const observer = new IntersectionObserver(
2191
+ (entries) => {
2192
+ const visible = /* @__PURE__ */ new Set();
2193
+ for (const id of controller.getState().visibleBlockIds) {
2194
+ visible.add(id);
2195
+ }
2196
+ for (const entry of entries) {
2197
+ const blockId = entry.target.getAttribute("data-block-id");
2198
+ if (!blockId) continue;
2199
+ if (entry.isIntersecting) visible.add(blockId);
2200
+ else visible.delete(blockId);
2201
+ }
2202
+ controller.updateVisibleBlockIds([...visible]);
2203
+ },
2204
+ { root: container, rootMargin: "200px 0px" }
2205
+ );
2206
+ const wrappers = container.querySelectorAll("[data-block-id]");
2207
+ for (const wrapper of wrappers) {
2208
+ observer.observe(wrapper);
2209
+ }
2210
+ const mutation = new MutationObserver((mutations) => {
2211
+ for (const m of mutations) {
2212
+ for (const node of m.addedNodes) {
2213
+ if (node instanceof HTMLElement && node.hasAttribute("data-block-id")) {
2214
+ observer.observe(node);
2215
+ }
2216
+ }
2217
+ }
2218
+ });
2219
+ mutation.observe(container, { childList: true, subtree: true });
2220
+ return () => {
2221
+ observer.disconnect();
2222
+ mutation.disconnect();
2223
+ };
2224
+ }, [viewportFirst, state]);
2225
+ return /* @__PURE__ */ jsx12(
2226
+ "div",
2227
+ {
2228
+ ref: (node) => {
2229
+ if (scrollController && scrollSync !== "off" && boundSurfaceRef.current !== node) {
2230
+ if (boundSurfaceRef.current) {
2231
+ scrollController.unbindSurface("preview", boundSurfaceRef.current);
2232
+ }
2233
+ if (node) {
2234
+ scrollController.bindSurface("preview", node);
2235
+ }
2236
+ boundSurfaceRef.current = node;
2237
+ } else if ((!scrollController || scrollSync === "off") && boundSurfaceRef.current) {
2238
+ scrollController?.unbindSurface("preview", boundSurfaceRef.current);
2239
+ boundSurfaceRef.current = null;
2240
+ }
2241
+ containerRef.current = node;
2242
+ if (typeof ref === "function") ref(node);
2243
+ else if (ref) ref.current = node;
2244
+ },
2245
+ className,
2246
+ role: "document",
2247
+ "aria-label": ariaLabel ?? "Preview",
2248
+ "aria-live": "polite",
2249
+ onScroll
2250
+ }
2251
+ );
2252
+ }
2253
+ );
2254
+
722
2255
  // src/useBlockContext.ts
723
- import { useState as useState3, useEffect as useEffect5 } from "react";
2256
+ import { useState as useState8, useEffect as useEffect9 } from "react";
724
2257
  function useBlockContext(core) {
725
- const [blockContext, setBlockContext] = useState3(
2258
+ const [blockContext, setBlockContext] = useState8(
726
2259
  () => core.getBlockContext()
727
2260
  );
728
- useEffect5(() => {
2261
+ useEffect9(() => {
729
2262
  setBlockContext(core.getBlockContext());
730
2263
  return core.onBlockContextChange((ctx) => {
731
2264
  setBlockContext(ctx);
@@ -737,24 +2270,52 @@ function useBlockContext(core) {
737
2270
  // src/index.ts
738
2271
  import { getThemeClassName as getThemeClassName2, THEME_LIGHT_CLASS, THEME_DARK_CLASS } from "@owomark/view";
739
2272
 
2273
+ // src/useScrollController.ts
2274
+ import { useEffect as useEffect10, useRef as useRef12 } from "react";
2275
+ import {
2276
+ createScrollController
2277
+ } from "@owomark/core/browser";
2278
+ function useScrollController(controller) {
2279
+ const scrollControllerRef = useRef12(null);
2280
+ if (!scrollControllerRef.current) {
2281
+ scrollControllerRef.current = createScrollController();
2282
+ }
2283
+ const sc = scrollControllerRef.current;
2284
+ useEffect10(() => {
2285
+ const state = controller.getState();
2286
+ sc.updateProjection(state.previewBlocks, state.contentVersion);
2287
+ return controller.subscribeWithSelector(
2288
+ (snapshot) => ({
2289
+ previewBlocks: snapshot.previewBlocks,
2290
+ contentVersion: snapshot.contentVersion
2291
+ }),
2292
+ ({ previewBlocks, contentVersion }) => {
2293
+ sc.updateProjection(previewBlocks, contentVersion);
2294
+ },
2295
+ (prev, next) => prev.contentVersion === next.contentVersion
2296
+ );
2297
+ }, [controller, sc]);
2298
+ return sc;
2299
+ }
2300
+
740
2301
  // src/useVirtualList.ts
741
- import { useState as useState4, useCallback as useCallback3, useRef as useRef6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
2302
+ import { useState as useState9, useCallback as useCallback5, useRef as useRef13, useEffect as useEffect11, useMemo as useMemo7 } from "react";
742
2303
  import {
743
2304
  buildVirtualRows,
744
2305
  computeVisibleRange
745
2306
  } from "@owomark/core";
746
2307
  function useVirtualList(options) {
747
2308
  const { blocks, containerRef, overscan = 5, charsPerLine = 80 } = options;
748
- const heightCacheRef = useRef6(/* @__PURE__ */ new Map());
749
- const [scrollTop, setScrollTop] = useState4(0);
750
- const [viewportHeight, setViewportHeight] = useState4(0);
751
- const [heightCacheVersion, setHeightCacheVersion] = useState4(0);
752
- const rafRef = useRef6(0);
753
- const [container, setContainer] = useState4(null);
754
- useEffect6(() => {
2309
+ const heightCacheRef = useRef13(/* @__PURE__ */ new Map());
2310
+ const [scrollTop, setScrollTop] = useState9(0);
2311
+ const [viewportHeight, setViewportHeight] = useState9(0);
2312
+ const [heightCacheVersion, setHeightCacheVersion] = useState9(0);
2313
+ const rafRef = useRef13(0);
2314
+ const [container, setContainer] = useState9(null);
2315
+ useEffect11(() => {
755
2316
  setContainer(containerRef.current);
756
2317
  });
757
- useEffect6(() => {
2318
+ useEffect11(() => {
758
2319
  if (!container) return;
759
2320
  setViewportHeight(container.clientHeight);
760
2321
  const ro = new ResizeObserver((entries) => {
@@ -765,7 +2326,7 @@ function useVirtualList(options) {
765
2326
  ro.observe(container);
766
2327
  return () => ro.disconnect();
767
2328
  }, [container]);
768
- useEffect6(() => {
2329
+ useEffect11(() => {
769
2330
  if (!container) return;
770
2331
  const onScroll = () => {
771
2332
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
@@ -779,27 +2340,27 @@ function useVirtualList(options) {
779
2340
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
780
2341
  };
781
2342
  }, [container]);
782
- const rows = useMemo2(
2343
+ const rows = useMemo7(
783
2344
  () => buildVirtualRows(blocks, heightCacheRef.current, charsPerLine),
784
2345
  // eslint-disable-next-line react-hooks/exhaustive-deps
785
2346
  [blocks, charsPerLine, heightCacheVersion]
786
2347
  );
787
- const visibleRange = useMemo2(
2348
+ const visibleRange = useMemo7(
788
2349
  () => computeVisibleRange(rows, scrollTop, viewportHeight, overscan),
789
2350
  [rows, scrollTop, viewportHeight, overscan]
790
2351
  );
791
- const updateBlockHeight = useCallback3((blockId, height) => {
2352
+ const updateBlockHeight = useCallback5((blockId, height) => {
792
2353
  const cache = heightCacheRef.current;
793
2354
  if (cache.get(blockId) !== height) {
794
2355
  cache.set(blockId, height);
795
2356
  setHeightCacheVersion((v) => v + 1);
796
2357
  }
797
2358
  }, []);
798
- const isBlockVisible = useCallback3(
2359
+ const isBlockVisible = useCallback5(
799
2360
  (blockIndex) => blockIndex >= visibleRange.startIndex && blockIndex <= visibleRange.endIndex,
800
2361
  [visibleRange]
801
2362
  );
802
- const invalidateAllHeights = useCallback3(() => {
2363
+ const invalidateAllHeights = useCallback5(() => {
803
2364
  heightCacheRef.current.clear();
804
2365
  setHeightCacheVersion((v) => v + 1);
805
2366
  }, []);
@@ -813,75 +2374,12 @@ function useVirtualList(options) {
813
2374
  };
814
2375
  }
815
2376
 
816
- // src/MdxSkeleton.tsx
817
- import { jsx as jsx7 } from "react/jsx-runtime";
818
- function MdxSkeleton({ height, lines = 3, className, style }) {
819
- const combinedStyle = {
820
- ...style,
821
- ...height != null ? { height, minHeight: height } : {}
822
- };
823
- return /* @__PURE__ */ jsx7(
824
- "div",
825
- {
826
- className: `owo-mdx-skeleton-block ${className ?? ""}`,
827
- style: combinedStyle,
828
- "aria-hidden": "true",
829
- children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx7("div", { className: "owo-mdx-skeleton owo-mdx-skeleton-line" }, i))
830
- }
831
- );
832
- }
833
-
834
- // src/MdxComponentShell.tsx
835
- import {
836
- useRef as useRef7,
837
- useState as useState5,
838
- useEffect as useEffect7,
839
- useCallback as useCallback4
840
- } from "react";
841
- import { jsx as jsx8 } from "react/jsx-runtime";
842
- var componentHeightCache = /* @__PURE__ */ new Map();
843
- var DEFAULT_ESTIMATED_HEIGHT = 80;
844
- function MdxComponentShell({ name, children }) {
845
- const containerRef = useRef7(null);
846
- const [ready, setReady] = useState5(false);
847
- const estimatedHeight = componentHeightCache.get(name) ?? DEFAULT_ESTIMATED_HEIGHT;
848
- useEffect7(() => {
849
- const id = requestAnimationFrame(() => setReady(true));
850
- return () => cancelAnimationFrame(id);
851
- }, []);
852
- const measureHeight = useCallback4(() => {
853
- const el = containerRef.current;
854
- if (!el) return;
855
- const h = el.getBoundingClientRect().height;
856
- if (h > 0) {
857
- componentHeightCache.set(name, h);
858
- }
859
- }, [name]);
860
- useEffect7(() => {
861
- const el = containerRef.current;
862
- if (!el || !ready) return;
863
- measureHeight();
864
- const ro = new ResizeObserver(() => measureHeight());
865
- ro.observe(el);
866
- return () => ro.disconnect();
867
- }, [ready, measureHeight]);
868
- if (!ready) {
869
- return /* @__PURE__ */ jsx8(MdxSkeleton, { height: estimatedHeight });
870
- }
871
- return /* @__PURE__ */ jsx8("div", { ref: containerRef, children });
872
- }
873
- function wrapWithShell(name, Component) {
874
- function ShellWrapped(props) {
875
- return /* @__PURE__ */ jsx8(MdxComponentShell, { name, children: /* @__PURE__ */ jsx8(Component, { ...props }) });
876
- }
877
- ShellWrapped.displayName = `MdxShell(${name})`;
878
- return ShellWrapped;
879
- }
880
- function clearComponentHeightCache() {
881
- componentHeightCache.clear();
882
- }
2377
+ // src/index.ts
2378
+ import { registerPlugin } from "@owomark/processor";
883
2379
  export {
2380
+ AsyncCodeBlock,
884
2381
  DEFAULT_EDITOR_CONFIG,
2382
+ DEFAULT_TOOLBAR_COMPONENT_COMMANDS,
885
2383
  EditorBlock,
886
2384
  EditorDecorator,
887
2385
  EditorLeaf,
@@ -892,15 +2390,21 @@ export {
892
2390
  SlashMenu,
893
2391
  THEME_DARK_CLASS,
894
2392
  THEME_LIGHT_CLASS,
2393
+ Toolbar,
2394
+ acquireMdxWorker,
895
2395
  clearComponentHeightCache,
896
2396
  computeMenuPosition,
897
2397
  deriveProcessorOptions,
898
2398
  getCaretRect,
899
2399
  getThemeClassName2 as getThemeClassName,
2400
+ registerPlugin,
2401
+ releaseMdxWorker,
900
2402
  resolveEditorConfig,
901
2403
  useBlockContext,
902
2404
  useOwoMarkCore,
903
2405
  useOwoMarkSharedState,
2406
+ useScrollController,
2407
+ useSharedStateSelector,
904
2408
  useSharedStateSnapshot,
905
2409
  useVirtualList,
906
2410
  wrapWithShell