From f3a31a49c0cab6d4be102a2b258d776b64e9275f Mon Sep 17 00:00:00 2001 From: jerrywcy Date: Thu, 17 Aug 2023 01:12:58 +0800 Subject: [PATCH] feat: add support for code block syntax highlighting --- package-lock.json | 143 +++++++++++++++++----- package.json | 2 + src/extensions/rich-text/rich-text-kit.ts | 17 ++- src/serializers/html/html.test.ts | 20 +-- 4 files changed, 138 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index bcc42828..f69896ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@tiptap/extension-character-count": "2.0.3", "@tiptap/extension-code": "2.0.3", "@tiptap/extension-code-block": "2.0.3", + "@tiptap/extension-code-block-lowlight": "2.1.0", "@tiptap/extension-document": "2.0.3", "@tiptap/extension-dropcursor": "2.0.3", "@tiptap/extension-gapcursor": "2.0.3", @@ -41,6 +42,7 @@ "@tiptap/pm": "2.0.3", "@tiptap/react": "2.0.3", "@tiptap/suggestion": "2.0.3", + "lowlight": "2.9.0", "prosemirror-codemark": "0.4.2" }, "devDependencies": { @@ -7974,6 +7976,20 @@ "@tiptap/pm": "^2.0.0" } }, + "node_modules/@tiptap/extension-code-block-lowlight": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.0.tgz", + "integrity": "sha512-PLsqQx4uhjcsFw1La0Gc6DLDl6wPmwXcnza3+S3QIJjCABCGv+H5kuCWpq7qWIkHGSO4JLfkP2gIb7+l5eOZgw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/extension-code-block": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, "node_modules/@tiptap/extension-document": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.3.tgz", @@ -13905,10 +13921,9 @@ } }, "node_modules/fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", "dependencies": { "format": "^0.2.0" }, @@ -14322,7 +14337,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true, "engines": { "node": ">=0.4.x" } @@ -15278,12 +15292,11 @@ } }, "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true, + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", "engines": { - "node": "*" + "node": ">=12.0.0" } }, "node_modules/hook-std": { @@ -17966,13 +17979,13 @@ } }, "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "dev": true, + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.9.0.tgz", + "integrity": "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==", "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.8.0" }, "funding": { "type": "github", @@ -25903,6 +25916,42 @@ "react": ">= 0.14.0" } }, + "node_modules/react-syntax-highlighter/node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/react-syntax-highlighter/node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -35416,6 +35465,12 @@ "integrity": "sha512-F4xMy18EwgpyY9f5Te7UuF7UwxRLptOtCq1p2c2DfxBvHDWhAjQqVqcW/sq/I/WuED7FwCnPLyyAasPiVPkLPw==", "requires": {} }, + "@tiptap/extension-code-block-lowlight": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.0.tgz", + "integrity": "sha512-PLsqQx4uhjcsFw1La0Gc6DLDl6wPmwXcnza3+S3QIJjCABCGv+H5kuCWpq7qWIkHGSO4JLfkP2gIb7+l5eOZgw==", + "requires": {} + }, "@tiptap/extension-document": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.3.tgz", @@ -39859,10 +39914,9 @@ } }, "fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", "requires": { "format": "^0.2.0" } @@ -40180,8 +40234,7 @@ "format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==" }, "forwarded": { "version": "0.2.0", @@ -40922,10 +40975,9 @@ } }, "highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==" }, "hook-std": { "version": "3.0.0", @@ -42834,13 +42886,13 @@ } }, "lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", - "dev": true, + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.9.0.tgz", + "integrity": "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==", "requires": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.8.0" } }, "lru-cache": { @@ -48315,6 +48367,33 @@ "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" + }, + "dependencies": { + "fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "requires": { + "format": "^0.2.0" + } + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, + "requires": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + } + } } }, "read-pkg": { diff --git a/package.json b/package.json index de512680..d5c0a943 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@tiptap/extension-character-count": "2.0.3", "@tiptap/extension-code": "2.0.3", "@tiptap/extension-code-block": "2.0.3", + "@tiptap/extension-code-block-lowlight": "2.1.0", "@tiptap/extension-document": "2.0.3", "@tiptap/extension-dropcursor": "2.0.3", "@tiptap/extension-gapcursor": "2.0.3", @@ -78,6 +79,7 @@ "@tiptap/pm": "2.0.3", "@tiptap/react": "2.0.3", "@tiptap/suggestion": "2.0.3", + "lowlight": "2.9.0", "prosemirror-codemark": "0.4.2" }, "devDependencies": { diff --git a/src/extensions/rich-text/rich-text-kit.ts b/src/extensions/rich-text/rich-text-kit.ts index 45424599..a65c3d1c 100644 --- a/src/extensions/rich-text/rich-text-kit.ts +++ b/src/extensions/rich-text/rich-text-kit.ts @@ -2,6 +2,7 @@ import { Extension } from '@tiptap/core' import { Blockquote } from '@tiptap/extension-blockquote' import { Bold } from '@tiptap/extension-bold' import { CodeBlock } from '@tiptap/extension-code-block' +import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' import { Dropcursor } from '@tiptap/extension-dropcursor' import { Gapcursor } from '@tiptap/extension-gapcursor' import { HardBreak } from '@tiptap/extension-hard-break' @@ -17,6 +18,7 @@ import { TaskList } from '@tiptap/extension-task-list' import { Text } from '@tiptap/extension-text' import { Typography } from '@tiptap/extension-typography' import { Underline } from '@tiptap/extension-underline' +import { lowlight } from 'lowlight/lib/common' import { BLOCKQUOTE_EXTENSION_PRIORITY } from '../../constants/extension-priorities' import { CopyMarkdownSource } from '../shared/copy-markdown-source' @@ -41,6 +43,7 @@ import type { BoldOptions } from '@tiptap/extension-bold' import type { BulletListOptions } from '@tiptap/extension-bullet-list' import type { CodeOptions } from '@tiptap/extension-code' import type { CodeBlockOptions } from '@tiptap/extension-code-block' +import type { CodeBlockLowlightOptions } from '@tiptap/extension-code-block-lowlight' import type { DropcursorOptions } from '@tiptap/extension-dropcursor' import type { HardBreakOptions } from '@tiptap/extension-hard-break' import type { HeadingOptions } from '@tiptap/extension-heading' @@ -87,6 +90,11 @@ type RichTextKitOptions = { */ codeBlock: Partial | false + /** + * Set options for the `CodeBlockLowlight` extension, or `false` to disable. + */ + codeBlockLowlight: Partial | false + /** * Set options for the `Document` extension, or `false` to disable. */ @@ -113,7 +121,7 @@ type RichTextKitOptions = { heading: Partial | false /** - * Set options for the `Heading` extension, or `false` to disable. + * Set options for the `Highlight` extension, or `false` to disable. */ highlight: Partial | false @@ -232,7 +240,6 @@ const RichTextKit = Extension.create({ if (this.options.code !== false) { extensions.push( RichTextCode.configure(this.options?.code), - // Enhances the Code extension capabilities with additional features CurvenoteCodemark, ) @@ -242,6 +249,12 @@ const RichTextKit = Extension.create({ extensions.push(CodeBlock.configure(this.options?.codeBlock)) } + if (this.options.codeBlockLowlight !== false) { + extensions.push( + CodeBlockLowlight.configure({ lowlight, ...this.options?.codeBlockLowlight }), + ) + } + if (this.options.document !== false) { extensions.push( RichTextDocument.configure(this.options?.document), diff --git a/src/serializers/html/html.test.ts b/src/serializers/html/html.test.ts index 8e243653..f838db01 100644 --- a/src/serializers/html/html.test.ts +++ b/src/serializers/html/html.test.ts @@ -637,14 +637,13 @@ I need to add another paragraph below the second list item. ) }) - test('code block HTML output is preserved', () => { + test('code block HTML output is turned into syntax highlighting', () => { expect(htmlSerializer.serialize(MARKDOWN_INPUT_CODE_BLOCK)).toBe( - `

\`\`\`

<html> + `
<html>
   <head>
     <title>Test</title>
   </head>
-</html>
-\`\`\``,
+</html>
`, ) }) @@ -655,12 +654,13 @@ I need to add another paragraph below the second list item. 2. Image: ![Octobi Wan Catnobi](https://octodex.github.com/images/octobiwan.jpg) 3. Codeblock: -\`\`\`

<html> - <head> - <title>Test</title> - </head> - </html> - \`\`\``) +\`\`\` +<html> +<head> +<title>Test</title> +</head> +</html> +\`\`\`

`) }) test('horizontal rules HTML output is preserved', () => {