@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
|
@@ -1,122 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* overwritten. A descendant found by walking the live tree is the exception: mutate it in place on
|
|
6
|
-
* a caller-unshared spine (`unshare.ts`), a STRUCTURAL one via `commitMultiScope`.
|
|
2
|
+
* The split and merge primitives, the two ops that re-tile a body's blocks around a caret, plus
|
|
3
|
+
* the join cleanup every destructive join crosses (live-mode.md § 4.5). They mutate and report;
|
|
4
|
+
* the settle around them is the ceremony's (`settle.ts`).
|
|
7
5
|
*/
|
|
8
6
|
import { DEV } from 'esm-env';
|
|
7
|
+
import { headingLevel } from '../core/nodes';
|
|
9
8
|
import { isBlankParagraph, isBlankSource, parse } from '../core/parser';
|
|
10
|
-
import { escalatedFenceLength, matchFenceOpen } from '../core/parsers/fence-syntax';
|
|
11
|
-
import { isBlockOpenerRegistered } from '../schema/block-openers';
|
|
12
9
|
import { getLiveJoinSeamCleaner, getLiveSplitRebalancer } from '../schema/inline-construct-policy';
|
|
13
10
|
import { displayLength, snapToScalarBoundary, terminateLine, trailingLineEnding, trimTrailingLineEnding } from '../core/lines';
|
|
14
11
|
import { devWarn } from '../dev-warn';
|
|
15
12
|
import { assignChildIdsDeep } from '../block-id';
|
|
16
|
-
import { perfEnabled, recordContainerKindReparse } from '../perf/instruments';
|
|
17
13
|
import { findMergeTarget } from '../schema/merge-rules';
|
|
18
14
|
import { rebuildAncestryRaw } from '../schema/container-raw';
|
|
19
15
|
import { getBlockKindDescriptor, tryGetBlockKindDescriptor } from '../schema/block-kind-descriptor';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { resyncChildIds, spliceChildren } from './children';
|
|
23
|
-
import { spliceMany } from './splice-many';
|
|
24
|
-
import { applyStructuralChangeToIdsRefs, replacePreservingFirst } from './structural-change';
|
|
16
|
+
import { ensureUnsharedPath } from './unshare';
|
|
17
|
+
import { replacePreservingFirst } from './structural-change';
|
|
25
18
|
import { assertInvariant } from '../assert';
|
|
26
|
-
import { dropChildSpans } from '../schema/child-spans';
|
|
27
19
|
import { checkSingleNodeSink } from '../invariants/single-node-sink';
|
|
28
20
|
import { checkSplitLanding } from '../invariants/split-landing';
|
|
29
|
-
import {
|
|
30
|
-
|
|
21
|
+
import { NEXT_PROSE_LINE, ensureEditableContainers, forBody, normalizeOwnRaw } from './node-primitives';
|
|
22
|
+
import { absorbSeamReading, deleteNode } from './settle';
|
|
23
|
+
import { adoptReparsedFields, probeLineOpensAsProse } from './content-write';
|
|
31
24
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
export function normalizeBodyWrite(ownerKind, raw) {
|
|
36
|
-
const owner = ownerKind === undefined ? undefined : tryGetBlockKindDescriptor(ownerKind);
|
|
37
|
-
return owner?.bodyWrite?.normalize(raw) ?? raw;
|
|
38
|
-
}
|
|
39
|
-
const forBody = (parent, raw) => normalizeBodyWrite(ownerKindOf(parent), raw);
|
|
40
|
-
/**
|
|
41
|
-
* `raw` made legal as `node`'s OWN bytes — {@link normalizeBodyWrite}'s node-side twin, for the
|
|
42
|
-
* kind's own grammar rather than its container's. Sinks that REPLACE the node with a reparse of
|
|
43
|
-
* the result call this door: the reparse re-derives metadata from the bytes, so structure the
|
|
44
|
-
* rule restores from the old metadata (a fence closer a truncation consumed) must land first.
|
|
45
|
-
*/
|
|
46
|
-
export function normalizeOwnRaw(node, raw) {
|
|
47
|
-
return tryGetBlockKindDescriptor(node.kind)?.normalizeRawWrite?.(raw, node) ?? raw;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Write `raw` as `node`'s OWN bytes through its kind's rule — {@link normalizeOwnRaw}'s in-place
|
|
51
|
-
* sink. Every sink writing a leaf's bytes without the kind's surface in front of it owes one of
|
|
52
|
-
* the two (pinned by `lint/leaf-raw-write-rule`).
|
|
53
|
-
*/
|
|
54
|
-
export function writeOwnRaw(node, raw, grammar) {
|
|
55
|
-
const descriptor = tryGetBlockKindDescriptor(node.kind);
|
|
56
|
-
const legal = descriptor?.normalizeRawWrite?.(raw, node) ?? raw;
|
|
57
|
-
node.raw = legal;
|
|
58
|
-
// A context-dependent kind's raw does not reparse to itself, so its metadata was never
|
|
59
|
-
// parse-derived and a fragment parse would only mis-read it.
|
|
60
|
-
if (descriptor?.contextDependentKind)
|
|
61
|
-
return;
|
|
62
|
-
// In place means no reparse replaces the node, so parse-owned metadata re-derives here —
|
|
63
|
-
// for the rule's own rewrite AND bytes the caller's edit already changed.
|
|
64
|
-
const reparsed = parse(legal, { grammar, scope: 'fragment' }).children;
|
|
65
|
-
if (reparsed.length === 1 && reparsed[0].kind === node.kind)
|
|
66
|
-
node.metadata = reparsed[0].metadata;
|
|
67
|
-
}
|
|
68
|
-
// ── Node minting ──
|
|
69
|
-
/**
|
|
70
|
-
* The paragraph mint. Every argument is required: a paragraph's raw ENDS in a line ending,
|
|
71
|
-
* so a mint site must answer which document it lands in (G4.20) rather than strand a lone
|
|
72
|
-
* LF in a CRLF file. Returns a fresh node every call — a shared instance would alias across
|
|
73
|
-
* tree positions and corrupt the snapshot/unshare model (G1.9).
|
|
74
|
-
*/
|
|
75
|
-
export function paragraphNode(leadingTrivia, text, lineEnding) {
|
|
76
|
-
return { kind: 'paragraph', leadingTrivia, raw: text + lineEnding };
|
|
77
|
-
}
|
|
78
|
-
/** The empty-paragraph placeholder keeping an emptied document or container caret-addressable. */
|
|
79
|
-
export function emptyParagraph(leadingTrivia, lineEnding) {
|
|
80
|
-
return paragraphNode(leadingTrivia, '', lineEnding);
|
|
81
|
-
}
|
|
82
|
-
export function nodeAt(doc, path) {
|
|
83
|
-
let cur = doc;
|
|
84
|
-
for (const idx of path) {
|
|
85
|
-
if (!cur.children || idx < 0 || idx >= cur.children.length)
|
|
86
|
-
return null;
|
|
87
|
-
cur = cur.children[idx];
|
|
88
|
-
}
|
|
89
|
-
return cur;
|
|
90
|
-
}
|
|
91
|
-
export function blockNodeAt(doc, path) {
|
|
92
|
-
const node = nodeAt(doc, path);
|
|
93
|
-
return node !== null && isBlockNode(node) ? node : null;
|
|
94
|
-
}
|
|
95
|
-
export function isBlockNode(node) {
|
|
96
|
-
return 'raw' in node;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* The landing a site is about to consume, held to the primitive's answer (G1.34). The
|
|
100
|
-
* top-level path seats the caret at it and the list path splices at it, so both cross this
|
|
101
|
-
* on their way to using it; a re-derived `blockIndex + 1` warns instead of shipping.
|
|
25
|
+
* The landing a site is about to consume, held to the primitive's answer (G1.34): a re-derived
|
|
26
|
+
* `blockIndex + 1` warns instead of shipping.
|
|
102
27
|
*/
|
|
103
28
|
export function assertSplitLanding(split, landing) {
|
|
104
29
|
assertInvariant('split-landing', () => checkSplitLanding(split.secondHalfIndex, landing));
|
|
105
30
|
}
|
|
106
31
|
/**
|
|
107
32
|
* What a one-slot sink is about to put in its slot, held to one node (G1.35). Asked at the WRITE
|
|
108
|
-
* with the nodes being written, so the guard answers for sink N+1
|
|
109
|
-
* siblings make, or splices a plural replacement into a slot that holds one.
|
|
33
|
+
* with the nodes being written, so the guard answers for sink N+1.
|
|
110
34
|
*/
|
|
111
35
|
export function assertSingleNodeSink(sink, installed) {
|
|
112
36
|
assertInvariant('single-node-sink', () => checkSingleNodeSink(sink, installed.length));
|
|
113
37
|
}
|
|
114
38
|
/**
|
|
115
39
|
* Split the node at `blockIndex` at raw `offset` (display-relative). The first half inherits the
|
|
116
|
-
* original ID and the whole structural suffix (a setext underline)
|
|
117
|
-
*
|
|
118
|
-
* ({@link separatorSplitsOffNextLine}) — without it GFM lazy continuation folds the halves back
|
|
119
|
-
* into one block on reload.
|
|
40
|
+
* original ID and the whole structural suffix (a setext underline); the second half opens with a
|
|
41
|
+
* blank separator wherever one does structural work ({@link separatorSplitsOffNextLine}).
|
|
120
42
|
*/
|
|
121
43
|
export function splitNode(parent, blockIndex, offset, sharing, presentationMode, linkRef) {
|
|
122
44
|
const noop = { change: { op: 'noop' }, secondHalfIndex: blockIndex + 1 };
|
|
@@ -130,17 +52,17 @@ export function splitNode(parent, blockIndex, offset, sharing, presentationMode,
|
|
|
130
52
|
return noop;
|
|
131
53
|
const rawText = node.raw;
|
|
132
54
|
const lineEnding = trailingLineEnding(rawText);
|
|
133
|
-
const cut = cutPastLineEnding(descriptor, node, offset);
|
|
55
|
+
const cut = headingHeadCut(descriptor, node, cutPastLineEnding(descriptor, node, offset));
|
|
134
56
|
const suffixSplit = structuralSuffixSplit(descriptor, node, cut);
|
|
135
|
-
// Both halves: each can collide alone
|
|
136
|
-
// a first half promoted to a bare terminator once its trailing text is cut away.
|
|
57
|
+
// Both halves: each can collide alone (a `</details>` stranded on the second half, or
|
|
58
|
+
// a first half promoted to a bare terminator once its trailing text is cut away).
|
|
137
59
|
let firstRaw = forBody(parent, suffixSplit ? suffixSplit.firstRaw : rawText.slice(0, cut));
|
|
138
60
|
let secondRaw = forBody(parent, suffixSplit ? suffixSplit.secondRaw : rawText.slice(cut));
|
|
139
61
|
firstRaw = terminateLine(firstRaw, rawText);
|
|
140
62
|
secondRaw = terminateLine(secondRaw, rawText);
|
|
141
63
|
// Live alone rebalances: there the delimiters around the cut are unpainted, so a byte-literal
|
|
142
|
-
// half would surface runs the reader never saw. The rebalancer
|
|
143
|
-
//
|
|
64
|
+
// half would surface runs the reader never saw. The rebalancer declines when its bytes do not
|
|
65
|
+
// parse back, leaving the literal cut every other mode gets.
|
|
144
66
|
if (presentationMode === 'live') {
|
|
145
67
|
const rebalanced = getLiveSplitRebalancer()?.(node, offset, firstRaw, secondRaw, linkRef);
|
|
146
68
|
if (rebalanced) {
|
|
@@ -158,7 +80,7 @@ export function splitNode(parent, blockIndex, offset, sharing, presentationMode,
|
|
|
158
80
|
// separator; `separator` answers no when the bytes already end in a blank line.
|
|
159
81
|
const second = reparseAsNodes(secondRaw, first.suffix + separator);
|
|
160
82
|
if (DEV && first.nodes.length > 1) {
|
|
161
|
-
// Legal
|
|
83
|
+
// Legal, since the landing index rides the result, but rare enough to keep visible.
|
|
162
84
|
devWarn('tree-ops', `splitNode: the first half parsed to ${first.nodes.length} blocks`);
|
|
163
85
|
}
|
|
164
86
|
const nodes = [...first.nodes, ...second.nodes];
|
|
@@ -166,9 +88,8 @@ export function splitNode(parent, blockIndex, offset, sharing, presentationMode,
|
|
|
166
88
|
nodes[nodes.length - 1].raw += second.suffix;
|
|
167
89
|
const splitTail = blockIndex === parent.children.length - 1;
|
|
168
90
|
parent.children.splice(blockIndex, 1, ...nodes);
|
|
169
|
-
// Floor at the seam itself: a wider window would reach back into the spliced set and
|
|
170
|
-
//
|
|
171
|
-
// the document's folded line is the settle funnel's question, not this sink's.
|
|
91
|
+
// Floor at the seam itself: a wider window would reach back into the spliced set and break
|
|
92
|
+
// the one-window accounting. At the tail the document's folded line is the settle funnel's.
|
|
172
93
|
const seamLeft = blockIndex + nodes.length - 1;
|
|
173
94
|
const eaten = splitTail ? 0 : absorbSeamReading(parent, seamLeft, seamLeft, sharing).eaten;
|
|
174
95
|
return {
|
|
@@ -177,10 +98,9 @@ export function splitNode(parent, blockIndex, offset, sharing, presentationMode,
|
|
|
177
98
|
};
|
|
178
99
|
}
|
|
179
100
|
/**
|
|
180
|
-
* The cut a split makes
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* range's end, past which the offset stops being a content position at all.
|
|
101
|
+
* The cut a split makes: an ending the offset lands ON terminates the FIRST half rather than
|
|
102
|
+
* opening the second, which would mint a blank line nobody typed. A CRLF is one boundary, and
|
|
103
|
+
* the cut clamps to a content range's end.
|
|
184
104
|
*/
|
|
185
105
|
function cutPastLineEnding(descriptor, node, offset) {
|
|
186
106
|
const raw = node.raw;
|
|
@@ -194,20 +114,18 @@ function cutPastLineEnding(descriptor, node, offset) {
|
|
|
194
114
|
return contentEnd === undefined ? at + ending.length : Math.min(at + ending.length, contentEnd);
|
|
195
115
|
}
|
|
196
116
|
/**
|
|
197
|
-
*
|
|
198
|
-
* line
|
|
199
|
-
* arbitrary code, so no line is unclaimable by construction — {@link probeLineOpensAsProse}
|
|
200
|
-
* is the runtime check.
|
|
117
|
+
* Enter at the head of an ATX heading's text moves the whole heading down under a new empty
|
|
118
|
+
* line: the marker belongs with its text, and an empty heading is nothing anyone asked for.
|
|
201
119
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
120
|
+
function headingHeadCut(descriptor, node, cut) {
|
|
121
|
+
const content = headingLevel(node) === null ? undefined : descriptor.getContentRange?.(node);
|
|
122
|
+
if (!content || content.start === 0 || content.end === content.start)
|
|
123
|
+
return cut;
|
|
124
|
+
return cut <= content.start ? 0 : cut;
|
|
206
125
|
}
|
|
207
126
|
/**
|
|
208
|
-
* Leading trivia for a freshly minted BLANK block
|
|
209
|
-
*
|
|
210
|
-
* open below, where the successor's own separator opens it.
|
|
127
|
+
* Leading trivia for a freshly minted BLANK block: a blank line is a block only past its run's
|
|
128
|
+
* first line, so it separates from a non-blank predecessor unless a run is already open below.
|
|
211
129
|
*/
|
|
212
130
|
function blankBlockTrivia(predecessorIsBlank, successor, lineEnding) {
|
|
213
131
|
if (predecessorIsBlank)
|
|
@@ -216,8 +134,8 @@ function blankBlockTrivia(predecessorIsBlank, successor, lineEnding) {
|
|
|
216
134
|
return runOpenBelow ? '' : lineEnding;
|
|
217
135
|
}
|
|
218
136
|
/**
|
|
219
|
-
* The second half's leading trivia
|
|
220
|
-
*
|
|
137
|
+
* The second half's leading trivia: a blank half follows {@link blankBlockTrivia}; a prose half
|
|
138
|
+
* takes a separator exactly when lazy continuation would fold the halves back together.
|
|
221
139
|
*/
|
|
222
140
|
function splitSeparator(firstRaw, secondRaw, lineEnding, successor) {
|
|
223
141
|
if (isBlankSource(secondRaw)) {
|
|
@@ -234,9 +152,8 @@ function blankHalfBecomesBlock(firstRaw, secondRaw, lineEnding) {
|
|
|
234
152
|
}
|
|
235
153
|
/**
|
|
236
154
|
* Would a blank line between `raw` and the line after it split off a second block? Asked of the
|
|
237
|
-
* bytes, never a kind list
|
|
238
|
-
*
|
|
239
|
-
* the separator materializes as one, and counting it would answer yes for every raw.
|
|
155
|
+
* bytes, never a kind list, so the separator never lands inside a body that swallows both forms.
|
|
156
|
+
* Blank blocks are discounted on both sides, or the separator would answer yes for every raw.
|
|
240
157
|
*/
|
|
241
158
|
function separatorSplitsOffNextLine(raw, secondRaw, lineEnding) {
|
|
242
159
|
if (DEV && !probeLineOpensAsProse()) {
|
|
@@ -252,9 +169,9 @@ function contentBlockCount(source) {
|
|
|
252
169
|
.length;
|
|
253
170
|
}
|
|
254
171
|
/**
|
|
255
|
-
* A split that keeps a kind's structural suffix
|
|
256
|
-
* underline
|
|
257
|
-
* start or inside the suffix itself
|
|
172
|
+
* A split that keeps a kind's structural suffix (raw beyond its content range, the setext
|
|
173
|
+
* underline) on the first half. Null when the kind has no suffix, or the offset is at block
|
|
174
|
+
* start or inside the suffix itself.
|
|
258
175
|
*/
|
|
259
176
|
function structuralSuffixSplit(descriptor, node, offset) {
|
|
260
177
|
const getRange = descriptor.getContentRange;
|
|
@@ -277,8 +194,7 @@ function structuralSuffixSplit(descriptor, node, offset) {
|
|
|
277
194
|
}
|
|
278
195
|
// ── Merge ──
|
|
279
196
|
/**
|
|
280
|
-
* `join.mergedRaw` with the delimiter runs the join orphaned at its seam dropped
|
|
281
|
-
* where those runs are unpainted and a literal concatenation surfaces bytes the reader never saw
|
|
197
|
+
* `join.mergedRaw` with the delimiter runs the join orphaned at its seam dropped, live only
|
|
282
198
|
* (live-mode.md § 4.5). The one registered cleaner verifies its own bytes and otherwise declines,
|
|
283
199
|
* leaving the literal join every other mode gets. Every destructive join crosses this door.
|
|
284
200
|
*/
|
|
@@ -291,7 +207,7 @@ export function cleanJoinedRaw(join, presentationMode) {
|
|
|
291
207
|
/**
|
|
292
208
|
* The bytes a single-block edit leaves when it deletes `range` out of `display`. A delete-then-
|
|
293
209
|
* insert is a join like any other, so it crosses the same cleanup, and the returned offset is
|
|
294
|
-
* where the two sides now meet
|
|
210
|
+
* where the two sides now meet.
|
|
295
211
|
*/
|
|
296
212
|
export function cutRangeFromDisplay(node, display, range, presentationMode, linkRef) {
|
|
297
213
|
// Both ends off any scalar interior before the slice: a half-pair here is unrecoverable
|
|
@@ -322,8 +238,7 @@ function joinRaw(prev, curr, presentationMode, linkRef) {
|
|
|
322
238
|
}
|
|
323
239
|
/**
|
|
324
240
|
* Merge `curr` into the deepest prose leaf of `prev`, writing into that leaf rather than
|
|
325
|
-
* reparsing concatenated raw
|
|
326
|
-
* leaves' inline caches. Pass `sharing` to unshare everything the merge writes. Null when
|
|
241
|
+
* reparsing concatenated raw, which preserves prev's component identity and IME state. Null when
|
|
327
242
|
* no mergeable leaf exists, so the caller can fall back to move-focus.
|
|
328
243
|
*/
|
|
329
244
|
export function mergeIntoPrevDeepLeaf(parent, blockIndex, sharing, presentationMode, linkRef, grammar) {
|
|
@@ -365,8 +280,7 @@ function holderChildrenAt(children, path) {
|
|
|
365
280
|
}
|
|
366
281
|
/**
|
|
367
282
|
* The deep-leaf merge's verdict: the absorbed bytes cross the kind's own rule and a fragment
|
|
368
|
-
* reparse. Null when they read as several blocks
|
|
369
|
-
* whole leaves a tree its own reload disagrees with (G1.35).
|
|
283
|
+
* reparse. Null when they read as several blocks, since the leaf is one slot (G1.35).
|
|
370
284
|
*/
|
|
371
285
|
function mergedLeafFor(target, raw, grammar) {
|
|
372
286
|
const written = normalizeOwnRaw(target, raw);
|
|
@@ -378,19 +292,6 @@ function mergedLeafFor(target, raw, grammar) {
|
|
|
378
292
|
const blocks = parse(written, { grammar, scope: 'fragment' }).children;
|
|
379
293
|
return blocks.length > 1 ? null : { written, blocks };
|
|
380
294
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Refresh a node in place from its own reparse, keeping its object identity. The id resync is
|
|
383
|
-
* unconditional: the reparse can change the child count while the caller reports `noop`, and the
|
|
384
|
-
* fresh children carry no `childIds` of their own.
|
|
385
|
-
*/
|
|
386
|
-
function adoptReparsedFields(target, parsed) {
|
|
387
|
-
target.metadata = parsed?.metadata;
|
|
388
|
-
target.children = parsed?.children;
|
|
389
|
-
resyncChildIds(target);
|
|
390
|
-
assignChildIdsDeep(target);
|
|
391
|
-
target.innerPrefix = parsed?.innerPrefix;
|
|
392
|
-
target.innerSuffix = parsed?.innerSuffix;
|
|
393
|
-
}
|
|
394
295
|
/** {@link mergedLeafFor}'s write, over the unshared spine the verdict was taken ahead of. */
|
|
395
296
|
function installMergedLeaf(holderChildren, slot, merged, sharing) {
|
|
396
297
|
const target = holderChildren[slot];
|
|
@@ -432,920 +333,10 @@ export function mergeWithNext(parent, blockIndex, presentationMode, linkRef) {
|
|
|
432
333
|
parent.children.splice(blockIndex, 2, ...installed);
|
|
433
334
|
return { change: replacePreservingFirst(blockIndex, 2, 1), joinOffset: seam };
|
|
434
335
|
}
|
|
435
|
-
// ── Separators ──
|
|
436
|
-
/**
|
|
437
|
-
* Settle the separator at `index`: nothing above it needs one at the body head or below a blank
|
|
438
|
-
* block, where the parser would read the extra line as one more empty paragraph. Every splice
|
|
439
|
-
* that changes what precedes a block settles through this family (syntax-tree.md § Blank lines).
|
|
440
|
-
* `sharing` owns the write (G1.9).
|
|
441
|
-
*/
|
|
442
|
-
export function clearRedundantSeparator(parent, index, sharing) {
|
|
443
|
-
retireChildSpans(parent);
|
|
444
|
-
const node = parent.children?.[index];
|
|
445
|
-
if (!node || node.leadingTrivia === '')
|
|
446
|
-
return;
|
|
447
|
-
const bodyStart = bodyStartIndex(parent);
|
|
448
|
-
if (index < bodyStart)
|
|
449
|
-
return;
|
|
450
|
-
const predecessor = index > bodyStart ? parent.children[index - 1] : undefined;
|
|
451
|
-
if (predecessor !== undefined && !isBlankParagraph(predecessor))
|
|
452
|
-
return;
|
|
453
|
-
const owned = sharing ? ensureUnsharedChild(parent, index, sharing) : node;
|
|
454
|
-
const freed = owned.leadingTrivia;
|
|
455
|
-
owned.leadingTrivia = '';
|
|
456
|
-
absorbWrapPrefix(parent, bodyStart, index, freed);
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* A blank block IS a blank line, so it and its follower share ONE separator: two of them reload
|
|
460
|
-
* as a second empty paragraph (G2.13). The follower's is the one that stands, so a later fill of
|
|
461
|
-
* this slot (a paste over a cut range) still finds the follower separated. The run-level twin
|
|
462
|
-
* {@link settleSeparatorOnBlank} keeps the first already-standing line instead, the same bytes
|
|
463
|
-
* the other way round.
|
|
464
|
-
*/
|
|
465
|
-
export function dropDoubledSeparator(parent, index, sharing) {
|
|
466
|
-
retireChildSpans(parent);
|
|
467
|
-
const node = parent.children?.[index];
|
|
468
|
-
if (!node || node.leadingTrivia === '' || !isBlankParagraph(node))
|
|
469
|
-
return;
|
|
470
|
-
if ((parent.children?.[index + 1]?.leadingTrivia ?? '') === '')
|
|
471
|
-
return;
|
|
472
|
-
const owned = sharing ? ensureUnsharedChild(parent, index, sharing) : node;
|
|
473
|
-
owned.leadingTrivia = '';
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* The separator a block takes back when it stops being blank: its own blank line was what stood
|
|
477
|
-
* between it and a non-blank predecessor. Call at the fill — {@link clearRedundantSeparator}
|
|
478
|
-
* frees a separator in exactly the cases this declines, and a block that was never blank keeps
|
|
479
|
-
* whatever its bytes earned (a paragraph under a heading needs none).
|
|
480
|
-
*/
|
|
481
|
-
export function restoreSeparatorOnFill(parent, index, sharing) {
|
|
482
|
-
retireChildSpans(parent);
|
|
483
|
-
const node = parent.children?.[index];
|
|
484
|
-
if (!node || node.leadingTrivia !== '' || isBlankParagraph(node))
|
|
485
|
-
return;
|
|
486
|
-
mintSeparator(parent, index, sharing);
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* The separator the block BELOW a consumed blank line takes back — the same mint minus the
|
|
490
|
-
* blank-self guard, since a blank follower needs the line as much as a prose one. It takes one
|
|
491
|
-
* only where its own follower holds none: two would reload as a second empty paragraph
|
|
492
|
-
* ({@link dropDoubledSeparator}'s rule, declined here rather than undone after).
|
|
493
|
-
*/
|
|
494
|
-
function restoreSeparatorAfterBlank(parent, index, sharing) {
|
|
495
|
-
retireChildSpans(parent);
|
|
496
|
-
const children = parent.children;
|
|
497
|
-
const node = children?.[index];
|
|
498
|
-
if (!children || !node || node.leadingTrivia !== '')
|
|
499
|
-
return;
|
|
500
|
-
if (isBlankParagraph(node) && (children[index + 1]?.leadingTrivia ?? '') !== '')
|
|
501
|
-
return;
|
|
502
|
-
mintSeparator(parent, index, sharing);
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* The settle a block turning INTO a blank line owes: it joins the blank run around it, and a run
|
|
506
|
-
* carries exactly ONE separating line — across every block in it AND its follower, since a blank
|
|
507
|
-
* block is the follower's line too. The line already standing is the one kept, wherever in the
|
|
508
|
-
* run it sits; a mint lands at the run's head, the only slot one may take.
|
|
509
|
-
*/
|
|
510
|
-
export function settleSeparatorOnBlank(parent, index, sharing) {
|
|
511
|
-
retireChildSpans(parent);
|
|
512
|
-
const children = parent.children;
|
|
513
|
-
const node = children?.[index];
|
|
514
|
-
if (!children || !node || !isBlankParagraph(node))
|
|
515
|
-
return;
|
|
516
|
-
const bodyStart = bodyStartIndex(parent);
|
|
517
|
-
let start = index;
|
|
518
|
-
while (start > bodyStart && isBlankParagraph(children[start - 1]))
|
|
519
|
-
start--;
|
|
520
|
-
let end = index;
|
|
521
|
-
while (end + 1 < children.length && isBlankParagraph(children[end + 1]))
|
|
522
|
-
end++;
|
|
523
|
-
const standing = [];
|
|
524
|
-
for (let i = start; i <= Math.min(end + 1, children.length - 1); i++) {
|
|
525
|
-
if (children[i].leadingTrivia !== '')
|
|
526
|
-
standing.push(i);
|
|
527
|
-
}
|
|
528
|
-
// A chrome line bounding the run beside PROSE eats one line as the wrap's peel
|
|
529
|
-
// (`innerPrefix`/`innerSuffix`), on top of the run's own count; an all-blank body owes none.
|
|
530
|
-
const wrap = bodyWrapOf(parent);
|
|
531
|
-
const slots = wrapSlotsOf(parent);
|
|
532
|
-
const bodyEnd = children.length - 1;
|
|
533
|
-
// A run of two or more that IS the whole body sits against both chrome lines, and the reload
|
|
534
|
-
// peels one line into each slot before it materializes a block — so it owes BOTH, where an
|
|
535
|
-
// arm bounded by prose on one side grants at most one.
|
|
536
|
-
const twoPeelBody = !!slots &&
|
|
537
|
-
wrap?.afterOpenerLine === true &&
|
|
538
|
-
wrap.beforeCloserLine === true &&
|
|
539
|
-
start === bodyStart &&
|
|
540
|
-
end === bodyEnd &&
|
|
541
|
-
start < end;
|
|
542
|
-
const tailBelowProse = start > bodyStart && end === bodyEnd;
|
|
543
|
-
if (slots && wrap?.beforeCloserLine && (tailBelowProse || twoPeelBody) && !slots.innerSuffix) {
|
|
544
|
-
slots.innerSuffix = trailingLineEnding(children[end].raw);
|
|
545
|
-
}
|
|
546
|
-
// The reverse: a deletion can leave a lone blank as the WHOLE body, where the closer
|
|
547
|
-
// peel no longer engages beside the opener's — the run gives the extra line back.
|
|
548
|
-
const loneBlankBody = start === bodyStart && end === bodyEnd && start === end;
|
|
549
|
-
if (slots && loneBlankBody && slots.innerSuffix && (slots.innerPrefix || standing.length > 0)) {
|
|
550
|
-
slots.innerSuffix = '';
|
|
551
|
-
}
|
|
552
|
-
const headUnderWrap = !!slots && wrap?.afterOpenerLine === true && start === bodyStart && end < bodyEnd;
|
|
553
|
-
// A line already standing IS the opener's peel on reload, so taking one as well would add a
|
|
554
|
-
// line — but a two-peel body's count comes out of the slots, not out of the run.
|
|
555
|
-
const takesOpenerPeel = twoPeelBody || (headUnderWrap && standing.length === 0);
|
|
556
|
-
if (slots && takesOpenerPeel && !slots.innerPrefix) {
|
|
557
|
-
slots.innerPrefix = trailingLineEnding(children[start].raw);
|
|
558
|
-
}
|
|
559
|
-
// Under the wrap the run keeps exactly the one peel line — in `innerPrefix` or still
|
|
560
|
-
// standing; elsewhere a run with no LINE above it (the document head, a plain container's
|
|
561
|
-
// body head) separates from nothing and materializes in full.
|
|
562
|
-
let wanted;
|
|
563
|
-
if (twoPeelBody)
|
|
564
|
-
wanted = 0;
|
|
565
|
-
else if (headUnderWrap)
|
|
566
|
-
wanted = slots?.innerPrefix ? 0 : 1;
|
|
567
|
-
else
|
|
568
|
-
wanted = start > 0 || wrap?.afterOpenerLine ? 1 : 0;
|
|
569
|
-
if (standing.length < wanted) {
|
|
570
|
-
mintSeparator(parent, start, sharing);
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
for (const at of standing.slice(wanted)) {
|
|
574
|
-
const owned = sharing ? ensureUnsharedChild(parent, at, sharing) : children[at];
|
|
575
|
-
owned.leadingTrivia = '';
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
materializeTailSuffix(parent, sharing);
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* The give-back twin of {@link settleSeparatorOnBlank}'s closer peel: a blank run reaching the
|
|
582
|
-
* body tail borrows a line into `innerSuffix` so the reload keeps the block, and a tail that
|
|
583
|
-
* stops being blank owes it back or the wrap emits a line nobody typed. A trailing blank the
|
|
584
|
-
* author wrote against the closer reaches the same shape and is spent here too — one cosmetic
|
|
585
|
-
* line, against a stray one after every tail split.
|
|
586
|
-
*/
|
|
587
|
-
function releaseWrapPeel(parent, index) {
|
|
588
|
-
retireChildSpans(parent);
|
|
589
|
-
const children = parent.children;
|
|
590
|
-
const slots = wrapSlotsOf(parent);
|
|
591
|
-
if (!children || children.length === 0 || !slots?.innerSuffix)
|
|
592
|
-
return;
|
|
593
|
-
if (!bodyWrapOf(parent)?.beforeCloserLine)
|
|
594
|
-
return;
|
|
595
|
-
if (index < children.length - 1)
|
|
596
|
-
return;
|
|
597
|
-
if (isBlankParagraph(children[children.length - 1]))
|
|
598
|
-
return;
|
|
599
|
-
slots.innerSuffix = '';
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* The parse folds the document's one trailing blank line into `suffix` only while the tail
|
|
603
|
-
* block is non-blank (`parseBlocks`' separator-spent rule); once the tail turns blank the
|
|
604
|
-
* reload reads that line as its own empty paragraph, so the settle materializes it.
|
|
605
|
-
* Document-level slot only — a container's `innerSuffix` twin stays with the wrap arms.
|
|
606
|
-
* Returns the blocks appended.
|
|
607
|
-
*/
|
|
608
|
-
function materializeTailSuffix(parent, sharing) {
|
|
609
|
-
retireChildSpans(parent);
|
|
610
|
-
const children = parent.children;
|
|
611
|
-
const suffix = parent.suffix;
|
|
612
|
-
if (!children || !suffix)
|
|
613
|
-
return 0;
|
|
614
|
-
// An emptied parent has no tail for the line to fold against, so it is the document's whole
|
|
615
|
-
// content and the reload reads it as the one block there is.
|
|
616
|
-
if (children.length > 0 && !isBlankParagraph(children[children.length - 1]))
|
|
617
|
-
return 0;
|
|
618
|
-
const minted = { kind: 'paragraph', leadingTrivia: '', raw: suffix };
|
|
619
|
-
if (sharing)
|
|
620
|
-
sharing.stamp(minted);
|
|
621
|
-
children.push(minted);
|
|
622
|
-
parent.suffix = '';
|
|
623
|
-
return 1;
|
|
624
|
-
}
|
|
625
|
-
// ── The splice settle funnel ──
|
|
626
|
-
/**
|
|
627
|
-
* The settle every splice owes its neighbourhood: the vacated separating line hands down, a
|
|
628
|
-
* now-redundant one frees, the two debts a consumed blank line leaves are paid (syntax-tree.md
|
|
629
|
-
* § Blank lines), the folded tail line materializes, and the window's joins are asked. `removed`
|
|
630
|
-
* is the pre-splice span, the only place was-blank survives a splice. `tracked` rides the folds
|
|
631
|
-
* for a door landing a caret in the spliced bytes. Returns `change` widened by everything the
|
|
632
|
-
* settle itself did.
|
|
633
|
-
*/
|
|
634
|
-
function settleSplicedWindow(parent, at, removed, added, change, sharing, tracked) {
|
|
635
|
-
if (!parent.children)
|
|
636
|
-
return change;
|
|
637
|
-
// Ahead of the arms: `settleSeparatorOnBlank` materializes the tail line itself, so a count
|
|
638
|
-
// read after it would leave that growth outside the window the sink reports.
|
|
639
|
-
const beforeMint = parent.children.length;
|
|
640
|
-
handDownVacatedSeparator(parent, at, removed[0]?.leadingTrivia ?? '', sharing);
|
|
641
|
-
clearRedundantSeparator(parent, at, sharing);
|
|
642
|
-
if (removed.some(isBlankParagraph)) {
|
|
643
|
-
// Both ends: the line was the slot's own AND the one below it stood on.
|
|
644
|
-
restoreSeparatorAfterBlank(parent, at, sharing);
|
|
645
|
-
if (added > 0)
|
|
646
|
-
restoreSeparatorAfterBlank(parent, at + added, sharing);
|
|
647
|
-
releaseWrapPeel(parent, at + Math.max(added - 1, 0));
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
settleSeparatorOnBlank(parent, at + Math.max(added - 1, 0), sharing);
|
|
651
|
-
}
|
|
652
|
-
// Unconditional, and the funnel's only home for it: every arm above probes a slot inside the
|
|
653
|
-
// window, and a delete window at the tail has none — the question is about the parent's LAST
|
|
654
|
-
// block, which no window position answers.
|
|
655
|
-
materializeTailSuffix(parent, sharing);
|
|
656
|
-
const widened = widenForTailMint(change, beforeMint, parent.children.length);
|
|
657
|
-
// The seam question is part of SETTLING, not a rule each door carries: paste, replace, delete
|
|
658
|
-
// and fill all splice through here, so door N+1 inherits it. A byte-shaped write
|
|
659
|
-
// reporting `noop` splices no window and is asked nothing.
|
|
660
|
-
return absorbWindowSeams(parent, at, added, at, widened, sharing, tracked,
|
|
661
|
-
// A one-slot window names the one block whose bytes changed, which is what lets the seam
|
|
662
|
-
// above decline on its first line; a plural one names no single block.
|
|
663
|
-
added === 1 ? at : undefined).change;
|
|
664
|
-
}
|
|
665
|
-
/** Whoever takes the slot inherits its line, having none of its own — {@link deleteNode}'s rule. */
|
|
666
|
-
function handDownVacatedSeparator(parent, at, vacated, sharing) {
|
|
667
|
-
retireChildSpans(parent);
|
|
668
|
-
const heir = parent.children?.[at];
|
|
669
|
-
if (vacated === '' || !heir || heir.leadingTrivia !== '')
|
|
670
|
-
return;
|
|
671
|
-
const owned = sharing ? ensureUnsharedChild(parent, at, sharing) : heir;
|
|
672
|
-
owned.leadingTrivia = vacated;
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* The commit ceremony's settle door: derive the spliced window from the change and settle it
|
|
676
|
-
* against `before`, the pre-mutate children the ceremony still holds. Nodes surviving inside the
|
|
677
|
-
* window are not removals, so a coarse descriptor over an in-place write settles as one.
|
|
678
|
-
* `tracked` is the committing door's caret, carried through the folds. Returns the change widened
|
|
679
|
-
* by a tail line the settle materialized.
|
|
680
|
-
*/
|
|
681
|
-
export function settleSeparator(parent, before, change, sharing, tracked) {
|
|
682
|
-
const window = splicedWindow(change);
|
|
683
|
-
const children = parent.children;
|
|
684
|
-
if (!window || !children)
|
|
685
|
-
return change;
|
|
686
|
-
// At the funnel's door, ahead of every arm: a window the mutate mis-derived reads `before`
|
|
687
|
-
// out of bounds and hands the arms a negative span, which each would clamp into silence.
|
|
688
|
-
assertInvariant('structural-descriptor', () => checkStructuralDescriptor(change, before.length));
|
|
689
|
-
const survivors = new Set(children.slice(window.at, window.at + window.added));
|
|
690
|
-
const removed = before
|
|
691
|
-
.slice(window.at, window.at + window.removed)
|
|
692
|
-
.filter((node) => !survivors.has(node));
|
|
693
|
-
return settleSplicedWindow(parent, window.at, removed, window.added, change, sharing, tracked);
|
|
694
|
-
}
|
|
695
|
-
function splicedWindow(change) {
|
|
696
|
-
switch (change.op) {
|
|
697
|
-
case 'noop':
|
|
698
|
-
return null;
|
|
699
|
-
case 'insert':
|
|
700
|
-
return { at: change.at, removed: 0, added: change.count };
|
|
701
|
-
case 'delete':
|
|
702
|
-
return { at: change.at, removed: change.count, added: 0 };
|
|
703
|
-
case 'replace':
|
|
704
|
-
return { at: change.at, removed: change.count, added: change.newCount };
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* The out-of-commit-scope twin of {@link settleSeparator}, for a container discovered by walking
|
|
709
|
-
* the live tree: it splices through the `childIds` door and reads the pre-splice span itself.
|
|
710
|
-
*/
|
|
711
|
-
export function spliceChildrenSettled(parent, at, removeCount, replacement, sharing) {
|
|
712
|
-
const children = parent.children;
|
|
713
|
-
if (!children || at < 0 || at > children.length)
|
|
714
|
-
return;
|
|
715
|
-
const removed = children.slice(at, at + removeCount);
|
|
716
|
-
spliceChildren(parent, at, removeCount, replacement);
|
|
717
|
-
// `noop` in, so what comes back describes the SETTLE alone: `spliceChildren` already carried
|
|
718
|
-
// the door's own splice into `childIds`, and out of commit scope nothing else publishes.
|
|
719
|
-
const settled = settleSplicedWindow(parent, at, removed, replacement.length, { op: 'noop' }, sharing);
|
|
720
|
-
const ids = parent.childIds;
|
|
721
|
-
if (ids)
|
|
722
|
-
applyStructuralChangeToIdsRefs(settled, ids, new Array(ids.length));
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* A splice can leave neighbours whose adjacent bytes re-read as fewer blocks on reload — a list
|
|
726
|
-
* standing above indented code absorbs it, and no separator can hold indentation apart.
|
|
727
|
-
* Absorb while the window's own bytes parse to fewer blocks, which is the reload's reading;
|
|
728
|
-
* byte-identical by construction. A blank run is transparent to a container's continuation, so the
|
|
729
|
-
* window anchors at the nearest non-blank block above the seam, never below `floor`, and cascades.
|
|
730
|
-
*/
|
|
731
|
-
function absorbSeamReading(parent, seamLeft, floor, sharing, tracked, headProbe, onBeforeSplice) {
|
|
732
|
-
const children = parent.children;
|
|
733
|
-
if (seamLeft < 0)
|
|
734
|
-
return { at: 0, span: 0, eaten: 0, spliced: false };
|
|
735
|
-
let left = seamLeft;
|
|
736
|
-
while (left > floor && isBlankParagraph(children[left]))
|
|
737
|
-
left--;
|
|
738
|
-
const at = left;
|
|
739
|
-
let span = seamLeft - at + 1;
|
|
740
|
-
let eaten = 0;
|
|
741
|
-
let spliced = false;
|
|
742
|
-
// Only the first pass: a fold re-tiles the window, so the probe's absolute index is stale.
|
|
743
|
-
let probe = headProbe;
|
|
744
|
-
for (;;) {
|
|
745
|
-
// The candidate edge crosses a blank run too: the absorbed content sits on the
|
|
746
|
-
// run's far side (a list continues into indented code across any number of blanks).
|
|
747
|
-
let right = at + span;
|
|
748
|
-
while (right < children.length && isBlankParagraph(children[right]))
|
|
749
|
-
right++;
|
|
750
|
-
const window = children.slice(at, Math.min(right + 1, children.length));
|
|
751
|
-
if (window.length <= span || window.length < 2)
|
|
752
|
-
break;
|
|
753
|
-
// A context-dependent kind has no standalone reading, so its seam is not askable.
|
|
754
|
-
if (window.some((node) => tryGetBlockKindDescriptor(node.kind)?.contextDependentKind))
|
|
755
|
-
break;
|
|
756
|
-
if (probe !== undefined && declinesOnHeadLine(window, probe - at))
|
|
757
|
-
break;
|
|
758
|
-
probe = undefined;
|
|
759
|
-
const reparsed = parse(joinedWindowBytes(window, window.length), { scope: 'fragment' });
|
|
760
|
-
const blocks = reparsed.children;
|
|
761
|
-
if (blocks.length === 0 || blocks.length >= window.length)
|
|
762
|
-
break;
|
|
763
|
-
// A fold may PROMOTE the head past what its bytes carry alone (a paragraph under the
|
|
764
|
-
// setext underline below it), so what must survive is the head's own reading, not its kind.
|
|
765
|
-
if (blocks[0].kind !== window[0].kind && !readsAsItselfAlone(window[0]))
|
|
766
|
-
break;
|
|
767
|
-
onBeforeSplice?.();
|
|
768
|
-
absorbFragmentPeel(parent, at + window.length, reparsed.suffix, blocks, sharing);
|
|
769
|
-
blocks[0].leadingTrivia = window[0].leadingTrivia;
|
|
770
|
-
for (const block of blocks) {
|
|
771
|
-
ensureEditableContainers(block);
|
|
772
|
-
if (sharing)
|
|
773
|
-
sharing.stamp(block);
|
|
774
|
-
assignChildIdsDeep(block);
|
|
775
|
-
}
|
|
776
|
-
if (tracked)
|
|
777
|
-
retrackThroughFold(tracked, at, window, blocks);
|
|
778
|
-
spliceMany(children, at, window.length, blocks);
|
|
779
|
-
eaten += window.length - blocks.length;
|
|
780
|
-
span = blocks.length;
|
|
781
|
-
spliced = true;
|
|
782
|
-
}
|
|
783
|
-
return { at, span, eaten, spliced };
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Whether a block's own bytes read back as that block. A structured container's children fail
|
|
787
|
-
* this by construction — one list item's bytes read as a LIST — which is how a scope whose
|
|
788
|
-
* children a document parse does NOT reproduce declines the seam question. Asked only where the
|
|
789
|
-
* fold changed the head's kind, so the kind-preserving folds pay nothing.
|
|
790
|
-
*/
|
|
791
|
-
function readsAsItselfAlone(node) {
|
|
792
|
-
const alone = parse(node.raw, { scope: 'fragment' }).children;
|
|
793
|
-
return alone.length === 1 && alone[0].kind === node.kind;
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Decline-only pre-parse for a window whose LAST member is the block that changed: join the
|
|
797
|
-
* others with only that block's first line. Block parsing is a left-to-right line scan, so the
|
|
798
|
-
* state entering that line is the same in both strings — if it opens a block here it opens one
|
|
799
|
-
* in the full join and no fold is possible. A pass falls through to the real parse, so no fold
|
|
800
|
-
* verdict is ever taken from truncated bytes. Costs the window minus the changed block, which is
|
|
801
|
-
* what keeps a keystroke inside a giant container off its own bytes.
|
|
802
|
-
*/
|
|
803
|
-
function declinesOnHeadLine(window, member) {
|
|
804
|
-
if (member <= 0 || member !== window.length - 1)
|
|
805
|
-
return false;
|
|
806
|
-
const raw = window[member].raw;
|
|
807
|
-
const nl = raw.indexOf('\n');
|
|
808
|
-
const joined = joinedWindowBytes(window, member) +
|
|
809
|
-
window[member].leadingTrivia +
|
|
810
|
-
(nl < 0 ? raw : raw.slice(0, nl + 1));
|
|
811
|
-
return parse(joined, { scope: 'fragment' }).children.length >= window.length;
|
|
812
|
-
}
|
|
813
|
-
/** How a fold reads a window: the head's raw, then each of the next `count - 1` members'
|
|
814
|
-
* leading trivia and raw. */
|
|
815
|
-
function joinedWindowBytes(window, count) {
|
|
816
|
-
let joined = window[0].raw;
|
|
817
|
-
for (let i = 1; i < count; i++)
|
|
818
|
-
joined += window[i].leadingTrivia + window[i].raw;
|
|
819
|
-
return joined;
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Where a byte position inside the folded window lands: the fold's own reparse re-tiles the
|
|
823
|
-
* joined bytes, and a position past the window only shifts by what the fold ate.
|
|
824
|
-
*/
|
|
825
|
-
function retrackThroughFold(tracked, at, window, blocks) {
|
|
826
|
-
const member = tracked.index - at;
|
|
827
|
-
if (member < 0)
|
|
828
|
-
return;
|
|
829
|
-
if (member >= window.length) {
|
|
830
|
-
tracked.index -= window.length - blocks.length;
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
// The offset walk mirrors {@link joinedWindowBytes}.
|
|
834
|
-
let joined = tracked.offset;
|
|
835
|
-
for (let i = 0; i < member; i++) {
|
|
836
|
-
joined += (i === 0 ? 0 : window[i].leadingTrivia.length) + window[i].raw.length;
|
|
837
|
-
}
|
|
838
|
-
if (member > 0)
|
|
839
|
-
joined += window[member].leadingTrivia.length;
|
|
840
|
-
const landed = focusTargetInReplacement(blocks, joined);
|
|
841
|
-
tracked.index = at + landed.index;
|
|
842
|
-
tracked.offset = landed.offset;
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* The seam question at every join the splice at `at` disturbed — its window's two edges and the
|
|
846
|
-
* joins inside it — since a move can invalidate a join that was already correct. Each
|
|
847
|
-
* fold cascades downward, so the next seam is asked past what that one ate. `tracked` rides the
|
|
848
|
-
* folds for a caller placing a caret in bytes an absorb can move. `headProbe` names the one block
|
|
849
|
-
* whose bytes changed, letting each ask decline on its first line alone; dropped once anything
|
|
850
|
-
* folds, since the index it names has moved by then.
|
|
851
|
-
*/
|
|
852
|
-
export function absorbWindowSeams(parent, at, added, landing, change, sharing, tracked, headProbe, onBeforeSplice) {
|
|
853
|
-
let settled = null;
|
|
854
|
-
let moved = landing;
|
|
855
|
-
let seamLeft = at - 1;
|
|
856
|
-
let last = at + added - 1;
|
|
857
|
-
while (seamLeft <= last) {
|
|
858
|
-
const seam = absorbSeamReading(parent, seamLeft, 0, sharing, tracked, settled ? undefined : headProbe, onBeforeSplice);
|
|
859
|
-
if (!seam.spliced) {
|
|
860
|
-
seamLeft++;
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
settled = settled ? unionAbsorptions(settled, seam) : seam;
|
|
864
|
-
moved = indexAfterAbsorb(moved, seam);
|
|
865
|
-
last = indexAfterAbsorb(last, seam);
|
|
866
|
-
seamLeft = seam.at + seam.span;
|
|
867
|
-
}
|
|
868
|
-
if (!settled)
|
|
869
|
-
return { change, landing: moved };
|
|
870
|
-
return { change: foldAbsorbIntoChange(change, settled), landing: moved };
|
|
871
|
-
}
|
|
872
|
-
/** Two folds as ONE window, which is what a change descriptor reports. The walk is left to
|
|
873
|
-
* right, so `later` never opens above `earlier`'s post-splice span. */
|
|
874
|
-
function unionAbsorptions(earlier, later) {
|
|
875
|
-
return {
|
|
876
|
-
at: earlier.at,
|
|
877
|
-
span: later.at + later.span - earlier.at,
|
|
878
|
-
eaten: earlier.eaten + later.eaten,
|
|
879
|
-
spliced: true
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
/** Where `index` sits once `seam` folded: a slot inside the absorbed span collapses into it. */
|
|
883
|
-
function indexAfterAbsorb(index, seam) {
|
|
884
|
-
if (index < seam.at)
|
|
885
|
-
return index;
|
|
886
|
-
const absorbedTo = seam.at + seam.span + seam.eaten;
|
|
887
|
-
return index >= absorbedTo ? index - seam.eaten : Math.min(index, seam.at + seam.span - 1);
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Where the fragment parse's peeled trailing blank run goes. At the parent's tail it stays in the
|
|
891
|
-
* last block's raw, the single-slot sink's rule; mid-document it joins the follower's run, where
|
|
892
|
-
* one line separates and every later one is a block of its own (syntax-tree.md § Blank lines).
|
|
893
|
-
*/
|
|
894
|
-
function absorbFragmentPeel(parent, followerIndex, peel, blocks, sharing) {
|
|
895
|
-
const follower = parent.children[followerIndex];
|
|
896
|
-
if (!follower) {
|
|
897
|
-
blocks[blocks.length - 1].raw += peel;
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
if (peel === '')
|
|
901
|
-
return;
|
|
902
|
-
const lines = blankLinesOf(peel + follower.leadingTrivia);
|
|
903
|
-
const owned = sharing ? ensureUnsharedChild(parent, followerIndex, sharing) : follower;
|
|
904
|
-
owned.leadingTrivia = lines.length > 1 ? '' : lines[0];
|
|
905
|
-
for (let i = 1; i < lines.length; i++) {
|
|
906
|
-
blocks.push({ kind: 'paragraph', leadingTrivia: i === 1 ? lines[0] : '', raw: lines[i] });
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
/** A blank run split back into the lines it is made of, each keeping its own ending. */
|
|
910
|
-
const blankLinesOf = (run) => run.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
911
|
-
/** The materialized tail reported inside the sink's one contiguous window. */
|
|
912
|
-
export function widenForTailMint(change, before, after) {
|
|
913
|
-
const grown = after - before;
|
|
914
|
-
if (grown === 0)
|
|
915
|
-
return change;
|
|
916
|
-
if (change.op === 'noop')
|
|
917
|
-
return { op: 'insert', at: before, count: grown };
|
|
918
|
-
if (change.op === 'insert' && change.at + change.count === before) {
|
|
919
|
-
return { ...change, count: change.count + grown };
|
|
920
|
-
}
|
|
921
|
-
if (change.op === 'replace' && change.at + change.newCount === before) {
|
|
922
|
-
return { ...change, newCount: change.newCount + grown };
|
|
923
|
-
}
|
|
924
|
-
// A delete that took the tail vacated the slot the mint lands in, so the two are one window.
|
|
925
|
-
if (change.op === 'delete' && change.at === before) {
|
|
926
|
-
return { op: 'replace', at: change.at, count: change.count, newCount: grown };
|
|
927
|
-
}
|
|
928
|
-
// The mint landed past a window that does not reach the tail, so no single contiguous
|
|
929
|
-
// span describes both; the parallel arrays would drift either way this widened it.
|
|
930
|
-
devWarn('tree-ops', 'a tail suffix materialized outside the reported window');
|
|
931
|
-
return change;
|
|
932
|
-
}
|
|
933
|
-
/** A blank line off the node's own bytes (G4.20), where one does structural work at all. */
|
|
934
|
-
function mintSeparator(parent, index, sharing) {
|
|
935
|
-
const children = parent.children;
|
|
936
|
-
if (!children || index <= bodyStartIndex(parent))
|
|
937
|
-
return;
|
|
938
|
-
if (isBlankParagraph(children[index - 1]))
|
|
939
|
-
return;
|
|
940
|
-
const owned = sharing
|
|
941
|
-
? ensureUnsharedChild(parent, index, sharing)
|
|
942
|
-
: children[index];
|
|
943
|
-
owned.leadingTrivia = trailingLineEnding(owned.raw);
|
|
944
|
-
}
|
|
945
|
-
/** Reserved chrome is not a body block, so the body window opens past it. */
|
|
946
|
-
function bodyStartIndex(parent) {
|
|
947
|
-
return bodyStartFor(ownerKindNameOf(parent));
|
|
948
|
-
}
|
|
949
|
-
/** The container's declared body wrap, whichever shape names the owner. */
|
|
950
|
-
function bodyWrapOf(parent) {
|
|
951
|
-
const kind = ownerKindNameOf(parent);
|
|
952
|
-
if (kind === undefined)
|
|
953
|
-
return undefined;
|
|
954
|
-
return tryGetBlockKindDescriptor(kind)?.bodyWrap;
|
|
955
|
-
}
|
|
956
|
-
/** The node carrying the wrap's peel slots: the sink's answer, or the parent when it IS the node. */
|
|
957
|
-
function wrapSlotsOf(parent) {
|
|
958
|
-
return ownerNodeOf(parent);
|
|
959
|
-
}
|
|
960
|
-
/** The container node these children belong to, where the caller answered for one. */
|
|
961
|
-
function ownerNodeOf(parent) {
|
|
962
|
-
return parent.owner ?? ('raw' in parent ? parent : undefined);
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* Every settle door rewrites bytes the owner's child spans describe (a sibling's separating line,
|
|
966
|
-
* a wrap slot) while leaving the children's shape alone, so the spans retire at the doors and the
|
|
967
|
-
* next rebuild re-derives (`schema/child-spans.ts`). The retire census in
|
|
968
|
-
* `lint/separator-write-doors.test.ts` names every door and reds the one that forgets.
|
|
969
|
-
*/
|
|
970
|
-
function retireChildSpans(parent) {
|
|
971
|
-
const owner = ownerNodeOf(parent);
|
|
972
|
-
if (owner)
|
|
973
|
-
dropChildSpans(owner);
|
|
974
|
-
}
|
|
975
|
-
/** The kind whose body these children are: the sink's answer, or the owner node's own. */
|
|
976
|
-
function ownerKindNameOf(parent) {
|
|
977
|
-
return 'ownerKind' in parent ? parent.ownerKind : parent.kind;
|
|
978
|
-
}
|
|
979
|
-
function bodyStartFor(kind) {
|
|
980
|
-
if (kind === undefined)
|
|
981
|
-
return 0;
|
|
982
|
-
return tryGetBlockKindDescriptor(kind)?.reservedChrome ? 1 : 0;
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* A chrome-wrapped container's parse peels the blank line against its opener into `innerPrefix`
|
|
986
|
-
* (`core/parser.parseContainerBody`), so a separator freed above the body head is that line
|
|
987
|
-
* rather than dead bytes: hand it over, or the peel eats the head block instead.
|
|
988
|
-
*/
|
|
989
|
-
function absorbWrapPrefix(parent, bodyStart, index, freed) {
|
|
990
|
-
const slots = wrapSlotsOf(parent);
|
|
991
|
-
if (!slots || (slots.innerPrefix ?? '') !== '')
|
|
992
|
-
return;
|
|
993
|
-
if (!bodyWrapOf(parent)?.afterOpenerLine)
|
|
994
|
-
return;
|
|
995
|
-
const head = parent.children?.[bodyStart];
|
|
996
|
-
if (!head || head.leadingTrivia !== '')
|
|
997
|
-
return;
|
|
998
|
-
if (index !== bodyStart && !isBlankParagraph(head))
|
|
999
|
-
return;
|
|
1000
|
-
slots.innerPrefix = trailingLineEnding(freed);
|
|
1001
|
-
}
|
|
1002
|
-
// ── Delete ──
|
|
1003
|
-
/**
|
|
1004
|
-
* Remove the node at `blockIndex`, leaving the next sibling separated from its new predecessor
|
|
1005
|
-
* and no more. Takes {@link BodyParentArg} because the settle can hand a freed line to the
|
|
1006
|
-
* owner's wrap slots. Pass `sharing` to unshare the nodes written — the successor's trivia is
|
|
1007
|
-
* the op's only in-place write.
|
|
1008
|
-
*/
|
|
1009
|
-
export function deleteNode(parent, blockIndex, sharing) {
|
|
1010
|
-
if (blockIndex < 0 || blockIndex >= parent.children.length)
|
|
1011
|
-
return { op: 'noop' };
|
|
1012
|
-
const deleted = parent.children[blockIndex];
|
|
1013
|
-
if (blockIndex + 1 < parent.children.length) {
|
|
1014
|
-
const successor = sharing
|
|
1015
|
-
? ensureUnsharedChild(parent, blockIndex + 1, sharing)
|
|
1016
|
-
: parent.children[blockIndex + 1];
|
|
1017
|
-
// The successor inherits the deleted separator only when it has none of its own:
|
|
1018
|
-
// concatenating both leaves behind a blank line the delete should have taken.
|
|
1019
|
-
successor.leadingTrivia = successor.leadingTrivia || deleted.leadingTrivia;
|
|
1020
|
-
}
|
|
1021
|
-
parent.children.splice(blockIndex, 1);
|
|
1022
|
-
clearRedundantSeparator(parent, blockIndex, sharing);
|
|
1023
|
-
// BOTH the survivor's edges: the delete puts it beside a new follower, and a merge door that
|
|
1024
|
-
// rewrote its bytes can equally have stopped it interrupting the block above.
|
|
1025
|
-
const survivor = Math.max(blockIndex - 1, 0);
|
|
1026
|
-
return absorbWindowSeams(parent, survivor, blockIndex - survivor, blockIndex, { op: 'delete', at: blockIndex, count: 1 }, sharing).change;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Update raw and re-parse. The sole re-parse transfer funnel: a kind change mints the reparsed
|
|
1030
|
-
* block into the slot rather than reassigning `kind` in place, and multi-block text mints every
|
|
1031
|
-
* parsed block. Only a same-kind single-block edit writes fields in place, so routine typing keeps
|
|
1032
|
-
* the node's object identity; `replacePreservingFirst` carries the id/ref across a mint. `sharing`
|
|
1033
|
-
* owns the separator settle's writes, which land on the run's OTHER blocks.
|
|
1034
|
-
*/
|
|
1035
|
-
export function updateNodeContent(parent, blockIndex, text, grammar, sharing) {
|
|
1036
|
-
const wasBlank = isBlankParagraph(parent.children[blockIndex]);
|
|
1037
|
-
const change = writeParsedContent(parent, blockIndex, text, grammar);
|
|
1038
|
-
const lastWritten = lastMintedIndex(change, blockIndex);
|
|
1039
|
-
// One blank line served BOTH sides: it separated this block from the one above and stood in
|
|
1040
|
-
// as the separator of the block beneath it. Ending it owes each their own.
|
|
1041
|
-
if (wasBlank && !isBlankParagraph(parent.children[blockIndex])) {
|
|
1042
|
-
restoreSeparatorOnFill(parent, blockIndex, sharing);
|
|
1043
|
-
restoreSeparatorAfterBlank(parent, followerIndexAfter(change, blockIndex), sharing);
|
|
1044
|
-
releaseWrapPeel(parent, lastWritten);
|
|
1045
|
-
return settleWriteSeams(parent, blockIndex, lastWritten, change, sharing);
|
|
1046
|
-
}
|
|
1047
|
-
// The reverse transition: the block IS the separating line now, so the run it joins gives
|
|
1048
|
-
// back the second one. The last block minted is the one that meets the follower.
|
|
1049
|
-
if (!wasBlank && isBlankParagraph(parent.children[lastWritten])) {
|
|
1050
|
-
const settled = parent.children.length;
|
|
1051
|
-
settleSeparatorOnBlank(parent, lastWritten, sharing);
|
|
1052
|
-
const widened = widenForTailMint(change, settled, parent.children.length);
|
|
1053
|
-
return settleWriteSeams(parent, blockIndex, lastWritten, widened, sharing);
|
|
1054
|
-
}
|
|
1055
|
-
// Both blank transitions settle above whatever their change op says; this guard is for
|
|
1056
|
-
// same-kind typing INSIDE content, which must never pay a neighbour reparse.
|
|
1057
|
-
if (change.op === 'noop')
|
|
1058
|
-
return { change, textStart: 0 };
|
|
1059
|
-
return settleWriteSeams(parent, blockIndex, lastWritten, change, sharing);
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* Ask every join the write disturbed, both edges of its window included, and report where the
|
|
1063
|
-
* written text ended up: an absorb ABOVE leaves the predecessor standing, so its bytes and the
|
|
1064
|
-
* join newline now sit in front of the text the caret was typed into.
|
|
1065
|
-
*/
|
|
1066
|
-
function settleWriteSeams(parent, blockIndex, lastWritten, change, sharing) {
|
|
1067
|
-
const tracked = { index: blockIndex, offset: 0 };
|
|
1068
|
-
const settled = absorbWindowSeams(parent, blockIndex, lastWritten - blockIndex + 1, blockIndex, change, sharing, tracked);
|
|
1069
|
-
return {
|
|
1070
|
-
change: settled.change,
|
|
1071
|
-
textStart: textOffsetInWindow(parent.children, settled.change, tracked)
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
/**
|
|
1075
|
-
* The tracked position as an offset in the settled window's committed text — the space every
|
|
1076
|
-
* caret door measures in ({@link focusTargetInReplacement}), where the head block's own
|
|
1077
|
-
* leading trivia is outside the window.
|
|
1078
|
-
*/
|
|
1079
|
-
function textOffsetInWindow(children, change, tracked) {
|
|
1080
|
-
const at = change.op === 'noop' ? tracked.index : change.at;
|
|
1081
|
-
let pos = 0;
|
|
1082
|
-
for (let i = at; i < tracked.index; i++) {
|
|
1083
|
-
pos += (i === at ? 0 : children[i].leadingTrivia.length) + children[i].raw.length;
|
|
1084
|
-
}
|
|
1085
|
-
const ownTrivia = tracked.index > at ? children[tracked.index].leadingTrivia.length : 0;
|
|
1086
|
-
return pos + ownTrivia + tracked.offset;
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* The absorbed window folded into the write's own, as the ONE contiguous window the sink
|
|
1090
|
-
* reports: `count` counts pre-write slots, so the union's span converts back across whatever
|
|
1091
|
-
* the write itself added or removed.
|
|
1092
|
-
*/
|
|
1093
|
-
function foldAbsorbIntoChange(change, seam) {
|
|
1094
|
-
const absorbedTo = seam.at + seam.span + seam.eaten;
|
|
1095
|
-
if (change.op === 'noop') {
|
|
1096
|
-
return {
|
|
1097
|
-
op: 'replace',
|
|
1098
|
-
at: seam.at,
|
|
1099
|
-
count: absorbedTo - seam.at,
|
|
1100
|
-
newCount: seam.span,
|
|
1101
|
-
idMap: { 0: 0 }
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
const written = change.op === 'insert' ? change.count : change.op === 'delete' ? 0 : change.newCount;
|
|
1105
|
-
const removed = change.op === 'insert' ? 0 : change.count;
|
|
1106
|
-
const lo = Math.min(seam.at, change.at);
|
|
1107
|
-
const hi = Math.max(absorbedTo, change.at + written);
|
|
1108
|
-
const count = hi - lo - written + removed;
|
|
1109
|
-
const newCount = hi - lo - seam.eaten;
|
|
1110
|
-
return {
|
|
1111
|
-
op: 'replace',
|
|
1112
|
-
at: lo,
|
|
1113
|
-
count,
|
|
1114
|
-
newCount,
|
|
1115
|
-
idMap: composeFoldIdMap(change, seam, { lo, count, newCount, written, removed })
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
/**
|
|
1119
|
-
* Identity through the fold: a slot the absorb did not re-mint still holds the block the change
|
|
1120
|
-
* put there, so its id composes through both steps instead of resetting. Slot 0 keeps the head
|
|
1121
|
-
* mapping wherever the walk has none — a fold EXTENDS its head, kind promotion included, which
|
|
1122
|
-
* is the identity `replacePreservingFirst` reports.
|
|
1123
|
-
*/
|
|
1124
|
-
function composeFoldIdMap(change, seam, window) {
|
|
1125
|
-
const idMap = {};
|
|
1126
|
-
for (let slot = 0; slot < window.newCount; slot++) {
|
|
1127
|
-
const index = window.lo + slot;
|
|
1128
|
-
if (index >= seam.at && index < seam.at + seam.span)
|
|
1129
|
-
continue;
|
|
1130
|
-
// Back through the fold: a slot past the absorbed span sat `eaten` further down before it.
|
|
1131
|
-
const spliced = index < seam.at ? index : index + seam.eaten;
|
|
1132
|
-
const old = preChangeIndex(change, spliced, window);
|
|
1133
|
-
if (old === null)
|
|
1134
|
-
continue;
|
|
1135
|
-
const oldSlot = old - window.lo;
|
|
1136
|
-
if (oldSlot >= 0 && oldSlot < window.count)
|
|
1137
|
-
idMap[slot] = oldSlot;
|
|
1138
|
-
}
|
|
1139
|
-
if (idMap[0] === undefined)
|
|
1140
|
-
idMap[0] = 0;
|
|
1141
|
-
return idMap;
|
|
1142
|
-
}
|
|
1143
|
-
/** Where `spliced` (a post-change index) stood before the change, or null for a slot it minted. */
|
|
1144
|
-
function preChangeIndex(change, spliced, window) {
|
|
1145
|
-
if (change.op === 'noop')
|
|
1146
|
-
return spliced;
|
|
1147
|
-
if (spliced < change.at)
|
|
1148
|
-
return spliced;
|
|
1149
|
-
if (spliced >= change.at + window.written)
|
|
1150
|
-
return spliced - window.written + window.removed;
|
|
1151
|
-
const inherited = change.op === 'replace' ? change.idMap?.[spliced - change.at] : undefined;
|
|
1152
|
-
return inherited === undefined ? null : change.at + inherited;
|
|
1153
|
-
}
|
|
1154
|
-
/** Where the filled block's follower ended up: a multi-block reparse pushes it down. */
|
|
1155
|
-
function followerIndexAfter(change, blockIndex) {
|
|
1156
|
-
return change.op === 'replace' ? change.at + change.newCount : blockIndex + 1;
|
|
1157
|
-
}
|
|
1158
|
-
/** The last block the write left in the slot: a multi-block reparse mints past the first. */
|
|
1159
|
-
function lastMintedIndex(change, blockIndex) {
|
|
1160
|
-
return change.op === 'replace' ? change.at + change.newCount - 1 : blockIndex;
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* The written bytes parsed, with the construct they leave OPEN closed off first. An unterminated
|
|
1164
|
-
* construct reads every block below it as its body at the next parse, and the seam settle
|
|
1165
|
-
* converges the live tree to exactly that reading. Declines where nothing is at stake: a
|
|
1166
|
-
* kind-stable single block never reaches the settle, and a construct with no follower eats
|
|
1167
|
-
* nothing.
|
|
1168
|
-
*/
|
|
1169
|
-
function closeWrittenConstruct(parent, blockIndex, text, oldKind, grammar) {
|
|
1170
|
-
// An absent grammar defaults to the global one. Fragment scope: this is one block's
|
|
1171
|
-
// bytes, whatever its position, so a position-scoped kind must not mint here.
|
|
1172
|
-
const parsed = parse(text, { grammar, scope: 'fragment' });
|
|
1173
|
-
if (blockIndex + 1 >= parent.children.length)
|
|
1174
|
-
return { text, parsed };
|
|
1175
|
-
if (parsed.children.length === 1 && parsed.children[0].kind === oldKind)
|
|
1176
|
-
return { text, parsed };
|
|
1177
|
-
const terminator = openConstructTerminator(text, parsed.children, grammar);
|
|
1178
|
-
if (!terminator)
|
|
1179
|
-
return { text, parsed };
|
|
1180
|
-
const closed = text + terminator;
|
|
1181
|
-
return { text: closed, parsed: parse(closed, { grammar, scope: 'fragment' }) };
|
|
1182
|
-
}
|
|
1183
|
-
/**
|
|
1184
|
-
* The terminator written bytes owe when their last construct absorbs to EOF, asked of the grammar
|
|
1185
|
-
* rather than a kind list: swallowing a blank line AND a prose line is what separates an absorber
|
|
1186
|
-
* from a paragraph continuing onto the next line. Null when the bytes terminate themselves, or
|
|
1187
|
-
* when no fence opener explains the absorb — the one family whose closer its opener determines.
|
|
1188
|
-
*/
|
|
1189
|
-
function openConstructTerminator(text, blocks, grammar) {
|
|
1190
|
-
// The terminator is a line of its own, so bytes whose last line is still open have none to
|
|
1191
|
-
// append to — G4.20's unterminated tail slice, which absorbs nothing while it stands alone.
|
|
1192
|
-
if (!text.endsWith('\n') || blocks.length === 0)
|
|
1193
|
-
return null;
|
|
1194
|
-
const ending = trailingLineEnding(text);
|
|
1195
|
-
const probe = parse(text + ending + NEXT_PROSE_LINE + ending, { grammar, scope: 'fragment' });
|
|
1196
|
-
if (probe.children.length !== blocks.length)
|
|
1197
|
-
return null;
|
|
1198
|
-
const raw = blocks[blocks.length - 1].raw;
|
|
1199
|
-
const nl = raw.indexOf('\n');
|
|
1200
|
-
const opener = matchFenceOpen(nl < 0 ? raw : raw.slice(0, nl));
|
|
1201
|
-
if (!opener)
|
|
1202
|
-
return null;
|
|
1203
|
-
const body = nl < 0 ? '' : raw.slice(nl + 1);
|
|
1204
|
-
const run = escalatedFenceLength(body, opener.marker, opener.length);
|
|
1205
|
-
return opener.indent + opener.marker.repeat(run) + ending;
|
|
1206
|
-
}
|
|
1207
|
-
function writeParsedContent(parent, blockIndex, text, grammar) {
|
|
1208
|
-
const node = parent.children[blockIndex];
|
|
1209
|
-
const oldKind = node.kind;
|
|
1210
|
-
const oldDescriptor = getBlockKindDescriptor(oldKind);
|
|
1211
|
-
// Ahead of every reparse below, so a write lands on the kind its committed bytes
|
|
1212
|
-
// describe, not the kind the pre-escape text would parse to (`bodyWrite`).
|
|
1213
|
-
const bodyText = forBody(parent, text);
|
|
1214
|
-
// A context-dependent kind (tableCell, plugin chrome) has no standalone recognizer, so
|
|
1215
|
-
// reparsing would downgrade it: keep the kind and write raw through the kind's own
|
|
1216
|
-
// legality pass, since a delimiter arriving bare would restructure the container.
|
|
1217
|
-
if (oldDescriptor.contextDependentKind) {
|
|
1218
|
-
writeOwnRaw(node, bodyText, grammar);
|
|
1219
|
-
return { op: 'noop' };
|
|
1220
|
-
}
|
|
1221
|
-
const { text: newText, parsed: reparsed } = closeWrittenConstruct(parent, blockIndex, bodyText, oldKind, grammar);
|
|
1222
|
-
const parsed = reparsed.children;
|
|
1223
|
-
const first = parsed[0];
|
|
1224
|
-
// A container whose empty body will be backfilled: a marker-consuming container (a
|
|
1225
|
-
// GitHub alert) needs its raw rebuilt from that body, or G1.1 stale-raw fires.
|
|
1226
|
-
const firstBackfilled = !!first && isEmptyEditableContainer(first);
|
|
1227
|
-
if (first)
|
|
1228
|
-
ensureEditableContainers(first);
|
|
1229
|
-
// Text-leading blanks fold into the first block's raw (the single-block shape); the
|
|
1230
|
-
// rest keep their own trivia.
|
|
1231
|
-
if (parsed.length > 1) {
|
|
1232
|
-
const rest = parsed.slice(1);
|
|
1233
|
-
for (const sibling of rest)
|
|
1234
|
-
ensureEditableContainers(sibling);
|
|
1235
|
-
first.raw = first.leadingTrivia + first.raw;
|
|
1236
|
-
first.leadingTrivia = node.leadingTrivia;
|
|
1237
|
-
if (firstBackfilled)
|
|
1238
|
-
reconcileBackfilledRaw(first);
|
|
1239
|
-
// The peeled line has no follower inside the splice, so it stays in raw.
|
|
1240
|
-
rest[rest.length - 1].raw += reparsed.suffix;
|
|
1241
|
-
spliceMany(parent.children, blockIndex, 1, parsed);
|
|
1242
|
-
return replacePreservingFirst(blockIndex, 1, parsed.length);
|
|
1243
|
-
}
|
|
1244
|
-
const newKind = first?.kind ?? 'paragraph';
|
|
1245
|
-
// In-place refresh keeps the node's object identity: component, IME state, and inline
|
|
1246
|
-
// cache are all keyed on it.
|
|
1247
|
-
if (newKind === oldKind) {
|
|
1248
|
-
node.raw = newText;
|
|
1249
|
-
adoptReparsedFields(node, first);
|
|
1250
|
-
if (firstBackfilled)
|
|
1251
|
-
reconcileBackfilledRaw(node);
|
|
1252
|
-
return { op: 'noop' };
|
|
1253
|
-
}
|
|
1254
|
-
const replacement = first ?? { kind: 'paragraph', leadingTrivia: '', raw: newText };
|
|
1255
|
-
replacement.raw = newText;
|
|
1256
|
-
replacement.leadingTrivia = node.leadingTrivia;
|
|
1257
|
-
if (firstBackfilled)
|
|
1258
|
-
reconcileBackfilledRaw(replacement);
|
|
1259
|
-
parent.children.splice(blockIndex, 1, replacement);
|
|
1260
|
-
return replacePreservingFirst(blockIndex, 1, 1);
|
|
1261
|
-
}
|
|
1262
|
-
// ── Container kind re-derivation ──
|
|
1263
|
-
/**
|
|
1264
|
-
* What the grammar opens `line` as, read in isolation — the opener-significance test, asked of
|
|
1265
|
-
* the opener registry and never a kind list, so a kind registered later is covered the day it
|
|
1266
|
-
* registers.
|
|
1267
|
-
*/
|
|
1268
|
-
export function lineOpensAs(line, grammar) {
|
|
1269
|
-
return parse(`${line}\n`, { grammar, scope: 'fragment' }).children[0]?.kind ?? 'paragraph';
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Re-derive the container at `index` from its own (already rebuilt) raw, replacing it in
|
|
1273
|
-
* the slot when that raw now opens as a different kind. The container twin of
|
|
1274
|
-
* `updateNodeContent`'s kind-change arm (editor.md § 8), which no leaf reparse can see.
|
|
1275
|
-
* Eligibility is the opener registry, not a kind list — registering an opener is exactly
|
|
1276
|
-
* the claim that `parse(raw)` reproduces the kind, so re-deriving one without would destroy it.
|
|
1277
|
-
*/
|
|
1278
|
-
export function reclassifyContainer(parent, index, grammar) {
|
|
1279
|
-
const node = parent.children[index];
|
|
1280
|
-
if (!node)
|
|
1281
|
-
return null;
|
|
1282
|
-
const descriptor = tryGetBlockKindDescriptor(node.kind);
|
|
1283
|
-
if (!descriptor?.isContainer || !isBlockOpenerRegistered(node.kind))
|
|
1284
|
-
return null;
|
|
1285
|
-
if (perfEnabled())
|
|
1286
|
-
recordContainerKindReparse();
|
|
1287
|
-
const parsed = parse(node.raw, { grammar, scope: 'fragment' }).children;
|
|
1288
|
-
// A container's raw is one block by construction; a multi-block reparse means bytes
|
|
1289
|
-
// this seam has no slot for — leave it to the gesture that owns the mutation.
|
|
1290
|
-
if (parsed.length !== 1 || parsed[0].kind === node.kind)
|
|
1291
|
-
return null;
|
|
1292
|
-
const replacement = parsed[0];
|
|
1293
|
-
const backfilled = isEmptyEditableContainer(replacement);
|
|
1294
|
-
ensureEditableContainers(replacement);
|
|
1295
|
-
// The slot's trivia is authoritative, so restore the bytes before overwriting it or
|
|
1296
|
-
// anything the parse split off the front vanishes with it.
|
|
1297
|
-
replacement.raw = node.raw;
|
|
1298
|
-
replacement.leadingTrivia = node.leadingTrivia;
|
|
1299
|
-
if (backfilled)
|
|
1300
|
-
reconcileBackfilledRaw(replacement);
|
|
1301
|
-
// A freshly-parsed node carries no childIds and this swap publishes under the slot's
|
|
1302
|
-
// reused component instance, so undefined keys would reach the nested keyed `{#each}`.
|
|
1303
|
-
assignChildIdsDeep(replacement);
|
|
1304
|
-
parent.children[index] = replacement;
|
|
1305
|
-
// Write-then-re-read (tree-operations/unshare.ts header).
|
|
1306
|
-
return parent.children[index];
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Map a post-edit caret offset (in the committed text) to the parsed block it falls in,
|
|
1310
|
-
* as a local display offset. The first block's body starts at 0; an offset inside
|
|
1311
|
-
* inter-block trivia lands at the next block's start; past-the-end clamps to the last.
|
|
1312
|
-
*/
|
|
1313
|
-
export function focusTargetInReplacement(nodes, offset) {
|
|
1314
|
-
let pos = 0;
|
|
1315
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1316
|
-
const bodyStart = i === 0 ? 0 : pos + nodes[i].leadingTrivia.length;
|
|
1317
|
-
const bodyEnd = bodyStart + trimTrailingLineEnding(nodes[i].raw).length;
|
|
1318
|
-
if (offset <= bodyEnd) {
|
|
1319
|
-
return { index: i, offset: Math.max(0, offset - bodyStart) };
|
|
1320
|
-
}
|
|
1321
|
-
pos = bodyStart + nodes[i].raw.length;
|
|
1322
|
-
}
|
|
1323
|
-
const last = nodes.length - 1;
|
|
1324
|
-
return { index: last, offset: trimTrailingLineEnding(nodes[last].raw).length };
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Where a caret at `offset` in the written text lands once {@link updateNodeContent}'s folds
|
|
1328
|
-
* settled: an absorb ABOVE leaves the predecessor holding the bytes, so the slot the gesture
|
|
1329
|
-
* named is gone and the offset carries what that predecessor put in front of it. Every caret
|
|
1330
|
-
* door onto the content funnel asks this, so the settle is answered in one place.
|
|
1331
|
-
*/
|
|
1332
|
-
export function settledCaretTarget(settled, at, offset, children) {
|
|
1333
|
-
const { change, textStart } = settled;
|
|
1334
|
-
if (change.op !== 'replace')
|
|
1335
|
-
return { index: at, offset };
|
|
1336
|
-
const shifted = offset + textStart;
|
|
1337
|
-
if (change.newCount <= 1)
|
|
1338
|
-
return { index: change.at, offset: shifted };
|
|
1339
|
-
const blocks = children.slice(change.at, change.at + change.newCount);
|
|
1340
|
-
const target = focusTargetInReplacement(blocks, shifted);
|
|
1341
|
-
return { index: change.at + target.index, offset: target.offset };
|
|
1342
|
-
}
|
|
1343
336
|
// ── Reparse helper (private) ──
|
|
1344
337
|
/**
|
|
1345
338
|
* Reparse a half's bytes as the blocks they hold, plus the trailing blank line the fragment
|
|
1346
|
-
* parse peels into `doc.suffix
|
|
1347
|
-
* because a half stands in a position its bytes never occupied, where a construct boundary
|
|
1348
|
-
* can newly materialize.
|
|
339
|
+
* parse peels into `doc.suffix`: every sink answers for it, or the bytes are lost.
|
|
1349
340
|
*/
|
|
1350
341
|
function reparseAsNodes(raw, leadingTrivia) {
|
|
1351
342
|
const doc = parse(raw, { scope: 'fragment' });
|
|
@@ -1359,8 +350,7 @@ function reparseAsNodes(raw, leadingTrivia) {
|
|
|
1359
350
|
}
|
|
1360
351
|
/**
|
|
1361
352
|
* The merge sinks' single-block twin, and their decline: a join whose bytes read as several
|
|
1362
|
-
* blocks has no home in one slot, so
|
|
1363
|
-
* refusal; the door returns noop and its caller falls back to move-focus.
|
|
353
|
+
* blocks has no home in one slot, so null refuses it rather than truncating (G1.35).
|
|
1364
354
|
*/
|
|
1365
355
|
function reparseAsNode(raw, leadingTrivia) {
|
|
1366
356
|
const { nodes, suffix } = reparseAsNodes(raw, leadingTrivia);
|
|
@@ -1370,59 +360,3 @@ function reparseAsNode(raw, leadingTrivia) {
|
|
|
1370
360
|
nodes[0].raw += suffix;
|
|
1371
361
|
return nodes[0];
|
|
1372
362
|
}
|
|
1373
|
-
// ── Replacement normalization ──
|
|
1374
|
-
/** First node inherits the original block's leadingTrivia; subsequent nodes keep theirs. */
|
|
1375
|
-
export function normalizeReplacementTrivia(original, replacement) {
|
|
1376
|
-
const originalTrivia = original.leadingTrivia ?? '';
|
|
1377
|
-
return replacement.map((node, i) => {
|
|
1378
|
-
const copy = { ...node };
|
|
1379
|
-
copy.leadingTrivia = i === 0 ? originalTrivia : (copy.leadingTrivia ?? '');
|
|
1380
|
-
return copy;
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
// ── Editable container backfill ──
|
|
1384
|
-
/**
|
|
1385
|
-
* A container `ensureEditableContainers` will backfill. Read BEFORE the backfill runs —
|
|
1386
|
-
* afterwards it holds the synthesized paragraph and no longer qualifies.
|
|
1387
|
-
*/
|
|
1388
|
-
function isEmptyEditableContainer(node) {
|
|
1389
|
-
const d = getBlockKindDescriptor(node.kind);
|
|
1390
|
-
return d.isContainer && d.blockFocus !== 'whole-block' && (node.children?.length ?? 0) === 0;
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Sync a just-backfilled container's `raw` to its synthesized body. Needed only for a
|
|
1394
|
-
* marker-consuming container whose typed raw lacks a blank body line (`> [!TYPE]`);
|
|
1395
|
-
* blockquote/listItem already strip to the blank, so their rebuild is a no-op.
|
|
1396
|
-
*/
|
|
1397
|
-
function reconcileBackfilledRaw(node) {
|
|
1398
|
-
getBlockKindDescriptor(node.kind).rebuildRaw?.(node);
|
|
1399
|
-
}
|
|
1400
|
-
/** Ensure every container has at least one child block, so the cursor always has a target. */
|
|
1401
|
-
export function ensureEditableContainers(node) {
|
|
1402
|
-
// A whole-block-focus kind is childless by design — the block itself is the caret
|
|
1403
|
-
// target, and a backfilled paragraph its raw can't account for trips opaque-stale-raw.
|
|
1404
|
-
if (getBlockKindDescriptor(node.kind).blockFocus === 'whole-block')
|
|
1405
|
-
return;
|
|
1406
|
-
if (node.children !== undefined) {
|
|
1407
|
-
if (node.children.length === 0) {
|
|
1408
|
-
// discovered-descendant mutation, see file header
|
|
1409
|
-
const chromeKind = reservedChromeKindOf(node.kind);
|
|
1410
|
-
// Backfilled lines take the container's own ending (G4.20) — they are pure
|
|
1411
|
-
// line ending, so a literal LF strands one inside a CRLF container.
|
|
1412
|
-
const lineEnding = trailingLineEnding(node.raw);
|
|
1413
|
-
// A chrome-declaring container must re-mint its child-0 leaf too, or the
|
|
1414
|
-
// backfilled paragraph would occupy the reserved slot and violate G1.14.
|
|
1415
|
-
if (chromeKind !== undefined) {
|
|
1416
|
-
// Runtime chrome kind, so the mint takes the generic cast.
|
|
1417
|
-
node.children.push({ kind: chromeKind, leadingTrivia: '', raw: lineEnding });
|
|
1418
|
-
}
|
|
1419
|
-
node.children.push(emptyParagraph('', lineEnding));
|
|
1420
|
-
// The synthesized paragraph's ending already represents the blank `parseBlocks`
|
|
1421
|
-
// routed into innerPrefix; keeping both double-counts the line on rebuild.
|
|
1422
|
-
node.innerPrefix = '';
|
|
1423
|
-
}
|
|
1424
|
-
for (const child of node.children) {
|
|
1425
|
-
ensureEditableContainers(child);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
}
|