@openeditor/react 0.0.28 → 0.0.29

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/README.md CHANGED
@@ -19,6 +19,40 @@ Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments a
19
19
 
20
20
  This package intentionally does not import CSS and does not render styled menus. Use `@openeditor/ui` for the optional styled components.
21
21
 
22
+ ## Read-only rendering and URL policy
23
+
24
+ `OpenEditorViewer` renders the portable document through an explicit registry of
25
+ built-in and structural renderers. Unknown nodes are preserved for forward
26
+ readability but carry `data-openeditor-unknown-node` so unsupported content never
27
+ silently appears to be a fully supported block. Built-in viewer output and safe
28
+ HTML export share stable `oe-*` classes and `data-openeditor-*` hooks for document
29
+ semantics such as columns, tasks, toggles, callouts, tables, pages, and files.
30
+
31
+ Viewer URLs use `openEditorPublicUrlPolicy` by default. The policy covers links,
32
+ images, pages, and attachments, including snapshots returned by page and
33
+ attachment runtimes. It accepts HTTP(S) and ordinary relative references, plus
34
+ `mailto:` and `tel:` for text links, while rejecting executable and local-preview
35
+ schemes. Owner previews can opt into a narrowly scoped policy:
36
+
37
+ ```tsx
38
+ import {
39
+ OpenEditorViewer,
40
+ openEditorPublicUrlPolicy,
41
+ type OpenEditorUrlPolicy,
42
+ } from "@openeditor/react";
43
+
44
+ const previewUrlPolicy: OpenEditorUrlPolicy = (value, context) => {
45
+ if (context === "attachment" && value.startsWith("blob:")) return value;
46
+ return openEditorPublicUrlPolicy(value, context);
47
+ };
48
+
49
+ <OpenEditorViewer document={document} urlPolicy={previewUrlPolicy} />;
50
+ ```
51
+
52
+ Custom viewer renderers receive both `urlPolicy` and a typed `resolveUrl` helper.
53
+ The host remains responsible for arbitrary custom renderer output and for actions
54
+ performed inside `openPage` or `openAttachment` callbacks.
55
+
22
56
  ## Block actions
23
57
 
24
58
  Every addressable node has a persistent `openeditor-id`. The block interaction
@@ -208,7 +242,11 @@ const productCard = defineOpenEditorReactNode({
208
242
  keywords: ["product", "commerce", "card"],
209
243
  order: 250,
210
244
  },
211
- viewer: ({ node }) => <article>{String(node.attrs?.title ?? "")}</article>,
245
+ viewer: ({ node, resolveUrl }) => (
246
+ <article>
247
+ <a href={resolveUrl(node.attrs?.href, "link")}>{String(node.attrs?.title ?? "")}</a>
248
+ </article>
249
+ ),
212
250
  exporters: {
213
251
  html: {
214
252
  acmeProductCard: ({ node, escapeHtml }) =>
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, RefObject, ComponentType } from 'react';
3
- import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
- export { OpenEditorBlockRef, OpenEditorPageRuntime, OpenEditorPageSnapshot } from '@openeditor/core';
3
+ import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorUrlPolicy, OpenEditorUrlContext, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
+ export { OpenEditorBlockRef, OpenEditorPageRuntime, OpenEditorPageSnapshot, OpenEditorUrlContext, OpenEditorUrlPolicy, openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openeditor/core';
5
5
  import { ExportFormat, OpenEditorExporters } from '@openeditor/exporters';
6
6
  export { seedDocument } from '@openeditor/extensions';
7
7
  import { AnyExtension, NodeConfig } from '@tiptap/core';
@@ -17,7 +17,7 @@ type OpenEditorBlockInteractionStore = {
17
17
  subscribe: (listener: () => void) => () => void;
18
18
  };
19
19
 
20
- declare const openEditorThemeTokenNames: readonly ["surface", "surfaceRaised", "surfaceMuted", "blockSurface", "text", "textSoft", "heading", "muted", "placeholder", "border", "borderStrong", "structuralLine", "accent", "accentText", "accentStrong", "buttonBackground", "buttonText", "codeBackground", "codeText", "link", "linkHover", "shadow", "fontSans", "fontMono", "radiusSmall", "radiusMedium", "radiusLarge", "spaceBlock", "spaceInline"];
20
+ declare const openEditorThemeTokenNames: readonly ["surface", "surfaceRaised", "surfaceMuted", "blockSurface", "text", "textSoft", "heading", "muted", "placeholder", "border", "borderStrong", "structuralLine", "accent", "accentText", "accentStrong", "buttonBackground", "buttonText", "codeBackground", "codeText", "link", "linkHover", "shadow", "fontSans", "fontMono", "radiusSmall", "radiusMedium", "radiusLarge", "spaceBlock", "spaceInline", "bodyFontSize", "bodyLineHeight", "headingFont", "headingLineHeight", "headingWeight", "heading1Size", "heading2Size", "heading3Size", "heading4Size", "heading5Size", "heading6Size"];
21
21
  type OpenEditorThemeToken = (typeof openEditorThemeTokenNames)[number];
22
22
  type OpenEditorTheme = Partial<Record<OpenEditorThemeToken, string>>;
23
23
  type OpenEditorThemeStyle = CSSProperties & Record<`--oe-${string}`, string | undefined>;
@@ -169,6 +169,8 @@ type OpenEditorBlockPickerItem = {
169
169
  type OpenEditorViewerRenderer = (context: {
170
170
  node: ProseMirrorNode;
171
171
  children: ReactNode;
172
+ urlPolicy: OpenEditorUrlPolicy;
173
+ resolveUrl: (value: unknown, context: OpenEditorUrlContext) => string | undefined;
172
174
  }) => ReactNode;
173
175
  type OpenEditorExtensionMenu = {
174
176
  label?: string;
@@ -210,6 +212,8 @@ type OpenEditorViewerProps = {
210
212
  extensions?: readonly OpenEditorReactExtension[];
211
213
  pageRuntime?: OpenEditorPageRuntime;
212
214
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
215
+ /** Safe by default. Supply an explicit host policy for trusted preview URLs such as `blob:`. */
216
+ urlPolicy?: OpenEditorUrlPolicy;
213
217
  };
214
218
  declare module "@tiptap/core" {
215
219
  interface Commands<ReturnType> {
@@ -250,7 +254,7 @@ declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]
250
254
  declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
251
255
  declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
252
256
  declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
253
- declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
257
+ declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, urlPolicy, }: OpenEditorViewerProps) => react.JSX.Element;
254
258
  declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
255
259
 
256
260
  export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, OpenEditorBlockDragHandle, type OpenEditorBlockDragHandleProps, type OpenEditorBlockPickerItem, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, OpenEditorPageHeader, type OpenEditorPageHeaderProps, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SelectionBubbleState, type SlashState, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorBlockInteraction, useOpenEditorController, useOpenEditorThemeStyle };
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { DEFAULT_CALLOUT_EMOJI, normalizeEmoji, DEFAULT_PAGE_EMOJI, normalizeDocument, toProseMirrorDocument, createDocument as createDocument$1, isOpenEditorBlockEnabled, createBlockRegistry, OPENEDITOR_BLOCK_ID_ATTR, createBlockId, fromProseMirrorDocument, findBlockSpecForNode } from '@openeditor/core';
1
+ import { DEFAULT_CALLOUT_EMOJI, normalizeEmoji, DEFAULT_PAGE_EMOJI, normalizeDocument, toProseMirrorDocument, createDocument as createDocument$1, openEditorPublicUrlPolicy, isOpenEditorBlockEnabled, createBlockRegistry, OPENEDITOR_BLOCK_ID_ATTR, createBlockId, fromProseMirrorDocument, findBlockSpecForNode } from '@openeditor/core';
2
+ export { openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openeditor/core';
2
3
  import { Checkbox } from '@base-ui/react/checkbox';
3
4
  import { shift as shift$1, autoUpdate, computePosition } from '@floating-ui/dom';
4
5
  import { OpenEditorEmojiPicker } from '@openeditor/emoji';
5
- import React, { createContext, forwardRef, useContext, useMemo, createRef, memo, createElement, version, useState, useEffect, useRef, useSyncExternalStore as useSyncExternalStore$1, useCallback, use, useDebugValue, Fragment as Fragment$2, useLayoutEffect } from 'react';
6
+ import React, { createContext, forwardRef, useContext, useMemo, createRef, memo, createElement, version, useState, useEffect, useRef, useSyncExternalStore as useSyncExternalStore$1, useCallback, use, useDebugValue, useLayoutEffect } from 'react';
6
7
  import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime';
7
8
  import { renderMermaidSVG } from 'beautiful-mermaid';
8
9
  import { exportDocumentUnsafe, exportDocument } from '@openeditor/exporters';
@@ -6288,9 +6289,20 @@ var openEditorThemeTokenNames = [
6288
6289
  "radiusMedium",
6289
6290
  "radiusLarge",
6290
6291
  "spaceBlock",
6291
- "spaceInline"
6292
+ "spaceInline",
6293
+ "bodyFontSize",
6294
+ "bodyLineHeight",
6295
+ "headingFont",
6296
+ "headingLineHeight",
6297
+ "headingWeight",
6298
+ "heading1Size",
6299
+ "heading2Size",
6300
+ "heading3Size",
6301
+ "heading4Size",
6302
+ "heading5Size",
6303
+ "heading6Size"
6292
6304
  ];
6293
- var cssName = (token) => `--oe-${token.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`;
6305
+ var cssName = (token) => `--oe-${token.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").replace(/(\d)([A-Z])/g, "$1-$2").toLowerCase()}`;
6294
6306
  var createOpenEditorThemeStyle = (theme) => Object.fromEntries(
6295
6307
  openEditorThemeTokenNames.flatMap(
6296
6308
  (token) => theme[token] === void 0 ? [] : [[cssName(token), theme[token]]]
@@ -30999,16 +31011,7 @@ var createStandardAttributes = (attributeNames) => Object.fromEntries(
30999
31011
  }
31000
31012
  ])
31001
31013
  );
31002
- var isSafeUrl = (value, allowDataImage) => {
31003
- const normalized = value.trim().toLowerCase();
31004
- if (normalized.startsWith("javascript:") || normalized.startsWith("vbscript:")) {
31005
- return false;
31006
- }
31007
- if (normalized.startsWith("data:")) {
31008
- return allowDataImage && normalized.startsWith("data:image/");
31009
- }
31010
- return true;
31011
- };
31014
+ var isSafePastedUrl = (value, context, allowDataImage) => allowDataImage && /^data:image\//i.test(value.trim()) || openEditorPublicUrlPolicy(value, context) !== null;
31012
31015
  var sanitizePastedHtml = (html) => {
31013
31016
  const parser = new DOMParser();
31014
31017
  const parsed = parser.parseFromString(html, "text/html");
@@ -31030,7 +31033,8 @@ var sanitizePastedHtml = (html) => {
31030
31033
  const allowDataImage = element.tagName.toLowerCase() === "img";
31031
31034
  for (const attribute of URL_ATTRIBUTES) {
31032
31035
  const value = element.getAttribute(attribute);
31033
- if (value !== null && !isSafeUrl(value, allowDataImage)) {
31036
+ const context = attribute === "src" ? "image" : "link";
31037
+ if (value !== null && !isSafePastedUrl(value, context, allowDataImage && attribute === "src")) {
31034
31038
  element.removeAttribute(attribute);
31035
31039
  }
31036
31040
  }
@@ -31442,7 +31446,10 @@ var Attachment = Node3.create({
31442
31446
  return ReactNodeViewRenderer(OpenEditorAttachmentNodeView);
31443
31447
  }
31444
31448
  });
31445
- var OpenEditorAttachmentViewer = ({ node }) => {
31449
+ var OpenEditorAttachmentViewer = ({
31450
+ node,
31451
+ urlPolicy
31452
+ }) => {
31446
31453
  const runtime = use(OpenEditorAttachmentRuntimeContext);
31447
31454
  const cached = attachmentSnapshotFromNode(node);
31448
31455
  const [state, setState] = useState({ phase: cached.attachmentId && runtime?.resolveAttachment ? "resolving" : cached.url ? "ready" : "missing", snapshot: cached });
@@ -31466,9 +31473,10 @@ var OpenEditorAttachmentViewer = ({ node }) => {
31466
31473
  /* @__PURE__ */ jsx("span", { className: "oe-attachment-meta", children: state.phase === "resolving" ? "Loading\u2026" : state.phase === "missing" ? "Missing or inaccessible" : [formatAttachmentSize(snapshot.size), attachmentTypeLabel(snapshot)].filter(Boolean).join(" \xB7 ") })
31467
31474
  ] })
31468
31475
  ] });
31469
- if (state.phase === "ready" && runtime?.openAttachment) return /* @__PURE__ */ jsx("button", { className: "oe-attachment oe-attachment-viewer", onClick: () => void runtime.openAttachment?.(snapshot), type: "button", children: content });
31470
- if (state.phase === "ready" && snapshot.url) return /* @__PURE__ */ jsx("a", { className: "oe-attachment oe-attachment-viewer", href: snapshot.url, rel: "noreferrer", target: "_blank", children: content });
31471
- return /* @__PURE__ */ jsx("div", { className: "oe-attachment oe-attachment-viewer", "data-state": state.phase, children: content });
31476
+ if (state.phase === "ready" && runtime?.openAttachment) return /* @__PURE__ */ jsx("button", { className: "oe-attachment oe-attachment-viewer", "data-openeditor-attachment": true, onClick: () => void runtime.openAttachment?.(snapshot), type: "button", children: content });
31477
+ const href = snapshot.url ? urlPolicy(snapshot.url, "attachment") : null;
31478
+ if (state.phase === "ready" && href) return /* @__PURE__ */ jsx("a", { className: "oe-attachment oe-attachment-viewer", "data-openeditor-attachment": true, href, rel: "noopener noreferrer", target: "_blank", children: content });
31479
+ return /* @__PURE__ */ jsx("div", { className: "oe-attachment oe-attachment-viewer", "data-openeditor-attachment": true, "data-state": state.phase, children: content });
31472
31480
  };
31473
31481
  var pageSnapshotFromNode = (node) => {
31474
31482
  const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
@@ -31609,7 +31617,10 @@ var Page = Node3.create({
31609
31617
  return ReactNodeViewRenderer(OpenEditorPageNodeView);
31610
31618
  }
31611
31619
  });
31612
- var OpenEditorPageViewer = ({ node }) => {
31620
+ var OpenEditorPageViewer = ({
31621
+ node,
31622
+ urlPolicy
31623
+ }) => {
31613
31624
  const runtime = use(OpenEditorPageRuntimeContext);
31614
31625
  const cached = pageSnapshotFromNode(node);
31615
31626
  const [page] = useResolvedPageSnapshot(cached, runtime);
@@ -31620,12 +31631,13 @@ var OpenEditorPageViewer = ({ node }) => {
31620
31631
  /* @__PURE__ */ jsx("span", { className: "oe-page-arrow", "aria-hidden": "true", children: "\u2192" })
31621
31632
  ] });
31622
31633
  if (page && runtime?.openPage) {
31623
- return /* @__PURE__ */ jsx("button", { className: "oe-page oe-page-viewer", onClick: () => void runtime.openPage?.(page), type: "button", children: content });
31634
+ return /* @__PURE__ */ jsx("button", { className: "oe-page oe-page-viewer", "data-openeditor-page": true, onClick: () => void runtime.openPage?.(page), type: "button", children: content });
31624
31635
  }
31625
- if (page?.href) {
31626
- return /* @__PURE__ */ jsx("a", { className: "oe-page oe-page-viewer", href: page.href, children: content });
31636
+ const href = page?.href ? urlPolicy(page.href, "page") : null;
31637
+ if (href) {
31638
+ return /* @__PURE__ */ jsx("a", { className: "oe-page oe-page-viewer", "data-openeditor-page": true, href, children: content });
31627
31639
  }
31628
- return /* @__PURE__ */ jsx("div", { className: "oe-page oe-page-viewer", children: content });
31640
+ return /* @__PURE__ */ jsx("div", { className: "oe-page oe-page-viewer", "data-openeditor-page": true, children: content });
31629
31641
  };
31630
31642
  var OpenEditorPageHeader = ({
31631
31643
  page,
@@ -31708,7 +31720,7 @@ var MermaidPreview = ({ code }) => {
31708
31720
  };
31709
31721
  var ToggleListViewerItem = ({ children, initiallyOpen }) => {
31710
31722
  const [open, setOpen] = useState(initiallyOpen);
31711
- return /* @__PURE__ */ jsxs("li", { className: "oe-toggle-list-item", "data-open": open ? "true" : "false", children: [
31723
+ return /* @__PURE__ */ jsxs("li", { className: "oe-toggle-list-item", "data-open": open ? "true" : "false", "data-openeditor-toggle-list-item": true, children: [
31712
31724
  /* @__PURE__ */ jsx(
31713
31725
  "button",
31714
31726
  {
@@ -32559,7 +32571,7 @@ var getDefaultSlashMenuItems = (controller) => {
32559
32571
  ...controller.slashMenuItems
32560
32572
  ]);
32561
32573
  };
32562
- var renderTextWithMarks = (text, marks = [], key) => marks.reduceRight((children, mark, index) => {
32574
+ var renderTextWithMarks = (text, marks = [], key, urlPolicy) => marks.reduceRight((children, mark, index) => {
32563
32575
  const nodeKey = `${key}-${mark.type}-${index}`;
32564
32576
  switch (mark.type) {
32565
32577
  case "bold":
@@ -32572,106 +32584,110 @@ var renderTextWithMarks = (text, marks = [], key) => marks.reduceRight((children
32572
32584
  return /* @__PURE__ */ jsx("s", { children }, nodeKey);
32573
32585
  case "code":
32574
32586
  return /* @__PURE__ */ jsx("code", { children }, nodeKey);
32575
- case "link":
32576
- return /* @__PURE__ */ jsx("a", { href: typeof mark.attrs?.href === "string" ? mark.attrs.href : void 0, children }, nodeKey);
32587
+ case "link": {
32588
+ const href = typeof mark.attrs?.href === "string" ? urlPolicy(mark.attrs.href, "link") : null;
32589
+ if (!href) return children;
32590
+ const target = typeof mark.attrs?.target === "string" && ["_blank", "_self", "_parent", "_top"].includes(mark.attrs.target) ? mark.attrs.target : void 0;
32591
+ const declaredRel = typeof mark.attrs?.rel === "string" ? mark.attrs.rel.split(/\s+/).filter(Boolean) : [];
32592
+ const rel = target === "_blank" ? Array.from(/* @__PURE__ */ new Set([...declaredRel, "noopener", "noreferrer"])).join(" ") : declaredRel.join(" ") || void 0;
32593
+ return /* @__PURE__ */ jsx("a", { href, rel, target, children }, nodeKey);
32594
+ }
32577
32595
  default:
32578
32596
  return children;
32579
32597
  }
32580
32598
  }, text);
32581
- var renderViewerNode = (node, key, renderers, registry = defaultBlockRegistry) => {
32599
+ var resolveViewerUrl = (urlPolicy, value, context) => typeof value === "string" ? urlPolicy(value, context) ?? void 0 : void 0;
32600
+ var viewerTextAlignStyle = (node) => {
32601
+ const textAlign = node.attrs?.textAlign;
32602
+ return typeof textAlign === "string" && ["left", "center", "right", "justify"].includes(textAlign) ? { textAlign } : void 0;
32603
+ };
32604
+ var positiveIntegerAttribute = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
32605
+ var renderHeadingViewer = (node, children) => {
32606
+ const props = { style: viewerTextAlignStyle(node) };
32607
+ const level = typeof node.attrs?.level === "number" ? Math.min(Math.max(node.attrs.level, 1), 6) : 2;
32608
+ switch (level) {
32609
+ case 1:
32610
+ return /* @__PURE__ */ jsx("h1", { ...props, children });
32611
+ case 2:
32612
+ return /* @__PURE__ */ jsx("h2", { ...props, children });
32613
+ case 3:
32614
+ return /* @__PURE__ */ jsx("h3", { ...props, children });
32615
+ case 4:
32616
+ return /* @__PURE__ */ jsx("h4", { ...props, children });
32617
+ case 5:
32618
+ return /* @__PURE__ */ jsx("h5", { ...props, children });
32619
+ default:
32620
+ return /* @__PURE__ */ jsx("h6", { ...props, children });
32621
+ }
32622
+ };
32623
+ var BUILT_IN_VIEWER_RENDERERS = {
32624
+ paragraph: ({ node, children }) => /* @__PURE__ */ jsx("p", { style: viewerTextAlignStyle(node), children: node.content?.length ? children : /* @__PURE__ */ jsx("br", {}) }),
32625
+ heading: ({ node, children }) => renderHeadingViewer(node, children),
32626
+ bulletList: ({ children }) => /* @__PURE__ */ jsx("ul", { children }),
32627
+ orderedList: ({ node, children }) => /* @__PURE__ */ jsx("ol", { start: positiveIntegerAttribute(node.attrs?.start), children }),
32628
+ taskList: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "oe-task-list", "data-openeditor-task-list": true, "data-type": "taskList", children }),
32629
+ blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", { children }),
32630
+ codeBlock: ({ node }) => {
32631
+ const language = typeof node.attrs?.language === "string" && node.attrs.language.trim() ? node.attrs.language.trim() : null;
32632
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children: getNodeText(node) }) });
32633
+ },
32634
+ divider: () => /* @__PURE__ */ jsx("hr", {}),
32635
+ image: ({ node, resolveUrl }) => {
32636
+ const src = resolveUrl(node.attrs?.src, "image");
32637
+ if (!src) return null;
32638
+ const dimension = (value) => typeof value === "string" || typeof value === "number" ? value : void 0;
32639
+ return /* @__PURE__ */ jsx("img", { alt: typeof node.attrs?.alt === "string" ? node.attrs.alt : "", height: dimension(node.attrs?.height), src, title: typeof node.attrs?.title === "string" ? node.attrs.title : void 0, width: dimension(node.attrs?.width) });
32640
+ },
32641
+ columns: ({ node, children }) => {
32642
+ const columnCount = Math.min(Math.max(node.content?.length ?? 0, 2), 4);
32643
+ return /* @__PURE__ */ jsx("section", { className: "oe-columns", "data-openeditor-columns": node.content?.length ?? 0, style: { "--oe-column-count": columnCount }, children });
32644
+ },
32645
+ table: ({ children }) => /* @__PURE__ */ jsx("table", { "data-openeditor-table": "", children: /* @__PURE__ */ jsx("tbody", { children }) }),
32646
+ toggleList: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "oe-toggle-list", "data-openeditor-toggle-list": true, children }),
32647
+ callout: ({ node, children }) => /* @__PURE__ */ jsxs("aside", { className: "oe-callout", "data-emoji": normalizeEmoji(node.attrs?.emoji, DEFAULT_CALLOUT_EMOJI), "data-openeditor-callout": true, children: [
32648
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-callout-emoji", children: normalizeEmoji(node.attrs?.emoji, DEFAULT_CALLOUT_EMOJI) }),
32649
+ /* @__PURE__ */ jsx("div", { className: "oe-callout-content", children })
32650
+ ] }),
32651
+ diagram: ({ node }) => {
32652
+ const code = typeof node.attrs?.code === "string" ? node.attrs.code : "";
32653
+ return /* @__PURE__ */ jsx("figure", { className: "oe-diagram", "data-openeditor-diagram": true, children: /* @__PURE__ */ jsx(MermaidPreview, { code }) });
32654
+ },
32655
+ page: ({ node, urlPolicy }) => /* @__PURE__ */ jsx(OpenEditorPageViewer, { node, urlPolicy }),
32656
+ attachment: ({ node, urlPolicy }) => /* @__PURE__ */ jsx(OpenEditorAttachmentViewer, { node, urlPolicy })
32657
+ };
32658
+ var STRUCTURAL_VIEWER_RENDERERS = {
32659
+ column: ({ children }) => /* @__PURE__ */ jsx("div", { className: "oe-column", "data-openeditor-column": true, children }),
32660
+ tableRow: ({ children }) => /* @__PURE__ */ jsx("tr", { children }),
32661
+ tableHeader: ({ node, children }) => /* @__PURE__ */ jsx("th", { colSpan: positiveIntegerAttribute(node.attrs?.colspan), rowSpan: positiveIntegerAttribute(node.attrs?.rowspan), scope: typeof node.attrs?.scope === "string" ? node.attrs.scope : void 0, children }),
32662
+ tableCell: ({ node, children }) => /* @__PURE__ */ jsx("td", { colSpan: positiveIntegerAttribute(node.attrs?.colspan), rowSpan: positiveIntegerAttribute(node.attrs?.rowspan), children }),
32663
+ listItem: ({ children }) => /* @__PURE__ */ jsx("li", { children }),
32664
+ taskItem: ({ node, children }) => {
32665
+ const checked = Boolean(node.attrs?.checked);
32666
+ return /* @__PURE__ */ jsxs("li", { className: "oe-task-item", "data-checked": checked ? "true" : "false", "data-openeditor-task-item": true, children: [
32667
+ /* @__PURE__ */ jsx("span", { className: "oe-task-item-checkbox", children: /* @__PURE__ */ jsx(OpenEditorTaskCheckbox, { checked, label: checked ? "Completed task" : "Incomplete task", readOnly: true, tabIndex: -1 }) }),
32668
+ /* @__PURE__ */ jsx("div", { children })
32669
+ ] });
32670
+ },
32671
+ toggleListItem: ({ node, children }) => /* @__PURE__ */ jsx(ToggleListViewerItem, { initiallyOpen: node.attrs?.open !== false, children }),
32672
+ hardBreak: () => /* @__PURE__ */ jsx("br", {})
32673
+ };
32674
+ var renderViewerNode = (node, key, renderers, registry = defaultBlockRegistry, urlPolicy = openEditorPublicUrlPolicy) => {
32582
32675
  if (node.type === "text") {
32583
- return renderTextWithMarks(node.text ?? "", node.marks, key);
32676
+ return renderTextWithMarks(node.text ?? "", node.marks, key, urlPolicy);
32584
32677
  }
32585
- const children = node.content?.map((child, index) => renderViewerNode(child, `${key}-${index}`, renderers, registry)) ?? null;
32678
+ const children = node.content?.map((child, index) => renderViewerNode(child, `${key}-${index}`, renderers, registry, urlPolicy)) ?? null;
32586
32679
  const blockKey = findBlockSpecForNode(registry, node)?.name ?? node.type;
32587
- const customRenderer = renderers?.[blockKey];
32588
- if (customRenderer) {
32589
- return /* @__PURE__ */ jsx(Fragment$2, { children: customRenderer({ node, children }) }, key);
32590
- }
32591
- switch (blockKey) {
32592
- case "paragraph":
32593
- return /* @__PURE__ */ jsx("p", { children: node.content?.length ? children : /* @__PURE__ */ jsx("br", {}) }, key);
32594
- case "heading": {
32595
- const level = typeof node.attrs?.level === "number" ? Math.min(Math.max(node.attrs.level, 1), 6) : 2;
32596
- switch (level) {
32597
- case 1:
32598
- return /* @__PURE__ */ jsx("h1", { children }, key);
32599
- case 2:
32600
- return /* @__PURE__ */ jsx("h2", { children }, key);
32601
- case 3:
32602
- return /* @__PURE__ */ jsx("h3", { children }, key);
32603
- case 4:
32604
- return /* @__PURE__ */ jsx("h4", { children }, key);
32605
- case 5:
32606
- return /* @__PURE__ */ jsx("h5", { children }, key);
32607
- default:
32608
- return /* @__PURE__ */ jsx("h6", { children }, key);
32609
- }
32610
- }
32611
- case "bulletList":
32612
- return /* @__PURE__ */ jsx("ul", { children }, key);
32613
- case "orderedList":
32614
- return /* @__PURE__ */ jsx("ol", { children }, key);
32615
- case "taskList":
32616
- return /* @__PURE__ */ jsx("ul", { "data-type": "taskList", children }, key);
32617
- case "blockquote":
32618
- return /* @__PURE__ */ jsx("blockquote", { children }, key);
32619
- case "codeBlock":
32620
- return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: node.content?.map((child) => child.text ?? "").join("") }) }, key);
32621
- case "divider":
32622
- return /* @__PURE__ */ jsx("hr", {}, key);
32623
- case "image":
32624
- return /* @__PURE__ */ jsx("img", { src: typeof node.attrs?.src === "string" ? node.attrs.src : "", alt: typeof node.attrs?.alt === "string" ? node.attrs.alt : "" }, key);
32625
- case "columns":
32626
- return /* @__PURE__ */ jsx("section", { children }, key);
32627
- case "table":
32628
- return /* @__PURE__ */ jsx("table", { "data-openeditor-table": "", children: /* @__PURE__ */ jsx("tbody", { children }) }, key);
32629
- case "toggleList":
32630
- return /* @__PURE__ */ jsx("ul", { "data-openeditor-toggle-list": true, children }, key);
32631
- case "callout":
32632
- return /* @__PURE__ */ jsxs("aside", { className: "oe-callout", children: [
32633
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-callout-emoji", children: normalizeEmoji(node.attrs?.emoji, DEFAULT_CALLOUT_EMOJI) }),
32634
- /* @__PURE__ */ jsx("div", { className: "oe-callout-content", children })
32635
- ] }, key);
32636
- case "diagram": {
32637
- const code = typeof node.attrs?.code === "string" ? node.attrs.code : "";
32638
- return /* @__PURE__ */ jsx("figure", { className: "oe-diagram", children: /* @__PURE__ */ jsx(MermaidPreview, { code }) }, key);
32639
- }
32640
- case "page":
32641
- return /* @__PURE__ */ jsx(OpenEditorPageViewer, { node }, key);
32642
- case "attachment":
32643
- return /* @__PURE__ */ jsx(OpenEditorAttachmentViewer, { node }, key);
32644
- default:
32645
- if (node.type === "column") {
32646
- return /* @__PURE__ */ jsx("div", { children }, key);
32647
- }
32648
- if (node.type === "tableRow") {
32649
- return /* @__PURE__ */ jsx("tr", { children }, key);
32650
- }
32651
- if (node.type === "tableHeader") {
32652
- return /* @__PURE__ */ jsx("th", { children }, key);
32653
- }
32654
- if (node.type === "tableCell") {
32655
- return /* @__PURE__ */ jsx("td", { children }, key);
32656
- }
32657
- if (node.type === "listItem") {
32658
- return /* @__PURE__ */ jsx("li", { children }, key);
32659
- }
32660
- if (node.type === "taskItem") {
32661
- const checked = Boolean(node.attrs?.checked);
32662
- return /* @__PURE__ */ jsxs("li", { "data-checked": checked ? "true" : "false", children: [
32663
- /* @__PURE__ */ jsx("span", { className: "oe-task-item-checkbox", "aria-hidden": "true", children: /* @__PURE__ */ jsx(OpenEditorTaskCheckbox, { checked, label: "Task item checkbox", readOnly: true, tabIndex: -1 }) }),
32664
- /* @__PURE__ */ jsx("div", { children })
32665
- ] }, key);
32666
- }
32667
- if (node.type === "toggleListItem") {
32668
- return /* @__PURE__ */ jsx(ToggleListViewerItem, { initiallyOpen: node.attrs?.open !== false, children }, key);
32669
- }
32670
- if (node.type === "hardBreak") {
32671
- return /* @__PURE__ */ jsx("br", {}, key);
32672
- }
32673
- return /* @__PURE__ */ jsx("div", { children }, key);
32674
- }
32680
+ const builtInRenderer = Object.hasOwn(BUILT_IN_VIEWER_RENDERERS, blockKey) ? BUILT_IN_VIEWER_RENDERERS[blockKey] : void 0;
32681
+ const renderer = renderers?.[blockKey] ?? builtInRenderer ?? STRUCTURAL_VIEWER_RENDERERS[node.type];
32682
+ const context = {
32683
+ node,
32684
+ children,
32685
+ urlPolicy,
32686
+ resolveUrl: (value, urlContext) => resolveViewerUrl(urlPolicy, value, urlContext)
32687
+ };
32688
+ if (!renderer) return /* @__PURE__ */ jsx("div", { "data-openeditor-unknown-node": node.type, children }, key);
32689
+ const Renderer = renderer;
32690
+ return /* @__PURE__ */ createElement(Renderer, { ...context, key });
32675
32691
  };
32676
32692
  var OpenEditorContent = ({
32677
32693
  controller,
@@ -32686,15 +32702,16 @@ var OpenEditorViewer = ({
32686
32702
  renderers,
32687
32703
  extensions = EMPTY_EXTENSIONS,
32688
32704
  pageRuntime,
32689
- attachmentRuntime
32705
+ attachmentRuntime,
32706
+ urlPolicy = openEditorPublicUrlPolicy
32690
32707
  }) => {
32691
- const registry = createReactBlockRegistry(extensions);
32692
- const extensionRenderers = Object.fromEntries(
32693
- extensions.map((extension) => [extension.block.name, extension.viewer])
32694
- );
32695
- const mergedRenderers = { ...extensionRenderers, ...renderers };
32708
+ const registry = useMemo(() => createReactBlockRegistry(extensions), [extensions]);
32709
+ const mergedRenderers = useMemo(() => ({
32710
+ ...Object.fromEntries(extensions.map((extension) => [extension.block.name, extension.viewer])),
32711
+ ...renderers
32712
+ }), [extensions, renderers]);
32696
32713
  return /* @__PURE__ */ jsx(OpenEditorAttachmentRuntimeContext.Provider, { value: attachmentRuntime, children: /* @__PURE__ */ jsx(OpenEditorPageRuntimeContext.Provider, { value: pageRuntime, children: /* @__PURE__ */ jsx("div", { className, children: normalizeDocument(document2).content.map(
32697
- (node, index) => renderViewerNode(node, `viewer-${index}`, mergedRenderers, registry)
32714
+ (node, index) => renderViewerNode(node, `viewer-${index}`, mergedRenderers, registry, urlPolicy)
32698
32715
  ) }) }) });
32699
32716
  };
32700
32717
  var useOpenEditor = useOpenEditorController;