@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 +60 -0
- package/dist/editor.d.ts +49 -0
- package/dist/editor.js +85 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +627 -0
- package/dist/index.js.map +1 -0
- package/dist/viewer.d.ts +37 -0
- package/dist/viewer.js +45 -0
- package/dist/viewer.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { validateDocument } from '@openeditor/core';
|
|
2
|
+
import { defaultDocumentContract } from '@openeditor/extensions';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var OPENEDITOR_CUSTOM_BLOCK_NODE = "customBlock";
|
|
6
|
+
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}$/;
|
|
9
|
+
var plainObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
var positiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
11
|
+
var clone = (value) => structuredClone(value);
|
|
12
|
+
var deepFreeze = (value) => {
|
|
13
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
14
|
+
Object.freeze(value);
|
|
15
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
var VALIDATION_LIMITS = { depth: 32, values: 1e4, stringLength: 1e6, arrayLength: 1e4, objectKeys: 1e4 };
|
|
20
|
+
var inspectJson = (value, path = "$.data", depth = 0, budget = { values: 0 }) => {
|
|
21
|
+
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 [];
|
|
26
|
+
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));
|
|
29
|
+
}
|
|
30
|
+
if (plainObject(value)) {
|
|
31
|
+
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));
|
|
34
|
+
}
|
|
35
|
+
return [{ path, message: "Custom block data must contain only JSON values." }];
|
|
36
|
+
};
|
|
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;
|
|
299
|
+
};
|
|
300
|
+
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 });
|
|
313
|
+
return Object.freeze(definition);
|
|
314
|
+
};
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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;
|
|
329
|
+
}
|
|
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
|
+
};
|
|
363
|
+
var createOpenEditorCustomBlockRegistry = (definitions, options = {}) => {
|
|
364
|
+
const byId = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const definition of definitions) {
|
|
366
|
+
try {
|
|
367
|
+
assertPortableSchema(definition.dataSchema);
|
|
368
|
+
assertPortableConstraints(definition.constraints);
|
|
369
|
+
} catch {
|
|
370
|
+
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}".`);
|
|
374
|
+
byId.set(definition.id, definition);
|
|
375
|
+
}
|
|
376
|
+
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;
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return { status: "invalid", node, blockId: envelope.blockId, diagnostics: [{ path: "$.data", message: error instanceof Error ? error.message : "Migration failed." }] };
|
|
396
|
+
}
|
|
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 };
|
|
409
|
+
};
|
|
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] };
|
|
424
|
+
}
|
|
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 };
|
|
439
|
+
};
|
|
440
|
+
return { tag: "div", children: document.content.map((node) => renderNode(node)) };
|
|
441
|
+
};
|
|
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 "";
|
|
447
|
+
if (node.type === OPENEDITOR_CUSTOM_BLOCK_NODE) {
|
|
448
|
+
const nested = resolve(node);
|
|
449
|
+
return nested.status === "ready" ? exportNestedText(nested) : "";
|
|
450
|
+
}
|
|
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
|
+
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");
|
|
457
|
+
};
|
|
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
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const exportNestedText = (result) => {
|
|
474
|
+
try {
|
|
475
|
+
return exportReadyText(result);
|
|
476
|
+
} catch {
|
|
477
|
+
return fallbackLabel(result.node, "invalid");
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
definitions: Object.freeze([...definitions]),
|
|
482
|
+
manifests: Object.freeze(definitions.map((item) => item.manifest)),
|
|
483
|
+
get: (id) => byId.get(id),
|
|
484
|
+
isEnabled: (id) => byId.has(id) && !disabled.has(id),
|
|
485
|
+
resolve,
|
|
486
|
+
toHtml: (node) => {
|
|
487
|
+
const result = resolve(node);
|
|
488
|
+
if (result.status !== "ready") return fallback(node, result);
|
|
489
|
+
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." }] });
|
|
492
|
+
} catch {
|
|
493
|
+
return fallback(node, { status: "invalid", blockId: result.definition.id});
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
toText: (node) => {
|
|
497
|
+
const result = resolve(node);
|
|
498
|
+
if (result.status !== "ready") return fallbackLabel(node, result.status);
|
|
499
|
+
try {
|
|
500
|
+
return exportReadyText(result);
|
|
501
|
+
} catch {
|
|
502
|
+
return fallbackLabel(node, "invalid");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
var createOpenEditorCustomBlockNode = (registry, id, data, options = {}) => {
|
|
508
|
+
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);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
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;
|
|
607
|
+
};
|
|
608
|
+
var conformOpenEditorCustomBlock = (definition) => {
|
|
609
|
+
const diagnostics = [];
|
|
610
|
+
try {
|
|
611
|
+
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);
|
|
616
|
+
const html = registry.toHtml(node);
|
|
617
|
+
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." });
|
|
619
|
+
} catch (error) {
|
|
620
|
+
diagnostics.push({ path: "$", message: error instanceof Error ? error.message : "Custom block conformance failed." });
|
|
621
|
+
}
|
|
622
|
+
return diagnostics;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
export { OPENEDITOR_CUSTOM_BLOCK_ASSET_ID_PATTERN, OPENEDITOR_CUSTOM_BLOCK_NODE, conformOpenEditorCustomBlock, createOpenEditorCustomBlockNode, createOpenEditorCustomBlockRegistry, defineOpenEditorCustomBlock, escapeOpenEditorCustomBlockHtml, extractOpenEditorCustomBlockAssetReferences, renderOpenEditorCustomBlockSafeHtml, resolveOpenEditorCustomBlockNode, validateOpenEditorCustomBlockDataValue, validateOpenEditorCustomBlockEnvelope };
|
|
626
|
+
//# sourceMappingURL=index.js.map
|
|
627
|
+
//# sourceMappingURL=index.js.map
|