From bd154ed44674f47ed4663828dbf2d48816befec4 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 06:35:16 +0200 Subject: [PATCH 1/7] refactor types and partial / full --- .vscode/settings.json | 3 + .../commands/insertBlocks/insertBlocks.ts | 9 +- .../commands/moveBlocks/moveBlocks.ts | 1 - .../commands/replaceBlocks/replaceBlocks.ts | 19 +- .../commands/updateBlock/updateBlock.ts | 48 +++- .../api/blockManipulation/tables/tables.ts | 39 ++- .../exporters/html/externalHTMLExporter.ts | 6 +- .../exporters/html/internalHTMLSerializer.ts | 5 +- .../html/util/serializeBlocksExternalHTML.ts | 60 ++-- .../html/util/serializeBlocksInternalHTML.ts | 12 +- .../exporters/markdown/markdownExporter.ts | 6 +- .../src/api/nodeConversions/blockToNode.ts | 44 ++- .../api/nodeConversions/fragmentToBlocks.ts | 4 +- packages/core/src/blocks/Audio/block.ts | 4 +- .../helpers/render/createAddFileButton.ts | 7 +- .../helpers/render/createFileBlockWrapper.ts | 7 +- .../helpers/render/createFileNameWithIcon.ts | 7 +- .../render/createResizableFileBlockWrapper.ts | 7 +- .../core/src/blocks/defaultBlockHelpers.ts | 4 +- packages/core/src/blocks/defaultBlocks.ts | 30 +- packages/core/src/editor/BlockNoteEditor.ts | 41 +-- .../core/src/editor/BlockNoteExtension.ts | 4 +- .../editor/managers/CollaborationManager.ts | 12 +- .../core/src/editor/managers/ExportManager.ts | 9 +- .../core/src/editor/managers/StyleManager.ts | 19 +- packages/core/src/exporter/mapping.ts | 4 +- .../src/extensions/Comments/CommentsPlugin.ts | 2 +- .../TableHandles/TableHandlesPlugin.ts | 52 ++-- .../{schema.ts => CustomBlockNoteSchema.ts} | 38 ++- packages/core/src/schema/blocks/types.ts | 87 +----- .../src/schema/blocks/types/tableContent.ts | 65 +++++ packages/core/src/schema/index.ts | 4 +- .../src/schema/inlineContent/createSpec.ts | 7 +- .../core/src/schema/inlineContent/types.ts | 4 +- .../src/schema/partialBlockToFullBlock.ts | 258 ++++++++++++++++++ packages/core/src/schema/styles/createSpec.ts | 4 +- packages/core/src/util/table.ts | 8 +- .../DefaultButtons/TableCellMergeButton.tsx | 3 +- .../TableHandleMenu/TableHandleMenuProps.ts | 6 +- packages/react/src/schema/ReactBlockSpec.tsx | 10 +- .../src/schema/ReactInlineContentSpec.tsx | 9 +- .../src/context/ServerBlockNoteEditor.ts | 11 +- .../src/context/react/ReactServer.test.tsx | 30 +- .../src/docx/docxExporter.test.ts | 114 ++++---- .../xl-docx-exporter/src/docx/docxExporter.ts | 4 +- .../test/conversions/htmlConversion.test.ts | 12 +- .../test/conversions/nodeConversion.test.ts | 23 +- .../src/odt/odtExporter.test.ts | 4 +- .../src/pdf/pdfExporter.test.tsx | 4 +- shared/formatConversionTestUtil.ts | 213 --------------- shared/testDocument.ts | 4 +- .../export/exportTestExecutors.ts | 43 +-- .../exportParseEqualityTestExecutors.ts | 30 +- 53 files changed, 759 insertions(+), 701 deletions(-) rename packages/core/src/schema/{schema.ts => CustomBlockNoteSchema.ts} (89%) create mode 100644 packages/core/src/schema/blocks/types/tableContent.ts create mode 100644 packages/core/src/schema/partialBlockToFullBlock.ts delete mode 100644 shared/formatConversionTestUtil.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c24081186..79a37e7587 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.addMissingImports": "explicit" + }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index 81390947bf..d5601458a9 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -6,12 +6,13 @@ import { BlockIdentifier, BlockSchema, InlineContentSchema, + partialBlockToFullBlock, StyleSchema, } from "../../../../schema/index.js"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; -import { getPmSchema } from "../../../pmUtil.js"; +import { getBlockNoteSchema, getPmSchema } from "../../../pmUtil.js"; export function insertBlocks< BSchema extends BlockSchema, @@ -19,6 +20,7 @@ export function insertBlocks< S extends StyleSchema, >( tr: Transaction, + // TBD: allow PartialBlock here? blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" = "before", @@ -27,7 +29,10 @@ export function insertBlocks< typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; const pmSchema = getPmSchema(tr); const nodesToInsert = blocksToInsert.map((block) => - blockToNode(block, pmSchema), + blockToNode( + partialBlockToFullBlock(getBlockNoteSchema(pmSchema), block), + pmSchema, + ), ); const posInfo = getNodeById(id, tr.doc); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index 8d4591123e..87bf59030a 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -5,7 +5,6 @@ import { Transaction, } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; - import { Block } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier } from "../../../../schema/index.js"; diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 4ba8107636..e2f8150792 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,15 +1,16 @@ import type { Node } from "prosemirror-model"; import type { Transaction } from "prosemirror-state"; import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { - BlockIdentifier, - BlockSchema, - InlineContentSchema, - StyleSchema, +import { + partialBlockToFullBlock, + type BlockIdentifier, + type BlockSchema, + type InlineContentSchema, + type StyleSchema, } from "../../../../schema/index.js"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; -import { getPmSchema } from "../../../pmUtil.js"; +import { getBlockNoteSchema, getPmSchema } from "../../../pmUtil.js"; export function removeAndInsertBlocks< BSchema extends BlockSchema, @@ -18,6 +19,7 @@ export function removeAndInsertBlocks< >( tr: Transaction, blocksToRemove: BlockIdentifier[], + // TBD: allow PartialBlock here? blocksToInsert: PartialBlock[], ): { insertedBlocks: Block[]; @@ -27,7 +29,10 @@ export function removeAndInsertBlocks< // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the // document. const nodesToInsert: Node[] = blocksToInsert.map((block) => - blockToNode(block, pmSchema), + blockToNode( + partialBlockToFullBlock(getBlockNoteSchema(pmSchema), block), + pmSchema, + ), ); const idsOfBlocksToRemove = new Set( diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index 4318b19ca7..5d3b94d0fe 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -14,6 +14,11 @@ import type { BlockSchema, } from "../../../../schema/blocks/types.js"; import type { InlineContentSchema } from "../../../../schema/inlineContent/types.js"; +import { + partialBlockToFullBlock, + partialInlineContentToInlineContent, + partialTableContentToTableContent, +} from "../../../../schema/partialBlockToFullBlock.js"; import type { StyleSchema } from "../../../../schema/styles/types.js"; import { UnreachableCaseError } from "../../../../util/typescript.js"; import { @@ -27,7 +32,7 @@ import { } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; -import { getPmSchema } from "../../../pmUtil.js"; +import { getBlockNoteSchema, getPmSchema } from "../../../pmUtil.js"; // for compatibility with tiptap. TODO: remove as we want to remove dependency on tiptap command interface export const updateBlockCommand = < @@ -71,7 +76,7 @@ export function updateBlockTr< } const pmSchema = getPmSchema(tr); - + const schema = getBlockNoteSchema(pmSchema); if ( replaceFromPos !== undefined && replaceToPos !== undefined && @@ -127,17 +132,19 @@ export function updateBlockTr< // currently, we calculate the new node and replace the entire node with the desired new node. // for this, we do a nodeToBlock on the existing block to get the children. // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case - const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema); + const newFullBlock = partialBlockToFullBlock(schema, block); + if (block.children === undefined) { + // if no children are passed in, use existing children + const existingBlock = nodeToBlock( + blockInfo.bnBlock.node, + pmSchema, + ); + newFullBlock.children = existingBlock.children; + } tr.replaceWith( blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos, - blockToNode( - { - children: existingBlock.children, // if no children are passed in, use existing children - ...block, - }, - pmSchema, - ), + blockToNode(newFullBlock, pmSchema), ); return; @@ -174,6 +181,7 @@ function updateBlockContentNode< replaceToOffset?: number, ) { const pmSchema = getPmSchema(tr); + const schema = getBlockNoteSchema(pmSchema); let content: PMNode[] | "keep" = "keep"; // Has there been any custom content provided? @@ -188,9 +196,22 @@ function updateBlockContentNode< } else if (Array.isArray(block.content)) { // Adds a text node with the provided styles converted into marks to the content, // for each InlineContent object. - content = inlineContentToNodes(block.content, pmSchema, newNodeType.name); + content = inlineContentToNodes( + partialInlineContentToInlineContent( + block.content, + schema.inlineContentSchema, + ), + pmSchema, + newNodeType.name, + ); } else if (block.content.type === "tableContent") { - content = tableContentToNodes(block.content, pmSchema); + content = tableContentToNodes( + partialTableContentToTableContent( + block.content, + schema.inlineContentSchema, + ), + pmSchema, + ); } else { throw new UnreachableCaseError(block.content.type); } @@ -276,9 +297,10 @@ function updateChildren< S extends StyleSchema, >(block: PartialBlock, tr: Transform, blockInfo: BlockInfo) { const pmSchema = getPmSchema(tr); + const schema = getBlockNoteSchema(pmSchema); if (block.children !== undefined && block.children.length > 0) { const childNodes = block.children.map((child) => { - return blockToNode(child, pmSchema); + return blockToNode(partialBlockToFullBlock(schema, child), pmSchema); }); // Checks if a blockGroup node already exists. diff --git a/packages/core/src/api/blockManipulation/tables/tables.ts b/packages/core/src/api/blockManipulation/tables/tables.ts index f770a34afd..1a9e62d410 100644 --- a/packages/core/src/api/blockManipulation/tables/tables.ts +++ b/packages/core/src/api/blockManipulation/tables/tables.ts @@ -1,11 +1,9 @@ import { DefaultBlockSchema } from "../../../blocks/defaultBlocks.js"; import { - BlockFromConfigNoChildren, - PartialTableContent, - TableCell, - TableContent, -} from "../../../schema/blocks/types.js"; -import { + type BlockFromConfig, + type PartialTableContent, + type TableCell, + type TableContent, isPartialLinkInlineContent, isStyledTextInlineContent, } from "../../../schema/index.js"; @@ -185,7 +183,7 @@ type OccupancyGrid = (RelativeCellIndices & { * @returns an {@link OccupancyGrid} */ export function getTableCellOccupancyGrid( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, ): OccupancyGrid { const { height, width } = getDimensionsOfTable(block); @@ -295,7 +293,7 @@ export function getAbsoluteTableCells( /** * The table block containing the cell. */ - block: BlockFromConfigNoChildren, + block: BlockFromConfig, /** * The occupancy grid of the table. */ @@ -327,7 +325,7 @@ export function getAbsoluteTableCells( * @returns The height and width of the table. */ export function getDimensionsOfTable( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, ): { /** * The number of rows in the table. @@ -370,7 +368,7 @@ export function getRelativeTableCells( /** * The table block containing the cell. */ - block: BlockFromConfigNoChildren, + block: BlockFromConfig, /** * The occupancy grid of the table. */ @@ -428,7 +426,7 @@ export function getRelativeTableCells( * @returns All of the cells associated with the relative row of the table. (All cells that have the same relative row index) */ export function getCellsAtRowHandle( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, relativeRowIndex: RelativeCellIndices["row"], ) { const occupancyGrid = getTableCellOccupancyGrid(block); @@ -507,7 +505,7 @@ export function getCellsAtRowHandle( * @returns All of the cells associated with the relative column of the table. (All cells that have the same relative column index) */ export function getCellsAtColumnHandle( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, relativeColumnIndex: RelativeCellIndices["col"], ) { const occupancyGrid = getTableCellOccupancyGrid(block); @@ -563,7 +561,7 @@ export function getCellsAtColumnHandle( * @note This is a destructive operation, it will modify the provided {@link OccupancyGrid} in place. */ export function moveColumn( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, fromColIndex: RelativeCellIndices["col"], toColIndex: RelativeCellIndices["col"], occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block), @@ -607,7 +605,7 @@ export function moveColumn( * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place. */ export function moveRow( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, fromRowIndex: RelativeCellIndices["row"], toRowIndex: RelativeCellIndices["row"], occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block), @@ -656,7 +654,8 @@ function isCellEmpty( return true; } if (isPartialTableCell(cell)) { - return isCellEmpty(cell.content); + // TODO: what happened here? + return isCellEmpty(cell); } else if (typeof cell === "string") { return cell.length === 0; } else if (Array.isArray(cell)) { @@ -682,7 +681,7 @@ function isCellEmpty( * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place. */ export function cropEmptyRowsOrColumns( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, removeEmpty: "columns" | "rows", occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block), ): TableContent["rows"] { @@ -744,7 +743,7 @@ export function cropEmptyRowsOrColumns( * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place. */ export function addRowsOrColumns( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, addType: "columns" | "rows", /** * The number of rows or columns to add. @@ -800,7 +799,7 @@ export function addRowsOrColumns( * Checks if a row can be safely dropped at the target row index without splitting merged cells. */ export function canRowBeDraggedInto( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, draggingIndex: RelativeCellIndices["row"], targetRowIndex: RelativeCellIndices["row"], ) { @@ -835,7 +834,7 @@ export function canRowBeDraggedInto( * Checks if a column can be safely dropped at the target column index without splitting merged cells. */ export function canColumnBeDraggedInto( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, draggingIndex: RelativeCellIndices["col"], targetColumnIndex: RelativeCellIndices["col"], ) { @@ -874,7 +873,7 @@ export function canColumnBeDraggedInto( export function areInSameColumn( from: RelativeCellIndices, to: RelativeCellIndices, - block: BlockFromConfigNoChildren, + block: BlockFromConfig, ) { // Table indices are relative to the table, so we need to resolve the absolute cell indices const anchorAbsoluteCellIndices = getAbsoluteTableCells(from, block); diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts index 12f486d563..27c77b5efa 100644 --- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts +++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts @@ -1,6 +1,6 @@ import { DOMSerializer, Schema } from "prosemirror-model"; -import { PartialBlock } from "../../../blocks/defaultBlocks.js"; +import { Block } from "../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; import { BlockSchema, @@ -40,7 +40,7 @@ export const createExternalHTMLExporter = < return { exportBlocks: ( - blocks: PartialBlock[], + blocks: Block[], options: { document?: Document }, ) => { const html = serializeBlocksExternalHTML( @@ -62,7 +62,7 @@ export const createExternalHTMLExporter = < ) => { const domFragment = serializeInlineContentExternalHTML( editor, - inlineContent as any, + inlineContent as unknown as never, serializer, options, ); diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts index 5b3003cf55..3f1da10613 100644 --- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts @@ -1,5 +1,6 @@ import { DOMSerializer, Schema } from "prosemirror-model"; -import { PartialBlock } from "../../../blocks/defaultBlocks.js"; + +import { Block } from "../../../blocks/index.js"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; import { BlockSchema, @@ -28,7 +29,7 @@ export const createInternalHTMLSerializer = < return { serializeBlocks: ( - blocks: PartialBlock[], + blocks: Block[], options: { document?: Document }, ) => { return serializeBlocksInternalHTML(editor, blocks, serializer, options) diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index 3992ef4696..79da4d5df0 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -1,6 +1,5 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; -import * as z from "zod/v4/core"; -import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import { Block } from "../../../../blocks/index.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { BlockImplementation, @@ -35,7 +34,7 @@ export function serializeInlineContentExternalHTML< S extends StyleSchema, >( editor: BlockNoteEditor, - blockContent: PartialBlock["content"], + blockContent: Block["content"], serializer: DOMSerializer, options?: { document?: Document }, ) { @@ -166,7 +165,7 @@ function serializeBlock< >( fragment: DocumentFragment, editor: BlockNoteEditor, - block: PartialBlock, + block: Block, serializer: DOMSerializer, orderedListItemBlockTypes: Set, unorderedListItemBlockTypes: Set, @@ -177,25 +176,25 @@ function serializeBlock< // set default props in case we were passed a partial block // TODO: should be a nicer way for this / or move to caller - const props = block.props || {}; - for (const [name, spec] of Object.entries( - editor.schema.blockSchema[ - block.type as keyof typeof editor.schema.blockSchema - ].propSchema._zod.def.shape, - )) { - if ( - !(name in props) && - spec instanceof z.$ZodDefault && - spec._zod.def.defaultValue !== undefined - ) { - (props as any)[name] = spec._zod.def.defaultValue; - } - } + // const props = block.props || {}; + // for (const [name, spec] of Object.entries( + // editor.schema.blockSchema[ + // block.type as keyof typeof editor.schema.blockSchema + // ].propSchema._zod.def.shape, + // )) { + // if ( + // !(name in props) && + // spec instanceof z.$ZodDefault && + // spec._zod.def.defaultValue !== undefined + // ) { + // (props as any)[name] = spec._zod.def.defaultValue; + // } + // } const bc = BC_NODE.spec?.toDOM?.( BC_NODE.create({ id: block.id, - ...props, + ...block.props, }), ) as { dom: HTMLElement; @@ -211,14 +210,10 @@ function serializeBlock< const ret = blockImplementation.toExternalHTML?.call( {}, - { ...block, props } as any, + { ...block } as any, editor as any, ) || - blockImplementation.render.call( - {}, - { ...block, props } as any, - editor as any, - ); + blockImplementation.render.call({}, { ...block } as any, editor as any); const elementFragment = doc.createDocumentFragment(); @@ -247,11 +242,10 @@ function serializeBlock< } else { elementFragment.append(ret.dom); } - if (ret.contentDOM && block.content) { const ic = serializeInlineContentExternalHTML( editor, - block.content as any, // TODO + block.content as unknown as never, // TODO serializer, options, ); @@ -272,11 +266,11 @@ function serializeBlock< if ( listType === "OL" && - "start" in props && - props.start && - props?.start !== 1 + "start" in block.props && + block.props.start && + block.props?.start !== 1 ) { - list.setAttribute("start", props.start + ""); + list.setAttribute("start", block.props.start + ""); } fragment.append(list); } @@ -326,7 +320,7 @@ const serializeBlocksToFragment = < >( fragment: DocumentFragment, editor: BlockNoteEditor, - blocks: PartialBlock[], + blocks: Block[], serializer: DOMSerializer, orderedListItemBlockTypes: Set, unorderedListItemBlockTypes: Set, @@ -351,7 +345,7 @@ export const serializeBlocksExternalHTML = < S extends StyleSchema, >( editor: BlockNoteEditor, - blocks: PartialBlock[], + blocks: Block[], serializer: DOMSerializer, orderedListItemBlockTypes: Set, unorderedListItemBlockTypes: Set, diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index d2934620d0..d582e4922d 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -1,6 +1,6 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; -import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import { Block } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { BlockSchema, @@ -21,7 +21,7 @@ export function serializeInlineContentInternalHTML< S extends StyleSchema, >( editor: BlockNoteEditor, - blockContent: PartialBlock["content"], + blockContent: Block["content"], serializer: DOMSerializer, blockType?: string, options?: { document?: Document }, @@ -133,7 +133,7 @@ function serializeBlock< S extends StyleSchema, >( editor: BlockNoteEditor, - block: PartialBlock, + block: Block, serializer: DOMSerializer, options?: { document?: Document }, ) { @@ -169,7 +169,7 @@ function serializeBlock< if (ret.contentDOM && block.content) { const ic = serializeInlineContentInternalHTML( editor, - block.content as any, // TODO + block.content as never, // TODO serializer, block.type, options, @@ -220,7 +220,7 @@ function serializeBlocks< S extends StyleSchema, >( editor: BlockNoteEditor, - blocks: PartialBlock[], + blocks: Block[], serializer: DOMSerializer, options?: { document?: Document }, ) { @@ -241,7 +241,7 @@ export const serializeBlocksInternalHTML = < S extends StyleSchema, >( editor: BlockNoteEditor, - blocks: PartialBlock[], + blocks: Block[], serializer: DOMSerializer, options?: { document?: Document }, ) => { diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts index 23aad8db7c..6950f17254 100644 --- a/packages/core/src/api/exporters/markdown/markdownExporter.ts +++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts @@ -5,7 +5,7 @@ import remarkGfm from "remark-gfm"; import remarkStringify from "remark-stringify"; import { unified } from "unified"; -import { PartialBlock } from "../../../blocks/defaultBlocks.js"; +import { Block } from "../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; import { BlockSchema, @@ -13,9 +13,9 @@ import { StyleSchema, } from "../../../schema/index.js"; import { createExternalHTMLExporter } from "../html/externalHTMLExporter.js"; -import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js"; import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin.js"; import { convertVideoToMarkdown } from "./util/convertVideoToMarkdownRehypePlugin.js"; +import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js"; // Needs to be sync because it's used in drag handler event (SideMenuPlugin) export function cleanHTMLToMarkdown(cleanHTMLString: string) { @@ -39,7 +39,7 @@ export function blocksToMarkdown< I extends InlineContentSchema, S extends StyleSchema, >( - blocks: PartialBlock[], + blocks: Block[], schema: Schema, editor: BlockNoteEditor, options: { document?: Document }, diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index 660d97406e..58f97b2c60 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -1,22 +1,21 @@ import { Attrs, Fragment, Mark, Node, Schema } from "@tiptap/pm/model"; -import UniqueID from "../../extensions/UniqueID/UniqueID.js"; import type { + CustomInlineContentFromConfig, + InlineContent, InlineContentSchema, - PartialCustomInlineContentFromConfig, - PartialInlineContent, - PartialLink, - PartialTableContent, + Link, StyleSchema, StyledText, + TableContent, } from "../../schema"; -import type { PartialBlock } from "../../blocks/defaultBlocks"; +import type { Block } from "../../blocks/index.js"; import { - isPartialLinkInlineContent, + isLinkInlineContent, isStyledTextInlineContent, } from "../../schema/inlineContent/types.js"; -import { getColspan, isPartialTableCell } from "../../util/table.js"; +import { getColspan, isTableCell } from "../../util/table.js"; import { UnreachableCaseError } from "../../util/typescript.js"; import { getAbsoluteTableCells } from "../blockManipulation/tables/tables.js"; import { getStyleSchema } from "../pmUtil.js"; @@ -83,7 +82,7 @@ function styledTextToNodes( * prosemirror text nodes with the appropriate marks */ function linkToNodes( - link: PartialLink, + link: Link, schema: Schema, styleSchema: StyleSchema, ): Node[] { @@ -144,7 +143,7 @@ export function inlineContentToNodes< I extends InlineContentSchema, S extends StyleSchema, >( - blockContent: PartialInlineContent, + blockContent: string[] | InlineContent[], schema: Schema, blockType?: string, styleSchema: S = getStyleSchema(schema), @@ -153,10 +152,11 @@ export function inlineContentToNodes< for (const content of blockContent) { if (typeof content === "string") { + // TODO: remove? nodes.push( ...styledTextArrayToNodes(content, schema, styleSchema, blockType), ); - } else if (isPartialLinkInlineContent(content)) { + } else if (isLinkInlineContent(content)) { nodes.push(...linkToNodes(content, schema, styleSchema)); } else if (isStyledTextInlineContent(content)) { nodes.push( @@ -178,7 +178,7 @@ export function tableContentToNodes< I extends InlineContentSchema, S extends StyleSchema, >( - tableContent: PartialTableContent, + tableContent: TableContent, schema: Schema, styleSchema: StyleSchema = getStyleSchema(schema), ): Node[] { @@ -227,7 +227,7 @@ export function tableContentToNodes< // No-op } else if (typeof cell === "string") { content = schema.text(cell); - } else if (isPartialTableCell(cell)) { + } else if (isTableCell(cell)) { if (cell.content) { content = inlineContentToNodes( cell.content, @@ -258,7 +258,7 @@ export function tableContentToNodes< isHeaderCol || isHeaderRow ? "tableHeader" : "tableCell" ].createChecked( { - ...(isPartialTableCell(cell) ? cell.props : {}), + ...(isTableCell(cell) ? cell.props : {}), colwidth, }, schema.nodes["tableParagraph"].createChecked(attrs, content), @@ -273,9 +273,7 @@ export function tableContentToNodes< } function blockOrInlineContentToContentNode( - block: - | PartialBlock - | PartialCustomInlineContentFromConfig, + block: Block | CustomInlineContentFromConfig, schema: Schema, styleSchema: StyleSchema, ) { @@ -322,16 +320,10 @@ function blockOrInlineContentToContentNode( * Converts a BlockNote block to a Prosemirror node. */ export function blockToNode( - block: PartialBlock, + block: Block, schema: Schema, styleSchema: StyleSchema = getStyleSchema(schema), ) { - let id = block.id; - - if (id === undefined) { - id = UniqueID.options.generateID(); - } - const children: Node[] = []; if (block.children) { @@ -360,7 +352,7 @@ export function blockToNode( return schema.nodes["blockContainer"].createChecked( { - id: id, + id: block.id, ...block.props, }, groupNode ? [contentNode, groupNode] : contentNode, @@ -369,7 +361,7 @@ export function blockToNode( // this is a bnBlock node like Column or ColumnList that directly translates to a prosemirror node return schema.nodes[block.type].createChecked( { - id: id, + id: block.id, ...block.props, }, children, diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts index 724b552bda..878c2eb032 100644 --- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts +++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts @@ -1,6 +1,6 @@ import { Fragment } from "@tiptap/pm/model"; +import type { Block } from "../../blocks/index.js"; import { - BlockNoDefaults, BlockSchema, InlineContentSchema, StyleSchema, @@ -18,7 +18,7 @@ export function fragmentToBlocks< >(fragment: Fragment) { // first convert selection to blocknote-style blocks, and then // pass these to the exporter - const blocks: BlockNoDefaults[] = []; + const blocks: Block[] = []; fragment.descendants((node) => { const pmSchema = getPmSchema(node); if (node.type.name === "blockContainer") { diff --git a/packages/core/src/blocks/Audio/block.ts b/packages/core/src/blocks/Audio/block.ts index 6dcf4e5139..d9effb12b0 100644 --- a/packages/core/src/blocks/Audio/block.ts +++ b/packages/core/src/blocks/Audio/block.ts @@ -1,6 +1,6 @@ -import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { - BlockFromConfig, + type BlockFromConfig, createBlockConfig, createBlockSpec, } from "../../schema/index.js"; diff --git a/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts b/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts index 227856b4ac..f322f2614d 100644 --- a/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts +++ b/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts @@ -1,11 +1,8 @@ import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { - BlockConfig, - BlockFromConfigNoChildren, -} from "../../../../schema/index.js"; +import type { Block } from "../../../index.js"; export const createAddFileButton = ( - block: BlockFromConfigNoChildren, any, any>, + block: Block, editor: BlockNoteEditor, buttonIcon?: HTMLElement, ) => { diff --git a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts index a55ddbbad7..d921b2dae7 100644 --- a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts +++ b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts @@ -1,8 +1,5 @@ import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { - BlockConfig, - BlockFromConfigNoChildren, -} from "../../../../schema/index.js"; +import type { BlockConfig, BlockFromConfig } from "../../../../schema/index.js"; import { baseFilePropSchema, optionalFileProps, @@ -15,7 +12,7 @@ const requiredPropSchema = baseFilePropSchema.extend({ }); export const createFileBlockWrapper = ( - block: BlockFromConfigNoChildren< + block: BlockFromConfig< BlockConfig, any, any diff --git a/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts b/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts index f9e97476a4..23d3b44eb1 100644 --- a/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts +++ b/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts @@ -1,13 +1,10 @@ -import { - BlockConfig, - BlockFromConfigNoChildren, -} from "../../../../schema/index.js"; +import { BlockConfig, BlockFromConfig } from "../../../../schema/index.js"; import { baseFilePropSchema } from "../../../defaultFileProps.js"; export const FILE_ICON_SVG = ``; export const createFileNameWithIcon = ( - block: BlockFromConfigNoChildren< + block: BlockFromConfig< BlockConfig, any, any diff --git a/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts b/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts index 375ddb36e2..88f7275dbe 100644 --- a/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts +++ b/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts @@ -1,8 +1,5 @@ import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { - BlockConfig, - BlockFromConfigNoChildren, -} from "../../../../schema/index.js"; +import { BlockConfig, BlockFromConfig } from "../../../../schema/index.js"; import { baseFilePropSchema, optionalFileProps, @@ -18,7 +15,7 @@ const requiredPropSchema = baseFilePropSchema.extend({ }); export const createResizableFileBlockWrapper = ( - block: BlockFromConfigNoChildren< + block: BlockFromConfig< BlockConfig, any, any diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts index ccadf93e11..d4c5b0590f 100644 --- a/packages/core/src/blocks/defaultBlockHelpers.ts +++ b/packages/core/src/blocks/defaultBlockHelpers.ts @@ -1,12 +1,12 @@ import { blockToNode } from "../api/nodeConversions/blockToNode.js"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import type { - BlockNoDefaults, BlockSchema, InlineContentSchema, StyleSchema, } from "../schema/index.js"; import { mergeCSSClasses } from "../util/browser.js"; +import type { Block } from "./index.js"; // Function that creates a ProseMirror `DOMOutputSpec` for a default block. // Since all default blocks have the same structure (`blockContent` div with a @@ -61,7 +61,7 @@ export const defaultBlockToHTML = < I extends InlineContentSchema, S extends StyleSchema, >( - block: BlockNoDefaults, + block: Block, editor: BlockNoteEditor, ): { dom: HTMLElement; diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 24fc32b07e..b58d155222 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -17,23 +17,21 @@ import { getInlineContentSchemaFromSpecs, getStyleSchemaFromSpecs, } from "../schema/index.js"; -import { - createAudioBlockSpec, - createBulletListItemBlockSpec, - createCheckListItemBlockSpec, - createCodeBlockSpec, - createDividerBlockSpec, - createFileBlockSpec, - createHeadingBlockSpec, - createImageBlockSpec, - createNumberedListItemBlockSpec, - createParagraphBlockSpec, - createQuoteBlockSpec, - createToggleListItemBlockSpec, - createVideoBlockSpec, - defaultProps, -} from "./index.js"; +import { createAudioBlockSpec } from "./Audio/block.js"; +import { createCodeBlockSpec } from "./Code/block.js"; +import { defaultProps } from "./defaultProps.js"; +import { createDividerBlockSpec } from "./Divider/block.js"; +import { createFileBlockSpec } from "./File/block.js"; +import { createHeadingBlockSpec } from "./Heading/block.js"; +import { createImageBlockSpec } from "./Image/block.js"; +import { createBulletListItemBlockSpec } from "./ListItem/BulletListItem/block.js"; +import { createCheckListItemBlockSpec } from "./ListItem/CheckListItem/block.js"; +import { createNumberedListItemBlockSpec } from "./ListItem/NumberedListItem/block.js"; +import { createToggleListItemBlockSpec } from "./ListItem/ToggleListItem/block.js"; +import { createParagraphBlockSpec } from "./Paragraph/block.js"; +import { createQuoteBlockSpec } from "./Quote/block.js"; import { createTableBlockSpec } from "./Table/block.js"; +import { createVideoBlockSpec } from "./Video/block.js"; export const defaultBlockSpecs = { audio: createAudioBlockSpec(), diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 692348f5e6..af3f7621aa 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -37,18 +37,19 @@ import type { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/T import { UniqueID } from "../extensions/UniqueID/UniqueID.js"; import type { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import type { - BlockIdentifier, - BlockNoteDOMAttributes, - BlockSchema, - BlockSpecs, - CustomBlockNoteSchema, - InlineContentSchema, - InlineContentSpecs, - PartialInlineContent, - Styles, - StyleSchema, - StyleSpecs, +import { + partialBlockToFullBlock, + type BlockIdentifier, + type BlockNoteDOMAttributes, + type BlockSchema, + type BlockSpecs, + type CustomBlockNoteSchema, + type InlineContentSchema, + type InlineContentSpecs, + type PartialInlineContent, + type Styles, + type StyleSchema, + type StyleSpecs, } from "../schema/index.js"; import { mergeCSSClasses } from "../util/browser.js"; import { EventEmitter } from "../util/EventEmitter.js"; @@ -59,13 +60,13 @@ import type { TextCursorPosition } from "./cursorPositionTypes.js"; import { BlockManager, CollaborationManager, - type CollaborationOptions, EventManager, ExportManager, ExtensionManager, SelectionManager, StateManager, StyleManager, + type CollaborationOptions, } from "./managers/index.js"; import type { Selection } from "./selectionTypes.js"; import { transformPasted } from "./transformPasted.js"; @@ -889,7 +890,11 @@ export class BlockNoteEditor< } const schema = getSchema(tiptapOptions.extensions!); const pmNodes = initialContent.map((b) => - blockToNode(b, schema, this.schema.styleSchema).toJSON(), + blockToNode( + partialBlockToFullBlock(this.schema, b), + schema, + this.schema.styleSchema, + ).toJSON(), ); const doc = createDocument( { @@ -1467,7 +1472,7 @@ export class BlockNoteEditor< * @returns The blocks, serialized as an HTML string. */ public blocksToHTMLLossy( - blocks: PartialBlock[] = this.document, + blocks: Block[] = this.document, ): string { return this._exportManager.blocksToHTMLLossy(blocks); } @@ -1481,9 +1486,7 @@ export class BlockNoteEditor< * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ - public blocksToFullHTML( - blocks: PartialBlock[], - ): string { + public blocksToFullHTML(blocks: Block[]): string { return this._exportManager.blocksToFullHTML(blocks); } /** @@ -1506,7 +1509,7 @@ export class BlockNoteEditor< * @returns The blocks, serialized as a Markdown string. */ public blocksToMarkdownLossy( - blocks: PartialBlock[] = this.document, + blocks: Block[] = this.document, ): string { return this._exportManager.blocksToMarkdownLossy(blocks); } diff --git a/packages/core/src/editor/BlockNoteExtension.ts b/packages/core/src/editor/BlockNoteExtension.ts index f56d6c736a..ecc84c38be 100644 --- a/packages/core/src/editor/BlockNoteExtension.ts +++ b/packages/core/src/editor/BlockNoteExtension.ts @@ -2,10 +2,10 @@ import { Plugin } from "prosemirror-state"; import { EventEmitter } from "../util/EventEmitter.js"; import { AnyExtension } from "@tiptap/core"; +import { PartialBlock } from "../blocks/index.js"; import { BlockSchema, InlineContentSchema, - PartialBlockNoDefaults, StyleSchema, } from "../schema/index.js"; import { BlockNoteEditor } from "./BlockNoteEditor.js"; @@ -93,7 +93,7 @@ export type InputRule = { * The editor instance */ editor: BlockNoteEditor; - }) => undefined | PartialBlockNoDefaults; + }) => undefined | PartialBlock; }; /** diff --git a/packages/core/src/editor/managers/CollaborationManager.ts b/packages/core/src/editor/managers/CollaborationManager.ts index 8273fb5cb4..952701c82c 100644 --- a/packages/core/src/editor/managers/CollaborationManager.ts +++ b/packages/core/src/editor/managers/CollaborationManager.ts @@ -1,14 +1,14 @@ -import * as Y from "yjs"; import { redoCommand, undoCommand } from "y-prosemirror"; -import { CommentsPlugin } from "../../extensions/Comments/CommentsPlugin.js"; -import { CommentMark } from "../../extensions/Comments/CommentMark.js"; +import * as Y from "yjs"; +import type { ThreadStore, User } from "../../comments/index.js"; +import { CursorPlugin } from "../../extensions/Collaboration/CursorPlugin.js"; import { ForkYDocPlugin } from "../../extensions/Collaboration/ForkYDocPlugin.js"; import { SyncPlugin } from "../../extensions/Collaboration/SyncPlugin.js"; import { UndoPlugin } from "../../extensions/Collaboration/UndoPlugin.js"; -import { CursorPlugin } from "../../extensions/Collaboration/CursorPlugin.js"; -import type { ThreadStore, User } from "../../comments/index.js"; +import { CommentMark } from "../../extensions/Comments/CommentMark.js"; +import { CommentsPlugin } from "../../extensions/Comments/CommentsPlugin.js"; +import { CustomBlockNoteSchema } from "../../schema/CustomBlockNoteSchema.js"; import type { BlockNoteEditor } from "../BlockNoteEditor.js"; -import { CustomBlockNoteSchema } from "../../schema/schema.js"; export interface CollaborationOptions { /** diff --git a/packages/core/src/editor/managers/ExportManager.ts b/packages/core/src/editor/managers/ExportManager.ts index aba001ce65..2bce921f6f 100644 --- a/packages/core/src/editor/managers/ExportManager.ts +++ b/packages/core/src/editor/managers/ExportManager.ts @@ -11,7 +11,6 @@ import { DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, - PartialBlock, } from "../../blocks/defaultBlocks.js"; import { BlockSchema, @@ -35,7 +34,7 @@ export class ExportManager< * @returns The blocks, serialized as an HTML string. */ public blocksToHTMLLossy( - blocks: PartialBlock[] = this.editor.document, + blocks: Block[] = this.editor.document, ): string { const exporter = createExternalHTMLExporter( this.editor.pmSchema, @@ -53,9 +52,7 @@ export class ExportManager< * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ - public blocksToFullHTML( - blocks: PartialBlock[], - ): string { + public blocksToFullHTML(blocks: Block[]): string { const exporter = createInternalHTMLSerializer( this.editor.pmSchema, this.editor, @@ -83,7 +80,7 @@ export class ExportManager< * @returns The blocks, serialized as a Markdown string. */ public blocksToMarkdownLossy( - blocks: PartialBlock[] = this.editor.document, + blocks: Block[] = this.editor.document, ): string { return blocksToMarkdown(blocks, this.editor.pmSchema, this.editor, {}); } diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts index e03c46a6d1..bb487d0744 100644 --- a/packages/core/src/editor/managers/StyleManager.ts +++ b/packages/core/src/editor/managers/StyleManager.ts @@ -1,18 +1,19 @@ +import { TextSelection } from "@tiptap/pm/state"; import { insertContentAt } from "../../api/blockManipulation/insertContentAt.js"; import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js"; +import { + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, +} from "../../blocks/defaultBlocks.js"; import { BlockSchema, InlineContentSchema, PartialInlineContent, StyleSchema, Styles, + partialInlineContentToInlineContent, } from "../../schema/index.js"; -import { - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, -} from "../../blocks/defaultBlocks.js"; -import { TextSelection } from "@tiptap/pm/state"; import { UnreachableCaseError } from "../../util/typescript.js"; import { BlockNoteEditor } from "../BlockNoteEditor.js"; @@ -32,7 +33,11 @@ export class StyleManager< content: PartialInlineContent, { updateSelection = false }: { updateSelection?: boolean } = {}, ) { - const nodes = inlineContentToNodes(content, this.editor.pmSchema); + const fullContent = partialInlineContentToInlineContent( + content, + this.editor.schema.inlineContentSchema, + ); + const nodes = inlineContentToNodes(fullContent, this.editor.pmSchema); this.editor.transact((tr) => { insertContentAt( diff --git a/packages/core/src/exporter/mapping.ts b/packages/core/src/exporter/mapping.ts index 0dca63ebc3..d75303438e 100644 --- a/packages/core/src/exporter/mapping.ts +++ b/packages/core/src/exporter/mapping.ts @@ -1,6 +1,6 @@ import { BlockNoteSchema } from "../blocks/BlockNoteSchema.js"; import { - BlockFromConfigNoChildren, + BlockFromConfig, BlockSchema, InlineContentFromConfig, InlineContentSchema, @@ -20,7 +20,7 @@ export type BlockMapping< RI, > = { [K in keyof B]: ( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, // we don't know the exact types that are supported by the exporter at this point, // because the mapping only knows about converting certain types (which might be a subset of the supported types) // this is why there are many `any` types here (same for types below) diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts index a5d1e24b6d..635bfacd2d 100644 --- a/packages/core/src/extensions/Comments/CommentsPlugin.ts +++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts @@ -10,7 +10,7 @@ import type { } from "../../comments/index.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js"; -import { CustomBlockNoteSchema } from "../../schema/schema.js"; +import { CustomBlockNoteSchema } from "../../schema/CustomBlockNoteSchema.js"; import { UserStore } from "./userstore/UserStore.js"; const PLUGIN_KEY = new PluginKey(`blocknote-comments`); diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index f90fe9e95b..0d32681869 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -27,14 +27,18 @@ import { import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../api/nodeUtil.js"; import { - editorHasBlockWithType, + blockHasType, isTableCellSelection, } from "../../blocks/defaultBlockTypeGuards.js"; -import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js"; +import { + DefaultBlockSchema, + defaultBlockSpecs, +} from "../../blocks/defaultBlocks.js"; +import { TableBlockConfig } from "../../blocks/index.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js"; import { - BlockFromConfigNoChildren, + BlockFromConfig, BlockSchemaWithBlock, InlineContentSchema, StyleSchema, @@ -54,7 +58,7 @@ export type TableHandlesState< referencePosCell: DOMRect | undefined; referencePosTable: DOMRect; - block: BlockFromConfigNoChildren; + block: BlockFromConfig; colIndex: number | undefined; rowIndex: number | undefined; @@ -164,7 +168,7 @@ export class TableHandlesView< constructor( private readonly editor: BlockNoteEditor< - BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + BlockSchemaWithBlock<"table", TableBlockConfig>, I, S >, @@ -260,7 +264,7 @@ export class TableHandlesView< this.tableElement = blockEl.node; let tableBlock: - | BlockFromConfigNoChildren + | BlockFromConfig | undefined; const pmNodeInfo = this.editor.transact((tr) => @@ -278,7 +282,14 @@ export class TableHandlesView< this.editor.schema.styleSchema, ); - if (editorHasBlockWithType(this.editor, "table")) { + if ( + blockHasType( + block, + this.editor, + "table", + defaultBlockSpecs.table.config.propSchema, + ) + ) { this.tablePos = pmNodeInfo.posBeforeNode + 1; tableBlock = block; } @@ -535,10 +546,16 @@ export class TableHandlesView< } // Hide handles if the table block has been removed. - this.state.block = this.editor.getBlock(this.state.block.id)!; + const block = this.editor.getBlock(this.state.block.id)!; + if ( - !this.state.block || - this.state.block.type !== "table" || + !block || + !blockHasType( + block, + this.editor, + "table", + defaultBlockSpecs.table.config.propSchema, + ) || // when collaborating, the table element might be replaced and out of date // because yjs replaces the element when for example you change the color via the side menu !this.tableElement?.isConnected @@ -551,6 +568,7 @@ export class TableHandlesView< return; } + this.state.block = block; const { height: rowCount, width: colCount } = getDimensionsOfTable( this.state.block, ); @@ -626,7 +644,7 @@ export class TableHandlesProsemirrorPlugin< constructor( private readonly editor: BlockNoteEditor< - BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + BlockSchemaWithBlock<"table", TableBlockConfig>, I, S >, @@ -921,7 +939,7 @@ export class TableHandlesProsemirrorPlugin< }; getCellsAtRowHandle = ( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, relativeRowIndex: RelativeCellIndices["row"], ) => { return getCellsAtRowHandle(block, relativeRowIndex); @@ -931,7 +949,7 @@ export class TableHandlesProsemirrorPlugin< * Get all the cells in a column of the table block. */ getCellsAtColumnHandle = ( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, relativeColumnIndex: RelativeCellIndices["col"], ) => { return getCellsAtColumnHandle(block, relativeColumnIndex); @@ -1161,9 +1179,7 @@ export class TableHandlesProsemirrorPlugin< * Returns undefined when there is no cell selection, or the selection is not within a table. */ getMergeDirection = ( - block: - | BlockFromConfigNoChildren - | undefined, + block: BlockFromConfig | undefined, ) => { return this.editor.transact((tr) => { const isSelectingTableCells = isTableCellSelection(tr.selection) @@ -1194,14 +1210,14 @@ export class TableHandlesProsemirrorPlugin< }; cropEmptyRowsOrColumns = ( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, removeEmpty: "columns" | "rows", ) => { return cropEmptyRowsOrColumns(block, removeEmpty); }; addRowsOrColumns = ( - block: BlockFromConfigNoChildren, + block: BlockFromConfig, addType: "columns" | "rows", numToAdd: number, ) => { diff --git a/packages/core/src/schema/schema.ts b/packages/core/src/schema/CustomBlockNoteSchema.ts similarity index 89% rename from packages/core/src/schema/schema.ts rename to packages/core/src/schema/CustomBlockNoteSchema.ts index 7accfbfc23..effa9f9e81 100644 --- a/packages/core/src/schema/schema.ts +++ b/packages/core/src/schema/CustomBlockNoteSchema.ts @@ -1,21 +1,20 @@ -import { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; +import type { Block, PartialBlock } from "../blocks/defaultBlocks.js"; +import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import { createDependencyGraph, toposortReverse } from "../util/topo-sort.js"; +import { addNodeAndExtensionsToSpec } from "./blocks/createSpec.js"; import { - BlockNoDefaults, - BlockSchema, - BlockSpecs, - InlineContentConfig, - InlineContentSchema, - InlineContentSpec, - InlineContentSpecs, - LooseBlockSpec, - PartialBlockNoDefaults, - StyleSchema, - StyleSpecs, - addNodeAndExtensionsToSpec, - getInlineContentSchemaFromSpecs, - getStyleSchemaFromSpecs, + type BlockSchema, + type BlockSpecs, + type InlineContentConfig, + type InlineContentSchema, + type InlineContentSpec, + type InlineContentSpecs, + type LooseBlockSpec, + type StyleSchema, + type StyleSpecs, } from "./index.js"; +import { getInlineContentSchemaFromSpecs } from "./inlineContent/internal.js"; +import { getStyleSchemaFromSpecs } from "./styles/internal.js"; function removeUndefined | undefined>(obj: T): T { if (!obj) { @@ -35,14 +34,11 @@ export class CustomBlockNoteSchema< public readonly BlockNoteEditor: BlockNoteEditor = "only for types" as any; - public readonly Block: BlockNoDefaults = + public readonly Block: Block = "only for types" as any; - public readonly PartialBlock: PartialBlockNoDefaults< - BSchema, - ISchema, - SSchema - > = "only for types" as any; + public readonly PartialBlock: PartialBlock = + "only for types" as any; public inlineContentSpecs: InlineContentSpecs; public styleSpecs: StyleSpecs; diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index b4c3500a17..b22babe091 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -12,6 +12,7 @@ import type { } from "../inlineContent/types.js"; import type { PropSchema, Props } from "../propTypes.js"; import type { StyleSchema } from "../styles/types.js"; +import { PartialTableContent, TableContent } from "./types/tableContent.js"; export type BlockNoteDOMElement = | "editor" @@ -222,44 +223,17 @@ export type BlockSpecsFromSchema = { }; }; -export type BlockSchemaWithBlock = { +export type BlockSchemaWithBlock< + T extends string, + C extends BlockConfig, +> = NamesMatch<{ [k in T]: C; -}; - -export type TableCellProps = { - backgroundColor: string; - textColor: string; - textAlignment: "left" | "center" | "right" | "justify"; - colspan?: number; - rowspan?: number; -}; - -export type TableCell< - I extends InlineContentSchema, - S extends StyleSchema = StyleSchema, -> = { - type: "tableCell"; - props: TableCellProps; - content: InlineContent[]; -}; - -export type TableContent< - I extends InlineContentSchema, - S extends StyleSchema = StyleSchema, -> = { - type: "tableContent"; - columnWidths: (number | undefined)[]; - headerRows?: number; - headerCols?: number; - rows: { - cells: InlineContent[][] | TableCell[]; - }[]; -}; +}>; // A BlockConfig has all the information to get the type of a Block (which is a specific instance of the BlockConfig. // i.e.: paragraphConfig: BlockConfig defines what a "paragraph" is / supports, and BlockFromConfigNoChildren is the shape of a specific paragraph block. // (for internal use) -export type BlockFromConfigNoChildren< +type BlockFromConfigNoChildren< B extends BlockConfig, I extends InlineContentSchema, S extends StyleSchema, @@ -297,6 +271,7 @@ type BlocksWithoutChildren< // Converts each block spec into a Block object without children, merges them // into a union type, and adds a children property +// TODO: should only be exposed internally export type BlockNoDefaults< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -314,35 +289,6 @@ export type SpecificBlock< children: BlockNoDefaults[]; }; -/** CODE FOR PARTIAL BLOCKS, analogous to above - * - * Partial blocks are convenience-wrappers to make it easier to - *create/update blocks in the editor. - * - */ - -export type PartialTableCell< - I extends InlineContentSchema, - S extends StyleSchema = StyleSchema, -> = { - type: "tableCell"; - props?: Partial; - content?: PartialInlineContent; -}; - -export type PartialTableContent< - I extends InlineContentSchema, - S extends StyleSchema = StyleSchema, -> = { - type: "tableContent"; - columnWidths?: (number | undefined)[]; - headerRows?: number; - headerCols?: number; - rows: { - cells: PartialInlineContent[] | PartialTableCell[]; - }[]; -}; - type PartialBlockFromConfigNoChildren< B extends BlockConfig, I extends InlineContentSchema, @@ -385,23 +331,6 @@ export type PartialBlockNoDefaults< children: PartialBlockNoDefaults[]; }>; -export type SpecificPartialBlock< - BSchema extends BlockSchema, - I extends InlineContentSchema, - BType extends keyof BSchema, - S extends StyleSchema, -> = PartialBlocksWithoutChildren[BType] & { - children?: BlockNoDefaults[]; -}; - -export type PartialBlockFromConfig< - B extends BlockConfig, - I extends InlineContentSchema, - S extends StyleSchema, -> = PartialBlockFromConfigNoChildren & { - children?: BlockNoDefaults[]; -}; - export type BlockIdentifier = { id: string } | string; export type BlockImplementation< diff --git a/packages/core/src/schema/blocks/types/tableContent.ts b/packages/core/src/schema/blocks/types/tableContent.ts new file mode 100644 index 0000000000..ad64e8bb8d --- /dev/null +++ b/packages/core/src/schema/blocks/types/tableContent.ts @@ -0,0 +1,65 @@ +import { + InlineContent, + InlineContentSchema, + PartialInlineContent, +} from "../../inlineContent/types.js"; +import { StyleSchema } from "../../styles/types.js"; + +export type TableCellProps = { + backgroundColor: string; + textColor: string; + textAlignment: "left" | "center" | "right" | "justify"; + colspan?: number; + rowspan?: number; +}; + +export type TableCell< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema, +> = { + type: "tableCell"; + props: TableCellProps; + content: InlineContent[]; +}; + +export type TableContent< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema, +> = { + type: "tableContent"; + columnWidths: (number | undefined)[]; + headerRows?: number; + headerCols?: number; + rows: { + cells: InlineContent[][] | TableCell[]; + }[]; +}; + +/** CODE FOR PARTIAL BLOCKS, analogous to above + * + * Partial blocks are convenience-wrappers to make it easier to + *create/update blocks in the editor. + * + */ + +export type PartialTableCell< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema, +> = { + type: "tableCell"; + props?: Partial; + content?: PartialInlineContent; +}; + +export type PartialTableContent< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema, +> = { + type: "tableContent"; + columnWidths?: (number | undefined)[]; + headerRows?: number; + headerCols?: number; + rows: { + cells: PartialInlineContent[] | PartialTableCell[]; + }[]; +}; diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 05585ab28b..8b2e9b7ad3 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -1,11 +1,13 @@ export * from "./blocks/createSpec.js"; export * from "./blocks/internal.js"; export * from "./blocks/types.js"; +export * from "./blocks/types/tableContent.js"; +export * from "./CustomBlockNoteSchema.js"; export * from "./inlineContent/createSpec.js"; export * from "./inlineContent/internal.js"; export * from "./inlineContent/types.js"; +export * from "./partialBlockToFullBlock.js"; export * from "./propTypes.js"; export * from "./styles/createSpec.js"; export * from "./styles/internal.js"; export * from "./styles/types.js"; -export * from "./schema.js"; diff --git a/packages/core/src/schema/inlineContent/createSpec.ts b/packages/core/src/schema/inlineContent/createSpec.ts index b48fd503b3..04251a8752 100644 --- a/packages/core/src/schema/inlineContent/createSpec.ts +++ b/packages/core/src/schema/inlineContent/createSpec.ts @@ -5,6 +5,7 @@ import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js"; import { nodeToCustomInlineContent } from "../../api/nodeConversions/nodeToBlock.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { propsToAttributes } from "../blocks/internal.js"; +import { partialInlineContentToInlineContent } from "../partialBlockToFullBlock.js"; import { Props } from "../propTypes.js"; import { StyleSchema } from "../styles/types.js"; import { @@ -191,7 +192,11 @@ export function createInlineContentSpec< editor.schema.styleSchema, ) as any as InlineContentFromConfig, // TODO: fix cast (update) => { - const content = inlineContentToNodes([update], editor.pmSchema); + const fullUpdate = partialInlineContentToInlineContent( + [update], + editor.schema.inlineContentSchema, + ); + const content = inlineContentToNodes(fullUpdate, editor.pmSchema); const pos = getPos(); diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts index 88ebfd740c..c6207e17c1 100644 --- a/packages/core/src/schema/inlineContent/types.ts +++ b/packages/core/src/schema/inlineContent/types.ts @@ -1,8 +1,8 @@ import { Node } from "@tiptap/core"; +import { ViewMutationRecord } from "prosemirror-view"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { PropSchema, Props } from "../propTypes.js"; import { StyleSchema, Styles } from "../styles/types.js"; -import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; -import { ViewMutationRecord } from "prosemirror-view"; export type CustomInlineContentConfig = { type: string; diff --git a/packages/core/src/schema/partialBlockToFullBlock.ts b/packages/core/src/schema/partialBlockToFullBlock.ts new file mode 100644 index 0000000000..66903c67dd --- /dev/null +++ b/packages/core/src/schema/partialBlockToFullBlock.ts @@ -0,0 +1,258 @@ +import * as z from "zod/v4/core"; +import type { Block, BlockNoteSchema, PartialBlock } from "../blocks/index.js"; +import { UniqueID } from "../extensions/UniqueID/UniqueID.js"; +import { mapTableCell } from "../util/table.js"; +import { UnreachableCaseError } from "../util/typescript.js"; +import type { BlockSchema } from "./blocks/types.js"; +import type { + PartialTableContent, + TableCell, + TableContent, +} from "./blocks/types/tableContent.js"; +import type { CustomBlockNoteSchema } from "./CustomBlockNoteSchema.js"; +import type { + InlineContent, + InlineContentSchema, + Link, + PartialInlineContent, + PartialLink, + StyledText, +} from "./inlineContent/types.js"; +import { + isPartialLinkInlineContent, + isStyledTextInlineContent, +} from "./inlineContent/types.js"; +import type { Props, PropSchema } from "./propTypes.js"; +import type { StyleSchema } from "./styles/types.js"; + +function partialPropsToProps( + partialProps: Partial> | undefined, + propSchema: PropSchema, +): Props { + const props: Props = partialProps || {}; + + Object.entries(propSchema._zod.def.shape).forEach(([propKey, propValue]) => { + if (props[propKey] === undefined) { + if (propValue instanceof z.$ZodDefault) { + props[propKey] = propValue._zod.def.defaultValue; + } + if (propValue instanceof z.$ZodOptional) { + props[propKey] = undefined; + } + } + }); + return props; +} + +function textStringToStyledText(text: string): StyledText { + return { + type: "text", + styles: {}, + text, + }; +} + +function partialLinkToLink(partialLink: PartialLink): Link { + return { + type: "link", + href: partialLink.href, + content: + typeof partialLink.content === "string" + ? [textStringToStyledText(partialLink.content)] + : partialLink.content, + }; +} + +export function partialInlineContentToInlineContent( + partialInlineContent: PartialInlineContent | undefined, + inlineContentSchema: InlineContentSchema, +): InlineContent[] { + if (partialInlineContent === undefined) { + return []; + } + + if (typeof partialInlineContent === "string") { + return [textStringToStyledText(partialInlineContent)]; + } + + return partialInlineContent.map((partialInlineContentElement) => { + if (typeof partialInlineContentElement === "string") { + return textStringToStyledText(partialInlineContentElement); + } + + if (isPartialLinkInlineContent(partialInlineContentElement)) { + return partialLinkToLink(partialInlineContentElement); + } + + if (isStyledTextInlineContent(partialInlineContentElement)) { + return partialInlineContentElement; + } + + const content = partialInlineContentElement.content; + const inlineContentConfig = + inlineContentSchema[partialInlineContentElement.type]; + + if (typeof inlineContentConfig === "string") { + throw new Error( + "unexpected, should be custom inline content (not 'text' or 'link'", + ); + } + + return { + type: partialInlineContentElement.type, + props: partialPropsToProps( + partialInlineContentElement.props, + inlineContentConfig.propSchema, + ), + content: + typeof content === "undefined" + ? undefined + : typeof content === "string" + ? [textStringToStyledText(content)] + : content, + }; + }); +} + +export function partialTableContentToTableContent( + partialTableContent: PartialTableContent, + inlineContentSchema: InlineContentSchema, +): TableContent { + const rows: { + cells: TableCell[]; + }[] = partialTableContent.rows.map((row) => { + return { + cells: row.cells.map((cell) => { + const fullCell = mapTableCell(cell); + // `mapTableCell` doesn't actually convert `PartialInlineContent` to + // `InlineContent`, so this is done manually here. + fullCell.content = partialInlineContentToInlineContent( + fullCell.content, + inlineContentSchema, + ); + + return fullCell; + }), + }; + }); + + const columnWidths = partialTableContent.columnWidths || []; + if (!partialTableContent.columnWidths) { + for (const cell of rows[0].cells) { + for (let i = 0; i < (cell.props?.colspan || 1); i++) { + columnWidths.push(undefined); + } + } + } + + return { + type: "tableContent", + headerRows: partialTableContent.headerRows, + headerCols: partialTableContent.headerCols, + columnWidths: columnWidths, + rows, + }; +} + +function partialBlockContentToBlockContent( + partialBlockContent: + | PartialTableContent + | PartialInlineContent + | undefined, + content: "table" | "inline" | "none", + inlineContentSchema: InlineContentSchema, +): + | TableContent + | InlineContent[] + | undefined { + if (content === "table") { + partialBlockContent = partialBlockContent || { + type: "tableContent", + rows: [], + }; + + if ( + typeof partialBlockContent !== "object" || + !("type" in partialBlockContent) || + partialBlockContent.type !== "tableContent" + ) { + throw new Error("Invalid partial block content"); + } + + return partialTableContentToTableContent( + partialBlockContent, + inlineContentSchema, + ); + } else if (content === "inline") { + partialBlockContent = partialBlockContent || ""; + + if ( + typeof partialBlockContent === "object" && + "type" in partialBlockContent + ) { + throw new Error("Invalid partial block content. Table content passed!?"); + } + + return partialInlineContentToInlineContent( + partialBlockContent, + inlineContentSchema, + ); + } else if (content === "none") { + return undefined; + } else { + throw new UnreachableCaseError(content); + } +} + +export function partialBlockToFullBlock< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + schema: CustomBlockNoteSchema, + partialBlock: PartialBlock, +): Block { + const id = partialBlock.id || UniqueID.options.generateID(); + + // TODO + const type: string = partialBlock.type || "paragraph"; + + const props = partialPropsToProps( + partialBlock.props, + schema.blockSchema[type].propSchema, + ); + + const content = partialBlockContentToBlockContent( + partialBlock.content, + schema.blockSchema[type].content, + schema.inlineContentSchema, + ); + + const children = + partialBlock.children?.map((child) => + partialBlockToFullBlock(schema, child), + ) || []; + + return { + id, + type, + props, + content, + children, + } as Block; +} + +export function partialBlocksToFullBlocks< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + // TODO: I think this should be a CustomBlockNoteSchema, + // but that breaks docxExporter etc + schema: BlockNoteSchema, + partialBlocks: PartialBlock[], +): Block[] { + return partialBlocks.map((partialBlock) => + partialBlockToFullBlock(schema, partialBlock), + ); +} diff --git a/packages/core/src/schema/styles/createSpec.ts b/packages/core/src/schema/styles/createSpec.ts index 4bd83b3ae0..547793a14a 100644 --- a/packages/core/src/schema/styles/createSpec.ts +++ b/packages/core/src/schema/styles/createSpec.ts @@ -1,13 +1,11 @@ import { Mark } from "@tiptap/core"; - import { ParseRule, TagParseRule } from "@tiptap/pm/model"; import { addStyleAttributes, createInternalStyleSpec, stylePropsToAttributes, } from "./internal.js"; -import { StyleConfig, StyleSpec } from "./types.js"; - +import type { StyleConfig, StyleSpec } from "./types.js"; export type CustomStyleImplementation = { render: (value: T["propSchema"] extends "boolean" ? undefined : string) => { dom: HTMLElement; diff --git a/packages/core/src/util/table.ts b/packages/core/src/util/table.ts index f7ecd84910..7d9f47a350 100644 --- a/packages/core/src/util/table.ts +++ b/packages/core/src/util/table.ts @@ -1,10 +1,11 @@ import type { + InlineContent, InlineContentSchema, - StyleSchema, PartialInlineContent, - InlineContent, + PartialTableCell, + StyleSchema, + TableCell, } from "../schema"; -import { PartialTableCell, TableCell } from "../schema/blocks/types.js"; /** * This will map a table cell to a TableCell object. @@ -36,6 +37,7 @@ export function mapTableCell< } : { type: "tableCell", + // FIXME: content can actually be Partial, we should probably handle this as well content: ([] as InlineContent[]).concat(content as any), props: { backgroundColor: "default", diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx index bb11326a08..7ca557efb9 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx @@ -1,4 +1,5 @@ import { + blockHasType, DefaultBlockSchema, InlineContentSchema, StyleSchema, @@ -32,7 +33,7 @@ export const TableCellMergeButton = () => { const block = selectedBlocks[0]; - if (block.type === "table") { + if (blockHasType(block, editor, "table")) { return editor.tableHandles?.getMergeDirection(block); } diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/TableHandleMenuProps.ts b/packages/react/src/components/TableHandles/TableHandleMenu/TableHandleMenuProps.ts index 74477f8286..315a2b2d0d 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/TableHandleMenuProps.ts +++ b/packages/react/src/components/TableHandles/TableHandleMenu/TableHandleMenuProps.ts @@ -1,9 +1,9 @@ -import { +import type { + BlockFromConfig, DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, - SpecificBlock, StyleSchema, } from "@blocknote/core"; @@ -12,6 +12,6 @@ export type TableHandleMenuProps< S extends StyleSchema = DefaultStyleSchema, > = { orientation: "row" | "column"; - block: SpecificBlock<{ table: DefaultBlockSchema["table"] }, "table", I, S>; + block: BlockFromConfig; index: number; }; diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index cf194a60c1..06b281d231 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -1,7 +1,7 @@ import { + Block, BlockConfig, BlockImplementation, - BlockNoDefaults, BlockNoteEditor, BlockNoteExtension, BlockSpec, @@ -29,11 +29,7 @@ export type ReactCustomBlockRenderProps< TProps extends PropSchema = PropSchema, TContent extends "inline" | "none" = "inline" | "none", > = { - block: BlockNoDefaults< - Record>, - any, - any - >; + block: Block>, any, any>; editor: BlockNoteEditor; contentRef: (node: HTMLElement | null) => void; }; @@ -265,7 +261,7 @@ export function createReactBlockSpec< // `ReactNodeViewRenderer` instead. const block = getBlockFromPos( props.getPos, - editor, + editor as any, props.editor, blockConfig.type, ); diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx index 7795eb4c22..e58c8fd02d 100644 --- a/packages/react/src/schema/ReactInlineContentSpec.tsx +++ b/packages/react/src/schema/ReactInlineContentSpec.tsx @@ -13,12 +13,12 @@ import { inlineContentToNodes, nodeToCustomInlineContent, PartialCustomInlineContentFromConfig, + partialInlineContentToInlineContent, Props, PropSchema, propsToAttributes, StyleSchema, } from "@blocknote/core"; -import * as z from "zod/v4/core"; import { Node } from "@tiptap/core"; import { NodeViewProps, @@ -26,6 +26,7 @@ import { ReactNodeViewRenderer, useReactNodeView, } from "@tiptap/react"; +import * as z from "zod/v4/core"; // import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; import { FC, JSX } from "react"; import { renderToDOMSpec } from "./@util/ReactRenderUtil.js"; @@ -207,8 +208,12 @@ export function createReactInlineContentSpec< ) as any as InlineContentFromConfig // TODO: fix cast } updateInlineContent={(update) => { - const content = inlineContentToNodes( + const fullUpdate = partialInlineContentToInlineContent( [update], + editor.schema.inlineContentSchema, + ); + const content = inlineContentToNodes( + fullUpdate, editor.pmSchema, ); diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.ts b/packages/server-util/src/context/ServerBlockNoteEditor.ts index 1f5914162a..f80fb37f8c 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.ts @@ -15,6 +15,7 @@ import { createExternalHTMLExporter, createInternalHTMLSerializer, nodeToBlock, + partialBlockToFullBlock, } from "@blocknote/core"; import { BlockNoteViewRaw } from "@blocknote/react"; @@ -138,7 +139,9 @@ export class ServerBlockNoteEditor< blocks: PartialBlock[], ) { const pmSchema = this.editor.pmSchema; - const pmNodes = blocks.map((b) => blockToNode(b, pmSchema)); + const pmNodes = blocks.map((b) => + blockToNode(partialBlockToFullBlock(this.editor.schema, b), pmSchema), + ); const doc = pmSchema.topNodeType.create( null, @@ -216,7 +219,7 @@ export class ServerBlockNoteEditor< * @returns The blocks, serialized as an HTML string. */ public async blocksToHTMLLossy( - blocks: PartialBlock[], + blocks: Block[], ): Promise { return this._withJSDOM(async () => { const exporter = createExternalHTMLExporter( @@ -240,7 +243,7 @@ export class ServerBlockNoteEditor< * @returns The blocks, serialized as an HTML string. */ public async blocksToFullHTML( - blocks: PartialBlock[], + blocks: Block[], ): Promise { return this._withJSDOM(async () => { const exporter = createInternalHTMLSerializer( @@ -278,7 +281,7 @@ export class ServerBlockNoteEditor< * @returns The blocks, serialized as a Markdown string. */ public async blocksToMarkdownLossy( - blocks: PartialBlock[], + blocks: Block[], ): Promise { return this._withJSDOM(async () => { return blocksToMarkdown(blocks, this.editor.pmSchema, this.editor, { diff --git a/packages/server-util/src/context/react/ReactServer.test.tsx b/packages/server-util/src/context/react/ReactServer.test.tsx index 57c66709e1..d68400311b 100644 --- a/packages/server-util/src/context/react/ReactServer.test.tsx +++ b/packages/server-util/src/context/react/ReactServer.test.tsx @@ -2,6 +2,7 @@ import { BlockNoteSchema, defaultBlockSpecs, defaultProps, + partialBlockToFullBlock, } from "@blocknote/core"; import { createReactBlockSpec } from "@blocknote/react"; import { createContext, useContext } from "react"; @@ -56,13 +57,12 @@ describe("Test ServerBlockNoteEditor with React blocks", () => { const editor = ServerBlockNoteEditor.create({ schema, }); - const html = await editor.blocksToFullHTML([ - { - id: "1", - type: "simpleReactCustomParagraph", - content: "React Custom Paragraph", - }, - ]); + const fullBlock = partialBlockToFullBlock(editor.editor.schema, { + id: "1", + type: "simpleReactCustomParagraph", + content: "React Custom Paragraph", + }); + const html = await editor.blocksToFullHTML([fullBlock]); expect(html).toMatchSnapshot(); }); @@ -75,14 +75,14 @@ describe("Test ServerBlockNoteEditor with React blocks", () => { ({ children }) => ( {children} ), - async () => - editor.blocksToFullHTML([ - { - id: "1", - type: "reactContextParagraph", - content: "React Context Paragraph", - }, - ]), + async () => { + const fullBlock = partialBlockToFullBlock(editor.editor.schema, { + id: "1", + type: "reactContextParagraph", + content: "React Context Paragraph", + }); + return editor.blocksToFullHTML([fullBlock]); + }, ); expect(html).toMatchSnapshot(); diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts index 96589d3c8e..2f2deb63ed 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts @@ -1,8 +1,10 @@ import { BlockNoteSchema, - defaultBlockSpecs, createPageBreakBlockSpec, + defaultBlockSpecs, + partialBlocksToFullBlocks, } from "@blocknote/core"; +import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; import { testDocument } from "@shared/testDocument.js"; import { BlobReader, Entry, TextWriter, ZipReader } from "@zip.js/zip.js"; import { Packer, Paragraph, TextRun } from "docx"; @@ -10,8 +12,6 @@ import { describe, expect, it } from "vitest"; import xmlFormat from "xml-formatter"; import { docxDefaultSchemaMappings } from "./defaultSchema/index.js"; import { DOCXExporter } from "./docxExporter.js"; -import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; -import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; const getZIPEntryContent = (entries: Entry[], fileName: string) => { const entry = entries.find((entry) => { @@ -140,64 +140,66 @@ describe("exporter", () => { }, }); const exporter = new DOCXExporter(schema, docxDefaultSchemaMappings); - const doc = await exporter.toDocxJsDocument( - partialBlocksToBlocksForTesting(schema, [ - { - type: "columnList", - children: [ - { - type: "column", - props: { - width: 0.8, - }, - children: [ - { - type: "paragraph", - content: "This paragraph is in a column!", - }, - ], + const x = partialBlocksToFullBlocks(schema, [ + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, }, - { - type: "column", - props: { - width: 1.4, + children: [ + { + type: "paragraph", + content: "This paragraph is in a column!", }, - children: [ - { - type: "heading", - content: "So is this heading!", - }, - ], + ], + }, + { + type: "column", + props: { + width: 1.4, }, - { - type: "column", - props: { - width: 0.8, + children: [ + { + type: "heading", + content: "So is this heading!", }, - children: [ - { - type: "paragraph", - content: "You can have multiple blocks in a column too", - }, - { - type: "bulletListItem", - content: "Block 1", - }, - { - type: "bulletListItem", - content: "Block 2", - }, - { - type: "bulletListItem", - content: "Block 3", - }, - ], + ], + }, + { + type: "column", + props: { + width: 0.8, }, - ], - }, - ]), - { sectionOptions: {}, documentOptions: {}, locale: "en-US" }, - ); + children: [ + { + type: "paragraph", + content: "You can have multiple blocks in a column too", + }, + { + type: "bulletListItem", + content: "Block 1", + }, + { + type: "bulletListItem", + content: "Block 2", + }, + { + type: "bulletListItem", + content: "Block 3", + }, + ], + }, + ], + }, + ]); + const doc = await exporter.toDocxJsDocument(x, { + sectionOptions: {}, + documentOptions: {}, + locale: "en-US", + }); const blob = await Packer.toBlob(doc); const zip = new ZipReader(new BlobReader(blob)); diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.ts b/packages/xl-docx-exporter/src/docx/docxExporter.ts index 6a79d5c53d..850906d671 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.ts @@ -1,8 +1,8 @@ import { Block, - BlockNoteSchema, BlockSchema, COLORS_DEFAULT, + CustomBlockNoteSchema, InlineContentSchema, StyleSchema, StyledText, @@ -54,7 +54,7 @@ export class DOCXExporter< /** * The schema of your editor. The mappings are automatically typed checked against this schema. */ - protected readonly schema: BlockNoteSchema, + protected readonly schema: CustomBlockNoteSchema, /** * The mappings that map the BlockNote schema to the docxjs content. * Pass {@link docxDefaultSchemaMappings} for the default schema. diff --git a/packages/xl-multi-column/src/test/conversions/htmlConversion.test.ts b/packages/xl-multi-column/src/test/conversions/htmlConversion.test.ts index 78d6462fde..58b0844d7a 100644 --- a/packages/xl-multi-column/src/test/conversions/htmlConversion.test.ts +++ b/packages/xl-multi-column/src/test/conversions/htmlConversion.test.ts @@ -8,11 +8,8 @@ import { StyleSchema, createExternalHTMLExporter, createInternalHTMLSerializer, + partialBlocksToFullBlocks, } from "@blocknote/core"; -import { - addIdsToBlocks, - partialBlocksToBlocksForTesting, -} from "@shared/formatConversionTestUtil.js"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { multiColumnSchemaTestCases } from "./testCases.js"; @@ -28,9 +25,9 @@ async function convertToHTMLAndCompareSnapshots< snapshotDirectory: string, snapshotName: string, ) { - addIdsToBlocks(blocks); + const fullBlocks = partialBlocksToFullBlocks(editor.schema, blocks); const serializer = createInternalHTMLSerializer(editor.pmSchema, editor); - const internalHTML = serializer.serializeBlocks(blocks, {}); + const internalHTML = serializer.serializeBlocks(fullBlocks, {}); const internalHTMLSnapshotPath = "./__snapshots__/" + snapshotDirectory + @@ -40,14 +37,13 @@ async function convertToHTMLAndCompareSnapshots< await expect(internalHTML).toMatchFileSnapshot(internalHTMLSnapshotPath); // turn the internalHTML back into blocks, and make sure no data was lost - const fullBlocks = partialBlocksToBlocksForTesting(editor.schema, blocks); const parsed = await editor.tryParseHTMLToBlocks(internalHTML); expect(parsed).toStrictEqual(fullBlocks); // Create the "external" HTML, which is a cleaned up HTML representation, but lossy const exporter = createExternalHTMLExporter(editor.pmSchema, editor); - const externalHTML = exporter.exportBlocks(blocks, {}); + const externalHTML = exporter.exportBlocks(fullBlocks, {}); const externalHTMLSnapshotPath = "./__snapshots__/" + snapshotDirectory + diff --git a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts index 5cad44fe9f..139fb880b0 100644 --- a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts +++ b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts @@ -3,40 +3,25 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor, PartialBlock, - UniqueID, blockToNode, nodeToBlock, + partialBlockToFullBlock, } from "@blocknote/core"; -import { partialBlockToBlockForTesting } from "@shared/formatConversionTestUtil.js"; import { multiColumnSchemaTestCases } from "./testCases.js"; -function addIdsToBlock(block: PartialBlock) { - if (!block.id) { - block.id = UniqueID.options.generateID(); - } - for (const child of block.children || []) { - addIdsToBlock(child); - } -} - function validateConversion( block: PartialBlock, editor: BlockNoteEditor, ) { - addIdsToBlock(block); - const node = blockToNode(block, editor.pmSchema); + const fullBlock = partialBlockToFullBlock(editor.schema, block); + const node = blockToNode(fullBlock, editor.pmSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock(node, editor.pmSchema); - const fullOriginalBlock = partialBlockToBlockForTesting( - editor.schema.blockSchema, - block, - ); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + expect(outputBlock).toStrictEqual(fullBlock); } const testCases = [multiColumnSchemaTestCases]; diff --git a/packages/xl-odt-exporter/src/odt/odtExporter.test.ts b/packages/xl-odt-exporter/src/odt/odtExporter.test.ts index 6e8b26c6b4..98c6b67466 100644 --- a/packages/xl-odt-exporter/src/odt/odtExporter.test.ts +++ b/packages/xl-odt-exporter/src/odt/odtExporter.test.ts @@ -3,8 +3,8 @@ import { createPageBreakBlockSpec, defaultBlockSpecs, } from "@blocknote/core"; +import { partialBlocksToFullBlocks } from "@blocknote/core/src/schema/partialBlockToFullBlock.js"; import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; -import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; import { testDocument } from "@shared/testDocument.js"; import { BlobReader, TextWriter, ZipReader } from "@zip.js/zip.js"; import { beforeAll, describe, expect, it } from "vitest"; @@ -78,7 +78,7 @@ describe("exporter", () => { }); const exporter = new ODTExporter(schema, odtDefaultSchemaMappings); const odt = await exporter.toODTDocument( - partialBlocksToBlocksForTesting(schema, [ + partialBlocksToFullBlocks(schema, [ { type: "columnList", children: [ diff --git a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx index f8f2ca9cc3..1f3d375ad0 100644 --- a/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx +++ b/packages/xl-pdf-exporter/src/pdf/pdfExporter.test.tsx @@ -7,10 +7,10 @@ import { defaultBlockSpecs, defaultInlineContentSpecs, defaultStyleSpecs, + partialBlocksToFullBlocks, } from "@blocknote/core"; import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; import { Text } from "@react-pdf/renderer"; -import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; import { testDocument } from "@shared/testDocument.js"; import reactElementToJSXString from "react-element-to-jsx-string"; import { describe, expect, it } from "vitest"; @@ -236,7 +236,7 @@ describe("exporter", () => { }); const exporter = new PDFExporter(schema, pdfDefaultSchemaMappings); const transformed = await exporter.toReactPDFDocument( - partialBlocksToBlocksForTesting(schema, [ + partialBlocksToFullBlocks(schema, [ { type: "columnList", children: [ diff --git a/shared/formatConversionTestUtil.ts b/shared/formatConversionTestUtil.ts deleted file mode 100644 index 6a723c6de0..0000000000 --- a/shared/formatConversionTestUtil.ts +++ /dev/null @@ -1,213 +0,0 @@ -// TODO: remove duplicate file - -import { - Block, - BlockNoteSchema, - BlockSchema, - InlineContent, - InlineContentSchema, - isPartialLinkInlineContent, - isStyledTextInlineContent, - PartialBlock, - PartialInlineContent, - PartialTableCell, - StyledText, - StyleSchema, - TableCell, - TableContent, - UniqueID, -} from "@blocknote/core"; -import * as z from "zod/v4/core"; - -function textShorthandToStyledText( - content: string | StyledText[] = "", -): StyledText[] { - if (typeof content === "string") { - return [ - { - type: "text", - text: content, - styles: {}, - }, - ]; - } - return content; -} - -function partialContentToInlineContent( - content: - | PartialInlineContent - | PartialTableCell - | TableContent - | undefined, -): - | InlineContent[] - | TableContent - | TableCell - | undefined { - if (typeof content === "string") { - return textShorthandToStyledText(content); - } - - if (Array.isArray(content)) { - return content.flatMap((partialContent) => { - if (typeof partialContent === "string") { - return textShorthandToStyledText(partialContent); - } else if (isPartialLinkInlineContent(partialContent)) { - return { - ...partialContent, - content: textShorthandToStyledText(partialContent.content), - }; - } else if (isStyledTextInlineContent(partialContent)) { - return partialContent; - } else { - // custom inline content - - return { - props: {}, - ...partialContent, - content: partialContentToInlineContent(partialContent.content), - } as any; - } - }); - } else if (content?.type === "tableContent") { - return { - type: "tableContent", - columnWidths: content.columnWidths, - headerRows: content.headerRows, - headerCols: content.headerCols, - rows: content.rows.map((row) => { - const cells: any[] = row.cells.map((cell) => { - if (!("type" in cell) || cell.type !== "tableCell") { - return partialContentToInlineContent({ - type: "tableCell", - content: cell as any, - }); - } - return partialContentToInlineContent(cell); - }); - - return { - ...row, - cells, - }; - }), - }; - } else if (content?.type === "tableCell") { - return { - type: "tableCell", - content: partialContentToInlineContent(content.content) as any[], - props: { - backgroundColor: content.props?.backgroundColor ?? "default", - textColor: content.props?.textColor ?? "default", - textAlignment: content.props?.textAlignment ?? "left", - colspan: content.props?.colspan ?? 1, - rowspan: content.props?.rowspan ?? 1, - }, - } satisfies TableCell; - } - - return content; -} - -export function partialBlocksToBlocksForTesting< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - schema: BlockNoteSchema, - partialBlocks: Array>, -): Array> { - return partialBlocks.map((partialBlock) => - partialBlockToBlockForTesting(schema.blockSchema, partialBlock), - ); -} - -export function partialBlockToBlockForTesting< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - schema: BSchema, - partialBlock: PartialBlock, -): Block { - const contentType: "inline" | "table" | "none" = - schema[partialBlock.type!].content; - - const withDefaults: Block = { - id: "", - type: partialBlock.type!, - props: {} as any, - content: - contentType === "inline" - ? [] - : contentType === "table" - ? { - type: "tableContent", - columnWidths: undefined, - headerRows: undefined, - headerCols: undefined, - rows: [], - } - : (undefined as any), - children: [] as any, - ...partialBlock, - }; - - Object.entries(schema[partialBlock.type!].propSchema._zod.def.shape).forEach( - ([propKey, propValue]) => { - if (withDefaults.props[propKey] === undefined) { - if (propValue instanceof z.$ZodDefault) { - (withDefaults.props as any)[propKey] = - propValue._zod.def.defaultValue; - } - if (propValue instanceof z.$ZodOptional) { - (withDefaults.props as any)[propKey] = undefined; - } - } - }, - ); - - if (contentType === "inline") { - const content = withDefaults.content as InlineContent[] | undefined; - withDefaults.content = partialContentToInlineContent(content) as any; - } else if (contentType === "table") { - const content = withDefaults.content as TableContent | undefined; - withDefaults.content = { - type: "tableContent", - columnWidths: - content?.columnWidths || - content?.rows[0]?.cells.map(() => undefined) || - [], - headerRows: content?.headerRows || undefined, - headerCols: content?.headerCols || undefined, - rows: - content?.rows.map((row) => ({ - cells: row.cells.map((cell) => partialContentToInlineContent(cell)), - })) || [], - } as any; - } - - return { - ...withDefaults, - content: partialContentToInlineContent(withDefaults.content), - children: withDefaults.children.map((c) => { - return partialBlockToBlockForTesting(schema, c); - }), - } as any; -} - -export function addIdsToBlock(block: PartialBlock) { - if (!block.id) { - block.id = UniqueID.options.generateID(); - } - if (block.children) { - addIdsToBlocks(block.children); - } -} - -export function addIdsToBlocks(blocks: PartialBlock[]) { - for (const block of blocks) { - addIdsToBlock(block); - } -} diff --git a/shared/testDocument.ts b/shared/testDocument.ts index 340e7d8779..f8dfe924d3 100644 --- a/shared/testDocument.ts +++ b/shared/testDocument.ts @@ -2,15 +2,15 @@ import { BlockNoteSchema, createPageBreakBlockSpec, defaultBlockSpecs, + partialBlocksToFullBlocks, } from "@blocknote/core"; import * as z from "zod/v4"; -import { partialBlocksToBlocksForTesting } from "./formatConversionTestUtil.js"; // @ts-ignore const y = z; // needed to fix build // TODO: Update tests that use this to the new format and remove -export const testDocument = partialBlocksToBlocksForTesting( +export const testDocument = partialBlocksToFullBlocks( BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, pageBreak: createPageBreakBlockSpec() }, }), diff --git a/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts b/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts index e7ccef6e5b..f590bfb198 100644 --- a/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts +++ b/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts @@ -3,9 +3,10 @@ import { BlockSchema, blockToNode, InlineContentSchema, + partialBlocksToFullBlocks, + partialBlockToFullBlock, StyleSchema, } from "@blocknote/core"; -import { addIdsToBlocks } from "@shared/formatConversionTestUtil.js"; import { prettify } from "htmlfy"; import { expect } from "vitest"; @@ -21,12 +22,15 @@ export const testExportBlockNoteHTML = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); - await expect( - prettify(await editor.blocksToFullHTML(testCase.content), { - tag_wrap: true, - }), + prettify( + await editor.blocksToFullHTML( + partialBlocksToFullBlocks(editor.schema, testCase.content), + ), + { + tag_wrap: true, + }, + ), ).toMatchFileSnapshot(`./__snapshots__/blocknoteHTML/${testCase.name}.html`); }; @@ -40,12 +44,15 @@ export const testExportHTML = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); - await expect( - prettify(await editor.blocksToHTMLLossy(testCase.content), { - tag_wrap: true, - }), + prettify( + await editor.blocksToHTMLLossy( + partialBlocksToFullBlocks(editor.schema, testCase.content), + ), + { + tag_wrap: true, + }, + ), ).toMatchFileSnapshot(`./__snapshots__/html/${testCase.name}.html`); }; @@ -59,10 +66,10 @@ export const testExportMarkdown = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); - await expect( - await editor.blocksToMarkdownLossy(testCase.content), + await editor.blocksToMarkdownLossy( + partialBlocksToFullBlocks(editor.schema, testCase.content), + ), ).toMatchFileSnapshot(`./__snapshots__/markdown/${testCase.name}.md`); }; @@ -76,11 +83,13 @@ export const testExportNodes = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); - await expect( testCase.content.map((block) => - blockToNode(block, editor.pmSchema, editor.schema.styleSchema), + blockToNode( + partialBlockToFullBlock(editor.schema, block), + editor.pmSchema, + editor.schema.styleSchema, + ), ), ).toMatchFileSnapshot(`./__snapshots__/nodes/${testCase.name}.json`); }; diff --git a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts index a42f7c7c4b..52cf6376bc 100644 --- a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts +++ b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts @@ -4,12 +4,9 @@ import { blockToNode, InlineContentSchema, nodeToBlock, + partialBlocksToFullBlocks, StyleSchema, } from "@blocknote/core"; -import { - addIdsToBlocks, - partialBlocksToBlocksForTesting, -} from "@shared/formatConversionTestUtil.js"; import { expect } from "vitest"; import { ExportParseEqualityTestCase } from "./exportParseEqualityTestCase.js"; @@ -23,19 +20,18 @@ export const testExportParseEqualityBlockNoteHTML = async < testCase: ExportParseEqualityTestCase, ) => { (window as any).__TEST_OPTIONS.mockID = 0; + const fullBlocks = partialBlocksToFullBlocks(editor.schema, testCase.content); - addIdsToBlocks(testCase.content); - - const exported = await editor.blocksToFullHTML(testCase.content); + const exported = await editor.blocksToFullHTML(fullBlocks); if (testCase.name.startsWith("malformed/")) { // We purposefully are okay with malformed response, we know they won't match expect(await editor.tryParseHTMLToBlocks(exported)).not.toStrictEqual( - partialBlocksToBlocksForTesting(editor.schema, testCase.content), + fullBlocks, ); } else { expect(await editor.tryParseHTMLToBlocks(exported)).toStrictEqual( - partialBlocksToBlocksForTesting(editor.schema, testCase.content), + fullBlocks, ); } }; @@ -50,17 +46,15 @@ export const testExportParseEqualityHTML = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); + const fullBlocks = partialBlocksToFullBlocks(editor.schema, testCase.content); - const exported = await editor.blocksToHTMLLossy(testCase.content); + const exported = await editor.blocksToHTMLLossy(fullBlocks); // Reset mock ID as we don't expect block IDs to be preserved in this // conversion. (window as any).__TEST_OPTIONS.mockID = 0; - expect(await editor.tryParseHTMLToBlocks(exported)).toStrictEqual( - partialBlocksToBlocksForTesting(editor.schema, testCase.content), - ); + expect(await editor.tryParseHTMLToBlocks(exported)).toStrictEqual(fullBlocks); }; export const testExportParseEqualityNodes = async < @@ -73,15 +67,13 @@ export const testExportParseEqualityNodes = async < ) => { (window as any).__TEST_OPTIONS.mockID = 0; - addIdsToBlocks(testCase.content); + const fullBlocks = partialBlocksToFullBlocks(editor.schema, testCase.content); - const exported = testCase.content.map((block) => + const exported = fullBlocks.map((block) => blockToNode(block, editor.pmSchema), ); expect( exported.map((node) => nodeToBlock(node, editor.pmSchema)), - ).toStrictEqual( - partialBlocksToBlocksForTesting(editor.schema, testCase.content), - ); + ).toStrictEqual(fullBlocks); }; From 30c55811c0d446dfef0a31999be19cbb822e57d7 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 06:41:38 +0200 Subject: [PATCH 2/7] remove unneeded code --- .../html/util/serializeBlocksExternalHTML.ts | 17 ----------------- .../html/util/serializeBlocksInternalHTML.ts | 18 ------------------ 2 files changed, 35 deletions(-) diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index 79da4d5df0..df27d31322 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -174,23 +174,6 @@ function serializeBlock< const doc = options?.document ?? document; const BC_NODE = editor.pmSchema.nodes["blockContainer"]; - // set default props in case we were passed a partial block - // TODO: should be a nicer way for this / or move to caller - // const props = block.props || {}; - // for (const [name, spec] of Object.entries( - // editor.schema.blockSchema[ - // block.type as keyof typeof editor.schema.blockSchema - // ].propSchema._zod.def.shape, - // )) { - // if ( - // !(name in props) && - // spec instanceof z.$ZodDefault && - // spec._zod.def.defaultValue !== undefined - // ) { - // (props as any)[name] = spec._zod.def.defaultValue; - // } - // } - const bc = BC_NODE.spec?.toDOM?.( BC_NODE.create({ id: block.id, diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index d582e4922d..2c6958db1d 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -13,7 +13,6 @@ import { tableContentToNodes, } from "../../../nodeConversions/blockToNode.js"; -import * as z from "zod/v4/core"; import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js"; export function serializeInlineContentInternalHTML< BSchema extends BlockSchema, @@ -139,23 +138,6 @@ function serializeBlock< ) { const BC_NODE = editor.pmSchema.nodes["blockContainer"]; - // set default props in case we were passed a partial block - // TODO: should be a nicer way for this / or move to caller - const props = block.props || {}; - for (const [name, spec] of Object.entries( - editor.schema.blockSchema[ - block.type as keyof typeof editor.schema.blockSchema - ].propSchema._zod.def.shape, - )) { - if ( - !(name in props) && - spec instanceof z.$ZodDefault && - spec._zod.def.defaultValue !== undefined - ) { - (props as any)[name] = spec._zod.def.defaultValue; - } - } - const impl = editor.blockImplementations[block.type as any].implementation; const ret = impl.render.call( { From f2d191e3a24a1b8b3142a42d566d81241bafb62f Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 06:45:59 +0200 Subject: [PATCH 3/7] fix ! --- packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 0d32681869..02f652a0ed 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -546,7 +546,7 @@ export class TableHandlesView< } // Hide handles if the table block has been removed. - const block = this.editor.getBlock(this.state.block.id)!; + const block = this.editor.getBlock(this.state.block.id); if ( !block || From 75bd4e56e33be05fc449f42d940019922359995b Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 06:55:34 +0200 Subject: [PATCH 4/7] fix build --- .../api/exporters/html/util/serializeBlocksInternalHTML.ts | 4 ++-- packages/xl-docx-exporter/src/docx/docxExporter.test.ts | 4 ++-- packages/xl-docx-exporter/src/docx/docxExporter.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index 2c6958db1d..e71221bdf4 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -144,7 +144,7 @@ function serializeBlock< renderType: "dom", props: undefined, }, - { ...block, props } as any, + block, editor as any, ); @@ -179,7 +179,7 @@ function serializeBlock< const bc = BC_NODE.spec?.toDOM?.( BC_NODE.create({ id: block.id, - ...props, + ...block.props, }), ) as { dom: HTMLElement; diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts index 2f2deb63ed..78a91e0bd0 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts @@ -140,7 +140,7 @@ describe("exporter", () => { }, }); const exporter = new DOCXExporter(schema, docxDefaultSchemaMappings); - const x = partialBlocksToFullBlocks(schema, [ + const blocks = partialBlocksToFullBlocks(schema, [ { type: "columnList", children: [ @@ -195,7 +195,7 @@ describe("exporter", () => { ], }, ]); - const doc = await exporter.toDocxJsDocument(x, { + const doc = await exporter.toDocxJsDocument(blocks, { sectionOptions: {}, documentOptions: {}, locale: "en-US", diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.ts b/packages/xl-docx-exporter/src/docx/docxExporter.ts index 850906d671..4fc419a612 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.ts @@ -2,7 +2,7 @@ import { Block, BlockSchema, COLORS_DEFAULT, - CustomBlockNoteSchema, + BlockNoteSchema, InlineContentSchema, StyleSchema, StyledText, @@ -54,7 +54,7 @@ export class DOCXExporter< /** * The schema of your editor. The mappings are automatically typed checked against this schema. */ - protected readonly schema: CustomBlockNoteSchema, + protected readonly schema: BlockNoteSchema, /** * The mappings that map the BlockNote schema to the docxjs content. * Pass {@link docxDefaultSchemaMappings} for the default schema. From 5eb10ec8f4201331cd441a7274ce4432741228a5 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 07:00:56 +0200 Subject: [PATCH 5/7] remove SpecificBlock --- packages/core/src/schema/blocks/internal.ts | 7 +++---- packages/core/src/schema/blocks/types.ts | 9 --------- .../DragHandleMenu/DefaultItems/TableHeadersItem.tsx | 5 ++--- .../TableHandles/TableCellMenu/TableCellMenuProps.ts | 3 +-- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index ef94f558bd..b96ad7ec6e 100644 --- a/packages/core/src/schema/blocks/internal.ts +++ b/packages/core/src/schema/blocks/internal.ts @@ -11,9 +11,9 @@ import { PropSchema, Props } from "../propTypes.js"; import { StyleSchema } from "../styles/types.js"; import { BlockConfig, + BlockFromConfig, BlockSchemaWithBlock, LooseBlockSpec, - SpecificBlock, } from "./types.js"; // Function that uses the 'propSchema' of a blockConfig to create a TipTap @@ -92,9 +92,8 @@ export function getBlockFromPos< } // Gets the block - const block = editor.getBlock(blockIdentifier)! as SpecificBlock< - BSchema, - BType, + const block = editor.getBlock(blockIdentifier)! as unknown as BlockFromConfig< + Config, I, S >; diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index b22babe091..f27ee2d303 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -280,15 +280,6 @@ export type BlockNoDefaults< children: BlockNoDefaults[]; }; -export type SpecificBlock< - BSchema extends BlockSchema, - BType extends keyof BSchema, - I extends InlineContentSchema, - S extends StyleSchema, -> = BlocksWithoutChildren[BType] & { - children: BlockNoDefaults[]; -}; - type PartialBlockFromConfigNoChildren< B extends BlockConfig, I extends InlineContentSchema, diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx index 29917f00ac..38c6638250 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx @@ -4,7 +4,6 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, - SpecificBlock, StyleSchema, } from "@blocknote/core"; import { ReactNode } from "react"; @@ -19,7 +18,7 @@ export const TableRowHeaderItem = < S extends StyleSchema = DefaultStyleSchema, >( props: Omit, "block"> & { - block: SpecificBlock<{ table: DefaultBlockSchema["table"] }, "table", I, S>; + block: BlockFromConfig; children: ReactNode; }, ) => { @@ -68,7 +67,7 @@ export const TableColumnHeaderItem = < S extends StyleSchema = DefaultStyleSchema, >( props: Omit, "block"> & { - block: SpecificBlock<{ table: DefaultBlockSchema["table"] }, "table", I, S>; + block: BlockFromConfig; children: ReactNode; }, ) => { diff --git a/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts b/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts index b2af7056d4..027ffde1ae 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts +++ b/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts @@ -3,7 +3,6 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, - SpecificBlock, StyleSchema, } from "@blocknote/core"; @@ -11,7 +10,7 @@ export type TableCellMenuProps< I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema, > = { - block: SpecificBlock<{ table: DefaultBlockSchema["table"] }, "table", I, S>; + block: BlockFromConfig; rowIndex: number; colIndex: number; }; From b99822ad0fb9e425ac9b06e4faaea4a468586ed0 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 07:07:21 +0200 Subject: [PATCH 6/7] fix build --- .../DefaultItems/TableHeadersItem.tsx | 1 + .../TableCellMenu/DefaultButtons/ColorPicker.tsx | 2 +- .../TableCellMenu/TableCellMenuProps.ts | 1 + packages/react/src/schema/ReactBlockSpec.tsx | 13 +++++++------ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx index 38c6638250..65cdffa3ca 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx @@ -1,4 +1,5 @@ import { + BlockFromConfig, BlockSchema, DefaultBlockSchema, DefaultInlineContentSchema, diff --git a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx index eddbc6b5c7..9800e93540 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx +++ b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx @@ -8,12 +8,12 @@ import { StyleSchema, } from "@blocknote/core"; +import { ReactNode } from "react"; import { useComponentsContext } from "../../../../editor/ComponentsContext.js"; import { useBlockNoteEditor } from "../../../../hooks/useBlockNoteEditor.js"; import { useDictionary } from "../../../../i18n/dictionary.js"; import { ColorPicker } from "../../../ColorPicker/ColorPicker.js"; import { TableCellMenuProps } from "../TableCellMenuProps.js"; -import { ReactNode } from "react"; export const ColorPickerButton = < I extends InlineContentSchema = DefaultInlineContentSchema, diff --git a/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts b/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts index 027ffde1ae..2223cb4260 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts +++ b/packages/react/src/components/TableHandles/TableCellMenu/TableCellMenuProps.ts @@ -1,4 +1,5 @@ import { + BlockFromConfig, DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index 06b281d231..0887611f86 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -259,12 +259,13 @@ export function createReactBlockSpec< // only created once, so the block we get in the node view will // be outdated. Therefore, we have to get the block in the // `ReactNodeViewRenderer` instead. - const block = getBlockFromPos( - props.getPos, - editor as any, - props.editor, - blockConfig.type, - ); + const block = getBlockFromPos< + TName, + BlockConfig, + any, + any, + any + >(props.getPos, editor as any, props.editor, blockConfig.type); const ref = useReactNodeView().nodeViewContentRef; From 53bd6d70ecbc45ec698148396748e2b33a7d4744 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 16 Oct 2025 07:14:30 +0200 Subject: [PATCH 7/7] fix core tests --- packages/core/src/api/blockManipulation/tables/tables.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/api/blockManipulation/tables/tables.ts b/packages/core/src/api/blockManipulation/tables/tables.ts index 1a9e62d410..811288c85f 100644 --- a/packages/core/src/api/blockManipulation/tables/tables.ts +++ b/packages/core/src/api/blockManipulation/tables/tables.ts @@ -654,8 +654,7 @@ function isCellEmpty( return true; } if (isPartialTableCell(cell)) { - // TODO: what happened here? - return isCellEmpty(cell); + return isCellEmpty(cell.content); } else if (typeof cell === "string") { return cell.length === 0; } else if (Array.isArray(cell)) {