Skip to content

Commit 9bcb0ad

Browse files
committed
refactor(core): simplify the BlockInfo API and make it the single vocabulary for block/children plumbing
Combines the former block-info and blockinfo-consolidation changes into one API-simplification pass over BlockInfo. Field renames (drop internal jargon; PM group name strings unchanged): - bnBlock -> block - blockContent -> content - childContainer -> children - isWrappedBlock -> hasContent Precomputed derived fields, replacing hand arithmetic at dozens of callsites (blockContent.beforePos + 1, afterPos - 1, empty-inline checks): - contentStart / contentEnd - children.childrenStart / children.childrenEnd - contentKind: "inline" | "none" | "table" | "other" - isContentEmpty Producer consolidation: 6 overlapping producers -> 4, named by what you have (getBlockInfoFromNode, getBlockInfoAt, getBlockInfoNearPos, getBlockInfoFromSelection). getBlockInfo and getBlockInfoFromResolvedPos are deleted; getBlockInfoWithManualOffset, getBlockInfoAtNearest and getBottomNestedBlockInfo are renamed. One shape resolver: getBlockRegions(node) -> { outer, content?, childrenHolder? } resolves container vs blockContainer shape in one place, consumed by getBlockInfoFromNode. Deleted the synonym vocabularies that answered "where do children live" in parallel: childrenHolder.ts, ChildrenWriteTarget, fixContainer's private repair targets, and the descend seal-variant trio (now one self-recursive descendToLastInsertionPos returning { pos, crossedSeal }). Deleted helpers that were bare property reads: getChildrenConfig, isContainerType, isPlaceableAnywhere, isInsertableChild; inlined deleteBlockCollapsingSingletonGroup and the table-caret +-4 arithmetic. Navigation helpers (getParentBlockInfo / getPrevBlockInfo / getNextBlockInfo / getLastDescendantBlockInfo) move from mergeBlocks.ts to getBlockInfoFromPos.ts and become public. getParentBlockInfo now has block-model semantics: a block inside a column parents to the column, not the columnList, fixing the Delete-at-end climb's seal check for container children.
1 parent 7ec114f commit 9bcb0ad

53 files changed

Lines changed: 1811 additions & 1020 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
StyleSchema,
1010
} from "../../../../schema/index.js";
1111
import { isContainerNode } from "../../../../schema/blocks/children.js";
12+
import { getBlockInfoFromNode } from "../../../getBlockInfoFromPos.js";
1213
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
1314
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
1415
import { getNodeById } from "../../../nodeUtil.js";
@@ -51,10 +52,10 @@ export function getInsertionPos(
5152
): { pos: number; wrapIn?: NodeType } | null {
5253
const { node, posBeforeNode } = reference;
5354

54-
const descend = (holder: Node, pos: number) =>
55+
const descend = (holder: { node: Node; beforePos: number }) =>
5556
placement === "start"
56-
? descendToFirstInsertionPos(holder, pos, nodeType)
57-
: descendToLastInsertionPos(holder, pos, nodeType);
57+
? descendToFirstInsertionPos(holder, nodeType)
58+
: descendToLastInsertionPos(holder, nodeType).pos;
5859

5960
if (placement === "before" || placement === "after") {
6061
const pos =
@@ -66,33 +67,31 @@ export function getInsertionPos(
6667
: null;
6768
}
6869

69-
// A container holds its children itself. The descent helpers ignore sealed
70-
// boundaries by default, which is correct here: an explicit `insertBlocks`
71-
// placement is an intentional crossing.
72-
if (isContainerNode(node.type)) {
73-
const pos = descend(node, posBeforeNode);
74-
75-
return pos === null ? null : { pos };
76-
}
77-
78-
// A regular block keeps its children in a `blockGroup` that only exists once
79-
// it has some.
80-
const blockGroupType = nodeType.schema.nodes["blockGroup"];
81-
if (node.type.name !== "blockContainer" || !blockGroupType) {
70+
// Neither a container nor a `blockContainer` (possible only for exotic
71+
// hand-written specs): nothing can nest inside it.
72+
if (!isContainerNode(node.type) && node.type.name !== "blockContainer") {
8273
return null;
8374
}
8475

85-
const blockGroupPos = posBeforeNode + 1 + node.firstChild!.nodeSize;
76+
const info = getBlockInfoFromNode(node, posBeforeNode);
8677

87-
if (node.childCount < 2) {
88-
return blockGroupType.contentMatch.matchType(nodeType)
89-
? { pos: blockGroupPos, wrapIn: blockGroupType }
90-
: null;
78+
if (info.children) {
79+
// The descent helpers report sealed boundaries but this caller ignores
80+
// them: an explicit `insertBlocks` placement is an intentional crossing.
81+
const pos = descend(info.children);
82+
83+
return pos === null ? null : { pos };
9184
}
9285

93-
const pos = descend(node.lastChild!, blockGroupPos);
86+
// No children holder implies a `blockContainer` with no children yet
87+
// (containers always have one): its `blockGroup` is lazy (`blockContent
88+
// blockGroup?`), so the position after the content node only becomes valid
89+
// once the nodes are wrapped in a new group.
90+
const blockGroupType = nodeType.schema.nodes["blockGroup"];
9491

95-
return pos === null ? null : { pos };
92+
return info.hasContent && blockGroupType?.contentMatch.matchType(nodeType)
93+
? { pos: info.content.afterPos, wrapIn: blockGroupType }
94+
: null;
9695
}
9796

9897
export function insertBlocks<

packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { describe, expect, it } from "vite-plus/test";
22

33
import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js";
44
import { setupTestEnv } from "../../setupTestEnv.js";
5-
import { getParentBlockInfo, mergeBlocksCommand } from "./mergeBlocks.js";
5+
import { getParentBlockInfo } from "../../../getBlockInfoFromPos.js";
6+
import { mergeBlocksCommand } from "./mergeBlocks.js";
67

78
const getEditor = setupTestEnv();
89

@@ -14,7 +15,7 @@ function mergeBlocks(posBetweenBlocks: number) {
1415

1516
function getPosBeforeSelectedBlock() {
1617
return getEditor().transact(
17-
(tr) => getBlockInfoFromSelection(tr).bnBlock.beforePos,
18+
(tr) => getBlockInfoFromSelection(tr).block.beforePos,
1819
);
1920
}
2021

packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts

Lines changed: 20 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,19 @@
1-
import { Node } from "prosemirror-model";
21
import { EditorState } from "prosemirror-state";
32

4-
import { isSealed } from "../../../../schema/blocks/children.js";
53
import {
64
BlockInfo,
7-
getBlockInfoFromResolvedPos,
5+
getBlockInfoAt,
6+
getLastDescendantBlockInfo,
7+
getPrevBlockInfo,
88
} from "../../../getBlockInfoFromPos.js";
99

10-
/**
11-
* Returns the block info from the parent block
12-
* or undefined if we're at the root
13-
*/
14-
export const getParentBlockInfo = (
15-
doc: Node,
16-
beforePos: number,
17-
): BlockInfo | undefined => {
18-
const $pos = doc.resolve(beforePos);
19-
const depth = $pos.depth - 1;
20-
21-
if (depth < 1) {
22-
return undefined;
23-
}
24-
25-
const parentBeforePos = $pos.before(depth);
26-
const parentNode = doc.resolve(parentBeforePos).nodeAfter;
27-
28-
if (!parentNode) {
29-
return undefined;
30-
}
31-
32-
if (!parentNode.type.spec.group?.includes("bnBlock")) {
33-
return getParentBlockInfo(doc, parentBeforePos);
34-
}
35-
36-
const parentBlockInfo = getBlockInfoFromResolvedPos(
37-
doc.resolve(parentBeforePos),
38-
);
39-
40-
return parentBlockInfo;
41-
};
42-
43-
/**
44-
* Returns the block info from the sibling block before (above) the given block,
45-
* or undefined if the given block is the first sibling.
46-
*/
47-
export const getPrevBlockInfo = (doc: Node, beforePos: number) => {
48-
const $pos = doc.resolve(beforePos);
49-
50-
const indexInParent = $pos.index();
51-
52-
if (indexInParent === 0) {
53-
return undefined;
54-
}
55-
56-
const prevBlockBeforePos = $pos.posAtIndex(indexInParent - 1);
57-
58-
const prevBlockInfo = getBlockInfoFromResolvedPos(
59-
doc.resolve(prevBlockBeforePos),
60-
);
61-
return prevBlockInfo;
62-
};
63-
64-
/**
65-
* Returns the block info from the sibling block after (below) the given block,
66-
* or undefined if the given block is the last sibling.
67-
*/
68-
export const getNextBlockInfo = (doc: Node, beforePos: number) => {
69-
const $pos = doc.resolve(beforePos);
70-
71-
const indexInParent = $pos.index();
72-
73-
if (indexInParent === $pos.node().childCount - 1) {
74-
return undefined;
75-
}
76-
77-
const nextBlockBeforePos = $pos.posAtIndex(indexInParent + 1);
78-
79-
const nextBlockInfo = getBlockInfoFromResolvedPos(
80-
doc.resolve(nextBlockBeforePos),
81-
);
82-
return nextBlockInfo;
83-
};
84-
85-
/**
86-
* If a block has children like this:
87-
* A
88-
* - B
89-
* - C
90-
* -- D
91-
*
92-
* Then the bottom nested block returned is D.
93-
*/
94-
export const getBottomNestedBlockInfo = (
95-
doc: Node,
96-
blockInfo: BlockInfo,
97-
// Callers that move content stop the descent at a sealed container, getting
98-
// the container itself rather than a block inside it. Caret-only callers
99-
// descend through. Sealed boundaries govern content, not navigation.
100-
opts?: { stopAtSealed?: boolean },
101-
) => {
102-
// A container that allows zero children can have an empty child container,
103-
// in which case the block itself is the bottom one.
104-
while (blockInfo.childContainer && blockInfo.childContainer.node.childCount) {
105-
if (opts?.stopAtSealed && isSealed(blockInfo.childContainer.node)) {
106-
break;
107-
}
108-
const group = blockInfo.childContainer.node;
109-
110-
const newPos = doc
111-
.resolve(blockInfo.childContainer.beforePos + 1)
112-
.posAtIndex(group.childCount - 1);
113-
blockInfo = getBlockInfoFromResolvedPos(doc.resolve(newPos));
114-
}
115-
116-
return blockInfo;
117-
};
118-
11910
const canMerge = (prevBlockInfo: BlockInfo, nextBlockInfo: BlockInfo) => {
12011
return (
121-
prevBlockInfo.isWrappedBlock &&
122-
prevBlockInfo.blockContent.node.type.spec.content === "inline*" &&
123-
prevBlockInfo.blockContent.node.childCount > 0 &&
124-
nextBlockInfo.isWrappedBlock &&
125-
nextBlockInfo.blockContent.node.type.spec.content === "inline*"
12+
prevBlockInfo.hasContent &&
13+
prevBlockInfo.contentKind === "inline" &&
14+
!prevBlockInfo.isContentEmpty &&
15+
nextBlockInfo.hasContent &&
16+
nextBlockInfo.contentKind === "inline"
12617
);
12718
};
12819

@@ -133,25 +24,25 @@ const mergeBlocks = (
13324
nextBlockInfo: BlockInfo,
13425
) => {
13526
// Un-nests all children of the next block.
136-
if (!nextBlockInfo.isWrappedBlock) {
27+
if (!nextBlockInfo.hasContent) {
13728
throw new Error(
138-
`Attempted to merge block at position ${nextBlockInfo.bnBlock.beforePos} into previous block at position ${prevBlockInfo.bnBlock.beforePos}, but next block is not a block container`,
29+
`Attempted to merge block at position ${nextBlockInfo.block.beforePos} into previous block at position ${prevBlockInfo.block.beforePos}, but next block is not a block container`,
13930
);
14031
}
14132

14233
// Removes a level of nesting all children of the next block by 1 level, if it contains both content and block
14334
// group nodes.
144-
if (nextBlockInfo.childContainer) {
35+
if (nextBlockInfo.children) {
14536
const childBlocksStart = state.doc.resolve(
146-
nextBlockInfo.childContainer.beforePos + 1,
37+
nextBlockInfo.children.childrenStart,
14738
);
14839
const childBlocksEnd = state.doc.resolve(
149-
nextBlockInfo.childContainer.afterPos - 1,
40+
nextBlockInfo.children.childrenEnd,
15041
);
15142
const childBlocksRange = childBlocksStart.blockRange(childBlocksEnd);
15243

15344
if (dispatch) {
154-
const pos = state.doc.resolve(nextBlockInfo.bnBlock.beforePos);
45+
const pos = state.doc.resolve(nextBlockInfo.block.beforePos);
15546
state.tr.lift(childBlocksRange!, pos.depth);
15647
}
15748
}
@@ -160,9 +51,9 @@ const mergeBlocks = (
16051
// removing the closing tags of the first block and the opening tags of the
16152
// second one to stitch them together.
16253
if (dispatch) {
163-
if (!prevBlockInfo.isWrappedBlock) {
54+
if (!prevBlockInfo.hasContent) {
16455
throw new Error(
165-
`Attempted to merge block at position ${nextBlockInfo.bnBlock.beforePos} into previous block at position ${prevBlockInfo.bnBlock.beforePos}, but previous block is not a block container`,
56+
`Attempted to merge block at position ${nextBlockInfo.block.beforePos} into previous block at position ${prevBlockInfo.block.beforePos}, but previous block is not a block container`,
16657
);
16758
}
16859

@@ -172,10 +63,7 @@ const mergeBlocks = (
17263
// `KeyboardShortcutsExtension` handle those cases by moving blocks
17364
// across the boundary instead of merging their content.
17465
dispatch(
175-
state.tr.delete(
176-
prevBlockInfo.blockContent.afterPos - 1,
177-
nextBlockInfo.blockContent.beforePos + 1,
178-
),
66+
state.tr.delete(prevBlockInfo.contentEnd, nextBlockInfo.contentStart),
17967
);
18068
}
18169

@@ -191,19 +79,18 @@ export const mergeBlocksCommand =
19179
state: EditorState;
19280
dispatch: ((args?: any) => any) | undefined;
19381
}) => {
194-
const $pos = state.doc.resolve(posBetweenBlocks);
195-
const nextBlockInfo = getBlockInfoFromResolvedPos($pos);
82+
const nextBlockInfo = getBlockInfoAt(state.doc, posBetweenBlocks);
19683

19784
const prevBlockInfo = getPrevBlockInfo(
19885
state.doc,
199-
nextBlockInfo.bnBlock.beforePos,
86+
nextBlockInfo.block.beforePos,
20087
);
20188

20289
if (!prevBlockInfo) {
20390
return false;
20491
}
20592

206-
const bottomNestedBlockInfo = getBottomNestedBlockInfo(
93+
const bottomNestedBlockInfo = getLastDescendantBlockInfo(
20794
state.doc,
20895
prevBlockInfo,
20996
);

packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CellSelection } from "prosemirror-tables";
33
import { describe, expect, it } from "vite-plus/test";
44

55
import {
6-
getBlockInfoAtNearest,
6+
getBlockInfoNearPos,
77
getBlockInfoFromSelection,
88
getNodeId,
99
} from "../../../getBlockInfoFromPos.js";
@@ -18,35 +18,35 @@ const getEditor = setupTestEnv();
1818

1919
function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") {
2020
const blockInfo = getEditor().transact((tr) => getBlockInfoFromSelection(tr));
21-
if (!blockInfo.isWrappedBlock) {
21+
if (!blockInfo.hasContent) {
2222
throw new Error(
2323
`Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node`,
2424
);
2525
}
26-
const { blockContent } = blockInfo;
26+
const { content } = blockInfo;
2727

2828
const editor = getEditor();
2929
if (selectionType === "cell") {
3030
editor.transact((tr) =>
3131
tr.setSelection(
3232
CellSelection.create(
3333
tr.doc,
34-
tr.doc.resolve(blockContent.beforePos + 3).before(),
35-
tr.doc.resolve(blockContent.afterPos - 3).before(),
34+
tr.doc.resolve(content.beforePos + 3).before(),
35+
tr.doc.resolve(content.afterPos - 3).before(),
3636
),
3737
),
3838
);
3939
} else if (selectionType === "node") {
4040
editor.transact((tr) =>
41-
tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)),
41+
tr.setSelection(NodeSelection.create(tr.doc, content.beforePos)),
4242
);
4343
} else {
4444
editor.transact((tr) =>
4545
tr.setSelection(
4646
TextSelection.create(
4747
tr.doc,
48-
blockContent.beforePos + 1,
49-
blockContent.afterPos - 1,
48+
content.beforePos + 1,
49+
content.afterPos - 1,
5050
),
5151
),
5252
);
@@ -223,11 +223,11 @@ describe("Test moveBlocksUp", () => {
223223

224224
const { anchorBlockId, headBlockId } = getEditor().transact((tr) => ({
225225
anchorBlockId: getNodeId(
226-
getBlockInfoAtNearest(tr, tr.selection.anchor).bnBlock.node,
226+
getBlockInfoNearPos(tr, tr.selection.anchor).block.node,
227227
tr.doc,
228228
),
229229
headBlockId: getNodeId(
230-
getBlockInfoAtNearest(tr, tr.selection.head).bnBlock.node,
230+
getBlockInfoNearPos(tr, tr.selection.head).block.node,
231231
tr.doc,
232232
),
233233
}));
@@ -347,11 +347,11 @@ describe("Test moveBlocksDown", () => {
347347

348348
const { anchorBlockId, headBlockId } = getEditor().transact((tr) => ({
349349
anchorBlockId: getNodeId(
350-
getBlockInfoAtNearest(tr, tr.selection.anchor).bnBlock.node,
350+
getBlockInfoNearPos(tr, tr.selection.anchor).block.node,
351351
tr.doc,
352352
),
353353
headBlockId: getNodeId(
354-
getBlockInfoAtNearest(tr, tr.selection.head).bnBlock.node,
354+
getBlockInfoNearPos(tr, tr.selection.head).block.node,
355355
tr.doc,
356356
),
357357
}));

0 commit comments

Comments
 (0)