Skip to content

Commit 3c16da4

Browse files
committed
refactor(core): simplify the BlockInfo API and make it the single vocabulary for block/children plumbing
Consolidates the ~8 vocabularies that answered "where do this block's children live?" down to two: `getBlockRegions` resolves block shape in one place, and `BlockInfo` is the position-annotated view everything reads. BlockInfo shape: - `bnBlock` -> `block`, `blockContent` -> `content`, `childContainer` -> `children`, `isWrappedBlock` -> `hasContent` - precomputed `contentStart`/`contentEnd`, `children.childrenStart`/`childrenEnd`, `contentKind`, `isContentEmpty` replace hand arithmetic at 40+ call sites - still a discriminated union on `hasContent`, so narrowing guards keep working Producers: 6 -> 4. `getBlockInfo` and `getBlockInfoFromResolvedPos` are gone; `getBlockInfoWithManualOffset`, `getBlockInfoAtNearest` and `getBottomNestedBlockInfo` become `getBlockInfoFromNode`, `getBlockInfoNearPos` and `getLastDescendantBlockInfo`. 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. This fixes the Delete-at-end climb running its seal check on the wrong node for container children. Deleted synonym vocabularies and bare-property-read helpers: `childrenHolder.ts`, `ChildrenWriteTarget`, `fixContainer`'s private repair targets, `getChildrenConfig`, `isContainerType`, `isPlaceableAnywhere`, `isInsertableChild`, `flattenNonInsertableBlocks`, `seedRefillChildren`, `assertSchemaInvariants.ts`, and most of `validateChildren.ts` (which duplicated checks ProseMirror already enforces). `descendToFirstInsertionPos`/`descendToLastInsertionPos` merge into `descendToInsertionPos(info, nodeType, edge, opts)`. `setSelection` and `setTextCursorPosition` drop their manual table arithmetic and content-type switches in favour of `blockEdgePos`/`blockEdgeSelection`. `canNestBlock` and `canUnnestBlock` now dry-run the real command instead of re-deriving its preconditions. `insertBlocks` placements `"start"`/`"end"` are renamed to `"first-child"`/ `"last-child"`. The cursor-placement vocabulary of `setTextCursorPosition` is unrelated and unchanged.
1 parent 7ec114f commit 3c16da4

59 files changed

Lines changed: 2150 additions & 2053 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.

docs/content/docs/features/custom-schemas/container-blocks.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ editor.insertBlocks([{ type: "paragraph" }], calloutId, "before");
169169
editor.insertBlocks([{ type: "paragraph" }], calloutId, "after");
170170

171171
// Nested inside it, as its first or last child:
172-
editor.insertBlocks([{ type: "paragraph" }], calloutId, "start");
173-
editor.insertBlocks([{ type: "paragraph" }], calloutId, "end");
172+
editor.insertBlocks([{ type: "paragraph" }], calloutId, "first-child");
173+
editor.insertBlocks([{ type: "paragraph" }], calloutId, "last-child");
174174
```
175175

176176
The nested placements are what addresses a container with no children to point at. A `min: 0` container that is currently empty has no child block to insert before or after. Whether a block fits is answered by the schema, so it's your `children` config that decides.

docs/content/docs/reference/editor/manipulating-content.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ editor.forEachBlock((block) => {
141141
insertBlocks(
142142
blocksToInsert: PartialBlock[],
143143
referenceBlock: BlockIdentifier,
144-
placement: "before" | "after" | "start" | "end" = "before"
144+
placement: "before" | "after" | "first-child" | "last-child" = "before"
145145
): void
146146
```
147147

148-
Inserts new blocks relative to an existing block. `"before"` and `"after"` make the new blocks siblings of the reference block; `"start"` and `"end"` nest them inside it, as its first or last children. See [Inserting into a container](/docs/features/custom-schemas/container-blocks#inserting-into-a-container).
148+
Inserts new blocks relative to an existing block. `"before"` and `"after"` make the new blocks siblings of the reference block; `"first-child"` and `"last-child"` nest them inside it. See [Inserting into a container](/docs/features/custom-schemas/container-blocks#inserting-into-a-container).
149149

150150
```typescript
151151
// Insert a paragraph before an existing block
@@ -169,7 +169,7 @@ editor.insertBlocks(
169169
editor.insertBlocks(
170170
[{ type: "paragraph", content: "Nested paragraph" }],
171171
"container-block-id",
172-
"end",
172+
"last-child",
173173
);
174174
```
175175

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

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,22 @@ import {
88
InlineContentSchema,
99
StyleSchema,
1010
} from "../../../../schema/index.js";
11-
import { isContainerNode } from "../../../../schema/blocks/children.js";
11+
import { getBlockInfoFromNode } from "../../../getBlockInfoFromPos.js";
1212
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
1313
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
1414
import { getNodeById } from "../../../nodeUtil.js";
1515
import { getPmSchema } from "../../../pmUtil.js";
16-
import {
17-
descendToFirstInsertionPos,
18-
descendToLastInsertionPos,
19-
} from "../../containers/containerNav.js";
16+
import { descendToInsertionPos } from "../../containers/containerNav.js";
2017

2118
/**
22-
* Where blocks go relative to a reference block. `"before"`/`"after"` make them
23-
* siblings of it; `"start"`/`"end"` nest them inside it, as its first or last
24-
* children.
19+
* Where blocks go relative to a reference block. `"before"`/`"after"` make
20+
* them siblings of it; `"first-child"`/`"last-child"` nest them inside it.
2521
*
2622
* The nested placements cover containers that have no children to point at:
2723
* a `min: 0` container that is currently empty has no child block to insert
2824
* before or after.
2925
*/
30-
export type BlockPlacement = "before" | "after" | "start" | "end";
26+
export type BlockPlacement = "before" | "after" | "first-child" | "last-child";
3127

3228
/**
3329
* Resolves a `placement` against a reference block into the document position
@@ -51,11 +47,6 @@ export function getInsertionPos(
5147
): { pos: number; wrapIn?: NodeType } | null {
5248
const { node, posBeforeNode } = reference;
5349

54-
const descend = (holder: Node, pos: number) =>
55-
placement === "start"
56-
? descendToFirstInsertionPos(holder, pos, nodeType)
57-
: descendToLastInsertionPos(holder, pos, nodeType);
58-
5950
if (placement === "before" || placement === "after") {
6051
const pos =
6152
placement === "before" ? posBeforeNode : posBeforeNode + node.nodeSize;
@@ -66,33 +57,30 @@ export function getInsertionPos(
6657
: null;
6758
}
6859

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);
60+
const info = getBlockInfoFromNode(node, posBeforeNode);
61+
62+
if (info.children) {
63+
// The descent helper can stop at sealed boundaries but this caller lets
64+
// it cross: an explicit `insertBlocks` placement is an intentional
65+
// crossing.
66+
const pos = descendToInsertionPos(
67+
info,
68+
nodeType,
69+
placement === "first-child" ? "first" : "last",
70+
);
7471

7572
return pos === null ? null : { pos };
7673
}
7774

78-
// A regular block keeps its children in a `blockGroup` that only exists once
79-
// it has some.
75+
// No children holder implies a `blockContainer` with no children yet
76+
// (containers always have one): its `blockGroup` is lazy (`blockContent
77+
// blockGroup?`), so the position after the content node only becomes valid
78+
// once the nodes are wrapped in a new group.
8079
const blockGroupType = nodeType.schema.nodes["blockGroup"];
81-
if (node.type.name !== "blockContainer" || !blockGroupType) {
82-
return null;
83-
}
84-
85-
const blockGroupPos = posBeforeNode + 1 + node.firstChild!.nodeSize;
86-
87-
if (node.childCount < 2) {
88-
return blockGroupType.contentMatch.matchType(nodeType)
89-
? { pos: blockGroupPos, wrapIn: blockGroupType }
90-
: null;
91-
}
92-
93-
const pos = descend(node.lastChild!, blockGroupPos);
9480

95-
return pos === null ? null : { pos };
81+
return info.hasContent && blockGroupType?.contentMatch.matchType(nodeType)
82+
? { pos: info.content.afterPos, wrapIn: blockGroupType }
83+
: null;
9684
}
9785

9886
export function insertBlocks<
@@ -134,7 +122,7 @@ export function insertBlocks<
134122
`Cannot insert a block of type "${blocksToInsert[0].type ?? "paragraph"}" ` +
135123
(placement === "before" || placement === "after"
136124
? `${placement} block with ID ${id}: its parent does not accept it.`
137-
: `at the ${placement} of block with ID ${id}: the block does not accept it as a child.`),
125+
: `as the ${placement} of block with ID ${id}: the block does not accept it as a child.`),
138126
);
139127
}
140128

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

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ const container = (type: string, config: Record<string, unknown>) =>
2525
const schema = BlockNoteSchema.create().extend({
2626
blockSpecs: {
2727
...defaultBlockSpecs,
28-
// Why `"start"`/`"end"` exist: a container that may legally hold nothing
29-
// has no child block to address, so `"before"`/`"after"` cannot reach
30-
// inside it.
28+
// Why `"first-child"`/`"last-child"` exist: a container that may legally
29+
// hold nothing has no child block to address, so `"before"`/`"after"`
30+
// cannot reach inside it.
3131
box: container("box", {
3232
content: "none",
3333
children: { allow: "any", min: 0 },
@@ -68,16 +68,24 @@ beforeEach(() => {
6868
]);
6969
});
7070

71-
describe('insertBlocks "start" / "end"', () => {
71+
describe('insertBlocks "first-child" / "last-child"', () => {
7272
it("inserts into a childless container", () => {
7373
editor.replaceBlocks(editor.document, [
7474
{ id: "b-0", type: "box" },
7575
{ id: "trailing", type: "paragraph", content: "" },
7676
]);
7777
expect(editor.getBlock("b-0")!.children).toHaveLength(0);
7878

79-
editor.insertBlocks([{ id: "first", type: "paragraph" }], "b-0", "start");
80-
editor.insertBlocks([{ id: "last", type: "paragraph" }], "b-0", "end");
79+
editor.insertBlocks(
80+
[{ id: "first", type: "paragraph" }],
81+
"b-0",
82+
"first-child",
83+
);
84+
editor.insertBlocks(
85+
[{ id: "last", type: "paragraph" }],
86+
"b-0",
87+
"last-child",
88+
);
8189

8290
expect(editor.getBlock("b-0")!.children.map((child) => child.id)).toEqual([
8391
"first",
@@ -95,8 +103,16 @@ describe('insertBlocks "start" / "end"', () => {
95103
{ id: "trailing", type: "paragraph", content: "" },
96104
]);
97105

98-
editor.insertBlocks([{ id: "first", type: "paragraph" }], "b-0", "start");
99-
editor.insertBlocks([{ id: "last", type: "paragraph" }], "b-0", "end");
106+
editor.insertBlocks(
107+
[{ id: "first", type: "paragraph" }],
108+
"b-0",
109+
"first-child",
110+
);
111+
editor.insertBlocks(
112+
[{ id: "last", type: "paragraph" }],
113+
"b-0",
114+
"last-child",
115+
);
100116

101117
expect(editor.getBlock("b-0")!.children.map((child) => child.id)).toEqual([
102118
"first",
@@ -120,8 +136,16 @@ describe('insertBlocks "start" / "end"', () => {
120136

121137
// `grid` itself only accepts `cell`s, so both placements have to find the
122138
// leading/trailing cell rather than giving up.
123-
editor.insertBlocks([{ id: "first", type: "paragraph" }], "g-0", "start");
124-
editor.insertBlocks([{ id: "last", type: "paragraph" }], "g-0", "end");
139+
editor.insertBlocks(
140+
[{ id: "first", type: "paragraph" }],
141+
"g-0",
142+
"first-child",
143+
);
144+
editor.insertBlocks(
145+
[{ id: "last", type: "paragraph" }],
146+
"g-0",
147+
"last-child",
148+
);
125149

126150
const grid = editor.getBlock("g-0")!;
127151
expect(grid.children[0].children.map((child: any) => child.id)).toContain(
@@ -137,8 +161,16 @@ describe('insertBlocks "start" / "end"', () => {
137161
{ id: "p-0", type: "paragraph", content: "Paragraph 0" },
138162
]);
139163

140-
editor.insertBlocks([{ id: "existing", type: "paragraph" }], "p-0", "end");
141-
editor.insertBlocks([{ id: "first", type: "paragraph" }], "p-0", "start");
164+
editor.insertBlocks(
165+
[{ id: "existing", type: "paragraph" }],
166+
"p-0",
167+
"last-child",
168+
);
169+
editor.insertBlocks(
170+
[{ id: "first", type: "paragraph" }],
171+
"p-0",
172+
"first-child",
173+
);
142174

143175
expect(editor.getBlock("p-0")!.children.map((child) => child.id)).toEqual([
144176
"first",
@@ -157,7 +189,7 @@ describe('insertBlocks "start" / "end"', () => {
157189
]);
158190

159191
expect(() =>
160-
editor.insertBlocks([{ type: "paragraph" }], "s-0", "end"),
192+
editor.insertBlocks([{ type: "paragraph" }], "s-0", "last-child"),
161193
).toThrow(/does not accept it as a child/);
162194
});
163195

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

0 commit comments

Comments
 (0)