diff --git a/docs/src/demos/Nodes/CodeBlock/index.vue b/docs/src/demos/Nodes/CodeBlock/index.vue
index 54b3c676..cea9f542 100644
--- a/docs/src/demos/Nodes/CodeBlock/index.vue
+++ b/docs/src/demos/Nodes/CodeBlock/index.vue
@@ -61,3 +61,26 @@ export default {
},
}
+
+
diff --git a/docs/src/demos/Nodes/CodeBlockLowlight/index.vue b/docs/src/demos/Nodes/CodeBlockLowlight/index.vue
new file mode 100644
index 00000000..356e25b2
--- /dev/null
+++ b/docs/src/demos/Nodes/CodeBlockLowlight/index.vue
@@ -0,0 +1,146 @@
+
+
` and `` HTML tags.
+
+Type ``` (three backticks and a space) or ∼∼∼ (three tildes and a space) and a code block is instantly added for you. You can even specify the language, try writing ```css . That should add a `language-css` class to the ``-tag.
+
+::: warning Restrictions
+The CodeBlock extension doesn’t come with styling and has no syntax highlighting built-in. It’s on our roadmap though.
+:::
+
+## Installation
+```bash
+# with npm
+npm install @tiptap/extension-code-block-lowlight
+
+# with Yarn
+yarn add @tiptap/extension-code-block-lowlight
+```
+
+## Settings
+| Option | Type | Default | Description |
+| ------------------- | -------- | ------------- | --------------------------------------------------------------------- |
+| HTMLAttributes | `Object` | `{}` | Custom HTML attributes that should be added to the rendered HTML tag. |
+| languageClassPrefix | `String` | `'language-'` | Adds a prefix to language classes that are applied to code tags. |
+
+## Commands
+| Command | Parameters | Description |
+| --------- | ---------- | ----------------------------- |
+| codeBlock | — | Wrap content in a code block. |
+
+## Keyboard shortcuts
+* Windows/Linux: `Control` `Alt` `C`
+* macOS: `Cmd` `Alt` `C`
+
+## Source code
+[packages/extension-code-block-lowlight/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-code-block-lowlight/)
+
+## Usage
+
diff --git a/docs/src/links.yaml b/docs/src/links.yaml
index 7950055b..d61a485e 100644
--- a/docs/src/links.yaml
+++ b/docs/src/links.yaml
@@ -131,6 +131,8 @@
link: /api/nodes/bullet-list
- title: CodeBlock
link: /api/nodes/code-block
+ - title: CodeBlockLowlight
+ link: /api/nodes/code-block-lowlight
- title: Document
link: /api/nodes/document
- title: Emoji
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index f5452202..843c5f18 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -58,6 +58,7 @@ export class Editor extends EventEmitter {
parseOptions: {},
enableInputRules: true,
enablePasteRules: true,
+ onBeforeCreate: () => null,
onCreate: () => null,
onUpdate: () => null,
onSelectionUpdate: () => null,
@@ -75,6 +76,8 @@ export class Editor extends EventEmitter {
this.createExtensionManager()
this.createCommandManager()
this.createSchema()
+ this.on('beforeCreate', this.options.onCreate)
+ this.emit('beforeCreate', { editor: this })
this.createView()
this.injectCSS()
this.on('create', this.options.onCreate)
diff --git a/packages/core/src/Extension.ts b/packages/core/src/Extension.ts
index db0af867..6660338b 100644
--- a/packages/core/src/Extension.ts
+++ b/packages/core/src/Extension.ts
@@ -94,6 +94,14 @@ declare module '@tiptap/core' {
[key: string]: any,
}) | null,
+ /**
+ * The editor is not ready yet.
+ */
+ onBeforeCreate?: ((this: {
+ options: Options,
+ editor: Editor,
+ }) => void) | null,
+
/**
* The editor is ready.
*/
diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts
index 7284d75c..8261260b 100644
--- a/packages/core/src/ExtensionManager.ts
+++ b/packages/core/src/ExtensionManager.ts
@@ -43,6 +43,13 @@ export default class ExtensionManager {
}
}
+ // console.log(extension.config.onBeforeCreate)
+
+ if (typeof extension.config.onBeforeCreate === 'function') {
+ console.log('JOOO')
+ this.editor.on('beforeCreate', extension.config.onBeforeCreate.bind(context))
+ }
+
if (typeof extension.config.onCreate === 'function') {
this.editor.on('create', extension.config.onCreate.bind(context))
}
diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts
index 7986b5df..b3ff2de2 100644
--- a/packages/core/src/Mark.ts
+++ b/packages/core/src/Mark.ts
@@ -104,6 +104,15 @@ declare module '@tiptap/core' {
[key: string]: any,
}) | null,
+ /**
+ * The editor is not ready yet.
+ */
+ onBeforeCreate?: ((this: {
+ options: Options,
+ editor: Editor,
+ type: MarkType,
+ }) => void) | null,
+
/**
* The editor is ready.
*/
diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts
index 315b38cc..8fafdb37 100644
--- a/packages/core/src/Node.ts
+++ b/packages/core/src/Node.ts
@@ -109,6 +109,15 @@ declare module '@tiptap/core' {
[key: string]: any,
}) | null,
+ /**
+ * The editor is not ready yet.
+ */
+ onBeforeCreate?: ((this: {
+ options: Options,
+ editor: Editor,
+ type: NodeType,
+ }) => void) | null,
+
/**
* The editor is ready.
*/
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 453d52c6..75d20111 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -29,6 +29,7 @@ export interface EditorOptions {
parseOptions: ParseOptions,
enableInputRules: boolean,
enablePasteRules: boolean,
+ onBeforeCreate: (props: { editor: Editor }) => void,
onCreate: (props: { editor: Editor }) => void,
onUpdate: (props: { editor: Editor }) => void,
onViewUpdate: (props: { editor: Editor }) => void,
diff --git a/packages/extension-code-block-lowlight/README.md b/packages/extension-code-block-lowlight/README.md
new file mode 100644
index 00000000..d950097e
--- /dev/null
+++ b/packages/extension-code-block-lowlight/README.md
@@ -0,0 +1,14 @@
+# @tiptap/extension-code-block-lowlight
+[](https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight)
+[](https://npmcharts.com/compare/tiptap?minimal=true)
+[](https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight)
+[](https://github.com/sponsors/ueberdosis)
+
+## Introduction
+tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
+
+## Offical Documentation
+Documentation can be found on the [tiptap website](https://tiptap.dev).
+
+## License
+tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).
diff --git a/packages/extension-code-block-lowlight/package.json b/packages/extension-code-block-lowlight/package.json
new file mode 100644
index 00000000..f0ab3100
--- /dev/null
+++ b/packages/extension-code-block-lowlight/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@tiptap/extension-code-block-lowlight",
+ "description": "code block extension for tiptap",
+ "version": "2.0.0-beta.1",
+ "homepage": "https://tiptap.dev",
+ "keywords": [
+ "tiptap",
+ "tiptap extension"
+ ],
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "main": "dist/tiptap-extension-code-block-lowlight.cjs.js",
+ "umd": "dist/tiptap-extension-code-block-lowlight.umd.js",
+ "module": "dist/tiptap-extension-code-block-lowlight.esm.js",
+ "unpkg": "dist/tiptap-extension-code-block-lowlight.bundle.umd.min.js",
+ "types": "dist/packages/extension-code-block-lowlight/src/index.d.ts",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ },
+ "dependencies": {
+ "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.1",
+ "@types/lowlight": "^0.0.1",
+ "lowlight": "^1.20.0",
+ "prosemirror-inputrules": "^1.1.3"
+ }
+}
diff --git a/packages/extension-code-block-lowlight/src/code-block-lowlight.ts b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
new file mode 100644
index 00000000..9b8670cc
--- /dev/null
+++ b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
@@ -0,0 +1,35 @@
+import CodeBlock from '@tiptap/extension-code-block'
+import low from 'lowlight/lib/core'
+import { LowlightPlugin } from './lowlight-plugin'
+
+export interface CodeBlockLowlightOptions {
+ languageClassPrefix: string,
+ HTMLAttributes: {
+ [key: string]: any
+ },
+ languages: {
+ [key: string]: Function
+ },
+}
+
+export const CodeBlockLowlight = CodeBlock.extend({
+ name: 'codeBlockLowlight',
+
+ defaultOptions: {
+ languageClassPrefix: 'language-',
+ HTMLAttributes: {},
+ languages: {},
+ },
+
+ onBeforeCreate() {
+ Object.entries(this.options.languages).forEach(([name, mapping]) => {
+ low.registerLanguage(name, mapping)
+ })
+ },
+
+ addProseMirrorPlugins() {
+ return [
+ LowlightPlugin({ name: 'codeBlockLowlight' }),
+ ]
+ },
+})
diff --git a/packages/extension-code-block-lowlight/src/index.ts b/packages/extension-code-block-lowlight/src/index.ts
new file mode 100644
index 00000000..257f48ac
--- /dev/null
+++ b/packages/extension-code-block-lowlight/src/index.ts
@@ -0,0 +1,5 @@
+import { CodeBlockLowlight } from './code-block-lowlight'
+
+export * from './code-block-lowlight'
+
+export default CodeBlockLowlight
diff --git a/packages/extension-code-block-lowlight/src/lowlight-plugin.ts b/packages/extension-code-block-lowlight/src/lowlight-plugin.ts
new file mode 100644
index 00000000..c8c47454
--- /dev/null
+++ b/packages/extension-code-block-lowlight/src/lowlight-plugin.ts
@@ -0,0 +1,110 @@
+import { Plugin, PluginKey } from 'prosemirror-state'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+import { Node as ProsemirrorNode } from 'prosemirror-model'
+import low from 'lowlight/lib/core'
+
+type NodeWithPos = {
+ node: ProsemirrorNode,
+ pos: number,
+}
+
+const findBlockNodes = (doc: ProsemirrorNode) => {
+ const nodes: NodeWithPos[] = []
+
+ doc.descendants((node, pos) => {
+ if (node.isBlock) {
+ nodes.push({
+ node,
+ pos,
+ })
+ }
+ })
+
+ return nodes
+}
+
+function getDecorations({ doc, name }: { doc: ProsemirrorNode, name: string}) {
+ const decorations: Decoration[] = []
+ const blocks = findBlockNodes(doc).filter(block => block.node.type.name === name)
+
+ function parseNodes(nodes: any[], className: string[] = []): any {
+ return nodes.map(node => {
+
+ const classes = [
+ ...className,
+ ...node.properties ? node.properties.className : [],
+ ]
+
+ if (node.children) {
+ return parseNodes(node.children, classes)
+ }
+
+ return {
+ text: node.value,
+ classes,
+ }
+ })
+ }
+
+ blocks.forEach(block => {
+ let startPos = block.pos + 1
+ const nodes = low.highlightAuto(block.node.textContent).value
+
+ parseNodes(nodes)
+ .flat()
+ .map((node: any) => {
+ const from = startPos
+ const to = from + node.text.length
+
+ startPos = to
+
+ return {
+ ...node,
+ from,
+ to,
+ }
+ })
+ .forEach((node: any) => {
+ const decoration = Decoration.inline(node.from, node.to, {
+ class: node.classes.join(' '),
+ })
+ decorations.push(decoration)
+ })
+ })
+
+ return DecorationSet.create(doc, decorations)
+}
+
+export function LowlightPlugin({ name }: { name: string }) {
+ return new Plugin({
+ key: new PluginKey('highlight'),
+
+ state: {
+ init: (_, { doc }) => getDecorations({ doc, name }),
+ apply: (transaction, decorationSet, oldState, newState) => {
+ // TODO: find way to cache decorations
+ // https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
+ const oldNodeName = oldState.selection.$head.parent.type.name
+ const newNodeName = newState.selection.$head.parent.type.name
+ const oldNodes = findBlockNodes(oldState.doc)
+ .filter(node => node.node.type.name === name)
+ const newNodes = findBlockNodes(newState.doc)
+ .filter(node => node.node.type.name === name)
+
+ // Apply decorations if selection includes named node, or transaction changes named node.
+ if (transaction.docChanged && ([oldNodeName, newNodeName].includes(name)
+ || newNodes.length !== oldNodes.length)) {
+ return getDecorations({ doc: transaction.doc, name })
+ }
+
+ return decorationSet.map(transaction.mapping, transaction.doc)
+ },
+ },
+
+ props: {
+ decorations(state) {
+ return this.getState(state)
+ },
+ },
+ })
+}
diff --git a/yarn.lock b/yarn.lock
index 1f2291fe..99c747f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2184,6 +2184,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+"@types/lowlight@^0.0.1":
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/@types/lowlight/-/lowlight-0.0.1.tgz#221bc67a6c517bae71e6f200fa1cad0feaeeb965"
+ integrity sha512-yPpbpV1KfpFOZ0ZZbsgwWumraiAKoX7/Ng75Ah//w+ZBt4j0xwrQ2aHSlk2kPzQVK4LiPbNFE1LjC00IL4nl/A==
+
"@types/mdast@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
@@ -6545,6 +6550,13 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
+fault@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
+ integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+ dependencies:
+ format "^0.2.0"
+
faye-websocket@^0.11.3:
version "0.11.3"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
@@ -6780,6 +6792,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
+format@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+ integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -7674,6 +7691,11 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+highlight.js@~10.7.0:
+ version "10.7.1"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.1.tgz#a8ec4152db24ea630c90927d6cae2a45f8ecb955"
+ integrity sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA==
+
hirestime@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/hirestime/-/hirestime-3.2.2.tgz#1b5ff4c796b6b70586fa6efa4850952c6e1be484"
@@ -9418,6 +9440,14 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+lowlight@^1.20.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+ integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+ dependencies:
+ fault "^1.0.0"
+ highlight.js "~10.7.0"
+
lpad-align@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/lpad-align/-/lpad-align-1.1.2.tgz#21f600ac1c3095c3c6e497ee67271ee08481fe9e"