smartrte-core 0.2.0 → 1.0.0-beta.1
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/CHANGELOG.md +16 -0
- package/README.md +23 -0
- package/dist/command.d.ts +8 -8
- package/dist/editor.d.ts +32 -0
- package/dist/editor.js +121 -0
- package/dist/foundation/annotations/index.d.ts +2 -0
- package/dist/foundation/annotations/index.js +2 -0
- package/dist/foundation/annotations/range.d.ts +54 -0
- package/dist/foundation/annotations/range.js +158 -0
- package/dist/foundation/annotations/types.d.ts +33 -0
- package/dist/foundation/annotations/types.js +1 -0
- package/dist/foundation/atom/commands.d.ts +11 -0
- package/dist/foundation/atom/commands.js +118 -0
- package/dist/foundation/atom/composition.d.ts +8 -0
- package/dist/foundation/atom/composition.js +22 -0
- package/dist/foundation/atom/declarations.d.ts +2 -0
- package/dist/foundation/atom/declarations.js +17 -0
- package/dist/foundation/atom/formats.d.ts +21 -0
- package/dist/foundation/atom/formats.js +77 -0
- package/dist/foundation/atom/index.d.ts +10 -0
- package/dist/foundation/atom/index.js +10 -0
- package/dist/foundation/atom/lifecycle.d.ts +12 -0
- package/dist/foundation/atom/lifecycle.js +41 -0
- package/dist/foundation/atom/persistence.d.ts +3 -0
- package/dist/foundation/atom/persistence.js +20 -0
- package/dist/foundation/atom/plugin.d.ts +20 -0
- package/dist/foundation/atom/plugin.js +55 -0
- package/dist/foundation/atom/schema.d.ts +3 -0
- package/dist/foundation/atom/schema.js +29 -0
- package/dist/foundation/atom/security.d.ts +10 -0
- package/dist/foundation/atom/security.js +20 -0
- package/dist/foundation/atom/selection.d.ts +3 -0
- package/dist/foundation/atom/selection.js +4 -0
- package/dist/foundation/atom/types.d.ts +47 -0
- package/dist/foundation/atom/types.js +1 -0
- package/dist/foundation/block/commands.d.ts +17 -0
- package/dist/foundation/block/commands.js +197 -0
- package/dist/foundation/block/formats.d.ts +33 -0
- package/dist/foundation/block/formats.js +57 -0
- package/dist/foundation/block/index.d.ts +5 -0
- package/dist/foundation/block/index.js +5 -0
- package/dist/foundation/block/input.d.ts +19 -0
- package/dist/foundation/block/input.js +67 -0
- package/dist/foundation/block/plugin.d.ts +26 -0
- package/dist/foundation/block/plugin.js +79 -0
- package/dist/foundation/block/schema.d.ts +49 -0
- package/dist/foundation/block/schema.js +27 -0
- package/dist/foundation/block/types.d.ts +40 -0
- package/dist/foundation/block/types.js +1 -0
- package/dist/foundation/boundaries.d.ts +11 -0
- package/dist/foundation/boundaries.js +117 -0
- package/dist/foundation/clipboard/detection.d.ts +3 -0
- package/dist/foundation/clipboard/detection.js +29 -0
- package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
- package/dist/foundation/clipboard/diagnostics.js +59 -0
- package/dist/foundation/clipboard/index.d.ts +8 -0
- package/dist/foundation/clipboard/index.js +8 -0
- package/dist/foundation/clipboard/insertion.d.ts +22 -0
- package/dist/foundation/clipboard/insertion.js +210 -0
- package/dist/foundation/clipboard/normalizers.d.ts +8 -0
- package/dist/foundation/clipboard/normalizers.js +229 -0
- package/dist/foundation/clipboard/pipeline.d.ts +13 -0
- package/dist/foundation/clipboard/pipeline.js +58 -0
- package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
- package/dist/foundation/clipboard/sanitizer.js +58 -0
- package/dist/foundation/clipboard/serialization.d.ts +5 -0
- package/dist/foundation/clipboard/serialization.js +33 -0
- package/dist/foundation/clipboard/transfer.d.ts +11 -0
- package/dist/foundation/clipboard/transfer.js +108 -0
- package/dist/foundation/clipboard/types.d.ts +65 -0
- package/dist/foundation/clipboard/types.js +1 -0
- package/dist/foundation/collab/index.d.ts +3 -0
- package/dist/foundation/collab/index.js +3 -0
- package/dist/foundation/collab/rebase.d.ts +28 -0
- package/dist/foundation/collab/rebase.js +82 -0
- package/dist/foundation/collab/transform.d.ts +73 -0
- package/dist/foundation/collab/transform.js +251 -0
- package/dist/foundation/collab/types.d.ts +52 -0
- package/dist/foundation/collab/types.js +1 -0
- package/dist/foundation/comments/fromSelection.d.ts +16 -0
- package/dist/foundation/comments/fromSelection.js +24 -0
- package/dist/foundation/comments/index.d.ts +3 -0
- package/dist/foundation/comments/index.js +3 -0
- package/dist/foundation/comments/rebase.d.ts +31 -0
- package/dist/foundation/comments/rebase.js +43 -0
- package/dist/foundation/comments/types.d.ts +37 -0
- package/dist/foundation/comments/types.js +1 -0
- package/dist/foundation/diff/documentDiff.d.ts +12 -0
- package/dist/foundation/diff/documentDiff.js +148 -0
- package/dist/foundation/diff/index.d.ts +3 -0
- package/dist/foundation/diff/index.js +3 -0
- package/dist/foundation/diff/textDiff.d.ts +12 -0
- package/dist/foundation/diff/textDiff.js +114 -0
- package/dist/foundation/diff/types.d.ts +74 -0
- package/dist/foundation/diff/types.js +1 -0
- package/dist/foundation/editor.d.ts +138 -0
- package/dist/foundation/editor.js +398 -0
- package/dist/foundation/formats/codec.d.ts +45 -0
- package/dist/foundation/formats/codec.js +1 -0
- package/dist/foundation/formats/docx/export.d.ts +4 -0
- package/dist/foundation/formats/docx/export.js +285 -0
- package/dist/foundation/formats/docx/import.d.ts +9 -0
- package/dist/foundation/formats/docx/import.js +37 -0
- package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
- package/dist/foundation/formats/docx/nestingGuard.js +66 -0
- package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
- package/dist/foundation/formats/docx/portableAtoms.js +66 -0
- package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
- package/dist/foundation/formats/docx/styledImport.js +205 -0
- package/dist/foundation/formats/featureCodecs.d.ts +37 -0
- package/dist/foundation/formats/featureCodecs.js +69 -0
- package/dist/foundation/formats/fidelity.d.ts +31 -0
- package/dist/foundation/formats/fidelity.js +123 -0
- package/dist/foundation/formats/index.d.ts +7 -0
- package/dist/foundation/formats/index.js +7 -0
- package/dist/foundation/formats/pdf/format.d.ts +29 -0
- package/dist/foundation/formats/pdf/format.js +190 -0
- package/dist/foundation/history.d.ts +20 -0
- package/dist/foundation/history.js +121 -0
- package/dist/foundation/identity.d.ts +8 -0
- package/dist/foundation/identity.js +30 -0
- package/dist/foundation/index.d.ts +30 -0
- package/dist/foundation/index.js +30 -0
- package/dist/foundation/list/commands.d.ts +28 -0
- package/dist/foundation/list/commands.js +422 -0
- package/dist/foundation/list/formats.d.ts +20 -0
- package/dist/foundation/list/formats.js +795 -0
- package/dist/foundation/list/index.d.ts +8 -0
- package/dist/foundation/list/index.js +8 -0
- package/dist/foundation/list/input.d.ts +35 -0
- package/dist/foundation/list/input.js +320 -0
- package/dist/foundation/list/plugin.d.ts +21 -0
- package/dist/foundation/list/plugin.js +71 -0
- package/dist/foundation/list/presets.d.ts +16 -0
- package/dist/foundation/list/presets.js +21 -0
- package/dist/foundation/list/schema.d.ts +35 -0
- package/dist/foundation/list/schema.js +29 -0
- package/dist/foundation/list/shadow.d.ts +54 -0
- package/dist/foundation/list/shadow.js +75 -0
- package/dist/foundation/list/shadowCorpus.d.ts +10 -0
- package/dist/foundation/list/shadowCorpus.js +41 -0
- package/dist/foundation/list/types.d.ts +50 -0
- package/dist/foundation/list/types.js +1 -0
- package/dist/foundation/mapping.d.ts +11 -0
- package/dist/foundation/mapping.js +40 -0
- package/dist/foundation/marks/canonical.d.ts +9 -0
- package/dist/foundation/marks/canonical.js +72 -0
- package/dist/foundation/marks/commands.d.ts +21 -0
- package/dist/foundation/marks/commands.js +136 -0
- package/dist/foundation/marks/dom.d.ts +2 -0
- package/dist/foundation/marks/dom.js +46 -0
- package/dist/foundation/marks/editor.d.ts +8 -0
- package/dist/foundation/marks/editor.js +50 -0
- package/dist/foundation/marks/formats.d.ts +37 -0
- package/dist/foundation/marks/formats.js +49 -0
- package/dist/foundation/marks/hardBreak.d.ts +8 -0
- package/dist/foundation/marks/hardBreak.js +80 -0
- package/dist/foundation/marks/index.d.ts +11 -0
- package/dist/foundation/marks/index.js +11 -0
- package/dist/foundation/marks/normalization.d.ts +2 -0
- package/dist/foundation/marks/normalization.js +49 -0
- package/dist/foundation/marks/plugin.d.ts +14 -0
- package/dist/foundation/marks/plugin.js +63 -0
- package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
- package/dist/foundation/marks/resolveMarkRun.js +47 -0
- package/dist/foundation/marks/schema.d.ts +167 -0
- package/dist/foundation/marks/schema.js +50 -0
- package/dist/foundation/marks/stored.d.ts +2 -0
- package/dist/foundation/marks/stored.js +31 -0
- package/dist/foundation/marks/types.d.ts +40 -0
- package/dist/foundation/marks/types.js +1 -0
- package/dist/foundation/modelDom.d.ts +38 -0
- package/dist/foundation/modelDom.js +327 -0
- package/dist/foundation/normalization.d.ts +22 -0
- package/dist/foundation/normalization.js +132 -0
- package/dist/foundation/operations.d.ts +28 -0
- package/dist/foundation/operations.js +568 -0
- package/dist/foundation/plugin/builtins.d.ts +10 -0
- package/dist/foundation/plugin/builtins.js +27 -0
- package/dist/foundation/plugin/dispatch.d.ts +33 -0
- package/dist/foundation/plugin/dispatch.js +34 -0
- package/dist/foundation/plugin/index.d.ts +4 -0
- package/dist/foundation/plugin/index.js +4 -0
- package/dist/foundation/plugin/registry.d.ts +36 -0
- package/dist/foundation/plugin/registry.js +115 -0
- package/dist/foundation/plugin/types.d.ts +141 -0
- package/dist/foundation/plugin/types.js +13 -0
- package/dist/foundation/positions.d.ts +20 -0
- package/dist/foundation/positions.js +121 -0
- package/dist/foundation/schema.d.ts +57 -0
- package/dist/foundation/schema.js +371 -0
- package/dist/foundation/schemaBuilder.d.ts +41 -0
- package/dist/foundation/schemaBuilder.js +96 -0
- package/dist/foundation/scope/index.d.ts +2 -0
- package/dist/foundation/scope/index.js +2 -0
- package/dist/foundation/scope/resolveScope.d.ts +13 -0
- package/dist/foundation/scope/resolveScope.js +860 -0
- package/dist/foundation/scope/types.d.ts +122 -0
- package/dist/foundation/scope/types.js +1 -0
- package/dist/foundation/security/urlPolicy.d.ts +21 -0
- package/dist/foundation/security/urlPolicy.js +75 -0
- package/dist/foundation/structural/contentTarget.d.ts +21 -0
- package/dist/foundation/structural/contentTarget.js +51 -0
- package/dist/foundation/structural/index.d.ts +2 -0
- package/dist/foundation/structural/index.js +2 -0
- package/dist/foundation/structural/move.d.ts +7 -0
- package/dist/foundation/structural/move.js +35 -0
- package/dist/foundation/suggestions/index.d.ts +3 -0
- package/dist/foundation/suggestions/index.js +3 -0
- package/dist/foundation/suggestions/inline.d.ts +64 -0
- package/dist/foundation/suggestions/inline.js +116 -0
- package/dist/foundation/suggestions/structural.d.ts +40 -0
- package/dist/foundation/suggestions/structural.js +47 -0
- package/dist/foundation/suggestions/types.d.ts +19 -0
- package/dist/foundation/suggestions/types.js +1 -0
- package/dist/foundation/surface/index.d.ts +3 -0
- package/dist/foundation/surface/index.js +3 -0
- package/dist/foundation/surface/input.d.ts +127 -0
- package/dist/foundation/surface/input.js +1761 -0
- package/dist/foundation/surface/renderer.d.ts +84 -0
- package/dist/foundation/surface/renderer.js +974 -0
- package/dist/foundation/surface/types.d.ts +48 -0
- package/dist/foundation/surface/types.js +1 -0
- package/dist/foundation/table/commands.d.ts +31 -0
- package/dist/foundation/table/commands.js +470 -0
- package/dist/foundation/table/formats.d.ts +26 -0
- package/dist/foundation/table/formats.js +40 -0
- package/dist/foundation/table/grid.d.ts +8 -0
- package/dist/foundation/table/grid.js +178 -0
- package/dist/foundation/table/index.d.ts +6 -0
- package/dist/foundation/table/index.js +6 -0
- package/dist/foundation/table/plugin.d.ts +23 -0
- package/dist/foundation/table/plugin.js +119 -0
- package/dist/foundation/table/schema.d.ts +2 -0
- package/dist/foundation/table/schema.js +38 -0
- package/dist/foundation/table/selection.d.ts +7 -0
- package/dist/foundation/table/selection.js +35 -0
- package/dist/foundation/table/types.d.ts +109 -0
- package/dist/foundation/table/types.js +1 -0
- package/dist/foundation/transactions.d.ts +41 -0
- package/dist/foundation/transactions.js +90 -0
- package/dist/foundation/types.d.ts +261 -0
- package/dist/foundation/types.js +1 -0
- package/dist/foundation/versioning/diffVersions.d.ts +5 -0
- package/dist/foundation/versioning/diffVersions.js +3 -0
- package/dist/foundation/versioning/index.d.ts +2 -0
- package/dist/foundation/versioning/index.js +2 -0
- package/dist/foundation/versioning/types.d.ts +17 -0
- package/dist/foundation/versioning/types.js +1 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +165 -0
- package/dist/html/compatibility.js +7 -1
- package/dist/index.d.ts +1 -14
- package/dist/index.js +9 -14
- package/dist/legacy/index.d.ts +45 -0
- package/dist/legacy/index.js +45 -0
- package/dist/legacyCommands/alignment.d.ts +20 -0
- package/dist/{commands → legacyCommands}/alignment.js +17 -0
- package/dist/legacyCommands/blocks.d.ts +14 -0
- package/dist/legacyCommands/blocks.js +105 -0
- package/dist/legacyCommands/checklist.d.ts +11 -0
- package/dist/legacyCommands/checklist.js +45 -0
- package/dist/legacyCommands/code.d.ts +15 -0
- package/dist/{commands → legacyCommands}/code.js +25 -1
- package/dist/legacyCommands/formula.d.ts +7 -0
- package/dist/legacyCommands/formula.js +67 -0
- package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
- package/dist/legacyCommands/inlineAtoms.js +58 -0
- package/dist/legacyCommands/list.d.ts +38 -0
- package/dist/legacyCommands/list.js +477 -0
- package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
- package/dist/{commands → legacyCommands}/marks.js +45 -11
- package/dist/legacyCommands/media.d.ts +31 -0
- package/dist/legacyCommands/media.js +205 -0
- package/dist/legacyCommands/move.d.ts +14 -0
- package/dist/legacyCommands/move.js +91 -0
- package/dist/legacyCommands/table.d.ts +91 -0
- package/dist/legacyCommands/table.js +547 -0
- package/dist/listPresets.d.ts +12 -0
- package/dist/listPresets.js +6 -0
- package/dist/listScope.d.ts +27 -0
- package/dist/listScope.js +104 -0
- package/dist/markdown/compatibility.d.ts +2 -0
- package/dist/markdown/compatibility.js +94 -2
- package/dist/marks.d.ts +5 -5
- package/dist/model.d.ts +65 -9
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +31 -0
- package/dist/plugins/alignment.d.ts +3 -0
- package/dist/plugins/alignment.js +6 -0
- package/dist/plugins/basicFormatting.d.ts +9 -0
- package/dist/plugins/basicFormatting.js +24 -0
- package/dist/plugins/blockType.d.ts +3 -0
- package/dist/plugins/blockType.js +6 -0
- package/dist/plugins/blockquote.d.ts +3 -0
- package/dist/plugins/blockquote.js +6 -0
- package/dist/plugins/checklist.d.ts +3 -0
- package/dist/plugins/checklist.js +10 -0
- package/dist/plugins/codeBlock.d.ts +3 -0
- package/dist/plugins/codeBlock.js +6 -0
- package/dist/plugins/formula.d.ts +3 -0
- package/dist/plugins/formula.js +9 -0
- package/dist/plugins/list.d.ts +3 -0
- package/dist/plugins/list.js +10 -0
- package/dist/plugins/media.d.ts +3 -0
- package/dist/plugins/media.js +12 -0
- package/dist/plugins/move.d.ts +3 -0
- package/dist/plugins/move.js +9 -0
- package/dist/plugins/table.d.ts +3 -0
- package/dist/plugins/table.js +22 -0
- package/dist/preset.d.ts +12 -0
- package/dist/preset.js +40 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +333 -0
- package/dist/security/urlPolicy.d.ts +1 -17
- package/dist/security/urlPolicy.js +3 -69
- package/dist/selection.d.ts +1 -1
- package/dist/selectionMapping.d.ts +4 -0
- package/dist/selectionMapping.js +222 -0
- package/dist/table.d.ts +3 -3
- package/dist/transaction.d.ts +35 -12
- package/dist/transaction.js +174 -28
- package/dist/tree.d.ts +8 -2
- package/dist/tree.js +44 -0
- package/package.json +27 -8
- package/dist/commands/alignment.d.ts +0 -18
- package/dist/commands/code.d.ts +0 -13
- package/dist/commands/list.d.ts +0 -20
- package/dist/commands/list.js +0 -52
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { assertUniqueNodeIds, cloneNode, createNodeId, isTextNode } from "./identity.js";
|
|
2
|
+
import { repairTableGeometry, validateTableGeometry } from "./table/grid.js";
|
|
3
|
+
import { canonicalMarkOrder, compareMarks } from "./marks/canonical.js";
|
|
4
|
+
import { hardBreakNodeSpec } from "./marks/schema.js";
|
|
5
|
+
import { builtInPlugins } from "./plugin/builtins.js";
|
|
6
|
+
import { createPluginRegistry } from "./plugin/registry.js";
|
|
7
|
+
import { parseContentExpression } from "./schemaBuilder.js";
|
|
8
|
+
export { createSchema, parseContentExpression } from "./schemaBuilder.js";
|
|
9
|
+
const groupForNode = (node, schema) => {
|
|
10
|
+
if (node.type === "unknown") {
|
|
11
|
+
const group = !isTextNode(node) ? node.attrs?.originalGroup : undefined;
|
|
12
|
+
return group === "inline" || group === "block" ? group : "block";
|
|
13
|
+
}
|
|
14
|
+
return schema.nodes[node.type]?.group;
|
|
15
|
+
};
|
|
16
|
+
const matchesName = (node, name, schema) => Boolean(node && (node.type === name || groupForNode(node, schema) === name));
|
|
17
|
+
const matchExpression = (expression, children, start, schema) => {
|
|
18
|
+
if (expression.kind === "name")
|
|
19
|
+
return matchesName(children[start], expression.value, schema)
|
|
20
|
+
? new Set([start + 1])
|
|
21
|
+
: new Set();
|
|
22
|
+
if (expression.kind === "choice") {
|
|
23
|
+
const result = new Set();
|
|
24
|
+
expression.items.forEach((item) => matchExpression(item, children, start, schema).forEach((end) => result.add(end)));
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
if (expression.kind === "sequence") {
|
|
28
|
+
let positions = new Set([start]);
|
|
29
|
+
expression.items.forEach((item) => {
|
|
30
|
+
const next = new Set();
|
|
31
|
+
positions.forEach((position) => matchExpression(item, children, position, schema).forEach((end) => next.add(end)));
|
|
32
|
+
positions = next;
|
|
33
|
+
});
|
|
34
|
+
return positions;
|
|
35
|
+
}
|
|
36
|
+
const result = new Set();
|
|
37
|
+
let frontier = new Set([start]);
|
|
38
|
+
if (expression.min === 0)
|
|
39
|
+
result.add(start);
|
|
40
|
+
for (let count = 1; frontier.size && count <= expression.max && count <= children.length + 1; count += 1) {
|
|
41
|
+
const next = new Set();
|
|
42
|
+
frontier.forEach((position) => matchExpression(expression.item, children, position, schema).forEach((end) => {
|
|
43
|
+
if (end !== position)
|
|
44
|
+
next.add(end);
|
|
45
|
+
}));
|
|
46
|
+
frontier = next;
|
|
47
|
+
if (count >= expression.min)
|
|
48
|
+
frontier.forEach((position) => result.add(position));
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
export const contentMatches = (expression, children, schema) => matchExpression(parseContentExpression(expression), children, 0, schema).has(children.length);
|
|
53
|
+
const stringAttr = { validate: (value) => typeof value === "string" };
|
|
54
|
+
/**
|
|
55
|
+
* Node/mark types no feature-family plugin owns and that must exist
|
|
56
|
+
* regardless of which plugins are registered: the document root, plain
|
|
57
|
+
* text, hard breaks, and `unknown` itself - the disable-safety passthrough
|
|
58
|
+
* node type (see repair() below), which would be a chicken-and-egg problem
|
|
59
|
+
* if any plugin had to own it.
|
|
60
|
+
*/
|
|
61
|
+
export const baseSchema = {
|
|
62
|
+
nodes: [
|
|
63
|
+
{ type: "doc", group: "document", content: "block+" },
|
|
64
|
+
{ type: "text", group: "inline", marks: "_all" },
|
|
65
|
+
hardBreakNodeSpec,
|
|
66
|
+
{ type: "unknown", group: "block", atomic: true, isolating: true, selectable: true, attributes: {
|
|
67
|
+
originalType: { required: true, ...stringAttr },
|
|
68
|
+
originalGroup: { required: true, validate: (v) => v === "block" || v === "inline" },
|
|
69
|
+
raw: { required: true },
|
|
70
|
+
editable: { default: false, validate: (v) => v === false },
|
|
71
|
+
} },
|
|
72
|
+
],
|
|
73
|
+
marks: [
|
|
74
|
+
/**
|
|
75
|
+
* The mark-side equivalent of the "unknown" node type above: a
|
|
76
|
+
* base-registered (not plugin-owned) passthrough for a mark type the
|
|
77
|
+
* current schema doesn't recognize, preserving it verbatim through
|
|
78
|
+
* repair() instead of silently dropping it (see repair()'s
|
|
79
|
+
* mark-filtering loop below and restoreUnknownMarks()).
|
|
80
|
+
*/
|
|
81
|
+
{ type: "unknown-mark", attributes: {
|
|
82
|
+
originalType: { required: true, ...stringAttr },
|
|
83
|
+
originalAttrs: {},
|
|
84
|
+
} },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The "all built-ins" preset, now derived from the plugin registry
|
|
89
|
+
* (plugin/registry.ts, plugin/builtins.ts) rather than a hand-maintained
|
|
90
|
+
* literal - Phase 10's runtime plugin registration applies to the default
|
|
91
|
+
* editor construction too, not just custom plugin lists. Computed once and
|
|
92
|
+
* exported alongside `foundationSchema` so editor.ts can default
|
|
93
|
+
* FoundationEditorOptions.commands/keyboardShortcuts to the same registry
|
|
94
|
+
* instance instead of recomputing it.
|
|
95
|
+
*/
|
|
96
|
+
export const foundationRegistry = createPluginRegistry(builtInPlugins, { baseSchema, schemaVersion: 2 });
|
|
97
|
+
export const foundationSchema = foundationRegistry.schema;
|
|
98
|
+
const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
99
|
+
const validateAttributes = (value, specs, path, errors) => {
|
|
100
|
+
Object.entries(specs || {}).forEach(([name, spec]) => {
|
|
101
|
+
const present = Boolean(value && own(value, name));
|
|
102
|
+
if (spec.required && !present && spec.default === undefined) {
|
|
103
|
+
errors.push({ path: [...path], code: "missing-attribute", message: `Required attribute "${name}" is missing.` });
|
|
104
|
+
}
|
|
105
|
+
else if (present && spec.validate && !spec.validate(value?.[name])) {
|
|
106
|
+
errors.push({ path: [...path], code: "invalid-attribute", message: `Attribute "${name}" is invalid.` });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
export const validate = (document, schema = foundationSchema) => {
|
|
111
|
+
const errors = [];
|
|
112
|
+
const ids = new Set();
|
|
113
|
+
const visit = (node, path, parentSpec) => {
|
|
114
|
+
const spec = schema.nodes[node.type];
|
|
115
|
+
if (!spec) {
|
|
116
|
+
errors.push({ path, code: "unknown-node-type", message: `Node type "${node.type}" is not registered.` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (isTextNode(node)) {
|
|
120
|
+
if (typeof node.text !== "string")
|
|
121
|
+
errors.push({ path, code: "invalid-text", message: "Text content must be a string." });
|
|
122
|
+
node.marks?.forEach((mark) => {
|
|
123
|
+
const markSpec = schema.marks[mark.type];
|
|
124
|
+
if (!markSpec)
|
|
125
|
+
errors.push({ path, code: "unknown-mark", message: `Mark type "${mark.type}" is not registered.` });
|
|
126
|
+
else {
|
|
127
|
+
if (parentSpec?.marks === "" || Array.isArray(parentSpec?.marks) && !parentSpec.marks.includes(mark.type)) {
|
|
128
|
+
errors.push({ path, code: "disallowed-mark", message: `Mark "${mark.type}" is not allowed in "${parentSpec.type}".` });
|
|
129
|
+
}
|
|
130
|
+
validateAttributes(mark.attrs, markSpec.attributes, path, errors);
|
|
131
|
+
const excluded = markSpec.excludes || [];
|
|
132
|
+
if (node.marks?.some((candidate) => candidate !== mark && excluded.includes(candidate.type))) {
|
|
133
|
+
errors.push({ path, code: "excluded-mark", message: `Mark "${mark.type}" excludes another active mark.` });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (node.marks?.some((mark, index) => index > 0 && compareMarks(node.marks[index - 1], mark) > 0)) {
|
|
138
|
+
errors.push({ path, code: "noncanonical-mark-order", message: "Text marks must be stored in canonical type/attribute order." });
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!node.id)
|
|
143
|
+
errors.push({ path, code: "missing-id", message: "Every non-text node requires an id." });
|
|
144
|
+
else if (ids.has(node.id))
|
|
145
|
+
errors.push({ path, code: "duplicate-id", message: `Duplicate node id "${node.id}".` });
|
|
146
|
+
else
|
|
147
|
+
ids.add(node.id);
|
|
148
|
+
validateAttributes(node.attrs, spec.attributes, path, errors);
|
|
149
|
+
const children = node.children || [];
|
|
150
|
+
if (spec.atomic && children.length)
|
|
151
|
+
errors.push({ path, code: "atomic-content", message: "Atomic nodes cannot contain children." });
|
|
152
|
+
if (spec.content && !contentMatches(spec.content, children, schema)) {
|
|
153
|
+
errors.push({ path, code: "invalid-content", message: `Children do not match "${spec.content}".` });
|
|
154
|
+
}
|
|
155
|
+
else if (!spec.content && children.length) {
|
|
156
|
+
errors.push({ path, code: "unexpected-content", message: `Node "${node.type}" does not allow children.` });
|
|
157
|
+
}
|
|
158
|
+
children.forEach((child, index) => visit(child, [...path, index], spec));
|
|
159
|
+
if (node.type === "table")
|
|
160
|
+
validateTableGeometry(node).forEach((issue) => errors.push({
|
|
161
|
+
path, code: `table-${issue.code}`, message: issue.message,
|
|
162
|
+
}));
|
|
163
|
+
};
|
|
164
|
+
visit(document, []);
|
|
165
|
+
return errors;
|
|
166
|
+
};
|
|
167
|
+
const emptyParagraph = () => ({ type: "paragraph", id: createNodeId(), children: [] });
|
|
168
|
+
const inferChildGroup = (parentType) => parentType === "paragraph" || parentType === "heading" ? "inline" : "block";
|
|
169
|
+
export const repair = (input, schema = foundationSchema) => {
|
|
170
|
+
const repairs = [];
|
|
171
|
+
const usedIds = new Set();
|
|
172
|
+
const repairNode = (source, path, expectedGroup) => {
|
|
173
|
+
if (!schema.nodes[source.type]) {
|
|
174
|
+
const unknown = {
|
|
175
|
+
type: "unknown",
|
|
176
|
+
id: !isTextNode(source) && source.id && !usedIds.has(source.id) ? source.id : createNodeId(),
|
|
177
|
+
attrs: { originalType: source.type, originalGroup: expectedGroup === "inline" ? "inline" : "block", raw: cloneNode(source), editable: false },
|
|
178
|
+
};
|
|
179
|
+
usedIds.add(unknown.id);
|
|
180
|
+
repairs.push({ path, code: "preserve-unknown", message: `Preserved unregistered node "${source.type}" as unknown.`, before: source, after: unknown });
|
|
181
|
+
return unknown;
|
|
182
|
+
}
|
|
183
|
+
if (isTextNode(source))
|
|
184
|
+
return { type: "text", text: String(source.text ?? ""), ...(source.marks?.length ? { marks: canonicalMarkOrder(source.marks) } : {}) };
|
|
185
|
+
let id = source.id;
|
|
186
|
+
if (!id || usedIds.has(id)) {
|
|
187
|
+
const before = id;
|
|
188
|
+
id = createNodeId();
|
|
189
|
+
repairs.push({ path, code: before ? "duplicate-id" : "missing-id", message: "Assigned a unique stable node id.", before, after: id });
|
|
190
|
+
}
|
|
191
|
+
usedIds.add(id);
|
|
192
|
+
const spec = schema.nodes[source.type];
|
|
193
|
+
const attrs = { ...(source.attrs || {}) };
|
|
194
|
+
Object.entries(spec.attributes || {}).forEach(([name, attribute]) => {
|
|
195
|
+
const valid = !own(attrs, name) || !attribute.validate || attribute.validate(attrs[name]);
|
|
196
|
+
if ((!own(attrs, name) || !valid) && attribute.default !== undefined) {
|
|
197
|
+
const before = attrs[name];
|
|
198
|
+
attrs[name] = structuredClone(attribute.default);
|
|
199
|
+
repairs.push({ path, code: "repair-attribute", message: `Applied the default for attribute "${name}".`, before, after: attrs[name] });
|
|
200
|
+
}
|
|
201
|
+
else if (!valid && !attribute.required) {
|
|
202
|
+
const before = attrs[name];
|
|
203
|
+
delete attrs[name];
|
|
204
|
+
repairs.push({ path, code: "remove-invalid-attribute", message: `Removed invalid attribute "${name}".`, before });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const group = inferChildGroup(source.type);
|
|
208
|
+
let children = (source.children || []).map((child, index) => repairNode(child, [...path, index], group));
|
|
209
|
+
children = children.map((child, index) => {
|
|
210
|
+
if (!isTextNode(child) || !child.marks?.length)
|
|
211
|
+
return child;
|
|
212
|
+
const accepted = [];
|
|
213
|
+
child.marks.forEach((mark) => {
|
|
214
|
+
const markSpec = schema.marks[mark.type];
|
|
215
|
+
if (!markSpec) {
|
|
216
|
+
// Unlike a mark that's registered but locally disallowed/invalid
|
|
217
|
+
// (handled below, and still correctly dropped), a mark type the
|
|
218
|
+
// schema has never heard of is preserved verbatim as an
|
|
219
|
+
// unknown-mark sentinel - the mark-side counterpart of repair()'s
|
|
220
|
+
// node-side "preserve-unknown" passthrough - rather than deleted.
|
|
221
|
+
const unknown = { type: "unknown-mark", attrs: { originalType: mark.type, ...(mark.attrs ? { originalAttrs: mark.attrs } : {}) } };
|
|
222
|
+
accepted.push(unknown);
|
|
223
|
+
repairs.push({ path: [...path, index], code: "preserve-unknown-mark", message: `Preserved unrecognized mark "${mark.type}" as unknown-mark.`, before: mark, after: unknown });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const allowed = spec.marks !== "" && (!Array.isArray(spec.marks) || spec.marks.includes(mark.type));
|
|
227
|
+
const attributeErrors = [];
|
|
228
|
+
validateAttributes(mark.attrs, markSpec.attributes, [...path, index], attributeErrors);
|
|
229
|
+
const excluded = Boolean(markSpec.excludes?.some((type) => accepted.some((candidate) => candidate.type === type)));
|
|
230
|
+
if (allowed && !attributeErrors.length && !excluded)
|
|
231
|
+
accepted.push(mark);
|
|
232
|
+
else
|
|
233
|
+
repairs.push({ path: [...path, index], code: "remove-invalid-mark", message: `Removed invalid or disallowed mark "${mark.type}".`, before: mark });
|
|
234
|
+
});
|
|
235
|
+
const ordered = canonicalMarkOrder(accepted);
|
|
236
|
+
return ordered.length ? { ...child, marks: ordered } : { type: "text", text: child.text };
|
|
237
|
+
});
|
|
238
|
+
const preserveMisplaced = (node, original, originalGroup) => {
|
|
239
|
+
const unknown = {
|
|
240
|
+
type: "unknown",
|
|
241
|
+
id: createNodeId(),
|
|
242
|
+
attrs: { originalType: original.type, originalGroup, raw: cloneNode(original), editable: false },
|
|
243
|
+
};
|
|
244
|
+
usedIds.add(unknown.id);
|
|
245
|
+
repairs.push({ path, code: "preserve-misplaced", message: `Preserved misplaced "${node.type}" content as unknown.`, before: original, after: unknown });
|
|
246
|
+
return unknown;
|
|
247
|
+
};
|
|
248
|
+
if (source.type === "doc") {
|
|
249
|
+
children = children.map((child, index) => {
|
|
250
|
+
if (groupForNode(child, schema) === "block")
|
|
251
|
+
return child;
|
|
252
|
+
return { type: "paragraph", id: createNodeId(), children: [isTextNode(child) ? child : preserveMisplaced(child, source.children?.[index] || child, "inline")] };
|
|
253
|
+
});
|
|
254
|
+
if (children.length === 0) {
|
|
255
|
+
children = [emptyParagraph()];
|
|
256
|
+
repairs.push({ path, code: "empty-document", message: "Inserted an empty paragraph into the document." });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (source.type === "paragraph" || source.type === "heading") {
|
|
260
|
+
children = children.map((child, index) => groupForNode(child, schema) === "inline"
|
|
261
|
+
? child
|
|
262
|
+
: preserveMisplaced(child, source.children?.[index] || child, "inline"));
|
|
263
|
+
}
|
|
264
|
+
if (source.type === "list" && children.length === 0) {
|
|
265
|
+
children = [{ type: "list_item", id: createNodeId(), children: [emptyParagraph()] }];
|
|
266
|
+
repairs.push({ path, code: "empty-list", message: "Inserted an empty list item." });
|
|
267
|
+
}
|
|
268
|
+
else if (source.type === "list") {
|
|
269
|
+
children = children.map((child) => child.type === "list_item"
|
|
270
|
+
? child
|
|
271
|
+
: {
|
|
272
|
+
type: "list_item",
|
|
273
|
+
id: createNodeId(),
|
|
274
|
+
children: [groupForNode(child, schema) === "block" ? child : { type: "paragraph", id: createNodeId(), children: [child] }],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (source.type === "list_item") {
|
|
278
|
+
if (!children.length) {
|
|
279
|
+
children = [emptyParagraph()];
|
|
280
|
+
repairs.push({ path, code: "empty-list-item", message: "Inserted an empty paragraph into the list item." });
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
children = children.map((child) => groupForNode(child, schema) === "block"
|
|
284
|
+
? child
|
|
285
|
+
: { type: "paragraph", id: createNodeId(), children: [child] });
|
|
286
|
+
}
|
|
287
|
+
// A quote is a block container, so an empty imported quote still needs a
|
|
288
|
+
// real editable owner. Renderer-only <br> projections cannot provide a
|
|
289
|
+
// canonical position for Enter, deletion, or selection mapping.
|
|
290
|
+
if (source.type === "blockquote" && children.length === 0) {
|
|
291
|
+
children = [emptyParagraph()];
|
|
292
|
+
repairs.push({ path, code: "empty-blockquote", message: "Inserted an empty paragraph into the blockquote." });
|
|
293
|
+
}
|
|
294
|
+
let repaired = { type: source.type, id, ...(Object.keys(attrs).length ? { attrs } : {}), ...(spec.content ? { children } : {}) };
|
|
295
|
+
if (source.type === "table") {
|
|
296
|
+
const geometry = repairTableGeometry(repaired);
|
|
297
|
+
repaired = geometry.table;
|
|
298
|
+
geometry.repairs.forEach((issue) => repairs.push({ path, code: `table-${issue.code}`, message: issue.message, before: source, after: repaired }));
|
|
299
|
+
}
|
|
300
|
+
return repaired;
|
|
301
|
+
};
|
|
302
|
+
const root = repairNode(input, [], "document");
|
|
303
|
+
if (isTextNode(root) || root.type !== schema.topNode) {
|
|
304
|
+
const wrapped = { type: "doc", id: createNodeId(), children: [isTextNode(root) ? emptyParagraph() : root] };
|
|
305
|
+
repairs.push({ path: [], code: "repair-root", message: `Wrapped content in top node "${schema.topNode}".`, before: root, after: wrapped });
|
|
306
|
+
return { doc: wrapped, repairs };
|
|
307
|
+
}
|
|
308
|
+
const doc = root;
|
|
309
|
+
assertUniqueNodeIds(doc);
|
|
310
|
+
return { doc, repairs };
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* The reverse of repair()'s "preserve-unknown" case: a node whose type is
|
|
314
|
+
* now recognized by `schema` (e.g. its owning plugin was re-enabled) is
|
|
315
|
+
* restored verbatim from the `unknown` wrapper's `raw` attribute, recursing
|
|
316
|
+
* into the restored subtree in case it too contains a now-restorable
|
|
317
|
+
* `unknown` node. This is the disable-safety round trip's other half -
|
|
318
|
+
* repair() already existed and demotes unrecognized content to `unknown`
|
|
319
|
+
* without any Phase 10 changes; this direction did not exist before.
|
|
320
|
+
*/
|
|
321
|
+
export const restoreUnknownNodes = (document, schema) => {
|
|
322
|
+
const restoreNode = (node) => {
|
|
323
|
+
if (isTextNode(node))
|
|
324
|
+
return node;
|
|
325
|
+
if (node.type === "unknown") {
|
|
326
|
+
const originalType = node.attrs?.originalType;
|
|
327
|
+
const raw = node.attrs?.raw;
|
|
328
|
+
if (typeof originalType === "string" && schema.nodes[originalType] && raw)
|
|
329
|
+
return restoreNode(cloneNode(raw));
|
|
330
|
+
}
|
|
331
|
+
return node.children ? { ...node, children: node.children.map(restoreNode) } : node;
|
|
332
|
+
};
|
|
333
|
+
const restored = restoreNode(document);
|
|
334
|
+
assertUniqueNodeIds(restored);
|
|
335
|
+
return restored;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* The mark-side counterpart of restoreUnknownNodes(): a text node's
|
|
339
|
+
* "unknown-mark" sentinels (see repair()'s mark-filtering loop above) are
|
|
340
|
+
* restored to their real mark type once `schema` recognizes it again,
|
|
341
|
+
* re-sorted into canonical order since the restored type string can sort
|
|
342
|
+
* differently than "unknown-mark" did.
|
|
343
|
+
*/
|
|
344
|
+
export const restoreUnknownMarks = (document, schema) => {
|
|
345
|
+
const restoreNode = (node) => {
|
|
346
|
+
if (isTextNode(node)) {
|
|
347
|
+
if (!node.marks?.length)
|
|
348
|
+
return node;
|
|
349
|
+
const restored = node.marks.map((mark) => {
|
|
350
|
+
if (mark.type !== "unknown-mark")
|
|
351
|
+
return mark;
|
|
352
|
+
const originalType = mark.attrs?.originalType;
|
|
353
|
+
if (typeof originalType !== "string" || !schema.marks[originalType])
|
|
354
|
+
return mark;
|
|
355
|
+
const originalAttrs = mark.attrs?.originalAttrs;
|
|
356
|
+
return { type: originalType, ...(originalAttrs ? { attrs: originalAttrs } : {}) };
|
|
357
|
+
});
|
|
358
|
+
return { ...node, marks: canonicalMarkOrder(restored) };
|
|
359
|
+
}
|
|
360
|
+
return node.children ? { ...node, children: node.children.map(restoreNode) } : node;
|
|
361
|
+
};
|
|
362
|
+
return restoreNode(document);
|
|
363
|
+
};
|
|
364
|
+
export const serializePersistedDocument = (value) => JSON.stringify(value);
|
|
365
|
+
export const parsePersistedDocument = (json) => {
|
|
366
|
+
const parsed = JSON.parse(json);
|
|
367
|
+
if (!Number.isInteger(parsed.schemaVersion) || !Number.isInteger(parsed.revision) || !parsed.document) {
|
|
368
|
+
throw new Error("Invalid persisted editor document envelope.");
|
|
369
|
+
}
|
|
370
|
+
return parsed;
|
|
371
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MarkSpec, NodeSpec } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure schema-shape construction, extracted out of schema.ts so it has zero
|
|
4
|
+
* dependency on plugin/ - plugin/registry.ts needs createSchema, and
|
|
5
|
+
* schema.ts's own foundationSchema is computed via the plugin registry
|
|
6
|
+
* (plugin/builtins.ts), so createSchema could not stay defined in schema.ts
|
|
7
|
+
* without a schema.ts <-> plugin/ import cycle. schema.ts re-exports
|
|
8
|
+
* everything here so no existing external import site needs to change.
|
|
9
|
+
*/
|
|
10
|
+
export type Expression = {
|
|
11
|
+
kind: "name";
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "sequence";
|
|
15
|
+
items: Expression[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: "choice";
|
|
18
|
+
items: Expression[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: "repeat";
|
|
21
|
+
item: Expression;
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
};
|
|
25
|
+
export declare const parseContentExpression: (source: string) => Expression;
|
|
26
|
+
export interface SchemaContribution {
|
|
27
|
+
nodes?: readonly NodeSpec[];
|
|
28
|
+
marks?: readonly MarkSpec[];
|
|
29
|
+
}
|
|
30
|
+
export declare const createSchema: (options: {
|
|
31
|
+
nodes: readonly NodeSpec[];
|
|
32
|
+
marks?: readonly MarkSpec[];
|
|
33
|
+
topNode?: string;
|
|
34
|
+
version: number;
|
|
35
|
+
extensions?: readonly SchemaContribution[];
|
|
36
|
+
}) => Readonly<{
|
|
37
|
+
nodes: Readonly<Record<string, Readonly<NodeSpec>>>;
|
|
38
|
+
marks: Readonly<Record<string, Readonly<MarkSpec>>>;
|
|
39
|
+
topNode: string;
|
|
40
|
+
version: number;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const tokenize = (source) => {
|
|
2
|
+
const tokens = source.match(/[A-Za-z_][A-Za-z0-9_-]*|[()|+*?]/g) || [];
|
|
3
|
+
if (tokens.join("").length !== source.replace(/\s+/g, "").length) {
|
|
4
|
+
throw new Error(`Invalid content expression "${source}".`);
|
|
5
|
+
}
|
|
6
|
+
return tokens;
|
|
7
|
+
};
|
|
8
|
+
export const parseContentExpression = (source) => {
|
|
9
|
+
const tokens = tokenize(source);
|
|
10
|
+
let cursor = 0;
|
|
11
|
+
const parseChoice = () => {
|
|
12
|
+
const items = [parseSequence()];
|
|
13
|
+
while (tokens[cursor] === "|") {
|
|
14
|
+
cursor += 1;
|
|
15
|
+
items.push(parseSequence());
|
|
16
|
+
}
|
|
17
|
+
return items.length === 1 ? items[0] : { kind: "choice", items };
|
|
18
|
+
};
|
|
19
|
+
const parseSequence = () => {
|
|
20
|
+
const items = [];
|
|
21
|
+
while (cursor < tokens.length && tokens[cursor] !== ")" && tokens[cursor] !== "|") {
|
|
22
|
+
let item;
|
|
23
|
+
if (tokens[cursor] === "(") {
|
|
24
|
+
cursor += 1;
|
|
25
|
+
item = parseChoice();
|
|
26
|
+
if (tokens[cursor] !== ")")
|
|
27
|
+
throw new Error(`Unclosed group in content expression "${source}".`);
|
|
28
|
+
cursor += 1;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const value = tokens[cursor++];
|
|
32
|
+
if (!value || /[+*?()|]/.test(value))
|
|
33
|
+
throw new Error(`Expected node name in content expression "${source}".`);
|
|
34
|
+
item = { kind: "name", value };
|
|
35
|
+
}
|
|
36
|
+
const quantifier = tokens[cursor];
|
|
37
|
+
if (quantifier === "+" || quantifier === "*" || quantifier === "?") {
|
|
38
|
+
cursor += 1;
|
|
39
|
+
item = {
|
|
40
|
+
kind: "repeat",
|
|
41
|
+
item,
|
|
42
|
+
min: quantifier === "+" ? 1 : 0,
|
|
43
|
+
max: quantifier === "?" ? 1 : Number.POSITIVE_INFINITY,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
items.push(item);
|
|
47
|
+
}
|
|
48
|
+
return items.length === 1 ? items[0] : { kind: "sequence", items };
|
|
49
|
+
};
|
|
50
|
+
const expression = parseChoice();
|
|
51
|
+
if (cursor !== tokens.length)
|
|
52
|
+
throw new Error(`Unexpected token in content expression "${source}".`);
|
|
53
|
+
return expression;
|
|
54
|
+
};
|
|
55
|
+
const freezeSpec = (spec) => {
|
|
56
|
+
const attributes = spec.attributes
|
|
57
|
+
? Object.freeze(Object.fromEntries(Object.entries(spec.attributes).map(([name, attribute]) => [name, Object.freeze({ ...attribute })])))
|
|
58
|
+
: undefined;
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
...spec,
|
|
61
|
+
...(attributes ? { attributes } : {}),
|
|
62
|
+
...(Array.isArray(spec.marks) ? { marks: Object.freeze([...spec.marks]) } : {}),
|
|
63
|
+
...(Array.isArray(spec.excludes) ? { excludes: Object.freeze([...spec.excludes]) } : {}),
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
export const createSchema = (options) => {
|
|
67
|
+
const nodes = {};
|
|
68
|
+
const marks = {};
|
|
69
|
+
const addNode = (spec) => {
|
|
70
|
+
if (nodes[spec.type])
|
|
71
|
+
throw new Error(`Duplicate node type "${spec.type}".`);
|
|
72
|
+
if (spec.content)
|
|
73
|
+
parseContentExpression(spec.content);
|
|
74
|
+
nodes[spec.type] = freezeSpec({ ...spec });
|
|
75
|
+
};
|
|
76
|
+
const addMark = (spec) => {
|
|
77
|
+
if (marks[spec.type])
|
|
78
|
+
throw new Error(`Duplicate mark type "${spec.type}".`);
|
|
79
|
+
marks[spec.type] = freezeSpec({ ...spec });
|
|
80
|
+
};
|
|
81
|
+
options.nodes.forEach(addNode);
|
|
82
|
+
options.marks?.forEach(addMark);
|
|
83
|
+
options.extensions?.forEach((extension) => {
|
|
84
|
+
extension.nodes?.forEach(addNode);
|
|
85
|
+
extension.marks?.forEach(addMark);
|
|
86
|
+
});
|
|
87
|
+
const topNode = options.topNode || "doc";
|
|
88
|
+
if (!nodes[topNode] || nodes[topNode].group !== "document")
|
|
89
|
+
throw new Error(`Invalid top node "${topNode}".`);
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
nodes: Object.freeze(nodes),
|
|
92
|
+
marks: Object.freeze(marks),
|
|
93
|
+
topNode,
|
|
94
|
+
version: options.version,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SmartDocument, SmartSchema, SmartSelection } from "../types.js";
|
|
2
|
+
import type { ScopeIndex, ScopeRequest, ScopeResult, PositionLookup } from "./types.js";
|
|
3
|
+
export declare class FoundationScopeIndex implements ScopeIndex {
|
|
4
|
+
private document;
|
|
5
|
+
private schema;
|
|
6
|
+
private index;
|
|
7
|
+
private update;
|
|
8
|
+
resolve(document: SmartDocument, selection: SmartSelection, request: ScopeRequest, schema: SmartSchema): ScopeResult;
|
|
9
|
+
positions(document: SmartDocument, schema: SmartSchema): PositionLookup;
|
|
10
|
+
get liveNodeCount(): number;
|
|
11
|
+
}
|
|
12
|
+
export declare const createScopeIndex: () => ScopeIndex;
|
|
13
|
+
export declare const resolveScope: (document: SmartDocument, selection: SmartSelection, request: ScopeRequest, schema: SmartSchema) => ScopeResult;
|