@owomark/view 0.1.5 → 0.1.6

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.
@@ -11,10 +11,9 @@ var HEAVY_LARGE_THRESHOLD = 50;
11
11
  var HEAVY_SMALL_HEIGHT = 60;
12
12
  var HEAVY_MEDIUM_HEIGHT = 200;
13
13
  var HEAVY_LARGE_HEIGHT = 500;
14
- var HEAVY_KINDS = /* @__PURE__ */ new Set(["mermaid", "katex", "math-block", "chart", "embed"]);
15
- function estimateBlockHeight(block) {
14
+ function estimateBlockHeight(block, registry) {
16
15
  const lineCount = block.endLine - block.startLine + 1;
17
- if (HEAVY_KINDS.has(block.kind)) {
16
+ if (registry?.isHeavy(block.kind)) {
18
17
  return estimateHeavyBlockHeight(lineCount);
19
18
  }
20
19
  if (block.kind === "code-fence") {
@@ -1,25 +1,44 @@
1
- // src/virtual/viewport-manager.ts
2
- var BLOCK_ID_ATTR = "data-block-id";
3
- var SOURCE_START_ATTR = "data-source-line-start";
4
- var SOURCE_END_ATTR = "data-source-line-end";
5
- var HEIGHT_TRANSITION = "top 0.15s ease-out";
6
- var TOTAL_HEIGHT_TRANSITION = "height 0.15s ease-out";
1
+ // src/dom/skeleton.ts
7
2
  var SKELETON_GAP = 8;
8
- function createSkeletonHtml(height) {
9
- const innerHeight = Math.max(height - SKELETON_GAP, 8);
10
- return `<div class="owo-skeleton" style="height:${innerHeight}px;margin-bottom:${SKELETON_GAP}px;border-radius:4px;background:linear-gradient(90deg,#e2e2e2 25%,#efefef 50%,#e2e2e2 75%);background-size:200% 100%;animation:zm-skeleton-pulse 1.5s ease-in-out infinite" aria-hidden="true"></div>`;
3
+ var SKELETON_LINE_HEIGHT = 14;
4
+ var SKELETON_PADDING = 24;
5
+ function createSkeletonHtml(options = {}) {
6
+ const { height, lines } = options;
7
+ const lineCount = lines ?? (height != null ? Math.max(1, Math.min(5, Math.round(Math.max(height - SKELETON_PADDING, 8) / (SKELETON_LINE_HEIGHT + SKELETON_GAP)))) : 3);
8
+ const heightStyle = height != null ? ` style="height:${height}px;min-height:${height}px"` : "";
9
+ const lineEls = [];
10
+ for (let i = 0; i < lineCount; i++) {
11
+ const isLast = i === lineCount - 1;
12
+ lineEls.push(`<div class="owo-mdx-skeleton owo-mdx-skeleton-line"${isLast ? ' style="width:60%"' : ""}></div>`);
13
+ }
14
+ return `<div class="owo-mdx-skeleton-block"${heightStyle} aria-hidden="true">${lineEls.join("")}</div>`;
11
15
  }
16
+ var SKELETON_CSS = `
17
+ .owo-mdx-skeleton{background:linear-gradient(90deg,var(--owo-skeleton-base,#e5e7eb) 25%,var(--owo-skeleton-shine,#f3f4f6) 50%,var(--owo-skeleton-base,#e5e7eb) 75%);background-size:200% 100%;animation:owo-mdx-skeleton-shimmer 1.5s ease-in-out infinite;border-radius:4px}
18
+ @keyframes owo-mdx-skeleton-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
19
+ .owo-mdx-skeleton-block{display:flex;flex-direction:column;gap:8px;padding:12px 0}
20
+ .owo-mdx-skeleton-line{height:14px}
21
+ .owo-mdx-skeleton-line:last-child{width:60%}
22
+ `.trim();
23
+ var STYLE_ID = "owo-mdx-skeleton-styles";
12
24
  function ensureSkeletonStyles(doc) {
13
25
  try {
14
- if (doc.getElementById?.("owo-skeleton-styles")) return;
26
+ if (doc.getElementById?.(STYLE_ID)) return;
15
27
  const style = doc.createElement("style");
16
- style.id = "owo-skeleton-styles";
17
- style.textContent = `@keyframes zm-skeleton-pulse{0%{background-position:200% 0}100%{background-position:-200% 0}}`;
28
+ style.id = STYLE_ID;
29
+ style.textContent = SKELETON_CSS;
18
30
  const target = doc.head ?? doc.body;
19
31
  target?.appendChild(style);
20
32
  } catch {
21
33
  }
22
34
  }
35
+
36
+ // src/virtual/viewport-manager.ts
37
+ var BLOCK_ID_ATTR = "data-block-id";
38
+ var SOURCE_START_ATTR = "data-source-line-start";
39
+ var SOURCE_END_ATTR = "data-source-line-end";
40
+ var HEIGHT_TRANSITION = "top 0.15s ease-out";
41
+ var TOTAL_HEIGHT_TRANSITION = "height 0.15s ease-out";
23
42
  var VirtualViewportManager = class {
24
43
  root = null;
25
44
  scrollContainer = null;
@@ -93,7 +112,7 @@ var VirtualViewportManager = class {
93
112
  wrapper.style.left = "0";
94
113
  wrapper.style.width = "100%";
95
114
  wrapper.style.transition = HEIGHT_TRANSITION;
96
- wrapper.innerHTML = createSkeletonHtml(layout.height);
115
+ wrapper.innerHTML = createSkeletonHtml({ height: layout.height });
97
116
  this.contentLayer.appendChild(wrapper);
98
117
  this.mounted.set(block.blockId, wrapper);
99
118
  newlyMounted.add(block.blockId);
@@ -188,5 +207,7 @@ var VirtualViewportManager = class {
188
207
  };
189
208
 
190
209
  export {
210
+ createSkeletonHtml,
211
+ ensureSkeletonStyles,
191
212
  VirtualViewportManager
192
213
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { OwoMarkCore, OwoMarkEditorInstance, OwoMarkSharedState, PreviewBlockKind, PreviewBlock } from '@owomark/core';
1
+ import { OwoMarkCore, OwoMarkEditorInstance, PreviewBlock } from '@owomark/core';
2
2
  export { DocumentChangeCallback } from '@owomark/core';
3
+ import { O as OwoMarkPreviewEngineOptions, a as OwoMarkPreviewEngine, P as PreviewRendererRegistry, b as PreviewTaskScheduler } from './types-DsL_4tUb.js';
4
+ export { c as PreviewBlockMetadata, d as PreviewBlockRenderer, e as PreviewCacheEntry, f as PreviewRenderContext, g as PreviewRenderPhase, h as PreviewRenderResult, i as PreviewRendererDefinition, j as PreviewRendererMode, k as PreviewStrategy, l as PreviewTaskPriority } from './types-DsL_4tUb.js';
3
5
 
4
6
  /**
5
7
  * Three-layer view engine for OwoMark.
@@ -45,77 +47,6 @@ declare function createOwoMarkView(core: OwoMarkCore, element: HTMLElement): Owo
45
47
  */
46
48
  declare function createOwoMarkVanillaEditor(): OwoMarkEditorInstance;
47
49
 
48
- type PreviewRenderPhase = 'idle' | 'rendering' | 'highlighting' | 'ready' | 'error';
49
- type PreviewCacheEntry = {
50
- blockId: string;
51
- renderKey: string;
52
- html: string;
53
- highlighted: boolean;
54
- themeKey: string;
55
- updatedAt: number;
56
- };
57
- type PreviewRenderContext = {
58
- version: number;
59
- themeKey: string;
60
- abortSignal?: AbortSignal;
61
- /**
62
- * Line offset to add to source-line attributes in the rendered HTML.
63
- * When a block starts at line N in the document, the renderer processes
64
- * its raw content starting from line 1; this offset (N - 1) must be
65
- * added to `data-source-line-start/end` so scroll sync anchors map
66
- * to the correct document lines.
67
- */
68
- sourceLineOffset: number;
69
- };
70
- type PreviewRenderResult = {
71
- kind: 'html';
72
- html: string;
73
- } | {
74
- kind: 'dom';
75
- mount: (container: HTMLElement) => void;
76
- unmount?: () => void;
77
- };
78
- type PreviewRendererMode = 'html-worker-safe' | 'dom-main-thread';
79
- type PreviewTaskPriority = 'realtime' | 'deferred';
80
- type PreviewRendererDefinition = {
81
- mode: PreviewRendererMode;
82
- priority: PreviewTaskPriority;
83
- render: PreviewBlockRenderer;
84
- version: string;
85
- };
86
- type PreviewBlockRenderer = (block: PreviewBlock, context: PreviewRenderContext) => Promise<PreviewRenderResult> | PreviewRenderResult;
87
- type PreviewRendererRegistry = {
88
- get(kind: PreviewBlockKind): PreviewRendererDefinition | null;
89
- register(kind: PreviewBlockKind, renderer: PreviewRendererDefinition): void;
90
- unregister(kind: PreviewBlockKind): void;
91
- };
92
- type PreviewStrategy = 'incremental' | 'virtual';
93
- type OwoMarkPreviewEngineOptions = {
94
- strategy?: PreviewStrategy;
95
- themeKey?: string;
96
- registry?: PreviewRendererRegistry;
97
- viewportFirst?: boolean;
98
- /**
99
- * External block renderer function. When provided, the engine uses this
100
- * instead of the built-in default renderer. This allows the host to supply
101
- * its own Markdown pipeline (unified, remark, rehype, Shiki, etc.) without
102
- * the preview package bundling those heavy dependencies.
103
- */
104
- renderBlock?: (block: PreviewBlock, context: PreviewRenderContext) => Promise<string>;
105
- /**
106
- * Called after every DOM mutation — including idle-backfilled and deferred
107
- * block renders — not just after the synchronous update() return.
108
- * Use for scroll sync or other post-DOM-update side effects.
109
- */
110
- onContentUpdate?: () => void;
111
- };
112
- type OwoMarkPreviewEngine = {
113
- mount(root: HTMLElement): void;
114
- destroy(): void;
115
- update(state: OwoMarkSharedState): void;
116
- getRenderedVersion(): number;
117
- };
118
-
119
50
  /**
120
51
  * OwoMark Preview Engine factory.
121
52
  *
@@ -145,6 +76,18 @@ declare function createRendererRegistry(): PreviewRendererRegistry;
145
76
  */
146
77
  declare function renderBlockDefault(block: PreviewBlock): string;
147
78
 
79
+ declare function createPreviewTaskScheduler(options?: {
80
+ poolSize?: number;
81
+ onCrash?: () => void;
82
+ }): PreviewTaskScheduler;
83
+
84
+ type SkeletonOptions = {
85
+ height?: number;
86
+ lines?: number;
87
+ };
88
+ declare function createSkeletonHtml(options?: SkeletonOptions): string;
89
+ declare function ensureSkeletonStyles(doc: Document): void;
90
+
148
91
  /**
149
92
  * DOM patcher: blockId-driven incremental DOM updates.
150
93
  *
@@ -226,4 +169,4 @@ declare const THEME_DARK_CLASS = "owo-theme-dark";
226
169
  type OwoMarkThemeName = 'light' | 'dark' | string;
227
170
  declare function getThemeClassName(theme: OwoMarkThemeName): string;
228
171
 
229
- export { type OwoMarkPreviewEngine, type OwoMarkPreviewEngineOptions, type OwoMarkThemeName, type OwoMarkView, type OwoMarkViewEngine, type PreviewBlockRenderer, type PreviewCacheEntry, PreviewDomPatcher, type PreviewRenderContext, type PreviewRenderPhase, type PreviewRenderResult, type PreviewRendererDefinition, type PreviewRendererMode, type PreviewRendererRegistry, type PreviewStrategy, type PreviewTaskPriority, SideAnnotationPositioner, THEME_DARK_CLASS, THEME_LIGHT_CLASS, type ViewEngineOptions, createOwoMarkPreviewEngine, createOwoMarkVanillaEditor, createOwoMarkView, createRendererRegistry, createViewEngine, getThemeClassName, renderBlockDefault };
172
+ export { OwoMarkPreviewEngine, OwoMarkPreviewEngineOptions, type OwoMarkThemeName, type OwoMarkView, type OwoMarkViewEngine, PreviewDomPatcher, PreviewRendererRegistry, PreviewTaskScheduler, SideAnnotationPositioner, type SkeletonOptions, THEME_DARK_CLASS, THEME_LIGHT_CLASS, type ViewEngineOptions, createOwoMarkPreviewEngine, createOwoMarkVanillaEditor, createOwoMarkView, createPreviewTaskScheduler, createRendererRegistry, createSkeletonHtml, createViewEngine, ensureSkeletonStyles, getThemeClassName, renderBlockDefault };
package/dist/index.js CHANGED
@@ -7,10 +7,12 @@ import {
7
7
  import {
8
8
  FALLBACK_BLOCK_HEIGHT,
9
9
  estimateBlockHeight
10
- } from "./chunk-Y72HQJQI.js";
10
+ } from "./chunk-KHKPOH74.js";
11
11
  import {
12
- VirtualViewportManager
13
- } from "./chunk-F3LG7AML.js";
12
+ VirtualViewportManager,
13
+ createSkeletonHtml,
14
+ ensureSkeletonStyles
15
+ } from "./chunk-WA6XHBZS.js";
14
16
 
15
17
  // src/editor.ts
16
18
  import { createDomAdapter } from "@owomark/core/internal/dom-adapter";
@@ -809,17 +811,60 @@ var PreviewDomPatcher = class {
809
811
  };
810
812
 
811
813
  // src/renderer/registry.ts
814
+ var DEFAULT_HEAVY_KINDS = [
815
+ { kind: "mermaid", meta: { mode: "html-worker-safe", priority: "deferred", heavy: true } },
816
+ { kind: "katex", meta: { mode: "html-worker-safe", priority: "deferred", heavy: true } },
817
+ { kind: "math-block", meta: { mode: "html-worker-safe", priority: "deferred", heavy: true } },
818
+ { kind: "chart", meta: { mode: "html-worker-safe", priority: "deferred", heavy: true } },
819
+ { kind: "embed", meta: { mode: "html-worker-safe", priority: "deferred", heavy: true } }
820
+ ];
812
821
  function createRendererRegistry() {
813
822
  const renderers = /* @__PURE__ */ new Map();
823
+ const metadata = /* @__PURE__ */ new Map();
824
+ for (const { kind, meta } of DEFAULT_HEAVY_KINDS) {
825
+ metadata.set(kind, meta);
826
+ }
814
827
  return {
815
828
  get(kind) {
816
829
  return renderers.get(kind) ?? null;
817
830
  },
818
831
  register(kind, renderer) {
819
832
  renderers.set(kind, renderer);
833
+ if (!metadata.has(kind)) {
834
+ metadata.set(kind, {
835
+ mode: renderer.mode,
836
+ priority: renderer.priority
837
+ });
838
+ }
820
839
  },
821
840
  unregister(kind) {
822
841
  renderers.delete(kind);
842
+ },
843
+ registerMetadata(kind, meta) {
844
+ metadata.set(kind, meta);
845
+ },
846
+ isHeavy(kind) {
847
+ const meta = metadata.get(kind);
848
+ if (meta?.heavy) return true;
849
+ const def = renderers.get(kind);
850
+ return def?.priority === "deferred" || false;
851
+ },
852
+ getMode(kind) {
853
+ const def = renderers.get(kind);
854
+ if (def) return def.mode;
855
+ return metadata.get(kind)?.mode ?? null;
856
+ },
857
+ getPriority(kind) {
858
+ const def = renderers.get(kind);
859
+ if (def) return def.priority;
860
+ return metadata.get(kind)?.priority ?? null;
861
+ },
862
+ listRegistered() {
863
+ const kinds = /* @__PURE__ */ new Set([
864
+ ...renderers.keys(),
865
+ ...metadata.keys()
866
+ ]);
867
+ return Array.from(kinds);
823
868
  }
824
869
  };
825
870
  }
@@ -928,7 +973,7 @@ function isDeferred(block, registry) {
928
973
  const def = registry.get(block.kind);
929
974
  return def?.priority === "deferred";
930
975
  }
931
- function createRenderBlockFull(registry, renderCache, externalRenderBlock) {
976
+ function createRenderBlockFull(registry, renderCache, externalRenderBlock, scheduler) {
932
977
  return async function renderBlockFull(block, baseContext) {
933
978
  const context = {
934
979
  ...baseContext,
@@ -936,6 +981,14 @@ function createRenderBlockFull(registry, renderCache, externalRenderBlock) {
936
981
  };
937
982
  const customRenderer = registry.get(block.kind);
938
983
  if (customRenderer) {
984
+ if (customRenderer.mode === "html-worker-safe" && customRenderer.workerModuleUrl && scheduler) {
985
+ try {
986
+ const html2 = await scheduler.submitWorkerTask(block, customRenderer, context);
987
+ renderCache.set(block.renderKey, html2);
988
+ return { kind: "html", html: html2 };
989
+ } catch {
990
+ }
991
+ }
939
992
  const result = await customRenderer.render(block, context);
940
993
  if (result.kind === "html") {
941
994
  renderCache.set(block.renderKey, result.html);
@@ -962,6 +1015,7 @@ function createIncrementalEngine(options) {
962
1015
  const themeKey = options?.themeKey ?? "";
963
1016
  const viewportFirst = options?.viewportFirst ?? false;
964
1017
  const registry = options?.registry ?? createRendererRegistry();
1018
+ const scheduler = options?.scheduler;
965
1019
  const externalRenderBlock = options?.renderBlock;
966
1020
  const onContentUpdate = options?.onContentUpdate;
967
1021
  const patcher = new PreviewDomPatcher();
@@ -972,7 +1026,7 @@ function createIncrementalEngine(options) {
972
1026
  let pendingAbort = null;
973
1027
  let pendingIdleIds = [];
974
1028
  let pendingBackfill = /* @__PURE__ */ new Set();
975
- const renderBlockFull = createRenderBlockFull(registry, renderCache, externalRenderBlock);
1029
+ const renderBlockFull = createRenderBlockFull(registry, renderCache, externalRenderBlock, scheduler);
976
1030
  function applyResult(blockId, result) {
977
1031
  if (result.kind === "html") {
978
1032
  patcher.patchBlockHtml(blockId, result.html);
@@ -1018,12 +1072,14 @@ function createIncrementalEngine(options) {
1018
1072
  return {
1019
1073
  mount(root) {
1020
1074
  patcher.mount(root);
1075
+ ensureSkeletonStyles(root.ownerDocument);
1021
1076
  mounted = true;
1022
1077
  },
1023
1078
  destroy() {
1024
1079
  pendingAbort?.abort();
1025
1080
  pendingAbort = null;
1026
1081
  cancelAllIdle(pendingIdleIds);
1082
+ scheduler?.release();
1027
1083
  patcher.destroy();
1028
1084
  renderCache.clear();
1029
1085
  pendingBackfill.clear();
@@ -1101,18 +1157,26 @@ function createIncrementalEngine(options) {
1101
1157
  }
1102
1158
  }
1103
1159
  if (signal.aborted) return;
1160
+ pendingBackfill = /* @__PURE__ */ new Set();
1161
+ for (const block of realtimeOffscreen) {
1162
+ const height = estimateBlockHeight(block, registry);
1163
+ htmlMap.set(block.blockId, createSkeletonHtml({ height }));
1164
+ pendingBackfill.add(block.blockId);
1165
+ }
1166
+ for (const block of deferredAll) {
1167
+ const height = estimateBlockHeight(block, registry);
1168
+ htmlMap.set(block.blockId, createSkeletonHtml({ height }));
1169
+ pendingBackfill.add(block.blockId);
1170
+ }
1104
1171
  patcher.fullRender(blocks, htmlMap);
1105
1172
  for (const { blockId, result } of domMounts) {
1106
1173
  if (signal.aborted) return;
1107
1174
  applyResult(blockId, result);
1108
1175
  }
1109
- pendingBackfill = /* @__PURE__ */ new Set();
1110
1176
  for (const block of realtimeOffscreen) {
1111
- pendingBackfill.add(block.blockId);
1112
1177
  scheduleBlockRender(block, baseContext, signal, state.version);
1113
1178
  }
1114
1179
  for (const block of deferredAll) {
1115
- pendingBackfill.add(block.blockId);
1116
1180
  scheduleBlockRender(block, baseContext, signal, state.version);
1117
1181
  }
1118
1182
  }
@@ -1229,6 +1293,7 @@ var OffscreenMeasurer = class {
1229
1293
  function createVirtualEngine(options) {
1230
1294
  const themeKey = options?.themeKey ?? "";
1231
1295
  const registry = options?.registry ?? createRendererRegistry();
1296
+ const scheduler = options?.scheduler;
1232
1297
  const externalRenderBlock = options?.renderBlock;
1233
1298
  const onContentUpdate = options?.onContentUpdate;
1234
1299
  const layoutMap = new BlockLayoutMap();
@@ -1236,7 +1301,7 @@ function createVirtualEngine(options) {
1236
1301
  const measurer = new OffscreenMeasurer();
1237
1302
  const viewport = new VirtualViewportManager();
1238
1303
  const renderCache = /* @__PURE__ */ new Map();
1239
- const renderBlockFull = createRenderBlockFull(registry, renderCache, externalRenderBlock);
1304
+ const renderBlockFull = createRenderBlockFull(registry, renderCache, externalRenderBlock, scheduler);
1240
1305
  let renderedVersion = 0;
1241
1306
  let lastBlocks = [];
1242
1307
  let blockMap = /* @__PURE__ */ new Map();
@@ -1253,7 +1318,7 @@ function createVirtualEngine(options) {
1253
1318
  function getInitialHeight(block) {
1254
1319
  const cached = heightCache.get(block.renderKey);
1255
1320
  if (cached !== void 0) return cached;
1256
- return estimateBlockHeight(block);
1321
+ return estimateBlockHeight(block, registry);
1257
1322
  }
1258
1323
  async function renderAndMountBlock(block, baseContext, signal) {
1259
1324
  if (signal.aborted) return;
@@ -1365,7 +1430,7 @@ function createVirtualEngine(options) {
1365
1430
  heightCache.clear();
1366
1431
  layoutMap.invalidateAll((blockId) => {
1367
1432
  const block = blockMap.get(blockId);
1368
- return block ? estimateBlockHeight(block) : FALLBACK_BLOCK_HEIGHT;
1433
+ return block ? estimateBlockHeight(block, registry) : FALLBACK_BLOCK_HEIGHT;
1369
1434
  });
1370
1435
  return true;
1371
1436
  }
@@ -1400,6 +1465,7 @@ function createVirtualEngine(options) {
1400
1465
  blockResizeObserver?.disconnect();
1401
1466
  blockResizeObserver = null;
1402
1467
  observedWrappers.clear();
1468
+ scheduler?.release();
1403
1469
  viewport.destroy();
1404
1470
  measurer.destroy();
1405
1471
  renderCache.clear();
@@ -1525,11 +1591,185 @@ function createOwoMarkPreviewEngine(options) {
1525
1591
  return createIncrementalEngine(options);
1526
1592
  case "virtual":
1527
1593
  return createVirtualEngine(options);
1594
+ case "mdx":
1595
+ throw new Error(
1596
+ 'The "mdx" strategy is implemented by @owomark/react OwoMarkPreview, not by @owomark/view createOwoMarkPreviewEngine().'
1597
+ );
1528
1598
  default:
1529
1599
  throw new Error(`Unknown preview strategy: ${strategy}`);
1530
1600
  }
1531
1601
  }
1532
1602
 
1603
+ // src/worker/preview-task-scheduler.ts
1604
+ var MAX_CONSECUTIVE_CRASHES = 3;
1605
+ var CRASH_WINDOW_MS = 3e4;
1606
+ function serializeBlock(block) {
1607
+ return {
1608
+ kind: block.kind,
1609
+ raw: block.raw,
1610
+ blockId: block.blockId,
1611
+ renderKey: block.renderKey,
1612
+ startLine: block.startLine,
1613
+ endLine: block.endLine
1614
+ };
1615
+ }
1616
+ function createPreviewTaskScheduler(options) {
1617
+ const poolSize = options?.poolSize ?? Math.max(1, (typeof navigator !== "undefined" ? navigator.hardwareConcurrency ?? 4 : 4) - 1);
1618
+ let refCount = 1;
1619
+ let taskCounter = 0;
1620
+ let permanentlyFailed = false;
1621
+ const crashTimestamps = [];
1622
+ const pendingTasks = /* @__PURE__ */ new Map();
1623
+ const workers = [];
1624
+ let nextWorkerIndex = 0;
1625
+ function createWorkerSlot() {
1626
+ try {
1627
+ if (typeof Worker === "undefined") return null;
1628
+ const worker = new Worker(
1629
+ new URL("./preview-render.worker.js", import.meta.url),
1630
+ { type: "module" }
1631
+ );
1632
+ const slot = { worker, pendingCount: 0, dead: false };
1633
+ worker.onmessage = (e) => {
1634
+ const resp = e.data;
1635
+ slot.pendingCount--;
1636
+ const pending = pendingTasks.get(resp.taskId);
1637
+ if (!pending) return;
1638
+ pendingTasks.delete(resp.taskId);
1639
+ if (resp.ok) {
1640
+ pending.resolve(resp.html);
1641
+ } else {
1642
+ pending.reject(new Error(resp.error));
1643
+ }
1644
+ };
1645
+ worker.onerror = () => {
1646
+ slot.dead = true;
1647
+ slot.pendingCount = 0;
1648
+ worker.terminate();
1649
+ recordCrash();
1650
+ options?.onCrash?.();
1651
+ };
1652
+ return slot;
1653
+ } catch {
1654
+ return null;
1655
+ }
1656
+ }
1657
+ function recordCrash() {
1658
+ const now = Date.now();
1659
+ crashTimestamps.push(now);
1660
+ while (crashTimestamps.length > 0 && now - crashTimestamps[0] > CRASH_WINDOW_MS) {
1661
+ crashTimestamps.shift();
1662
+ }
1663
+ if (crashTimestamps.length >= MAX_CONSECUTIVE_CRASHES) {
1664
+ permanentlyFailed = true;
1665
+ terminateAll();
1666
+ }
1667
+ }
1668
+ function getLeastBusySlot() {
1669
+ if (permanentlyFailed) return null;
1670
+ let best = null;
1671
+ for (const slot of workers) {
1672
+ if (slot.dead) continue;
1673
+ if (!best || slot.pendingCount < best.pendingCount) {
1674
+ best = slot;
1675
+ }
1676
+ }
1677
+ if (best) return best;
1678
+ if (workers.length < poolSize) {
1679
+ const slot = createWorkerSlot();
1680
+ if (slot) {
1681
+ workers.push(slot);
1682
+ return slot;
1683
+ }
1684
+ }
1685
+ for (let i = 0; i < workers.length; i++) {
1686
+ if (workers[i].dead) {
1687
+ const slot = createWorkerSlot();
1688
+ if (slot) {
1689
+ workers[i] = slot;
1690
+ return slot;
1691
+ }
1692
+ }
1693
+ }
1694
+ return null;
1695
+ }
1696
+ function terminateAll() {
1697
+ for (const slot of workers) {
1698
+ if (!slot.dead) {
1699
+ slot.dead = true;
1700
+ slot.worker.terminate();
1701
+ }
1702
+ }
1703
+ workers.length = 0;
1704
+ for (const [, pending] of pendingTasks) {
1705
+ pending.reject(new Error("Scheduler terminated"));
1706
+ }
1707
+ pendingTasks.clear();
1708
+ }
1709
+ const scheduler = {
1710
+ submitWorkerTask(block, rendererDef, context) {
1711
+ if (permanentlyFailed || !rendererDef.workerModuleUrl) {
1712
+ return Promise.reject(new Error("Worker unavailable"));
1713
+ }
1714
+ const slot = getLeastBusySlot();
1715
+ if (!slot) {
1716
+ return Promise.reject(new Error("No worker available"));
1717
+ }
1718
+ const taskId = ++taskCounter;
1719
+ return new Promise((resolve, reject) => {
1720
+ pendingTasks.set(taskId, { resolve, reject });
1721
+ slot.pendingCount++;
1722
+ slot.worker.postMessage({
1723
+ type: "render",
1724
+ taskId,
1725
+ block: serializeBlock(block),
1726
+ rendererModuleUrl: rendererDef.workerModuleUrl,
1727
+ context: {
1728
+ version: context.version,
1729
+ themeKey: context.themeKey,
1730
+ sourceLineOffset: context.sourceLineOffset
1731
+ }
1732
+ });
1733
+ });
1734
+ },
1735
+ cancel(taskId) {
1736
+ const pending = pendingTasks.get(taskId);
1737
+ if (pending) {
1738
+ pendingTasks.delete(taskId);
1739
+ pending.reject(new Error("Task cancelled"));
1740
+ }
1741
+ const msg = { type: "cancel", taskId };
1742
+ for (const slot of workers) {
1743
+ if (!slot.dead) slot.worker.postMessage(msg);
1744
+ }
1745
+ },
1746
+ cancelAll() {
1747
+ for (const [, pending] of pendingTasks) {
1748
+ pending.reject(new Error("All tasks cancelled"));
1749
+ }
1750
+ pendingTasks.clear();
1751
+ const msg = { type: "cancel-all" };
1752
+ for (const slot of workers) {
1753
+ if (!slot.dead) {
1754
+ slot.pendingCount = 0;
1755
+ slot.worker.postMessage(msg);
1756
+ }
1757
+ }
1758
+ },
1759
+ acquire() {
1760
+ refCount++;
1761
+ return scheduler;
1762
+ },
1763
+ release() {
1764
+ if (--refCount <= 0) {
1765
+ refCount = 0;
1766
+ terminateAll();
1767
+ }
1768
+ }
1769
+ };
1770
+ return scheduler;
1771
+ }
1772
+
1533
1773
  // src/dom/side-annotation-positioner.ts
1534
1774
  var SideAnnotationPositioner = class {
1535
1775
  container;
@@ -1641,8 +1881,11 @@ export {
1641
1881
  createOwoMarkPreviewEngine,
1642
1882
  createOwoMarkVanillaEditor,
1643
1883
  createOwoMarkView,
1884
+ createPreviewTaskScheduler,
1644
1885
  createRendererRegistry,
1886
+ createSkeletonHtml,
1645
1887
  createViewEngine,
1888
+ ensureSkeletonStyles,
1646
1889
  getThemeClassName,
1647
1890
  renderBlockDefault
1648
1891
  };
@@ -1,13 +1,6 @@
1
1
  import { PreviewBlock } from '@owomark/core';
2
+ import { P as PreviewRendererRegistry } from '../../types-DsL_4tUb.js';
2
3
 
3
- /**
4
- * Heuristic height estimation for blocks before measurement.
5
- *
6
- * Uses block kind and source line count to produce a rough height estimate.
7
- * These estimates are used as initial placeholder heights in the coordinate
8
- * table until real measurement completes.
9
- */
10
-
11
- declare function estimateBlockHeight(block: PreviewBlock): number;
4
+ declare function estimateBlockHeight(block: PreviewBlock, registry?: PreviewRendererRegistry): number;
12
5
 
13
6
  export { estimateBlockHeight };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  estimateBlockHeight
3
- } from "../../chunk-Y72HQJQI.js";
3
+ } from "../../chunk-KHKPOH74.js";
4
4
  export {
5
5
  estimateBlockHeight
6
6
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VirtualViewportManager
3
- } from "../../chunk-F3LG7AML.js";
3
+ } from "../../chunk-WA6XHBZS.js";
4
4
  export {
5
5
  VirtualViewportManager
6
6
  };
@@ -0,0 +1,93 @@
1
+ import { OwoMarkSharedState, PreviewBlockKind, PreviewBlock } from '@owomark/core';
2
+
3
+ type PreviewRenderPhase = 'idle' | 'rendering' | 'highlighting' | 'ready' | 'error';
4
+ type PreviewCacheEntry = {
5
+ blockId: string;
6
+ renderKey: string;
7
+ html: string;
8
+ highlighted: boolean;
9
+ themeKey: string;
10
+ updatedAt: number;
11
+ };
12
+ type PreviewRenderContext = {
13
+ version: number;
14
+ themeKey: string;
15
+ abortSignal?: AbortSignal;
16
+ /**
17
+ * Line offset to add to source-line attributes in the rendered HTML.
18
+ * When a block starts at line N in the document, the renderer processes
19
+ * its raw content starting from line 1; this offset (N - 1) must be
20
+ * added to `data-source-line-start/end` so scroll sync anchors map
21
+ * to the correct document lines.
22
+ */
23
+ sourceLineOffset: number;
24
+ };
25
+ type PreviewRenderResult = {
26
+ kind: 'html';
27
+ html: string;
28
+ } | {
29
+ kind: 'dom';
30
+ mount: (container: HTMLElement) => void;
31
+ unmount?: () => void;
32
+ };
33
+ type PreviewRendererMode = 'html-worker-safe' | 'dom-main-thread';
34
+ type PreviewTaskPriority = 'realtime' | 'deferred';
35
+ type PreviewRendererDefinition = {
36
+ mode: PreviewRendererMode;
37
+ priority: PreviewTaskPriority;
38
+ render: PreviewBlockRenderer;
39
+ version: string;
40
+ workerModuleUrl?: string;
41
+ };
42
+ type PreviewBlockRenderer = (block: PreviewBlock, context: PreviewRenderContext) => Promise<PreviewRenderResult> | PreviewRenderResult;
43
+ type PreviewBlockMetadata = {
44
+ mode: PreviewRendererMode;
45
+ priority: PreviewTaskPriority;
46
+ heavy?: boolean;
47
+ };
48
+ type PreviewRendererRegistry = {
49
+ get(kind: PreviewBlockKind): PreviewRendererDefinition | null;
50
+ register(kind: PreviewBlockKind, renderer: PreviewRendererDefinition): void;
51
+ unregister(kind: PreviewBlockKind): void;
52
+ registerMetadata(kind: PreviewBlockKind, meta: PreviewBlockMetadata): void;
53
+ isHeavy(kind: PreviewBlockKind): boolean;
54
+ getMode(kind: PreviewBlockKind): PreviewRendererMode | null;
55
+ getPriority(kind: PreviewBlockKind): PreviewTaskPriority | null;
56
+ listRegistered(): PreviewBlockKind[];
57
+ };
58
+ type PreviewTaskScheduler = {
59
+ submitWorkerTask(block: PreviewBlock, rendererDef: PreviewRendererDefinition, context: PreviewRenderContext): Promise<string>;
60
+ cancel(taskId: number): void;
61
+ cancelAll(): void;
62
+ acquire(): PreviewTaskScheduler;
63
+ release(): void;
64
+ };
65
+ type PreviewStrategy = 'incremental' | 'virtual' | 'mdx';
66
+ type OwoMarkPreviewEngineOptions = {
67
+ strategy?: PreviewStrategy;
68
+ themeKey?: string;
69
+ registry?: PreviewRendererRegistry;
70
+ scheduler?: PreviewTaskScheduler;
71
+ viewportFirst?: boolean;
72
+ /**
73
+ * External block renderer function. When provided, the engine uses this
74
+ * instead of the built-in default renderer. This allows the host to supply
75
+ * its own Markdown pipeline (unified, remark, rehype, Shiki, etc.) without
76
+ * the preview package bundling those heavy dependencies.
77
+ */
78
+ renderBlock?: (block: PreviewBlock, context: PreviewRenderContext) => Promise<string>;
79
+ /**
80
+ * Called after every DOM mutation — including idle-backfilled and deferred
81
+ * block renders — not just after the synchronous update() return.
82
+ * Use for scroll sync or other post-DOM-update side effects.
83
+ */
84
+ onContentUpdate?: () => void;
85
+ };
86
+ type OwoMarkPreviewEngine = {
87
+ mount(root: HTMLElement): void;
88
+ destroy(): void;
89
+ update(state: OwoMarkSharedState): void;
90
+ getRenderedVersion(): number;
91
+ };
92
+
93
+ export type { OwoMarkPreviewEngineOptions as O, PreviewRendererRegistry as P, OwoMarkPreviewEngine as a, PreviewTaskScheduler as b, PreviewBlockMetadata as c, PreviewBlockRenderer as d, PreviewCacheEntry as e, PreviewRenderContext as f, PreviewRenderPhase as g, PreviewRenderResult as h, PreviewRendererDefinition as i, PreviewRendererMode as j, PreviewStrategy as k, PreviewTaskPriority as l };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owomark/view",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Rendering engine, preview engine, DOM view layer, and official base theme for OwoMark.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,7 +62,7 @@
62
62
  "access": "public"
63
63
  },
64
64
  "dependencies": {
65
- "@owomark/core": "^0.1.5"
65
+ "@owomark/core": "^0.1.6"
66
66
  },
67
67
  "devDependencies": {
68
68
  "tsup": "^8.5.1"
@@ -59,8 +59,14 @@
59
59
  --owo-syntax-code-fence: #7f849c;
60
60
  --owo-syntax-code-lang: #fab387;
61
61
  --owo-syntax-list-marker: #fab387;
62
+ --owo-syntax-task-marker: #f9c74f;
63
+ --owo-syntax-task-marker-checked: #a6e3a1;
64
+ --owo-syntax-table-separator: #6c7086;
65
+ --owo-syntax-strikethrough: #cdd6f4;
62
66
  --owo-syntax-blockquote-marker: #585b70;
63
67
  --owo-syntax-hr: #45475a;
68
+ --owo-syntax-html: #cba6f7;
69
+ --owo-syntax-math: #94e2d5;
64
70
 
65
71
  /* Toolbar */
66
72
  --owo-toolbar-bg: #252536;
@@ -59,8 +59,14 @@
59
59
  --owo-syntax-code-fence: #64748b;
60
60
  --owo-syntax-code-lang: #ea580c;
61
61
  --owo-syntax-list-marker: #ea580c;
62
+ --owo-syntax-task-marker: #d97706;
63
+ --owo-syntax-task-marker-checked: #16a34a;
64
+ --owo-syntax-table-separator: #94a3b8;
65
+ --owo-syntax-strikethrough: #1f2937;
62
66
  --owo-syntax-blockquote-marker: #9ca3af;
63
67
  --owo-syntax-hr: #d1d5db;
68
+ --owo-syntax-html: #7c3aed;
69
+ --owo-syntax-math: #0f766e;
64
70
 
65
71
  /* Toolbar */
66
72
  --owo-toolbar-bg: #f8fafc;
@@ -126,6 +126,20 @@
126
126
  font-weight: 600;
127
127
  }
128
128
 
129
+ .owo-syntax-task-marker {
130
+ color: var(--owo-syntax-task-marker, var(--owo-syntax-list-marker, currentColor));
131
+ font-weight: 600;
132
+ }
133
+
134
+ .owo-syntax-task-marker-checked {
135
+ color: var(--owo-syntax-task-marker-checked, var(--owo-success, currentColor));
136
+ font-weight: 700;
137
+ }
138
+
139
+ .owo-syntax-table-separator {
140
+ color: var(--owo-syntax-table-separator, var(--owo-syntax-marker, currentColor));
141
+ }
142
+
129
143
  .owo-syntax-blockquote-marker {
130
144
  color: transparent;
131
145
  font-size: inherit;
@@ -149,15 +163,21 @@
149
163
  font-size: 0.9em;
150
164
  }
151
165
 
166
+ .owo-syntax-strikethrough {
167
+ color: var(--owo-syntax-strikethrough, var(--owo-editor-text, currentColor));
168
+ text-decoration-line: line-through;
169
+ text-decoration-thickness: 0.08em;
170
+ }
171
+
152
172
  .owo-syntax-hr {
153
173
  color: var(--owo-syntax-hr, #d1d5db);
154
174
  }
155
175
 
156
176
  .owo-syntax-html {
157
- color: var(--owo-syntax-keyword, #7c3aed);
177
+ color: var(--owo-syntax-html, var(--owo-syntax-keyword, currentColor));
158
178
  }
159
179
 
160
180
  .owo-syntax-math {
161
- color: var(--owo-syntax-string, #0f766e);
181
+ color: var(--owo-syntax-math, var(--owo-syntax-string, currentColor));
162
182
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
163
183
  }