@openeditor/custom-block 0.0.46 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,627 +1,492 @@
1
1
  import { validateDocument } from '@openeditor/core';
2
- import { defaultDocumentContract } from '@openeditor/extensions';
3
2
 
4
3
  // src/index.ts
5
4
  var OPENEDITOR_CUSTOM_BLOCK_NODE = "customBlock";
6
5
  var ID_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
7
- var CUSTOM_BLOCK_DEFINITION_BRAND = /* @__PURE__ */ Symbol.for("@openeditor/custom-block/definition");
8
- var OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
6
+ var DEFINITION_BRAND = /* @__PURE__ */ Symbol.for("@openeditor/custom-block/definition");
7
+ var LIMITS = {
8
+ depth: 32,
9
+ values: 1e4,
10
+ stringLength: 1e6,
11
+ arrayLength: 1e4,
12
+ objectKeys: 1e4
13
+ };
9
14
  var plainObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
10
- var positiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
15
+ var positiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
11
16
  var clone = (value) => structuredClone(value);
12
17
  var deepFreeze = (value) => {
13
18
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
14
19
  Object.freeze(value);
15
- for (const child of Object.values(value)) deepFreeze(child);
20
+ for (const child of Object.values(value))
21
+ deepFreeze(child);
16
22
  }
17
23
  return value;
18
24
  };
19
- var VALIDATION_LIMITS = { depth: 32, values: 1e4, stringLength: 1e6, arrayLength: 1e4, objectKeys: 1e4 };
20
25
  var inspectJson = (value, path = "$.data", depth = 0, budget = { values: 0 }) => {
21
26
  budget.values += 1;
22
- if (budget.values > VALIDATION_LIMITS.values) return [{ path, message: "Custom block data exceeds the value limit." }];
23
- if (depth > VALIDATION_LIMITS.depth) return [{ path, message: "Custom block data exceeds the nesting limit." }];
24
- if (typeof value === "string" && value.length > VALIDATION_LIMITS.stringLength) return [{ path, message: "String exceeds the size limit." }];
25
- if (value === null || ["string", "boolean"].includes(typeof value) || typeof value === "number" && Number.isFinite(value)) return [];
27
+ if (budget.values > LIMITS.values)
28
+ return [{ path, message: "Custom block data exceeds the value limit." }];
29
+ if (depth > LIMITS.depth)
30
+ return [{ path, message: "Custom block data exceeds the nesting limit." }];
31
+ if (typeof value === "string" && value.length > LIMITS.stringLength)
32
+ return [{ path, message: "String exceeds the size limit." }];
33
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
34
+ return [];
26
35
  if (Array.isArray(value)) {
27
- if (value.length > VALIDATION_LIMITS.arrayLength) return [{ path, message: "Array exceeds the size limit." }];
28
- return value.flatMap((item, index) => inspectJson(item, `${path}[${index}]`, depth + 1, budget));
36
+ if (value.length > LIMITS.arrayLength)
37
+ return [{ path, message: "Array exceeds the size limit." }];
38
+ return value.flatMap(
39
+ (item, index) => inspectJson(item, `${path}[${index}]`, depth + 1, budget)
40
+ );
29
41
  }
30
42
  if (plainObject(value)) {
31
43
  const entries = Object.entries(value);
32
- if (entries.length > VALIDATION_LIMITS.objectKeys) return [{ path, message: "Object exceeds the key limit." }];
33
- return entries.flatMap(([key, item]) => inspectJson(item, `${path}.${key}`, depth + 1, budget));
44
+ if (entries.length > LIMITS.objectKeys)
45
+ return [{ path, message: "Object exceeds the key limit." }];
46
+ return entries.flatMap(
47
+ ([key, item]) => inspectJson(item, `${path}.${key}`, depth + 1, budget)
48
+ );
34
49
  }
35
50
  return [{ path, message: "Custom block data must contain only JSON values." }];
36
51
  };
37
- var PORTABLE_SCHEMA_TYPES = /* @__PURE__ */ new Set(["any", "string", "number", "boolean", "null", "oneOf", "document", "array", "object"]);
38
- var PORTABLE_PATH_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_-]*|\*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\*))*$/;
39
- var PORTABLE_FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*$/;
40
- var requirePortablePath = (value, label, allowWildcards = true) => {
41
- if (typeof value !== "string" || !(allowWildcards ? PORTABLE_PATH_PATTERN : PORTABLE_FIELD_PATTERN).test(value)) throw new Error(`Invalid portable constraint ${label}.`);
42
- };
43
- var assertPortableSchema = (schema, path = "$.dataSchema", depth = 0) => {
44
- if (depth > VALIDATION_LIMITS.depth || !plainObject(schema) || typeof schema.type !== "string" || !PORTABLE_SCHEMA_TYPES.has(schema.type)) throw new Error(`Invalid portable schema at ${path}: type is not supported.`);
45
- if (schema.nullable !== void 0 && typeof schema.nullable !== "boolean") throw new Error(`Invalid portable schema at ${path}: nullable must be a boolean.`);
46
- if (schema.enum !== void 0 && (!Array.isArray(schema.enum) || inspectJson(schema.enum, `${path}.enum`).length)) throw new Error(`Invalid portable schema at ${path}: enum must contain JSON values.`);
47
- const boundedInteger = (name) => {
48
- const value = schema[name];
49
- if (value !== void 0 && (!Number.isSafeInteger(value) || Number(value) < 0)) throw new Error(`Invalid portable schema at ${path}: ${name} must be a nonnegative integer.`);
50
- };
51
- if (schema.type === "string") {
52
- boundedInteger("minLength");
53
- boundedInteger("maxLength");
54
- if (schema.minLength !== void 0 && schema.maxLength !== void 0 && Number(schema.minLength) > Number(schema.maxLength)) throw new Error(`Invalid portable schema at ${path}: string bounds are inconsistent.`);
55
- if (schema.format !== void 0 && schema.format !== "asset-id") throw new Error(`Invalid portable schema at ${path}: format is not supported.`);
56
- } else if (schema.type === "number") {
57
- if (schema.integer !== void 0 && typeof schema.integer !== "boolean") throw new Error(`Invalid portable schema at ${path}: integer must be a boolean.`);
58
- for (const name of ["minimum", "maximum"]) if (schema[name] !== void 0 && (typeof schema[name] !== "number" || !Number.isFinite(schema[name]))) throw new Error(`Invalid portable schema at ${path}: ${name} must be finite.`);
59
- if (schema.minimum !== void 0 && schema.maximum !== void 0 && Number(schema.minimum) > Number(schema.maximum)) throw new Error(`Invalid portable schema at ${path}: number bounds are inconsistent.`);
60
- } else if (schema.type === "array") {
61
- boundedInteger("minItems");
62
- boundedInteger("maxItems");
63
- if (schema.minItems !== void 0 && schema.maxItems !== void 0 && Number(schema.minItems) > Number(schema.maxItems)) throw new Error(`Invalid portable schema at ${path}: array bounds are inconsistent.`);
64
- if (schema.items !== void 0) assertPortableSchema(schema.items, `${path}.items`, depth + 1);
65
- } else if (schema.type === "object") {
66
- if (schema.properties !== void 0 && !plainObject(schema.properties)) throw new Error(`Invalid portable schema at ${path}: properties must be an object.`);
67
- const properties = plainObject(schema.properties) ? schema.properties : {};
68
- for (const [key, child] of Object.entries(properties)) assertPortableSchema(child, `${path}.properties.${key}`, depth + 1);
69
- if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string") || new Set(schema.required).size !== schema.required.length)) throw new Error(`Invalid portable schema at ${path}: required must contain unique strings.`);
70
- if (Array.isArray(schema.required) && schema.required.some((key) => !(key in properties))) throw new Error(`Invalid portable schema at ${path}: required fields must have schemas.`);
71
- if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") assertPortableSchema(schema.additionalProperties, `${path}.additionalProperties`, depth + 1);
72
- } else if (schema.type === "oneOf") {
73
- if (!Array.isArray(schema.variants) || schema.variants.length < 1) throw new Error(`Invalid portable schema at ${path}: variants must be a nonempty array.`);
74
- schema.variants.forEach((variant, index) => assertPortableSchema(variant, `${path}.variants[${index}]`, depth + 1));
75
- }
76
- };
77
- var assertPortableConstraints = (constraints) => {
78
- if (constraints === void 0) return;
79
- if (!Array.isArray(constraints)) throw new Error("Invalid portable constraints: expected an array.");
80
- for (const constraint of constraints) {
81
- if (!plainObject(constraint) || typeof constraint.kind !== "string") throw new Error("Invalid portable constraint shape.");
82
- if (constraint.kind === "unique") requirePortablePath(constraint.array, "array path");
83
- else if (constraint.kind === "uniqueBy") {
84
- requirePortablePath(constraint.array, "array path");
85
- if (!Array.isArray(constraint.keys) || !constraint.keys.length || constraint.keys.some((key) => {
86
- try {
87
- requirePortablePath(key, "key", false);
88
- return false;
89
- } catch {
90
- return true;
91
- }
92
- })) throw new Error("Invalid portable constraint keys.");
93
- } else if (constraint.kind === "keysIn") {
94
- if (constraint.scope !== void 0) requirePortablePath(constraint.scope, "scope path");
95
- requirePortablePath(constraint.objects, "objects path");
96
- requirePortablePath(constraint.keys, "keys path");
97
- if (constraint.requireAll !== void 0 && typeof constraint.requireAll !== "boolean") throw new Error("Invalid portable constraint requireAll flag.");
98
- } else if (constraint.kind === "reference") {
99
- requirePortablePath(constraint.array, "array path");
100
- requirePortablePath(constraint.field, "field", false);
101
- requirePortablePath(constraint.targetArray, "target array path");
102
- requirePortablePath(constraint.targetField, "target field", false);
103
- if (constraint.nullable !== void 0 && typeof constraint.nullable !== "boolean") throw new Error("Invalid portable constraint nullable flag.");
104
- } else if (constraint.kind === "acyclic" || constraint.kind === "graph") {
105
- requirePortablePath(constraint.array, "array path");
106
- requirePortablePath(constraint.id, "id", false);
107
- requirePortablePath(constraint.parent, "parent", false);
108
- if (constraint.siblingKeys !== void 0 && (!Array.isArray(constraint.siblingKeys) || constraint.siblingKeys.some((key) => {
109
- try {
110
- requirePortablePath(key, "sibling key", false);
111
- return false;
112
- } catch {
113
- return true;
114
- }
115
- }))) throw new Error("Invalid portable constraint sibling keys.");
116
- } else if (constraint.kind === "url") {
117
- requirePortablePath(constraint.path, "URL path");
118
- for (const flag of ["allowRelative", "requireSchemeSeparator"]) if (constraint[flag] !== void 0 && typeof constraint[flag] !== "boolean") throw new Error(`Invalid portable constraint ${flag}.`);
119
- for (const name of ["schemes", "denySchemes"]) if (constraint[name] !== void 0 && (!Array.isArray(constraint[name]) || constraint[name].some((scheme) => typeof scheme !== "string" || !/^[a-z][a-z\d+.-]*$/.test(scheme)))) throw new Error(`Invalid portable constraint ${name} scheme.`);
120
- if (constraint.when !== void 0 && (!plainObject(constraint.when) || (() => {
121
- try {
122
- requirePortablePath(constraint.when.field, "condition field", false);
123
- return false;
124
- } catch {
125
- return true;
126
- }
127
- })() || inspectJson(constraint.when.equals, "$.when.equals").length)) throw new Error("Invalid portable constraint condition.");
128
- } else throw new Error(`Invalid portable constraint kind "${constraint.kind}".`);
129
- }
130
- };
131
- var recursivelyFrozen = (value, seen = /* @__PURE__ */ new Set()) => {
132
- if (!value || typeof value !== "object") return true;
133
- if (seen.has(value)) return true;
134
- seen.add(value);
135
- return Object.isFrozen(value) && Object.values(value).every((child) => recursivelyFrozen(child, seen));
136
- };
137
- var validateValue = (value, schema, path, depth = 0, budget = { values: 0 }) => {
138
- budget.values += 1;
139
- if (budget.values > VALIDATION_LIMITS.values) return [{ path, message: "Custom block data exceeds the value limit." }];
140
- if (depth > VALIDATION_LIMITS.depth) return [{ path, message: "Custom block data exceeds the nesting limit." }];
141
- if (value === null && schema.nullable) return [];
142
- if (schema.enum && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) return [{ path, message: "Value is not in the allowed set." }];
143
- const errors = [];
144
- if (schema.type === "any") return errors;
145
- if (schema.type === "oneOf") {
146
- const results = schema.variants.map((variant) => validateValue(value, variant, path, depth + 1, { values: budget.values }));
147
- const matches = results.filter((result) => result.length === 0).length;
148
- return matches === 1 ? [] : [{ path, message: matches === 0 ? "Value does not match an allowed variant." : "Value matches more than one variant." }];
149
- }
150
- if (schema.type === "null") return value === null ? errors : [{ path, message: "Expected null." }];
151
- if (schema.type === "document") {
152
- if (!plainObject(value) || value.type !== "doc" || value.version !== 1 || !Array.isArray(value.content)) return [{ path, message: "Expected an OpenEditor document." }];
153
- const result = validateDocument(value, { contract: defaultDocumentContract, limits: { requireNodeIds: true } });
154
- return result.valid ? [] : result.issues.map((issue) => ({ path: `${path}${issue.path.slice(1)}`, message: issue.message }));
155
- }
156
- if (schema.type === "string") {
157
- if (typeof value !== "string") return [{ path, message: "Expected a string." }];
158
- if (value.length > VALIDATION_LIMITS.stringLength) return [{ path, message: "String exceeds the size limit." }];
159
- if (schema.minLength !== void 0 && value.length < schema.minLength) errors.push({ path, message: `Expected at least ${schema.minLength} characters.` });
160
- if (schema.maxLength !== void 0 && value.length > schema.maxLength) errors.push({ path, message: `Expected at most ${schema.maxLength} characters.` });
161
- if (schema.format === "asset-id" && !OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN.test(value)) errors.push({ path, message: "Expected an opaque host asset ID." });
162
- } else if (schema.type === "number") {
163
- if (typeof value !== "number" || !Number.isFinite(value)) return [{ path, message: "Expected a finite number." }];
164
- if (schema.integer && !Number.isInteger(value)) errors.push({ path, message: "Expected an integer." });
165
- if (schema.minimum !== void 0 && value < schema.minimum) errors.push({ path, message: `Expected ${schema.minimum} or more.` });
166
- if (schema.maximum !== void 0 && value > schema.maximum) errors.push({ path, message: `Expected ${schema.maximum} or less.` });
167
- } else if (schema.type === "boolean") {
168
- if (typeof value !== "boolean") errors.push({ path, message: "Expected a boolean." });
169
- } else if (schema.type === "array") {
170
- if (!Array.isArray(value)) return [{ path, message: "Expected an array." }];
171
- if (value.length > VALIDATION_LIMITS.arrayLength) return [{ path, message: "Array exceeds the size limit." }];
172
- if (schema.minItems !== void 0 && value.length < schema.minItems) errors.push({ path, message: `Expected at least ${schema.minItems} items.` });
173
- if (schema.maxItems !== void 0 && value.length > schema.maxItems) errors.push({ path, message: `Expected at most ${schema.maxItems} items.` });
174
- if (schema.items) value.forEach((item, index) => errors.push(...validateValue(item, schema.items, `${path}[${index}]`, depth + 1, budget)));
175
- } else if (schema.type === "object") {
176
- if (!plainObject(value)) return [{ path, message: "Expected an object." }];
177
- if (Object.keys(value).length > VALIDATION_LIMITS.objectKeys) return [{ path, message: "Object exceeds the key limit." }];
178
- for (const key of schema.required ?? []) if (!(key in value)) errors.push({ path: `${path}.${key}`, message: "Required value is missing." });
179
- for (const [key, item] of Object.entries(value)) {
180
- const property = schema.properties?.[key];
181
- if (property) errors.push(...validateValue(item, property, `${path}.${key}`, depth + 1, budget));
182
- else if (schema.additionalProperties === false) errors.push({ path: `${path}.${key}`, message: "Additional values are not allowed." });
183
- else if (plainObject(schema.additionalProperties)) errors.push(...validateValue(item, schema.additionalProperties, `${path}.${key}`, depth + 1, budget));
184
- }
185
- }
186
- return errors;
187
- };
188
- var validateOpenEditorCustomBlockDataValue = (value, schema) => {
189
- const diagnostics = [...inspectJson(value, "$"), ...validateValue(value, schema, "$")].sort((a, b) => a.path.localeCompare(b.path));
190
- return diagnostics.length ? { valid: false, diagnostics } : { valid: true, diagnostics: [] };
191
- };
192
- var valuesAt = (root, expression) => expression.split(".").filter(Boolean).reduce((current, segment) => current.flatMap((item) => {
193
- if (segment === "*") return Array.isArray(item.value) ? item.value.map((value, index) => ({ value, path: `${item.path}[${index}]` })) : [];
194
- return plainObject(item.value) && segment in item.value ? [{ value: item.value[segment], path: `${item.path}.${segment}` }] : [];
195
- }), [{ value: root, path: "$.data" }]);
196
- var fieldAt = (value, field) => field.split(".").reduce((current, key) => plainObject(current) ? current[key] : void 0, value);
197
- var hasUnsafeUrlSyntax = (value) => {
198
- if (/[\u0000-\u001f\u007f]/.test(value)) return true;
199
- const colon = value.indexOf(":");
200
- const path = value.indexOf("/");
201
- const query = value.indexOf("?");
202
- const firstPathOrQuery = Math.min(...[path, query].filter((index) => index >= 0), Number.POSITIVE_INFINITY);
203
- return colon >= 0 && colon < firstPathOrQuery && value.slice(0, colon).includes("&");
204
- };
205
- var validateConstraints = (data, constraints = []) => {
206
- const diagnostics = [];
207
- for (const constraint of constraints) {
208
- if (constraint.kind === "unique") for (const located of valuesAt(data, constraint.array)) {
209
- if (!Array.isArray(located.value)) continue;
210
- const seen = /* @__PURE__ */ new Set();
211
- located.value.forEach((item, index) => {
212
- const key = JSON.stringify(item);
213
- if (seen.has(key)) diagnostics.push({ path: `${located.path}[${index}]`, message: "Array values must be unique." });
214
- else seen.add(key);
215
- });
216
- }
217
- else if (constraint.kind === "uniqueBy") for (const located of valuesAt(data, constraint.array)) {
218
- if (!Array.isArray(located.value)) continue;
219
- const seen = /* @__PURE__ */ new Map();
220
- located.value.forEach((item, index) => {
221
- const key = JSON.stringify(constraint.keys.map((field) => fieldAt(item, field)));
222
- if (seen.has(key)) diagnostics.push({ path: `${located.path}[${index}]`, message: `Values must be unique by ${constraint.keys.join(", ")}.` });
223
- else seen.set(key, index);
224
- });
225
- }
226
- else if (constraint.kind === "keysIn") {
227
- const scopes = constraint.scope ? valuesAt(data, constraint.scope) : [{ value: data, path: "$.data" }];
228
- for (const scope of scopes) {
229
- const objects = valuesAt(scope.value, constraint.objects);
230
- const keySet = valuesAt(scope.value, constraint.keys)[0]?.value;
231
- const allowed = new Set(Array.isArray(keySet) ? keySet : []);
232
- for (const located of objects) {
233
- if (!plainObject(located.value)) continue;
234
- const objectPath = `${scope.path}${located.path.slice("$.data".length)}`;
235
- for (const key of Object.keys(located.value)) if (!allowed.has(key)) diagnostics.push({ path: `${objectPath}.${key}`, message: "Object key is not in the allowed key collection." });
236
- if (constraint.requireAll) {
237
- for (const key of allowed) if (typeof key === "string" && !(key in located.value)) diagnostics.push({ path: `${objectPath}.${key}`, message: "Required object key is missing." });
238
- }
239
- }
240
- }
241
- } else if (constraint.kind === "reference") {
242
- const targets = new Set(valuesAt(data, constraint.targetArray).flatMap((item) => Array.isArray(item.value) ? item.value.map((target) => fieldAt(target, constraint.targetField)) : []));
243
- for (const located of valuesAt(data, constraint.array)) if (Array.isArray(located.value)) located.value.forEach((item, index) => {
244
- const reference = fieldAt(item, constraint.field);
245
- if ((reference === null || reference === void 0) && constraint.nullable) return;
246
- if (!targets.has(reference)) diagnostics.push({ path: `${located.path}[${index}].${constraint.field}`, message: "Reference does not identify an available value." });
247
- });
248
- } else if (constraint.kind === "acyclic" || constraint.kind === "graph") for (const located of valuesAt(data, constraint.array)) {
249
- if (!Array.isArray(located.value)) continue;
250
- const parents = new Map(located.value.map((item) => [fieldAt(item, constraint.id), fieldAt(item, constraint.parent)]));
251
- if (constraint.kind === "graph") {
252
- const ids = /* @__PURE__ */ new Set();
253
- const siblings = /* @__PURE__ */ new Set();
254
- located.value.forEach((item, index) => {
255
- const id = fieldAt(item, constraint.id);
256
- if (ids.has(id)) diagnostics.push({ path: `${located.path}[${index}].${constraint.id}`, message: "Node identifiers must be unique in the collection." });
257
- else ids.add(id);
258
- });
259
- located.value.forEach((item, index) => {
260
- const parent = fieldAt(item, constraint.parent);
261
- if (parent !== null && parent !== void 0 && !ids.has(parent)) diagnostics.push({ path: `${located.path}[${index}].${constraint.parent}`, message: "Parent must identify a value in the same collection." });
262
- const sibling = JSON.stringify([parent, ...(constraint.siblingKeys ?? []).map((field) => fieldAt(item, field))]);
263
- if (siblings.has(sibling)) diagnostics.push({ path: `${located.path}[${index}]`, message: "Sibling ordering values must be unique." });
264
- else siblings.add(sibling);
265
- });
266
- }
267
- for (const [id] of parents) {
268
- const seen = /* @__PURE__ */ new Set();
269
- let current = id;
270
- while (current !== null && current !== void 0) {
271
- if (seen.has(current)) {
272
- diagnostics.push({ path: located.path, message: "Parent references must be acyclic." });
273
- break;
274
- }
275
- seen.add(current);
276
- current = parents.get(current);
277
- }
278
- }
279
- }
280
- else if (constraint.kind === "url") for (const located of valuesAt(data, constraint.path)) {
281
- if (located.value === null || located.value === void 0) continue;
282
- if (typeof located.value !== "string") {
283
- diagnostics.push({ path: located.path, message: "URL must be a string." });
284
- continue;
285
- }
286
- if (constraint.when) {
287
- const parentPath = located.path.replace(/\.[^.\[]+$/, "");
288
- const parentExpression = parentPath.replace(/^\$\.data\.?/, "").replace(/\[\d+\]/g, ".*");
289
- const parent = valuesAt(data, parentExpression).find((item) => item.path === parentPath)?.value;
290
- if (!plainObject(parent) || JSON.stringify(fieldAt(parent, constraint.when.field)) !== JSON.stringify(constraint.when.equals)) continue;
291
- }
292
- const normalized = located.value.trim();
293
- const scheme = /^([a-z][a-z\d+.-]*):/i.exec(normalized)?.[1]?.toLowerCase();
294
- const unsafeRelative = !scheme && (!normalized.startsWith("/") || normalized.startsWith("//") || normalized.includes("\\"));
295
- if (!scheme && (!constraint.allowRelative || unsafeRelative) || scheme && (constraint.requireSchemeSeparator && !normalized.toLowerCase().startsWith(`${scheme}://`) || constraint.schemes && !constraint.schemes.includes(scheme) || constraint.denySchemes?.includes(scheme)) || hasUnsafeUrlSyntax(normalized)) diagnostics.push({ path: located.path, message: "URL is not allowed." });
296
- }
297
- }
298
- return diagnostics;
52
+ var envelopeFrom = (value) => {
53
+ if (!plainObject(value)) return null;
54
+ const blockId = value.blockId;
55
+ const version = value.version;
56
+ const data = value.data;
57
+ return typeof blockId === "string" && ID_PATTERN.test(blockId) && positiveInteger(version) && plainObject(data) ? {
58
+ blockId,
59
+ version,
60
+ data
61
+ } : null;
299
62
  };
300
63
  var defineOpenEditorCustomBlock = (input) => {
301
- if (!ID_PATTERN.test(input.id)) throw new Error(`OpenEditor custom block IDs must be namespaced lowercase identifiers. Received "${input.id}".`);
302
- if (!Number.isSafeInteger(input.version) || input.version < 1) throw new Error("OpenEditor custom block versions must be positive integers.");
303
- if (typeof input.label !== "string" || !input.label.trim() || input.label.length > 200) throw new Error("OpenEditor custom block labels must be nonempty strings of 200 characters or less.");
304
- assertPortableSchema(input.dataSchema);
305
- assertPortableConstraints(input.constraints);
306
- if (input.dataSchema.type !== "object") throw new Error('OpenEditor custom block data schemas must have type "object".');
307
- const dataSchema = deepFreeze(clone(input.dataSchema));
308
- const constraints = input.constraints ? deepFreeze(clone(input.constraints)) : void 0;
309
- const migrations = input.migrations ? Object.freeze({ ...input.migrations }) : void 0;
310
- const manifest = deepFreeze({ id: input.id, label: input.label, version: input.version, dataSchema, ...constraints ? { constraints } : {} });
311
- const definition = { ...input, dataSchema, ...constraints ? { constraints } : {}, ...migrations ? { migrations } : {}, manifest };
312
- Object.defineProperty(definition, CUSTOM_BLOCK_DEFINITION_BRAND, { value: true });
64
+ if (!ID_PATTERN.test(input.id))
65
+ throw new Error(
66
+ `OpenEditor custom block IDs must be namespaced lowercase identifiers. Received "${input.id}".`
67
+ );
68
+ if (!positiveInteger(input.version))
69
+ throw new Error(
70
+ "OpenEditor custom block versions must be positive integers."
71
+ );
72
+ if (!input.label.trim() || input.label.length > 200)
73
+ throw new Error(
74
+ "OpenEditor custom block labels must be nonempty strings of 200 characters or less."
75
+ );
76
+ const manifest = deepFreeze({
77
+ id: input.id,
78
+ label: input.label,
79
+ version: input.version
80
+ });
81
+ const definition = { ...input, manifest };
82
+ Object.defineProperty(definition, DEFINITION_BRAND, { value: true });
313
83
  return Object.freeze(definition);
314
84
  };
315
- var envelopeFromNode = (node) => {
316
- const attrs = node.attrs;
317
- return attrs && typeof attrs.blockId === "string" && typeof attrs.version === "number" && plainObject(attrs.data) ? { blockId: attrs.blockId, version: attrs.version, data: attrs.data } : null;
318
- };
319
- var escapeOpenEditorCustomBlockHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
320
- var SAFE_TAGS = /* @__PURE__ */ new Set(["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"]);
321
- var SAFE_ATTRS = /* @__PURE__ */ new Set(["aria-label", "aria-current", "role", "title", "href", "src", "alt", "width", "height", "start", "colspan", "rowspan", "scope"]);
322
- var safeStaticUrl = (value, context) => {
323
- const normalized = value.trim();
324
- if (!normalized || hasUnsafeUrlSyntax(normalized)) return null;
325
- const scheme = /^([a-z][a-z\d+.-]*):/i.exec(normalized)?.[1]?.toLowerCase();
326
- if (!scheme) {
327
- if (normalized.startsWith("//") || normalized.includes("\\")) return null;
328
- return normalized;
85
+ var parseDefinitionData = (definition, data) => {
86
+ const diagnostics = inspectJson(data);
87
+ if (diagnostics.length) return { valid: false, diagnostics };
88
+ try {
89
+ const parsed = definition.parseData(deepFreeze(clone(data)));
90
+ const parsedDiagnostics = inspectJson(parsed);
91
+ if (!plainObject(parsed) || parsedDiagnostics.length)
92
+ return {
93
+ valid: false,
94
+ diagnostics: parsedDiagnostics.length ? parsedDiagnostics : [{ path: "$.data", message: "The block parser must return an object." }]
95
+ };
96
+ return { valid: true, data: deepFreeze(clone(parsed)) };
97
+ } catch (error) {
98
+ return {
99
+ valid: false,
100
+ diagnostics: [
101
+ {
102
+ path: "$.data",
103
+ message: error instanceof Error ? error.message : "Custom block data is invalid."
104
+ }
105
+ ]
106
+ };
329
107
  }
330
- const schemes = context === "asset" ? ["http", "https"] : ["http", "https", "mailto", "tel"];
331
- return schemes.includes(scheme) ? normalized : null;
332
- };
333
- var renderOpenEditorCustomBlockSafeHtml = (value) => {
334
- const seen = /* @__PURE__ */ new WeakSet();
335
- let nodes = 0;
336
- let stringBytes = 0;
337
- const countRendered = (rendered) => {
338
- stringBytes += rendered.length;
339
- if (stringBytes > VALIDATION_LIMITS.stringLength) throw new Error("Safe HTML output exceeds its text limit.");
340
- return rendered;
341
- };
342
- const render = (item, depth) => {
343
- nodes += 1;
344
- if (nodes > VALIDATION_LIMITS.values || depth > VALIDATION_LIMITS.depth) throw new Error("Safe HTML output exceeds its structural limit.");
345
- if (item && typeof item === "object") {
346
- if (seen.has(item)) throw new Error("Safe HTML output must not contain cycles.");
347
- seen.add(item);
348
- }
349
- const value2 = item;
350
- if (value2 === null || value2 === false) return "";
351
- if (typeof value2 === "string" || typeof value2 === "number") return countRendered(escapeOpenEditorCustomBlockHtml(value2));
352
- if (!plainObject(value2) || typeof value2.tag !== "string" || !SAFE_TAGS.has(value2.tag)) return "";
353
- const attrs = Object.entries(value2.attrs ?? {}).flatMap(([name, raw]) => {
354
- if (raw === void 0 || !SAFE_ATTRS.has(name) || name === "href" && value2.tag !== "a" || name === "src" && value2.tag !== "img" || name === "start" && value2.tag !== "ol" || ["colspan", "rowspan", "scope"].includes(name) && !["td", "th"].includes(value2.tag)) return [];
355
- const safe = name === "href" ? safeStaticUrl(String(raw), "navigation") : name === "src" ? safeStaticUrl(String(raw), "asset") : String(raw);
356
- return safe === null ? [] : [countRendered(` ${name}="${escapeOpenEditorCustomBlockHtml(safe)}"`)];
357
- }).join("");
358
- const children = (value2.children ?? []).map((child) => render(child, depth + 1)).join("");
359
- return ["img", "br", "hr"].includes(value2.tag) ? `<${value2.tag}${attrs}>` : `<${value2.tag}${attrs}>${children}</${value2.tag}>`;
360
- };
361
- return render(value, 0);
362
108
  };
363
109
  var createOpenEditorCustomBlockRegistry = (definitions, options = {}) => {
364
110
  const byId = /* @__PURE__ */ new Map();
365
111
  for (const definition of definitions) {
366
- try {
367
- assertPortableSchema(definition.dataSchema);
368
- assertPortableConstraints(definition.constraints);
369
- } catch {
112
+ if (definition[DEFINITION_BRAND] !== true || !Object.isFrozen(definition) || !ID_PATTERN.test(definition.id) || !positiveInteger(definition.version))
370
113
  throw new Error(`Invalid custom block definition "${definition.id ?? "unknown"}".`);
371
- }
372
- if (definition[CUSTOM_BLOCK_DEFINITION_BRAND] !== true || !recursivelyFrozen(definition.dataSchema) || !recursivelyFrozen(definition.constraints) || !recursivelyFrozen(definition.manifest) || !recursivelyFrozen(definition.migrations) || !Object.isFrozen(definition) || !ID_PATTERN.test(definition.id) || !Number.isSafeInteger(definition.version) || definition.version < 1 || definition.dataSchema?.type !== "object" || definition.manifest?.id !== definition.id || definition.manifest?.version !== definition.version || definition.manifest?.dataSchema !== definition.dataSchema || definition.manifest?.constraints !== definition.constraints) throw new Error(`Invalid custom block definition "${definition.id ?? "unknown"}".`);
373
- if (byId.has(definition.id)) throw new Error(`Duplicate custom block ID "${definition.id}".`);
114
+ if (byId.has(definition.id))
115
+ throw new Error(`Duplicate custom block ID "${definition.id}".`);
374
116
  byId.set(definition.id, definition);
375
117
  }
376
118
  const disabled = new Set(options.disabled ?? []);
377
- const resolve = (node) => {
378
- const envelope = envelopeFromNode(node);
379
- if (node.type !== OPENEDITOR_CUSTOM_BLOCK_NODE || !envelope) return { status: "invalid", node, diagnostics: [{ path: "$", message: "Expected a valid OpenEditor custom block envelope." }] };
380
- if (typeof node.attrs?.["openeditor-id"] !== "string" || !node.attrs["openeditor-id"].trim()) return { status: "invalid", node, blockId: envelope.blockId, diagnostics: [{ path: "$.attrs.openeditor-id", message: "Custom block instance ID is required." }] };
381
- const definition = byId.get(envelope.blockId);
382
- if (!definition) return { status: "missing", node, blockId: envelope.blockId, diagnostics: [] };
383
- if (disabled.has(envelope.blockId)) return { status: "disabled", node, blockId: envelope.blockId, diagnostics: [] };
384
- if (envelope.version > definition.version) return { status: "incompatible", node, blockId: envelope.blockId, diagnostics: [{ path: "$.version", message: `Stored version ${envelope.version} is newer than supported version ${definition.version}.` }] };
385
- let version = envelope.version;
386
- let data = clone(envelope.data);
387
- try {
388
- while (version < definition.version) {
389
- const migrate = definition.migrations?.[version];
390
- if (!migrate) return { status: "incompatible", node, blockId: envelope.blockId, diagnostics: [{ path: "$.version", message: `No migration exists from version ${version}.` }] };
391
- data = clone(migrate(Object.freeze(clone(data))));
392
- version += 1;
119
+ const resolveEnvelope = (raw) => {
120
+ const initial = envelopeFrom(raw);
121
+ if (!initial)
122
+ return {
123
+ status: "invalid",
124
+ diagnostics: [
125
+ {
126
+ path: "$",
127
+ message: "Expected a valid OpenEditor custom block envelope."
128
+ }
129
+ ]
130
+ };
131
+ const definition = byId.get(initial.blockId);
132
+ if (!definition)
133
+ return { status: "missing", blockId: initial.blockId, diagnostics: [] };
134
+ if (disabled.has(initial.blockId))
135
+ return { status: "disabled", blockId: initial.blockId, diagnostics: [] };
136
+ if (initial.version > definition.version)
137
+ return {
138
+ status: "incompatible",
139
+ blockId: initial.blockId,
140
+ diagnostics: [
141
+ {
142
+ path: "$.version",
143
+ message: `Stored version ${initial.version} is newer than supported version ${definition.version}.`
144
+ }
145
+ ]
146
+ };
147
+ let envelope = clone(initial);
148
+ let attempts = 0;
149
+ while (envelope.version < definition.version) {
150
+ if (!definition.migrate)
151
+ return {
152
+ status: "incompatible",
153
+ blockId: initial.blockId,
154
+ diagnostics: [
155
+ {
156
+ path: "$.version",
157
+ message: `No migration exists from version ${envelope.version}.`
158
+ }
159
+ ]
160
+ };
161
+ try {
162
+ const next = definition.migrate({
163
+ version: envelope.version,
164
+ data: deepFreeze(clone(envelope.data))
165
+ });
166
+ const checked = envelopeFrom(next);
167
+ if (!checked || checked.blockId !== definition.id || checked.version <= envelope.version || checked.version > definition.version)
168
+ throw new Error("The custom block migration returned an invalid envelope.");
169
+ envelope = checked;
170
+ } catch (error) {
171
+ return {
172
+ status: "invalid",
173
+ blockId: initial.blockId,
174
+ diagnostics: [
175
+ {
176
+ path: "$.data",
177
+ message: error instanceof Error ? error.message : "Migration failed."
178
+ }
179
+ ]
180
+ };
393
181
  }
394
- } catch (error) {
395
- return { status: "invalid", node, blockId: envelope.blockId, diagnostics: [{ path: "$.data", message: error instanceof Error ? error.message : "Migration failed." }] };
182
+ attempts += 1;
183
+ if (attempts > 100)
184
+ return {
185
+ status: "invalid",
186
+ blockId: initial.blockId,
187
+ diagnostics: [
188
+ { path: "$.version", message: "Migration exceeded the step limit." }
189
+ ]
190
+ };
396
191
  }
397
- const diagnostics = [...inspectJson(data), ...validateValue(data, definition.dataSchema, "$.data"), ...validateConstraints(data, definition.constraints)].sort((a, b) => a.path.localeCompare(b.path));
398
- try {
399
- const refinement = definition.validateData?.(deepFreeze(clone(data)));
400
- if (typeof refinement === "string") diagnostics.push({ path: "$.data", message: refinement });
401
- else for (const message of refinement ?? []) diagnostics.push({ path: "$.data", message });
402
- } catch (error) {
403
- diagnostics.push({ path: "$.data", message: error instanceof Error ? error.message : "Custom validation failed." });
404
- }
405
- if (diagnostics.length) return { status: "invalid", node, blockId: envelope.blockId, diagnostics };
406
- const migrated = version !== envelope.version;
407
- const readyData = deepFreeze(clone(data));
408
- return { status: "ready", definition, data: readyData, migrated, node: migrated ? { ...node, attrs: { ...node.attrs, blockId: definition.id, version, data: readyData } } : node };
192
+ const parsed = parseDefinitionData(definition, envelope.data);
193
+ return parsed.valid ? {
194
+ status: "ready",
195
+ definition,
196
+ envelope: { ...envelope, data: parsed.data },
197
+ migrated: envelope.version !== initial.version
198
+ } : {
199
+ status: "invalid",
200
+ blockId: initial.blockId,
201
+ diagnostics: parsed.diagnostics
202
+ };
409
203
  };
410
- const defaultDocumentSafeTree = (document) => {
411
- let count = 0;
412
- const renderNode = (node, depth = 0) => {
413
- count += 1;
414
- if (count > VALIDATION_LIMITS.values || depth > VALIDATION_LIMITS.depth) return "";
415
- if (node.type === "text") {
416
- let output = node.text ?? "";
417
- for (const mark of [...node.marks ?? []].reverse()) {
418
- if (mark.type === "bold") output = { tag: "strong", children: [output] };
419
- else if (mark.type === "italic") output = { tag: "em", children: [output] };
420
- else if (mark.type === "underline") output = { tag: "u", children: [output] };
421
- else if (mark.type === "strike") output = { tag: "s", children: [output] };
422
- else if (mark.type === "code") output = { tag: "code", children: [output] };
423
- else if (mark.type === "link" && typeof mark.attrs?.href === "string") output = { tag: "a", attrs: { href: mark.attrs.href }, children: [output] };
204
+ const resolve = (node) => {
205
+ const resolved = resolveEnvelope(node.attrs);
206
+ if (node.type !== OPENEDITOR_CUSTOM_BLOCK_NODE)
207
+ return {
208
+ status: "invalid",
209
+ node,
210
+ diagnostics: [
211
+ { path: "$.type", message: "Expected a customBlock node." }
212
+ ]
213
+ };
214
+ if (resolved.status !== "ready") return { ...resolved, node };
215
+ if (typeof node.attrs?.["openeditor-id"] !== "string" || !node.attrs["openeditor-id"].trim())
216
+ return {
217
+ status: "invalid",
218
+ node,
219
+ blockId: resolved.definition.id,
220
+ diagnostics: [
221
+ {
222
+ path: "$.attrs.openeditor-id",
223
+ message: "Custom block instance ID is required."
224
+ }
225
+ ]
226
+ };
227
+ const migratedNode = {
228
+ ...node,
229
+ attrs: { ...node.attrs, ...resolved.envelope }
230
+ };
231
+ return {
232
+ status: "ready",
233
+ definition: resolved.definition,
234
+ data: resolved.envelope.data,
235
+ migrated: resolved.migrated,
236
+ node: migratedNode
237
+ };
238
+ };
239
+ const renderNestedHtml = (document) => {
240
+ const valid = validateDocument(document);
241
+ if (!valid.valid) return "";
242
+ return {
243
+ tag: "div",
244
+ children: document.content.map((node) => {
245
+ if (node.type === "text") return node.text ?? "";
246
+ if (node.type === OPENEDITOR_CUSTOM_BLOCK_NODE) {
247
+ const nested = resolve(node);
248
+ return nested.status === "ready" ? nested.definition.toHtml({
249
+ data: nested.data,
250
+ renderDocument: options.renderDocument ?? renderNestedHtml,
251
+ documentToText: options.documentToText ?? renderNestedText
252
+ }) : `[${String(node.attrs?.blockId ?? "custom block")}: ${nested.status}]`;
424
253
  }
425
- return output;
426
- }
427
- if (node.type === OPENEDITOR_CUSTOM_BLOCK_NODE) {
428
- const nested = resolve(node);
429
- return nested.status === "ready" ? exportNestedSafeTree(nested) : `[${envelopeFromNode(node)?.blockId ?? "custom block"}: ${nested.status}]`;
430
- }
431
- const children = node.content?.map((child) => renderNode(child, depth + 1)) ?? [];
432
- if (node.type === "image") return { tag: "figure", children: [{ tag: "img", attrs: { alt: typeof node.attrs?.alt === "string" ? node.attrs.alt : "Image" } }] };
433
- if (node.type === "page") return { tag: "span", children: children.length ? children : [typeof node.attrs?.title === "string" ? node.attrs.title : "Untitled page"] };
434
- if (node.type === "attachment") return { tag: "span", children: [typeof node.attrs?.name === "string" && node.attrs.name ? node.attrs.name : "Attachment"] };
435
- const heading = node.type === "heading" ? `h${Math.min(6, Math.max(1, Number(node.attrs?.level) || 2))}` : null;
436
- const tag = heading ?? (node.type === "paragraph" ? "p" : node.type === "bulletList" || node.type === "taskList" ? "ul" : node.type === "orderedList" ? "ol" : ["listItem", "taskItem", "toggleListItem"].includes(node.type) ? "li" : node.type === "blockquote" ? "blockquote" : node.type === "codeBlock" ? "pre" : node.type === "table" ? "table" : node.type === "tableRow" ? "tr" : node.type === "tableHeader" ? "th" : node.type === "tableCell" ? "td" : node.type === "hardBreak" ? "br" : node.type === "divider" || node.type === "horizontalRule" ? "hr" : node.type === "callout" ? "aside" : node.type === "diagram" || node.type === "mermaidDiagram" ? "figure" : "div");
437
- const attrs = node.type === "orderedList" && positiveInteger(node.attrs?.start) ? { start: Number(node.attrs?.start) } : node.type === "tableHeader" || node.type === "tableCell" ? { ...positiveInteger(node.attrs?.colspan) ? { colspan: Number(node.attrs?.colspan) } : {}, ...positiveInteger(node.attrs?.rowspan) ? { rowspan: Number(node.attrs?.rowspan) } : {}, ...node.type === "tableHeader" && typeof node.attrs?.scope === "string" ? { scope: node.attrs.scope } : {} } : void 0;
438
- return { tag, ...attrs ? { attrs } : {}, children };
254
+ const children = node.content?.map(
255
+ (child) => renderNestedHtml({ type: "doc", version: 1, content: [child] })
256
+ );
257
+ return {
258
+ tag: node.type === "paragraph" ? "p" : "div",
259
+ children
260
+ };
261
+ })
439
262
  };
440
- return { tag: "div", children: document.content.map((node) => renderNode(node)) };
441
263
  };
442
- const defaultDocumentText = (document) => {
443
- let count = 0;
444
- const renderNode = (node, depth = 0) => {
445
- count += 1;
446
- if (count > VALIDATION_LIMITS.values || depth > VALIDATION_LIMITS.depth) return "";
264
+ const renderNestedText = (document) => {
265
+ const visit = (node) => {
447
266
  if (node.type === OPENEDITOR_CUSTOM_BLOCK_NODE) {
448
267
  const nested = resolve(node);
449
- return nested.status === "ready" ? exportNestedText(nested) : "";
268
+ return nested.status === "ready" ? nested.definition.toText({
269
+ data: nested.data,
270
+ renderDocument: options.renderDocument ?? renderNestedHtml,
271
+ documentToText: options.documentToText ?? renderNestedText
272
+ }) : "";
450
273
  }
451
- if (node.type === "image") return typeof node.attrs?.alt === "string" ? node.attrs.alt : "Image";
452
- if (node.type === "page" && !node.content?.length) return typeof node.attrs?.title === "string" ? node.attrs.title : "Untitled page";
453
- if (node.type === "attachment") return typeof node.attrs?.name === "string" && node.attrs.name ? node.attrs.name : "Attachment";
454
274
  if (node.text) return node.text;
455
- const content = node.content?.map((child) => renderNode(child, depth + 1)).filter(Boolean) ?? [];
456
- return ["doc", "paragraph", "heading", "listItem", "taskItem", "blockquote", "codeBlock", "tableRow", "callout"].includes(node.type) ? content.join("") : content.join("\n");
275
+ return node.content?.map(visit).filter(Boolean).join("\n") ?? "";
457
276
  };
458
- return renderNode(document).trim();
459
- };
460
- const fallbackLabel = (node, status) => `Custom block ${envelopeFromNode(node)?.blockId ?? "unknown"} is unavailable: ${status}.`;
461
- const fallback = (node, result) => `<p role="status" data-openeditor-custom-block-error="${escapeOpenEditorCustomBlockHtml(result.status)}" data-block-id="${escapeOpenEditorCustomBlockHtml(envelopeFromNode(node)?.blockId ?? "unknown")}">${escapeOpenEditorCustomBlockHtml(fallbackLabel(node, result.status))}</p>`;
462
- const exportReadySafeTree = (result) => result.definition.toHtml({ data: result.data, renderDocument: options.renderDocument ?? defaultDocumentSafeTree, documentToText: options.documentToText ?? defaultDocumentText });
463
- const exportReadyText = (result) => result.definition.toText({ data: result.data, renderDocument: options.renderDocument ?? defaultDocumentSafeTree, documentToText: options.documentToText ?? defaultDocumentText });
464
- const exportNestedSafeTree = (result) => {
465
- try {
466
- const tree = exportReadySafeTree(result);
467
- renderOpenEditorCustomBlockSafeHtml(tree);
468
- return tree;
469
- } catch {
470
- return { tag: "p", attrs: { role: "status" }, children: [fallbackLabel(result.node, "invalid")] };
471
- }
277
+ return document.content.map(visit).filter(Boolean).join("\n").trim();
472
278
  };
473
- const exportNestedText = (result) => {
474
- try {
475
- return exportReadyText(result);
476
- } catch {
477
- return fallbackLabel(result.node, "invalid");
478
- }
479
- };
480
- return Object.freeze({
279
+ const fallbackLabel = (node, status) => `Custom block ${String(node.attrs?.blockId ?? "unknown")} is unavailable: ${status}.`;
280
+ const registry = {
481
281
  definitions: Object.freeze([...definitions]),
482
282
  manifests: Object.freeze(definitions.map((item) => item.manifest)),
483
283
  get: (id) => byId.get(id),
484
284
  isEnabled: (id) => byId.has(id) && !disabled.has(id),
485
285
  resolve,
286
+ validate: (raw) => {
287
+ const result = resolveEnvelope(raw);
288
+ return result.status === "ready" ? { valid: true, envelope: result.envelope } : { valid: false, diagnostics: result.diagnostics };
289
+ },
290
+ assets: (raw) => {
291
+ const result = resolveEnvelope(raw);
292
+ if (result.status !== "ready" || !result.definition.assets) return [];
293
+ try {
294
+ return result.definition.assets(result.envelope.data).filter(
295
+ (reference) => typeof reference.id === "string" && reference.id.length > 0 && typeof reference.path === "string" && reference.path.startsWith("$.data")
296
+ );
297
+ } catch {
298
+ return [];
299
+ }
300
+ },
486
301
  toHtml: (node) => {
487
302
  const result = resolve(node);
488
- if (result.status !== "ready") return fallback(node, result);
303
+ if (result.status !== "ready")
304
+ return `<p role="status" data-openeditor-custom-block-error="${escapeOpenEditorCustomBlockHtml(result.status)}">${escapeOpenEditorCustomBlockHtml(fallbackLabel(node, result.status))}</p>`;
489
305
  try {
490
- const rendered = renderOpenEditorCustomBlockSafeHtml(exportReadySafeTree(result));
491
- return rendered || fallback(node, { status: "invalid", node, blockId: result.definition.id, diagnostics: [{ path: "$.toHtml", message: "Static HTML output is invalid." }] });
306
+ return renderOpenEditorCustomBlockSafeHtml(
307
+ result.definition.toHtml({
308
+ data: result.data,
309
+ renderDocument: options.renderDocument ?? renderNestedHtml,
310
+ documentToText: options.documentToText ?? renderNestedText
311
+ })
312
+ );
492
313
  } catch {
493
- return fallback(node, { status: "invalid", blockId: result.definition.id});
314
+ return `<p role="status" data-openeditor-custom-block-error="invalid">${escapeOpenEditorCustomBlockHtml(fallbackLabel(node, "invalid"))}</p>`;
494
315
  }
495
316
  },
496
317
  toText: (node) => {
497
318
  const result = resolve(node);
498
319
  if (result.status !== "ready") return fallbackLabel(node, result.status);
499
320
  try {
500
- return exportReadyText(result);
321
+ return result.definition.toText({
322
+ data: result.data,
323
+ renderDocument: options.renderDocument ?? renderNestedHtml,
324
+ documentToText: options.documentToText ?? renderNestedText
325
+ });
501
326
  } catch {
502
327
  return fallbackLabel(node, "invalid");
503
328
  }
504
329
  }
505
- });
330
+ };
331
+ return Object.freeze(registry);
506
332
  };
507
333
  var createOpenEditorCustomBlockNode = (registry, id, data, options = {}) => {
508
334
  const definition = registry.get(id);
509
- if (!definition) throw new Error(`OpenEditor custom block "${id}" is not registered.`);
510
- const value = clone(data ?? definition.initialData());
511
- const diagnostics = validateValue(value, definition.dataSchema, "$.data");
512
- if (diagnostics.length) throw new Error(`Invalid initial data for "${id}": ${diagnostics.map((item) => `${item.path}: ${item.message}`).join("; ")}`);
513
- const instanceId = options.instanceId ?? options.createInstanceId?.() ?? globalThis.crypto.randomUUID();
514
- if (!instanceId.trim()) throw new Error("OpenEditor custom block instance IDs must not be empty.");
515
- const node = { type: OPENEDITOR_CUSTOM_BLOCK_NODE, attrs: { "openeditor-id": instanceId, blockId: definition.id, version: definition.version, data: value } };
516
- const resolved = registry.resolve(node);
517
- if (resolved.status !== "ready") throw new Error(`Invalid initial data for "${id}": ${resolved.diagnostics.map((item) => `${item.path}: ${item.message}`).join("; ") || resolved.status}`);
518
- return node;
519
- };
520
- var resolveOpenEditorCustomBlockNode = (registry, node) => registry.resolve(node);
521
- var validateOpenEditorCustomBlockEnvelope = (value, manifests, options = {}) => {
522
- const ids = /* @__PURE__ */ new Set();
523
- for (const manifest2 of manifests) {
524
- if (ids.has(manifest2.id)) return { valid: false, diagnostics: [{ path: "$.manifests", message: `Duplicate custom block manifest "${manifest2.id}".` }] };
525
- ids.add(manifest2.id);
526
- if (!ID_PATTERN.test(manifest2.id) || !Number.isSafeInteger(manifest2.version) || manifest2.version < 1) return { valid: false, diagnostics: [{ path: "$.manifests", message: "Custom block manifest identity or version is invalid." }] };
527
- }
528
- if (!plainObject(value) || typeof value.blockId !== "string" || typeof value.version !== "number" || !plainObject(value.data)) return { valid: false, diagnostics: [{ path: "$", message: "Expected a custom block envelope." }] };
529
- const manifest = manifests.find((item) => item.id === value.blockId);
530
- if (options.mode === "preserve" && (!manifest || options.disabled?.includes(value.blockId) || manifest && value.version !== manifest.version)) {
531
- const diagnostics2 = inspectJson(value.data);
532
- return diagnostics2.length ? { valid: false, diagnostics: diagnostics2 } : { valid: true, diagnostics: [], status: "preserved" };
533
- }
534
- if (!manifest) return { valid: false, diagnostics: [{ path: "$.blockId", message: `Unknown custom block "${value.blockId}".` }] };
535
- if (value.version !== manifest.version) return { valid: false, diagnostics: [{ path: "$.version", message: `Expected version ${manifest.version}.` }] };
536
- const nestedDiagnostics = [];
537
- const nestedSeen = /* @__PURE__ */ new WeakSet();
538
- const inspectNested = (input, path) => {
539
- if (!plainObject(input) || nestedSeen.has(input)) return;
540
- nestedSeen.add(input);
541
- if (input.type === OPENEDITOR_CUSTOM_BLOCK_NODE && plainObject(input.attrs)) {
542
- const nested = validateOpenEditorCustomBlockEnvelope(input.attrs, manifests, options);
543
- if (!nested.valid) nestedDiagnostics.push(...nested.diagnostics.map((item) => ({ path: `${path}.attrs${item.path.slice(1)}`, message: item.message })));
544
- return;
545
- }
546
- if (Array.isArray(input.content)) input.content.forEach((item, index) => inspectNested(item, `${path}.content[${index}]`));
547
- };
548
- const inspectDeclaredDocuments = (input, schema, path) => {
549
- if (schema.type === "document") {
550
- inspectNested(input, path);
551
- return;
552
- }
553
- if (schema.type === "array" && Array.isArray(input) && schema.items) input.forEach((item, index) => inspectDeclaredDocuments(item, schema.items, `${path}[${index}]`));
554
- if (schema.type === "object" && plainObject(input)) for (const [key, item] of Object.entries(input)) {
555
- const property = schema.properties?.[key];
556
- if (property) inspectDeclaredDocuments(item, property, `${path}.${key}`);
557
- else if (plainObject(schema.additionalProperties)) inspectDeclaredDocuments(item, schema.additionalProperties, `${path}.${key}`);
558
- }
559
- if (schema.type === "oneOf") {
560
- for (const variant of schema.variants) if (validateValue(input, variant, path).length === 0) inspectDeclaredDocuments(input, variant, path);
335
+ if (!definition)
336
+ throw new Error(`OpenEditor custom block "${id}" is not registered.`);
337
+ const node = {
338
+ type: OPENEDITOR_CUSTOM_BLOCK_NODE,
339
+ attrs: {
340
+ "openeditor-id": options.instanceId ?? options.createInstanceId?.() ?? crypto.randomUUID(),
341
+ blockId: definition.id,
342
+ version: definition.version,
343
+ data: data ?? definition.createData()
561
344
  }
562
345
  };
563
- inspectDeclaredDocuments(value.data, manifest.dataSchema, "$.data");
564
- const diagnostics = [...inspectJson(value.data), ...validateValue(value.data, manifest.dataSchema, "$.data"), ...validateConstraints(value.data, manifest.constraints), ...nestedDiagnostics].sort((a, b) => a.path.localeCompare(b.path));
565
- if (options.mode === "preserve" && diagnostics.length && inspectJson(value.data).length === 0) return { valid: true, diagnostics, status: "preserved-invalid" };
566
- return diagnostics.length ? { valid: false, diagnostics } : { valid: true, diagnostics: [] };
567
- };
568
- var extractOpenEditorCustomBlockAssetReferences = (value, manifests) => {
569
- const byId = new Map(manifests.map((manifest) => [manifest.id, manifest]));
570
- const references = [];
571
- const seen = /* @__PURE__ */ new Set();
572
- const visitSchema = (input, schema, path) => {
573
- if (input === null || input === void 0) return;
574
- if (schema.type === "string" && schema.format === "asset-id" && typeof input === "string" && OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN.test(input)) {
575
- const key = `${path}\0${input}`;
576
- if (!seen.has(key)) {
577
- seen.add(key);
578
- references.push({ id: input, path });
579
- }
580
- return;
581
- }
582
- if (schema.type === "array" && Array.isArray(input) && schema.items) input.forEach((item, index) => visitSchema(item, schema.items, `${path}[${index}]`));
583
- if (schema.type === "object" && plainObject(input)) for (const [key, item] of Object.entries(input)) {
584
- const property = schema.properties?.[key];
585
- if (property) visitSchema(item, property, `${path}.${key}`);
586
- else if (plainObject(schema.additionalProperties)) visitSchema(item, schema.additionalProperties, `${path}.${key}`);
587
- }
588
- if (schema.type === "document" && plainObject(input)) visitNested(input, path);
589
- if (schema.type === "oneOf") for (const variant of schema.variants) {
590
- if (validateValue(input, variant, path).length === 0) visitSchema(input, variant, path);
591
- }
592
- };
593
- const visitNested = (input, path) => {
594
- if (!plainObject(input)) return;
595
- if (input.type === OPENEDITOR_CUSTOM_BLOCK_NODE && plainObject(input.attrs)) {
596
- const manifest = typeof input.attrs.blockId === "string" ? byId.get(input.attrs.blockId) : void 0;
597
- if (manifest && plainObject(input.attrs.data) && validateOpenEditorCustomBlockEnvelope(input.attrs, manifests).valid) visitSchema(input.attrs.data, manifest.dataSchema, `${path}.attrs.data`);
598
- return;
599
- }
600
- if (Array.isArray(input.content)) input.content.forEach((item, index) => visitNested(item, `${path}.content[${index}]`));
601
- };
602
- if (plainObject(value) && typeof value.blockId === "string" && plainObject(value.data)) {
603
- const manifest = byId.get(value.blockId);
604
- if (manifest && validateOpenEditorCustomBlockEnvelope(value, manifests).valid) visitSchema(value.data, manifest.dataSchema, "$.data");
605
- }
606
- return references;
346
+ const resolved = registry.resolve(node);
347
+ if (resolved.status !== "ready")
348
+ throw new Error(
349
+ `Invalid initial data for custom block "${id}": ${resolved.diagnostics.map((item) => item.message).join(" ")}`
350
+ );
351
+ return resolved.node;
607
352
  };
353
+ var resolveOpenEditorCustomBlockNode = (registry, node) => registry.resolve(node);
354
+ var validateOpenEditorCustomBlockEnvelope = (value, registry) => registry.validate(value);
355
+ var extractOpenEditorCustomBlockAssetReferences = (value, registry) => registry.assets(value);
608
356
  var conformOpenEditorCustomBlock = (definition) => {
609
- const diagnostics = [];
610
357
  try {
611
358
  const registry = createOpenEditorCustomBlockRegistry([definition]);
612
- const node = createOpenEditorCustomBlockNode(registry, definition.id, void 0, { instanceId: "conformance-instance" });
613
- const envelope = node.attrs;
614
- const validated = validateOpenEditorCustomBlockEnvelope(envelope, registry.manifests);
615
- if (!validated.valid) diagnostics.push(...validated.diagnostics);
359
+ const node = createOpenEditorCustomBlockNode(
360
+ registry,
361
+ definition.id,
362
+ void 0,
363
+ { instanceId: "conformance-instance" }
364
+ );
616
365
  const html = registry.toHtml(node);
617
366
  const text = registry.toText(node);
618
- if (html.includes('data-openeditor-custom-block-error="invalid"') || text === `Custom block ${definition.id} is unavailable: invalid.`) diagnostics.push({ path: "$.staticExport", message: "Custom block static export failed conformance." });
367
+ return html.includes('data-openeditor-custom-block-error="invalid"') || text === `Custom block ${definition.id} is unavailable: invalid.` ? [
368
+ {
369
+ path: "$.staticExport",
370
+ message: "Custom block static export failed conformance."
371
+ }
372
+ ] : [];
619
373
  } catch (error) {
620
- diagnostics.push({ path: "$", message: error instanceof Error ? error.message : "Custom block conformance failed." });
374
+ return [
375
+ {
376
+ path: "$",
377
+ message: error instanceof Error ? error.message : "Custom block conformance failed."
378
+ }
379
+ ];
621
380
  }
622
- return diagnostics;
381
+ };
382
+ var escapeOpenEditorCustomBlockHtml = (value) => String(value).replace(
383
+ /[&<>"']/g,
384
+ (character) => ({
385
+ "&": "&amp;",
386
+ "<": "&lt;",
387
+ ">": "&gt;",
388
+ '"': "&quot;",
389
+ "'": "&#39;"
390
+ })[character]
391
+ );
392
+ var SAFE_TAGS = /* @__PURE__ */ new Set([
393
+ "div",
394
+ "span",
395
+ "p",
396
+ "h1",
397
+ "h2",
398
+ "h3",
399
+ "h4",
400
+ "h5",
401
+ "h6",
402
+ "article",
403
+ "section",
404
+ "aside",
405
+ "blockquote",
406
+ "figure",
407
+ "figcaption",
408
+ "ul",
409
+ "ol",
410
+ "li",
411
+ "dl",
412
+ "dt",
413
+ "dd",
414
+ "table",
415
+ "caption",
416
+ "thead",
417
+ "tbody",
418
+ "tr",
419
+ "th",
420
+ "td",
421
+ "strong",
422
+ "em",
423
+ "u",
424
+ "s",
425
+ "code",
426
+ "pre",
427
+ "a",
428
+ "img",
429
+ "br",
430
+ "hr"
431
+ ]);
432
+ var SAFE_ATTRS = /* @__PURE__ */ new Set([
433
+ "aria-label",
434
+ "aria-current",
435
+ "role",
436
+ "title",
437
+ "href",
438
+ "src",
439
+ "alt",
440
+ "width",
441
+ "height",
442
+ "start",
443
+ "colspan",
444
+ "rowspan",
445
+ "scope"
446
+ ]);
447
+ var safeStaticUrl = (value, context) => {
448
+ const normalized = value.trim();
449
+ if (!normalized) return null;
450
+ const scheme = /^([a-z][a-z\d+.-]*):/i.exec(normalized)?.[1]?.toLowerCase();
451
+ if (!scheme)
452
+ return normalized.startsWith("//") || normalized.includes("\\") ? null : normalized;
453
+ const allowed = context === "asset" ? ["http", "https"] : ["http", "https", "mailto", "tel"];
454
+ return allowed.includes(scheme) ? normalized : null;
455
+ };
456
+ var renderOpenEditorCustomBlockSafeHtml = (value) => {
457
+ const seen = /* @__PURE__ */ new WeakSet();
458
+ let nodes = 0;
459
+ let stringBytes = 0;
460
+ const count = (rendered) => {
461
+ stringBytes += rendered.length;
462
+ if (stringBytes > LIMITS.stringLength)
463
+ throw new Error("Safe HTML output exceeds its text limit.");
464
+ return rendered;
465
+ };
466
+ const render = (item, depth) => {
467
+ nodes += 1;
468
+ if (nodes > LIMITS.values || depth > LIMITS.depth)
469
+ throw new Error("Safe HTML output exceeds its structural limit.");
470
+ if (item && typeof item === "object") {
471
+ if (seen.has(item)) throw new Error("Safe HTML output must not contain cycles.");
472
+ seen.add(item);
473
+ }
474
+ if (item === null || item === false) return "";
475
+ if (typeof item === "string" || typeof item === "number")
476
+ return count(escapeOpenEditorCustomBlockHtml(item));
477
+ if (!plainObject(item) || typeof item.tag !== "string" || !SAFE_TAGS.has(item.tag))
478
+ return "";
479
+ const attrs = Object.entries(item.attrs ?? {}).flatMap(([name, raw]) => {
480
+ if (raw === void 0 || !SAFE_ATTRS.has(name)) return [];
481
+ const safe = name === "href" ? safeStaticUrl(String(raw), "navigation") : name === "src" ? safeStaticUrl(String(raw), "asset") : String(raw);
482
+ return safe === null ? [] : [count(` ${name}="${escapeOpenEditorCustomBlockHtml(safe)}"`)];
483
+ }).join("");
484
+ const children = (item.children ?? []).map((child) => render(child, depth + 1)).join("");
485
+ return ["img", "br", "hr"].includes(item.tag) ? `<${item.tag}${attrs}>` : `<${item.tag}${attrs}>${children}</${item.tag}>`;
486
+ };
487
+ return render(value, 0);
623
488
  };
624
489
 
625
- export { OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN, OPENEDITOR_CUSTOM_BLOCK_NODE, conformOpenEditorCustomBlock, createOpenEditorCustomBlockNode, createOpenEditorCustomBlockRegistry, defineOpenEditorCustomBlock, escapeOpenEditorCustomBlockHtml, extractOpenEditorCustomBlockAssetReferences, renderOpenEditorCustomBlockSafeHtml, resolveOpenEditorCustomBlockNode, validateOpenEditorCustomBlockDataValue, validateOpenEditorCustomBlockEnvelope };
490
+ export { OPENEDITOR_CUSTOM_BLOCK_NODE, conformOpenEditorCustomBlock, createOpenEditorCustomBlockNode, createOpenEditorCustomBlockRegistry, defineOpenEditorCustomBlock, escapeOpenEditorCustomBlockHtml, extractOpenEditorCustomBlockAssetReferences, renderOpenEditorCustomBlockSafeHtml, resolveOpenEditorCustomBlockNode, validateOpenEditorCustomBlockEnvelope };
626
491
  //# sourceMappingURL=index.js.map
627
492
  //# sourceMappingURL=index.js.map