@owomark/react 0.1.6 → 0.1.8

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,7 +757,7 @@ var OwoMarkEditor = forwardRef(
588
757
  );
589
758
 
590
759
  // src/OwoMarkPreview.tsx
591
- import { useRef as useRef8, useEffect as useEffect6 } 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";
@@ -596,31 +765,31 @@ import {
596
765
  // src/MdxPreview.tsx
597
766
  import {
598
767
  Component,
599
- useEffect as useEffect5,
600
- useMemo as useMemo4,
601
- useRef as useRef7,
602
- useState as useState5,
603
- useSyncExternalStore
768
+ forwardRef as forwardRef2,
769
+ useEffect as useEffect7,
770
+ useMemo as useMemo6,
771
+ useRef as useRef10,
772
+ useState as useState7
604
773
  } from "react";
605
774
  import {
606
775
  DEFAULT_MDX_COMPONENTS
607
776
  } from "@owomark/processor";
608
777
 
609
778
  // src/MdxSkeleton.tsx
610
- import { useMemo as useMemo2, useRef as useRef4 } from "react";
779
+ import { useMemo as useMemo3, useRef as useRef5 } from "react";
611
780
  import { createSkeletonHtml, ensureSkeletonStyles } from "@owomark/view";
612
- import { jsx as jsx6 } from "react/jsx-runtime";
781
+ import { jsx as jsx7 } from "react/jsx-runtime";
613
782
  function MdxSkeleton({ height, lines, className, style }) {
614
- const injected = useRef4(false);
783
+ const injected = useRef5(false);
615
784
  if (!injected.current && typeof document !== "undefined") {
616
785
  ensureSkeletonStyles(document);
617
786
  injected.current = true;
618
787
  }
619
- const html = useMemo2(
788
+ const html = useMemo3(
620
789
  () => createSkeletonHtml({ height, lines: lines ?? (height == null ? 3 : void 0) }),
621
790
  [height, lines]
622
791
  );
623
- return /* @__PURE__ */ jsx6(
792
+ return /* @__PURE__ */ jsx7(
624
793
  "div",
625
794
  {
626
795
  className: className ?? void 0,
@@ -632,13 +801,13 @@ function MdxSkeleton({ height, lines, className, style }) {
632
801
 
633
802
  // src/MdxComponentShell.tsx
634
803
  import {
635
- useRef as useRef5,
636
- useState as useState3,
637
- useEffect as useEffect3,
804
+ useRef as useRef6,
805
+ useState as useState4,
806
+ useEffect as useEffect4,
638
807
  useLayoutEffect as useLayoutEffect3,
639
- useCallback as useCallback2
808
+ useCallback as useCallback3
640
809
  } from "react";
641
- import { jsx as jsx7 } from "react/jsx-runtime";
810
+ import { jsx as jsx8 } from "react/jsx-runtime";
642
811
  var componentHeightCache = /* @__PURE__ */ new Map();
643
812
  var DEFAULT_ESTIMATED_HEIGHT = 80;
644
813
  function shallowPropsFingerprint(props) {
@@ -652,11 +821,11 @@ function shallowPropsFingerprint(props) {
652
821
  return parts.join("&");
653
822
  }
654
823
  function MdxComponentShell({ name, propsFingerprint, children }) {
655
- const containerRef = useRef5(null);
656
- const [status, setStatus] = useState3("skeleton");
824
+ const containerRef = useRef6(null);
825
+ const [status, setStatus] = useState4("skeleton");
657
826
  const cacheKey = propsFingerprint ? `${name}:${propsFingerprint}` : name;
658
827
  const estimatedHeight = componentHeightCache.get(cacheKey) ?? DEFAULT_ESTIMATED_HEIGHT;
659
- useEffect3(() => {
828
+ useEffect4(() => {
660
829
  const id = requestAnimationFrame(() => setStatus("mounting"));
661
830
  return () => cancelAnimationFrame(id);
662
831
  }, []);
@@ -665,7 +834,7 @@ function MdxComponentShell({ name, propsFingerprint, children }) {
665
834
  const id = requestAnimationFrame(() => setStatus("ready"));
666
835
  return () => cancelAnimationFrame(id);
667
836
  }, [status]);
668
- const measureHeight = useCallback2(() => {
837
+ const measureHeight = useCallback3(() => {
669
838
  const el = containerRef.current;
670
839
  if (!el) return;
671
840
  const h = el.getBoundingClientRect().height;
@@ -673,7 +842,7 @@ function MdxComponentShell({ name, propsFingerprint, children }) {
673
842
  componentHeightCache.set(cacheKey, h);
674
843
  }
675
844
  }, [cacheKey]);
676
- useEffect3(() => {
845
+ useEffect4(() => {
677
846
  const el = containerRef.current;
678
847
  if (!el || status !== "ready") return;
679
848
  measureHeight();
@@ -682,14 +851,14 @@ function MdxComponentShell({ name, propsFingerprint, children }) {
682
851
  return () => ro.disconnect();
683
852
  }, [status, measureHeight]);
684
853
  if (status === "skeleton") {
685
- return /* @__PURE__ */ jsx7(MdxSkeleton, { height: estimatedHeight });
854
+ return /* @__PURE__ */ jsx8(MdxSkeleton, { height: estimatedHeight });
686
855
  }
687
- return /* @__PURE__ */ jsx7("div", { ref: containerRef, children });
856
+ return /* @__PURE__ */ jsx8("div", { ref: containerRef, children });
688
857
  }
689
858
  function wrapWithShell(name, Component2) {
690
859
  function ShellWrapped(props) {
691
860
  const fingerprint = shallowPropsFingerprint(props);
692
- return /* @__PURE__ */ jsx7(MdxComponentShell, { name, propsFingerprint: fingerprint, children: /* @__PURE__ */ jsx7(Component2, { ...props }) });
861
+ return /* @__PURE__ */ jsx8(MdxComponentShell, { name, propsFingerprint: fingerprint, children: /* @__PURE__ */ jsx8(Component2, { ...props }) });
693
862
  }
694
863
  ShellWrapped.displayName = `MdxShell(${name})`;
695
864
  return ShellWrapped;
@@ -730,8 +899,8 @@ function parseCompileError(error, kind) {
730
899
  }
731
900
 
732
901
  // src/useMdxPreviewCompilation.ts
733
- import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef6, useState as useState4 } from "react";
734
- import { deriveRenderKey } from "@owomark/core";
902
+ import { useEffect as useEffect6, useMemo as useMemo5, useRef as useRef8, useState as useState6 } from "react";
903
+ import { deriveRenderKey as deriveRenderKey2 } from "@owomark/core";
735
904
 
736
905
  // src/mdx-runtime.ts
737
906
  import { compile, run } from "@mdx-js/mdx";
@@ -745,6 +914,7 @@ import {
745
914
  var workerInstance = null;
746
915
  var workerRefCount = 0;
747
916
  var requestCounter = 0;
917
+ var WORKER_COMPILE_TIMEOUT_MS = 5e3;
748
918
  var MAX_CRASHES = 3;
749
919
  var CRASH_COOLDOWN_MS = 5e3;
750
920
  var CRASH_WINDOW_MS = 3e4;
@@ -783,6 +953,7 @@ function tryCreateWorker() {
783
953
  const pending = pendingRequests.get(response.id);
784
954
  if (!pending) return;
785
955
  pendingRequests.delete(response.id);
956
+ pending.cleanup?.();
786
957
  if (response.ok) {
787
958
  pending.resolve(response.code);
788
959
  } else {
@@ -795,6 +966,7 @@ function tryCreateWorker() {
795
966
  workerInstance = null;
796
967
  for (const [id, pending] of pendingRequests) {
797
968
  pendingRequests.delete(id);
969
+ pending.cleanup?.();
798
970
  pending.reject({ message: "MDX compile worker crashed", kind: "compile" });
799
971
  }
800
972
  };
@@ -833,8 +1005,15 @@ function compileInWorker(markdown, options, inspection) {
833
1005
  const workerRef = maybeWorker;
834
1006
  const id = ++requestCounter;
835
1007
  let cancelled = false;
1008
+ let timeoutId = null;
1009
+ const cleanup = () => {
1010
+ if (timeoutId !== null) {
1011
+ clearTimeout(timeoutId);
1012
+ timeoutId = null;
1013
+ }
1014
+ };
836
1015
  const promise = new Promise((resolve, reject) => {
837
- pendingRequests.set(id, { resolve, reject });
1016
+ pendingRequests.set(id, { resolve, reject, cleanup });
838
1017
  workerRef.postMessage({
839
1018
  type: "compile",
840
1019
  id,
@@ -850,6 +1029,17 @@ function compileInWorker(markdown, options, inspection) {
850
1029
  sourceMap: inspection?.sourceMap
851
1030
  }
852
1031
  });
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);
853
1043
  });
854
1044
  function cancel() {
855
1045
  if (cancelled) return;
@@ -857,6 +1047,7 @@ function compileInWorker(markdown, options, inspection) {
857
1047
  const pending = pendingRequests.get(id);
858
1048
  if (pending) {
859
1049
  pendingRequests.delete(id);
1050
+ pending.cleanup?.();
860
1051
  pending.reject({ message: "Compilation cancelled", kind: "compile" });
861
1052
  }
862
1053
  workerRef.postMessage({ type: "cancel", id });
@@ -894,6 +1085,7 @@ async function compileOnMainThread(markdown, options, inspection) {
894
1085
  }
895
1086
  }
896
1087
  function requiresMainThread(options) {
1088
+ if (options.preferWorker !== true) return true;
897
1089
  if (options.extraRemarkPlugins?.length || options.extraRehypePlugins?.length) return true;
898
1090
  if (options.extraRemarkDescriptors?.length && !allBuiltinDescriptors(options.extraRemarkDescriptors)) return true;
899
1091
  if (options.extraRehypeDescriptors?.length && !allBuiltinDescriptors(options.extraRehypeDescriptors)) return true;
@@ -933,7 +1125,439 @@ async function runMdxCode(code) {
933
1125
 
934
1126
  // src/mdx-preview-utils.tsx
935
1127
  import { renderBlockDefault } from "@owomark/view";
936
- import { jsx as jsx8 } from "react/jsx-runtime";
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: () => {
1196
+ }
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");
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;
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()
1363
+ });
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);
1486
+ return () => {
1487
+ clearTimeout(timer);
1488
+ activeCancel?.();
1489
+ activeRequestRef.current = null;
1490
+ };
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,
1543
+ {
1544
+ code,
1545
+ language,
1546
+ meta,
1547
+ themeKey,
1548
+ documentVersion,
1549
+ className: child.props.className,
1550
+ codeProps: child.props,
1551
+ preProps: nativePreProps
1552
+ }
1553
+ );
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";
937
1561
  var objectIdentityMap = /* @__PURE__ */ new WeakMap();
938
1562
  var nextObjectIdentity = 1;
939
1563
  function getObjectIdentity(value) {
@@ -944,8 +1568,21 @@ function getObjectIdentity(value) {
944
1568
  objectIdentityMap.set(value, id);
945
1569
  return String(id);
946
1570
  }
947
- function buildRuntimeComponents(mdxAnalysis, baseComponents) {
948
- const components = { ...baseComponents };
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
+ };
949
1586
  function registerMissingComponentPath(componentName) {
950
1587
  const parts = componentName.split(".");
951
1588
  let cursor = components;
@@ -957,7 +1594,7 @@ function buildRuntimeComponents(mdxAnalysis, baseComponents) {
957
1594
  if (existing === void 0) {
958
1595
  cursor[part] = function MissingComponent(props) {
959
1596
  const { children, ...restProps } = props;
960
- return /* @__PURE__ */ jsx8("div", { "data-mdx-missing": componentName, ...restProps, children });
1597
+ return /* @__PURE__ */ jsx10("div", { "data-mdx-missing": componentName, ...restProps, children });
961
1598
  };
962
1599
  }
963
1600
  return;
@@ -1010,10 +1647,10 @@ function useMdxPreviewCompilation({
1010
1647
  mdx,
1011
1648
  wrappedComponents
1012
1649
  }) {
1013
- const requestIdRef = useRef6(0);
1014
- const cancelRef = useRef6(null);
1015
- const blockCacheRef = useRef6(/* @__PURE__ */ new Map());
1016
- const [mdxDisplay, setMdxDisplay] = useState4({
1650
+ const requestIdRef = useRef8(0);
1651
+ const cancelRef = useRef8(null);
1652
+ const blockCacheRef = useRef8(/* @__PURE__ */ new Map());
1653
+ const [mdxDisplay, setMdxDisplay] = useState6({
1017
1654
  code: null,
1018
1655
  Content: null,
1019
1656
  runtimeComponents: null,
@@ -1022,8 +1659,8 @@ function useMdxPreviewCompilation({
1022
1659
  pending: false,
1023
1660
  error: null
1024
1661
  });
1025
- const [markdownHtml, setMarkdownHtml] = useState4(null);
1026
- useEffect4(() => {
1662
+ const [markdownHtml, setMarkdownHtml] = useState6(null);
1663
+ useEffect6(() => {
1027
1664
  acquireMdxWorker();
1028
1665
  return () => {
1029
1666
  cancelRef.current?.();
@@ -1031,7 +1668,7 @@ function useMdxPreviewCompilation({
1031
1668
  releaseMdxWorker();
1032
1669
  };
1033
1670
  }, []);
1034
- const mdxInspection = useMemo3(
1671
+ const mdxInspection = useMemo5(
1035
1672
  () => inspectMdxSource2(snapshot.markdown, {
1036
1673
  enableMath: mdx?.enableMath,
1037
1674
  enableSideAnnotation: mdx?.enableSideAnnotation,
@@ -1046,14 +1683,13 @@ function useMdxPreviewCompilation({
1046
1683
  snapshot.markdown
1047
1684
  ]
1048
1685
  );
1049
- const hasMdx = mdxInspection.hasMdxSyntax;
1050
- const optionsFingerprint = useMemo3(() => {
1686
+ const hasMdx = mdx?.forceFullDocumentCompile === true || mdxInspection.hasMdxSyntax;
1687
+ const optionsFingerprint = useMemo5(() => {
1051
1688
  const b = (v) => v === true ? "1" : v === false ? "0" : "_";
1052
1689
  const ds = (arr) => arr?.length ? arr.map((d) => d.options ? `${d.name}(${JSON.stringify(d.options)})` : d.name).join(",") : "";
1053
1690
  return [
1054
1691
  b(mdx?.enableMath),
1055
1692
  b(mdx?.enableSideAnnotation),
1056
- b(mdx?.enableCodeHighlight),
1057
1693
  b(mdx?.sourceAnchors),
1058
1694
  ds(mdx?.extraRemarkDescriptors),
1059
1695
  ds(mdx?.extraRehypeDescriptors),
@@ -1063,26 +1699,25 @@ function useMdxPreviewCompilation({
1063
1699
  }, [
1064
1700
  mdx?.enableMath,
1065
1701
  mdx?.enableSideAnnotation,
1066
- mdx?.enableCodeHighlight,
1067
1702
  mdx?.sourceAnchors,
1068
1703
  mdx?.extraRemarkDescriptors,
1069
1704
  mdx?.extraRehypeDescriptors,
1070
1705
  mdx?.extraRemarkPlugins,
1071
1706
  mdx?.extraRehypePlugins
1072
1707
  ]);
1073
- const compileKey = useMemo3(
1074
- () => deriveRenderKey(`${snapshot.markdown}:${optionsFingerprint}`, "custom", ""),
1708
+ const compileKey = useMemo5(
1709
+ () => deriveRenderKey2(`${snapshot.markdown}:${optionsFingerprint}`, "custom", ""),
1075
1710
  [snapshot.markdown, optionsFingerprint]
1076
1711
  );
1077
- const componentFingerprint = useMemo3(
1078
- () => `cp${getObjectIdentity(mdx?.components)}`,
1079
- [mdx?.components]
1712
+ const componentFingerprint = useMemo5(
1713
+ () => `cp${getObjectIdentity(mdx?.components)}:theme:${themeKey}`,
1714
+ [mdx?.components, themeKey]
1080
1715
  );
1081
- const renderKey = useMemo3(
1716
+ const renderKey = useMemo5(
1082
1717
  () => `${compileKey}:${componentFingerprint}`,
1083
1718
  [compileKey, componentFingerprint]
1084
1719
  );
1085
- useEffect4(() => {
1720
+ useEffect6(() => {
1086
1721
  if (!snapshot.markdown.trim()) {
1087
1722
  cancelRef.current?.();
1088
1723
  cancelRef.current = null;
@@ -1131,7 +1766,10 @@ function useMdxPreviewCompilation({
1131
1766
  return {
1132
1767
  ...prev,
1133
1768
  Content,
1134
- runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents),
1769
+ runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents, {
1770
+ themeKey,
1771
+ documentVersion: snapshot.contentVersion
1772
+ }),
1135
1773
  renderKey,
1136
1774
  pending: false,
1137
1775
  error: null
@@ -1157,7 +1795,7 @@ function useMdxPreviewCompilation({
1157
1795
  const { promise, cancel } = compileMdx(snapshot.markdown, {
1158
1796
  enableMath: mdx?.enableMath,
1159
1797
  enableSideAnnotation: mdx?.enableSideAnnotation,
1160
- enableCodeHighlight: mdx?.enableCodeHighlight,
1798
+ enableCodeHighlight: false,
1161
1799
  sourceAnchors: mdx?.sourceAnchors,
1162
1800
  extraRemarkDescriptors: mdx?.extraRemarkDescriptors,
1163
1801
  extraRehypeDescriptors: mdx?.extraRehypeDescriptors,
@@ -1174,7 +1812,10 @@ function useMdxPreviewCompilation({
1174
1812
  setMdxDisplay({
1175
1813
  code,
1176
1814
  Content,
1177
- runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents),
1815
+ runtimeComponents: buildRuntimeComponents(mdxInspection, wrappedComponents, {
1816
+ themeKey,
1817
+ documentVersion: snapshot.contentVersion
1818
+ }),
1178
1819
  compileKey,
1179
1820
  renderKey,
1180
1821
  pending: false,
@@ -1198,9 +1839,9 @@ function useMdxPreviewCompilation({
1198
1839
  }, [
1199
1840
  compileKey,
1200
1841
  hasMdx,
1201
- mdx?.enableCodeHighlight,
1202
1842
  mdx?.enableMath,
1203
1843
  mdx?.enableSideAnnotation,
1844
+ mdx?.forceFullDocumentCompile,
1204
1845
  mdx?.extraRehypeDescriptors,
1205
1846
  mdx?.extraRehypePlugins,
1206
1847
  mdx?.extraRemarkDescriptors,
@@ -1213,6 +1854,7 @@ function useMdxPreviewCompilation({
1213
1854
  renderBlock,
1214
1855
  renderKey,
1215
1856
  snapshot.markdown,
1857
+ snapshot.contentVersion,
1216
1858
  snapshot.previewBlocks,
1217
1859
  themeKey,
1218
1860
  wrappedComponents
@@ -1224,8 +1866,43 @@ function useMdxPreviewCompilation({
1224
1866
  };
1225
1867
  }
1226
1868
 
1869
+ // src/useOwoMarkSharedState.ts
1870
+ import { useRef as useRef9, useSyncExternalStore, useCallback as useCallback4 } from "react";
1871
+ import {
1872
+ createSharedStateStore
1873
+ } from "@owomark/core";
1874
+ function useOwoMarkSharedState(options) {
1875
+ const controllerRef = useRef9(null);
1876
+ if (!controllerRef.current) {
1877
+ controllerRef.current = createSharedStateStore(options);
1878
+ }
1879
+ return controllerRef.current;
1880
+ }
1881
+ function useSharedStateSnapshot(controller) {
1882
+ const subscribe = useCallback4(
1883
+ (onStoreChange) => controller.subscribe(onStoreChange),
1884
+ [controller]
1885
+ );
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, []);
1901
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1902
+ }
1903
+
1227
1904
  // src/MdxPreview.tsx
1228
- import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1905
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1229
1906
  function locationText(error) {
1230
1907
  if (error.line == null) return null;
1231
1908
  if (error.column == null) return `\u884C ${error.line}`;
@@ -1234,11 +1911,11 @@ function locationText(error) {
1234
1911
  function ErrorNotice({ error }) {
1235
1912
  const location = locationText(error);
1236
1913
  const label = error.kind === "compile" ? "MDX Compile" : "MDX Runtime";
1237
- return /* @__PURE__ */ jsx9("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__ */ jsxs3("div", { className: "flex items-start gap-2", children: [
1238
- /* @__PURE__ */ jsx9("span", { className: "mt-0.5 shrink-0 font-mono text-[11px] font-bold uppercase", children: label }),
1239
- /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1240
- location && /* @__PURE__ */ jsx9("span", { className: "mr-2 rounded bg-[var(--owo-cmp-danger-border)] px-1.5 py-0.5 font-mono text-xs", children: location }),
1241
- /* @__PURE__ */ jsx9("span", { className: "break-words", children: error.message })
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 })
1242
1919
  ] })
1243
1920
  ] }) });
1244
1921
  }
@@ -1252,28 +1929,41 @@ var RuntimeErrorBoundary = class extends Component {
1252
1929
  }
1253
1930
  render() {
1254
1931
  if (this.state.error) {
1255
- return /* @__PURE__ */ jsx9(ErrorNotice, { error: this.state.error });
1932
+ return /* @__PURE__ */ jsx11(ErrorNotice, { error: this.state.error });
1256
1933
  }
1257
1934
  return this.props.children;
1258
1935
  }
1259
1936
  };
1260
- function useStoreSnapshot(store) {
1261
- return useSyncExternalStore(store.subscribe, store.getState, store.getState);
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
+ );
1262
1947
  }
1263
- function MdxPreview(props) {
1948
+ var MdxPreview = forwardRef2(function MdxPreview2(props, ref) {
1264
1949
  const {
1265
1950
  state,
1951
+ markdown,
1266
1952
  className,
1267
1953
  themeKey = "",
1268
1954
  ariaLabel,
1955
+ onScroll,
1956
+ scrollController,
1957
+ scrollSync = "bidirectional",
1269
1958
  renderBlock,
1270
1959
  mdx,
1271
1960
  onContentUpdate
1272
1961
  } = props;
1273
- const snapshot = useStoreSnapshot(state);
1274
- const rootRef = useRef7(null);
1275
- const [runtimeError, setRuntimeError] = useState5(null);
1276
- const wrappedComponents = useMemo4(() => {
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(() => {
1277
1967
  const wrapComponentMap = (componentMap) => {
1278
1968
  const wrapped = {};
1279
1969
  for (const [name, component] of Object.entries(componentMap)) {
@@ -1298,192 +1988,277 @@ function MdxPreview(props) {
1298
1988
  mdx,
1299
1989
  wrappedComponents
1300
1990
  });
1301
- useEffect5(() => {
1991
+ useEffect7(() => {
1302
1992
  setRuntimeError(null);
1303
1993
  }, [mdxDisplay.renderKey]);
1304
- useEffect5(() => {
1994
+ useEffect7(() => {
1305
1995
  const root = rootRef.current;
1306
- if (!root || !onContentUpdate) return;
1307
- const observer = new ResizeObserver(() => onContentUpdate());
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());
1308
2004
  observer.observe(root);
1309
2005
  return () => observer.disconnect();
1310
- }, [onContentUpdate]);
1311
- useEffect5(() => {
1312
- if (!onContentUpdate) return;
1313
- const id = requestAnimationFrame(() => onContentUpdate());
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
+ });
1314
2015
  return () => cancelAnimationFrame(id);
1315
- }, [mdxDisplay, markdownHtml, runtimeError, onContentUpdate]);
2016
+ }, [mdxDisplay, markdownHtml, runtimeError, onContentUpdate, scrollController, scrollSync]);
1316
2017
  const hasContent = mdxDisplay.Content !== null || markdownHtml !== null;
1317
- return /* @__PURE__ */ jsxs3(
2018
+ return /* @__PURE__ */ jsxs5(
1318
2019
  "div",
1319
2020
  {
1320
- ref: rootRef,
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
+ },
1321
2038
  className,
1322
2039
  role: "document",
1323
2040
  "aria-label": ariaLabel ?? "Preview",
1324
2041
  "aria-live": "polite",
2042
+ onScroll,
1325
2043
  children: [
1326
- mdxDisplay.pending && hasContent && /* @__PURE__ */ jsx9("div", { className: "pointer-events-none sticky top-0 z-10 mb-3 h-0.5 w-full animate-pulse rounded-full bg-p400" }),
1327
- mdxDisplay.error && /* @__PURE__ */ jsx9(ErrorNotice, { error: mdxDisplay.error }),
1328
- runtimeError && /* @__PURE__ */ jsx9(ErrorNotice, { error: runtimeError }),
1329
- !hasContent && mdxDisplay.pending && /* @__PURE__ */ jsx9(MdxSkeleton, { lines: 8 }),
1330
- markdownHtml !== null && markdownHtml.map(({ blockId, html }) => /* @__PURE__ */ jsx9("div", { dangerouslySetInnerHTML: { __html: html } }, blockId)),
1331
- mdxDisplay.Content && mdxDisplay.runtimeComponents && /* @__PURE__ */ jsx9(RuntimeErrorBoundary, { onError: setRuntimeError, children: /* @__PURE__ */ jsx9(mdxDisplay.Content, { components: mdxDisplay.runtimeComponents }) }, mdxDisplay.renderKey)
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)
1332
2050
  ]
1333
2051
  }
1334
2052
  );
1335
- }
2053
+ });
1336
2054
 
1337
2055
  // src/OwoMarkPreview.tsx
1338
- import { jsx as jsx10 } from "react/jsx-runtime";
2056
+ import { jsx as jsx12 } from "react/jsx-runtime";
1339
2057
  function isController(store) {
1340
2058
  return typeof store.updateVisibleBlockIds === "function";
1341
2059
  }
1342
- var OwoMarkPreview = function OwoMarkPreview2(props) {
1343
- const {
1344
- state,
1345
- className,
1346
- strategy,
1347
- themeKey,
1348
- registry,
1349
- viewportFirst,
1350
- ariaLabel,
1351
- renderBlock,
1352
- mdx,
1353
- onContentUpdate
1354
- } = props;
1355
- if (strategy === "mdx") {
1356
- return /* @__PURE__ */ jsx10(
1357
- MdxPreview,
1358
- {
1359
- state,
1360
- className,
1361
- themeKey,
1362
- ariaLabel,
1363
- renderBlock,
1364
- mdx,
1365
- onContentUpdate
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();
1366
2083
  }
1367
- );
1368
- }
1369
- const onContentUpdateRef = useRef8(onContentUpdate);
1370
- onContentUpdateRef.current = onContentUpdate;
1371
- const renderBlockRef = useRef8(renderBlock);
1372
- renderBlockRef.current = renderBlock;
1373
- const hasRenderBlock = !!renderBlock;
1374
- const containerRef = useRef8(null);
1375
- const engineRef = useRef8(null);
1376
- useEffect6(() => {
1377
- if (!containerRef.current) return;
1378
- const engineOptions = {
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,
1379
2108
  strategy,
1380
2109
  themeKey,
1381
2110
  registry,
1382
2111
  viewportFirst,
1383
- renderBlock: hasRenderBlock ? (block, ctx) => renderBlockRef.current(block, ctx) : void 0,
1384
- onContentUpdate: () => onContentUpdateRef.current?.()
1385
- };
1386
- const engine = createOwoMarkPreviewEngine(engineOptions);
1387
- engine.mount(containerRef.current);
1388
- engineRef.current = engine;
1389
- const currentState = state.getState();
1390
- void engine.update(currentState);
1391
- return () => {
1392
- engine.destroy();
1393
- engineRef.current = null;
1394
- };
1395
- }, [strategy, themeKey, registry, viewportFirst, hasRenderBlock]);
1396
- useEffect6(() => {
1397
- const unsub = state.subscribe((newState) => {
1398
- const engine = engineRef.current;
1399
- if (engine) {
1400
- void engine.update(newState);
1401
- }
1402
- });
1403
- return unsub;
1404
- }, [state]);
1405
- useEffect6(() => {
1406
- const container = containerRef.current;
1407
- if (!viewportFirst || !container || !isController(state)) return;
1408
- const controller = state;
1409
- const observer = new IntersectionObserver(
1410
- (entries) => {
1411
- const visible = /* @__PURE__ */ new Set();
1412
- for (const id of controller.getState().visibleBlockIds) {
1413
- visible.add(id);
1414
- }
1415
- for (const entry of entries) {
1416
- const blockId = entry.target.getAttribute("data-block-id");
1417
- if (!blockId) continue;
1418
- if (entry.isIntersecting) {
1419
- visible.add(blockId);
1420
- } else {
1421
- visible.delete(blockId);
1422
- }
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
1423
2136
  }
1424
- controller.updateVisibleBlockIds([...visible]);
1425
- },
1426
- { root: container, rootMargin: "200px 0px" }
1427
- );
1428
- const wrappers = container.querySelectorAll("[data-block-id]");
1429
- for (const wrapper of wrappers) {
1430
- observer.observe(wrapper);
2137
+ );
1431
2138
  }
1432
- const mutation = new MutationObserver((mutations) => {
1433
- for (const m of mutations) {
1434
- for (const node of m.addedNodes) {
1435
- if (node instanceof HTMLElement && node.hasAttribute("data-block-id")) {
1436
- observer.observe(node);
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
+ }
1437
2216
  }
1438
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
1439
2250
  }
1440
- });
1441
- mutation.observe(container, { childList: true, subtree: true });
1442
- return () => {
1443
- observer.disconnect();
1444
- mutation.disconnect();
1445
- };
1446
- }, [viewportFirst, state]);
1447
- return /* @__PURE__ */ jsx10(
1448
- "div",
1449
- {
1450
- ref: containerRef,
1451
- className,
1452
- role: "document",
1453
- "aria-label": ariaLabel ?? "Preview",
1454
- "aria-live": "polite"
1455
- }
1456
- );
1457
- };
1458
-
1459
- // src/useOwoMarkSharedState.ts
1460
- import { useRef as useRef9, useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback3 } from "react";
1461
- import {
1462
- createSharedStateStore
1463
- } from "@owomark/core";
1464
- function useOwoMarkSharedState(options) {
1465
- const controllerRef = useRef9(null);
1466
- if (!controllerRef.current) {
1467
- controllerRef.current = createSharedStateStore(options);
2251
+ );
1468
2252
  }
1469
- return controllerRef.current;
1470
- }
1471
- function useSharedStateSnapshot(controller) {
1472
- const subscribe = useCallback3(
1473
- (onStoreChange) => controller.subscribe(onStoreChange),
1474
- [controller]
1475
- );
1476
- const getSnapshot = useCallback3(() => controller.getState(), [controller]);
1477
- return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
1478
- }
2253
+ );
1479
2254
 
1480
2255
  // src/useBlockContext.ts
1481
- import { useState as useState6, useEffect as useEffect8 } from "react";
2256
+ import { useState as useState8, useEffect as useEffect9 } from "react";
1482
2257
  function useBlockContext(core) {
1483
- const [blockContext, setBlockContext] = useState6(
2258
+ const [blockContext, setBlockContext] = useState8(
1484
2259
  () => core.getBlockContext()
1485
2260
  );
1486
- useEffect8(() => {
2261
+ useEffect9(() => {
1487
2262
  setBlockContext(core.getBlockContext());
1488
2263
  return core.onBlockContextChange((ctx) => {
1489
2264
  setBlockContext(ctx);
@@ -1495,24 +2270,52 @@ function useBlockContext(core) {
1495
2270
  // src/index.ts
1496
2271
  import { getThemeClassName as getThemeClassName2, THEME_LIGHT_CLASS, THEME_DARK_CLASS } from "@owomark/view";
1497
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
+
1498
2301
  // src/useVirtualList.ts
1499
- import { useState as useState7, useCallback as useCallback4, useRef as useRef10, useEffect as useEffect9, useMemo as useMemo5 } from "react";
2302
+ import { useState as useState9, useCallback as useCallback5, useRef as useRef13, useEffect as useEffect11, useMemo as useMemo7 } from "react";
1500
2303
  import {
1501
2304
  buildVirtualRows,
1502
2305
  computeVisibleRange
1503
2306
  } from "@owomark/core";
1504
2307
  function useVirtualList(options) {
1505
2308
  const { blocks, containerRef, overscan = 5, charsPerLine = 80 } = options;
1506
- const heightCacheRef = useRef10(/* @__PURE__ */ new Map());
1507
- const [scrollTop, setScrollTop] = useState7(0);
1508
- const [viewportHeight, setViewportHeight] = useState7(0);
1509
- const [heightCacheVersion, setHeightCacheVersion] = useState7(0);
1510
- const rafRef = useRef10(0);
1511
- const [container, setContainer] = useState7(null);
1512
- useEffect9(() => {
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(() => {
1513
2316
  setContainer(containerRef.current);
1514
2317
  });
1515
- useEffect9(() => {
2318
+ useEffect11(() => {
1516
2319
  if (!container) return;
1517
2320
  setViewportHeight(container.clientHeight);
1518
2321
  const ro = new ResizeObserver((entries) => {
@@ -1523,7 +2326,7 @@ function useVirtualList(options) {
1523
2326
  ro.observe(container);
1524
2327
  return () => ro.disconnect();
1525
2328
  }, [container]);
1526
- useEffect9(() => {
2329
+ useEffect11(() => {
1527
2330
  if (!container) return;
1528
2331
  const onScroll = () => {
1529
2332
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
@@ -1537,27 +2340,27 @@ function useVirtualList(options) {
1537
2340
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
1538
2341
  };
1539
2342
  }, [container]);
1540
- const rows = useMemo5(
2343
+ const rows = useMemo7(
1541
2344
  () => buildVirtualRows(blocks, heightCacheRef.current, charsPerLine),
1542
2345
  // eslint-disable-next-line react-hooks/exhaustive-deps
1543
2346
  [blocks, charsPerLine, heightCacheVersion]
1544
2347
  );
1545
- const visibleRange = useMemo5(
2348
+ const visibleRange = useMemo7(
1546
2349
  () => computeVisibleRange(rows, scrollTop, viewportHeight, overscan),
1547
2350
  [rows, scrollTop, viewportHeight, overscan]
1548
2351
  );
1549
- const updateBlockHeight = useCallback4((blockId, height) => {
2352
+ const updateBlockHeight = useCallback5((blockId, height) => {
1550
2353
  const cache = heightCacheRef.current;
1551
2354
  if (cache.get(blockId) !== height) {
1552
2355
  cache.set(blockId, height);
1553
2356
  setHeightCacheVersion((v) => v + 1);
1554
2357
  }
1555
2358
  }, []);
1556
- const isBlockVisible = useCallback4(
2359
+ const isBlockVisible = useCallback5(
1557
2360
  (blockIndex) => blockIndex >= visibleRange.startIndex && blockIndex <= visibleRange.endIndex,
1558
2361
  [visibleRange]
1559
2362
  );
1560
- const invalidateAllHeights = useCallback4(() => {
2363
+ const invalidateAllHeights = useCallback5(() => {
1561
2364
  heightCacheRef.current.clear();
1562
2365
  setHeightCacheVersion((v) => v + 1);
1563
2366
  }, []);
@@ -1574,7 +2377,9 @@ function useVirtualList(options) {
1574
2377
  // src/index.ts
1575
2378
  import { registerPlugin } from "@owomark/processor";
1576
2379
  export {
2380
+ AsyncCodeBlock,
1577
2381
  DEFAULT_EDITOR_CONFIG,
2382
+ DEFAULT_TOOLBAR_COMPONENT_COMMANDS,
1578
2383
  EditorBlock,
1579
2384
  EditorDecorator,
1580
2385
  EditorLeaf,
@@ -1585,6 +2390,7 @@ export {
1585
2390
  SlashMenu,
1586
2391
  THEME_DARK_CLASS,
1587
2392
  THEME_LIGHT_CLASS,
2393
+ Toolbar,
1588
2394
  acquireMdxWorker,
1589
2395
  clearComponentHeightCache,
1590
2396
  computeMenuPosition,
@@ -1597,6 +2403,8 @@ export {
1597
2403
  useBlockContext,
1598
2404
  useOwoMarkCore,
1599
2405
  useOwoMarkSharedState,
2406
+ useScrollController,
2407
+ useSharedStateSelector,
1600
2408
  useSharedStateSnapshot,
1601
2409
  useVirtualList,
1602
2410
  wrapWithShell