From 20f691bf65d8d98336e868e39e1043e4a433a675 Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 20 Nov 2023 21:30:35 +0100 Subject: [PATCH 01/31] wip custom styles --- examples/vanilla/src/main.tsx | 6 +- packages/core/src/BlockNoteEditor.test.ts | 2 +- packages/core/src/BlockNoteEditor.ts | 268 ++++++++++++------ packages/core/src/BlockNoteExtensions.ts | 13 +- .../blockManipulation.test.ts | 2 +- .../blockManipulation/blockManipulation.ts | 42 ++- .../nodeConversions/nodeConversions.test.ts | 180 +++++++++--- .../api/nodeConversions/nodeConversions.ts | 185 ++++++------ .../core/src/api/nodeConversions/testUtil.ts | 22 +- .../clipboardHandlerExtension.ts | 18 +- .../html/externalHTMLExporter.ts | 33 ++- .../serialization/html/htmlConversion.test.ts | 51 ++-- .../html/internalHTMLSerializer.ts | 23 +- .../html/sharedHTMLConversion.ts | 24 +- .../core/src/extensions/Blocks/api/block.ts | 13 +- .../src/extensions/Blocks/api/blockTypes.ts | 165 +++++++---- .../Blocks/api/cursorPositionTypes.ts | 12 +- .../src/extensions/Blocks/api/customBlocks.ts | 28 +- .../extensions/Blocks/api/defaultBlocks.ts | 77 ++++- .../Blocks/api/inlineContentTypes.ts | 36 +-- .../core/src/extensions/Blocks/api/styles.ts | 47 +++ .../extensions/Blocks/nodes/BlockContainer.ts | 34 ++- .../ImageBlockContent/ImageBlockContent.ts | 5 +- .../nodes/BlockContent/defaultBlockHelpers.ts | 18 +- .../FormattingToolbarPlugin.ts | 13 +- .../HyperlinkToolbarPlugin.ts | 12 +- .../ImageToolbar/ImageToolbarPlugin.ts | 37 ++- .../src/extensions/SideMenu/SideMenuPlugin.ts | 31 +- .../extensions/SlashMenu/BaseSlashMenuItem.ts | 13 +- .../extensions/SlashMenu/SlashMenuPlugin.ts | 8 +- .../SlashMenu/defaultSlashMenuItems.ts | 41 +-- .../TableHandles/TableHandlesPlugin.ts | 58 ++-- .../plugins/suggestion/SuggestionPlugin.ts | 17 +- packages/react/src/BlockNoteView.tsx | 15 +- .../DefaultButtons/ColorStyleButton.tsx | 14 +- .../DefaultButtons/ImageCaptionButton.tsx | 14 +- .../DefaultButtons/ToggledStyleButton.tsx | 24 +- .../DefaultDropdowns/BlockTypeDropdown.tsx | 4 +- .../FormattingToolbarPositioner.tsx | 4 +- .../components/DefaultHyperlinkToolbar.tsx | 12 +- .../components/HyperlinkToolbarPositioner.tsx | 16 +- .../components/ImageToolbarPositioner.tsx | 9 +- packages/react/src/ReactBlockSpec.tsx | 26 +- .../components/SideMenuPositioner.tsx | 28 +- .../react/src/SlashMenu/ReactSlashMenuItem.ts | 7 +- .../components/SlashMenuPositioner.tsx | 6 +- .../SlashMenu/defaultReactSlashMenuItems.tsx | 12 +- .../components/DefaultTableHandle.tsx | 28 +- .../components/TableHandlePositioner.tsx | 43 +-- packages/react/src/hooks/useBlockNote.ts | 45 ++- packages/react/src/hooks/useEditorChange.ts | 6 +- .../react/src/hooks/useEditorContentChange.ts | 5 +- .../src/hooks/useEditorSelectionChange.ts | 5 +- packages/react/src/hooks/useSelectedBlocks.ts | 12 +- packages/react/src/htmlConversion.test.tsx | 31 +- packages/website/docs/docs/vanilla-js.md | 4 +- 56 files changed, 1250 insertions(+), 654 deletions(-) create mode 100644 packages/core/src/extensions/Blocks/api/styles.ts diff --git a/examples/vanilla/src/main.tsx b/examples/vanilla/src/main.tsx index 6f8a84712a..926d39d3fb 100644 --- a/examples/vanilla/src/main.tsx +++ b/examples/vanilla/src/main.tsx @@ -1,11 +1,11 @@ import { BlockNoteEditor } from "@blocknote/core"; import "./index.css"; -import { addSideMenu } from "./ui/addSideMenu"; import { addFormattingToolbar } from "./ui/addFormattingToolbar"; -import { addSlashMenu } from "./ui/addSlashMenu"; import { addHyperlinkToolbar } from "./ui/addHyperlinkToolbar"; +import { addSideMenu } from "./ui/addSideMenu"; +import { addSlashMenu } from "./ui/addSlashMenu"; -const editor = new BlockNoteEditor({ +const editor = BlockNoteEditor.create({ parentElement: document.getElementById("root")!, onEditorContentChange: () => { console.log(editor.topLevelBlocks); diff --git a/packages/core/src/BlockNoteEditor.test.ts b/packages/core/src/BlockNoteEditor.test.ts index 9c1b60fb12..f295c76fab 100644 --- a/packages/core/src/BlockNoteEditor.test.ts +++ b/packages/core/src/BlockNoteEditor.test.ts @@ -6,7 +6,7 @@ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFro * @vitest-environment jsdom */ it("creates an editor", () => { - const editor = new BlockNoteEditor({}); + const editor = BlockNoteEditor.create(); const blockInfo = getBlockInfoFromPos(editor._tiptapEditor.state.doc, 2); expect(blockInfo?.contentNode.type.name).toEqual("paragraph"); }); diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 2f1b79c23b..7bfdefa058 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -20,19 +20,25 @@ import { BlockIdentifier, BlockNoteDOMAttributes, BlockSchema, + BlockSpecs, PartialBlock, } from "./extensions/Blocks/api/blockTypes"; import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; import { DefaultBlockSchema, + DefaultStyleSchema, defaultBlockSchema, + defaultBlockSpecs, + defaultStyleSpecs, + getBlockSchemaFromSpecs, + getStyleSchemaFromSpecs, } from "./extensions/Blocks/api/defaultBlocks"; +import { Selection } from "./extensions/Blocks/api/selectionTypes"; import { - ColorStyle, + StyleSchema, + StyleSpecs, Styles, - ToggledStyle, -} from "./extensions/Blocks/api/inlineContentTypes"; -import { Selection } from "./extensions/Blocks/api/selectionTypes"; +} from "./extensions/Blocks/api/styles"; import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos"; import "prosemirror-tables/style/tables.css"; @@ -48,7 +54,14 @@ import { TableHandlesProsemirrorPlugin } from "./extensions/TableHandles/TableHa import { UniqueID } from "./extensions/UniqueID/UniqueID"; import { UnreachableCaseError, mergeCSSClasses } from "./shared/utils"; -export type BlockNoteEditorOptions = { +export type BlockNoteEditorOptions< + BSpecs extends BlockSpecs, + SSpecs extends StyleSpecs, + BSchema extends BlockSchema = { + [key in keyof BSpecs]: BSpecs[key]["config"]; + }, + SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } +> = { // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them. enableBlockNoteExtensions: boolean; /** @@ -57,7 +70,7 @@ export type BlockNoteEditorOptions = { * * @default defaultSlashMenuItems from `./extensions/SlashMenu` */ - slashMenuItems: BaseSlashMenuItem[]; + slashMenuItems: BaseSlashMenuItem[]; /** * The HTML element that should be used as the parent element for the editor. @@ -74,15 +87,17 @@ export type BlockNoteEditorOptions = { /** * A callback function that runs when the editor is ready to be used. */ - onEditorReady: (editor: BlockNoteEditor) => void; + onEditorReady: (editor: BlockNoteEditor) => void; /** * A callback function that runs whenever the editor's contents change. */ - onEditorContentChange: (editor: BlockNoteEditor) => void; + onEditorContentChange: (editor: BlockNoteEditor) => void; /** * A callback function that runs whenever the text cursor position changes. */ - onTextCursorPositionChange: (editor: BlockNoteEditor) => void; + onTextCursorPositionChange: ( + editor: BlockNoteEditor + ) => void; /** * Locks the editor from being editable by the user if set to `false`. */ @@ -90,7 +105,7 @@ export type BlockNoteEditorOptions = { /** * The content that should be in the editor when it's created, represented as an array of partial block objects. */ - initialContent: PartialBlock[]; + initialContent: PartialBlock[]; /** * Use default BlockNote font and reset the styles of

  • elements etc., that are used in BlockNote. * @@ -101,7 +116,9 @@ export type BlockNoteEditorOptions = { /** * A list of block types that should be available in the editor. */ - blockSchema: BSchema; + blockSpecs: BSpecs; + + styleSpecs: SSpecs; /** * A custom function to handle file uploads. @@ -145,28 +162,74 @@ const blockNoteTipTapOptions = { enableCoreExtensions: false, }; -export class BlockNoteEditor { +// const ss = { +// paragraph: Paragraph, +// } as const; + +// const xa = createEditor({ +// blockSchema: ss, +// }); + +// xa.schema. + +export class BlockNoteEditor< + BSchema extends BlockSchema = DefaultBlockSchema, + SSchema extends StyleSchema = DefaultStyleSchema +> { public readonly _tiptapEditor: TiptapEditor & { contentComponent: any }; - public blockCache = new WeakMap>(); - public readonly schema: BSchema; + public blockCache = new WeakMap>(); + public readonly blockSchema: BSchema; + public readonly styleSchema: SSchema; + + public readonly blockImplementations: BlockSpecs; + public readonly styleImplementations: StyleSpecs; + public ready = false; - public readonly sideMenu: SideMenuProsemirrorPlugin; - public readonly formattingToolbar: FormattingToolbarProsemirrorPlugin; - public readonly slashMenu: SlashMenuProsemirrorPlugin; - public readonly hyperlinkToolbar: HyperlinkToolbarProsemirrorPlugin; - public readonly imageToolbar: ImageToolbarProsemirrorPlugin; - public readonly tableHandles: TableHandlesProsemirrorPlugin; + public readonly sideMenu: SideMenuProsemirrorPlugin; + public readonly formattingToolbar: FormattingToolbarProsemirrorPlugin; + public readonly slashMenu: SlashMenuProsemirrorPlugin; + public readonly hyperlinkToolbar: HyperlinkToolbarProsemirrorPlugin< + BSchema, + SSchema + >; + public readonly imageToolbar: ImageToolbarProsemirrorPlugin; + public readonly tableHandles: + | TableHandlesProsemirrorPlugin + | undefined; public readonly uploadFile: ((file: File) => Promise) | undefined; - constructor( - private readonly options: Partial> = {} + public static create< + BSpecs extends BlockSpecs = typeof defaultBlockSpecs, + SSpecs extends StyleSpecs = typeof defaultStyleSpecs, + BSchema extends BlockSchema = { + [key in keyof BSpecs]: BSpecs[key]["config"]; + }, + SSchema extends StyleSchema = { + [key in keyof SSpecs]: SSpecs[key]["config"]; + } + >( + options: Partial< + BlockNoteEditorOptions + > = {} + ) { + return new BlockNoteEditor(options); + } + + private constructor( + private readonly options: Partial< + BlockNoteEditorOptions + > ) { // apply defaults - const newOptions: Omit & { + const newOptions: Omit< + typeof options, + "defaultStyles" | "blockSpecs" | "styleSpecs" + > & { defaultStyles: boolean; - blockSchema: BSchema; + blockSpecs: BlockSpecs; + styleSpecs: StyleSpecs; } = { defaultStyles: true, // TODO: There's a lot of annoying typing stuff to deal with here. If @@ -174,25 +237,32 @@ export class BlockNoteEditor { // If BSchema is not specified, then options.blockSchema should also not // be defined. Unfortunately, trying to implement these constraints seems // to be a huge pain, hence the `as any` casts. - blockSchema: options.blockSchema || (defaultBlockSchema as any), + blockSpecs: options.blockSpecs || (defaultBlockSchema as any), + styleSpecs: options.styleSpecs || (defaultStyleSpecs as any), ...options, }; + this.blockSchema = getBlockSchemaFromSpecs(newOptions.blockSpecs) as any; + this.styleSchema = getStyleSchemaFromSpecs(newOptions.styleSpecs) as any; + this.sideMenu = new SideMenuProsemirrorPlugin(this); this.formattingToolbar = new FormattingToolbarProsemirrorPlugin(this); this.slashMenu = new SlashMenuProsemirrorPlugin( this, - newOptions.slashMenuItems || - getDefaultSlashMenuItems(newOptions.blockSchema) + newOptions.slashMenuItems || getDefaultSlashMenuItems(this.blockSchema) ); this.hyperlinkToolbar = new HyperlinkToolbarProsemirrorPlugin(this); this.imageToolbar = new ImageToolbarProsemirrorPlugin(this); - this.tableHandles = new TableHandlesProsemirrorPlugin(this); - const extensions = getBlockNoteExtensions({ + if (this.blockSchema.table === defaultBlockSchema.table) { + this.tableHandles = new TableHandlesProsemirrorPlugin(this as any); + } + + const extensions = getBlockNoteExtensions({ editor: this, domAttributes: newOptions.domAttributes || {}, - blockSchema: newOptions.blockSchema, + blockSchema: this.blockSchema, + blockSpecs: newOptions.blockSpecs, collaboration: newOptions.collaboration, }); @@ -206,13 +276,14 @@ export class BlockNoteEditor { this.slashMenu.plugin, this.hyperlinkToolbar.plugin, this.imageToolbar.plugin, - this.tableHandles.plugin, + ...(this.tableHandles ? [this.tableHandles.plugin] : []), ]; }, }); extensions.push(blockNoteUIExtension); - this.schema = newOptions.blockSchema; + this.blockImplementations = newOptions.blockSpecs; + this.styleImplementations = newOptions.styleSpecs; this.uploadFile = newOptions.uploadFile; @@ -226,6 +297,7 @@ export class BlockNoteEditor { id: UniqueID.options.generateID(), }, ]); + const styleSchema = this.styleSchema; const tiptapOptions: Partial = { ...blockNoteTipTapOptions, @@ -245,7 +317,11 @@ export class BlockNoteEditor { "doc", undefined, schema.node("blockGroup", undefined, [ - blockToNode({ id: "initialBlock", type: "paragraph" }, schema), + blockToNode( + { id: "initialBlock", type: "paragraph" }, + schema, + styleSchema + ), ]) ); editor.editor.options.content = root.toJSON(); @@ -340,7 +416,9 @@ export class BlockNoteEditor { const blocks: Block[] = []; this._tiptapEditor.state.doc.firstChild!.descendants((node) => { - blocks.push(nodeToBlock(node, this.schema, this.blockCache)); + blocks.push( + nodeToBlock(node, this.blockSchema, this.styleSchema, this.blockCache) + ); return false; }); @@ -371,7 +449,12 @@ export class BlockNoteEditor { return true; } - newBlock = nodeToBlock(node, this.schema, this.blockCache); + newBlock = nodeToBlock( + node, + this.blockSchema, + this.styleSchema, + this.blockCache + ); return false; }); @@ -385,7 +468,7 @@ export class BlockNoteEditor { * @param reverse Whether the blocks should be traversed in reverse order. */ public forEachBlock( - callback: (block: Block) => boolean, + callback: (block: Block) => boolean, reverse = false ): void { const blocks = this.topLevelBlocks.slice(); @@ -394,7 +477,9 @@ export class BlockNoteEditor { blocks.reverse(); } - function traverseBlockArray(blockArray: Block[]): boolean { + function traverseBlockArray( + blockArray: Block[] + ): boolean { for (const block of blockArray) { if (!callback(block)) { return false; @@ -435,7 +520,7 @@ export class BlockNoteEditor { * Gets a snapshot of the current text cursor position. * @returns A snapshot of the current text cursor position. */ - public getTextCursorPosition(): TextCursorPosition { + public getTextCursorPosition(): TextCursorPosition { const { node, depth, startPos, endPos } = getBlockInfoFromPos( this._tiptapEditor.state.doc, this._tiptapEditor.state.selection.from @@ -463,15 +548,30 @@ export class BlockNoteEditor { } return { - block: nodeToBlock(node, this.schema, this.blockCache), + block: nodeToBlock( + node, + this.blockSchema, + this.styleSchema, + this.blockCache + ), prevBlock: prevNode === undefined ? undefined - : nodeToBlock(prevNode, this.schema, this.blockCache), + : nodeToBlock( + prevNode, + this.blockSchema, + this.styleSchema, + this.blockCache + ), nextBlock: nextNode === undefined ? undefined - : nodeToBlock(nextNode, this.schema, this.blockCache), + : nodeToBlock( + nextNode, + this.blockSchema, + this.styleSchema, + this.blockCache + ), }; } @@ -494,7 +594,7 @@ export class BlockNoteEditor { )!; const contentType: "none" | "inline" | "table" = - this.schema[contentNode.type.name]!.config.content; + this.blockSchema[contentNode.type.name]!.content; if (contentType === "none") { this._tiptapEditor.commands.setNodeSelection(startPos); @@ -555,7 +655,8 @@ export class BlockNoteEditor { blocks.push( nodeToBlock( this._tiptapEditor.state.doc.resolve(pos).node(), - this.schema, + this.blockSchema, + this.styleSchema, this.blockCache ) ); @@ -591,11 +692,11 @@ export class BlockNoteEditor { * `referenceBlock`. Inserts the blocks at the start of the existing block's children if "nested" is used. */ public insertBlocks( - blocksToInsert: PartialBlock[], + blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before" ): void { - insertBlocks(blocksToInsert, referenceBlock, placement, this._tiptapEditor); + insertBlocks(blocksToInsert, referenceBlock, placement, this); } /** @@ -607,7 +708,7 @@ export class BlockNoteEditor { */ public updateBlock( blockToUpdate: BlockIdentifier, - update: PartialBlock + update: PartialBlock ) { updateBlock(blockToUpdate, update, this._tiptapEditor); } @@ -629,32 +730,27 @@ export class BlockNoteEditor { */ public replaceBlocks( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[] + blocksToInsert: PartialBlock[] ) { - replaceBlocks(blocksToRemove, blocksToInsert, this._tiptapEditor); + replaceBlocks(blocksToRemove, blocksToInsert, this); } /** * Gets the active text styles at the text cursor position or at the end of the current selection if it's active. */ public getActiveStyles() { - const styles: Styles = {}; + const styles: Styles = {}; const marks = this._tiptapEditor.state.selection.$to.marks(); - const toggleStyles = new Set([ - "bold", - "italic", - "underline", - "strike", - "code", - ]); - const colorStyles = new Set(["textColor", "backgroundColor"]); - for (const mark of marks) { - if (toggleStyles.has(mark.type.name as ToggledStyle)) { - styles[mark.type.name as ToggledStyle] = true; - } else if (colorStyles.has(mark.type.name as ColorStyle)) { - styles[mark.type.name as ColorStyle] = mark.attrs.color; + const config = this.styleSchema[mark.type.name]; + if (!config) { + continue; + } + if (config.propSchema === "boolean") { + (styles as any)[config.type] = true; + } else { + (styles as any)[config.type] = mark.attrs.stringValue; } } @@ -665,23 +761,20 @@ export class BlockNoteEditor { * Adds styles to the currently selected content. * @param styles The styles to add. */ - public addStyles(styles: Styles) { - const toggleStyles = new Set([ - "bold", - "italic", - "underline", - "strike", - "code", - ]); - const colorStyles = new Set(["textColor", "backgroundColor"]); - + public addStyles(styles: Styles) { this._tiptapEditor.view.focus(); for (const [style, value] of Object.entries(styles)) { - if (toggleStyles.has(style as ToggledStyle)) { + const config = this.styleSchema[style]; + if (!config) { + throw new Error(`style ${style} not found in styleSchema`); + } + if (config.propSchema === "boolean") { this._tiptapEditor.commands.setMark(style); - } else if (colorStyles.has(style as ColorStyle)) { - this._tiptapEditor.commands.setMark(style, { color: value }); + } else if (config.propSchema === "string") { + this._tiptapEditor.commands.setMark(style, { stringValue: value }); + } else { + throw new UnreachableCaseError(config.propSchema); } } } @@ -690,7 +783,7 @@ export class BlockNoteEditor { * Removes styles from the currently selected content. * @param styles The styles to remove. */ - public removeStyles(styles: Styles) { + public removeStyles(styles: Styles) { this._tiptapEditor.view.focus(); for (const style of Object.keys(styles)) { @@ -702,23 +795,20 @@ export class BlockNoteEditor { * Toggles styles on the currently selected content. * @param styles The styles to toggle. */ - public toggleStyles(styles: Styles) { - const toggleStyles = new Set([ - "bold", - "italic", - "underline", - "strike", - "code", - ]); - const colorStyles = new Set(["textColor", "backgroundColor"]); - + public toggleStyles(styles: Styles) { this._tiptapEditor.view.focus(); for (const [style, value] of Object.entries(styles)) { - if (toggleStyles.has(style as ToggledStyle)) { + const config = this.styleSchema[style]; + if (!config) { + throw new Error(`style ${style} not found in styleSchema`); + } + if (config.propSchema === "boolean") { this._tiptapEditor.commands.toggleMark(style); - } else if (colorStyles.has(style as ColorStyle)) { - this._tiptapEditor.commands.toggleMark(style, { color: value }); + } else if (config.propSchema === "string") { + this._tiptapEditor.commands.toggleMark(style, { stringValue: value }); + } else { + throw new UnreachableCaseError(config.propSchema); } } } diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 16ae7853a9..3b6e000b44 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -23,7 +23,9 @@ import { BlockContainer, BlockGroup, Doc } from "./extensions/Blocks"; import { BlockNoteDOMAttributes, BlockSchema, + BlockSpecs, } from "./extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "./extensions/Blocks/api/styles"; import { TableExtension } from "./extensions/Blocks/nodes/BlockContent/TableBlockContent/TableExtension"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension"; @@ -35,10 +37,14 @@ import UniqueID from "./extensions/UniqueID/UniqueID"; /** * Get all the Tiptap extensions BlockNote is configured with by default */ -export const getBlockNoteExtensions = (opts: { - editor: BlockNoteEditor; +export const getBlockNoteExtensions = < + BSchema extends BlockSchema, + S extends StyleSchema +>(opts: { + editor: BlockNoteEditor; domAttributes: Partial; blockSchema: BSchema; + blockSpecs: BlockSpecs; collaboration?: { fragment: Y.XmlFragment; user: { @@ -89,13 +95,14 @@ export const getBlockNoteExtensions = (opts: { // nodes Doc, BlockContainer.configure({ + editor: opts.editor as any, domAttributes: opts.domAttributes, }), BlockGroup.configure({ domAttributes: opts.domAttributes, }), TableExtension, - ...Object.values(opts.blockSchema).flatMap((blockSpec) => { + ...Object.values(opts.blockSpecs).flatMap((blockSpec) => { return [ // dependent nodes (e.g.: tablecell / row) ...(blockSpec.implementation.requiredNodes || []).map((node) => diff --git a/packages/core/src/api/blockManipulation/blockManipulation.test.ts b/packages/core/src/api/blockManipulation/blockManipulation.test.ts index cc6733d5d2..35cfb559f7 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.test.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.test.ts @@ -21,7 +21,7 @@ let multipleBlocks: PartialBlock[]; let insert: (placement: "before" | "nested" | "after") => Block[]; beforeEach(() => { - editor = new BlockNoteEditor(); + editor = BlockNoteEditor.create(); singleBlock = { type: "paragraph", diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index 3b763f85aa..3b32ba32b8 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -1,30 +1,39 @@ import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; +import { BlockNoteEditor } from "../.."; import { BlockIdentifier, BlockSchema, PartialBlock, } from "../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../extensions/Blocks/api/styles"; import { blockToNode } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../util/nodeUtil"; -export function insertBlocks( - blocksToInsert: PartialBlock[], +export function insertBlocks< + BSchema extends BlockSchema, + S extends StyleSchema +>( + blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before", - editor: Editor + editor: BlockNoteEditor ): void { + const ttEditor = editor._tiptapEditor; + const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; const nodesToInsert: Node[] = []; for (const blockSpec of blocksToInsert) { - nodesToInsert.push(blockToNode(blockSpec, editor.schema)); + nodesToInsert.push( + blockToNode(blockSpec, ttEditor.schema, editor.styleSchema) + ); } let insertionPos = -1; - const { node, posBeforeNode } = getNodeById(id, editor.state.doc); + const { node, posBeforeNode } = getNodeById(id, ttEditor.state.doc); if (placement === "before") { insertionPos = posBeforeNode; @@ -39,13 +48,13 @@ export function insertBlocks( if (node.childCount < 2) { insertionPos = posBeforeNode + node.firstChild!.nodeSize + 1; - const blockGroupNode = editor.state.schema.nodes["blockGroup"].create( + const blockGroupNode = ttEditor.state.schema.nodes["blockGroup"].create( {}, nodesToInsert ); - editor.view.dispatch( - editor.state.tr.insert(insertionPos, blockGroupNode) + ttEditor.view.dispatch( + ttEditor.state.tr.insert(insertionPos, blockGroupNode) ); return; @@ -54,12 +63,12 @@ export function insertBlocks( insertionPos = posBeforeNode + node.firstChild!.nodeSize + 2; } - editor.view.dispatch(editor.state.tr.insert(insertionPos, nodesToInsert)); + ttEditor.view.dispatch(ttEditor.state.tr.insert(insertionPos, nodesToInsert)); } -export function updateBlock( +export function updateBlock( blockToUpdate: BlockIdentifier, - update: PartialBlock, + update: PartialBlock, editor: Editor ) { const id = @@ -116,11 +125,14 @@ export function removeBlocks( } } -export function replaceBlocks( +export function replaceBlocks< + BSchema extends BlockSchema, + S extends StyleSchema +>( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[], - editor: Editor + blocksToInsert: PartialBlock[], + editor: BlockNoteEditor ) { insertBlocks(blocksToInsert, blocksToRemove[0], "before", editor); - removeBlocks(blocksToRemove, editor); + removeBlocks(blocksToRemove, editor._tiptapEditor); } diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index 0207f27054..666308701a 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -3,7 +3,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor, PartialBlock } from "../.."; import { DefaultBlockSchema, + DefaultStyleSchema, defaultBlockSchema, + defaultStyleSchema, } from "../../extensions/Blocks/api/defaultBlocks"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { blockToNode, nodeToBlock } from "./nodeConversions"; @@ -13,7 +15,7 @@ let editor: BlockNoteEditor; let tt: Editor; beforeEach(() => { - editor = new BlockNoteEditor(); + editor = BlockNoteEditor.create(); tt = editor._tiptapEditor; }); @@ -28,10 +30,10 @@ describe("Simple ProseMirror Node Conversions", () => { const block: PartialBlock = { type: "paragraph", }; - const firstNodeConversion = blockToNode( - block, - tt.schema - ); + const firstNodeConversion = blockToNode< + DefaultBlockSchema, + DefaultStyleSchema + >(block, tt.schema, defaultStyleSchema); expect(firstNodeConversion).toMatchSnapshot(); }); @@ -41,14 +43,18 @@ describe("Simple ProseMirror Node Conversions", () => { { id: UniqueID.options.generateID() }, tt.schema.nodes["paragraph"].create() ); - const firstBlockConversion = nodeToBlock(node, defaultBlockSchema); + const firstBlockConversion = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode( - firstBlockConversion, - tt.schema - ); + const firstNodeConversion = blockToNode< + DefaultBlockSchema, + DefaultStyleSchema + >(firstBlockConversion, tt.schema, defaultStyleSchema); expect(firstNodeConversion).toStrictEqual(node); }); @@ -96,10 +102,10 @@ describe("Complex ProseMirror Node Conversions", () => { }, ], }; - const firstNodeConversion = blockToNode( - block, - tt.schema - ); + const firstNodeConversion = blockToNode< + DefaultBlockSchema, + DefaultStyleSchema + >(block, tt.schema, defaultStyleSchema); expect(firstNodeConversion).toMatchSnapshot(); }); @@ -142,14 +148,18 @@ describe("Complex ProseMirror Node Conversions", () => { ]), ] ); - const firstBlockConversion = nodeToBlock(node, defaultBlockSchema); + const firstBlockConversion = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode( - firstBlockConversion, - tt.schema - ); + const firstNodeConversion = blockToNode< + DefaultBlockSchema, + DefaultStyleSchema + >(firstBlockConversion, tt.schema, defaultStyleSchema); expect(firstNodeConversion).toStrictEqual(node); }); @@ -168,9 +178,17 @@ describe("links", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -206,9 +224,17 @@ describe("links", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); // expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -237,9 +263,17 @@ describe("links", () => { ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -264,9 +298,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -289,9 +331,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -314,9 +364,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -339,9 +397,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -364,9 +430,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -394,9 +468,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -419,9 +501,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, @@ -449,9 +539,17 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema); + const node = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema); + const outputBlock = nodeToBlock( + node, + defaultBlockSchema, + defaultStyleSchema + ); // Temporary fix to set props to {}, because at this point // we don't have an easy way to access default props at runtime, diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 2974824ebd..84781be8df 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -2,47 +2,44 @@ import { Mark } from "@tiptap/pm/model"; import { Node, Schema } from "prosemirror-model"; import { Block, - BlockConfig, BlockSchema, - BlockSpec, PartialBlock, PartialTableContent, TableContent, } from "../../extensions/Blocks/api/blockTypes"; import { - ColorStyle, InlineContent, PartialInlineContent, PartialLink, StyledText, - Styles, - ToggledStyle, } from "../../extensions/Blocks/api/inlineContentTypes"; +import { StyleSchema, Styles } from "../../extensions/Blocks/api/styles"; import { getBlockInfo } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { UnreachableCaseError } from "../../shared/utils"; -const toggleStyles = new Set([ - "bold", - "italic", - "underline", - "strike", - "code", -]); -const colorStyles = new Set(["textColor", "backgroundColor"]); - /** * Convert a StyledText inline element to a * prosemirror text node with the appropriate marks */ -function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] { +function styledTextToNodes( + styledText: StyledText, + schema: Schema, + styleSchema: T +): Node[] { const marks: Mark[] = []; for (const [style, value] of Object.entries(styledText.styles)) { - if (toggleStyles.has(style as ToggledStyle)) { + const config = styleSchema[style]; + if (!config) { + throw new Error(`style ${style} not found in styleSchema`); + } + if (config.propSchema === "boolean") { marks.push(schema.mark(style)); - } else if (colorStyles.has(style as ColorStyle)) { - marks.push(schema.mark(style, { color: value })); + } else if (config.propSchema === "string") { + marks.push(schema.mark(style, { stringValue: value })); + } else { + throw new UnreachableCaseError(config.propSchema); } } @@ -68,42 +65,53 @@ function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] { * Converts a Link inline content element to * prosemirror text nodes with the appropriate marks */ -function linkToNodes(link: PartialLink, schema: Schema): Node[] { +function linkToNodes( + link: PartialLink, + schema: Schema, + styleSchema: StyleSchema +): Node[] { const linkMark = schema.marks.link.create({ href: link.href, }); - return styledTextArrayToNodes(link.content, schema).map((node) => { - if (node.type.name === "text") { - return node.mark([...node.marks, linkMark]); - } + return styledTextArrayToNodes(link.content, schema, styleSchema).map( + (node) => { + if (node.type.name === "text") { + return node.mark([...node.marks, linkMark]); + } - if (node.type.name === "hardBreak") { - return node; + if (node.type.name === "hardBreak") { + return node; + } + throw new Error("unexpected node type"); } - throw new Error("unexpected node type"); - }); + ); } /** * Converts an array of StyledText inline content elements to * prosemirror text nodes with the appropriate marks */ -function styledTextArrayToNodes( - content: string | StyledText[], - schema: Schema +function styledTextArrayToNodes( + content: string | StyledText[], + schema: Schema, + styleSchema: S ): Node[] { const nodes: Node[] = []; if (typeof content === "string") { nodes.push( - ...styledTextToNodes({ type: "text", text: content, styles: {} }, schema) + ...styledTextToNodes( + { type: "text", text: content, styles: {} }, + schema, + styleSchema + ) ); return nodes; } for (const styledText of content) { - nodes.push(...styledTextToNodes(styledText, schema)); + nodes.push(...styledTextToNodes(styledText, schema, styleSchema)); } return nodes; } @@ -111,17 +119,18 @@ function styledTextArrayToNodes( /** * converts an array of inline content elements to prosemirror nodes */ -export function inlineContentToNodes( - blockContent: PartialInlineContent[], - schema: Schema +export function inlineContentToNodes( + blockContent: PartialInlineContent[], + schema: Schema, + styleSchema: S ): Node[] { const nodes: Node[] = []; for (const content of blockContent) { if (content.type === "link") { - nodes.push(...linkToNodes(content, schema)); + nodes.push(...linkToNodes(content, schema, styleSchema)); } else if (content.type === "text") { - nodes.push(...styledTextArrayToNodes([content], schema)); + nodes.push(...styledTextArrayToNodes([content], schema, styleSchema)); } else { throw new UnreachableCaseError(content); } @@ -132,9 +141,10 @@ export function inlineContentToNodes( /** * converts an array of inline content elements to prosemirror nodes */ -export function tableContentToNodes( - tableContent: PartialTableContent, - schema: Schema +export function tableContentToNodes( + tableContent: PartialTableContent, + schema: Schema, + styleSchema: StyleSchema ): Node[] { const rowNodes: Node[] = []; @@ -147,7 +157,7 @@ export function tableContentToNodes( } else if (typeof cell === "string") { pNode = schema.nodes["tableParagraph"].create({}, schema.text(cell)); } else { - const textNodes = inlineContentToNodes(cell, schema); + const textNodes = inlineContentToNodes(cell, schema, styleSchema); pNode = schema.nodes["tableParagraph"].create({}, textNodes); } @@ -163,9 +173,10 @@ export function tableContentToNodes( /** * Converts a BlockNote block to a TipTap node. */ -export function blockToNode( +export function blockToNode( block: PartialBlock, - schema: Schema + schema: Schema, + styleSchema: S ) { let id = block.id; @@ -189,10 +200,10 @@ export function blockToNode( schema.text(block.content) ); } else if (Array.isArray(block.content)) { - const nodes = inlineContentToNodes(block.content, schema); + const nodes = inlineContentToNodes(block.content, schema, styleSchema); contentNode = schema.nodes[type].create(block.props, nodes); } else if (block.content.type === "tableContent") { - const nodes = tableContentToNodes(block.content, schema); + const nodes = tableContentToNodes(block.content, schema, styleSchema); contentNode = schema.nodes[type].create(block.props, nodes); } else { throw new UnreachableCaseError(block.content.type); @@ -202,7 +213,7 @@ export function blockToNode( if (block.children) { for (const child of block.children) { - children.push(blockToNode(child, schema)); + children.push(blockToNode(child, schema, styleSchema)); } } @@ -220,19 +231,24 @@ export function blockToNode( /** * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent */ -function contentNodeToTableContent(contentNode: Node) { - const ret: TableContent = { +function contentNodeToTableContent( + contentNode: Node, + styleSchema: S +) { + const ret: TableContent = { type: "tableContent", rows: [], }; contentNode.content.forEach((rowNode) => { - const row: TableContent["rows"][0] = { + const row: TableContent["rows"][0] = { cells: [], }; rowNode.content.forEach((cellNode) => { - row.cells.push(contentNodeToInlineContent(cellNode.firstChild!)); + row.cells.push( + contentNodeToInlineContent(cellNode.firstChild!, styleSchema) + ); }); ret.rows.push(row); @@ -244,9 +260,12 @@ function contentNodeToTableContent(contentNode: Node) { /** * Converts an internal (prosemirror) content node to a BlockNote InlineContent array. */ -function contentNodeToInlineContent(contentNode: Node) { - const content: InlineContent[] = []; - let currentContent: InlineContent | undefined = undefined; +function contentNodeToInlineContent( + contentNode: Node, + styleSchema: S +) { + const content: InlineContent[] = []; + let currentContent: InlineContent | undefined = undefined; // Most of the logic below is for handling links because in ProseMirror links are marks // while in BlockNote links are a type of inline content @@ -276,18 +295,24 @@ function contentNodeToInlineContent(contentNode: Node) { return; } - const styles: Styles = {}; + const styles: Styles = {}; let linkMark: Mark | undefined; for (const mark of node.marks) { if (mark.type.name === "link") { linkMark = mark; - } else if (toggleStyles.has(mark.type.name as ToggledStyle)) { - styles[mark.type.name as ToggledStyle] = true; - } else if (colorStyles.has(mark.type.name as ColorStyle)) { - styles[mark.type.name as ColorStyle] = mark.attrs.color; } else { - throw Error("Mark is of an unrecognized type: " + mark.type.name); + const config = styleSchema[mark.type.name]; + if (!config) { + throw new Error(`style ${mark.type.name} not found in styleSchema`); + } + if (config.propSchema === "boolean") { + (styles as any)[config.type] = true; + } else if (config.propSchema === "string") { + (styles as any)[config.type] = mark.attrs.stringValue; + } else { + throw new UnreachableCaseError(config.propSchema); + } } } @@ -412,11 +437,12 @@ function contentNodeToInlineContent(contentNode: Node) { /** * Convert a TipTap node to a BlockNote block. */ -export function nodeToBlock( +export function nodeToBlock( node: Node, blockSchema: BSchema, - blockCache?: WeakMap> -): Block { + styleSchema: S, + blockCache?: WeakMap> +): Block { if (node.type.name !== "blockContainer") { throw Error( "Node must be of type blockContainer, but is of type" + @@ -445,9 +471,7 @@ export function nodeToBlock( ...node.attrs, ...blockInfo.contentNode.attrs, })) { - const blockSpec = blockSchema[ - blockInfo.contentType.name - ] as BlockSpec; // TODO: fix cast + const blockSpec = blockSchema[blockInfo.contentType.name]; if (!blockSpec) { throw Error( @@ -455,43 +479,46 @@ export function nodeToBlock( ); } - const propSchema = blockSpec.config.propSchema; + const propSchema = blockSpec.propSchema; if (attr in propSchema) { props[attr] = value; } } - const blockSpec = blockSchema[ - blockInfo.contentType.name - ] as BlockSpec; // TODO: fix cast + const blockConfig = blockSchema[blockInfo.contentType.name]; - const children: Block[] = []; + const children: Block[] = []; for (let i = 0; i < blockInfo.numChildBlocks; i++) { children.push( - nodeToBlock(node.lastChild!.child(i), blockSchema, blockCache) + nodeToBlock( + node.lastChild!.child(i), + blockSchema, + styleSchema, + blockCache + ) ); } let content: Block["content"]; - if (blockSpec.config.content === "inline") { - content = contentNodeToInlineContent(blockInfo.contentNode); - } else if (blockSpec.config.content === "table") { - content = contentNodeToTableContent(blockInfo.contentNode); - } else if (blockSpec.config.content === "none") { + if (blockConfig.content === "inline") { + content = contentNodeToInlineContent(blockInfo.contentNode, styleSchema); + } else if (blockConfig.content === "table") { + content = contentNodeToTableContent(blockInfo.contentNode, styleSchema); + } else if (blockConfig.content === "none") { content = undefined; } else { - throw new UnreachableCaseError(blockSpec.config.content); + throw new UnreachableCaseError(blockConfig.content); } const block = { id, - type: blockSpec.config.type, + type: blockConfig.type, props, content, children, - } as Block; + } as Block; blockCache?.set(node, block); diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index 490f989958..40a7769906 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -9,10 +9,11 @@ import { PartialInlineContent, StyledText, } from "../../extensions/Blocks/api/inlineContentTypes"; +import { StyleSchema } from "../../extensions/Blocks/api/styles"; function textShorthandToStyledText( - content: string | StyledText[] = "" -): StyledText[] { + content: string | StyledText[] = "" +): StyledText[] { if (typeof content === "string") { return [ { @@ -26,8 +27,8 @@ function textShorthandToStyledText( } function partialContentToInlineContent( - content: string | PartialInlineContent[] | TableContent = "" -): InlineContent[] | TableContent { + content: string | PartialInlineContent[] | TableContent = "" +): InlineContent[] | TableContent { if (typeof content === "string") { return textShorthandToStyledText(content); } @@ -48,10 +49,11 @@ function partialContentToInlineContent( return content; } -export function partialBlockToBlockForTesting( - partialBlock: PartialBlock -): Block { - const withDefaults: Block = { +export function partialBlockToBlockForTesting< + BSchema extends BlockSchema, + S extends StyleSchema +>(partialBlock: PartialBlock): Block { + const withDefaults: Block = { id: "", type: "paragraph", // because at this point we don't have an easy way to access default props at runtime, @@ -65,6 +67,8 @@ export function partialBlockToBlockForTesting( return { ...withDefaults, content: partialContentToInlineContent(withDefaults.content), - children: withDefaults.children.map(partialBlockToBlockForTesting), + children: withDefaults.children.map((c) => { + return partialBlockToBlockForTesting(c); + }), } as any; } diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/serialization/clipboardHandlerExtension.ts index ef19fef292..a4f3ec68c7 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/serialization/clipboardHandlerExtension.ts @@ -1,10 +1,11 @@ -import { BlockSchema } from "../../extensions/Blocks/api/blockTypes"; -import { BlockNoteEditor } from "../../BlockNoteEditor"; import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; -import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; -import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { BlockSchema } from "../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../extensions/Blocks/api/styles"; import { markdown } from "../formatConversions/formatConversions"; +import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; +import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; const acceptedMIMETypes = [ "blocknote/html", @@ -12,10 +13,13 @@ const acceptedMIMETypes = [ "text/plain", ] as const; -export const createClipboardHandlerExtension = ( - editor: BlockNoteEditor +export const createClipboardHandlerExtension = < + BSchema extends BlockSchema, + SSchema extends StyleSchema +>( + editor: BlockNoteEditor ) => - Extension.create<{ editor: BlockNoteEditor }, undefined>({ + Extension.create<{ editor: BlockNoteEditor }, undefined>({ addProseMirrorPlugins() { const tiptap = this.editor; const schema = this.editor.schema; diff --git a/packages/core/src/api/serialization/html/externalHTMLExporter.ts b/packages/core/src/api/serialization/html/externalHTMLExporter.ts index 9cbb149634..f6c591d995 100644 --- a/packages/core/src/api/serialization/html/externalHTMLExporter.ts +++ b/packages/core/src/api/serialization/html/externalHTMLExporter.ts @@ -1,14 +1,15 @@ import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model"; -import { blockToNode } from "../../nodeConversions/nodeConversions"; +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { unified } from "unified"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, PartialBlock, } from "../../../extensions/Blocks/api/blockTypes"; -import { unified } from "unified"; -import rehypeParse from "rehype-parse"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles"; import { simplifyBlocks } from "../../formatConversions/simplifyBlocksRehypePlugin"; -import rehypeStringify from "rehype-stringify"; +import { blockToNode } from "../../nodeConversions/nodeConversions"; import { serializeNodeInner, serializeProseMirrorFragment, @@ -33,15 +34,21 @@ import { // `exportFragment`: Exports a ProseMirror fragment to HTML. This is mostly // useful if you want to export a selection which may not start/end at the // start/end of a block. -export interface ExternalHTMLExporter { - exportBlocks: (blocks: PartialBlock[]) => string; +export interface ExternalHTMLExporter< + BSchema extends BlockSchema, + S extends StyleSchema +> { + exportBlocks: (blocks: PartialBlock[]) => string; exportProseMirrorFragment: (fragment: Fragment) => string; } -export const createExternalHTMLExporter = ( +export const createExternalHTMLExporter = < + BSchema extends BlockSchema, + S extends StyleSchema +>( schema: Schema, - editor: BlockNoteEditor -): ExternalHTMLExporter => { + editor: BlockNoteEditor +): ExternalHTMLExporter => { const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { serializeNodeInner: ( node: Node, @@ -50,7 +57,7 @@ export const createExternalHTMLExporter = ( // TODO: Should not be async, but is since we're using a rehype plugin to // convert internal HTML to external HTML. exportProseMirrorFragment: (fragment: Fragment) => string; - exportBlocks: (blocks: PartialBlock[]) => string; + exportBlocks: (blocks: PartialBlock[]) => string; }; serializer.serializeNodeInner = ( @@ -74,8 +81,10 @@ export const createExternalHTMLExporter = ( return externalHTML.value as string; }; - serializer.exportBlocks = (blocks: PartialBlock[]) => { - const nodes = blocks.map((block) => blockToNode(block, schema)); + serializer.exportBlocks = (blocks: PartialBlock[]) => { + const nodes = blocks.map((block) => + blockToNode(block, schema, editor.styleSchema) + ); const blockGroup = schema.nodes["blockGroup"].create(null, nodes); return serializer.exportProseMirrorFragment(Fragment.from(blockGroup)); diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index d5cf5f4882..97a1385ec2 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -3,11 +3,12 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { - BlockSchema, + BlockSchemaFromSpecs, + BlockSpecs, PartialBlock, } from "../../../extensions/Blocks/api/blockTypes"; import { createBlockSpec } from "../../../extensions/Blocks/api/customBlocks"; -import { defaultBlockSchema } from "../../../extensions/Blocks/api/defaultBlocks"; +import { defaultBlockSpecs } from "../../../extensions/Blocks/api/defaultBlocks"; import { defaultProps } from "../../../extensions/Blocks/api/defaultProps"; import { imagePropSchema, @@ -76,19 +77,21 @@ const SimpleCustomParagraph = createBlockSpec( } ); -const customSchema = { - ...defaultBlockSchema, +const customSpecs = { + ...defaultBlockSpecs, simpleImage: SimpleImage, customParagraph: CustomParagraph, simpleCustomParagraph: SimpleCustomParagraph, -} satisfies BlockSchema; +} satisfies BlockSpecs; -let editor: BlockNoteEditor; +type CustomSchemaType = BlockSchemaFromSpecs; + +let editor: BlockNoteEditor; let tt: Editor; beforeEach(() => { - editor = new BlockNoteEditor({ - blockSchema: customSchema, + editor = BlockNoteEditor.create({ + blockSpecs: customSpecs, uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, }); tt = editor._tiptapEditor; @@ -103,7 +106,7 @@ afterEach(() => { }); function convertToHTMLAndCompareSnapshots( - blocks: PartialBlock[], + blocks: PartialBlock[], snapshotDirectory: string, snapshotName: string ) { @@ -130,7 +133,7 @@ function convertToHTMLAndCompareSnapshots( describe("Convert paragraphs to HTML", () => { it("Convert paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "paragraph", content: "Paragraph", @@ -141,7 +144,7 @@ describe("Convert paragraphs to HTML", () => { }); it("Convert styled paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "paragraph", props: { @@ -185,7 +188,7 @@ describe("Convert paragraphs to HTML", () => { }); it("Convert nested paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "paragraph", content: "Paragraph", @@ -208,7 +211,7 @@ describe("Convert paragraphs to HTML", () => { describe("Convert images to HTML", () => { it("Convert add image button to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "image", }, @@ -218,7 +221,7 @@ describe("Convert images to HTML", () => { }); it("Convert image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "image", props: { @@ -233,7 +236,7 @@ describe("Convert images to HTML", () => { }); it("Convert nested image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "image", props: { @@ -260,7 +263,7 @@ describe("Convert images to HTML", () => { describe("Convert simple images to HTML", () => { it("Convert simple add image button to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleImage", }, @@ -270,7 +273,7 @@ describe("Convert simple images to HTML", () => { }); it("Convert simple image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleImage", props: { @@ -285,7 +288,7 @@ describe("Convert simple images to HTML", () => { }); it("Convert nested image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleImage", props: { @@ -312,7 +315,7 @@ describe("Convert simple images to HTML", () => { describe("Convert custom blocks with inline content to HTML", () => { it("Convert custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "customParagraph", content: "Custom Paragraph", @@ -323,7 +326,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert styled custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "customParagraph", props: { @@ -367,7 +370,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert nested block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "customParagraph", content: "Custom Paragraph", @@ -390,7 +393,7 @@ describe("Convert custom blocks with inline content to HTML", () => { describe("Convert custom blocks with non-exported inline content to HTML", () => { it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleCustomParagraph", content: "Custom Paragraph", @@ -401,7 +404,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleCustomParagraph", props: { @@ -445,7 +448,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock[] = [ { type: "simpleCustomParagraph", content: "Custom Paragraph", diff --git a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts index 77ed002d23..35e6a87b0f 100644 --- a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts @@ -4,11 +4,12 @@ import { BlockSchema, PartialBlock, } from "../../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +import { blockToNode } from "../../nodeConversions/nodeConversions"; import { serializeNodeInner, serializeProseMirrorFragment, } from "./sharedHTMLConversion"; -import { blockToNode } from "../../nodeConversions/nodeConversions"; // Used to serialize BlockNote blocks and ProseMirror nodes to HTML without // losing data. Blocks are exported using the `toInternalHTML` method in their @@ -25,17 +26,23 @@ import { blockToNode } from "../../nodeConversions/nodeConversions"; // mostly useful if you want to serialize a selection which may not start/end at // the start/end of a block. // `serializeBlocks`: Serializes an array of blocks to HTML. -export interface InternalHTMLSerializer { +export interface InternalHTMLSerializer< + BSchema extends BlockSchema, + S extends StyleSchema +> { // TODO: Ideally we would expand the BlockNote API to support partial // selections so we don't need this. serializeProseMirrorFragment: (fragment: Fragment) => string; - serializeBlocks: (blocks: PartialBlock[]) => string; + serializeBlocks: (blocks: PartialBlock[]) => string; } -export const createInternalHTMLSerializer = ( +export const createInternalHTMLSerializer = < + BSchema extends BlockSchema, + S extends StyleSchema +>( schema: Schema, - editor: BlockNoteEditor -): InternalHTMLSerializer => { + editor: BlockNoteEditor +): InternalHTMLSerializer => { const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { serializeNodeInner: ( node: Node, @@ -58,7 +65,9 @@ export const createInternalHTMLSerializer = ( serializeProseMirrorFragment(fragment, serializer); serializer.serializeBlocks = (blocks: PartialBlock[]) => { - const nodes = blocks.map((block) => blockToNode(block, schema)); + const nodes = blocks.map((block) => + blockToNode(block, schema, editor.styleSchema) + ); const blockGroup = schema.nodes["blockGroup"].create(null, nodes); return serializer.serializeProseMirrorFragment(Fragment.from(blockGroup)); diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index dfe1216b68..c5c1928c2d 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -1,6 +1,7 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles"; import { nodeToBlock } from "../../nodeConversions/nodeConversions"; function doc(options: { document?: Document }) { @@ -13,11 +14,14 @@ function doc(options: { document?: Document }) { // `blockContent` node, the `toInternalHTML` or `toExternalHTML` function of its // corresponding block is used for serialization instead of the node's // `renderHTML` method. -export const serializeNodeInner = ( +export const serializeNodeInner = < + BSchema extends BlockSchema, + S extends StyleSchema +>( node: Node, options: { document?: Document }, serializer: DOMSerializer, - editor: BlockNoteEditor, + editor: BlockNoteEditor, toExternalHTML: boolean ) => { const { dom, contentDOM } = DOMSerializer.renderSpec( @@ -34,14 +38,20 @@ export const serializeNodeInner = ( if (node.type.name === "blockContainer") { // Converts `blockContent` node using the custom `blockSpec`'s // `toExternalHTML` or `toInternalHTML` function. - const blockSpec = - editor.schema[node.firstChild!.type.name as keyof BSchema]; + const blockImpl = + editor.blockImplementations[node.firstChild!.type.name as string] + .implementation; const toHTML = toExternalHTML - ? blockSpec.implementation.toExternalHTML - : blockSpec.implementation.toInternalHTML; + ? blockImpl.toExternalHTML + : blockImpl.toInternalHTML; const blockContent = toHTML( - nodeToBlock(node, editor.schema, editor.blockCache), + nodeToBlock( + node, + editor.blockSchema, + editor.styleSchema, + editor.blockCache + ), editor as any ); diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 75a045a39c..3b4a5793a8 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -13,6 +13,7 @@ import { TiptapBlockImplementation, } from "./blockTypes"; import { inheritedProps } from "./defaultProps"; +import { StyleSchema } from "./styles"; export function camelToDataKebab(str: string): string { return "data-" + str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); @@ -127,10 +128,11 @@ export function parse(blockConfig: BlockConfig) { export function getBlockFromPos< BType extends string, Config extends BlockConfig, - BSchema extends BlockSchemaWithBlock + BSchema extends BlockSchemaWithBlock, + S extends StyleSchema >( getPos: (() => number) | boolean, - editor: BlockNoteEditor, + editor: BlockNoteEditor, tipTapEditor: Editor, type: BType ) { @@ -148,7 +150,8 @@ export function getBlockFromPos< // Gets the block const block = editor.getBlock(blockIdentifier)! as SpecificBlock< BSchema, - BType + BType, + S >; if (block.type !== type) { throw new Error("Block type does not match"); @@ -238,12 +241,12 @@ export function createStronglyTypedTiptapNode< // config and implementation that conform to the type of Config export function createInternalBlockSpec( config: T, - implementation: TiptapBlockImplementation + implementation: TiptapBlockImplementation ) { return { config, implementation, - } satisfies BlockSpec; + } satisfies BlockSpec; } export function createBlockSpecFromStronglyTypedTiptapNode< diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index 9042d348e7..6c212f86a6 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -1,7 +1,12 @@ /** Define the main block types **/ import { Node } from "@tiptap/core"; -import { BlockNoteEditor, DefaultBlockSchema } from "../../.."; +import { + BlockNoteEditor, + DefaultBlockSchema, + DefaultStyleSchema, +} from "../../.."; import { InlineContent, PartialInlineContent } from "./inlineContentTypes"; +import { StyleSchema } from "./styles"; export type BlockNoteDOMElement = | "editor" @@ -56,19 +61,27 @@ export type BlockConfig = { // Block implementation contains the "implementation" info about a Block // such as the functions / Nodes required to render and / or serialize it -export type TiptapBlockImplementation = { +export type TiptapBlockImplementation< + T extends BlockConfig, + B extends BlockSchema, + S extends StyleSchema +> = { requiredNodes?: Node[]; node: Node; toInternalHTML: ( - block: Block, - editor: BlockNoteEditor> + block: BlockFromConfigNoChildren & { + children: Block[]; + }, + editor: BlockNoteEditor ) => { dom: HTMLElement; contentDOM?: HTMLElement; }; toExternalHTML: ( - block: Block, - editor: BlockNoteEditor> + block: BlockFromConfigNoChildren & { + children: Block[]; + }, + editor: BlockNoteEditor ) => { dom: HTMLElement; contentDOM?: HTMLElement; @@ -77,130 +90,160 @@ export type TiptapBlockImplementation = { // Container for both the config and implementation of a block, // and the type of BlockImplementation is based on that of the config -export type BlockSpec = { +export type BlockSpec< + T extends BlockConfig, + B extends BlockSchema, + S extends StyleSchema +> = { config: T; - implementation: TiptapBlockImplementation; + implementation: TiptapBlockImplementation; }; // Utility type. For a given object block schema, ensures that the key of each // block spec matches the name of the TipTap node in it. -type NamesMatch>> = - Blocks extends { - [Type in keyof Blocks]: Type extends string - ? Blocks[Type]["config"] extends { type: Type } - ? Blocks[Type] - : never - : never; - } - ? Blocks +type NamesMatch> = Blocks extends { + [Type in keyof Blocks]: Type extends string + ? Blocks[Type] extends { type: Type } + ? Blocks[Type] + : never : never; +} + ? Blocks + : never; // Defines multiple block specs. Also ensures that the key of each block schema // is the same as name of the TipTap node in it. This should be passed in the // `blocks` option of the BlockNoteEditor. From a block schema, we can derive // both the blocks' internal implementation (as TipTap nodes) and the type // information for the external API. -export type BlockSchema = NamesMatch>>; +export type BlockSchema = NamesMatch>; + +export type BlockSpecs = Record>; + +export type BlockImplementations = Record< + string, + TiptapBlockImplementation +>; + +export type BlockSchemaFromSpecs = { + [K in keyof T]: T[K]["config"]; +}; export type BlockSchemaWithBlock< BType extends string, C extends BlockConfig > = { - [k in BType]: BlockSpec; + [k in BType]: C; }; -export type TableContent = { +export type TableContent = { type: "tableContent"; rows: { - cells: InlineContent[][]; + cells: InlineContent[][]; }[]; }; -export type PartialTableContent = { +export type PartialTableContent = { type: "tableContent"; rows: { - cells: (PartialInlineContent[] | string)[]; + cells: (PartialInlineContent[] | string)[]; }[]; }; // 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 BlockFromBlockConfig is the shape of a specific paragraph block. +// i.e.: paragraphConfig: BlockConfig defines what a "paragraph" is / supports, and BlockFromConfigNoChildren is the shape of a specific paragraph block. // (for internal use) -type BlockFromBlockConfig = { +type BlockFromConfigNoChildren = { id: string; type: B["type"]; props: Props; content: B["content"] extends "inline" - ? InlineContent[] + ? InlineContent[] : B["content"] extends "table" - ? TableContent + ? TableContent : B["content"] extends "none" ? undefined : never; }; +export type BlockFromConfig< + B extends BlockConfig, + S extends StyleSchema +> = BlockFromConfigNoChildren & { + children: Block[]; +}; + // Converts each block spec into a Block object without children. We later merge // them into a union type and add a children property to create the Block and // PartialBlock objects we use in the external API. -type BlocksWithoutChildren = { - [BType in keyof BSchema]: BlockFromBlockConfig; +type BlocksWithoutChildren< + BSchema extends BlockSchema, + S extends StyleSchema +> = { + [BType in keyof BSchema]: BlockFromConfigNoChildren; }; // Converts each block spec into a Block object without children, merges them // into a union type, and adds a children property -export type Block = - (T extends BlockSchema - ? BlocksWithoutChildren[keyof T] - : T extends BlockConfig - ? BlockFromBlockConfig - : never) & { - children: Block< - T extends BlockSchema ? T : any // any should probably be BlockSchemaWithBlock; - >[]; - }; +export type Block< + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema +> = BlocksWithoutChildren[keyof BSchema] & { + children: Block[]; +}; export type SpecificBlock< BSchema extends BlockSchema, - BType extends keyof BSchema -> = BlocksWithoutChildren[BType] & { - children: Block[]; + BType extends keyof BSchema, + S extends StyleSchema +> = BlocksWithoutChildren[BType] & { + children: Block[]; }; /** CODE FOR PARTIAL BLOCKS, analogous to above */ -type PartialBlockFromBlockConfig = { +type PartialBlockFromConfigNoChildren< + B extends BlockConfig, + S extends StyleSchema +> = { id?: string; type?: B["type"]; props?: Partial>; content?: B["content"] extends "inline" - ? PartialInlineContent[] | string + ? PartialInlineContent[] | string : B["content"] extends "table" - ? PartialTableContent + ? PartialTableContent : undefined; }; // Same as BlockWithoutChildren, but as a partial type with some changes to make // it easier to create/update blocks in the editor. -type PartialBlocksWithoutChildren = { - [BType in keyof BSchema]: PartialBlockFromBlockConfig< - BSchema[BType]["config"] - >; +type PartialBlocksWithoutChildren< + BSchema extends BlockSchema, + S extends StyleSchema +> = { + [BType in keyof BSchema]: PartialBlockFromConfigNoChildren; }; // Same as Block, but as a partial type with some changes to make it easier to // create/update blocks in the editor. -export type PartialBlock = - PartialBlocksWithoutChildren[keyof PartialBlocksWithoutChildren] & - Partial<{ - children: PartialBlock[]; - }>; +export type PartialBlock< + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema +> = PartialBlocksWithoutChildren[keyof PartialBlocksWithoutChildren< + BSchema, + S +>] & + Partial<{ + children: PartialBlock[]; + }>; // export type PartialBlock = // T extends BlockSchema // ? PartialBlocksWithoutChildren[keyof T] // : T extends BlockConfig -// ? PartialBlockFromBlockConfig +// ? PartialBlockFromConfigNoChildren // : never; // & { @@ -211,9 +254,15 @@ export type PartialBlock = export type SpecificPartialBlock< BSchema extends BlockSchema, - BType extends keyof BSchema -> = PartialBlocksWithoutChildren[BType] & { - children?: Block[]; + BType extends keyof BSchema, + S extends StyleSchema +> = PartialBlocksWithoutChildren[BType] & { + children?: Block[]; }; export type BlockIdentifier = { id: string } | string; + +export type Schema = { + blocks: B; + styles: S; +}; diff --git a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts index eb17e098f3..23218a9a75 100644 --- a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts +++ b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts @@ -1,7 +1,11 @@ import { Block, BlockSchema } from "./blockTypes"; +import { StyleSchema } from "./styles"; -export type TextCursorPosition = { - block: Block; - prevBlock: Block | undefined; - nextBlock: Block | undefined; +export type TextCursorPosition< + BSchema extends BlockSchema, + S extends StyleSchema +> = { + block: Block; + prevBlock: Block | undefined; + nextBlock: Block | undefined; }; diff --git a/packages/core/src/extensions/Blocks/api/customBlocks.ts b/packages/core/src/extensions/Blocks/api/customBlocks.ts index fcff18a666..93c2e14055 100644 --- a/packages/core/src/extensions/Blocks/api/customBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/customBlocks.ts @@ -7,25 +7,33 @@ import { propsToAttributes, wrapInBlockStructure, } from "./block"; -import { Block, BlockConfig, BlockSchemaWithBlock } from "./blockTypes"; +import { + BlockConfig, + BlockFromConfig, + BlockSchemaWithBlock, +} from "./blockTypes"; +import { StyleSchema } from "./styles"; // restrict content to "inline" and "none" only export type CustomBlockConfig = BlockConfig & { content: "inline" | "none"; }; -export type CustomBlockImplementation = { +export type CustomBlockImplementation< + T extends CustomBlockConfig, + S extends StyleSchema +> = { render: ( /** * The custom block to render */ - block: Block, + block: BlockFromConfig, /** * The BlockNote editor instance * This is typed generically. If you want an editor with your custom schema, you need to * cast it manually, e.g.: `const e = editor as BlockNoteEditor;` */ - editor: BlockNoteEditor> + editor: BlockNoteEditor, S> // (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations // or allow manually passing , but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics ) => { @@ -38,8 +46,8 @@ export type CustomBlockImplementation = { // BlockNote. // TODO: Maybe can return undefined to ignore when serializing? toExternalHTML?: ( - block: Block, - editor: BlockNoteEditor> + block: BlockFromConfig, + editor: BlockNoteEditor, S> ) => { dom: HTMLElement; contentDOM?: HTMLElement; @@ -48,10 +56,10 @@ export type CustomBlockImplementation = { // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead -export function createBlockSpec( - blockConfig: T, - blockImplementation: CustomBlockImplementation -) { +export function createBlockSpec< + T extends CustomBlockConfig, + S extends StyleSchema +>(blockConfig: T, blockImplementation: CustomBlockImplementation) { const node = createStronglyTypedTiptapNode({ name: blockConfig.type as T["type"], content: (blockConfig.content === "inline" diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 41a8c2906e..86a62d6cbe 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -4,15 +4,86 @@ import { BulletListItem } from "../nodes/BlockContent/ListItemBlockContent/Bulle import { NumberedListItem } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { Paragraph } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { Table } from "../nodes/BlockContent/TableBlockContent/TableBlockContent"; -import { BlockSchema } from "./blockTypes"; +import { BlockSchemaFromSpecs, BlockSpecs } from "./blockTypes"; +import { StyleSchemaFromSpecs, StyleSpecs } from "./styles"; -export const defaultBlockSchema = { +export const defaultBlockSpecs = { paragraph: Paragraph, heading: Heading, bulletListItem: BulletListItem, numberedListItem: NumberedListItem, image: Image, table: Table, -} satisfies BlockSchema; +} satisfies BlockSpecs; + +export function getBlockSchemaFromSpecs(specs: T) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as BlockSchemaFromSpecs; +} + +export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); export type DefaultBlockSchema = typeof defaultBlockSchema; + +export const defaultStyleSpecs = { + bold: { + config: { + type: "bold", + propSchema: "boolean", + }, + implementation: {}, + }, + italic: { + config: { + type: "italic", + propSchema: "boolean", + }, + implementation: {}, + }, + underline: { + config: { + type: "underline", + propSchema: "boolean", + }, + implementation: {}, + }, + strike: { + config: { + type: "strike", + propSchema: "boolean", + }, + implementation: {}, + }, + code: { + config: { + type: "code", + propSchema: "boolean", + }, + implementation: {}, + }, + textColor: { + config: { + type: "textColor", + propSchema: "string", + }, + implementation: {}, + }, + backgroundColor: { + config: { + type: "backgroundColor", + propSchema: "string", + }, + implementation: {}, + }, +} satisfies StyleSpecs; + +export function getStyleSchemaFromSpecs(specs: T) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as StyleSchemaFromSpecs; +} + +export const defaultStyleSchema = getStyleSchemaFromSpecs(defaultStyleSpecs); + +export type DefaultStyleSchema = typeof defaultStyleSchema; diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts index 9d63930d95..e83d4b3ea1 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts @@ -1,36 +1,22 @@ -export type Styles = { - bold?: true; - italic?: true; - underline?: true; - strike?: true; - code?: true; - textColor?: string; - backgroundColor?: string; -}; - -export type ToggledStyle = { - [K in keyof Styles]-?: Required[K] extends true ? K : never; -}[keyof Styles]; - -export type ColorStyle = { - [K in keyof Styles]-?: Required[K] extends string ? K : never; -}[keyof Styles]; +import { StyleSchema, Styles } from "./styles"; -export type StyledText = { +export type StyledText = { type: "text"; text: string; - styles: Styles; + styles: Styles; }; -export type Link = { +export type Link = { type: "link"; href: string; - content: StyledText[]; + content: StyledText[]; }; -export type PartialLink = Omit & { - content: string | Link["content"]; +export type PartialLink = Omit, "content"> & { + content: string | Link["content"]; }; -export type InlineContent = StyledText | Link; -export type PartialInlineContent = StyledText | PartialLink; +export type InlineContent = StyledText | Link; +export type PartialInlineContent = + | StyledText + | PartialLink; diff --git a/packages/core/src/extensions/Blocks/api/styles.ts b/packages/core/src/extensions/Blocks/api/styles.ts new file mode 100644 index 0000000000..d03c27a15b --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/styles.ts @@ -0,0 +1,47 @@ +export type StyleConfig = { + type: string; + readonly propSchema: "boolean" | "string"; + // content: "inline" | "none" | "table"; +}; + +// @ts-ignore +export type StyleImplementation = any; + +// Container for both the config and implementation of a block, +// and the type of BlockImplementation is based on that of the config +export type StyleSpec = { + config: T; + implementation: StyleImplementation; +}; + +export type StyleSchema = Record; + +export type StyleSpecs = Record>; + +export type StyleSchemaFromSpecs = { + [K in keyof T]: T[K]["config"]; +}; + +export type Styles = { + [K in keyof T]?: T[K]["propSchema"] extends "boolean" + ? boolean + : T[K]["propSchema"] extends "string" + ? string + : never; +}; + +// bold?: true; +// italic?: true; +// underline?: true; +// strike?: true; +// code?: true; +// textColor?: string; +// backgroundColor?: string; + +// export type ToggledStyle = { +// [K in keyof Styles]-?: Required[K] extends true ? K : never; +// }[keyof Styles]; + +// export type ColorStyle = { +// [K in keyof Styles]-?: Required[K] extends string ? K : never; +// }[keyof Styles]; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 44071074b3..f3725d9596 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -2,6 +2,7 @@ import { Node } from "@tiptap/core"; import { Fragment, Node as PMNode, Slice } from "prosemirror-model"; import { NodeSelection, TextSelection } from "prosemirror-state"; +import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { blockToNode, inlineContentToNodes, @@ -15,6 +16,7 @@ import { BlockSchema, PartialBlock, } from "../api/blockTypes"; +import { StyleSchema } from "../api/styles"; import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"; import BlockAttributes from "./BlockAttributes"; @@ -25,13 +27,16 @@ declare module "@tiptap/core" { BNDeleteBlock: (posInBlock: number) => ReturnType; BNMergeBlocks: (posBetweenBlocks: number) => ReturnType; BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType; - BNUpdateBlock: ( + BNUpdateBlock: ( posInBlock: number, - block: PartialBlock + block: PartialBlock ) => ReturnType; - BNCreateOrUpdateBlock: ( + BNCreateOrUpdateBlock: < + BSchema extends BlockSchema, + S extends StyleSchema + >( posInBlock: number, - block: PartialBlock + block: PartialBlock ) => ReturnType; }; } @@ -42,6 +47,7 @@ declare module "@tiptap/core" { */ export const BlockContainer = Node.create<{ domAttributes?: BlockNoteDOMAttributes; + editor: BlockNoteEditor; }>({ name: "blockContainer", group: "blockContainer", @@ -158,7 +164,13 @@ export const BlockContainer = Node.create<{ // Creates ProseMirror nodes for each child block, including their descendants. for (const child of block.children) { - childNodes.push(blockToNode(child, state.schema)); + childNodes.push( + blockToNode( + child, + state.schema, + this.options.editor.styleSchema + ) + ); } // Checks if a blockGroup node already exists. @@ -193,9 +205,17 @@ export const BlockContainer = Node.create<{ } 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, state.schema); + content = inlineContentToNodes( + block.content, + state.schema, + this.options.editor.styleSchema + ); } else if (block.content.type === "tableContent") { - content = tableContentToNodes(block.content, state.schema); + content = tableContentToNodes( + block.content, + state.schema, + this.options.editor.styleSchema + ); } else { throw new UnreachableCaseError(block.content.type); } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts index 85f1d3592e..19dee16acd 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts @@ -2,12 +2,13 @@ import { BlockNoteEditor } from "../../../../../BlockNoteEditor"; import { imageToolbarPluginKey } from "../../../../ImageToolbar/ImageToolbarPlugin"; import { - Block, + BlockFromConfig, BlockSchemaWithBlock, PropSchema, } from "../../../api/blockTypes"; import { CustomBlockConfig, createBlockSpec } from "../../../api/customBlocks"; import { defaultProps } from "../../../api/defaultProps"; +import { StyleSchema } from "../../../api/styles"; export const imagePropSchema = { textAlignment: defaultProps.textAlignment, @@ -52,7 +53,7 @@ const blockConfig = { } satisfies CustomBlockConfig; export const renderImage = ( - block: Block, + block: BlockFromConfig, editor: BlockNoteEditor> ) => { // Wrapper element to set the image alignment, contains both image/image diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts index e6e828c08f..2bfa409a42 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts @@ -1,7 +1,8 @@ -import { Block } from "../../../.."; +import { Block, BlockSchema } from "../../../.."; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { blockToNode } from "../../../../api/nodeConversions/nodeConversions"; import { mergeCSSClasses } from "../../../../shared/utils"; +import { StyleSchema } from "../../api/styles"; // Function that creates a ProseMirror `DOMOutputSpec` for a default block. // Since all default blocks have the same structure (`blockContent` div with a @@ -51,14 +52,21 @@ export function createDefaultBlockDOMOutputSpec( // Function used to convert default blocks to HTML. It uses the corresponding // node's `renderHTML` method to do the conversion by using a default // `DOMSerializer`. -export const defaultBlockToHTML = ( - block: Block, - editor: BlockNoteEditor +export const defaultBlockToHTML = < + BSchema extends BlockSchema, + S extends StyleSchema +>( + block: Block, + editor: BlockNoteEditor ): { dom: HTMLElement; contentDOM?: HTMLElement; } => { - const node = blockToNode(block, editor._tiptapEditor.schema).firstChild!; + const node = blockToNode( + block, + editor._tiptapEditor.schema, + editor.styleSchema + ).firstChild!; const toDOM = editor._tiptapEditor.schema.nodes[node.type.name].spec.toDOM; if (toDOM === undefined) { diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 29d893fccb..a2c7f8d40f 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -8,12 +8,13 @@ import { BlockSchema, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; +import { StyleSchema } from "../Blocks/api/styles"; export type FormattingToolbarCallbacks = BaseUiElementCallbacks; export type FormattingToolbarState = BaseUiElementState; -export class FormattingToolbarView { +export class FormattingToolbarView { private formattingToolbarState?: FormattingToolbarState; public updateFormattingToolbar: () => void; @@ -40,7 +41,7 @@ export class FormattingToolbarView { }; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, updateFormattingToolbar: ( formattingToolbarState: FormattingToolbarState @@ -216,13 +217,11 @@ export const formattingToolbarPluginKey = new PluginKey( "FormattingToolbarPlugin" ); -export class FormattingToolbarProsemirrorPlugin< - BSchema extends BlockSchema -> extends EventEmitter { - private view: FormattingToolbarView | undefined; +export class FormattingToolbarProsemirrorPlugin extends EventEmitter { + private view: FormattingToolbarView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: formattingToolbarPluginKey, diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 65ae274279..6bc71296f4 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -6,6 +6,7 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; import { BlockSchema } from "../Blocks/api/blockTypes"; +import { StyleSchema } from "../Blocks/api/styles"; export type HyperlinkToolbarState = BaseUiElementState & { // The hovered hyperlink's URL, and the text it's displayed with in the @@ -14,7 +15,7 @@ export type HyperlinkToolbarState = BaseUiElementState & { text: string; }; -class HyperlinkToolbarView { +class HyperlinkToolbarView { private hyperlinkToolbarState?: HyperlinkToolbarState; public updateHyperlinkToolbar: () => void; @@ -32,7 +33,7 @@ class HyperlinkToolbarView { hyperlinkMarkRange: Range | undefined; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, updateHyperlinkToolbar: ( hyperlinkToolbarState: HyperlinkToolbarState @@ -275,12 +276,13 @@ export const hyperlinkToolbarPluginKey = new PluginKey( ); export class HyperlinkToolbarProsemirrorPlugin< - BSchema extends BlockSchema + BSchema extends BlockSchema, + S extends StyleSchema > extends EventEmitter { - private view: HyperlinkToolbarView | undefined; + private view: HyperlinkToolbarView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: hyperlinkToolbarPluginKey, diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index acf7f015f8..63809906d9 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -5,18 +5,24 @@ import { BaseUiElementState, BlockNoteEditor, BlockSchema, + SpecificBlock, } from "../.."; -import { Block } from "../../extensions/Blocks/api/blockTypes"; import { EventEmitter } from "../../shared/EventEmitter"; -import { Image } from "../Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent"; +import { StyleSchema } from "../Blocks/api/styles"; export type ImageToolbarCallbacks = BaseUiElementCallbacks; -export type ImageToolbarState = BaseUiElementState & { - block: Block<(typeof Image)["config"]>; +export type ImageToolbarState< + B extends BlockSchema, + S extends StyleSchema = StyleSchema +> = BaseUiElementState & { + block: SpecificBlock; }; -export class ImageToolbarView { - private imageToolbarState?: ImageToolbarState; +export class ImageToolbarView< + BSchema extends BlockSchema, + S extends StyleSchema +> { + private imageToolbarState?: ImageToolbarState; public updateImageToolbar: () => void; public prevWasEditable: boolean | null = null; @@ -24,7 +30,9 @@ export class ImageToolbarView { constructor( private readonly pluginKey: PluginKey, private readonly pmView: EditorView, - updateImageToolbar: (imageToolbarState: ImageToolbarState) => void + updateImageToolbar: ( + imageToolbarState: ImageToolbarState + ) => void ) { this.updateImageToolbar = () => { if (!this.imageToolbarState) { @@ -94,7 +102,7 @@ export class ImageToolbarView { update(view: EditorView, prevState: EditorState) { const pluginState: { - block: Block<(typeof Image)["config"]>; + block: SpecificBlock; } = this.pluginKey.getState(view.state); if (!this.imageToolbarState?.show && pluginState.block) { @@ -139,15 +147,16 @@ export class ImageToolbarView { export const imageToolbarPluginKey = new PluginKey("ImageToolbarPlugin"); export class ImageToolbarProsemirrorPlugin< - BSchema extends BlockSchema + BSchema extends BlockSchema, + S extends StyleSchema > extends EventEmitter { - private view: ImageToolbarView | undefined; + private view: ImageToolbarView | undefined; public readonly plugin: Plugin; - constructor(_editor: BlockNoteEditor) { + constructor(_editor: BlockNoteEditor) { super(); this.plugin = new Plugin<{ - block: Block<(typeof Image)["config"]> | undefined; + block: SpecificBlock | undefined; }>({ key: imageToolbarPluginKey, view: (editorView) => { @@ -168,7 +177,7 @@ export class ImageToolbarProsemirrorPlugin< }; }, apply: (transaction) => { - const block: Block<(typeof Image)["config"]> | undefined = + const block: SpecificBlock | undefined = transaction.getMeta(imageToolbarPluginKey)?.block; return { @@ -179,7 +188,7 @@ export class ImageToolbarProsemirrorPlugin< }); } - public onUpdate(callback: (state: ImageToolbarState) => void) { + public onUpdate(callback: (state: ImageToolbarState) => void) { return this.on("update", callback); } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index c4e5823426..f21219d63e 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -9,15 +9,19 @@ import { createInternalHTMLSerializer } from "../../api/serialization/html/inter import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; import { Block, BlockSchema } from "../Blocks/api/blockTypes"; +import { StyleSchema } from "../Blocks/api/styles"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin"; import { MultipleNodeSelection } from "./MultipleNodeSelection"; let dragImageElement: Element | undefined; -export type SideMenuState = BaseUiElementState & { +export type SideMenuState< + BSchema extends BlockSchema, + S extends StyleSchema +> = BaseUiElementState & { // The block that the side menu is attached to. - block: Block; + block: Block; }; export function getDraggableBlockFromCoords( @@ -170,9 +174,9 @@ function unsetDragImage() { } } -function dragStart( +function dragStart( e: { dataTransfer: DataTransfer | null; clientY: number }, - editor: BlockNoteEditor + editor: BlockNoteEditor ) { if (!e.dataTransfer) { return; @@ -236,8 +240,10 @@ function dragStart( } } -export class SideMenuView implements PluginView { - private sideMenuState?: SideMenuState; +export class SideMenuView + implements PluginView +{ + private sideMenuState?: SideMenuState; // When true, the drag handle with be anchored at the same level as root elements // When false, the drag handle with be just to the left of the element @@ -253,10 +259,10 @@ export class SideMenuView implements PluginView { public menuFrozen = false; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, private readonly updateSideMenu: ( - sideMenuState: SideMenuState + sideMenuState: SideMenuState ) => void ) { this.horizontalPosAnchoredAtRoot = true; @@ -561,12 +567,13 @@ export class SideMenuView implements PluginView { export const sideMenuPluginKey = new PluginKey("SideMenuPlugin"); export class SideMenuProsemirrorPlugin< - BSchema extends BlockSchema + BSchema extends BlockSchema, + S extends StyleSchema > extends EventEmitter { - private sideMenuView: SideMenuView | undefined; + private sideMenuView: SideMenuView | undefined; public readonly plugin: Plugin; - constructor(private readonly editor: BlockNoteEditor) { + constructor(private readonly editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: sideMenuPluginKey, @@ -583,7 +590,7 @@ export class SideMenuProsemirrorPlugin< }); } - public onUpdate(callback: (state: SideMenuState) => void) { + public onUpdate(callback: (state: SideMenuState) => void) { return this.on("update", callback); } diff --git a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts index 41fc78917c..71c9895ad2 100644 --- a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts +++ b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts @@ -1,11 +1,16 @@ -import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; import { BlockSchema } from "../Blocks/api/blockTypes"; -import { DefaultBlockSchema } from "../Blocks/api/defaultBlocks"; +import { + DefaultBlockSchema, + DefaultStyleSchema, +} from "../Blocks/api/defaultBlocks"; +import { StyleSchema } from "../Blocks/api/styles"; export type BaseSlashMenuItem< - BSchema extends BlockSchema = DefaultBlockSchema + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema > = SuggestionItem & { - execute: (editor: BlockNoteEditor) => void; + execute: (editor: BlockNoteEditor) => void; aliases?: string[]; }; diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts index a190cc3209..682e7c8511 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts @@ -7,20 +7,22 @@ import { setupSuggestionsMenu, } from "../../shared/plugins/suggestion/SuggestionPlugin"; import { BlockSchema } from "../Blocks/api/blockTypes"; +import { StyleSchema } from "../Blocks/api/styles"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; export const slashMenuPluginKey = new PluginKey("SlashMenuPlugin"); export class SlashMenuProsemirrorPlugin< BSchema extends BlockSchema, - SlashMenuItem extends BaseSlashMenuItem + S extends StyleSchema, + SlashMenuItem extends BaseSlashMenuItem > extends EventEmitter { public readonly plugin: Plugin; public readonly itemCallback: (item: SlashMenuItem) => void; - constructor(editor: BlockNoteEditor, items: SlashMenuItem[]) { + constructor(editor: BlockNoteEditor, items: SlashMenuItem[]) { super(); - const suggestions = setupSuggestionsMenu( + const suggestions = setupSuggestionsMenu( editor, (state) => { this.emit("update", state); diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts index 130c691da2..c2589eb915 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts @@ -1,14 +1,15 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { Block, BlockSchema, PartialBlock } from "../Blocks/api/blockTypes"; -import { defaultBlockSchema } from "../Blocks/api/defaultBlocks"; +import { StyleSchema } from "../Blocks/api/styles"; import { imageToolbarPluginKey } from "../ImageToolbar/ImageToolbarPlugin"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; -function setSelectionToNextContentEditableBlock( - editor: BlockNoteEditor -) { +function setSelectionToNextContentEditableBlock< + BSchema extends BlockSchema, + S extends StyleSchema +>(editor: BlockNoteEditor) { let block = editor.getTextCursorPosition().block; - let contentType = editor.schema[block.type].config.content as + let contentType = editor.blockSchema[block.type].content as | "inline" | "table" | "none"; @@ -16,7 +17,7 @@ function setSelectionToNextContentEditableBlock( while (contentType === "none") { editor.setTextCursorPosition(block, "start"); block = editor.getTextCursorPosition().nextBlock!; - contentType = editor.schema[block.type].config.content as + contentType = editor.blockSchema[block.type].content as | "inline" | "table" | "none"; @@ -25,10 +26,13 @@ function setSelectionToNextContentEditableBlock( editor.setTextCursorPosition(block, "start"); } -function insertOrUpdateBlock( - editor: BlockNoteEditor, - block: PartialBlock -): Block { +function insertOrUpdateBlock< + BSchema extends BlockSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + block: PartialBlock +): Block { const currentBlock = editor.getTextCursorPosition().block; if (currentBlock.content === undefined) { @@ -54,18 +58,21 @@ function insertOrUpdateBlock( return insertedBlock; } -export const getDefaultSlashMenuItems = ( +export const getDefaultSlashMenuItems = < + BSchema extends BlockSchema, + S extends StyleSchema +>( // This type casting is weird, but it's the best way of doing it, as it allows // the schema type to be automatically inferred if it is defined, or be // inferred as any if it is not defined. I don't think it's possible to make it // infer to DefaultBlockSchema if it is not defined. - schema: BSchema = defaultBlockSchema as unknown as BSchema + schema: BSchema ) => { - const slashMenuItems: BaseSlashMenuItem[] = []; + const slashMenuItems: BaseSlashMenuItem[] = []; - if ("heading" in schema && "level" in schema.heading.config.propSchema) { + if ("heading" in schema && "level" in schema.heading.propSchema) { // Command for creating a level 1 heading - if (schema.heading.config.propSchema.level.values?.includes(1)) { + if (schema.heading.propSchema.level.values?.includes(1)) { slashMenuItems.push({ name: "Heading", aliases: ["h", "heading1", "h1"], @@ -78,7 +85,7 @@ export const getDefaultSlashMenuItems = ( } // Command for creating a level 2 heading - if (schema.heading.config.propSchema.level.values?.includes(2)) { + if (schema.heading.propSchema.level.values?.includes(2)) { slashMenuItems.push({ name: "Heading 2", aliases: ["h2", "heading2", "subheading"], @@ -91,7 +98,7 @@ export const getDefaultSlashMenuItems = ( } // Command for creating a level 3 heading - if (schema.heading.config.propSchema.level.values?.includes(3)) { + if (schema.heading.propSchema.level.values?.includes(3)) { slashMenuItems.push({ name: "Heading 3", aliases: ["h3", "heading3", "subheading"], diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 01ba195d5a..75371569e8 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -3,22 +3,25 @@ import { EditorView } from "prosemirror-view"; import { BaseUiElementCallbacks, BlockNoteEditor, - BlockSchema, + BlockSchemaWithBlock, + DefaultBlockSchema, + SpecificBlock, getDraggableBlockFromCoords, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; -import { Block } from "../Blocks/api/blockTypes"; -import { Image } from "../Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent"; -import { Table } from "../Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent"; +import { StyleSchema } from "../Blocks/api/styles"; export type TableHandlesCallbacks = BaseUiElementCallbacks; -export type TableHandlesState = { +export type TableHandlesState< + BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + S extends StyleSchema +> = { show: boolean; referencePosTop: { top: number; left: number }; referencePosLeft: { top: number; left: number }; colIndex: number; rowIndex: number; - block: Block<(typeof Table)["config"]>; + block: SpecificBlock; }; function getChildIndex(node: HTMLElement) { @@ -34,18 +37,19 @@ function domCellAround(target: HTMLElement | null): HTMLElement | null { return target; } -export class TableHandlesView { - private state?: TableHandlesState; +export class TableHandlesView< + BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + S extends StyleSchema +> { + private state?: TableHandlesState; public updateState: () => void; public prevWasEditable: boolean | null = null; constructor( - private readonly editor: BlockNoteEditor, - // @ts-ignore - private readonly pluginKey: PluginKey, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateState: (state: TableHandlesState) => void + updateState: (state: TableHandlesState) => void ) { this.updateState = () => { if (!this.state) { @@ -83,8 +87,10 @@ export class TableHandlesView { target.parentElement!.parentElement!.getBoundingClientRect(); const blockEl = getDraggableBlockFromCoords(cellRect, this.pmView); - const block = this.editor.getBlock(blockEl!.id)! as Block< - (typeof Table)["config"] + const block = this.editor.getBlock(blockEl!.id)! as any as SpecificBlock< + BSchema, + "table", + S >; this.state = { @@ -193,26 +199,22 @@ export class TableHandlesView { export const tableHandlesPluginKey = new PluginKey("TableHandlesPlugin"); export class TableHandlesProsemirrorPlugin< - BSchema extends BlockSchema + BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + S extends StyleSchema > extends EventEmitter { - private view: TableHandlesView | undefined; + private view: TableHandlesView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin<{ - block: Block<(typeof Image)["config"]> | undefined; + block: SpecificBlock | undefined; }>({ key: tableHandlesPluginKey, view: (editorView) => { - this.view = new TableHandlesView( - editor, - tableHandlesPluginKey, - editorView, - (state) => { - this.emit("update", state); - } - ); + this.view = new TableHandlesView(editor, editorView, (state) => { + this.emit("update", state); + }); return this.view; }, state: { @@ -222,7 +224,7 @@ export class TableHandlesProsemirrorPlugin< }; }, apply: (transaction) => { - const block: Block<(typeof Image)["config"]> | undefined = + const block: SpecificBlock | undefined = transaction.getMeta(tableHandlesPluginKey)?.block; return { @@ -233,7 +235,7 @@ export class TableHandlesProsemirrorPlugin< }); } - public onUpdate(callback: (state: TableHandlesState) => void) { + public onUpdate(callback: (state: TableHandlesState) => void) { return this.on("update", callback); } } diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 8cfdbfc841..96032599bb 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -2,6 +2,7 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import { BaseUiElementState } from "../../BaseUiElementTypes"; import { SuggestionItem } from "./SuggestionItem"; @@ -16,7 +17,8 @@ export type SuggestionsMenuState = class SuggestionsMenuView< T extends SuggestionItem, - BSchema extends BlockSchema + BSchema extends BlockSchema, + S extends StyleSchema > { private suggestionsMenuState?: SuggestionsMenuState; public updateSuggestionsMenu: () => void; @@ -24,7 +26,7 @@ class SuggestionsMenuView< pluginState: SuggestionPluginState; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pluginKey: PluginKey, updateSuggestionsMenu: ( suggestionsMenuState: SuggestionsMenuState @@ -147,9 +149,10 @@ function getDefaultPluginState< */ export const setupSuggestionsMenu = < T extends SuggestionItem, - BSchema extends BlockSchema + BSchema extends BlockSchema, + S extends StyleSchema >( - editor: BlockNoteEditor, + editor: BlockNoteEditor, updateSuggestionsMenu: ( suggestionsMenuState: SuggestionsMenuState ) => void, @@ -159,7 +162,7 @@ export const setupSuggestionsMenu = < items: (query: string) => T[] = () => [], onSelectItem: (props: { item: T; - editor: BlockNoteEditor; + editor: BlockNoteEditor; }) => void = () => { // noop } @@ -169,7 +172,7 @@ export const setupSuggestionsMenu = < throw new Error("'char' should be a single character"); } - let suggestionsPluginView: SuggestionsMenuView; + let suggestionsPluginView: SuggestionsMenuView; const deactivate = (view: EditorView) => { view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true })); @@ -180,7 +183,7 @@ export const setupSuggestionsMenu = < key: pluginKey, view: () => { - suggestionsPluginView = new SuggestionsMenuView( + suggestionsPluginView = new SuggestionsMenuView( editor, pluginKey, diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx index 1dddab9f30..1d1dcdf0d1 100644 --- a/packages/react/src/BlockNoteView.tsx +++ b/packages/react/src/BlockNoteView.tsx @@ -1,4 +1,5 @@ import { BlockNoteEditor, BlockSchema, mergeCSSClasses } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { MantineProvider, createStyles } from "@mantine/core"; import { EditorContent } from "@tiptap/react"; import { HTMLAttributes, ReactNode, useMemo } from "react"; @@ -13,9 +14,12 @@ import { TableHandlesPositioner } from "./TableHandles/components/TableHandlePos import { darkDefaultTheme, lightDefaultTheme } from "./defaultThemes"; // Renders the editor as well as all menus & toolbars using default styles. -function BaseBlockNoteView( +function BaseBlockNoteView< + BSchema extends BlockSchema, + SSchema extends StyleSchema +>( props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; children?: ReactNode; } & HTMLAttributes ) { @@ -44,9 +48,12 @@ function BaseBlockNoteView( ); } -export function BlockNoteView( +export function BlockNoteView< + BSchema extends BlockSchema, + SSchema extends StyleSchema +>( props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; theme?: | "light" | "dark" diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx index 1af9e4de01..6fcd751700 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -1,16 +1,20 @@ -import { useCallback, useMemo, useState } from "react"; +import { + BlockNoteEditor, + BlockSchema, + DefaultStyleSchema, +} from "@blocknote/core"; import { Menu } from "@mantine/core"; -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { useCallback, useMemo, useState } from "react"; -import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; import { ColorIcon } from "../../../SharedComponents/ColorPicker/components/ColorIcon"; import { ColorPicker } from "../../../SharedComponents/ColorPicker/components/ColorPicker"; -import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; import { useEditorChange } from "../../../hooks/useEditorChange"; import { usePreventMenuOverflow } from "../../../hooks/usePreventMenuOverflow"; +import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; export const ColorStyleButton = (props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; }) => { const selectedBlocks = useSelectedBlocks(props.editor); diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx index aabe94e609..94333c6a7f 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx @@ -28,19 +28,19 @@ export const ImageCaptionButton = (props: { selectedBlocks[0].type === "image" && // Checks if the block has a `caption` prop which can take any string // value. - "caption" in props.editor.schema["image"].config.propSchema && - typeof props.editor.schema["image"].config.propSchema.caption.default === + "caption" in props.editor.blockSchema["image"].propSchema && + typeof props.editor.blockSchema["image"].propSchema.caption.default === "string" && - props.editor.schema["image"].config.propSchema.caption.values === + props.editor.blockSchema["image"].propSchema.caption.values === undefined && // Checks if the block has a `url` prop which can take any string value. - "url" in props.editor.schema["image"].config.propSchema && - typeof props.editor.schema["image"].config.propSchema.url.default === + "url" in props.editor.blockSchema["image"].propSchema && + typeof props.editor.blockSchema["image"].propSchema.url.default === "string" && - props.editor.schema["image"].config.propSchema.url.values === undefined && + props.editor.blockSchema["image"].propSchema.url.values === undefined && // Checks if the `url` prop is not set to an empty string. selectedBlocks[0].props.url !== "", - [props.editor.schema, selectedBlocks] + [props.editor.blockSchema, selectedBlocks] ); const [currentCaption, setCurrentCaption] = useState( diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx index c71824e873..04fe526e68 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx @@ -1,6 +1,5 @@ -import { BlockNoteEditor, BlockSchema, ToggledStyle } from "@blocknote/core"; +import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; import { useMemo, useState } from "react"; -import { IconType } from "react-icons"; import { RiBold, RiCodeFill, @@ -9,12 +8,13 @@ import { RiUnderline, } from "react-icons/ri"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; import { useEditorChange } from "../../../hooks/useEditorChange"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; import { formatKeyboardShortcut } from "../../../utils"; -const shortcuts: Record = { +const shortcuts = { bold: "Mod+B", italic: "Mod+I", underline: "Mod+U", @@ -22,7 +22,7 @@ const shortcuts: Record = { code: "", }; -const icons: Record = { +const icons = { bold: RiBold, italic: RiItalic, underline: RiUnderline, @@ -30,9 +30,12 @@ const icons: Record = { code: RiCodeFill, }; -export const ToggledStyleButton = (props: { - editor: BlockNoteEditor; - toggledStyle: ToggledStyle; +export const ToggledStyleButton = < + BSchema extends BlockSchema, + S extends StyleSchema +>(props: { + editor: BlockNoteEditor; + toggledStyle: keyof typeof shortcuts; }) => { const selectedBlocks = useSelectedBlocks(props.editor); @@ -44,9 +47,12 @@ export const ToggledStyleButton = (props: { setActive(props.toggledStyle in props.editor.getActiveStyles()); }); - const toggleStyle = (style: ToggledStyle) => { + const toggleStyle = (style: typeof props.toggledStyle) => { props.editor.focus(); - props.editor.toggleStyles({ [style]: true }); + if (props.editor.styleSchema[style].propSchema !== "boolean") { + throw new Error("can only toggle boolean styles"); + } + props.editor.toggleStyles({ [style]: true } as any); }; const show = useMemo(() => { diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx index b3f1f621f6..86d08d19fa 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx @@ -87,13 +87,13 @@ export const BlockTypeDropdown = (props: { const filteredItems: BlockTypeDropdownItem[] = useMemo(() => { return (props.items || defaultBlockTypeDropdownItems).filter((item) => { // Checks if block type exists in the schema - if (!(item.type in props.editor.schema)) { + if (!(item.type in props.editor.blockSchema)) { return false; } // Checks if props for the block type are valid for (const [prop, value] of Object.entries(item.props || {})) { - const propSchema = props.editor.schema[item.type].config.propSchema; + const propSchema = props.editor.blockSchema[item.type].propSchema; // Checks if the prop exists for the block type if (!(prop in propSchema)) { diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx index 891c5fc0d5..b89cf1b16f 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx @@ -8,8 +8,8 @@ import Tippy, { tippy } from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; import { sticky } from "tippy.js"; -import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; import { useEditorChange } from "../../hooks/useEditorChange"; +import { DefaultFormattingToolbar } from "./DefaultFormattingToolbar"; const textAlignmentToPlacement = ( textAlignment: DefaultProps["textAlignment"] @@ -35,7 +35,7 @@ export type FormattingToolbarProps< export const FormattingToolbarPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; formattingToolbar?: FC>; }) => { const [show, setShow] = useState(false); diff --git a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx index dc4a1a5f5f..ee9b6143a3 100644 --- a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx @@ -1,14 +1,18 @@ +import { BlockSchema } from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { BlockSchema } from "@blocknote/core"; -import { HyperlinkToolbarProps } from "./HyperlinkToolbarPositioner"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton"; import { EditHyperlinkMenu } from "./EditHyperlinkMenu/components/EditHyperlinkMenu"; +import { HyperlinkToolbarProps } from "./HyperlinkToolbarPositioner"; -export const DefaultHyperlinkToolbar = ( - props: HyperlinkToolbarProps +export const DefaultHyperlinkToolbar = < + BSchema extends BlockSchema, + S extends StyleSchema +>( + props: HyperlinkToolbarProps ) => { const [isEditing, setIsEditing] = useState(false); const editMenuRef = useRef(null); diff --git a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx index 66b76706ce..00efea496e 100644 --- a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx +++ b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx @@ -3,25 +3,31 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultStyleSchema, HyperlinkToolbarProsemirrorPlugin, HyperlinkToolbarState, } from "@blocknote/core"; import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; -export type HyperlinkToolbarProps = Pick< - HyperlinkToolbarProsemirrorPlugin, +export type HyperlinkToolbarProps< + BSchema extends BlockSchema, + S extends StyleSchema +> = Pick< + HyperlinkToolbarProsemirrorPlugin, "editHyperlink" | "deleteHyperlink" | "startHideTimer" | "stopHideTimer" > & Omit; export const HyperlinkToolbarPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema >(props: { - editor: BlockNoteEditor; - hyperlinkToolbar?: FC>; + editor: BlockNoteEditor; + hyperlinkToolbar?: FC>; }) => { const [show, setShow] = useState(false); const [url, setUrl] = useState(); diff --git a/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx b/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx index ccc677759f..5db05625ea 100644 --- a/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx +++ b/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx @@ -1,10 +1,10 @@ import { BaseUiElementState, - Block, BlockNoteEditor, BlockSchema, DefaultBlockSchema, ImageToolbarState, + SpecificBlock, } from "@blocknote/core"; import Tippy, { tippy } from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; @@ -13,19 +13,18 @@ import { DefaultImageToolbar } from "./DefaultImageToolbar"; export type ImageToolbarProps< BSchema extends BlockSchema = DefaultBlockSchema -> = Omit & { +> = Omit, keyof BaseUiElementState> & { editor: BlockNoteEditor; }; export const ImageToolbarPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; imageToolbar?: FC>; }) => { const [show, setShow] = useState(false); - const [block, setBlock] = - useState>(); + const [block, setBlock] = useState>(); const referencePos = useRef(); diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index ea992828af..bf43ad7003 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -1,5 +1,5 @@ import { - Block, + BlockFromConfig, BlockNoteDOMAttributes, BlockNoteEditor, BlockSchemaWithBlock, @@ -15,6 +15,7 @@ import { PropSchema, propsToAttributes, } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { NodeViewContent, NodeViewProps, @@ -27,14 +28,17 @@ import { renderToString } from "react-dom/server"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks // extend BlockConfig but use a React render function -export type ReactCustomBlockImplementation = { +export type ReactCustomBlockImplementation< + T extends CustomBlockConfig, + S extends StyleSchema +> = { render: FC<{ - block: Block; - editor: BlockNoteEditor>; + block: BlockFromConfig; + editor: BlockNoteEditor, S>; }>; toExternalHTML?: FC<{ - block: Block; - editor: BlockNoteEditor>; + block: BlockFromConfig; + editor: BlockNoteEditor, S>; }>; }; @@ -114,10 +118,10 @@ export function reactWrapInBlockStructure< // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead -export function createReactBlockSpec( - blockConfig: T, - blockImplementation: ReactCustomBlockImplementation -) { +export function createReactBlockSpec< + T extends CustomBlockConfig, + S extends StyleSchema +>(blockConfig: T, blockImplementation: ReactCustomBlockImplementation) { const node = createStronglyTypedTiptapNode({ name: blockConfig.type as T["type"], content: (blockConfig.content === "inline" @@ -153,7 +157,7 @@ export function createReactBlockSpec( const Content = blockImplementation.render; const BlockContent = reactWrapInBlockStructure( - , + , block.type, block.props, blockConfig.propSchema, diff --git a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx index c77caa6cea..4ad4c561a6 100644 --- a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx +++ b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx @@ -3,32 +3,32 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultStyleSchema, SideMenuProsemirrorPlugin, } from "@blocknote/core"; import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { DefaultSideMenu } from "./DefaultSideMenu"; import { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; -export type SideMenuProps = - Pick< - SideMenuProsemirrorPlugin, - | "blockDragStart" - | "blockDragEnd" - | "addBlock" - | "freezeMenu" - | "unfreezeMenu" - > & { - block: Block; - editor: BlockNoteEditor; - dragHandleMenu?: FC>; - }; +export type SideMenuProps< + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema +> = Pick< + SideMenuProsemirrorPlugin, + "blockDragStart" | "blockDragEnd" | "addBlock" | "freezeMenu" | "unfreezeMenu" +> & { + block: Block; + editor: BlockNoteEditor; + dragHandleMenu?: FC>; +}; export const SideMenuPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; sideMenu?: FC>; }) => { const [show, setShow] = useState(false); diff --git a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts index b5e6f24091..768506dee5 100644 --- a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts +++ b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts @@ -2,11 +2,14 @@ import { BaseSlashMenuItem, BlockSchema, DefaultBlockSchema, + DefaultStyleSchema, } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; export type ReactSlashMenuItem< - BSchema extends BlockSchema = DefaultBlockSchema -> = BaseSlashMenuItem & { + BSchema extends BlockSchema = DefaultBlockSchema, + S extends StyleSchema = DefaultStyleSchema +> = BaseSlashMenuItem & { group: string; icon: JSX.Element; hint?: string; diff --git a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx index e3e005b68e..2918a1f80d 100644 --- a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx +++ b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx @@ -8,12 +8,12 @@ import { import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; +import { usePreventMenuOverflow } from "../../hooks/usePreventMenuOverflow"; import { ReactSlashMenuItem } from "../ReactSlashMenuItem"; import { DefaultSlashMenu } from "./DefaultSlashMenu"; -import { usePreventMenuOverflow } from "../../hooks/usePreventMenuOverflow"; export type SlashMenuProps = - Pick, "itemCallback"> & + Pick, "itemCallback"> & Pick< SuggestionsMenuState>, "filteredItems" | "keyboardHoveredItemIndex" @@ -22,7 +22,7 @@ export type SlashMenuProps = export const SlashMenuPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; slashMenu?: FC>; }) => { const [show, setShow] = useState(false); diff --git a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx index 73b9628607..0ad43bbdca 100644 --- a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx +++ b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx @@ -5,6 +5,7 @@ import { DefaultBlockSchema, getDefaultSlashMenuItems, } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { RiH1, RiH2, @@ -74,14 +75,17 @@ const extraFields: Record< }, }; -export function getDefaultReactSlashMenuItems( +export function getDefaultReactSlashMenuItems< + BSchema extends BlockSchema, + S extends StyleSchema +>( // This type casting is weird, but it's the best way of doing it, as it allows // the schema type to be automatically inferred if it is defined, or be // inferred as any if it is not defined. I don't think it's possible to make it // infer to DefaultBlockSchema if it is not defined. - schema: BSchema = defaultBlockSchema as unknown as BSchema -): ReactSlashMenuItem[] { - const slashMenuItems: BaseSlashMenuItem[] = + schema: BSchema = defaultBlockSchema as any as BSchema +): ReactSlashMenuItem[] { + const slashMenuItems: BaseSlashMenuItem[] = getDefaultSlashMenuItems(schema); return slashMenuItems.map((item) => ({ diff --git a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx index 845a45ec9a..8e4afd90a3 100644 --- a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx +++ b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx @@ -1,11 +1,21 @@ -import { TableContent } from "@blocknote/core"; +import { + BlockSchemaWithBlock, + DefaultBlockSchema, + TableContent, +} from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { Menu } from "@mantine/core"; import { MdDragIndicator } from "react-icons/md"; import { SideMenuButton } from "../../SideMenu/components/SideMenuButton"; import { TableHandlesProps } from "./TableHandlePositioner"; -const DefaultTableHandleLeft = (props: TableHandlesProps) => { +const DefaultTableHandleLeft = ( + props: TableHandlesProps< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + StyleSchema + > +) => { return ( { ); }; -const DefaultTableHandleTop = (props: TableHandlesProps) => { +const DefaultTableHandleTop = ( + props: TableHandlesProps< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + StyleSchema + > +) => { return ( { ); }; -export const DefaultTableHandle = (props: TableHandlesProps) => { +export const DefaultTableHandle = ( + props: TableHandlesProps< + BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + any + > +) => { if (props.side === "left") { return ; } else { diff --git a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx index 48e8952018..4b384f2b22 100644 --- a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx +++ b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx @@ -1,47 +1,48 @@ import { - Block, BlockNoteEditor, BlockSchemaWithBlock, DefaultBlockSchema, + SpecificBlock, TableHandlesState, } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import Tippy, { tippy } from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; import { DefaultTableHandle } from "./DefaultTableHandle"; -export type TableHandlesProps = Omit< - TableHandlesState, +export type TableHandlesProps< + BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + S extends StyleSchema +> = Omit< + TableHandlesState, "referencePosLeft" | "referencePosTop" | "show" > & { - editor: BlockNoteEditor< - BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]["config"]> - >; + editor: BlockNoteEditor; side: "top" | "left"; }; export const TableHandlesPositioner = < - BSchema extends BlockSchemaWithBlock< - "table", - DefaultBlockSchema["table"]["config"] - > + BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + S extends StyleSchema >(props: { - editor: BlockNoteEditor; - tableHandle?: FC; + editor: BlockNoteEditor; + tableHandle?: FC>; }) => { const [show, setShow] = useState(false); - const [block, setBlock] = - useState>(); + const [block, setBlock] = useState>(); const [colIndex, setColIndex] = useState(); const [rowIndex, setRowIndex] = useState(); const [_, setForceUpdate] = useState(0); - const referencePosLeft = useRef(); - const referencePosTop = useRef(); + const referencePosLeft = + useRef["referencePosLeft"]>(); + const referencePosTop = + useRef["referencePosTop"]>(); useEffect(() => { tippy.setDefaultProps({ maxWidth: "" }); - return props.editor.tableHandles.onUpdate((state) => { + return props.editor.tableHandles!.onUpdate((state) => { console.log("update", state); setShow(state.show); setBlock(state.block); @@ -77,7 +78,9 @@ export const TableHandlesPositioner = < ); const tableHandleElementTop = useMemo(() => { - const TableHandle = props.tableHandle || DefaultTableHandle; + const TableHandle = + props.tableHandle || + (DefaultTableHandle as FC>); return ( { - const TableHandle = props.tableHandle || DefaultTableHandle; + const TableHandle = + props.tableHandle || + (DefaultTableHandle as FC>); return ( ( - options: Partial> +const initEditor = < + BSpecs extends BlockSpecs, + SSpecs extends StyleSpecs, + BSchema extends BlockSchema = { + [key in keyof BSpecs]: BSpecs[key]["config"]; + }, + SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } +>( + options: Partial> ) => - new BlockNoteEditor({ - slashMenuItems: getDefaultReactSlashMenuItems( - options.blockSchema || defaultBlockSchema + BlockNoteEditor.create({ + slashMenuItems: getDefaultReactSlashMenuItems( + options.blockSpecs || (defaultBlockSpecs as any) ), ...options, }); @@ -21,18 +32,28 @@ const initEditor = ( /** * Main hook for importing a BlockNote editor into a React project */ -export const useBlockNote = ( - options: Partial> = {}, +export const useBlockNote = < + BSpecs extends BlockSpecs, + SSpecs extends StyleSpecs, + BSchema extends BlockSchema = { + [key in keyof BSpecs]: BSpecs[key]["config"]; + }, + SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } +>( + options: Partial> = {}, deps: DependencyList = [] -): BlockNoteEditor => { - const editorRef = useRef>(); +): BlockNoteEditor => { + const editorRef = useRef>(); return useMemo(() => { if (editorRef.current) { editorRef.current._tiptapEditor.destroy(); } - editorRef.current = initEditor(options); + editorRef.current = initEditor(options) as BlockNoteEditor< + BSchema, + SSchema + >; return editorRef.current; }, deps); //eslint-disable-line react-hooks/exhaustive-deps }; diff --git a/packages/react/src/hooks/useEditorChange.ts b/packages/react/src/hooks/useEditorChange.ts index f9408af9ba..6bc15a7034 100644 --- a/packages/react/src/hooks/useEditorChange.ts +++ b/packages/react/src/hooks/useEditorChange.ts @@ -1,9 +1,9 @@ -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { BlockNoteEditor } from "@blocknote/core"; import { useEditorContentChange } from "./useEditorContentChange"; import { useEditorSelectionChange } from "./useEditorSelectionChange"; -export function useEditorChange( - editor: BlockNoteEditor, +export function useEditorChange( + editor: BlockNoteEditor, callback: () => void ) { useEditorContentChange(editor, callback); diff --git a/packages/react/src/hooks/useEditorContentChange.ts b/packages/react/src/hooks/useEditorContentChange.ts index 64882bcf29..a884f00c61 100644 --- a/packages/react/src/hooks/useEditorContentChange.ts +++ b/packages/react/src/hooks/useEditorContentChange.ts @@ -1,8 +1,9 @@ import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; -export function useEditorContentChange( - editor: BlockNoteEditor, +export function useEditorContentChange( + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/hooks/useEditorSelectionChange.ts b/packages/react/src/hooks/useEditorSelectionChange.ts index 1072b31973..99e4e4fb85 100644 --- a/packages/react/src/hooks/useEditorSelectionChange.ts +++ b/packages/react/src/hooks/useEditorSelectionChange.ts @@ -1,8 +1,9 @@ import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; -export function useEditorSelectionChange( - editor: BlockNoteEditor, +export function useEditorSelectionChange( + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/hooks/useSelectedBlocks.ts b/packages/react/src/hooks/useSelectedBlocks.ts index 1a64543948..6f63464f39 100644 --- a/packages/react/src/hooks/useSelectedBlocks.ts +++ b/packages/react/src/hooks/useSelectedBlocks.ts @@ -1,11 +1,15 @@ import { Block, BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useState } from "react"; import { useEditorChange } from "./useEditorChange"; -export function useSelectedBlocks( - editor: BlockNoteEditor -) { - const [selectedBlocks, setSelectedBlocks] = useState[]>( +export function useSelectedBlocks< + BSchema extends BlockSchema, + SSchema extends StyleSchema +>(editor: BlockNoteEditor) { + const [selectedBlocks, setSelectedBlocks] = useState< + Block[] + >( () => editor.getSelection()?.blocks || [editor.getTextCursorPosition().block] ); diff --git a/packages/react/src/htmlConversion.test.tsx b/packages/react/src/htmlConversion.test.tsx index fa5bd43161..b419d26f8f 100644 --- a/packages/react/src/htmlConversion.test.tsx +++ b/packages/react/src/htmlConversion.test.tsx @@ -1,10 +1,11 @@ import { BlockNoteEditor, - BlockSchema, + BlockSchemaFromSpecs, + BlockSpecs, PartialBlock, createExternalHTMLExporter, createInternalHTMLSerializer, - defaultBlockSchema, + defaultBlockSpecs, defaultProps, uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; @@ -41,18 +42,18 @@ const SimpleReactCustomParagraph = createReactBlockSpec( } ); -const customSchema = { - ...defaultBlockSchema, +const customSpecs = { + ...defaultBlockSpecs, reactCustomParagraph: ReactCustomParagraph, simpleReactCustomParagraph: SimpleReactCustomParagraph, -} satisfies BlockSchema; +} satisfies BlockSpecs; -let editor: BlockNoteEditor; +let editor: BlockNoteEditor>; let tt: Editor; beforeEach(() => { - editor = new BlockNoteEditor({ - blockSchema: customSchema, + editor = BlockNoteEditor.create({ + blockSpecs: customSpecs, uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, }); tt = editor._tiptapEditor; @@ -67,7 +68,7 @@ afterEach(() => { }); function convertToHTMLAndCompareSnapshots( - blocks: PartialBlock[], + blocks: PartialBlock<(typeof editor)["blockSchema"]>[], snapshotDirectory: string, snapshotName: string ) { @@ -94,7 +95,7 @@ function convertToHTMLAndCompareSnapshots( describe("Convert custom blocks with inline content to HTML", () => { it("Convert custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "reactCustomParagraph", content: "React Custom Paragraph", @@ -105,7 +106,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert styled custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "reactCustomParagraph", props: { @@ -149,7 +150,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert nested block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "reactCustomParagraph", content: "React Custom Paragraph", @@ -172,7 +173,7 @@ describe("Convert custom blocks with inline content to HTML", () => { describe("Convert custom blocks with non-exported inline content to HTML", () => { it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "simpleReactCustomParagraph", content: "React Custom Paragraph", @@ -187,7 +188,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "simpleReactCustomParagraph", props: { @@ -235,7 +236,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ { type: "simpleReactCustomParagraph", content: "Custom React Paragraph", diff --git a/packages/website/docs/docs/vanilla-js.md b/packages/website/docs/docs/vanilla-js.md index 6d2b9ce4f4..e290034274 100644 --- a/packages/website/docs/docs/vanilla-js.md +++ b/packages/website/docs/docs/vanilla-js.md @@ -25,7 +25,7 @@ This is how to create a new BlockNote editor: ``` import { BlockNoteEditor } from "@blocknote/core"; -const editor = new BlockNoteEditor({ +const editor = BlockNoteEditor.create({ element: document.getElementById("root")!, // element to append the editor to onUpdate: ({ editor }) => { console.log(editor.getJSON()); @@ -47,7 +47,7 @@ Because we can't use the built-in React elements, you'll need to create and regi You can do this by passing custom component factories as `uiFactories`, e.g.: ``` -const editor = new BlockNoteEditor({ +const editor = BlockNoteEditor.create({ element: document.getElementById("root")!, uiFactories: { formattingToolbarFactory: customFormattingToolbarFactory, From bbaf604ec24dba5e966f4898acf5c40d4f88ff7e Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 20 Nov 2023 21:52:10 +0100 Subject: [PATCH 02/31] fix --- packages/core/src/BlockNoteEditor.ts | 9 +---- .../BackgroundColorExtension.ts | 39 +------------------ .../BackgroundColor/BackgroundColorMark.ts | 31 +++------------ .../TextColor/TextColorExtension.ts | 32 +-------------- .../src/extensions/TextColor/TextColorMark.ts | 29 ++------------ .../DefaultButtons/ColorStyleButton.tsx | 1 + packages/react/src/hooks/useBlockNote.ts | 5 ++- 7 files changed, 19 insertions(+), 127 deletions(-) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 7bfdefa058..93a7af1d9f 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -232,13 +232,8 @@ export class BlockNoteEditor< styleSpecs: StyleSpecs; } = { defaultStyles: true, - // TODO: There's a lot of annoying typing stuff to deal with here. If - // BSchema is specified, then options.blockSchema should also be required. - // If BSchema is not specified, then options.blockSchema should also not - // be defined. Unfortunately, trying to implement these constraints seems - // to be a huge pain, hence the `as any` casts. - blockSpecs: options.blockSpecs || (defaultBlockSchema as any), - styleSpecs: options.styleSpecs || (defaultStyleSpecs as any), + blockSpecs: options.blockSpecs || defaultBlockSpecs, + styleSpecs: options.styleSpecs || defaultStyleSpecs, ...options, }; diff --git a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts index caa76f6416..66d4df7203 100644 --- a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts +++ b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts @@ -1,18 +1,6 @@ import { Extension } from "@tiptap/core"; -import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { defaultProps } from "../Blocks/api/defaultProps"; -declare module "@tiptap/core" { - interface Commands { - blockBackgroundColor: { - setBlockBackgroundColor: ( - posInBlock: number, - color: string - ) => ReturnType; - }; - } -} - export const BackgroundColorExtension = Extension.create({ name: "blockBackgroundColor", @@ -28,36 +16,13 @@ export const BackgroundColorExtension = Extension.create({ ? element.getAttribute("data-background-color") : defaultProps.backgroundColor.default, renderHTML: (attributes) => - attributes.backgroundColor !== + attributes.stringValue !== defaultProps.backgroundColor.default && { - "data-background-color": attributes.backgroundColor, + "data-background-color": attributes.stringValue, }, }, }, }, ]; }, - - addCommands() { - return { - setBlockBackgroundColor: - (posInBlock, color) => - ({ state, view }) => { - const blockInfo = getBlockInfoFromPos(state.doc, posInBlock); - if (blockInfo === undefined) { - return false; - } - - state.tr.setNodeAttribute( - blockInfo.startPos - 1, - "backgroundColor", - color - ); - - view.focus(); - - return true; - }, - }; - }, }); diff --git a/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts b/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts index adcdca387f..4c8d93f838 100644 --- a/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts +++ b/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts @@ -1,24 +1,15 @@ import { Mark } from "@tiptap/core"; -import { defaultProps } from "../Blocks/api/defaultProps"; - -declare module "@tiptap/core" { - interface Commands { - backgroundColor: { - setBackgroundColor: (color: string) => ReturnType; - }; - } -} export const BackgroundColorMark = Mark.create({ name: "backgroundColor", addAttributes() { return { - color: { + stringValue: { default: undefined, parseHTML: (element) => element.getAttribute("data-background-color"), renderHTML: (attributes) => ({ - "data-background-color": attributes.color, + "data-background-color": attributes.stringValue, }), }, }; @@ -34,7 +25,9 @@ export const BackgroundColorMark = Mark.create({ } if (element.hasAttribute("data-background-color")) { - return { color: element.getAttribute("data-background-color") }; + return { + stringValue: element.getAttribute("data-background-color"), + }; } return false; @@ -46,18 +39,4 @@ export const BackgroundColorMark = Mark.create({ renderHTML({ HTMLAttributes }) { return ["span", HTMLAttributes, 0]; }, - - addCommands() { - return { - setBackgroundColor: - (color) => - ({ commands }) => { - if (color !== defaultProps.backgroundColor.default) { - return commands.setMark(this.name, { color: color }); - } - - return commands.unsetMark(this.name); - }, - }; - }, }); diff --git a/packages/core/src/extensions/TextColor/TextColorExtension.ts b/packages/core/src/extensions/TextColor/TextColorExtension.ts index a3ab7b8db8..40be0046cd 100644 --- a/packages/core/src/extensions/TextColor/TextColorExtension.ts +++ b/packages/core/src/extensions/TextColor/TextColorExtension.ts @@ -1,15 +1,6 @@ import { Extension } from "@tiptap/core"; -import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { defaultProps } from "../Blocks/api/defaultProps"; -declare module "@tiptap/core" { - interface Commands { - blockTextColor: { - setBlockTextColor: (posInBlock: number, color: string) => ReturnType; - }; - } -} - export const TextColorExtension = Extension.create({ name: "blockTextColor", @@ -25,31 +16,12 @@ export const TextColorExtension = Extension.create({ ? element.getAttribute("data-text-color") : defaultProps.textColor.default, renderHTML: (attributes) => - attributes.textColor !== defaultProps.textColor.default && { - "data-text-color": attributes.textColor, + attributes.stringValue !== defaultProps.textColor.default && { + "data-text-color": attributes.stringValue, }, }, }, }, ]; }, - - addCommands() { - return { - setBlockTextColor: - (posInBlock, color) => - ({ state, view }) => { - const blockInfo = getBlockInfoFromPos(state.doc, posInBlock); - if (blockInfo === undefined) { - return false; - } - - state.tr.setNodeAttribute(blockInfo.startPos - 1, "textColor", color); - - view.focus(); - - return true; - }, - }; - }, }); diff --git a/packages/core/src/extensions/TextColor/TextColorMark.ts b/packages/core/src/extensions/TextColor/TextColorMark.ts index ce8a0cb4ca..9737985f9c 100644 --- a/packages/core/src/extensions/TextColor/TextColorMark.ts +++ b/packages/core/src/extensions/TextColor/TextColorMark.ts @@ -1,24 +1,15 @@ import { Mark } from "@tiptap/core"; -import { defaultProps } from "../Blocks/api/defaultProps"; - -declare module "@tiptap/core" { - interface Commands { - textColor: { - setTextColor: (color: string) => ReturnType; - }; - } -} export const TextColorMark = Mark.create({ name: "textColor", addAttributes() { return { - color: { + stringValue: { default: undefined, parseHTML: (element) => element.getAttribute("data-text-color"), renderHTML: (attributes) => ({ - "data-text-color": attributes.color, + "data-text-color": attributes.stringValue, }), }, }; @@ -34,7 +25,7 @@ export const TextColorMark = Mark.create({ } if (element.hasAttribute("data-text-color")) { - return { color: element.getAttribute("data-text-color") }; + return { stringValue: element.getAttribute("data-text-color") }; } return false; @@ -46,18 +37,4 @@ export const TextColorMark = Mark.create({ renderHTML({ HTMLAttributes }) { return ["span", HTMLAttributes, 0]; }, - - addCommands() { - return { - setTextColor: - (color) => - ({ commands }) => { - if (color !== defaultProps.textColor.default) { - return commands.setMark(this.name, { color: color }); - } - - return commands.unsetMark(this.name); - }, - }; - }, }); diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx index 6fcd751700..0e63628042 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -37,6 +37,7 @@ export const ColorStyleButton = (props: { const setTextColor = useCallback( (color: string) => { props.editor.focus(); + debugger; color === "default" ? props.editor.removeStyles({ textColor: color }) : props.editor.addStyles({ textColor: color }); diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 9482273df1..e0635fb655 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -4,6 +4,7 @@ import { BlockSchema, BlockSpecs, defaultBlockSpecs, + getBlockSchemaFromSpecs, } from "@blocknote/core"; import { StyleSchema, @@ -24,7 +25,9 @@ const initEditor = < ) => BlockNoteEditor.create({ slashMenuItems: getDefaultReactSlashMenuItems( - options.blockSpecs || (defaultBlockSpecs as any) + getBlockSchemaFromSpecs( + options.blockSpecs || defaultBlockSpecs + ) as BSchema ), ...options, }); From 2bc1cd9a25f7b2297ea4bbb0b7326c35523f2d37 Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 20 Nov 2023 22:19:56 +0100 Subject: [PATCH 03/31] fix tests --- .../extensions/BackgroundColor/BackgroundColorExtension.ts | 4 ++-- packages/core/src/extensions/TextColor/TextColorExtension.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts index 66d4df7203..3f24ecdfea 100644 --- a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts +++ b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts @@ -16,9 +16,9 @@ export const BackgroundColorExtension = Extension.create({ ? element.getAttribute("data-background-color") : defaultProps.backgroundColor.default, renderHTML: (attributes) => - attributes.stringValue !== + attributes.backgroundColor !== defaultProps.backgroundColor.default && { - "data-background-color": attributes.stringValue, + "data-background-color": attributes.backgroundColor, }, }, }, diff --git a/packages/core/src/extensions/TextColor/TextColorExtension.ts b/packages/core/src/extensions/TextColor/TextColorExtension.ts index 40be0046cd..09a5d894f4 100644 --- a/packages/core/src/extensions/TextColor/TextColorExtension.ts +++ b/packages/core/src/extensions/TextColor/TextColorExtension.ts @@ -16,8 +16,8 @@ export const TextColorExtension = Extension.create({ ? element.getAttribute("data-text-color") : defaultProps.textColor.default, renderHTML: (attributes) => - attributes.stringValue !== defaultProps.textColor.default && { - "data-text-color": attributes.stringValue, + attributes.textColor !== defaultProps.textColor.default && { + "data-text-color": attributes.textColor, }, }, }, From 5f907c342058eec14e9d0a0920788ef269553197 Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 20 Nov 2023 22:23:55 +0100 Subject: [PATCH 04/31] simplify PartialInlineContent --- packages/core/src/api/nodeConversions/nodeConversions.ts | 6 ++++-- packages/core/src/api/nodeConversions/testUtil.ts | 8 +++++--- packages/core/src/extensions/Blocks/api/blockTypes.ts | 4 ++-- .../core/src/extensions/Blocks/api/inlineContentTypes.ts | 7 ++++++- .../components/DefaultButtons/ColorStyleButton.tsx | 1 - 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 84781be8df..c93aa639a0 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -120,14 +120,16 @@ function styledTextArrayToNodes( * converts an array of inline content elements to prosemirror nodes */ export function inlineContentToNodes( - blockContent: PartialInlineContent[], + blockContent: PartialInlineContent, schema: Schema, styleSchema: S ): Node[] { const nodes: Node[] = []; for (const content of blockContent) { - if (content.type === "link") { + if (typeof content === "string") { + nodes.push(...styledTextArrayToNodes(content, schema, styleSchema)); + } else if (content.type === "link") { nodes.push(...linkToNodes(content, schema, styleSchema)); } else if (content.type === "text") { nodes.push(...styledTextArrayToNodes([content], schema, styleSchema)); diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index 40a7769906..eb052f1bf0 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -27,15 +27,17 @@ function textShorthandToStyledText( } function partialContentToInlineContent( - content: string | PartialInlineContent[] | TableContent = "" + content: PartialInlineContent | TableContent = "" ): InlineContent[] | TableContent { if (typeof content === "string") { return textShorthandToStyledText(content); } if (Array.isArray(content)) { - return content.map((partialContent) => { - if (partialContent.type === "link") { + return content.flatMap((partialContent) => { + if (typeof partialContent === "string") { + return textShorthandToStyledText(partialContent); + } else if (partialContent.type === "link") { return { ...partialContent, content: textShorthandToStyledText(partialContent.content), diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index 6c212f86a6..0bb78ecdeb 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -146,7 +146,7 @@ export type TableContent = { export type PartialTableContent = { type: "tableContent"; rows: { - cells: (PartialInlineContent[] | string)[]; + cells: PartialInlineContent[]; }[]; }; @@ -210,7 +210,7 @@ type PartialBlockFromConfigNoChildren< type?: B["type"]; props?: Partial>; content?: B["content"] extends "inline" - ? PartialInlineContent[] | string + ? PartialInlineContent : B["content"] extends "table" ? PartialTableContent : undefined; diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts index e83d4b3ea1..d37b8330ed 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts @@ -17,6 +17,11 @@ export type PartialLink = Omit, "content"> & { }; export type InlineContent = StyledText | Link; -export type PartialInlineContent = +type PartialInlineContentElement = + | string | StyledText | PartialLink; + +export type PartialInlineContent = + | PartialInlineContentElement[] + | string; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx index 0e63628042..6fcd751700 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -37,7 +37,6 @@ export const ColorStyleButton = (props: { const setTextColor = useCallback( (color: string) => { props.editor.focus(); - debugger; color === "default" ? props.editor.removeStyles({ textColor: color }) : props.editor.addStyles({ textColor: color }); From 7d6a75b948600b06870b46a890c99062bee95f72 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 10:41:28 +0100 Subject: [PATCH 05/31] custom inline content --- examples/vanilla/src/ui/addSlashMenu.ts | 4 +- packages/core/src/BlockNoteEditor.ts | 105 ++++++++++---- packages/core/src/BlockNoteExtensions.ts | 4 +- .../blockManipulation.test.ts | 33 ++++- .../blockManipulation/blockManipulation.ts | 20 ++- .../nodeConversions/nodeConversions.test.ts | 130 ++++++------------ .../api/nodeConversions/nodeConversions.ts | 84 ++++++----- .../core/src/api/nodeConversions/testUtil.ts | 14 +- .../clipboardHandlerExtension.ts | 8 +- .../html/externalHTMLExporter.ts | 13 +- .../serialization/html/htmlConversion.test.ts | 104 +++++++++++--- .../html/internalHTMLSerializer.ts | 13 +- .../html/sharedHTMLConversion.ts | 4 +- .../core/src/extensions/Blocks/api/block.ts | 14 +- .../src/extensions/Blocks/api/blockTypes.ts | 114 +++++++++------ .../Blocks/api/cursorPositionTypes.ts | 8 +- .../src/extensions/Blocks/api/customBlocks.ts | 13 +- .../extensions/Blocks/api/defaultBlocks.ts | 20 +++ .../Blocks/api/inlineContentTypes.ts | 98 ++++++++++++- .../extensions/Blocks/api/selectionTypes.ts | 10 +- .../extensions/Blocks/nodes/BlockContainer.ts | 14 +- .../ImageBlockContent/ImageBlockContent.ts | 3 +- .../nodes/BlockContent/defaultBlockHelpers.ts | 7 +- .../FormattingToolbarPlugin.ts | 9 +- .../HyperlinkToolbarPlugin.ts | 6 +- .../ImageToolbar/ImageToolbarPlugin.ts | 22 +-- .../src/extensions/SideMenu/SideMenuPlugin.ts | 32 +++-- .../extensions/SlashMenu/BaseSlashMenuItem.ts | 12 +- .../extensions/SlashMenu/SlashMenuPlugin.ts | 8 +- .../SlashMenu/defaultSlashMenuItems.ts | 34 +++-- .../TableHandles/TableHandlesPlugin.ts | 23 ++-- .../plugins/suggestion/SuggestionPlugin.ts | 13 +- packages/react/src/BlockNoteView.tsx | 13 +- .../DefaultButtons/ColorStyleButton.tsx | 7 +- .../DefaultButtons/ImageCaptionButton.tsx | 4 +- .../DefaultButtons/TextAlignButton.tsx | 4 +- .../DefaultButtons/ToggledStyleButton.tsx | 9 +- .../DefaultDropdowns/BlockTypeDropdown.tsx | 4 +- .../FormattingToolbarPositioner.tsx | 4 +- .../components/DefaultHyperlinkToolbar.tsx | 5 +- .../components/HyperlinkToolbarPositioner.tsx | 10 +- .../components/DefaultImageToolbar.tsx | 26 ++-- .../components/ImageToolbarPositioner.tsx | 18 ++- packages/react/src/ReactBlockSpec.tsx | 16 ++- .../DefaultButtons/AddBlockButton.tsx | 4 +- .../components/DefaultButtons/DragHandle.tsx | 8 +- .../SideMenu/components/DefaultSideMenu.tsx | 15 +- .../DefaultButtons/BlockColorsButton.tsx | 14 +- .../DefaultButtons/RemoveBlockButton.tsx | 4 +- .../DragHandleMenu/DefaultDragHandleMenu.tsx | 6 +- .../DragHandleMenu/DragHandleMenu.tsx | 20 ++- .../components/SideMenuPositioner.tsx | 21 +-- .../react/src/SlashMenu/ReactSlashMenuItem.ts | 5 +- .../components/SlashMenuPositioner.tsx | 4 +- .../SlashMenu/defaultReactSlashMenuItems.tsx | 8 +- .../components/DefaultTableHandle.tsx | 12 +- .../components/TableHandlePositioner.tsx | 21 +-- packages/react/src/hooks/useBlockNote.ts | 23 +++- packages/react/src/hooks/useEditorChange.ts | 2 +- .../react/src/hooks/useEditorContentChange.ts | 8 +- .../src/hooks/useEditorSelectionChange.ts | 8 +- packages/react/src/hooks/useSelectedBlocks.ts | 12 +- packages/react/src/htmlConversion.test.tsx | 21 ++- 63 files changed, 885 insertions(+), 447 deletions(-) diff --git a/examples/vanilla/src/ui/addSlashMenu.ts b/examples/vanilla/src/ui/addSlashMenu.ts index 3ecbd7fc46..936fcbcb75 100644 --- a/examples/vanilla/src/ui/addSlashMenu.ts +++ b/examples/vanilla/src/ui/addSlashMenu.ts @@ -9,8 +9,8 @@ export const addSlashMenu = (editor: BlockNoteEditor) => { let element: HTMLElement; function updateItems( - items: BaseSlashMenuItem[], - onClick: (item: BaseSlashMenuItem) => void, + items: BaseSlashMenuItem[], + onClick: (item: BaseSlashMenuItem) => void, selected: number ) { element.innerHTML = ""; diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 93a7af1d9f..071a425d4e 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -26,11 +26,14 @@ import { import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; import { DefaultBlockSchema, + DefaultInlineContentSchema, DefaultStyleSchema, defaultBlockSchema, defaultBlockSpecs, + defaultInlineContentSpecs, defaultStyleSpecs, getBlockSchemaFromSpecs, + getInlineContentSchemaFromSpecs, getStyleSchemaFromSpecs, } from "./extensions/Blocks/api/defaultBlocks"; import { Selection } from "./extensions/Blocks/api/selectionTypes"; @@ -43,6 +46,10 @@ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFro import "prosemirror-tables/style/tables.css"; import "./editor.css"; +import { + InlineContentSchema, + InlineContentSpecs, +} from "./extensions/Blocks/api/inlineContentTypes"; import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; import { HyperlinkToolbarProsemirrorPlugin } from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin"; @@ -56,10 +63,14 @@ import { UnreachableCaseError, mergeCSSClasses } from "./shared/utils"; export type BlockNoteEditorOptions< BSpecs extends BlockSpecs, + ISpecs extends InlineContentSpecs, SSpecs extends StyleSpecs, BSchema extends BlockSchema = { [key in keyof BSpecs]: BSpecs[key]["config"]; }, + ISchema extends InlineContentSchema = { + [key in keyof ISpecs]: ISpecs[key]["config"]; + }, SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } > = { // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them. @@ -70,7 +81,7 @@ export type BlockNoteEditorOptions< * * @default defaultSlashMenuItems from `./extensions/SlashMenu` */ - slashMenuItems: BaseSlashMenuItem[]; + slashMenuItems: BaseSlashMenuItem[]; /** * The HTML element that should be used as the parent element for the editor. @@ -87,16 +98,18 @@ export type BlockNoteEditorOptions< /** * A callback function that runs when the editor is ready to be used. */ - onEditorReady: (editor: BlockNoteEditor) => void; + onEditorReady: (editor: BlockNoteEditor) => void; /** * A callback function that runs whenever the editor's contents change. */ - onEditorContentChange: (editor: BlockNoteEditor) => void; + onEditorContentChange: ( + editor: BlockNoteEditor + ) => void; /** * A callback function that runs whenever the text cursor position changes. */ onTextCursorPositionChange: ( - editor: BlockNoteEditor + editor: BlockNoteEditor ) => void; /** * Locks the editor from being editable by the user if set to `false`. @@ -105,7 +118,7 @@ export type BlockNoteEditorOptions< /** * The content that should be in the editor when it's created, represented as an array of partial block objects. */ - initialContent: PartialBlock[]; + initialContent: PartialBlock[]; /** * Use default BlockNote font and reset the styles of

  • elements etc., that are used in BlockNote. * @@ -120,6 +133,8 @@ export type BlockNoteEditorOptions< styleSpecs: SSpecs; + inlineContentSpecs: ISpecs; + /** * A custom function to handle file uploads. * @param file The file that should be uploaded. @@ -174,44 +189,65 @@ const blockNoteTipTapOptions = { export class BlockNoteEditor< BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, SSchema extends StyleSchema = DefaultStyleSchema > { public readonly _tiptapEditor: TiptapEditor & { contentComponent: any }; public blockCache = new WeakMap>(); public readonly blockSchema: BSchema; + public readonly inlineContentSchema: ISchema; public readonly styleSchema: SSchema; public readonly blockImplementations: BlockSpecs; + public readonly inlineContentImplementations: InlineContentSpecs; public readonly styleImplementations: StyleSpecs; public ready = false; - public readonly sideMenu: SideMenuProsemirrorPlugin; + public readonly sideMenu: SideMenuProsemirrorPlugin< + BSchema, + ISchema, + SSchema + >; public readonly formattingToolbar: FormattingToolbarProsemirrorPlugin; - public readonly slashMenu: SlashMenuProsemirrorPlugin; + public readonly slashMenu: SlashMenuProsemirrorPlugin< + BSchema, + ISchema, + SSchema, + any + >; public readonly hyperlinkToolbar: HyperlinkToolbarProsemirrorPlugin< BSchema, + ISchema, + SSchema + >; + public readonly imageToolbar: ImageToolbarProsemirrorPlugin< + BSchema, + ISchema, SSchema >; - public readonly imageToolbar: ImageToolbarProsemirrorPlugin; public readonly tableHandles: - | TableHandlesProsemirrorPlugin + | TableHandlesProsemirrorPlugin | undefined; public readonly uploadFile: ((file: File) => Promise) | undefined; public static create< BSpecs extends BlockSpecs = typeof defaultBlockSpecs, + ISpecs extends InlineContentSpecs = typeof defaultInlineContentSpecs, SSpecs extends StyleSpecs = typeof defaultStyleSpecs, BSchema extends BlockSchema = { [key in keyof BSpecs]: BSpecs[key]["config"]; }, + ISchema extends InlineContentSchema = { + [key in keyof ISpecs]: ISpecs[key]["config"]; + }, SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"]; } >( options: Partial< - BlockNoteEditorOptions + BlockNoteEditorOptions > = {} ) { return new BlockNoteEditor(options); @@ -219,32 +255,46 @@ export class BlockNoteEditor< private constructor( private readonly options: Partial< - BlockNoteEditorOptions + BlockNoteEditorOptions< + BlockSpecs, + InlineContentSpecs, + StyleSpecs, + BSchema, + ISchema, + SSchema + > > ) { // apply defaults const newOptions: Omit< typeof options, - "defaultStyles" | "blockSpecs" | "styleSpecs" + "defaultStyles" | "blockSpecs" | "styleSpecs" | "inlineContentSpecs" > & { defaultStyles: boolean; blockSpecs: BlockSpecs; + inlineContentSpecs: InlineContentSpecs; styleSpecs: StyleSpecs; } = { defaultStyles: true, blockSpecs: options.blockSpecs || defaultBlockSpecs, styleSpecs: options.styleSpecs || defaultStyleSpecs, + inlineContentSpecs: + options.inlineContentSpecs || defaultInlineContentSpecs, ...options, }; this.blockSchema = getBlockSchemaFromSpecs(newOptions.blockSpecs) as any; + this.inlineContentSchema = getInlineContentSchemaFromSpecs( + newOptions.inlineContentSpecs + ) as any; this.styleSchema = getStyleSchemaFromSpecs(newOptions.styleSpecs) as any; this.sideMenu = new SideMenuProsemirrorPlugin(this); this.formattingToolbar = new FormattingToolbarProsemirrorPlugin(this); this.slashMenu = new SlashMenuProsemirrorPlugin( this, - newOptions.slashMenuItems || getDefaultSlashMenuItems(this.blockSchema) + newOptions.slashMenuItems || + (getDefaultSlashMenuItems(this.blockSchema) as any) ); this.hyperlinkToolbar = new HyperlinkToolbarProsemirrorPlugin(this); this.imageToolbar = new ImageToolbarProsemirrorPlugin(this); @@ -278,6 +328,7 @@ export class BlockNoteEditor< extensions.push(blockNoteUIExtension); this.blockImplementations = newOptions.blockSpecs; + this.inlineContentImplementations = newOptions.inlineContentSpecs; this.styleImplementations = newOptions.styleSpecs; this.uploadFile = newOptions.uploadFile; @@ -407,8 +458,8 @@ export class BlockNoteEditor< * Gets a snapshot of all top-level (non-nested) blocks in the editor. * @returns A snapshot of all top-level (non-nested) blocks in the editor. */ - public get topLevelBlocks(): Block[] { - const blocks: Block[] = []; + public get topLevelBlocks(): Block[] { + const blocks: Block[] = []; this._tiptapEditor.state.doc.firstChild!.descendants((node) => { blocks.push( @@ -428,12 +479,12 @@ export class BlockNoteEditor< */ public getBlock( blockIdentifier: BlockIdentifier - ): Block | undefined { + ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - let newBlock: Block | undefined = undefined; + let newBlock: Block | undefined = undefined; this._tiptapEditor.state.doc.firstChild!.descendants((node) => { if (typeof newBlock !== "undefined") { @@ -463,7 +514,7 @@ export class BlockNoteEditor< * @param reverse Whether the blocks should be traversed in reverse order. */ public forEachBlock( - callback: (block: Block) => boolean, + callback: (block: Block) => boolean, reverse = false ): void { const blocks = this.topLevelBlocks.slice(); @@ -473,7 +524,7 @@ export class BlockNoteEditor< } function traverseBlockArray( - blockArray: Block[] + blockArray: Block[] ): boolean { for (const block of blockArray) { if (!callback(block)) { @@ -515,7 +566,11 @@ export class BlockNoteEditor< * Gets a snapshot of the current text cursor position. * @returns A snapshot of the current text cursor position. */ - public getTextCursorPosition(): TextCursorPosition { + public getTextCursorPosition(): TextCursorPosition< + BSchema, + ISchema, + SSchema + > { const { node, depth, startPos, endPos } = getBlockInfoFromPos( this._tiptapEditor.state.doc, this._tiptapEditor.state.selection.from @@ -620,7 +675,7 @@ export class BlockNoteEditor< /** * Gets a snapshot of the current selection. */ - public getSelection(): Selection | undefined { + public getSelection(): Selection | undefined { // Either the TipTap selection is empty, or it's a node selection. In either // case, it only spans one block, so we return undefined. if ( @@ -631,7 +686,7 @@ export class BlockNoteEditor< return undefined; } - const blocks: Block[] = []; + const blocks: Block[] = []; // TODO: This adds all child blocks to the same array. Needs to find min // depth and only add blocks at that depth. @@ -687,7 +742,7 @@ export class BlockNoteEditor< * `referenceBlock`. Inserts the blocks at the start of the existing block's children if "nested" is used. */ public insertBlocks( - blocksToInsert: PartialBlock[], + blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before" ): void { @@ -703,7 +758,7 @@ export class BlockNoteEditor< */ public updateBlock( blockToUpdate: BlockIdentifier, - update: PartialBlock + update: PartialBlock ) { updateBlock(blockToUpdate, update, this._tiptapEditor); } @@ -725,7 +780,7 @@ export class BlockNoteEditor< */ public replaceBlocks( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[] + blocksToInsert: PartialBlock[] ) { replaceBlocks(blocksToRemove, blocksToInsert, this); } diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 3b6e000b44..9c2df8b090 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -25,6 +25,7 @@ import { BlockSchema, BlockSpecs, } from "./extensions/Blocks/api/blockTypes"; +import { InlineContentSchema } from "./extensions/Blocks/api/inlineContentTypes"; import { StyleSchema } from "./extensions/Blocks/api/styles"; import { TableExtension } from "./extensions/Blocks/nodes/BlockContent/TableBlockContent/TableExtension"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; @@ -39,9 +40,10 @@ import UniqueID from "./extensions/UniqueID/UniqueID"; */ export const getBlockNoteExtensions = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >(opts: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; domAttributes: Partial; blockSchema: BSchema; blockSpecs: BlockSpecs; diff --git a/packages/core/src/api/blockManipulation/blockManipulation.test.ts b/packages/core/src/api/blockManipulation/blockManipulation.test.ts index 35cfb559f7..7f075d4e47 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.test.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.test.ts @@ -1,5 +1,12 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { Block, BlockNoteEditor, PartialBlock } from "../.."; +import { + Block, + BlockNoteEditor, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + PartialBlock, +} from "../.."; let editor: BlockNoteEditor; @@ -14,11 +21,25 @@ function waitForEditor() { }); } -let singleBlock: PartialBlock; - -let multipleBlocks: PartialBlock[]; - -let insert: (placement: "before" | "nested" | "after") => Block[]; +let singleBlock: PartialBlock< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema +>; + +let multipleBlocks: PartialBlock< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema +>[]; + +let insert: ( + placement: "before" | "nested" | "after" +) => Block< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema +>[]; beforeEach(() => { editor = BlockNoteEditor.create(); diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index 3b32ba32b8..f195aa1472 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -1,6 +1,6 @@ import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; -import { BlockNoteEditor } from "../.."; +import { BlockNoteEditor, InlineContentSchema } from "../.."; import { BlockIdentifier, BlockSchema, @@ -12,12 +12,13 @@ import { getNodeById } from "../util/nodeUtil"; export function insertBlocks< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( - blocksToInsert: PartialBlock[], + blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before", - editor: BlockNoteEditor + editor: BlockNoteEditor ): void { const ttEditor = editor._tiptapEditor; @@ -66,9 +67,13 @@ export function insertBlocks< ttEditor.view.dispatch(ttEditor.state.tr.insert(insertionPos, nodesToInsert)); } -export function updateBlock( +export function updateBlock< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( blockToUpdate: BlockIdentifier, - update: PartialBlock, + update: PartialBlock, editor: Editor ) { const id = @@ -127,11 +132,12 @@ export function removeBlocks( export function replaceBlocks< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[], - editor: BlockNoteEditor + blocksToInsert: PartialBlock[], + editor: BlockNoteEditor ) { insertBlocks(blocksToInsert, blocksToRemove[0], "before", editor); removeBlocks(blocksToRemove, editor._tiptapEditor); diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index 666308701a..ddf1e54738 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -2,8 +2,6 @@ import { Editor } from "@tiptap/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor, PartialBlock } from "../.."; import { - DefaultBlockSchema, - DefaultStyleSchema, defaultBlockSchema, defaultStyleSchema, } from "../../extensions/Blocks/api/defaultBlocks"; @@ -27,13 +25,14 @@ afterEach(() => { describe("Simple ProseMirror Node Conversions", () => { it("Convert simple block to node", async () => { - const block: PartialBlock = { + const block: PartialBlock = { type: "paragraph", }; - const firstNodeConversion = blockToNode< - DefaultBlockSchema, - DefaultStyleSchema - >(block, tt.schema, defaultStyleSchema); + const firstNodeConversion = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(firstNodeConversion).toMatchSnapshot(); }); @@ -51,10 +50,11 @@ describe("Simple ProseMirror Node Conversions", () => { expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode< - DefaultBlockSchema, - DefaultStyleSchema - >(firstBlockConversion, tt.schema, defaultStyleSchema); + const firstNodeConversion = blockToNode( + firstBlockConversion, + tt.schema, + defaultStyleSchema + ); expect(firstNodeConversion).toStrictEqual(node); }); @@ -62,7 +62,7 @@ describe("Simple ProseMirror Node Conversions", () => { describe("Complex ProseMirror Node Conversions", () => { it("Convert complex block to node", async () => { - const block: PartialBlock = { + const block: PartialBlock = { type: "heading", props: { backgroundColor: "blue", @@ -102,10 +102,11 @@ describe("Complex ProseMirror Node Conversions", () => { }, ], }; - const firstNodeConversion = blockToNode< - DefaultBlockSchema, - DefaultStyleSchema - >(block, tt.schema, defaultStyleSchema); + const firstNodeConversion = blockToNode( + block, + tt.schema, + defaultStyleSchema + ); expect(firstNodeConversion).toMatchSnapshot(); }); @@ -156,10 +157,11 @@ describe("Complex ProseMirror Node Conversions", () => { expect(firstBlockConversion).toMatchSnapshot(); - const firstNodeConversion = blockToNode< - DefaultBlockSchema, - DefaultStyleSchema - >(firstBlockConversion, tt.schema, defaultStyleSchema); + const firstNodeConversion = blockToNode( + firstBlockConversion, + tt.schema, + defaultStyleSchema + ); expect(firstNodeConversion).toStrictEqual(node); }); @@ -167,7 +169,7 @@ describe("Complex ProseMirror Node Conversions", () => { describe("links", () => { it("Convert a block with link", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -178,11 +180,7 @@ describe("links", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -200,7 +198,7 @@ describe("links", () => { }); it("Convert link block with marks", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -224,11 +222,7 @@ describe("links", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); // expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -246,7 +240,7 @@ describe("links", () => { }); it("Convert two adjacent links in a block", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -263,11 +257,7 @@ describe("links", () => { ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -287,7 +277,7 @@ describe("links", () => { describe("hard breaks", () => { it("Convert a block with a hard break", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -298,11 +288,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -320,7 +306,7 @@ describe("hard breaks", () => { }); it("Convert a block with multiple hard breaks", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -331,11 +317,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -353,7 +335,7 @@ describe("hard breaks", () => { }); it("Convert a block with a hard break at the start", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -364,11 +346,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -386,7 +364,7 @@ describe("hard breaks", () => { }); it("Convert a block with a hard break at the end", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -397,11 +375,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -419,7 +393,7 @@ describe("hard breaks", () => { }); it("Convert a block with only a hard break", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -430,11 +404,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -452,7 +422,7 @@ describe("hard breaks", () => { }); it("Convert a block with a hard break and different styles", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -468,11 +438,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -490,7 +456,7 @@ describe("hard breaks", () => { }); it("Convert a block with a hard break in a link", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -501,11 +467,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, @@ -523,7 +485,7 @@ describe("hard breaks", () => { }); it("Convert a block with a hard break between links", async () => { - const block: PartialBlock = { + const block: PartialBlock = { id: UniqueID.options.generateID(), type: "paragraph", content: [ @@ -539,11 +501,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); + const node = blockToNode(block, tt.schema, defaultStyleSchema); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock( node, diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index c93aa639a0..f2b9f56704 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -9,9 +9,13 @@ import { } from "../../extensions/Blocks/api/blockTypes"; import { InlineContent, + InlineContentSchema, PartialInlineContent, PartialLink, StyledText, + isLinkInlineContent, + isPartialLinkInlineContent, + isStyledTextInlineContent, } from "../../extensions/Blocks/api/inlineContentTypes"; import { StyleSchema, Styles } from "../../extensions/Blocks/api/styles"; import { getBlockInfo } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; @@ -119,8 +123,11 @@ function styledTextArrayToNodes( /** * converts an array of inline content elements to prosemirror nodes */ -export function inlineContentToNodes( - blockContent: PartialInlineContent, +export function inlineContentToNodes< + I extends InlineContentSchema, + S extends StyleSchema +>( + blockContent: PartialInlineContent, schema: Schema, styleSchema: S ): Node[] { @@ -129,12 +136,14 @@ export function inlineContentToNodes( for (const content of blockContent) { if (typeof content === "string") { nodes.push(...styledTextArrayToNodes(content, schema, styleSchema)); - } else if (content.type === "link") { + } else if (isPartialLinkInlineContent(content)) { nodes.push(...linkToNodes(content, schema, styleSchema)); - } else if (content.type === "text") { + } else if (isStyledTextInlineContent(content)) { nodes.push(...styledTextArrayToNodes([content], schema, styleSchema)); } else { - throw new UnreachableCaseError(content); + // TODO + // let s = content.type; + // throw new UnreachableCaseError(content); } } return nodes; @@ -143,8 +152,11 @@ export function inlineContentToNodes( /** * converts an array of inline content elements to prosemirror nodes */ -export function tableContentToNodes( - tableContent: PartialTableContent, +export function tableContentToNodes< + I extends InlineContentSchema, + S extends StyleSchema +>( + tableContent: PartialTableContent, schema: Schema, styleSchema: StyleSchema ): Node[] { @@ -175,10 +187,10 @@ export function tableContentToNodes( /** * Converts a BlockNote block to a TipTap node. */ -export function blockToNode( - block: PartialBlock, +export function blockToNode( + block: PartialBlock, schema: Schema, - styleSchema: S + styleSchema: StyleSchema ) { let id = block.id; @@ -233,17 +245,17 @@ export function blockToNode( /** * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent */ -function contentNodeToTableContent( - contentNode: Node, - styleSchema: S -) { - const ret: TableContent = { +function contentNodeToTableContent< + I extends InlineContentSchema, + S extends StyleSchema +>(contentNode: Node, styleSchema: S) { + const ret: TableContent = { type: "tableContent", rows: [], }; contentNode.content.forEach((rowNode) => { - const row: TableContent["rows"][0] = { + const row: TableContent["rows"][0] = { cells: [], }; @@ -262,12 +274,12 @@ function contentNodeToTableContent( /** * Converts an internal (prosemirror) content node to a BlockNote InlineContent array. */ -function contentNodeToInlineContent( - contentNode: Node, - styleSchema: S -) { - const content: InlineContent[] = []; - let currentContent: InlineContent | undefined = undefined; +function contentNodeToInlineContent< + I extends InlineContentSchema, + S extends StyleSchema +>(contentNode: Node, styleSchema: S) { + const content: InlineContent[] = []; + let currentContent: InlineContent | undefined = undefined; // Most of the logic below is for handling links because in ProseMirror links are marks // while in BlockNote links are a type of inline content @@ -277,13 +289,15 @@ function contentNodeToInlineContent( if (node.type.name === "hardBreak") { if (currentContent) { // Current content exists. - if (currentContent.type === "text") { + if (isStyledTextInlineContent(currentContent)) { // Current content is text. currentContent.text += "\n"; - } else if (currentContent.type === "link") { + } else if (isLinkInlineContent(currentContent)) { // Current content is a link. currentContent.content[currentContent.content.length - 1].text += "\n"; + } else { + // TODO } } else { // Current content does not exist. @@ -322,7 +336,7 @@ function contentNodeToInlineContent( // Current content exists. if (currentContent) { // Current content is text. - if (currentContent.type === "text") { + if (isStyledTextInlineContent(currentContent)) { if (!linkMark) { // Node is text (same type as current content). if ( @@ -354,7 +368,7 @@ function contentNodeToInlineContent( ], }; } - } else if (currentContent.type === "link") { + } else if (isLinkInlineContent(currentContent)) { // Current content is a link. if (linkMark) { // Node is a link (same type as current content). @@ -400,6 +414,8 @@ function contentNodeToInlineContent( styles, }; } + } else { + // TODO } } // Current content does not exist. @@ -439,12 +455,16 @@ function contentNodeToInlineContent( /** * Convert a TipTap node to a BlockNote block. */ -export function nodeToBlock( +export function nodeToBlock< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( node: Node, blockSchema: BSchema, styleSchema: S, - blockCache?: WeakMap> -): Block { + blockCache?: WeakMap> +): Block { if (node.type.name !== "blockContainer") { throw Error( "Node must be of type blockContainer, but is of type" + @@ -490,7 +510,7 @@ export function nodeToBlock( const blockConfig = blockSchema[blockInfo.contentType.name]; - const children: Block[] = []; + const children: Block[] = []; for (let i = 0; i < blockInfo.numChildBlocks; i++) { children.push( nodeToBlock( @@ -502,7 +522,7 @@ export function nodeToBlock( ); } - let content: Block["content"]; + let content: Block["content"]; if (blockConfig.content === "inline") { content = contentNodeToInlineContent(blockInfo.contentNode, styleSchema); @@ -520,7 +540,7 @@ export function nodeToBlock( props, content, children, - } as Block; + } as Block; blockCache?.set(node, block); diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index eb052f1bf0..c2c473bbcc 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -6,8 +6,10 @@ import { } from "../../extensions/Blocks/api/blockTypes"; import { InlineContent, + InlineContentSchema, PartialInlineContent, StyledText, + isPartialLinkInlineContent, } from "../../extensions/Blocks/api/inlineContentTypes"; import { StyleSchema } from "../../extensions/Blocks/api/styles"; @@ -27,8 +29,8 @@ function textShorthandToStyledText( } function partialContentToInlineContent( - content: PartialInlineContent | TableContent = "" -): InlineContent[] | TableContent { + content: PartialInlineContent | TableContent = "" +): InlineContent[] | TableContent { if (typeof content === "string") { return textShorthandToStyledText(content); } @@ -37,12 +39,13 @@ function partialContentToInlineContent( return content.flatMap((partialContent) => { if (typeof partialContent === "string") { return textShorthandToStyledText(partialContent); - } else if (partialContent.type === "link") { + } else if (isPartialLinkInlineContent(partialContent)) { return { ...partialContent, content: textShorthandToStyledText(partialContent.content), }; } else { + // TODO? return partialContent; } }); @@ -53,9 +56,10 @@ function partialContentToInlineContent( export function partialBlockToBlockForTesting< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema ->(partialBlock: PartialBlock): Block { - const withDefaults: Block = { +>(partialBlock: PartialBlock): Block { + const withDefaults: Block = { id: "", type: "paragraph", // because at this point we don't have an easy way to access default props at runtime, diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/serialization/clipboardHandlerExtension.ts index a4f3ec68c7..60a7484515 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/serialization/clipboardHandlerExtension.ts @@ -1,5 +1,6 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; +import { InlineContentSchema } from "../.."; import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BlockSchema } from "../../extensions/Blocks/api/blockTypes"; import { StyleSchema } from "../../extensions/Blocks/api/styles"; @@ -15,11 +16,12 @@ const acceptedMIMETypes = [ export const createClipboardHandlerExtension = < BSchema extends BlockSchema, - SSchema extends StyleSchema + I extends InlineContentSchema, + S extends StyleSchema >( - editor: BlockNoteEditor + editor: BlockNoteEditor ) => - Extension.create<{ editor: BlockNoteEditor }, undefined>({ + Extension.create<{ editor: BlockNoteEditor }, undefined>({ addProseMirrorPlugins() { const tiptap = this.editor; const schema = this.editor.schema; diff --git a/packages/core/src/api/serialization/html/externalHTMLExporter.ts b/packages/core/src/api/serialization/html/externalHTMLExporter.ts index f6c591d995..b9ed6badd9 100644 --- a/packages/core/src/api/serialization/html/externalHTMLExporter.ts +++ b/packages/core/src/api/serialization/html/externalHTMLExporter.ts @@ -2,6 +2,7 @@ import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; import { unified } from "unified"; +import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, @@ -36,19 +37,21 @@ import { // start/end of a block. export interface ExternalHTMLExporter< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > { - exportBlocks: (blocks: PartialBlock[]) => string; + exportBlocks: (blocks: PartialBlock[]) => string; exportProseMirrorFragment: (fragment: Fragment) => string; } export const createExternalHTMLExporter = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( schema: Schema, - editor: BlockNoteEditor -): ExternalHTMLExporter => { + editor: BlockNoteEditor +): ExternalHTMLExporter => { const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { serializeNodeInner: ( node: Node, @@ -57,7 +60,7 @@ export const createExternalHTMLExporter = < // TODO: Should not be async, but is since we're using a rehype plugin to // convert internal HTML to external HTML. exportProseMirrorFragment: (fragment: Fragment) => string; - exportBlocks: (blocks: PartialBlock[]) => string; + exportBlocks: (blocks: PartialBlock[]) => string; }; serializer.serializeNodeInner = ( @@ -81,7 +84,7 @@ export const createExternalHTMLExporter = < return externalHTML.value as string; }; - serializer.exportBlocks = (blocks: PartialBlock[]) => { + serializer.exportBlocks = (blocks: PartialBlock[]) => { const nodes = blocks.map((block) => blockToNode(block, schema, editor.styleSchema) ); diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index 97a1385ec2..f515197962 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -8,7 +8,11 @@ import { PartialBlock, } from "../../../extensions/Blocks/api/blockTypes"; import { createBlockSpec } from "../../../extensions/Blocks/api/customBlocks"; -import { defaultBlockSpecs } from "../../../extensions/Blocks/api/defaultBlocks"; +import { + DefaultInlineContentSchema, + DefaultStyleSchema, + defaultBlockSpecs, +} from "../../../extensions/Blocks/api/defaultBlocks"; import { defaultProps } from "../../../extensions/Blocks/api/defaultProps"; import { imagePropSchema, @@ -86,7 +90,11 @@ const customSpecs = { type CustomSchemaType = BlockSchemaFromSpecs; -let editor: BlockNoteEditor; +let editor: BlockNoteEditor< + CustomSchemaType, + DefaultInlineContentSchema, + DefaultStyleSchema +>; let tt: Editor; beforeEach(() => { @@ -106,7 +114,7 @@ afterEach(() => { }); function convertToHTMLAndCompareSnapshots( - blocks: PartialBlock[], + blocks: PartialBlock[], snapshotDirectory: string, snapshotName: string ) { @@ -133,7 +141,11 @@ function convertToHTMLAndCompareSnapshots( describe("Convert paragraphs to HTML", () => { it("Convert paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "paragraph", content: "Paragraph", @@ -144,7 +156,11 @@ describe("Convert paragraphs to HTML", () => { }); it("Convert styled paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "paragraph", props: { @@ -188,7 +204,11 @@ describe("Convert paragraphs to HTML", () => { }); it("Convert nested paragraph to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "paragraph", content: "Paragraph", @@ -211,7 +231,11 @@ describe("Convert paragraphs to HTML", () => { describe("Convert images to HTML", () => { it("Convert add image button to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "image", }, @@ -221,7 +245,11 @@ describe("Convert images to HTML", () => { }); it("Convert image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "image", props: { @@ -236,7 +264,11 @@ describe("Convert images to HTML", () => { }); it("Convert nested image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "image", props: { @@ -263,7 +295,11 @@ describe("Convert images to HTML", () => { describe("Convert simple images to HTML", () => { it("Convert simple add image button to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleImage", }, @@ -273,7 +309,11 @@ describe("Convert simple images to HTML", () => { }); it("Convert simple image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleImage", props: { @@ -288,7 +328,11 @@ describe("Convert simple images to HTML", () => { }); it("Convert nested image to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleImage", props: { @@ -315,7 +359,11 @@ describe("Convert simple images to HTML", () => { describe("Convert custom blocks with inline content to HTML", () => { it("Convert custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "customParagraph", content: "Custom Paragraph", @@ -326,7 +374,11 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert styled custom block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "customParagraph", props: { @@ -370,7 +422,11 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert nested block with inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "customParagraph", content: "Custom Paragraph", @@ -393,7 +449,11 @@ describe("Convert custom blocks with inline content to HTML", () => { describe("Convert custom blocks with non-exported inline content to HTML", () => { it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleCustomParagraph", content: "Custom Paragraph", @@ -404,7 +464,11 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleCustomParagraph", props: { @@ -448,7 +512,11 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock[] = [ + const blocks: PartialBlock< + CustomSchemaType, + DefaultInlineContentSchema, + any + >[] = [ { type: "simpleCustomParagraph", content: "Custom Paragraph", diff --git a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts index 35e6a87b0f..a2542a97e2 100644 --- a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts @@ -1,4 +1,5 @@ import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model"; +import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, @@ -28,27 +29,29 @@ import { // `serializeBlocks`: Serializes an array of blocks to HTML. export interface InternalHTMLSerializer< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > { // TODO: Ideally we would expand the BlockNote API to support partial // selections so we don't need this. serializeProseMirrorFragment: (fragment: Fragment) => string; - serializeBlocks: (blocks: PartialBlock[]) => string; + serializeBlocks: (blocks: PartialBlock[]) => string; } export const createInternalHTMLSerializer = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( schema: Schema, - editor: BlockNoteEditor -): InternalHTMLSerializer => { + editor: BlockNoteEditor +): InternalHTMLSerializer => { const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { serializeNodeInner: ( node: Node, options: { document?: Document } ) => HTMLElement; - serializeBlocks: (blocks: PartialBlock[]) => string; + serializeBlocks: (blocks: PartialBlock[]) => string; serializeProseMirrorFragment: ( fragment: Fragment, options?: { document?: Document | undefined } | undefined, @@ -64,7 +67,7 @@ export const createInternalHTMLSerializer = < serializer.serializeProseMirrorFragment = (fragment: Fragment) => serializeProseMirrorFragment(fragment, serializer); - serializer.serializeBlocks = (blocks: PartialBlock[]) => { + serializer.serializeBlocks = (blocks: PartialBlock[]) => { const nodes = blocks.map((block) => blockToNode(block, schema, editor.styleSchema) ); diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index c5c1928c2d..696fd9a09d 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -1,4 +1,5 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; +import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; import { StyleSchema } from "../../../extensions/Blocks/api/styles"; @@ -16,12 +17,13 @@ function doc(options: { document?: Document }) { // `renderHTML` method. export const serializeNodeInner = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( node: Node, options: { document?: Document }, serializer: DOMSerializer, - editor: BlockNoteEditor, + editor: BlockNoteEditor, toExternalHTML: boolean ) => { const { dom, contentDOM } = DOMSerializer.renderSpec( diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 3b4a5793a8..064432ecff 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -13,6 +13,7 @@ import { TiptapBlockImplementation, } from "./blockTypes"; import { inheritedProps } from "./defaultProps"; +import { InlineContentSchema } from "./inlineContentTypes"; import { StyleSchema } from "./styles"; export function camelToDataKebab(str: string): string { @@ -129,10 +130,11 @@ export function getBlockFromPos< BType extends string, Config extends BlockConfig, BSchema extends BlockSchemaWithBlock, + I extends InlineContentSchema, S extends StyleSchema >( getPos: (() => number) | boolean, - editor: BlockNoteEditor, + editor: BlockNoteEditor, tipTapEditor: Editor, type: BType ) { @@ -151,6 +153,7 @@ export function getBlockFromPos< const block = editor.getBlock(blockIdentifier)! as SpecificBlock< BSchema, BType, + I, S >; if (block.type !== type) { @@ -241,12 +244,17 @@ export function createStronglyTypedTiptapNode< // config and implementation that conform to the type of Config export function createInternalBlockSpec( config: T, - implementation: TiptapBlockImplementation + implementation: TiptapBlockImplementation< + T, + any, + InlineContentSchema, + StyleSchema + > ) { return { config, implementation, - } satisfies BlockSpec; + } satisfies BlockSpec; } export function createBlockSpecFromStronglyTypedTiptapNode< diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index 0bb78ecdeb..0171deb212 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -1,11 +1,11 @@ /** Define the main block types **/ import { Node } from "@tiptap/core"; +import { BlockNoteEditor, DefaultStyleSchema } from "../../.."; import { - BlockNoteEditor, - DefaultBlockSchema, - DefaultStyleSchema, -} from "../../.."; -import { InlineContent, PartialInlineContent } from "./inlineContentTypes"; + InlineContent, + InlineContentSchema, + PartialInlineContent, +} from "./inlineContentTypes"; import { StyleSchema } from "./styles"; export type BlockNoteDOMElement = @@ -64,24 +64,25 @@ export type BlockConfig = { export type TiptapBlockImplementation< T extends BlockConfig, B extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = { requiredNodes?: Node[]; node: Node; toInternalHTML: ( - block: BlockFromConfigNoChildren & { - children: Block[]; + block: BlockFromConfigNoChildren & { + children: Block[]; }, - editor: BlockNoteEditor + editor: BlockNoteEditor ) => { dom: HTMLElement; contentDOM?: HTMLElement; }; toExternalHTML: ( - block: BlockFromConfigNoChildren & { - children: Block[]; + block: BlockFromConfigNoChildren & { + children: Block[]; }, - editor: BlockNoteEditor + editor: BlockNoteEditor ) => { dom: HTMLElement; contentDOM?: HTMLElement; @@ -93,10 +94,11 @@ export type TiptapBlockImplementation< export type BlockSpec< T extends BlockConfig, B extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = { config: T; - implementation: TiptapBlockImplementation; + implementation: TiptapBlockImplementation; }; // Utility type. For a given object block schema, ensures that the key of each @@ -118,11 +120,14 @@ type NamesMatch> = Blocks extends { // information for the external API. export type BlockSchema = NamesMatch>; -export type BlockSpecs = Record>; +export type BlockSpecs = Record< + string, + BlockSpec +>; export type BlockImplementations = Record< string, - TiptapBlockImplementation + TiptapBlockImplementation >; export type BlockSchemaFromSpecs = { @@ -136,31 +141,41 @@ export type BlockSchemaWithBlock< [k in BType]: C; }; -export type TableContent = { +export type TableContent< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema +> = { type: "tableContent"; rows: { - cells: InlineContent[][]; + cells: InlineContent[][]; }[]; }; -export type PartialTableContent = { +export type PartialTableContent< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema +> = { type: "tableContent"; rows: { - cells: PartialInlineContent[]; + cells: PartialInlineContent[]; }[]; }; // 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) -type BlockFromConfigNoChildren = { +type BlockFromConfigNoChildren< + B extends BlockConfig, + I extends InlineContentSchema, + S extends StyleSchema +> = { id: string; type: B["type"]; props: Props; content: B["content"] extends "inline" - ? InlineContent[] + ? InlineContent[] : B["content"] extends "table" - ? TableContent + ? TableContent : B["content"] extends "none" ? undefined : never; @@ -168,9 +183,10 @@ type BlockFromConfigNoChildren = { export type BlockFromConfig< B extends BlockConfig, + I extends InlineContentSchema, S extends StyleSchema -> = BlockFromConfigNoChildren & { - children: Block[]; +> = BlockFromConfigNoChildren & { + children: Block[]; }; // Converts each block spec into a Block object without children. We later merge @@ -178,41 +194,45 @@ export type BlockFromConfig< // PartialBlock objects we use in the external API. type BlocksWithoutChildren< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = { - [BType in keyof BSchema]: BlockFromConfigNoChildren; + [BType in keyof BSchema]: BlockFromConfigNoChildren; }; // Converts each block spec into a Block object without children, merges them // into a union type, and adds a children property export type Block< - BSchema extends BlockSchema = DefaultBlockSchema, + BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema = DefaultStyleSchema -> = BlocksWithoutChildren[keyof BSchema] & { - children: Block[]; +> = BlocksWithoutChildren[keyof BSchema] & { + children: Block[]; }; export type SpecificBlock< BSchema extends BlockSchema, BType extends keyof BSchema, + I extends InlineContentSchema, S extends StyleSchema -> = BlocksWithoutChildren[BType] & { - children: Block[]; +> = BlocksWithoutChildren[BType] & { + children: Block[]; }; /** CODE FOR PARTIAL BLOCKS, analogous to above */ type PartialBlockFromConfigNoChildren< B extends BlockConfig, + I extends InlineContentSchema, S extends StyleSchema > = { id?: string; type?: B["type"]; props?: Partial>; content?: B["content"] extends "inline" - ? PartialInlineContent + ? PartialInlineContent : B["content"] extends "table" - ? PartialTableContent + ? PartialTableContent : undefined; }; @@ -220,23 +240,30 @@ type PartialBlockFromConfigNoChildren< // it easier to create/update blocks in the editor. type PartialBlocksWithoutChildren< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = { - [BType in keyof BSchema]: PartialBlockFromConfigNoChildren; + [BType in keyof BSchema]: PartialBlockFromConfigNoChildren< + BSchema[BType], + I, + S + >; }; // Same as Block, but as a partial type with some changes to make it easier to // create/update blocks in the editor. export type PartialBlock< - BSchema extends BlockSchema = DefaultBlockSchema, - S extends StyleSchema = DefaultStyleSchema -> = PartialBlocksWithoutChildren[keyof PartialBlocksWithoutChildren< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +> = PartialBlocksWithoutChildren< BSchema, + I, S ->] & +>[keyof PartialBlocksWithoutChildren] & Partial<{ - children: PartialBlock[]; + children: PartialBlock[]; }>; // export type PartialBlock = @@ -254,15 +281,16 @@ export type PartialBlock< export type SpecificPartialBlock< BSchema extends BlockSchema, + I extends InlineContentSchema, BType extends keyof BSchema, S extends StyleSchema -> = PartialBlocksWithoutChildren[BType] & { - children?: Block[]; +> = PartialBlocksWithoutChildren[BType] & { + children?: Block[]; }; export type BlockIdentifier = { id: string } | string; -export type Schema = { - blocks: B; - styles: S; -}; +// export type Schema = { +// blocks: B; +// styles: S; +// }; diff --git a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts index 23218a9a75..3df1cad2b4 100644 --- a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts +++ b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts @@ -1,11 +1,13 @@ import { Block, BlockSchema } from "./blockTypes"; +import { InlineContentSchema } from "./inlineContentTypes"; import { StyleSchema } from "./styles"; export type TextCursorPosition< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = { - block: Block; - prevBlock: Block | undefined; - nextBlock: Block | undefined; + block: Block; + prevBlock: Block | undefined; + nextBlock: Block | undefined; }; diff --git a/packages/core/src/extensions/Blocks/api/customBlocks.ts b/packages/core/src/extensions/Blocks/api/customBlocks.ts index 93c2e14055..9649a08d9f 100644 --- a/packages/core/src/extensions/Blocks/api/customBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/customBlocks.ts @@ -1,3 +1,4 @@ +import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { createInternalBlockSpec, @@ -21,19 +22,20 @@ export type CustomBlockConfig = BlockConfig & { export type CustomBlockImplementation< T extends CustomBlockConfig, + I extends InlineContentSchema, S extends StyleSchema > = { render: ( /** * The custom block to render */ - block: BlockFromConfig, + block: BlockFromConfig, /** * The BlockNote editor instance * This is typed generically. If you want an editor with your custom schema, you need to * cast it manually, e.g.: `const e = editor as BlockNoteEditor;` */ - editor: BlockNoteEditor, S> + editor: BlockNoteEditor, I, S> // (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations // or allow manually passing , but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics ) => { @@ -46,8 +48,8 @@ export type CustomBlockImplementation< // BlockNote. // TODO: Maybe can return undefined to ignore when serializing? toExternalHTML?: ( - block: BlockFromConfig, - editor: BlockNoteEditor, S> + block: BlockFromConfig, + editor: BlockNoteEditor, I, S> ) => { dom: HTMLElement; contentDOM?: HTMLElement; @@ -58,8 +60,9 @@ export type CustomBlockImplementation< // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createBlockSpec< T extends CustomBlockConfig, + I extends InlineContentSchema, S extends StyleSchema ->(blockConfig: T, blockImplementation: CustomBlockImplementation) { +>(blockConfig: T, blockImplementation: CustomBlockImplementation) { const node = createStronglyTypedTiptapNode({ name: blockConfig.type as T["type"], content: (blockConfig.content === "inline" diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 86a62d6cbe..624fdf4ac4 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -5,6 +5,10 @@ import { NumberedListItem } from "../nodes/BlockContent/ListItemBlockContent/Num import { Paragraph } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { Table } from "../nodes/BlockContent/TableBlockContent/TableBlockContent"; import { BlockSchemaFromSpecs, BlockSpecs } from "./blockTypes"; +import { + InlineContentSchemaFromSpecs, + InlineContentSpecs, +} from "./inlineContentTypes"; import { StyleSchemaFromSpecs, StyleSpecs } from "./styles"; export const defaultBlockSpecs = { @@ -87,3 +91,19 @@ export function getStyleSchemaFromSpecs(specs: T) { export const defaultStyleSchema = getStyleSchemaFromSpecs(defaultStyleSpecs); export type DefaultStyleSchema = typeof defaultStyleSchema; + +export const defaultInlineContentSpecs = {} satisfies InlineContentSpecs; + +export function getInlineContentSchemaFromSpecs( + specs: T +) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as InlineContentSchemaFromSpecs; +} + +export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( + defaultInlineContentSpecs +); + +export type DefaultInlineContentSchema = typeof defaultInlineContentSchema; diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts index d37b8330ed..ebbf8460c1 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts @@ -1,5 +1,64 @@ +import { PropSchema, Props } from "./blockTypes"; import { StyleSchema, Styles } from "./styles"; +export type InlineContentConfig = { + type: string; + content: "styled" | "raw" | "none"; + readonly propSchema: PropSchema; + // content: "inline" | "none" | "table"; +}; + +// @ts-ignore +export type InlineContentImplementation = any; + +// Container for both the config and implementation of a block, +// and the type of BlockImplementation is based on that of the config +export type InlineContentSpec = { + config: T; + implementation: InlineContentImplementation; +}; + +export type InlineContentSchema = Record; + +export type InlineContentSpecs = Record< + string, + InlineContentSpec +>; + +export type InlineContentSchemaFromSpecs = { + [K in keyof T]: T[K]["config"]; +}; + +type InlineContentFromConfig< + I extends InlineContentConfig, + S extends StyleSchema +> = { + type: I["type"]; + props: Props; + content: I["content"] extends "styled" + ? StyledText[] + : I["content"] extends "raw" + ? string + : I["content"] extends "none" + ? undefined + : never; +}; + +type PartialInlineContentFromConfig< + I extends InlineContentConfig, + S extends StyleSchema +> = { + type: I["type"]; + props: Props; + content: I["content"] extends "styled" + ? StyledText[] | string + : I["content"] extends "raw" + ? string + : I["content"] extends "none" + ? undefined + : never; +}; + export type StyledText = { type: "text"; text: string; @@ -16,12 +75,39 @@ export type PartialLink = Omit, "content"> & { content: string | Link["content"]; }; -export type InlineContent = StyledText | Link; -type PartialInlineContentElement = +export type InlineContent< + I extends InlineContentSchema, + T extends StyleSchema +> = StyledText | Link | InlineContentFromConfig; + +type PartialInlineContentElement< + I extends InlineContentSchema, + T extends StyleSchema +> = | string | StyledText - | PartialLink; + | PartialLink + | PartialInlineContentFromConfig; + +export type PartialInlineContent< + I extends InlineContentSchema, + T extends StyleSchema +> = PartialInlineContentElement[] | string; + +export function isLinkInlineContent( + content: InlineContent +): content is Link { + return content.type === "link"; +} + +export function isPartialLinkInlineContent( + content: PartialInlineContentElement +): content is PartialLink { + return typeof content !== "string" && content.type === "link"; +} -export type PartialInlineContent = - | PartialInlineContentElement[] - | string; +export function isStyledTextInlineContent( + content: InlineContent +): content is StyledText { + return content.type === "text"; +} diff --git a/packages/core/src/extensions/Blocks/api/selectionTypes.ts b/packages/core/src/extensions/Blocks/api/selectionTypes.ts index 8a23f48094..cd668c4e98 100644 --- a/packages/core/src/extensions/Blocks/api/selectionTypes.ts +++ b/packages/core/src/extensions/Blocks/api/selectionTypes.ts @@ -1,5 +1,11 @@ import { Block, BlockSchema } from "./blockTypes"; +import { InlineContentSchema } from "./inlineContentTypes"; +import { StyleSchema } from "./styles"; -export type Selection = { - blocks: Block[]; +export type Selection< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +> = { + blocks: Block[]; }; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index f3725d9596..68245d1f3a 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -16,6 +16,7 @@ import { BlockSchema, PartialBlock, } from "../api/blockTypes"; +import { InlineContentSchema } from "../api/inlineContentTypes"; import { StyleSchema } from "../api/styles"; import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"; import BlockAttributes from "./BlockAttributes"; @@ -27,16 +28,21 @@ declare module "@tiptap/core" { BNDeleteBlock: (posInBlock: number) => ReturnType; BNMergeBlocks: (posBetweenBlocks: number) => ReturnType; BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType; - BNUpdateBlock: ( + BNUpdateBlock: < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema + >( posInBlock: number, - block: PartialBlock + block: PartialBlock ) => ReturnType; BNCreateOrUpdateBlock: < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( posInBlock: number, - block: PartialBlock + block: PartialBlock ) => ReturnType; }; } @@ -47,7 +53,7 @@ declare module "@tiptap/core" { */ export const BlockContainer = Node.create<{ domAttributes?: BlockNoteDOMAttributes; - editor: BlockNoteEditor; + editor: BlockNoteEditor; }>({ name: "blockContainer", group: "blockContainer", diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts index 19dee16acd..3e171e8b14 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts @@ -8,6 +8,7 @@ import { } from "../../../api/blockTypes"; import { CustomBlockConfig, createBlockSpec } from "../../../api/customBlocks"; import { defaultProps } from "../../../api/defaultProps"; +import { InlineContentSchema } from "../../../api/inlineContentTypes"; import { StyleSchema } from "../../../api/styles"; export const imagePropSchema = { @@ -53,7 +54,7 @@ const blockConfig = { } satisfies CustomBlockConfig; export const renderImage = ( - block: BlockFromConfig, + block: BlockFromConfig, editor: BlockNoteEditor> ) => { // Wrapper element to set the image alignment, contains both image/image diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts index 2bfa409a42..79abe614d1 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts @@ -1,4 +1,4 @@ -import { Block, BlockSchema } from "../../../.."; +import { Block, BlockSchema, InlineContentSchema } from "../../../.."; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { blockToNode } from "../../../../api/nodeConversions/nodeConversions"; import { mergeCSSClasses } from "../../../../shared/utils"; @@ -54,10 +54,11 @@ export function createDefaultBlockDOMOutputSpec( // `DOMSerializer`. export const defaultBlockToHTML = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( - block: Block, - editor: BlockNoteEditor + block: Block, + editor: BlockNoteEditor ): { dom: HTMLElement; contentDOM?: HTMLElement; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index a2c7f8d40f..948195c970 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -6,6 +6,7 @@ import { BaseUiElementState, BlockNoteEditor, BlockSchema, + InlineContentSchema, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; import { StyleSchema } from "../Blocks/api/styles"; @@ -41,7 +42,11 @@ export class FormattingToolbarView { }; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >, private readonly pmView: EditorView, updateFormattingToolbar: ( formattingToolbarState: FormattingToolbarState @@ -221,7 +226,7 @@ export class FormattingToolbarProsemirrorPlugin extends EventEmitter { private view: FormattingToolbarView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: formattingToolbarPluginKey, diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 6bc71296f4..a316ef3d46 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -6,6 +6,7 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; import { BlockSchema } from "../Blocks/api/blockTypes"; +import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; import { StyleSchema } from "../Blocks/api/styles"; export type HyperlinkToolbarState = BaseUiElementState & { @@ -33,7 +34,7 @@ class HyperlinkToolbarView { hyperlinkMarkRange: Range | undefined; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, updateHyperlinkToolbar: ( hyperlinkToolbarState: HyperlinkToolbarState @@ -277,12 +278,13 @@ export const hyperlinkToolbarPluginKey = new PluginKey( export class HyperlinkToolbarProsemirrorPlugin< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { private view: HyperlinkToolbarView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: hyperlinkToolbarPluginKey, diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index 63809906d9..a55f26ca34 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -5,6 +5,7 @@ import { BaseUiElementState, BlockNoteEditor, BlockSchema, + InlineContentSchema, SpecificBlock, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; @@ -13,16 +14,18 @@ export type ImageToolbarCallbacks = BaseUiElementCallbacks; export type ImageToolbarState< B extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema = StyleSchema > = BaseUiElementState & { - block: SpecificBlock; + block: SpecificBlock; }; export class ImageToolbarView< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > { - private imageToolbarState?: ImageToolbarState; + private imageToolbarState?: ImageToolbarState; public updateImageToolbar: () => void; public prevWasEditable: boolean | null = null; @@ -31,7 +34,7 @@ export class ImageToolbarView< private readonly pluginKey: PluginKey, private readonly pmView: EditorView, updateImageToolbar: ( - imageToolbarState: ImageToolbarState + imageToolbarState: ImageToolbarState ) => void ) { this.updateImageToolbar = () => { @@ -102,7 +105,7 @@ export class ImageToolbarView< update(view: EditorView, prevState: EditorState) { const pluginState: { - block: SpecificBlock; + block: SpecificBlock; } = this.pluginKey.getState(view.state); if (!this.imageToolbarState?.show && pluginState.block) { @@ -148,15 +151,16 @@ export const imageToolbarPluginKey = new PluginKey("ImageToolbarPlugin"); export class ImageToolbarProsemirrorPlugin< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { - private view: ImageToolbarView | undefined; + private view: ImageToolbarView | undefined; public readonly plugin: Plugin; - constructor(_editor: BlockNoteEditor) { + constructor(_editor: BlockNoteEditor) { super(); this.plugin = new Plugin<{ - block: SpecificBlock | undefined; + block: SpecificBlock | undefined; }>({ key: imageToolbarPluginKey, view: (editorView) => { @@ -177,7 +181,7 @@ export class ImageToolbarProsemirrorPlugin< }; }, apply: (transaction) => { - const block: SpecificBlock | undefined = + const block: SpecificBlock | undefined = transaction.getMeta(imageToolbarPluginKey)?.block; return { @@ -188,7 +192,7 @@ export class ImageToolbarProsemirrorPlugin< }); } - public onUpdate(callback: (state: ImageToolbarState) => void) { + public onUpdate(callback: (state: ImageToolbarState) => void) { return this.on("update", callback); } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index f21219d63e..bc1fc87595 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -9,6 +9,7 @@ import { createInternalHTMLSerializer } from "../../api/serialization/html/inter import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; import { Block, BlockSchema } from "../Blocks/api/blockTypes"; +import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; import { StyleSchema } from "../Blocks/api/styles"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin"; @@ -18,10 +19,11 @@ let dragImageElement: Element | undefined; export type SideMenuState< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = BaseUiElementState & { // The block that the side menu is attached to. - block: Block; + block: Block; }; export function getDraggableBlockFromCoords( @@ -174,9 +176,13 @@ function unsetDragImage() { } } -function dragStart( +function dragStart< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( e: { dataTransfer: DataTransfer | null; clientY: number }, - editor: BlockNoteEditor + editor: BlockNoteEditor ) { if (!e.dataTransfer) { return; @@ -240,10 +246,13 @@ function dragStart( } } -export class SideMenuView - implements PluginView +export class SideMenuView< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +> implements PluginView { - private sideMenuState?: SideMenuState; + private sideMenuState?: SideMenuState; // When true, the drag handle with be anchored at the same level as root elements // When false, the drag handle with be just to the left of the element @@ -259,10 +268,10 @@ export class SideMenuView public menuFrozen = false; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, private readonly updateSideMenu: ( - sideMenuState: SideMenuState + sideMenuState: SideMenuState ) => void ) { this.horizontalPosAnchoredAtRoot = true; @@ -568,12 +577,13 @@ export const sideMenuPluginKey = new PluginKey("SideMenuPlugin"); export class SideMenuProsemirrorPlugin< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { - private sideMenuView: SideMenuView | undefined; + private sideMenuView: SideMenuView | undefined; public readonly plugin: Plugin; - constructor(private readonly editor: BlockNoteEditor) { + constructor(private readonly editor: BlockNoteEditor) { super(); this.plugin = new Plugin({ key: sideMenuPluginKey, @@ -590,7 +600,7 @@ export class SideMenuProsemirrorPlugin< }); } - public onUpdate(callback: (state: SideMenuState) => void) { + public onUpdate(callback: (state: SideMenuState) => void) { return this.on("update", callback); } diff --git a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts index 71c9895ad2..c6be9b3717 100644 --- a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts +++ b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts @@ -1,16 +1,14 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; import { BlockSchema } from "../Blocks/api/blockTypes"; -import { - DefaultBlockSchema, - DefaultStyleSchema, -} from "../Blocks/api/defaultBlocks"; +import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; import { StyleSchema } from "../Blocks/api/styles"; export type BaseSlashMenuItem< - BSchema extends BlockSchema = DefaultBlockSchema, - S extends StyleSchema = DefaultStyleSchema + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema > = SuggestionItem & { - execute: (editor: BlockNoteEditor) => void; + execute: (editor: BlockNoteEditor) => void; aliases?: string[]; }; diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts index 682e7c8511..e1f4081815 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts @@ -7,6 +7,7 @@ import { setupSuggestionsMenu, } from "../../shared/plugins/suggestion/SuggestionPlugin"; import { BlockSchema } from "../Blocks/api/blockTypes"; +import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; import { StyleSchema } from "../Blocks/api/styles"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; @@ -14,15 +15,16 @@ export const slashMenuPluginKey = new PluginKey("SlashMenuPlugin"); export class SlashMenuProsemirrorPlugin< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema, - SlashMenuItem extends BaseSlashMenuItem + SlashMenuItem extends BaseSlashMenuItem > extends EventEmitter { public readonly plugin: Plugin; public readonly itemCallback: (item: SlashMenuItem) => void; - constructor(editor: BlockNoteEditor, items: SlashMenuItem[]) { + constructor(editor: BlockNoteEditor, items: SlashMenuItem[]) { super(); - const suggestions = setupSuggestionsMenu( + const suggestions = setupSuggestionsMenu( editor, (state) => { this.emit("update", state); diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts index c2589eb915..12b583b040 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts @@ -1,13 +1,18 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { Block, BlockSchema, PartialBlock } from "../Blocks/api/blockTypes"; +import { + InlineContentSchema, + isStyledTextInlineContent, +} from "../Blocks/api/inlineContentTypes"; import { StyleSchema } from "../Blocks/api/styles"; import { imageToolbarPluginKey } from "../ImageToolbar/ImageToolbarPlugin"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; function setSelectionToNextContentEditableBlock< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema ->(editor: BlockNoteEditor) { +>(editor: BlockNoteEditor) { let block = editor.getTextCursorPosition().block; let contentType = editor.blockSchema[block.type].content as | "inline" @@ -28,11 +33,12 @@ function setSelectionToNextContentEditableBlock< function insertOrUpdateBlock< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, - block: PartialBlock -): Block { + editor: BlockNoteEditor, + block: PartialBlock +): Block { const currentBlock = editor.getTextCursorPosition().block; if (currentBlock.content === undefined) { @@ -42,6 +48,7 @@ function insertOrUpdateBlock< if ( Array.isArray(currentBlock.content) && ((currentBlock.content.length === 1 && + isStyledTextInlineContent(currentBlock.content[0]) && currentBlock.content[0].type === "text" && currentBlock.content[0].text === "/") || currentBlock.content.length === 0) @@ -60,6 +67,7 @@ function insertOrUpdateBlock< export const getDefaultSlashMenuItems = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( // This type casting is weird, but it's the best way of doing it, as it allows @@ -68,7 +76,7 @@ export const getDefaultSlashMenuItems = < // infer to DefaultBlockSchema if it is not defined. schema: BSchema ) => { - const slashMenuItems: BaseSlashMenuItem[] = []; + const slashMenuItems: BaseSlashMenuItem[] = []; if ("heading" in schema && "level" in schema.heading.propSchema) { // Command for creating a level 1 heading @@ -80,7 +88,7 @@ export const getDefaultSlashMenuItems = < insertOrUpdateBlock(editor, { type: "heading", props: { level: 1 }, - } as PartialBlock), + } as PartialBlock), }); } @@ -93,7 +101,7 @@ export const getDefaultSlashMenuItems = < insertOrUpdateBlock(editor, { type: "heading", props: { level: 2 }, - } as PartialBlock), + } as PartialBlock), }); } @@ -106,7 +114,7 @@ export const getDefaultSlashMenuItems = < insertOrUpdateBlock(editor, { type: "heading", props: { level: 3 }, - } as PartialBlock), + } as PartialBlock), }); } } @@ -118,7 +126,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "bulletListItem", - } as PartialBlock), + } as PartialBlock), }); } @@ -129,7 +137,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "numberedListItem", - } as PartialBlock), + } as PartialBlock), }); } @@ -140,7 +148,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "paragraph", - } as PartialBlock), + } as PartialBlock), }); } @@ -167,7 +175,7 @@ export const getDefaultSlashMenuItems = < }, ], }, - } as PartialBlock); + } as PartialBlock); }, }); } @@ -189,7 +197,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => { const insertedBlock = insertOrUpdateBlock(editor, { type: "image", - } as PartialBlock); + } as PartialBlock); // Immediately open the image toolbar editor._tiptapEditor.view.dispatch( diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 75371569e8..a9f090d933 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -5,6 +5,7 @@ import { BlockNoteEditor, BlockSchemaWithBlock, DefaultBlockSchema, + InlineContentSchema, SpecificBlock, getDraggableBlockFromCoords, } from "../.."; @@ -14,6 +15,7 @@ export type TableHandlesCallbacks = BaseUiElementCallbacks; export type TableHandlesState< BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I extends InlineContentSchema, S extends StyleSchema > = { show: boolean; @@ -21,7 +23,7 @@ export type TableHandlesState< referencePosLeft: { top: number; left: number }; colIndex: number; rowIndex: number; - block: SpecificBlock; + block: SpecificBlock; }; function getChildIndex(node: HTMLElement) { @@ -39,17 +41,18 @@ function domCellAround(target: HTMLElement | null): HTMLElement | null { export class TableHandlesView< BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I extends InlineContentSchema, S extends StyleSchema > { - private state?: TableHandlesState; + private state?: TableHandlesState; public updateState: () => void; public prevWasEditable: boolean | null = null; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateState: (state: TableHandlesState) => void + updateState: (state: TableHandlesState) => void ) { this.updateState = () => { if (!this.state) { @@ -90,6 +93,7 @@ export class TableHandlesView< const block = this.editor.getBlock(blockEl!.id)! as any as SpecificBlock< BSchema, "table", + I, S >; @@ -200,15 +204,16 @@ export const tableHandlesPluginKey = new PluginKey("TableHandlesPlugin"); export class TableHandlesProsemirrorPlugin< BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I extends InlineContentSchema, S extends StyleSchema > extends EventEmitter { - private view: TableHandlesView | undefined; + private view: TableHandlesView | undefined; public readonly plugin: Plugin; - constructor(editor: BlockNoteEditor) { + constructor(editor: BlockNoteEditor) { super(); this.plugin = new Plugin<{ - block: SpecificBlock | undefined; + block: SpecificBlock | undefined; }>({ key: tableHandlesPluginKey, view: (editorView) => { @@ -224,7 +229,7 @@ export class TableHandlesProsemirrorPlugin< }; }, apply: (transaction) => { - const block: SpecificBlock | undefined = + const block: SpecificBlock | undefined = transaction.getMeta(tableHandlesPluginKey)?.block; return { @@ -235,7 +240,7 @@ export class TableHandlesProsemirrorPlugin< }); } - public onUpdate(callback: (state: TableHandlesState) => void) { + public onUpdate(callback: (state: TableHandlesState) => void) { return this.on("update", callback); } } diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 96032599bb..36d7f83a52 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -2,6 +2,7 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContentTypes"; import { StyleSchema } from "../../../extensions/Blocks/api/styles"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import { BaseUiElementState } from "../../BaseUiElementTypes"; @@ -18,6 +19,7 @@ export type SuggestionsMenuState = class SuggestionsMenuView< T extends SuggestionItem, BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > { private suggestionsMenuState?: SuggestionsMenuState; @@ -26,7 +28,7 @@ class SuggestionsMenuView< pluginState: SuggestionPluginState; constructor( - private readonly editor: BlockNoteEditor, + private readonly editor: BlockNoteEditor, private readonly pluginKey: PluginKey, updateSuggestionsMenu: ( suggestionsMenuState: SuggestionsMenuState @@ -150,9 +152,10 @@ function getDefaultPluginState< export const setupSuggestionsMenu = < T extends SuggestionItem, BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + editor: BlockNoteEditor, updateSuggestionsMenu: ( suggestionsMenuState: SuggestionsMenuState ) => void, @@ -162,7 +165,7 @@ export const setupSuggestionsMenu = < items: (query: string) => T[] = () => [], onSelectItem: (props: { item: T; - editor: BlockNoteEditor; + editor: BlockNoteEditor; }) => void = () => { // noop } @@ -172,7 +175,7 @@ export const setupSuggestionsMenu = < throw new Error("'char' should be a single character"); } - let suggestionsPluginView: SuggestionsMenuView; + let suggestionsPluginView: SuggestionsMenuView; const deactivate = (view: EditorView) => { view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true })); @@ -183,7 +186,7 @@ export const setupSuggestionsMenu = < key: pluginKey, view: () => { - suggestionsPluginView = new SuggestionsMenuView( + suggestionsPluginView = new SuggestionsMenuView( editor, pluginKey, diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx index 1d1dcdf0d1..9315738b88 100644 --- a/packages/react/src/BlockNoteView.tsx +++ b/packages/react/src/BlockNoteView.tsx @@ -1,4 +1,9 @@ -import { BlockNoteEditor, BlockSchema, mergeCSSClasses } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + mergeCSSClasses, +} from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { MantineProvider, createStyles } from "@mantine/core"; import { EditorContent } from "@tiptap/react"; @@ -16,10 +21,11 @@ import { darkDefaultTheme, lightDefaultTheme } from "./defaultThemes"; // Renders the editor as well as all menus & toolbars using default styles. function BaseBlockNoteView< BSchema extends BlockSchema, + ISchema extends InlineContentSchema, SSchema extends StyleSchema >( props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; children?: ReactNode; } & HTMLAttributes ) { @@ -50,10 +56,11 @@ function BaseBlockNoteView< export function BlockNoteView< BSchema extends BlockSchema, + ISchema extends InlineContentSchema, SSchema extends StyleSchema >( props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; theme?: | "light" | "dark" diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx index 6fcd751700..65c5c068a8 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -1,6 +1,7 @@ import { BlockNoteEditor, BlockSchema, + DefaultInlineContentSchema, DefaultStyleSchema, } from "@blocknote/core"; import { Menu } from "@mantine/core"; @@ -14,7 +15,11 @@ import { usePreventMenuOverflow } from "../../../hooks/usePreventMenuOverflow"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; export const ColorStyleButton = (props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor< + BSchema, + DefaultInlineContentSchema, + DefaultStyleSchema + >; }) => { const selectedBlocks = useSelectedBlocks(props.editor); diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx index 94333c6a7f..cf2dd29e2b 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ImageCaptionButton.tsx @@ -16,7 +16,7 @@ import { ToolbarInputDropdownItem } from "../../../SharedComponents/Toolbar/comp import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; export const ImageCaptionButton = (props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; }) => { const selectedBlocks = useSelectedBlocks(props.editor); @@ -64,7 +64,7 @@ export const ImageCaptionButton = (props: { props: { caption: currentCaption, }, - } as PartialBlock); + } as PartialBlock); } }, [currentCaption, props.editor, selectedBlocks] diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx index cf89ecef97..c56c8ecc30 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx @@ -26,7 +26,7 @@ const icons: Record = { }; export const TextAlignButton = (props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; textAlignment: TextAlignment; }) => { const selectedBlocks = useSelectedBlocks(props.editor); @@ -48,7 +48,7 @@ export const TextAlignButton = (props: { for (const block of selectedBlocks) { props.editor.updateBlock(block, { props: { textAlignment: textAlignment }, - } as PartialBlock); + } as PartialBlock); } }, [props.editor, selectedBlocks] diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx index 04fe526e68..652a2fea48 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx @@ -1,4 +1,8 @@ -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; import { useMemo, useState } from "react"; import { RiBold, @@ -32,9 +36,10 @@ const icons = { export const ToggledStyleButton = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; toggledStyle: keyof typeof shortcuts; }) => { const selectedBlocks = useSelectedBlocks(props.editor); diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx index 86d08d19fa..a4c15b31fc 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx @@ -20,7 +20,7 @@ export type BlockTypeDropdownItem = { type: string; props?: Record; icon: IconType; - isSelected: (block: Block) => boolean; + isSelected: (block: Block) => boolean; }; export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [ @@ -134,7 +134,7 @@ export const BlockTypeDropdown = (props: { text: item.name, icon: item.icon, onClick: () => onClick(item), - isSelected: item.isSelected(block as Block), + isSelected: item.isSelected(block as Block), })); }, [block, filteredItems, props.editor, selectedBlocks]); diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx index b89cf1b16f..9441f5e5fa 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbarPositioner.tsx @@ -29,13 +29,13 @@ const textAlignmentToPlacement = ( export type FormattingToolbarProps< BSchema extends BlockSchema = DefaultBlockSchema > = { - editor: BlockNoteEditor; + editor: BlockNoteEditor; }; export const FormattingToolbarPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; formattingToolbar?: FC>; }) => { const [show, setShow] = useState(false); diff --git a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx index ee9b6143a3..4aebff08cb 100644 --- a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx @@ -1,4 +1,4 @@ -import { BlockSchema } from "@blocknote/core"; +import { BlockSchema, InlineContentSchema } from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; @@ -10,9 +10,10 @@ import { HyperlinkToolbarProps } from "./HyperlinkToolbarPositioner"; export const DefaultHyperlinkToolbar = < BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( - props: HyperlinkToolbarProps + props: HyperlinkToolbarProps ) => { const [isEditing, setIsEditing] = useState(false); const editMenuRef = useRef(null); diff --git a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx index 00efea496e..5df7e620d2 100644 --- a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx +++ b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx @@ -3,9 +3,11 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultInlineContentSchema, DefaultStyleSchema, HyperlinkToolbarProsemirrorPlugin, HyperlinkToolbarState, + InlineContentSchema, } from "@blocknote/core"; import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; @@ -15,19 +17,21 @@ import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; export type HyperlinkToolbarProps< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema > = Pick< - HyperlinkToolbarProsemirrorPlugin, + HyperlinkToolbarProsemirrorPlugin, "editHyperlink" | "deleteHyperlink" | "startHideTimer" | "stopHideTimer" > & Omit; export const HyperlinkToolbarPositioner = < BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema >(props: { - editor: BlockNoteEditor; - hyperlinkToolbar?: FC>; + editor: BlockNoteEditor; + hyperlinkToolbar?: FC>; }) => { const [show, setShow] = useState(false); const [url, setUrl] = useState(); diff --git a/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx b/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx index d04226f116..49260e289a 100644 --- a/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx +++ b/packages/react/src/ImageToolbar/components/DefaultImageToolbar.tsx @@ -1,14 +1,5 @@ import { BlockSchema, PartialBlock } from "@blocknote/core"; -import { ImageToolbarProps } from "./ImageToolbarPositioner"; -import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; -import { - ChangeEvent, - KeyboardEvent, - useCallback, - useEffect, - useState, -} from "react"; import { Button, FileInput, @@ -17,9 +8,18 @@ import { Text, TextInput, } from "@mantine/core"; +import { + ChangeEvent, + KeyboardEvent, + useCallback, + useEffect, + useState, +} from "react"; +import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; +import { ImageToolbarProps } from "./ImageToolbarPositioner"; export const DefaultImageToolbar = ( - props: ImageToolbarProps + props: ImageToolbarProps ) => { const [openTab, setOpenTab] = useState<"upload" | "embed">( props.editor.uploadFile !== undefined ? "upload" : "embed" @@ -46,7 +46,7 @@ export const DefaultImageToolbar = ( props: { url: uploaded, }, - } as PartialBlock); + } as PartialBlock); } catch (e) { setUploadFailed(true); } finally { @@ -75,7 +75,7 @@ export const DefaultImageToolbar = ( props: { url: currentURL, }, - } as PartialBlock); + } as PartialBlock); } }, [currentURL, props.block, props.editor] @@ -87,7 +87,7 @@ export const DefaultImageToolbar = ( props: { url: currentURL, }, - } as PartialBlock); + } as PartialBlock); }, [currentURL, props.block, props.editor]); return ( diff --git a/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx b/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx index 5db05625ea..7bcfdd8615 100644 --- a/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx +++ b/packages/react/src/ImageToolbar/components/ImageToolbarPositioner.tsx @@ -3,7 +3,9 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultInlineContentSchema, ImageToolbarState, + InlineContentSchema, SpecificBlock, } from "@blocknote/core"; import Tippy, { tippy } from "@tippyjs/react"; @@ -12,19 +14,21 @@ import { FC, useEffect, useMemo, useRef, useState } from "react"; import { DefaultImageToolbar } from "./DefaultImageToolbar"; export type ImageToolbarProps< - BSchema extends BlockSchema = DefaultBlockSchema -> = Omit, keyof BaseUiElementState> & { - editor: BlockNoteEditor; + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema +> = Omit, keyof BaseUiElementState> & { + editor: BlockNoteEditor; }; export const ImageToolbarPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema >(props: { - editor: BlockNoteEditor; - imageToolbar?: FC>; + editor: BlockNoteEditor; + imageToolbar?: FC>; }) => { const [show, setShow] = useState(false); - const [block, setBlock] = useState>(); + const [block, setBlock] = useState>(); const referencePos = useRef(); diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index bf43ad7003..2bc548b01e 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -9,6 +9,7 @@ import { CustomBlockConfig, getBlockFromPos, inheritedProps, + InlineContentSchema, mergeCSSClasses, parse, Props, @@ -30,15 +31,16 @@ import { renderToString } from "react-dom/server"; // extend BlockConfig but use a React render function export type ReactCustomBlockImplementation< T extends CustomBlockConfig, + I extends InlineContentSchema, S extends StyleSchema > = { render: FC<{ - block: BlockFromConfig; - editor: BlockNoteEditor, S>; + block: BlockFromConfig; + editor: BlockNoteEditor, I, S>; }>; toExternalHTML?: FC<{ - block: BlockFromConfig; - editor: BlockNoteEditor, S>; + block: BlockFromConfig; + editor: BlockNoteEditor, I, S>; }>; }; @@ -120,8 +122,12 @@ export function reactWrapInBlockStructure< // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactBlockSpec< T extends CustomBlockConfig, + I extends InlineContentSchema, S extends StyleSchema ->(blockConfig: T, blockImplementation: ReactCustomBlockImplementation) { +>( + blockConfig: T, + blockImplementation: ReactCustomBlockImplementation +) { const node = createStronglyTypedTiptapNode({ name: blockConfig.type as T["type"], content: (blockConfig.content === "inline" diff --git a/packages/react/src/SideMenu/components/DefaultButtons/AddBlockButton.tsx b/packages/react/src/SideMenu/components/DefaultButtons/AddBlockButton.tsx index aa4985d700..0dadb3aac2 100644 --- a/packages/react/src/SideMenu/components/DefaultButtons/AddBlockButton.tsx +++ b/packages/react/src/SideMenu/components/DefaultButtons/AddBlockButton.tsx @@ -1,10 +1,10 @@ +import { BlockSchema } from "@blocknote/core"; import { AiOutlinePlus } from "react-icons/ai"; import { SideMenuButton } from "../SideMenuButton"; import { SideMenuProps } from "../SideMenuPositioner"; -import { BlockSchema } from "@blocknote/core"; export const AddBlockButton = ( - props: SideMenuProps + props: SideMenuProps ) => ( ( - props: SideMenuProps + props: SideMenuProps ) => { const DragHandleMenu = props.dragHandleMenu || DefaultDragHandleMenu; diff --git a/packages/react/src/SideMenu/components/DefaultSideMenu.tsx b/packages/react/src/SideMenu/components/DefaultSideMenu.tsx index 46623a9c12..fbd686065a 100644 --- a/packages/react/src/SideMenu/components/DefaultSideMenu.tsx +++ b/packages/react/src/SideMenu/components/DefaultSideMenu.tsx @@ -1,12 +1,17 @@ -import { BlockSchema } from "@blocknote/core"; +import { BlockSchema, InlineContentSchema } from "@blocknote/core"; -import { SideMenuProps } from "./SideMenuPositioner"; -import { SideMenu } from "./SideMenu"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { AddBlockButton } from "./DefaultButtons/AddBlockButton"; import { DragHandle } from "./DefaultButtons/DragHandle"; +import { SideMenu } from "./SideMenu"; +import { SideMenuProps } from "./SideMenuPositioner"; -export const DefaultSideMenu = ( - props: SideMenuProps +export const DefaultSideMenu = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + props: SideMenuProps ) => ( diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx index f8ea41fa56..bb7dbcfb48 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx @@ -1,15 +1,15 @@ -import { ReactNode, useCallback, useRef, useState } from "react"; +import { BlockSchema, PartialBlock } from "@blocknote/core"; import { Box, Menu } from "@mantine/core"; +import { ReactNode, useCallback, useRef, useState } from "react"; import { HiChevronRight } from "react-icons/hi"; -import { BlockSchema, PartialBlock } from "@blocknote/core"; -import { DragHandleMenuProps } from "../DragHandleMenu"; -import { DragHandleMenuItem } from "../DragHandleMenuItem"; import { ColorPicker } from "../../../../SharedComponents/ColorPicker/components/ColorPicker"; import { usePreventMenuOverflow } from "../../../../hooks/usePreventMenuOverflow"; +import { DragHandleMenuProps } from "../DragHandleMenu"; +import { DragHandleMenuItem } from "../DragHandleMenuItem"; export const BlockColorsButton = ( - props: DragHandleMenuProps & { children: ReactNode } + props: DragHandleMenuProps & { children: ReactNode } ) => { const [opened, setOpened] = useState(false); @@ -73,7 +73,7 @@ export const BlockColorsButton = ( setColor: (color) => props.editor.updateBlock(props.block, { props: { textColor: color }, - } as PartialBlock), + } as PartialBlock), } : undefined } @@ -85,7 +85,7 @@ export const BlockColorsButton = ( setColor: (color) => props.editor.updateBlock(props.block, { props: { backgroundColor: color }, - } as PartialBlock), + } as PartialBlock), } : undefined } diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/RemoveBlockButton.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/RemoveBlockButton.tsx index bbd5e2331c..1b05fff510 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/RemoveBlockButton.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/RemoveBlockButton.tsx @@ -1,11 +1,11 @@ -import { ReactNode } from "react"; import { BlockSchema } from "@blocknote/core"; +import { ReactNode } from "react"; import { DragHandleMenuProps } from "../DragHandleMenu"; import { DragHandleMenuItem } from "../DragHandleMenuItem"; export const RemoveBlockButton = ( - props: DragHandleMenuProps & { children: ReactNode } + props: DragHandleMenuProps & { children: ReactNode } ) => { return ( ( - props: DragHandleMenuProps + props: DragHandleMenuProps ) => ( Delete diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx index b67dd98836..4c59a5dd91 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx @@ -1,10 +1,20 @@ +import { + Block, + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; +import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { Menu, createStyles } from "@mantine/core"; import { ReactNode } from "react"; -import { createStyles, Menu } from "@mantine/core"; -import { Block, BlockNoteEditor, BlockSchema } from "@blocknote/core"; -export type DragHandleMenuProps = { - editor: BlockNoteEditor; - block: Block; +export type DragHandleMenuProps< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +> = { + editor: BlockNoteEditor; + block: Block; }; export const DragHandleMenu = (props: { children: ReactNode }) => { diff --git a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx index 4ad4c561a6..1926d0a7e0 100644 --- a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx +++ b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx @@ -3,7 +3,9 @@ import { BlockNoteEditor, BlockSchema, DefaultBlockSchema, + DefaultInlineContentSchema, DefaultStyleSchema, + InlineContentSchema, SideMenuProsemirrorPlugin, } from "@blocknote/core"; import Tippy from "@tippyjs/react"; @@ -15,24 +17,27 @@ import { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; export type SideMenuProps< BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema > = Pick< - SideMenuProsemirrorPlugin, + SideMenuProsemirrorPlugin, "blockDragStart" | "blockDragEnd" | "addBlock" | "freezeMenu" | "unfreezeMenu" > & { - block: Block; - editor: BlockNoteEditor; - dragHandleMenu?: FC>; + block: Block; + editor: BlockNoteEditor; + dragHandleMenu?: FC>; }; export const SideMenuPositioner = < - BSchema extends BlockSchema = DefaultBlockSchema + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema >(props: { - editor: BlockNoteEditor; - sideMenu?: FC>; + editor: BlockNoteEditor; + sideMenu?: FC>; }) => { const [show, setShow] = useState(false); - const [block, setBlock] = useState>(); + const [block, setBlock] = useState>(); const referencePos = useRef(); diff --git a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts index 768506dee5..f12fce328b 100644 --- a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts +++ b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts @@ -2,14 +2,17 @@ import { BaseSlashMenuItem, BlockSchema, DefaultBlockSchema, + DefaultInlineContentSchema, DefaultStyleSchema, + InlineContentSchema, } from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; export type ReactSlashMenuItem< BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema -> = BaseSlashMenuItem & { +> = BaseSlashMenuItem & { group: string; icon: JSX.Element; hint?: string; diff --git a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx index 2918a1f80d..6c084ad245 100644 --- a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx +++ b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx @@ -13,7 +13,7 @@ import { ReactSlashMenuItem } from "../ReactSlashMenuItem"; import { DefaultSlashMenu } from "./DefaultSlashMenu"; export type SlashMenuProps = - Pick, "itemCallback"> & + Pick, "itemCallback"> & Pick< SuggestionsMenuState>, "filteredItems" | "keyboardHoveredItemIndex" @@ -22,7 +22,7 @@ export type SlashMenuProps = export const SlashMenuPositioner = < BSchema extends BlockSchema = DefaultBlockSchema >(props: { - editor: BlockNoteEditor; + editor: BlockNoteEditor; slashMenu?: FC>; }) => { const [show, setShow] = useState(false); diff --git a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx index 0ad43bbdca..f6c02498db 100644 --- a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx +++ b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx @@ -4,6 +4,7 @@ import { defaultBlockSchema, DefaultBlockSchema, getDefaultSlashMenuItems, + InlineContentSchema, } from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { @@ -23,7 +24,7 @@ const extraFields: Record< string, Omit< ReactSlashMenuItem, - keyof BaseSlashMenuItem + keyof BaseSlashMenuItem > > = { Heading: { @@ -77,6 +78,7 @@ const extraFields: Record< export function getDefaultReactSlashMenuItems< BSchema extends BlockSchema, + I extends InlineContentSchema, S extends StyleSchema >( // This type casting is weird, but it's the best way of doing it, as it allows @@ -84,8 +86,8 @@ export function getDefaultReactSlashMenuItems< // inferred as any if it is not defined. I don't think it's possible to make it // infer to DefaultBlockSchema if it is not defined. schema: BSchema = defaultBlockSchema as any as BSchema -): ReactSlashMenuItem[] { - const slashMenuItems: BaseSlashMenuItem[] = +): ReactSlashMenuItem[] { + const slashMenuItems: BaseSlashMenuItem[] = getDefaultSlashMenuItems(schema); return slashMenuItems.map((item) => ({ diff --git a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx index 8e4afd90a3..d5475823ce 100644 --- a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx +++ b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx @@ -1,6 +1,7 @@ import { BlockSchemaWithBlock, DefaultBlockSchema, + InlineContentSchema, TableContent, } from "@blocknote/core"; @@ -13,6 +14,7 @@ import { TableHandlesProps } from "./TableHandlePositioner"; const DefaultTableHandleLeft = ( props: TableHandlesProps< BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + InlineContentSchema, StyleSchema > ) => { @@ -33,7 +35,7 @@ const DefaultTableHandleLeft = ( { - const content: TableContent = { + const content: TableContent = { type: "tableContent", rows: props.block.content.rows.filter( (_, index) => index !== props.rowIndex @@ -92,6 +94,7 @@ const DefaultTableHandleLeft = ( const DefaultTableHandleTop = ( props: TableHandlesProps< BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + InlineContentSchema, StyleSchema > ) => { @@ -116,7 +119,7 @@ const DefaultTableHandleTop = ( { - const content: TableContent = { + const content: TableContent = { type: "tableContent", rows: props.block.content.rows.map((row) => ({ cells: row.cells.filter((_, index) => index !== props.colIndex), @@ -129,7 +132,7 @@ const DefaultTableHandleTop = ( { - const content: TableContent = { + const content: TableContent = { type: "tableContent", rows: props.block.content.rows.map((row) => { const cells = [...row.cells]; @@ -144,7 +147,7 @@ const DefaultTableHandleTop = ( { - const content: TableContent = { + const content: TableContent = { type: "tableContent", rows: props.block.content.rows.map((row) => { const cells = [...row.cells]; @@ -165,6 +168,7 @@ const DefaultTableHandleTop = ( export const DefaultTableHandle = ( props: TableHandlesProps< BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + any, any > ) => { diff --git a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx index 4b384f2b22..7c2b9ba1c2 100644 --- a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx +++ b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx @@ -2,6 +2,7 @@ import { BlockNoteEditor, BlockSchemaWithBlock, DefaultBlockSchema, + InlineContentSchema, SpecificBlock, TableHandlesState, } from "@blocknote/core"; @@ -12,32 +13,34 @@ import { DefaultTableHandle } from "./DefaultTableHandle"; export type TableHandlesProps< BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I extends InlineContentSchema, S extends StyleSchema > = Omit< - TableHandlesState, + TableHandlesState, "referencePosLeft" | "referencePosTop" | "show" > & { - editor: BlockNoteEditor; + editor: BlockNoteEditor; side: "top" | "left"; }; export const TableHandlesPositioner = < BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, + I extends InlineContentSchema, S extends StyleSchema >(props: { - editor: BlockNoteEditor; - tableHandle?: FC>; + editor: BlockNoteEditor; + tableHandle?: FC>; }) => { const [show, setShow] = useState(false); - const [block, setBlock] = useState>(); + const [block, setBlock] = useState>(); const [colIndex, setColIndex] = useState(); const [rowIndex, setRowIndex] = useState(); const [_, setForceUpdate] = useState(0); const referencePosLeft = - useRef["referencePosLeft"]>(); + useRef["referencePosLeft"]>(); const referencePosTop = - useRef["referencePosTop"]>(); + useRef["referencePosTop"]>(); useEffect(() => { tippy.setDefaultProps({ maxWidth: "" }); @@ -80,7 +83,7 @@ export const TableHandlesPositioner = < const tableHandleElementTop = useMemo(() => { const TableHandle = props.tableHandle || - (DefaultTableHandle as FC>); + (DefaultTableHandle as FC>); return ( { const TableHandle = props.tableHandle || - (DefaultTableHandle as FC>); + (DefaultTableHandle as FC>); return ( ( - options: Partial> + options: Partial> ) => BlockNoteEditor.create({ - slashMenuItems: getDefaultReactSlashMenuItems( + slashMenuItems: getDefaultReactSlashMenuItems( getBlockSchemaFromSpecs( options.blockSpecs || defaultBlockSpecs ) as BSchema - ), + ) as any, ...options, }); @@ -37,16 +43,20 @@ const initEditor = < */ export const useBlockNote = < BSpecs extends BlockSpecs, + ISpecs extends InlineContentSpecs, SSpecs extends StyleSpecs, BSchema extends BlockSchema = { [key in keyof BSpecs]: BSpecs[key]["config"]; }, + ISchema extends InlineContentSchema = { + [key in keyof ISpecs]: ISpecs[key]["config"]; + }, SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } >( - options: Partial> = {}, + options: Partial> = {}, deps: DependencyList = [] -): BlockNoteEditor => { - const editorRef = useRef>(); +): BlockNoteEditor => { + const editorRef = useRef>(); return useMemo(() => { if (editorRef.current) { @@ -55,6 +65,7 @@ export const useBlockNote = < editorRef.current = initEditor(options) as BlockNoteEditor< BSchema, + ISchema, SSchema >; return editorRef.current; diff --git a/packages/react/src/hooks/useEditorChange.ts b/packages/react/src/hooks/useEditorChange.ts index 6bc15a7034..207c8fcd83 100644 --- a/packages/react/src/hooks/useEditorChange.ts +++ b/packages/react/src/hooks/useEditorChange.ts @@ -3,7 +3,7 @@ import { useEditorContentChange } from "./useEditorContentChange"; import { useEditorSelectionChange } from "./useEditorSelectionChange"; export function useEditorChange( - editor: BlockNoteEditor, + editor: BlockNoteEditor, callback: () => void ) { useEditorContentChange(editor, callback); diff --git a/packages/react/src/hooks/useEditorContentChange.ts b/packages/react/src/hooks/useEditorContentChange.ts index a884f00c61..262012b681 100644 --- a/packages/react/src/hooks/useEditorContentChange.ts +++ b/packages/react/src/hooks/useEditorContentChange.ts @@ -1,9 +1,13 @@ -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; export function useEditorContentChange( - editor: BlockNoteEditor, + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/hooks/useEditorSelectionChange.ts b/packages/react/src/hooks/useEditorSelectionChange.ts index 99e4e4fb85..f32aa27181 100644 --- a/packages/react/src/hooks/useEditorSelectionChange.ts +++ b/packages/react/src/hooks/useEditorSelectionChange.ts @@ -1,9 +1,13 @@ -import { BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; export function useEditorSelectionChange( - editor: BlockNoteEditor, + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/hooks/useSelectedBlocks.ts b/packages/react/src/hooks/useSelectedBlocks.ts index 6f63464f39..29fc41ef74 100644 --- a/packages/react/src/hooks/useSelectedBlocks.ts +++ b/packages/react/src/hooks/useSelectedBlocks.ts @@ -1,14 +1,20 @@ -import { Block, BlockNoteEditor, BlockSchema } from "@blocknote/core"; +import { + Block, + BlockNoteEditor, + BlockSchema, + InlineContentSchema, +} from "@blocknote/core"; import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useState } from "react"; import { useEditorChange } from "./useEditorChange"; export function useSelectedBlocks< BSchema extends BlockSchema, + ISchema extends InlineContentSchema, SSchema extends StyleSchema ->(editor: BlockNoteEditor) { +>(editor: BlockNoteEditor) { const [selectedBlocks, setSelectedBlocks] = useState< - Block[] + Block[] >( () => editor.getSelection()?.blocks || [editor.getTextCursorPosition().block] diff --git a/packages/react/src/htmlConversion.test.tsx b/packages/react/src/htmlConversion.test.tsx index b419d26f8f..e5209e7573 100644 --- a/packages/react/src/htmlConversion.test.tsx +++ b/packages/react/src/htmlConversion.test.tsx @@ -49,6 +49,13 @@ const customSpecs = { } satisfies BlockSpecs; let editor: BlockNoteEditor>; + +type CustomPartialBlock = PartialBlock< + (typeof editor)["blockSchema"], + (typeof editor)["inlineContentSchema"], + (typeof editor)["styleSchema"] +>; + let tt: Editor; beforeEach(() => { @@ -68,7 +75,7 @@ afterEach(() => { }); function convertToHTMLAndCompareSnapshots( - blocks: PartialBlock<(typeof editor)["blockSchema"]>[], + blocks: CustomPartialBlock[], snapshotDirectory: string, snapshotName: string ) { @@ -95,7 +102,7 @@ function convertToHTMLAndCompareSnapshots( describe("Convert custom blocks with inline content to HTML", () => { it("Convert custom block with inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "reactCustomParagraph", content: "React Custom Paragraph", @@ -106,7 +113,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert styled custom block with inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "reactCustomParagraph", props: { @@ -150,7 +157,7 @@ describe("Convert custom blocks with inline content to HTML", () => { }); it("Convert nested block with inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "reactCustomParagraph", content: "React Custom Paragraph", @@ -173,7 +180,7 @@ describe("Convert custom blocks with inline content to HTML", () => { describe("Convert custom blocks with non-exported inline content to HTML", () => { it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "simpleReactCustomParagraph", content: "React Custom Paragraph", @@ -188,7 +195,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "simpleReactCustomParagraph", props: { @@ -236,7 +243,7 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => }); it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock<(typeof editor)["blockSchema"]>[] = [ + const blocks: CustomPartialBlock[] = [ { type: "simpleReactCustomParagraph", content: "Custom React Paragraph", From 522c6dbbb9de225deccda11ce5ad79a96bb889ea Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 11:11:44 +0100 Subject: [PATCH 06/31] clean nodeconversions test --- .../formatConversions.testOld.ts | 749 ------------------ .../nodeConversions.test.ts.snap | 52 ++ .../nodeConversions/nodeConversions.test.ts | 291 +------ .../core/src/api/nodeConversions/testUtil.ts | 19 +- 4 files changed, 100 insertions(+), 1011 deletions(-) delete mode 100644 packages/core/src/api/formatConversions/formatConversions.testOld.ts diff --git a/packages/core/src/api/formatConversions/formatConversions.testOld.ts b/packages/core/src/api/formatConversions/formatConversions.testOld.ts deleted file mode 100644 index ddb9908858..0000000000 --- a/packages/core/src/api/formatConversions/formatConversions.testOld.ts +++ /dev/null @@ -1,749 +0,0 @@ -// import { afterEach, beforeEach, describe, expect, it } from "vitest"; -// import { Block, BlockNoteEditor } from "../.."; -// import UniqueID from "../../extensions/UniqueID/UniqueID"; -// -// let editor: BlockNoteEditor; -// -// const getNonNestedBlocks = (): Block[] => [ -// { -// id: UniqueID.options.generateID(), -// type: "heading", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// level: 1, -// }, -// content: [ -// { -// type: "text", -// text: "Heading", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Paragraph", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ]; -// -// const getNestedBlocks = (): Block[] => [ -// { -// id: UniqueID.options.generateID(), -// type: "heading", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// level: 1, -// }, -// content: [ -// { -// type: "text", -// text: "Heading", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Paragraph", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ], -// }, -// ], -// }, -// ], -// }, -// ]; -// -// const getStyledBlocks = (): Block[] => [ -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bold", -// styles: { -// bold: true, -// }, -// }, -// { -// type: "text", -// text: "Italic", -// styles: { -// italic: true, -// }, -// }, -// { -// type: "text", -// text: "Underline", -// styles: { -// underline: true, -// }, -// }, -// { -// type: "text", -// text: "Strikethrough", -// styles: { -// strike: true, -// }, -// }, -// { -// type: "text", -// text: "TextColor", -// styles: { -// textColor: "red", -// }, -// }, -// { -// type: "text", -// text: "BackgroundColor", -// styles: { -// backgroundColor: "red", -// }, -// }, -// { -// type: "text", -// text: "Multiple", -// styles: { -// bold: true, -// italic: true, -// }, -// }, -// ], -// children: [], -// }, -// ]; -// -// const getComplexBlocks = (): Block[] => [ -// { -// id: UniqueID.options.generateID(), -// type: "heading", -// props: { -// backgroundColor: "red", -// textColor: "yellow", -// textAlignment: "right", -// level: 1, -// }, -// content: [ -// { -// type: "text", -// text: "Heading 1", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "heading", -// props: { -// backgroundColor: "orange", -// textColor: "orange", -// textAlignment: "center", -// level: 2, -// }, -// content: [ -// { -// type: "text", -// text: "Heading 2", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "heading", -// props: { -// backgroundColor: "yellow", -// textColor: "red", -// textAlignment: "left", -// level: 3, -// }, -// content: [ -// { -// type: "text", -// text: "Heading 3", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ], -// }, -// ], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Paragraph", -// styles: { -// textColor: "purple", -// backgroundColor: "green", -// }, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "P", -// styles: {}, -// }, -// { -// type: "text", -// text: "ara", -// styles: { -// bold: true, -// }, -// }, -// { -// type: "text", -// text: "grap", -// styles: { -// italic: true, -// }, -// }, -// { -// type: "text", -// text: "h", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "P", -// styles: {}, -// }, -// { -// type: "text", -// text: "ara", -// styles: { -// underline: true, -// }, -// }, -// { -// type: "text", -// text: "grap", -// styles: { -// strike: true, -// }, -// }, -// { -// type: "text", -// text: "h", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "paragraph", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Paragraph", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [ -// { -// id: UniqueID.options.generateID(), -// type: "numberedListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Numbered List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ], -// }, -// { -// id: UniqueID.options.generateID(), -// type: "bulletListItem", -// props: { -// backgroundColor: "default", -// textColor: "default", -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Bullet List Item", -// styles: {}, -// }, -// ], -// children: [], -// }, -// ]; -// -// function removeInlineContentClass(html: string) { -// return html.replace(/ class="_inlineContent_([a-zA-Z0-9_-])+"/g, ""); -// } -// -// beforeEach(() => { -// editor = new BlockNoteEditor(); -// }); -// -// afterEach(() => { -// editor._tiptapEditor.destroy(); -// editor = undefined as any; -// }); -// -// describe("Non-Nested Block/HTML/Markdown Conversions", () => { -// it("Convert non-nested blocks to HTML", async () => { -// const output = await editor.blocksToHTML(getNonNestedBlocks()); -// -// expect(removeInlineContentClass(output)).toMatchSnapshot(); -// }); -// -// it("Convert non-nested blocks to Markdown", async () => { -// const output = await editor.blocksToMarkdown(getNonNestedBlocks()); -// -// expect(output).toMatchSnapshot(); -// }); -// -// it("Convert non-nested HTML to blocks", async () => { -// const html = `

    Heading

    Paragraph

    • Bullet List Item

    1. Numbered List Item

    `; -// const output = await editor.HTMLToBlocks(html); -// -// expect(output).toMatchSnapshot(); -// }); -// -// it("Convert non-nested Markdown to blocks", async () => { -// const markdown = `# Heading -// -// Paragraph -// -// * Bullet List Item -// -// 1. Numbered List Item -// `; -// const output = await editor.markdownToBlocks(markdown); -// -// expect(output).toMatchSnapshot(); -// }); -// }); -// -// describe("Nested Block/HTML/Markdown Conversions", () => { -// it("Convert nested blocks to HTML", async () => { -// const output = await editor.blocksToHTML(getNestedBlocks()); -// -// expect(removeInlineContentClass(output)).toMatchSnapshot(); -// }); -// -// it("Convert nested blocks to Markdown", async () => { -// const output = await editor.blocksToMarkdown(getNestedBlocks()); -// -// expect(output).toMatchSnapshot(); -// }); -// // // Failing due to nested block parsing bug. -// // it("Convert nested HTML to blocks", async () => { -// // const html = `

    Heading

    Paragraph

    • Bullet List Item

      1. Numbered List Item

    `; -// // const output = await editor.HTMLToBlocks(html); -// // -// // expect(output).toMatchSnapshot(); -// // }); -// // // Failing due to nested block parsing bug. -// // it("Convert nested Markdown to blocks", async () => { -// // const markdown = `# Heading -// // -// // Paragraph -// // -// // * Bullet List Item -// // -// // 1. Numbered List Item -// // `; -// // const output = await editor.markdownToBlocks(markdown); -// // -// // expect(output).toMatchSnapshot(); -// // }); -// }); -// -// describe("Styled Block/HTML/Markdown Conversions", () => { -// it("Convert styled blocks to HTML", async () => { -// const output = await editor.blocksToHTML(getStyledBlocks()); -// -// expect(removeInlineContentClass(output)).toMatchSnapshot(); -// }); -// -// it("Convert styled blocks to Markdown", async () => { -// const output = await editor.blocksToMarkdown(getStyledBlocks()); -// -// expect(output).toMatchSnapshot(); -// }); -// -// it("Convert styled HTML to blocks", async () => { -// const html = `

    BoldItalicUnderlineStrikethroughTextColorBackgroundColorMultiple

    `; -// const output = await editor.HTMLToBlocks(html); -// -// expect(output).toMatchSnapshot(); -// }); -// -// it("Convert styled Markdown to blocks", async () => { -// const markdown = `**Bold***Italic*Underline~~Strikethrough~~TextColorBackgroundColor***Multiple***`; -// const output = await editor.markdownToBlocks(markdown); -// -// expect(output).toMatchSnapshot(); -// }); -// }); -// -// describe("Complex Block/HTML/Markdown Conversions", () => { -// it("Convert complex blocks to HTML", async () => { -// const output = await editor.blocksToHTML(getComplexBlocks()); -// -// expect(removeInlineContentClass(output)).toMatchSnapshot(); -// }); -// -// it("Convert complex blocks to Markdown", async () => { -// const output = await editor.blocksToMarkdown(getComplexBlocks()); -// -// expect(output).toMatchSnapshot(); -// }); -// // // Failing due to nested block parsing bug. -// // it("Convert complex HTML to blocks", async () => { -// // const html = `

    Heading 1

    Heading 2

    Heading 3

    Paragraph

    Paragraph

    Paragraph

    • Bullet List Item

    • Bullet List Item

      • Bullet List Item

        • Bullet List Item

        Paragraph

        1. Numbered List Item

        2. Numbered List Item

        3. Numbered List Item

          1. Numbered List Item

        • Bullet List Item

      • Bullet List Item

    • Bullet List Item

    `; -// // const output = await editor.HTMLToBlocks(html); -// // -// // expect(output).toMatchSnapshot(); -// // }); -// // // Failing due to nested block parsing bug. -// // it("Convert complex Markdown to blocks", async () => { -// // const markdown = `# Heading 1 -// // -// // ## Heading 2 -// // -// // ### Heading 3 -// // -// // Paragraph -// // -// // P**ara***grap*h -// // -// // P*ara*~~grap~~h -// // -// // * Bullet List Item -// // -// // * Bullet List Item -// // -// // * Bullet List Item -// // -// // * Bullet List Item -// // -// // Paragraph -// // -// // 1. Numbered List Item -// // -// // 2. Numbered List Item -// // -// // 3. Numbered List Item -// // -// // 1. Numbered List Item -// // -// // * Bullet List Item -// // -// // * Bullet List Item -// // -// // * Bullet List Item -// // `; -// // const output = await editor.markdownToBlocks(markdown); -// // -// // expect(output).toMatchSnapshot(); -// // }); -// }); diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index 92be1196e5..872514ded9 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -510,6 +510,58 @@ exports[`links > Convert a block with link 1`] = ` } `; +exports[`links > Convert link block with marks 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + { + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Web", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "site", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + exports[`links > Convert two adjacent links in a block 1`] = ` { "attrs": { diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index ddf1e54738..0c490e9ed5 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -1,6 +1,6 @@ import { Editor } from "@tiptap/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { BlockNoteEditor, PartialBlock } from "../.."; +import { BlockNoteEditor, BlockSchema, PartialBlock } from "../.."; import { defaultBlockSchema, defaultStyleSchema, @@ -23,46 +23,35 @@ afterEach(() => { tt = undefined as any; }); -describe("Simple ProseMirror Node Conversions", () => { - it("Convert simple block to node", async () => { - const block: PartialBlock = { - type: "paragraph", - }; - const firstNodeConversion = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); +function validateConversion( + block: PartialBlock, + schema: BlockSchema +) { + const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(firstNodeConversion).toMatchSnapshot(); - }); + expect(node).toMatchSnapshot(); - it("Convert simple node to block", async () => { - const node = tt.schema.nodes["blockContainer"].create( - { id: UniqueID.options.generateID() }, - tt.schema.nodes["paragraph"].create() - ); - const firstBlockConversion = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); + const outputBlock = nodeToBlock(node, defaultBlockSchema, defaultStyleSchema); - expect(firstBlockConversion).toMatchSnapshot(); + const fullOriginalBlock = partialBlockToBlockForTesting(schema, block); - const firstNodeConversion = blockToNode( - firstBlockConversion, - tt.schema, - defaultStyleSchema - ); + expect(outputBlock).toStrictEqual(fullOriginalBlock); +} - expect(firstNodeConversion).toStrictEqual(node); +describe("Simple ProseMirror Node Conversions", () => { + it("Convert simple block to node", async () => { + const block: PartialBlock = { + id: UniqueID.options.generateID(), + type: "paragraph", + }; + validateConversion(block, defaultBlockSchema); }); }); describe("Complex ProseMirror Node Conversions", () => { it("Convert complex block to node", async () => { const block: PartialBlock = { + id: UniqueID.options.generateID(), type: "heading", props: { backgroundColor: "blue", @@ -90,6 +79,7 @@ describe("Complex ProseMirror Node Conversions", () => { ], children: [ { + id: UniqueID.options.generateID(), type: "paragraph", props: { backgroundColor: "red", @@ -98,72 +88,13 @@ describe("Complex ProseMirror Node Conversions", () => { children: [], }, { + id: UniqueID.options.generateID(), type: "bulletListItem", + props: {}, }, ], }; - const firstNodeConversion = blockToNode( - block, - tt.schema, - defaultStyleSchema - ); - - expect(firstNodeConversion).toMatchSnapshot(); - }); - - it("Convert complex node to block", async () => { - const node = tt.schema.nodes["blockContainer"].create( - { - id: UniqueID.options.generateID(), - backgroundColor: "blue", - textColor: "yellow", - }, - [ - tt.schema.nodes["heading"].create( - { textAlignment: "right", level: 2 }, - [ - tt.schema.text("Heading ", [ - tt.schema.mark("bold"), - tt.schema.mark("underline"), - ]), - tt.schema.text("2", [ - tt.schema.mark("italic"), - tt.schema.mark("strike"), - ]), - ] - ), - tt.schema.nodes["blockGroup"].create({}, [ - tt.schema.nodes["blockContainer"].create( - { id: UniqueID.options.generateID(), backgroundColor: "red" }, - [ - tt.schema.nodes["paragraph"].create( - {}, - tt.schema.text("Paragraph") - ), - ] - ), - tt.schema.nodes["blockContainer"].create( - { id: UniqueID.options.generateID() }, - [tt.schema.nodes["bulletListItem"].create()] - ), - ]), - ] - ); - const firstBlockConversion = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - expect(firstBlockConversion).toMatchSnapshot(); - - const firstNodeConversion = blockToNode( - firstBlockConversion, - tt.schema, - defaultStyleSchema - ); - - expect(firstNodeConversion).toStrictEqual(node); + validateConversion(block, defaultBlockSchema); }); }); @@ -180,21 +111,7 @@ describe("links", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert link block with marks", async () => { @@ -222,21 +139,7 @@ describe("links", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - // expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert two adjacent links in a block", async () => { @@ -257,21 +160,7 @@ describe("links", () => { ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); }); @@ -288,21 +177,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with multiple hard breaks", async () => { @@ -317,21 +192,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with a hard break at the start", async () => { @@ -346,21 +207,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with a hard break at the end", async () => { @@ -375,21 +222,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with only a hard break", async () => { @@ -404,21 +237,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with a hard break and different styles", async () => { @@ -438,21 +257,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with a hard break in a link", async () => { @@ -467,21 +272,7 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); it("Convert a block with a hard break between links", async () => { @@ -501,20 +292,6 @@ describe("hard breaks", () => { }, ], }; - const node = blockToNode(block, tt.schema, defaultStyleSchema); - expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - defaultBlockSchema, - defaultStyleSchema - ); - - // Temporary fix to set props to {}, because at this point - // we don't have an easy way to access default props at runtime, - // so partialBlockToBlockForTesting will not set them. - (outputBlock as any).props = {}; - const fullOriginalBlock = partialBlockToBlockForTesting(block); - - expect(outputBlock).toStrictEqual(fullOriginalBlock); + validateConversion(block, defaultBlockSchema); }); }); diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index c2c473bbcc..13b8ff681d 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -58,23 +58,32 @@ export function partialBlockToBlockForTesting< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->(partialBlock: PartialBlock): Block { +>( + schema: BSchema, + partialBlock: PartialBlock +): Block { const withDefaults: Block = { id: "", - type: "paragraph", - // because at this point we don't have an easy way to access default props at runtime, - // partialBlockToBlockForTesting will not set them. + type: partialBlock.type!, props: {} as any, content: [] as any, children: [] as any, ...partialBlock, }; + Object.entries(schema[partialBlock.type!].propSchema).forEach( + ([propKey, propValue]) => { + if (withDefaults.props[propKey] === undefined) { + (withDefaults.props as any)[propKey] = propValue.default; + } + } + ); + return { ...withDefaults, content: partialContentToInlineContent(withDefaults.content), children: withDefaults.children.map((c) => { - return partialBlockToBlockForTesting(c); + return partialBlockToBlockForTesting(schema, c); }), } as any; } From 3852fac32d7cbbd7add4650fe38c951046bc52f7 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 13:14:08 +0100 Subject: [PATCH 07/31] streamline tests --- packages/core/src/BlockNoteEditor.ts | 10 - .../nodeConversions.test.ts.snap | 495 +++++++++---- .../nodeConversions/nodeConversions.test.ts | 311 ++------ .../core/src/api/nodeConversions/testUtil.ts | 7 +- .../__snapshots__/complex/misc/external.html | 1 + .../__snapshots__/complex/misc/internal.html | 1 + .../hardbreak/basic/external.html | 1 + .../hardbreak/basic/internal.html | 1 + .../hardbreak/between-links/external.html | 1 + .../hardbreak/between-links/internal.html | 1 + .../__snapshots__/hardbreak/end/external.html | 1 + .../__snapshots__/hardbreak/end/internal.html | 1 + .../hardbreak/link/external.html | 1 + .../hardbreak/link/internal.html | 1 + .../hardbreak/multiple/external.html | 1 + .../hardbreak/multiple/internal.html | 1 + .../hardbreak/only/external.html | 1 + .../hardbreak/only/internal.html | 1 + .../hardbreak/start/external.html | 1 + .../hardbreak/start/internal.html | 1 + .../hardbreak/styles/external.html | 1 + .../hardbreak/styles/internal.html | 1 + .../__snapshots__/link/adjacent/external.html | 1 + .../__snapshots__/link/adjacent/internal.html | 1 + .../__snapshots__/link/basic/external.html | 1 + .../__snapshots__/link/basic/internal.html | 1 + .../__snapshots__/link/styled/external.html | 1 + .../__snapshots__/link/styled/internal.html | 1 + .../paragraph/empty/external.html | 1 + .../paragraph/empty/internal.html | 1 + .../serialization/html/htmlConversion.test.ts | 674 +++++++----------- .../src/api/testCases/cases/defaultSchema.ts | 400 +++++++++++ packages/core/src/api/testCases/index.ts | 19 + 33 files changed, 1079 insertions(+), 863 deletions(-) create mode 100644 packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html create mode 100644 packages/core/src/api/testCases/cases/defaultSchema.ts create mode 100644 packages/core/src/api/testCases/index.ts diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 071a425d4e..f7176f4a3b 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -177,16 +177,6 @@ const blockNoteTipTapOptions = { enableCoreExtensions: false, }; -// const ss = { -// paragraph: Paragraph, -// } as const; - -// const xa = createEditor({ -// blockSchema: ss, -// }); - -// xa.schema. - export class BlockNoteEditor< BSchema extends BlockSchema = DefaultBlockSchema, ISchema extends InlineContentSchema = DefaultInlineContentSchema, diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index 872514ded9..f6a8eceece 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Complex ProseMirror Node Conversions > Convert complex block to node 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert complex/misc to HTML 1`] = ` { "attrs": { "backgroundColor": "blue", @@ -89,68 +89,39 @@ exports[`Complex ProseMirror Node Conversions > Convert complex block to node 1` } `; -exports[`Complex ProseMirror Node Conversions > Convert complex node to block 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/basic to HTML 1`] = ` { - "children": [ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ { - "children": [], + "attrs": { + "textAlignment": "left", + }, "content": [ { - "styles": {}, - "text": "Paragraph", + "text": "Text1", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "text": "Text2", "type": "text", }, ], - "id": "2", - "props": { - "backgroundColor": "red", - "textAlignment": "left", - "textColor": "default", - }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "bulletListItem", - }, - ], - "content": [ - { - "styles": { - "bold": true, - "underline": true, - }, - "text": "Heading ", - "type": "text", - }, - { - "styles": { - "italic": true, - "strike": true, - }, - "text": "2", - "type": "text", - }, ], - "id": "1", - "props": { - "backgroundColor": "blue", - "level": 2, - "textAlignment": "right", - "textColor": "yellow", - }, - "type": "heading", + "type": "blockContainer", } `; -exports[`Simple ProseMirror Node Conversions > Convert simple block to node 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/between-links to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -162,6 +133,39 @@ exports[`Simple ProseMirror Node Conversions > Convert simple block to node 1`] "attrs": { "textAlignment": "left", }, + "content": [ + { + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Link1", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website2.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Link2", + "type": "text", + }, + ], "type": "paragraph", }, ], @@ -169,21 +173,7 @@ exports[`Simple ProseMirror Node Conversions > Convert simple block to node 1`] } `; -exports[`Simple ProseMirror Node Conversions > Convert simple node to block 1`] = ` -{ - "children": [], - "content": [], - "id": "1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", -} -`; - -exports[`hard breaks > Convert a block with a hard break 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/end to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -203,10 +193,6 @@ exports[`hard breaks > Convert a block with a hard break 1`] = ` { "type": "hardBreak", }, - { - "text": "Text2", - "type": "text", - }, ], "type": "paragraph", }, @@ -215,7 +201,7 @@ exports[`hard breaks > Convert a block with a hard break 1`] = ` } `; -exports[`hard breaks > Convert a block with a hard break and different styles 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/link to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -229,7 +215,17 @@ exports[`hard breaks > Convert a block with a hard break and different styles 1` }, "content": [ { - "text": "Text1", + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Link1", "type": "text", }, { @@ -238,10 +234,15 @@ exports[`hard breaks > Convert a block with a hard break and different styles 1` { "marks": [ { - "type": "bold", + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", }, ], - "text": "Text2", + "text": "Link1", "type": "text", }, ], @@ -252,7 +253,7 @@ exports[`hard breaks > Convert a block with a hard break and different styles 1` } `; -exports[`hard breaks > Convert a block with a hard break at the end 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/multiple to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -272,6 +273,17 @@ exports[`hard breaks > Convert a block with a hard break at the end 1`] = ` { "type": "hardBreak", }, + { + "text": "Text2", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "text": "Text3", + "type": "text", + }, ], "type": "paragraph", }, @@ -280,7 +292,7 @@ exports[`hard breaks > Convert a block with a hard break at the end 1`] = ` } `; -exports[`hard breaks > Convert a block with a hard break at the start 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/only to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -296,10 +308,6 @@ exports[`hard breaks > Convert a block with a hard break at the start 1`] = ` { "type": "hardBreak", }, - { - "text": "Text1", - "type": "text", - }, ], "type": "paragraph", }, @@ -308,7 +316,7 @@ exports[`hard breaks > Convert a block with a hard break at the start 1`] = ` } `; -exports[`hard breaks > Convert a block with a hard break between links 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/start to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -321,35 +329,11 @@ exports[`hard breaks > Convert a block with a hard break between links 1`] = ` "textAlignment": "left", }, "content": [ - { - "marks": [ - { - "attrs": { - "class": null, - "href": "https://www.website.com", - "target": "_blank", - }, - "type": "link", - }, - ], - "text": "Link1", - "type": "text", - }, { "type": "hardBreak", }, { - "marks": [ - { - "attrs": { - "class": null, - "href": "https://www.website2.com", - "target": "_blank", - }, - "type": "link", - }, - ], - "text": "Link2", + "text": "Text1", "type": "text", }, ], @@ -360,7 +344,7 @@ exports[`hard breaks > Convert a block with a hard break between links 1`] = ` } `; -exports[`hard breaks > Convert a block with a hard break in a link 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/styles to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -374,17 +358,7 @@ exports[`hard breaks > Convert a block with a hard break in a link 1`] = ` }, "content": [ { - "marks": [ - { - "attrs": { - "class": null, - "href": "https://www.website.com", - "target": "_blank", - }, - "type": "link", - }, - ], - "text": "Link1", + "text": "Text1", "type": "text", }, { @@ -393,15 +367,10 @@ exports[`hard breaks > Convert a block with a hard break in a link 1`] = ` { "marks": [ { - "attrs": { - "class": null, - "href": "https://www.website.com", - "target": "_blank", - }, - "type": "link", + "type": "bold", }, ], - "text": "Link1", + "text": "Text2", "type": "text", }, ], @@ -412,7 +381,51 @@ exports[`hard breaks > Convert a block with a hard break in a link 1`] = ` } `; -exports[`hard breaks > Convert a block with multiple hard breaks 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/basic to HTML 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "caption": "Caption", + "textAlignment": "left", + "url": "exampleURL", + "width": 256, + }, + "type": "image", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/button to HTML 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "caption": "", + "textAlignment": "left", + "url": "", + "width": 512, + }, + "type": "image", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/nested to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -422,36 +435,43 @@ exports[`hard breaks > Convert a block with multiple hard breaks 1`] = ` "content": [ { "attrs": { + "caption": "Caption", "textAlignment": "left", + "url": "exampleURL", + "width": 256, }, + "type": "image", + }, + { "content": [ { - "text": "Text1", - "type": "text", - }, - { - "type": "hardBreak", - }, - { - "text": "Text2", - "type": "text", - }, - { - "type": "hardBreak", - }, - { - "text": "Text3", - "type": "text", + "attrs": { + "backgroundColor": "default", + "id": "2", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "caption": "Caption", + "textAlignment": "left", + "url": "exampleURL", + "width": 256, + }, + "type": "image", + }, + ], + "type": "blockContainer", }, ], - "type": "paragraph", + "type": "blockGroup", }, ], "type": "blockContainer", } `; -exports[`hard breaks > Convert a block with only a hard break 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/adjacent to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -465,7 +485,32 @@ exports[`hard breaks > Convert a block with only a hard break 1`] = ` }, "content": [ { - "type": "hardBreak", + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Website", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "class": null, + "href": "https://www.website2.com", + "target": "_blank", + }, + "type": "link", + }, + ], + "text": "Website2", + "type": "text", }, ], "type": "paragraph", @@ -475,7 +520,7 @@ exports[`hard breaks > Convert a block with only a hard break 1`] = ` } `; -exports[`links > Convert a block with link 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/basic to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -510,7 +555,7 @@ exports[`links > Convert a block with link 1`] = ` } `; -exports[`links > Convert link block with marks 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/styled to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -562,7 +607,51 @@ exports[`links > Convert link block with marks 1`] = ` } `; -exports[`links > Convert two adjacent links in a block 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/basic to HTML 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/empty to HTML 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/nested to HTML 1`] = ` { "attrs": { "backgroundColor": "default", @@ -575,32 +664,124 @@ exports[`links > Convert two adjacent links in a block 1`] = ` "textAlignment": "left", }, "content": [ + { + "text": "Paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "id": "2", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "backgroundColor": "default", + "id": "3", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested Paragraph 2", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/styled to HTML 1`] = ` +{ + "attrs": { + "backgroundColor": "pink", + "id": "1", + "textColor": "orange", + }, + "content": [ + { + "attrs": { + "textAlignment": "center", + }, + "content": [ + { + "text": "Plain ", + "type": "text", + }, { "marks": [ { "attrs": { - "class": null, - "href": "https://www.website.com", - "target": "_blank", + "stringValue": "red", }, - "type": "link", + "type": "textColor", }, ], - "text": "Website", + "text": "Red Text ", "type": "text", }, { "marks": [ { "attrs": { - "class": null, - "href": "https://www.website2.com", - "target": "_blank", + "stringValue": "blue", }, - "type": "link", + "type": "backgroundColor", }, ], - "text": "Website2", + "text": "Blue Background ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Mixed Colors", "type": "text", }, ], diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index 0c490e9ed5..c93441cf94 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -1,297 +1,70 @@ -import { Editor } from "@tiptap/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { BlockNoteEditor, BlockSchema, PartialBlock } from "../.."; +import { BlockNoteEditor, PartialBlock } from "../.."; import { defaultBlockSchema, defaultStyleSchema, } from "../../extensions/Blocks/api/defaultBlocks"; import UniqueID from "../../extensions/UniqueID/UniqueID"; +import { defaultSchemaTestCases } from "../testCases/cases/defaultSchema"; import { blockToNode, nodeToBlock } from "./nodeConversions"; import { partialBlockToBlockForTesting } from "./testUtil"; -let editor: BlockNoteEditor; -let tt: Editor; - -beforeEach(() => { - editor = BlockNoteEditor.create(); - tt = editor._tiptapEditor; -}); - -afterEach(() => { - tt.destroy(); - editor = undefined as any; - tt = undefined as any; -}); +function addIdsToBlock(block: PartialBlock) { + if (!block.id) { + block.id = UniqueID.options.generateID(); + } + for (const child of block.children || []) { + addIdsToBlock(child); + } +} function validateConversion( block: PartialBlock, - schema: BlockSchema + editor: BlockNoteEditor ) { - const node = blockToNode(block, tt.schema, defaultStyleSchema); + addIdsToBlock(block); + const node = blockToNode( + block, + editor._tiptapEditor.schema, + defaultStyleSchema + ); expect(node).toMatchSnapshot(); const outputBlock = nodeToBlock(node, defaultBlockSchema, defaultStyleSchema); - const fullOriginalBlock = partialBlockToBlockForTesting(schema, block); + const fullOriginalBlock = partialBlockToBlockForTesting( + editor.blockSchema, + block + ); expect(outputBlock).toStrictEqual(fullOriginalBlock); } -describe("Simple ProseMirror Node Conversions", () => { - it("Convert simple block to node", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - }; - validateConversion(block, defaultBlockSchema); - }); -}); - -describe("Complex ProseMirror Node Conversions", () => { - it("Convert complex block to node", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "heading", - props: { - backgroundColor: "blue", - textColor: "yellow", - textAlignment: "right", - level: 2, - }, - content: [ - { - type: "text", - text: "Heading ", - styles: { - bold: true, - underline: true, - }, - }, - { - type: "text", - text: "2", - styles: { - italic: true, - strike: true, - }, - }, - ], - children: [ - { - id: UniqueID.options.generateID(), - type: "paragraph", - props: { - backgroundColor: "red", - }, - content: "Paragraph", - children: [], - }, - { - id: UniqueID.options.generateID(), - type: "bulletListItem", - props: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); -}); - -describe("links", () => { - it("Convert a block with link", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "link", - href: "https://www.website.com", - content: "Website", - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); - - it("Convert link block with marks", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "link", - href: "https://www.website.com", - content: [ - { - type: "text", - text: "Web", - styles: { - bold: true, - }, - }, - { - type: "text", - text: "site", - styles: {}, - }, - ], - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); - - it("Convert two adjacent links in a block", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "link", - href: "https://www.website.com", - content: "Website", - }, - { - type: "link", - href: "https://www.website2.com", - content: "Website2", - }, - ], - }; - - validateConversion(block, defaultBlockSchema); - }); -}); - -describe("hard breaks", () => { - it("Convert a block with a hard break", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "Text1\nText2", - styles: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); - - it("Convert a block with multiple hard breaks", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "Text1\nText2\nText3", - styles: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); - - it("Convert a block with a hard break at the start", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "\nText1", - styles: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); +const testCases = [defaultSchemaTestCases]; - it("Convert a block with a hard break at the end", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "Text1\n", - styles: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); +describe("Test BlockNote-Prosemirror conversion", () => { + for (const testCase of testCases) { + describe("Case: " + testCase.name, () => { + let editor: BlockNoteEditor; - it("Convert a block with only a hard break", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "\n", - styles: {}, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); + beforeEach(() => { + editor = testCase.createEditor(); + }); - it("Convert a block with a hard break and different styles", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "text", - text: "Text1\n", - styles: {}, - }, - { - type: "text", - text: "Text2", - styles: { bold: true }, - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); + afterEach(() => { + editor._tiptapEditor.destroy(); + editor = undefined as any; - it("Convert a block with a hard break in a link", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "link", - href: "https://www.website.com", - content: "Link1\nLink1", - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; + }); - it("Convert a block with a hard break between links", async () => { - const block: PartialBlock = { - id: UniqueID.options.generateID(), - type: "paragraph", - content: [ - { - type: "link", - href: "https://www.website.com", - content: "Link1\n", - }, - { - type: "link", - href: "https://www.website2.com", - content: "Link2", - }, - ], - }; - validateConversion(block, defaultBlockSchema); - }); + for (const document of testCase.documents) { + // eslint-disable-next-line no-loop-func + it("Convert " + document.name + " to HTML", () => { + validateConversion(document.blocks[0], editor); + }); + } + }); + } }); diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index 13b8ff681d..5f45707961 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -29,8 +29,8 @@ function textShorthandToStyledText( } function partialContentToInlineContent( - content: PartialInlineContent | TableContent = "" -): InlineContent[] | TableContent { + content: PartialInlineContent | TableContent | undefined +): InlineContent[] | TableContent | undefined { if (typeof content === "string") { return textShorthandToStyledText(content); } @@ -66,7 +66,8 @@ export function partialBlockToBlockForTesting< id: "", type: partialBlock.type!, props: {} as any, - content: [] as any, + content: + schema[partialBlock.type!].content === "inline" ? [] : (undefined as any), children: [] as any, ...partialBlock, }; diff --git a/packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html b/packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html new file mode 100644 index 0000000000..c6f43c11b1 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html @@ -0,0 +1 @@ +

    Heading 2

    Paragraph

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html b/packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html new file mode 100644 index 0000000000..efec8f89d3 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html @@ -0,0 +1 @@ +

    Heading 2

    Paragraph

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html new file mode 100644 index 0000000000..d9af93c752 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html @@ -0,0 +1 @@ +

    Text1
    Text2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html new file mode 100644 index 0000000000..a88858f652 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html @@ -0,0 +1 @@ +

    Text1
    Text2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html new file mode 100644 index 0000000000..bb3c90b25c --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html @@ -0,0 +1 @@ +

    Link1
    Link2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html new file mode 100644 index 0000000000..f710f08741 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html new file mode 100644 index 0000000000..755d65be05 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html @@ -0,0 +1 @@ +

    Text1

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html new file mode 100644 index 0000000000..d441ef69af --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html @@ -0,0 +1 @@ +

    Text1

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html new file mode 100644 index 0000000000..70d35a5d8c --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html @@ -0,0 +1 @@ +

    Link1
    Link1

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html new file mode 100644 index 0000000000..eb0b99808d --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html new file mode 100644 index 0000000000..db553727c0 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html @@ -0,0 +1 @@ +

    Text1
    Text2
    Text3

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html new file mode 100644 index 0000000000..5ae6ac8b30 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html @@ -0,0 +1 @@ +

    Text1
    Text2
    Text3

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html new file mode 100644 index 0000000000..82093bacd3 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html @@ -0,0 +1 @@ +


    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html new file mode 100644 index 0000000000..c78443c0ac --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html @@ -0,0 +1 @@ +


    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html new file mode 100644 index 0000000000..550b2b88d2 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html @@ -0,0 +1 @@ +


    Text1

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html new file mode 100644 index 0000000000..436596e499 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html @@ -0,0 +1 @@ +


    Text1

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html new file mode 100644 index 0000000000..193b4d61aa --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html @@ -0,0 +1 @@ +

    Text1
    Text2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html new file mode 100644 index 0000000000..f08d9c579f --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html @@ -0,0 +1 @@ +

    Text1
    Text2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html b/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html new file mode 100644 index 0000000000..8876f46341 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html @@ -0,0 +1 @@ +

    WebsiteWebsite2

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html b/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html new file mode 100644 index 0000000000..e11c631cac --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html new file mode 100644 index 0000000000..1b68f7c926 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html @@ -0,0 +1 @@ +

    Website

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html new file mode 100644 index 0000000000..5d7d50c2bc --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html b/packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html new file mode 100644 index 0000000000..36a369a5e4 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html @@ -0,0 +1 @@ +

    Website

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html b/packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html new file mode 100644 index 0000000000..84e54b7e4a --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html b/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html new file mode 100644 index 0000000000..c659260f6e --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html @@ -0,0 +1 @@ +

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html b/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html new file mode 100644 index 0000000000..96547312cd --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html @@ -0,0 +1 @@ +

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index f515197962..6993c8b59b 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -1,8 +1,8 @@ -import { Editor } from "@tiptap/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { + BlockSchema, BlockSchemaFromSpecs, BlockSpecs, PartialBlock, @@ -14,11 +14,15 @@ import { defaultBlockSpecs, } from "../../../extensions/Blocks/api/defaultBlocks"; import { defaultProps } from "../../../extensions/Blocks/api/defaultProps"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContentTypes"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles"; import { imagePropSchema, renderImage, } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; +import { EditorTestCases } from "../../testCases"; +import { defaultSchemaTestCases } from "../../testCases/cases/defaultSchema"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; import { createInternalHTMLSerializer } from "./internalHTMLSerializer"; @@ -88,37 +92,220 @@ const customSpecs = { simpleCustomParagraph: SimpleCustomParagraph, } satisfies BlockSpecs; -type CustomSchemaType = BlockSchemaFromSpecs; - -let editor: BlockNoteEditor< - CustomSchemaType, +const editorTestCases: EditorTestCases< + BlockSchemaFromSpecs, DefaultInlineContentSchema, DefaultStyleSchema ->; -let tt: Editor; - -beforeEach(() => { - editor = BlockNoteEditor.create({ - blockSpecs: customSpecs, - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, - }); - tt = editor._tiptapEditor; -}); - -afterEach(() => { - tt.destroy(); - editor = undefined as any; - tt = undefined as any; - - delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; -}); - -function convertToHTMLAndCompareSnapshots( - blocks: PartialBlock[], +> = { + name: "custom schema", + createEditor: () => { + return BlockNoteEditor.create({ + blockSpecs: customSpecs, + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + }, + documents: [ + { + name: "simpleImage/button", + blocks: [ + { + type: "simpleImage" as const, + }, + ], + }, + { + name: "simpleImage/basic", + blocks: [ + { + type: "simpleImage" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + } as const, + }, + ], + }, + { + name: "simpleImage/nested", + blocks: [ + { + type: "simpleImage" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + } as const, + children: [ + { + type: "simpleImage" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + } as const, + }, + ], + }, + ], + }, + { + name: "customParagraph/basic", + blocks: [ + { + type: "customParagraph" as const, + content: "Custom Paragraph", + }, + ], + }, + { + name: "customParagraph/styled", + blocks: [ + { + type: "customParagraph" as const, + props: { + textAlignment: "center", + textColor: "orange", + backgroundColor: "pink", + } as const, + content: [ + { + type: "text", + styles: {}, + text: "Plain ", + }, + { + type: "text", + styles: { + textColor: "red", + }, + text: "Red Text ", + }, + { + type: "text", + styles: { + backgroundColor: "blue", + }, + text: "Blue Background ", + }, + { + type: "text", + styles: { + textColor: "red", + backgroundColor: "blue", + }, + text: "Mixed Colors", + }, + ], + }, + ], + }, + { + name: "customParagraph/nested", + blocks: [ + { + type: "customParagraph" as const, + content: "Custom Paragraph", + children: [ + { + type: "customParagraph" as const, + content: "Nested Custom Paragraph 1", + }, + { + type: "customParagraph" as const, + content: "Nested Custom Paragraph 2", + }, + ], + }, + ], + }, + { + name: "simpleCustomParagraph/basic", + blocks: [ + { + type: "simpleCustomParagraph" as const, + content: "Custom Paragraph", + }, + ], + }, + { + name: "simpleCustomParagraph/styled", + blocks: [ + { + type: "simpleCustomParagraph" as const, + props: { + textAlignment: "center", + textColor: "orange", + backgroundColor: "pink", + } as const, + content: [ + { + type: "text", + styles: {}, + text: "Plain ", + }, + { + type: "text", + styles: { + textColor: "red", + }, + text: "Red Text ", + }, + { + type: "text", + styles: { + backgroundColor: "blue", + }, + text: "Blue Background ", + }, + { + type: "text", + styles: { + textColor: "red", + backgroundColor: "blue", + }, + text: "Mixed Colors", + }, + ], + }, + ], + }, + { + name: "simpleCustomParagraph/nested", + blocks: [ + { + type: "simpleCustomParagraph" as const, + content: "Custom Paragraph", + children: [ + { + type: "simpleCustomParagraph" as const, + content: "Nested Custom Paragraph 1", + }, + { + type: "simpleCustomParagraph" as const, + content: "Nested Custom Paragraph 2", + }, + ], + }, + ], + }, + ], +}; + +function convertToHTMLAndCompareSnapshots< + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], snapshotDirectory: string, snapshotName: string ) { - const serializer = createInternalHTMLSerializer(tt.schema, editor); + const serializer = createInternalHTMLSerializer( + editor._tiptapEditor.schema, + editor + ); const internalHTML = serializer.serializeBlocks(blocks); const internalHTMLSnapshotPath = "./__snapshots__/" + @@ -128,7 +315,10 @@ function convertToHTMLAndCompareSnapshots( "/internal.html"; expect(internalHTML).toMatchFileSnapshot(internalHTMLSnapshotPath); - const exporter = createExternalHTMLExporter(tt.schema, editor); + const exporter = createExternalHTMLExporter( + editor._tiptapEditor.schema, + editor + ); const externalHTML = exporter.exportBlocks(blocks); const externalHTMLSnapshotPath = "./__snapshots__/" + @@ -139,400 +329,36 @@ function convertToHTMLAndCompareSnapshots( expect(externalHTML).toMatchFileSnapshot(externalHTMLSnapshotPath); } -describe("Convert paragraphs to HTML", () => { - it("Convert paragraph to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "paragraph", - content: "Paragraph", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "paragraph", "basic"); - }); - - it("Convert styled paragraph to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "paragraph", - props: { - textAlignment: "center", - textColor: "orange", - backgroundColor: "pink", - }, - content: [ - { - type: "text", - styles: {}, - text: "Plain ", - }, - { - type: "text", - styles: { - textColor: "red", - }, - text: "Red Text ", - }, - { - type: "text", - styles: { - backgroundColor: "blue", - }, - text: "Blue Background ", - }, - { - type: "text", - styles: { - textColor: "red", - backgroundColor: "blue", - }, - text: "Mixed Colors", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "paragraph", "styled"); - }); - - it("Convert nested paragraph to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "paragraph", - content: "Paragraph", - children: [ - { - type: "paragraph", - content: "Nested Paragraph 1", - }, - { - type: "paragraph", - content: "Nested Paragraph 2", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "paragraph", "nested"); - }); -}); - -describe("Convert images to HTML", () => { - it("Convert add image button to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "image", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "image", "button"); - }); - - it("Convert image to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "image", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "image", "basic"); - }); - - it("Convert nested image to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "image", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - children: [ - { - type: "image", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "image", "nested"); - }); -}); - -describe("Convert simple images to HTML", () => { - it("Convert simple add image button to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleImage", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleImage", "button"); - }); - - it("Convert simple image to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleImage", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleImage", "basic"); - }); - - it("Convert nested image to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleImage", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - children: [ - { - type: "simpleImage", - props: { - url: "exampleURL", - caption: "Caption", - width: 256, - }, - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleImage", "nested"); - }); -}); - -describe("Convert custom blocks with inline content to HTML", () => { - it("Convert custom block with inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "customParagraph", - content: "Custom Paragraph", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "customParagraph", "basic"); - }); - - it("Convert styled custom block with inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "customParagraph", - props: { - textAlignment: "center", - textColor: "orange", - backgroundColor: "pink", - }, - content: [ - { - type: "text", - styles: {}, - text: "Plain ", - }, - { - type: "text", - styles: { - textColor: "red", - }, - text: "Red Text ", - }, - { - type: "text", - styles: { - backgroundColor: "blue", - }, - text: "Blue Background ", - }, - { - type: "text", - styles: { - textColor: "red", - backgroundColor: "blue", - }, - text: "Mixed Colors", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "customParagraph", "styled"); - }); - - it("Convert nested block with inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "customParagraph", - content: "Custom Paragraph", - children: [ - { - type: "customParagraph", - content: "Nested Custom Paragraph 1", - }, - { - type: "customParagraph", - content: "Nested Custom Paragraph 2", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "customParagraph", "nested"); - }); -}); - -describe("Convert custom blocks with non-exported inline content to HTML", () => { - it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleCustomParagraph", - content: "Custom Paragraph", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleCustomParagraph", "basic"); - }); - - it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleCustomParagraph", - props: { - textAlignment: "center", - textColor: "orange", - backgroundColor: "pink", - }, - content: [ - { - type: "text", - styles: {}, - text: "Plain ", - }, - { - type: "text", - styles: { - textColor: "red", - }, - text: "Red Text ", - }, - { - type: "text", - styles: { - backgroundColor: "blue", - }, - text: "Blue Background ", - }, - { - type: "text", - styles: { - textColor: "red", - backgroundColor: "blue", - }, - text: "Mixed Colors", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleCustomParagraph", "styled"); - }); - - it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: PartialBlock< - CustomSchemaType, - DefaultInlineContentSchema, - any - >[] = [ - { - type: "simpleCustomParagraph", - content: "Custom Paragraph", - children: [ - { - type: "simpleCustomParagraph", - content: "Nested Custom Paragraph 1", - }, - { - type: "simpleCustomParagraph", - content: "Nested Custom Paragraph 2", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "simpleCustomParagraph", "nested"); - }); +const testCases = [defaultSchemaTestCases, editorTestCases]; + +describe("Test HTML conversion", () => { + for (const testCase of testCases) { + describe("Case: " + testCase.name, () => { + let editor: BlockNoteEditor; + + beforeEach(() => { + editor = testCase.createEditor(); + }); + + afterEach(() => { + editor._tiptapEditor.destroy(); + editor = undefined as any; + + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; + }); + + for (const document of testCase.documents) { + // eslint-disable-next-line no-loop-func + it("Convert " + document.name + " to HTML", () => { + const nameSplit = document.name.split("/"); + convertToHTMLAndCompareSnapshots( + editor, + document.blocks, + nameSplit[0], + nameSplit[1] + ); + }); + } + }); + } }); diff --git a/packages/core/src/api/testCases/cases/defaultSchema.ts b/packages/core/src/api/testCases/cases/defaultSchema.ts new file mode 100644 index 0000000000..38817dcb9d --- /dev/null +++ b/packages/core/src/api/testCases/cases/defaultSchema.ts @@ -0,0 +1,400 @@ +import { EditorTestCases } from ".."; +import { + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "../../.."; +import { BlockNoteEditor } from "../../../BlockNoteEditor"; + +export const defaultSchemaTestCases: EditorTestCases< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema +> = { + name: "default schema", + createEditor: () => { + // debugger; + + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + }, + documents: [ + { + name: "paragraph/empty", + blocks: [ + { + type: "paragraph" as const, + }, + ], + }, + { + name: "paragraph/basic", + blocks: [ + { + type: "paragraph" as const, + content: "Paragraph", + }, + ], + }, + { + name: "paragraph/styled", + blocks: [ + { + type: "paragraph" as const, + props: { + textAlignment: "center", + textColor: "orange", + backgroundColor: "pink", + } as const, + content: [ + { + type: "text", + styles: {}, + text: "Plain ", + }, + { + type: "text", + styles: { + textColor: "red", + }, + text: "Red Text ", + }, + { + type: "text", + styles: { + backgroundColor: "blue", + }, + text: "Blue Background ", + }, + { + type: "text", + styles: { + textColor: "red", + backgroundColor: "blue", + }, + text: "Mixed Colors", + }, + ], + }, + ], + }, + { + name: "paragraph/nested", + blocks: [ + { + type: "paragraph" as const, + content: "Paragraph", + children: [ + { + type: "paragraph" as const, + content: "Nested Paragraph 1", + }, + { + type: "paragraph" as const, + content: "Nested Paragraph 2", + }, + ], + }, + ], + }, + { + name: "image/button", + blocks: [ + { + type: "image" as const, + }, + ], + }, + { + name: "image/basic", + blocks: [ + { + type: "image" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + }, + }, + ], + }, + { + name: "image/nested", + blocks: [ + { + type: "image" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + } as const, + children: [ + { + type: "image" as const, + props: { + url: "exampleURL", + caption: "Caption", + width: 256, + } as const, + }, + ], + }, + ], + }, + { + name: "link/basic", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: "Website", + }, + ], + }, + ], + }, + { + name: "link/styled", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: [ + { + type: "text", + text: "Web", + styles: { + bold: true, + }, + }, + { + type: "text", + text: "site", + styles: {}, + }, + ], + }, + ], + }, + ], + }, + { + name: "link/adjacent", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: "Website", + }, + { + type: "link", + href: "https://www.website2.com", + content: "Website2", + }, + ], + }, + ], + }, + { + name: "hardbreak/basic", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "Text1\nText2", + styles: {}, + }, + ], + }, + ], + }, + { + name: "hardbreak/multiple", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "Text1\nText2\nText3", + styles: {}, + }, + ], + }, + ], + }, + { + name: "hardbreak/start", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "\nText1", + styles: {}, + }, + ], + }, + ], + }, + { + name: "hardbreak/end", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "Text1\n", + styles: {}, + }, + ], + }, + ], + }, + { + name: "hardbreak/only", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "\n", + styles: {}, + }, + ], + }, + ], + }, + { + name: "hardbreak/styles", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "text", + text: "Text1\n", + styles: {}, + }, + { + type: "text", + text: "Text2", + styles: { bold: true }, + }, + ], + }, + ], + }, + { + name: "hardbreak/link", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: "Link1\nLink1", + }, + ], + }, + ], + }, + { + name: "hardbreak/between-links", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: "Link1\n", + }, + { + type: "link", + href: "https://www.website2.com", + content: "Link2", + }, + ], + }, + ], + }, + { + name: "complex/misc", + blocks: [ + { + // id: UniqueID.options.generateID(), + type: "heading", + props: { + backgroundColor: "blue", + textColor: "yellow", + textAlignment: "right", + level: 2, + }, + content: [ + { + type: "text", + text: "Heading ", + styles: { + bold: true, + underline: true, + }, + }, + { + type: "text", + text: "2", + styles: { + italic: true, + strike: true, + }, + }, + ], + children: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + props: { + backgroundColor: "red", + }, + content: "Paragraph", + children: [], + }, + { + // id: UniqueID.options.generateID(), + type: "bulletListItem", + props: {}, + }, + ], + }, + ], + }, + ], +}; diff --git a/packages/core/src/api/testCases/index.ts b/packages/core/src/api/testCases/index.ts new file mode 100644 index 0000000000..3e640c9b71 --- /dev/null +++ b/packages/core/src/api/testCases/index.ts @@ -0,0 +1,19 @@ +import { BlockNoteEditor, InlineContentSchema } from "../.."; +import { + BlockSchema, + PartialBlock, +} from "../../extensions/Blocks/api/blockTypes"; +import { StyleSchema } from "../../extensions/Blocks/api/styles"; + +export type EditorTestCases< + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +> = { + name: string; + createEditor: () => BlockNoteEditor; + documents: Array<{ + name: string; + blocks: PartialBlock[]; + }>; +}; From 0c647dae034f5a0411de7cb6ae555b6cbfd4f39e Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 13:15:27 +0100 Subject: [PATCH 08/31] update tests --- .../nodeConversions.test.ts.snap | 38 +++++++++---------- .../nodeConversions/nodeConversions.test.ts | 3 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index f6a8eceece..bbe8c4732d 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert complex/misc to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert complex/misc to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "blue", @@ -89,7 +89,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/basic to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/basic to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -121,7 +121,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/between-links to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/between-links to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -173,7 +173,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/end to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/end to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -201,7 +201,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/link to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/link to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -253,7 +253,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/multiple to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/multiple to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -292,7 +292,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/only to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/only to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -316,7 +316,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/start to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/start to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -344,7 +344,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/styles to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert hardbreak/styles to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -381,7 +381,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/basic to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/basic to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -403,7 +403,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/button to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/button to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -425,7 +425,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/nested to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert image/nested to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -471,7 +471,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/adjacent to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/adjacent to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -520,7 +520,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/basic to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/basic to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -555,7 +555,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/styled to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert link/styled to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -607,7 +607,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/basic to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/basic to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -632,7 +632,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/empty to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/empty to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -651,7 +651,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/nested to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/nested to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "default", @@ -725,7 +725,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert } `; -exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/styled to HTML 1`] = ` +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert paragraph/styled to/from prosemirror 1`] = ` { "attrs": { "backgroundColor": "pink", diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index c93441cf94..5ec7748d08 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -61,7 +61,8 @@ describe("Test BlockNote-Prosemirror conversion", () => { for (const document of testCase.documents) { // eslint-disable-next-line no-loop-func - it("Convert " + document.name + " to HTML", () => { + it("Convert " + document.name + " to/from prosemirror", () => { + // NOTE: only converts first block validateConversion(document.blocks[0], editor); }); } From 3f86bd4dd9e04d7aec783f5929f7715fd1c68471 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 15:24:08 +0100 Subject: [PATCH 09/31] move schema files --- packages/core/src/BlockNoteEditor.ts | 7 +- packages/core/src/BlockNoteExtensions.ts | 26 +++---- .../blockManipulation/blockManipulation.ts | 4 +- .../api/nodeConversions/nodeConversions.ts | 6 +- .../core/src/api/nodeConversions/testUtil.ts | 6 +- .../clipboardHandlerExtension.ts | 4 +- .../html/externalHTMLExporter.ts | 4 +- .../serialization/html/htmlConversion.test.ts | 8 +-- .../html/internalHTMLSerializer.ts | 4 +- .../html/sharedHTMLConversion.ts | 4 +- .../src/api/testCases/cases/customStyles.ts | 69 ++++++++++++++++++ packages/core/src/api/testCases/index.ts | 4 +- .../BackgroundColor/BackgroundColorMark.ts | 8 ++- .../{customBlocks.ts => blocks/createSpec.ts} | 14 ++-- .../api/{block.ts => blocks/internal.ts} | 14 ++-- .../api/{blockTypes.ts => blocks/types.ts} | 6 +- .../Blocks/api/cursorPositionTypes.ts | 6 +- .../extensions/Blocks/api/defaultBlocks.ts | 70 +++++-------------- .../src/extensions/Blocks/api/defaultProps.ts | 2 +- .../types.ts} | 4 +- .../extensions/Blocks/api/selectionTypes.ts | 6 +- .../Blocks/api/styles/createSpec.ts | 43 ++++++++++++ .../extensions/Blocks/api/styles/internal.ts | 34 +++++++++ .../Blocks/api/{styles.ts => styles/types.ts} | 13 ++-- .../extensions/Blocks/nodes/BlockContainer.ts | 6 +- .../HeadingBlockContent.ts | 4 +- .../ImageBlockContent/ImageBlockContent.ts | 11 +-- .../BulletListItemBlockContent.ts | 4 +- .../NumberedListItemBlockContent.ts | 4 +- .../ParagraphBlockContent.ts | 2 +- .../TableBlockContent/TableBlockContent.ts | 2 +- .../nodes/BlockContent/defaultBlockHelpers.ts | 2 +- .../src/extensions/Blocks/nodes/BlockGroup.ts | 2 +- .../FormattingToolbarPlugin.ts | 2 +- .../HyperlinkToolbarPlugin.ts | 6 +- .../ImageToolbar/ImageToolbarPlugin.ts | 2 +- .../src/extensions/SideMenu/SideMenuPlugin.ts | 6 +- .../extensions/SlashMenu/BaseSlashMenuItem.ts | 6 +- .../extensions/SlashMenu/SlashMenuPlugin.ts | 6 +- .../SlashMenu/defaultSlashMenuItems.ts | 6 +- .../TableHandles/TableHandlesPlugin.ts | 2 +- .../src/extensions/TextColor/TextColorMark.ts | 5 +- packages/core/src/index.ts | 8 +-- .../plugins/suggestion/SuggestionPlugin.ts | 6 +- 44 files changed, 288 insertions(+), 170 deletions(-) create mode 100644 packages/core/src/api/testCases/cases/customStyles.ts rename packages/core/src/extensions/Blocks/api/{customBlocks.ts => blocks/createSpec.ts} (94%) rename packages/core/src/extensions/Blocks/api/{block.ts => blocks/internal.ts} (95%) rename packages/core/src/extensions/Blocks/api/{blockTypes.ts => blocks/types.ts} (98%) rename packages/core/src/extensions/Blocks/api/{inlineContentTypes.ts => inlineContent/types.ts} (96%) create mode 100644 packages/core/src/extensions/Blocks/api/styles/createSpec.ts create mode 100644 packages/core/src/extensions/Blocks/api/styles/internal.ts rename packages/core/src/extensions/Blocks/api/{styles.ts => styles/types.ts} (78%) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index f7176f4a3b..3903e5aebe 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -22,7 +22,7 @@ import { BlockSchema, BlockSpecs, PartialBlock, -} from "./extensions/Blocks/api/blockTypes"; +} from "./extensions/Blocks/api/blocks/types"; import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; import { DefaultBlockSchema, @@ -41,7 +41,7 @@ import { StyleSchema, StyleSpecs, Styles, -} from "./extensions/Blocks/api/styles"; +} from "./extensions/Blocks/api/styles/types"; import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos"; import "prosemirror-tables/style/tables.css"; @@ -49,7 +49,7 @@ import "./editor.css"; import { InlineContentSchema, InlineContentSpecs, -} from "./extensions/Blocks/api/inlineContentTypes"; +} from "./extensions/Blocks/api/inlineContent/types"; import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; import { HyperlinkToolbarProsemirrorPlugin } from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin"; @@ -298,6 +298,7 @@ export class BlockNoteEditor< domAttributes: newOptions.domAttributes || {}, blockSchema: this.blockSchema, blockSpecs: newOptions.blockSpecs, + styleSpecs: newOptions.styleSpecs, collaboration: newOptions.collaboration, }); diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 9c2df8b090..1fce2b3dd7 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -2,36 +2,29 @@ import { Extensions, extensions } from "@tiptap/core"; import { BlockNoteEditor } from "./BlockNoteEditor"; -import { Bold } from "@tiptap/extension-bold"; -import { Code } from "@tiptap/extension-code"; import Collaboration from "@tiptap/extension-collaboration"; import CollaborationCursor from "@tiptap/extension-collaboration-cursor"; import { Dropcursor } from "@tiptap/extension-dropcursor"; import { Gapcursor } from "@tiptap/extension-gapcursor"; import { HardBreak } from "@tiptap/extension-hard-break"; import { History } from "@tiptap/extension-history"; -import { Italic } from "@tiptap/extension-italic"; import { Link } from "@tiptap/extension-link"; -import { Strike } from "@tiptap/extension-strike"; import { Text } from "@tiptap/extension-text"; -import { Underline } from "@tiptap/extension-underline"; import * as Y from "yjs"; import { createClipboardHandlerExtension } from "./api/serialization/clipboardHandlerExtension"; import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension"; -import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark"; import { BlockContainer, BlockGroup, Doc } from "./extensions/Blocks"; import { BlockNoteDOMAttributes, BlockSchema, BlockSpecs, -} from "./extensions/Blocks/api/blockTypes"; -import { InlineContentSchema } from "./extensions/Blocks/api/inlineContentTypes"; -import { StyleSchema } from "./extensions/Blocks/api/styles"; +} from "./extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "./extensions/Blocks/api/inlineContent/types"; +import { StyleSchema, StyleSpecs } from "./extensions/Blocks/api/styles/types"; import { TableExtension } from "./extensions/Blocks/nodes/BlockContent/TableBlockContent/TableExtension"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension"; import { TextColorExtension } from "./extensions/TextColor/TextColorExtension"; -import { TextColorMark } from "./extensions/TextColor/TextColorMark"; import { TrailingNode } from "./extensions/TrailingNode/TrailingNodeExtension"; import UniqueID from "./extensions/UniqueID/UniqueID"; @@ -47,6 +40,7 @@ export const getBlockNoteExtensions = < domAttributes: Partial; blockSchema: BSchema; blockSpecs: BlockSpecs; + styleSpecs: StyleSpecs; collaboration?: { fragment: Y.XmlFragment; user: { @@ -82,15 +76,13 @@ export const getBlockNoteExtensions = < Text, // marks: - Bold, - Code, - Italic, - Strike, - Underline, Link, - TextColorMark, + ...Object.values(opts.styleSpecs).map((styleSpec) => { + return styleSpec.implementation.mark; + }), + TextColorExtension, - BackgroundColorMark, + BackgroundColorExtension, TextAlignmentExtension, diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index f195aa1472..835c6fee20 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -5,8 +5,8 @@ import { BlockIdentifier, BlockSchema, PartialBlock, -} from "../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../extensions/Blocks/api/styles"; +} from "../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; import { blockToNode } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../util/nodeUtil"; diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index f2b9f56704..e360d3a49b 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -6,7 +6,7 @@ import { PartialBlock, PartialTableContent, TableContent, -} from "../../extensions/Blocks/api/blockTypes"; +} from "../../extensions/Blocks/api/blocks/types"; import { InlineContent, InlineContentSchema, @@ -16,8 +16,8 @@ import { isLinkInlineContent, isPartialLinkInlineContent, isStyledTextInlineContent, -} from "../../extensions/Blocks/api/inlineContentTypes"; -import { StyleSchema, Styles } from "../../extensions/Blocks/api/styles"; +} from "../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema, Styles } from "../../extensions/Blocks/api/styles/types"; import { getBlockInfo } from "../../extensions/Blocks/helpers/getBlockInfoFromPos"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { UnreachableCaseError } from "../../shared/utils"; diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index 5f45707961..e17cf1ba5b 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -3,15 +3,15 @@ import { BlockSchema, PartialBlock, TableContent, -} from "../../extensions/Blocks/api/blockTypes"; +} from "../../extensions/Blocks/api/blocks/types"; import { InlineContent, InlineContentSchema, PartialInlineContent, StyledText, isPartialLinkInlineContent, -} from "../../extensions/Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../../extensions/Blocks/api/styles"; +} from "../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; function textShorthandToStyledText( content: string | StyledText[] = "" diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/serialization/clipboardHandlerExtension.ts index 60a7484515..96fc1fb691 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/serialization/clipboardHandlerExtension.ts @@ -2,8 +2,8 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; import { InlineContentSchema } from "../.."; import { BlockNoteEditor } from "../../BlockNoteEditor"; -import { BlockSchema } from "../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../extensions/Blocks/api/styles"; +import { BlockSchema } from "../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; import { markdown } from "../formatConversions/formatConversions"; import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; diff --git a/packages/core/src/api/serialization/html/externalHTMLExporter.ts b/packages/core/src/api/serialization/html/externalHTMLExporter.ts index b9ed6badd9..85aa8d9563 100644 --- a/packages/core/src/api/serialization/html/externalHTMLExporter.ts +++ b/packages/core/src/api/serialization/html/externalHTMLExporter.ts @@ -7,8 +7,8 @@ import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, PartialBlock, -} from "../../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +} from "../../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { simplifyBlocks } from "../../formatConversions/simplifyBlocksRehypePlugin"; import { blockToNode } from "../../nodeConversions/nodeConversions"; import { diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index 6993c8b59b..75682fc389 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -1,21 +1,21 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; +import { createBlockSpec } from "../../../extensions/Blocks/api/blocks/createSpec"; import { BlockSchema, BlockSchemaFromSpecs, BlockSpecs, PartialBlock, -} from "../../../extensions/Blocks/api/blockTypes"; -import { createBlockSpec } from "../../../extensions/Blocks/api/customBlocks"; +} from "../../../extensions/Blocks/api/blocks/types"; import { DefaultInlineContentSchema, DefaultStyleSchema, defaultBlockSpecs, } from "../../../extensions/Blocks/api/defaultBlocks"; import { defaultProps } from "../../../extensions/Blocks/api/defaultProps"; -import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { imagePropSchema, renderImage, diff --git a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts index a2542a97e2..85eb30ae91 100644 --- a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts @@ -4,8 +4,8 @@ import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, PartialBlock, -} from "../../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +} from "../../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { blockToNode } from "../../nodeConversions/nodeConversions"; import { serializeNodeInner, diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index 696fd9a09d..5c5b7abfbc 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -1,8 +1,8 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; -import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +import { BlockSchema } from "../../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { nodeToBlock } from "../../nodeConversions/nodeConversions"; function doc(options: { document?: Document }) { diff --git a/packages/core/src/api/testCases/cases/customStyles.ts b/packages/core/src/api/testCases/cases/customStyles.ts new file mode 100644 index 0000000000..009a402f1b --- /dev/null +++ b/packages/core/src/api/testCases/cases/customStyles.ts @@ -0,0 +1,69 @@ +import { EditorTestCases } from ".."; +import { + DefaultBlockSchema, + DefaultInlineContentSchema, + defaultStyleSpecs, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "../../.."; +import { BlockNoteEditor } from "../../../BlockNoteEditor"; +import { createStyleSpec } from "../../../extensions/Blocks/api/styles/createSpec"; +import { + StyleSchemaFromSpecs, + StyleSpecs, +} from "../../../extensions/Blocks/api/styles/types"; + +const small = createStyleSpec( + { + type: "small", + propSchema: "boolean", + }, + { + render: () => { + const dom = document.createElement("small"); + return { + dom, + contentDOM: dom, + }; + }, + } +); + +const customStyles = { + ...defaultStyleSpecs, + small, +} satisfies StyleSpecs; + +export const customStylesTestCases: EditorTestCases< + DefaultBlockSchema, + DefaultInlineContentSchema, + StyleSchemaFromSpecs +> = { + name: "custom style schema", + createEditor: () => { + // debugger; + + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + styleSpecs: customStyles, + }); + }, + documents: [ + { + name: "small/basic", + blocks: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "This is a small text", + styles: { + small: true, + }, + }, + ], + }, + ], + }, + ], +}; diff --git a/packages/core/src/api/testCases/index.ts b/packages/core/src/api/testCases/index.ts index 3e640c9b71..d0a9f27173 100644 --- a/packages/core/src/api/testCases/index.ts +++ b/packages/core/src/api/testCases/index.ts @@ -2,8 +2,8 @@ import { BlockNoteEditor, InlineContentSchema } from "../.."; import { BlockSchema, PartialBlock, -} from "../../extensions/Blocks/api/blockTypes"; -import { StyleSchema } from "../../extensions/Blocks/api/styles"; +} from "../../extensions/Blocks/api/blocks/types"; +import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; export type EditorTestCases< B extends BlockSchema, diff --git a/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts b/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts index 4c8d93f838..df4b257588 100644 --- a/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts +++ b/packages/core/src/extensions/BackgroundColor/BackgroundColorMark.ts @@ -1,6 +1,7 @@ import { Mark } from "@tiptap/core"; +import { createStyleSpecFromTipTapMark } from "../Blocks/api/styles/internal"; -export const BackgroundColorMark = Mark.create({ +const BackgroundColorMark = Mark.create({ name: "backgroundColor", addAttributes() { @@ -40,3 +41,8 @@ export const BackgroundColorMark = Mark.create({ return ["span", HTMLAttributes, 0]; }, }); + +export const BackgroundColor = createStyleSpecFromTipTapMark( + BackgroundColorMark, + "string" +); diff --git a/packages/core/src/extensions/Blocks/api/customBlocks.ts b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts similarity index 94% rename from packages/core/src/extensions/Blocks/api/customBlocks.ts rename to packages/core/src/extensions/Blocks/api/blocks/createSpec.ts index 9649a08d9f..2bc9f958d9 100644 --- a/packages/core/src/extensions/Blocks/api/customBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts @@ -1,5 +1,6 @@ -import { InlineContentSchema } from "../../.."; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; +import { InlineContentSchema } from "../../../.."; +import { BlockNoteEditor } from "../../../../BlockNoteEditor"; +import { StyleSchema } from "../styles/types"; import { createInternalBlockSpec, createStronglyTypedTiptapNode, @@ -7,13 +8,8 @@ import { parse, propsToAttributes, wrapInBlockStructure, -} from "./block"; -import { - BlockConfig, - BlockFromConfig, - BlockSchemaWithBlock, -} from "./blockTypes"; -import { StyleSchema } from "./styles"; +} from "./internal"; +import { BlockConfig, BlockFromConfig, BlockSchemaWithBlock } from "./types"; // restrict content to "inline" and "none" only export type CustomBlockConfig = BlockConfig & { diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/blocks/internal.ts similarity index 95% rename from packages/core/src/extensions/Blocks/api/block.ts rename to packages/core/src/extensions/Blocks/api/blocks/internal.ts index 064432ecff..80b83a7a92 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/internal.ts @@ -1,8 +1,11 @@ import { Attribute, Attributes, Editor, Node, NodeConfig } from "@tiptap/core"; import { ParseRule } from "prosemirror-model"; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; -import { mergeCSSClasses } from "../../../shared/utils"; -import { defaultBlockToHTML } from "../nodes/BlockContent/defaultBlockHelpers"; +import { BlockNoteEditor } from "../../../../BlockNoteEditor"; +import { mergeCSSClasses } from "../../../../shared/utils"; +import { defaultBlockToHTML } from "../../nodes/BlockContent/defaultBlockHelpers"; +import { inheritedProps } from "../defaultProps"; +import { InlineContentSchema } from "../inlineContent/types"; +import { StyleSchema } from "../styles/types"; import { BlockConfig, BlockSchemaWithBlock, @@ -11,10 +14,7 @@ import { Props, SpecificBlock, TiptapBlockImplementation, -} from "./blockTypes"; -import { inheritedProps } from "./defaultProps"; -import { InlineContentSchema } from "./inlineContentTypes"; -import { StyleSchema } from "./styles"; +} from "./types"; export function camelToDataKebab(str: string): string { return "data-" + str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts similarity index 98% rename from packages/core/src/extensions/Blocks/api/blockTypes.ts rename to packages/core/src/extensions/Blocks/api/blocks/types.ts index 0171deb212..4a87eaf299 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -1,12 +1,12 @@ /** Define the main block types **/ import { Node } from "@tiptap/core"; -import { BlockNoteEditor, DefaultStyleSchema } from "../../.."; +import { BlockNoteEditor, DefaultStyleSchema } from "../../../.."; import { InlineContent, InlineContentSchema, PartialInlineContent, -} from "./inlineContentTypes"; -import { StyleSchema } from "./styles"; +} from "../inlineContent/types"; +import { StyleSchema } from "../styles/types"; export type BlockNoteDOMElement = | "editor" diff --git a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts index 3df1cad2b4..ce21cda6f4 100644 --- a/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts +++ b/packages/core/src/extensions/Blocks/api/cursorPositionTypes.ts @@ -1,6 +1,6 @@ -import { Block, BlockSchema } from "./blockTypes"; -import { InlineContentSchema } from "./inlineContentTypes"; -import { StyleSchema } from "./styles"; +import { Block, BlockSchema } from "./blocks/types"; +import { InlineContentSchema } from "./inlineContent/types"; +import { StyleSchema } from "./styles/types"; export type TextCursorPosition< BSchema extends BlockSchema, diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 624fdf4ac4..701f203611 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -1,15 +1,23 @@ +import Bold from "@tiptap/extension-bold"; +import Code from "@tiptap/extension-code"; +import Italic from "@tiptap/extension-italic"; +import Strike from "@tiptap/extension-strike"; +import Underline from "@tiptap/extension-underline"; +import { BackgroundColor } from "../../BackgroundColor/BackgroundColorMark"; +import { TextColor } from "../../TextColor/TextColorMark"; import { Heading } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent"; import { Image } from "../nodes/BlockContent/ImageBlockContent/ImageBlockContent"; import { BulletListItem } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItem } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { Paragraph } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { Table } from "../nodes/BlockContent/TableBlockContent/TableBlockContent"; -import { BlockSchemaFromSpecs, BlockSpecs } from "./blockTypes"; +import { BlockSchemaFromSpecs, BlockSpecs } from "./blocks/types"; import { InlineContentSchemaFromSpecs, InlineContentSpecs, -} from "./inlineContentTypes"; -import { StyleSchemaFromSpecs, StyleSpecs } from "./styles"; +} from "./inlineContent/types"; +import { createStyleSpecFromTipTapMark } from "./styles/internal"; +import { StyleSchemaFromSpecs, StyleSpecs } from "./styles/types"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -31,55 +39,13 @@ export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); export type DefaultBlockSchema = typeof defaultBlockSchema; export const defaultStyleSpecs = { - bold: { - config: { - type: "bold", - propSchema: "boolean", - }, - implementation: {}, - }, - italic: { - config: { - type: "italic", - propSchema: "boolean", - }, - implementation: {}, - }, - underline: { - config: { - type: "underline", - propSchema: "boolean", - }, - implementation: {}, - }, - strike: { - config: { - type: "strike", - propSchema: "boolean", - }, - implementation: {}, - }, - code: { - config: { - type: "code", - propSchema: "boolean", - }, - implementation: {}, - }, - textColor: { - config: { - type: "textColor", - propSchema: "string", - }, - implementation: {}, - }, - backgroundColor: { - config: { - type: "backgroundColor", - propSchema: "string", - }, - implementation: {}, - }, + bold: createStyleSpecFromTipTapMark(Bold, "boolean"), + italic: createStyleSpecFromTipTapMark(Italic, "boolean"), + underline: createStyleSpecFromTipTapMark(Underline, "boolean"), + strike: createStyleSpecFromTipTapMark(Strike, "boolean"), + code: createStyleSpecFromTipTapMark(Code, "boolean"), + textColor: TextColor, + backgroundColor: BackgroundColor, } satisfies StyleSpecs; export function getStyleSchemaFromSpecs(specs: T) { diff --git a/packages/core/src/extensions/Blocks/api/defaultProps.ts b/packages/core/src/extensions/Blocks/api/defaultProps.ts index 17783364a1..43f36d7a6b 100644 --- a/packages/core/src/extensions/Blocks/api/defaultProps.ts +++ b/packages/core/src/extensions/Blocks/api/defaultProps.ts @@ -1,4 +1,4 @@ -import { Props, PropSchema } from "./blockTypes"; +import { Props, PropSchema } from "./blocks/types"; export const defaultProps = { backgroundColor: { diff --git a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts similarity index 96% rename from packages/core/src/extensions/Blocks/api/inlineContentTypes.ts rename to packages/core/src/extensions/Blocks/api/inlineContent/types.ts index ebbf8460c1..0b1118f3b8 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContentTypes.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts @@ -1,5 +1,5 @@ -import { PropSchema, Props } from "./blockTypes"; -import { StyleSchema, Styles } from "./styles"; +import { PropSchema, Props } from "../blocks/types"; +import { StyleSchema, Styles } from "../styles/types"; export type InlineContentConfig = { type: string; diff --git a/packages/core/src/extensions/Blocks/api/selectionTypes.ts b/packages/core/src/extensions/Blocks/api/selectionTypes.ts index cd668c4e98..61d8086ed4 100644 --- a/packages/core/src/extensions/Blocks/api/selectionTypes.ts +++ b/packages/core/src/extensions/Blocks/api/selectionTypes.ts @@ -1,6 +1,6 @@ -import { Block, BlockSchema } from "./blockTypes"; -import { InlineContentSchema } from "./inlineContentTypes"; -import { StyleSchema } from "./styles"; +import { Block, BlockSchema } from "./blocks/types"; +import { InlineContentSchema } from "./inlineContent/types"; +import { StyleSchema } from "./styles/types"; export type Selection< BSchema extends BlockSchema, diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts new file mode 100644 index 0000000000..106b405004 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -0,0 +1,43 @@ +import { Mark } from "@tiptap/core"; +import { StyleConfig, StyleSpec } from "./types"; + +export type CustomStyleImplementation = { + render: () => { + dom: HTMLElement; + contentDOM?: HTMLElement; + }; + // Exports block to external HTML. If not defined, the output will be the same + // as `render(...).dom`. Used to create clipboard data when pasting outside + // BlockNote. + // TODO: Maybe can return undefined to ignore when serializing? + // toExternalHTML?: ( + // block: BlockFromConfig, + // editor: BlockNoteEditor, I, S> + // ) => { + // dom: HTMLElement; + // contentDOM?: HTMLElement; + // }; +}; + +export function createStyleSpec( + styleConfig: T, + styleImplementation: CustomStyleImplementation +): StyleSpec { + const mark = Mark.create({ + name: styleConfig.type, + renderHTML() { + const renderResult = styleImplementation.render(); + return { + dom: renderResult.dom, + contentDOM: renderResult.contentDOM, + }; + }, + }); + + return { + config: styleConfig, + implementation: { + mark, + }, + }; +} diff --git a/packages/core/src/extensions/Blocks/api/styles/internal.ts b/packages/core/src/extensions/Blocks/api/styles/internal.ts new file mode 100644 index 0000000000..d843f4777d --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/styles/internal.ts @@ -0,0 +1,34 @@ +import { Mark } from "@tiptap/core"; +import { + StyleConfig, + StyleImplementation, + StylePropSchema, + StyleSpec, +} from "./types"; + +// This helper function helps to instantiate a stylespec with a +// config and implementation that conform to the type of Config +export function createInternalStyleSpec( + config: T, + implementation: StyleImplementation +) { + return { + config, + implementation, + } satisfies StyleSpec; +} + +export function createStyleSpecFromTipTapMark< + T extends Mark, + P extends StylePropSchema +>(mark: T, propSchema: P) { + return createInternalStyleSpec( + { + type: mark.name as T["name"], + propSchema, + }, + { + mark, + } + ); +} diff --git a/packages/core/src/extensions/Blocks/api/styles.ts b/packages/core/src/extensions/Blocks/api/styles/types.ts similarity index 78% rename from packages/core/src/extensions/Blocks/api/styles.ts rename to packages/core/src/extensions/Blocks/api/styles/types.ts index d03c27a15b..96be7a447a 100644 --- a/packages/core/src/extensions/Blocks/api/styles.ts +++ b/packages/core/src/extensions/Blocks/api/styles/types.ts @@ -1,17 +1,22 @@ +import { Mark } from "@tiptap/core"; + +export type StylePropSchema = "boolean" | "string"; // TODO: use PropSchema as name? Use objects as type similar to blocks? + export type StyleConfig = { type: string; - readonly propSchema: "boolean" | "string"; + readonly propSchema: StylePropSchema; // content: "inline" | "none" | "table"; }; -// @ts-ignore -export type StyleImplementation = any; +export type StyleImplementation = { + mark: Mark; +}; // Container for both the config and implementation of a block, // and the type of BlockImplementation is based on that of the config export type StyleSpec = { config: T; - implementation: StyleImplementation; + implementation: StyleImplementation; }; export type StyleSchema = Record; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 68245d1f3a..9c93e1ffb1 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -15,9 +15,9 @@ import { BlockNoteDOMAttributes, BlockSchema, PartialBlock, -} from "../api/blockTypes"; -import { InlineContentSchema } from "../api/inlineContentTypes"; -import { StyleSchema } from "../api/styles"; +} from "../api/blocks/types"; +import { InlineContentSchema } from "../api/inlineContent/types"; +import { StyleSchema } from "../api/styles/types"; import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"; import BlockAttributes from "./BlockAttributes"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts index dcb82fa5b6..3cfbea0518 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts @@ -2,8 +2,8 @@ import { InputRule } from "@tiptap/core"; import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, -} from "../../../api/block"; -import { PropSchema } from "../../../api/blockTypes"; +} from "../../../api/blocks/internal"; +import { PropSchema } from "../../../api/blocks/types"; import { defaultProps } from "../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts index 3e171e8b14..2fea7b6f71 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts @@ -1,15 +1,18 @@ import { BlockNoteEditor } from "../../../../../BlockNoteEditor"; import { imageToolbarPluginKey } from "../../../../ImageToolbar/ImageToolbarPlugin"; +import { + CustomBlockConfig, + createBlockSpec, +} from "../../../api/blocks/createSpec"; import { BlockFromConfig, BlockSchemaWithBlock, PropSchema, -} from "../../../api/blockTypes"; -import { CustomBlockConfig, createBlockSpec } from "../../../api/customBlocks"; +} from "../../../api/blocks/types"; import { defaultProps } from "../../../api/defaultProps"; -import { InlineContentSchema } from "../../../api/inlineContentTypes"; -import { StyleSchema } from "../../../api/styles"; +import { InlineContentSchema } from "../../../api/inlineContent/types"; +import { StyleSchema } from "../../../api/styles/types"; export const imagePropSchema = { textAlignment: defaultProps.textAlignment, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 46777fa1a6..6362288e02 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -2,8 +2,8 @@ import { InputRule } from "@tiptap/core"; import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, -} from "../../../../api/block"; -import { PropSchema } from "../../../../api/blockTypes"; +} from "../../../../api/blocks/internal"; +import { PropSchema } from "../../../../api/blocks/types"; import { defaultProps } from "../../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers"; import { handleEnter } from "../ListItemKeyboardShortcuts"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index 1ce586fd4b..5c838415e0 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -2,8 +2,8 @@ import { InputRule } from "@tiptap/core"; import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, -} from "../../../../api/block"; -import { PropSchema } from "../../../../api/blockTypes"; +} from "../../../../api/blocks/internal"; +import { PropSchema } from "../../../../api/blocks/types"; import { defaultProps } from "../../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers"; import { handleEnter } from "../ListItemKeyboardShortcuts"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts index d0d4c5d34e..a645ba347c 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts @@ -1,7 +1,7 @@ import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, -} from "../../../api/block"; +} from "../../../api/blocks/internal"; import { defaultProps } from "../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts index f24ff7ce4d..b3793ccb07 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts @@ -5,7 +5,7 @@ import { TableRow } from "@tiptap/extension-table-row"; import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, -} from "../../../api/block"; +} from "../../../api/blocks/internal"; import { defaultProps } from "../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers"; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts index 79abe614d1..8bd002dfbb 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts @@ -2,7 +2,7 @@ import { Block, BlockSchema, InlineContentSchema } from "../../../.."; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { blockToNode } from "../../../../api/nodeConversions/nodeConversions"; import { mergeCSSClasses } from "../../../../shared/utils"; -import { StyleSchema } from "../../api/styles"; +import { StyleSchema } from "../../api/styles/types"; // Function that creates a ProseMirror `DOMOutputSpec` for a default block. // Since all default blocks have the same structure (`blockContent` div with a diff --git a/packages/core/src/extensions/Blocks/nodes/BlockGroup.ts b/packages/core/src/extensions/Blocks/nodes/BlockGroup.ts index 31d9af1516..88f4a3025c 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockGroup.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockGroup.ts @@ -1,6 +1,6 @@ import { Node } from "@tiptap/core"; -import { BlockNoteDOMAttributes } from "../api/blockTypes"; import { mergeCSSClasses } from "../../../shared/utils"; +import { BlockNoteDOMAttributes } from "../api/blocks/types"; export const BlockGroup = Node.create<{ domAttributes?: BlockNoteDOMAttributes; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 948195c970..f394c1deea 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -9,7 +9,7 @@ import { InlineContentSchema, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; -import { StyleSchema } from "../Blocks/api/styles"; +import { StyleSchema } from "../Blocks/api/styles/types"; export type FormattingToolbarCallbacks = BaseUiElementCallbacks; diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index a316ef3d46..ea1b726ac8 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -5,9 +5,9 @@ import { Plugin, PluginKey } from "prosemirror-state"; import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; -import { BlockSchema } from "../Blocks/api/blockTypes"; -import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../Blocks/api/styles"; +import { BlockSchema } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; +import { StyleSchema } from "../Blocks/api/styles/types"; export type HyperlinkToolbarState = BaseUiElementState & { // The hovered hyperlink's URL, and the text it's displayed with in the diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index a55f26ca34..90392ca906 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -9,7 +9,7 @@ import { SpecificBlock, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; -import { StyleSchema } from "../Blocks/api/styles"; +import { StyleSchema } from "../Blocks/api/styles/types"; export type ImageToolbarCallbacks = BaseUiElementCallbacks; export type ImageToolbarState< diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index bc1fc87595..8f023ad70f 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -8,9 +8,9 @@ import { createExternalHTMLExporter } from "../../api/serialization/html/externa import { createInternalHTMLSerializer } from "../../api/serialization/html/internalHTMLSerializer"; import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; -import { Block, BlockSchema } from "../Blocks/api/blockTypes"; -import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../Blocks/api/styles"; +import { Block, BlockSchema } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; +import { StyleSchema } from "../Blocks/api/styles/types"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { slashMenuPluginKey } from "../SlashMenu/SlashMenuPlugin"; import { MultipleNodeSelection } from "./MultipleNodeSelection"; diff --git a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts index c6be9b3717..6bcfd8c361 100644 --- a/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts +++ b/packages/core/src/extensions/SlashMenu/BaseSlashMenuItem.ts @@ -1,8 +1,8 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; -import { BlockSchema } from "../Blocks/api/blockTypes"; -import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../Blocks/api/styles"; +import { BlockSchema } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; +import { StyleSchema } from "../Blocks/api/styles/types"; export type BaseSlashMenuItem< BSchema extends BlockSchema, diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts index e1f4081815..67aec3cdb0 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuPlugin.ts @@ -6,9 +6,9 @@ import { SuggestionsMenuState, setupSuggestionsMenu, } from "../../shared/plugins/suggestion/SuggestionPlugin"; -import { BlockSchema } from "../Blocks/api/blockTypes"; -import { InlineContentSchema } from "../Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../Blocks/api/styles"; +import { BlockSchema } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; +import { StyleSchema } from "../Blocks/api/styles/types"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; export const slashMenuPluginKey = new PluginKey("SlashMenuPlugin"); diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts index 12b583b040..798bffea20 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts @@ -1,10 +1,10 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; -import { Block, BlockSchema, PartialBlock } from "../Blocks/api/blockTypes"; +import { Block, BlockSchema, PartialBlock } from "../Blocks/api/blocks/types"; import { InlineContentSchema, isStyledTextInlineContent, -} from "../Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../Blocks/api/styles"; +} from "../Blocks/api/inlineContent/types"; +import { StyleSchema } from "../Blocks/api/styles/types"; import { imageToolbarPluginKey } from "../ImageToolbar/ImageToolbarPlugin"; import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index a9f090d933..f2963fbbcf 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -10,7 +10,7 @@ import { getDraggableBlockFromCoords, } from "../.."; import { EventEmitter } from "../../shared/EventEmitter"; -import { StyleSchema } from "../Blocks/api/styles"; +import { StyleSchema } from "../Blocks/api/styles/types"; export type TableHandlesCallbacks = BaseUiElementCallbacks; export type TableHandlesState< diff --git a/packages/core/src/extensions/TextColor/TextColorMark.ts b/packages/core/src/extensions/TextColor/TextColorMark.ts index 9737985f9c..c18ab0b374 100644 --- a/packages/core/src/extensions/TextColor/TextColorMark.ts +++ b/packages/core/src/extensions/TextColor/TextColorMark.ts @@ -1,6 +1,7 @@ import { Mark } from "@tiptap/core"; +import { createStyleSpecFromTipTapMark } from "../Blocks/api/styles/internal"; -export const TextColorMark = Mark.create({ +const TextColorMark = Mark.create({ name: "textColor", addAttributes() { @@ -38,3 +39,5 @@ export const TextColorMark = Mark.create({ return ["span", HTMLAttributes, 0]; }, }); + +export const TextColor = createStyleSpecFromTipTapMark(TextColorMark, "string"); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e7849ebd06..6d1441daba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,12 +2,12 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./api/serialization/html/externalHTMLExporter"; export * from "./api/serialization/html/internalHTMLSerializer"; -export * from "./extensions/Blocks/api/block"; -export * from "./extensions/Blocks/api/blockTypes"; -export * from "./extensions/Blocks/api/customBlocks"; +export * from "./extensions/Blocks/api/blocks/createSpec"; +export * from "./extensions/Blocks/api/blocks/internal"; +export * from "./extensions/Blocks/api/blocks/types"; export * from "./extensions/Blocks/api/defaultBlocks"; export * from "./extensions/Blocks/api/defaultProps"; -export * from "./extensions/Blocks/api/inlineContentTypes"; +export * from "./extensions/Blocks/api/inlineContent/types"; export * from "./extensions/Blocks/api/selectionTypes"; export * as blockStyles from "./extensions/Blocks/nodes/Block.css"; export * from "./extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 36d7f83a52..480d935db3 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -1,9 +1,9 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; -import { BlockSchema } from "../../../extensions/Blocks/api/blockTypes"; -import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContentTypes"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles"; +import { BlockSchema } from "../../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import { BaseUiElementState } from "../../BaseUiElementTypes"; import { SuggestionItem } from "./SuggestionItem"; From b7dfa52045e12fde621fefc8d74abef4a0797434 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 15:55:37 +0100 Subject: [PATCH 10/31] add custom style test --- .../nodeConversions.test.ts.snap | 63 +++++++++++++++++ .../nodeConversions/nodeConversions.test.ts | 11 ++- .../api/nodeConversions/nodeConversions.ts | 6 ++ .../fontSize/basic/external.html | 1 + .../fontSize/basic/internal.html | 1 + .../__snapshots__/small/basic/external.html | 1 + .../__snapshots__/small/basic/internal.html | 1 + .../serialization/html/htmlConversion.test.ts | 7 +- .../src/api/testCases/cases/customStyles.ts | 37 +++++++++- .../Blocks/api/styles/createSpec.ts | 67 +++++++++++++------ 10 files changed, 164 insertions(+), 31 deletions(-) create mode 100644 packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index bbe8c4732d..a90ce25c7f 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -1,5 +1,68 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "stringValue": "18px", + }, + "type": "fontSize", + }, + ], + "text": "This is text with a custom fontSize", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "type": "small", + }, + ], + "text": "This is a small text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert complex/misc to/from prosemirror 1`] = ` { "attrs": { diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index 5ec7748d08..9485a58e93 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -1,10 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor, PartialBlock } from "../.."; -import { - defaultBlockSchema, - defaultStyleSchema, -} from "../../extensions/Blocks/api/defaultBlocks"; import UniqueID from "../../extensions/UniqueID/UniqueID"; +import { customStylesTestCases } from "../testCases/cases/customStyles"; import { defaultSchemaTestCases } from "../testCases/cases/defaultSchema"; import { blockToNode, nodeToBlock } from "./nodeConversions"; import { partialBlockToBlockForTesting } from "./testUtil"; @@ -26,12 +23,12 @@ function validateConversion( const node = blockToNode( block, editor._tiptapEditor.schema, - defaultStyleSchema + editor.styleSchema ); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, defaultBlockSchema, defaultStyleSchema); + const outputBlock = nodeToBlock(node, editor.blockSchema, editor.styleSchema); const fullOriginalBlock = partialBlockToBlockForTesting( editor.blockSchema, @@ -41,7 +38,7 @@ function validateConversion( expect(outputBlock).toStrictEqual(fullOriginalBlock); } -const testCases = [defaultSchemaTestCases]; +const testCases = [defaultSchemaTestCases, customStylesTestCases]; describe("Test BlockNote-Prosemirror conversion", () => { for (const testCase of testCases) { diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index e360d3a49b..19b9fc66f0 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -38,6 +38,9 @@ function styledTextToNodes( if (!config) { throw new Error(`style ${style} not found in styleSchema`); } + if (style === "fontSize") { + debugger; + } if (config.propSchema === "boolean") { marks.push(schema.mark(style)); } else if (config.propSchema === "string") { @@ -318,6 +321,9 @@ function contentNodeToInlineContent< if (mark.type.name === "link") { linkMark = mark; } else { + if (mark.type.name === "fontSize") { + debugger; + } const config = styleSchema[mark.type.name]; if (!config) { throw new Error(`style ${mark.type.name} not found in styleSchema`); diff --git a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html new file mode 100644 index 0000000000..4c7e8f174d --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html @@ -0,0 +1 @@ +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html new file mode 100644 index 0000000000..3e2beaedd6 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html @@ -0,0 +1 @@ +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html new file mode 100644 index 0000000000..4206d07a95 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html @@ -0,0 +1 @@ +

    This is a small text

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html new file mode 100644 index 0000000000..805c78112e --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html @@ -0,0 +1 @@ +

    This is a small text

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index 75682fc389..adc32c96f8 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -22,6 +22,7 @@ import { } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; import { EditorTestCases } from "../../testCases"; +import { customStylesTestCases } from "../../testCases/cases/customStyles"; import { defaultSchemaTestCases } from "../../testCases/cases/defaultSchema"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; import { createInternalHTMLSerializer } from "./internalHTMLSerializer"; @@ -329,7 +330,11 @@ function convertToHTMLAndCompareSnapshots< expect(externalHTML).toMatchFileSnapshot(externalHTMLSnapshotPath); } -const testCases = [defaultSchemaTestCases, editorTestCases]; +const testCases = [ + defaultSchemaTestCases, + editorTestCases, + customStylesTestCases, +]; describe("Test HTML conversion", () => { for (const testCase of testCases) { diff --git a/packages/core/src/api/testCases/cases/customStyles.ts b/packages/core/src/api/testCases/cases/customStyles.ts index 009a402f1b..e99c45d885 100644 --- a/packages/core/src/api/testCases/cases/customStyles.ts +++ b/packages/core/src/api/testCases/cases/customStyles.ts @@ -28,9 +28,27 @@ const small = createStyleSpec( } ); +const fontSize = createStyleSpec( + { + type: "fontSize", + propSchema: "string", + }, + { + render: (value) => { + const dom = document.createElement("span"); + dom.setAttribute("style", "font-size: " + value); + return { + dom, + contentDOM: dom, + }; + }, + } +); + const customStyles = { ...defaultStyleSpecs, small, + fontSize, } satisfies StyleSpecs; export const customStylesTestCases: EditorTestCases< @@ -40,8 +58,6 @@ export const customStylesTestCases: EditorTestCases< > = { name: "custom style schema", createEditor: () => { - // debugger; - return BlockNoteEditor.create({ uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, styleSpecs: customStyles, @@ -65,5 +81,22 @@ export const customStylesTestCases: EditorTestCases< }, ], }, + { + name: "fontSize/basic", + blocks: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "This is text with a custom fontSize", + styles: { + fontSize: "18px", + }, + }, + ], + }, + ], + }, ], }; diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts index 106b405004..3668abf895 100644 --- a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -1,37 +1,62 @@ import { Mark } from "@tiptap/core"; +import { UnreachableCaseError } from "../../../../shared/utils"; import { StyleConfig, StyleSpec } from "./types"; -export type CustomStyleImplementation = { - render: () => { - dom: HTMLElement; - contentDOM?: HTMLElement; - }; - // Exports block to external HTML. If not defined, the output will be the same - // as `render(...).dom`. Used to create clipboard data when pasting outside - // BlockNote. - // TODO: Maybe can return undefined to ignore when serializing? - // toExternalHTML?: ( - // block: BlockFromConfig, - // editor: BlockNoteEditor, I, S> - // ) => { - // dom: HTMLElement; - // contentDOM?: HTMLElement; - // }; +export type CustomStyleImplementation = { + render: T["propSchema"] extends "boolean" + ? () => { + dom: HTMLElement; + contentDOM?: HTMLElement; + } + : (value: string) => { + dom: HTMLElement; + contentDOM?: HTMLElement; + }; }; export function createStyleSpec( styleConfig: T, - styleImplementation: CustomStyleImplementation + styleImplementation: CustomStyleImplementation ): StyleSpec { const mark = Mark.create({ name: styleConfig.type, - renderHTML() { - const renderResult = styleImplementation.render(); + + addAttributes() { + if (styleConfig.propSchema === "boolean") { + return {}; + } return { - dom: renderResult.dom, - contentDOM: renderResult.contentDOM, + stringValue: { + default: undefined, + // TODO: parsing + + // parseHTML: (element) => + // element.getAttribute(`data-${styleConfig.type}`), + // renderHTML: (attributes) => ({ + // [`data-${styleConfig.type}`]: attributes.stringValue, + // }), + }, }; }, + + renderHTML({ mark }) { + let renderResult: { + dom: HTMLElement; + contentDOM?: HTMLElement; + }; + + if (styleConfig.propSchema === "boolean") { + // @ts-ignore not sure why this is complaining + renderResult = styleImplementation.render(); + } else if (styleConfig.propSchema === "string") { + renderResult = styleImplementation.render(mark.attrs.stringValue); + } else { + throw new UnreachableCaseError(styleConfig.propSchema); + } + + // const renderResult = styleImplementation.render(); + return renderResult; + }, }); return { From a4e707159f203f86c2cf61761fd26b9c75c2e294 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 20:20:33 +0100 Subject: [PATCH 11/31] inline content + tests --- packages/core/src/BlockNoteEditor.ts | 14 +- packages/core/src/BlockNoteExtensions.ts | 12 +- .../nodeConversions.test.ts.snap | 65 +++++++++ .../nodeConversions/nodeConversions.test.ts | 14 +- .../api/nodeConversions/nodeConversions.ts | 135 ++++++++++++++---- .../core/src/api/nodeConversions/testUtil.ts | 12 +- .../__snapshots__/mention/basic/external.html | 1 + .../__snapshots__/mention/basic/internal.html | 1 + .../__snapshots__/tag/basic/external.html | 1 + .../__snapshots__/tag/basic/internal.html | 1 + .../serialization/html/htmlConversion.test.ts | 2 + .../html/sharedHTMLConversion.ts | 1 + .../testCases/cases/customInlineContent.ts | 109 ++++++++++++++ .../Blocks/api/blocks/createSpec.ts | 2 +- .../extensions/Blocks/api/blocks/internal.ts | 5 +- .../src/extensions/Blocks/api/blocks/types.ts | 5 - .../Blocks/api/inlineContent/createSpec.ts | 80 +++++++++++ .../Blocks/api/inlineContent/internal.ts | 35 +++++ .../Blocks/api/inlineContent/types.ts | 21 +-- .../Blocks/api/styles/createSpec.ts | 12 +- 20 files changed, 470 insertions(+), 58 deletions(-) create mode 100644 packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html create mode 100644 packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html create mode 100644 packages/core/src/api/testCases/cases/customInlineContent.ts create mode 100644 packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts create mode 100644 packages/core/src/extensions/Blocks/api/inlineContent/internal.ts diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 3903e5aebe..198f741e71 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -299,6 +299,7 @@ export class BlockNoteEditor< blockSchema: this.blockSchema, blockSpecs: newOptions.blockSpecs, styleSpecs: newOptions.styleSpecs, + inlineContentSpecs: newOptions.inlineContentSpecs, collaboration: newOptions.collaboration, }); @@ -454,7 +455,13 @@ export class BlockNoteEditor< this._tiptapEditor.state.doc.firstChild!.descendants((node) => { blocks.push( - nodeToBlock(node, this.blockSchema, this.styleSchema, this.blockCache) + nodeToBlock( + node, + this.blockSchema, + this.inlineContentSchema, + this.styleSchema, + this.blockCache + ) ); return false; @@ -489,6 +496,7 @@ export class BlockNoteEditor< newBlock = nodeToBlock( node, this.blockSchema, + this.inlineContentSchema, this.styleSchema, this.blockCache ); @@ -592,6 +600,7 @@ export class BlockNoteEditor< block: nodeToBlock( node, this.blockSchema, + this.inlineContentSchema, this.styleSchema, this.blockCache ), @@ -601,6 +610,7 @@ export class BlockNoteEditor< : nodeToBlock( prevNode, this.blockSchema, + this.inlineContentSchema, this.styleSchema, this.blockCache ), @@ -610,6 +620,7 @@ export class BlockNoteEditor< : nodeToBlock( nextNode, this.blockSchema, + this.inlineContentSchema, this.styleSchema, this.blockCache ), @@ -697,6 +708,7 @@ export class BlockNoteEditor< nodeToBlock( this._tiptapEditor.state.doc.resolve(pos).node(), this.blockSchema, + this.inlineContentSchema, this.styleSchema, this.blockCache ) diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 1fce2b3dd7..7cdc37746e 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -19,7 +19,10 @@ import { BlockSchema, BlockSpecs, } from "./extensions/Blocks/api/blocks/types"; -import { InlineContentSchema } from "./extensions/Blocks/api/inlineContent/types"; +import { + InlineContentSchema, + InlineContentSpecs, +} from "./extensions/Blocks/api/inlineContent/types"; import { StyleSchema, StyleSpecs } from "./extensions/Blocks/api/styles/types"; import { TableExtension } from "./extensions/Blocks/nodes/BlockContent/TableBlockContent/TableExtension"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; @@ -40,6 +43,7 @@ export const getBlockNoteExtensions = < domAttributes: Partial; blockSchema: BSchema; blockSpecs: BlockSpecs; + inlineContentSpecs: InlineContentSpecs; styleSpecs: StyleSpecs; collaboration?: { fragment: Y.XmlFragment; @@ -96,6 +100,12 @@ export const getBlockNoteExtensions = < domAttributes: opts.domAttributes, }), TableExtension, + ...Object.values(opts.inlineContentSpecs).map((inlineContentSpec) => { + return inlineContentSpec.implementation.node.configure({ + editor: opts.editor as any, + }); + }), + ...Object.values(opts.blockSpecs).flatMap((blockSpec) => { return [ // dependent nodes (e.g.: tablecell / row) diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index a90ce25c7f..05bf59965a 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -33,6 +33,37 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con } `; +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert mention/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "I enjoy working with", + "type": "text", + }, + { + "attrs": { + "user": "Matthew", + }, + "type": "mention", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = ` { "attrs": { @@ -63,6 +94,40 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con } `; +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert tag/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "I love ", + "type": "text", + }, + { + "content": [ + { + "text": "BlockNote", + "type": "text", + }, + ], + "type": "tag", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert complex/misc to/from prosemirror 1`] = ` { "attrs": { diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index 9485a58e93..f77622ba6c 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor, PartialBlock } from "../.."; import UniqueID from "../../extensions/UniqueID/UniqueID"; +import { customInlineContentTestCases } from "../testCases/cases/customInlineContent"; import { customStylesTestCases } from "../testCases/cases/customStyles"; import { defaultSchemaTestCases } from "../testCases/cases/defaultSchema"; import { blockToNode, nodeToBlock } from "./nodeConversions"; @@ -28,7 +29,12 @@ function validateConversion( expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, editor.blockSchema, editor.styleSchema); + const outputBlock = nodeToBlock( + node, + editor.blockSchema, + editor.inlineContentSchema, + editor.styleSchema + ); const fullOriginalBlock = partialBlockToBlockForTesting( editor.blockSchema, @@ -38,7 +44,11 @@ function validateConversion( expect(outputBlock).toStrictEqual(fullOriginalBlock); } -const testCases = [defaultSchemaTestCases, customStylesTestCases]; +const testCases = [ + defaultSchemaTestCases, + customStylesTestCases, + customInlineContentTestCases, +]; describe("Test BlockNote-Prosemirror conversion", () => { for (const testCase of testCases) { diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 19b9fc66f0..4bb01a1ccf 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -9,8 +9,10 @@ import { } from "../../extensions/Blocks/api/blocks/types"; import { InlineContent, + InlineContentFromConfig, InlineContentSchema, PartialInlineContent, + PartialInlineContentFromConfig, PartialLink, StyledText, isLinkInlineContent, @@ -38,9 +40,7 @@ function styledTextToNodes( if (!config) { throw new Error(`style ${style} not found in styleSchema`); } - if (style === "fontSize") { - debugger; - } + if (config.propSchema === "boolean") { marks.push(schema.mark(style)); } else if (config.propSchema === "string") { @@ -144,9 +144,9 @@ export function inlineContentToNodes< } else if (isStyledTextInlineContent(content)) { nodes.push(...styledTextArrayToNodes([content], schema, styleSchema)); } else { - // TODO - // let s = content.type; - // throw new UnreachableCaseError(content); + nodes.push( + blockOrInlineContentToContentNode(content, schema, styleSchema) + ); } } return nodes; @@ -187,28 +187,22 @@ export function tableContentToNodes< return rowNodes; } -/** - * Converts a BlockNote block to a TipTap node. - */ -export function blockToNode( - block: PartialBlock, +function blockOrInlineContentToContentNode( + block: PartialBlock | PartialInlineContentFromConfig, schema: Schema, styleSchema: StyleSchema ) { - let id = block.id; - - if (id === undefined) { - id = UniqueID.options.generateID(); - } - + let contentNode: Node; let type = block.type; + // TODO: needed? came from previous code if (type === undefined) { type = "paragraph"; } - let contentNode: Node; - + if (type === "tag" || type === "mention") { + debugger; + } if (!block.content) { contentNode = schema.nodes[type].create(block.props); } else if (typeof block.content === "string") { @@ -225,6 +219,27 @@ export function blockToNode( } else { throw new UnreachableCaseError(block.content.type); } + return contentNode; +} +/** + * Converts a BlockNote block to a TipTap node. + */ +export function blockToNode( + block: PartialBlock, + schema: Schema, + styleSchema: StyleSchema +) { + let id = block.id; + + if (id === undefined) { + id = UniqueID.options.generateID(); + } + + const contentNode = blockOrInlineContentToContentNode( + block, + schema, + styleSchema + ); const children: Node[] = []; @@ -251,7 +266,7 @@ export function blockToNode( function contentNodeToTableContent< I extends InlineContentSchema, S extends StyleSchema ->(contentNode: Node, styleSchema: S) { +>(contentNode: Node, inlineContentSchema: I, styleSchema: S) { const ret: TableContent = { type: "tableContent", rows: [], @@ -264,7 +279,11 @@ function contentNodeToTableContent< rowNode.content.forEach((cellNode) => { row.cells.push( - contentNodeToInlineContent(cellNode.firstChild!, styleSchema) + contentNodeToInlineContent( + cellNode.firstChild!, + inlineContentSchema, + styleSchema + ) ); }); @@ -277,10 +296,10 @@ function contentNodeToTableContent< /** * Converts an internal (prosemirror) content node to a BlockNote InlineContent array. */ -function contentNodeToInlineContent< +export function contentNodeToInlineContent< I extends InlineContentSchema, S extends StyleSchema ->(contentNode: Node, styleSchema: S) { +>(contentNode: Node, inlineContentSchema: I, styleSchema: S) { const content: InlineContent[] = []; let currentContent: InlineContent | undefined = undefined; @@ -300,7 +319,7 @@ function contentNodeToInlineContent< currentContent.content[currentContent.content.length - 1].text += "\n"; } else { - // TODO + throw new Error("unexpected"); } } else { // Current content does not exist. @@ -314,6 +333,19 @@ function contentNodeToInlineContent< return; } + if (inlineContentSchema[node.type.name]) { + if (currentContent) { + content.push(currentContent); + currentContent = undefined; + } + + content.push( + nodeToCustomInlineContent(node, inlineContentSchema, styleSchema) + ); + + return; + } + const styles: Styles = {}; let linkMark: Mark | undefined; @@ -321,9 +353,6 @@ function contentNodeToInlineContent< if (mark.type.name === "link") { linkMark = mark; } else { - if (mark.type.name === "fontSize") { - debugger; - } const config = styleSchema[mark.type.name]; if (!config) { throw new Error(`style ${mark.type.name} not found in styleSchema`); @@ -458,6 +487,44 @@ function contentNodeToInlineContent< return content; } +export function nodeToCustomInlineContent< + I extends InlineContentSchema, + S extends StyleSchema +>(node: Node, inlineContentSchema: I, styleSchema: S): InlineContent { + const props: any = {}; + const icConfig = inlineContentSchema[node.type.name]; + for (const [attr, value] of Object.entries(node.attrs)) { + if (!icConfig) { + throw Error("ic node is of an unrecognized type: " + node.type.name); + } + + const propSchema = icConfig.propSchema; + + if (attr in propSchema) { + props[attr] = value; + } + } + + let content: InlineContentFromConfig["content"]; + + if (icConfig.content === "styled") { + content = contentNodeToInlineContent( + node, + inlineContentSchema, + styleSchema + ) as any; // TODO: is this safe? could we have Links here that are undesired? + } else { + content = undefined; + } + + const ic = { + type: node.type.name, + props, + content, + } as InlineContentFromConfig; + return ic; +} + /** * Convert a TipTap node to a BlockNote block. */ @@ -468,6 +535,7 @@ export function nodeToBlock< >( node: Node, blockSchema: BSchema, + inlineContentSchema: I, styleSchema: S, blockCache?: WeakMap> ): Block { @@ -522,6 +590,7 @@ export function nodeToBlock< nodeToBlock( node.lastChild!.child(i), blockSchema, + inlineContentSchema, styleSchema, blockCache ) @@ -531,9 +600,17 @@ export function nodeToBlock< let content: Block["content"]; if (blockConfig.content === "inline") { - content = contentNodeToInlineContent(blockInfo.contentNode, styleSchema); + content = contentNodeToInlineContent( + blockInfo.contentNode, + inlineContentSchema, + styleSchema + ); } else if (blockConfig.content === "table") { - content = contentNodeToTableContent(blockInfo.contentNode, styleSchema); + content = contentNodeToTableContent( + blockInfo.contentNode, + inlineContentSchema, + styleSchema + ); } else if (blockConfig.content === "none") { content = undefined; } else { diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts index e17cf1ba5b..e38638ea02 100644 --- a/packages/core/src/api/nodeConversions/testUtil.ts +++ b/packages/core/src/api/nodeConversions/testUtil.ts @@ -10,6 +10,7 @@ import { PartialInlineContent, StyledText, isPartialLinkInlineContent, + isStyledTextInlineContent, } from "../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; @@ -44,9 +45,16 @@ function partialContentToInlineContent( ...partialContent, content: textShorthandToStyledText(partialContent.content), }; - } else { - // TODO? + } else if (isStyledTextInlineContent(partialContent)) { return partialContent; + } else { + // custom inline content + + return { + props: {}, + ...partialContent, + content: partialContentToInlineContent(partialContent.content), + } as any; } }); } diff --git a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html new file mode 100644 index 0000000000..e1513fed2d --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html @@ -0,0 +1 @@ +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html new file mode 100644 index 0000000000..7af6dad9c7 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html @@ -0,0 +1 @@ +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html new file mode 100644 index 0000000000..4229ae0a83 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html @@ -0,0 +1 @@ +

    I love #BlockNote

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html new file mode 100644 index 0000000000..dac5db0ca8 --- /dev/null +++ b/packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html @@ -0,0 +1 @@ +

    I love #BlockNote

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index adc32c96f8..6c3d95acbd 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -22,6 +22,7 @@ import { } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; import { EditorTestCases } from "../../testCases"; +import { customInlineContentTestCases } from "../../testCases/cases/customInlineContent"; import { customStylesTestCases } from "../../testCases/cases/customStyles"; import { defaultSchemaTestCases } from "../../testCases/cases/defaultSchema"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; @@ -334,6 +335,7 @@ const testCases = [ defaultSchemaTestCases, editorTestCases, customStylesTestCases, + customInlineContentTestCases, ]; describe("Test HTML conversion", () => { diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index 5c5b7abfbc..064cb071fc 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -51,6 +51,7 @@ export const serializeNodeInner = < nodeToBlock( node, editor.blockSchema, + editor.inlineContentSchema, editor.styleSchema, editor.blockCache ), diff --git a/packages/core/src/api/testCases/cases/customInlineContent.ts b/packages/core/src/api/testCases/cases/customInlineContent.ts new file mode 100644 index 0000000000..08d9d47cf8 --- /dev/null +++ b/packages/core/src/api/testCases/cases/customInlineContent.ts @@ -0,0 +1,109 @@ +import { EditorTestCases } from ".."; +import { + DefaultBlockSchema, + DefaultStyleSchema, + InlineContentSchemaFromSpecs, + InlineContentSpecs, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "../../.."; +import { BlockNoteEditor } from "../../../BlockNoteEditor"; +import { createInlineContentSpec } from "../../../extensions/Blocks/api/inlineContent/createSpec"; + +const mention = createInlineContentSpec( + { + type: "mention", + propSchema: { + user: { + default: "", + }, + }, + content: "none", + }, + { + render: (ic) => { + const dom = document.createElement("span"); + dom.appendChild(document.createTextNode("@" + ic.props.user)); + + return { + dom, + }; + }, + } +); + +const tag = createInlineContentSpec( + { + type: "tag", + propSchema: {}, + content: "styled", + }, + { + render: () => { + const dom = document.createElement("span"); + dom.textContent = "#"; + + const contentDOM = document.createElement("span"); + dom.appendChild(contentDOM); + + return { + dom, + contentDOM, + }; + }, + } +); + +const customInlineContent = { + mention, + tag, +} satisfies InlineContentSpecs; + +export const customInlineContentTestCases: EditorTestCases< + DefaultBlockSchema, + InlineContentSchemaFromSpecs, + DefaultStyleSchema +> = { + name: "custom style schema", + createEditor: () => { + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + inlineContentSpecs: customInlineContent, + }); + }, + documents: [ + { + name: "mention/basic", + blocks: [ + { + type: "paragraph", + content: [ + "I enjoy working with", + { + type: "mention", + props: { + user: "Matthew", + }, + content: undefined, + } as any, + ], + }, + ], + }, + { + name: "tag/basic", + blocks: [ + { + type: "paragraph", + content: [ + "I love ", + { + type: "tag", + // props: {}, + content: "BlockNote", + } as any, + ], + }, + ], + }, + ], +}; diff --git a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts index 2bc9f958d9..fde009be15 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts @@ -68,7 +68,7 @@ export function createBlockSpec< selectable: true, addAttributes() { - return propsToAttributes(blockConfig); + return propsToAttributes(blockConfig.propSchema); }, parseHTML() { diff --git a/packages/core/src/extensions/Blocks/api/blocks/internal.ts b/packages/core/src/extensions/Blocks/api/blocks/internal.ts index 80b83a7a92..cee31137bb 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/internal.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/internal.ts @@ -22,10 +22,11 @@ export function camelToDataKebab(str: string): string { // Function that uses the 'propSchema' of a blockConfig to create a TipTap // node's `addAttributes` property. -export function propsToAttributes(blockConfig: BlockConfig): Attributes { +// TODO: extract function +export function propsToAttributes(propSchema: PropSchema): Attributes { const tiptapAttributes: Record = {}; - Object.entries(blockConfig.propSchema) + Object.entries(propSchema) .filter(([name, _spec]) => !inheritedProps.includes(name)) .forEach(([name, spec]) => { tiptapAttributes[name] = { diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index 4a87eaf299..bb18015c17 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -289,8 +289,3 @@ export type SpecificPartialBlock< }; export type BlockIdentifier = { id: string } | string; - -// export type Schema = { -// blocks: B; -// styles: S; -// }; diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts new file mode 100644 index 0000000000..67a9f6c4d8 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -0,0 +1,80 @@ +import { Node } from "@tiptap/core"; +import { nodeToCustomInlineContent } from "../../../../api/nodeConversions/nodeConversions"; +import { propsToAttributes } from "../blocks/internal"; +import { StyleSchema } from "../styles/types"; +import { createInlineContentSpecFromTipTapNode } from "./internal"; +import { + InlineContentConfig, + InlineContentFromConfig, + InlineContentSpec, +} from "./types"; + +// TODO: support serialization + +export type CustomInlineContentImplementation< + T extends InlineContentConfig, + // B extends BlockSchema, + // I extends InlineContentSchema, + S extends StyleSchema +> = { + render: ( + /** + * The custom inline content to render + */ + inlineContent: InlineContentFromConfig + /** + * The BlockNote editor instance + * This is typed generically. If you want an editor with your custom schema, you need to + * cast it manually, e.g.: `const e = editor as BlockNoteEditor;` + */ + // editor: BlockNoteEditor + // (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations + // or allow manually passing , but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics + ) => { + dom: HTMLElement; + contentDOM?: HTMLElement; + // destroy?: () => void; + }; +}; + +export function createInlineContentSpec< + T extends InlineContentConfig, + S extends StyleSchema +>( + inlineContentConfig: T, + inlineContentImplementation: CustomInlineContentImplementation +): InlineContentSpec { + const node = Node.create({ + name: inlineContentConfig.type, + inline: true, + content: + inlineContentConfig.content === "styled" + ? "inline*" + : ("inline" as T["content"] extends "styled" ? "inline*" : "inline"), + + addAttributes() { + return propsToAttributes(inlineContentConfig.propSchema); + }, + + renderHTML({ node }) { + const editor = this.options.editor; + if (node.type.name === "mention") { + debugger; + } + const output = inlineContentImplementation.render( + nodeToCustomInlineContent( + node, + editor.inlineContentSchema, + editor.styleSchema + ) as any as InlineContentFromConfig // TODO: fix cast + ); + + return output; + }, + }); + + return createInlineContentSpecFromTipTapNode( + node, + inlineContentConfig.propSchema + ) as InlineContentSpec; // TODO: fix cast +} diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts new file mode 100644 index 0000000000..a327adb349 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts @@ -0,0 +1,35 @@ +import { Node } from "@tiptap/core"; +import { PropSchema } from "../blocks/types"; +import { + InlineContentConfig, + InlineContentImplementation, + InlineContentSpec, +} from "./types"; + +// This helper function helps to instantiate a InlineContentSpec with a +// config and implementation that conform to the type of Config +export function createInternalInlineContentSpec( + config: T, + implementation: InlineContentImplementation +) { + return { + config, + implementation, + } satisfies InlineContentSpec; +} + +export function createInlineContentSpecFromTipTapNode< + T extends Node, + P extends PropSchema +>(node: T, propSchema: P) { + return createInternalInlineContentSpec( + { + type: node.name as T["name"], + propSchema, + content: node.config.content === "inline*" ? "styled" : "none", + }, + { + node, + } + ); +} diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts index 0b1118f3b8..dd0c65a2d4 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts @@ -1,15 +1,18 @@ +import { Node } from "@tiptap/core"; import { PropSchema, Props } from "../blocks/types"; import { StyleSchema, Styles } from "../styles/types"; export type InlineContentConfig = { type: string; - content: "styled" | "raw" | "none"; + content: "styled" | "none"; // | "plain" readonly propSchema: PropSchema; // content: "inline" | "none" | "table"; }; // @ts-ignore -export type InlineContentImplementation = any; +export type InlineContentImplementation = { + node: Node; +}; // Container for both the config and implementation of a block, // and the type of BlockImplementation is based on that of the config @@ -29,7 +32,7 @@ export type InlineContentSchemaFromSpecs = { [K in keyof T]: T[K]["config"]; }; -type InlineContentFromConfig< +export type InlineContentFromConfig< I extends InlineContentConfig, S extends StyleSchema > = { @@ -37,22 +40,22 @@ type InlineContentFromConfig< props: Props; content: I["content"] extends "styled" ? StyledText[] - : I["content"] extends "raw" + : I["content"] extends "plain" ? string : I["content"] extends "none" ? undefined : never; }; -type PartialInlineContentFromConfig< +export type PartialInlineContentFromConfig< I extends InlineContentConfig, S extends StyleSchema > = { type: I["type"]; - props: Props; + props?: Props; content: I["content"] extends "styled" ? StyledText[] | string - : I["content"] extends "raw" + : I["content"] extends "plain" ? string : I["content"] extends "none" ? undefined @@ -107,7 +110,7 @@ export function isPartialLinkInlineContent( } export function isStyledTextInlineContent( - content: InlineContent + content: PartialInlineContentElement ): content is StyledText { - return content.type === "text"; + return typeof content !== "string" && content.type === "text"; } diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts index 3668abf895..9f0d742f75 100644 --- a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -1,5 +1,6 @@ import { Mark } from "@tiptap/core"; import { UnreachableCaseError } from "../../../../shared/utils"; +import { createInternalStyleSpec } from "./internal"; import { StyleConfig, StyleSpec } from "./types"; export type CustomStyleImplementation = { @@ -14,6 +15,8 @@ export type CustomStyleImplementation = { }; }; +// TODO: support serialization + export function createStyleSpec( styleConfig: T, styleImplementation: CustomStyleImplementation @@ -59,10 +62,7 @@ export function createStyleSpec( }, }); - return { - config: styleConfig, - implementation: { - mark, - }, - }; + return createInternalStyleSpec(styleConfig, { + mark, + }); } From de88cb728a6afc6bc23cc5abd12b97a82c890539 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 21 Nov 2023 20:27:40 +0100 Subject: [PATCH 12/31] misc --- packages/core/src/api/nodeConversions/nodeConversions.ts | 3 --- packages/core/src/api/testCases/cases/defaultSchema.ts | 2 -- .../src/extensions/Blocks/api/inlineContent/createSpec.ts | 4 +--- packages/core/src/index.ts | 5 +++++ packages/react/src/ReactBlockSpec.tsx | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 4bb01a1ccf..402c517faa 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -200,9 +200,6 @@ function blockOrInlineContentToContentNode( type = "paragraph"; } - if (type === "tag" || type === "mention") { - debugger; - } if (!block.content) { contentNode = schema.nodes[type].create(block.props); } else if (typeof block.content === "string") { diff --git a/packages/core/src/api/testCases/cases/defaultSchema.ts b/packages/core/src/api/testCases/cases/defaultSchema.ts index 38817dcb9d..35064c7ef5 100644 --- a/packages/core/src/api/testCases/cases/defaultSchema.ts +++ b/packages/core/src/api/testCases/cases/defaultSchema.ts @@ -14,8 +14,6 @@ export const defaultSchemaTestCases: EditorTestCases< > = { name: "default schema", createEditor: () => { - // debugger; - return BlockNoteEditor.create({ uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, }); diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 67a9f6c4d8..757002edbc 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -58,9 +58,7 @@ export function createInlineContentSpec< renderHTML({ node }) { const editor = this.options.editor; - if (node.type.name === "mention") { - debugger; - } + const output = inlineContentImplementation.render( nodeToCustomInlineContent( node, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6d1441daba..f613eee979 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,8 +7,13 @@ export * from "./extensions/Blocks/api/blocks/internal"; export * from "./extensions/Blocks/api/blocks/types"; export * from "./extensions/Blocks/api/defaultBlocks"; export * from "./extensions/Blocks/api/defaultProps"; +export * from "./extensions/Blocks/api/inlineContent/createSpec"; +export * from "./extensions/Blocks/api/inlineContent/internal"; export * from "./extensions/Blocks/api/inlineContent/types"; export * from "./extensions/Blocks/api/selectionTypes"; +export * from "./extensions/Blocks/api/styles/createSpec"; +export * from "./extensions/Blocks/api/styles/internal"; +export * from "./extensions/Blocks/api/styles/types"; export * as blockStyles from "./extensions/Blocks/nodes/Block.css"; export * from "./extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index 2bc548b01e..dad2adf029 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -15,8 +15,8 @@ import { Props, PropSchema, propsToAttributes, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { NodeViewContent, NodeViewProps, From 17651ce51a5d15eb5ef6c49c5db1e574710e31a8 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 00:36:33 +0100 Subject: [PATCH 13/31] clean imports --- .../blockManipulation.test.ts | 7 +- .../blockManipulation/blockManipulation.ts | 4 +- .../nodeConversions/nodeConversions.test.ts | 4 +- .../clipboardHandlerExtension.ts | 3 +- .../html/externalHTMLExporter.ts | 3 +- .../html/internalHTMLSerializer.ts | 2 +- .../html/sharedHTMLConversion.ts | 3 +- .../testCases/cases/customInlineContent.ts | 11 +- .../src/api/testCases/cases/customStyles.ts | 7 +- .../src/api/testCases/cases/defaultSchema.ts | 7 +- packages/core/src/api/testCases/index.ts | 3 +- .../Blocks/api/blocks/createSpec.ts | 2 +- .../src/extensions/Blocks/api/blocks/types.ts | 4 +- .../ListItemKeyboardShortcuts.ts | 1 + .../nodes/BlockContent/defaultBlockHelpers.ts | 3 +- .../FormattingToolbarPlugin.ts | 9 +- .../HyperlinkToolbarPlugin.ts | 2 +- .../ImageToolbar/ImageToolbarPlugin.ts | 10 +- .../TableHandles/TableHandlesPlugin.ts | 15 +- packages/core/src/index.ts | 1 + packages/react/src/BlockNoteView.tsx | 2 +- .../DefaultButtons/ToggledStyleButton.tsx | 2 +- .../components/DefaultHyperlinkToolbar.tsx | 2 +- .../components/HyperlinkToolbarPositioner.tsx | 2 +- packages/react/src/ReactBlockSpec.tsx | 2 +- packages/react/src/ReactInlineContentSpec.tsx | 84 +++++ packages/react/src/ReactStyleSpec.tsx | 105 +++++++ .../SideMenu/components/DefaultSideMenu.tsx | 2 +- .../DefaultButtons/BlockColorsButton.tsx | 2 +- .../DragHandleMenu/DragHandleMenu.tsx | 2 +- .../components/SideMenuPositioner.tsx | 2 +- .../react/src/SlashMenu/ReactSlashMenuItem.ts | 2 +- .../SlashMenu/defaultReactSlashMenuItems.tsx | 2 +- .../components/DefaultTableHandle.tsx | 2 +- .../components/TableHandlePositioner.tsx | 2 +- packages/react/src/hooks/useBlockNote.ts | 6 +- .../react/src/hooks/useEditorContentChange.ts | 2 +- .../src/hooks/useEditorSelectionChange.ts | 2 +- packages/react/src/hooks/useSelectedBlocks.ts | 2 +- packages/react/src/htmlConversion.test.tsx | 293 ++++-------------- .../react/src/testCases/customReactBlocks.tsx | 202 ++++++++++++ packages/react/vite.config.ts | 13 +- 42 files changed, 534 insertions(+), 302 deletions(-) create mode 100644 packages/react/src/ReactInlineContentSpec.tsx create mode 100644 packages/react/src/ReactStyleSpec.tsx create mode 100644 packages/react/src/testCases/customReactBlocks.tsx diff --git a/packages/core/src/api/blockManipulation/blockManipulation.test.ts b/packages/core/src/api/blockManipulation/blockManipulation.test.ts index 7f075d4e47..3f8acaa97b 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.test.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.test.ts @@ -1,12 +1,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { Block, PartialBlock } from "../../extensions/Blocks/api/blocks/types"; import { - Block, - BlockNoteEditor, DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, - PartialBlock, -} from "../.."; +} from "../../extensions/Blocks/api/defaultBlocks"; let editor: BlockNoteEditor; diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index 835c6fee20..1054e0e35b 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -1,11 +1,13 @@ import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; -import { BlockNoteEditor, InlineContentSchema } from "../.."; + +import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BlockIdentifier, BlockSchema, PartialBlock, } from "../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; import { blockToNode } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../util/nodeUtil"; diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index f77622ba6c..c645c3a2c0 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -1,5 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { BlockNoteEditor, PartialBlock } from "../.."; + +import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { PartialBlock } from "../../extensions/Blocks/api/blocks/types"; import UniqueID from "../../extensions/UniqueID/UniqueID"; import { customInlineContentTestCases } from "../testCases/cases/customInlineContent"; import { customStylesTestCases } from "../testCases/cases/customStyles"; diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/serialization/clipboardHandlerExtension.ts index 96fc1fb691..5ca2c22f1d 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/serialization/clipboardHandlerExtension.ts @@ -1,8 +1,9 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; -import { InlineContentSchema } from "../.."; + import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BlockSchema } from "../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; import { markdown } from "../formatConversions/formatConversions"; import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; diff --git a/packages/core/src/api/serialization/html/externalHTMLExporter.ts b/packages/core/src/api/serialization/html/externalHTMLExporter.ts index 85aa8d9563..76021c1333 100644 --- a/packages/core/src/api/serialization/html/externalHTMLExporter.ts +++ b/packages/core/src/api/serialization/html/externalHTMLExporter.ts @@ -2,12 +2,13 @@ import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; import { unified } from "unified"; -import { InlineContentSchema } from "../../.."; + import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, PartialBlock, } from "../../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { simplifyBlocks } from "../../formatConversions/simplifyBlocksRehypePlugin"; import { blockToNode } from "../../nodeConversions/nodeConversions"; diff --git a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts index 85eb30ae91..bd32aa8950 100644 --- a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/serialization/html/internalHTMLSerializer.ts @@ -1,10 +1,10 @@ import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model"; -import { InlineContentSchema } from "../../.."; import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema, PartialBlock, } from "../../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { blockToNode } from "../../nodeConversions/nodeConversions"; import { diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index 064cb071fc..8d8d23a778 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -1,7 +1,8 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; -import { InlineContentSchema } from "../../.."; + import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { BlockSchema } from "../../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; import { nodeToBlock } from "../../nodeConversions/nodeConversions"; diff --git a/packages/core/src/api/testCases/cases/customInlineContent.ts b/packages/core/src/api/testCases/cases/customInlineContent.ts index 08d9d47cf8..c82c3e2ef3 100644 --- a/packages/core/src/api/testCases/cases/customInlineContent.ts +++ b/packages/core/src/api/testCases/cases/customInlineContent.ts @@ -1,13 +1,16 @@ import { EditorTestCases } from ".."; + +import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { DefaultBlockSchema, DefaultStyleSchema, +} from "../../../extensions/Blocks/api/defaultBlocks"; +import { createInlineContentSpec } from "../../../extensions/Blocks/api/inlineContent/createSpec"; +import { InlineContentSchemaFromSpecs, InlineContentSpecs, - uploadToTmpFilesDotOrg_DEV_ONLY, -} from "../../.."; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; -import { createInlineContentSpec } from "../../../extensions/Blocks/api/inlineContent/createSpec"; +} from "../../../extensions/Blocks/api/inlineContent/types"; +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; const mention = createInlineContentSpec( { diff --git a/packages/core/src/api/testCases/cases/customStyles.ts b/packages/core/src/api/testCases/cases/customStyles.ts index e99c45d885..e7a4390e63 100644 --- a/packages/core/src/api/testCases/cases/customStyles.ts +++ b/packages/core/src/api/testCases/cases/customStyles.ts @@ -1,16 +1,17 @@ import { EditorTestCases } from ".."; + +import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { DefaultBlockSchema, DefaultInlineContentSchema, defaultStyleSpecs, - uploadToTmpFilesDotOrg_DEV_ONLY, -} from "../../.."; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; +} from "../../../extensions/Blocks/api/defaultBlocks"; import { createStyleSpec } from "../../../extensions/Blocks/api/styles/createSpec"; import { StyleSchemaFromSpecs, StyleSpecs, } from "../../../extensions/Blocks/api/styles/types"; +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; const small = createStyleSpec( { diff --git a/packages/core/src/api/testCases/cases/defaultSchema.ts b/packages/core/src/api/testCases/cases/defaultSchema.ts index 35064c7ef5..bb7ccf4526 100644 --- a/packages/core/src/api/testCases/cases/defaultSchema.ts +++ b/packages/core/src/api/testCases/cases/defaultSchema.ts @@ -1,11 +1,12 @@ import { EditorTestCases } from ".."; + +import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, - uploadToTmpFilesDotOrg_DEV_ONLY, -} from "../../.."; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; +} from "../../../extensions/Blocks/api/defaultBlocks"; +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY"; export const defaultSchemaTestCases: EditorTestCases< DefaultBlockSchema, diff --git a/packages/core/src/api/testCases/index.ts b/packages/core/src/api/testCases/index.ts index d0a9f27173..90e1f06005 100644 --- a/packages/core/src/api/testCases/index.ts +++ b/packages/core/src/api/testCases/index.ts @@ -1,8 +1,9 @@ -import { BlockNoteEditor, InlineContentSchema } from "../.."; +import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BlockSchema, PartialBlock, } from "../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; export type EditorTestCases< diff --git a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts index fde009be15..81e6d370c9 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts @@ -1,5 +1,5 @@ -import { InlineContentSchema } from "../../../.."; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; +import { InlineContentSchema } from "../inlineContent/types"; import { StyleSchema } from "../styles/types"; import { createInternalBlockSpec, diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index bb18015c17..86e9959806 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -1,6 +1,8 @@ /** Define the main block types **/ import { Node } from "@tiptap/core"; -import { BlockNoteEditor, DefaultStyleSchema } from "../../../.."; + +import { BlockNoteEditor } from "../../../../BlockNoteEditor"; +import { DefaultStyleSchema } from "../defaultBlocks"; import { InlineContent, InlineContentSchema, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 01f7474eab..4ca84b7682 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -10,6 +10,7 @@ export const handleEnter = (editor: Editor) => { const selectionEmpty = editor.state.selection.anchor === editor.state.selection.head; + debugger; if (!contentType.name.endsWith("ListItem") || !selectionEmpty) { return false; } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts index 8bd002dfbb..b5ade2c570 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/defaultBlockHelpers.ts @@ -1,7 +1,8 @@ -import { Block, BlockSchema, InlineContentSchema } from "../../../.."; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { blockToNode } from "../../../../api/nodeConversions/nodeConversions"; import { mergeCSSClasses } from "../../../../shared/utils"; +import { Block, BlockSchema } from "../../api/blocks/types"; +import { InlineContentSchema } from "../../api/inlineContent/types"; import { StyleSchema } from "../../api/styles/types"; // Function that creates a ProseMirror `DOMOutputSpec` for a default block. diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index f394c1deea..94fb437e54 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -1,14 +1,15 @@ import { isNodeSelection, isTextSelection, posToDOMRect } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; + +import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BaseUiElementCallbacks, BaseUiElementState, - BlockNoteEditor, - BlockSchema, - InlineContentSchema, -} from "../.."; +} from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; +import { BlockSchema } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; import { StyleSchema } from "../Blocks/api/styles/types"; export type FormattingToolbarCallbacks = BaseUiElementCallbacks; diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index ea1b726ac8..abdd1f4cab 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -20,7 +20,7 @@ class HyperlinkToolbarView { private hyperlinkToolbarState?: HyperlinkToolbarState; public updateHyperlinkToolbar: () => void; - menuUpdateTimer: NodeJS.Timeout | undefined; + menuUpdateTimer: ReturnType | undefined; startMenuUpdateTimer: () => void; stopMenuUpdateTimer: () => void; diff --git a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts index 90392ca906..9ecb162d3a 100644 --- a/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +++ b/packages/core/src/extensions/ImageToolbar/ImageToolbarPlugin.ts @@ -1,14 +1,14 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; + +import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BaseUiElementCallbacks, BaseUiElementState, - BlockNoteEditor, - BlockSchema, - InlineContentSchema, - SpecificBlock, -} from "../.."; +} from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; +import { BlockSchema, SpecificBlock } from "../Blocks/api/blocks/types"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; import { StyleSchema } from "../Blocks/api/styles/types"; export type ImageToolbarCallbacks = BaseUiElementCallbacks; diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index f2963fbbcf..8089902948 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -1,16 +1,17 @@ import { Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; + +import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { BaseUiElementCallbacks } from "../../shared/BaseUiElementTypes"; +import { EventEmitter } from "../../shared/EventEmitter"; import { - BaseUiElementCallbacks, - BlockNoteEditor, BlockSchemaWithBlock, - DefaultBlockSchema, - InlineContentSchema, SpecificBlock, - getDraggableBlockFromCoords, -} from "../.."; -import { EventEmitter } from "../../shared/EventEmitter"; +} from "../Blocks/api/blocks/types"; +import { DefaultBlockSchema } from "../Blocks/api/defaultBlocks"; +import { InlineContentSchema } from "../Blocks/api/inlineContent/types"; import { StyleSchema } from "../Blocks/api/styles/types"; +import { getDraggableBlockFromCoords } from "../SideMenu/SideMenuPlugin"; export type TableHandlesCallbacks = BaseUiElementCallbacks; export type TableHandlesState< diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f613eee979..25cb3ce75c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./api/serialization/html/externalHTMLExporter"; export * from "./api/serialization/html/internalHTMLSerializer"; +export * from "./api/testCases/index"; export * from "./extensions/Blocks/api/blocks/createSpec"; export * from "./extensions/Blocks/api/blocks/internal"; export * from "./extensions/Blocks/api/blocks/types"; diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx index 9315738b88..7a126b8f48 100644 --- a/packages/react/src/BlockNoteView.tsx +++ b/packages/react/src/BlockNoteView.tsx @@ -2,9 +2,9 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + StyleSchema, mergeCSSClasses, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { MantineProvider, createStyles } from "@mantine/core"; import { EditorContent } from "@tiptap/react"; import { HTMLAttributes, ReactNode, useMemo } from "react"; diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx index 652a2fea48..95895d32db 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ToggledStyleButton.tsx @@ -12,7 +12,7 @@ import { RiUnderline, } from "react-icons/ri"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; import { useEditorChange } from "../../../hooks/useEditorChange"; import { useSelectedBlocks } from "../../../hooks/useSelectedBlocks"; diff --git a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx index 4aebff08cb..269de59419 100644 --- a/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx +++ b/packages/react/src/HyperlinkToolbar/components/DefaultHyperlinkToolbar.tsx @@ -2,7 +2,7 @@ import { BlockSchema, InlineContentSchema } from "@blocknote/core"; import { useRef, useState } from "react"; import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton"; import { EditHyperlinkMenu } from "./EditHyperlinkMenu/components/EditHyperlinkMenu"; diff --git a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx index 5df7e620d2..6890e5df61 100644 --- a/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx +++ b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbarPositioner.tsx @@ -12,7 +12,7 @@ import { import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { DefaultHyperlinkToolbar } from "./DefaultHyperlinkToolbar"; export type HyperlinkToolbarProps< diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index dad2adf029..98ff0ff839 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -137,7 +137,7 @@ export function createReactBlockSpec< selectable: true, addAttributes() { - return propsToAttributes(blockConfig); + return propsToAttributes(blockConfig as any); // TODO: cast }, parseHTML() { diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx new file mode 100644 index 0000000000..cbbcbd2701 --- /dev/null +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -0,0 +1,84 @@ +import { + createInternalInlineContentSpec, + createStronglyTypedTiptapNode, + InlineContentConfig, + InlineContentFromConfig, + StyleSchema, +} from "@blocknote/core"; +import { nodeToCustomInlineContent } from "@blocknote/core/src/api/nodeConversions/nodeConversions"; +import { NodeViewProps, ReactNodeViewRenderer } from "@tiptap/react"; +import { FC } from "react"; + +// this file is mostly analogoues to `customBlocks.ts`, but for React blocks + +// extend BlockConfig but use a React render function +export type ReactInlineContentImplementation< + T extends InlineContentConfig, + // I extends InlineContentSchema, + S extends StyleSchema +> = { + render: FC<{ + inlineContent: InlineContentFromConfig; + }>; + // TODO? + // toExternalHTML?: FC<{ + // block: BlockFromConfig; + // editor: BlockNoteEditor, I, S>; + // }>; +}; + +// A function to create custom block for API consumers +// we want to hide the tiptap node from API consumers and provide a simpler API surface instead +export function createReactInlineContentSpec< + T extends InlineContentConfig, + // I extends InlineContentSchema, + S extends StyleSchema +>( + inlineContentConfig: T, + inlineContentImplementation: ReactInlineContentImplementation +) { + const node = createStronglyTypedTiptapNode({ + name: inlineContentConfig.type as T["type"], + content: (inlineContentConfig.content === "styled" + ? "inline*" + : "") as T["content"] extends "styled" ? "inline*" : "", + + // addAttributes() { + // return propsToAttributes(blockConfig); + // }, + + // parseHTML() { + // return parse(blockConfig); + // }, + + // TODO: needed? + addNodeView() { + const editor = this.options.editor; + + return (props) => + ReactNodeViewRenderer( + (props: NodeViewProps) => { + const Content = inlineContentImplementation.render; + return ( + // TODO: fix cast + } + /> + ); + }, + { + className: "bn-react-node-view-renderer", + } + )(props); + }, + }); + + return createInternalInlineContentSpec(inlineContentConfig, { + node: node, + }); +} diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx new file mode 100644 index 0000000000..82aba6ce96 --- /dev/null +++ b/packages/react/src/ReactStyleSpec.tsx @@ -0,0 +1,105 @@ +import { + BlockNoteDOMAttributes, + createInternalStyleSpec, + mergeCSSClasses, + StyleConfig, + UnreachableCaseError, +} from "@blocknote/core"; +import { Mark, NodeViewContent } from "@tiptap/react"; +import { createContext, ElementType, FC, HTMLProps, useContext } from "react"; +import { renderToString } from "react-dom/server"; + +// this file is mostly analogoues to `customBlocks.ts`, but for React blocks + +// extend BlockConfig but use a React render function +export type ReactCustomStyleImplementation = { + render: T["propSchema"] extends "boolean" ? FC : FC<{ value: string }>; +}; + +const BlockNoteDOMAttributesContext = createContext({}); + +export const InlineContent = ( + props: { as?: Tag } & HTMLProps +) => { + const inlineContentDOMAttributes = + useContext(BlockNoteDOMAttributesContext).inlineContent || {}; + + const classNames = mergeCSSClasses( + props.className || "", + "bn-inline-content", + inlineContentDOMAttributes.class + ); + + return ( + key !== "class" + ) + )} + {...props} + className={classNames} + /> + ); +}; + +// A function to create custom block for API consumers +// we want to hide the tiptap node from API consumers and provide a simpler API surface instead +export function createReactStyleSpec( + styleConfig: T, + styleImplementation: ReactCustomStyleImplementation +) { + const mark = Mark.create({ + name: styleConfig.type, + + addAttributes() { + if (styleConfig.propSchema === "boolean") { + return {}; + } + return { + stringValue: { + default: undefined, + // TODO: parsing + + // parseHTML: (element) => + // element.getAttribute(`data-${styleConfig.type}`), + // renderHTML: (attributes) => ({ + // [`data-${styleConfig.type}`]: attributes.stringValue, + // }), + }, + }; + }, + + renderHTML({ mark }) { + if (styleConfig.propSchema === "boolean") { + const Content = styleImplementation.render as FC; + const parent = document.createElement("div"); + parent.innerHTML = renderToString(); + + return { + dom: parent.firstElementChild! as HTMLElement, + contentDOM: (parent.querySelector(".bn-inline-content") || + undefined) as HTMLElement | undefined, + }; + } else if (styleConfig.propSchema === "string") { + const Content = styleImplementation.render as FC<{ value: string }>; + const parent = document.createElement("div"); + parent.innerHTML = renderToString( + + ); + + return { + dom: parent.firstElementChild! as HTMLElement, + contentDOM: (parent.querySelector(".bn-inline-content") || + undefined) as HTMLElement | undefined, + }; + } else { + throw new UnreachableCaseError(styleConfig.propSchema); + } + }, + }); + + return createInternalStyleSpec(styleConfig, { + mark, + }); +} diff --git a/packages/react/src/SideMenu/components/DefaultSideMenu.tsx b/packages/react/src/SideMenu/components/DefaultSideMenu.tsx index fbd686065a..1ae3fcb99e 100644 --- a/packages/react/src/SideMenu/components/DefaultSideMenu.tsx +++ b/packages/react/src/SideMenu/components/DefaultSideMenu.tsx @@ -1,6 +1,6 @@ import { BlockSchema, InlineContentSchema } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { AddBlockButton } from "./DefaultButtons/AddBlockButton"; import { DragHandle } from "./DefaultButtons/DragHandle"; import { SideMenu } from "./SideMenu"; diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx index bb7dbcfb48..e198ebf46e 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx @@ -15,7 +15,7 @@ export const BlockColorsButton = ( const { ref, updateMaxHeight } = usePreventMenuOverflow(); - const menuCloseTimer = useRef(); + const menuCloseTimer = useRef | undefined>(); const startMenuCloseTimer = useCallback(() => { if (menuCloseTimer.current) { diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx index 4c59a5dd91..be806fd5a1 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx @@ -3,8 +3,8 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { Menu, createStyles } from "@mantine/core"; import { ReactNode } from "react"; diff --git a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx index 1926d0a7e0..b0c94d7b71 100644 --- a/packages/react/src/SideMenu/components/SideMenuPositioner.tsx +++ b/packages/react/src/SideMenu/components/SideMenuPositioner.tsx @@ -11,7 +11,7 @@ import { import Tippy from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { DefaultSideMenu } from "./DefaultSideMenu"; import { DragHandleMenuProps } from "./DragHandleMenu/DragHandleMenu"; diff --git a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts index f12fce328b..65ceb044f7 100644 --- a/packages/react/src/SlashMenu/ReactSlashMenuItem.ts +++ b/packages/react/src/SlashMenu/ReactSlashMenuItem.ts @@ -5,8 +5,8 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; export type ReactSlashMenuItem< BSchema extends BlockSchema = DefaultBlockSchema, diff --git a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx index f6c02498db..66411dda62 100644 --- a/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx +++ b/packages/react/src/SlashMenu/defaultReactSlashMenuItems.tsx @@ -5,8 +5,8 @@ import { DefaultBlockSchema, getDefaultSlashMenuItems, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { RiH1, RiH2, diff --git a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx index d5475823ce..9762bd30d7 100644 --- a/packages/react/src/TableHandles/components/DefaultTableHandle.tsx +++ b/packages/react/src/TableHandles/components/DefaultTableHandle.tsx @@ -5,7 +5,7 @@ import { TableContent, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; +import { StyleSchema } from "@blocknote/core"; import { Menu } from "@mantine/core"; import { MdDragIndicator } from "react-icons/md"; import { SideMenuButton } from "../../SideMenu/components/SideMenuButton"; diff --git a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx index 7c2b9ba1c2..45b85b5236 100644 --- a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx +++ b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx @@ -4,9 +4,9 @@ import { DefaultBlockSchema, InlineContentSchema, SpecificBlock, + StyleSchema, TableHandlesState, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import Tippy, { tippy } from "@tippyjs/react"; import { FC, useEffect, useMemo, useRef, useState } from "react"; import { DefaultTableHandle } from "./DefaultTableHandle"; diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index d02d5b9bfe..9a1ef8f8b5 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -5,13 +5,11 @@ import { BlockSpecs, InlineContentSchema, InlineContentSpecs, + StyleSchema, + StyleSpecs, defaultBlockSpecs, getBlockSchemaFromSpecs, } from "@blocknote/core"; -import { - StyleSchema, - StyleSpecs, -} from "@blocknote/core/src/extensions/Blocks/api/styles"; import { DependencyList, useMemo, useRef } from "react"; import { getDefaultReactSlashMenuItems } from "../SlashMenu/defaultReactSlashMenuItems"; diff --git a/packages/react/src/hooks/useEditorContentChange.ts b/packages/react/src/hooks/useEditorContentChange.ts index 262012b681..7cb067c6ca 100644 --- a/packages/react/src/hooks/useEditorContentChange.ts +++ b/packages/react/src/hooks/useEditorContentChange.ts @@ -2,8 +2,8 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; export function useEditorContentChange( diff --git a/packages/react/src/hooks/useEditorSelectionChange.ts b/packages/react/src/hooks/useEditorSelectionChange.ts index f32aa27181..3051970284 100644 --- a/packages/react/src/hooks/useEditorSelectionChange.ts +++ b/packages/react/src/hooks/useEditorSelectionChange.ts @@ -2,8 +2,8 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useEffect } from "react"; export function useEditorSelectionChange( diff --git a/packages/react/src/hooks/useSelectedBlocks.ts b/packages/react/src/hooks/useSelectedBlocks.ts index 29fc41ef74..63ab136ed7 100644 --- a/packages/react/src/hooks/useSelectedBlocks.ts +++ b/packages/react/src/hooks/useSelectedBlocks.ts @@ -3,8 +3,8 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, + StyleSchema, } from "@blocknote/core"; -import { StyleSchema } from "@blocknote/core/src/extensions/Blocks/api/styles"; import { useState } from "react"; import { useEditorChange } from "./useEditorChange"; diff --git a/packages/react/src/htmlConversion.test.tsx b/packages/react/src/htmlConversion.test.tsx index e5209e7573..83aed33830 100644 --- a/packages/react/src/htmlConversion.test.tsx +++ b/packages/react/src/htmlConversion.test.tsx @@ -1,85 +1,31 @@ +// @vitest-environment jsdom + import { BlockNoteEditor, - BlockSchemaFromSpecs, - BlockSpecs, + BlockSchema, + InlineContentSchema, PartialBlock, + StyleSchema, createExternalHTMLExporter, createInternalHTMLSerializer, - defaultBlockSpecs, - defaultProps, - uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; -import { Editor } from "@tiptap/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { InlineContent, createReactBlockSpec } from "./ReactBlockSpec"; - -const ReactCustomParagraph = createReactBlockSpec( - { - type: "reactCustomParagraph" as const, - propSchema: defaultProps, - content: "inline", - }, - { - render: () => ( - - ), - toExternalHTML: () => ( -

    Hello World

    - ), - } -); - -const SimpleReactCustomParagraph = createReactBlockSpec( - { - type: "simpleReactCustomParagraph" as const, - propSchema: defaultProps, - content: "inline", - }, - { - render: () => ( - - ), - } -); - -const customSpecs = { - ...defaultBlockSpecs, - reactCustomParagraph: ReactCustomParagraph, - simpleReactCustomParagraph: SimpleReactCustomParagraph, -} satisfies BlockSpecs; - -let editor: BlockNoteEditor>; - -type CustomPartialBlock = PartialBlock< - (typeof editor)["blockSchema"], - (typeof editor)["inlineContentSchema"], - (typeof editor)["styleSchema"] ->; - -let tt: Editor; - -beforeEach(() => { - editor = BlockNoteEditor.create({ - blockSpecs: customSpecs, - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, - }); - tt = editor._tiptapEditor; -}); - -afterEach(() => { - tt.destroy(); - editor = undefined as any; - tt = undefined as any; - - delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; -}); - -function convertToHTMLAndCompareSnapshots( - blocks: CustomPartialBlock[], +import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks.jsx"; + +function convertToHTMLAndCompareSnapshots< + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], snapshotDirectory: string, snapshotName: string ) { - const serializer = createInternalHTMLSerializer(tt.schema, editor); + const serializer = createInternalHTMLSerializer( + editor._tiptapEditor.schema, + editor + ); const internalHTML = serializer.serializeBlocks(blocks); const internalHTMLSnapshotPath = "./__snapshots__/" + @@ -89,7 +35,10 @@ function convertToHTMLAndCompareSnapshots( "/internal.html"; expect(internalHTML).toMatchFileSnapshot(internalHTMLSnapshotPath); - const exporter = createExternalHTMLExporter(tt.schema, editor); + const exporter = createExternalHTMLExporter( + editor._tiptapEditor.schema, + editor + ); const externalHTML = exporter.exportBlocks(blocks); const externalHTMLSnapshotPath = "./__snapshots__/" + @@ -100,170 +49,36 @@ function convertToHTMLAndCompareSnapshots( expect(externalHTML).toMatchFileSnapshot(externalHTMLSnapshotPath); } -describe("Convert custom blocks with inline content to HTML", () => { - it("Convert custom block with inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "reactCustomParagraph", - content: "React Custom Paragraph", - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "reactCustomParagraph", "basic"); - }); - - it("Convert styled custom block with inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "reactCustomParagraph", - props: { - textAlignment: "center", - textColor: "orange", - backgroundColor: "pink", - }, - content: [ - { - type: "text", - styles: {}, - text: "Plain ", - }, - { - type: "text", - styles: { - textColor: "red", - }, - text: "Red Text ", - }, - { - type: "text", - styles: { - backgroundColor: "blue", - }, - text: "Blue Background ", - }, - { - type: "text", - styles: { - textColor: "red", - backgroundColor: "blue", - }, - text: "Mixed Colors", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "reactCustomParagraph", "styled"); - }); - - it("Convert nested block with inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "reactCustomParagraph", - content: "React Custom Paragraph", - children: [ - { - type: "reactCustomParagraph", - content: "Nested React Custom Paragraph 1", - }, - { - type: "reactCustomParagraph", - content: "Nested React Custom Paragraph 2", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots(blocks, "reactCustomParagraph", "nested"); - }); -}); - -describe("Convert custom blocks with non-exported inline content to HTML", () => { - it("Convert custom block with non-exported inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "simpleReactCustomParagraph", - content: "React Custom Paragraph", - }, - ]; - - convertToHTMLAndCompareSnapshots( - blocks, - "simpleReactCustomParagraph", - "basic" - ); - }); - - it("Convert styled custom block with non-exported inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "simpleReactCustomParagraph", - props: { - textAlignment: "center", - textColor: "orange", - backgroundColor: "pink", - }, - content: [ - { - type: "text", - styles: {}, - text: "Plain ", - }, - { - type: "text", - styles: { - textColor: "red", - }, - text: "Red Text ", - }, - { - type: "text", - styles: { - backgroundColor: "blue", - }, - text: "Blue Background ", - }, - { - type: "text", - styles: { - textColor: "red", - backgroundColor: "blue", - }, - text: "Mixed Colors", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots( - blocks, - "simpleReactCustomParagraph", - "styled" - ); - }); - - it("Convert nested block with non-exported inline content to HTML", async () => { - const blocks: CustomPartialBlock[] = [ - { - type: "simpleReactCustomParagraph", - content: "Custom React Paragraph", - children: [ - { - type: "simpleReactCustomParagraph", - content: "Nested React Custom Paragraph 1", - }, - { - type: "simpleReactCustomParagraph", - content: "Nested React Custom Paragraph 2", - }, - ], - }, - ]; - - convertToHTMLAndCompareSnapshots( - blocks, - "simpleReactCustomParagraph", - "nested" - ); - }); +const testCases = [customReactBlockSchemaTestCases]; + +describe("Test React HTML conversion", () => { + for (const testCase of testCases) { + describe("Case: " + testCase.name, () => { + let editor: BlockNoteEditor; + + beforeEach(() => { + editor = testCase.createEditor(); + }); + + afterEach(() => { + editor._tiptapEditor.destroy(); + editor = undefined as any; + + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; + }); + + for (const document of testCase.documents) { + // eslint-disable-next-line no-loop-func + it("Convert " + document.name + " to HTML", () => { + const nameSplit = document.name.split("/"); + convertToHTMLAndCompareSnapshots( + editor, + document.blocks, + nameSplit[0], + nameSplit[1] + ); + }); + } + }); + } }); diff --git a/packages/react/src/testCases/customReactBlocks.tsx b/packages/react/src/testCases/customReactBlocks.tsx new file mode 100644 index 0000000000..88a8553076 --- /dev/null +++ b/packages/react/src/testCases/customReactBlocks.tsx @@ -0,0 +1,202 @@ +import { + BlockNoteEditor, + BlockSchemaFromSpecs, + BlockSpecs, + DefaultInlineContentSchema, + DefaultStyleSchema, + EditorTestCases, + defaultBlockSpecs, + defaultProps, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "@blocknote/core"; +import { InlineContent, createReactBlockSpec } from "../ReactBlockSpec"; + +const ReactCustomParagraph = createReactBlockSpec( + { + type: "reactCustomParagraph" as const, + propSchema: defaultProps, + content: "inline", + }, + { + render: () => ( + + ), + toExternalHTML: () => ( +

    Hello World

    + ), + } +); + +const SimpleReactCustomParagraph = createReactBlockSpec( + { + type: "simpleReactCustomParagraph" as const, + propSchema: defaultProps, + content: "inline", + }, + { + render: () => ( + + ), + } +); + +const customSpecs = { + ...defaultBlockSpecs, + reactCustomParagraph: ReactCustomParagraph, + simpleReactCustomParagraph: SimpleReactCustomParagraph, +} satisfies BlockSpecs; + +export const customReactBlockSchemaTestCases: EditorTestCases< + BlockSchemaFromSpecs, + DefaultInlineContentSchema, + DefaultStyleSchema +> = { + name: "custom react block schema", + createEditor: () => { + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + }, + documents: [ + { + name: "reactCustomParagraph/basic", + blocks: [ + { + type: "reactCustomParagraph", + content: "React Custom Paragraph", + }, + ], + }, + { + name: "reactCustomParagraph/styled", + blocks: [ + { + type: "reactCustomParagraph", + props: { + textAlignment: "center", + textColor: "orange", + backgroundColor: "pink", + }, + content: [ + { + type: "text", + styles: {}, + text: "Plain ", + }, + { + type: "text", + styles: { + textColor: "red", + }, + text: "Red Text ", + }, + { + type: "text", + styles: { + backgroundColor: "blue", + }, + text: "Blue Background ", + }, + { + type: "text", + styles: { + textColor: "red", + backgroundColor: "blue", + }, + text: "Mixed Colors", + }, + ], + }, + ], + }, + { + name: "reactCustomParagraph/nested", + blocks: [ + { + type: "reactCustomParagraph", + content: "React Custom Paragraph", + children: [ + { + type: "reactCustomParagraph", + content: "Nested React Custom Paragraph 1", + }, + { + type: "reactCustomParagraph", + content: "Nested React Custom Paragraph 2", + }, + ], + }, + ], + }, + { + name: "simpleReactCustomParagraph/basic", + blocks: [ + { + type: "simpleReactCustomParagraph", + content: "React Custom Paragraph", + }, + ], + }, + { + name: "simpleReactCustomParagraph/styled", + blocks: [ + { + type: "simpleReactCustomParagraph", + props: { + textAlignment: "center", + textColor: "orange", + backgroundColor: "pink", + }, + content: [ + { + type: "text", + styles: {}, + text: "Plain ", + }, + { + type: "text", + styles: { + textColor: "red", + }, + text: "Red Text ", + }, + { + type: "text", + styles: { + backgroundColor: "blue", + }, + text: "Blue Background ", + }, + { + type: "text", + styles: { + textColor: "red", + backgroundColor: "blue", + }, + text: "Mixed Colors", + }, + ], + }, + ], + }, + { + name: "simpleReactCustomParagraph/nested", + blocks: [ + { + type: "simpleReactCustomParagraph", + content: "Custom React Paragraph", + children: [ + { + type: "simpleReactCustomParagraph", + content: "Nested React Custom Paragraph 1", + }, + { + type: "simpleReactCustomParagraph", + content: "Nested React Custom Paragraph 2", + }, + ], + }, + ], + }, + ], +}; diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 83603cc619..c98edbfdd3 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -5,12 +5,21 @@ import pkg from "./package.json"; // import eslintPlugin from "vite-plugin-eslint"; // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig((conf) => ({ test: { environment: "jsdom", setupFiles: ["./vitestSetup.ts"], }, plugins: [react()], + resolve: { + alias: + conf.command === "build" + ? ({} as Record) + : ({ + // load live from sources with live reload working + "@blocknote/core": path.resolve(__dirname, "../core/src/"), + } as Record), + }, build: { sourcemap: true, lib: { @@ -37,4 +46,4 @@ export default defineConfig({ }, }, }, -}); +})); From 3989912796dc9b00c28373a5c32f59de4ea4f19f Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 00:45:36 +0100 Subject: [PATCH 14/31] fix react tests --- .../api/nodeConversions/nodeConversions.ts | 4 ++ .../TextAlignment/TextAlignmentExtension.ts | 50 ------------------- packages/react/src/ReactBlockSpec.tsx | 2 +- .../react/src/testCases/customReactBlocks.tsx | 1 + 4 files changed, 6 insertions(+), 51 deletions(-) diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 402c517faa..04fedd07a8 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -200,6 +200,10 @@ function blockOrInlineContentToContentNode( type = "paragraph"; } + if (!schema.nodes[type]) { + throw new Error(`node type ${type} not found in schema`); + } + if (!block.content) { contentNode = schema.nodes[type].create(block.props); } else if (typeof block.content === "string") { diff --git a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts index 6a99548918..4535866dad 100644 --- a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts +++ b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts @@ -1,15 +1,4 @@ import { Extension } from "@tiptap/core"; -import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; - -declare module "@tiptap/core" { - interface Commands { - textAlignment: { - setTextAlignment: ( - textAlignment: "left" | "center" | "right" | "justify" - ) => ReturnType; - }; - } -} export const TextAlignmentExtension = Extension.create({ name: "textAlignment", @@ -33,43 +22,4 @@ export const TextAlignmentExtension = Extension.create({ }, ]; }, - - addCommands() { - return { - setTextAlignment: - (textAlignment) => - ({ state }) => { - const positionsBeforeSelectedContent = []; - - const blockInfo = getBlockInfoFromPos( - state.doc, - state.selection.from - ); - if (blockInfo === undefined) { - return false; - } - - // Finds all blockContent nodes that the current selection is in. - let pos = blockInfo.startPos; - while (pos < state.selection.to) { - if ( - state.doc.resolve(pos).node().type.spec.group === "blockContent" - ) { - positionsBeforeSelectedContent.push(pos - 1); - - pos += state.doc.resolve(pos).node().nodeSize - 1; - } else { - pos += 1; - } - } - - // Sets text alignment for all blockContent nodes that the current selection is in. - for (const pos of positionsBeforeSelectedContent) { - state.tr.setNodeAttribute(pos, "textAlignment", textAlignment); - } - - return true; - }, - }; - }, }); diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index 98ff0ff839..e23077b998 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -137,7 +137,7 @@ export function createReactBlockSpec< selectable: true, addAttributes() { - return propsToAttributes(blockConfig as any); // TODO: cast + return propsToAttributes(blockConfig.propSchema); }, parseHTML() { diff --git a/packages/react/src/testCases/customReactBlocks.tsx b/packages/react/src/testCases/customReactBlocks.tsx index 88a8553076..c74f3cc2be 100644 --- a/packages/react/src/testCases/customReactBlocks.tsx +++ b/packages/react/src/testCases/customReactBlocks.tsx @@ -54,6 +54,7 @@ export const customReactBlockSchemaTestCases: EditorTestCases< name: "custom react block schema", createEditor: () => { return BlockNoteEditor.create({ + blockSpecs: customSpecs, uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, }); }, From 4b2b249ea3c09e942170f6d4919d6419226e8b68 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 00:50:19 +0100 Subject: [PATCH 15/31] add react nodeconversions tests --- packages/core/src/index.ts | 4 + .../nodeConversion.test.tsx.snap | 333 ++++++++++++++++++ packages/react/src/nodeConversion.test.tsx | 77 ++++ 3 files changed, 414 insertions(+) create mode 100644 packages/react/src/__snapshots__/nodeConversion.test.tsx.snap create mode 100644 packages/react/src/nodeConversion.test.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 25cb3ce75c..81d2c288a8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,3 +29,7 @@ export * from "./shared/BaseUiElementTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./shared/plugins/suggestion/SuggestionPlugin"; export * from "./shared/utils"; +// for testing from react (TODO: move): +export * from "./api/nodeConversions/nodeConversions"; +export * from "./api/nodeConversions/testUtil"; +export * from "./extensions/UniqueID/UniqueID"; diff --git a/packages/react/src/__snapshots__/nodeConversion.test.tsx.snap b/packages/react/src/__snapshots__/nodeConversion.test.tsx.snap new file mode 100644 index 0000000000..b3a5a0765b --- /dev/null +++ b/packages/react/src/__snapshots__/nodeConversion.test.tsx.snap @@ -0,0 +1,333 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert reactCustomParagraph/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "React Custom Paragraph", + "type": "text", + }, + ], + "type": "reactCustomParagraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert reactCustomParagraph/nested to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "React Custom Paragraph", + "type": "text", + }, + ], + "type": "reactCustomParagraph", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "id": "2", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested React Custom Paragraph 1", + "type": "text", + }, + ], + "type": "reactCustomParagraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "backgroundColor": "default", + "id": "3", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested React Custom Paragraph 2", + "type": "text", + }, + ], + "type": "reactCustomParagraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert reactCustomParagraph/styled to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "pink", + "id": "1", + "textColor": "orange", + }, + "content": [ + { + "attrs": { + "textAlignment": "center", + }, + "content": [ + { + "text": "Plain ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + ], + "text": "Red Text ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Blue Background ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Mixed Colors", + "type": "text", + }, + ], + "type": "reactCustomParagraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert simpleReactCustomParagraph/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "React Custom Paragraph", + "type": "text", + }, + ], + "type": "simpleReactCustomParagraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert simpleReactCustomParagraph/nested to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Custom React Paragraph", + "type": "text", + }, + ], + "type": "simpleReactCustomParagraph", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "id": "2", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested React Custom Paragraph 1", + "type": "text", + }, + ], + "type": "simpleReactCustomParagraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "backgroundColor": "default", + "id": "3", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Nested React Custom Paragraph 2", + "type": "text", + }, + ], + "type": "simpleReactCustomParagraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block schema > Convert simpleReactCustomParagraph/styled to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "pink", + "id": "1", + "textColor": "orange", + }, + "content": [ + { + "attrs": { + "textAlignment": "center", + }, + "content": [ + { + "text": "Plain ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + ], + "text": "Red Text ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Blue Background ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Mixed Colors", + "type": "text", + }, + ], + "type": "simpleReactCustomParagraph", + }, + ], + "type": "blockContainer", +} +`; diff --git a/packages/react/src/nodeConversion.test.tsx b/packages/react/src/nodeConversion.test.tsx new file mode 100644 index 0000000000..852ddf83b9 --- /dev/null +++ b/packages/react/src/nodeConversion.test.tsx @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + BlockNoteEditor, + PartialBlock, + UniqueID, + blockToNode, + nodeToBlock, + partialBlockToBlockForTesting, +} from "@blocknote/core"; +import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; + +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._tiptapEditor.schema, + editor.styleSchema + ); + + expect(node).toMatchSnapshot(); + + const outputBlock = nodeToBlock( + node, + editor.blockSchema, + editor.inlineContentSchema, + editor.styleSchema + ); + + const fullOriginalBlock = partialBlockToBlockForTesting( + editor.blockSchema, + block + ); + + expect(outputBlock).toStrictEqual(fullOriginalBlock); +} + +const testCases = [customReactBlockSchemaTestCases]; + +describe("Test React BlockNote-Prosemirror conversion", () => { + for (const testCase of testCases) { + describe("Case: " + testCase.name, () => { + let editor: BlockNoteEditor; + + beforeEach(() => { + editor = testCase.createEditor(); + }); + + afterEach(() => { + editor._tiptapEditor.destroy(); + editor = undefined as any; + + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; + }); + + for (const document of testCase.documents) { + // eslint-disable-next-line no-loop-func + it("Convert " + document.name + " to/from prosemirror", () => { + // NOTE: only converts first block + validateConversion(document.blocks[0], editor); + }); + } + }); + } +}); From d3a282a4ce0b2b01f51320342a9391d618d55321 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 07:34:25 +0100 Subject: [PATCH 16/31] move tests and add test for ReactStyles --- packages/react/package.json | 3 +- packages/react/src/ReactStyleSpec.tsx | 81 ++++++---------- .../fontSize/basic/external.html | 1 + .../fontSize/basic/internal.html | 1 + .../nodeConversion.test.tsx.snap | 63 +++++++++++++ .../reactCustomParagraph/basic/external.html | 0 .../reactCustomParagraph/basic/internal.html | 0 .../reactCustomParagraph/nested/external.html | 0 .../reactCustomParagraph/nested/internal.html | 0 .../reactCustomParagraph/styled/external.html | 0 .../reactCustomParagraph/styled/internal.html | 0 .../basic/external.html | 0 .../basic/internal.html | 0 .../nested/external.html | 0 .../nested/internal.html | 0 .../styled/external.html | 0 .../styled/internal.html | 0 .../__snapshots__/small/basic/external.html | 1 + .../__snapshots__/small/basic/internal.html | 1 + .../src/{ => test}/htmlConversion.test.tsx | 18 +++- .../src/{ => test}/nodeConversion.test.tsx | 3 +- .../testCases/customReactBlocks.tsx | 2 +- .../src/test/testCases/customReactStyles.tsx | 93 +++++++++++++++++++ 23 files changed, 209 insertions(+), 58 deletions(-) create mode 100644 packages/react/src/test/__snapshots__/fontSize/basic/external.html create mode 100644 packages/react/src/test/__snapshots__/fontSize/basic/internal.html rename packages/react/src/{ => test}/__snapshots__/nodeConversion.test.tsx.snap (85%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/basic/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/basic/internal.html (100%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/nested/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/nested/internal.html (100%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/styled/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/reactCustomParagraph/styled/internal.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/basic/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/basic/internal.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/nested/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/nested/internal.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/styled/external.html (100%) rename packages/react/src/{ => test}/__snapshots__/simpleReactCustomParagraph/styled/internal.html (100%) create mode 100644 packages/react/src/test/__snapshots__/small/basic/external.html create mode 100644 packages/react/src/test/__snapshots__/small/basic/internal.html rename packages/react/src/{ => test}/htmlConversion.test.tsx (80%) rename packages/react/src/{ => test}/nodeConversion.test.tsx (92%) rename packages/react/src/{ => test}/testCases/customReactBlocks.tsx (98%) create mode 100644 packages/react/src/test/testCases/customReactStyles.tsx diff --git a/packages/react/package.json b/packages/react/package.json index 2405e5568c..c2a2ee4fca 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -45,7 +45,8 @@ "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", "preview": "vite preview", "lint": "eslint src --max-warnings 0", - "test": "vitest --run" + "test": "vitest --run", + "test:watch": "vitest --watch" }, "dependencies": { "@blocknote/core": "^0.9.6", diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index 82aba6ce96..1297710d66 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -1,48 +1,24 @@ import { BlockNoteDOMAttributes, createInternalStyleSpec, - mergeCSSClasses, StyleConfig, - UnreachableCaseError, } from "@blocknote/core"; -import { Mark, NodeViewContent } from "@tiptap/react"; -import { createContext, ElementType, FC, HTMLProps, useContext } from "react"; -import { renderToString } from "react-dom/server"; +import { Mark } from "@tiptap/react"; +import { createContext, FC } from "react"; +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks // extend BlockConfig but use a React render function export type ReactCustomStyleImplementation = { - render: T["propSchema"] extends "boolean" ? FC : FC<{ value: string }>; + render: T["propSchema"] extends "boolean" + ? FC<{ contentRef: (el: HTMLElement | null) => void }> + : FC<{ contentRef: (el: HTMLElement | null) => void; value: string }>; }; const BlockNoteDOMAttributesContext = createContext({}); -export const InlineContent = ( - props: { as?: Tag } & HTMLProps -) => { - const inlineContentDOMAttributes = - useContext(BlockNoteDOMAttributesContext).inlineContent || {}; - - const classNames = mergeCSSClasses( - props.className || "", - "bn-inline-content", - inlineContentDOMAttributes.class - ); - - return ( - key !== "class" - ) - )} - {...props} - className={classNames} - /> - ); -}; - // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactStyleSpec( @@ -71,31 +47,30 @@ export function createReactStyleSpec( }, renderHTML({ mark }) { - if (styleConfig.propSchema === "boolean") { - const Content = styleImplementation.render as FC; - const parent = document.createElement("div"); - parent.innerHTML = renderToString(); + const props: any = {}; - return { - dom: parent.firstElementChild! as HTMLElement, - contentDOM: (parent.querySelector(".bn-inline-content") || - undefined) as HTMLElement | undefined, - }; - } else if (styleConfig.propSchema === "string") { - const Content = styleImplementation.render as FC<{ value: string }>; - const parent = document.createElement("div"); - parent.innerHTML = renderToString( - + if (styleConfig.propSchema === "string") { + props.value = mark.attrs.stringValue; + } + + const Content = styleImplementation.render; + + let contentDOM: HTMLElement | undefined; + const div = document.createElement("div"); + const root = createRoot(div); + flushSync(() => { + root.render( + (contentDOM = el || undefined)} + /> ); + }); - return { - dom: parent.firstElementChild! as HTMLElement, - contentDOM: (parent.querySelector(".bn-inline-content") || - undefined) as HTMLElement | undefined, - }; - } else { - throw new UnreachableCaseError(styleConfig.propSchema); - } + return { + dom: div.firstElementChild! as HTMLElement, + contentDOM, + }; }, }); diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/external.html b/packages/react/src/test/__snapshots__/fontSize/basic/external.html new file mode 100644 index 0000000000..00a5bc6b6e --- /dev/null +++ b/packages/react/src/test/__snapshots__/fontSize/basic/external.html @@ -0,0 +1 @@ +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/internal.html b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html new file mode 100644 index 0000000000..a41d39869a --- /dev/null +++ b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html @@ -0,0 +1 @@ +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/react/src/__snapshots__/nodeConversion.test.tsx.snap b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap similarity index 85% rename from packages/react/src/__snapshots__/nodeConversion.test.tsx.snap rename to packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap index b3a5a0765b..3e67d467ee 100644 --- a/packages/react/src/__snapshots__/nodeConversion.test.tsx.snap +++ b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap @@ -331,3 +331,66 @@ exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block "type": "blockContainer", } `; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "stringValue": "18px", + }, + "type": "fontSize", + }, + ], + "text": "This is text with a custom fontSize", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "type": "small", + }, + ], + "text": "This is a small text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/basic/external.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/external.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/basic/external.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/basic/external.html diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/basic/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/basic/internal.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/nested/external.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/external.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/nested/external.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/nested/external.html diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/nested/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/nested/internal.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/styled/external.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/external.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/styled/external.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/styled/external.html diff --git a/packages/react/src/__snapshots__/reactCustomParagraph/styled/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html similarity index 100% rename from packages/react/src/__snapshots__/reactCustomParagraph/styled/internal.html rename to packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/basic/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/basic/external.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/basic/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/basic/internal.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/nested/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/nested/external.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/nested/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/nested/internal.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/styled/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/styled/external.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html diff --git a/packages/react/src/__snapshots__/simpleReactCustomParagraph/styled/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html similarity index 100% rename from packages/react/src/__snapshots__/simpleReactCustomParagraph/styled/internal.html rename to packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html diff --git a/packages/react/src/test/__snapshots__/small/basic/external.html b/packages/react/src/test/__snapshots__/small/basic/external.html new file mode 100644 index 0000000000..4206d07a95 --- /dev/null +++ b/packages/react/src/test/__snapshots__/small/basic/external.html @@ -0,0 +1 @@ +

    This is a small text

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/small/basic/internal.html b/packages/react/src/test/__snapshots__/small/basic/internal.html new file mode 100644 index 0000000000..805c78112e --- /dev/null +++ b/packages/react/src/test/__snapshots__/small/basic/internal.html @@ -0,0 +1 @@ +

    This is a small text

    \ No newline at end of file diff --git a/packages/react/src/htmlConversion.test.tsx b/packages/react/src/test/htmlConversion.test.tsx similarity index 80% rename from packages/react/src/htmlConversion.test.tsx rename to packages/react/src/test/htmlConversion.test.tsx index 83aed33830..e573c510ee 100644 --- a/packages/react/src/htmlConversion.test.tsx +++ b/packages/react/src/test/htmlConversion.test.tsx @@ -9,8 +9,11 @@ import { createExternalHTMLExporter, createInternalHTMLSerializer, } from "@blocknote/core"; +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks.jsx"; +import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; +import { customReactStylesTestCases } from "./testCases/customReactStyles"; function convertToHTMLAndCompareSnapshots< B extends BlockSchema, @@ -49,7 +52,7 @@ function convertToHTMLAndCompareSnapshots< expect(externalHTML).toMatchFileSnapshot(externalHTMLSnapshotPath); } -const testCases = [customReactBlockSchemaTestCases]; +const testCases = [customReactBlockSchemaTestCases, customReactStylesTestCases]; describe("Test React HTML conversion", () => { for (const testCase of testCases) { @@ -82,3 +85,14 @@ describe("Test React HTML conversion", () => { }); } }); + +it("test react render", () => { + const div = document.createElement("div"); + const root = createRoot(div); + function hello(el: HTMLElement | null) { + console.log("ELEMENT", el?.innerHTML); + } + flushSync(() => { + root.render(
    sdf
    ); + }); +}); diff --git a/packages/react/src/nodeConversion.test.tsx b/packages/react/src/test/nodeConversion.test.tsx similarity index 92% rename from packages/react/src/nodeConversion.test.tsx rename to packages/react/src/test/nodeConversion.test.tsx index 852ddf83b9..b3def315a5 100644 --- a/packages/react/src/nodeConversion.test.tsx +++ b/packages/react/src/test/nodeConversion.test.tsx @@ -9,6 +9,7 @@ import { partialBlockToBlockForTesting, } from "@blocknote/core"; import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; +import { customReactStylesTestCases } from "./testCases/customReactStyles"; function addIdsToBlock(block: PartialBlock) { if (!block.id) { @@ -47,7 +48,7 @@ function validateConversion( expect(outputBlock).toStrictEqual(fullOriginalBlock); } -const testCases = [customReactBlockSchemaTestCases]; +const testCases = [customReactBlockSchemaTestCases, customReactStylesTestCases]; describe("Test React BlockNote-Prosemirror conversion", () => { for (const testCase of testCases) { diff --git a/packages/react/src/testCases/customReactBlocks.tsx b/packages/react/src/test/testCases/customReactBlocks.tsx similarity index 98% rename from packages/react/src/testCases/customReactBlocks.tsx rename to packages/react/src/test/testCases/customReactBlocks.tsx index c74f3cc2be..fc709cb2a6 100644 --- a/packages/react/src/testCases/customReactBlocks.tsx +++ b/packages/react/src/test/testCases/customReactBlocks.tsx @@ -9,7 +9,7 @@ import { defaultProps, uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; -import { InlineContent, createReactBlockSpec } from "../ReactBlockSpec"; +import { InlineContent, createReactBlockSpec } from "../../ReactBlockSpec"; const ReactCustomParagraph = createReactBlockSpec( { diff --git a/packages/react/src/test/testCases/customReactStyles.tsx b/packages/react/src/test/testCases/customReactStyles.tsx new file mode 100644 index 0000000000..a8507af90c --- /dev/null +++ b/packages/react/src/test/testCases/customReactStyles.tsx @@ -0,0 +1,93 @@ +import { + BlockNoteEditor, + DefaultBlockSchema, + DefaultInlineContentSchema, + EditorTestCases, + StyleSchemaFromSpecs, + StyleSpecs, + defaultStyleSpecs, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "@blocknote/core"; +import { createReactStyleSpec } from "../../ReactStyleSpec"; + +const small = createReactStyleSpec( + { + type: "small", + propSchema: "boolean", + }, + { + render: (props) => { + return ; + }, + } +); + +const fontSize = createReactStyleSpec( + { + type: "fontSize", + propSchema: "string", + }, + { + render: (props) => { + return ( + + ); + }, + } +); + +const customReactStyles = { + ...defaultStyleSpecs, + small, + fontSize, +} satisfies StyleSpecs; + +export const customReactStylesTestCases: EditorTestCases< + DefaultBlockSchema, + DefaultInlineContentSchema, + StyleSchemaFromSpecs +> = { + name: "custom style schema", + createEditor: () => { + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + styleSpecs: customReactStyles, + }); + }, + documents: [ + { + name: "small/basic", + blocks: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "This is a small text", + styles: { + small: true, + }, + }, + ], + }, + ], + }, + { + name: "fontSize/basic", + blocks: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "This is text with a custom fontSize", + styles: { + fontSize: "18px", + }, + }, + ], + }, + ], + }, + ], +}; From 70b97de2802cfd5e6b3492943c980208cbfaa47d Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 08:49:53 +0100 Subject: [PATCH 17/31] fix react tests --- .../html/sharedHTMLConversion.ts | 3 + packages/react/src/ReactInlineContentSpec.tsx | 62 ++++++++- packages/react/src/ReactStyleSpec.tsx | 23 ++-- .../__snapshots__/mention/basic/external.html | 1 + .../__snapshots__/mention/basic/internal.html | 1 + .../nodeConversion.test.tsx.snap | 128 ++++++++++++++++++ .../__snapshots__/tag/basic/external.html | 1 + .../__snapshots__/tag/basic/internal.html | 1 + .../react/src/test/htmlConversion.test.tsx | 7 +- .../react/src/test/nodeConversion.test.tsx | 7 +- .../testCases/customReactInlineContent.tsx | 99 ++++++++++++++ .../src/test/testCases/customReactStyles.tsx | 2 +- 12 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 packages/react/src/test/__snapshots__/mention/basic/external.html create mode 100644 packages/react/src/test/__snapshots__/mention/basic/internal.html create mode 100644 packages/react/src/test/__snapshots__/tag/basic/external.html create mode 100644 packages/react/src/test/__snapshots__/tag/basic/internal.html create mode 100644 packages/react/src/test/testCases/customReactInlineContent.tsx diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts index 8d8d23a778..1f91309287 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/serialization/html/sharedHTMLConversion.ts @@ -27,6 +27,9 @@ export const serializeNodeInner = < editor: BlockNoteEditor, toExternalHTML: boolean ) => { + if (!serializer.nodes[node.type.name]) { + throw new Error("Serializer is missing a node type: " + node.type.name); + } const { dom, contentDOM } = DOMSerializer.renderSpec( doc(options), serializer.nodes[node.type.name](node) diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index cbbcbd2701..c672ef86de 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -3,11 +3,19 @@ import { createStronglyTypedTiptapNode, InlineContentConfig, InlineContentFromConfig, + nodeToCustomInlineContent, + propsToAttributes, StyleSchema, } from "@blocknote/core"; -import { nodeToCustomInlineContent } from "@blocknote/core/src/api/nodeConversions/nodeConversions"; -import { NodeViewProps, ReactNodeViewRenderer } from "@tiptap/react"; +import { + NodeViewContent, + NodeViewProps, + ReactNodeViewRenderer, +} from "@tiptap/react"; +// import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; import { FC } from "react"; +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks @@ -19,6 +27,7 @@ export type ReactInlineContentImplementation< > = { render: FC<{ inlineContent: InlineContentFromConfig; + contentRef: (node: HTMLElement | null) => void; }>; // TODO? // toExternalHTML?: FC<{ @@ -43,14 +52,52 @@ export function createReactInlineContentSpec< ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", - // addAttributes() { - // return propsToAttributes(blockConfig); - // }, + addAttributes() { + return propsToAttributes(inlineContentConfig.propSchema); + }, // parseHTML() { // return parse(blockConfig); // }, + renderHTML({ node }) { + const editor = this.options.editor; + + const ic = nodeToCustomInlineContent( + node, + editor.inlineContentSchema, + editor.styleSchema + ) as any as InlineContentFromConfig; // TODO: fix cast + + const Content = inlineContentImplementation.render; + + let contentDOM: HTMLElement | undefined; + const div = document.createElement("div"); + const root = createRoot(div); + flushSync(() => { + root.render( + (contentDOM = el || undefined)} + /> + ); + }); + + // clone so we can unmount the react root + contentDOM?.setAttribute("data-tmp-find", "true"); + const cloneRoot = div.cloneNode(true) as HTMLElement; + const dom = cloneRoot.firstElementChild! as HTMLElement; + const contentDOMClone = cloneRoot.querySelector("[data-tmp-find]"); + contentDOMClone?.removeAttribute("data-tmp-find"); + + root.unmount(); + + return { + dom, + contentDOM: contentDOMClone, + }; + }, + // TODO: needed? addNodeView() { const editor = this.options.editor; @@ -58,9 +105,14 @@ export function createReactInlineContentSpec< return (props) => ReactNodeViewRenderer( (props: NodeViewProps) => { + // TODO + const test = NodeViewContent({}); + debugger; + // const ctx = useReactNodeView(); const Content = inlineContentImplementation.render; return ( = { : FC<{ contentRef: (el: HTMLElement | null) => void; value: string }>; }; -const BlockNoteDOMAttributesContext = createContext({}); - // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactStyleSpec( @@ -67,9 +61,18 @@ export function createReactStyleSpec( ); }); + // clone so we can unmount the react root + contentDOM?.setAttribute("data-tmp-find", "true"); + const cloneRoot = div.cloneNode(true) as HTMLElement; + const dom = cloneRoot.firstElementChild! as HTMLElement; + const contentDOMClone = cloneRoot.querySelector("[data-tmp-find]"); + contentDOMClone?.removeAttribute("data-tmp-find"); + + root.unmount(); + return { - dom: div.firstElementChild! as HTMLElement, - contentDOM, + dom, + contentDOM: contentDOMClone, }; }, }); diff --git a/packages/react/src/test/__snapshots__/mention/basic/external.html b/packages/react/src/test/__snapshots__/mention/basic/external.html new file mode 100644 index 0000000000..e1513fed2d --- /dev/null +++ b/packages/react/src/test/__snapshots__/mention/basic/external.html @@ -0,0 +1 @@ +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/mention/basic/internal.html b/packages/react/src/test/__snapshots__/mention/basic/internal.html new file mode 100644 index 0000000000..7af6dad9c7 --- /dev/null +++ b/packages/react/src/test/__snapshots__/mention/basic/internal.html @@ -0,0 +1 @@ +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap index 3e67d467ee..c3d786711e 100644 --- a/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap +++ b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap @@ -332,6 +332,134 @@ exports[`Test React BlockNote-Prosemirror conversion > Case: custom react block } `; +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react inline content schema > Convert mention/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "I enjoy working with", + "type": "text", + }, + { + "attrs": { + "user": "Matthew", + }, + "type": "mention", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react inline content schema > Convert tag/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "I love ", + "type": "text", + }, + { + "content": [ + { + "text": "BlockNote", + "type": "text", + }, + ], + "type": "tag", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react style schema > Convert fontSize/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "stringValue": "18px", + }, + "type": "fontSize", + }, + ], + "text": "This is text with a custom fontSize", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test React BlockNote-Prosemirror conversion > Case: custom react style schema > Convert small/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "marks": [ + { + "type": "small", + }, + ], + "text": "This is a small text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", +} +`; + exports[`Test React BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = ` { "attrs": { diff --git a/packages/react/src/test/__snapshots__/tag/basic/external.html b/packages/react/src/test/__snapshots__/tag/basic/external.html new file mode 100644 index 0000000000..d5a66eb072 --- /dev/null +++ b/packages/react/src/test/__snapshots__/tag/basic/external.html @@ -0,0 +1 @@ +

    I love @BlockNote

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/internal.html b/packages/react/src/test/__snapshots__/tag/basic/internal.html new file mode 100644 index 0000000000..4b24322ad0 --- /dev/null +++ b/packages/react/src/test/__snapshots__/tag/basic/internal.html @@ -0,0 +1 @@ +

    I love @BlockNote

    \ No newline at end of file diff --git a/packages/react/src/test/htmlConversion.test.tsx b/packages/react/src/test/htmlConversion.test.tsx index e573c510ee..0bf09e5c60 100644 --- a/packages/react/src/test/htmlConversion.test.tsx +++ b/packages/react/src/test/htmlConversion.test.tsx @@ -13,6 +13,7 @@ import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; +import { customReactInlineContentTestCases } from "./testCases/customReactInlineContent"; import { customReactStylesTestCases } from "./testCases/customReactStyles"; function convertToHTMLAndCompareSnapshots< @@ -52,7 +53,11 @@ function convertToHTMLAndCompareSnapshots< expect(externalHTML).toMatchFileSnapshot(externalHTMLSnapshotPath); } -const testCases = [customReactBlockSchemaTestCases, customReactStylesTestCases]; +const testCases = [ + customReactBlockSchemaTestCases, + customReactStylesTestCases, + customReactInlineContentTestCases, +]; describe("Test React HTML conversion", () => { for (const testCase of testCases) { diff --git a/packages/react/src/test/nodeConversion.test.tsx b/packages/react/src/test/nodeConversion.test.tsx index b3def315a5..6c48f557a4 100644 --- a/packages/react/src/test/nodeConversion.test.tsx +++ b/packages/react/src/test/nodeConversion.test.tsx @@ -9,6 +9,7 @@ import { partialBlockToBlockForTesting, } from "@blocknote/core"; import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; +import { customReactInlineContentTestCases } from "./testCases/customReactInlineContent"; import { customReactStylesTestCases } from "./testCases/customReactStyles"; function addIdsToBlock(block: PartialBlock) { @@ -48,7 +49,11 @@ function validateConversion( expect(outputBlock).toStrictEqual(fullOriginalBlock); } -const testCases = [customReactBlockSchemaTestCases, customReactStylesTestCases]; +const testCases = [ + customReactBlockSchemaTestCases, + customReactStylesTestCases, + customReactInlineContentTestCases, +]; describe("Test React BlockNote-Prosemirror conversion", () => { for (const testCase of testCases) { diff --git a/packages/react/src/test/testCases/customReactInlineContent.tsx b/packages/react/src/test/testCases/customReactInlineContent.tsx new file mode 100644 index 0000000000..76f0dc50e9 --- /dev/null +++ b/packages/react/src/test/testCases/customReactInlineContent.tsx @@ -0,0 +1,99 @@ +import { + BlockNoteEditor, + DefaultBlockSchema, + DefaultStyleSchema, + EditorTestCases, + InlineContentSchemaFromSpecs, + InlineContentSpecs, + uploadToTmpFilesDotOrg_DEV_ONLY, +} from "@blocknote/core"; +import { createReactInlineContentSpec } from "../../ReactInlineContentSpec"; + +const mention = createReactInlineContentSpec( + { + type: "mention", + propSchema: { + user: { + default: "", + }, + }, + content: "none", + }, + { + render: (props) => { + return @{props.inlineContent.props.user}; + }, + } +); + +const tag = createReactInlineContentSpec( + { + type: "tag", + propSchema: {}, + content: "styled", + }, + { + render: (props) => { + return ( + + @ + + ); + }, + } +); + +const customReactInlineContent = { + tag, + mention, +} satisfies InlineContentSpecs; + +export const customReactInlineContentTestCases: EditorTestCases< + DefaultBlockSchema, + InlineContentSchemaFromSpecs, + DefaultStyleSchema +> = { + name: "custom react inline content schema", + createEditor: () => { + return BlockNoteEditor.create({ + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + inlineContentSpecs: customReactInlineContent, + }); + }, + documents: [ + { + name: "mention/basic", + blocks: [ + { + type: "paragraph", + content: [ + "I enjoy working with", + { + type: "mention", + props: { + user: "Matthew", + }, + content: undefined, + } as any, + ], + }, + ], + }, + { + name: "tag/basic", + blocks: [ + { + type: "paragraph", + content: [ + "I love ", + { + type: "tag", + // props: {}, + content: "BlockNote", + } as any, + ], + }, + ], + }, + ], +}; diff --git a/packages/react/src/test/testCases/customReactStyles.tsx b/packages/react/src/test/testCases/customReactStyles.tsx index a8507af90c..ea7126d6ce 100644 --- a/packages/react/src/test/testCases/customReactStyles.tsx +++ b/packages/react/src/test/testCases/customReactStyles.tsx @@ -47,7 +47,7 @@ export const customReactStylesTestCases: EditorTestCases< DefaultInlineContentSchema, StyleSchemaFromSpecs > = { - name: "custom style schema", + name: "custom react style schema", createEditor: () => { return BlockNoteEditor.create({ uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, From 2aac331ba0bfbe374d9d4440dcf30a1c9199593d Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 22 Nov 2023 21:42:27 +0100 Subject: [PATCH 18/31] basis of new examples --- examples/editor/examples/App.tsx | 52 ++ examples/editor/examples/Two.tsx | 52 ++ examples/editor/package.json | 6 +- examples/editor/src/App.tsx | 29 - examples/editor/src/main.tsx | 95 ++- .../editor/src/{App.module.css => style.css} | 9 + examples/editor/tsconfig.json | 2 +- package-lock.json | 657 ++++++++++++++---- packages/react/src/hooks/useBlockNote.ts | 2 +- 9 files changed, 733 insertions(+), 171 deletions(-) create mode 100644 examples/editor/examples/App.tsx create mode 100644 examples/editor/examples/Two.tsx delete mode 100644 examples/editor/src/App.tsx rename examples/editor/src/{App.module.css => style.css} (50%) diff --git a/examples/editor/examples/App.tsx b/examples/editor/examples/App.tsx new file mode 100644 index 0000000000..6ab357080d --- /dev/null +++ b/examples/editor/examples/App.tsx @@ -0,0 +1,52 @@ +// import logo from './logo.svg' +import "@blocknote/core/style.css"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; + +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; + +// import YPartyKitProvider from "y-partykit/provider"; +// import * as Y from "yjs"; + +// const doc = new Y.Doc(); + +// const provider = new YPartyKitProvider( +// "blocknote-dev.yousefed.partykit.dev", +// // use a unique name as a "room" for your application: +// "your-project-namesss", +// doc +// ); + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +export function App() { + const editor = useBlockNote({ + onEditorContentChange: (editor) => { + console.log(editor.topLevelBlocks); + }, + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + // collaboration: { + // // The Yjs Provider responsible for transporting updates: + // provider, + // // Where to store BlockNote data in the Y.Doc: + // fragment: doc.getXmlFragment("document-storesss"), + // // Information (name and color) for this user: + // user: { + // name: "My Username" + Math.random(), + // color: "#ff0000", + // }, + // }, + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + + // Give tests a way to get prosemirror instance + (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + + return ; +} + +export default App; diff --git a/examples/editor/examples/Two.tsx b/examples/editor/examples/Two.tsx new file mode 100644 index 0000000000..f75af15f2b --- /dev/null +++ b/examples/editor/examples/Two.tsx @@ -0,0 +1,52 @@ +// import logo from './logo.svg' +import "@blocknote/core/style.css"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; + +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; + +// import YPartyKitProvider from "y-partykit/provider"; +// import * as Y from "yjs"; + +// const doc = new Y.Doc(); + +// const provider = new YPartyKitProvider( +// "blocknote-dev.yousefed.partykit.dev", +// // use a unique name as a "room" for your application: +// "your-project-namesss", +// doc +// ); + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +export function Two() { + const editor = useBlockNote({ + onEditorContentChange: (editor) => { + console.log(editor.topLevelBlocks); + }, + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + // collaboration: { + // // The Yjs Provider responsible for transporting updates: + // provider, + // // Where to store BlockNote data in the Y.Doc: + // fragment: doc.getXmlFragment("document-storesss"), + // // Information (name and color) for this user: + // user: { + // name: "My Username" + Math.random(), + // color: "#ff0000", + // }, + // }, + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + + // Give tests a way to get prosemirror instance + (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + + return ; +} + +export default Two; diff --git a/examples/editor/package.json b/examples/editor/package.json index 3ad74335a3..dad6b03ab9 100644 --- a/examples/editor/package.json +++ b/examples/editor/package.json @@ -11,8 +11,12 @@ "dependencies": { "@blocknote/core": "^0.9.6", "@blocknote/react": "^0.9.6", + "@mantine/core": "^5.6.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "y-partykit": "^0.0.0-4c022c1", + "yjs": "^13.6.10" }, "devDependencies": { "@types/react": "^18.0.25", diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx deleted file mode 100644 index 128cdee58a..0000000000 --- a/examples/editor/src/App.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// import logo from './logo.svg' -import "@blocknote/core/style.css"; -import { BlockNoteView, useBlockNote } from "@blocknote/react"; -import styles from "./App.module.css"; -import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; - -type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; - -function App() { - const editor = useBlockNote({ - onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); - }, - domAttributes: { - editor: { - class: styles.editor, - "data-test": "editor", - }, - }, - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, - }); - - // Give tests a way to get prosemirror instance - (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; - - return ; -} - -export default App; diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index f87c123a2c..6497948d32 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,12 +1,99 @@ +import { AppShell, Navbar, ScrollArea } from "@mantine/core"; import React from "react"; import { createRoot } from "react-dom/client"; -import App from "./App"; +import { + Link, + Outlet, + RouterProvider, + createBrowserRouter, +} from "react-router-dom"; +import { App } from "../examples/App"; +import { Two } from "../examples/Two"; +import "./style.css"; window.React = React; +function Root() { + // const linkStyles = (theme) => ({ + // root: { + // // background: "red", + // ...theme.fn.hover({ + // backgroundColor: "#dfdfdd", + // }), + + // "&[data-active]": { + // backgroundColor: "rgba(0, 0, 0, 0.04)", + // }, + // }, + // // "root:hover": { background: "blue" }, + // }); + return ( + + +
    + Simple +
    +
    + Two +
    + {/* manitne } + // rightSection={} + /> + } + // rightSection={} + /> */} +
    + + } + header={<>} + // header={
    + // {/* Header content */} + //
    } + styles={(theme) => ({ + main: { + backgroundColor: "white", + // theme.colorScheme === "dark" + // ? theme.colors.dark[8] + // : theme.colors.gray[0], + }, + })}> + +
    + ); +} +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + path: "simple", + element: , + }, + { + path: "two", + element: , + }, + ], + }, +]); + const root = createRoot(document.getElementById("root")!); root.render( - - - + // TODO: StrictMode is causing duplicate mounts and conflicts with collaboration + // + // + + // ); diff --git a/examples/editor/src/App.module.css b/examples/editor/src/style.css similarity index 50% rename from examples/editor/src/App.module.css rename to examples/editor/src/style.css index 8a90b5cd3f..8918687e58 100644 --- a/examples/editor/src/App.module.css +++ b/examples/editor/src/style.css @@ -2,3 +2,12 @@ margin: 0 calc((100% - 731px) / 2); height: 100%; } + +body { + margin: 0; +} + +.root { + height: 100%; + width: 100%; +} diff --git a/examples/editor/tsconfig.json b/examples/editor/tsconfig.json index 4f17a5d5b9..41460fa792 100644 --- a/examples/editor/tsconfig.json +++ b/examples/editor/tsconfig.json @@ -17,7 +17,7 @@ "jsx": "react-jsx", "composite": true }, - "include": ["src"], + "include": ["src", "examples"], "references": [ { "path": "./tsconfig.node.json" }, { "path": "../../packages/core/" }, diff --git a/package-lock.json b/package-lock.json index eb2008b14a..89b2c135ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,12 @@ "dependencies": { "@blocknote/core": "^0.9.6", "@blocknote/react": "^0.9.6", + "@mantine/core": "^5.6.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "y-partykit": "^0.0.0-4c022c1", + "yjs": "^13.6.10" }, "devDependencies": { "@types/react": "^18.0.25", @@ -603,6 +607,23 @@ } } }, + "examples/playground": { + "version": "0.1.0", + "extraneous": true, + "dependencies": { + "next": "14.0.3", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.0.3", + "typescript": "^5" + } + }, "examples/vanilla": { "name": "@blocknote/example-vanilla", "version": "0.9.6", @@ -6170,6 +6191,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@remix-run/router": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", + "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@resvg/resvg-wasm": { "version": "2.4.1", "license": "MPL-2.0", @@ -6190,9 +6219,10 @@ } }, "node_modules/@rushstack/eslint-patch": { - "version": "1.3.0", - "dev": true, - "license": "MIT" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz", + "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==", + "dev": true }, "node_modules/@shuding/opentype.js": { "version": "1.4.0-beta.0", @@ -7691,14 +7721,15 @@ "license": "MIT" }, "node_modules/array-includes": { - "version": "3.1.6", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -7717,16 +7748,16 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", - "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7736,13 +7767,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.1", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -7753,13 +7785,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -7781,6 +7814,27 @@ "get-intrinsic": "^1.1.3" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arrify": { "version": "1.0.1", "dev": true, @@ -7812,6 +7866,15 @@ "dev": true, "license": "MIT" }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "dev": true, @@ -8225,12 +8288,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9098,6 +9163,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "dev": true, @@ -9107,10 +9186,12 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, - "license": "MIT", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -9367,24 +9448,26 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -9392,19 +9475,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -9432,6 +9519,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "dev": true, @@ -10017,13 +10126,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -10076,27 +10186,26 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz", - "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.findlastindex": "^1.2.2", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", + "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "has": "^1.0.3", - "is-core-module": "^2.12.1", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.6", - "object.groupby": "^1.0.0", - "object.values": "^1.1.6", - "resolve": "^1.22.3", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.14.2" }, @@ -10179,14 +10288,16 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -10196,7 +10307,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -10505,9 +10616,10 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.2.12", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -10771,18 +10883,23 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -10841,14 +10958,15 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11183,6 +11301,7 @@ }, "node_modules/has": { "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -11258,6 +11377,17 @@ "dev": true, "license": "ISC" }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-util-embedded": { "version": "2.0.1", "license": "MIT", @@ -11954,6 +12084,21 @@ "version": "0.2.1", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "dev": true, @@ -12035,10 +12180,11 @@ } }, "node_modules/is-core-module": { - "version": "2.12.1", - "license": "MIT", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12100,6 +12246,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "dev": true, @@ -12108,6 +12266,21 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "dev": true, @@ -12303,15 +12476,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -12416,6 +12586,19 @@ "node": ">=0.12" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jake": { "version": "10.8.7", "dev": true, @@ -12797,8 +12980,9 @@ } }, "node_modules/lib0": { - "version": "0.2.78", - "license": "MIT", + "version": "0.2.88", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.88.tgz", + "integrity": "sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ==", "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -15346,9 +15530,10 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15407,13 +15592,14 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.6", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -15423,14 +15609,14 @@ } }, "node_modules/object.groupby": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", - "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "es-abstract": "^1.21.2", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1" } }, @@ -15467,13 +15653,14 @@ } }, "node_modules/object.values": { - "version": "1.1.6", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -16371,7 +16558,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -16387,7 +16576,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -16805,6 +16993,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", + "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", + "dependencies": { + "@remix-run/router": "1.13.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", + "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", + "dependencies": { + "@remix-run/router": "1.13.0", + "react-router": "6.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-textarea-autosize": { "version": "8.3.4", "license": "MIT", @@ -17146,6 +17364,26 @@ "node": ">=8" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "dev": true, @@ -17175,13 +17413,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -17353,11 +17592,11 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", - "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.12.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -17528,6 +17767,24 @@ "node": ">=6" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "dev": true, @@ -17737,6 +17994,35 @@ "dev": true, "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "dev": true, @@ -18027,13 +18313,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -18043,26 +18330,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18503,6 +18792,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "dev": true, @@ -19339,6 +19679,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-collection": { "version": "1.0.1", "dev": true, @@ -19354,16 +19720,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -19644,6 +20010,18 @@ "node": ">=0.4" } }, + "node_modules/y-partykit": { + "version": "0.0.0-4c022c1", + "resolved": "https://registry.npmjs.org/y-partykit/-/y-partykit-0.0.0-4c022c1.tgz", + "integrity": "sha512-DC4+2SdYjp4TfgcPjZFmHiMrhBkmBgapAN/KQ2ZlnSUYGnFtZZ11+Mkk6bAMCmwRKYKWA0lwVjznd7jpsoQe8g==", + "dependencies": { + "lib0": "^0.2.86", + "lodash.debounce": "^4.0.8", + "react": "^18.2.0", + "y-protocols": "^1.0.6", + "yjs": "^13.6.8" + } + }, "node_modules/y-prosemirror": { "version": "1.0.20", "license": "MIT", @@ -19663,14 +20041,22 @@ } }, "node_modules/y-protocols": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", "dependencies": { - "lib0": "^0.2.42" + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" }, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" } }, "node_modules/y18n": { @@ -19718,10 +20104,11 @@ } }, "node_modules/yjs": { - "version": "13.6.1", - "license": "MIT", + "version": "13.6.10", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.10.tgz", + "integrity": "sha512-1JcyQek1vaMyrDm7Fqfa+pvHg/DURSbVo4VmeN7wjnTKB/lZrfIPhdCj7d8sboK6zLfRBJXegTjc9JlaDd8/Zw==", "dependencies": { - "lib0": "^0.2.74" + "lib0": "^0.2.86" }, "engines": { "node": ">=16.0.0", diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 39a8dc238f..4d63ea5ad2 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -26,7 +26,7 @@ export const useBlockNote = ( deps: DependencyList = [] ): BlockNoteEditor => { const editorRef = useRef>(); - + console.log("USE"); return useMemo(() => { if (editorRef.current) { editorRef.current._tiptapEditor.destroy(); From 91a5c5a2e27da20279e1ab4672243f09bd7c6d49 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 05:20:21 +0100 Subject: [PATCH 19/31] add react examples --- .../editor/examples/ReactInlineContent.tsx | 92 +++++++++++++++++++ examples/editor/examples/ReactStyles.tsx | 84 +++++++++++++++++ examples/editor/src/main.tsx | 16 ++++ packages/react/src/ReactInlineContentSpec.tsx | 41 +++++---- packages/react/src/ReactStyleSpec.tsx | 5 +- packages/react/src/index.ts | 36 ++++---- .../testCases/customReactInlineContent.tsx | 2 +- 7 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 examples/editor/examples/ReactInlineContent.tsx create mode 100644 examples/editor/examples/ReactStyles.tsx diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/ReactInlineContent.tsx new file mode 100644 index 0000000000..0f2a744412 --- /dev/null +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -0,0 +1,92 @@ +// import logo from './logo.svg' +import "@blocknote/core/style.css"; +import { + BlockNoteView, + createReactInlineContentSpec, + useBlockNote, +} from "@blocknote/react"; + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +const mention = createReactInlineContentSpec( + { + type: "mention", + propSchema: { + user: { + default: "", + }, + }, + content: "none", + }, + { + render: (props) => { + return @{props.inlineContent.props.user}; + }, + } +); + +const tag = createReactInlineContentSpec( + { + type: "tag", + propSchema: {}, + content: "styled", + }, + { + render: (props) => { + return ( + + # + + ); + }, + } +); + +export function ReactInlineContent() { + const editor = useBlockNote({ + inlineContentSpecs: { + mention, + tag, + }, + onEditorContentChange: (editor) => { + console.log(editor.topLevelBlocks); + }, + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + initialContent: [ + { + type: "paragraph", + content: [ + "I enjoy working with", + { + type: "mention", + props: { + user: "Matthew", + }, + content: undefined, + } as any, + ], + }, + { + type: "paragraph", + content: [ + "I love ", + { + type: "tag", + // props: {}, + content: "BlockNote", + } as any, + ], + }, + ], + }); + + // Give tests a way to get prosemirror instance + (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + + return ; +} diff --git a/examples/editor/examples/ReactStyles.tsx b/examples/editor/examples/ReactStyles.tsx new file mode 100644 index 0000000000..50bede0640 --- /dev/null +++ b/examples/editor/examples/ReactStyles.tsx @@ -0,0 +1,84 @@ +// import logo from './logo.svg' +import "@blocknote/core/style.css"; +import { + BlockNoteView, + createReactStyleSpec, + useBlockNote, +} from "@blocknote/react"; + +import { defaultStyleSpecs } from "@blocknote/core"; + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +const small = createReactStyleSpec( + { + type: "small", + propSchema: "boolean", + }, + { + render: (props) => { + return ; + }, + } +); + +const fontSize = createReactStyleSpec( + { + type: "fontSize", + propSchema: "string", + }, + { + render: (props) => { + return ( + + ); + }, + } +); + +const customReactStyles = { + ...defaultStyleSpecs, + small, + fontSize, +}; + +export function ReactStyles() { + const editor = useBlockNote({ + styleSpecs: customReactStyles, + onEditorContentChange: (editor) => { + console.log(editor.topLevelBlocks); + }, + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + initialContent: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "large text", + styles: { + fontSize: "30px", + }, + }, + { + type: "text", + text: "small text", + styles: { + small: true, + }, + }, + ], + }, + ], + }); + + // Give tests a way to get prosemirror instance + (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + + return ; +} diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 6497948d32..de442afe12 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -9,6 +9,8 @@ import { } from "react-router-dom"; import { App } from "../examples/App"; +import { ReactInlineContent } from "../examples/ReactInlineContent"; +import { ReactStyles } from "../examples/ReactStyles"; import { Two } from "../examples/Two"; import "./style.css"; window.React = React; @@ -39,6 +41,12 @@ function Root() {
    Two
    +
    + React custom styles +
    +
    + React inline content +
    {/* manitne , }, + { + path: "react-styles", + element: , + }, + { + path: "react-inline-content", + element: , + }, ], }, ]); diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index c672ef86de..21fc3f015e 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -10,6 +10,7 @@ import { import { NodeViewContent, NodeViewProps, + NodeViewWrapper, ReactNodeViewRenderer, } from "@tiptap/react"; // import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; @@ -48,6 +49,7 @@ export function createReactInlineContentSpec< ) { const node = createStronglyTypedTiptapNode({ name: inlineContentConfig.type as T["type"], + inline: true, content: (inlineContentConfig.content === "styled" ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", @@ -87,14 +89,16 @@ export function createReactInlineContentSpec< contentDOM?.setAttribute("data-tmp-find", "true"); const cloneRoot = div.cloneNode(true) as HTMLElement; const dom = cloneRoot.firstElementChild! as HTMLElement; - const contentDOMClone = cloneRoot.querySelector("[data-tmp-find]"); + const contentDOMClone = cloneRoot.querySelector( + "[data-tmp-find]" + ) as HTMLElement | null; contentDOMClone?.removeAttribute("data-tmp-find"); root.unmount(); return { dom, - contentDOM: contentDOMClone, + contentDOM: contentDOMClone || undefined, }; }, @@ -105,26 +109,29 @@ export function createReactInlineContentSpec< return (props) => ReactNodeViewRenderer( (props: NodeViewProps) => { - // TODO - const test = NodeViewContent({}); - debugger; - // const ctx = useReactNodeView(); + // hacky, should export `useReactNodeView` from tiptap to get access to ref + const ref = (NodeViewContent({}) as any).ref; + const Content = inlineContentImplementation.render; return ( - // TODO: fix cast - } - /> + + // TODO: fix cast + } + /> + ); }, { - className: "bn-react-node-view-renderer", + className: "bn-ic-react-node-view-renderer", + as: "span", + // contentDOMElementTag: "span", (requires tt upgrade) } )(props); }, diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index c81b3fc5f6..87e1fb11bd 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -65,14 +65,15 @@ export function createReactStyleSpec( contentDOM?.setAttribute("data-tmp-find", "true"); const cloneRoot = div.cloneNode(true) as HTMLElement; const dom = cloneRoot.firstElementChild! as HTMLElement; - const contentDOMClone = cloneRoot.querySelector("[data-tmp-find]"); + const contentDOMClone = + (cloneRoot.querySelector("[data-tmp-find]") as HTMLElement) || null; contentDOMClone?.removeAttribute("data-tmp-find"); root.unmount(); return { dom, - contentDOM: contentDOMClone, + contentDOM: contentDOMClone || undefined, }; }, }); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0f0c8c6977..10c1595339 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,50 +1,52 @@ // TODO: review directories -export * from "./BlockNoteView"; export * from "./BlockNoteTheme"; +export * from "./BlockNoteView"; export * from "./defaultThemes"; -export * from "./FormattingToolbar/components/FormattingToolbarPositioner"; -export * from "./FormattingToolbar/components/DefaultFormattingToolbar"; -export * from "./FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown"; export * from "./FormattingToolbar/components/DefaultButtons/ColorStyleButton"; export * from "./FormattingToolbar/components/DefaultButtons/CreateLinkButton"; export * from "./FormattingToolbar/components/DefaultButtons/NestBlockButtons"; export * from "./FormattingToolbar/components/DefaultButtons/TextAlignButton"; export * from "./FormattingToolbar/components/DefaultButtons/ToggledStyleButton"; +export * from "./FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown"; +export * from "./FormattingToolbar/components/DefaultFormattingToolbar"; +export * from "./FormattingToolbar/components/FormattingToolbarPositioner"; export * from "./HyperlinkToolbar/components/HyperlinkToolbarPositioner"; -export * from "./SideMenu/components/SideMenuPositioner"; -export * from "./SideMenu/components/SideMenu"; -export * from "./SideMenu/components/SideMenuButton"; -export * from "./SideMenu/components/DefaultSideMenu"; export * from "./SideMenu/components/DefaultButtons/AddBlockButton"; export * from "./SideMenu/components/DefaultButtons/DragHandle"; +export * from "./SideMenu/components/DefaultSideMenu"; +export * from "./SideMenu/components/SideMenu"; +export * from "./SideMenu/components/SideMenuButton"; +export * from "./SideMenu/components/SideMenuPositioner"; -export * from "./SideMenu/components/DragHandleMenu/DragHandleMenu"; -export * from "./SideMenu/components/DragHandleMenu/DragHandleMenuItem"; -export * from "./SideMenu/components/DragHandleMenu/DefaultDragHandleMenu"; export * from "./SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton"; export * from "./SideMenu/components/DragHandleMenu/DefaultButtons/RemoveBlockButton"; +export * from "./SideMenu/components/DragHandleMenu/DefaultDragHandleMenu"; +export * from "./SideMenu/components/DragHandleMenu/DragHandleMenu"; +export * from "./SideMenu/components/DragHandleMenu/DragHandleMenuItem"; -export * from "./SlashMenu/components/SlashMenuPositioner"; -export * from "./SlashMenu/components/SlashMenuItem"; -export * from "./SlashMenu/components/DefaultSlashMenu"; export * from "./SlashMenu/ReactSlashMenuItem"; +export * from "./SlashMenu/components/DefaultSlashMenu"; +export * from "./SlashMenu/components/SlashMenuItem"; +export * from "./SlashMenu/components/SlashMenuPositioner"; export * from "./SlashMenu/defaultReactSlashMenuItems"; -export * from "./ImageToolbar/components/ImageToolbarPositioner"; export * from "./ImageToolbar/components/DefaultImageToolbar"; +export * from "./ImageToolbar/components/ImageToolbarPositioner"; export * from "./SharedComponents/Toolbar/components/Toolbar"; export * from "./SharedComponents/Toolbar/components/ToolbarButton"; export * from "./SharedComponents/Toolbar/components/ToolbarDropdown"; export * from "./hooks/useBlockNote"; -export * from "./hooks/useEditorForceUpdate"; +export * from "./hooks/useEditorChange"; export * from "./hooks/useEditorContentChange"; +export * from "./hooks/useEditorForceUpdate"; export * from "./hooks/useEditorSelectionChange"; -export * from "./hooks/useEditorChange"; export * from "./hooks/useSelectedBlocks"; export * from "./ReactBlockSpec"; +export * from "./ReactInlineContentSpec"; +export * from "./ReactStyleSpec"; diff --git a/packages/react/src/test/testCases/customReactInlineContent.tsx b/packages/react/src/test/testCases/customReactInlineContent.tsx index 76f0dc50e9..f1a4f45072 100644 --- a/packages/react/src/test/testCases/customReactInlineContent.tsx +++ b/packages/react/src/test/testCases/customReactInlineContent.tsx @@ -36,7 +36,7 @@ const tag = createReactInlineContentSpec( render: (props) => { return ( - @ + # ); }, From b1c8934b2cb83cdf3e11ed3c771a77dc0fa8676b Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 05:37:53 +0100 Subject: [PATCH 20/31] fix bug --- .../core/src/extensions/Blocks/api/inlineContent/createSpec.ts | 1 + packages/react/src/ReactInlineContentSpec.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 757002edbc..124923268a 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -47,6 +47,7 @@ export function createInlineContentSpec< const node = Node.create({ name: inlineContentConfig.type, inline: true, + group: "inline", content: inlineContentConfig.content === "styled" ? "inline*" diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 21fc3f015e..eb8c1e82b6 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -50,6 +50,7 @@ export function createReactInlineContentSpec< const node = createStronglyTypedTiptapNode({ name: inlineContentConfig.type as T["type"], inline: true, + group: "inline", content: (inlineContentConfig.content === "styled" ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", From e45c38997a17a285f2cd38fe70916a64ac50a8b7 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 15:30:17 +0100 Subject: [PATCH 21/31] misc fixes --- examples/editor/examples/Basic.tsx | 26 ++++ .../examples/{App.tsx => Collaboration.tsx} | 0 .../editor/examples/ReactInlineContent.tsx | 3 - examples/editor/examples/ReactStyles.tsx | 119 +++++++++++++----- examples/editor/src/main.tsx | 8 +- packages/core/src/BlockNoteEditor.ts | 82 ++++++------ .../src/extensions/Blocks/api/blocks/types.ts | 3 +- .../FormattingToolbarPlugin.ts | 15 +-- .../DefaultDropdowns/BlockTypeDropdown.tsx | 2 +- packages/react/src/ReactInlineContentSpec.tsx | 8 ++ packages/react/src/ReactStyleSpec.tsx | 8 ++ packages/react/src/hooks/useActiveStyles.ts | 22 ++++ packages/react/src/hooks/useBlockNote.ts | 55 ++++---- .../react/src/hooks/useEditorContentChange.ts | 9 +- .../src/hooks/useEditorSelectionChange.ts | 9 +- packages/react/src/index.ts | 1 + 16 files changed, 231 insertions(+), 139 deletions(-) create mode 100644 examples/editor/examples/Basic.tsx rename examples/editor/examples/{App.tsx => Collaboration.tsx} (100%) create mode 100644 packages/react/src/hooks/useActiveStyles.ts diff --git a/examples/editor/examples/Basic.tsx b/examples/editor/examples/Basic.tsx new file mode 100644 index 0000000000..c206c32b96 --- /dev/null +++ b/examples/editor/examples/Basic.tsx @@ -0,0 +1,26 @@ +// import logo from './logo.svg' +import "@blocknote/core/style.css"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; + +import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; + +type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; + +export function App() { + const editor = useBlockNote({ + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + }); + + // Give tests a way to get prosemirror instance + (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; + + return ; +} + +export default App; diff --git a/examples/editor/examples/App.tsx b/examples/editor/examples/Collaboration.tsx similarity index 100% rename from examples/editor/examples/App.tsx rename to examples/editor/examples/Collaboration.tsx diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/ReactInlineContent.tsx index 0f2a744412..d721d3a35f 100644 --- a/examples/editor/examples/ReactInlineContent.tsx +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -48,9 +48,6 @@ export function ReactInlineContent() { mention, tag, }, - onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); - }, domAttributes: { editor: { class: "editor", diff --git a/examples/editor/examples/ReactStyles.tsx b/examples/editor/examples/ReactStyles.tsx index 50bede0640..b60b4c9098 100644 --- a/examples/editor/examples/ReactStyles.tsx +++ b/examples/editor/examples/ReactStyles.tsx @@ -2,11 +2,21 @@ import "@blocknote/core/style.css"; import { BlockNoteView, + FormattingToolbarPositioner, + Toolbar, + ToolbarButton, createReactStyleSpec, + useActiveStyles, useBlockNote, } from "@blocknote/react"; -import { defaultStyleSpecs } from "@blocknote/core"; +import { + BlockNoteEditor, + DefaultBlockSchema, + DefaultInlineContentSchema, + StyleSchemaFromSpecs, + defaultStyleSpecs, +} from "@blocknote/core"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; @@ -42,43 +52,88 @@ const customReactStyles = { fontSize, }; +type MyEditorType = BlockNoteEditor< + DefaultBlockSchema, + DefaultInlineContentSchema, + StyleSchemaFromSpecs +>; + +const CustomFormattingToolbar = (props: { editor: MyEditorType }) => { + const activeStyles = useActiveStyles(props.editor); + + return ( + + { + props.editor.toggleStyles({ + small: true, + }); + }} + isSelected={activeStyles.small}> + Small + + { + props.editor.toggleStyles({ + fontSize: "30px", + }); + }} + isSelected={!!activeStyles.fontSize}> + Font size + + + ); +}; + export function ReactStyles() { - const editor = useBlockNote({ - styleSpecs: customReactStyles, - onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); - }, - domAttributes: { - editor: { - class: "editor", - "data-test": "editor", + const editor = useBlockNote( + { + styleSpecs: customReactStyles, + onEditorContentChange: (editor) => { + console.log(editor.topLevelBlocks); }, - }, - initialContent: [ - { - type: "paragraph", - content: [ - { - type: "text", - text: "large text", - styles: { - fontSize: "30px", + domAttributes: { + editor: { + class: "editor", + "data-test": "editor", + }, + }, + initialContent: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "large text", + styles: { + fontSize: "30px", + }, }, - }, - { - type: "text", - text: "small text", - styles: { - small: true, + { + type: "text", + text: "small text", + styles: { + small: true, + }, }, - }, - ], - }, - ], - }); + ], + }, + ], + }, + [] + ); // Give tests a way to get prosemirror instance (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; - return ; + return ( + + + + ); } diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index de442afe12..171638d511 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -8,7 +8,7 @@ import { createBrowserRouter, } from "react-router-dom"; -import { App } from "../examples/App"; +import { App } from "../examples/Basic"; import { ReactInlineContent } from "../examples/ReactInlineContent"; import { ReactStyles } from "../examples/ReactStyles"; import { Two } from "../examples/Two"; @@ -87,7 +87,7 @@ const router = createBrowserRouter([ children: [ { path: "simple", - element: , + element: , }, { path: "two", @@ -95,11 +95,11 @@ const router = createBrowserRouter([ }, { path: "react-styles", - element: , + element: , }, { path: "react-inline-content", - element: , + element: , }, ], }, diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 198f741e71..4d5ca66530 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -20,6 +20,7 @@ import { BlockIdentifier, BlockNoteDOMAttributes, BlockSchema, + BlockSchemaFromSpecs, BlockSpecs, PartialBlock, } from "./extensions/Blocks/api/blocks/types"; @@ -39,6 +40,7 @@ import { import { Selection } from "./extensions/Blocks/api/selectionTypes"; import { StyleSchema, + StyleSchemaFromSpecs, StyleSpecs, Styles, } from "./extensions/Blocks/api/styles/types"; @@ -48,6 +50,7 @@ import "prosemirror-tables/style/tables.css"; import "./editor.css"; import { InlineContentSchema, + InlineContentSchemaFromSpecs, InlineContentSpecs, } from "./extensions/Blocks/api/inlineContent/types"; import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; @@ -64,14 +67,7 @@ import { UnreachableCaseError, mergeCSSClasses } from "./shared/utils"; export type BlockNoteEditorOptions< BSpecs extends BlockSpecs, ISpecs extends InlineContentSpecs, - SSpecs extends StyleSpecs, - BSchema extends BlockSchema = { - [key in keyof BSpecs]: BSpecs[key]["config"]; - }, - ISchema extends InlineContentSchema = { - [key in keyof ISpecs]: ISpecs[key]["config"]; - }, - SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } + SSpecs extends StyleSpecs > = { // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them. enableBlockNoteExtensions: boolean; @@ -81,7 +77,11 @@ export type BlockNoteEditorOptions< * * @default defaultSlashMenuItems from `./extensions/SlashMenu` */ - slashMenuItems: BaseSlashMenuItem[]; + slashMenuItems: BaseSlashMenuItem< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + >[]; /** * The HTML element that should be used as the parent element for the editor. @@ -98,18 +98,32 @@ export type BlockNoteEditorOptions< /** * A callback function that runs when the editor is ready to be used. */ - onEditorReady: (editor: BlockNoteEditor) => void; + onEditorReady: ( + editor: BlockNoteEditor< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + > + ) => void; /** * A callback function that runs whenever the editor's contents change. */ onEditorContentChange: ( - editor: BlockNoteEditor + editor: BlockNoteEditor< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + > ) => void; /** * A callback function that runs whenever the text cursor position changes. */ onTextCursorPositionChange: ( - editor: BlockNoteEditor + editor: BlockNoteEditor< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + > ) => void; /** * Locks the editor from being editable by the user if set to `false`. @@ -118,7 +132,11 @@ export type BlockNoteEditorOptions< /** * The content that should be in the editor when it's created, represented as an array of partial block objects. */ - initialContent: PartialBlock[]; + initialContent: PartialBlock< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + >[]; /** * Use default BlockNote font and reset the styles of

  • elements etc., that are used in BlockNote. * @@ -183,7 +201,7 @@ export class BlockNoteEditor< SSchema extends StyleSchema = DefaultStyleSchema > { public readonly _tiptapEditor: TiptapEditor & { contentComponent: any }; - public blockCache = new WeakMap>(); + public blockCache = new WeakMap>(); public readonly blockSchema: BSchema; public readonly inlineContentSchema: ISchema; public readonly styleSchema: SSchema; @@ -225,35 +243,17 @@ export class BlockNoteEditor< public static create< BSpecs extends BlockSpecs = typeof defaultBlockSpecs, ISpecs extends InlineContentSpecs = typeof defaultInlineContentSpecs, - SSpecs extends StyleSpecs = typeof defaultStyleSpecs, - BSchema extends BlockSchema = { - [key in keyof BSpecs]: BSpecs[key]["config"]; - }, - ISchema extends InlineContentSchema = { - [key in keyof ISpecs]: ISpecs[key]["config"]; - }, - SSchema extends StyleSchema = { - [key in keyof SSpecs]: SSpecs[key]["config"]; - } - >( - options: Partial< - BlockNoteEditorOptions - > = {} - ) { - return new BlockNoteEditor(options); + SSpecs extends StyleSpecs = typeof defaultStyleSpecs + >(options: Partial> = {}) { + return new BlockNoteEditor(options) as BlockNoteEditor< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + >; } private constructor( - private readonly options: Partial< - BlockNoteEditorOptions< - BlockSpecs, - InlineContentSpecs, - StyleSpecs, - BSchema, - ISchema, - SSchema - > - > + private readonly options: Partial> ) { // apply defaults const newOptions: Omit< @@ -370,7 +370,7 @@ export class BlockNoteEditor< // initial content, as the schema may contain custom blocks which need // it to render. if (initialContent !== undefined) { - this.replaceBlocks(this.topLevelBlocks, initialContent); + this.replaceBlocks(this.topLevelBlocks, initialContent as any); } newOptions.onEditorReady?.(this); diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index 86e9959806..8b74288a74 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -2,7 +2,6 @@ import { Node } from "@tiptap/core"; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; -import { DefaultStyleSchema } from "../defaultBlocks"; import { InlineContent, InlineContentSchema, @@ -207,7 +206,7 @@ type BlocksWithoutChildren< export type Block< BSchema extends BlockSchema, I extends InlineContentSchema, - S extends StyleSchema = DefaultStyleSchema + S extends StyleSchema > = BlocksWithoutChildren[keyof BSchema] & { children: Block[]; }; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 94fb437e54..1af1cc2328 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -29,17 +29,14 @@ export class FormattingToolbarView { state: EditorState; from: number; to: number; - }) => boolean = ({ view, state, from, to }) => { - const { doc, selection } = state; + }) => boolean = ({ state }) => { + const { selection } = state; const { empty } = selection; - // Sometime check for `empty` is not enough. - // Doubleclick an empty paragraph returns a node size of 2. - // So we check also for an empty text size. - const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); - - return !(!view.hasFocus() || empty || isEmptyTextBlock); + if (!isTextSelection(selection)) { + return false; + } + return !empty; }; constructor( diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx index a4c15b31fc..4d15de5be6 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx @@ -20,7 +20,7 @@ export type BlockTypeDropdownItem = { type: string; props?: Record; icon: IconType; - isSelected: (block: Block) => boolean; + isSelected: (block: Block) => boolean; }; export const defaultBlockTypeDropdownItems: BlockTypeDropdownItem[] = [ diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index eb8c1e82b6..8da9be42bc 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -86,6 +86,14 @@ export function createReactInlineContentSpec< ); }); + if (!div.childElementCount) { + // TODO + console.warn("ReactInlineContentSpec: renderHTML() failed"); + return { + dom: document.createElement("span"), + }; + } + // clone so we can unmount the react root contentDOM?.setAttribute("data-tmp-find", "true"); const cloneRoot = div.cloneNode(true) as HTMLElement; diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index 87e1fb11bd..e9baef7503 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -61,6 +61,14 @@ export function createReactStyleSpec( ); }); + if (!div.childElementCount) { + // TODO + console.warn("ReactSdtyleSpec: renderHTML() failed"); + return { + dom: document.createElement("span"), + }; + } + // clone so we can unmount the react root contentDOM?.setAttribute("data-tmp-find", "true"); const cloneRoot = div.cloneNode(true) as HTMLElement; diff --git a/packages/react/src/hooks/useActiveStyles.ts b/packages/react/src/hooks/useActiveStyles.ts new file mode 100644 index 0000000000..93e4ad41ac --- /dev/null +++ b/packages/react/src/hooks/useActiveStyles.ts @@ -0,0 +1,22 @@ +import { BlockNoteEditor, StyleSchema } from "@blocknote/core"; +import { useState } from "react"; +import { useEditorContentChange } from "./useEditorContentChange"; +import { useEditorSelectionChange } from "./useEditorSelectionChange"; + +export function useActiveStyles( + editor: BlockNoteEditor +) { + const [styles, setStyles] = useState(() => editor.getActiveStyles()); + + // Updates state on editor content change. + useEditorContentChange(editor, () => { + setStyles(editor.getActiveStyles()); + }); + + // Updates state on selection change. + useEditorSelectionChange(editor, () => { + setStyles(editor.getActiveStyles()); + }); + + return styles; +} diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 9a1ef8f8b5..d9282a654c 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -1,13 +1,15 @@ import { BlockNoteEditor, BlockNoteEditorOptions, - BlockSchema, + BlockSchemaFromSpecs, BlockSpecs, - InlineContentSchema, + InlineContentSchemaFromSpecs, InlineContentSpecs, - StyleSchema, + StyleSchemaFromSpecs, StyleSpecs, defaultBlockSpecs, + defaultInlineContentSpecs, + defaultStyleSpecs, getBlockSchemaFromSpecs, } from "@blocknote/core"; import { DependencyList, useMemo, useRef } from "react"; @@ -16,22 +18,13 @@ import { getDefaultReactSlashMenuItems } from "../SlashMenu/defaultReactSlashMen const initEditor = < BSpecs extends BlockSpecs, ISpecs extends InlineContentSpecs, - SSpecs extends StyleSpecs, - BSchema extends BlockSchema = { - [key in keyof BSpecs]: BSpecs[key]["config"]; - }, - ISchema extends InlineContentSchema = { - [key in keyof ISpecs]: ISpecs[key]["config"]; - }, - SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } + SSpecs extends StyleSpecs >( options: Partial> ) => BlockNoteEditor.create({ - slashMenuItems: getDefaultReactSlashMenuItems( - getBlockSchemaFromSpecs( - options.blockSpecs || defaultBlockSpecs - ) as BSchema + slashMenuItems: getDefaultReactSlashMenuItems( + getBlockSchemaFromSpecs(options.blockSpecs || defaultBlockSpecs) ) as any, ...options, }); @@ -40,32 +33,28 @@ const initEditor = < * Main hook for importing a BlockNote editor into a React project */ export const useBlockNote = < - BSpecs extends BlockSpecs, - ISpecs extends InlineContentSpecs, - SSpecs extends StyleSpecs, - BSchema extends BlockSchema = { - [key in keyof BSpecs]: BSpecs[key]["config"]; - }, - ISchema extends InlineContentSchema = { - [key in keyof ISpecs]: ISpecs[key]["config"]; - }, - SSchema extends StyleSchema = { [key in keyof SSpecs]: SSpecs[key]["config"] } + BSpecs extends BlockSpecs = typeof defaultBlockSpecs, + ISpecs extends InlineContentSpecs = typeof defaultInlineContentSpecs, + SSpecs extends StyleSpecs = typeof defaultStyleSpecs >( options: Partial> = {}, deps: DependencyList = [] -): BlockNoteEditor => { - const editorRef = useRef>(); +) => { + const editorRef = + useRef< + BlockNoteEditor< + BlockSchemaFromSpecs, + InlineContentSchemaFromSpecs, + StyleSchemaFromSpecs + > + >(); return useMemo(() => { if (editorRef.current) { editorRef.current._tiptapEditor.destroy(); } - editorRef.current = initEditor(options) as BlockNoteEditor< - BSchema, - ISchema, - SSchema - >; - return editorRef.current; + editorRef.current = initEditor(options); + return editorRef.current!; }, deps); //eslint-disable-line react-hooks/exhaustive-deps }; diff --git a/packages/react/src/hooks/useEditorContentChange.ts b/packages/react/src/hooks/useEditorContentChange.ts index 7cb067c6ca..ab98072142 100644 --- a/packages/react/src/hooks/useEditorContentChange.ts +++ b/packages/react/src/hooks/useEditorContentChange.ts @@ -1,13 +1,8 @@ -import { - BlockNoteEditor, - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; +import { BlockNoteEditor } from "@blocknote/core"; import { useEffect } from "react"; export function useEditorContentChange( - editor: BlockNoteEditor, + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/hooks/useEditorSelectionChange.ts b/packages/react/src/hooks/useEditorSelectionChange.ts index 3051970284..000f12b060 100644 --- a/packages/react/src/hooks/useEditorSelectionChange.ts +++ b/packages/react/src/hooks/useEditorSelectionChange.ts @@ -1,13 +1,8 @@ -import { - BlockNoteEditor, - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; +import { BlockNoteEditor } from "@blocknote/core"; import { useEffect } from "react"; export function useEditorSelectionChange( - editor: BlockNoteEditor, + editor: BlockNoteEditor, callback: () => void ) { useEffect(() => { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 10c1595339..2748d2cd74 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -40,6 +40,7 @@ export * from "./SharedComponents/Toolbar/components/Toolbar"; export * from "./SharedComponents/Toolbar/components/ToolbarButton"; export * from "./SharedComponents/Toolbar/components/ToolbarDropdown"; +export * from "./hooks/useActiveStyles"; export * from "./hooks/useBlockNote"; export * from "./hooks/useEditorChange"; export * from "./hooks/useEditorContentChange"; From 6fc78c99ddc37465cbe8fe4b70b0f46b9fbd4b6f Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 15:35:30 +0100 Subject: [PATCH 22/31] wip --- examples/editor/examples/Basic.tsx | 1 - examples/editor/examples/Collaboration.tsx | 44 +++++++--------- .../editor/examples/ReactInlineContent.tsx | 1 - examples/editor/examples/ReactStyles.tsx | 1 - examples/editor/examples/Two.tsx | 52 ------------------- 5 files changed, 20 insertions(+), 79 deletions(-) delete mode 100644 examples/editor/examples/Two.tsx diff --git a/examples/editor/examples/Basic.tsx b/examples/editor/examples/Basic.tsx index c206c32b96..6c3213b0dd 100644 --- a/examples/editor/examples/Basic.tsx +++ b/examples/editor/examples/Basic.tsx @@ -1,4 +1,3 @@ -// import logo from './logo.svg' import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; diff --git a/examples/editor/examples/Collaboration.tsx b/examples/editor/examples/Collaboration.tsx index 6ab357080d..8bec4b84c9 100644 --- a/examples/editor/examples/Collaboration.tsx +++ b/examples/editor/examples/Collaboration.tsx @@ -1,45 +1,41 @@ -// import logo from './logo.svg' import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; -// import YPartyKitProvider from "y-partykit/provider"; -// import * as Y from "yjs"; +import YPartyKitProvider from "y-partykit/provider"; +import * as Y from "yjs"; -// const doc = new Y.Doc(); +const doc = new Y.Doc(); -// const provider = new YPartyKitProvider( -// "blocknote-dev.yousefed.partykit.dev", -// // use a unique name as a "room" for your application: -// "your-project-namesss", -// doc -// ); +const provider = new YPartyKitProvider( + "blocknote-dev.yousefed.partykit.dev", + // use a unique name as a "room" for your application: + "your-project-name", + doc +); type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; export function App() { const editor = useBlockNote({ - onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); - }, domAttributes: { editor: { class: "editor", "data-test": "editor", }, }, - // collaboration: { - // // The Yjs Provider responsible for transporting updates: - // provider, - // // Where to store BlockNote data in the Y.Doc: - // fragment: doc.getXmlFragment("document-storesss"), - // // Information (name and color) for this user: - // user: { - // name: "My Username" + Math.random(), - // color: "#ff0000", - // }, - // }, + collaboration: { + // The Yjs Provider responsible for transporting updates: + provider, + // Where to store BlockNote data in the Y.Doc: + fragment: doc.getXmlFragment("document-storesss"), + // Information (name and color) for this user: + user: { + name: "My Username", + color: "#ff0000", + }, + }, uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, }); diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/ReactInlineContent.tsx index d721d3a35f..6ee154fb3e 100644 --- a/examples/editor/examples/ReactInlineContent.tsx +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -1,4 +1,3 @@ -// import logo from './logo.svg' import "@blocknote/core/style.css"; import { BlockNoteView, diff --git a/examples/editor/examples/ReactStyles.tsx b/examples/editor/examples/ReactStyles.tsx index b60b4c9098..6c82ca2bcf 100644 --- a/examples/editor/examples/ReactStyles.tsx +++ b/examples/editor/examples/ReactStyles.tsx @@ -1,4 +1,3 @@ -// import logo from './logo.svg' import "@blocknote/core/style.css"; import { BlockNoteView, diff --git a/examples/editor/examples/Two.tsx b/examples/editor/examples/Two.tsx deleted file mode 100644 index f75af15f2b..0000000000 --- a/examples/editor/examples/Two.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// import logo from './logo.svg' -import "@blocknote/core/style.css"; -import { BlockNoteView, useBlockNote } from "@blocknote/react"; - -import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; - -// import YPartyKitProvider from "y-partykit/provider"; -// import * as Y from "yjs"; - -// const doc = new Y.Doc(); - -// const provider = new YPartyKitProvider( -// "blocknote-dev.yousefed.partykit.dev", -// // use a unique name as a "room" for your application: -// "your-project-namesss", -// doc -// ); - -type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any }; - -export function Two() { - const editor = useBlockNote({ - onEditorContentChange: (editor) => { - console.log(editor.topLevelBlocks); - }, - domAttributes: { - editor: { - class: "editor", - "data-test": "editor", - }, - }, - // collaboration: { - // // The Yjs Provider responsible for transporting updates: - // provider, - // // Where to store BlockNote data in the Y.Doc: - // fragment: doc.getXmlFragment("document-storesss"), - // // Information (name and color) for this user: - // user: { - // name: "My Username" + Math.random(), - // color: "#ff0000", - // }, - // }, - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, - }); - - // Give tests a way to get prosemirror instance - (window as WindowWithProseMirror).ProseMirror = editor?._tiptapEditor; - - return ; -} - -export default Two; From 0442ddc1c2fce0c429aa863183c73f72bfa6dbef Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 16:25:26 +0100 Subject: [PATCH 23/31] clean --- examples/editor/src/main.tsx | 59 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 171638d511..0d2f29eefe 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -11,10 +11,27 @@ import { import { App } from "../examples/Basic"; import { ReactInlineContent } from "../examples/ReactInlineContent"; import { ReactStyles } from "../examples/ReactStyles"; -import { Two } from "../examples/Two"; import "./style.css"; window.React = React; +const editors = [ + { + title: "Basic", + path: "/simple", + component: App, + }, + { + title: "React custom styles", + path: "/react-styles", + component: ReactStyles, + }, + { + title: "React inline content", + path: "/react-inline-content", + component: ReactInlineContent, + }, +]; + function Root() { // const linkStyles = (theme) => ({ // root: { @@ -35,18 +52,12 @@ function Root() { navbar={ -
    - Simple -
    -
    - Two -
    -
    - React custom styles -
    -
    - React inline content -
    + {editors.map((editor, i) => ( +
    + {editor.title} +
    + ))} + {/* manitne , - children: [ - { - path: "simple", - element: , - }, - { - path: "two", - element: , - }, - { - path: "react-styles", - element: , - }, - { - path: "react-inline-content", - element: , - }, - ], + children: editors.map((editor) => ({ + path: editor.path, + element: , + })), }, ]); From 6862008ca54be8f3dc3dde79d52ea0b85e5319d5 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 23 Nov 2023 22:09:56 +0100 Subject: [PATCH 24/31] small cleanup --- .../editor/examples/ReactInlineContent.tsx | 2 +- packages/core/src/BlockNoteEditor.ts | 23 ++++++------------- .../SlashMenu/defaultSlashMenuItems.ts | 12 ++++------ 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/ReactInlineContent.tsx index 6ee154fb3e..a95f2c1883 100644 --- a/examples/editor/examples/ReactInlineContent.tsx +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -57,7 +57,7 @@ export function ReactInlineContent() { { type: "paragraph", content: [ - "I enjoy working with", + "I enjoy working with ", { type: "mention", props: { diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 4d5ca66530..35cfff9752 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -256,15 +256,7 @@ export class BlockNoteEditor< private readonly options: Partial> ) { // apply defaults - const newOptions: Omit< - typeof options, - "defaultStyles" | "blockSpecs" | "styleSpecs" | "inlineContentSpecs" - > & { - defaultStyles: boolean; - blockSpecs: BlockSpecs; - inlineContentSpecs: InlineContentSpecs; - styleSpecs: StyleSpecs; - } = { + const newOptions = { defaultStyles: true, blockSpecs: options.blockSpecs || defaultBlockSpecs, styleSpecs: options.styleSpecs || defaultStyleSpecs, @@ -273,11 +265,14 @@ export class BlockNoteEditor< ...options, }; - this.blockSchema = getBlockSchemaFromSpecs(newOptions.blockSpecs) as any; + this.blockSchema = getBlockSchemaFromSpecs(newOptions.blockSpecs); this.inlineContentSchema = getInlineContentSchemaFromSpecs( newOptions.inlineContentSpecs - ) as any; - this.styleSchema = getStyleSchemaFromSpecs(newOptions.styleSpecs) as any; + ); + this.styleSchema = getStyleSchemaFromSpecs(newOptions.styleSpecs); + this.blockImplementations = newOptions.blockSpecs; + this.inlineContentImplementations = newOptions.inlineContentSpecs; + this.styleImplementations = newOptions.styleSpecs; this.sideMenu = new SideMenuProsemirrorPlugin(this); this.formattingToolbar = new FormattingToolbarProsemirrorPlugin(this); @@ -319,10 +314,6 @@ export class BlockNoteEditor< }); extensions.push(blockNoteUIExtension); - this.blockImplementations = newOptions.blockSpecs; - this.inlineContentImplementations = newOptions.inlineContentSpecs; - this.styleImplementations = newOptions.styleSpecs; - this.uploadFile = newOptions.uploadFile; const initialContent = diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts index 798bffea20..cc87627e87 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts @@ -70,10 +70,6 @@ export const getDefaultSlashMenuItems = < I extends InlineContentSchema, S extends StyleSchema >( - // This type casting is weird, but it's the best way of doing it, as it allows - // the schema type to be automatically inferred if it is defined, or be - // inferred as any if it is not defined. I don't think it's possible to make it - // infer to DefaultBlockSchema if it is not defined. schema: BSchema ) => { const slashMenuItems: BaseSlashMenuItem[] = []; @@ -126,7 +122,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "bulletListItem", - } as PartialBlock), + }), }); } @@ -137,7 +133,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "numberedListItem", - } as PartialBlock), + }), }); } @@ -148,7 +144,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => insertOrUpdateBlock(editor, { type: "paragraph", - } as PartialBlock), + }), }); } @@ -197,7 +193,7 @@ export const getDefaultSlashMenuItems = < execute: (editor) => { const insertedBlock = insertOrUpdateBlock(editor, { type: "image", - } as PartialBlock); + }); // Immediately open the image toolbar editor._tiptapEditor.view.dispatch( From 3fd377f0ea85ee79682895d76426ae43783bc4cd Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 27 Nov 2023 10:35:36 +0100 Subject: [PATCH 25/31] add comments --- .../src/extensions/Blocks/api/blocks/types.ts | 58 +++++++------------ .../Blocks/api/inlineContent/types.ts | 10 +++- .../src/extensions/Blocks/api/styles/types.ts | 26 +++------ 3 files changed, 37 insertions(+), 57 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index 8b74288a74..c16e723e6b 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -53,7 +53,8 @@ export type Props = { : never; }; -// BlockConfig contains the "schema" info about a Block +// BlockConfig contains the "schema" info about a Block type +// i.e. what props it supports, what content it supports, etc. export type BlockConfig = { type: string; readonly propSchema: PropSchema; @@ -90,8 +91,7 @@ export type TiptapBlockImplementation< }; }; -// Container for both the config and implementation of a block, -// and the type of BlockImplementation is based on that of the config +// A Spec contains both the Config and Implementation export type BlockSpec< T extends BlockConfig, B extends BlockSchema, @@ -114,11 +114,8 @@ type NamesMatch> = Blocks extends { ? Blocks : never; -// Defines multiple block specs. Also ensures that the key of each block schema -// is the same as name of the TipTap node in it. This should be passed in the -// `blocks` option of the BlockNoteEditor. From a block schema, we can derive -// both the blocks' internal implementation (as TipTap nodes) and the type -// information for the external API. +// A Schema contains all the types (Configs) supported in an editor +// The keys are the "type" of a block export type BlockSchema = NamesMatch>; export type BlockSpecs = Record< @@ -152,16 +149,6 @@ export type TableContent< }[]; }; -export type PartialTableContent< - I extends InlineContentSchema, - S extends StyleSchema = StyleSchema -> = { - type: "tableContent"; - rows: { - cells: PartialInlineContent[]; - }[]; -}; - // 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) @@ -220,7 +207,22 @@ export type SpecificBlock< children: Block[]; }; -/** CODE FOR PARTIAL BLOCKS, analogous to above */ +/** 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 PartialTableContent< + I extends InlineContentSchema, + S extends StyleSchema = StyleSchema +> = { + type: "tableContent"; + rows: { + cells: PartialInlineContent[]; + }[]; +}; type PartialBlockFromConfigNoChildren< B extends BlockConfig, @@ -237,8 +239,6 @@ type PartialBlockFromConfigNoChildren< : undefined; }; -// Same as BlockWithoutChildren, but as a partial type with some changes to make -// it easier to create/update blocks in the editor. type PartialBlocksWithoutChildren< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -251,9 +251,6 @@ type PartialBlocksWithoutChildren< >; }; -// Same as Block, but as a partial type with some changes to make it easier to -// create/update blocks in the editor. - export type PartialBlock< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -267,19 +264,6 @@ export type PartialBlock< children: PartialBlock[]; }>; -// export type PartialBlock = -// T extends BlockSchema -// ? PartialBlocksWithoutChildren[keyof T] -// : T extends BlockConfig -// ? PartialBlockFromConfigNoChildren -// : never; - -// & { -// children?: PartialBlock< -// T extends BlockSchema ? T : any // any should probably be BlockSchemaWithBlock; -// >[]; -// }; - export type SpecificPartialBlock< BSchema extends BlockSchema, I extends InlineContentSchema, diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts index dd0c65a2d4..4049850ab5 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts @@ -2,6 +2,8 @@ import { Node } from "@tiptap/core"; import { PropSchema, Props } from "../blocks/types"; import { StyleSchema, Styles } from "../styles/types"; +// InlineContentConfig contains the "schema" info about an InlineContent type +// i.e. what props it supports, what content it supports, etc. export type InlineContentConfig = { type: string; content: "styled" | "none"; // | "plain" @@ -9,18 +11,22 @@ export type InlineContentConfig = { // content: "inline" | "none" | "table"; }; +// InlineContentImplementation contains the "implementation" info about an InlineContent element +// such as the functions / Nodes required to render and / or serialize it // @ts-ignore export type InlineContentImplementation = { node: Node; }; -// Container for both the config and implementation of a block, -// and the type of BlockImplementation is based on that of the config +// Container for both the config and implementation of InlineContent, +// and the type of `implementation` is based on that of the config export type InlineContentSpec = { config: T; implementation: InlineContentImplementation; }; +// A Schema contains all the types (Configs) supported in an editor +// The keys are the "type" of InlineContent elements export type InlineContentSchema = Record; export type InlineContentSpecs = Record< diff --git a/packages/core/src/extensions/Blocks/api/styles/types.ts b/packages/core/src/extensions/Blocks/api/styles/types.ts index 96be7a447a..69caf021c9 100644 --- a/packages/core/src/extensions/Blocks/api/styles/types.ts +++ b/packages/core/src/extensions/Blocks/api/styles/types.ts @@ -2,23 +2,29 @@ import { Mark } from "@tiptap/core"; export type StylePropSchema = "boolean" | "string"; // TODO: use PropSchema as name? Use objects as type similar to blocks? +// StyleConfig contains the "schema" info about a Style type +// i.e. what props it supports, what content it supports, etc. export type StyleConfig = { type: string; readonly propSchema: StylePropSchema; // content: "inline" | "none" | "table"; }; +// StyleImplementation contains the "implementation" info about a Style element. +// Currently, the implementation is always a TipTap Mark export type StyleImplementation = { mark: Mark; }; -// Container for both the config and implementation of a block, -// and the type of BlockImplementation is based on that of the config +// Container for both the config and implementation of a Style, +// and the type of `implementation` is based on that of the config export type StyleSpec = { config: T; implementation: StyleImplementation; }; +// A Schema contains all the types (Configs) supported in an editor +// The keys are the "type" of Styles supported export type StyleSchema = Record; export type StyleSpecs = Record>; @@ -34,19 +40,3 @@ export type Styles = { ? string : never; }; - -// bold?: true; -// italic?: true; -// underline?: true; -// strike?: true; -// code?: true; -// textColor?: string; -// backgroundColor?: string; - -// export type ToggledStyle = { -// [K in keyof Styles]-?: Required[K] extends true ? K : never; -// }[keyof Styles]; - -// export type ColorStyle = { -// [K in keyof Styles]-?: Required[K] extends string ? K : never; -// }[keyof Styles]; From 80c644a17d1649febeb782b1db5de3f9f4938952 Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 27 Nov 2023 10:42:55 +0100 Subject: [PATCH 26/31] move funcs --- packages/core/src/BlockNoteEditor.ts | 6 ++-- .../extensions/Blocks/api/blocks/internal.ts | 8 +++++ .../extensions/Blocks/api/defaultBlocks.ts | 34 +++++-------------- .../Blocks/api/inlineContent/internal.ts | 10 ++++++ .../extensions/Blocks/api/styles/internal.ts | 8 +++++ 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 35cfff9752..e78398bc27 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -33,9 +33,6 @@ import { defaultBlockSpecs, defaultInlineContentSpecs, defaultStyleSpecs, - getBlockSchemaFromSpecs, - getInlineContentSchemaFromSpecs, - getStyleSchemaFromSpecs, } from "./extensions/Blocks/api/defaultBlocks"; import { Selection } from "./extensions/Blocks/api/selectionTypes"; import { @@ -48,11 +45,14 @@ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFro import "prosemirror-tables/style/tables.css"; import "./editor.css"; +import { getBlockSchemaFromSpecs } from "./extensions/Blocks/api/blocks/internal"; +import { getInlineContentSchemaFromSpecs } from "./extensions/Blocks/api/inlineContent/internal"; import { InlineContentSchema, InlineContentSchemaFromSpecs, InlineContentSpecs, } from "./extensions/Blocks/api/inlineContent/types"; +import { getStyleSchemaFromSpecs } from "./extensions/Blocks/api/styles/internal"; import { FormattingToolbarProsemirrorPlugin } from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; import { HyperlinkToolbarProsemirrorPlugin } from "./extensions/HyperlinkToolbar/HyperlinkToolbarPlugin"; import { ImageToolbarProsemirrorPlugin } from "./extensions/ImageToolbar/ImageToolbarPlugin"; diff --git a/packages/core/src/extensions/Blocks/api/blocks/internal.ts b/packages/core/src/extensions/Blocks/api/blocks/internal.ts index cee31137bb..34e39c6ad2 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/internal.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/internal.ts @@ -8,8 +8,10 @@ import { InlineContentSchema } from "../inlineContent/types"; import { StyleSchema } from "../styles/types"; import { BlockConfig, + BlockSchemaFromSpecs, BlockSchemaWithBlock, BlockSpec, + BlockSpecs, PropSchema, Props, SpecificBlock, @@ -284,3 +286,9 @@ export function createBlockSpecFromStronglyTypedTiptapNode< } ); } + +export function getBlockSchemaFromSpecs(specs: T) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as BlockSchemaFromSpecs; +} diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 701f203611..707a5fd244 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -11,13 +11,15 @@ import { BulletListItem } from "../nodes/BlockContent/ListItemBlockContent/Bulle import { NumberedListItem } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; import { Paragraph } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; import { Table } from "../nodes/BlockContent/TableBlockContent/TableBlockContent"; -import { BlockSchemaFromSpecs, BlockSpecs } from "./blocks/types"; +import { getBlockSchemaFromSpecs } from "./blocks/internal"; +import { BlockSpecs } from "./blocks/types"; +import { getInlineContentSchemaFromSpecs } from "./inlineContent/internal"; +import { InlineContentSpecs } from "./inlineContent/types"; import { - InlineContentSchemaFromSpecs, - InlineContentSpecs, -} from "./inlineContent/types"; -import { createStyleSpecFromTipTapMark } from "./styles/internal"; -import { StyleSchemaFromSpecs, StyleSpecs } from "./styles/types"; + createStyleSpecFromTipTapMark, + getStyleSchemaFromSpecs, +} from "./styles/internal"; +import { StyleSpecs } from "./styles/types"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -28,12 +30,6 @@ export const defaultBlockSpecs = { table: Table, } satisfies BlockSpecs; -export function getBlockSchemaFromSpecs(specs: T) { - return Object.fromEntries( - Object.entries(specs).map(([key, value]) => [key, value.config]) - ) as BlockSchemaFromSpecs; -} - export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); export type DefaultBlockSchema = typeof defaultBlockSchema; @@ -48,26 +44,12 @@ export const defaultStyleSpecs = { backgroundColor: BackgroundColor, } satisfies StyleSpecs; -export function getStyleSchemaFromSpecs(specs: T) { - return Object.fromEntries( - Object.entries(specs).map(([key, value]) => [key, value.config]) - ) as StyleSchemaFromSpecs; -} - export const defaultStyleSchema = getStyleSchemaFromSpecs(defaultStyleSpecs); export type DefaultStyleSchema = typeof defaultStyleSchema; export const defaultInlineContentSpecs = {} satisfies InlineContentSpecs; -export function getInlineContentSchemaFromSpecs( - specs: T -) { - return Object.fromEntries( - Object.entries(specs).map(([key, value]) => [key, value.config]) - ) as InlineContentSchemaFromSpecs; -} - export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( defaultInlineContentSpecs ); diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts index a327adb349..9c623c44cf 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts @@ -3,7 +3,9 @@ import { PropSchema } from "../blocks/types"; import { InlineContentConfig, InlineContentImplementation, + InlineContentSchemaFromSpecs, InlineContentSpec, + InlineContentSpecs, } from "./types"; // This helper function helps to instantiate a InlineContentSpec with a @@ -33,3 +35,11 @@ export function createInlineContentSpecFromTipTapNode< } ); } + +export function getInlineContentSchemaFromSpecs( + specs: T +) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as InlineContentSchemaFromSpecs; +} diff --git a/packages/core/src/extensions/Blocks/api/styles/internal.ts b/packages/core/src/extensions/Blocks/api/styles/internal.ts index d843f4777d..648bb133d5 100644 --- a/packages/core/src/extensions/Blocks/api/styles/internal.ts +++ b/packages/core/src/extensions/Blocks/api/styles/internal.ts @@ -3,7 +3,9 @@ import { StyleConfig, StyleImplementation, StylePropSchema, + StyleSchemaFromSpecs, StyleSpec, + StyleSpecs, } from "./types"; // This helper function helps to instantiate a stylespec with a @@ -32,3 +34,9 @@ export function createStyleSpecFromTipTapMark< } ); } + +export function getStyleSchemaFromSpecs(specs: T) { + return Object.fromEntries( + Object.entries(specs).map(([key, value]) => [key, value.config]) + ) as StyleSchemaFromSpecs; +} From 5bb51fd5cab5d2e09e3ada1c2020a08b8baa14ed Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 27 Nov 2023 11:04:02 +0100 Subject: [PATCH 27/31] fix tests --- .../nodeConversion.test.tsx.snap | 63 ------------------- .../__snapshots__/tag/basic/external.html | 2 +- .../__snapshots__/tag/basic/internal.html | 2 +- 3 files changed, 2 insertions(+), 65 deletions(-) diff --git a/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap index c3d786711e..d61a928c5a 100644 --- a/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap +++ b/packages/react/src/test/__snapshots__/nodeConversion.test.tsx.snap @@ -459,66 +459,3 @@ exports[`Test React BlockNote-Prosemirror conversion > Case: custom react style "type": "blockContainer", } `; - -exports[`Test React BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = ` -{ - "attrs": { - "backgroundColor": "default", - "id": "1", - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "content": [ - { - "marks": [ - { - "attrs": { - "stringValue": "18px", - }, - "type": "fontSize", - }, - ], - "text": "This is text with a custom fontSize", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "blockContainer", -} -`; - -exports[`Test React BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = ` -{ - "attrs": { - "backgroundColor": "default", - "id": "1", - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "content": [ - { - "marks": [ - { - "type": "small", - }, - ], - "text": "This is a small text", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "blockContainer", -} -`; diff --git a/packages/react/src/test/__snapshots__/tag/basic/external.html b/packages/react/src/test/__snapshots__/tag/basic/external.html index d5a66eb072..4229ae0a83 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/external.html +++ b/packages/react/src/test/__snapshots__/tag/basic/external.html @@ -1 +1 @@ -

    I love @BlockNote

    \ No newline at end of file +

    I love #BlockNote

    \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/internal.html b/packages/react/src/test/__snapshots__/tag/basic/internal.html index 4b24322ad0..dac5db0ca8 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/internal.html +++ b/packages/react/src/test/__snapshots__/tag/basic/internal.html @@ -1 +1 @@ -

    I love @BlockNote

    \ No newline at end of file +

    I love #BlockNote

    \ No newline at end of file From bdddbd5c83c187d9f7234e4eddd228ac705669d0 Mon Sep 17 00:00:00 2001 From: yousefed Date: Mon, 27 Nov 2023 11:04:26 +0100 Subject: [PATCH 28/31] address PR feedback --- packages/core/src/BlockNoteEditor.ts | 1 + .../ListItemBlockContent/ListItemKeyboardShortcuts.ts | 1 - .../src/extensions/SlashMenu/defaultSlashMenuItems.ts | 3 ++- packages/react/vite.config.ts | 9 --------- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index e78398bc27..1c33385b17 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -789,6 +789,7 @@ export class BlockNoteEditor< for (const mark of marks) { const config = this.styleSchema[mark.type.name]; if (!config) { + console.warn("mark not found in styleschema", mark.type.name); continue; } if (config.propSchema === "boolean") { diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 4ca84b7682..01f7474eab 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -10,7 +10,6 @@ export const handleEnter = (editor: Editor) => { const selectionEmpty = editor.state.selection.anchor === editor.state.selection.head; - debugger; if (!contentType.name.endsWith("ListItem") || !selectionEmpty) { return false; } diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts index cc87627e87..bf817a0458 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.ts @@ -1,5 +1,6 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { Block, BlockSchema, PartialBlock } from "../Blocks/api/blocks/types"; +import { defaultBlockSchema } from "../Blocks/api/defaultBlocks"; import { InlineContentSchema, isStyledTextInlineContent, @@ -70,7 +71,7 @@ export const getDefaultSlashMenuItems = < I extends InlineContentSchema, S extends StyleSchema >( - schema: BSchema + schema: BSchema = defaultBlockSchema as unknown as BSchema ) => { const slashMenuItems: BaseSlashMenuItem[] = []; diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index c98edbfdd3..0557d683ef 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -11,15 +11,6 @@ export default defineConfig((conf) => ({ setupFiles: ["./vitestSetup.ts"], }, plugins: [react()], - resolve: { - alias: - conf.command === "build" - ? ({} as Record) - : ({ - // load live from sources with live reload working - "@blocknote/core": path.resolve(__dirname, "../core/src/"), - } as Record), - }, build: { sourcemap: true, lib: { From 3ef4cc141ceb8cdaa0187411c6be7e1ed583fef2 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 28 Nov 2023 11:05:38 +0100 Subject: [PATCH 29/31] fix inline content types --- .../editor/examples/ReactInlineContent.tsx | 2 + packages/core/src/BlockNoteExtensions.ts | 12 ++-- .../api/nodeConversions/nodeConversions.ts | 29 ++++++--- .../testCases/cases/customInlineContent.ts | 10 ++-- .../extensions/Blocks/api/defaultBlocks.ts | 5 +- .../Blocks/api/inlineContent/createSpec.ts | 3 +- .../Blocks/api/inlineContent/types.ts | 60 +++++++++++++------ packages/react/src/ReactInlineContentSpec.tsx | 5 +- .../react/src/test/htmlConversion.test.tsx | 13 ---- .../testCases/customReactInlineContent.tsx | 2 + 10 files changed, 88 insertions(+), 53 deletions(-) diff --git a/examples/editor/examples/ReactInlineContent.tsx b/examples/editor/examples/ReactInlineContent.tsx index a95f2c1883..07ec3deb13 100644 --- a/examples/editor/examples/ReactInlineContent.tsx +++ b/examples/editor/examples/ReactInlineContent.tsx @@ -1,3 +1,4 @@ +import { defaultInlineContentSpecs } from "@blocknote/core"; import "@blocknote/core/style.css"; import { BlockNoteView, @@ -46,6 +47,7 @@ export function ReactInlineContent() { inlineContentSpecs: { mention, tag, + ...defaultInlineContentSpecs, }, domAttributes: { editor: { diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 7cdc37746e..7458c73754 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -100,11 +100,13 @@ export const getBlockNoteExtensions = < domAttributes: opts.domAttributes, }), TableExtension, - ...Object.values(opts.inlineContentSpecs).map((inlineContentSpec) => { - return inlineContentSpec.implementation.node.configure({ - editor: opts.editor as any, - }); - }), + ...Object.values(opts.inlineContentSpecs) + .filter((a) => a.config !== "link" && a.config !== "text") + .map((inlineContentSpec) => { + return inlineContentSpec.implementation!.node.configure({ + editor: opts.editor as any, + }); + }), ...Object.values(opts.blockSpecs).flatMap((blockSpec) => { return [ diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 04fedd07a8..391fbe1e5d 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -8,11 +8,13 @@ import { TableContent, } from "../../extensions/Blocks/api/blocks/types"; import { + CustomInlineContentConfig, + CustomInlineContentFromConfig, InlineContent, InlineContentFromConfig, InlineContentSchema, + PartialCustomInlineContentFromConfig, PartialInlineContent, - PartialInlineContentFromConfig, PartialLink, StyledText, isLinkInlineContent, @@ -188,7 +190,9 @@ export function tableContentToNodes< } function blockOrInlineContentToContentNode( - block: PartialBlock | PartialInlineContentFromConfig, + block: + | PartialBlock + | PartialCustomInlineContentFromConfig, schema: Schema, styleSchema: StyleSchema ) { @@ -301,8 +305,8 @@ export function contentNodeToInlineContent< I extends InlineContentSchema, S extends StyleSchema >(contentNode: Node, inlineContentSchema: I, styleSchema: S) { - const content: InlineContent[] = []; - let currentContent: InlineContent | undefined = undefined; + const content: InlineContent[] = []; + let currentContent: InlineContent | undefined = undefined; // Most of the logic below is for handling links because in ProseMirror links are marks // while in BlockNote links are a type of inline content @@ -334,7 +338,11 @@ export function contentNodeToInlineContent< return; } - if (inlineContentSchema[node.type.name]) { + if ( + node.type.name !== "link" && + node.type.name !== "text" && + inlineContentSchema[node.type.name] + ) { if (currentContent) { content.push(currentContent); currentContent = undefined; @@ -485,15 +493,20 @@ export function contentNodeToInlineContent< content.push(currentContent); } - return content; + return content as InlineContent[]; } export function nodeToCustomInlineContent< I extends InlineContentSchema, S extends StyleSchema >(node: Node, inlineContentSchema: I, styleSchema: S): InlineContent { + if (node.type.name === "text" || node.type.name === "link") { + throw new Error("unexpected"); + } const props: any = {}; - const icConfig = inlineContentSchema[node.type.name]; + const icConfig = inlineContentSchema[ + node.type.name + ] as CustomInlineContentConfig; for (const [attr, value] of Object.entries(node.attrs)) { if (!icConfig) { throw Error("ic node is of an unrecognized type: " + node.type.name); @@ -506,7 +519,7 @@ export function nodeToCustomInlineContent< } } - let content: InlineContentFromConfig["content"]; + let content: CustomInlineContentFromConfig["content"]; if (icConfig.content === "styled") { content = contentNodeToInlineContent( diff --git a/packages/core/src/api/testCases/cases/customInlineContent.ts b/packages/core/src/api/testCases/cases/customInlineContent.ts index c82c3e2ef3..13e4bb6e2d 100644 --- a/packages/core/src/api/testCases/cases/customInlineContent.ts +++ b/packages/core/src/api/testCases/cases/customInlineContent.ts @@ -4,6 +4,7 @@ import { BlockNoteEditor } from "../../../BlockNoteEditor"; import { DefaultBlockSchema, DefaultStyleSchema, + defaultInlineContentSpecs, } from "../../../extensions/Blocks/api/defaultBlocks"; import { createInlineContentSpec } from "../../../extensions/Blocks/api/inlineContent/createSpec"; import { @@ -14,7 +15,7 @@ import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/node const mention = createInlineContentSpec( { - type: "mention", + type: "mention" as const, propSchema: { user: { default: "", @@ -36,7 +37,7 @@ const mention = createInlineContentSpec( const tag = createInlineContentSpec( { - type: "tag", + type: "tag" as const, propSchema: {}, content: "styled", }, @@ -57,6 +58,7 @@ const tag = createInlineContentSpec( ); const customInlineContent = { + ...defaultInlineContentSpecs, mention, tag, } satisfies InlineContentSpecs; @@ -87,7 +89,7 @@ export const customInlineContentTestCases: EditorTestCases< user: "Matthew", }, content: undefined, - } as any, + }, ], }, ], @@ -103,7 +105,7 @@ export const customInlineContentTestCases: EditorTestCases< type: "tag", // props: {}, content: "BlockNote", - } as any, + }, ], }, ], diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index 707a5fd244..dd15f12f74 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -48,7 +48,10 @@ export const defaultStyleSchema = getStyleSchemaFromSpecs(defaultStyleSpecs); export type DefaultStyleSchema = typeof defaultStyleSchema; -export const defaultInlineContentSpecs = {} satisfies InlineContentSpecs; +export const defaultInlineContentSpecs = { + text: { config: "text", implementation: {} as any }, + link: { config: "link", implementation: {} as any }, +} satisfies InlineContentSpecs; export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( defaultInlineContentSpecs diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 124923268a..7e68f51cbb 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -4,6 +4,7 @@ import { propsToAttributes } from "../blocks/internal"; import { StyleSchema } from "../styles/types"; import { createInlineContentSpecFromTipTapNode } from "./internal"; import { + CustomInlineContentConfig, InlineContentConfig, InlineContentFromConfig, InlineContentSpec, @@ -38,7 +39,7 @@ export type CustomInlineContentImplementation< }; export function createInlineContentSpec< - T extends InlineContentConfig, + T extends CustomInlineContentConfig, S extends StyleSchema >( inlineContentConfig: T, diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts index 4049850ab5..b50622816d 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/types.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/types.ts @@ -2,21 +2,25 @@ import { Node } from "@tiptap/core"; import { PropSchema, Props } from "../blocks/types"; import { StyleSchema, Styles } from "../styles/types"; -// InlineContentConfig contains the "schema" info about an InlineContent type -// i.e. what props it supports, what content it supports, etc. -export type InlineContentConfig = { +export type CustomInlineContentConfig = { type: string; content: "styled" | "none"; // | "plain" readonly propSchema: PropSchema; // content: "inline" | "none" | "table"; }; +// InlineContentConfig contains the "schema" info about an InlineContent type +// i.e. what props it supports, what content it supports, etc. +export type InlineContentConfig = CustomInlineContentConfig | "text" | "link"; // InlineContentImplementation contains the "implementation" info about an InlineContent element // such as the functions / Nodes required to render and / or serialize it // @ts-ignore -export type InlineContentImplementation = { - node: Node; -}; +export type InlineContentImplementation = + T extends "link" | "text" + ? undefined + : { + node: Node; + }; // Container for both the config and implementation of InlineContent, // and the type of `implementation` is based on that of the config @@ -29,17 +33,17 @@ export type InlineContentSpec = { // The keys are the "type" of InlineContent elements export type InlineContentSchema = Record; -export type InlineContentSpecs = Record< - string, - InlineContentSpec ->; +export type InlineContentSpecs = { + text: { config: "text"; implementation: undefined }; + link: { config: "link"; implementation: undefined }; +} & Record>; export type InlineContentSchemaFromSpecs = { [K in keyof T]: T[K]["config"]; }; -export type InlineContentFromConfig< - I extends InlineContentConfig, +export type CustomInlineContentFromConfig< + I extends CustomInlineContentConfig, S extends StyleSchema > = { type: I["type"]; @@ -53,9 +57,20 @@ export type InlineContentFromConfig< : never; }; -export type PartialInlineContentFromConfig< +export type InlineContentFromConfig< I extends InlineContentConfig, S extends StyleSchema +> = I extends "text" + ? StyledText + : I extends "link" + ? Link + : I extends CustomInlineContentConfig + ? CustomInlineContentFromConfig + : never; + +export type PartialCustomInlineContentFromConfig< + I extends CustomInlineContentConfig, + S extends StyleSchema > = { type: I["type"]; props?: Props; @@ -68,6 +83,17 @@ export type PartialInlineContentFromConfig< : never; }; +export type PartialInlineContentFromConfig< + I extends InlineContentConfig, + S extends StyleSchema +> = I extends "text" + ? string | StyledText + : I extends "link" + ? PartialLink + : I extends CustomInlineContentConfig + ? PartialCustomInlineContentFromConfig + : never; + export type StyledText = { type: "text"; text: string; @@ -87,16 +113,12 @@ export type PartialLink = Omit, "content"> & { export type InlineContent< I extends InlineContentSchema, T extends StyleSchema -> = StyledText | Link | InlineContentFromConfig; +> = InlineContentFromConfig; type PartialInlineContentElement< I extends InlineContentSchema, T extends StyleSchema -> = - | string - | StyledText - | PartialLink - | PartialInlineContentFromConfig; +> = PartialInlineContentFromConfig; export type PartialInlineContent< I extends InlineContentSchema, diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 8da9be42bc..99415a1bc9 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -1,6 +1,7 @@ import { createInternalInlineContentSpec, createStronglyTypedTiptapNode, + CustomInlineContentConfig, InlineContentConfig, InlineContentFromConfig, nodeToCustomInlineContent, @@ -40,7 +41,7 @@ export type ReactInlineContentImplementation< // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactInlineContentSpec< - T extends InlineContentConfig, + T extends CustomInlineContentConfig, // I extends InlineContentSchema, S extends StyleSchema >( @@ -148,5 +149,5 @@ export function createReactInlineContentSpec< return createInternalInlineContentSpec(inlineContentConfig, { node: node, - }); + } as any); } diff --git a/packages/react/src/test/htmlConversion.test.tsx b/packages/react/src/test/htmlConversion.test.tsx index 0bf09e5c60..5a2f466e3f 100644 --- a/packages/react/src/test/htmlConversion.test.tsx +++ b/packages/react/src/test/htmlConversion.test.tsx @@ -9,8 +9,6 @@ import { createExternalHTMLExporter, createInternalHTMLSerializer, } from "@blocknote/core"; -import { flushSync } from "react-dom"; -import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; import { customReactInlineContentTestCases } from "./testCases/customReactInlineContent"; @@ -90,14 +88,3 @@ describe("Test React HTML conversion", () => { }); } }); - -it("test react render", () => { - const div = document.createElement("div"); - const root = createRoot(div); - function hello(el: HTMLElement | null) { - console.log("ELEMENT", el?.innerHTML); - } - flushSync(() => { - root.render(
    sdf
    ); - }); -}); diff --git a/packages/react/src/test/testCases/customReactInlineContent.tsx b/packages/react/src/test/testCases/customReactInlineContent.tsx index f1a4f45072..4b6db0e07e 100644 --- a/packages/react/src/test/testCases/customReactInlineContent.tsx +++ b/packages/react/src/test/testCases/customReactInlineContent.tsx @@ -5,6 +5,7 @@ import { EditorTestCases, InlineContentSchemaFromSpecs, InlineContentSpecs, + defaultInlineContentSpecs, uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; import { createReactInlineContentSpec } from "../../ReactInlineContentSpec"; @@ -44,6 +45,7 @@ const tag = createReactInlineContentSpec( ); const customReactInlineContent = { + ...defaultInlineContentSpecs, tag, mention, } satisfies InlineContentSpecs; From 5a74b58a8bcc5a9230167acd8b1d2049173dc14f Mon Sep 17 00:00:00 2001 From: Yousef Date: Wed, 29 Nov 2023 10:54:48 +0100 Subject: [PATCH 30/31] feat: HTML paste handling (#422) * refactor parse * fix parse-divsc * add test case * add extra test (that should be fixed) * readd markdown functions * fix tests * remove unused file * remove comments * add comment * nested list handling * add todos * added comment * use refs for blocks (#424) * use refs for blocks * update react htmlConversion test * Custom inline content and styles commands/copy & paste fixes (#425) * Fixed commands and internal copy/paste for inline content * Fixed internal copy/paste for styles * Small cleanup * fix some tests --------- Co-authored-by: yousefed --------- Co-authored-by: Matthew Lipski <50169049+matthewlipski@users.noreply.github.com> * use processSync --------- Co-authored-by: Matthew Lipski <50169049+matthewlipski@users.noreply.github.com> --- package-lock.json | 160 +++++++++++ packages/core/package.json | 1 + packages/core/src/BlockNoteEditor.ts | 159 ++++++++--- packages/core/src/BlockNoteExtensions.ts | 12 +- .../copyExtension.ts} | 33 +-- .../__snapshots__/complex/misc/external.html | 0 .../__snapshots__/complex/misc/internal.html | 0 .../customParagraph/basic/external.html | 0 .../customParagraph/basic/internal.html | 0 .../customParagraph/nested/external.html | 0 .../customParagraph/nested/internal.html | 0 .../customParagraph/styled/external.html | 0 .../customParagraph/styled/internal.html | 0 .../fontSize/basic/external.html | 1 + .../fontSize/basic/internal.html | 2 +- .../hardbreak/basic/external.html | 0 .../hardbreak/basic/internal.html | 0 .../hardbreak/between-links/external.html | 0 .../hardbreak/between-links/internal.html | 0 .../__snapshots__/hardbreak/end/external.html | 0 .../__snapshots__/hardbreak/end/internal.html | 0 .../hardbreak/link/external.html | 0 .../hardbreak/link/internal.html | 0 .../hardbreak/multiple/external.html | 0 .../hardbreak/multiple/internal.html | 0 .../hardbreak/only/external.html | 0 .../hardbreak/only/internal.html | 0 .../hardbreak/start/external.html | 0 .../hardbreak/start/internal.html | 0 .../hardbreak/styles/external.html | 0 .../hardbreak/styles/internal.html | 0 .../__snapshots__/image/basic/external.html | 0 .../__snapshots__/image/basic/internal.html | 0 .../__snapshots__/image/button/external.html | 0 .../__snapshots__/image/button/internal.html | 0 .../__snapshots__/image/nested/external.html | 0 .../__snapshots__/image/nested/internal.html | 0 .../__snapshots__/link/adjacent/external.html | 0 .../__snapshots__/link/adjacent/internal.html | 0 .../__snapshots__/link/basic/external.html | 0 .../__snapshots__/link/basic/internal.html | 0 .../__snapshots__/link/styled/external.html | 0 .../__snapshots__/link/styled/internal.html | 0 .../__snapshots__/mention/basic/external.html | 1 + .../mention}/basic/internal.html | 2 +- .../paragraph/basic/external.html | 0 .../paragraph/basic/internal.html | 0 .../paragraph/empty/external.html | 0 .../paragraph/empty/internal.html | 0 .../paragraph/nested/external.html | 0 .../paragraph/nested/internal.html | 0 .../paragraph/styled/external.html | 0 .../paragraph/styled/internal.html | 0 .../paste/parse-basic-block-types.json | 140 +++++++++ .../paste/parse-deep-nested-content.json | 240 ++++++++++++++++ .../paste/parse-div-with-inline-content.json | 91 ++++++ .../html/__snapshots__/paste/parse-divs.json | 19 ++ .../paste/parse-fake-image-caption.json | 31 ++ .../paste/parse-mixed-nested-lists.json | 70 +++++ .../parse-nested-lists-with-paragraphs.json | 70 +++++ .../paste/parse-nested-lists.json | 70 +++++ .../simpleCustomParagraph/basic/external.html | 0 .../simpleCustomParagraph/basic/internal.html | 0 .../nested/external.html | 0 .../nested/internal.html | 0 .../styled/external.html | 0 .../styled/internal.html | 0 .../simpleImage/basic/external.html | 0 .../simpleImage/basic/internal.html | 0 .../simpleImage/button/external.html | 0 .../simpleImage/button/internal.html | 0 .../simpleImage/nested/external.html | 0 .../simpleImage/nested/internal.html | 0 .../__snapshots__/small/basic/external.html | 1 + .../__snapshots__/small}/basic/internal.html | 2 +- .../__snapshots__/tag/basic/external.html | 1 + .../__snapshots__/tag}/basic/internal.html | 2 +- .../html/externalHTMLExporter.ts | 4 +- .../html/htmlConversion.test.ts | 18 +- .../html/internalHTMLSerializer.ts | 2 +- .../html/util}/sharedHTMLConversion.ts | 10 +- .../html/util}/simplifyBlocksRehypePlugin.ts | 0 .../formatConversions.test.ts.snap | 0 .../exporters/markdown/markdownExporter.ts | 43 +++ .../markdown}/removeUnderlinesRehypePlugin.ts | 0 .../formatConversions/formatConversions.ts | 140 --------- .../nodeConversions.test.ts.snap | 56 ++-- .../nodeConversions/nodeConversions.test.ts | 12 +- .../core/src/api/nodeConversions/testUtil.ts | 29 ++ .../html/__snapshots__/paste/list-test.json | 105 +++++++ .../paste/parse-basic-block-types.json | 140 +++++++++ .../paste/parse-deep-nested-content.json | 240 ++++++++++++++++ .../paste/parse-div-with-inline-content.json | 91 ++++++ .../html/__snapshots__/paste/parse-divs.json | 121 ++++++++ .../paste/parse-fake-image-caption.json | 31 ++ .../paste/parse-mixed-nested-lists.json | 140 +++++++++ .../parse-nested-lists-with-paragraphs.json | 140 +++++++++ .../paste/parse-nested-lists.json | 157 ++++++++++ .../__snapshots__/paste/parse-two-divs.json | 36 +++ .../src/api/parsers/html/parseHTML.test.ts | 267 ++++++++++++++++++ .../core/src/api/parsers/html/parseHTML.ts | 36 +++ .../__snapshots__/nestedLists.test.ts.snap | 129 +++++++++ .../api/parsers/html/util/nestedLists.test.ts | 176 ++++++++++++ .../src/api/parsers/html/util/nestedLists.ts | 113 ++++++++ .../src/api/parsers/markdown/parseMarkdown.ts | 80 ++++++ .../core/src/api/parsers/pasteExtension.ts | 61 ++++ .../fontSize/basic/external.html | 1 - .../__snapshots__/mention/basic/external.html | 1 - .../__snapshots__/small/basic/external.html | 1 - .../__snapshots__/tag/basic/external.html | 1 - .../testCases/cases/customInlineContent.ts | 2 +- .../src/api/testCases/cases/defaultSchema.ts | 26 +- .../Blocks/api/blocks/createSpec.ts | 63 ++++- .../extensions/Blocks/api/blocks/internal.ts | 60 +--- .../src/extensions/Blocks/api/blocks/types.ts | 12 +- .../Blocks/api/inlineContent/createSpec.ts | 31 +- .../Blocks/api/inlineContent/internal.ts | 35 ++- .../Blocks/api/styles/createSpec.ts | 43 +-- .../extensions/Blocks/api/styles/internal.ts | 49 +++- .../extensions/Blocks/nodes/BlockContainer.ts | 28 +- .../HeadingBlockContent.ts | 22 +- .../ImageBlockContent/ImageBlockContent.ts | 36 ++- .../BulletListItemBlockContent.ts | 8 +- .../NumberedListItemBlockContent.ts | 8 +- .../ParagraphBlockContent.ts | 1 + .../TableBlockContent/TableBlockContent.ts | 18 +- .../src/extensions/SideMenu/SideMenuPlugin.ts | 8 +- .../TextAlignment/TextAlignmentExtension.ts | 4 +- packages/core/src/index.ts | 4 +- packages/react/src/BlockNoteView.tsx | 4 +- packages/react/src/ReactBlockSpec.tsx | 111 +++----- packages/react/src/ReactInlineContentSpec.tsx | 128 +++++---- packages/react/src/ReactRenderUtil.ts | 37 +++ packages/react/src/ReactStyleSpec.tsx | 71 ++--- .../fontSize/basic/external.html | 2 +- .../fontSize/basic/internal.html | 2 +- .../__snapshots__/mention/basic/external.html | 2 +- .../__snapshots__/mention/basic/internal.html | 2 +- .../reactCustomParagraph/basic/internal.html | 2 +- .../reactCustomParagraph/nested/internal.html | 2 +- .../reactCustomParagraph/styled/internal.html | 2 +- .../basic/external.html | 2 +- .../basic/internal.html | 2 +- .../nested/external.html | 2 +- .../nested/internal.html | 2 +- .../styled/external.html | 2 +- .../styled/internal.html | 2 +- .../__snapshots__/small/basic/external.html | 2 +- .../__snapshots__/small/basic/internal.html | 2 +- .../__snapshots__/tag/basic/external.html | 2 +- .../__snapshots__/tag/basic/internal.html | 2 +- .../react/src/test/htmlConversion.test.tsx | 20 +- .../src/test/testCases/customReactBlocks.tsx | 10 +- packages/react/vite.config.ts | 10 + 154 files changed, 3770 insertions(+), 602 deletions(-) rename packages/core/src/api/{serialization/clipboardHandlerExtension.ts => exporters/copyExtension.ts} (72%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/complex/misc/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/complex/misc/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/nested/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/nested/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/styled/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/customParagraph/styled/internal.html (100%) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/fontSize/basic/internal.html (58%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/between-links/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/between-links/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/end/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/end/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/link/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/link/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/multiple/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/multiple/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/only/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/only/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/start/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/start/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/styles/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/hardbreak/styles/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/button/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/button/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/nested/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/image/nested/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/adjacent/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/adjacent/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/styled/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/link/styled/internal.html (100%) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html rename packages/core/src/api/{serialization/html/__snapshots__/tag => exporters/html/__snapshots__/mention}/basic/internal.html (58%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/empty/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/empty/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/nested/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/nested/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/styled/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/paragraph/styled/internal.html (100%) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-basic-block-types.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-deep-nested-content.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-div-with-inline-content.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-divs.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-fake-image-caption.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-mixed-nested-lists.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json create mode 100644 packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists.json rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/nested/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/nested/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/styled/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleCustomParagraph/styled/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/basic/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/basic/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/button/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/button/internal.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/nested/external.html (100%) rename packages/core/src/api/{serialization => exporters}/html/__snapshots__/simpleImage/nested/internal.html (100%) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html rename packages/core/src/api/{serialization/html/__snapshots__/mention => exporters/html/__snapshots__/small}/basic/internal.html (66%) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html rename packages/core/src/api/{serialization/html/__snapshots__/small => exporters/html/__snapshots__/tag}/basic/internal.html (61%) rename packages/core/src/api/{serialization => exporters}/html/externalHTMLExporter.ts (97%) rename packages/core/src/api/{serialization => exporters}/html/htmlConversion.test.ts (93%) rename packages/core/src/api/{serialization => exporters}/html/internalHTMLSerializer.ts (98%) rename packages/core/src/api/{serialization/html => exporters/html/util}/sharedHTMLConversion.ts (89%) rename packages/core/src/api/{formatConversions => exporters/html/util}/simplifyBlocksRehypePlugin.ts (100%) rename packages/core/src/api/{formatConversions => exporters/markdown}/__snapshots__/formatConversions.test.ts.snap (100%) create mode 100644 packages/core/src/api/exporters/markdown/markdownExporter.ts rename packages/core/src/api/{formatConversions => exporters/markdown}/removeUnderlinesRehypePlugin.ts (100%) delete mode 100644 packages/core/src/api/formatConversions/formatConversions.ts create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json create mode 100644 packages/core/src/api/parsers/html/parseHTML.test.ts create mode 100644 packages/core/src/api/parsers/html/parseHTML.ts create mode 100644 packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap create mode 100644 packages/core/src/api/parsers/html/util/nestedLists.test.ts create mode 100644 packages/core/src/api/parsers/html/util/nestedLists.ts create mode 100644 packages/core/src/api/parsers/markdown/parseMarkdown.ts create mode 100644 packages/core/src/api/parsers/pasteExtension.ts delete mode 100644 packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html delete mode 100644 packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html delete mode 100644 packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html delete mode 100644 packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html create mode 100644 packages/react/src/ReactRenderUtil.ts diff --git a/package-lock.json b/package-lock.json index 3520c0102b..a14117e164 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11710,6 +11710,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.0.tgz", + "integrity": "sha512-KlClZ3/Qy5UgvpvVvDomGhnQhNWH5INE8GwvSIQ9CWt1K0zbbXrl7eN5bWaafOZgtmO3jMPwUqmrmEwinhPq1w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "dev": true, @@ -17500,6 +17509,156 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype-format": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.0.tgz", + "integrity": "sha512-kM4II8krCHmUhxrlvzFSptvaWh280Fr7UGNJU5DCMuvmAwGCNmGfi9CvFAQK6JDjsNoRMWQStglK3zKJH685Wg==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "rehype-minify-whitespace": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/@types/hast": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.3.tgz", + "integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/rehype-format/node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, + "node_modules/rehype-format/node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/hast-util-is-body-ok-link": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.0.tgz", + "integrity": "sha512-VFHY5bo2nY8HiV6nir2ynmEB1XkxzuUffhEGeVx7orbu/B1KaGyeGgMZldvMVx5xWrDlLLG/kQ6YkJAMkBEx0w==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/rehype-minify-whitespace": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.0.tgz", + "integrity": "sha512-i9It4YHR0Sf3GsnlR5jFUKXRr9oayvEk9GKQUkwZv6hs70OH9q3OCZrq9PpLvIGKt3W+JxBOxCidNVpH/6rWdA==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-minify-whitespace": { "version": "5.0.1", "license": "MIT", @@ -20215,6 +20374,7 @@ "prosemirror-tables": "^1.3.4", "prosemirror-transform": "^1.7.2", "prosemirror-view": "^1.31.4", + "rehype-format": "^5.0.0", "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", diff --git a/packages/core/package.json b/packages/core/package.json index 4ed2dc62aa..2984e7b9d8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -82,6 +82,7 @@ "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", + "rehype-format":"^5.0.0", "remark-gfm": "^3.0.1", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index ed20941ced..7ba1ba709f 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -1,5 +1,5 @@ import { Editor, EditorOptions, Extension } from "@tiptap/core"; -import { Node } from "prosemirror-model"; +import { Fragment, Node, Slice } from "prosemirror-model"; // import "./blocknote.css"; import { Editor as TiptapEditor } from "@tiptap/core/dist/packages/core/src/Editor"; import * as Y from "yjs"; @@ -24,7 +24,6 @@ import { BlockSpecs, PartialBlock, } from "./extensions/Blocks/api/blocks/types"; -import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; import { DefaultBlockSchema, DefaultInlineContentSchema, @@ -44,8 +43,14 @@ import { import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos"; import "prosemirror-tables/style/tables.css"; + +import { createExternalHTMLExporter } from "./api/exporters/html/externalHTMLExporter"; +import { blocksToMarkdown } from "./api/exporters/markdown/markdownExporter"; +import { HTMLToBlocks } from "./api/parsers/html/parseHTML"; +import { markdownToBlocks } from "./api/parsers/markdown/parseMarkdown"; import "./editor.css"; import { getBlockSchemaFromSpecs } from "./extensions/Blocks/api/blocks/internal"; +import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes"; import { getInlineContentSchemaFromSpecs } from "./extensions/Blocks/api/inlineContent/internal"; import { InlineContentSchema, @@ -409,6 +414,50 @@ export class BlockNoteEditor< newOptions.domAttributes?.editor?.class || "" ), }, + transformPasted(slice, view) { + // helper function + function removeChild(node: Fragment, n: number) { + const children: any[] = []; + node.forEach((child, _, i) => { + if (i !== n) { + children.push(child); + } + }); + return Fragment.from(children); + } + + // fix for https://github.com/ProseMirror/prosemirror/issues/1430#issuecomment-1822570821 + let f = Fragment.from(slice.content); + for (let i = 0; i < f.childCount; i++) { + if (f.child(i).type.spec.group === "blockContent") { + const content = [f.child(i)]; + if (i + 1 < f.childCount) { + // when there is a blockGroup, it should be nested in the new blockcontainer + if (f.child(i + 1).type.spec.group === "blockGroup") { + const nestedChild = f + .child(i + 1) + .child(0) + .child(0); + + if ( + nestedChild.type.name === "bulletListItem" || + nestedChild.type.name === "numberedListItem" + ) { + content.push(f.child(i + 1)); + f = removeChild(f, i + 1); + } + } + } + const container = view.state.schema.nodes.blockContainer.create( + undefined, + content + ); + f = f.replaceChild(i, container); + } + } + + return new Slice(f, slice.openStart, slice.openEnd); + }, }, }; @@ -942,47 +991,71 @@ export class BlockNoteEditor< } // TODO: Fix when implementing HTML/Markdown import & export - // /** - // * Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list - // * items are un-nested in the output HTML. - // * @param blocks An array of blocks that should be serialized into HTML. - // * @returns The blocks, serialized as an HTML string. - // */ - // public async blocksToHTML(blocks: Block[]): Promise { - // return blocksToHTML(blocks, this._tiptapEditor.schema, this); - // } - // - // /** - // * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and - // * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote - // * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text. - // * @param html The HTML string to parse blocks from. - // * @returns The blocks parsed from the HTML string. - // */ - // public async HTMLToBlocks(html: string): Promise[]> { - // return HTMLToBlocks(html, this.schema, this._tiptapEditor.schema); - // } - // - // /** - // * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of - // * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed. - // * @param blocks An array of blocks that should be serialized into Markdown. - // * @returns The blocks, serialized as a Markdown string. - // */ - // public async blocksToMarkdown(blocks: Block[]): Promise { - // return blocksToMarkdown(blocks, this._tiptapEditor.schema, this); - // } - // - // /** - // * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on - // * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it - // * as text. - // * @param markdown The Markdown string to parse blocks from. - // * @returns The blocks parsed from the Markdown string. - // */ - // public async markdownToBlocks(markdown: string): Promise[]> { - // return markdownToBlocks(markdown, this.schema, this._tiptapEditor.schema); - // } + /** + * Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list + * items are un-nested in the output HTML. + * @param blocks An array of blocks that should be serialized into HTML. + * @returns The blocks, serialized as an HTML string. + */ + public async blocksToHTMLLossy( + blocks = this.topLevelBlocks + ): Promise { + const exporter = createExternalHTMLExporter( + this._tiptapEditor.schema, + this + ); + return exporter.exportBlocks(blocks); + } + + /** + * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and + * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote + * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text. + * @param html The HTML string to parse blocks from. + * @returns The blocks parsed from the HTML string. + */ + public async tryParseHTMLToBlocks( + html: string + ): Promise[]> { + return HTMLToBlocks( + html, + this.blockSchema, + this.inlineContentSchema, + this.styleSchema, + this._tiptapEditor.schema + ); + } + + /** + * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of + * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed. + * @param blocks An array of blocks that should be serialized into Markdown. + * @returns The blocks, serialized as a Markdown string. + */ + public async blocksToMarkdownLossy( + blocks = this.topLevelBlocks + ): Promise { + return blocksToMarkdown(blocks, this._tiptapEditor.schema, this); + } + + /** + * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on + * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it + * as text. + * @param markdown The Markdown string to parse blocks from. + * @returns The blocks parsed from the Markdown string. + */ + public async tryParseMarkdownToBlocks( + markdown: string + ): Promise[]> { + return markdownToBlocks( + markdown, + this.blockSchema, + this.inlineContentSchema, + this.styleSchema, + this._tiptapEditor.schema + ); + } /** * Updates the user info for the current user that's shown to other collaborators. diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index 7458c73754..e7c52358fd 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -11,7 +11,8 @@ import { History } from "@tiptap/extension-history"; import { Link } from "@tiptap/extension-link"; import { Text } from "@tiptap/extension-text"; import * as Y from "yjs"; -import { createClipboardHandlerExtension } from "./api/serialization/clipboardHandlerExtension"; +import { createCopyToClipboardExtension } from "./api/exporters/copyExtension"; +import { createPasteFromClipboardExtension } from "./api/parsers/pasteExtension"; import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension"; import { BlockContainer, BlockGroup, Doc } from "./extensions/Blocks"; import { @@ -24,7 +25,6 @@ import { InlineContentSpecs, } from "./extensions/Blocks/api/inlineContent/types"; import { StyleSchema, StyleSpecs } from "./extensions/Blocks/api/styles/types"; -import { TableExtension } from "./extensions/Blocks/nodes/BlockContent/TableBlockContent/TableExtension"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension"; import { TextColorExtension } from "./extensions/TextColor/TextColorExtension"; @@ -99,7 +99,6 @@ export const getBlockNoteExtensions = < BlockGroup.configure({ domAttributes: opts.domAttributes, }), - TableExtension, ...Object.values(opts.inlineContentSpecs) .filter((a) => a.config !== "link" && a.config !== "text") .map((inlineContentSpec) => { @@ -111,8 +110,8 @@ export const getBlockNoteExtensions = < ...Object.values(opts.blockSpecs).flatMap((blockSpec) => { return [ // dependent nodes (e.g.: tablecell / row) - ...(blockSpec.implementation.requiredNodes || []).map((node) => - node.configure({ + ...(blockSpec.implementation.requiredExtensions || []).map((ext) => + ext.configure({ editor: opts.editor, domAttributes: opts.domAttributes, }) @@ -124,7 +123,8 @@ export const getBlockNoteExtensions = < }), ]; }), - createClipboardHandlerExtension(opts.editor), + createCopyToClipboardExtension(opts.editor), + createPasteFromClipboardExtension(opts.editor), Dropcursor.configure({ width: 5, color: "#ddeeff" }), // This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command), diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/exporters/copyExtension.ts similarity index 72% rename from packages/core/src/api/serialization/clipboardHandlerExtension.ts rename to packages/core/src/api/exporters/copyExtension.ts index 5ca2c22f1d..4b580b1f86 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/exporters/copyExtension.ts @@ -5,17 +5,11 @@ import { BlockNoteEditor } from "../../BlockNoteEditor"; import { BlockSchema } from "../../extensions/Blocks/api/blocks/types"; import { InlineContentSchema } from "../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../extensions/Blocks/api/styles/types"; -import { markdown } from "../formatConversions/formatConversions"; import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; +import { cleanHTMLToMarkdown } from "./markdown/markdownExporter"; -const acceptedMIMETypes = [ - "blocknote/html", - "text/html", - "text/plain", -] as const; - -export const createClipboardHandlerExtension = < +export const createCopyToClipboardExtension = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -23,6 +17,7 @@ export const createClipboardHandlerExtension = < editor: BlockNoteEditor ) => Extension.create<{ editor: BlockNoteEditor }, undefined>({ + name: "copyToClipboard", addProseMirrorPlugins() { const tiptap = this.editor; const schema = this.editor.schema; @@ -56,7 +51,7 @@ export const createClipboardHandlerExtension = < selectedFragment ); - const plainText = markdown(externalHTML); + const plainText = cleanHTMLToMarkdown(externalHTML); // TODO: Writing to other MIME types not working in Safari for // some reason. @@ -67,26 +62,6 @@ export const createClipboardHandlerExtension = < // Prevent default PM handler to be called return true; }, - paste(_view, event) { - event.preventDefault(); - - let format: (typeof acceptedMIMETypes)[number] | null = null; - - for (const mimeType of acceptedMIMETypes) { - if (event.clipboardData!.types.includes(mimeType)) { - format = mimeType; - break; - } - } - - if (format !== null) { - editor._tiptapEditor.view.pasteHTML( - event.clipboardData!.getData(format!) - ); - } - - return true; - }, }, }, }), diff --git a/packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html b/packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/complex/misc/external.html rename to packages/core/src/api/exporters/html/__snapshots__/complex/misc/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html b/packages/core/src/api/exporters/html/__snapshots__/complex/misc/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/complex/misc/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/complex/misc/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/external.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/external.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/external.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/internal.html b/packages/core/src/api/exporters/html/__snapshots__/customParagraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/customParagraph/styled/internal.html diff --git a/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html new file mode 100644 index 0000000000..49b9ce6858 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html @@ -0,0 +1 @@ +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html similarity index 58% rename from packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html index 3e2beaedd6..3fe864246c 100644 --- a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html @@ -1 +1 @@ -

    This is text with a custom fontSize

    \ No newline at end of file +

    This is text with a custom fontSize

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/between-links/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/between-links/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/between-links/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/between-links/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/between-links/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/end/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/end/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/end/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/end/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/end/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/link/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/link/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/link/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/link/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/link/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/multiple/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/multiple/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/multiple/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/multiple/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/multiple/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/only/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/only/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/only/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/only/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/only/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/start/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/start/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/start/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/start/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/start/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/styles/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/external.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/styles/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html b/packages/core/src/api/exporters/html/__snapshots__/hardbreak/styles/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/hardbreak/styles/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/hardbreak/styles/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/image/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/image/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/image/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/image/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/button/external.html b/packages/core/src/api/exporters/html/__snapshots__/image/button/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/button/external.html rename to packages/core/src/api/exporters/html/__snapshots__/image/button/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/button/internal.html b/packages/core/src/api/exporters/html/__snapshots__/image/button/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/button/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/image/button/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/image/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/nested/external.html rename to packages/core/src/api/exporters/html/__snapshots__/image/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/image/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/nested/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/image/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html b/packages/core/src/api/exporters/html/__snapshots__/link/adjacent/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/adjacent/external.html rename to packages/core/src/api/exporters/html/__snapshots__/link/adjacent/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html b/packages/core/src/api/exporters/html/__snapshots__/link/adjacent/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/adjacent/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/link/adjacent/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/link/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/link/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/link/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/link/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html b/packages/core/src/api/exporters/html/__snapshots__/link/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/styled/external.html rename to packages/core/src/api/exporters/html/__snapshots__/link/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html b/packages/core/src/api/exporters/html/__snapshots__/link/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/link/styled/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/link/styled/internal.html diff --git a/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html new file mode 100644 index 0000000000..2e6f533ca1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html @@ -0,0 +1 @@ +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html similarity index 58% rename from packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html index dac5db0ca8..6ca7d81c2c 100644 --- a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html @@ -1 +1 @@ -

    I love #BlockNote

    \ No newline at end of file +

    I enjoy working with@Matthew

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/empty/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/external.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/empty/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/empty/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/empty/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/empty/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/external.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/external.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/external.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/internal.html b/packages/core/src/api/exporters/html/__snapshots__/paragraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/paragraph/styled/internal.html diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-basic-block-types.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-basic-block-types.json new file mode 100644 index 0000000000..2d11e081f6 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-basic-block-types.json @@ -0,0 +1,140 @@ +[ + { + "id": "1", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Heading 1", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Heading 2", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 3 + }, + "content": [ + { + "type": "text", + "text": "Heading 3", + "styles": {} + } + ], + "children": [] + }, + { + "id": "4", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "5", + "type": "image", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "url": "exampleURL", + "caption": "Image Caption", + "width": 512 + }, + "children": [] + }, + { + "id": "6", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "None ", + "styles": {} + }, + { + "type": "text", + "text": "Bold ", + "styles": { + "bold": true + } + }, + { + "type": "text", + "text": "Italic ", + "styles": { + "italic": true + } + }, + { + "type": "text", + "text": "Underline ", + "styles": { + "underline": true + } + }, + { + "type": "text", + "text": "Strikethrough ", + "styles": { + "strike": true + } + }, + { + "type": "text", + "text": "All", + "styles": { + "bold": true, + "italic": true, + "underline": true, + "strike": true + } + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-deep-nested-content.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-deep-nested-content.json new file mode 100644 index 0000000000..ae11e36cb7 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-deep-nested-content.json @@ -0,0 +1,240 @@ +[ + { + "id": "1", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Outer 1 Div Before", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " Outer 2 Div Before", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " Outer 3 Div Before", + "styles": {} + } + ], + "children": [] + }, + { + "id": "4", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " Outer 4 Div Before", + "styles": {} + } + ], + "children": [] + }, + { + "id": "5", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Heading 1", + "styles": {} + } + ], + "children": [] + }, + { + "id": "6", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Heading 2", + "styles": {} + } + ], + "children": [] + }, + { + "id": "7", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 3 + }, + "content": [ + { + "type": "text", + "text": "Heading 3", + "styles": {} + } + ], + "children": [] + }, + { + "id": "8", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "9", + "type": "image", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "url": "exampleURL", + "caption": "Image Caption", + "width": 512 + }, + "children": [] + }, + { + "id": "10", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bold", + "styles": { + "bold": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "Italic", + "styles": { + "italic": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "Underline", + "styles": { + "underline": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "Strikethrough", + "styles": { + "strike": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "All", + "styles": { + "bold": true, + "italic": true, + "underline": true, + "strike": true + } + } + ], + "children": [] + }, + { + "id": "11", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " Outer 4 Div After Outer 3 Div After Outer 2 Div After Outer 1 Div After", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-div-with-inline-content.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-div-with-inline-content.json new file mode 100644 index 0000000000..d06969a05f --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-div-with-inline-content.json @@ -0,0 +1,91 @@ +[ + { + "id": "1", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "None ", + "styles": {} + }, + { + "type": "text", + "text": "Bold ", + "styles": { + "bold": true + } + }, + { + "type": "text", + "text": "Italic ", + "styles": { + "italic": true + } + }, + { + "type": "text", + "text": "Underline ", + "styles": { + "underline": true + } + }, + { + "type": "text", + "text": "Strikethrough ", + "styles": { + "strike": true + } + }, + { + "type": "text", + "text": "All", + "styles": { + "bold": true, + "italic": true, + "underline": true, + "strike": true + } + } + ], + "children": [] + }, + { + "id": "2", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Div", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Paragraph", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-divs.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-divs.json new file mode 100644 index 0000000000..33f2f5010b --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-divs.json @@ -0,0 +1,19 @@ +[ + { + "id": "1", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Single Div", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-fake-image-caption.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-fake-image-caption.json new file mode 100644 index 0000000000..86a0cb8168 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-fake-image-caption.json @@ -0,0 +1,31 @@ +[ + { + "id": "1", + "type": "image", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "url": "exampleURL", + "caption": "", + "width": 512 + }, + "children": [] + }, + { + "id": "2", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Image Caption", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-mixed-nested-lists.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-mixed-nested-lists.json new file mode 100644 index 0000000000..1acc524e82 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-mixed-nested-lists.json @@ -0,0 +1,70 @@ +[ + { + "id": "1", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2", + "type": "numberedListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "numberedListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "4", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json new file mode 100644 index 0000000000..6c5bcf5056 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json @@ -0,0 +1,70 @@ +[ + { + "id": "1", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "4", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists.json b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists.json new file mode 100644 index 0000000000..6c5bcf5056 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/paste/parse-nested-lists.json @@ -0,0 +1,70 @@ +[ + { + "id": "1", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Nested Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "4", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleCustomParagraph/styled/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/button/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/button/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/button/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/button/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/external.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/external.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/internal.html b/packages/core/src/api/exporters/html/__snapshots__/simpleImage/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/simpleImage/nested/internal.html diff --git a/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html new file mode 100644 index 0000000000..35c3d5c232 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html @@ -0,0 +1 @@ +

    This is a small text

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html similarity index 66% rename from packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html index 7af6dad9c7..73836f647d 100644 --- a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html @@ -1 +1 @@ -

    I enjoy working with@Matthew

    \ No newline at end of file +

    This is a small text

    \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html new file mode 100644 index 0000000000..b8387e9a55 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html @@ -0,0 +1 @@ +

    I love #BlockNote

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html similarity index 61% rename from packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html rename to packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html index 805c78112e..bac28633b0 100644 --- a/packages/core/src/api/serialization/html/__snapshots__/small/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html @@ -1 +1 @@ -

    This is a small text

    \ No newline at end of file +

    I love #BlockNote

    \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts similarity index 97% rename from packages/core/src/api/serialization/html/externalHTMLExporter.ts rename to packages/core/src/api/exporters/html/externalHTMLExporter.ts index 76021c1333..8d62dd587c 100644 --- a/packages/core/src/api/serialization/html/externalHTMLExporter.ts +++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts @@ -10,12 +10,12 @@ import { } from "../../../extensions/Blocks/api/blocks/types"; import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; -import { simplifyBlocks } from "../../formatConversions/simplifyBlocksRehypePlugin"; import { blockToNode } from "../../nodeConversions/nodeConversions"; import { serializeNodeInner, serializeProseMirrorFragment, -} from "./sharedHTMLConversion"; +} from "./util/sharedHTMLConversion"; +import { simplifyBlocks } from "./util/simplifyBlocksRehypePlugin"; // Used to export BlockNote blocks and ProseMirror nodes to HTML for use outside // the editor. Blocks are exported using the `toExternalHTML` method in their diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts similarity index 93% rename from packages/core/src/api/serialization/html/htmlConversion.test.ts rename to packages/core/src/api/exporters/html/htmlConversion.test.ts index 6c3d95acbd..f6592f1bb7 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../BlockNoteEditor"; +import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../.."; import { createBlockSpec } from "../../../extensions/Blocks/api/blocks/createSpec"; import { BlockSchema, @@ -294,7 +295,7 @@ const editorTestCases: EditorTestCases< ], }; -function convertToHTMLAndCompareSnapshots< +async function convertToHTMLAndCompareSnapshots< B extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -304,6 +305,7 @@ function convertToHTMLAndCompareSnapshots< snapshotDirectory: string, snapshotName: string ) { + addIdsToBlocks(blocks); const serializer = createInternalHTMLSerializer( editor._tiptapEditor.schema, editor @@ -317,6 +319,16 @@ function convertToHTMLAndCompareSnapshots< "/internal.html"; expect(internalHTML).toMatchFileSnapshot(internalHTMLSnapshotPath); + // turn the internalHTML back into blocks, and make sure no data was lost + const fullBlocks = partialBlocksToBlocksForTesting( + editor.blockSchema, + 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._tiptapEditor.schema, editor @@ -356,9 +368,9 @@ describe("Test HTML conversion", () => { for (const document of testCase.documents) { // eslint-disable-next-line no-loop-func - it("Convert " + document.name + " to HTML", () => { + it("Convert " + document.name + " to HTML", async () => { const nameSplit = document.name.split("/"); - convertToHTMLAndCompareSnapshots( + await convertToHTMLAndCompareSnapshots( editor, document.blocks, nameSplit[0], diff --git a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts similarity index 98% rename from packages/core/src/api/serialization/html/internalHTMLSerializer.ts rename to packages/core/src/api/exporters/html/internalHTMLSerializer.ts index bd32aa8950..77785dd0ac 100644 --- a/packages/core/src/api/serialization/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts @@ -10,7 +10,7 @@ import { blockToNode } from "../../nodeConversions/nodeConversions"; import { serializeNodeInner, serializeProseMirrorFragment, -} from "./sharedHTMLConversion"; +} from "./util/sharedHTMLConversion"; // Used to serialize BlockNote blocks and ProseMirror nodes to HTML without // losing data. Blocks are exported using the `toInternalHTML` method in their diff --git a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts b/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts similarity index 89% rename from packages/core/src/api/serialization/html/sharedHTMLConversion.ts rename to packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts index 1f91309287..79413388ad 100644 --- a/packages/core/src/api/serialization/html/sharedHTMLConversion.ts +++ b/packages/core/src/api/exporters/html/util/sharedHTMLConversion.ts @@ -1,10 +1,10 @@ import { DOMSerializer, Fragment, Node } from "prosemirror-model"; -import { BlockNoteEditor } from "../../../BlockNoteEditor"; -import { BlockSchema } from "../../../extensions/Blocks/api/blocks/types"; -import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; -import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; -import { nodeToBlock } from "../../nodeConversions/nodeConversions"; +import { BlockNoteEditor } from "../../../../BlockNoteEditor"; +import { BlockSchema } from "../../../../extensions/Blocks/api/blocks/types"; +import { InlineContentSchema } from "../../../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema } from "../../../../extensions/Blocks/api/styles/types"; +import { nodeToBlock } from "../../../nodeConversions/nodeConversions"; function doc(options: { document?: Document }) { return options.document || window.document; diff --git a/packages/core/src/api/formatConversions/simplifyBlocksRehypePlugin.ts b/packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts similarity index 100% rename from packages/core/src/api/formatConversions/simplifyBlocksRehypePlugin.ts rename to packages/core/src/api/exporters/html/util/simplifyBlocksRehypePlugin.ts diff --git a/packages/core/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap b/packages/core/src/api/exporters/markdown/__snapshots__/formatConversions.test.ts.snap similarity index 100% rename from packages/core/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap rename to packages/core/src/api/exporters/markdown/__snapshots__/formatConversions.test.ts.snap diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts new file mode 100644 index 0000000000..fbe1fdd15c --- /dev/null +++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts @@ -0,0 +1,43 @@ +import { Schema } from "prosemirror-model"; +import rehypeParse from "rehype-parse"; +import rehypeRemark from "rehype-remark"; +import remarkGfm from "remark-gfm"; +import remarkStringify from "remark-stringify"; +import { unified } from "unified"; +import { + Block, + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + StyleSchema, + createExternalHTMLExporter, +} from "../../.."; +import { removeUnderlines } from "./removeUnderlinesRehypePlugin"; + +export function cleanHTMLToMarkdown(cleanHTMLString: string) { + const markdownString = unified() + .use(rehypeParse, { fragment: true }) + .use(removeUnderlines) + .use(rehypeRemark) + .use(remarkGfm) + .use(remarkStringify) + .processSync(cleanHTMLString); + + return markdownString.value as string; +} + +// TODO: add tests +export function blocksToMarkdown< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + blocks: Block[], + schema: Schema, + editor: BlockNoteEditor +): string { + const exporter = createExternalHTMLExporter(schema, editor); + const externalHTML = exporter.exportBlocks(blocks); + + return cleanHTMLToMarkdown(externalHTML); +} diff --git a/packages/core/src/api/formatConversions/removeUnderlinesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/removeUnderlinesRehypePlugin.ts similarity index 100% rename from packages/core/src/api/formatConversions/removeUnderlinesRehypePlugin.ts rename to packages/core/src/api/exporters/markdown/removeUnderlinesRehypePlugin.ts diff --git a/packages/core/src/api/formatConversions/formatConversions.ts b/packages/core/src/api/formatConversions/formatConversions.ts deleted file mode 100644 index 5f82683cd8..0000000000 --- a/packages/core/src/api/formatConversions/formatConversions.ts +++ /dev/null @@ -1,140 +0,0 @@ -import rehypeParse from "rehype-parse"; -import rehypeRemark from "rehype-remark"; -import remarkGfm from "remark-gfm"; -import remarkStringify from "remark-stringify"; -import { unified } from "unified"; - -import { removeUnderlines } from "./removeUnderlinesRehypePlugin"; - -// export async function blocksToHTML( -// blocks: Block[], -// schema: Schema, -// editor: BlockNoteEditor -// ): Promise { -// const htmlParentElement = document.createElement("div"); -// const serializer = createInternalHTMLSerializer(schema, editor); -// -// for (const block of blocks) { -// const node = blockToNode(block, schema); -// const htmlNode = serializer.serializeNode(node); -// htmlParentElement.appendChild(htmlNode); -// } -// -// const htmlString = await unified() -// .use(rehypeParse, { fragment: true }) -// .use(simplifyBlocks, { -// orderedListItemBlockTypes: new Set(["numberedListItem"]), -// unorderedListItemBlockTypes: new Set(["bulletListItem"]), -// }) -// .use(rehypeStringify) -// .process(htmlParentElement.innerHTML); -// -// return htmlString.value as string; -// } -// -// export async function HTMLToBlocks( -// html: string, -// blockSchema: BSchema, -// schema: Schema -// ): Promise[]> { -// const htmlNode = document.createElement("div"); -// htmlNode.innerHTML = html.trim(); -// -// const parser = DOMParser.fromSchema(schema); -// const parentNode = parser.parse(htmlNode); //, { preserveWhitespace: "full" }); -// -// const blocks: Block[] = []; -// -// for (let i = 0; i < parentNode.firstChild!.childCount; i++) { -// blocks.push(nodeToBlock(parentNode.firstChild!.child(i), blockSchema)); -// } -// -// return blocks; -// } -// -// export async function blocksToMarkdown( -// blocks: Block[], -// schema: Schema, -// editor: BlockNoteEditor -// ): Promise { -// const markdownString = await unified() -// .use(rehypeParse, { fragment: true }) -// .use(removeUnderlines) -// .use(rehypeRemark) -// .use(remarkGfm) -// .use(remarkStringify) -// .process(await blocksToHTML(blocks, schema, editor)); -// -// return markdownString.value as string; -// } -// -// // modefied version of https://github.com/syntax-tree/mdast-util-to-hast/blob/main/lib/handlers/code.js -// // that outputs a data-language attribute instead of a CSS class (e.g.: language-typescript) -// function code(state: any, node: any) { -// const value = node.value ? node.value + "\n" : ""; -// /** @type {Properties} */ -// const properties: any = {}; -// -// if (node.lang) { -// // changed line -// properties["data-language"] = node.lang; -// } -// -// // Create ``. -// /** @type {Element} */ -// let result: any = { -// type: "element", -// tagName: "code", -// properties, -// children: [{ type: "text", value }], -// }; -// -// if (node.meta) { -// result.data = { meta: node.meta }; -// } -// -// state.patch(node, result); -// result = state.applyData(node, result); -// -// // Create `
    `.
    -//   result = {
    -//     type: "element",
    -//     tagName: "pre",
    -//     properties: {},
    -//     children: [result],
    -//   };
    -//   state.patch(node, result);
    -//   return result;
    -// }
    -//
    -// export async function markdownToBlocks(
    -//   markdown: string,
    -//   blockSchema: BSchema,
    -//   schema: Schema
    -// ): Promise[]> {
    -//   const htmlString = await unified()
    -//     .use(remarkParse)
    -//     .use(remarkGfm)
    -//     .use(remarkRehype, {
    -//       handlers: {
    -//         ...(defaultHandlers as any),
    -//         code,
    -//       },
    -//     })
    -//     .use(rehypeStringify)
    -//     .process(markdown);
    -//
    -//   return HTMLToBlocks(htmlString.value as string, blockSchema, schema);
    -// }
    -
    -export function markdown(cleanHTMLString: string) {
    -  const markdownString = unified()
    -    .use(rehypeParse, { fragment: true })
    -    .use(removeUnderlines)
    -    .use(rehypeRemark)
    -    .use(remarkGfm)
    -    .use(remarkStringify)
    -    .processSync(cleanHTMLString);
    -
    -  return markdownString.value as string;
    -}
    diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
    index 05bf59965a..41b67fb5ca 100644
    --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
    +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap
    @@ -1,6 +1,6 @@
     // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
     
    -exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = `
    +exports[`Test BlockNote-Prosemirror conversion > Case: custom inline content schema > Convert mention/basic to/from prosemirror 1`] = `
     {
       "attrs": {
         "backgroundColor": "default",
    @@ -14,17 +14,15 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
           },
           "content": [
             {
    -          "marks": [
    -            {
    -              "attrs": {
    -                "stringValue": "18px",
    -              },
    -              "type": "fontSize",
    -            },
    -          ],
    -          "text": "This is text with a custom fontSize",
    +          "text": "I enjoy working with",
               "type": "text",
             },
    +        {
    +          "attrs": {
    +            "user": "Matthew",
    +          },
    +          "type": "mention",
    +        },
           ],
           "type": "paragraph",
         },
    @@ -33,7 +31,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
     }
     `;
     
    -exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert mention/basic to/from prosemirror 1`] = `
    +exports[`Test BlockNote-Prosemirror conversion > Case: custom inline content schema > Convert tag/basic to/from prosemirror 1`] = `
     {
       "attrs": {
         "backgroundColor": "default",
    @@ -47,14 +45,17 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
           },
           "content": [
             {
    -          "text": "I enjoy working with",
    +          "text": "I love ",
               "type": "text",
             },
             {
    -          "attrs": {
    -            "user": "Matthew",
    -          },
    -          "type": "mention",
    +          "content": [
    +            {
    +              "text": "BlockNote",
    +              "type": "text",
    +            },
    +          ],
    +          "type": "tag",
             },
           ],
           "type": "paragraph",
    @@ -64,7 +65,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
     }
     `;
     
    -exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = `
    +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert fontSize/basic to/from prosemirror 1`] = `
     {
       "attrs": {
         "backgroundColor": "default",
    @@ -80,10 +81,13 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
             {
               "marks": [
                 {
    -              "type": "small",
    +              "attrs": {
    +                "stringValue": "18px",
    +              },
    +              "type": "fontSize",
                 },
               ],
    -          "text": "This is a small text",
    +          "text": "This is text with a custom fontSize",
               "type": "text",
             },
           ],
    @@ -94,7 +98,7 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
     }
     `;
     
    -exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert tag/basic to/from prosemirror 1`] = `
    +exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Convert small/basic to/from prosemirror 1`] = `
     {
       "attrs": {
         "backgroundColor": "default",
    @@ -108,17 +112,13 @@ exports[`Test BlockNote-Prosemirror conversion > Case: custom style schema > Con
           },
           "content": [
             {
    -          "text": "I love ",
    -          "type": "text",
    -        },
    -        {
    -          "content": [
    +          "marks": [
                 {
    -              "text": "BlockNote",
    -              "type": "text",
    +              "type": "small",
                 },
               ],
    -          "type": "tag",
    +          "text": "This is a small text",
    +          "type": "text",
             },
           ],
           "type": "paragraph",
    diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts
    index c645c3a2c0..be1d1cfaf2 100644
    --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts
    +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts
    @@ -2,21 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
     
     import { BlockNoteEditor } from "../../BlockNoteEditor";
     import { PartialBlock } from "../../extensions/Blocks/api/blocks/types";
    -import UniqueID from "../../extensions/UniqueID/UniqueID";
     import { customInlineContentTestCases } from "../testCases/cases/customInlineContent";
     import { customStylesTestCases } from "../testCases/cases/customStyles";
     import { defaultSchemaTestCases } from "../testCases/cases/defaultSchema";
     import { blockToNode, nodeToBlock } from "./nodeConversions";
    -import { partialBlockToBlockForTesting } from "./testUtil";
    -
    -function addIdsToBlock(block: PartialBlock) {
    -  if (!block.id) {
    -    block.id = UniqueID.options.generateID();
    -  }
    -  for (const child of block.children || []) {
    -    addIdsToBlock(child);
    -  }
    -}
    +import { addIdsToBlock, partialBlockToBlockForTesting } from "./testUtil";
     
     function validateConversion(
       block: PartialBlock,
    diff --git a/packages/core/src/api/nodeConversions/testUtil.ts b/packages/core/src/api/nodeConversions/testUtil.ts
    index e38638ea02..3398e19d2d 100644
    --- a/packages/core/src/api/nodeConversions/testUtil.ts
    +++ b/packages/core/src/api/nodeConversions/testUtil.ts
    @@ -13,6 +13,7 @@ import {
       isStyledTextInlineContent,
     } from "../../extensions/Blocks/api/inlineContent/types";
     import { StyleSchema } from "../../extensions/Blocks/api/styles/types";
    +import UniqueID from "../../extensions/UniqueID/UniqueID";
     
     function textShorthandToStyledText(
       content: string | StyledText[] = ""
    @@ -62,6 +63,19 @@ function partialContentToInlineContent(
       return content;
     }
     
    +export function partialBlocksToBlocksForTesting<
    +  BSchema extends BlockSchema,
    +  I extends InlineContentSchema,
    +  S extends StyleSchema
    +>(
    +  schema: BSchema,
    +  partialBlocks: Array>
    +): Array> {
    +  return partialBlocks.map((partialBlock) =>
    +    partialBlockToBlockForTesting(schema, partialBlock)
    +  );
    +}
    +
     export function partialBlockToBlockForTesting<
       BSchema extends BlockSchema,
       I extends InlineContentSchema,
    @@ -96,3 +110,18 @@ export function partialBlockToBlockForTesting<
         }),
       } 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/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json b/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
    new file mode 100644
    index 0000000000..7ef10bf491
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
    @@ -0,0 +1,105 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "First",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Second",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "3",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Third",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "4",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Five Parent",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "5",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Child 1",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "6",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Child 2",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json
    new file mode 100644
    index 0000000000..2d11e081f6
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json
    @@ -0,0 +1,140 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 1
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 1",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 2
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 2",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "3",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 3
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 3",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "4",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Paragraph",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "5",
    +    "type": "image",
    +    "props": {
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "url": "exampleURL",
    +      "caption": "Image Caption",
    +      "width": 512
    +    },
    +    "children": []
    +  },
    +  {
    +    "id": "6",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "None ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "Bold ",
    +        "styles": {
    +          "bold": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Italic ",
    +        "styles": {
    +          "italic": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Underline ",
    +        "styles": {
    +          "underline": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Strikethrough ",
    +        "styles": {
    +          "strike": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "All",
    +        "styles": {
    +          "bold": true,
    +          "italic": true,
    +          "underline": true,
    +          "strike": true
    +        }
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json
    new file mode 100644
    index 0000000000..ae11e36cb7
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json
    @@ -0,0 +1,240 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Outer 1 Div Before",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": " Outer 2 Div Before",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "3",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": " Outer 3 Div Before",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "4",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": " Outer 4 Div Before",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "5",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 1
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 1",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "6",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 2
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 2",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "7",
    +    "type": "heading",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "level": 3
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Heading 3",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "8",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Paragraph",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "9",
    +    "type": "image",
    +    "props": {
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "url": "exampleURL",
    +      "caption": "Image Caption",
    +      "width": 512
    +    },
    +    "children": []
    +  },
    +  {
    +    "id": "10",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bold",
    +        "styles": {
    +          "bold": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": " ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "Italic",
    +        "styles": {
    +          "italic": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": " ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "Underline",
    +        "styles": {
    +          "underline": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": " ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "Strikethrough",
    +        "styles": {
    +          "strike": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": " ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "All",
    +        "styles": {
    +          "bold": true,
    +          "italic": true,
    +          "underline": true,
    +          "strike": true
    +        }
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "11",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": " Outer 4 Div After Outer 3 Div After Outer 2 Div After Outer 1 Div After",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json
    new file mode 100644
    index 0000000000..d06969a05f
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json
    @@ -0,0 +1,91 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "None ",
    +        "styles": {}
    +      },
    +      {
    +        "type": "text",
    +        "text": "Bold ",
    +        "styles": {
    +          "bold": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Italic ",
    +        "styles": {
    +          "italic": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Underline ",
    +        "styles": {
    +          "underline": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "Strikethrough ",
    +        "styles": {
    +          "strike": true
    +        }
    +      },
    +      {
    +        "type": "text",
    +        "text": "All",
    +        "styles": {
    +          "bold": true,
    +          "italic": true,
    +          "underline": true,
    +          "strike": true
    +        }
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "3",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Paragraph",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json
    new file mode 100644
    index 0000000000..764afd66ac
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json
    @@ -0,0 +1,121 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Single Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": " Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "3",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "4",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "5",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Single Div 2",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "6",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "7",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Nested Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json
    new file mode 100644
    index 0000000000..86a0cb8168
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json
    @@ -0,0 +1,31 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "image",
    +    "props": {
    +      "backgroundColor": "default",
    +      "textAlignment": "left",
    +      "url": "exampleURL",
    +      "caption": "",
    +      "width": 512
    +    },
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Image Caption",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
    new file mode 100644
    index 0000000000..7bb12cd2cb
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
    @@ -0,0 +1,140 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "2",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "3",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "4",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "5",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "6",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "7",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "8",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
    new file mode 100644
    index 0000000000..cc6065d2d4
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
    @@ -0,0 +1,140 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "2",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "3",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "4",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "5",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "6",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "7",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "8",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
    new file mode 100644
    index 0000000000..e20435c9c8
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
    @@ -0,0 +1,157 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "3",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "4",
    +        "type": "bulletListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Bullet List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "5",
    +    "type": "bulletListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Bullet List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "6",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": [
    +      {
    +        "id": "7",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      },
    +      {
    +        "id": "8",
    +        "type": "numberedListItem",
    +        "props": {
    +          "textColor": "default",
    +          "backgroundColor": "default",
    +          "textAlignment": "left"
    +        },
    +        "content": [
    +          {
    +            "type": "text",
    +            "text": "Nested Numbered List Item",
    +            "styles": {}
    +          }
    +        ],
    +        "children": []
    +      }
    +    ]
    +  },
    +  {
    +    "id": "9",
    +    "type": "numberedListItem",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Numbered List Item",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json
    new file mode 100644
    index 0000000000..aa21de34f0
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json
    @@ -0,0 +1,36 @@
    +[
    +  {
    +    "id": "1",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "Single Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  },
    +  {
    +    "id": "2",
    +    "type": "paragraph",
    +    "props": {
    +      "textColor": "default",
    +      "backgroundColor": "default",
    +      "textAlignment": "left"
    +    },
    +    "content": [
    +      {
    +        "type": "text",
    +        "text": "second Div",
    +        "styles": {}
    +      }
    +    ],
    +    "children": []
    +  }
    +]
    \ No newline at end of file
    diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts
    new file mode 100644
    index 0000000000..5bd8238e3f
    --- /dev/null
    +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts
    @@ -0,0 +1,267 @@
    +import { describe, expect, it } from "vitest";
    +import { BlockNoteEditor } from "../../..";
    +import { nestedListsToBlockNoteStructure } from "./util/nestedLists";
    +
    +async function parseHTMLAndCompareSnapshots(
    +  html: string,
    +  snapshotName: string
    +) {
    +  const view: any = await import("prosemirror-view");
    +
    +  const editor = BlockNoteEditor.create();
    +  const blocks = await editor.tryParseHTMLToBlocks(html);
    +
    +  const snapshotPath = "./__snapshots__/paste/" + snapshotName + ".json";
    +  expect(JSON.stringify(blocks, undefined, 2)).toMatchFileSnapshot(
    +    snapshotPath
    +  );
    +
    +  // Now, we also want to test actually pasting in the editor, and not just calling
    +  // tryParseHTMLToBlocks directly.
    +  // The reason is that the prosemirror logic for pasting can be a bit different, because
    +  // it's related to the context of where the user is pasting exactly (selection)
    +  //
    +  // The internal difference come that in tryParseHTMLToBlocks, we use DOMParser.parse,
    +  // while when pasting, Prosemirror uses DOMParser.parseSlice, and then tries to fit the
    +  // slice in the document. This fitting might change the structure / interpretation of the pasted blocks
    +
    +  // Simulate a paste event (this uses DOMParser.parseSlice internally)
    +
    +  (window as any).__TEST_OPTIONS.mockID = 0; // reset id counter
    +  const htmlNode = nestedListsToBlockNoteStructure(html);
    +  const tt = editor._tiptapEditor;
    +
    +  const slice = view.__parseFromClipboard(
    +    tt.view,
    +    "",
    +    htmlNode.innerHTML,
    +    false,
    +    tt.view.state.selection.$from
    +  );
    +  tt.view.dispatch(tt.view.state.tr.replaceSelection(slice));
    +
    +  // alternative paste simulation doesn't work in a non-browser vitest env
    +  //   editor._tiptapEditor.view.pasteHTML(html, {
    +  //     preventDefault: () => {
    +  //       // noop
    +  //     },
    +  //     clipboardData: {
    +  //       types: ["text/html"],
    +  //       getData: () => html,
    +  //     },
    +  //   } as any);
    +
    +  const pastedBlocks = editor.topLevelBlocks;
    +  pastedBlocks.pop(); // trailing paragraph
    +  expect(pastedBlocks).toStrictEqual(blocks);
    +}
    +
    +describe("Parse HTML", () => {
    +  it("Parse basic block types", async () => {
    +    const html = `

    Heading 1

    +

    Heading 2

    +

    Heading 3

    +

    Paragraph

    +
    Image Caption
    +

    None Bold Italic Underline Strikethrough All

    `; + + await parseHTMLAndCompareSnapshots(html, "parse-basic-block-types"); + }); + + it("list test", async () => { + const html = `
      +
    • First
    • +
    • Second
    • +
    • Third
    • +
    • Five Parent +
        +
      • Child 1
      • +
      • Child 2
      • +
      +
    • +
    `; + await parseHTMLAndCompareSnapshots(html, "list-test"); + }); + + it("Parse nested lists", async () => { + const html = `
      +
    • Bullet List Item
    • +
    • Bullet List Item
    • +
        +
      • + Nested Bullet List Item +
      • +
      • + Nested Bullet List Item +
      • +
      +
    • + Bullet List Item +
    • +
    +
      +
    1. + Numbered List Item +
        +
      1. + Nested Numbered List Item +
      2. +
      3. + Nested Numbered List Item +
      4. +
      +
    2. +
    3. + Numbered List Item +
    4. +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-nested-lists"); + }); + + it("Parse nested lists with paragraphs", async () => { + const html = `
      +
    • +

      Bullet List Item

      +
        +
      • +

        Nested Bullet List Item

        +
      • +
      • +

        Nested Bullet List Item

        +
      • +
      +
    • +
    • +

      Bullet List Item

      +
    • +
    +
      +
    1. +

      Numbered List Item

      +
        +
      1. +

        Nested Numbered List Item

        +
      2. +
      3. +

        Nested Numbered List Item

        +
      4. +
      +
    2. +
    3. +

      Numbered List Item

      +
    4. +
    `; + + await parseHTMLAndCompareSnapshots( + html, + "parse-nested-lists-with-paragraphs" + ); + }); + + it("Parse mixed nested lists", async () => { + const html = `
      +
    • + Bullet List Item +
        +
      1. + Nested Numbered List Item +
      2. +
      3. + Nested Numbered List Item +
      4. +
      +
    • +
    • + Bullet List Item +
    • +
    +
      +
    1. + Numbered List Item +
        +
      • +

        Nested Bullet List Item

        +
      • +
      • +

        Nested Bullet List Item

        +
      • +
      +
    2. +
    3. + Numbered List Item +
    4. +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-mixed-nested-lists"); + }); + + it("Parse divs", async () => { + const html = `
    Single Div
    +
    + Div +
    Nested Div
    +
    Nested Div
    +
    +
    Single Div 2
    +
    +
    Nested Div
    +
    Nested Div
    +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-divs"); + }); + + it("Parse two divs", async () => { + const html = `
    Single Div
    second Div
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-two-divs"); + }); + + it("Parse fake image caption", async () => { + const html = `
    + +

    Image Caption

    +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-fake-image-caption"); + }); + + // TODO: this one fails + it.skip("Parse deep nested content", async () => { + const html = `
    + Outer 1 Div Before +
    + Outer 2 Div Before +
    + Outer 3 Div Before +
    + Outer 4 Div Before +

    Heading 1

    +

    Heading 2

    +

    Heading 3

    +

    Paragraph

    +
    Image Caption
    +

    Bold Italic Underline Strikethrough All

    + Outer 4 Div After +
    + Outer 3 Div After +
    + Outer 2 Div After +
    + Outer 1 Div After +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-deep-nested-content"); + }); + + it("Parse div with inline content and nested blocks", async () => { + const html = `
    + None Bold Italic Underline Strikethrough All +
    Nested Div
    +

    Nested Paragraph

    +
    `; + + await parseHTMLAndCompareSnapshots(html, "parse-div-with-inline-content"); + }); +}); diff --git a/packages/core/src/api/parsers/html/parseHTML.ts b/packages/core/src/api/parsers/html/parseHTML.ts new file mode 100644 index 0000000000..cf4e983248 --- /dev/null +++ b/packages/core/src/api/parsers/html/parseHTML.ts @@ -0,0 +1,36 @@ +import { DOMParser, Schema } from "prosemirror-model"; +import { Block, BlockSchema, nodeToBlock } from "../../.."; +import { InlineContentSchema } from "../../../extensions/Blocks/api/inlineContent/types"; +import { StyleSchema } from "../../../extensions/Blocks/api/styles/types"; +import { nestedListsToBlockNoteStructure } from "./util/nestedLists"; + +export async function HTMLToBlocks< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + html: string, + blockSchema: BSchema, + icSchema: I, + styleSchema: S, + pmSchema: Schema +): Promise[]> { + const htmlNode = nestedListsToBlockNoteStructure(html); + const parser = DOMParser.fromSchema(pmSchema); + + // const doc = pmSchema.nodes["doc"].createAndFill()!; + + const parentNode = parser.parse(htmlNode, { + topNode: pmSchema.nodes["blockGroup"].create(), + // context: doc.resolve(3), + }); //, { preserveWhitespace: "full" }); + const blocks: Block[] = []; + + for (let i = 0; i < parentNode.childCount; i++) { + blocks.push( + nodeToBlock(parentNode.child(i), blockSchema, icSchema, styleSchema) + ); + } + + return blocks; +} diff --git a/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap b/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap new file mode 100644 index 0000000000..d697b8db72 --- /dev/null +++ b/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap @@ -0,0 +1,129 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Lift nested lists > Lifts multiple bullet lists 1`] = ` +" +
      +
      +
    • Bullet List Item 1
    • +
      +
        +
      • Nested Bullet List Item 1
      • +
      • Nested Bullet List Item 2
      • +
      +
        +
      • Nested Bullet List Item 3
      • +
      • Nested Bullet List Item 4
      • +
      +
      +
      +
    • Bullet List Item 2
    • +
    +" +`; + +exports[`Lift nested lists > Lifts multiple bullet lists with content in between 1`] = ` +" +
      +
      +
    • Bullet List Item 1
    • +
      +
        +
      • Nested Bullet List Item 1
      • +
      • Nested Bullet List Item 2
      • +
      +
      +
      +
      +
    • In between content
    • +
      +
        +
      • Nested Bullet List Item 3
      • +
      • Nested Bullet List Item 4
      • +
      +
      +
      +
    • Bullet List Item 2
    • +
    +" +`; + +exports[`Lift nested lists > Lifts nested bullet lists 1`] = ` +" +
      +
      +
    • Bullet List Item 1
    • +
      +
        +
      • Nested Bullet List Item 1
      • +
      • Nested Bullet List Item 2
      • +
      +
      +
      +
    • Bullet List Item 2
    • +
    +" +`; + +exports[`Lift nested lists > Lifts nested bullet lists with content after nested list 1`] = ` +" +
      +
      +
    • Bullet List Item 1
    • +
      +
        +
      • Nested Bullet List Item 1
      • +
      • Nested Bullet List Item 2
      • +
      +
      +
      +
    • More content in list item 1
    • +
    • Bullet List Item 2
    • +
    +" +`; + +exports[`Lift nested lists > Lifts nested bullet lists without li 1`] = ` +" +
      Bullet List Item 1 +
        +
      • Nested Bullet List Item 1
      • +
      • Nested Bullet List Item 2
      • +
      +
    • Bullet List Item 2
    • +
    +" +`; + +exports[`Lift nested lists > Lifts nested mixed lists 1`] = ` +" +
      +
      +
    1. Numbered List Item 1
    2. +
      +
        +
      • Bullet List Item 1
      • +
      • Bullet List Item 2
      • +
      +
      +
      +
    3. Numbered List Item 2
    4. +
    +" +`; + +exports[`Lift nested lists > Lifts nested numbered lists 1`] = ` +" +
      +
      +
    1. Numbered List Item 1
    2. +
      +
        +
      1. Nested Numbered List Item 1
      2. +
      3. Nested Numbered List Item 2
      4. +
      +
      +
      +
    3. Numbered List Item 2
    4. +
    +" +`; diff --git a/packages/core/src/api/parsers/html/util/nestedLists.test.ts b/packages/core/src/api/parsers/html/util/nestedLists.test.ts new file mode 100644 index 0000000000..96b0e1e9d2 --- /dev/null +++ b/packages/core/src/api/parsers/html/util/nestedLists.test.ts @@ -0,0 +1,176 @@ +import rehypeFormat from "rehype-format"; +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { unified } from "unified"; +import { describe, expect, it } from "vitest"; +import { nestedListsToBlockNoteStructure } from "./nestedLists"; + +async function testHTML(html: string) { + const htmlNode = nestedListsToBlockNoteStructure(html); + + const pretty = await unified() + .use(rehypeParse, { fragment: true }) + .use(rehypeFormat) + .use(rehypeStringify) + .process(htmlNode.innerHTML); + + expect(pretty.value).toMatchSnapshot(); +} + +describe("Lift nested lists", () => { + it("Lifts nested bullet lists", async () => { + const html = `
      +
    • + Bullet List Item 1 +
        +
      • + Nested Bullet List Item 1 +
      • +
      • + Nested Bullet List Item 2 +
      • +
      +
    • +
    • + Bullet List Item 2 +
    • +
    `; + await testHTML(html); + }); + + it("Lifts nested bullet lists without li", async () => { + const html = `
      + Bullet List Item 1 +
        +
      • + Nested Bullet List Item 1 +
      • +
      • + Nested Bullet List Item 2 +
      • +
      +
    • + Bullet List Item 2 +
    • +
    `; + await testHTML(html); + }); + + it("Lifts nested bullet lists with content after nested list", async () => { + const html = `
      +
    • + Bullet List Item 1 +
        +
      • + Nested Bullet List Item 1 +
      • +
      • + Nested Bullet List Item 2 +
      • +
      + More content in list item 1 +
    • +
    • + Bullet List Item 2 +
    • +
    `; + await testHTML(html); + }); + + it("Lifts multiple bullet lists", async () => { + const html = `
      +
    • + Bullet List Item 1 +
        +
      • + Nested Bullet List Item 1 +
      • +
      • + Nested Bullet List Item 2 +
      • +
      +
        +
      • + Nested Bullet List Item 3 +
      • +
      • + Nested Bullet List Item 4 +
      • +
      +
    • +
    • + Bullet List Item 2 +
    • +
    `; + await testHTML(html); + }); + + it("Lifts multiple bullet lists with content in between", async () => { + const html = `
      +
    • + Bullet List Item 1 +
        +
      • + Nested Bullet List Item 1 +
      • +
      • + Nested Bullet List Item 2 +
      • +
      + In between content +
        +
      • + Nested Bullet List Item 3 +
      • +
      • + Nested Bullet List Item 4 +
      • +
      +
    • +
    • + Bullet List Item 2 +
    • +
    `; + await testHTML(html); + }); + + it("Lifts nested numbered lists", async () => { + const html = `
      +
    1. + Numbered List Item 1 +
        +
      1. + Nested Numbered List Item 1 +
      2. +
      3. + Nested Numbered List Item 2 +
      4. +
      +
    2. +
    3. + Numbered List Item 2 +
    4. +
    `; + await testHTML(html); + }); + + it("Lifts nested mixed lists", async () => { + const html = `
      +
    1. + Numbered List Item 1 +
        +
      • + Bullet List Item 1 +
      • +
      • + Bullet List Item 2 +
      • +
      +
    2. +
    3. + Numbered List Item 2 +
    4. +
    `; + await testHTML(html); + }); +}); diff --git a/packages/core/src/api/parsers/html/util/nestedLists.ts b/packages/core/src/api/parsers/html/util/nestedLists.ts new file mode 100644 index 0000000000..78c60b2a1a --- /dev/null +++ b/packages/core/src/api/parsers/html/util/nestedLists.ts @@ -0,0 +1,113 @@ +function getChildIndex(node: Element) { + return Array.prototype.indexOf.call(node.parentElement!.childNodes, node); +} + +function isWhitespaceNode(node: Node) { + return node.nodeType === 3 && !/\S/.test(node.nodeValue || ""); +} + +/** + * Step 1, Turns: + * + *
      + *
    • item
    • + *
    • + *
        + *
      • ...
      • + *
      • ...
      • + *
      + *
    • + * + * Into: + *
        + *
      • item
      • + *
          + *
        • ...
        • + *
        • ...
        • + *
        + *
      + * + */ +function liftNestedListsToParent(element: HTMLElement) { + element.querySelectorAll("li > ul, li > ol").forEach((list) => { + const index = getChildIndex(list); + const parentListItem = list.parentElement!; + const siblingsAfter = Array.from(parentListItem.childNodes).slice( + index + 1 + ); + list.remove(); + siblingsAfter.forEach((sibling) => { + sibling.remove(); + }); + + parentListItem.insertAdjacentElement("afterend", list); + + siblingsAfter.reverse().forEach((sibling) => { + if (isWhitespaceNode(sibling)) { + return; + } + const siblingContainer = document.createElement("li"); + siblingContainer.append(sibling); + list.insertAdjacentElement("afterend", siblingContainer); + }); + if (parentListItem.childNodes.length === 0) { + parentListItem.remove(); + } + }); +} + +/** + * Step 2, Turns (output of liftNestedListsToParent): + * + *
    • item
    • + *
        + *
      • ...
      • + *
      • ...
      • + *
      + * + * Into: + *
      + *
    • item
    • + *
      + *
        + *
      • ...
      • + *
      • ...
      • + *
      + *
      + *
      + * + * This resulting format is parsed + */ +function createGroups(element: HTMLElement) { + element.querySelectorAll("li + ul, li + ol").forEach((list) => { + const listItem = list.previousElementSibling as HTMLElement; + const blockContainer = document.createElement("div"); + + listItem.insertAdjacentElement("afterend", blockContainer); + blockContainer.append(listItem); + + const blockGroup = document.createElement("div"); + blockGroup.setAttribute("data-node-type", "blockGroup"); + blockContainer.append(blockGroup); + + while ( + blockContainer.nextElementSibling?.nodeName === "UL" || + blockContainer.nextElementSibling?.nodeName === "OL" + ) { + blockGroup.append(blockContainer.nextElementSibling); + } + }); +} + +export function nestedListsToBlockNoteStructure( + elementOrHTML: HTMLElement | string +) { + if (typeof elementOrHTML === "string") { + const element = document.createElement("div"); + element.innerHTML = elementOrHTML; + elementOrHTML = element; + } + liftNestedListsToParent(elementOrHTML); + createGroups(elementOrHTML); + return elementOrHTML; +} diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.ts new file mode 100644 index 0000000000..f81cb7a0b3 --- /dev/null +++ b/packages/core/src/api/parsers/markdown/parseMarkdown.ts @@ -0,0 +1,80 @@ +import { Schema } from "prosemirror-model"; +import rehypeStringify from "rehype-stringify"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import remarkRehype, { defaultHandlers } from "remark-rehype"; +import { unified } from "unified"; +import { Block, BlockSchema, InlineContentSchema, StyleSchema } from "../../.."; +import { HTMLToBlocks } from "../html/parseHTML"; + +// modified version of https://github.com/syntax-tree/mdast-util-to-hast/blob/main/lib/handlers/code.js +// that outputs a data-language attribute instead of a CSS class (e.g.: language-typescript) +function code(state: any, node: any) { + const value = node.value ? node.value + "\n" : ""; + /** @type {Properties} */ + const properties: any = {}; + + if (node.lang) { + // changed line + properties["data-language"] = node.lang; + } + + // Create ``. + /** @type {Element} */ + let result: any = { + type: "element", + tagName: "code", + properties, + children: [{ type: "text", value }], + }; + + if (node.meta) { + result.data = { meta: node.meta }; + } + + state.patch(node, result); + result = state.applyData(node, result); + + // Create `
      `.
      +  result = {
      +    type: "element",
      +    tagName: "pre",
      +    properties: {},
      +    children: [result],
      +  };
      +  state.patch(node, result);
      +  return result;
      +}
      +
      +// TODO: add tests
      +export function markdownToBlocks<
      +  BSchema extends BlockSchema,
      +  I extends InlineContentSchema,
      +  S extends StyleSchema
      +>(
      +  markdown: string,
      +  blockSchema: BSchema,
      +  icSchema: I,
      +  styleSchema: S,
      +  pmSchema: Schema
      +): Promise[]> {
      +  const htmlString = unified()
      +    .use(remarkParse)
      +    .use(remarkGfm)
      +    .use(remarkRehype, {
      +      handlers: {
      +        ...(defaultHandlers as any),
      +        code,
      +      },
      +    })
      +    .use(rehypeStringify)
      +    .processSync(markdown);
      +
      +  return HTMLToBlocks(
      +    htmlString.value as string,
      +    blockSchema,
      +    icSchema,
      +    styleSchema,
      +    pmSchema
      +  );
      +}
      diff --git a/packages/core/src/api/parsers/pasteExtension.ts b/packages/core/src/api/parsers/pasteExtension.ts
      new file mode 100644
      index 0000000000..f0dec4f86d
      --- /dev/null
      +++ b/packages/core/src/api/parsers/pasteExtension.ts
      @@ -0,0 +1,61 @@
      +import { Extension } from "@tiptap/core";
      +import { Plugin } from "prosemirror-state";
      +
      +import { BlockNoteEditor } from "../../BlockNoteEditor";
      +import { BlockSchema } from "../../extensions/Blocks/api/blocks/types";
      +import { InlineContentSchema } from "../../extensions/Blocks/api/inlineContent/types";
      +import { StyleSchema } from "../../extensions/Blocks/api/styles/types";
      +import { nestedListsToBlockNoteStructure } from "./html/util/nestedLists";
      +
      +const acceptedMIMETypes = [
      +  "blocknote/html",
      +  "text/html",
      +  "text/plain",
      +] as const;
      +
      +export const createPasteFromClipboardExtension = <
      +  BSchema extends BlockSchema,
      +  I extends InlineContentSchema,
      +  S extends StyleSchema
      +>(
      +  editor: BlockNoteEditor
      +) =>
      +  Extension.create<{ editor: BlockNoteEditor }, undefined>({
      +    name: "pasteFromClipboard",
      +    addProseMirrorPlugins() {
      +      return [
      +        new Plugin({
      +          props: {
      +            handleDOMEvents: {
      +              paste(_view, event) {
      +                event.preventDefault();
      +                let format: (typeof acceptedMIMETypes)[number] | null = null;
      +
      +                for (const mimeType of acceptedMIMETypes) {
      +                  if (event.clipboardData!.types.includes(mimeType)) {
      +                    format = mimeType;
      +                    break;
      +                  }
      +                }
      +
      +                if (format !== null) {
      +                  let data = event.clipboardData!.getData(format);
      +                  if (format === "text/html") {
      +                    const htmlNode = nestedListsToBlockNoteStructure(
      +                      data.trim()
      +                    );
      +
      +                    data = htmlNode.innerHTML;
      +                    console.log(data);
      +                  }
      +                  editor._tiptapEditor.view.pasteHTML(data);
      +                }
      +
      +                return true;
      +              },
      +            },
      +          },
      +        }),
      +      ];
      +    },
      +  });
      diff --git a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html
      deleted file mode 100644
      index 4c7e8f174d..0000000000
      --- a/packages/core/src/api/serialization/html/__snapshots__/fontSize/basic/external.html
      +++ /dev/null
      @@ -1 +0,0 @@
      -

      This is text with a custom fontSize

      \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html deleted file mode 100644 index e1513fed2d..0000000000 --- a/packages/core/src/api/serialization/html/__snapshots__/mention/basic/external.html +++ /dev/null @@ -1 +0,0 @@ -

      I enjoy working with@Matthew

      \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html deleted file mode 100644 index 4206d07a95..0000000000 --- a/packages/core/src/api/serialization/html/__snapshots__/small/basic/external.html +++ /dev/null @@ -1 +0,0 @@ -

      This is a small text

      \ No newline at end of file diff --git a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html deleted file mode 100644 index 4229ae0a83..0000000000 --- a/packages/core/src/api/serialization/html/__snapshots__/tag/basic/external.html +++ /dev/null @@ -1 +0,0 @@ -

      I love #BlockNote

      \ No newline at end of file diff --git a/packages/core/src/api/testCases/cases/customInlineContent.ts b/packages/core/src/api/testCases/cases/customInlineContent.ts index 13e4bb6e2d..a1603f4a87 100644 --- a/packages/core/src/api/testCases/cases/customInlineContent.ts +++ b/packages/core/src/api/testCases/cases/customInlineContent.ts @@ -68,7 +68,7 @@ export const customInlineContentTestCases: EditorTestCases< InlineContentSchemaFromSpecs, DefaultStyleSchema > = { - name: "custom style schema", + name: "custom inline content schema", createEditor: () => { return BlockNoteEditor.create({ uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, diff --git a/packages/core/src/api/testCases/cases/defaultSchema.ts b/packages/core/src/api/testCases/cases/defaultSchema.ts index bb7ccf4526..87aa6b01b1 100644 --- a/packages/core/src/api/testCases/cases/defaultSchema.ts +++ b/packages/core/src/api/testCases/cases/defaultSchema.ts @@ -24,7 +24,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/empty", blocks: [ { - type: "paragraph" as const, + type: "paragraph", }, ], }, @@ -32,7 +32,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/basic", blocks: [ { - type: "paragraph" as const, + type: "paragraph", content: "Paragraph", }, ], @@ -41,12 +41,12 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/styled", blocks: [ { - type: "paragraph" as const, + type: "paragraph", props: { textAlignment: "center", textColor: "orange", backgroundColor: "pink", - } as const, + }, content: [ { type: "text", @@ -83,15 +83,15 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/nested", blocks: [ { - type: "paragraph" as const, + type: "paragraph", content: "Paragraph", children: [ { - type: "paragraph" as const, + type: "paragraph", content: "Nested Paragraph 1", }, { - type: "paragraph" as const, + type: "paragraph", content: "Nested Paragraph 2", }, ], @@ -102,7 +102,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/button", blocks: [ { - type: "image" as const, + type: "image", }, ], }, @@ -110,7 +110,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/basic", blocks: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", @@ -123,20 +123,20 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/nested", blocks: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", width: 256, - } as const, + }, children: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", width: 256, - } as const, + }, }, ], }, diff --git a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts index 81e6d370c9..18b0d780f4 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/createSpec.ts @@ -1,3 +1,4 @@ +import { ParseRule } from "@tiptap/pm/model"; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { InlineContentSchema } from "../inlineContent/types"; import { StyleSchema } from "../styles/types"; @@ -5,11 +6,15 @@ import { createInternalBlockSpec, createStronglyTypedTiptapNode, getBlockFromPos, - parse, propsToAttributes, wrapInBlockStructure, } from "./internal"; -import { BlockConfig, BlockFromConfig, BlockSchemaWithBlock } from "./types"; +import { + BlockConfig, + BlockFromConfig, + BlockSchemaWithBlock, + PartialBlockFromConfig, +} from "./types"; // restrict content to "inline" and "none" only export type CustomBlockConfig = BlockConfig & { @@ -50,8 +55,60 @@ export type CustomBlockImplementation< dom: HTMLElement; contentDOM?: HTMLElement; }; + + parse?: (el: HTMLElement) => PartialBlockFromConfig | undefined; }; +// Function that uses the 'parse' function of a blockConfig to create a +// TipTap node's `parseHTML` property. This is only used for parsing content +// from the clipboard. +export function getParseRules( + config: BlockConfig, + customParseFunction: CustomBlockImplementation["parse"] +) { + const rules: ParseRule[] = [ + { + tag: "div[data-content-type=" + config.type + "]", + }, + ]; + + if (customParseFunction) { + rules.push({ + tag: "*", + getAttrs(node: string | HTMLElement) { + if (typeof node === "string") { + return false; + } + + const block = customParseFunction?.(node); + + if (block === undefined) { + return false; + } + + return block.props || {}; + }, + }); + } + // getContent(node, schema) { + // const block = blockConfig.parse?.(node as HTMLElement); + // + // if (block !== undefined && block.content !== undefined) { + // return Fragment.from( + // typeof block.content === "string" + // ? schema.text(block.content) + // : inlineContentToNodes(block.content, schema) + // ); + // } + // + // return Fragment.empty; + // }, + // }); + // } + + return rules; +} + // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createBlockSpec< @@ -72,7 +129,7 @@ export function createBlockSpec< }, parseHTML() { - return parse(blockConfig); + return getParseRules(blockConfig, blockImplementation.parse); }, addNodeView() { diff --git a/packages/core/src/extensions/Blocks/api/blocks/internal.ts b/packages/core/src/extensions/Blocks/api/blocks/internal.ts index 34e39c6ad2..58f8b84d50 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/internal.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/internal.ts @@ -1,5 +1,11 @@ -import { Attribute, Attributes, Editor, Node, NodeConfig } from "@tiptap/core"; -import { ParseRule } from "prosemirror-model"; +import { + Attribute, + Attributes, + Editor, + Extension, + Node, + NodeConfig, +} from "@tiptap/core"; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { mergeCSSClasses } from "../../../../shared/utils"; import { defaultBlockToHTML } from "../../nodes/BlockContent/defaultBlockHelpers"; @@ -82,51 +88,6 @@ export function propsToAttributes(propSchema: PropSchema): Attributes { return tiptapAttributes; } -// Function that uses the 'parse' function of a blockConfig to create a -// TipTap node's `parseHTML` property. This is only used for parsing content -// from the clipboard. -export function parse(blockConfig: BlockConfig) { - const rules: ParseRule[] = [ - { - tag: "div[data-content-type=" + blockConfig.type + "]", - }, - ]; - - // if (blockConfig.parse) { - // rules.push({ - // tag: "*", - // getAttrs(node: string | HTMLElement) { - // if (typeof node === "string") { - // return false; - // } - // - // const block = blockConfig.parse?.(node); - // - // if (block === undefined) { - // return false; - // } - // - // return block.props || {}; - // }, - // getContent(node, schema) { - // const block = blockConfig.parse?.(node as HTMLElement); - // - // if (block !== undefined && block.content !== undefined) { - // return Fragment.from( - // typeof block.content === "string" - // ? schema.text(block.content) - // : inlineContentToNodes(block.content, schema) - // ); - // } - // - // return Fragment.empty; - // }, - // }); - // } - - return rules; -} - // Used to figure out which block should be rendered. This block is then used to // create the node view. export function getBlockFromPos< @@ -263,7 +224,7 @@ export function createInternalBlockSpec( export function createBlockSpecFromStronglyTypedTiptapNode< T extends Node, P extends PropSchema ->(node: T, propSchema: P, requiredNodes?: Node[]) { +>(node: T, propSchema: P, requiredExtensions?: Array) { return createInternalBlockSpec( { type: node.name as T["name"], @@ -280,9 +241,10 @@ export function createBlockSpecFromStronglyTypedTiptapNode< }, { node, - requiredNodes, + requiredExtensions, toInternalHTML: defaultBlockToHTML, toExternalHTML: defaultBlockToHTML, + // parse: () => undefined, // parse rules are in node already } ); } diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index c16e723e6b..e98453b41a 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -1,5 +1,5 @@ /** Define the main block types **/ -import { Node } from "@tiptap/core"; +import { Extension, Node } from "@tiptap/core"; import { BlockNoteEditor } from "../../../../BlockNoteEditor"; import { @@ -69,7 +69,7 @@ export type TiptapBlockImplementation< I extends InlineContentSchema, S extends StyleSchema > = { - requiredNodes?: Node[]; + requiredExtensions?: Array; node: Node; toInternalHTML: ( block: BlockFromConfigNoChildren & { @@ -273,4 +273,12 @@ export type SpecificPartialBlock< children?: Block[]; }; +export type PartialBlockFromConfig< + B extends BlockConfig, + I extends InlineContentSchema, + S extends StyleSchema +> = PartialBlockFromConfigNoChildren & { + children?: Block[]; +}; + export type BlockIdentifier = { id: string } | string; diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 7e68f51cbb..6b6bff26a4 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -1,8 +1,13 @@ import { Node } from "@tiptap/core"; +import { ParseRule } from "@tiptap/pm/model"; import { nodeToCustomInlineContent } from "../../../../api/nodeConversions/nodeConversions"; import { propsToAttributes } from "../blocks/internal"; +import { Props } from "../blocks/types"; import { StyleSchema } from "../styles/types"; -import { createInlineContentSpecFromTipTapNode } from "./internal"; +import { + addInlineContentAttributes, + createInlineContentSpecFromTipTapNode, +} from "./internal"; import { CustomInlineContentConfig, InlineContentConfig, @@ -38,6 +43,16 @@ export type CustomInlineContentImplementation< }; }; +export function getInlineContentParseRules( + config: InlineContentConfig +): ParseRule[] { + return [ + { + tag: `.bn-inline-content-section[data-inline-content-type="${config.type}"]`, + }, + ]; +} + export function createInlineContentSpec< T extends CustomInlineContentConfig, S extends StyleSchema @@ -58,6 +73,10 @@ export function createInlineContentSpec< return propsToAttributes(inlineContentConfig.propSchema); }, + parseHTML() { + return getInlineContentParseRules(inlineContentConfig); + }, + renderHTML({ node }) { const editor = this.options.editor; @@ -69,7 +88,15 @@ export function createInlineContentSpec< ) as any as InlineContentFromConfig // TODO: fix cast ); - return output; + return { + dom: addInlineContentAttributes( + output.dom, + inlineContentConfig.type, + node.attrs as Props, + inlineContentConfig.propSchema + ), + contentDOM: output.contentDOM, + }; }, }); diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts index 9c623c44cf..d081338be8 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts @@ -1,5 +1,6 @@ import { Node } from "@tiptap/core"; -import { PropSchema } from "../blocks/types"; +import { camelToDataKebab } from "../blocks/internal"; +import { Props, PropSchema } from "../blocks/types"; import { InlineContentConfig, InlineContentImplementation, @@ -7,6 +8,38 @@ import { InlineContentSpec, InlineContentSpecs, } from "./types"; +import { mergeCSSClasses } from "../../../../shared/utils"; + +// Function that adds necessary classes and attributes to the `dom` element +// returned from a custom inline content's 'render' function, to ensure no data +// is lost on internal copy & paste. +export function addInlineContentAttributes< + IType extends string, + PSchema extends PropSchema +>( + element: HTMLElement, + inlineContentType: IType, + inlineContentProps: Props, + propSchema: PSchema +): HTMLElement { + // Sets inline content section class + element.className = mergeCSSClasses( + "bn-inline-content-section", + element.className + ); + // Sets content type attribute + element.setAttribute("data-inline-content-type", inlineContentType); + // Adds props as HTML attributes in kebab-case with "data-" prefix. Skips props + // set to their default values. + Object.entries(inlineContentProps) + .filter(([prop, value]) => value !== propSchema[prop].default) + .map(([prop, value]) => { + return [camelToDataKebab(prop), value]; + }) + .forEach(([prop, value]) => element.setAttribute(prop, value)); + + return element; +} // This helper function helps to instantiate a InlineContentSpec with a // config and implementation that conform to the type of Config diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts index 9f0d742f75..14c1c2274f 100644 --- a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -1,6 +1,11 @@ import { Mark } from "@tiptap/core"; +import { ParseRule } from "@tiptap/pm/model"; import { UnreachableCaseError } from "../../../../shared/utils"; -import { createInternalStyleSpec } from "./internal"; +import { + addStyleAttributes, + createInternalStyleSpec, + stylePropsToAttributes, +} from "./internal"; import { StyleConfig, StyleSpec } from "./types"; export type CustomStyleImplementation = { @@ -17,6 +22,14 @@ export type CustomStyleImplementation = { // TODO: support serialization +export function getStyleParseRules(config: StyleConfig): ParseRule[] { + return [ + { + tag: `.bn-style[data-style-type="${config.type}"]`, + }, + ]; +} + export function createStyleSpec( styleConfig: T, styleImplementation: CustomStyleImplementation @@ -25,21 +38,11 @@ export function createStyleSpec( name: styleConfig.type, addAttributes() { - if (styleConfig.propSchema === "boolean") { - return {}; - } - return { - stringValue: { - default: undefined, - // TODO: parsing + return stylePropsToAttributes(styleConfig.propSchema); + }, - // parseHTML: (element) => - // element.getAttribute(`data-${styleConfig.type}`), - // renderHTML: (attributes) => ({ - // [`data-${styleConfig.type}`]: attributes.stringValue, - // }), - }, - }; + parseHTML() { + return getStyleParseRules(styleConfig); }, renderHTML({ mark }) { @@ -58,7 +61,15 @@ export function createStyleSpec( } // const renderResult = styleImplementation.render(); - return renderResult; + return { + dom: addStyleAttributes( + renderResult.dom, + styleConfig.type, + mark.attrs.stringValue, + styleConfig.propSchema + ), + contentDOM: renderResult.contentDOM, + }; }, }); diff --git a/packages/core/src/extensions/Blocks/api/styles/internal.ts b/packages/core/src/extensions/Blocks/api/styles/internal.ts index 648bb133d5..27b32a3f7a 100644 --- a/packages/core/src/extensions/Blocks/api/styles/internal.ts +++ b/packages/core/src/extensions/Blocks/api/styles/internal.ts @@ -1,4 +1,4 @@ -import { Mark } from "@tiptap/core"; +import { Attributes, Mark } from "@tiptap/core"; import { StyleConfig, StyleImplementation, @@ -7,6 +7,53 @@ import { StyleSpec, StyleSpecs, } from "./types"; +import { mergeCSSClasses } from "../../../../shared/utils"; + +export function stylePropsToAttributes( + propSchema: StylePropSchema +): Attributes { + if (propSchema === "boolean") { + return {}; + } + return { + stringValue: { + default: undefined, + keepOnSplit: true, + parseHTML: (element) => element.getAttribute("data-value"), + renderHTML: (attributes) => + attributes.stringValue !== undefined + ? { + "data-value": attributes.stringValue, + } + : {}, + }, + }; +} + +// Function that adds necessary classes and attributes to the `dom` element +// returned from a custom style's 'render' function, to ensure no data is lost +// on internal copy & paste. +export function addStyleAttributes< + SType extends string, + PSchema extends StylePropSchema +>( + element: HTMLElement, + styleType: SType, + styleValue: PSchema extends "boolean" ? undefined : string, + propSchema: PSchema +): HTMLElement { + // Sets inline content section class + element.className = mergeCSSClasses("bn-style", element.className); + // Sets content type attribute + element.setAttribute("data-style-type", styleType); + // Adds style value as an HTML attribute in kebab-case with "data-" prefix, if + // the style takes a string value. + if (propSchema === "string") { + element.setAttribute("data-value", styleValue as string); + } + + return element; +} // This helper function helps to instantiate a stylespec with a // config and implementation that conform to the type of Config diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 443cc7d7fd..bba83b4308 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -483,13 +483,12 @@ export const BlockContainer = Node.create<{ // Reverts block content type to a paragraph if the selection is at the start of the block. () => commands.command(({ state }) => { - const { contentType } = getBlockInfoFromPos( + const { contentType, startPos } = getBlockInfoFromPos( state.doc, state.selection.from )!; - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; + const selectionAtBlockStart = state.selection.from === startPos + 1; const isParagraph = contentType.name === "paragraph"; if (selectionAtBlockStart && !isParagraph) { @@ -504,8 +503,12 @@ export const BlockContainer = Node.create<{ // Removes a level of nesting if the block is indented if the selection is at the start of the block. () => commands.command(({ state }) => { - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; + const { startPos } = getBlockInfoFromPos( + state.doc, + state.selection.from + )!; + + const selectionAtBlockStart = state.selection.from === startPos + 1; if (selectionAtBlockStart) { return commands.liftListItem("blockContainer"); @@ -522,10 +525,8 @@ export const BlockContainer = Node.create<{ state.selection.from )!; - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; - const selectionEmpty = - state.selection.anchor === state.selection.head; + const selectionAtBlockStart = state.selection.from === startPos + 1; + const selectionEmpty = state.selection.empty; const blockAtDocStart = startPos === 2; const posBetweenBlocks = startPos - 1; @@ -552,17 +553,14 @@ export const BlockContainer = Node.create<{ // end of the block. () => commands.command(({ state }) => { - const { node, contentNode, depth, endPos } = getBlockInfoFromPos( + const { node, depth, endPos } = getBlockInfoFromPos( state.doc, state.selection.from )!; const blockAtDocEnd = false; - const selectionAtBlockEnd = - state.selection.$anchor.parentOffset === - contentNode.firstChild!.nodeSize; - const selectionEmpty = - state.selection.anchor === state.selection.head; + const selectionAtBlockEnd = state.selection.from === endPos - 1; + const selectionEmpty = state.selection.empty; const hasChildBlocks = node.childCount === 2; if ( diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts index 3cfbea0518..50a0b74197 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts @@ -21,7 +21,14 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ level: { default: 1, // instead of "level" attributes, use "data-level" - parseHTML: (element) => element.getAttribute("data-level")!, + parseHTML: (element) => { + const attr = element.getAttribute("data-level")!; + const parsed = parseInt(attr); + if (isFinite(parsed)) { + return parsed; + } + return undefined; + }, renderHTML: (attributes) => { return { "data-level": (attributes.level as number).toString(), @@ -78,9 +85,20 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ }), }; }, - parseHTML() { return [ + { + tag: "div[data-content-type=" + this.name + "]", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + return { + level: element.getAttribute("data-level"), + }; + }, + }, { tag: "h1", attrs: { level: 1 }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts index 2fea7b6f71..4a373c03e5 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts @@ -371,17 +371,29 @@ export const Image = createBlockSpec( dom: figure, }; }, + parse: (element: HTMLElement) => { + if (element.tagName === "FIGURE") { + const img = element.querySelector("img"); + const caption = element.querySelector("figcaption"); + return { + type: "image", + props: { + url: img?.getAttribute("src") || "", + caption: + caption?.textContent || img?.getAttribute("alt") || undefined, + }, + }; + } else if (element.tagName === "IMG") { + return { + type: "image", + props: { + url: element.getAttribute("src") || "", + caption: element.getAttribute("alt") || undefined, + }, + }; + } + + return undefined; + }, } - // parse: (element) => { - // if (element.tagName === "IMG") { - // return { - // type: "image", - // props: { - // url: element.getAttribute("src") || "", - // }, - // }; - // } - // - // return; - // }, ); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 6362288e02..602510ade1 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -48,6 +48,9 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({ parseHTML() { return [ // Case for regular HTML list structure. + { + tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this + }, { tag: "li", getAttrs: (element) => { @@ -61,7 +64,10 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({ return false; } - if (parent.tagName === "UL") { + if ( + parent.tagName === "UL" || + (parent.tagName === "DIV" && parent.parentElement!.tagName === "UL") + ) { return {}; } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index 5c838415e0..e8db16998f 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -66,6 +66,9 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ parseHTML() { return [ + { + tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this + }, // Case for regular HTML list structure. // (e.g.: when pasting from other apps) { @@ -81,7 +84,10 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ return false; } - if (parent.tagName === "OL") { + if ( + parent.tagName === "OL" || + (parent.tagName === "DIV" && parent.parentElement!.tagName === "OL") + ) { return {}; } diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts index a645ba347c..8c826f413e 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts @@ -15,6 +15,7 @@ export const ParagraphBlockContent = createStronglyTypedTiptapNode({ group: "blockContent", parseHTML() { return [ + { tag: "div[data-content-type=" + this.name + "]" }, { tag: "p", priority: 200, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts index b3793ccb07..8807586fdc 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/TableBlockContent/TableBlockContent.ts @@ -1,4 +1,4 @@ -import { Paragraph } from "@tiptap/extension-paragraph"; +import { Node, mergeAttributes } from "@tiptap/core"; import { TableCell } from "@tiptap/extension-table-cell"; import { TableHeader } from "@tiptap/extension-table-header"; import { TableRow } from "@tiptap/extension-table-row"; @@ -8,6 +8,7 @@ import { } from "../../../api/blocks/internal"; import { defaultProps } from "../../../api/defaultProps"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers"; +import { TableExtension } from "./TableExtension"; export const tablePropSchema = { ...defaultProps, @@ -38,15 +39,28 @@ export const TableBlockContent = createStronglyTypedTiptapNode({ }, }); -const TableParagraph = Paragraph.extend({ +const TableParagraph = Node.create({ name: "tableParagraph", group: "tableContent", + + parseHTML() { + return [{ tag: "p" }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "p", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + 0, + ]; + }, }); export const Table = createBlockSpecFromStronglyTypedTiptapNode( TableBlockContent, tablePropSchema, [ + TableExtension, TableParagraph, TableHeader.extend({ content: "tableContent", diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 8f023ad70f..ed87b5df07 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -3,9 +3,9 @@ import { Node } from "prosemirror-model"; import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { BlockNoteEditor } from "../../BlockNoteEditor"; -import { markdown } from "../../api/formatConversions/formatConversions"; -import { createExternalHTMLExporter } from "../../api/serialization/html/externalHTMLExporter"; -import { createInternalHTMLSerializer } from "../../api/serialization/html/internalHTMLSerializer"; +import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter"; +import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer"; +import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter"; import { BaseUiElementState } from "../../shared/BaseUiElementTypes"; import { EventEmitter } from "../../shared/EventEmitter"; import { Block, BlockSchema } from "../Blocks/api/blocks/types"; @@ -234,7 +234,7 @@ function dragStart< selectedSlice.content ); - const plainText = markdown(externalHTML); + const plainText = cleanHTMLToMarkdown(externalHTML); e.dataTransfer.clearData(); e.dataTransfer.setData("blocknote/html", internalHTML); diff --git a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts index 4535866dad..7f9fb505ea 100644 --- a/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts +++ b/packages/core/src/extensions/TextAlignment/TextAlignmentExtension.ts @@ -12,7 +12,9 @@ export const TextAlignmentExtension = Extension.create({ attributes: { textAlignment: { default: "left", - parseHTML: (element) => element.getAttribute("data-text-alignment"), + parseHTML: (element) => { + return element.getAttribute("data-text-alignment"); + }, renderHTML: (attributes) => attributes.textAlignment !== "left" && { "data-text-alignment": attributes.textAlignment, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 81d2c288a8..41637442bb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; -export * from "./api/serialization/html/externalHTMLExporter"; -export * from "./api/serialization/html/internalHTMLSerializer"; +export * from "./api/exporters/html/externalHTMLExporter"; +export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testCases/index"; export * from "./extensions/Blocks/api/blocks/createSpec"; export * from "./extensions/Blocks/api/blocks/internal"; diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx index 7a126b8f48..c65cbc9b26 100644 --- a/packages/react/src/BlockNoteView.tsx +++ b/packages/react/src/BlockNoteView.tsx @@ -47,7 +47,9 @@ function BaseBlockNoteView< - + {props.editor.blockSchema.table && ( + + )} )} diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx index e23077b998..b5f48dc2df 100644 --- a/packages/react/src/ReactBlockSpec.tsx +++ b/packages/react/src/ReactBlockSpec.tsx @@ -1,6 +1,5 @@ import { BlockFromConfig, - BlockNoteDOMAttributes, BlockNoteEditor, BlockSchemaWithBlock, camelToDataKebab, @@ -8,10 +7,11 @@ import { createStronglyTypedTiptapNode, CustomBlockConfig, getBlockFromPos, + getParseRules, inheritedProps, InlineContentSchema, mergeCSSClasses, - parse, + PartialBlockFromConfig, Props, PropSchema, propsToAttributes, @@ -23,8 +23,8 @@ import { NodeViewWrapper, ReactNodeViewRenderer, } from "@tiptap/react"; -import { createContext, ElementType, FC, HTMLProps, useContext } from "react"; -import { renderToString } from "react-dom/server"; +import { FC } from "react"; +import { renderToDOMSpec } from "./ReactRenderUtil"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks @@ -37,38 +37,14 @@ export type ReactCustomBlockImplementation< render: FC<{ block: BlockFromConfig; editor: BlockNoteEditor, I, S>; + contentRef: (node: HTMLElement | null) => void; }>; toExternalHTML?: FC<{ block: BlockFromConfig; editor: BlockNoteEditor, I, S>; + contentRef: (node: HTMLElement | null) => void; }>; -}; - -const BlockNoteDOMAttributesContext = createContext({}); - -export const InlineContent = ( - props: { as?: Tag } & HTMLProps -) => { - const inlineContentDOMAttributes = - useContext(BlockNoteDOMAttributesContext).inlineContent || {}; - - const classNames = mergeCSSClasses( - props.className || "", - "bn-inline-content", - inlineContentDOMAttributes.class - ); - - return ( - key !== "class" - ) - )} - {...props} - className={classNames} - /> - ); + parse?: (el: HTMLElement) => PartialBlockFromConfig | undefined; }; // Function that wraps the React component returned from 'blockConfig.render' in @@ -141,7 +117,7 @@ export function createReactBlockSpec< }, parseHTML() { - return parse(blockConfig); + return getParseRules(blockConfig, blockImplementation.parse); }, addNodeView() { @@ -161,9 +137,12 @@ export function createReactBlockSpec< const blockContentDOMAttributes = this.options.domAttributes?.blockContent || {}; + // hacky, should export `useReactNodeView` from tiptap to get access to ref + const ref = (NodeViewContent({}) as any).ref; + const Content = blockImplementation.render; const BlockContent = reactWrapInBlockStructure( - , + , block.type, block.props, blockConfig.propSchema, @@ -186,47 +165,43 @@ export function createReactBlockSpec< node.options.domAttributes?.blockContent || {}; const Content = blockImplementation.render; - const BlockContent = reactWrapInBlockStructure( - , - block.type, - block.props, - blockConfig.propSchema, - blockContentDOMAttributes - ); - const parent = document.createElement("div"); - parent.innerHTML = renderToString(); - - return { - dom: parent.firstElementChild! as HTMLElement, - contentDOM: (parent.querySelector(".bn-inline-content") || - undefined) as HTMLElement | undefined, - }; + return renderToDOMSpec((refCB) => { + const BlockContent = reactWrapInBlockStructure( + , + block.type, + block.props, + blockConfig.propSchema, + blockContentDOMAttributes + ); + return ; + }); }, toExternalHTML: (block, editor) => { const blockContentDOMAttributes = node.options.domAttributes?.blockContent || {}; - let Content = blockImplementation.toExternalHTML; - if (Content === undefined) { - Content = blockImplementation.render; - } - const BlockContent = reactWrapInBlockStructure( - , - block.type, - block.props, - blockConfig.propSchema, - blockContentDOMAttributes - ); - - const parent = document.createElement("div"); - parent.innerHTML = renderToString(); - - return { - dom: parent.firstElementChild! as HTMLElement, - contentDOM: (parent.querySelector(".bn-inline-content") || - undefined) as HTMLElement | undefined, - }; + const Content = + blockImplementation.toExternalHTML || blockImplementation.render; + + return renderToDOMSpec((refCB) => { + const BlockContent = reactWrapInBlockStructure( + , + block.type, + block.props, + blockConfig.propSchema, + blockContentDOMAttributes + ); + return ; + }); }, }); } diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 99415a1bc9..6d598990e0 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -1,12 +1,17 @@ import { - createInternalInlineContentSpec, - createStronglyTypedTiptapNode, CustomInlineContentConfig, InlineContentConfig, InlineContentFromConfig, + PropSchema, + Props, + StyleSchema, + addInlineContentAttributes, + camelToDataKebab, + createInternalInlineContentSpec, + createStronglyTypedTiptapNode, + getInlineContentParseRules, nodeToCustomInlineContent, propsToAttributes, - StyleSchema, } from "@blocknote/core"; import { NodeViewContent, @@ -16,8 +21,7 @@ import { } from "@tiptap/react"; // import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; import { FC } from "react"; -import { flushSync } from "react-dom"; -import { createRoot } from "react-dom/client"; +import { renderToDOMSpec } from "./ReactRenderUtil"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks @@ -38,6 +42,40 @@ export type ReactInlineContentImplementation< // }>; }; +// Function that adds a wrapper with necessary classes and attributes to the +// component returned from a custom inline content's 'render' function, to +// ensure no data is lost on internal copy & paste. +export function reactWrapInInlineContentStructure< + IType extends string, + PSchema extends PropSchema +>( + element: JSX.Element, + inlineContentType: IType, + inlineContentProps: Props, + propSchema: PSchema +) { + return () => ( + // Creates inline content section element + value !== propSchema[prop].default) + .map(([prop, value]) => { + return [camelToDataKebab(prop), value]; + }) + )}> + {element} + + ); +} + // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactInlineContentSpec< @@ -52,6 +90,8 @@ export function createReactInlineContentSpec< name: inlineContentConfig.type as T["type"], inline: true, group: "inline", + selectable: inlineContentConfig.content === "styled", + atom: inlineContentConfig.content === "none", content: (inlineContentConfig.content === "styled" ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", @@ -60,9 +100,9 @@ export function createReactInlineContentSpec< return propsToAttributes(inlineContentConfig.propSchema); }, - // parseHTML() { - // return parse(blockConfig); - // }, + parseHTML() { + return getInlineContentParseRules(inlineContentConfig); + }, renderHTML({ node }) { const editor = this.options.editor; @@ -72,43 +112,19 @@ export function createReactInlineContentSpec< editor.inlineContentSchema, editor.styleSchema ) as any as InlineContentFromConfig; // TODO: fix cast - const Content = inlineContentImplementation.render; - - let contentDOM: HTMLElement | undefined; - const div = document.createElement("div"); - const root = createRoot(div); - flushSync(() => { - root.render( - (contentDOM = el || undefined)} - /> - ); - }); - - if (!div.childElementCount) { - // TODO - console.warn("ReactInlineContentSpec: renderHTML() failed"); - return { - dom: document.createElement("span"), - }; - } - - // clone so we can unmount the react root - contentDOM?.setAttribute("data-tmp-find", "true"); - const cloneRoot = div.cloneNode(true) as HTMLElement; - const dom = cloneRoot.firstElementChild! as HTMLElement; - const contentDOMClone = cloneRoot.querySelector( - "[data-tmp-find]" - ) as HTMLElement | null; - contentDOMClone?.removeAttribute("data-tmp-find"); - - root.unmount(); + const output = renderToDOMSpec((refCB) => ( + + )); return { - dom, - contentDOM: contentDOMClone || undefined, + dom: addInlineContentAttributes( + output.dom, + inlineContentConfig.type, + node.attrs as Props, + inlineContentConfig.propSchema + ), + contentDOM: output.contentDOM, }; }, @@ -123,20 +139,22 @@ export function createReactInlineContentSpec< const ref = (NodeViewContent({}) as any).ref; const Content = inlineContentImplementation.render; - return ( - - // TODO: fix cast - } - /> - + const FullContent = reactWrapInInlineContentStructure( + // TODO: fix cast + } + />, + inlineContentConfig.type, + props.node.attrs as Props, + inlineContentConfig.propSchema ); + return ; }, { className: "bn-ic-react-node-view-renderer", diff --git a/packages/react/src/ReactRenderUtil.ts b/packages/react/src/ReactRenderUtil.ts new file mode 100644 index 0000000000..36262e9392 --- /dev/null +++ b/packages/react/src/ReactRenderUtil.ts @@ -0,0 +1,37 @@ +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; + +export function renderToDOMSpec( + fc: (refCB: (ref: HTMLElement | null) => void) => React.ReactNode +) { + let contentDOM: HTMLElement | undefined; + const div = document.createElement("div"); + const root = createRoot(div); + flushSync(() => { + root.render(fc((el) => (contentDOM = el || undefined))); + }); + + if (!div.childElementCount) { + // TODO + console.warn("ReactInlineContentSpec: renderHTML() failed"); + return { + dom: document.createElement("span"), + }; + } + + // clone so we can unmount the react root + contentDOM?.setAttribute("data-tmp-find", "true"); + const cloneRoot = div.cloneNode(true) as HTMLElement; + const dom = cloneRoot.firstElementChild! as HTMLElement; + const contentDOMClone = cloneRoot.querySelector( + "[data-tmp-find]" + ) as HTMLElement | null; + contentDOMClone?.removeAttribute("data-tmp-find"); + + root.unmount(); + + return { + dom, + contentDOM: contentDOMClone || undefined, + }; +} diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index e9baef7503..cb401850b7 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -1,8 +1,13 @@ -import { createInternalStyleSpec, StyleConfig } from "@blocknote/core"; +import { + addStyleAttributes, + createInternalStyleSpec, + getStyleParseRules, + StyleConfig, + stylePropsToAttributes, +} from "@blocknote/core"; import { Mark } from "@tiptap/react"; import { FC } from "react"; -import { flushSync } from "react-dom"; -import { createRoot } from "react-dom/client"; +import { renderToDOMSpec } from "./ReactRenderUtil"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks @@ -23,21 +28,11 @@ export function createReactStyleSpec( name: styleConfig.type, addAttributes() { - if (styleConfig.propSchema === "boolean") { - return {}; - } - return { - stringValue: { - default: undefined, - // TODO: parsing + return stylePropsToAttributes(styleConfig.propSchema); + }, - // parseHTML: (element) => - // element.getAttribute(`data-${styleConfig.type}`), - // renderHTML: (attributes) => ({ - // [`data-${styleConfig.type}`]: attributes.stringValue, - // }), - }, - }; + parseHTML() { + return getStyleParseRules(styleConfig); }, renderHTML({ mark }) { @@ -48,40 +43,18 @@ export function createReactStyleSpec( } const Content = styleImplementation.render; - - let contentDOM: HTMLElement | undefined; - const div = document.createElement("div"); - const root = createRoot(div); - flushSync(() => { - root.render( - (contentDOM = el || undefined)} - /> - ); - }); - - if (!div.childElementCount) { - // TODO - console.warn("ReactSdtyleSpec: renderHTML() failed"); - return { - dom: document.createElement("span"), - }; - } - - // clone so we can unmount the react root - contentDOM?.setAttribute("data-tmp-find", "true"); - const cloneRoot = div.cloneNode(true) as HTMLElement; - const dom = cloneRoot.firstElementChild! as HTMLElement; - const contentDOMClone = - (cloneRoot.querySelector("[data-tmp-find]") as HTMLElement) || null; - contentDOMClone?.removeAttribute("data-tmp-find"); - - root.unmount(); + const renderResult = renderToDOMSpec((refCB) => ( + + )); return { - dom, - contentDOM: contentDOMClone || undefined, + dom: addStyleAttributes( + renderResult.dom, + styleConfig.type, + mark.attrs.stringValue, + styleConfig.propSchema + ), + contentDOM: renderResult.contentDOM, }; }, }); diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/external.html b/packages/react/src/test/__snapshots__/fontSize/basic/external.html index 00a5bc6b6e..6c8910692f 100644 --- a/packages/react/src/test/__snapshots__/fontSize/basic/external.html +++ b/packages/react/src/test/__snapshots__/fontSize/basic/external.html @@ -1 +1 @@ -

      This is text with a custom fontSize

      \ No newline at end of file +

      This is text with a custom fontSize

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/internal.html b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html index a41d39869a..998d9bcf8b 100644 --- a/packages/react/src/test/__snapshots__/fontSize/basic/internal.html +++ b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html @@ -1 +1 @@ -

      This is text with a custom fontSize

      \ No newline at end of file +

      This is text with a custom fontSize

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/mention/basic/external.html b/packages/react/src/test/__snapshots__/mention/basic/external.html index e1513fed2d..2e6f533ca1 100644 --- a/packages/react/src/test/__snapshots__/mention/basic/external.html +++ b/packages/react/src/test/__snapshots__/mention/basic/external.html @@ -1 +1 @@ -

      I enjoy working with@Matthew

      \ No newline at end of file +

      I enjoy working with@Matthew

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/mention/basic/internal.html b/packages/react/src/test/__snapshots__/mention/basic/internal.html index 7af6dad9c7..6ca7d81c2c 100644 --- a/packages/react/src/test/__snapshots__/mention/basic/internal.html +++ b/packages/react/src/test/__snapshots__/mention/basic/internal.html @@ -1 +1 @@ -

      I enjoy working with@Matthew

      \ No newline at end of file +

      I enjoy working with@Matthew

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html index 91eec85769..edde3826ef 100644 --- a/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html +++ b/packages/react/src/test/__snapshots__/reactCustomParagraph/basic/internal.html @@ -1 +1 @@ -

      React Custom Paragraph

      \ No newline at end of file +

      React Custom Paragraph

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html index 22dd233fa1..faec73f053 100644 --- a/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html +++ b/packages/react/src/test/__snapshots__/reactCustomParagraph/nested/internal.html @@ -1 +1 @@ -

      React Custom Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file +

      React Custom Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html b/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html index ec4f7f99a2..dd2e249332 100644 --- a/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html +++ b/packages/react/src/test/__snapshots__/reactCustomParagraph/styled/internal.html @@ -1 +1 @@ -

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file +

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html index 1a5c3daa4a..a12e18e1e3 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/external.html @@ -1 +1 @@ -

      React Custom Paragraph

      \ No newline at end of file +

      React Custom Paragraph

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html index 08534f9e77..ef4a1496c0 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/basic/internal.html @@ -1 +1 @@ -

      React Custom Paragraph

      \ No newline at end of file +

      React Custom Paragraph

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html index a61e824d02..f34364cb2a 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/external.html @@ -1 +1 @@ -

      Custom React Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file +

      Custom React Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html index 5ce1aa3e93..b036c67a6d 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/nested/internal.html @@ -1 +1 @@ -

      Custom React Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file +

      Custom React Paragraph

      Nested React Custom Paragraph 1

      Nested React Custom Paragraph 2

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html index 816f2ca547..df6c3a0e11 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/external.html @@ -1 +1 @@ -

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file +

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html index fefa7e8680..fdc04d2f52 100644 --- a/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html +++ b/packages/react/src/test/__snapshots__/simpleReactCustomParagraph/styled/internal.html @@ -1 +1 @@ -

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file +

      Plain Red Text Blue Background Mixed Colors

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/small/basic/external.html b/packages/react/src/test/__snapshots__/small/basic/external.html index 4206d07a95..35c3d5c232 100644 --- a/packages/react/src/test/__snapshots__/small/basic/external.html +++ b/packages/react/src/test/__snapshots__/small/basic/external.html @@ -1 +1 @@ -

      This is a small text

      \ No newline at end of file +

      This is a small text

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/small/basic/internal.html b/packages/react/src/test/__snapshots__/small/basic/internal.html index 805c78112e..73836f647d 100644 --- a/packages/react/src/test/__snapshots__/small/basic/internal.html +++ b/packages/react/src/test/__snapshots__/small/basic/internal.html @@ -1 +1 @@ -

      This is a small text

      \ No newline at end of file +

      This is a small text

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/external.html b/packages/react/src/test/__snapshots__/tag/basic/external.html index 4229ae0a83..b8387e9a55 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/external.html +++ b/packages/react/src/test/__snapshots__/tag/basic/external.html @@ -1 +1 @@ -

      I love #BlockNote

      \ No newline at end of file +

      I love #BlockNote

      \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/internal.html b/packages/react/src/test/__snapshots__/tag/basic/internal.html index dac5db0ca8..bac28633b0 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/internal.html +++ b/packages/react/src/test/__snapshots__/tag/basic/internal.html @@ -1 +1 @@ -

      I love #BlockNote

      \ No newline at end of file +

      I love #BlockNote

      \ No newline at end of file diff --git a/packages/react/src/test/htmlConversion.test.tsx b/packages/react/src/test/htmlConversion.test.tsx index 5a2f466e3f..08c01088db 100644 --- a/packages/react/src/test/htmlConversion.test.tsx +++ b/packages/react/src/test/htmlConversion.test.tsx @@ -6,15 +6,18 @@ import { InlineContentSchema, PartialBlock, StyleSchema, + addIdsToBlocks, createExternalHTMLExporter, createInternalHTMLSerializer, + partialBlocksToBlocksForTesting, } from "@blocknote/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { customReactBlockSchemaTestCases } from "./testCases/customReactBlocks"; import { customReactInlineContentTestCases } from "./testCases/customReactInlineContent"; import { customReactStylesTestCases } from "./testCases/customReactStyles"; -function convertToHTMLAndCompareSnapshots< +// TODO: code same from @blocknote/core, maybe create separate test util package +async function convertToHTMLAndCompareSnapshots< B extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -24,6 +27,7 @@ function convertToHTMLAndCompareSnapshots< snapshotDirectory: string, snapshotName: string ) { + addIdsToBlocks(blocks); const serializer = createInternalHTMLSerializer( editor._tiptapEditor.schema, editor @@ -37,6 +41,16 @@ function convertToHTMLAndCompareSnapshots< "/internal.html"; expect(internalHTML).toMatchFileSnapshot(internalHTMLSnapshotPath); + // turn the internalHTML back into blocks, and make sure no data was lost + const fullBlocks = partialBlocksToBlocksForTesting( + editor.blockSchema, + 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._tiptapEditor.schema, editor @@ -75,9 +89,9 @@ describe("Test React HTML conversion", () => { for (const document of testCase.documents) { // eslint-disable-next-line no-loop-func - it("Convert " + document.name + " to HTML", () => { + it("Convert " + document.name + " to HTML", async () => { const nameSplit = document.name.split("/"); - convertToHTMLAndCompareSnapshots( + await convertToHTMLAndCompareSnapshots( editor, document.blocks, nameSplit[0], diff --git a/packages/react/src/test/testCases/customReactBlocks.tsx b/packages/react/src/test/testCases/customReactBlocks.tsx index fc709cb2a6..8dd528f74d 100644 --- a/packages/react/src/test/testCases/customReactBlocks.tsx +++ b/packages/react/src/test/testCases/customReactBlocks.tsx @@ -9,7 +9,7 @@ import { defaultProps, uploadToTmpFilesDotOrg_DEV_ONLY, } from "@blocknote/core"; -import { InlineContent, createReactBlockSpec } from "../../ReactBlockSpec"; +import { createReactBlockSpec } from "../../ReactBlockSpec"; const ReactCustomParagraph = createReactBlockSpec( { @@ -18,8 +18,8 @@ const ReactCustomParagraph = createReactBlockSpec( content: "inline", }, { - render: () => ( - + render: (props) => ( +

      ), toExternalHTML: () => (

      Hello World

      @@ -34,8 +34,8 @@ const SimpleReactCustomParagraph = createReactBlockSpec( content: "inline", }, { - render: () => ( - + render: (props) => ( +

      ), } ); diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 0557d683ef..41e980486e 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -11,6 +11,16 @@ export default defineConfig((conf) => ({ setupFiles: ["./vitestSetup.ts"], }, plugins: [react()], + // used so that vitest resolves the core package from the sources instead of the built version + resolve: { + alias: + conf.command === "build" + ? ({} as Record) + : ({ + // load live from sources with live reload working + "@blocknote/core": path.resolve(__dirname, "../core/src/"), + } as Record), + }, build: { sourcemap: true, lib: { From 296085a59b260f5ee79bb89b16629f8a18222459 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 29 Nov 2023 11:20:17 +0100 Subject: [PATCH 31/31] fix build --- packages/core/src/BlockNoteEditor.ts | 12 +++++++++++- .../core/src/extensions/Blocks/api/blocks/types.ts | 2 +- .../Blocks/api/inlineContent/createSpec.ts | 2 +- .../extensions/TableHandles/TableHandlesPlugin.ts | 13 ++++++------- .../components/TableHandlePositioner.tsx | 8 +++++--- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 7ba1ba709f..bf41f4ba60 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -21,6 +21,7 @@ import { BlockNoteDOMAttributes, BlockSchema, BlockSchemaFromSpecs, + BlockSchemaWithBlock, BlockSpecs, PartialBlock, } from "./extensions/Blocks/api/blocks/types"; @@ -240,7 +241,16 @@ export class BlockNoteEditor< SSchema >; public readonly tableHandles: - | TableHandlesProsemirrorPlugin + | TableHandlesProsemirrorPlugin< + BSchema extends BlockSchemaWithBlock< + "table", + DefaultBlockSchema["table"] + > + ? BSchema + : any, + ISchema, + SSchema + > | undefined; public readonly uploadFile: ((file: File) => Promise) | undefined; diff --git a/packages/core/src/extensions/Blocks/api/blocks/types.ts b/packages/core/src/extensions/Blocks/api/blocks/types.ts index e98453b41a..29b4acfe79 100644 --- a/packages/core/src/extensions/Blocks/api/blocks/types.ts +++ b/packages/core/src/extensions/Blocks/api/blocks/types.ts @@ -152,7 +152,7 @@ export type TableContent< // 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) -type BlockFromConfigNoChildren< +export type BlockFromConfigNoChildren< B extends BlockConfig, I extends InlineContentSchema, S extends StyleSchema diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 6b6bff26a4..220e85c6a3 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -44,7 +44,7 @@ export type CustomInlineContentImplementation< }; export function getInlineContentParseRules( - config: InlineContentConfig + config: CustomInlineContentConfig ): ParseRule[] { return [ { diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index d721c20dd4..f600bbf4d4 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -2,6 +2,7 @@ import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { Block, + BlockFromConfigNoChildren, BlockNoteEditor, BlockSchemaWithBlock, DefaultBlockSchema, @@ -9,7 +10,6 @@ import { PartialBlock, SpecificBlock, StyleSchema, - TableContent, getDraggableBlockFromCoords, nodeToBlock, } from "../.."; @@ -36,7 +36,6 @@ function unsetHiddenDragImage() { } export type TableHandlesState< - BSchema extends BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>, I extends InlineContentSchema, S extends StyleSchema > = { @@ -44,7 +43,7 @@ export type TableHandlesState< referencePosCell: DOMRect; referencePosTable: DOMRect; - block: SpecificBlock; + block: BlockFromConfigNoChildren; colIndex: number; rowIndex: number; @@ -89,7 +88,7 @@ export class TableHandlesView< S extends StyleSchema > implements PluginView { - public state?: TableHandlesState; + public state?: TableHandlesState; public updateState: () => void; public tableId: string | undefined; @@ -102,7 +101,7 @@ export class TableHandlesView< constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, - updateState: (state: TableHandlesState) => void + updateState: (state: TableHandlesState) => void ) { this.updateState = () => { if (!this.state) { @@ -304,7 +303,7 @@ export class TableHandlesView< event.preventDefault(); - const rows = (this.state.block.content as TableContent).rows; + const rows = this.state.block.content.rows; if (this.state.draggingState.draggedCellOrientation === "row") { const rowToMove = rows[this.state.draggingState.originalIndex]; @@ -507,7 +506,7 @@ export class TableHandlesProsemirrorPlugin< }); } - public onUpdate(callback: (state: TableHandlesState) => void) { + public onUpdate(callback: (state: TableHandlesState) => void) { return this.on("update", callback); } diff --git a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx index 3fa6c983d6..78f6102a36 100644 --- a/packages/react/src/TableHandles/components/TableHandlePositioner.tsx +++ b/packages/react/src/TableHandles/components/TableHandlePositioner.tsx @@ -1,9 +1,9 @@ import { + BlockFromConfigNoChildren, BlockNoteEditor, BlockSchemaWithBlock, DefaultBlockSchema, InlineContentSchema, - SpecificBlock, StyleSchema, TableHandlesProsemirrorPlugin, TableHandlesState, @@ -22,7 +22,7 @@ export type TableHandleProps< "dragEnd" | "freezeHandles" | "unfreezeHandles" > & Omit< - TableHandlesState, + TableHandlesState, | "rowIndex" | "colIndex" | "referencePosCell" @@ -52,7 +52,9 @@ export const TableHandlesPositioner = < const [show, setShow] = useState(false); const [hideRow, setHideRow] = useState(false); const [hideCol, setHideCol] = useState(false); - const [block, setBlock] = useState>(); + const [block, setBlock] = + useState>(); + const [rowIndex, setRowIndex] = useState(); const [colIndex, setColIndex] = useState();