From ff8eed64833014af6f002a995016f88f82132b45 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 9 Jan 2025 04:56:24 -0500 Subject: [PATCH] feat(core): support `validate` options in node and mark attribute definitions. (#5991) ProseMirror allows node and mark specs to define a `validate` function or type, which is used when deserializing JSON, parsing from the DOM, etc. See https://prosemirror.net/docs/ref/#model.AttributeSpec.validate Currently, `default` is the only attribute spec option passed through to ProseMirror when resolving the schema. This change updates `getAttributesFromExtensions` and `getSchemaByResolvedExtensions` (and relevant types) to also pass through any defined `validate` attribute option. To use this, add a `validate` type or function option in any extension's `addAttributes` or `addGlobalAttributes` definition, e.g.: ``` export const NodeId = Extension.create({ name: 'nodeId', addGlobalAttributes() { return [ { types: [...], attributes: { nodeId: { default: '__node_id__', validate: 'string', ... } } } ] } }); ``` A more complex validation could ensure that the ID is shaped like a UUID, or allow it to be `null` or `undefined`. Co-authored-by: Nick Perez --- .changeset/nervous-hairs-walk.md | 5 +++++ packages/core/src/helpers/getAttributesFromExtensions.ts | 3 ++- packages/core/src/helpers/getSchemaByResolvedExtensions.ts | 4 ++-- packages/core/src/types.ts | 3 ++- 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/nervous-hairs-walk.md diff --git a/.changeset/nervous-hairs-walk.md b/.changeset/nervous-hairs-walk.md new file mode 100644 index 000000000..c7d5e4b80 --- /dev/null +++ b/.changeset/nervous-hairs-walk.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": minor +--- + +Support `validate` options in node and mark attribute definitions. diff --git a/packages/core/src/helpers/getAttributesFromExtensions.ts b/packages/core/src/helpers/getAttributesFromExtensions.ts index 9d2650807..4b6638a30 100644 --- a/packages/core/src/helpers/getAttributesFromExtensions.ts +++ b/packages/core/src/helpers/getAttributesFromExtensions.ts @@ -11,8 +11,9 @@ export function getAttributesFromExtensions(extensions: Extensions): ExtensionAt const extensionAttributes: ExtensionAttribute[] = [] const { nodeExtensions, markExtensions } = splitExtensions(extensions) const nodeAndMarkExtensions = [...nodeExtensions, ...markExtensions] - const defaultAttribute: Required = { + const defaultAttribute: Required> & Pick = { default: null, + validate: undefined, rendered: true, renderHTML: null, parseHTML: null, diff --git a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts index d8585425e..074b1b62d 100644 --- a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts +++ b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts @@ -71,7 +71,7 @@ export function getSchemaByResolvedExtensions(extensions: Extensions, editor?: E isolating: callOrReturn(getExtensionField(extension, 'isolating', context)), attrs: Object.fromEntries( extensionAttributes.map(extensionAttribute => { - return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] + return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default, validate: extensionAttribute?.attribute?.validate }] }), ), }) @@ -132,7 +132,7 @@ export function getSchemaByResolvedExtensions(extensions: Extensions, editor?: E code: callOrReturn(getExtensionField(extension, 'code', context)), attrs: Object.fromEntries( extensionAttributes.map(extensionAttribute => { - return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] + return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default, validate: extensionAttribute?.attribute?.validate }] }), ), }) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index fd6e3221f..690f8e69c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -225,6 +225,7 @@ export type KeyboardShortcutCommand = (props: { editor: Editor }) => boolean export type Attribute = { default?: any + validate?: string | ((value: any) => void) rendered?: boolean renderHTML?: ((attributes: Record) => Record | null) | null parseHTML?: ((element: HTMLElement) => any | null) | null @@ -239,7 +240,7 @@ export type Attributes = { export type ExtensionAttribute = { type: string name: string - attribute: Required + attribute: Required> & Pick } export type GlobalAttributes = {