From 7adf1853d741d8c3ae8c492628f71caad39153c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 2 Apr 2021 00:07:40 +0200 Subject: [PATCH] add basic lowlight extension --- docs/src/demos/Nodes/CodeBlock/index.vue | 23 +++ .../demos/Nodes/CodeBlockLowlight/index.vue | 146 ++++++++++++++++++ .../docPages/api/nodes/code-block-lowlight.md | 41 +++++ docs/src/links.yaml | 2 + packages/core/src/Editor.ts | 3 + packages/core/src/Extension.ts | 8 + packages/core/src/ExtensionManager.ts | 7 + packages/core/src/Mark.ts | 9 ++ packages/core/src/Node.ts | 9 ++ packages/core/src/types.ts | 1 + .../extension-code-block-lowlight/README.md | 14 ++ .../package.json | 33 ++++ .../src/code-block-lowlight.ts | 35 +++++ .../src/index.ts | 5 + .../src/lowlight-plugin.ts | 110 +++++++++++++ yarn.lock | 30 ++++ 16 files changed, 476 insertions(+) create mode 100644 docs/src/demos/Nodes/CodeBlockLowlight/index.vue create mode 100644 docs/src/docPages/api/nodes/code-block-lowlight.md create mode 100644 packages/extension-code-block-lowlight/README.md create mode 100644 packages/extension-code-block-lowlight/package.json create mode 100644 packages/extension-code-block-lowlight/src/code-block-lowlight.ts create mode 100644 packages/extension-code-block-lowlight/src/index.ts create mode 100644 packages/extension-code-block-lowlight/src/lowlight-plugin.ts diff --git a/docs/src/demos/Nodes/CodeBlock/index.vue b/docs/src/demos/Nodes/CodeBlock/index.vue index 54b3c676a..cea9f5422 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 000000000..356e25b27 --- /dev/null +++ b/docs/src/demos/Nodes/CodeBlockLowlight/index.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/docs/src/docPages/api/nodes/code-block-lowlight.md b/docs/src/docPages/api/nodes/code-block-lowlight.md new file mode 100644 index 000000000..47a9a8b9a --- /dev/null +++ b/docs/src/docPages/api/nodes/code-block-lowlight.md @@ -0,0 +1,41 @@ +# CodeBlockLowlight +[![Version](https://img.shields.io/npm/v/@tiptap/extension-code-block-lowlight.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight) +[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-code-block-lowlight.svg)](https://npmcharts.com/compare/@tiptap/extension-code-block-lowlight?minimal=true) + +With the CodeBlock extension you can add fenced code blocks to your documents. It’ll wrap the code in `
` 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 7950055b1..d61a485e5 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 f54522026..843c5f186 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 db0af8673..6660338bc 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 7284d75c2..8261260be 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 7986b5df9..b3ff2de26 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 315b38ccd..8fafdb37f 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 453d52c69..75d201118 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 000000000..d950097e8
--- /dev/null
+++ b/packages/extension-code-block-lowlight/README.md
@@ -0,0 +1,14 @@
+# @tiptap/extension-code-block-lowlight
+[![Version](https://img.shields.io/npm/v/@tiptap/extension-code-block-lowlight.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight)
+[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-code-block-lowlight.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
+[![License](https://img.shields.io/npm/l/@tiptap/extension-code-block-lowlight.svg)](https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight)
+[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](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 000000000..f0ab31000
--- /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 000000000..9b8670ccf
--- /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 000000000..257f48ac0
--- /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 000000000..c8c47454b
--- /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 1f2291fef..99c747f20 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"