@tiptap/core 2.5.0-beta.0 → 2.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,10 @@ import { ExtensionManager } from './ExtensionManager.js';
6
6
  import { NodePos } from './NodePos.js';
7
7
  import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
8
8
  export * as extensions from './extensions/index.js';
9
- export interface HTMLElement {
10
- editor?: Editor;
9
+ declare global {
10
+ interface HTMLElement {
11
+ editor?: Editor;
12
+ }
11
13
  }
12
14
  export declare class Editor extends EventEmitter<EditorEvents> {
13
15
  private commandManager;
@@ -28,8 +28,18 @@ declare module '@tiptap/core' {
28
28
  * Whether to update the selection after inserting the content.
29
29
  */
30
30
  updateSelection?: boolean;
31
+ /**
32
+ * Whether to apply input rules after inserting the content.
33
+ */
31
34
  applyInputRules?: boolean;
35
+ /**
36
+ * Whether to apply paste rules after inserting the content.
37
+ */
32
38
  applyPasteRules?: boolean;
39
+ /**
40
+ * Whether to throw an error if the content is invalid.
41
+ */
42
+ errorOnInvalidContent?: boolean;
33
43
  }) => ReturnType;
34
44
  };
35
45
  }
@@ -24,7 +24,16 @@ declare module '@tiptap/core' {
24
24
  * Options for parsing the content.
25
25
  * @default {}
26
26
  */
27
- parseOptions?: ParseOptions) => ReturnType;
27
+ parseOptions?: ParseOptions,
28
+ /**
29
+ * Options for `setContent`.
30
+ */
31
+ options?: {
32
+ /**
33
+ * Whether to throw an error if the content is invalid.
34
+ */
35
+ errorOnInvalidContent?: boolean;
36
+ }) => ReturnType;
28
37
  };
29
38
  }
30
39
  }
@@ -7,4 +7,6 @@ import { Content } from '../types.js';
7
7
  * @param parseOptions Options for the parser
8
8
  * @returns The created Prosemirror document node
9
9
  */
10
- export declare function createDocument(content: Content, schema: Schema, parseOptions?: ParseOptions): ProseMirrorNode;
10
+ export declare function createDocument(content: Content, schema: Schema, parseOptions?: ParseOptions, options?: {
11
+ errorOnInvalidContent?: boolean;
12
+ }): ProseMirrorNode;
@@ -3,6 +3,7 @@ import { Content } from '../types.js';
3
3
  export declare type CreateNodeFromContentOptions = {
4
4
  slice?: boolean;
5
5
  parseOptions?: ParseOptions;
6
+ errorOnInvalidContent?: boolean;
6
7
  };
7
8
  /**
8
9
  * Takes a JSON or HTML content and creates a Prosemirror node or fragment from it.
@@ -23,6 +23,15 @@ export interface EditorEvents {
23
23
  create: {
24
24
  editor: Editor;
25
25
  };
26
+ contentError: {
27
+ editor: Editor;
28
+ error: Error;
29
+ /**
30
+ * If called, will re-initialize the editor with the collaboration extension removed.
31
+ * This will prevent syncing back deletions of content not present in the current schema.
32
+ */
33
+ disableCollaboration: () => void;
34
+ };
26
35
  update: {
27
36
  editor: Editor;
28
37
  transaction: Transaction;
@@ -66,8 +75,20 @@ export interface EditorOptions {
66
75
  enableInputRules: EnableRules;
67
76
  enablePasteRules: EnableRules;
68
77
  enableCoreExtensions: boolean;
78
+ /**
79
+ * If `true`, the editor will check the content for errors on initialization.
80
+ * Emitting the `contentError` event if the content is invalid.
81
+ * Which can be used to show a warning or error message to the user.
82
+ * @default false
83
+ */
84
+ enableContentCheck: boolean;
69
85
  onBeforeCreate: (props: EditorEvents['beforeCreate']) => void;
70
86
  onCreate: (props: EditorEvents['create']) => void;
87
+ /**
88
+ * Called when the editor encounters an error while parsing the content.
89
+ * Only enabled if `enableContentCheck` is `true`.
90
+ */
91
+ onContentError: (props: EditorEvents['contentError']) => void;
71
92
  onUpdate: (props: EditorEvents['update']) => void;
72
93
  onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void;
73
94
  onTransaction: (props: EditorEvents['transaction']) => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/core",
3
3
  "description": "headless rich text editor",
4
- "version": "2.5.0-beta.0",
4
+ "version": "2.5.0-beta.1",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -32,7 +32,7 @@
32
32
  "dist"
33
33
  ],
34
34
  "devDependencies": {
35
- "@tiptap/pm": "^2.5.0-beta.0"
35
+ "@tiptap/pm": "^2.5.0-beta.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tiptap/pm": "^2.0.0"
package/src/Editor.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import {
2
- MarkType, NodeType, Schema,
2
+ MarkType,
3
+ Node as ProseMirrorNode,
4
+ NodeType,
5
+ Schema,
3
6
  } from '@tiptap/pm/model'
4
7
  import {
5
8
  EditorState, Plugin, PluginKey, Transaction,
@@ -36,8 +39,10 @@ import { isFunction } from './utilities/isFunction.js'
36
39
 
37
40
  export * as extensions from './extensions/index.js'
38
41
 
39
- export interface HTMLElement {
40
- editor?: Editor
42
+ declare global {
43
+ interface HTMLElement {
44
+ editor?: Editor;
45
+ }
41
46
  }
42
47
 
43
48
  export class Editor extends EventEmitter<EditorEvents> {
@@ -69,6 +74,7 @@ export class Editor extends EventEmitter<EditorEvents> {
69
74
  enableInputRules: true,
70
75
  enablePasteRules: true,
71
76
  enableCoreExtensions: true,
77
+ enableContentCheck: false,
72
78
  onBeforeCreate: () => null,
73
79
  onCreate: () => null,
74
80
  onUpdate: () => null,
@@ -77,6 +83,7 @@ export class Editor extends EventEmitter<EditorEvents> {
77
83
  onFocus: () => null,
78
84
  onBlur: () => null,
79
85
  onDestroy: () => null,
86
+ onContentError: ({ error }) => { throw error },
80
87
  }
81
88
 
82
89
  constructor(options: Partial<EditorOptions> = {}) {
@@ -87,6 +94,7 @@ export class Editor extends EventEmitter<EditorEvents> {
87
94
  this.createSchema()
88
95
  this.on('beforeCreate', this.options.onBeforeCreate)
89
96
  this.emit('beforeCreate', { editor: this })
97
+ this.on('contentError', this.options.onContentError)
90
98
  this.createView()
91
99
  this.injectCSS()
92
100
  this.on('create', this.options.onCreate)
@@ -276,7 +284,40 @@ export class Editor extends EventEmitter<EditorEvents> {
276
284
  * Creates a ProseMirror view.
277
285
  */
278
286
  private createView(): void {
279
- const doc = createDocument(this.options.content, this.schema, this.options.parseOptions)
287
+ let doc: ProseMirrorNode
288
+
289
+ try {
290
+ doc = createDocument(
291
+ this.options.content,
292
+ this.schema,
293
+ this.options.parseOptions,
294
+ { errorOnInvalidContent: this.options.enableContentCheck },
295
+ )
296
+ } catch (e) {
297
+ if (!(e instanceof Error) || !['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)) {
298
+ // Not the content error we were expecting
299
+ throw e
300
+ }
301
+ this.emit('contentError', {
302
+ editor: this,
303
+ error: e as Error,
304
+ disableCollaboration: () => {
305
+ // To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
306
+ this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration')
307
+
308
+ // Restart the initialization process by recreating the extension manager with the new set of extensions
309
+ this.createExtensionManager()
310
+ },
311
+ })
312
+
313
+ // Content is invalid, but attempt to create it anyway, stripping out the invalid parts
314
+ doc = createDocument(
315
+ this.options.content,
316
+ this.schema,
317
+ this.options.parseOptions,
318
+ { errorOnInvalidContent: false },
319
+ )
320
+ }
280
321
  const selection = resolveFocusPosition(doc, this.options.autofocus)
281
322
 
282
323
  this.view = new EditorView(this.options.element, {
@@ -35,8 +35,21 @@ declare module '@tiptap/core' {
35
35
  * Whether to update the selection after inserting the content.
36
36
  */
37
37
  updateSelection?: boolean
38
+
39
+ /**
40
+ * Whether to apply input rules after inserting the content.
41
+ */
38
42
  applyInputRules?: boolean
43
+
44
+ /**
45
+ * Whether to apply paste rules after inserting the content.
46
+ */
39
47
  applyPasteRules?: boolean
48
+
49
+ /**
50
+ * Whether to throw an error if the content is invalid.
51
+ */
52
+ errorOnInvalidContent?: boolean
40
53
  },
41
54
  ) => ReturnType
42
55
  }
@@ -57,12 +70,19 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
57
70
  ...options,
58
71
  }
59
72
 
60
- const content = createNodeFromContent(value, editor.schema, {
61
- parseOptions: {
62
- preserveWhitespace: 'full',
63
- ...options.parseOptions,
64
- },
65
- })
73
+ let content: Fragment | ProseMirrorNode
74
+
75
+ try {
76
+ content = createNodeFromContent(value, editor.schema, {
77
+ parseOptions: {
78
+ preserveWhitespace: 'full',
79
+ ...options.parseOptions,
80
+ },
81
+ errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
82
+ })
83
+ } catch (e) {
84
+ return false
85
+ }
66
86
 
67
87
  // don’t dispatch an empty fragment because this can lead to strange errors
68
88
  if (content.toString() === '<>') {
@@ -1,4 +1,4 @@
1
- import { ParseOptions } from '@tiptap/pm/model'
1
+ import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
2
2
 
3
3
  import { createDocument } from '../helpers/createDocument.js'
4
4
  import { Content, RawCommands } from '../types.js'
@@ -30,14 +30,32 @@ declare module '@tiptap/core' {
30
30
  * @default {}
31
31
  */
32
32
  parseOptions?: ParseOptions,
33
+ /**
34
+ * Options for `setContent`.
35
+ */
36
+ options?: {
37
+ /**
38
+ * Whether to throw an error if the content is invalid.
39
+ */
40
+ errorOnInvalidContent?: boolean
41
+ },
33
42
  ) => ReturnType
34
43
  }
35
44
  }
36
45
  }
37
46
 
38
- export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}) => ({ tr, editor, dispatch }) => {
47
+ export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}, options = {}) => ({ tr, editor, dispatch }) => {
39
48
  const { doc } = tr
40
- const document = createDocument(content, editor.schema, parseOptions)
49
+
50
+ let document: Fragment | ProseMirrorNode
51
+
52
+ try {
53
+ document = createDocument(content, editor.schema, parseOptions, {
54
+ errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
55
+ })
56
+ } catch (e) {
57
+ return false
58
+ }
41
59
 
42
60
  if (dispatch) {
43
61
  tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
@@ -14,6 +14,11 @@ export function createDocument(
14
14
  content: Content,
15
15
  schema: Schema,
16
16
  parseOptions: ParseOptions = {},
17
+ options: { errorOnInvalidContent?: boolean } = {},
17
18
  ): ProseMirrorNode {
18
- return createNodeFromContent(content, schema, { slice: false, parseOptions }) as ProseMirrorNode
19
+ return createNodeFromContent(content, schema, {
20
+ slice: false,
21
+ parseOptions,
22
+ errorOnInvalidContent: options.errorOnInvalidContent,
23
+ }) as ProseMirrorNode
19
24
  }
@@ -12,6 +12,7 @@ import { elementFromString } from '../utilities/elementFromString.js'
12
12
  export type CreateNodeFromContentOptions = {
13
13
  slice?: boolean
14
14
  parseOptions?: ParseOptions
15
+ errorOnInvalidContent?: boolean
15
16
  }
16
17
 
17
18
  /**
@@ -46,6 +47,10 @@ export function createNodeFromContent(
46
47
 
47
48
  return schema.nodeFromJSON(content)
48
49
  } catch (error) {
50
+ if (options.errorOnInvalidContent) {
51
+ throw new Error('[tiptap error]: Invalid JSON content', { cause: error as Error })
52
+ }
53
+
49
54
  console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error)
50
55
 
51
56
  return createNodeFromContent('', schema, options)
@@ -53,11 +58,46 @@ export function createNodeFromContent(
53
58
  }
54
59
 
55
60
  if (isTextContent) {
56
- const parser = DOMParser.fromSchema(schema)
61
+ let schemaToUse = schema
62
+ let hasInvalidContent = false
63
+
64
+ // Only ever check for invalid content if we're supposed to throw an error
65
+ if (options.errorOnInvalidContent) {
66
+ schemaToUse = new Schema({
67
+ topNode: schema.spec.topNode,
68
+ marks: schema.spec.marks,
69
+ // Prosemirror's schemas are executed such that: the last to execute, matches last
70
+ // This means that we can add a catch-all node at the end of the schema to catch any content that we don't know how to handle
71
+ nodes: schema.spec.nodes.append({
72
+ __tiptap__private__unknown__catch__all__node: {
73
+ content: 'inline*',
74
+ group: 'block',
75
+ parseDOM: [
76
+ {
77
+ tag: '*',
78
+ getAttrs: () => {
79
+ // If this is ever called, we know that the content has something that we don't know how to handle in the schema
80
+ hasInvalidContent = true
81
+ return null
82
+ },
83
+ },
84
+ ],
85
+ },
86
+ }),
87
+ })
88
+ }
57
89
 
58
- return options.slice
90
+ const parser = DOMParser.fromSchema(schemaToUse)
91
+
92
+ const response = options.slice
59
93
  ? parser.parseSlice(elementFromString(content), options.parseOptions).content
60
94
  : parser.parse(elementFromString(content), options.parseOptions)
95
+
96
+ if (options.errorOnInvalidContent && hasInvalidContent) {
97
+ throw new Error('[tiptap error]: Invalid HTML content')
98
+ }
99
+
100
+ return response
61
101
  }
62
102
 
63
103
  return createNodeFromContent('', schema, options)
package/src/types.ts CHANGED
@@ -39,6 +39,15 @@ export type MaybeThisParameterType<T> = Exclude<T, Primitive> extends (...args:
39
39
  export interface EditorEvents {
40
40
  beforeCreate: { editor: Editor }
41
41
  create: { editor: Editor }
42
+ contentError: {
43
+ editor: Editor,
44
+ error: Error,
45
+ /**
46
+ * If called, will re-initialize the editor with the collaboration extension removed.
47
+ * This will prevent syncing back deletions of content not present in the current schema.
48
+ */
49
+ disableCollaboration: () => void
50
+ }
42
51
  update: { editor: Editor; transaction: Transaction }
43
52
  selectionUpdate: { editor: Editor; transaction: Transaction }
44
53
  transaction: { editor: Editor; transaction: Transaction }
@@ -67,8 +76,20 @@ export interface EditorOptions {
67
76
  enableInputRules: EnableRules
68
77
  enablePasteRules: EnableRules
69
78
  enableCoreExtensions: boolean
79
+ /**
80
+ * If `true`, the editor will check the content for errors on initialization.
81
+ * Emitting the `contentError` event if the content is invalid.
82
+ * Which can be used to show a warning or error message to the user.
83
+ * @default false
84
+ */
85
+ enableContentCheck: boolean
70
86
  onBeforeCreate: (props: EditorEvents['beforeCreate']) => void
71
87
  onCreate: (props: EditorEvents['create']) => void
88
+ /**
89
+ * Called when the editor encounters an error while parsing the content.
90
+ * Only enabled if `enableContentCheck` is `true`.
91
+ */
92
+ onContentError: (props: EditorEvents['contentError']) => void
72
93
  onUpdate: (props: EditorEvents['update']) => void
73
94
  onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void
74
95
  onTransaction: (props: EditorEvents['transaction']) => void