smartrte-core 0.2.1 → 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 +12 -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 +28 -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,860 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { occupancyGridFor } from "../table/index.js";
|
|
3
|
+
const pathKey = (path) => path.join("/");
|
|
4
|
+
const copyPos = (pos) => ({ path: [...pos.path], offset: pos.offset });
|
|
5
|
+
const copyRange = (range) => ({ from: copyPos(range.from), to: copyPos(range.to) });
|
|
6
|
+
const typeKey = (type) => type.toLowerCase().replace(/-/g, "_");
|
|
7
|
+
const fallbackRole = (type) => {
|
|
8
|
+
const key = typeKey(type);
|
|
9
|
+
if (key === "list")
|
|
10
|
+
return "list";
|
|
11
|
+
if (key === "list_item" || key === "listitem")
|
|
12
|
+
return "list-item";
|
|
13
|
+
if (key === "table")
|
|
14
|
+
return "table";
|
|
15
|
+
if (key === "table_row" || key === "tablerow")
|
|
16
|
+
return "table-row";
|
|
17
|
+
if (["table_cell", "table_header", "table_header_cell", "tablecell"].includes(key))
|
|
18
|
+
return "table-cell";
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
/** The single semantic-role resolution boundary: explicit role, then legacy type fallback. */
|
|
22
|
+
const roleOf = (node, schema) => schema.nodes[node.type]?.semanticRole ?? fallbackRole(node.type);
|
|
23
|
+
const isList = (entry) => entry.role === "list";
|
|
24
|
+
const isListItem = (entry) => entry.role === "list-item";
|
|
25
|
+
const isTable = (entry) => entry.role === "table";
|
|
26
|
+
const isTableRow = (entry) => entry.role === "table-row";
|
|
27
|
+
const isTableCell = (entry) => entry.role === "table-cell";
|
|
28
|
+
const schemaGroup = (node, schema) => {
|
|
29
|
+
if (node.type === "unknown") {
|
|
30
|
+
return node.attrs?.originalGroup === "inline" ? "inline" : "block";
|
|
31
|
+
}
|
|
32
|
+
return schema.nodes[node.type]?.group ?? "block";
|
|
33
|
+
};
|
|
34
|
+
const contentAcceptsInline = (node, schema) => {
|
|
35
|
+
const expression = schema.nodes[node.type]?.content;
|
|
36
|
+
return Boolean(expression && /(^|[^A-Za-z0-9_-])(?:inline|text)([^A-Za-z0-9_-]|$)/.test(expression));
|
|
37
|
+
};
|
|
38
|
+
const buildIndex = (document, schema) => {
|
|
39
|
+
const entries = [];
|
|
40
|
+
const byId = new Map();
|
|
41
|
+
const byPath = new Map();
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
let preorder = 0;
|
|
44
|
+
const visit = (node, path, parent, index) => {
|
|
45
|
+
if (node.attrs?.["data-smart-ui"] === true) {
|
|
46
|
+
throw new Error("Editor UI nodes may not appear in the canonical model.");
|
|
47
|
+
}
|
|
48
|
+
if (byId.has(node.id))
|
|
49
|
+
throw new Error(`Duplicate node id "${node.id}".`);
|
|
50
|
+
const spec = schema.nodes[node.type];
|
|
51
|
+
const group = schemaGroup(node, schema);
|
|
52
|
+
const atomic = Boolean(spec?.atomic || node.type === "unknown");
|
|
53
|
+
const entry = {
|
|
54
|
+
node,
|
|
55
|
+
path: [...path],
|
|
56
|
+
parent,
|
|
57
|
+
children: [],
|
|
58
|
+
index,
|
|
59
|
+
depth: path.length,
|
|
60
|
+
preorder: preorder++,
|
|
61
|
+
open: cursor++,
|
|
62
|
+
contentStart: cursor,
|
|
63
|
+
contentEnd: cursor,
|
|
64
|
+
end: cursor,
|
|
65
|
+
inlineOwner: !atomic && contentAcceptsInline(node, schema),
|
|
66
|
+
group,
|
|
67
|
+
atomic,
|
|
68
|
+
isolating: Boolean(spec?.isolating),
|
|
69
|
+
role: roleOf(node, schema),
|
|
70
|
+
inlineChildren: [],
|
|
71
|
+
};
|
|
72
|
+
entries.push(entry);
|
|
73
|
+
byId.set(node.id, entry);
|
|
74
|
+
byPath.set(pathKey(path), entry);
|
|
75
|
+
if (entry.inlineOwner) {
|
|
76
|
+
let inlineOffset = 0;
|
|
77
|
+
for (const child of node.children || []) {
|
|
78
|
+
const width = isTextNode(child) ? child.text.length : 1;
|
|
79
|
+
entry.inlineChildren.push({ node: child, from: inlineOffset, to: inlineOffset + width });
|
|
80
|
+
if (isTextNode(child)) {
|
|
81
|
+
cursor += width;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (!schema.nodes[child.type]?.atomic && child.type !== "unknown") {
|
|
85
|
+
throw new Error(`Inline child "${child.type}" must be atomic.`);
|
|
86
|
+
}
|
|
87
|
+
const atomStart = cursor;
|
|
88
|
+
const atom = visit(child, [...path, entry.inlineChildren.length - 1], entry, entry.inlineChildren.length - 1);
|
|
89
|
+
atom.open = atomStart;
|
|
90
|
+
atom.contentStart = atomStart;
|
|
91
|
+
atom.contentEnd = atomStart + 1;
|
|
92
|
+
atom.end = atomStart + 1;
|
|
93
|
+
cursor = atomStart + 1;
|
|
94
|
+
}
|
|
95
|
+
inlineOffset += width;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (!atomic) {
|
|
99
|
+
(node.children || []).forEach((child, childIndex) => {
|
|
100
|
+
if (!isTextNode(child))
|
|
101
|
+
entry.children.push(visit(child, [...path, childIndex], entry, childIndex));
|
|
102
|
+
else
|
|
103
|
+
cursor += child.text.length;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
entry.contentEnd = cursor;
|
|
107
|
+
entry.end = cursor++;
|
|
108
|
+
return entry;
|
|
109
|
+
};
|
|
110
|
+
const root = visit(document, [], null, 0);
|
|
111
|
+
const hasBlockDescendant = (entry) => entry.children.some((child) => (child.group === "block" && !isListItem(child) && !isTableRow(child) && !isTableCell(child)) || hasBlockDescendant(child));
|
|
112
|
+
const blocks = entries.filter((entry) => entry.group === "block"
|
|
113
|
+
&& !isListItem(entry)
|
|
114
|
+
&& !isTableRow(entry)
|
|
115
|
+
&& !isTableCell(entry)
|
|
116
|
+
&& (entry.atomic || !hasBlockDescendant(entry)));
|
|
117
|
+
const atoms = entries.filter((entry) => entry.atomic);
|
|
118
|
+
return { root, entries, byId, byPath, blocks, blockSet: new Set(blocks), atoms };
|
|
119
|
+
};
|
|
120
|
+
/** Reuses every cached entry/map when node identity says topology is unchanged. */
|
|
121
|
+
const refreshIndex = (index, document, schema) => {
|
|
122
|
+
const update = (entry, node) => {
|
|
123
|
+
if (entry.node === node)
|
|
124
|
+
return true;
|
|
125
|
+
if (entry.node.id !== node.id || entry.node.type !== node.type)
|
|
126
|
+
return false;
|
|
127
|
+
entry.node = node;
|
|
128
|
+
entry.isolating = Boolean(schema.nodes[node.type]?.isolating);
|
|
129
|
+
entry.role = roleOf(node, schema);
|
|
130
|
+
if (entry.inlineOwner) {
|
|
131
|
+
const beforeAtoms = entry.inlineChildren.filter((child) => !isTextNode(child.node)).map((child) => child.node.id);
|
|
132
|
+
const afterAtoms = (node.children || []).filter((child) => !isTextNode(child)).map((child) => child.id);
|
|
133
|
+
if (beforeAtoms.length !== afterAtoms.length || beforeAtoms.some((id, position) => id !== afterAtoms[position]))
|
|
134
|
+
return false;
|
|
135
|
+
let offset = 0;
|
|
136
|
+
entry.inlineChildren = (node.children || []).map((child) => {
|
|
137
|
+
const from = offset;
|
|
138
|
+
offset += isTextNode(child) ? child.text.length : 1;
|
|
139
|
+
if (!isTextNode(child)) {
|
|
140
|
+
const atom = index.byId.get(child.id);
|
|
141
|
+
if (!atom || !update(atom, child))
|
|
142
|
+
return { node: child, from, to: offset };
|
|
143
|
+
}
|
|
144
|
+
return { node: child, from, to: offset };
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
const children = (node.children || []).filter((child) => !isTextNode(child));
|
|
149
|
+
if (children.length !== entry.children.length)
|
|
150
|
+
return false;
|
|
151
|
+
for (let position = 0; position < children.length; position += 1) {
|
|
152
|
+
if (children[position].id !== entry.children[position].node.id || !update(entry.children[position], children[position]))
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
if (!update(index.root, document))
|
|
158
|
+
return false;
|
|
159
|
+
let cursor = 0;
|
|
160
|
+
const reflow = (entry) => {
|
|
161
|
+
entry.open = cursor++;
|
|
162
|
+
entry.contentStart = cursor;
|
|
163
|
+
if (entry.inlineOwner) {
|
|
164
|
+
entry.inlineChildren.forEach((child) => {
|
|
165
|
+
if (isTextNode(child.node))
|
|
166
|
+
cursor += child.node.text.length;
|
|
167
|
+
else {
|
|
168
|
+
const atom = index.byId.get(child.node.id);
|
|
169
|
+
if (atom) {
|
|
170
|
+
atom.open = cursor;
|
|
171
|
+
atom.contentStart = cursor;
|
|
172
|
+
atom.contentEnd = cursor + 1;
|
|
173
|
+
atom.end = cursor + 1;
|
|
174
|
+
}
|
|
175
|
+
cursor += 1;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else if (!entry.atomic)
|
|
180
|
+
entry.children.forEach(reflow);
|
|
181
|
+
entry.contentEnd = cursor;
|
|
182
|
+
entry.end = cursor++;
|
|
183
|
+
};
|
|
184
|
+
reflow(index.root);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
const limitFor = (entry) => entry.inlineOwner
|
|
188
|
+
? entry.inlineChildren.reduce((size, child) => Math.max(size, child.to), 0)
|
|
189
|
+
: entry.children.length;
|
|
190
|
+
const nodeAroundInlineOffset = (entry, offset, before) => {
|
|
191
|
+
const children = entry.inlineChildren;
|
|
192
|
+
if (before) {
|
|
193
|
+
for (let index = children.length - 1; index >= 0; index -= 1)
|
|
194
|
+
if (children[index].to <= offset)
|
|
195
|
+
return children[index].node;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
for (const child of children)
|
|
199
|
+
if (child.from >= offset)
|
|
200
|
+
return child.node;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
/** The only place in the scope runtime that interprets stored SmartPos fields. */
|
|
205
|
+
const resolveEndpoint = (index, pos) => {
|
|
206
|
+
const entry = index.byPath.get(pathKey(pos.path));
|
|
207
|
+
if (!entry)
|
|
208
|
+
throw new Error("A SmartPos path must resolve to an owning non-text node.");
|
|
209
|
+
if (entry.atomic)
|
|
210
|
+
throw new Error("A position cannot resolve inside an atomic node; address its owning container boundary.");
|
|
211
|
+
const limit = limitFor(entry);
|
|
212
|
+
if (!Number.isInteger(pos.offset) || pos.offset < 0 || pos.offset > limit) {
|
|
213
|
+
throw new Error(`Position offset ${pos.offset} is outside 0..${limit}.`);
|
|
214
|
+
}
|
|
215
|
+
const ancestors = [];
|
|
216
|
+
let cursor = entry;
|
|
217
|
+
while (cursor) {
|
|
218
|
+
ancestors.unshift(cursor.node);
|
|
219
|
+
cursor = cursor.parent;
|
|
220
|
+
}
|
|
221
|
+
const rank = entry.inlineOwner
|
|
222
|
+
? entry.contentStart + pos.offset
|
|
223
|
+
: pos.offset < entry.children.length ? entry.children[pos.offset].open : entry.contentEnd;
|
|
224
|
+
const resolved = {
|
|
225
|
+
pos: copyPos(pos),
|
|
226
|
+
kind: entry.inlineOwner ? "inline" : "structural",
|
|
227
|
+
nodeId: entry.node.id,
|
|
228
|
+
parent: entry.node,
|
|
229
|
+
depth: entry.depth,
|
|
230
|
+
affinity: "backward",
|
|
231
|
+
ancestors,
|
|
232
|
+
nodeBefore: entry.inlineOwner
|
|
233
|
+
? nodeAroundInlineOffset(entry, pos.offset, true)
|
|
234
|
+
: pos.offset > 0 ? entry.node.children?.[pos.offset - 1] ?? null : null,
|
|
235
|
+
nodeAfter: entry.inlineOwner
|
|
236
|
+
? nodeAroundInlineOffset(entry, pos.offset, false)
|
|
237
|
+
: pos.offset < limit ? entry.node.children?.[pos.offset] ?? null : null,
|
|
238
|
+
atStart: pos.offset === 0,
|
|
239
|
+
atEnd: pos.offset === limit,
|
|
240
|
+
indexAt(depth) {
|
|
241
|
+
if (!Number.isInteger(depth) || depth < 0 || depth >= pos.path.length)
|
|
242
|
+
throw new Error("Position depth is out of range.");
|
|
243
|
+
return pos.path[depth];
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
cursor = entry;
|
|
247
|
+
let isolation = null;
|
|
248
|
+
while (cursor) {
|
|
249
|
+
if (cursor.isolating) {
|
|
250
|
+
isolation = cursor;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
cursor = cursor.parent;
|
|
254
|
+
}
|
|
255
|
+
return { resolved, entry, rank, isolation };
|
|
256
|
+
};
|
|
257
|
+
const stablePosKey = (pos) => `${pos.path.map((part) => String(part).padStart(8, "0")).join(".")}:${String(pos.offset).padStart(12, "0")}`;
|
|
258
|
+
const normalizeSelection = (index, selection) => {
|
|
259
|
+
const anchor = resolveEndpoint(index, selection.anchor);
|
|
260
|
+
const head = resolveEndpoint(index, selection.head);
|
|
261
|
+
const anchorFirst = anchor.rank < head.rank || (anchor.rank === head.rank && stablePosKey(anchor.resolved.pos) <= stablePosKey(head.resolved.pos));
|
|
262
|
+
return anchorFirst
|
|
263
|
+
? { from: anchor, to: head, range: { from: copyPos(anchor.resolved.pos), to: copyPos(head.resolved.pos) } }
|
|
264
|
+
: { from: head, to: anchor, range: { from: copyPos(head.resolved.pos), to: copyPos(anchor.resolved.pos) } };
|
|
265
|
+
};
|
|
266
|
+
const atomicParentSpan = (entry) => {
|
|
267
|
+
if (!entry.atomic || !entry.parent)
|
|
268
|
+
return null;
|
|
269
|
+
if (entry.parent.inlineOwner) {
|
|
270
|
+
const span = entry.parent.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === entry.node.id);
|
|
271
|
+
return span ? { parent: entry.parent, from: span.from, to: span.to } : null;
|
|
272
|
+
}
|
|
273
|
+
return { parent: entry.parent, from: entry.index, to: entry.index + 1 };
|
|
274
|
+
};
|
|
275
|
+
const entryStartPos = (entry) => {
|
|
276
|
+
const atom = atomicParentSpan(entry);
|
|
277
|
+
return atom ? { path: [...atom.parent.path], offset: atom.from } : { path: [...entry.path], offset: 0 };
|
|
278
|
+
};
|
|
279
|
+
const entryEndPos = (entry) => {
|
|
280
|
+
const atom = atomicParentSpan(entry);
|
|
281
|
+
return atom ? { path: [...atom.parent.path], offset: atom.to } : { path: [...entry.path], offset: limitFor(entry) };
|
|
282
|
+
};
|
|
283
|
+
const afterEntryPos = (entry) => entry.parent
|
|
284
|
+
? { path: [...entry.parent.path], offset: entry.index + 1 }
|
|
285
|
+
: entryEndPos(entry);
|
|
286
|
+
const isAncestor = (ancestor, descendant) => {
|
|
287
|
+
let cursor = descendant;
|
|
288
|
+
while (cursor) {
|
|
289
|
+
if (cursor === ancestor)
|
|
290
|
+
return true;
|
|
291
|
+
cursor = cursor.parent;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
};
|
|
295
|
+
const outermostIsolation = (entry) => {
|
|
296
|
+
let result = entry;
|
|
297
|
+
let cursor = entry.parent;
|
|
298
|
+
while (cursor) {
|
|
299
|
+
if (cursor.isolating)
|
|
300
|
+
result = cursor;
|
|
301
|
+
cursor = cursor.parent;
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
const spansIsolating = (index, from, to) => {
|
|
306
|
+
if (from.isolation?.node.id !== to.isolation?.node.id)
|
|
307
|
+
return true;
|
|
308
|
+
return index.entries.some((entry) => entry.isolating
|
|
309
|
+
&& entry.open > from.rank
|
|
310
|
+
&& entry.end < to.rank
|
|
311
|
+
&& entry !== from.isolation);
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Shared isolation clamp. To preserve the stronger reverse-selection invariant,
|
|
315
|
+
* the active side is the normalized document-order end rather than the mutable
|
|
316
|
+
* anchor/head direction. Table-grid may cross sibling cells in the same table.
|
|
317
|
+
*/
|
|
318
|
+
const clampToIsolating = (index, from, to, request) => {
|
|
319
|
+
const crossCellGrid = request.want === "table-grid"
|
|
320
|
+
&& from.isolation && to.isolation
|
|
321
|
+
&& isTableCell(from.isolation) && isTableCell(to.isolation)
|
|
322
|
+
&& nearestAncestor(from.entry, isTable) === nearestAncestor(to.entry, isTable);
|
|
323
|
+
if (!spansIsolating(index, from, to) || crossCellGrid) {
|
|
324
|
+
return { from, to, range: { from: copyPos(from.resolved.pos), to: copyPos(to.resolved.pos) }, clamped: false };
|
|
325
|
+
}
|
|
326
|
+
if (to.isolation) {
|
|
327
|
+
const start = resolveEndpoint(index, entryStartPos(to.isolation));
|
|
328
|
+
const end = resolveEndpoint(index, entryEndPos(to.isolation));
|
|
329
|
+
const clampedFrom = from.rank < start.rank ? start : from;
|
|
330
|
+
const clampedTo = to.rank > end.rank ? end : to;
|
|
331
|
+
return {
|
|
332
|
+
from: clampedFrom,
|
|
333
|
+
to: clampedTo,
|
|
334
|
+
range: { from: copyPos(clampedFrom.resolved.pos), to: copyPos(clampedTo.resolved.pos) },
|
|
335
|
+
clamped: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const crossed = index.entries
|
|
339
|
+
.filter((entry) => entry.isolating && entry.end > from.rank && entry.open < to.rank)
|
|
340
|
+
.map(outermostIsolation)
|
|
341
|
+
.filter((entry, position, all) => all.indexOf(entry) === position)
|
|
342
|
+
.sort((left, right) => right.end - left.end)[0];
|
|
343
|
+
if (!crossed)
|
|
344
|
+
return { from, to, range: { from: copyPos(from.resolved.pos), to: copyPos(to.resolved.pos) }, clamped: false };
|
|
345
|
+
const clampedFrom = resolveEndpoint(index, afterEntryPos(crossed));
|
|
346
|
+
return {
|
|
347
|
+
from: clampedFrom,
|
|
348
|
+
to,
|
|
349
|
+
range: { from: copyPos(clampedFrom.resolved.pos), to: copyPos(to.resolved.pos) },
|
|
350
|
+
clamped: true,
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
const nearestAncestor = (entry, predicate) => {
|
|
354
|
+
let cursor = entry;
|
|
355
|
+
while (cursor) {
|
|
356
|
+
if (predicate(cursor))
|
|
357
|
+
return cursor;
|
|
358
|
+
cursor = cursor.parent;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
};
|
|
362
|
+
const commonAncestor = (entries) => {
|
|
363
|
+
if (!entries.length)
|
|
364
|
+
return null;
|
|
365
|
+
let cursor = entries[0];
|
|
366
|
+
while (cursor) {
|
|
367
|
+
if (entries.every((entry) => isAncestor(cursor, entry)))
|
|
368
|
+
return cursor;
|
|
369
|
+
cursor = cursor.parent;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
};
|
|
373
|
+
const baseFor = (context, isolation) => ({
|
|
374
|
+
range: copyRange(context.range),
|
|
375
|
+
isolatingAncestorId: (isolation === undefined ? commonIsolation(context) : isolation)?.node.id ?? null,
|
|
376
|
+
clamped: context.clamped,
|
|
377
|
+
...(context.clampReason ? { clampReason: context.clampReason } : {}),
|
|
378
|
+
});
|
|
379
|
+
const commonIsolation = (context) => {
|
|
380
|
+
const left = context.from.isolation;
|
|
381
|
+
const right = context.to.isolation;
|
|
382
|
+
return left && left === right ? left : null;
|
|
383
|
+
};
|
|
384
|
+
const emptyScope = (context) => ({ kind: "empty", ...baseFor(context) });
|
|
385
|
+
const overlaps = (entry, from, to) => entry.contentStart < to && entry.contentEnd >= from;
|
|
386
|
+
const containingBlock = (context, endpoint) => nearestAncestor(endpoint.entry, (entry) => context.index.blockSet.has(entry));
|
|
387
|
+
const touchedBlocks = (context) => {
|
|
388
|
+
if (context.from.rank === context.to.rank) {
|
|
389
|
+
const block = containingBlock(context, context.to);
|
|
390
|
+
return block ? [block] : [];
|
|
391
|
+
}
|
|
392
|
+
const first = containingBlock(context, context.from);
|
|
393
|
+
const last = containingBlock(context, context.to);
|
|
394
|
+
if (first && first === last)
|
|
395
|
+
return [first];
|
|
396
|
+
return context.index.blocks.filter((entry) => {
|
|
397
|
+
if (entry.contentStart === entry.contentEnd)
|
|
398
|
+
return entry.contentStart > context.from.rank && entry.contentStart <= context.to.rank;
|
|
399
|
+
return entry.contentStart < context.to.rank && entry.contentEnd >= context.from.rank;
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
const blockPart = (context, blocks) => {
|
|
403
|
+
const first = blocks[0];
|
|
404
|
+
const last = blocks[blocks.length - 1];
|
|
405
|
+
const parents = new Set(blocks.map((entry) => entry.parent?.node.id ?? null));
|
|
406
|
+
return {
|
|
407
|
+
kind: "block-range",
|
|
408
|
+
...baseFor(context),
|
|
409
|
+
blockIds: blocks.map((entry) => entry.node.id),
|
|
410
|
+
promotedFromPartial: context.from.rank > first.contentStart || context.to.rank < last.contentEnd,
|
|
411
|
+
commonParentId: parents.size === 1 ? [...parents][0] : null,
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
const ownerSlice = (entry, fromRank, toRank) => ({
|
|
415
|
+
from: Math.max(0, Math.min(limitFor(entry), fromRank - entry.contentStart)),
|
|
416
|
+
to: Math.max(0, Math.min(limitFor(entry), toRank - entry.contentStart)),
|
|
417
|
+
});
|
|
418
|
+
const resolveInlineRange = (context) => {
|
|
419
|
+
const owners = context.from.entry === context.to.entry && context.from.entry.inlineOwner
|
|
420
|
+
? [context.from.entry]
|
|
421
|
+
: context.index.entries.filter((entry) => entry.inlineOwner && (context.from.rank === context.to.rank
|
|
422
|
+
? context.from.entry === entry
|
|
423
|
+
: entry.contentStart < context.to.rank && entry.contentEnd >= context.from.rank));
|
|
424
|
+
if (!owners.length)
|
|
425
|
+
return emptyScope(context);
|
|
426
|
+
const runs = owners.map((owner) => {
|
|
427
|
+
const slice = ownerSlice(owner, context.from.rank, context.to.rank);
|
|
428
|
+
return {
|
|
429
|
+
ownerNodeId: owner.node.id,
|
|
430
|
+
from: slice.from,
|
|
431
|
+
to: slice.to,
|
|
432
|
+
containsAtoms: owner.inlineChildren.some((child) => !isTextNode(child.node) && child.from < slice.to && child.to > slice.from),
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
const collapsed = context.from.rank === context.to.rank;
|
|
436
|
+
return {
|
|
437
|
+
kind: "inline-range",
|
|
438
|
+
...baseFor(context),
|
|
439
|
+
runs,
|
|
440
|
+
collapsed,
|
|
441
|
+
...(collapsed ? { storedMarkAnchor: context.from.resolved } : {}),
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
const resolveBlockRange = (context) => {
|
|
445
|
+
const blocks = touchedBlocks(context);
|
|
446
|
+
if (!blocks.length)
|
|
447
|
+
return emptyScope(context);
|
|
448
|
+
return blockPart(context, blocks);
|
|
449
|
+
};
|
|
450
|
+
const subtreeIds = (root) => {
|
|
451
|
+
const result = [];
|
|
452
|
+
const visit = (entry) => {
|
|
453
|
+
result.push(entry.node.id);
|
|
454
|
+
entry.children.forEach(visit);
|
|
455
|
+
entry.inlineChildren.forEach((child) => {
|
|
456
|
+
if (!isTextNode(child.node))
|
|
457
|
+
result.push(child.node.id);
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
visit(root);
|
|
461
|
+
return result;
|
|
462
|
+
};
|
|
463
|
+
const resolveContainerTree = (context, stopAt) => {
|
|
464
|
+
const blocks = touchedBlocks(context);
|
|
465
|
+
if (!blocks.length)
|
|
466
|
+
return emptyScope(context);
|
|
467
|
+
let root = commonAncestor(blocks) ?? blocks[0];
|
|
468
|
+
if (stopAt) {
|
|
469
|
+
const stops = blocks.map((block) => nearestAncestor(block, (entry) => stopAt(entry.node))).filter((entry) => Boolean(entry));
|
|
470
|
+
if (stops.length === blocks.length && stops.every((entry) => entry === stops[0]))
|
|
471
|
+
root = stops[0];
|
|
472
|
+
}
|
|
473
|
+
const first = blocks[0];
|
|
474
|
+
const last = blocks[blocks.length - 1];
|
|
475
|
+
return {
|
|
476
|
+
kind: "container-tree",
|
|
477
|
+
...baseFor(context),
|
|
478
|
+
rootId: root.node.id,
|
|
479
|
+
nodeIds: subtreeIds(root),
|
|
480
|
+
promotedFromPartial: context.from.rank > first.contentStart || context.to.rank < last.contentEnd,
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
const atomSelectedExactly = (context, atom) => {
|
|
484
|
+
if (atom.group === "inline") {
|
|
485
|
+
const owner = atom.parent;
|
|
486
|
+
const span = owner?.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === atom.node.id);
|
|
487
|
+
return Boolean(owner && span
|
|
488
|
+
&& context.from.entry === owner && context.to.entry === owner
|
|
489
|
+
&& context.from.resolved.pos.offset === span.from && context.to.resolved.pos.offset === span.to);
|
|
490
|
+
}
|
|
491
|
+
const parent = atom.parent;
|
|
492
|
+
return Boolean(parent
|
|
493
|
+
&& context.from.entry === parent && context.to.entry === parent
|
|
494
|
+
&& context.from.resolved.pos.offset === atom.index
|
|
495
|
+
&& context.to.resolved.pos.offset === atom.index + 1);
|
|
496
|
+
};
|
|
497
|
+
const atomsTouched = (context) => context.from.rank === context.to.rank ? [] : context.index.atoms.filter((atom) => {
|
|
498
|
+
if (atomSelectedExactly(context, atom))
|
|
499
|
+
return true;
|
|
500
|
+
if (context.from.rank === context.to.rank)
|
|
501
|
+
return false;
|
|
502
|
+
if (atom.group === "inline") {
|
|
503
|
+
const owner = atom.parent;
|
|
504
|
+
const span = owner?.inlineChildren.find((child) => !isTextNode(child.node) && child.node.id === atom.node.id);
|
|
505
|
+
if (!owner || !span)
|
|
506
|
+
return false;
|
|
507
|
+
const slice = ownerSlice(owner, context.from.rank, context.to.rank);
|
|
508
|
+
return context.from.entry === owner || context.to.entry === owner || overlaps(owner, context.from.rank, context.to.rank)
|
|
509
|
+
? span.from < slice.to && span.to > slice.from
|
|
510
|
+
: false;
|
|
511
|
+
}
|
|
512
|
+
return atom.open < context.to.rank && atom.end > context.from.rank;
|
|
513
|
+
});
|
|
514
|
+
const atomScope = (context, atom) => ({
|
|
515
|
+
kind: "atomic-node",
|
|
516
|
+
...baseFor(context, nearestAncestor(atom, (entry) => entry.isolating)),
|
|
517
|
+
nodeId: atom.node.id,
|
|
518
|
+
inline: atom.group === "inline",
|
|
519
|
+
});
|
|
520
|
+
const resolveAtomicNode = (context) => {
|
|
521
|
+
const atoms = atomsTouched(context).filter((atom) => atomSelectedExactly(context, atom));
|
|
522
|
+
if (!atoms.length)
|
|
523
|
+
return emptyScope(context);
|
|
524
|
+
if (atoms.length === 1)
|
|
525
|
+
return atomScope(context, atoms[0]);
|
|
526
|
+
return { kind: "mixed", ...baseFor(context), parts: atoms.map((atom) => atomScope(context, atom)) };
|
|
527
|
+
};
|
|
528
|
+
const touchedListItems = (context) => {
|
|
529
|
+
if (context.from.rank === context.to.rank) {
|
|
530
|
+
const item = nearestAncestor(context.to.entry, isListItem);
|
|
531
|
+
return item ? [item] : [];
|
|
532
|
+
}
|
|
533
|
+
// A ranged selection wholly inside one nested list must resolve to that
|
|
534
|
+
// list, not to every ancestor list item whose content interval contains the
|
|
535
|
+
// descendant. Ancestor intervals intentionally include nested content for
|
|
536
|
+
// block promotion, but list-type commands act on the nearest list whose
|
|
537
|
+
// items the user actually selected. When the endpoints belong to
|
|
538
|
+
// different list levels we retain the broader traversal below so a range
|
|
539
|
+
// that genuinely crosses a parent/child boundary still promotes to the
|
|
540
|
+
// containing list.
|
|
541
|
+
const fromItem = nearestAncestor(context.from.entry, isListItem);
|
|
542
|
+
const toItem = nearestAncestor(context.to.entry, isListItem);
|
|
543
|
+
const fromList = fromItem ? nearestAncestor(fromItem.parent ?? fromItem, isList) : null;
|
|
544
|
+
const toList = toItem ? nearestAncestor(toItem.parent ?? toItem, isList) : null;
|
|
545
|
+
if (fromList && fromList === toList) {
|
|
546
|
+
return context.index.entries.filter((entry) => isListItem(entry)
|
|
547
|
+
&& isAncestor(fromList, entry)
|
|
548
|
+
&& overlaps(entry, context.from.rank, context.to.rank)
|
|
549
|
+
&& !(entry === toItem && endpointAtContentStart(context.to, entry)));
|
|
550
|
+
}
|
|
551
|
+
return context.index.entries.filter((entry) => isListItem(entry) && overlaps(entry, context.from.rank, context.to.rank));
|
|
552
|
+
};
|
|
553
|
+
const listDepth = (item, list) => {
|
|
554
|
+
let depth = 0;
|
|
555
|
+
let cursor = item.parent;
|
|
556
|
+
while (cursor && cursor !== list) {
|
|
557
|
+
if (isList(cursor))
|
|
558
|
+
depth += 1;
|
|
559
|
+
cursor = cursor.parent;
|
|
560
|
+
}
|
|
561
|
+
return depth;
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* A range ending at the first editable position of a list item has not
|
|
565
|
+
* entered that item yet. Entry content ranges include the item's wrapper and
|
|
566
|
+
* first block, so a raw rank overlap would otherwise promote the endpoint
|
|
567
|
+
* item as well (and can turn a one-item deletion into a whole-list deletion).
|
|
568
|
+
*/
|
|
569
|
+
const firstContentEntry = (entry) => {
|
|
570
|
+
if (entry.inlineOwner || entry.atomic || !entry.children.length)
|
|
571
|
+
return entry;
|
|
572
|
+
return firstContentEntry(entry.children[0]);
|
|
573
|
+
};
|
|
574
|
+
const endpointAtContentStart = (endpoint, entry) => {
|
|
575
|
+
const first = firstContentEntry(entry);
|
|
576
|
+
return endpoint.entry === first && endpoint.resolved.pos.offset === 0;
|
|
577
|
+
};
|
|
578
|
+
const listPart = (context, list, items) => {
|
|
579
|
+
const selected = new Set(items);
|
|
580
|
+
const hasChildList = (item) => context.index.entries.some((entry) => isList(entry) && entry !== list && isAncestor(item, entry));
|
|
581
|
+
const partialSubtree = items.some((item) => hasChildList(item)
|
|
582
|
+
&& context.index.entries.some((entry) => isListItem(entry) && isAncestor(item, entry) && entry !== item && !selected.has(entry)));
|
|
583
|
+
return {
|
|
584
|
+
kind: "list-selection",
|
|
585
|
+
...baseFor(context),
|
|
586
|
+
listId: list.node.id,
|
|
587
|
+
items: items.map((item) => ({ itemId: item.node.id, depth: listDepth(item, list), hasChildList: hasChildList(item) })),
|
|
588
|
+
partialSubtree,
|
|
589
|
+
promotedFromPartial: context.from.rank > items[0].contentStart || context.to.rank < items[items.length - 1].contentEnd,
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
const selectedListRoot = (item, selected) => {
|
|
593
|
+
let list = nearestAncestor(item.parent ?? item, isList);
|
|
594
|
+
if (!list)
|
|
595
|
+
return null;
|
|
596
|
+
while (true) {
|
|
597
|
+
const ownerItem = nearestAncestor(list.parent ?? list, isListItem);
|
|
598
|
+
if (!ownerItem || !selected.has(ownerItem))
|
|
599
|
+
return list;
|
|
600
|
+
const outer = nearestAncestor(ownerItem.parent ?? ownerItem, isList);
|
|
601
|
+
if (!outer)
|
|
602
|
+
return list;
|
|
603
|
+
list = outer;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const resolveListSelection = (context) => {
|
|
607
|
+
const items = touchedListItems(context);
|
|
608
|
+
const selected = new Set(items);
|
|
609
|
+
const groups = new Map();
|
|
610
|
+
items.forEach((item) => {
|
|
611
|
+
const list = selectedListRoot(item, selected);
|
|
612
|
+
if (list)
|
|
613
|
+
groups.set(list, [...(groups.get(list) || []), item]);
|
|
614
|
+
});
|
|
615
|
+
const parts = [...groups].map(([list, listItems]) => ({
|
|
616
|
+
order: list.preorder,
|
|
617
|
+
scope: listPart(context, list, listItems),
|
|
618
|
+
}));
|
|
619
|
+
const plainBlocks = touchedBlocks(context).filter((block) => !nearestAncestor(block, isListItem));
|
|
620
|
+
if (plainBlocks.length)
|
|
621
|
+
parts.push({ order: plainBlocks[0].preorder, scope: blockPart(context, plainBlocks) });
|
|
622
|
+
parts.sort((left, right) => left.order - right.order);
|
|
623
|
+
if (!parts.length)
|
|
624
|
+
return emptyScope(context);
|
|
625
|
+
if (parts.length === 1)
|
|
626
|
+
return parts[0].scope;
|
|
627
|
+
return { kind: "mixed", ...baseFor(context), parts: parts.map((part) => part.scope) };
|
|
628
|
+
};
|
|
629
|
+
const endpointCell = (endpoint) => nearestAncestor(endpoint.entry, isTableCell);
|
|
630
|
+
const touchedCells = (context, table) => {
|
|
631
|
+
if (context.from.rank === context.to.rank) {
|
|
632
|
+
const cell = endpointCell(context.to);
|
|
633
|
+
return cell && isAncestor(table, cell) ? [cell] : [];
|
|
634
|
+
}
|
|
635
|
+
return context.index.entries.filter((entry) => isTableCell(entry) && isAncestor(table, entry) && overlaps(entry, context.from.rank, context.to.rank));
|
|
636
|
+
};
|
|
637
|
+
const tablePart = (context, table) => {
|
|
638
|
+
const grid = occupancyGridFor(table.node);
|
|
639
|
+
const fromCell = endpointCell(context.from);
|
|
640
|
+
const toCell = endpointCell(context.to);
|
|
641
|
+
let selected;
|
|
642
|
+
if (fromCell && toCell && context.from.resolved.pos && context.to.resolved.pos) {
|
|
643
|
+
const first = grid.anchors.find((cell) => cell.cellId === fromCell.node.id);
|
|
644
|
+
const last = grid.anchors.find((cell) => cell.cellId === toCell.node.id);
|
|
645
|
+
if (first && last && context.from.resolved.pos && context.to.resolved.pos && fromCell !== toCell) {
|
|
646
|
+
const bounds = {
|
|
647
|
+
top: Math.min(first.top, last.top), left: Math.min(first.left, last.left),
|
|
648
|
+
bottom: Math.max(first.bottom, last.bottom), right: Math.max(first.right, last.right),
|
|
649
|
+
};
|
|
650
|
+
selected = grid.anchors.filter((cell) => cell.top >= bounds.top && cell.left >= bounds.left && cell.top < bounds.bottom && cell.left < bounds.right);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const touched = new Set(touchedCells(context, table).map((entry) => entry.node.id));
|
|
654
|
+
selected = grid.anchors.filter((cell) => touched.has(cell.cellId));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
const touched = new Set(touchedCells(context, table).map((entry) => entry.node.id));
|
|
659
|
+
selected = grid.anchors.filter((cell) => touched.has(cell.cellId));
|
|
660
|
+
}
|
|
661
|
+
if (!selected.length)
|
|
662
|
+
return emptyScope(context);
|
|
663
|
+
const rect = {
|
|
664
|
+
top: Math.min(...selected.map((cell) => cell.top)),
|
|
665
|
+
left: Math.min(...selected.map((cell) => cell.left)),
|
|
666
|
+
bottom: Math.max(...selected.map((cell) => cell.bottom)) - 1,
|
|
667
|
+
right: Math.max(...selected.map((cell) => cell.right)) - 1,
|
|
668
|
+
};
|
|
669
|
+
const selectedSet = new Set(selected.map((cell) => cell.cellId));
|
|
670
|
+
const covered = new Set();
|
|
671
|
+
let rectangular = true;
|
|
672
|
+
for (let row = rect.top; row <= rect.bottom; row += 1) {
|
|
673
|
+
for (let column = rect.left; column <= rect.right; column += 1) {
|
|
674
|
+
const occupant = grid.at(row, column);
|
|
675
|
+
if (!occupant || !selectedSet.has(occupant.cellId))
|
|
676
|
+
rectangular = false;
|
|
677
|
+
if (occupant && (occupant.top < rect.top || occupant.left < rect.left))
|
|
678
|
+
covered.add(occupant.cellId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (selected.some((cell) => cell.bottom - 1 > rect.bottom || cell.right - 1 > rect.right))
|
|
682
|
+
rectangular = false;
|
|
683
|
+
return {
|
|
684
|
+
kind: "table-grid",
|
|
685
|
+
...baseFor(context, nearestAncestor(table, (entry) => entry.isolating)),
|
|
686
|
+
tableId: table.node.id,
|
|
687
|
+
rect,
|
|
688
|
+
cellIds: selected.map((cell) => cell.cellId),
|
|
689
|
+
coveredCellIds: [...covered],
|
|
690
|
+
rectangular,
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
const resolveTableGrid = (context) => {
|
|
694
|
+
const tables = new Set();
|
|
695
|
+
const fromTable = nearestAncestor(context.from.entry, isTable);
|
|
696
|
+
const toTable = nearestAncestor(context.to.entry, isTable);
|
|
697
|
+
if (fromTable)
|
|
698
|
+
tables.add(fromTable);
|
|
699
|
+
if (toTable)
|
|
700
|
+
tables.add(toTable);
|
|
701
|
+
if (fromTable !== toTable || !fromTable)
|
|
702
|
+
context.index.entries.forEach((entry) => {
|
|
703
|
+
if (isTable(entry) && overlaps(entry, context.from.rank, context.to.rank))
|
|
704
|
+
tables.add(entry);
|
|
705
|
+
});
|
|
706
|
+
const parts = [...tables].sort((a, b) => a.preorder - b.preorder).map((table) => tablePart(context, table)).filter((part) => part.kind === "table-grid");
|
|
707
|
+
if (!parts.length)
|
|
708
|
+
return emptyScope(context);
|
|
709
|
+
if (parts.length === 1)
|
|
710
|
+
return parts[0];
|
|
711
|
+
return { kind: "mixed", ...baseFor(context), parts };
|
|
712
|
+
};
|
|
713
|
+
const markKey = (mark) => JSON.stringify([mark.type, mark.attrs ?? null]);
|
|
714
|
+
const marksForRange = (context) => {
|
|
715
|
+
if (context.from.rank === context.to.rank && context.from.entry.inlineOwner) {
|
|
716
|
+
const offset = context.from.resolved.pos.offset;
|
|
717
|
+
// A collapsed caret strictly inside a run (not at any run boundary) has
|
|
718
|
+
// exactly one containing run - `from <= offset <= to` - regardless of
|
|
719
|
+
// how many other runs the owner has. At an exact boundary between two
|
|
720
|
+
// runs, both the preceding and following run satisfy that inequality;
|
|
721
|
+
// prefer the preceding one (`to === offset`), matching this function's
|
|
722
|
+
// original tie-break (continuing the run just typed/edited, not the
|
|
723
|
+
// one about to be entered). The previous version only ever found a run
|
|
724
|
+
// via that boundary tie-break, which by construction never matches a
|
|
725
|
+
// caret that isn't at some run's edge - a collapsed caret anywhere in
|
|
726
|
+
// the interior of a run (the overwhelmingly common case) silently
|
|
727
|
+
// returned no marks at all.
|
|
728
|
+
const touching = context.from.entry.inlineChildren.filter((candidate) => candidate.from <= offset && offset <= candidate.to);
|
|
729
|
+
const child = touching.find((candidate) => candidate.to === offset) ?? touching[0];
|
|
730
|
+
return child && isTextNode(child.node)
|
|
731
|
+
? child.node.marks?.map((mark) => ({ mark, coverage: "all" })) || []
|
|
732
|
+
: [];
|
|
733
|
+
}
|
|
734
|
+
const occurrences = new Map();
|
|
735
|
+
let total = 0;
|
|
736
|
+
context.index.entries.filter((entry) => entry.inlineOwner).forEach((owner) => {
|
|
737
|
+
const slice = ownerSlice(owner, context.from.rank, context.to.rank);
|
|
738
|
+
owner.inlineChildren.forEach((child) => {
|
|
739
|
+
if (!isTextNode(child.node))
|
|
740
|
+
return;
|
|
741
|
+
const width = Math.max(0, Math.min(child.to, slice.to) - Math.max(child.from, slice.from));
|
|
742
|
+
if (!width)
|
|
743
|
+
return;
|
|
744
|
+
total += width;
|
|
745
|
+
(child.node.marks || []).forEach((mark) => {
|
|
746
|
+
const key = markKey(mark);
|
|
747
|
+
const current = occurrences.get(key);
|
|
748
|
+
occurrences.set(key, { mark, covered: (current?.covered || 0) + width });
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
return [...occurrences.values()].map(({ mark, covered }) => ({ mark, coverage: covered === total ? "all" : "partial" }));
|
|
753
|
+
};
|
|
754
|
+
const describeSelection = (unclamped) => {
|
|
755
|
+
const blocks = touchedBlocks(unclamped);
|
|
756
|
+
const canonical = unclamped.to;
|
|
757
|
+
const item = nearestAncestor(canonical.entry, isListItem);
|
|
758
|
+
const list = item ? nearestAncestor(item.parent ?? item, isList) : null;
|
|
759
|
+
const cell = endpointCell(canonical);
|
|
760
|
+
const table = cell ? nearestAncestor(cell.parent ?? cell, isTable) : null;
|
|
761
|
+
return {
|
|
762
|
+
blockTypes: [...new Set(blocks.map((entry) => entry.node.type))],
|
|
763
|
+
marks: marksForRange(unclamped),
|
|
764
|
+
inList: list && item ? { listId: list.node.id, depth: listDepth(item, list) } : null,
|
|
765
|
+
inTable: table && cell ? { tableId: table.node.id, cellId: cell.node.id } : null,
|
|
766
|
+
isolatingAncestorId: canonical.isolation?.node.id ?? null,
|
|
767
|
+
atoms: atomsTouched(unclamped).map((entry) => entry.node.id),
|
|
768
|
+
collapsed: unclamped.from.rank === unclamped.to.rank,
|
|
769
|
+
spansIsolatingBoundary: spansIsolating(unclamped.index, unclamped.from, unclamped.to),
|
|
770
|
+
};
|
|
771
|
+
};
|
|
772
|
+
const contextFor = (index, schema, original, from, to, range, clamped) => ({
|
|
773
|
+
index,
|
|
774
|
+
schema,
|
|
775
|
+
original: copyRange(original),
|
|
776
|
+
from,
|
|
777
|
+
to,
|
|
778
|
+
range: copyRange(range),
|
|
779
|
+
clamped,
|
|
780
|
+
...(clamped ? { clampReason: "isolating" } : {}),
|
|
781
|
+
});
|
|
782
|
+
const resolveWithIndex = (index, selection, request, schema) => {
|
|
783
|
+
const normalized = normalizeSelection(index, selection);
|
|
784
|
+
const unclamped = contextFor(index, schema, normalized.range, normalized.from, normalized.to, normalized.range, false);
|
|
785
|
+
if (request.want === "describe")
|
|
786
|
+
return describeSelection(unclamped);
|
|
787
|
+
const clamped = clampToIsolating(index, normalized.from, normalized.to, request);
|
|
788
|
+
const context = contextFor(index, schema, normalized.range, clamped.from, clamped.to, clamped.range, clamped.clamped);
|
|
789
|
+
switch (request.want) {
|
|
790
|
+
case "inline-range": return resolveInlineRange(context);
|
|
791
|
+
case "block-range": return resolveBlockRange(context);
|
|
792
|
+
case "container-tree": return resolveContainerTree(context, request.stopAt);
|
|
793
|
+
case "list-selection": return resolveListSelection(context);
|
|
794
|
+
case "table-grid": return resolveTableGrid(context);
|
|
795
|
+
case "atomic-node": return resolveAtomicNode(context);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const outerRange = (entry) => {
|
|
799
|
+
if (!entry.parent)
|
|
800
|
+
return { from: entryStartPos(entry), to: entryEndPos(entry) };
|
|
801
|
+
const atom = atomicParentSpan(entry);
|
|
802
|
+
if (atom)
|
|
803
|
+
return {
|
|
804
|
+
from: { path: [...atom.parent.path], offset: atom.from },
|
|
805
|
+
to: { path: [...atom.parent.path], offset: atom.to },
|
|
806
|
+
};
|
|
807
|
+
return {
|
|
808
|
+
from: { path: [...entry.parent.path], offset: entry.index },
|
|
809
|
+
to: { path: [...entry.parent.path], offset: entry.index + 1 },
|
|
810
|
+
};
|
|
811
|
+
};
|
|
812
|
+
const lookupFor = (index) => ({
|
|
813
|
+
positionOf(nodeId) {
|
|
814
|
+
const entry = index.byId.get(nodeId);
|
|
815
|
+
if (!entry)
|
|
816
|
+
return null;
|
|
817
|
+
return resolveEndpoint(index, outerRange(entry).from).resolved;
|
|
818
|
+
},
|
|
819
|
+
rangeOf(nodeId) {
|
|
820
|
+
const entry = index.byId.get(nodeId);
|
|
821
|
+
return entry ? copyRange(outerRange(entry)) : null;
|
|
822
|
+
},
|
|
823
|
+
contentRangeOf(nodeId) {
|
|
824
|
+
const entry = index.byId.get(nodeId);
|
|
825
|
+
if (!entry)
|
|
826
|
+
return null;
|
|
827
|
+
if (entry.atomic)
|
|
828
|
+
return copyRange(outerRange(entry));
|
|
829
|
+
return { from: entryStartPos(entry), to: entryEndPos(entry) };
|
|
830
|
+
},
|
|
831
|
+
exists(nodeId) { return index.byId.has(nodeId); },
|
|
832
|
+
});
|
|
833
|
+
export class FoundationScopeIndex {
|
|
834
|
+
constructor() {
|
|
835
|
+
this.document = null;
|
|
836
|
+
this.schema = null;
|
|
837
|
+
this.index = null;
|
|
838
|
+
}
|
|
839
|
+
update(document, schema) {
|
|
840
|
+
if (this.document === document && this.schema === schema && this.index)
|
|
841
|
+
return this.index;
|
|
842
|
+
if (this.schema === schema && this.index && refreshIndex(this.index, document, schema)) {
|
|
843
|
+
this.document = document;
|
|
844
|
+
return this.index;
|
|
845
|
+
}
|
|
846
|
+
this.document = document;
|
|
847
|
+
this.schema = schema;
|
|
848
|
+
this.index = buildIndex(document, schema);
|
|
849
|
+
return this.index;
|
|
850
|
+
}
|
|
851
|
+
resolve(document, selection, request, schema) {
|
|
852
|
+
return resolveWithIndex(this.update(document, schema), selection, request, schema);
|
|
853
|
+
}
|
|
854
|
+
positions(document, schema) {
|
|
855
|
+
return lookupFor(this.update(document, schema));
|
|
856
|
+
}
|
|
857
|
+
get liveNodeCount() { return this.index?.entries.length ?? 0; }
|
|
858
|
+
}
|
|
859
|
+
export const createScopeIndex = () => new FoundationScopeIndex();
|
|
860
|
+
export const resolveScope = (document, selection, request, schema) => resolveWithIndex(buildIndex(document, schema), selection, request, schema);
|