diff --git a/packages/core/package.json b/packages/core/package.json index c409f97fc5..afea1daadb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -112,7 +112,8 @@ "uuid": "^8.3.2", "y-prosemirror": "^1.3.4", "y-protocols": "^1.0.6", - "yjs": "^13.6.15" + "yjs": "^13.6.15", + "zod": "^3.25.30" }, "devDependencies": { "@types/emoji-mart": "^3.0.14", diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index f74757c8d7..a279f6ffb8 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -1,5 +1,6 @@ import { DOMSerializer, Fragment } from "prosemirror-model"; +import * as z from "zod/v4/core"; import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { @@ -90,10 +91,13 @@ function serializeBlock< if (!block.props) { props = {}; for (const [name, spec] of Object.entries( - editor.schema.blockSchema[block.type as any].propSchema, + (editor.schema.blockSchema[block.type as any].propSchema as z.$ZodObject) // TODO + ._zod.def.shape, )) { - if (spec.default !== undefined) { - (props as any)[name] = spec.default; + const def = + spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined; + if (def !== undefined) { + (props as any)[name] = def; } } } diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index 0bd7722172..b94661460e 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -1,5 +1,6 @@ import { DOMSerializer, Fragment } from "prosemirror-model"; +import * as z from "zod/v4/core"; import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; import { @@ -65,10 +66,13 @@ function serializeBlock< if (!block.props) { props = {}; for (const [name, spec] of Object.entries( - editor.schema.blockSchema[block.type as any].propSchema, + (editor.schema.blockSchema[block.type as any].propSchema as z.$ZodObject) // TODO + ._zod.def.shape, )) { - if (spec.default !== undefined) { - (props as any)[name] = spec.default; + const def = + spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined; + if (def !== undefined) { + (props as any)[name] = def; } } } diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 1f5d2c75d4..f60d9fedee 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -1,4 +1,5 @@ import { Mark, Node, Schema, Slice } from "@tiptap/pm/model"; +import * as z from "zod/v4/core"; import type { Block } from "../../blocks/defaultBlocks.js"; import UniqueID from "../../extensions/UniqueID/UniqueID.js"; import type { @@ -429,12 +430,22 @@ export function nodeToBlock< ...node.attrs, ...(blockInfo.isBlockContainer ? blockInfo.blockContent.node.attrs : {}), })) { - const propSchema = blockSpec.propSchema; + const propSchema = + blockSpec.propSchema._zod.def.shape[ + attr as keyof typeof blockSpec.propSchema._zod.def.shape + ]; - if ( - attr in propSchema && - !(propSchema[attr].default === undefined && value === undefined) - ) { + if (!propSchema) { + continue; + } + + const def = + propSchema instanceof z.$ZodDefault + ? propSchema._zod.def.defaultValue + : undefined; + + // TODO: is this if statement correct? + if (!(def === undefined && value === undefined)) { props[attr] = value; } } diff --git a/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts b/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts index 7a3e0101fe..488e6f5f73 100644 --- a/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts +++ b/packages/core/src/blocks/AudioBlockContent/AudioBlockContent.ts @@ -1,13 +1,16 @@ +import z from "zod/v4"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfig, createBlockSpec, FileBlockConfig, - Props, - PropSchema, } from "../../schema/index.js"; import { defaultProps } from "../defaultProps.js"; +import { + baseFilePropSchema, + optionalFilePropFields, +} from "../FileBlockContent/FileBlockContent.js"; import { parseFigureElement } from "../FileBlockContent/helpers/parse/parseFigureElement.js"; import { createFileBlockWrapper } from "../FileBlockContent/helpers/render/createFileBlockWrapper.js"; import { createFigureWithCaption } from "../FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.js"; @@ -17,30 +20,22 @@ import { parseAudioElement } from "./parseAudioElement.js"; export const FILE_AUDIO_ICON_SVG = ''; -export const audioPropSchema = { - backgroundColor: defaultProps.backgroundColor, - // File name. - name: { - default: "" as const, - }, - // File url. - url: { - default: "" as const, - }, - // File caption. - caption: { - default: "" as const, - }, - - showPreview: { - default: true, - }, -} satisfies PropSchema; +export const audioPropSchema = z.object({ + ...defaultProps.pick({ + backgroundColor: true, + }).shape, + ...baseFilePropSchema.shape, + ...optionalFilePropFields.pick({ + url: true, + showPreview: true, + previewWidth: true, + }).shape, +}); export const audioBlockConfig = { type: "audio" as const, propSchema: audioPropSchema, - content: "none", + content: "none" as const, isFileBlock: true, fileBlockAccept: ["audio/*"], } satisfies FileBlockConfig; @@ -76,7 +71,7 @@ export const audioRender = ( export const audioParse = ( element: HTMLElement, -): Partial> | undefined => { +): Partial> | undefined => { if (element.tagName === "AUDIO") { // Ignore if parent figure has already been parsed. if (element.closest("figure")) { diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts index e322a83be9..90f0e70aea 100644 --- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts +++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts @@ -3,14 +3,13 @@ import { InputRule, isTextSelection } from "@tiptap/core"; import { TextSelection } from "@tiptap/pm/state"; import { Parser, createHighlightPlugin } from "prosemirror-highlight"; import { createParser } from "prosemirror-highlight/shiki"; +import { z } from "zod/v4"; import { BlockNoteEditor } from "../../index.js"; import { - PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, } from "../../schema/index.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; - export type CodeBlockOptions = { /** * Whether to indent lines with a tab when the user presses `Tab` in a code block. @@ -66,11 +65,9 @@ export const shikiParserSymbol = Symbol.for("blocknote.shikiParser"); export const shikiHighlighterPromiseSymbol = Symbol.for( "blocknote.shikiHighlighterPromise", ); -export const defaultCodeBlockPropSchema = { - language: { - default: "text", - }, -} satisfies PropSchema; +export const defaultCodeBlockPropSchema = z.object({ + language: z.string().default("text"), +}); const CodeBlockContent = createStronglyTypedTiptapNode({ name: "codeBlock", diff --git a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts index 433487d8e0..c3f85e1c85 100644 --- a/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts +++ b/packages/core/src/blocks/FileBlockContent/FileBlockContent.ts @@ -1,9 +1,9 @@ +import * as z from "zod/v4"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfig, - FileBlockConfig, - PropSchema, createBlockSpec, + FileBlockConfig, } from "../../schema/index.js"; import { defaultProps } from "../defaultProps.js"; import { parseEmbedElement } from "./helpers/parse/parseEmbedElement.js"; @@ -11,21 +11,29 @@ import { parseFigureElement } from "./helpers/parse/parseFigureElement.js"; import { createFileBlockWrapper } from "./helpers/render/createFileBlockWrapper.js"; import { createLinkWithCaption } from "./helpers/toExternalHTML/createLinkWithCaption.js"; -export const filePropSchema = { - backgroundColor: defaultProps.backgroundColor, - // File name. - name: { - default: "" as const, - }, - // File url. - url: { - default: "" as const, - }, - // File caption. - caption: { - default: "" as const, - }, -} satisfies PropSchema; +export const baseFilePropSchema = z.object({ + caption: z.string().default(""), + name: z.string().default(""), +}); + +export const optionalFilePropFields = z.object({ + // URL is optional, as we also want to accept files with no URL, but for example ids + // (ids can be used for files that are resolved on the backend) + url: z.string().default(""), + // Whether to show the file preview or the name only. + // This is useful for some file blocks, but not all + // (e.g.: not relevant for default "file" block which doesn;'t show previews) + showPreview: z.boolean().default(true), + // File preview width in px. + previewWidth: z.number(), +}); + +export const filePropSchema = defaultProps + .pick({ + backgroundColor: true, + }) + .extend(baseFilePropSchema.shape) + .extend(optionalFilePropFields.pick({ url: true }).shape); export const fileBlockConfig = { type: "file" as const, diff --git a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts index 8299892a03..8014e54b8c 100644 --- a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts @@ -1,8 +1,8 @@ import { InputRule } from "@tiptap/core"; +import * as z from "zod/v4"; import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { - PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, propsToAttributes, @@ -10,10 +10,9 @@ import { import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -export const headingPropSchema = { - ...defaultProps, - level: { default: 1, values: [1, 2, 3] as const }, -} satisfies PropSchema; +export const headingPropSchema = defaultProps.extend({ + level: z.number().int().min(1).max(3).default(1), +}); const HeadingBlockContent = createStronglyTypedTiptapNode({ name: "heading", diff --git a/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts index 32b7338640..9a4ec0e318 100644 --- a/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/blocks/ImageBlockContent/ImageBlockContent.ts @@ -1,46 +1,37 @@ +import z from "zod/v4"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfig, createBlockSpec, FileBlockConfig, - Props, - PropSchema, } from "../../schema/index.js"; import { defaultProps } from "../defaultProps.js"; import { parseFigureElement } from "../FileBlockContent/helpers/parse/parseFigureElement.js"; +import { createResizableFileBlockWrapper } from "../FileBlockContent/helpers/render/createResizableFileBlockWrapper.js"; import { createFigureWithCaption } from "../FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.js"; import { createLinkWithCaption } from "../FileBlockContent/helpers/toExternalHTML/createLinkWithCaption.js"; -import { createResizableFileBlockWrapper } from "../FileBlockContent/helpers/render/createResizableFileBlockWrapper.js"; import { parseImageElement } from "./parseImageElement.js"; export const FILE_IMAGE_ICON_SVG = ''; -export const imagePropSchema = { - textAlignment: defaultProps.textAlignment, - backgroundColor: defaultProps.backgroundColor, - // File name. - name: { - default: "" as const, - }, - // File url. - url: { - default: "" as const, - }, - // File caption. - caption: { - default: "" as const, - }, - - showPreview: { - default: true, - }, - // File preview width in px. - previewWidth: { - default: undefined, - type: "number", - }, -} satisfies PropSchema; +export const imagePropSchema = defaultProps + .pick({ + textAlignment: true, + backgroundColor: true, + }) + .extend({ + // File name. + name: z.string().default(""), + // File url. + url: z.string().default(""), + // File caption. + caption: z.string().default(""), + // Show preview. + showPreview: z.boolean().default(true), + // File preview width in px. + previewWidth: z.number().optional(), + }); export const imageBlockConfig = { type: "image" as const, @@ -87,7 +78,7 @@ export const imageRender = ( export const imageParse = ( element: HTMLElement, -): Partial> | undefined => { +): Partial> | undefined => { if (element.tagName === "IMG") { // Ignore if parent figure has already been parsed. if (element.closest("figure")) { diff --git a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index e6412633c4..3b23092949 100644 --- a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -2,7 +2,6 @@ import { InputRule } from "@tiptap/core"; import { updateBlockCommand } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromSelection } from "../../../api/getBlockInfoFromPos.js"; import { - PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, } from "../../../schema/index.js"; @@ -11,9 +10,7 @@ import { defaultProps } from "../../defaultProps.js"; import { getListItemContent } from "../getListItemContent.js"; import { handleEnter } from "../ListItemKeyboardShortcuts.js"; -export const bulletListItemPropSchema = { - ...defaultProps, -} satisfies PropSchema; +export const bulletListItemPropSchema = defaultProps; const BulletListItemBlockContent = createStronglyTypedTiptapNode({ name: "bulletListItem", diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts index 8ebf62aa63..9f2650cc56 100644 --- a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts @@ -1,11 +1,11 @@ import { InputRule } from "@tiptap/core"; +import * as z from "zod/v4"; import { updateBlockCommand } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromSelection, getNearestBlockPos, } from "../../../api/getBlockInfoFromPos.js"; import { - PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, propsToAttributes, @@ -15,12 +15,9 @@ import { defaultProps } from "../../defaultProps.js"; import { getListItemContent } from "../getListItemContent.js"; import { handleEnter } from "../ListItemKeyboardShortcuts.js"; -export const checkListItemPropSchema = { - ...defaultProps, - checked: { - default: false, - }, -} satisfies PropSchema; +export const checkListItemPropSchema = defaultProps.extend({ + checked: z.boolean().default(false), +}); const checkListItemBlockContent = createStronglyTypedTiptapNode({ name: "checkListItem", diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index 4e271bae14..598791d04b 100644 --- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -1,8 +1,8 @@ import { InputRule } from "@tiptap/core"; +import { z } from "zod/v4"; import { updateBlockCommand } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromSelection } from "../../../api/getBlockInfoFromPos.js"; import { - PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, propsToAttributes, @@ -13,10 +13,9 @@ import { getListItemContent } from "../getListItemContent.js"; import { handleEnter } from "../ListItemKeyboardShortcuts.js"; import { NumberedListIndexingPlugin } from "./NumberedListIndexingPlugin.js"; -export const numberedListItemPropSchema = { - ...defaultProps, - start: { default: undefined, type: "number" }, -} satisfies PropSchema; +export const numberedListItemPropSchema = defaultProps.extend({ + start: z.number().optional(), +}); const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ name: "numberedListItem", diff --git a/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts b/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts index c8343a30f3..178bfab5a2 100644 --- a/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts +++ b/packages/core/src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts @@ -1,12 +1,9 @@ -import { - createBlockSpec, - CustomBlockConfig, - Props, -} from "../../schema/index.js"; +import z from "zod/v4"; +import { createBlockSpec, CustomBlockConfig } from "../../schema/index.js"; export const pageBreakConfig = { type: "pageBreak" as const, - propSchema: {}, + propSchema: z.object(), content: "none", isFileBlock: false, isSelectable: false, @@ -23,7 +20,7 @@ export const pageBreakRender = () => { }; export const pageBreakParse = ( element: HTMLElement, -): Partial> | undefined => { +): Partial> | undefined => { if (element.tagName === "DIV" && element.hasAttribute("data-page-break")) { return { type: "pageBreak", diff --git a/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts index 0c35c117a7..ae3c6b401c 100644 --- a/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts @@ -7,9 +7,7 @@ import { import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -export const paragraphPropSchema = { - ...defaultProps, -}; +export const paragraphPropSchema = defaultProps; export const ParagraphBlockContent = createStronglyTypedTiptapNode({ name: "paragraph", diff --git a/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts b/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts index 3c13c56c2d..5e4dc501bd 100644 --- a/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts +++ b/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts @@ -1,16 +1,14 @@ +import { InputRule } from "@tiptap/core"; +import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; +import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, } from "../../schema/index.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; -import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; -import { InputRule } from "@tiptap/core"; -export const quotePropSchema = { - ...defaultProps, -}; +export const quotePropSchema = defaultProps; export const QuoteBlockContent = createStronglyTypedTiptapNode({ name: "quote", diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index 6d26b8ec54..06544f3964 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -13,9 +13,9 @@ import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; -export const tablePropSchema = { - textColor: defaultProps.textColor, -}; +export const tablePropSchema = defaultProps.pick({ + textColor: true, +}); export const TableBlockContent = createStronglyTypedTiptapNode({ name: "table", diff --git a/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts b/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts index af65e3d0df..45245fafaa 100644 --- a/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts +++ b/packages/core/src/blocks/VideoBlockContent/VideoBlockContent.ts @@ -1,46 +1,37 @@ +import z from "zod/v4"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfig, createBlockSpec, FileBlockConfig, - Props, - PropSchema, } from "../../schema/index.js"; import { defaultProps } from "../defaultProps.js"; import { parseFigureElement } from "../FileBlockContent/helpers/parse/parseFigureElement.js"; +import { createResizableFileBlockWrapper } from "../FileBlockContent/helpers/render/createResizableFileBlockWrapper.js"; import { createFigureWithCaption } from "../FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.js"; import { createLinkWithCaption } from "../FileBlockContent/helpers/toExternalHTML/createLinkWithCaption.js"; -import { createResizableFileBlockWrapper } from "../FileBlockContent/helpers/render/createResizableFileBlockWrapper.js"; import { parseVideoElement } from "./parseVideoElement.js"; export const FILE_VIDEO_ICON_SVG = ''; -export const videoPropSchema = { - textAlignment: defaultProps.textAlignment, - backgroundColor: defaultProps.backgroundColor, - // File name. - name: { - default: "" as const, - }, - // File url. - url: { - default: "" as const, - }, - // File caption. - caption: { - default: "" as const, - }, - - showPreview: { - default: true, - }, - // File preview width in px. - previewWidth: { - default: undefined, - type: "number", - }, -} satisfies PropSchema; +export const videoPropSchema = defaultProps + .pick({ + textAlignment: true, + backgroundColor: true, + }) + .extend({ + // File name. + name: z.string().default(""), + // File url. + url: z.string().default(""), + // File caption. + caption: z.string().default(""), + // Show preview. + showPreview: z.boolean().default(true), + // File preview width in px. + previewWidth: z.number().optional(), + }); export const videoBlockConfig = { type: "video" as const, @@ -72,7 +63,9 @@ export const videoRender = ( video.controls = true; video.contentEditable = "false"; video.draggable = false; - video.width = block.props.previewWidth; + if (block.props.previewWidth) { + video.width = block.props.previewWidth; + } videoWrapper.appendChild(video); return createResizableFileBlockWrapper( @@ -87,7 +80,7 @@ export const videoRender = ( export const videoParse = ( element: HTMLElement, -): Partial> | undefined => { +): Partial> | undefined => { if (element.tagName === "VIDEO") { // Ignore if parent figure has already been parsed. if (element.closest("figure")) { diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 5db988da9a..f0db7f38a0 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -1,4 +1,6 @@ +import { Selection } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; +import * as z from "zod/v4/core"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import { BlockFromConfig, @@ -15,8 +17,6 @@ import { defaultInlineContentSchema, } from "./defaultBlocks.js"; import { defaultProps } from "./defaultProps.js"; -import { Selection } from "prosemirror-state"; - export function checkDefaultBlockTypeInSchema< BlockType extends keyof DefaultBlockSchema, I extends InlineContentSchema, @@ -89,9 +89,10 @@ export function checkBlockIsFileBlockWithPreview< block: Block, editor: BlockNoteEditor, ): block is BlockFromConfig< - FileBlockConfig & { - propSchema: Required; - }, + // FileBlockConfig & { + // propSchema: Required; + // }, + any, // TODO I, S > { @@ -113,7 +114,7 @@ export function checkBlockIsFileBlockWithPlaceholder< } export function checkBlockTypeHasDefaultProp< - Prop extends keyof typeof defaultProps, + Prop extends keyof typeof defaultProps.def.shape, I extends InlineContentSchema, S extends StyleSchema, >( @@ -124,9 +125,9 @@ export function checkBlockTypeHasDefaultProp< { [BT in string]: { type: BT; - propSchema: { - [P in Prop]: (typeof defaultProps)[P]; - }; + propSchema: z.$ZodObject<{ + [P in Prop]: (typeof defaultProps.def.shape)[P]; + }>; content: "table" | "inline" | "none"; }; }, @@ -136,12 +137,13 @@ export function checkBlockTypeHasDefaultProp< return ( blockType in editor.schema.blockSchema && prop in editor.schema.blockSchema[blockType].propSchema && - editor.schema.blockSchema[blockType].propSchema[prop] === defaultProps[prop] + editor.schema.blockSchema[blockType].propSchema[prop] === + defaultProps.def.shape[prop] ); } export function checkBlockHasDefaultProp< - Prop extends keyof typeof defaultProps, + Prop extends keyof typeof defaultProps.def.shape, I extends InlineContentSchema, S extends StyleSchema, >( @@ -149,13 +151,14 @@ export function checkBlockHasDefaultProp< block: Block, editor: BlockNoteEditor, ): block is BlockFromConfig< - { - type: string; - propSchema: { - [P in Prop]: (typeof defaultProps)[P]; - }; - content: "table" | "inline" | "none"; - }, + // { + // type: string; + // propSchema: { + // [P in Prop]: (typeof defaultProps)[P]; + // }; + // content: "table" | "inline" | "none"; + // }, + any, // TODO I, S > { diff --git a/packages/core/src/blocks/defaultProps.ts b/packages/core/src/blocks/defaultProps.ts index 4fb0b838c8..5b807c4597 100644 --- a/packages/core/src/blocks/defaultProps.ts +++ b/packages/core/src/blocks/defaultProps.ts @@ -1,22 +1,14 @@ -import type { Props, PropSchema } from "../schema/index.js"; - +import * as z from "zod/v4"; // TODO: this system should probably be moved / refactored. // The dependency from schema on this file doesn't make sense -export const defaultProps = { - backgroundColor: { - default: "default" as const, - }, - textColor: { - default: "default" as const, - }, - textAlignment: { - default: "left" as const, - values: ["left", "center", "right", "justify"] as const, - }, -} satisfies PropSchema; +export const defaultProps = z.object({ + backgroundColor: z.string().default("default"), + textColor: z.string().default("default"), + textAlignment: z.enum(["left", "center", "right", "justify"]).default("left"), +}); -export type DefaultProps = Props; +export type DefaultProps = z.infer; // Default props which are set on `blockContainer` nodes rather than // `blockContent` nodes. Ensures that they are not redundantly added to diff --git a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts index fca4922a1a..95e5137553 100644 --- a/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts +++ b/packages/core/src/extensions/BackgroundColor/BackgroundColorExtension.ts @@ -10,15 +10,15 @@ export const BackgroundColorExtension = Extension.create({ types: ["blockContainer", "tableCell", "tableHeader"], attributes: { backgroundColor: { - default: defaultProps.backgroundColor.default, + default: defaultProps.shape.backgroundColor._zod.def.defaultValue, parseHTML: (element) => element.hasAttribute("data-background-color") ? element.getAttribute("data-background-color") - : defaultProps.backgroundColor.default, + : defaultProps.shape.backgroundColor._zod.def.defaultValue, renderHTML: (attributes) => { if ( attributes.backgroundColor === - defaultProps.backgroundColor.default + defaultProps.shape.backgroundColor._zod.def.defaultValue ) { return {}; } diff --git a/packages/core/src/extensions/TextColor/TextColorExtension.ts b/packages/core/src/extensions/TextColor/TextColorExtension.ts index 4060fea6d6..d3a1934e89 100644 --- a/packages/core/src/extensions/TextColor/TextColorExtension.ts +++ b/packages/core/src/extensions/TextColor/TextColorExtension.ts @@ -10,13 +10,16 @@ export const TextColorExtension = Extension.create({ types: ["blockContainer", "tableCell", "tableHeader"], attributes: { textColor: { - default: defaultProps.textColor.default, + default: defaultProps.shape.textColor._zod.def.defaultValue, parseHTML: (element) => element.hasAttribute("data-text-color") ? element.getAttribute("data-text-color") - : defaultProps.textColor.default, + : defaultProps.shape.textColor._zod.def.defaultValue, renderHTML: (attributes) => { - if (attributes.textColor === defaultProps.textColor.default) { + if ( + attributes.textColor === + defaultProps.shape.textColor._zod.def.defaultValue + ) { return {}; } return { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 312e7dd537..8c35514363 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,5 +78,6 @@ export * from "./api/parsers/html/parseHTML.js"; export * from "./api/parsers/markdown/parseMarkdown.js"; // TODO: for ai, remove? +export type * from "zod/v4"; export * from "./api/blockManipulation/getBlock/getBlock.js"; export * from "./api/positionMapping.js"; diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index d3749ab53a..b807b65dd0 100644 --- a/packages/core/src/schema/blocks/internal.ts +++ b/packages/core/src/schema/blocks/internal.ts @@ -6,13 +6,13 @@ import { Node, NodeConfig, } from "@tiptap/core"; +import * as z from "zod/v4/core"; import { defaultBlockToHTML } from "../../blocks/defaultBlockHelpers.js"; import { inheritedProps } from "../../blocks/defaultProps.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { mergeCSSClasses } from "../../util/browser.js"; import { camelToDataKebab } from "../../util/string.js"; import { InlineContentSchema } from "../inlineContent/types.js"; -import { PropSchema, Props } from "../propTypes.js"; import { StyleSchema } from "../styles/types.js"; import { BlockConfig, @@ -26,15 +26,18 @@ import { // Function that uses the 'propSchema' of a blockConfig to create a TipTap // node's `addAttributes` property. -// TODO: extract function -export function propsToAttributes(propSchema: PropSchema): Attributes { +// TODO: extract function0 +export function propsToAttributes(propSchema: z.$ZodObject): Attributes { const tiptapAttributes: Record = {}; - Object.entries(propSchema) + Object.entries(propSchema._zod.def.shape) .filter(([name, _spec]) => !inheritedProps.includes(name)) .forEach(([name, spec]) => { + const def = + spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined; + tiptapAttributes[name] = { - default: spec.default, + default: def, keepOnSplit: true, // Props are displayed in kebab-case as HTML attributes. If a prop's // value is the same as its default, we don't display an HTML @@ -46,41 +49,19 @@ export function propsToAttributes(propSchema: PropSchema): Attributes { return null; } - if ( - (spec.default === undefined && spec.type === "boolean") || - (spec.default !== undefined && typeof spec.default === "boolean") - ) { - if (value === "true") { - return true; - } - - if (value === "false") { - return false; - } - - return null; + // TBD: this might not be fault proof, but it's also ugly to store prop=""..."" for strings + try { + const jsonValue = JSON.parse(value); + // it was a number / boolean / json object stored as attribute + return z.parse(spec, jsonValue); + } catch (e) { + // it might have been a string directly stored as attribute + return z.parse(spec, value); } - - if ( - (spec.default === undefined && spec.type === "number") || - (spec.default !== undefined && typeof spec.default === "number") - ) { - const asNumber = parseFloat(value); - const isNumeric = - !Number.isNaN(asNumber) && Number.isFinite(asNumber); - - if (isNumeric) { - return asNumber; - } - - return null; - } - - return value; }, renderHTML: (attributes) => { // don't render to html if the value is the same as the default - return attributes[name] !== spec.default + return attributes[name] !== def ? { [camelToDataKebab(name)]: attributes[name], } @@ -142,7 +123,7 @@ export function getBlockFromPos< // an `inlineContent` class to it. export function wrapInBlockStructure< BType extends string, - PSchema extends PropSchema, + PSchema extends z.$ZodObject, >( element: { dom: HTMLElement; @@ -150,7 +131,7 @@ export function wrapInBlockStructure< destroy?: () => void; }, blockType: BType, - blockProps: Props, + blockProps: z.output, propSchema: PSchema, isFileBlock = false, domAttributes?: Record, @@ -181,10 +162,18 @@ export function wrapInBlockStructure< // which are already added as HTML attributes to the parent `blockContent` // element (inheritedProps) and props set to their default values. for (const [prop, value] of Object.entries(blockProps)) { - const spec = propSchema[prop]; - const defaultValue = spec.default; + const spec = propSchema._zod.def.shape[prop]; + const defaultValue = + spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined; if (!inheritedProps.includes(prop) && value !== defaultValue) { - blockContent.setAttribute(camelToDataKebab(prop), value); + if (typeof value === "string") { + blockContent.setAttribute(camelToDataKebab(prop), value); + } else { + blockContent.setAttribute( + camelToDataKebab(prop), + JSON.stringify(value), + ); + } } } // Adds file block attribute @@ -249,7 +238,7 @@ export function createInternalBlockSpec( export function createBlockSpecFromStronglyTypedTiptapNode< T extends Node, - P extends PropSchema, + P extends z.$ZodObject, >(node: T, propSchema: P, requiredExtensions?: Array) { return createInternalBlockSpec( { diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 0f97205638..1889c1afee 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -1,13 +1,14 @@ /** Define the main block types **/ import type { Extension, Node } from "@tiptap/core"; - +import * as z from "zod/v4"; +import * as zCore from "zod/v4/core"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import type { InlineContent, InlineContentSchema, PartialInlineContent, } from "../inlineContent/types.js"; -import type { PropSchema, Props } from "../propTypes.js"; + import type { StyleSchema } from "../styles/types.js"; export type BlockNoteDOMElement = @@ -21,34 +22,23 @@ export type BlockNoteDOMAttributes = Partial<{ [DOMElement in BlockNoteDOMElement]: Record; }>; +const filePropSchema = z.looseObject({ + caption: z.string().default(""), + name: z.string().default(""), + // URL is optional, as we also want to accept files with no URL, but for example ids + // (ids can be used for files that are resolved on the backend) + // url: z.string().default("").optional(), + // // Whether to show the file preview or the name only. + // // This is useful for some file blocks, but not all + // // (e.g.: not relevant for default "file" block which doesn;'t show previews) + // showPreview: z.boolean().default(true).optional(), + // // File preview width in px. + // previewWidth: z.number().optional(), +}); + export type FileBlockConfig = { type: string; - readonly propSchema: PropSchema & { - caption: { - default: ""; - }; - name: { - default: ""; - }; - - // URL is optional, as we also want to accept files with no URL, but for example ids - // (ids can be used for files that are resolved on the backend) - url?: { - default: ""; - }; - - // Whether to show the file preview or the name only. - // This is useful for some file blocks, but not all - // (e.g.: not relevant for default "file" block which doesn;'t show previews) - showPreview?: { - default: boolean; - }; - // File preview width in px. - previewWidth?: { - default: undefined; - type: "number"; - }; - }; + readonly propSchema: typeof filePropSchema; content: "none"; isSelectable?: boolean; isFileBlock: true; @@ -60,7 +50,7 @@ export type FileBlockConfig = { export type BlockConfig = | { type: string; - readonly propSchema: PropSchema; + readonly propSchema: zCore.$ZodObject; content: "inline" | "none" | "table"; isSelectable?: boolean; isFileBlock?: false; @@ -186,7 +176,7 @@ export type BlockFromConfigNoChildren< > = { id: string; type: B["type"]; - props: Props; + props: z.output; content: B["content"] extends "inline" ? InlineContent[] : B["content"] extends "table" @@ -270,7 +260,7 @@ type PartialBlockFromConfigNoChildren< > = { id?: string; type?: B["type"]; - props?: Partial>; + props?: Partial>; content?: B["content"] extends "inline" ? PartialInlineContent : B["content"] extends "table" diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 7f7cafc561..cf987a86e0 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -4,7 +4,6 @@ export * from "./blocks/types.js"; export * from "./inlineContent/createSpec.js"; export * from "./inlineContent/internal.js"; export * from "./inlineContent/types.js"; -export * from "./propTypes.js"; export * from "./styles/createSpec.js"; export * from "./styles/internal.js"; export * from "./styles/types.js"; diff --git a/packages/core/src/schema/inlineContent/createSpec.ts b/packages/core/src/schema/inlineContent/createSpec.ts index 9168c1207b..4bd7d248f2 100644 --- a/packages/core/src/schema/inlineContent/createSpec.ts +++ b/packages/core/src/schema/inlineContent/createSpec.ts @@ -5,7 +5,8 @@ import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js"; import { nodeToCustomInlineContent } from "../../api/nodeConversions/nodeToBlock.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { propsToAttributes } from "../blocks/internal.js"; -import { Props } from "../propTypes.js"; + +import * as z from "zod/v4/core"; import { StyleSchema } from "../styles/types.js"; import { addInlineContentAttributes, @@ -18,7 +19,6 @@ import { InlineContentSpec, PartialCustomInlineContentFromConfig, } from "./types.js"; - // TODO: support serialization export type CustomInlineContentImplementation< @@ -116,7 +116,7 @@ export function createInlineContentSpec< return addInlineContentAttributes( output, inlineContentConfig.type, - node.attrs as Props, + node.attrs as z.infer, inlineContentConfig.propSchema, ); }, @@ -148,7 +148,7 @@ export function createInlineContentSpec< return addInlineContentAttributes( output, inlineContentConfig.type, - node.attrs as Props, + node.attrs as z.infer, inlineContentConfig.propSchema, ); }; diff --git a/packages/core/src/schema/inlineContent/internal.ts b/packages/core/src/schema/inlineContent/internal.ts index 3e438d7cfb..1039c6f7f0 100644 --- a/packages/core/src/schema/inlineContent/internal.ts +++ b/packages/core/src/schema/inlineContent/internal.ts @@ -1,7 +1,8 @@ import { KeyboardShortcutCommand, Node } from "@tiptap/core"; import { camelToDataKebab } from "../../util/string.js"; -import { PropSchema, Props } from "../propTypes.js"; + +import * as z from "zod/v4/core"; import { CustomInlineContentConfig, InlineContentConfig, @@ -10,21 +11,20 @@ import { InlineContentSpec, InlineContentSpecs, } from "./types.js"; - // 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, + PSchema extends z.$ZodObject, >( element: { dom: HTMLElement; contentDOM?: HTMLElement; }, inlineContentType: IType, - inlineContentProps: Props, - propSchema: PSchema, + inlineContentProps: z.infer, + propSchema: z.$ZodObject, ): { dom: HTMLElement; contentDOM?: HTMLElement; @@ -35,13 +35,17 @@ export function addInlineContentAttributes< // set to their default values. Object.entries(inlineContentProps) .filter(([prop, value]) => { - const spec = propSchema[prop]; - return value !== spec.default; + const spec = propSchema._zod.def.shape[prop]; + const defaultValue = + spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined; + return value !== defaultValue; }) .map(([prop, value]) => { - return [camelToDataKebab(prop), value]; + return [camelToDataKebab(prop), value] satisfies [string, unknown]; }) - .forEach(([prop, value]) => element.dom.setAttribute(prop, value)); + .forEach(([prop, value]) => + element.dom.setAttribute(prop, JSON.stringify(value)), + ); if (element.contentDOM !== undefined) { element.contentDOM.setAttribute("data-editable", ""); @@ -85,7 +89,7 @@ export function createInternalInlineContentSpec( export function createInlineContentSpecFromTipTapNode< T extends Node, - P extends PropSchema, + P extends z.$ZodObject, >(node: T, propSchema: P) { return createInternalInlineContentSpec( { diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts index 6ec87055d4..a975923899 100644 --- a/packages/core/src/schema/inlineContent/types.ts +++ b/packages/core/src/schema/inlineContent/types.ts @@ -1,11 +1,11 @@ import { Node } from "@tiptap/core"; -import { PropSchema, Props } from "../propTypes.js"; -import { StyleSchema, Styles } from "../styles/types.js"; +import * as z from "zod/v4/core"; +import { StyleSchema, Styles } from "../styles/types.js"; export type CustomInlineContentConfig = { type: string; content: "styled" | "none"; // | "plain" - readonly propSchema: PropSchema; + readonly propSchema: z.$ZodObject; // content: "inline" | "none" | "table"; }; // InlineContentConfig contains the "schema" info about an InlineContent type @@ -47,7 +47,7 @@ export type CustomInlineContentFromConfig< S extends StyleSchema, > = { type: I["type"]; - props: Props; + props: z.infer; content: I["content"] extends "styled" ? StyledText[] : I["content"] extends "plain" @@ -73,7 +73,7 @@ export type PartialCustomInlineContentFromConfig< S extends StyleSchema, > = { type: I["type"]; - props?: Props; + props?: Partial>; content?: I["content"] extends "styled" ? StyledText[] | string : I["content"] extends "plain" diff --git a/packages/core/src/schema/propTypes.ts b/packages/core/src/schema/propTypes.ts deleted file mode 100644 index 76a8df2769..0000000000 --- a/packages/core/src/schema/propTypes.ts +++ /dev/null @@ -1,55 +0,0 @@ -// The PropSpec specifies the type of a prop and possibly a default value. -// Note that props are always optional when used as "input" -// (i.e., when creating a PartialBlock, for example by calling `insertBlocks({...})`) -// -// However, internally they're always set to `default`, unless a prop is marked optional -// -// At some point we should migrate this to zod or effect-schema -export type PropSpec = - | { - // We infer the type of the prop from the default value - default: PType; - // a list of possible values, for example for a string prop (this will then be used as a string union type) - values?: readonly PType[]; - } - | { - default: undefined; - // Because there is no default value (for an optional prop, the default value is undefined), - // we need to specify the type of the prop manually (we can't infer it from the default value) - type: "string" | "number" | "boolean"; - values?: readonly PType[]; - }; - -// Defines multiple block prop specs. The key of each prop is the name of the -// prop, while the value is a corresponding prop spec. This should be included -// in a block config or schema. From a prop schema, we can derive both the props' -// internal implementation (as TipTap node attributes) and the type information -// for the external API. -export type PropSchema = Record>; - -// Defines Props objects for use in Block objects in the external API. Converts -// each prop spec into a union type of its possible values, or a string if no -// values are specified. -export type Props = { - // for required props, get type from type of "default" value, - // and if values are specified, get type from values - [PName in keyof PSchema]: ( - PSchema[PName] extends { default: boolean } | { type: "boolean" } - ? PSchema[PName]["values"] extends readonly boolean[] - ? PSchema[PName]["values"][number] - : boolean - : PSchema[PName] extends { default: number } | { type: "number" } - ? PSchema[PName]["values"] extends readonly number[] - ? PSchema[PName]["values"][number] - : number - : PSchema[PName] extends { default: string } | { type: "string" } - ? PSchema[PName]["values"] extends readonly string[] - ? PSchema[PName]["values"][number] - : string - : never - ) extends infer T - ? PSchema[PName] extends { optional: true } - ? T | undefined - : T - : never; -}; diff --git a/packages/react/package.json b/packages/react/package.json index 68c50eec8f..e9dcad935f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -66,7 +66,8 @@ "@tiptap/react": "^2.12.0", "emoji-mart": "^5.6.0", "lodash.merge": "^4.6.2", - "react-icons": "^5.2.1" + "react-icons": "^5.2.1", + "zod": "^3.25.36" }, "devDependencies": { "@types/emoji-mart": "^3.0.14", diff --git a/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx b/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx index b602aaa971..46914e61e2 100644 --- a/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx +++ b/packages/react/src/blocks/AudioBlockContent/AudioBlockContent.tsx @@ -1,4 +1,4 @@ -import { FileBlockConfig, audioBlockConfig, audioParse } from "@blocknote/core"; +import { audioBlockConfig, audioParse } from "@blocknote/core"; import { RiVolumeUpFill } from "react-icons/ri"; @@ -6,18 +6,18 @@ import { ReactCustomBlockRenderProps, createReactBlockSpec, } from "../../schema/ReactBlockSpec.js"; -import { useResolveUrl } from "../FileBlockContent/useResolveUrl.js"; -import { FigureWithCaption } from "../FileBlockContent/helpers/toExternalHTML/FigureWithCaption.js"; import { FileBlockWrapper } from "../FileBlockContent/helpers/render/FileBlockWrapper.js"; +import { FigureWithCaption } from "../FileBlockContent/helpers/toExternalHTML/FigureWithCaption.js"; import { LinkWithCaption } from "../FileBlockContent/helpers/toExternalHTML/LinkWithCaption.js"; +import { useResolveUrl } from "../FileBlockContent/useResolveUrl.js"; export const AudioPreview = ( props: Omit< - ReactCustomBlockRenderProps, + ReactCustomBlockRenderProps, "contentRef" >, ) => { - const resolved = useResolveUrl(props.block.props.url!); + const resolved = useResolveUrl(props.block.props.url); return (