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

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.
package/dist/index.cjs CHANGED
@@ -1774,15 +1774,49 @@ function createNodeFromContent(content, schema, options) {
1774
1774
  return schema.nodeFromJSON(content);
1775
1775
  }
1776
1776
  catch (error) {
1777
+ if (options.errorOnInvalidContent) {
1778
+ throw new Error('[tiptap error]: Invalid JSON content', { cause: error });
1779
+ }
1777
1780
  console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error);
1778
1781
  return createNodeFromContent('', schema, options);
1779
1782
  }
1780
1783
  }
1781
1784
  if (isTextContent) {
1782
- const parser = model.DOMParser.fromSchema(schema);
1783
- return options.slice
1785
+ let schemaToUse = schema;
1786
+ let hasInvalidContent = false;
1787
+ // Only ever check for invalid content if we're supposed to throw an error
1788
+ if (options.errorOnInvalidContent) {
1789
+ schemaToUse = new model.Schema({
1790
+ topNode: schema.spec.topNode,
1791
+ marks: schema.spec.marks,
1792
+ // Prosemirror's schemas are executed such that: the last to execute, matches last
1793
+ // 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
1794
+ nodes: schema.spec.nodes.append({
1795
+ __tiptap__private__unknown__catch__all__node: {
1796
+ content: 'inline*',
1797
+ group: 'block',
1798
+ parseDOM: [
1799
+ {
1800
+ tag: '*',
1801
+ getAttrs: () => {
1802
+ // If this is ever called, we know that the content has something that we don't know how to handle in the schema
1803
+ hasInvalidContent = true;
1804
+ return null;
1805
+ },
1806
+ },
1807
+ ],
1808
+ },
1809
+ }),
1810
+ });
1811
+ }
1812
+ const parser = model.DOMParser.fromSchema(schemaToUse);
1813
+ const response = options.slice
1784
1814
  ? parser.parseSlice(elementFromString(content), options.parseOptions).content
1785
1815
  : parser.parse(elementFromString(content), options.parseOptions);
1816
+ if (options.errorOnInvalidContent && hasInvalidContent) {
1817
+ throw new Error('[tiptap error]: Invalid HTML content');
1818
+ }
1819
+ return response;
1786
1820
  }
1787
1821
  return createNodeFromContent('', schema, options);
1788
1822
  }
@@ -1811,6 +1845,7 @@ const isFragment = (nodeOrFragment) => {
1811
1845
  return nodeOrFragment.toString().startsWith('<');
1812
1846
  };
1813
1847
  const insertContentAt = (position, value, options) => ({ tr, dispatch, editor }) => {
1848
+ var _a;
1814
1849
  if (dispatch) {
1815
1850
  options = {
1816
1851
  parseOptions: {},
@@ -1819,12 +1854,19 @@ const insertContentAt = (position, value, options) => ({ tr, dispatch, editor })
1819
1854
  applyPasteRules: false,
1820
1855
  ...options,
1821
1856
  };
1822
- const content = createNodeFromContent(value, editor.schema, {
1823
- parseOptions: {
1824
- preserveWhitespace: 'full',
1825
- ...options.parseOptions,
1826
- },
1827
- });
1857
+ let content;
1858
+ try {
1859
+ content = createNodeFromContent(value, editor.schema, {
1860
+ parseOptions: {
1861
+ preserveWhitespace: 'full',
1862
+ ...options.parseOptions,
1863
+ },
1864
+ errorOnInvalidContent: (_a = options.errorOnInvalidContent) !== null && _a !== void 0 ? _a : editor.options.enableContentCheck,
1865
+ });
1866
+ }
1867
+ catch (e) {
1868
+ return false;
1869
+ }
1828
1870
  // don’t dispatch an empty fragment because this can lead to strange errors
1829
1871
  if (content.toString() === '<>') {
1830
1872
  return true;
@@ -2188,13 +2230,26 @@ const selectTextblockStart = () => ({ state, dispatch }) => {
2188
2230
  * @param parseOptions Options for the parser
2189
2231
  * @returns The created Prosemirror document node
2190
2232
  */
2191
- function createDocument(content, schema, parseOptions = {}) {
2192
- return createNodeFromContent(content, schema, { slice: false, parseOptions });
2233
+ function createDocument(content, schema, parseOptions = {}, options = {}) {
2234
+ return createNodeFromContent(content, schema, {
2235
+ slice: false,
2236
+ parseOptions,
2237
+ errorOnInvalidContent: options.errorOnInvalidContent,
2238
+ });
2193
2239
  }
2194
2240
 
2195
- const setContent = (content, emitUpdate = false, parseOptions = {}) => ({ tr, editor, dispatch }) => {
2241
+ const setContent = (content, emitUpdate = false, parseOptions = {}, options = {}) => ({ tr, editor, dispatch }) => {
2242
+ var _a;
2196
2243
  const { doc } = tr;
2197
- const document = createDocument(content, editor.schema, parseOptions);
2244
+ let document;
2245
+ try {
2246
+ document = createDocument(content, editor.schema, parseOptions, {
2247
+ errorOnInvalidContent: (_a = options.errorOnInvalidContent) !== null && _a !== void 0 ? _a : editor.options.enableContentCheck,
2248
+ });
2249
+ }
2250
+ catch (e) {
2251
+ return false;
2252
+ }
2198
2253
  if (dispatch) {
2199
2254
  tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate);
2200
2255
  }
@@ -3301,30 +3356,40 @@ const updateAttributes = (typeOrName, attributes = {}) => ({ tr, state, dispatch
3301
3356
  markType = getMarkType(typeOrName, state.schema);
3302
3357
  }
3303
3358
  if (dispatch) {
3304
- tr.selection.ranges.forEach(range => {
3359
+ let lastPos;
3360
+ let lastNode;
3361
+ let trimmedFrom;
3362
+ let trimmedTo;
3363
+ tr.selection.ranges.forEach((range) => {
3305
3364
  const from = range.$from.pos;
3306
3365
  const to = range.$to.pos;
3307
3366
  state.doc.nodesBetween(from, to, (node, pos) => {
3308
3367
  if (nodeType && nodeType === node.type) {
3309
- tr.setNodeMarkup(pos, undefined, {
3310
- ...node.attrs,
3311
- ...attributes,
3312
- });
3313
- }
3314
- if (markType && node.marks.length) {
3315
- node.marks.forEach(mark => {
3316
- if (markType === mark.type) {
3317
- const trimmedFrom = Math.max(pos, from);
3318
- const trimmedTo = Math.min(pos + node.nodeSize, to);
3319
- tr.addMark(trimmedFrom, trimmedTo, markType.create({
3320
- ...mark.attrs,
3321
- ...attributes,
3322
- }));
3323
- }
3324
- });
3368
+ trimmedFrom = Math.max(pos, from);
3369
+ trimmedTo = Math.min(pos + node.nodeSize, to);
3370
+ lastPos = pos;
3371
+ lastNode = node;
3325
3372
  }
3326
3373
  });
3327
3374
  });
3375
+ if (lastNode) {
3376
+ if (lastPos !== undefined) {
3377
+ tr.setNodeMarkup(lastPos, undefined, {
3378
+ ...lastNode.attrs,
3379
+ ...attributes,
3380
+ });
3381
+ }
3382
+ if (markType && lastNode.marks.length) {
3383
+ lastNode.marks.forEach((mark) => {
3384
+ if (markType === mark.type) {
3385
+ tr.addMark(trimmedFrom, trimmedTo, markType.create({
3386
+ ...mark.attrs,
3387
+ ...attributes,
3388
+ }));
3389
+ }
3390
+ });
3391
+ }
3392
+ }
3328
3393
  }
3329
3394
  return true;
3330
3395
  };
@@ -3883,6 +3948,7 @@ class Editor extends EventEmitter {
3883
3948
  enableInputRules: true,
3884
3949
  enablePasteRules: true,
3885
3950
  enableCoreExtensions: true,
3951
+ enableContentCheck: false,
3886
3952
  onBeforeCreate: () => null,
3887
3953
  onCreate: () => null,
3888
3954
  onUpdate: () => null,
@@ -3891,6 +3957,7 @@ class Editor extends EventEmitter {
3891
3957
  onFocus: () => null,
3892
3958
  onBlur: () => null,
3893
3959
  onDestroy: () => null,
3960
+ onContentError: ({ error }) => { throw error; },
3894
3961
  };
3895
3962
  this.isCapturingTransaction = false;
3896
3963
  this.capturedTransaction = null;
@@ -3900,6 +3967,7 @@ class Editor extends EventEmitter {
3900
3967
  this.createSchema();
3901
3968
  this.on('beforeCreate', this.options.onBeforeCreate);
3902
3969
  this.emit('beforeCreate', { editor: this });
3970
+ this.on('contentError', this.options.onContentError);
3903
3971
  this.createView();
3904
3972
  this.injectCSS();
3905
3973
  this.on('create', this.options.onCreate);
@@ -4059,7 +4127,28 @@ class Editor extends EventEmitter {
4059
4127
  * Creates a ProseMirror view.
4060
4128
  */
4061
4129
  createView() {
4062
- const doc = createDocument(this.options.content, this.schema, this.options.parseOptions);
4130
+ let doc;
4131
+ try {
4132
+ doc = createDocument(this.options.content, this.schema, this.options.parseOptions, { errorOnInvalidContent: this.options.enableContentCheck });
4133
+ }
4134
+ catch (e) {
4135
+ if (!(e instanceof Error) || !['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)) {
4136
+ // Not the content error we were expecting
4137
+ throw e;
4138
+ }
4139
+ this.emit('contentError', {
4140
+ editor: this,
4141
+ error: e,
4142
+ disableCollaboration: () => {
4143
+ // To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
4144
+ this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration');
4145
+ // Restart the initialization process by recreating the extension manager with the new set of extensions
4146
+ this.createExtensionManager();
4147
+ },
4148
+ });
4149
+ // Content is invalid, but attempt to create it anyway, stripping out the invalid parts
4150
+ doc = createDocument(this.options.content, this.schema, this.options.parseOptions, { errorOnInvalidContent: false });
4151
+ }
4063
4152
  const selection = resolveFocusPosition(doc, this.options.autofocus);
4064
4153
  this.view = new view.EditorView(this.options.element, {
4065
4154
  ...this.options.editorProps,