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,568 @@
|
|
|
1
|
+
import { assertUniqueNodeIds, cloneNode, isTextNode } from "./identity.js";
|
|
2
|
+
import { nodeAtPath, samePath } from "./positions.js";
|
|
3
|
+
import { canonicalMarkOrder } from "./marks/canonical.js";
|
|
4
|
+
const sameValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
5
|
+
const parentAndIndex = (document, pos) => {
|
|
6
|
+
const parent = nodeAtPath(document, pos.path);
|
|
7
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
8
|
+
throw new Error("Operation position does not resolve to a container.");
|
|
9
|
+
if (!Number.isInteger(pos.offset) || pos.offset < 0 || pos.offset > parent.children.length)
|
|
10
|
+
throw new Error("Operation child offset is out of bounds.");
|
|
11
|
+
return { parent, index: pos.offset };
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Transaction-local copy-on-write tree. A node is copied at most once in one
|
|
15
|
+
* operation batch; unchanged siblings retain reference identity. This identity
|
|
16
|
+
* guarantee is the memoization contract for scope indexing and DOM rendering.
|
|
17
|
+
*/
|
|
18
|
+
class PathCopySession {
|
|
19
|
+
constructor(input) {
|
|
20
|
+
this.input = input;
|
|
21
|
+
this.copies = new WeakMap();
|
|
22
|
+
this.mutable = new WeakSet();
|
|
23
|
+
this.root = input;
|
|
24
|
+
}
|
|
25
|
+
document() { return this.root; }
|
|
26
|
+
node(path) { return nodeAtPath(this.root, path); }
|
|
27
|
+
copy(node) {
|
|
28
|
+
if (this.mutable.has(node))
|
|
29
|
+
return node;
|
|
30
|
+
const existing = this.copies.get(node);
|
|
31
|
+
if (existing)
|
|
32
|
+
return existing;
|
|
33
|
+
const draft = { ...node, ...(node.children ? { children: [...node.children] } : {}) };
|
|
34
|
+
this.copies.set(node, draft);
|
|
35
|
+
this.mutable.add(draft);
|
|
36
|
+
return draft;
|
|
37
|
+
}
|
|
38
|
+
container(path) {
|
|
39
|
+
let current = this.copy(this.root);
|
|
40
|
+
this.root = current;
|
|
41
|
+
for (const index of path) {
|
|
42
|
+
const children = current.children;
|
|
43
|
+
const child = children?.[index];
|
|
44
|
+
if (!child || isTextNode(child))
|
|
45
|
+
throw new Error("Container path does not resolve to an element.");
|
|
46
|
+
const next = this.copy(child);
|
|
47
|
+
children[index] = next;
|
|
48
|
+
current = next;
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
replace(path, replacement) {
|
|
53
|
+
if (!path.length) {
|
|
54
|
+
if (isTextNode(replacement) || replacement.type !== "doc")
|
|
55
|
+
throw new Error("Document root replacement must be a doc node.");
|
|
56
|
+
this.root = replacement;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const parent = this.container(path.slice(0, -1));
|
|
60
|
+
const index = path[path.length - 1];
|
|
61
|
+
if (!parent.children || index < 0 || index >= parent.children.length)
|
|
62
|
+
throw new Error("Replacement index is out of bounds.");
|
|
63
|
+
parent.children[index] = replacement;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const marksKey = (marks) => JSON.stringify(canonicalMarkOrder(marks));
|
|
67
|
+
const normalizeInline = (children) => {
|
|
68
|
+
const result = [];
|
|
69
|
+
children.forEach((node) => {
|
|
70
|
+
if (isTextNode(node) && !node.text)
|
|
71
|
+
return;
|
|
72
|
+
if (isTextNode(node) && node.marks?.length)
|
|
73
|
+
node = { ...node, marks: canonicalMarkOrder(node.marks) };
|
|
74
|
+
const previous = result[result.length - 1];
|
|
75
|
+
if (isTextNode(node) && previous && isTextNode(previous) && marksKey(previous.marks) === marksKey(node.marks)) {
|
|
76
|
+
result[result.length - 1] = { ...previous, text: previous.text + node.text };
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
result.push(node);
|
|
80
|
+
});
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
const splitInlineAt = (children, offset) => {
|
|
84
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
85
|
+
throw new Error("Inline offset is invalid.");
|
|
86
|
+
const before = [];
|
|
87
|
+
const after = [];
|
|
88
|
+
let consumed = 0;
|
|
89
|
+
let split = false;
|
|
90
|
+
children.forEach((node) => {
|
|
91
|
+
if (split)
|
|
92
|
+
return after.push(node);
|
|
93
|
+
const size = isTextNode(node) ? node.text.length : 1;
|
|
94
|
+
if (offset > consumed + size) {
|
|
95
|
+
before.push(node);
|
|
96
|
+
consumed += size;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const local = offset - consumed;
|
|
100
|
+
if (isTextNode(node)) {
|
|
101
|
+
if (local > 0)
|
|
102
|
+
before.push({ ...node, text: node.text.slice(0, local) });
|
|
103
|
+
if (local < size)
|
|
104
|
+
after.push({ ...node, text: node.text.slice(local) });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (local === 0)
|
|
108
|
+
after.push(node);
|
|
109
|
+
else if (local === 1)
|
|
110
|
+
before.push(node);
|
|
111
|
+
else
|
|
112
|
+
throw new Error("Atomic inline offset is invalid.");
|
|
113
|
+
}
|
|
114
|
+
split = true;
|
|
115
|
+
consumed += size;
|
|
116
|
+
});
|
|
117
|
+
if (!split && offset !== consumed)
|
|
118
|
+
throw new Error("Inline offset is out of bounds.");
|
|
119
|
+
return [before, after];
|
|
120
|
+
};
|
|
121
|
+
const inlineContainer = (document, pos) => {
|
|
122
|
+
const node = nodeAtPath(document, pos.path);
|
|
123
|
+
if (!node || isTextNode(node) || !node.children) {
|
|
124
|
+
throw new Error("Text operation must target an inline container.");
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
};
|
|
128
|
+
const insertText = (session, pos, text, marks) => {
|
|
129
|
+
const container = inlineContainer(session.document(), pos);
|
|
130
|
+
const [before, after] = splitInlineAt(container.children || [], pos.offset);
|
|
131
|
+
const inserted = text ? [{ type: "text", text, ...(marks?.length ? { marks: structuredClone(marks) } : {}) }] : [];
|
|
132
|
+
const draft = session.container(pos.path);
|
|
133
|
+
draft.children = normalizeInline([...before, ...inserted, ...after]);
|
|
134
|
+
};
|
|
135
|
+
const deleteText = (session, pos, text) => {
|
|
136
|
+
const container = inlineContainer(session.document(), pos);
|
|
137
|
+
const [before, tail] = splitInlineAt(container.children || [], pos.offset);
|
|
138
|
+
const [removed, after] = splitInlineAt(tail, text.length);
|
|
139
|
+
const actual = removed.map((node) => isTextNode(node) ? node.text : "\uFFFC").join("");
|
|
140
|
+
if (actual !== text)
|
|
141
|
+
throw new Error(`deleteText payload does not match document content.`);
|
|
142
|
+
const draft = session.container(pos.path);
|
|
143
|
+
draft.children = normalizeInline([...before, ...after]);
|
|
144
|
+
};
|
|
145
|
+
const applyMark = (session, range, mark, add) => {
|
|
146
|
+
const comparePath = (left, right) => {
|
|
147
|
+
const length = Math.min(left.length, right.length);
|
|
148
|
+
for (let index = 0; index < length; index += 1)
|
|
149
|
+
if (left[index] !== right[index])
|
|
150
|
+
return left[index] - right[index];
|
|
151
|
+
return left.length - right.length;
|
|
152
|
+
};
|
|
153
|
+
if (comparePath(range.from.path, range.to.path) > 0 || (samePath(range.from.path, range.to.path) && range.from.offset > range.to.offset)) {
|
|
154
|
+
throw new Error("Mark operation range must be normalized from/to.");
|
|
155
|
+
}
|
|
156
|
+
const paths = [];
|
|
157
|
+
const visit = (node, path) => {
|
|
158
|
+
if (isTextNode(node))
|
|
159
|
+
return;
|
|
160
|
+
if (samePath(path, range.from.path) && samePath(path, range.to.path))
|
|
161
|
+
paths.push(path);
|
|
162
|
+
else if ((node.type === "paragraph" || node.type === "heading") &&
|
|
163
|
+
comparePath(path, range.from.path) >= 0 && comparePath(path, range.to.path) <= 0)
|
|
164
|
+
paths.push(path);
|
|
165
|
+
node.children?.forEach((child, index) => visit(child, [...path, index]));
|
|
166
|
+
};
|
|
167
|
+
visit(session.document(), []);
|
|
168
|
+
paths.forEach((path) => {
|
|
169
|
+
const container = inlineContainer(session.document(), { path, offset: 0 });
|
|
170
|
+
const size = (container.children || []).reduce((total, child) => total + (isTextNode(child) ? child.text.length : 1), 0);
|
|
171
|
+
const start = samePath(path, range.from.path) ? range.from.offset : 0;
|
|
172
|
+
const end = samePath(path, range.to.path) ? range.to.offset : size;
|
|
173
|
+
const [before, tail] = splitInlineAt(container.children || [], start);
|
|
174
|
+
const [middle, after] = splitInlineAt(tail, end - start);
|
|
175
|
+
const changed = middle.map((node) => {
|
|
176
|
+
if (!isTextNode(node))
|
|
177
|
+
return node;
|
|
178
|
+
const marks = node.marks || [];
|
|
179
|
+
if (add) {
|
|
180
|
+
if (marks.some((candidate) => sameValue(candidate, mark)))
|
|
181
|
+
return node;
|
|
182
|
+
return { ...node, marks: canonicalMarkOrder([...marks, structuredClone(mark)]) };
|
|
183
|
+
}
|
|
184
|
+
const next = marks.filter((candidate) => !sameValue(candidate, mark));
|
|
185
|
+
return { ...node, ...(next.length ? { marks: next } : { marks: undefined }) };
|
|
186
|
+
});
|
|
187
|
+
const draft = session.container(path);
|
|
188
|
+
draft.children = normalizeInline([...before, ...changed, ...after]);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
const applyToSession = (session, operation) => {
|
|
192
|
+
const document = session.document();
|
|
193
|
+
if (operation.type === "insertNode") {
|
|
194
|
+
const { parent, index } = parentAndIndex(document, operation.pos);
|
|
195
|
+
const draft = session.container(operation.pos.path);
|
|
196
|
+
draft.children = [...(parent.children || []).slice(0, index), cloneNode(operation.node), ...(parent.children || []).slice(index)];
|
|
197
|
+
}
|
|
198
|
+
else if (operation.type === "removeNode") {
|
|
199
|
+
const { parent, index } = parentAndIndex(document, operation.pos);
|
|
200
|
+
const existing = parent.children?.[index];
|
|
201
|
+
if (!existing || !sameValue(existing, operation.node))
|
|
202
|
+
throw new Error("removeNode payload does not match document node.");
|
|
203
|
+
const draft = session.container(operation.pos.path);
|
|
204
|
+
draft.children = [...(parent.children || []).slice(0, index), ...(parent.children || []).slice(index + 1)];
|
|
205
|
+
}
|
|
206
|
+
else if (operation.type === "replaceNode") {
|
|
207
|
+
const { parent, index } = parentAndIndex(document, operation.pos);
|
|
208
|
+
if (!sameValue(parent.children?.[index], operation.before))
|
|
209
|
+
throw new Error("replaceNode before payload does not match document node.");
|
|
210
|
+
const draft = session.container(operation.pos.path);
|
|
211
|
+
draft.children = (parent.children || []).map((node, nodeIndex) => nodeIndex === index ? cloneNode(operation.after) : node);
|
|
212
|
+
}
|
|
213
|
+
else if (operation.type === "moveNode") {
|
|
214
|
+
const source = parentAndIndex(document, operation.from);
|
|
215
|
+
const moving = source.parent.children?.[source.index];
|
|
216
|
+
if (!moving || isTextNode(moving) || moving.id !== operation.nodeId)
|
|
217
|
+
throw new Error("moveNode source does not match nodeId.");
|
|
218
|
+
const sourceDraft = session.container(operation.from.path);
|
|
219
|
+
sourceDraft.children = [...(source.parent.children || []).slice(0, source.index), ...(source.parent.children || []).slice(source.index + 1)];
|
|
220
|
+
const destination = parentAndIndex(session.document(), operation.to);
|
|
221
|
+
const destinationDraft = session.container(operation.to.path);
|
|
222
|
+
destinationDraft.children = [...(destination.parent.children || []).slice(0, destination.index), moving, ...(destination.parent.children || []).slice(destination.index)];
|
|
223
|
+
}
|
|
224
|
+
else if (operation.type === "splitNode") {
|
|
225
|
+
const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
|
|
226
|
+
const target = session.node(targetPath);
|
|
227
|
+
if (!target || isTextNode(target) || !target.children)
|
|
228
|
+
throw new Error("splitNode target must be a container.");
|
|
229
|
+
if (operation.pos.offset < 0 || operation.pos.offset > target.children.length)
|
|
230
|
+
throw new Error("splitNode offset is out of bounds.");
|
|
231
|
+
const left = { ...target, children: target.children.slice(0, operation.pos.offset) };
|
|
232
|
+
const right = { ...target, id: operation.newId, children: target.children.slice(operation.pos.offset) };
|
|
233
|
+
const parentPath = targetPath.slice(0, -1);
|
|
234
|
+
const index = targetPath[targetPath.length - 1];
|
|
235
|
+
const parent = session.node(parentPath);
|
|
236
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
237
|
+
throw new Error("splitNode parent is invalid.");
|
|
238
|
+
const draft = session.container(parentPath);
|
|
239
|
+
draft.children = [...parent.children.slice(0, index), left, right, ...parent.children.slice(index + 1)];
|
|
240
|
+
}
|
|
241
|
+
else if (operation.type === "mergeNode") {
|
|
242
|
+
const parent = session.node(operation.pos.path);
|
|
243
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
244
|
+
throw new Error("mergeNode parent is invalid.");
|
|
245
|
+
const rightIndex = operation.pos.offset;
|
|
246
|
+
const left = parent.children[rightIndex - 1];
|
|
247
|
+
const right = parent.children[rightIndex];
|
|
248
|
+
if (!left || !right || isTextNode(left) || isTextNode(right) || right.id !== operation.retiredId)
|
|
249
|
+
throw new Error("mergeNode nodes are invalid.");
|
|
250
|
+
const comparable = (node) => {
|
|
251
|
+
const { id: _id, children: _children, ...rest } = node;
|
|
252
|
+
return rest;
|
|
253
|
+
};
|
|
254
|
+
if (!sameValue(comparable(left), comparable(right)) || operation.splitOffset !== (left.children || []).length) {
|
|
255
|
+
throw new Error("mergeNode requires structurally compatible siblings and the exact split offset.");
|
|
256
|
+
}
|
|
257
|
+
const merged = { ...left, children: [...(left.children || []), ...(right.children || [])] };
|
|
258
|
+
const draft = session.container(operation.pos.path);
|
|
259
|
+
draft.children = [...parent.children.slice(0, rightIndex - 1), merged, ...parent.children.slice(rightIndex + 1)];
|
|
260
|
+
}
|
|
261
|
+
else if (operation.type === "setNodeAttributes") {
|
|
262
|
+
const target = session.node(operation.pos.path);
|
|
263
|
+
if (!target || isTextNode(target) || !sameValue(target.attrs || {}, operation.before))
|
|
264
|
+
throw new Error("setNodeAttributes before payload does not match.");
|
|
265
|
+
// Reassign in place rather than destructure-then-respread: the latter
|
|
266
|
+
// always re-adds `attrs` as the object's last key, silently reordering
|
|
267
|
+
// it even when only the value changed. sameValue elsewhere compares
|
|
268
|
+
// nodes via JSON.stringify, which is key-order sensitive - a later
|
|
269
|
+
// replaceNode/removeNode whose `before` payload was captured pre-reorder
|
|
270
|
+
// would then fail to match an otherwise-identical node. See
|
|
271
|
+
// docs/bugs/setnodeattributes-reorders-keys-breaking-exact-match.md.
|
|
272
|
+
const next = { ...target };
|
|
273
|
+
if (Object.keys(operation.after).length)
|
|
274
|
+
next.attrs = structuredClone(operation.after);
|
|
275
|
+
else
|
|
276
|
+
delete next.attrs;
|
|
277
|
+
session.replace(operation.pos.path, next);
|
|
278
|
+
}
|
|
279
|
+
else if (operation.type === "setNodeType") {
|
|
280
|
+
const target = session.node(operation.pos.path);
|
|
281
|
+
if (!target || isTextNode(target) || target.type !== operation.before || !sameValue(target.attrs || {}, operation.beforeAttrs)) {
|
|
282
|
+
throw new Error("setNodeType before payload does not match.");
|
|
283
|
+
}
|
|
284
|
+
session.replace(operation.pos.path, {
|
|
285
|
+
type: operation.after,
|
|
286
|
+
id: target.id,
|
|
287
|
+
...(Object.keys(operation.afterAttrs).length ? { attrs: structuredClone(operation.afterAttrs) } : {}),
|
|
288
|
+
...(target.children ? { children: target.children } : {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (operation.type === "insertText")
|
|
292
|
+
insertText(session, operation.pos, operation.text, operation.marks);
|
|
293
|
+
else if (operation.type === "deleteText")
|
|
294
|
+
deleteText(session, operation.pos, operation.text);
|
|
295
|
+
else if (operation.type === "addMark")
|
|
296
|
+
applyMark(session, operation.range, operation.mark, true);
|
|
297
|
+
else
|
|
298
|
+
applyMark(session, operation.range, operation.mark, false);
|
|
299
|
+
return operation.type === "insertNode" || operation.type === "replaceNode" || operation.type === "splitNode";
|
|
300
|
+
};
|
|
301
|
+
export const applyOperation = (input, operation) => {
|
|
302
|
+
const session = new PathCopySession(input);
|
|
303
|
+
const identityChanged = applyToSession(session, operation);
|
|
304
|
+
const document = session.document();
|
|
305
|
+
if (identityChanged)
|
|
306
|
+
assertUniqueNodeIds(document);
|
|
307
|
+
return document;
|
|
308
|
+
};
|
|
309
|
+
export const invertOperation = (operation) => {
|
|
310
|
+
if (operation.type === "insertNode")
|
|
311
|
+
return { type: "removeNode", pos: operation.pos, node: cloneNode(operation.node) };
|
|
312
|
+
if (operation.type === "removeNode")
|
|
313
|
+
return { type: "insertNode", pos: operation.pos, node: cloneNode(operation.node) };
|
|
314
|
+
if (operation.type === "replaceNode")
|
|
315
|
+
return { ...operation, before: cloneNode(operation.after), after: cloneNode(operation.before) };
|
|
316
|
+
if (operation.type === "moveNode")
|
|
317
|
+
return { ...operation, from: operation.to, to: operation.from };
|
|
318
|
+
if (operation.type === "splitNode") {
|
|
319
|
+
const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
|
|
320
|
+
return { type: "mergeNode", pos: { path: targetPath.slice(0, -1), offset: targetPath[targetPath.length - 1] + 1 }, depth: operation.depth, retiredId: operation.newId, splitOffset: operation.pos.offset };
|
|
321
|
+
}
|
|
322
|
+
if (operation.type === "mergeNode") {
|
|
323
|
+
const leftPath = [...operation.pos.path, operation.pos.offset - 1];
|
|
324
|
+
return { type: "splitNode", pos: { path: leftPath, offset: operation.splitOffset }, depth: operation.depth, newId: operation.retiredId };
|
|
325
|
+
}
|
|
326
|
+
if (operation.type === "setNodeAttributes")
|
|
327
|
+
return { ...operation, before: structuredClone(operation.after), after: structuredClone(operation.before) };
|
|
328
|
+
if (operation.type === "setNodeType")
|
|
329
|
+
return {
|
|
330
|
+
...operation,
|
|
331
|
+
before: operation.after,
|
|
332
|
+
after: operation.before,
|
|
333
|
+
beforeAttrs: structuredClone(operation.afterAttrs),
|
|
334
|
+
afterAttrs: structuredClone(operation.beforeAttrs),
|
|
335
|
+
};
|
|
336
|
+
if (operation.type === "insertText")
|
|
337
|
+
return { type: "deleteText", pos: operation.pos, text: operation.text, ...(operation.marks ? { marks: structuredClone(operation.marks) } : {}) };
|
|
338
|
+
if (operation.type === "deleteText")
|
|
339
|
+
return { type: "insertText", pos: operation.pos, text: operation.text, ...(operation.marks ? { marks: structuredClone(operation.marks) } : {}) };
|
|
340
|
+
if (operation.type === "addMark")
|
|
341
|
+
return { type: "removeMark", range: operation.range, mark: structuredClone(operation.mark) };
|
|
342
|
+
return { type: "addMark", range: operation.range, mark: structuredClone(operation.mark) };
|
|
343
|
+
};
|
|
344
|
+
export const applyOperations = (document, operations) => {
|
|
345
|
+
const session = new PathCopySession(document);
|
|
346
|
+
let identityChanged = false;
|
|
347
|
+
operations.forEach((operation) => { identityChanged = applyToSession(session, operation) || identityChanged; });
|
|
348
|
+
const result = session.document();
|
|
349
|
+
if (identityChanged)
|
|
350
|
+
assertUniqueNodeIds(result);
|
|
351
|
+
return result;
|
|
352
|
+
};
|
|
353
|
+
export const invertOperations = (operations) => [...operations].reverse().map(invertOperation);
|
|
354
|
+
const shiftPath = (pos, parent, delta) => {
|
|
355
|
+
if (!samePath(pos.path.slice(0, parent.path.length), parent.path) || pos.path.length <= parent.path.length)
|
|
356
|
+
return pos;
|
|
357
|
+
const path = [...pos.path];
|
|
358
|
+
const index = path[parent.path.length];
|
|
359
|
+
if (index >= parent.offset)
|
|
360
|
+
path[parent.path.length] += delta;
|
|
361
|
+
return { ...pos, path };
|
|
362
|
+
};
|
|
363
|
+
export const mapPosThroughOperation = (pos, operation, bias = 1) => {
|
|
364
|
+
if (operation.type === "insertNode") {
|
|
365
|
+
if (samePath(pos.path, operation.pos.path) && pos.offset === operation.pos.offset)
|
|
366
|
+
return { pos: { ...pos, offset: pos.offset + (bias > 0 ? 1 : 0) }, deleted: false };
|
|
367
|
+
// A second position/operation at the *same* parent, past the insertion
|
|
368
|
+
// point, needs its own offset bumped directly - shiftPath only handles a
|
|
369
|
+
// position strictly *deeper* than the insertion point (pos.path.length >
|
|
370
|
+
// parent.path.length), so a same-depth sibling reference (e.g. another
|
|
371
|
+
// insertNode's own .pos.offset into the identical array) fell through
|
|
372
|
+
// unshifted. removeNode's branch below already has this same-path check;
|
|
373
|
+
// insertNode's was missing it - see the TP1 property test that caught
|
|
374
|
+
// this (two sibling insertNode ops at offsets 2 and 5 diverged depending
|
|
375
|
+
// on transform order).
|
|
376
|
+
if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset)
|
|
377
|
+
return { pos: { ...pos, offset: pos.offset + 1 }, deleted: false };
|
|
378
|
+
return { pos: shiftPath(pos, operation.pos, 1), deleted: false };
|
|
379
|
+
}
|
|
380
|
+
if (operation.type === "removeNode") {
|
|
381
|
+
const removedPath = [...operation.pos.path, operation.pos.offset];
|
|
382
|
+
if (samePath(pos.path.slice(0, removedPath.length), removedPath))
|
|
383
|
+
return { pos: { path: [...operation.pos.path], offset: operation.pos.offset }, deleted: true };
|
|
384
|
+
if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset)
|
|
385
|
+
return { pos: { ...pos, offset: pos.offset - 1 }, deleted: false };
|
|
386
|
+
return { pos: shiftPath(pos, operation.pos, -1), deleted: false };
|
|
387
|
+
}
|
|
388
|
+
if (operation.type === "replaceNode") {
|
|
389
|
+
const replaced = [...operation.pos.path, operation.pos.offset];
|
|
390
|
+
if (samePath(pos.path.slice(0, replaced.length), replaced)
|
|
391
|
+
&& !isTextNode(operation.before) && !isTextNode(operation.after)
|
|
392
|
+
&& operation.before.id === operation.after.id) {
|
|
393
|
+
const relative = pos.path.slice(replaced.length);
|
|
394
|
+
let owner = operation.before;
|
|
395
|
+
for (const index of relative)
|
|
396
|
+
owner = !owner || isTextNode(owner) ? undefined : owner.children?.[index];
|
|
397
|
+
if (owner && !isTextNode(owner)) {
|
|
398
|
+
const find = (node, id, path = []) => {
|
|
399
|
+
if (!isTextNode(node) && node.id === id)
|
|
400
|
+
return path;
|
|
401
|
+
if (isTextNode(node))
|
|
402
|
+
return null;
|
|
403
|
+
for (let index = 0; index < (node.children?.length || 0); index += 1) {
|
|
404
|
+
const found = find(node.children[index], id, [...path, index]);
|
|
405
|
+
if (found)
|
|
406
|
+
return found;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
};
|
|
410
|
+
const nextRelative = find(operation.after, owner.id);
|
|
411
|
+
if (nextRelative)
|
|
412
|
+
return { pos: { path: [...replaced, ...nextRelative], offset: pos.offset }, deleted: false };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return samePath(pos.path.slice(0, replaced.length), replaced)
|
|
416
|
+
? { pos: { path: [...operation.pos.path], offset: operation.pos.offset + (bias > 0 ? 1 : 0) }, deleted: true }
|
|
417
|
+
: { pos, deleted: false };
|
|
418
|
+
}
|
|
419
|
+
if (operation.type === "moveNode") {
|
|
420
|
+
const sourcePath = [...operation.from.path, operation.from.offset];
|
|
421
|
+
if (samePath(pos.path.slice(0, sourcePath.length), sourcePath)) {
|
|
422
|
+
return { pos: { ...pos, path: [...operation.to.path, operation.to.offset, ...pos.path.slice(sourcePath.length)] }, deleted: false };
|
|
423
|
+
}
|
|
424
|
+
const removed = mapPosThroughOperation(pos, { type: "removeNode", pos: operation.from, node: { type: "unknown", id: operation.nodeId } }, bias);
|
|
425
|
+
return mapPosThroughOperation(removed.pos, { type: "insertNode", pos: operation.to, node: { type: "unknown", id: operation.nodeId } }, bias);
|
|
426
|
+
}
|
|
427
|
+
if (operation.type === "splitNode") {
|
|
428
|
+
const targetPath = operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
|
|
429
|
+
const parentPath = targetPath.slice(0, -1);
|
|
430
|
+
const targetIndex = targetPath[targetPath.length - 1];
|
|
431
|
+
// Exact-tie case (bias>0 mirrors insertNode's/insertText's own tie
|
|
432
|
+
// handling above): two concurrent splitNode ops at the identical
|
|
433
|
+
// boundary had no resolution at all before this - pos.offset ===
|
|
434
|
+
// operation.pos.offset satisfied neither this branch (originally
|
|
435
|
+
// `>` only) nor any other, silently falling through to "unchanged" at
|
|
436
|
+
// the bottom of this function, which produced a swapped-identity
|
|
437
|
+
// divergence (found via a TP1 property test: whichever split ends up
|
|
438
|
+
// empty differs depending on transform order). Merging the tie into
|
|
439
|
+
// this existing shift condition, rather than adding a separate branch,
|
|
440
|
+
// keeps the same formula for both the tie and the already-correct
|
|
441
|
+
// greater-than case.
|
|
442
|
+
if (samePath(pos.path, targetPath) && (pos.offset > operation.pos.offset || (pos.offset === operation.pos.offset && bias > 0))) {
|
|
443
|
+
return { pos: { path: [...parentPath, targetIndex + 1], offset: pos.offset - operation.pos.offset }, deleted: false };
|
|
444
|
+
}
|
|
445
|
+
if (samePath(pos.path, parentPath) && pos.offset > targetIndex) {
|
|
446
|
+
return { pos: { ...pos, offset: pos.offset + 1 }, deleted: false };
|
|
447
|
+
}
|
|
448
|
+
if (samePath(pos.path.slice(0, parentPath.length), parentPath) && pos.path.length > parentPath.length && pos.path[parentPath.length] > targetIndex) {
|
|
449
|
+
const path = [...pos.path];
|
|
450
|
+
path[parentPath.length] += 1;
|
|
451
|
+
return { pos: { ...pos, path }, deleted: false };
|
|
452
|
+
}
|
|
453
|
+
if (samePath(pos.path.slice(0, targetPath.length), targetPath) && pos.path.length > targetPath.length) {
|
|
454
|
+
const childIndex = pos.path[targetPath.length];
|
|
455
|
+
if (childIndex >= operation.pos.offset) {
|
|
456
|
+
return { pos: { ...pos, path: [...parentPath, targetIndex + 1, childIndex - operation.pos.offset, ...pos.path.slice(targetPath.length + 1)] }, deleted: false };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (operation.type === "mergeNode") {
|
|
461
|
+
const rightPath = [...operation.pos.path, operation.pos.offset];
|
|
462
|
+
const leftPath = [...operation.pos.path, operation.pos.offset - 1];
|
|
463
|
+
if (samePath(pos.path, rightPath)) {
|
|
464
|
+
return { pos: { path: leftPath, offset: operation.splitOffset + pos.offset }, deleted: false };
|
|
465
|
+
}
|
|
466
|
+
if (samePath(pos.path.slice(0, rightPath.length), rightPath) && pos.path.length > rightPath.length) {
|
|
467
|
+
return { pos: { ...pos, path: [...leftPath, operation.splitOffset + pos.path[rightPath.length], ...pos.path.slice(rightPath.length + 1)] }, deleted: false };
|
|
468
|
+
}
|
|
469
|
+
if (samePath(pos.path, operation.pos.path) && pos.offset > operation.pos.offset) {
|
|
470
|
+
return { pos: { ...pos, offset: pos.offset - 1 }, deleted: false };
|
|
471
|
+
}
|
|
472
|
+
if (samePath(pos.path.slice(0, operation.pos.path.length), operation.pos.path) && pos.path.length > operation.pos.path.length && pos.path[operation.pos.path.length] > operation.pos.offset) {
|
|
473
|
+
const path = [...pos.path];
|
|
474
|
+
path[operation.pos.path.length] -= 1;
|
|
475
|
+
return { pos: { ...pos, path }, deleted: false };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (operation.type === "insertText" && samePath(pos.path, operation.pos.path)) {
|
|
479
|
+
if (pos.offset > operation.pos.offset || (pos.offset === operation.pos.offset && bias > 0))
|
|
480
|
+
return { pos: { ...pos, offset: pos.offset + operation.text.length }, deleted: false };
|
|
481
|
+
}
|
|
482
|
+
if (operation.type === "deleteText" && samePath(pos.path, operation.pos.path)) {
|
|
483
|
+
const end = operation.pos.offset + operation.text.length;
|
|
484
|
+
if (pos.offset > operation.pos.offset && pos.offset <= end)
|
|
485
|
+
return { pos: { ...pos, offset: operation.pos.offset }, deleted: true };
|
|
486
|
+
if (pos.offset > end)
|
|
487
|
+
return { pos: { ...pos, offset: pos.offset - operation.text.length }, deleted: false };
|
|
488
|
+
}
|
|
489
|
+
return { pos: { path: [...pos.path], offset: pos.offset }, deleted: false };
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* `tieBreakBias` only affects the exact-tie case (operation's own structural
|
|
493
|
+
* position coincides exactly with `through`'s insertion point - e.g. two
|
|
494
|
+
* concurrent insertNode ops at the identical offset) - every other case's
|
|
495
|
+
* bias is unaffected. Defaults to 1 (through's content sorts first),
|
|
496
|
+
* preserving this function's existing, already-tested behavior exactly for
|
|
497
|
+
* every caller that doesn't pass it. A concurrent-rebase caller should
|
|
498
|
+
* derive this from a total order over the two operations' authors (e.g.
|
|
499
|
+
* lexicographic authorId comparison) so both transform directions agree:
|
|
500
|
+
* mapOperation(A, B, {tieBreakBias: A-sorts-first ? -1 : 1}) and
|
|
501
|
+
* mapOperation(B, A, {tieBreakBias: B-sorts-first ? -1 : 1}) must reach the
|
|
502
|
+
* same final order regardless of which one is "operation" and which is
|
|
503
|
+
* "through". This does not affect the addMark/removeMark range-endpoint
|
|
504
|
+
* biases below (-1/+1), which serve an unrelated purpose (range-growth
|
|
505
|
+
* semantics at an insertion boundary) and are deliberately left hardcoded.
|
|
506
|
+
*/
|
|
507
|
+
export const mapOperation = (operation, through, options = {}) => {
|
|
508
|
+
// tieBreakBias only overrides the default when this is a genuine tie
|
|
509
|
+
// between two operations of the *same* kind competing for the identical
|
|
510
|
+
// insertion point (insertNode-vs-insertNode, insertText-vs-insertText).
|
|
511
|
+
// For every other pairing (e.g. removeNode vs. insertNode at a
|
|
512
|
+
// numerically coincidental offset), the default (1) is always correct
|
|
513
|
+
// regardless of authorship - removeNode identifies its target by node
|
|
514
|
+
// identity, not by an ordering competition, so "which one sorts first"
|
|
515
|
+
// has no meaning there; forcing the override through anyway (confirmed
|
|
516
|
+
// via a TP1 property test) shifted a removeNode's target to the wrong
|
|
517
|
+
// slot depending on which author happened to sort first.
|
|
518
|
+
const isGenuineTie = operation.type === through.type && (operation.type === "insertNode" || operation.type === "insertText" || operation.type === "splitNode");
|
|
519
|
+
const structuralBias = isGenuineTie ? options.tieBreakBias ?? 1 : 1;
|
|
520
|
+
const map = (pos, bias = structuralBias) => mapPosThroughOperation(pos, through, bias);
|
|
521
|
+
if (operation.type === "insertNode" || operation.type === "removeNode" || operation.type === "replaceNode") {
|
|
522
|
+
// removeNode/replaceNode's `.pos` addresses an *occupied* slot (the
|
|
523
|
+
// node currently there); insertNode's `.pos` addresses a *gap* (the
|
|
524
|
+
// boundary before an index) - two structurally different meanings that
|
|
525
|
+
// happen to share the same {path, offset} shape. `mapPosThroughOperation`
|
|
526
|
+
// is generic pure position arithmetic with no idea which convention a
|
|
527
|
+
// given `pos` follows, so an exact-offset coincidence between the two
|
|
528
|
+
// conventions is not a real "same target" match (confirmed via a TP1
|
|
529
|
+
// property test: an insertNode and a removeNode at the identical offset
|
|
530
|
+
// are two unrelated actions - "insert before index 4" and "remove index
|
|
531
|
+
// 4" - not a conflict over the same node). This check is therefore
|
|
532
|
+
// scoped here, at the one call site that actually knows both
|
|
533
|
+
// operations' real types, rather than inside the shared, type-blind
|
|
534
|
+
// position mapper - only removeNode-vs-removeNode/replaceNode (both
|
|
535
|
+
// occupied-slot semantics) can mean "the exact same node," e.g. two
|
|
536
|
+
// concurrent removeNode ops targeting the identical node.
|
|
537
|
+
if (operation.type !== "insertNode" && (through.type === "removeNode" || through.type === "replaceNode")
|
|
538
|
+
&& samePath(operation.pos.path, through.pos.path) && operation.pos.offset === through.pos.offset) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const result = map(operation.pos);
|
|
542
|
+
return result.deleted ? null : { ...operation, pos: result.pos };
|
|
543
|
+
}
|
|
544
|
+
if (operation.type === "moveNode") {
|
|
545
|
+
const from = map(operation.from);
|
|
546
|
+
const to = map(operation.to);
|
|
547
|
+
return from.deleted ? null : { ...operation, from: from.pos, to: to.pos };
|
|
548
|
+
}
|
|
549
|
+
if (operation.type === "splitNode" || operation.type === "mergeNode" || operation.type === "setNodeAttributes" || operation.type === "setNodeType" || operation.type === "insertText" || operation.type === "deleteText") {
|
|
550
|
+
const result = map(operation.pos);
|
|
551
|
+
return result.deleted ? null : { ...operation, pos: result.pos };
|
|
552
|
+
}
|
|
553
|
+
const from = map(operation.range.from, -1);
|
|
554
|
+
// `to` used to hardcode bias=1 ("an insertion exactly at this boundary
|
|
555
|
+
// grows the range to include it"), which disagreed with insertText's own,
|
|
556
|
+
// separately-established ground truth: applyToSession's insertText (via
|
|
557
|
+
// splitInlineAt, ~line 96-104 above) puts a plain-marks insertion at an
|
|
558
|
+
// exact run boundary on the *following* side, never absorbing the
|
|
559
|
+
// preceding run's marks - confirmed via docs/bugs/
|
|
560
|
+
// addmark-boundary-bias-inconsistent-with-inserttext.md's TP1 test, which
|
|
561
|
+
// found the two conventions genuinely diverged (not just "look different
|
|
562
|
+
// but agree"). bias=-1 matches that existing, unexamined-until-now
|
|
563
|
+
// resolution: a mark's own `to` endpoint does not grow to include a
|
|
564
|
+
// same-offset insertion, exactly as insertText itself never grows the
|
|
565
|
+
// preceding run to include new unmarked text.
|
|
566
|
+
const to = map(operation.range.to, -1);
|
|
567
|
+
return from.deleted && to.deleted ? null : { ...operation, range: { from: from.pos, to: to.pos } };
|
|
568
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FoundationPlugin } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One FoundationPlugin per built-in feature family. Each is filled in as
|
|
4
|
+
* that family is converted (Phase 10 tasks: marks -> list -> block ->
|
|
5
|
+
* table -> atom) - wrapping its existing, already-tested command functions
|
|
6
|
+
* with the required description/examples metadata, not rewriting them.
|
|
7
|
+
* Families not yet converted still contribute their schema (so foundationSchema
|
|
8
|
+
* stays complete) with an empty command set.
|
|
9
|
+
*/
|
|
10
|
+
export declare const builtInPlugins: readonly FoundationPlugin[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { blockNodeSpecs } from "../block/schema.js";
|
|
2
|
+
import { blockPluginCommands, blockPluginShortcuts, blockToolbarContributions } from "../block/plugin.js";
|
|
3
|
+
import { listNodeSpecs } from "../list/schema.js";
|
|
4
|
+
import { listPluginCommands, listPluginShortcuts } from "../list/plugin.js";
|
|
5
|
+
import { tableNodeSpecs } from "../table/schema.js";
|
|
6
|
+
import { tablePluginCommands, tableContextMenuContributions } from "../table/plugin.js";
|
|
7
|
+
import { atomNodeSpecs } from "../atom/schema.js";
|
|
8
|
+
import { atomPluginCommands } from "../atom/plugin.js";
|
|
9
|
+
import { markPluginCommands, markToolbarContributions, inlineMarkSpecs } from "../marks/plugin.js";
|
|
10
|
+
/**
|
|
11
|
+
* One FoundationPlugin per built-in feature family. Each is filled in as
|
|
12
|
+
* that family is converted (Phase 10 tasks: marks -> list -> block ->
|
|
13
|
+
* table -> atom) - wrapping its existing, already-tested command functions
|
|
14
|
+
* with the required description/examples metadata, not rewriting them.
|
|
15
|
+
* Families not yet converted still contribute their schema (so foundationSchema
|
|
16
|
+
* stays complete) with an empty command set.
|
|
17
|
+
*/
|
|
18
|
+
export const builtInPlugins = [
|
|
19
|
+
{
|
|
20
|
+
id: "marks", version: "1.0.0", schema: { marks: inlineMarkSpecs }, commands: markPluginCommands,
|
|
21
|
+
toolbar: markToolbarContributions,
|
|
22
|
+
},
|
|
23
|
+
{ id: "list", version: "1.0.0", schema: { nodes: listNodeSpecs }, commands: listPluginCommands, keyboardShortcuts: listPluginShortcuts },
|
|
24
|
+
{ id: "block", version: "1.0.0", schema: { nodes: blockNodeSpecs }, commands: blockPluginCommands, toolbar: blockToolbarContributions, keyboardShortcuts: blockPluginShortcuts },
|
|
25
|
+
{ id: "table", version: "1.0.0", schema: { nodes: tableNodeSpecs }, commands: tablePluginCommands, contextMenu: tableContextMenuContributions },
|
|
26
|
+
{ id: "atom", version: "1.0.0", schema: { nodes: atomNodeSpecs }, commands: atomPluginCommands },
|
|
27
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SmartOperation } from "../types.js";
|
|
2
|
+
import type { KeyboardShortcutContribution } from "./types.js";
|
|
3
|
+
export interface KeyLike {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly metaKey?: boolean;
|
|
6
|
+
readonly ctrlKey?: boolean;
|
|
7
|
+
readonly altKey?: boolean;
|
|
8
|
+
readonly shiftKey?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tries every key-matching shortcut contribution in priority order (ties
|
|
12
|
+
* broken by registration order, which is what PluginRegistry.keyboardShortcuts
|
|
13
|
+
* already provides), invoking `tryShortcut` for each and returning the
|
|
14
|
+
* first that actually produces operations.
|
|
15
|
+
*
|
|
16
|
+
* This is a fallback chain, not a static "one scope-kind owns this key"
|
|
17
|
+
* lookup, because ScopeKind alone cannot distinguish every case a real
|
|
18
|
+
* shortcut cares about (e.g. "inside a code block" and "inside a plain
|
|
19
|
+
* paragraph" are both "block-range" - see scope/resolveScope.ts). It
|
|
20
|
+
* reproduces surface/input.ts's actual current Tab precedence (code-block
|
|
21
|
+
* wins if applicable, then list unless inTable, then table/native
|
|
22
|
+
* fallthrough if nothing applies) by asking each candidate, in order,
|
|
23
|
+
* "does this genuinely apply here" via `tryShortcut` - which resolves the
|
|
24
|
+
* scope the contribution declares and runs its command - rather than by
|
|
25
|
+
* pre-filtering on a single precomputed scope-kind label. `scopeKinds` on
|
|
26
|
+
* a contribution remains meaningful as declared metadata for callers
|
|
27
|
+
* (which `resolveScope({want})` to attempt) and for doc generation, it's
|
|
28
|
+
* just no longer what this function filters by directly.
|
|
29
|
+
*/
|
|
30
|
+
export declare const resolveShortcut: (shortcuts: readonly KeyboardShortcutContribution[], event: KeyLike, tryShortcut: (shortcut: KeyboardShortcutContribution) => readonly SmartOperation[] | null) => {
|
|
31
|
+
shortcut: KeyboardShortcutContribution;
|
|
32
|
+
operations: readonly SmartOperation[];
|
|
33
|
+
} | null;
|