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 `
  • { - if (!editor) { - return null - } - const onCutToStart = useCallback(() => { editor.chain().cut({ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos }, 1).run() }, [editor]) @@ -20,6 +16,10 @@ const MenuBar = ({ editor }) => { editor.chain().cut({ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos }, editor.state.doc.nodeSize - 2).run() }, [editor]) + if (!editor) { + return null + } + return (
    diff --git a/demos/src/Examples/CodeBlockLanguage/React/index.jsx b/demos/src/Examples/CodeBlockLanguage/React/index.jsx index cdb1d35de..a94b42327 100644 --- a/demos/src/Examples/CodeBlockLanguage/React/index.jsx +++ b/demos/src/Examples/CodeBlockLanguage/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,16 +9,21 @@ 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' -import CodeBlockComponent from './CodeBlockComponent.jsx' +// eslint-disable-next-line +import CodeBlockComponent from './CodeBlockComponent' -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 individual languages +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) const MenuBar = ({ editor }) => { if (!editor) { @@ -56,7 +57,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/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)
    +
    - + {rendered && } ) } diff --git a/demos/src/Experiments/All/Vue/index.vue b/demos/src/Experiments/All/Vue/index.vue index cdd835a23..5b1e370f7 100644 --- a/demos/src/Experiments/All/Vue/index.vue +++ b/demos/src/Experiments/All/Vue/index.vue @@ -108,7 +108,9 @@ import TextAlign from '@tiptap/extension-text-align' import TextStyle from '@tiptap/extension-text-style' import Underline from '@tiptap/extension-underline' import { Editor, EditorContent } from '@tiptap/vue-3' -import { lowlight } from 'lowlight' +import { all, createLowlight } from 'lowlight' + +const lowlight = createLowlight(all) export default { components: { @@ -182,14 +184,14 @@ export default {

    • - That’s a bullet list with one … + That's a bullet list with one …
    • … or two list items.

    - 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! 👏
    — Mom
    @@ -214,9 +216,9 @@ export default {

    first paragraph

    second paragraph

    Color

    -

    Oh, for some reason that’s purple.

    +

    Oh, for some reason that's purple.

    Highlight

    -

    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/"],