Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ const mergeBlocks = (
}

// Save suggestion node content before reconstruction
// drops prev suggestionBefore and next suggestionAfter on merge
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the previous fixes, mergeBlocks needs to be updated for suggestions anyway, so still 1

const savedPrevSuggAfter = currentPrevInfo.suggestionAfter
? currentPrevInfo.suggestionAfter.node.copy(
currentPrevInfo.suggestionAfter.node.content,
Expand Down Expand Up @@ -236,6 +237,7 @@ const mergeBlocks = (
}

// Create the new blockContainer with the prev block's ID and attributes
// create() skips validation; bad child order ships silently
const newBlockContainer = currentPrevInfo.bnBlock.node.type.create(
currentPrevInfo.bnBlock.node.attrs,
newChildren,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function updateBlockSelectionFromData(
anchorBlockPos + data.headCellOffset,
);
} else if (data.type === "node") {
// +1 assumes blockContent is first child; may be a leading suggestion node
selection = NodeSelection.create(tr.doc, anchorBlockPos + 1);
} else {
const headBlockPos = getNodeById(data.headBlockId, tr.doc)?.posBeforeNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function sinkItem(
if (nodeBefore.type !== itemType) {
return false;
}
// lastChild may be a trailing suggestion node, not blockGroup
const nestedBefore =
nodeBefore.lastChild && nodeBefore.lastChild.type === groupType; // change 2
const inner = Fragment.from(nestedBefore ? itemType.create() : null);
Expand Down Expand Up @@ -102,6 +103,7 @@ function liftToOuterList(
// There are siblings after the lifted items, which must become
// children of the last item
const blockBeingLifted = range.parent.child(range.endIndex - 1);
// lastChild may be a trailing suggestion node, not blockGroup
const nestedAfter =
blockBeingLifted.lastChild &&
blockBeingLifted.lastChild.type === groupType; // change 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function isEmptyColumn(column: Node) {
throw new Error("Invalid column: does not have child node.");
}

// firstChild may be a suggestion node; childCount===1 below assumes no suggestions
const blockContent = blockContainer.firstChild;
if (!blockContent) {
throw new Error("Invalid blockContainer: does not have child node.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const splitBlockTr = (
// with the first block and the split happens at the blockContent boundary.
let effectivePos = posInBlock;
const $pos = tr.doc.resolve(posInBlock);
// dead guard — group is compound "suggestionBlockContent blockContent", never ===
if ($pos.parent.type.spec.group === "suggestionBlockContent") {
effectivePos = info.blockContent.beforePos + 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ function updateChildren<
throw new Error("impossible");
}
// Inserts a new blockGroup containing the child nodes created earlier.
// inserts blockGroup before trailing suggestion, invalid content order
tr.insert(
blockInfo.blockContent.afterPos,
pmSchema.nodes["blockGroup"].createChecked({}, childNodes),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function getBlock<
typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id;
const pmSchema = getPmSchema(doc);

// suggested-deleted blocks resolve as live, no deletion flag
const posInfo = getNodeById(id, doc);
if (!posInfo) {
return undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/blockManipulation/insertContentAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function insertContentAt(
// replace an empty paragraph by an inserted image
// instead of inserting the image below the paragraph
if (from === to && isOnlyBlockContent) {
// parent may be an empty suggestion shadow node, not blockContent
const { parent } = tr.doc.resolve(from);
const isEmptyTextBlock =
parent.isTextblock && !parent.type.spec.code && !parent.childCount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) {
// the correct information about whether content is cut at the start or end of a block

// if the end is at the end of a node (|</span></p>) move it forward so we include all closing tags (</span></p>|)
// end.parent may be a suggestion shadow node when crossing a block boundary
while (end.parentOffset >= end.parent.nodeSize - 2 && end.depth > 0) {
end = tr.doc.resolve(end.pos + 1);
}
Expand All @@ -251,6 +252,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) {
}

// if the start is at the end of a node (|</p><p><span>|) move it forwards so we drop all closing tags (|<p><span>)
// start.parent may be a suggestion shadow node when crossing a block boundary
while (start.parentOffset >= start.parent.nodeSize - 2 && start.depth > 0) {
start = tr.doc.resolve(start.pos + 1);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/api/clipboard/toClipboard/copyExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function fragmentToExternalHTML<
(child) =>
child.type.isInGroup("bnBlock") ||
child.type.name === "blockGroup" ||
// === "blockContent" misclassifies suggestion nodes (compound group)
child.type.spec.group === "blockContent",
) === undefined;
if (isWithinBlockContent) {
Expand Down Expand Up @@ -118,16 +119,19 @@ export function selectedFragmentToHTML<
// selected, e.g. an image block.
if (
"node" in view.state.selection &&
// === "blockContent" misclassifies suggestion nodes (compound group)
(view.state.selection.node as Node).type.spec.group === "blockContent"
) {
editor.transact((tr) =>
// from-1 block expansion assumes blockContent adjacency; off with leading suggestion node
tr.setSelection(
new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)),
),
);
}

// Uses default ProseMirror clipboard serialization.
// serializeForClipboard emits shadow nodes; paste re-creates them
const clipboardHTML: string = view.serializeForClipboard(
view.state.selection.content(),
).dom.innerHTML;
Expand Down Expand Up @@ -268,6 +272,7 @@ export const createCopyToClipboardExtension = <

// Expands the selection to the parent `blockContainer` node.
editor.transact((tr) =>
// from-1 block expansion assumes blockContent adjacency; off with leading suggestion node
tr.setSelection(
new NodeSelection(
tr.doc.resolve(view.state.selection.from - 1),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function serializeChildren(node: Node, ctx: SerializeContext): string {
}

function serializeNode(node: Node, ctx: SerializeContext): string {
// no data-suggestion handling; shadow text duplicated to markdown
if (node.nodeType === 3 /* Node.TEXT_NODE */) {
return node.textContent || "";
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/api/getBlockInfoFromPos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type BlockInfo = {
* Whether bnBlock is a blockContainer node
*/
isBlockContainer: true;
// (Affects ~58 callsites) consumers use blockContent/bnBlock but never branch on suggestionBefore/After
/**
* A suggestion node that appears before the blockContent, if present.
* Suggestion nodes have group "suggestionBlockContent" and are used for
Expand Down Expand Up @@ -191,6 +192,7 @@ export function getBlockInfoWithManualOffset(
afterPos: suggestionAfterPos,
};

// singular suggestionBefore/After, schema allows suggestionBlockContent*
if (!foundBlockContent) {
suggestionBefore = info;
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/nodeConversions/blockToNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ function blockOrInlineContentToContentNode(
/**
* Converts a BlockNote block to a Prosemirror node.
*/
// (Affects ~7 callsites) emits no suggestion nodes, round-trips drop suggestions
export function blockToNode(
block: PartialBlock<any, any, any>,
schema: Schema,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/nodeConversions/fragmentToBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function fragmentToBlocks<
fragment.descendants((node) => {
const pmSchema = getPmSchema(node);
if (node.type.name === "blockContainer") {
// firstChild may be a suggestion node, not blockContent or blockGroup
if (node.firstChild?.type.name === "blockGroup") {
// selection started within a block group
// in this case the fragment starts with:
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/api/nodeConversions/nodeToBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ export function nodeToBlock<
throw new UnreachableCaseError(blockConfig.content);
}

// (Affects ~23 callsites) strips suggestion state, Block API is suggestion-blind
const block = {
id,
type: blockConfig.type,
Expand Down Expand Up @@ -604,6 +605,7 @@ export function prosemirrorSliceToSlicedBlocks<
let firstNonSuggestionChild: Node | undefined;
let blockGroupChild: Node | undefined;
blockContainer.forEach((child) => {
// dead guard — group is compound "suggestionBlockContent blockContent", never ===
if (child.type.spec.group === "suggestionBlockContent") {
return; // skip suggestion nodes
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/nodeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function getNodeById(
}

// Keeps traversing nodes if block with target ID has not been found.
// suggested-deleted blocks resolve as live, no deletion flag
if (!isNodeBlock(node) || node.attrs.id !== id) {
return true;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/parsers/html/parseHTML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function HTMLToBlocks<
// const doc = pmSchema.nodes["doc"].createAndFill()!;
// and context: doc.resolve(3),

// parse may auto-create *--attributed shadow nodes from BlockNote HTML
const parentNode = parser.parse(htmlNode, {
topNode: pmSchema.nodes["blockGroup"].create(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function calculateListItemIndex(
tr: Transaction,
map: Map<Node, number>,
): { index: number; isFirst: boolean; hasStart: boolean } {
// firstChild may be a suggestion node, not blockContent
const hasStart = !!node.firstChild!.attrs["start"];

// Fast path: previous sibling already in cache
Expand Down Expand Up @@ -93,6 +94,7 @@ function calculateListItemIndex(
isFirst = false;
} else {
// Start of a new list
// firstChild may be a suggestion node, not blockContent
index = (lastInChain.node.firstChild!.attrs["start"] || 1) - 1;
isFirst = true;
}
Expand Down Expand Up @@ -156,6 +158,7 @@ function getDecorations(

if (
node.type.name === "blockContainer" &&
// firstChild may be a suggestion node, not blockContent
node.firstChild!.type.name === "numberedListItem"
) {
const { index, isFirst, hasStart } = calculateListItemIndex(
Expand All @@ -168,6 +171,7 @@ function getDecorations(
// Search only the numberedListItem node range, not the full
// blockContainer (which includes nested blockGroups whose
// decorations could falsely match).
// pos + 1 assumes blockContent is first child, not a suggestion node
const blockNode = tr.doc.nodeAt(pos + 1)!;
const existingDecorations = nextDecorationSet.find(
pos + 1,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/blocks/defaultBlockHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const defaultBlockToHTML = <

if (node.type.name === "blockContainer") {
// for regular blocks, get the toDOM spec from the blockContent node
// firstChild safe here; node built by suggestion-blind blockToNode
node = node.firstChild!;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/comments/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function getUpdatedThreadPositions(doc: Node, markType: string) {
const threadPositions = new Map<string, { from: number; to: number }>();

// find all thread marks and store their position + create decoration for selected thread
// comment marks inside shadow nodes anchor threads to suggestion content
doc.descendants((node, pos) => {
node.marks.forEach((mark) => {
if (mark.type.name === markType) {
Expand Down Expand Up @@ -228,6 +229,7 @@ export const CommentsExtension = createExtension(
return false;
}

// click may land in a suggestion shadow node, resolving its comment marks
const node = view.state.doc.nodeAt(pos);

if (!node) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/editor/Block.css
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ div[data-type="modification"] {
text-decoration: none;
}

/* no block-level suggestion-state styling (deletion rule disabled) */
.bn-root del,
[DISABLED-data-node-deletion] {
color: rgba(100, 90, 75, 0.3);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/editor/managers/ExportManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class ExportManager<
* @param blocks An array of blocks that should be serialized into HTML.
* @returns The blocks, serialized as an HTML string.
*/
// (Affects ~3) exports suggestion-blind Block JSON, drops shadow content
public blocksToHTMLLossy(
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.editor.document,
): string {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/editor/transformPasted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export function transformPasted(slice: Slice, view: EditorView) {
}

for (let i = 0; i < f.childCount; i++) {
// === "blockContent" misclassifies suggestion nodes (compound group)
if (f.child(i).type.spec.group === "blockContent") {
const content = [f.child(i)];

Expand All @@ -154,6 +155,7 @@ export function transformPasted(slice: Slice, view: EditorView) {
i + 1 < f.childCount &&
f.child(i + 1).type.name === "blockGroup" // TODO
) {
// .child(0).child(0) assumes blockContent is first child of blockContainer
const nestedChild = f
.child(i + 1)
.child(0)
Expand Down Expand Up @@ -227,6 +229,7 @@ function retypeLeadingParagraphForEmptyTarget(

const blockGroup = fragment.firstChild;
const blockContainer = blockGroup?.firstChild;
// firstChild may be a suggestion node, not blockContent
const leading = blockContainer?.firstChild;
if (
blockGroup?.type.name !== "blockGroup" ||
Expand All @@ -238,6 +241,7 @@ function retypeLeadingParagraphForEmptyTarget(

const retyped = target.type.create(target.attrs, leading.content);
const newBlockContainer = blockContainer.copy(
// replaceChild(0,...) assumes index 0 is blockContent, not a suggestion node
blockContainer.content.replaceChild(0, retyped),
);
const newBlockGroup = blockGroup.copy(
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/extensions/DropCursor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ export function getBlockDropRect(
return null;
}

// nodeBefore may be a suggestion node, not blockContent
const before = $pos.nodeBefore;

// nodeAfter may be a suggestion node, not blockContent
const after = $pos.nodeAfter;

if (!before && !after) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ export const NodeSelectionKeyboardExtension = createExtension(
const tr = view.state.tr;
view.dispatch(
tr
// $to.after() may land before a trailing suggestion node
.insert(
view.state.tr.selection.$to.after(),
view.state.schema.nodes["paragraph"].createChecked(),
)
.setSelection(
new TextSelection(
tr.doc.resolve(
// +1 may resolve inside a trailing suggestion node
view.state.tr.selection.$to.after() + 1,
),
),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extensions/Placeholder/Placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const PlaceholderExtension = createExtension(

// decoration for when there's only one empty block
// positions are hardcoded for now
// hardcoded size===6 and positions 2,4 assume no suggestion nodes in empty doc
if (state.doc.content.size === 6) {
decs.push(
Decoration.node(2, 4, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const PreviousBlockTypeExtension = createExtension(() => {
for (const node of newNodes) {
const oldNode = oldNodesById.get(node.node.attrs.id);

// firstChild may be a suggestion node, not blockContent
const oldContentNode = oldNode?.node.firstChild;
const newContentNode = node.node.firstChild;

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extensions/SideMenu/SideMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export class SideMenuView<
blockContentBoundingBox.width,
blockContentBoundingBox.height,
),
// resolves suggested-deleted/modified blocks as if live (no suggestion state)
block: this.editor.getBlock(
this.hoveredBlock!.getAttribute("data-id")!,
)!,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extensions/SideMenu/dragging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function blockPositionsFromSelection(selection: Selection, doc: Node) {
// the same blocks again. If this happens, the anchor & head move out of the block content node they were originally
// in. If the anchor should update but the head shouldn't and vice versa, it means the user selection is outside a
// block content node, which should never happen.
// === "blockContent" misclassifies suggestion nodes (compound group)
const selectionStartInBlockContent =
doc.resolve(selection.from).node().type.spec.group === "blockContent";
const selectionEndInBlockContent =
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
transaction.getMeta("blur") ||
transaction.getMeta("pointer") ||
// Moving the caret before the character which triggered the menu should hide it.
// no cursor-in-suggestion-node guard (same-parent check is suggestion-blind)
(prev.triggerCharacter !== undefined &&
newState.selection.from < prev.queryStartPos()) ||
// Moving the caret to a new block should hide the menu.
Expand Down Expand Up @@ -381,6 +382,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
const blockNode = findBlock(state.selection);
if (blockNode) {
return DecorationSet.create(state.doc, [
// decoration spans whole container incl suggestion shadow nodes
Decoration.node(
blockNode.pos,
blockNode.pos + blockNode.node.nodeSize,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extensions/TableHandles/TableHandles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export class TableHandlesView implements PluginView {
);

if (editorHasBlockWithType(this.editor, "table")) {
// posBeforeNode+1 assumes blockContent is first child; off with leading suggestion node
this.tablePos = pmNodeInfo.posBeforeNode + 1;
tableBlock = block;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/extensions/TrailingNode/TrailingNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function shouldShowTrailingWidget(doc: PMNode, isEditable: boolean): boolean {

const rootGroup = doc.lastChild;
const lastBlock = rootGroup?.lastChild;
// firstChild may be a suggestion node, not blockContent
const lastContent = lastBlock?.firstChild;

return !(
Expand Down
Loading
Loading