diff --git a/docs/src/demos/Examples/Book/index.vue b/docs/src/demos/Examples/Book/index.vue
index 5558e0c2..8d0848dc 100644
--- a/docs/src/demos/Examples/Book/index.vue
+++ b/docs/src/demos/Examples/Book/index.vue
@@ -138,6 +138,7 @@ export default {
code {
color: inherit;
+ padding: 0;
background: none;
font-size: 0.8rem;
}
diff --git a/docs/src/demos/Examples/CodeBlockLanguage/CodeBlockComponent.vue b/docs/src/demos/Examples/CodeBlockLanguage/CodeBlockComponent.vue
new file mode 100644
index 00000000..f5095930
--- /dev/null
+++ b/docs/src/demos/Examples/CodeBlockLanguage/CodeBlockComponent.vue
@@ -0,0 +1,59 @@
+
+
+
` and `` HTML tags.
-TODO
+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.
+
+## 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/docPages/examples/code-block-language.md b/docs/src/docPages/examples/code-block-language.md
new file mode 100644
index 00000000..bdac1b35
--- /dev/null
+++ b/docs/src/docPages/examples/code-block-language.md
@@ -0,0 +1,3 @@
+# Code block language
+
+
diff --git a/docs/src/links.yaml b/docs/src/links.yaml
index c526fac2..33f95d42 100644
--- a/docs/src/links.yaml
+++ b/docs/src/links.yaml
@@ -86,6 +86,9 @@
link: /examples/savvy
- title: Interactivity
link: /examples/interactivity
+ - title: Code block language
+ link: /examples/code-block-language
+ type: new
- title: Guide
items:
@@ -155,7 +158,7 @@
link: /api/nodes/code-block
- title: CodeBlockLowlight
link: /api/nodes/code-block-lowlight
- type: draft
+ type: new
- title: Document
link: /api/nodes/document
- title: Emoji
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index f5ff9f38..0c1f0651 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -57,6 +57,7 @@ export class Editor extends EventEmitter {
parseOptions: {},
enableInputRules: true,
enablePasteRules: true,
+ onBeforeCreate: () => null,
onCreate: () => null,
onUpdate: () => null,
onSelectionUpdate: () => null,
@@ -73,6 +74,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 059b770c..f63ab802 100644
--- a/packages/core/src/Extension.ts
+++ b/packages/core/src/Extension.ts
@@ -99,6 +99,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 364623a5..028db8d3 100644
--- a/packages/core/src/ExtensionManager.ts
+++ b/packages/core/src/ExtensionManager.ts
@@ -43,6 +43,10 @@ export default class ExtensionManager {
}
}
+ if (typeof extension.config.onBeforeCreate === 'function') {
+ 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 d2b2dbc9..819338f5 100644
--- a/packages/core/src/Mark.ts
+++ b/packages/core/src/Mark.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: MarkType,
+ }) => void) | null,
+
/**
* The editor is ready.
*/
diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts
index b0facfb8..9b731ceb 100644
--- a/packages/core/src/Node.ts
+++ b/packages/core/src/Node.ts
@@ -114,6 +114,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/helpers/findChildren.ts b/packages/core/src/helpers/findChildren.ts
new file mode 100644
index 00000000..c580ec49
--- /dev/null
+++ b/packages/core/src/helpers/findChildren.ts
@@ -0,0 +1,22 @@
+import { Node as ProseMirrorNode } from 'prosemirror-model'
+import { Predicate } from '../types'
+
+type NodeWithPos = {
+ node: ProseMirrorNode,
+ pos: number,
+}
+
+export default function findChildren(node: ProseMirrorNode, predicate: Predicate): NodeWithPos[] {
+ const nodesWithPos: NodeWithPos[] = []
+
+ node.descendants((child, pos) => {
+ if (predicate(child)) {
+ nodesWithPos.push({
+ node: child,
+ pos,
+ })
+ }
+ })
+
+ return nodesWithPos
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 78e57616..9e650ac9 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -12,6 +12,9 @@ export { default as markPasteRule } from './pasteRules/markPasteRule'
export { default as callOrReturn } from './utilities/callOrReturn'
export { default as mergeAttributes } from './utilities/mergeAttributes'
+export { default as findChildren } from './helpers/findChildren'
+export { default as findParentNode } from './helpers/findParentNode'
+export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
export { default as generateHTML } from './helpers/generateHTML'
export { default as getSchema } from './helpers/getSchema'
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
@@ -22,7 +25,6 @@ export { default as isNodeActive } from './helpers/isNodeActive'
export { default as isNodeEmpty } from './helpers/isNodeEmpty'
export { default as isNodeSelection } from './helpers/isNodeSelection'
export { default as isTextSelection } from './helpers/isTextSelection'
-export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
export interface Commands {}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 8a6e7aa7..6858a11f 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,
onSelectionUpdate: (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..b1443c5e
--- /dev/null
+++ b/packages/extension-code-block-lowlight/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@tiptap/extension-code-block-lowlight",
+ "description": "code block extension for tiptap",
+ "version": "2.0.0-beta.0",
+ "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": "^2.0.0-beta.1",
+ "@types/lowlight": "^0.0.1",
+ "lowlight": "^1.20.0",
+ "prosemirror-model": "^1.14.0",
+ "prosemirror-state": "^1.3.4",
+ "prosemirror-view": "^1.18.2"
+ }
+}
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..77f82919
--- /dev/null
+++ b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts
@@ -0,0 +1,10 @@
+import CodeBlock from '@tiptap/extension-code-block'
+import { LowlightPlugin } from './lowlight-plugin'
+
+export const CodeBlockLowlight = CodeBlock.extend({
+ addProseMirrorPlugins() {
+ return [
+ LowlightPlugin({ name: 'codeBlock' }),
+ ]
+ },
+})
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..7b099358
--- /dev/null
+++ b/packages/extension-code-block-lowlight/src/lowlight-plugin.ts
@@ -0,0 +1,111 @@
+import { Plugin, PluginKey } from 'prosemirror-state'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+import { Node as ProsemirrorNode } from 'prosemirror-model'
+import { findChildren } from '@tiptap/core'
+import lowlight from 'lowlight/lib/core'
+
+function parseNodes(nodes: any[], className: string[] = []): { text: string, classes: string[] }[] {
+ 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,
+ }
+ })
+ .flat()
+}
+
+function getDecorations({ doc, name }: { doc: ProsemirrorNode, name: string}) {
+ const decorations: Decoration[] = []
+
+ findChildren(doc, node => node.type.name === name)
+ .forEach(block => {
+ let from = block.pos + 1
+ const { language } = block.node.attrs
+ // TODO: add missing type for `listLanguages`
+ // @ts-ignore
+ const languages = lowlight.listLanguages() as string[]
+ const nodes = language && languages.includes(language)
+ ? lowlight.highlight(language, block.node.textContent).value
+ : lowlight.highlightAuto(block.node.textContent).value
+
+ parseNodes(nodes).forEach(node => {
+ const to = from + node.text.length
+
+ if (node.classes.length) {
+ const decoration = Decoration.inline(from, to, {
+ class: node.classes.join(' '),
+ })
+
+ decorations.push(decoration)
+ }
+
+ from = to
+ })
+ })
+
+ return DecorationSet.create(doc, decorations)
+}
+
+export function LowlightPlugin({ name }: { name: string }) {
+ return new Plugin({
+ key: new PluginKey('lowlight'),
+
+ state: {
+ init: (_, { doc }) => getDecorations({ doc, name }),
+ apply: (transaction, decorationSet, oldState, newState) => {
+ const oldNodeName = oldState.selection.$head.parent.type.name
+ const newNodeName = newState.selection.$head.parent.type.name
+ const oldNodes = findChildren(oldState.doc, node => node.type.name === name)
+ const newNodes = findChildren(newState.doc, node => node.type.name === name)
+
+ if (
+ transaction.docChanged
+ // Apply decorations if:
+ && (
+ // selection includes named node,
+ [oldNodeName, newNodeName].includes(name)
+ // OR transaction adds/removes named node,
+ || newNodes.length !== oldNodes.length
+ // OR transaction has changes that completely encapsulte a node
+ // (for example, a transaction that affects the entire document).
+ // Such transactions can happen during collab syncing via y-prosemirror, for example.
+ || transaction.steps.some(step => {
+ // @ts-ignore
+ return step.from !== undefined
+ // @ts-ignore
+ && step.to !== undefined
+ && oldNodes.some(node => {
+ // @ts-ignore
+ return node.pos >= step.from
+ // @ts-ignore
+ && node.pos + node.node.nodeSize <= step.to
+ })
+ })
+ )
+ ) {
+ 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 a25dfbd9..3be799a4 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"
@@ -9406,6 +9428,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"