diff --git a/.changeset/early-singers-begin.md b/.changeset/early-singers-begin.md deleted file mode 100644 index 2b37530fa..000000000 --- a/.changeset/early-singers-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tiptap/core": patch ---- - -fix(core): findDuplicates - use Array.from when converting Set diff --git a/.changeset/wise-beers-reflect.md b/.changeset/wise-beers-reflect.md deleted file mode 100644 index 7815d7891..000000000 --- a/.changeset/wise-beers-reflect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tiptap/extension-task-item": patch ---- - -allow task items to be parsed when only having `
- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:
for (var i=1; i <= 20; i++)
{
diff --git a/demos/src/Examples/CodeBlockLanguage/Vue/index.vue b/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
index 6a30301cd..4846fb30c 100644
--- a/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
+++ b/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
@@ -21,20 +21,19 @@ import css from 'highlight.js/lib/languages/css'
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
-// load all highlight.js languages
-import { lowlight } from 'lowlight'
+// load all languages with "all" or common languages with "common"
+import { all, createLowlight } from 'lowlight'
import CodeBlockComponent from './CodeBlockComponent.vue'
-lowlight.registerLanguage('html', html)
-lowlight.registerLanguage('css', css)
-lowlight.registerLanguage('js', js)
-lowlight.registerLanguage('ts', ts)
+// create a lowlight instance
+const lowlight = createLowlight(all)
-// load specific languages only
-// import { lowlight } from 'lowlight/lib/core'
-// import javascript from 'highlight.js/lib/languages/javascript'
-// lowlight.registerLanguage('javascript', javascript)
+// you can also register languages
+lowlight.register('html', html)
+lowlight.register('css', css)
+lowlight.register('js', js)
+lowlight.register('ts', ts)
export default {
components: {
@@ -63,7 +62,7 @@ export default {
],
content: `
- That’s a boring paragraph followed by a fenced code block:
+ That's a boring paragraph followed by a fenced code block:
for (var i=1; i <= 20; i++)
{
diff --git a/demos/src/Examples/Performance/React/index.jsx b/demos/src/Examples/Performance/React/index.jsx
index 7ecdae9cb..20c1025c6 100644
--- a/demos/src/Examples/Performance/React/index.jsx
+++ b/demos/src/Examples/Performance/React/index.jsx
@@ -62,7 +62,7 @@ function EditorInstance({ shouldOptimizeRendering }) {
})
return (
- <>
+
Number of renders: {countRenderRef.current}
@@ -89,12 +89,13 @@ function EditorInstance({ shouldOptimizeRendering }) {
)}
- >
+
)
}
const EditorControls = () => {
const [shouldOptimizeRendering, setShouldOptimizeRendering] = React.useState(true)
+ const [rendered, setRendered] = React.useState(true)
return (
<>
@@ -123,8 +124,9 @@ const EditorControls = () => {
Render every transaction (default behavior)
- Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: + Isn't that great? And all of that is editable. But wait, there's more. Let's try a code block:
for (var i=1; i <= 20; i++)
{
@@ -203,10 +205,10 @@ export default {
console.log(i);
}
- I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. + I know, I know, this is impressive. It's only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
- Wow, that’s amazing. Good work, boy! 👏 + Wow, that's amazing. Good work, boy! 👏@@ -214,9 +216,9 @@ export default {
— Mom
first paragraph
second paragraph
Oh, for some reason that’s purple.
+Oh, for some reason that's purple.
This isn’t highlighted.
+This isn't highlighted.
But that one is.
And this is highlighted too, but in a different color.
And this one has a data attribute.
diff --git a/demos/src/GuideContent/ReadOnly/React/index.jsx b/demos/src/GuideContent/ReadOnly/React/index.jsx index 657c33382..095d3540c 100644 --- a/demos/src/GuideContent/ReadOnly/React/index.jsx +++ b/demos/src/GuideContent/ReadOnly/React/index.jsx @@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react' export default () => { const [editable, setEditable] = useState(false) const editor = useEditor({ + shouldRerenderOnTransaction: false, editable, content: `diff --git a/demos/src/Nodes/CodeBlockLowlight/React/index.jsx b/demos/src/Nodes/CodeBlockLowlight/React/index.jsx index 6a5264f0c..2f131dd35 100644 --- a/demos/src/Nodes/CodeBlockLowlight/React/index.jsx +++ b/demos/src/Nodes/CodeBlockLowlight/React/index.jsx @@ -1,7 +1,3 @@ -// load specific languages only -// import { lowlight } from 'lowlight/lib/core' -// import javascript from 'highlight.js/lib/languages/javascript' -// lowlight.registerLanguage('javascript', javascript) import './styles.scss' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' @@ -13,14 +9,29 @@ import css from 'highlight.js/lib/languages/css' import js from 'highlight.js/lib/languages/javascript' import ts from 'highlight.js/lib/languages/typescript' import html from 'highlight.js/lib/languages/xml' -// load all highlight.js languages -import { lowlight } from 'lowlight' +// load all languages with "all" or common languages with "common" +import { all, createLowlight } from 'lowlight' import React from 'react' -lowlight.registerLanguage('html', html) -lowlight.registerLanguage('css', css) -lowlight.registerLanguage('js', js) -lowlight.registerLanguage('ts', ts) +// create a lowlight instance with all languages loaded +const lowlight = createLowlight(all) + +// This is only an example, all supported languages are already loaded above +// but you can also register only specific languages to reduce bundle-size +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) + +/** + * Lowlight version 2.x had a different API + * import { lowlight } from 'lowlight' + * + * lowlight.registerLanguage('html', html) + * lowlight.registerLanguage('css', css) + * lowlight.registerLanguage('js', js) + * lowlight.registerLanguage('ts', ts) + */ export default () => { const editor = useEditor({ @@ -34,7 +45,7 @@ export default () => { ], content: `
- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:
for (var i=1; i <= 20; i++)
{
diff --git a/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue b/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
index dfd6a3def..5ee047ec5 100644
--- a/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
+++ b/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
@@ -25,13 +25,17 @@ import css from 'highlight.js/lib/languages/css'
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
-// load all highlight.js languages
-import { lowlight } from 'lowlight'
+// load all languages with "all" or common languages with "common"
+import { all, createLowlight } from 'lowlight'
-lowlight.registerLanguage('html', html)
-lowlight.registerLanguage('css', css)
-lowlight.registerLanguage('js', js)
-lowlight.registerLanguage('ts', ts)
+// create a lowlight instance
+const lowlight = createLowlight(all)
+
+// you can also register languages
+lowlight.register('html', html)
+lowlight.register('css', css)
+lowlight.register('js', js)
+lowlight.register('ts', ts)
export default {
components: {
@@ -56,7 +60,7 @@ export default {
],
content: `
- That’s a boring paragraph followed by a fenced code block:
+ That's a boring paragraph followed by a fenced code block:
for (var i=1; i <= 20; i++)
{
diff --git a/package-lock.json b/package-lock.json
index 0cbfefd92..692c1c2a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"eslint-plugin-cypress": "^2.15.2",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-vue": "^9.27.0",
"husky": "^8.0.3",
@@ -53,19 +54,19 @@
},
"demos": {
"name": "tiptap-demos",
- "version": "2.4.1",
+ "version": "2.4.2",
"dependencies": {
"@hocuspocus/provider": "^2.13.5",
"@lexical/react": "^0.11.1",
"d3": "^7.3.0",
"fast-glob": "^3.2.11",
- "highlight.js": "^11.6.0",
+ "highlight.js": "^11.10.0",
"lexical": "^0.11.1",
- "lowlight": "^2.7.0",
+ "lowlight": "^3.1.0",
"remixicon": "^2.5.0",
"shiki": "^1.10.3",
"simplify-js": "^1.2.4",
- "y-prosemirror": "^1.2.9",
+ "y-prosemirror": "^1.2.11",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.18"
},
@@ -322,6 +323,14 @@
"node": ">=8"
}
},
+ "demos/node_modules/highlight.js": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
+ "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"demos/node_modules/hosted-git-info": {
"version": "4.1.0",
"dev": true,
@@ -348,6 +357,28 @@
"node": ">=8"
}
},
+ "demos/node_modules/lowlight": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz",
+ "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.9.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "demos/node_modules/lowlight/node_modules/highlight.js": {
+ "version": "11.9.0",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
+ "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"demos/node_modules/lru-cache": {
"version": "6.0.0",
"dev": true,
@@ -744,6 +775,29 @@
}
}
},
+ "demos/node_modules/y-prosemirror": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.11.tgz",
+ "integrity": "sha512-MUGMYyokOb9DpBRHr4Cadob2KheDCKW2LHceAM2yrWp9dfX+3HZZUNEubEPd4zszq4DF2fGCFhE3N66zOTLoxA==",
+ "dependencies": {
+ "lib0": "^0.2.42"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.7.1",
+ "prosemirror-state": "^1.2.3",
+ "prosemirror-view": "^1.9.10",
+ "y-protocols": "^1.0.1",
+ "yjs": "^13.5.38"
+ }
+ },
"demos/node_modules/yallist": {
"version": "4.0.0",
"dev": true,
@@ -3364,21 +3418,6 @@
"ms": "^2.1.1"
}
},
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"dev": true,
@@ -4172,30 +4211,6 @@
}
}
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.18.0",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.18.0",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/@shikijs/core": {
"version": "1.10.3",
"license": "MIT",
@@ -4548,13 +4563,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/hast": {
- "version": "2.3.10",
- "license": "MIT",
- "dependencies": {
- "@types/unist": "^2"
- }
- },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"dev": true,
@@ -7207,6 +7215,14 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-file": {
"version": "1.0.0",
"dev": true,
@@ -7223,6 +7239,18 @@
"node": ">=8"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"dev": true,
@@ -7798,6 +7826,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
"node_modules/eslint-plugin-simple-import-sort": {
"version": "7.0.0",
"dev": true,
@@ -8370,17 +8410,6 @@
"reusify": "^1.0.4"
}
},
- "node_modules/fault": {
- "version": "2.0.1",
- "license": "MIT",
- "dependencies": {
- "format": "^0.2.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/fd-slicer": {
"version": "1.1.0",
"dev": true,
@@ -8621,12 +8650,6 @@
"node": ">= 0.12"
}
},
- "node_modules/format": {
- "version": "0.2.2",
- "engines": {
- "node": ">=0.4.x"
- }
- },
"node_modules/fraction.js": {
"version": "4.3.7",
"dev": true,
@@ -9074,13 +9097,6 @@
"node": ">= 0.4"
}
},
- "node_modules/highlight.js": {
- "version": "11.9.0",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"dev": true,
@@ -10909,26 +10925,6 @@
"loose-envify": "cli.js"
}
},
- "node_modules/lowlight": {
- "version": "2.9.0",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^2.0.0",
- "fault": "^2.0.0",
- "highlight.js": "~11.8.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/lowlight/node_modules/highlight.js": {
- "version": "11.8.0",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
@@ -14502,18 +14498,6 @@
"turbo-windows-arm64": "2.0.6"
}
},
- "node_modules/turbo-linux-64": {
- "version": "2.0.6",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/tweetnacl": {
"version": "0.14.5",
"dev": true,
@@ -14855,22 +14839,6 @@
}
}
},
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.18.20",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/vite/node_modules/esbuild": {
"version": "0.18.20",
"dev": true,
@@ -15431,6 +15399,7 @@
},
"node_modules/y-prosemirror": {
"version": "1.2.9",
+ "dev": true,
"license": "MIT",
"dependencies": {
"lib0": "^0.2.42"
diff --git a/package.json b/package.json
index cf21df115..84132d251 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"eslint-plugin-cypress": "^2.15.2",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-vue": "^9.27.0",
"husky": "^8.0.3",
diff --git a/packages/core/src/helpers/getMarksBetween.ts b/packages/core/src/helpers/getMarksBetween.ts
index 177b9bbce..99c85940a 100644
--- a/packages/core/src/helpers/getMarksBetween.ts
+++ b/packages/core/src/helpers/getMarksBetween.ts
@@ -12,7 +12,7 @@ export function getMarksBetween(from: number, to: number, doc: ProseMirrorNode):
.resolve(from)
.marks()
.forEach(mark => {
- const $pos = doc.resolve(from - 1)
+ const $pos = doc.resolve(from)
const range = getMarkRange($pos, mark.type)
if (!range) {
diff --git a/packages/core/src/helpers/isNodeEmpty.ts b/packages/core/src/helpers/isNodeEmpty.ts
index 5d7a777d4..8d9a76400 100644
--- a/packages/core/src/helpers/isNodeEmpty.ts
+++ b/packages/core/src/helpers/isNodeEmpty.ts
@@ -1,40 +1,61 @@
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
/**
- * Returns true if the given node is empty.
- * When `checkChildren` is true (default), it will also check if all children are empty.
+ * Returns true if the given prosemirror node is empty.
*/
export function isNodeEmpty(
node: ProseMirrorNode,
- { checkChildren }: { checkChildren: boolean } = { checkChildren: true },
+ {
+ checkChildren = true,
+ ignoreWhitespace = false,
+ }: {
+ /**
+ * When true (default), it will also check if all children are empty.
+ */
+ checkChildren?: boolean;
+ /**
+ * When true, it will ignore whitespace when checking for emptiness.
+ */
+ ignoreWhitespace?: boolean;
+ } = {},
): boolean {
+ if (ignoreWhitespace) {
+ if (node.type.name === 'hardBreak') {
+ // Hard breaks are considered empty
+ return true
+ }
+ if (node.isText) {
+ return /^\s*$/m.test(node.text ?? '')
+ }
+ }
+
if (node.isText) {
return !node.text
}
+ if (node.isAtom || node.isLeaf) {
+ return false
+ }
+
if (node.content.childCount === 0) {
return true
}
- if (node.isLeaf) {
- return false
- }
-
if (checkChildren) {
- let hasSameContent = true
+ let isContentEmpty = true
node.content.forEach(childNode => {
- if (hasSameContent === false) {
+ if (isContentEmpty === false) {
// Exit early for perf
return
}
- if (!isNodeEmpty(childNode)) {
- hasSameContent = false
+ if (!isNodeEmpty(childNode, { ignoreWhitespace, checkChildren })) {
+ isContentEmpty = false
}
})
- return hasSameContent
+ return isContentEmpty
}
return false
diff --git a/packages/extension-code-block-lowlight/src/code-block-lowlight.ts b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
index c1eb4dcbf..3b952e329 100644
--- a/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
+++ b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
@@ -7,13 +7,6 @@ export interface CodeBlockLowlightOptions extends CodeBlockOptions {
* The lowlight instance.
*/
lowlight: any,
-
- /**
- * The default language.
- * @default null
- * @example 'javascript'
- */
- defaultLanguage: string | null | undefined,
}
/**
@@ -25,7 +18,6 @@ export const CodeBlockLowlight = CodeBlock.extend({
return {
...this.parent?.(),
lowlight: {},
- defaultLanguage: null,
}
},
diff --git a/packages/extension-code-block/src/code-block.ts b/packages/extension-code-block/src/code-block.ts
index 8d6b22d4c..122e476b4 100644
--- a/packages/extension-code-block/src/code-block.ts
+++ b/packages/extension-code-block/src/code-block.ts
@@ -22,6 +22,12 @@ export interface CodeBlockOptions {
* @default true
*/
exitOnArrowDown: boolean
+ /**
+ * The default language.
+ * @default null
+ * @example 'js'
+ */
+ defaultLanguage: string | null | undefined
/**
* Custom HTML attributes that should be added to the rendered HTML tag.
* @default {}
@@ -71,6 +77,7 @@ export const CodeBlock = Node.create({
languageClassPrefix: 'language-',
exitOnTripleEnter: true,
exitOnArrowDown: true,
+ defaultLanguage: null,
HTMLAttributes: {},
}
},
@@ -88,7 +95,7 @@ export const CodeBlock = Node.create({
addAttributes() {
return {
language: {
- default: null,
+ default: this.options.defaultLanguage,
parseHTML: element => {
const { languageClassPrefix } = this.options
const classNames = [...(element.firstElementChild?.classList || [])]
diff --git a/packages/extension-collaboration-cursor/src/collaboration-cursor.ts b/packages/extension-collaboration-cursor/src/collaboration-cursor.ts
index ed17ad06f..3c14708ed 100644
--- a/packages/extension-collaboration-cursor/src/collaboration-cursor.ts
+++ b/packages/extension-collaboration-cursor/src/collaboration-cursor.ts
@@ -127,6 +127,9 @@ export const CollaborationCursor = Extension.create({
}
const ySyncPluginOptions: YSyncOpts = {
- ...(this.options.ySyncOptions ? { ...this.options.ySyncOptions } : {}),
- ...(this.options.onFirstRender ? { ...this.options.onFirstRender } : {}),
+ ...this.options.ySyncOptions,
+ onFirstRender: this.options.onFirstRender,
}
const ySyncPluginInstance = ySyncPlugin(fragment, ySyncPluginOptions)
diff --git a/packages/extension-placeholder/src/placeholder.ts b/packages/extension-placeholder/src/placeholder.ts
index ad0fb8638..08427a257 100644
--- a/packages/extension-placeholder/src/placeholder.ts
+++ b/packages/extension-placeholder/src/placeholder.ts
@@ -31,6 +31,12 @@ export interface PlaceholderOptions {
}) => string)
| string
+ /**
+ * See https://github.com/ueberdosis/tiptap/pull/5278 for more information.
+ * @deprecated This option is no longer respected and this type will be removed in the next major version.
+ */
+ considerAnyAsEmpty?: boolean
+
/**
* **Checks if the placeholder should be only shown when the editor is editable.**
*
diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts
index 2eca1f2f0..2fde5b2ce 100644
--- a/packages/react/src/useEditor.ts
+++ b/packages/react/src/useEditor.ts
@@ -1,8 +1,13 @@
import { EditorOptions } from '@tiptap/core'
import {
- DependencyList, MutableRefObject,
- useDebugValue, useEffect, useRef, useState,
+ DependencyList,
+ MutableRefObject,
+ useDebugValue,
+ useEffect,
+ useRef,
+ useState,
} from 'react'
+import { useSyncExternalStore } from 'use-sync-external-store/shim'
import { Editor } from './Editor.js'
import { useEditorState } from './useEditorState.js'
@@ -31,22 +36,241 @@ export type UseEditorOptions = Partial & {
};
/**
- * Create a new editor instance. And attach event listeners.
+ * This class handles the creation, destruction, and re-creation of the editor instance.
*/
-function createEditor(options: MutableRefObject): Editor {
- const editor = new Editor(options.current)
+class EditorInstanceManager {
+ /**
+ * The current editor instance.
+ */
+ private editor: Editor | null = null
- editor.on('beforeCreate', (...args) => options.current.onBeforeCreate?.(...args))
- editor.on('blur', (...args) => options.current.onBlur?.(...args))
- editor.on('create', (...args) => options.current.onCreate?.(...args))
- editor.on('destroy', (...args) => options.current.onDestroy?.(...args))
- editor.on('focus', (...args) => options.current.onFocus?.(...args))
- editor.on('selectionUpdate', (...args) => options.current.onSelectionUpdate?.(...args))
- editor.on('transaction', (...args) => options.current.onTransaction?.(...args))
- editor.on('update', (...args) => options.current.onUpdate?.(...args))
- editor.on('contentError', (...args) => options.current.onContentError?.(...args))
+ /**
+ * The most recent options to apply to the editor.
+ */
+ private options: MutableRefObject
- return editor
+ /**
+ * The subscriptions to notify when the editor instance
+ * has been created or destroyed.
+ */
+ private subscriptions = new Set<() => void>()
+
+ /**
+ * A timeout to destroy the editor if it was not mounted within a time frame.
+ */
+ private scheduledDestructionTimeout: ReturnType | undefined
+
+ /**
+ * Whether the editor has been mounted.
+ */
+ private isComponentMounted = false
+
+ /**
+ * The most recent dependencies array.
+ */
+ private previousDeps: DependencyList | null = null
+
+ /**
+ * The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
+ */
+ public instanceId = ''
+
+ constructor(options: MutableRefObject) {
+ this.options = options
+ this.subscriptions = new Set<() => void>()
+ this.setEditor(this.getInitialEditor())
+
+ this.getEditor = this.getEditor.bind(this)
+ this.getServerSnapshot = this.getServerSnapshot.bind(this)
+ this.subscribe = this.subscribe.bind(this)
+ this.refreshEditorInstance = this.refreshEditorInstance.bind(this)
+ this.scheduleDestroy = this.scheduleDestroy.bind(this)
+ this.onRender = this.onRender.bind(this)
+ this.createEditor = this.createEditor.bind(this)
+ }
+
+ private setEditor(editor: Editor | null) {
+ this.editor = editor
+ this.instanceId = Math.random().toString(36).slice(2, 9)
+
+ // Notify all subscribers that the editor instance has been created
+ this.subscriptions.forEach(cb => cb())
+ }
+
+ private getInitialEditor() {
+ if (this.options.current.immediatelyRender === undefined) {
+ if (isSSR || isNext) {
+ // TODO in the next major release, we should throw an error here
+ if (isDev) {
+ /**
+ * Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
+ * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
+ */
+ console.warn(
+ 'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.',
+ )
+ }
+
+ // Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
+ return null
+ }
+
+ // Default to immediately rendering when client-side rendering
+ return this.createEditor()
+ }
+
+ if (this.options.current.immediatelyRender && isSSR && isDev) {
+ // Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
+ throw new Error(
+ 'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.',
+ )
+ }
+
+ if (this.options.current.immediatelyRender) {
+ return this.createEditor()
+ }
+
+ return null
+ }
+
+ /**
+ * Create a new editor instance. And attach event listeners.
+ */
+ private createEditor(): Editor {
+ const editor = new Editor(this.options.current)
+
+ // Always call the most recent version of the callback function by default
+ editor.on('beforeCreate', (...args) => this.options.current.onBeforeCreate?.(...args))
+ editor.on('blur', (...args) => this.options.current.onBlur?.(...args))
+ editor.on('create', (...args) => this.options.current.onCreate?.(...args))
+ editor.on('destroy', (...args) => this.options.current.onDestroy?.(...args))
+ editor.on('focus', (...args) => this.options.current.onFocus?.(...args))
+ editor.on('selectionUpdate', (...args) => this.options.current.onSelectionUpdate?.(...args))
+ editor.on('transaction', (...args) => this.options.current.onTransaction?.(...args))
+ editor.on('update', (...args) => this.options.current.onUpdate?.(...args))
+ editor.on('contentError', (...args) => this.options.current.onContentError?.(...args))
+
+ // no need to keep track of the event listeners, they will be removed when the editor is destroyed
+
+ return editor
+ }
+
+ /**
+ * Get the current editor instance.
+ */
+ getEditor(): Editor | null {
+ return this.editor
+ }
+
+ /**
+ * Always disable the editor on the server-side.
+ */
+ getServerSnapshot(): null {
+ return null
+ }
+
+ /**
+ * Subscribe to the editor instance's changes.
+ */
+ subscribe(onStoreChange: () => void) {
+ this.subscriptions.add(onStoreChange)
+
+ return () => {
+ this.subscriptions.delete(onStoreChange)
+ }
+ }
+
+ /**
+ * On each render, we will create, update, or destroy the editor instance.
+ * @param deps The dependencies to watch for changes
+ * @returns A cleanup function
+ */
+ onRender(deps: DependencyList) {
+ // The returned callback will run on each render
+ return () => {
+ this.isComponentMounted = true
+ // Cleanup any scheduled destructions, since we are currently rendering
+ clearTimeout(this.scheduledDestructionTimeout)
+
+ if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
+ // if the editor does exist & deps are empty, we don't need to re-initialize the editor
+ // we can fast-path to update the editor options on the existing instance
+ this.editor.setOptions(this.options.current)
+ } else {
+ // When the editor:
+ // - does not yet exist
+ // - is destroyed
+ // - the deps array changes
+ // We need to destroy the editor instance and re-initialize it
+ this.refreshEditorInstance(deps)
+ }
+
+ return () => {
+ this.isComponentMounted = false
+ this.scheduleDestroy()
+ }
+ }
+ }
+
+ /**
+ * Recreate the editor instance if the dependencies have changed.
+ */
+ private refreshEditorInstance(deps: DependencyList) {
+
+ if (this.editor && !this.editor.isDestroyed) {
+ // Editor instance already exists
+ if (this.previousDeps === null) {
+ // If lastDeps has not yet been initialized, reuse the current editor instance
+ this.previousDeps = deps
+ return
+ }
+ const depsAreEqual = this.previousDeps.length === deps.length
+ && this.previousDeps.every((dep, index) => dep === deps[index])
+
+ if (depsAreEqual) {
+ // deps exist and are equal, no need to recreate
+ return
+ }
+ }
+
+ if (this.editor && !this.editor.isDestroyed) {
+ // Destroy the editor instance if it exists
+ this.editor.destroy()
+ }
+
+ this.setEditor(this.createEditor())
+
+ // Update the lastDeps to the current deps
+ this.previousDeps = deps
+ }
+
+ /**
+ * Schedule the destruction of the editor instance.
+ * This will only destroy the editor if it was not mounted on the next tick.
+ * This is to avoid destroying the editor instance when it's actually still mounted.
+ */
+ private scheduleDestroy() {
+ const currentInstanceId = this.instanceId
+ const currentEditor = this.editor
+
+ // Wait a tick to see if the component is still mounted
+ this.scheduledDestructionTimeout = setTimeout(() => {
+ if (this.isComponentMounted && this.instanceId === currentInstanceId) {
+ // If still mounted on the next tick, with the same instanceId, do not destroy the editor
+ if (currentEditor) {
+ // just re-apply options as they might have changed
+ currentEditor.setOptions(this.options.current)
+ }
+ return
+ }
+ if (currentEditor && !currentEditor.isDestroyed) {
+ currentEditor.destroy()
+ if (this.instanceId === currentInstanceId) {
+ this.setEditor(null)
+ }
+ }
+ }, 0)
+ }
}
/**
@@ -68,99 +292,29 @@ export function useEditor(
* @returns The editor instance
* @example const editor = useEditor({ extensions: [...] })
*/
-export function useEditor(
- options?: UseEditorOptions,
- deps?: DependencyList
-): Editor | null;
+export function useEditor(options?: UseEditorOptions, deps?: DependencyList): Editor | null;
export function useEditor(
options: UseEditorOptions = {},
deps: DependencyList = [],
): Editor | null {
const mostRecentOptions = useRef(options)
- const [editor, setEditor] = useState(() => {
- if (options.immediatelyRender === undefined) {
- if (isSSR || isNext) {
- // TODO in the next major release, we should throw an error here
- if (isDev) {
- /**
- * Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
- * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
- */
- console.warn(
- 'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.',
- )
- }
- // Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
- return null
- }
+ mostRecentOptions.current = options
- // Default to immediately rendering when client-side rendering
- return createEditor(mostRecentOptions)
- }
+ const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions))
- if (options.immediatelyRender && isSSR && isDev) {
- // Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
- throw new Error(
- 'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.',
- )
- }
-
- if (options.immediatelyRender) {
- return createEditor(mostRecentOptions)
- }
-
- return null
- })
- const mostRecentEditor = useRef(editor)
-
- mostRecentEditor.current = editor
+ const editor = useSyncExternalStore(
+ instanceManager.subscribe,
+ instanceManager.getEditor,
+ instanceManager.getServerSnapshot,
+ )
useDebugValue(editor)
// This effect will handle creating/updating the editor instance
- useEffect(() => {
- const destroyUnusedEditor = (editorInstance: Editor | null) => {
- if (editorInstance) {
- // We need to destroy the editor asynchronously to avoid memory leaks
- // because the editor instance is still being used in the component.
-
- setTimeout(() => {
- // re-use the editor instance if it hasn't been replaced yet
- // otherwise, asynchronously destroy the old editor instance
- if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
- editorInstance.destroy()
- }
- })
- }
- }
-
- let editorInstance = mostRecentEditor.current
-
- if (!editorInstance) {
- editorInstance = createEditor(mostRecentOptions)
- setEditor(editorInstance)
- return () => destroyUnusedEditor(editorInstance)
- }
-
- if (!Array.isArray(deps) || deps.length === 0) {
- // if the editor does exist & deps are empty, we don't need to re-initialize the editor
- // we can fast-path to update the editor options on the existing instance
- editorInstance.setOptions(options)
-
- return () => destroyUnusedEditor(editorInstance)
- }
-
- // We need to destroy the editor instance and re-initialize it
- // when the deps array changes
- editorInstance.destroy()
-
- // the deps array is used to re-initialize the editor instance
- editorInstance = createEditor(mostRecentOptions)
- setEditor(editorInstance)
- return () => destroyUnusedEditor(editorInstance)
- }, deps)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(instanceManager.onRender(deps))
// The default behavior is to re-render on each transaction
// This is legacy behavior that will be removed in future versions
diff --git a/packages/react/src/useEditorState.ts b/packages/react/src/useEditorState.ts
index 0c8f02517..079605a80 100644
--- a/packages/react/src/useEditorState.ts
+++ b/packages/react/src/useEditorState.ts
@@ -30,68 +30,83 @@ export type UseEditorStateOptions<
* To synchronize the editor instance with the component state,
* we need to create a separate instance that is not affected by the component re-renders.
*/
-function makeEditorStateInstance(initialEditor: TEditor) {
- let transactionNumber = 0
- let lastTransactionNumber = 0
- let lastSnapshot: EditorStateSnapshot = { editor: initialEditor, transactionNumber: 0 }
- let editor = initialEditor
- const subscribers = new Set<() => void>()
+class EditorStateManager {
+ private transactionNumber = 0
- const editorInstance = {
- /**
- * Get the current editor instance.
- */
- getSnapshot(): EditorStateSnapshot {
- if (transactionNumber === lastTransactionNumber) {
- return lastSnapshot
- }
- lastTransactionNumber = transactionNumber
- lastSnapshot = { editor, transactionNumber }
- return lastSnapshot
- },
- /**
- * Always disable the editor on the server-side.
- */
- getServerSnapshot(): EditorStateSnapshot {
- return { editor: null, transactionNumber: 0 }
- },
- /**
- * Subscribe to the editor instance's changes.
- */
- subscribe(callback: () => void) {
- subscribers.add(callback)
- return () => {
- subscribers.delete(callback)
- }
- },
- /**
- * Watch the editor instance for changes.
- */
- watch(nextEditor: Editor | null) {
- editor = nextEditor as TEditor
+ private lastTransactionNumber = 0
- if (editor) {
- /**
- * This will force a re-render when the editor state changes.
- * This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
- * This could be more efficient, but it's a good trade-off for now.
- */
- const fn = () => {
- transactionNumber += 1
- subscribers.forEach(callback => callback())
- }
+ private lastSnapshot: EditorStateSnapshot
- const currentEditor = editor
+ private editor: TEditor
- currentEditor.on('transaction', fn)
- return () => {
- currentEditor.off('transaction', fn)
- }
- }
- },
+ private subscribers = new Set<() => void>()
+
+ constructor(initialEditor: TEditor) {
+ this.editor = initialEditor
+ this.lastSnapshot = { editor: initialEditor, transactionNumber: 0 }
+
+ this.getSnapshot = this.getSnapshot.bind(this)
+ this.getServerSnapshot = this.getServerSnapshot.bind(this)
+ this.watch = this.watch.bind(this)
+ this.subscribe = this.subscribe.bind(this)
}
- return editorInstance
+ /**
+ * Get the current editor instance.
+ */
+ getSnapshot(): EditorStateSnapshot {
+ if (this.transactionNumber === this.lastTransactionNumber) {
+ return this.lastSnapshot
+ }
+ this.lastTransactionNumber = this.transactionNumber
+ this.lastSnapshot = { editor: this.editor, transactionNumber: this.transactionNumber }
+ return this.lastSnapshot
+ }
+
+ /**
+ * Always disable the editor on the server-side.
+ */
+ getServerSnapshot(): EditorStateSnapshot {
+ return { editor: null, transactionNumber: 0 }
+ }
+
+ /**
+ * Subscribe to the editor instance's changes.
+ */
+ subscribe(callback: () => void): () => void {
+ this.subscribers.add(callback)
+ return () => {
+ this.subscribers.delete(callback)
+ }
+ }
+
+ /**
+ * Watch the editor instance for changes.
+ */
+ watch(nextEditor: Editor | null): undefined | (() => void) {
+ this.editor = nextEditor as TEditor
+
+ if (this.editor) {
+ /**
+ * This will force a re-render when the editor state changes.
+ * This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
+ * This could be more efficient, but it's a good trade-off for now.
+ */
+ const fn = () => {
+ this.transactionNumber += 1
+ this.subscribers.forEach(callback => callback())
+ }
+
+ const currentEditor = this.editor
+
+ currentEditor.on('transaction', fn)
+ return () => {
+ currentEditor.off('transaction', fn)
+ }
+ }
+
+ return undefined
+ }
}
export function useEditorState(
@@ -104,7 +119,7 @@ export function useEditorState(
export function useEditorState(
options: UseEditorStateOptions | UseEditorStateOptions,
): TSelectorResult | null {
- const [editorInstance] = useState(() => makeEditorStateInstance(options.editor))
+ const [editorInstance] = useState(() => new EditorStateManager(options.editor))
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state
const selectedState = useSyncExternalStoreWithSelector(
@@ -117,7 +132,7 @@ export function useEditorState(
useEffect(() => {
return editorInstance.watch(options.editor)
- }, [options.editor])
+ }, [options.editor, editorInstance])
useDebugValue(selectedState)
diff --git a/packages/vue-3/src/Editor.ts b/packages/vue-3/src/Editor.ts
index 8d10526ca..9ed5597d3 100644
--- a/packages/vue-3/src/Editor.ts
+++ b/packages/vue-3/src/Editor.ts
@@ -1,3 +1,4 @@
+/* eslint-disable react-hooks/rules-of-hooks */
import { Editor as CoreEditor, EditorOptions } from '@tiptap/core'
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
import {
diff --git a/tests/cypress/integration/core/isNodeEmpty.spec.ts b/tests/cypress/integration/core/isNodeEmpty.spec.ts
index 7db88996d..e4c1dc946 100644
--- a/tests/cypress/integration/core/isNodeEmpty.spec.ts
+++ b/tests/cypress/integration/core/isNodeEmpty.spec.ts
@@ -3,13 +3,53 @@
import { getSchema, isNodeEmpty } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Image from '@tiptap/extension-image'
+import Mention from '@tiptap/extension-mention'
import StarterKit from '@tiptap/starter-kit'
-const schema = getSchema([StarterKit])
-const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })])
-const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image])
+const schema = getSchema([StarterKit, Mention])
+const modifiedSchema = getSchema([
+ StarterKit.configure({ document: false }),
+ Document.extend({ content: 'heading block*' }),
+])
+const imageSchema = getSchema([
+ StarterKit.configure({ document: false }),
+ Document.extend({ content: 'image block*' }),
+ Image,
+])
describe('isNodeEmpty', () => {
+ describe('ignoreWhitespace=true', () => {
+ it('should return true when text has only whitespace', () => {
+ const node = schema.nodeFromJSON({ type: 'text', text: ' \n\t\r\n' })
+
+ expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
+ })
+
+ it('should return true when a paragraph has only whitespace', () => {
+ const node = schema.nodeFromJSON({
+ type: 'paragraph',
+ content: [{ type: 'text', text: ' \n\t\r\n' }],
+ })
+
+ expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
+ })
+
+ it('should return true for a hardbreak', () => {
+ const node = schema.nodeFromJSON({ type: 'hardBreak' })
+
+ expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
+ })
+
+ it('should return true when a paragraph has only a hardbreak', () => {
+ const node = schema.nodeFromJSON({
+ type: 'paragraph',
+ content: [{ type: 'hardBreak' }],
+ })
+
+ expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true)
+ })
+ })
+
describe('with default schema', () => {
it('should return false when text has content', () => {
const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' })
@@ -26,6 +66,32 @@ describe('isNodeEmpty', () => {
expect(isNodeEmpty(node)).to.eq(false)
})
+ it('should return false when a paragraph has hardbreaks', () => {
+ const node = schema.nodeFromJSON({
+ type: 'paragraph',
+ content: [{ type: 'hardBreak' }],
+ })
+
+ expect(isNodeEmpty(node)).to.eq(false)
+ })
+
+ it('should return false when a paragraph has a mention', () => {
+ const node = schema.nodeFromJSON({
+ type: 'paragraph',
+ content: [
+ {
+ type: 'mention',
+ attrs: {
+ id: 'Winona Ryder',
+ label: null,
+ },
+ },
+ ],
+ })
+
+ expect(isNodeEmpty(node)).to.eq(false)
+ })
+
it('should return true when a paragraph has no content', () => {
const node = schema.nodeFromJSON({
type: 'paragraph',
@@ -95,9 +161,7 @@ describe('isNodeEmpty', () => {
content: [
{
type: 'heading',
- content: [
- { type: 'text', text: 'Hello world!' },
- ],
+ content: [{ type: 'text', text: 'Hello world!' }],
},
],
})
@@ -112,9 +176,7 @@ describe('isNodeEmpty', () => {
{ type: 'heading' },
{
type: 'paragraph',
- content: [
- { type: 'text', text: 'Hello world!' },
- ],
+ content: [{ type: 'text', text: 'Hello world!' }],
},
],
})
@@ -137,9 +199,7 @@ describe('isNodeEmpty', () => {
it('should return true when a document has an empty heading with attrs', () => {
const node = modifiedSchema.nodeFromJSON({
type: 'doc',
- content: [
- { type: 'heading', content: [], attrs: { level: 2 } },
- ],
+ content: [{ type: 'heading', content: [], attrs: { level: 2 } }],
})
expect(isNodeEmpty(node)).to.eq(true)
@@ -177,7 +237,7 @@ describe('isNodeEmpty', () => {
],
})
- expect(isNodeEmpty(node)).to.eq(true)
+ expect(isNodeEmpty(node)).to.eq(false)
})
})
})
diff --git a/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts b/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
index e3b6fd6d1..00f1df625 100644
--- a/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
+++ b/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
@@ -5,7 +5,9 @@ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'
import { Document } from '@tiptap/extension-document'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Text } from '@tiptap/extension-text'
-import { lowlight } from 'lowlight'
+import { all, createLowlight } from 'lowlight'
+
+const lowlight = createLowlight(all)
describe('code block highlight', () => {
let Frontmatter
diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json
index ebd9a998f..7967d1ebd 100644
--- a/tests/cypress/tsconfig.json
+++ b/tests/cypress/tsconfig.json
@@ -6,7 +6,7 @@
"sourceMap": false,
"types": ["cypress", "react", "react-dom"],
"paths": {
- "@tiptap/*": ["packages/*/dist", "packages/*/src"],
+ "@tiptap/*": ["packages/*/src", "packages/*/dist"],
"@tiptap/pm/*": ["../../pm/*/dist"]
},
"typeRoots": ["../../node_modules/@types", "../../node_modules/"],