@openeditor/custom-block 0.0.46 → 0.0.48
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 +23 -41
- package/dist/editor.d.ts +9 -1
- package/dist/editor.js +27 -10
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +47 -154
- package/dist/index.js +409 -544
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
8
|
-
var
|
|
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.
|
|
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))
|
|
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 >
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
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 >
|
|
28
|
-
|
|
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 >
|
|
33
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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))
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
443
|
-
|
|
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" ?
|
|
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
|
-
|
|
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
|
|
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
|
|
474
|
-
|
|
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")
|
|
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
|
-
|
|
491
|
-
|
|
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
|
|
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
|
|
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)
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
+
return [
|
|
375
|
+
{
|
|
376
|
+
path: "$",
|
|
377
|
+
message: error instanceof Error ? error.message : "Custom block conformance failed."
|
|
378
|
+
}
|
|
379
|
+
];
|
|
621
380
|
}
|
|
622
|
-
|
|
381
|
+
};
|
|
382
|
+
var escapeOpenEditorCustomBlockHtml = (value) => String(value).replace(
|
|
383
|
+
/[&<>"']/g,
|
|
384
|
+
(character) => ({
|
|
385
|
+
"&": "&",
|
|
386
|
+
"<": "<",
|
|
387
|
+
">": ">",
|
|
388
|
+
'"': """,
|
|
389
|
+
"'": "'"
|
|
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 {
|
|
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
|