@voithos-labs/aragonite 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +18 -0
- package/dist/a11y-strings.js +18 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +35 -24
- package/dist/components/BlockHost.svelte +16 -9
- package/dist/components/Editor.svelte +381 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +17 -3
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +4 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +11 -0
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +243 -29
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
- package/dist/components/blocks/text/live-selection-edit.js +27 -0
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +144 -22
- package/dist/components/drag-handle.d.ts +35 -0
- package/dist/components/drag-handle.js +126 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +10 -6
- package/dist/components/editor-root-listeners.js +23 -22
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +59 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +11 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/widget-offset.d.ts +6 -0
- package/dist/cursor/widget-offset.js +61 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +21 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +11 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +36 -3
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +97 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +49 -24
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +25 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +1 -1
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +32 -30
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +62 -13
- package/dist/selection/dead-space-caret.d.ts +10 -0
- package/dist/selection/dead-space-caret.js +47 -1
- package/dist/selection/double-click-trim.d.ts +17 -0
- package/dist/selection/double-click-trim.js +57 -0
- package/dist/selection/drag-pointer.d.ts +7 -2
- package/dist/selection/drag-pointer.js +61 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.js +1 -1
- package/dist/selection/path-lookup.js +1 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.js +30 -2
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +57 -28
- package/dist/styles/editor.css +217 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +126 -96
- package/docs/guide/plugin-api.md +31 -3
- package/docs/guide/plugin-guide.md +26 -3
- package/package.json +4 -2
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- package/dist/components/blocks/table/table-reorder-drag.js +0 -97
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The separator settle every splice owes its neighbourhood (syntax-tree.md § Blank lines), the
|
|
3
|
+
* seam absorb behind it (editor.md § 8), and the delete primitive built on both. `sharing` owns
|
|
4
|
+
* every in-place write (G1.9); the retire census in `lint/separator-write-doors.test.ts` names
|
|
5
|
+
* every door here.
|
|
6
|
+
*/
|
|
7
|
+
import { isBlankParagraph, parse } from '../core/parser';
|
|
8
|
+
import { trailingLineEnding, trimTrailingLineEnding } from '../core/lines';
|
|
9
|
+
import { devWarn } from '../dev-warn';
|
|
10
|
+
import { assignChildIdsDeep } from '../block-id';
|
|
11
|
+
import { tryGetBlockKindDescriptor } from '../schema/block-kind-descriptor';
|
|
12
|
+
import { dropChildSpans } from '../schema/child-spans';
|
|
13
|
+
import { assertInvariant } from '../assert';
|
|
14
|
+
import { checkStructuralDescriptor } from '../invariants/structural-descriptor';
|
|
15
|
+
import { ensureUnsharedChild } from './unshare';
|
|
16
|
+
import { spliceChildren } from './children';
|
|
17
|
+
import { spliceMany } from './splice-many';
|
|
18
|
+
import { applyStructuralChangeToIdsRefs } from './structural-change';
|
|
19
|
+
import { ensureEditableContainers } from './node-primitives';
|
|
20
|
+
// ── Separators ──
|
|
21
|
+
/**
|
|
22
|
+
* Settle the separator at `index`: nothing needs one at the body head or below a blank block,
|
|
23
|
+
* where the parser would read the extra line as one more empty paragraph.
|
|
24
|
+
*/
|
|
25
|
+
export function clearRedundantSeparator(parent, index, sharing) {
|
|
26
|
+
retireChildSpans(parent);
|
|
27
|
+
const node = parent.children?.[index];
|
|
28
|
+
if (!node || node.leadingTrivia === '')
|
|
29
|
+
return;
|
|
30
|
+
const bodyStart = bodyStartIndex(parent);
|
|
31
|
+
if (index < bodyStart)
|
|
32
|
+
return;
|
|
33
|
+
const predecessor = index > bodyStart ? parent.children[index - 1] : undefined;
|
|
34
|
+
if (predecessor !== undefined && !isBlankParagraph(predecessor))
|
|
35
|
+
return;
|
|
36
|
+
const owned = sharing ? ensureUnsharedChild(parent, index, sharing) : node;
|
|
37
|
+
const freed = owned.leadingTrivia;
|
|
38
|
+
owned.leadingTrivia = '';
|
|
39
|
+
absorbWrapPrefix(parent, bodyStart, index, freed);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A blank block IS a blank line, so it and its follower share ONE separator (G2.13). The
|
|
43
|
+
* follower's stands, so a later fill of this slot still finds the follower separated.
|
|
44
|
+
*/
|
|
45
|
+
export function dropDoubledSeparator(parent, index, sharing) {
|
|
46
|
+
retireChildSpans(parent);
|
|
47
|
+
const node = parent.children?.[index];
|
|
48
|
+
if (!node || node.leadingTrivia === '' || !isBlankParagraph(node))
|
|
49
|
+
return;
|
|
50
|
+
if ((parent.children?.[index + 1]?.leadingTrivia ?? '') === '')
|
|
51
|
+
return;
|
|
52
|
+
const owned = sharing ? ensureUnsharedChild(parent, index, sharing) : node;
|
|
53
|
+
owned.leadingTrivia = '';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The separator a block takes back when it stops being blank: its own blank line was what stood
|
|
57
|
+
* between it and a non-blank predecessor. Call at the fill.
|
|
58
|
+
*/
|
|
59
|
+
export function restoreSeparatorOnFill(parent, index, sharing) {
|
|
60
|
+
retireChildSpans(parent);
|
|
61
|
+
const node = parent.children?.[index];
|
|
62
|
+
if (!node || node.leadingTrivia !== '' || isBlankParagraph(node))
|
|
63
|
+
return;
|
|
64
|
+
mintSeparator(parent, index, sharing);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The separator the block BELOW a consumed blank line takes back: the same mint minus the
|
|
68
|
+
* blank-self guard, declined where its own follower already holds one (G2.13).
|
|
69
|
+
*/
|
|
70
|
+
export function restoreSeparatorAfterBlank(parent, index, sharing) {
|
|
71
|
+
retireChildSpans(parent);
|
|
72
|
+
const children = parent.children;
|
|
73
|
+
const node = children?.[index];
|
|
74
|
+
if (!children || !node || node.leadingTrivia !== '')
|
|
75
|
+
return;
|
|
76
|
+
if (isBlankParagraph(node) && (children[index + 1]?.leadingTrivia ?? '') !== '')
|
|
77
|
+
return;
|
|
78
|
+
mintSeparator(parent, index, sharing);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The settle a block turning INTO a blank line owes: the run it joins carries exactly ONE
|
|
82
|
+
* separating line across every block in it AND its follower. The line already standing is
|
|
83
|
+
* kept; a mint lands at the run's head, the only slot one may take.
|
|
84
|
+
*/
|
|
85
|
+
export function settleSeparatorOnBlank(parent, index, sharing) {
|
|
86
|
+
retireChildSpans(parent);
|
|
87
|
+
const children = parent.children;
|
|
88
|
+
const node = children?.[index];
|
|
89
|
+
if (!children || !node || !isBlankParagraph(node))
|
|
90
|
+
return;
|
|
91
|
+
const bodyStart = bodyStartIndex(parent);
|
|
92
|
+
let start = index;
|
|
93
|
+
while (start > bodyStart && isBlankParagraph(children[start - 1]))
|
|
94
|
+
start--;
|
|
95
|
+
let end = index;
|
|
96
|
+
while (end + 1 < children.length && isBlankParagraph(children[end + 1]))
|
|
97
|
+
end++;
|
|
98
|
+
const standing = [];
|
|
99
|
+
for (let i = start; i <= Math.min(end + 1, children.length - 1); i++) {
|
|
100
|
+
if (children[i].leadingTrivia !== '')
|
|
101
|
+
standing.push(i);
|
|
102
|
+
}
|
|
103
|
+
// A chrome line bounding the run beside PROSE eats one line as the wrap's peel
|
|
104
|
+
// (`innerPrefix`/`innerSuffix`), on top of the run's own count; an all-blank body owes none.
|
|
105
|
+
const wrap = bodyWrapOf(parent);
|
|
106
|
+
const slots = wrapSlotsOf(parent);
|
|
107
|
+
const bodyEnd = children.length - 1;
|
|
108
|
+
// A run of two or more that IS the whole body sits against both chrome lines: the reload
|
|
109
|
+
// peels a line into each slot before it materializes a block, so it owes BOTH.
|
|
110
|
+
const twoPeelBody = !!slots &&
|
|
111
|
+
wrap?.afterOpenerLine === true &&
|
|
112
|
+
wrap.beforeCloserLine === true &&
|
|
113
|
+
start === bodyStart &&
|
|
114
|
+
end === bodyEnd &&
|
|
115
|
+
start < end;
|
|
116
|
+
const tailBelowProse = start > bodyStart && end === bodyEnd;
|
|
117
|
+
if (slots && wrap?.beforeCloserLine && (tailBelowProse || twoPeelBody) && !slots.innerSuffix) {
|
|
118
|
+
slots.innerSuffix = trailingLineEnding(children[end].raw);
|
|
119
|
+
}
|
|
120
|
+
// The reverse: a deletion can leave a lone blank as the WHOLE body, where the closer
|
|
121
|
+
// peel no longer engages beside the opener's, so the run gives the extra line back.
|
|
122
|
+
const loneBlankBody = start === bodyStart && end === bodyEnd && start === end;
|
|
123
|
+
if (slots && loneBlankBody && slots.innerSuffix && (slots.innerPrefix || standing.length > 0)) {
|
|
124
|
+
slots.innerSuffix = '';
|
|
125
|
+
}
|
|
126
|
+
const headUnderWrap = !!slots && wrap?.afterOpenerLine === true && start === bodyStart && end < bodyEnd;
|
|
127
|
+
// A line already standing IS the opener's peel on reload, so taking one as well would add a
|
|
128
|
+
// line; a two-peel body's count comes out of the slots, not out of the run.
|
|
129
|
+
const takesOpenerPeel = twoPeelBody || (headUnderWrap && standing.length === 0);
|
|
130
|
+
if (slots && takesOpenerPeel && !slots.innerPrefix) {
|
|
131
|
+
slots.innerPrefix = trailingLineEnding(children[start].raw);
|
|
132
|
+
}
|
|
133
|
+
// Under the wrap the run keeps exactly one peel line, in `innerPrefix` or still standing;
|
|
134
|
+
// elsewhere a run with no line above it separates from nothing and materializes in full.
|
|
135
|
+
let wanted;
|
|
136
|
+
if (twoPeelBody)
|
|
137
|
+
wanted = 0;
|
|
138
|
+
else if (headUnderWrap)
|
|
139
|
+
wanted = slots?.innerPrefix ? 0 : 1;
|
|
140
|
+
else
|
|
141
|
+
wanted = start > 0 || wrap?.afterOpenerLine ? 1 : 0;
|
|
142
|
+
if (standing.length < wanted) {
|
|
143
|
+
mintSeparator(parent, start, sharing);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
for (const at of standing.slice(wanted)) {
|
|
147
|
+
const owned = sharing ? ensureUnsharedChild(parent, at, sharing) : children[at];
|
|
148
|
+
owned.leadingTrivia = '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
materializeTailSuffix(parent, sharing);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The give-back twin of {@link settleSeparatorOnBlank}'s closer peel: a tail that stops being
|
|
155
|
+
* blank owes the borrowed `innerSuffix` line back, or the wrap emits a line nobody typed.
|
|
156
|
+
*/
|
|
157
|
+
export function releaseWrapPeel(parent, index) {
|
|
158
|
+
retireChildSpans(parent);
|
|
159
|
+
const children = parent.children;
|
|
160
|
+
const slots = wrapSlotsOf(parent);
|
|
161
|
+
if (!children || children.length === 0 || !slots?.innerSuffix)
|
|
162
|
+
return;
|
|
163
|
+
if (!bodyWrapOf(parent)?.beforeCloserLine)
|
|
164
|
+
return;
|
|
165
|
+
if (index < children.length - 1)
|
|
166
|
+
return;
|
|
167
|
+
if (isBlankParagraph(children[children.length - 1]))
|
|
168
|
+
return;
|
|
169
|
+
slots.innerSuffix = '';
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The parse folds the document's one trailing blank line into `suffix` only while the tail block
|
|
173
|
+
* is non-blank; once the tail turns blank the reload reads that line as its own empty paragraph,
|
|
174
|
+
* so the settle materializes it. Document-level slot only. Returns the blocks appended.
|
|
175
|
+
*/
|
|
176
|
+
function materializeTailSuffix(parent, sharing) {
|
|
177
|
+
retireChildSpans(parent);
|
|
178
|
+
const children = parent.children;
|
|
179
|
+
const suffix = parent.suffix;
|
|
180
|
+
if (!children || !suffix)
|
|
181
|
+
return 0;
|
|
182
|
+
// An emptied parent has no tail for the line to fold against, so it is the document's whole
|
|
183
|
+
// content and the reload reads it as the one block there is.
|
|
184
|
+
if (children.length > 0 && !isBlankParagraph(children[children.length - 1]))
|
|
185
|
+
return 0;
|
|
186
|
+
const minted = { kind: 'paragraph', leadingTrivia: '', raw: suffix };
|
|
187
|
+
if (sharing)
|
|
188
|
+
sharing.stamp(minted);
|
|
189
|
+
children.push(minted);
|
|
190
|
+
parent.suffix = '';
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
// ── The splice settle funnel ──
|
|
194
|
+
/**
|
|
195
|
+
* The settle every splice owes its neighbourhood, then the seam question over the window.
|
|
196
|
+
* `removed` is the pre-splice span, the only place was-blank survives a splice; `tracked` rides
|
|
197
|
+
* the folds for a door landing a caret in the spliced bytes.
|
|
198
|
+
*/
|
|
199
|
+
function settleSplicedWindow(parent, at, removed, added, change, sharing, tracked) {
|
|
200
|
+
if (!parent.children)
|
|
201
|
+
return change;
|
|
202
|
+
// Ahead of the arms: `settleSeparatorOnBlank` materializes the tail line itself, so a count
|
|
203
|
+
// read after it would leave that growth outside the window the sink reports.
|
|
204
|
+
const beforeMint = parent.children.length;
|
|
205
|
+
handDownVacatedSeparator(parent, at, removed[0]?.leadingTrivia ?? '', sharing);
|
|
206
|
+
clearRedundantSeparator(parent, at, sharing);
|
|
207
|
+
if (removed.some(isBlankParagraph)) {
|
|
208
|
+
// Both ends: the line was the slot's own AND the one below it stood on.
|
|
209
|
+
restoreSeparatorAfterBlank(parent, at, sharing);
|
|
210
|
+
if (added > 0)
|
|
211
|
+
restoreSeparatorAfterBlank(parent, at + added, sharing);
|
|
212
|
+
releaseWrapPeel(parent, at + Math.max(added - 1, 0));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
settleSeparatorOnBlank(parent, at + Math.max(added - 1, 0), sharing);
|
|
216
|
+
}
|
|
217
|
+
// Unconditional, and the funnel's only home for it: a delete window at the tail probes no
|
|
218
|
+
// slot, and the question is about the parent's LAST block.
|
|
219
|
+
materializeTailSuffix(parent, sharing);
|
|
220
|
+
const widened = widenForTailMint(change, beforeMint, parent.children.length);
|
|
221
|
+
// The seam question is part of SETTLING, not a rule each door carries, so door N+1 inherits
|
|
222
|
+
// it. A byte-shaped write reporting `noop` splices no window and is asked nothing.
|
|
223
|
+
return absorbWindowSeams(parent, at, added, at, widened, sharing, tracked,
|
|
224
|
+
// A one-slot window names the one block whose bytes changed, which is what lets the seam
|
|
225
|
+
// above decline on its first line; a plural one names no single block.
|
|
226
|
+
added === 1 ? at : undefined).change;
|
|
227
|
+
}
|
|
228
|
+
/** Whoever takes the slot inherits its line, having none of its own ({@link deleteNode}'s rule). */
|
|
229
|
+
function handDownVacatedSeparator(parent, at, vacated, sharing) {
|
|
230
|
+
retireChildSpans(parent);
|
|
231
|
+
const heir = parent.children?.[at];
|
|
232
|
+
if (vacated === '' || !heir || heir.leadingTrivia !== '')
|
|
233
|
+
return;
|
|
234
|
+
const owned = sharing ? ensureUnsharedChild(parent, at, sharing) : heir;
|
|
235
|
+
owned.leadingTrivia = vacated;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The commit ceremony's settle door: derive the spliced window from the change and settle it
|
|
239
|
+
* against `before`, the pre-mutate children. Nodes surviving inside the window are not removals,
|
|
240
|
+
* so a coarse descriptor over an in-place write settles as one.
|
|
241
|
+
*/
|
|
242
|
+
export function settleSeparator(parent, before, change, sharing, tracked) {
|
|
243
|
+
const window = splicedWindow(change);
|
|
244
|
+
const children = parent.children;
|
|
245
|
+
if (!window || !children)
|
|
246
|
+
return change;
|
|
247
|
+
// At the funnel's door, ahead of every arm: a window the mutate mis-derived reads `before`
|
|
248
|
+
// out of bounds and hands the arms a negative span, which each would clamp into silence.
|
|
249
|
+
assertInvariant('structural-descriptor', () => checkStructuralDescriptor(change, before.length));
|
|
250
|
+
const survivors = new Set(children.slice(window.at, window.at + window.added));
|
|
251
|
+
const removed = before
|
|
252
|
+
.slice(window.at, window.at + window.removed)
|
|
253
|
+
.filter((node) => !survivors.has(node));
|
|
254
|
+
return settleSplicedWindow(parent, window.at, removed, window.added, change, sharing, tracked);
|
|
255
|
+
}
|
|
256
|
+
function splicedWindow(change) {
|
|
257
|
+
switch (change.op) {
|
|
258
|
+
case 'noop':
|
|
259
|
+
return null;
|
|
260
|
+
case 'insert':
|
|
261
|
+
return { at: change.at, removed: 0, added: change.count };
|
|
262
|
+
case 'delete':
|
|
263
|
+
return { at: change.at, removed: change.count, added: 0 };
|
|
264
|
+
case 'replace':
|
|
265
|
+
return { at: change.at, removed: change.count, added: change.newCount };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The out-of-commit-scope twin of {@link settleSeparator}, for a container discovered by walking
|
|
270
|
+
* the live tree: it splices through the `childIds` door and reads the pre-splice span itself.
|
|
271
|
+
*/
|
|
272
|
+
export function spliceChildrenSettled(parent, at, removeCount, replacement, sharing) {
|
|
273
|
+
const children = parent.children;
|
|
274
|
+
if (!children || at < 0 || at > children.length)
|
|
275
|
+
return;
|
|
276
|
+
const removed = children.slice(at, at + removeCount);
|
|
277
|
+
spliceChildren(parent, at, removeCount, replacement);
|
|
278
|
+
// `noop` in, so what comes back describes the SETTLE alone: `spliceChildren` already carried
|
|
279
|
+
// the door's own splice into `childIds`, and out of commit scope nothing else publishes.
|
|
280
|
+
const settled = settleSplicedWindow(parent, at, removed, replacement.length, { op: 'noop' }, sharing);
|
|
281
|
+
const ids = parent.childIds;
|
|
282
|
+
if (ids)
|
|
283
|
+
applyStructuralChangeToIdsRefs(settled, ids, new Array(ids.length));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* A splice can leave neighbours whose adjacent bytes re-read as fewer blocks on reload. Absorb
|
|
287
|
+
* while the window's own bytes parse to fewer blocks, which is the reload's reading; a blank run
|
|
288
|
+
* is transparent to a container's continuation, so the window anchors at the nearest non-blank
|
|
289
|
+
* block above the seam, never below `floor`, and cascades.
|
|
290
|
+
*/
|
|
291
|
+
export function absorbSeamReading(parent, seamLeft, floor, sharing, tracked, headProbe, onBeforeSplice) {
|
|
292
|
+
const children = parent.children;
|
|
293
|
+
if (seamLeft < 0)
|
|
294
|
+
return { at: 0, span: 0, eaten: 0, spliced: false };
|
|
295
|
+
let left = seamLeft;
|
|
296
|
+
while (left > floor && isBlankParagraph(children[left]))
|
|
297
|
+
left--;
|
|
298
|
+
const at = left;
|
|
299
|
+
let span = seamLeft - at + 1;
|
|
300
|
+
let eaten = 0;
|
|
301
|
+
let spliced = false;
|
|
302
|
+
// Only the first pass: a fold re-tiles the window, so the probe's absolute index is stale.
|
|
303
|
+
let probe = headProbe;
|
|
304
|
+
for (;;) {
|
|
305
|
+
// The candidate edge crosses a blank run too: the absorbed content sits on the
|
|
306
|
+
// run's far side (a list continues into indented code across any number of blanks).
|
|
307
|
+
let right = at + span;
|
|
308
|
+
while (right < children.length && isBlankParagraph(children[right]))
|
|
309
|
+
right++;
|
|
310
|
+
const window = children.slice(at, Math.min(right + 1, children.length));
|
|
311
|
+
if (window.length <= span || window.length < 2)
|
|
312
|
+
break;
|
|
313
|
+
// A context-dependent kind has no standalone reading, so its seam is not askable.
|
|
314
|
+
if (window.some((node) => tryGetBlockKindDescriptor(node.kind)?.contextDependentKind))
|
|
315
|
+
break;
|
|
316
|
+
if (probe !== undefined && declinesOnHeadLine(window, probe - at))
|
|
317
|
+
break;
|
|
318
|
+
probe = undefined;
|
|
319
|
+
const reparsed = parse(joinedWindowBytes(window, window.length), { scope: 'fragment' });
|
|
320
|
+
const blocks = reparsed.children;
|
|
321
|
+
if (blocks.length === 0 || blocks.length >= window.length)
|
|
322
|
+
break;
|
|
323
|
+
// A fold may PROMOTE the head past what its bytes carry alone (a paragraph under the
|
|
324
|
+
// setext underline below it), so what must survive is the head's own reading, not its kind.
|
|
325
|
+
if (blocks[0].kind !== window[0].kind && !readsAsItselfAlone(window[0]))
|
|
326
|
+
break;
|
|
327
|
+
onBeforeSplice?.();
|
|
328
|
+
absorbFragmentPeel(parent, at + window.length, reparsed.suffix, blocks, sharing);
|
|
329
|
+
blocks[0].leadingTrivia = window[0].leadingTrivia;
|
|
330
|
+
for (const block of blocks) {
|
|
331
|
+
ensureEditableContainers(block);
|
|
332
|
+
if (sharing)
|
|
333
|
+
sharing.stamp(block);
|
|
334
|
+
assignChildIdsDeep(block);
|
|
335
|
+
}
|
|
336
|
+
if (tracked)
|
|
337
|
+
retrackThroughFold(tracked, at, window, blocks);
|
|
338
|
+
spliceMany(children, at, window.length, blocks);
|
|
339
|
+
eaten += window.length - blocks.length;
|
|
340
|
+
span = blocks.length;
|
|
341
|
+
spliced = true;
|
|
342
|
+
}
|
|
343
|
+
return { at, span, eaten, spliced };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Whether a block's own bytes read back as that block. A structured container's children fail
|
|
347
|
+
* this by construction (one list item's bytes read as a LIST), which is how a scope whose
|
|
348
|
+
* children a document parse does NOT reproduce declines the seam question.
|
|
349
|
+
*/
|
|
350
|
+
function readsAsItselfAlone(node) {
|
|
351
|
+
const alone = parse(node.raw, { scope: 'fragment' }).children;
|
|
352
|
+
return alone.length === 1 && alone[0].kind === node.kind;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Decline-only pre-parse for a window whose LAST member is the block that changed: join the
|
|
356
|
+
* others with only that block's first line. Block parsing is a left-to-right line scan, so a
|
|
357
|
+
* block opening here opens in the full join too; a pass falls through to the real parse.
|
|
358
|
+
*/
|
|
359
|
+
function declinesOnHeadLine(window, member) {
|
|
360
|
+
if (member <= 0 || member !== window.length - 1)
|
|
361
|
+
return false;
|
|
362
|
+
const raw = window[member].raw;
|
|
363
|
+
const nl = raw.indexOf('\n');
|
|
364
|
+
const joined = joinedWindowBytes(window, member) +
|
|
365
|
+
window[member].leadingTrivia +
|
|
366
|
+
(nl < 0 ? raw : raw.slice(0, nl + 1));
|
|
367
|
+
return parse(joined, { scope: 'fragment' }).children.length >= window.length;
|
|
368
|
+
}
|
|
369
|
+
/** How a fold reads a window: the head's raw, then each of the next `count - 1` members'
|
|
370
|
+
* leading trivia and raw. */
|
|
371
|
+
function joinedWindowBytes(window, count) {
|
|
372
|
+
let joined = window[0].raw;
|
|
373
|
+
for (let i = 1; i < count; i++)
|
|
374
|
+
joined += window[i].leadingTrivia + window[i].raw;
|
|
375
|
+
return joined;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Where a byte position inside the folded window lands: the fold's own reparse re-tiles the
|
|
379
|
+
* joined bytes, and a position past the window only shifts by what the fold ate.
|
|
380
|
+
*/
|
|
381
|
+
function retrackThroughFold(tracked, at, window, blocks) {
|
|
382
|
+
const member = tracked.index - at;
|
|
383
|
+
if (member < 0)
|
|
384
|
+
return;
|
|
385
|
+
if (member >= window.length) {
|
|
386
|
+
tracked.index -= window.length - blocks.length;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// The offset walk mirrors {@link joinedWindowBytes}.
|
|
390
|
+
let joined = tracked.offset;
|
|
391
|
+
for (let i = 0; i < member; i++) {
|
|
392
|
+
joined += (i === 0 ? 0 : window[i].leadingTrivia.length) + window[i].raw.length;
|
|
393
|
+
}
|
|
394
|
+
if (member > 0)
|
|
395
|
+
joined += window[member].leadingTrivia.length;
|
|
396
|
+
const landed = focusTargetInReplacement(blocks, joined);
|
|
397
|
+
tracked.index = at + landed.index;
|
|
398
|
+
tracked.offset = landed.offset;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Map a post-edit caret offset (in the committed text) to the parsed block it falls in, as a
|
|
402
|
+
* local display offset. An offset inside inter-block trivia lands at the next block's start;
|
|
403
|
+
* past-the-end clamps to the last.
|
|
404
|
+
*/
|
|
405
|
+
export function focusTargetInReplacement(nodes, offset) {
|
|
406
|
+
let pos = 0;
|
|
407
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
408
|
+
const bodyStart = i === 0 ? 0 : pos + nodes[i].leadingTrivia.length;
|
|
409
|
+
const bodyEnd = bodyStart + trimTrailingLineEnding(nodes[i].raw).length;
|
|
410
|
+
if (offset <= bodyEnd) {
|
|
411
|
+
return { index: i, offset: Math.max(0, offset - bodyStart) };
|
|
412
|
+
}
|
|
413
|
+
pos = bodyStart + nodes[i].raw.length;
|
|
414
|
+
}
|
|
415
|
+
const last = nodes.length - 1;
|
|
416
|
+
return { index: last, offset: trimTrailingLineEnding(nodes[last].raw).length };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The seam question at every join the splice at `at` disturbed, its window's two edges and the
|
|
420
|
+
* joins inside it, since a move can invalidate a join that was already correct. Each fold
|
|
421
|
+
* cascades downward. `headProbe` names the one block whose bytes changed, letting each ask
|
|
422
|
+
* decline on its first line alone; dropped once anything folds, since its index has moved.
|
|
423
|
+
*/
|
|
424
|
+
export function absorbWindowSeams(parent, at, added, landing, change, sharing, tracked, headProbe, onBeforeSplice) {
|
|
425
|
+
let settled = null;
|
|
426
|
+
let moved = landing;
|
|
427
|
+
let seamLeft = at - 1;
|
|
428
|
+
let last = at + added - 1;
|
|
429
|
+
while (seamLeft <= last) {
|
|
430
|
+
const seam = absorbSeamReading(parent, seamLeft, 0, sharing, tracked, settled ? undefined : headProbe, onBeforeSplice);
|
|
431
|
+
if (!seam.spliced) {
|
|
432
|
+
seamLeft++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
settled = settled ? unionAbsorptions(settled, seam) : seam;
|
|
436
|
+
moved = indexAfterAbsorb(moved, seam);
|
|
437
|
+
last = indexAfterAbsorb(last, seam);
|
|
438
|
+
seamLeft = seam.at + seam.span;
|
|
439
|
+
}
|
|
440
|
+
if (!settled)
|
|
441
|
+
return { change, landing: moved };
|
|
442
|
+
return { change: foldAbsorbIntoChange(change, settled), landing: moved };
|
|
443
|
+
}
|
|
444
|
+
/** Two folds as ONE window, which is what a change descriptor reports. The walk is left to
|
|
445
|
+
* right, so `later` never opens above `earlier`'s post-splice span. */
|
|
446
|
+
function unionAbsorptions(earlier, later) {
|
|
447
|
+
return {
|
|
448
|
+
at: earlier.at,
|
|
449
|
+
span: later.at + later.span - earlier.at,
|
|
450
|
+
eaten: earlier.eaten + later.eaten,
|
|
451
|
+
spliced: true
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Where `index` sits once `seam` folded: a slot inside the absorbed span collapses into it. */
|
|
455
|
+
function indexAfterAbsorb(index, seam) {
|
|
456
|
+
if (index < seam.at)
|
|
457
|
+
return index;
|
|
458
|
+
const absorbedTo = seam.at + seam.span + seam.eaten;
|
|
459
|
+
return index >= absorbedTo ? index - seam.eaten : Math.min(index, seam.at + seam.span - 1);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The absorbed window folded into the write's own, as the ONE contiguous window the sink
|
|
463
|
+
* reports: `count` counts pre-write slots, so the union's span converts back across whatever
|
|
464
|
+
* the write itself added or removed.
|
|
465
|
+
*/
|
|
466
|
+
function foldAbsorbIntoChange(change, seam) {
|
|
467
|
+
const absorbedTo = seam.at + seam.span + seam.eaten;
|
|
468
|
+
if (change.op === 'noop') {
|
|
469
|
+
return {
|
|
470
|
+
op: 'replace',
|
|
471
|
+
at: seam.at,
|
|
472
|
+
count: absorbedTo - seam.at,
|
|
473
|
+
newCount: seam.span,
|
|
474
|
+
idMap: { 0: 0 }
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const written = change.op === 'insert' ? change.count : change.op === 'delete' ? 0 : change.newCount;
|
|
478
|
+
const removed = change.op === 'insert' ? 0 : change.count;
|
|
479
|
+
const lo = Math.min(seam.at, change.at);
|
|
480
|
+
const hi = Math.max(absorbedTo, change.at + written);
|
|
481
|
+
const count = hi - lo - written + removed;
|
|
482
|
+
const newCount = hi - lo - seam.eaten;
|
|
483
|
+
return {
|
|
484
|
+
op: 'replace',
|
|
485
|
+
at: lo,
|
|
486
|
+
count,
|
|
487
|
+
newCount,
|
|
488
|
+
idMap: composeFoldIdMap(change, seam, { lo, count, newCount, written, removed })
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Identity through the fold: a slot the absorb did not re-mint still holds the block the change
|
|
493
|
+
* put there, so its id composes through both steps instead of resetting. Slot 0 keeps the head
|
|
494
|
+
* mapping wherever the walk has none, since a fold EXTENDS its head, kind promotion included.
|
|
495
|
+
*/
|
|
496
|
+
function composeFoldIdMap(change, seam, window) {
|
|
497
|
+
const idMap = {};
|
|
498
|
+
for (let slot = 0; slot < window.newCount; slot++) {
|
|
499
|
+
const index = window.lo + slot;
|
|
500
|
+
if (index >= seam.at && index < seam.at + seam.span)
|
|
501
|
+
continue;
|
|
502
|
+
// Back through the fold: a slot past the absorbed span sat `eaten` further down before it.
|
|
503
|
+
const spliced = index < seam.at ? index : index + seam.eaten;
|
|
504
|
+
const old = preChangeIndex(change, spliced, window);
|
|
505
|
+
if (old === null)
|
|
506
|
+
continue;
|
|
507
|
+
const oldSlot = old - window.lo;
|
|
508
|
+
if (oldSlot >= 0 && oldSlot < window.count)
|
|
509
|
+
idMap[slot] = oldSlot;
|
|
510
|
+
}
|
|
511
|
+
if (idMap[0] === undefined)
|
|
512
|
+
idMap[0] = 0;
|
|
513
|
+
return idMap;
|
|
514
|
+
}
|
|
515
|
+
/** Where `spliced` (a post-change index) stood before the change, or null for a slot it minted. */
|
|
516
|
+
function preChangeIndex(change, spliced, window) {
|
|
517
|
+
if (change.op === 'noop')
|
|
518
|
+
return spliced;
|
|
519
|
+
if (spliced < change.at)
|
|
520
|
+
return spliced;
|
|
521
|
+
if (spliced >= change.at + window.written)
|
|
522
|
+
return spliced - window.written + window.removed;
|
|
523
|
+
const inherited = change.op === 'replace' ? change.idMap?.[spliced - change.at] : undefined;
|
|
524
|
+
return inherited === undefined ? null : change.at + inherited;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Where the fragment parse's peeled trailing blank run goes. At the parent's tail it stays in the
|
|
528
|
+
* last block's raw, the single-slot sink's rule; mid-document it joins the follower's run, where
|
|
529
|
+
* one line separates and every later one is a block of its own (syntax-tree.md § Blank lines).
|
|
530
|
+
*/
|
|
531
|
+
function absorbFragmentPeel(parent, followerIndex, peel, blocks, sharing) {
|
|
532
|
+
const follower = parent.children[followerIndex];
|
|
533
|
+
if (!follower) {
|
|
534
|
+
blocks[blocks.length - 1].raw += peel;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (peel === '')
|
|
538
|
+
return;
|
|
539
|
+
const lines = blankLinesOf(peel + follower.leadingTrivia);
|
|
540
|
+
const owned = sharing ? ensureUnsharedChild(parent, followerIndex, sharing) : follower;
|
|
541
|
+
owned.leadingTrivia = lines.length > 1 ? '' : lines[0];
|
|
542
|
+
for (let i = 1; i < lines.length; i++) {
|
|
543
|
+
blocks.push({ kind: 'paragraph', leadingTrivia: i === 1 ? lines[0] : '', raw: lines[i] });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** A blank run split back into the lines it is made of, each keeping its own ending. */
|
|
547
|
+
const blankLinesOf = (run) => run.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
548
|
+
/** The materialized tail reported inside the sink's one contiguous window. */
|
|
549
|
+
export function widenForTailMint(change, before, after) {
|
|
550
|
+
const grown = after - before;
|
|
551
|
+
if (grown === 0)
|
|
552
|
+
return change;
|
|
553
|
+
if (change.op === 'noop')
|
|
554
|
+
return { op: 'insert', at: before, count: grown };
|
|
555
|
+
if (change.op === 'insert' && change.at + change.count === before) {
|
|
556
|
+
return { ...change, count: change.count + grown };
|
|
557
|
+
}
|
|
558
|
+
if (change.op === 'replace' && change.at + change.newCount === before) {
|
|
559
|
+
return { ...change, newCount: change.newCount + grown };
|
|
560
|
+
}
|
|
561
|
+
// A delete that took the tail vacated the slot the mint lands in, so the two are one window.
|
|
562
|
+
if (change.op === 'delete' && change.at === before) {
|
|
563
|
+
return { op: 'replace', at: change.at, count: change.count, newCount: grown };
|
|
564
|
+
}
|
|
565
|
+
// The mint landed past a window that does not reach the tail, so no single contiguous
|
|
566
|
+
// span describes both; the parallel arrays would drift either way this widened it.
|
|
567
|
+
devWarn('tree-ops', 'a tail suffix materialized outside the reported window');
|
|
568
|
+
return change;
|
|
569
|
+
}
|
|
570
|
+
/** A blank line off the node's own bytes (G4.20), where one does structural work at all. */
|
|
571
|
+
function mintSeparator(parent, index, sharing) {
|
|
572
|
+
const children = parent.children;
|
|
573
|
+
if (!children || index <= bodyStartIndex(parent))
|
|
574
|
+
return;
|
|
575
|
+
if (isBlankParagraph(children[index - 1]))
|
|
576
|
+
return;
|
|
577
|
+
const owned = sharing
|
|
578
|
+
? ensureUnsharedChild(parent, index, sharing)
|
|
579
|
+
: children[index];
|
|
580
|
+
owned.leadingTrivia = trailingLineEnding(owned.raw);
|
|
581
|
+
}
|
|
582
|
+
/** Reserved chrome is not a body block, so the body window opens past it. */
|
|
583
|
+
function bodyStartIndex(parent) {
|
|
584
|
+
return bodyStartFor(ownerKindNameOf(parent));
|
|
585
|
+
}
|
|
586
|
+
/** The container's declared body wrap, whichever shape names the owner. */
|
|
587
|
+
function bodyWrapOf(parent) {
|
|
588
|
+
const kind = ownerKindNameOf(parent);
|
|
589
|
+
if (kind === undefined)
|
|
590
|
+
return undefined;
|
|
591
|
+
return tryGetBlockKindDescriptor(kind)?.bodyWrap;
|
|
592
|
+
}
|
|
593
|
+
/** The node carrying the wrap's peel slots: the sink's answer, or the parent when it IS the node. */
|
|
594
|
+
function wrapSlotsOf(parent) {
|
|
595
|
+
return ownerNodeOf(parent);
|
|
596
|
+
}
|
|
597
|
+
/** The container node these children belong to, where the caller answered for one. */
|
|
598
|
+
function ownerNodeOf(parent) {
|
|
599
|
+
return parent.owner ?? ('raw' in parent ? parent : undefined);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Every settle door rewrites bytes the owner's child spans describe while leaving the children's
|
|
603
|
+
* shape alone, so the spans retire at the doors and the next rebuild re-derives them.
|
|
604
|
+
*/
|
|
605
|
+
function retireChildSpans(parent) {
|
|
606
|
+
const owner = ownerNodeOf(parent);
|
|
607
|
+
if (owner)
|
|
608
|
+
dropChildSpans(owner);
|
|
609
|
+
}
|
|
610
|
+
/** The kind whose body these children are: the sink's answer, or the owner node's own. */
|
|
611
|
+
function ownerKindNameOf(parent) {
|
|
612
|
+
return 'ownerKind' in parent ? parent.ownerKind : parent.kind;
|
|
613
|
+
}
|
|
614
|
+
function bodyStartFor(kind) {
|
|
615
|
+
if (kind === undefined)
|
|
616
|
+
return 0;
|
|
617
|
+
return tryGetBlockKindDescriptor(kind)?.reservedChrome ? 1 : 0;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* A chrome-wrapped container's parse peels the blank line against its opener into `innerPrefix`,
|
|
621
|
+
* so a separator freed above the body head is that line: hand it over, or the peel eats the head.
|
|
622
|
+
*/
|
|
623
|
+
function absorbWrapPrefix(parent, bodyStart, index, freed) {
|
|
624
|
+
const slots = wrapSlotsOf(parent);
|
|
625
|
+
if (!slots || (slots.innerPrefix ?? '') !== '')
|
|
626
|
+
return;
|
|
627
|
+
if (!bodyWrapOf(parent)?.afterOpenerLine)
|
|
628
|
+
return;
|
|
629
|
+
const head = parent.children?.[bodyStart];
|
|
630
|
+
if (!head || head.leadingTrivia !== '')
|
|
631
|
+
return;
|
|
632
|
+
if (index !== bodyStart && !isBlankParagraph(head))
|
|
633
|
+
return;
|
|
634
|
+
slots.innerPrefix = trailingLineEnding(freed);
|
|
635
|
+
}
|
|
636
|
+
// ── Delete ──
|
|
637
|
+
/**
|
|
638
|
+
* Remove the node at `blockIndex`, leaving the next sibling separated from its new predecessor
|
|
639
|
+
* and no more. Takes {@link BodyParentArg} because the settle can hand a freed line to the
|
|
640
|
+
* owner's wrap slots; the successor's trivia is the op's only in-place write.
|
|
641
|
+
*/
|
|
642
|
+
export function deleteNode(parent, blockIndex, sharing) {
|
|
643
|
+
if (blockIndex < 0 || blockIndex >= parent.children.length)
|
|
644
|
+
return { op: 'noop' };
|
|
645
|
+
const deleted = parent.children[blockIndex];
|
|
646
|
+
if (blockIndex + 1 < parent.children.length) {
|
|
647
|
+
const successor = sharing
|
|
648
|
+
? ensureUnsharedChild(parent, blockIndex + 1, sharing)
|
|
649
|
+
: parent.children[blockIndex + 1];
|
|
650
|
+
// The successor inherits the deleted separator only when it has none of its own:
|
|
651
|
+
// concatenating both leaves behind a blank line the delete should have taken.
|
|
652
|
+
successor.leadingTrivia = successor.leadingTrivia || deleted.leadingTrivia;
|
|
653
|
+
}
|
|
654
|
+
parent.children.splice(blockIndex, 1);
|
|
655
|
+
clearRedundantSeparator(parent, blockIndex, sharing);
|
|
656
|
+
// BOTH the survivor's edges: the delete puts it beside a new follower, and a merge door that
|
|
657
|
+
// rewrote its bytes can equally have stopped it interrupting the block above.
|
|
658
|
+
const survivor = Math.max(blockIndex - 1, 0);
|
|
659
|
+
return absorbWindowSeams(parent, survivor, blockIndex - survivor, blockIndex, { op: 'delete', at: blockIndex, count: 1 }, sharing).change;
|
|
660
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grid a clipboard payload holds, for a spreadsheet-like paste into a table: tab-separated
|
|
3
|
+
* rows (what Excel and Sheets write to text/plain) or a GFM table (what this editor's own
|
|
4
|
+
* rectangle copy writes). Anything else is not a grid and takes the ordinary paste route. The
|
|
5
|
+
* inverse, an HTML table for the spreadsheets that read text/html, lives here too.
|
|
6
|
+
*/
|
|
7
|
+
import type { NodeView } from '../core/node-views';
|
|
8
|
+
/** Rows of cell texts, rectangular: every row padded to the widest. Null for a non-grid payload. */
|
|
9
|
+
export declare function parseClipboardGrid(text: string): string[][] | null;
|
|
10
|
+
/** A rectangle's cell texts, pipes unescaped, as the clipboard's grid. */
|
|
11
|
+
export declare function rectangleGrid(table: NodeView, a: {
|
|
12
|
+
rowIdx: number;
|
|
13
|
+
colIdx: number;
|
|
14
|
+
}, b: {
|
|
15
|
+
rowIdx: number;
|
|
16
|
+
colIdx: number;
|
|
17
|
+
}): string[][];
|
|
18
|
+
/** The grid repeated to fill a selection whose sides are multiples of it (a spreadsheet's
|
|
19
|
+
* tiling); any other selection takes the grid once from its top-left corner. */
|
|
20
|
+
export declare function tileGridTo(grid: string[][], rows: number, cols: number): string[][];
|
|
21
|
+
export declare function gridToHtmlTable(grid: string[][]): string;
|