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,122 @@
|
|
|
1
|
+
import type { ResolvedPos, SmartDocument, SmartMark, SmartNode, SmartRange, SmartSchema, SmartSelection } from "../types.js";
|
|
2
|
+
export type ScopeRequest = {
|
|
3
|
+
want: "inline-range";
|
|
4
|
+
} | {
|
|
5
|
+
want: "block-range";
|
|
6
|
+
} | {
|
|
7
|
+
want: "container-tree";
|
|
8
|
+
stopAt?: (node: SmartNode) => boolean;
|
|
9
|
+
} | {
|
|
10
|
+
want: "list-selection";
|
|
11
|
+
} | {
|
|
12
|
+
want: "table-grid";
|
|
13
|
+
} | {
|
|
14
|
+
want: "atomic-node";
|
|
15
|
+
} | {
|
|
16
|
+
want: "describe";
|
|
17
|
+
};
|
|
18
|
+
export type ScopeKind = "inline-range" | "block-range" | "container-tree" | "list-selection" | "table-grid" | "atomic-node" | "mixed" | "empty";
|
|
19
|
+
export interface ScopeBase {
|
|
20
|
+
kind: ScopeKind;
|
|
21
|
+
/** Normalized, direction-independent. Always from <= to. */
|
|
22
|
+
range: SmartRange;
|
|
23
|
+
/** Nearest isolating ancestor fully containing this scope, if any. */
|
|
24
|
+
isolatingAncestorId: string | null;
|
|
25
|
+
/** True when the request could not be satisfied exactly and was clamped. */
|
|
26
|
+
clamped: boolean;
|
|
27
|
+
/** Why it was clamped — isolating boundary, atom boundary, document edge. */
|
|
28
|
+
clampReason?: "isolating" | "atomic" | "document-edge" | "schema";
|
|
29
|
+
}
|
|
30
|
+
export interface InlineRun {
|
|
31
|
+
ownerNodeId: string;
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
containsAtoms: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface InlineRangeScope extends ScopeBase {
|
|
37
|
+
kind: "inline-range";
|
|
38
|
+
runs: InlineRun[];
|
|
39
|
+
collapsed: boolean;
|
|
40
|
+
storedMarkAnchor?: ResolvedPos;
|
|
41
|
+
}
|
|
42
|
+
export interface BlockRangeScope extends ScopeBase {
|
|
43
|
+
kind: "block-range";
|
|
44
|
+
blockIds: string[];
|
|
45
|
+
promotedFromPartial: boolean;
|
|
46
|
+
commonParentId: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface ContainerTreeScope extends ScopeBase {
|
|
49
|
+
kind: "container-tree";
|
|
50
|
+
rootId: string;
|
|
51
|
+
nodeIds: string[];
|
|
52
|
+
promotedFromPartial: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface ListSelectionScope extends ScopeBase {
|
|
55
|
+
kind: "list-selection";
|
|
56
|
+
listId: string;
|
|
57
|
+
items: {
|
|
58
|
+
itemId: string;
|
|
59
|
+
depth: number;
|
|
60
|
+
hasChildList: boolean;
|
|
61
|
+
}[];
|
|
62
|
+
partialSubtree: boolean;
|
|
63
|
+
promotedFromPartial: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface TableGridScope extends ScopeBase {
|
|
66
|
+
kind: "table-grid";
|
|
67
|
+
tableId: string;
|
|
68
|
+
rect: {
|
|
69
|
+
top: number;
|
|
70
|
+
left: number;
|
|
71
|
+
bottom: number;
|
|
72
|
+
right: number;
|
|
73
|
+
};
|
|
74
|
+
cellIds: string[];
|
|
75
|
+
coveredCellIds: string[];
|
|
76
|
+
rectangular: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface AtomicNodeScope extends ScopeBase {
|
|
79
|
+
kind: "atomic-node";
|
|
80
|
+
nodeId: string;
|
|
81
|
+
inline: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface MixedScope extends ScopeBase {
|
|
84
|
+
kind: "mixed";
|
|
85
|
+
parts: ResolvedScope[];
|
|
86
|
+
}
|
|
87
|
+
export interface EmptyScope extends ScopeBase {
|
|
88
|
+
kind: "empty";
|
|
89
|
+
}
|
|
90
|
+
export type ResolvedScope = InlineRangeScope | BlockRangeScope | ContainerTreeScope | ListSelectionScope | TableGridScope | AtomicNodeScope | MixedScope | EmptyScope;
|
|
91
|
+
export interface SelectionDescription {
|
|
92
|
+
blockTypes: string[];
|
|
93
|
+
marks: {
|
|
94
|
+
mark: SmartMark;
|
|
95
|
+
coverage: "all" | "partial";
|
|
96
|
+
}[];
|
|
97
|
+
inList: {
|
|
98
|
+
listId: string;
|
|
99
|
+
depth: number;
|
|
100
|
+
} | null;
|
|
101
|
+
inTable: {
|
|
102
|
+
tableId: string;
|
|
103
|
+
cellId: string;
|
|
104
|
+
} | null;
|
|
105
|
+
isolatingAncestorId: string | null;
|
|
106
|
+
atoms: string[];
|
|
107
|
+
collapsed: boolean;
|
|
108
|
+
spansIsolatingBoundary: boolean;
|
|
109
|
+
}
|
|
110
|
+
export type ScopeResult = ResolvedScope | SelectionDescription;
|
|
111
|
+
export interface PositionLookup {
|
|
112
|
+
positionOf(nodeId: string): ResolvedPos | null;
|
|
113
|
+
rangeOf(nodeId: string): SmartRange | null;
|
|
114
|
+
contentRangeOf(nodeId: string): SmartRange | null;
|
|
115
|
+
exists(nodeId: string): boolean;
|
|
116
|
+
}
|
|
117
|
+
export interface ScopeIndex {
|
|
118
|
+
resolve(document: SmartDocument, selection: SmartSelection, request: ScopeRequest, schema: SmartSchema): ScopeResult;
|
|
119
|
+
positions(document: SmartDocument, schema: SmartSchema): PositionLookup;
|
|
120
|
+
readonly liveNodeCount: number;
|
|
121
|
+
}
|
|
122
|
+
export type ResolveScope = (document: SmartDocument, selection: SmartSelection, request: ScopeRequest, schema: SmartSchema) => ScopeResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SafeLinkAttrs {
|
|
2
|
+
href: string;
|
|
3
|
+
target?: string;
|
|
4
|
+
}
|
|
5
|
+
export type NormalizedLinkKind = "url" | "email" | "phone" | "anchor" | "invalid";
|
|
6
|
+
export interface NormalizedLinkResult {
|
|
7
|
+
kind: NormalizedLinkKind;
|
|
8
|
+
href: string | null;
|
|
9
|
+
input: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const normalizeLinkInput: (input: string | undefined | null) => NormalizedLinkResult;
|
|
12
|
+
export declare const sanitizeLinkHref: (href: string | undefined | null) => string | null;
|
|
13
|
+
export declare const sanitizeLinkTarget: (target: string | undefined) => string | undefined;
|
|
14
|
+
export declare const sanitizeLinkAttrs: (input: {
|
|
15
|
+
href?: string;
|
|
16
|
+
target?: string;
|
|
17
|
+
}) => SafeLinkAttrs | null;
|
|
18
|
+
export declare const sanitizeResourceUrl: (input: string | undefined | null, options?: {
|
|
19
|
+
allowBlob?: boolean;
|
|
20
|
+
allowedDataMimeTypes?: ReadonlySet<string>;
|
|
21
|
+
}) => string | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const allowedProtocols = new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
2
|
+
const resourceProtocols = new Set(["http:", "https:"]);
|
|
3
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4
|
+
const bareDomainPattern = /^(?:www\.)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+(?:[/?#][^\s]*)?$/;
|
|
5
|
+
const phonePattern = /^\+?[\d\s().-]{7,}$/;
|
|
6
|
+
const normalizePhone = (value) => {
|
|
7
|
+
const hasLeadingPlus = value.trim().startsWith("+");
|
|
8
|
+
const digits = value.replace(/[^\d]/g, "");
|
|
9
|
+
if (digits.length < 7)
|
|
10
|
+
return null;
|
|
11
|
+
return `${hasLeadingPlus ? "+" : ""}${digits}`;
|
|
12
|
+
};
|
|
13
|
+
export const normalizeLinkInput = (input) => {
|
|
14
|
+
const trimmed = input?.trim() || "";
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return { kind: "invalid", href: null, input: "" };
|
|
17
|
+
if (/^mailto:/i.test(trimmed)) {
|
|
18
|
+
const email = trimmed.slice("mailto:".length).trim();
|
|
19
|
+
const href = emailPattern.test(email) ? `mailto:${email}` : null;
|
|
20
|
+
return { kind: href ? "email" : "invalid", href, input: trimmed };
|
|
21
|
+
}
|
|
22
|
+
if (/^tel:/i.test(trimmed)) {
|
|
23
|
+
const phone = normalizePhone(trimmed.slice("tel:".length));
|
|
24
|
+
const href = phone ? `tel:${phone}` : null;
|
|
25
|
+
return { kind: href ? "phone" : "invalid", href, input: trimmed };
|
|
26
|
+
}
|
|
27
|
+
if (emailPattern.test(trimmed))
|
|
28
|
+
return { kind: "email", href: `mailto:${trimmed}`, input: trimmed };
|
|
29
|
+
const phone = normalizePhone(trimmed);
|
|
30
|
+
if (phone && (trimmed.startsWith("+") || phonePattern.test(trimmed))) {
|
|
31
|
+
return { kind: "phone", href: `tel:${phone}`, input: trimmed };
|
|
32
|
+
}
|
|
33
|
+
if (/^#[A-Za-z][\w:.-]*$/.test(trimmed))
|
|
34
|
+
return { kind: "anchor", href: trimmed, input: trimmed };
|
|
35
|
+
if (bareDomainPattern.test(trimmed))
|
|
36
|
+
return { kind: "url", href: `https://${trimmed}`, input: trimmed };
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(trimmed);
|
|
39
|
+
const href = allowedProtocols.has(parsed.protocol) ? trimmed : null;
|
|
40
|
+
return { kind: href ? "url" : "invalid", href, input: trimmed };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { kind: "invalid", href: null, input: trimmed };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const sanitizeLinkHref = (href) => normalizeLinkInput(href).href;
|
|
47
|
+
export const sanitizeLinkTarget = (target) => target?.trim() || undefined;
|
|
48
|
+
export const sanitizeLinkAttrs = (input) => {
|
|
49
|
+
const href = sanitizeLinkHref(input.href);
|
|
50
|
+
return href ? { href, target: sanitizeLinkTarget(input.target) } : null;
|
|
51
|
+
};
|
|
52
|
+
export const sanitizeResourceUrl = (input, options = {}) => {
|
|
53
|
+
const value = input?.trim() || "";
|
|
54
|
+
if (!value || /[\u0000-\u001f\u007f]/.test(value))
|
|
55
|
+
return null;
|
|
56
|
+
if (/^(?:\/[^/]|\.\.?\/)/.test(value))
|
|
57
|
+
return value;
|
|
58
|
+
// A path-relative resource such as `images/photo.png` is valid persisted
|
|
59
|
+
// content. Reject anything that looks like a scheme so executable protocols
|
|
60
|
+
// cannot disguise themselves as relative paths.
|
|
61
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) && !value.startsWith("//") && !/\s/.test(value))
|
|
62
|
+
return value;
|
|
63
|
+
if (options.allowBlob && /^blob:/i.test(value))
|
|
64
|
+
return value;
|
|
65
|
+
const data = value.match(/^data:([^;,]+)(?:;[^,]*)?,/i);
|
|
66
|
+
if (data)
|
|
67
|
+
return options.allowedDataMimeTypes?.has(data[1].toLowerCase()) ? value : null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(value);
|
|
70
|
+
return resourceProtocols.has(parsed.protocol) ? value : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
2
|
+
import type { SmartDocument, SmartSchema } from "../types.js";
|
|
3
|
+
export interface ContentTargetContext {
|
|
4
|
+
readonly schema: SmartSchema;
|
|
5
|
+
readonly positions: PositionLookup;
|
|
6
|
+
}
|
|
7
|
+
export interface ContentTargetLineageEntry {
|
|
8
|
+
readonly nodeId: string;
|
|
9
|
+
readonly type: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ResolvedContentTarget {
|
|
12
|
+
readonly ownerId: string;
|
|
13
|
+
/** Target-subtree lineage, outermost first and including the owner. */
|
|
14
|
+
readonly lineage: readonly ContentTargetLineageEntry[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the previous visible content owner in logical document order.
|
|
18
|
+
* It descends through the deepest last sibling subtree, climbs across parent
|
|
19
|
+
* boundaries when needed, and never crosses the nearest isolating ancestor.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolvePrecedingContentTarget: (document: SmartDocument, nodeId: string, ctx: ContentTargetContext) => ResolvedContentTarget | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { nodeAtPath } from "../positions.js";
|
|
3
|
+
const isContentOwner = (node, schema) => {
|
|
4
|
+
const expression = schema.nodes[node.type]?.content || "";
|
|
5
|
+
return expression.includes("inline") || expression.includes("text");
|
|
6
|
+
};
|
|
7
|
+
const deepestLastContentOwner = (node, schema, lineage = []) => {
|
|
8
|
+
const nextLineage = [...lineage, { nodeId: node.id, type: node.type }];
|
|
9
|
+
for (let index = (node.children?.length || 0) - 1; index >= 0; index -= 1) {
|
|
10
|
+
const child = node.children?.[index];
|
|
11
|
+
if (!child || isTextNode(child))
|
|
12
|
+
continue;
|
|
13
|
+
const target = deepestLastContentOwner(child, schema, nextLineage);
|
|
14
|
+
if (target)
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
return isContentOwner(node, schema) ? { ownerId: node.id, lineage: nextLineage } : null;
|
|
18
|
+
};
|
|
19
|
+
const nearestIsolatingPath = (document, path, schema) => {
|
|
20
|
+
let result = [];
|
|
21
|
+
for (let depth = 0; depth <= path.length; depth += 1) {
|
|
22
|
+
const candidate = path.slice(0, depth);
|
|
23
|
+
const node = nodeAtPath(document, candidate);
|
|
24
|
+
if (node && !isTextNode(node) && schema.nodes[node.type]?.isolating)
|
|
25
|
+
result = candidate;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the previous visible content owner in logical document order.
|
|
31
|
+
* It descends through the deepest last sibling subtree, climbs across parent
|
|
32
|
+
* boundaries when needed, and never crosses the nearest isolating ancestor.
|
|
33
|
+
*/
|
|
34
|
+
export const resolvePrecedingContentTarget = (document, nodeId, ctx) => {
|
|
35
|
+
const located = ctx.positions.positionOf(nodeId);
|
|
36
|
+
if (!located)
|
|
37
|
+
return null;
|
|
38
|
+
let path = [...located.pos.path, located.pos.offset];
|
|
39
|
+
const isolationPath = nearestIsolatingPath(document, path, ctx.schema);
|
|
40
|
+
while (path.length > isolationPath.length) {
|
|
41
|
+
const parentPath = path.slice(0, -1);
|
|
42
|
+
const index = path[path.length - 1];
|
|
43
|
+
if (index > 0) {
|
|
44
|
+
const sibling = nodeAtPath(document, [...parentPath, index - 1]);
|
|
45
|
+
if (sibling && !isTextNode(sibling))
|
|
46
|
+
return deepestLastContentOwner(sibling, ctx.schema);
|
|
47
|
+
}
|
|
48
|
+
path = parentPath;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
2
|
+
import type { SmartOperation } from "../types.js";
|
|
3
|
+
export interface SiblingMoveContext {
|
|
4
|
+
readonly positions: PositionLookup;
|
|
5
|
+
}
|
|
6
|
+
/** Shared implementation used by block.move and list.move. */
|
|
7
|
+
export declare const moveContiguousSiblings: (nodeIds: readonly string[], direction: "up" | "down", ctx: SiblingMoveContext) => SmartOperation[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
/** Shared implementation used by block.move and list.move. */
|
|
3
|
+
export const moveContiguousSiblings = (nodeIds, direction, ctx) => {
|
|
4
|
+
const located = [...new Set(nodeIds)].map((nodeId) => {
|
|
5
|
+
const resolved = ctx.positions.positionOf(nodeId);
|
|
6
|
+
const node = resolved?.parent.children?.[resolved.pos.offset];
|
|
7
|
+
if (!resolved || !node || isTextNode(node) || node.id !== nodeId)
|
|
8
|
+
return null;
|
|
9
|
+
return { nodeId, parentId: resolved.parent.id, path: resolved.pos.path, index: resolved.pos.offset };
|
|
10
|
+
});
|
|
11
|
+
if (located.some((entry) => !entry))
|
|
12
|
+
return [];
|
|
13
|
+
const entries = located.filter((entry) => Boolean(entry))
|
|
14
|
+
.sort((left, right) => left.index - right.index);
|
|
15
|
+
if (!entries.length || entries.some((entry) => entry.parentId !== entries[0].parentId)
|
|
16
|
+
|| entries.some((entry, index) => index > 0 && entry.index !== entries[index - 1].index + 1))
|
|
17
|
+
return [];
|
|
18
|
+
const first = entries[0].index;
|
|
19
|
+
const last = entries[entries.length - 1].index;
|
|
20
|
+
const parent = ctx.positions.positionOf(entries[0].nodeId).parent;
|
|
21
|
+
if (direction === "up") {
|
|
22
|
+
if (first === 0)
|
|
23
|
+
return [];
|
|
24
|
+
const preceding = parent.children?.[first - 1];
|
|
25
|
+
if (!preceding || isTextNode(preceding))
|
|
26
|
+
return [];
|
|
27
|
+
return [{ type: "moveNode", from: { path: [...entries[0].path], offset: first - 1 }, to: { path: [...entries[0].path], offset: last }, nodeId: preceding.id }];
|
|
28
|
+
}
|
|
29
|
+
if (last >= (parent.children?.length || 0) - 1)
|
|
30
|
+
return [];
|
|
31
|
+
const following = parent.children?.[last + 1];
|
|
32
|
+
if (!following || isTextNode(following))
|
|
33
|
+
return [];
|
|
34
|
+
return [{ type: "moveNode", from: { path: [...entries[0].path], offset: last + 1 }, to: { path: [...entries[0].path], offset: first }, nodeId: following.id }];
|
|
35
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { MarkCommandContext } from "../marks/types.js";
|
|
2
|
+
import type { ResolvedScope } from "../scope/types.js";
|
|
3
|
+
import type { SmartDocument, SmartMark, SmartOperation, SmartPos, SmartRange } from "../types.js";
|
|
4
|
+
import type { SuggestionKind } from "./types.js";
|
|
5
|
+
export interface SuggestSpanParams {
|
|
6
|
+
readonly authorId: string;
|
|
7
|
+
readonly id?: string;
|
|
8
|
+
readonly createdAt?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Builds a single insertText operation carrying a fresh "insert"-kind
|
|
12
|
+
* suggestion mark alongside whatever other marks the caller wants (e.g.
|
|
13
|
+
* storedMarks/bold). The text is inserted for real - track-changes mode
|
|
14
|
+
* means the document contains the real, live content immediately, tagged
|
|
15
|
+
* for later accept/reject, not a deferred change.
|
|
16
|
+
*/
|
|
17
|
+
export declare const suggestInsertOperation: (pos: SmartPos, text: string, params: SuggestSpanParams, extraMarks?: readonly SmartMark[]) => SmartOperation;
|
|
18
|
+
/**
|
|
19
|
+
* Marks a selection's content with a "delete"-kind suggestion instead of
|
|
20
|
+
* actually deleting it - the content stays in the document (rendered
|
|
21
|
+
* struck-through) until the suggestion is accepted. Reuses
|
|
22
|
+
* applyMarkCommand directly: a suggestion mark is, mechanically, just
|
|
23
|
+
* another mark, so the existing per-owner range splitting/exclusion
|
|
24
|
+
* machinery already does exactly what's needed for a selection spanning
|
|
25
|
+
* multiple paragraphs.
|
|
26
|
+
*/
|
|
27
|
+
export declare const suggestDeleteCommand: (document: SmartDocument, scope: ResolvedScope, params: SuggestSpanParams, ctx: MarkCommandContext) => SmartOperation[];
|
|
28
|
+
/**
|
|
29
|
+
* The single-range counterpart to suggestDeleteCommand, for a caller that
|
|
30
|
+
* already has a concrete single-owner SmartRange in hand (e.g. the input
|
|
31
|
+
* pipeline's ambient track-changes mode) and has no ResolvedScope to
|
|
32
|
+
* resolve - one addMark operation, no scope/positions machinery involved.
|
|
33
|
+
*/
|
|
34
|
+
export declare const suggestDeleteRangeOperation: (range: SmartRange, params: SuggestSpanParams) => SmartOperation;
|
|
35
|
+
export interface SuggestionRun {
|
|
36
|
+
readonly ownerNodeId: string;
|
|
37
|
+
readonly range: SmartRange;
|
|
38
|
+
readonly mark: SmartMark;
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Finds every contiguous run of text carrying a suggestion mark with the
|
|
43
|
+
* given id, anywhere in the document - not scoped to a caret position
|
|
44
|
+
* (unlike resolveMarkRun), since accept/reject is driven by a UI marker
|
|
45
|
+
* click, not necessarily a caret inside the run. A suggestion can in
|
|
46
|
+
* principle be split across owners by an intervening structural edit
|
|
47
|
+
* (rare), so this returns one entry per owner it's found in, not
|
|
48
|
+
* assuming a single contiguous span.
|
|
49
|
+
*/
|
|
50
|
+
export declare const findSuggestionRuns: (document: SmartDocument, id: string) => SuggestionRun[];
|
|
51
|
+
/** Accept: "insert" runs keep their text (mark stripped); "delete" runs are actually removed. */
|
|
52
|
+
export declare const acceptSuggestionCommand: (document: SmartDocument, id: string) => SmartOperation[];
|
|
53
|
+
/** Reject: "insert" runs are removed (the proposed addition never happened); "delete" runs keep their text (mark stripped). */
|
|
54
|
+
export declare const rejectSuggestionCommand: (document: SmartDocument, id: string) => SmartOperation[];
|
|
55
|
+
export interface InlineSuggestionSummary {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly authorId: string;
|
|
58
|
+
readonly kind: SuggestionKind;
|
|
59
|
+
readonly createdAt: number;
|
|
60
|
+
/** Concatenation of every run's text sharing this id, in document order - a display preview, not authoritative content. */
|
|
61
|
+
readonly text: string;
|
|
62
|
+
}
|
|
63
|
+
/** One entry per distinct suggestion id found anywhere in the document - the panel-listing counterpart to findSuggestionRuns' by-id lookup. */
|
|
64
|
+
export declare const listInlineSuggestions: (document: SmartDocument) => InlineSuggestionSummary[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createNodeId, isTextNode } from "../identity.js";
|
|
2
|
+
import { comparePos } from "../positions.js";
|
|
3
|
+
import { applyMarkCommand } from "../marks/commands.js";
|
|
4
|
+
const suggestionMark = (kind, params) => ({
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
attrs: { id: params.id || createNodeId(), authorId: params.authorId, kind, createdAt: params.createdAt ?? Date.now() },
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Builds a single insertText operation carrying a fresh "insert"-kind
|
|
10
|
+
* suggestion mark alongside whatever other marks the caller wants (e.g.
|
|
11
|
+
* storedMarks/bold). The text is inserted for real - track-changes mode
|
|
12
|
+
* means the document contains the real, live content immediately, tagged
|
|
13
|
+
* for later accept/reject, not a deferred change.
|
|
14
|
+
*/
|
|
15
|
+
export const suggestInsertOperation = (pos, text, params, extraMarks = []) => ({
|
|
16
|
+
type: "insertText",
|
|
17
|
+
pos,
|
|
18
|
+
text,
|
|
19
|
+
marks: [...extraMarks, suggestionMark("insert", params)],
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Marks a selection's content with a "delete"-kind suggestion instead of
|
|
23
|
+
* actually deleting it - the content stays in the document (rendered
|
|
24
|
+
* struck-through) until the suggestion is accepted. Reuses
|
|
25
|
+
* applyMarkCommand directly: a suggestion mark is, mechanically, just
|
|
26
|
+
* another mark, so the existing per-owner range splitting/exclusion
|
|
27
|
+
* machinery already does exactly what's needed for a selection spanning
|
|
28
|
+
* multiple paragraphs.
|
|
29
|
+
*/
|
|
30
|
+
export const suggestDeleteCommand = (document, scope, params, ctx) => applyMarkCommand(document, scope, { markType: "suggestion", attrs: suggestionMark("delete", params).attrs }, ctx);
|
|
31
|
+
/**
|
|
32
|
+
* The single-range counterpart to suggestDeleteCommand, for a caller that
|
|
33
|
+
* already has a concrete single-owner SmartRange in hand (e.g. the input
|
|
34
|
+
* pipeline's ambient track-changes mode) and has no ResolvedScope to
|
|
35
|
+
* resolve - one addMark operation, no scope/positions machinery involved.
|
|
36
|
+
*/
|
|
37
|
+
export const suggestDeleteRangeOperation = (range, params) => ({
|
|
38
|
+
type: "addMark", range, mark: suggestionMark("delete", params),
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Finds every contiguous run of text carrying a suggestion mark with the
|
|
42
|
+
* given id, anywhere in the document - not scoped to a caret position
|
|
43
|
+
* (unlike resolveMarkRun), since accept/reject is driven by a UI marker
|
|
44
|
+
* click, not necessarily a caret inside the run. A suggestion can in
|
|
45
|
+
* principle be split across owners by an intervening structural edit
|
|
46
|
+
* (rare), so this returns one entry per owner it's found in, not
|
|
47
|
+
* assuming a single contiguous span.
|
|
48
|
+
*/
|
|
49
|
+
export const findSuggestionRuns = (document, id) => {
|
|
50
|
+
const runs = [];
|
|
51
|
+
const visit = (node, path) => {
|
|
52
|
+
if (isTextNode(node))
|
|
53
|
+
return;
|
|
54
|
+
const children = node.children || [];
|
|
55
|
+
if (children.some((child) => isTextNode(child))) {
|
|
56
|
+
let offset = 0;
|
|
57
|
+
let current = null;
|
|
58
|
+
const flush = (to) => {
|
|
59
|
+
if (current)
|
|
60
|
+
runs.push({ ownerNodeId: node.id, range: { from: { path: [...path], offset: current.from }, to: { path: [...path], offset: to } }, mark: current.mark, text: current.text });
|
|
61
|
+
current = null;
|
|
62
|
+
};
|
|
63
|
+
children.forEach((child) => {
|
|
64
|
+
const width = isTextNode(child) ? child.text.length : 1;
|
|
65
|
+
const mark = isTextNode(child) ? child.marks?.find((candidate) => candidate.type === "suggestion" && candidate.attrs?.id === id) : undefined;
|
|
66
|
+
if (mark && isTextNode(child))
|
|
67
|
+
current = current ? { ...current, text: current.text + child.text } : { from: offset, mark, text: child.text };
|
|
68
|
+
else
|
|
69
|
+
flush(offset);
|
|
70
|
+
offset += width;
|
|
71
|
+
});
|
|
72
|
+
flush(offset);
|
|
73
|
+
}
|
|
74
|
+
children.forEach((child, index) => visit(child, [...path, index]));
|
|
75
|
+
};
|
|
76
|
+
visit(document, []);
|
|
77
|
+
return runs;
|
|
78
|
+
};
|
|
79
|
+
// deleteText ops targeting the same owner must run back-to-front, or an
|
|
80
|
+
// earlier deletion shifts the offsets a later one still expects.
|
|
81
|
+
const byDescendingPosition = (a, b) => -comparePos(a.range.from, b.range.from);
|
|
82
|
+
/** Accept: "insert" runs keep their text (mark stripped); "delete" runs are actually removed. */
|
|
83
|
+
export const acceptSuggestionCommand = (document, id) => [...findSuggestionRuns(document, id)].sort(byDescendingPosition).map((run) => run.mark.attrs?.kind === "delete"
|
|
84
|
+
? { type: "deleteText", pos: run.range.from, text: run.text }
|
|
85
|
+
: { type: "removeMark", range: run.range, mark: run.mark });
|
|
86
|
+
/** Reject: "insert" runs are removed (the proposed addition never happened); "delete" runs keep their text (mark stripped). */
|
|
87
|
+
export const rejectSuggestionCommand = (document, id) => [...findSuggestionRuns(document, id)].sort(byDescendingPosition).map((run) => run.mark.attrs?.kind === "insert"
|
|
88
|
+
? { type: "deleteText", pos: run.range.from, text: run.text }
|
|
89
|
+
: { type: "removeMark", range: run.range, mark: run.mark });
|
|
90
|
+
/** One entry per distinct suggestion id found anywhere in the document - the panel-listing counterpart to findSuggestionRuns' by-id lookup. */
|
|
91
|
+
export const listInlineSuggestions = (document) => {
|
|
92
|
+
const summaries = new Map();
|
|
93
|
+
const visit = (node) => {
|
|
94
|
+
if (isTextNode(node))
|
|
95
|
+
return;
|
|
96
|
+
(node.children || []).forEach((child) => {
|
|
97
|
+
if (isTextNode(child)) {
|
|
98
|
+
const mark = child.marks?.find((candidate) => candidate.type === "suggestion");
|
|
99
|
+
if (mark?.attrs) {
|
|
100
|
+
const id = String(mark.attrs.id);
|
|
101
|
+
const existing = summaries.get(id);
|
|
102
|
+
summaries.set(id, {
|
|
103
|
+
id,
|
|
104
|
+
authorId: String(mark.attrs.authorId),
|
|
105
|
+
kind: mark.attrs.kind,
|
|
106
|
+
createdAt: Number(mark.attrs.createdAt),
|
|
107
|
+
text: (existing?.text || "") + child.text,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
visit(child);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
visit(document);
|
|
115
|
+
return [...summaries.values()];
|
|
116
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AnnotationRange } from "../annotations/types.js";
|
|
2
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
3
|
+
import type { SmartDocument, SmartOperation, SmartSchema, SmartTransaction } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* A proposed removal of a whole structural node (a paragraph, list item,
|
|
6
|
+
* table row, ...) - the AnnotationRange half of Phase 12a §2.3's hybrid
|
|
7
|
+
* architecture (2026-08-24 decision). Unlike an inline suggestion (a mark
|
|
8
|
+
* on real text, see inline.ts), the node stays exactly as it is in the
|
|
9
|
+
* live document until accepted - there is no equivalent "tag the node and
|
|
10
|
+
* insert it for real" mechanism for a *new* structural node yet (see the
|
|
11
|
+
* module-level limitation note below); only proposed removal is supported.
|
|
12
|
+
*
|
|
13
|
+
* `range` always anchors both endpoints to the same node id (a whole-node
|
|
14
|
+
* span, offsets undefined) - the exact same shape CommentMarkers/
|
|
15
|
+
* resolveAnnotationRange already render and rebase correctly for whole-
|
|
16
|
+
* node anchors.
|
|
17
|
+
*/
|
|
18
|
+
export interface StructuralSuggestion {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly authorId: string;
|
|
21
|
+
readonly kind: "removeNode";
|
|
22
|
+
readonly range: AnnotationRange;
|
|
23
|
+
readonly createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const structuralSuggestionFromNode: (nodeId: string, authorId: string, opts?: {
|
|
26
|
+
readonly id?: string;
|
|
27
|
+
readonly createdAt?: number;
|
|
28
|
+
}) => StructuralSuggestion;
|
|
29
|
+
/** Accept: actually removes the node. No-op (empty operations) if it no longer resolves - already gone, nothing to do. */
|
|
30
|
+
export declare const acceptStructuralSuggestionCommand: (document: SmartDocument, suggestion: StructuralSuggestion, positions: PositionLookup) => SmartOperation[];
|
|
31
|
+
/**
|
|
32
|
+
* Replays a transaction's operations to keep every suggestion's range
|
|
33
|
+
* anchored, exactly mirroring comments/rebase.ts's
|
|
34
|
+
* rebaseCommentThreadsThroughTransaction (same reasoning: a fresh
|
|
35
|
+
* PositionLookup per operation, reference-identity preserved for untouched
|
|
36
|
+
* suggestions). Kept as a separate, structurally-parallel implementation
|
|
37
|
+
* rather than sharing code with comments/rebase.ts, to avoid touching that
|
|
38
|
+
* already-shipped, already-tested module for this.
|
|
39
|
+
*/
|
|
40
|
+
export declare const rebaseStructuralSuggestionsThroughTransaction: (suggestions: readonly StructuralSuggestion[], beforeDocument: SmartDocument, transaction: SmartTransaction, schema: SmartSchema) => readonly StructuralSuggestion[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createNodeId } from "../identity.js";
|
|
2
|
+
import { applyOperation } from "../operations.js";
|
|
3
|
+
import { createScopeIndex } from "../scope/resolveScope.js";
|
|
4
|
+
import { defaultMergeOrphanPolicy, rebaseAnnotationRange, resolveAnnotationRange } from "../annotations/range.js";
|
|
5
|
+
export const structuralSuggestionFromNode = (nodeId, authorId, opts = {}) => ({
|
|
6
|
+
id: opts.id || createNodeId(),
|
|
7
|
+
authorId,
|
|
8
|
+
kind: "removeNode",
|
|
9
|
+
range: { startId: nodeId, endId: nodeId },
|
|
10
|
+
createdAt: opts.createdAt ?? Date.now(),
|
|
11
|
+
});
|
|
12
|
+
/** Accept: actually removes the node. No-op (empty operations) if it no longer resolves - already gone, nothing to do. */
|
|
13
|
+
export const acceptStructuralSuggestionCommand = (document, suggestion, positions) => {
|
|
14
|
+
const resolvedRange = resolveAnnotationRange(suggestion.range, positions);
|
|
15
|
+
if (!resolvedRange)
|
|
16
|
+
return [];
|
|
17
|
+
const resolved = positions.positionOf(suggestion.range.startId);
|
|
18
|
+
const node = resolved?.parent.children?.[resolved.pos.offset];
|
|
19
|
+
if (!resolved || !node)
|
|
20
|
+
return [];
|
|
21
|
+
return [{ type: "removeNode", pos: resolved.pos, node }];
|
|
22
|
+
};
|
|
23
|
+
// Reject never touches the document - a proposed removal never removed
|
|
24
|
+
// anything, so discarding the StructuralSuggestion record itself (a
|
|
25
|
+
// caller/storage concern, not a core one) is the entire "reject" action.
|
|
26
|
+
/**
|
|
27
|
+
* Replays a transaction's operations to keep every suggestion's range
|
|
28
|
+
* anchored, exactly mirroring comments/rebase.ts's
|
|
29
|
+
* rebaseCommentThreadsThroughTransaction (same reasoning: a fresh
|
|
30
|
+
* PositionLookup per operation, reference-identity preserved for untouched
|
|
31
|
+
* suggestions). Kept as a separate, structurally-parallel implementation
|
|
32
|
+
* rather than sharing code with comments/rebase.ts, to avoid touching that
|
|
33
|
+
* already-shipped, already-tested module for this.
|
|
34
|
+
*/
|
|
35
|
+
export const rebaseStructuralSuggestionsThroughTransaction = (suggestions, beforeDocument, transaction, schema) => {
|
|
36
|
+
if (!suggestions.length || !transaction.operations.length)
|
|
37
|
+
return suggestions;
|
|
38
|
+
let runningDocument = beforeDocument;
|
|
39
|
+
let ranges = suggestions.map((suggestion) => suggestion.range);
|
|
40
|
+
for (const operation of transaction.operations) {
|
|
41
|
+
const positions = createScopeIndex().positions(runningDocument, schema);
|
|
42
|
+
const onOrphan = (range, mergeOp, lookup) => defaultMergeOrphanPolicy(runningDocument)(range, mergeOp, lookup) ?? range;
|
|
43
|
+
ranges = ranges.map((range) => rebaseAnnotationRange(range, operation, positions, onOrphan) ?? range);
|
|
44
|
+
runningDocument = applyOperation(runningDocument, operation);
|
|
45
|
+
}
|
|
46
|
+
return suggestions.map((suggestion, index) => ranges[index] === suggestion.range ? suggestion : { ...suggestion, range: ranges[index] });
|
|
47
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type SuggestionKind = "insert" | "delete";
|
|
2
|
+
/**
|
|
3
|
+
* Attrs carried on the "suggestion" text mark (Phase 12a §2.3, hybrid
|
|
4
|
+
* architecture per user decision 2026-08-24): an inline suggestion is real
|
|
5
|
+
* content in the live document, tagged the same way bold/italic/link are,
|
|
6
|
+
* rather than a separate not-yet-applied proposal. "insert" marks text that
|
|
7
|
+
* was actually inserted, pending acceptance; "delete" marks text that is
|
|
8
|
+
* still present but proposed for removal. This is deliberately NOT a
|
|
9
|
+
* separate SuggestionThread-style out-of-band record the way CommentThread
|
|
10
|
+
* is - reusing the marks pipeline means split/merge/undo/rendering all
|
|
11
|
+
* already work for free (see marks/dom.ts, operations.ts's generic mark
|
|
12
|
+
* carry-through).
|
|
13
|
+
*/
|
|
14
|
+
export interface SuggestionMarkAttrs {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly authorId: string;
|
|
17
|
+
readonly kind: SuggestionKind;
|
|
18
|
+
readonly createdAt: number;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|