@openeditor/custom-block 0.0.46

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 ADDED
@@ -0,0 +1,60 @@
1
+ # OpenEditor custom blocks
2
+
3
+ `@openeditor/custom-block` defines the only public custom-block interface. Every installed block uses the fixed `customBlock` document node. Installation does not change the ProseMirror schema.
4
+
5
+ Use the headless entry point for the portable definition, registry, validation manifest, migrations, and static export. Import authoring code from `@openeditor/custom-block/editor`. Import published Viewer code from `@openeditor/custom-block/viewer`. The Viewer entry point does not import Tiptap or editor code.
6
+
7
+ ```ts
8
+ import { defineOpenEditorCustomBlock } from "@openeditor/custom-block";
9
+
10
+ export const card = defineOpenEditorCustomBlock({
11
+ id: "acme.card",
12
+ label: "Card",
13
+ version: 1,
14
+ dataSchema: {
15
+ type: "object",
16
+ properties: { title: { type: "string", minLength: 1 } },
17
+ required: ["title"],
18
+ additionalProperties: false,
19
+ },
20
+ initialData: () => ({ title: "Untitled" }),
21
+ toHtml: ({ data }) => ({ tag: "article", children: [data.title] }),
22
+ toText: ({ data }) => data.title,
23
+ });
24
+ ```
25
+
26
+ Static HTML uses an OpenEditor-owned safe tree. It does not accept raw HTML. Use the general `{ type: "document" }` data schema and the Editor and Viewer `host.fields.document` adapters for nested rich documents. Use `host.resolveUrl`, `host.links`, `host.icons`, and `host.assets` for product-neutral host facilities.
27
+
28
+ Define authoring and published Viewer adapters in separate files:
29
+
30
+ ```tsx
31
+ // card-editor.tsx
32
+ import { defineOpenEditorCustomBlockEditor } from "@openeditor/custom-block/editor";
33
+ import { card } from "./card";
34
+
35
+ export const cardEditor = defineOpenEditorCustomBlockEditor({
36
+ block: card,
37
+ render: ({ data, updateData }) => (
38
+ <input value={data.title} onChange={(event) => updateData({ title: event.target.value })} />
39
+ ),
40
+ });
41
+ ```
42
+
43
+ ```tsx
44
+ // card-viewer.tsx
45
+ import { defineOpenEditorCustomBlockViewer } from "@openeditor/custom-block/viewer";
46
+ import { card } from "./card";
47
+
48
+ export const cardViewer = defineOpenEditorCustomBlockViewer({
49
+ block: card,
50
+ render: ({ data }) => <article>{data.title}</article>,
51
+ });
52
+ ```
53
+
54
+ Create one registry from headless definitions. Pass Editor adapters only to authoring surfaces and Viewer adapters only to `@openeditor/react/viewer`. The host provides `fields.document`, safe URL resolution, optional navigation, icon catalog, and managed raster assets. A Viewer adapter cannot update saved data.
55
+
56
+ Use integer-keyed `migrations` to convert stored data to the current version. Missing, disabled, incompatible, and invalid blocks preserve the original node and return an explicit registry resolution status. Backends validate `registry.manifests` with `validateOpenEditorCustomBlockEnvelope`; manifests contain JSON only and do not execute extension code. Use `conformOpenEditorCustomBlock(definition)` in each block package test to verify initial data, manifest validation, and safe static output.
57
+
58
+ Use `{ type: "string", format: "asset-id" }` for a host-managed asset reference. Asset IDs are opaque, have a maximum of 128 characters, and cannot contain URLs, paths, whitespace, or control characters. A backend can call `extractOpenEditorCustomBlockAssetReferences(envelope, manifests)` to authorize all asset references, including references in nested OpenEditor document fields, without loading block code.
59
+
60
+ Portable `constraints` cover primitive and keyed uniqueness, scoped map keys, references, acyclic graphs, and conditional URL policies. For example, `keysIn` can validate each directory row against the column IDs in the same directory. These constraints are JSON data in the manifest and run in backend validation.
@@ -0,0 +1,49 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { OpenEditorDocument, ProseMirrorNode } from '@openeditor/core';
4
+ import { OpenEditorCustomBlockData, OpenEditorCustomBlockDefinition, OpenEditorCustomBlockHost, OpenEditorCustomBlockRegistry } from './index.js';
5
+ import { Node } from '@tiptap/core';
6
+
7
+ type OpenEditorCustomBlockDocumentEditorProps = {
8
+ value: OpenEditorDocument;
9
+ onChange: (value: OpenEditorDocument) => void;
10
+ ariaLabel: string;
11
+ };
12
+ type OpenEditorCustomBlockEditorHost = OpenEditorCustomBlockHost & {
13
+ fields: {
14
+ document: ComponentType<OpenEditorCustomBlockDocumentEditorProps>;
15
+ };
16
+ };
17
+ type OpenEditorCustomBlockEditorContext<TData extends OpenEditorCustomBlockData> = {
18
+ data: Readonly<TData>;
19
+ instanceId: string;
20
+ host: OpenEditorCustomBlockEditorHost;
21
+ updateData: (update: TData | ((current: Readonly<TData>) => TData)) => void;
22
+ selected: boolean;
23
+ };
24
+ type OpenEditorCustomBlockEditorAdapter<TData extends OpenEditorCustomBlockData = any> = {
25
+ block: OpenEditorCustomBlockDefinition<TData>;
26
+ render: ComponentType<OpenEditorCustomBlockEditorContext<TData>>;
27
+ };
28
+ declare const defineOpenEditorCustomBlockEditor: <TData extends OpenEditorCustomBlockData>(adapter: OpenEditorCustomBlockEditorAdapter<TData>) => OpenEditorCustomBlockEditorAdapter<TData>;
29
+ type OpenEditorCustomBlockEditorSurfaceProps = {
30
+ node: ProseMirrorNode;
31
+ registry: OpenEditorCustomBlockRegistry;
32
+ adapters: readonly OpenEditorCustomBlockEditorAdapter[];
33
+ host: OpenEditorCustomBlockEditorHost;
34
+ updateNode: (node: ProseMirrorNode) => void;
35
+ selected?: boolean;
36
+ fallback?: (input: {
37
+ node: ProseMirrorNode;
38
+ status: string;
39
+ }) => ReactNode;
40
+ };
41
+ declare const OpenEditorCustomBlockEditorSurface: ({ node, registry, adapters, host, updateNode, selected, fallback }: OpenEditorCustomBlockEditorSurfaceProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element;
42
+ /** Installs the editor lifecycle on the one fixed customBlock schema node. */
43
+ declare const createOpenEditorCustomBlockEditorExtension: (options: {
44
+ registry: OpenEditorCustomBlockRegistry;
45
+ adapters: readonly OpenEditorCustomBlockEditorAdapter[];
46
+ host: OpenEditorCustomBlockEditorHost;
47
+ }) => Node<any, any>;
48
+
49
+ export { type OpenEditorCustomBlockDocumentEditorProps, type OpenEditorCustomBlockEditorAdapter, type OpenEditorCustomBlockEditorContext, type OpenEditorCustomBlockEditorHost, OpenEditorCustomBlockEditorSurface, type OpenEditorCustomBlockEditorSurfaceProps, createOpenEditorCustomBlockEditorExtension, defineOpenEditorCustomBlockEditor };
package/dist/editor.js ADDED
@@ -0,0 +1,85 @@
1
+ import { useRef, useEffect, Component } from 'react';
2
+ import { Node } from '@tiptap/core';
3
+ import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/editor.tsx
7
+ var defineOpenEditorCustomBlockEditor = (adapter) => adapter;
8
+ var EditorAdapterErrorBoundary = class extends Component {
9
+ state = { failed: false };
10
+ static getDerivedStateFromError() {
11
+ return { failed: true };
12
+ }
13
+ componentDidCatch() {
14
+ }
15
+ render() {
16
+ return this.state.failed ? /* @__PURE__ */ jsx("div", { "data-openeditor-custom-block-error": "editor-error", role: "status", children: "Custom block editor unavailable." }) : this.props.children;
17
+ }
18
+ };
19
+ var ReadyEditorSurface = ({ resolved, adapter, host, updateNode, selected }) => {
20
+ const persistedMigration = useRef(null);
21
+ useEffect(() => {
22
+ if (!resolved.migrated) {
23
+ persistedMigration.current = null;
24
+ return;
25
+ }
26
+ const migrationKey = `${String(resolved.node.attrs?.["openeditor-id"])}:${String(resolved.node.attrs?.version)}`;
27
+ if (persistedMigration.current === migrationKey) return;
28
+ persistedMigration.current = migrationKey;
29
+ updateNode(resolved.node);
30
+ }, [resolved.migrated, resolved.node, updateNode]);
31
+ const Render = adapter.render;
32
+ const instanceId = String(resolved.node.attrs?.["openeditor-id"]);
33
+ const updateData = (update) => {
34
+ const data = typeof update === "function" ? update(resolved.data) : update;
35
+ updateNode({ ...resolved.node, attrs: { ...resolved.node.attrs, data } });
36
+ };
37
+ return /* @__PURE__ */ jsx(EditorAdapterErrorBoundary, { children: /* @__PURE__ */ jsx(Render, { data: resolved.data, host, instanceId, selected, updateData }) }, `${String(resolved.node.attrs?.["openeditor-id"])}:${resolved.definition.id}`);
38
+ };
39
+ var OpenEditorCustomBlockEditorSurface = ({ node, registry, adapters, host, updateNode, selected = false, fallback }) => {
40
+ const ids = /* @__PURE__ */ new Set();
41
+ for (const adapter2 of adapters) {
42
+ if (ids.has(adapter2.block.id)) throw new Error(`Duplicate custom block editor adapter "${adapter2.block.id}".`);
43
+ ids.add(adapter2.block.id);
44
+ }
45
+ const resolved = registry.resolve(node);
46
+ if (resolved.status !== "ready") return fallback?.({ node, status: resolved.status }) ?? /* @__PURE__ */ jsxs("div", { "data-openeditor-custom-block-error": resolved.status, role: "status", children: [
47
+ "Custom block unavailable: ",
48
+ resolved.status,
49
+ "."
50
+ ] });
51
+ const adapter = adapters.find((item) => item.block.id === resolved.definition.id);
52
+ if (!adapter) return fallback?.({ node, status: "missing-editor" }) ?? /* @__PURE__ */ jsx("div", { "data-openeditor-custom-block-error": "missing-editor", role: "status", children: "Custom block editor unavailable." });
53
+ if (adapter.block !== resolved.definition) return fallback?.({ node, status: "incompatible-editor" }) ?? /* @__PURE__ */ jsx("div", { "data-openeditor-custom-block-error": "incompatible-editor", role: "status", children: "Custom block editor unavailable." });
54
+ return /* @__PURE__ */ jsx(ReadyEditorSurface, { adapter, host, resolved, selected, updateNode: (next) => {
55
+ const checked = registry.resolve(next);
56
+ if (checked.status !== "ready") throw new Error(`Invalid data for custom block "${resolved.definition.id}".`);
57
+ updateNode(checked.node);
58
+ } });
59
+ };
60
+ var createOpenEditorCustomBlockEditorExtension = (options) => Node.create({
61
+ name: "customBlock",
62
+ group: "block",
63
+ atom: true,
64
+ isolating: true,
65
+ draggable: true,
66
+ addAttributes: () => ({ "openeditor-id": { default: null }, blockId: { default: null }, version: { default: null }, data: { default: {} } }),
67
+ renderHTML: ({ node }) => ["div", { "data-openeditor-custom-block": node.attrs.blockId ?? "unknown" }],
68
+ addNodeView() {
69
+ return ReactNodeViewRenderer((props) => /* @__PURE__ */ jsx(NodeViewWrapper, { "data-openeditor-custom-block": props.node.attrs.blockId ?? "unknown", children: /* @__PURE__ */ jsx(
70
+ OpenEditorCustomBlockEditorSurface,
71
+ {
72
+ adapters: options.adapters,
73
+ host: options.host,
74
+ node: props.node.toJSON(),
75
+ registry: options.registry,
76
+ selected: props.selected,
77
+ updateNode: (next) => props.updateAttributes(next.attrs ?? {})
78
+ }
79
+ ) }));
80
+ }
81
+ });
82
+
83
+ export { OpenEditorCustomBlockEditorSurface, createOpenEditorCustomBlockEditorExtension, defineOpenEditorCustomBlockEditor };
84
+ //# sourceMappingURL=editor.js.map
85
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/editor.tsx"],"names":["adapter"],"mappings":"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","file":"editor.js","sourcesContent":["import type { OpenEditorDocument, ProseMirrorNode } from \"@openeditor/core\";\nimport {\n type OpenEditorCustomBlockData,\n type OpenEditorCustomBlockDefinition,\n type OpenEditorCustomBlockHost,\n type OpenEditorCustomBlockRegistry,\n} from \"./index\";\nimport { Component, useEffect, useRef, type ComponentType, type ReactNode } from \"react\";\nimport { Node } from \"@tiptap/core\";\nimport { NodeViewWrapper, ReactNodeViewRenderer, type ReactNodeViewProps } from \"@tiptap/react\";\n\nexport type OpenEditorCustomBlockDocumentEditorProps = {\n value: OpenEditorDocument;\n onChange: (value: OpenEditorDocument) => void;\n ariaLabel: string;\n};\nexport type OpenEditorCustomBlockEditorHost = OpenEditorCustomBlockHost & {\n fields: { document: ComponentType<OpenEditorCustomBlockDocumentEditorProps> };\n};\nexport type OpenEditorCustomBlockEditorContext<TData extends OpenEditorCustomBlockData> = {\n data: Readonly<TData>;\n instanceId: string;\n host: OpenEditorCustomBlockEditorHost;\n updateData: (update: TData | ((current: Readonly<TData>) => TData)) => void;\n selected: boolean;\n};\nexport type OpenEditorCustomBlockEditorAdapter<TData extends OpenEditorCustomBlockData = any> = {\n block: OpenEditorCustomBlockDefinition<TData>;\n render: ComponentType<OpenEditorCustomBlockEditorContext<TData>>;\n};\nexport const defineOpenEditorCustomBlockEditor = <TData extends OpenEditorCustomBlockData>(adapter: OpenEditorCustomBlockEditorAdapter<TData>) => adapter;\n\nexport type OpenEditorCustomBlockEditorSurfaceProps = {\n node: ProseMirrorNode;\n registry: OpenEditorCustomBlockRegistry;\n adapters: readonly OpenEditorCustomBlockEditorAdapter[];\n host: OpenEditorCustomBlockEditorHost;\n updateNode: (node: ProseMirrorNode) => void;\n selected?: boolean;\n fallback?: (input: { node: ProseMirrorNode; status: string }) => ReactNode;\n};\n\nclass EditorAdapterErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {\n state = { failed: false };\n static getDerivedStateFromError() { return { failed: true }; }\n componentDidCatch() {}\n render() {\n return this.state.failed\n ? <div data-openeditor-custom-block-error=\"editor-error\" role=\"status\">Custom block editor unavailable.</div>\n : this.props.children;\n }\n}\n\nconst ReadyEditorSurface = ({ resolved, adapter, host, updateNode, selected }: { resolved: Extract<ReturnType<OpenEditorCustomBlockRegistry[\"resolve\"]>, { status: \"ready\" }>; adapter: OpenEditorCustomBlockEditorAdapter; host: OpenEditorCustomBlockEditorHost; updateNode: (node: ProseMirrorNode) => void; selected: boolean }) => {\n const persistedMigration = useRef<string | null>(null);\n useEffect(() => {\n if (!resolved.migrated) { persistedMigration.current = null; return; }\n const migrationKey = `${String(resolved.node.attrs?.[\"openeditor-id\"])}:${String(resolved.node.attrs?.version)}`;\n if (persistedMigration.current === migrationKey) return;\n persistedMigration.current = migrationKey;\n updateNode(resolved.node);\n }, [resolved.migrated, resolved.node, updateNode]);\n const Render = adapter.render;\n const instanceId = String(resolved.node.attrs?.[\"openeditor-id\"]);\n const updateData = (update: OpenEditorCustomBlockData | ((current: Readonly<OpenEditorCustomBlockData>) => OpenEditorCustomBlockData)) => {\n const data = typeof update === \"function\" ? update(resolved.data) : update;\n updateNode({ ...resolved.node, attrs: { ...resolved.node.attrs, data } });\n };\n return (\n <EditorAdapterErrorBoundary key={`${String(resolved.node.attrs?.[\"openeditor-id\"])}:${resolved.definition.id}`}>\n <Render data={resolved.data} host={host} instanceId={instanceId} selected={selected} updateData={updateData} />\n </EditorAdapterErrorBoundary>\n );\n};\n\nexport const OpenEditorCustomBlockEditorSurface = ({ node, registry, adapters, host, updateNode, selected = false, fallback }: OpenEditorCustomBlockEditorSurfaceProps) => {\n const ids = new Set<string>();\n for (const adapter of adapters) {\n if (ids.has(adapter.block.id)) throw new Error(`Duplicate custom block editor adapter \"${adapter.block.id}\".`);\n ids.add(adapter.block.id);\n }\n const resolved = registry.resolve(node);\n if (resolved.status !== \"ready\") return fallback?.({ node, status: resolved.status }) ?? <div data-openeditor-custom-block-error={resolved.status} role=\"status\">Custom block unavailable: {resolved.status}.</div>;\n const adapter = adapters.find((item) => item.block.id === resolved.definition.id);\n if (!adapter) return fallback?.({ node, status: \"missing-editor\" }) ?? <div data-openeditor-custom-block-error=\"missing-editor\" role=\"status\">Custom block editor unavailable.</div>;\n if (adapter.block !== resolved.definition) return fallback?.({ node, status: \"incompatible-editor\" }) ?? <div data-openeditor-custom-block-error=\"incompatible-editor\" role=\"status\">Custom block editor unavailable.</div>;\n return <ReadyEditorSurface adapter={adapter} host={host} resolved={resolved} selected={selected} updateNode={(next) => { const checked = registry.resolve(next); if (checked.status !== \"ready\") throw new Error(`Invalid data for custom block \"${resolved.definition.id}\".`); updateNode(checked.node); }} />;\n};\n\n/** Installs the editor lifecycle on the one fixed customBlock schema node. */\nexport const createOpenEditorCustomBlockEditorExtension = (options: {\n registry: OpenEditorCustomBlockRegistry;\n adapters: readonly OpenEditorCustomBlockEditorAdapter[];\n host: OpenEditorCustomBlockEditorHost;\n}) => Node.create({\n name: \"customBlock\",\n group: \"block\",\n atom: true,\n isolating: true,\n draggable: true,\n addAttributes: () => ({ \"openeditor-id\": { default: null }, blockId: { default: null }, version: { default: null }, data: { default: {} } }),\n renderHTML: ({ node }) => [\"div\", { \"data-openeditor-custom-block\": node.attrs.blockId ?? \"unknown\" }],\n addNodeView() {\n return ReactNodeViewRenderer((props: ReactNodeViewProps) => (\n <NodeViewWrapper data-openeditor-custom-block={props.node.attrs.blockId ?? \"unknown\"}>\n <OpenEditorCustomBlockEditorSurface\n adapters={options.adapters}\n host={options.host}\n node={props.node.toJSON()}\n registry={options.registry}\n selected={props.selected}\n updateNode={(next) => props.updateAttributes(next.attrs ?? {})}\n />\n </NodeViewWrapper>\n ));\n },\n});\n"]}
@@ -0,0 +1,257 @@
1
+ import { JsonValue, OpenEditorDocument, ProseMirrorNode } from '@openeditor/core';
2
+ export { JsonObject as OpenEditorCustomBlockJsonObject, JsonValue as OpenEditorCustomBlockJsonValue, OpenEditorDocument } from '@openeditor/core';
3
+
4
+ declare const OPENEDITOR_CUSTOM_BLOCK_NODE: "customBlock";
5
+
6
+ type OpenEditorCustomBlockId = `${string}.${string}`;
7
+ /**
8
+ * A block-owned data record. The portable schema and bounded JSON validator
9
+ * enforce JSON at runtime. `unknown` values at this type boundary also allow
10
+ * the structurally wider `OpenEditorDocument` type in declared document fields.
11
+ */
12
+ type OpenEditorCustomBlockData = Record<string, unknown>;
13
+ /** Preserves a block's precise field types, including nested document fields. */
14
+ type OpenEditorCustomBlockDataShape<TData extends object> = TData;
15
+ type OpenEditorCustomBlockEnvelope<TData extends OpenEditorCustomBlockData = OpenEditorCustomBlockData> = {
16
+ blockId: OpenEditorCustomBlockId;
17
+ version: number;
18
+ data: TData;
19
+ };
20
+ type OpenEditorCustomBlockNode<TData extends OpenEditorCustomBlockData = OpenEditorCustomBlockData> = ProseMirrorNode & {
21
+ type: typeof OPENEDITOR_CUSTOM_BLOCK_NODE;
22
+ attrs: OpenEditorCustomBlockEnvelope<TData> & Record<string, unknown>;
23
+ };
24
+ type OpenEditorCustomBlockDiagnostic = {
25
+ path: string;
26
+ message: string;
27
+ };
28
+ type PortableSchemaBase = {
29
+ nullable?: boolean;
30
+ enum?: readonly JsonValue[];
31
+ };
32
+ type OpenEditorCustomBlockDataSchema = PortableSchemaBase & ({
33
+ type: "any";
34
+ } | {
35
+ type: "string";
36
+ minLength?: number;
37
+ maxLength?: number;
38
+ format?: "asset-id";
39
+ } | {
40
+ type: "number";
41
+ integer?: boolean;
42
+ minimum?: number;
43
+ maximum?: number;
44
+ } | {
45
+ type: "boolean";
46
+ } | {
47
+ type: "null";
48
+ } | {
49
+ type: "oneOf";
50
+ variants: readonly OpenEditorCustomBlockDataSchema[];
51
+ } | {
52
+ type: "document";
53
+ } | {
54
+ type: "array";
55
+ items?: OpenEditorCustomBlockDataSchema;
56
+ minItems?: number;
57
+ maxItems?: number;
58
+ } | {
59
+ type: "object";
60
+ properties?: Readonly<Record<string, OpenEditorCustomBlockDataSchema>>;
61
+ required?: readonly string[];
62
+ additionalProperties?: boolean | OpenEditorCustomBlockDataSchema;
63
+ });
64
+ type OpenEditorCustomBlockManifest = {
65
+ id: OpenEditorCustomBlockId;
66
+ label: string;
67
+ version: number;
68
+ dataSchema: OpenEditorCustomBlockDataSchema;
69
+ constraints?: readonly OpenEditorCustomBlockConstraint[];
70
+ };
71
+ type OpenEditorCustomBlockConstraint = {
72
+ kind: "unique";
73
+ array: string;
74
+ } | {
75
+ kind: "uniqueBy";
76
+ array: string;
77
+ keys: readonly string[];
78
+ } | {
79
+ kind: "keysIn";
80
+ scope?: string;
81
+ objects: string;
82
+ keys: string;
83
+ requireAll?: boolean;
84
+ } | {
85
+ kind: "reference";
86
+ array: string;
87
+ field: string;
88
+ targetArray: string;
89
+ targetField: string;
90
+ nullable?: boolean;
91
+ } | {
92
+ kind: "acyclic";
93
+ array: string;
94
+ id: string;
95
+ parent: string;
96
+ } | {
97
+ kind: "graph";
98
+ array: string;
99
+ id: string;
100
+ parent: string;
101
+ siblingKeys?: readonly string[];
102
+ } | {
103
+ kind: "url";
104
+ path: string;
105
+ allowRelative?: boolean;
106
+ requireSchemeSeparator?: boolean;
107
+ schemes?: readonly string[];
108
+ denySchemes?: readonly string[];
109
+ when?: {
110
+ field: string;
111
+ equals: JsonValue;
112
+ };
113
+ };
114
+ type OpenEditorCustomBlockMigration = (data: Readonly<OpenEditorCustomBlockData>) => OpenEditorCustomBlockData;
115
+ type OpenEditorCustomBlockStaticContext<TData extends OpenEditorCustomBlockData> = {
116
+ data: Readonly<TData>;
117
+ renderDocument: (document: OpenEditorDocument) => OpenEditorCustomBlockSafeHtml;
118
+ documentToText: (document: OpenEditorDocument) => string;
119
+ };
120
+ type OpenEditorCustomBlockSafeHtml = string | number | null | false | {
121
+ tag: "div" | "span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "article" | "section" | "aside" | "blockquote" | "figure" | "figcaption" | "ul" | "ol" | "li" | "dl" | "dt" | "dd" | "table" | "caption" | "thead" | "tbody" | "tr" | "th" | "td" | "strong" | "em" | "u" | "s" | "code" | "pre" | "a" | "img" | "br" | "hr";
122
+ attrs?: Readonly<Partial<Record<"aria-label" | "aria-current" | "role" | "title" | "href" | "src" | "alt" | "width" | "height" | "start" | "colspan" | "rowspan" | "scope", string | number | undefined>>>;
123
+ children?: readonly OpenEditorCustomBlockSafeHtml[];
124
+ };
125
+ type OpenEditorCustomBlockDefinition<TData extends OpenEditorCustomBlockData = any> = {
126
+ id: OpenEditorCustomBlockId;
127
+ label: string;
128
+ version: number;
129
+ dataSchema: OpenEditorCustomBlockDataSchema;
130
+ initialData: () => TData;
131
+ /** Optional in-process refinement. This function is not included in the portable manifest. */
132
+ validateData?: (data: Readonly<TData>) => string | readonly string[] | null | undefined;
133
+ constraints?: readonly OpenEditorCustomBlockConstraint[];
134
+ migrations?: Readonly<Record<number, OpenEditorCustomBlockMigration>>;
135
+ toHtml: (context: OpenEditorCustomBlockStaticContext<TData>) => OpenEditorCustomBlockSafeHtml;
136
+ toText: (context: OpenEditorCustomBlockStaticContext<TData>) => string;
137
+ manifest: OpenEditorCustomBlockManifest;
138
+ };
139
+ /** Opaque host asset IDs. URLs, paths, whitespace, and control characters are not valid. */
140
+ declare const OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN: RegExp;
141
+ /** Validates one value against a portable JSON-only custom-block schema. */
142
+ declare const validateOpenEditorCustomBlockDataValue: (value: unknown, schema: OpenEditorCustomBlockDataSchema) => {
143
+ valid: true;
144
+ diagnostics: readonly [];
145
+ } | {
146
+ valid: false;
147
+ diagnostics: readonly OpenEditorCustomBlockDiagnostic[];
148
+ };
149
+ declare const defineOpenEditorCustomBlock: <TData extends OpenEditorCustomBlockData>(input: Omit<OpenEditorCustomBlockDefinition<TData>, "manifest">) => OpenEditorCustomBlockDefinition<TData>;
150
+ type UnavailableStatus = "missing" | "disabled" | "incompatible" | "invalid";
151
+ type OpenEditorResolvedCustomBlock<TData extends OpenEditorCustomBlockData = OpenEditorCustomBlockData> = {
152
+ status: "ready";
153
+ definition: OpenEditorCustomBlockDefinition<TData>;
154
+ node: OpenEditorCustomBlockNode<TData>;
155
+ data: TData;
156
+ migrated: boolean;
157
+ } | {
158
+ status: UnavailableStatus;
159
+ node: ProseMirrorNode;
160
+ blockId?: string;
161
+ diagnostics: readonly OpenEditorCustomBlockDiagnostic[];
162
+ };
163
+ type OpenEditorCustomBlockRegistry = {
164
+ definitions: readonly OpenEditorCustomBlockDefinition[];
165
+ manifests: readonly OpenEditorCustomBlockManifest[];
166
+ get: (id: string) => OpenEditorCustomBlockDefinition | undefined;
167
+ isEnabled: (id: string) => boolean;
168
+ resolve: (node: ProseMirrorNode) => OpenEditorResolvedCustomBlock;
169
+ toHtml: (node: ProseMirrorNode) => string;
170
+ toText: (node: ProseMirrorNode) => string;
171
+ };
172
+ declare const escapeOpenEditorCustomBlockHtml: (value: unknown) => string;
173
+ declare const renderOpenEditorCustomBlockSafeHtml: (value: OpenEditorCustomBlockSafeHtml) => string;
174
+ declare const createOpenEditorCustomBlockRegistry: (definitions: readonly OpenEditorCustomBlockDefinition[], options?: {
175
+ disabled?: readonly string[];
176
+ renderDocument?: (document: OpenEditorDocument) => OpenEditorCustomBlockSafeHtml;
177
+ documentToText?: (document: OpenEditorDocument) => string;
178
+ }) => OpenEditorCustomBlockRegistry;
179
+ declare const createOpenEditorCustomBlockNode: <TData extends OpenEditorCustomBlockData>(registry: OpenEditorCustomBlockRegistry, id: string, data?: TData, options?: {
180
+ instanceId?: string;
181
+ createInstanceId?: () => string;
182
+ }) => OpenEditorCustomBlockNode<TData>;
183
+ declare const resolveOpenEditorCustomBlockNode: (registry: OpenEditorCustomBlockRegistry, node: ProseMirrorNode) => OpenEditorResolvedCustomBlock<OpenEditorCustomBlockData>;
184
+ declare const validateOpenEditorCustomBlockEnvelope: (value: unknown, manifests: readonly OpenEditorCustomBlockManifest[], options?: {
185
+ mode?: "strict" | "preserve";
186
+ disabled?: readonly string[];
187
+ }) => {
188
+ valid: false;
189
+ diagnostics: {
190
+ path: string;
191
+ message: string;
192
+ }[];
193
+ status?: undefined;
194
+ } | {
195
+ valid: true;
196
+ diagnostics: readonly [];
197
+ status: "preserved";
198
+ } | {
199
+ valid: true;
200
+ diagnostics: OpenEditorCustomBlockDiagnostic[];
201
+ status: "preserved-invalid";
202
+ } | {
203
+ valid: true;
204
+ diagnostics: readonly [];
205
+ status?: undefined;
206
+ };
207
+ type OpenEditorCustomBlockAssetReference = {
208
+ id: string;
209
+ path: string;
210
+ };
211
+ /** Extracts host asset IDs from a portable manifest without loading extension code. */
212
+ declare const extractOpenEditorCustomBlockAssetReferences: (value: unknown, manifests: readonly OpenEditorCustomBlockManifest[]) => readonly OpenEditorCustomBlockAssetReference[];
213
+ type OpenEditorCustomBlockIcon = {
214
+ id: string;
215
+ label: string;
216
+ };
217
+ type OpenEditorCustomBlockAsset = {
218
+ id: string;
219
+ kind: "raster";
220
+ alt: string;
221
+ width?: number;
222
+ height?: number;
223
+ };
224
+ type OpenEditorCustomBlockHost = {
225
+ resolveUrl: (value: string, context: "navigation" | "asset") => string | null;
226
+ links?: {
227
+ resolve: (destination: {
228
+ href: string;
229
+ kind?: string;
230
+ }) => {
231
+ href: string;
232
+ external: boolean;
233
+ label?: string;
234
+ } | null;
235
+ };
236
+ navigate?: (url: string) => void | Promise<void>;
237
+ icons?: {
238
+ list: () => readonly OpenEditorCustomBlockIcon[];
239
+ render: (id: string) => unknown;
240
+ };
241
+ assets?: {
242
+ pick?: () => Promise<OpenEditorCustomBlockAsset | null>;
243
+ resolve: (id: string) => Promise<{
244
+ src: string;
245
+ alt: string;
246
+ } | null>;
247
+ };
248
+ };
249
+ type OpenEditorCustomBlockDocumentField = {
250
+ value: OpenEditorDocument;
251
+ onChange?: (value: OpenEditorDocument) => void;
252
+ readOnly: boolean;
253
+ };
254
+ /** Runs portable checks that custom-block packages can include in their test suites. */
255
+ declare const conformOpenEditorCustomBlock: (definition: OpenEditorCustomBlockDefinition) => readonly OpenEditorCustomBlockDiagnostic[];
256
+
257
+ export { OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN, OPENEDITOR_CUSTOM_BLOCK_NODE, type OpenEditorCustomBlockAsset, type OpenEditorCustomBlockAssetReference, type OpenEditorCustomBlockConstraint, type OpenEditorCustomBlockData, type OpenEditorCustomBlockDataSchema, type OpenEditorCustomBlockDataShape, type OpenEditorCustomBlockDefinition, type OpenEditorCustomBlockDiagnostic, type OpenEditorCustomBlockDocumentField, type OpenEditorCustomBlockEnvelope, type OpenEditorCustomBlockHost, type OpenEditorCustomBlockIcon, type OpenEditorCustomBlockId, type OpenEditorCustomBlockManifest, type OpenEditorCustomBlockMigration, type OpenEditorCustomBlockNode, type OpenEditorCustomBlockRegistry, type OpenEditorCustomBlockSafeHtml, type OpenEditorCustomBlockStaticContext, type OpenEditorResolvedCustomBlock, conformOpenEditorCustomBlock, createOpenEditorCustomBlockNode, createOpenEditorCustomBlockRegistry, defineOpenEditorCustomBlock, escapeOpenEditorCustomBlockHtml, extractOpenEditorCustomBlockAssetReferences, renderOpenEditorCustomBlockSafeHtml, resolveOpenEditorCustomBlockNode, validateOpenEditorCustomBlockDataValue, validateOpenEditorCustomBlockEnvelope };