tiptap-extension-shiki 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,28 +5,20 @@ on:
5
5
  branches:
6
6
  # 触发ci/cd的代码分支
7
7
  - master
8
+ # 必须添加这个权限,否则无法获取临时身份证明
9
+ permissions:
10
+ id-token: write # Required for OIDC
11
+ contents: read
8
12
 
9
13
  jobs:
10
14
  build_and_publish:
11
15
  runs-on: ubuntu-latest
12
- # 必须添加这个权限,否则无法获取临时身份证明
13
- permissions:
14
- contents: read
15
- id-token: write
16
-
17
16
  steps:
18
17
  - uses: actions/checkout@v4
19
18
  - uses: actions/setup-node@v4
20
19
  with:
21
- node-version: '20'
22
- # 注意:这里不再需要配置 registry-url 或 NODE_AUTH_TOKEN
23
-
24
- - name: Install dependencies
25
- run: npm install
26
-
27
- - name: Build package
28
- run: npm run build
29
-
30
- - name: Publish to NPM
31
- # 使用专用 Action,它会自动识别 OIDC 身份并完成发布
32
- run: npm publish --provenance --access public
20
+ node-version: '24'
21
+ registry-url: 'https://registry.npmjs.org'
22
+ - run: npm ci
23
+ - run: npm run build
24
+ - run: npm publish
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Tiptap Extension Shiki
2
2
 
3
- [📖 View Chinese Version](./README_CN.md)
3
+ [📖 查看中文版](./README_CN.md)
4
4
 
5
5
  ### Installation
6
6
 
@@ -10,6 +10,8 @@ npm install shiki tiptap-extension-shiki
10
10
 
11
11
  ### Usage
12
12
 
13
+ [DEMO](https://codesandbox.io/p/sandbox/tiptap-extension-shiki-demo-lgl8lw)
14
+
13
15
  ```typescript
14
16
  new Editor({
15
17
  content: "",
package/README_CN.md CHANGED
@@ -10,6 +10,7 @@ npm install shiki tiptap-extension-shiki
10
10
 
11
11
  ### 使用方法
12
12
 
13
+ [DEMO](https://codesandbox.io/p/sandbox/tiptap-extension-shiki-demo-lgl8lw)
13
14
  ```typescript
14
15
  new Editor({
15
16
  content: "",
package/dist/index.cjs.js CHANGED
@@ -19,10 +19,8 @@ var view = require('@tiptap/pm/view');
19
19
  * @returns 装饰器集合,用于渲染语法高亮
20
20
  */
21
21
  function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, }) {
22
- console.log("doc", doc);
23
22
  // 查找文档中所有指定类型的节点(shiki代码块)
24
23
  const decorations = core.findChildren(doc, (node) => {
25
- console.log("node node ", node);
26
24
  return node.type.name === name;
27
25
  }).reduce((acc, block) => {
28
26
  // 为每个代码块生成装饰
@@ -259,6 +257,8 @@ const TiptapShiki = CodeBlock.extend({
259
257
  * @returns HTML 数组或元素
260
258
  */
261
259
  renderHTML({ node, HTMLAttributes }) {
260
+ var _a;
261
+ const extraRenderHTMLAttributes = (_a = this.options) === null || _a === void 0 ? void 0 : _a.extraRenderHTMLAttributes;
262
262
  // 如果启用了高亮 HTML 生成并且有高亮器实例
263
263
  if (this.options.getHighlighHTML && this.options.highlighter) {
264
264
  // 获取代码内容、编程语言和主题
@@ -273,8 +273,29 @@ const TiptapShiki = CodeBlock.extend({
273
273
  // 解析生成的 HTML 字符串并返回第一个元素
274
274
  const container = document.createElement("div");
275
275
  container.innerHTML = highlightedCode;
276
- if (container.firstElementChild)
277
- return container.firstElementChild;
276
+ if (container.firstElementChild) {
277
+ const rsDOM = container.firstElementChild;
278
+ if (extraRenderHTMLAttributes) {
279
+ // 处理 classList
280
+ if (extraRenderHTMLAttributes.classList)
281
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
282
+ // 处理 styles
283
+ if (extraRenderHTMLAttributes.styles) {
284
+ for (const key in extraRenderHTMLAttributes.styles) {
285
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
286
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
287
+ }
288
+ }
289
+ // 处理 attrs
290
+ if (extraRenderHTMLAttributes.attrs) {
291
+ for (const key in extraRenderHTMLAttributes.attrs) {
292
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
293
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
294
+ }
295
+ }
296
+ }
297
+ return rsDOM;
298
+ }
278
299
  }
279
300
  // 返回标准的 pre/code 标签结构
280
301
  return ["pre", HTMLAttributes, ["code", 0]];
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -25,6 +25,11 @@ declare const TiptapShiki: import("@tiptap/core").Node<{
25
25
  defaultLanguage: StringLiteralUnion<SpecialLanguage>;
26
26
  highlighter?: HighlighterGeneric<BundledLanguage, BundledTheme>;
27
27
  getHighlighHTML?: boolean;
28
+ extraRenderHTMLAttributes?: {
29
+ classList?: string[];
30
+ styles?: Record<string, string>;
31
+ attrs?: Record<string, string>;
32
+ };
28
33
  renderToolbar?: (props: {
29
34
  toolbarDOM: HTMLElement;
30
35
  language: StringLiteralUnion<SpecialLanguage>;
package/dist/index.js CHANGED
@@ -15,10 +15,8 @@ import { DecorationSet, Decoration } from '@tiptap/pm/view';
15
15
  * @returns 装饰器集合,用于渲染语法高亮
16
16
  */
17
17
  function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, }) {
18
- console.log("doc", doc);
19
18
  // 查找文档中所有指定类型的节点(shiki代码块)
20
19
  const decorations = findChildren(doc, (node) => {
21
- console.log("node node ", node);
22
20
  return node.type.name === name;
23
21
  }).reduce((acc, block) => {
24
22
  // 为每个代码块生成装饰
@@ -255,6 +253,8 @@ const TiptapShiki = CodeBlock.extend({
255
253
  * @returns HTML 数组或元素
256
254
  */
257
255
  renderHTML({ node, HTMLAttributes }) {
256
+ var _a;
257
+ const extraRenderHTMLAttributes = (_a = this.options) === null || _a === void 0 ? void 0 : _a.extraRenderHTMLAttributes;
258
258
  // 如果启用了高亮 HTML 生成并且有高亮器实例
259
259
  if (this.options.getHighlighHTML && this.options.highlighter) {
260
260
  // 获取代码内容、编程语言和主题
@@ -269,8 +269,29 @@ const TiptapShiki = CodeBlock.extend({
269
269
  // 解析生成的 HTML 字符串并返回第一个元素
270
270
  const container = document.createElement("div");
271
271
  container.innerHTML = highlightedCode;
272
- if (container.firstElementChild)
273
- return container.firstElementChild;
272
+ if (container.firstElementChild) {
273
+ const rsDOM = container.firstElementChild;
274
+ if (extraRenderHTMLAttributes) {
275
+ // 处理 classList
276
+ if (extraRenderHTMLAttributes.classList)
277
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
278
+ // 处理 styles
279
+ if (extraRenderHTMLAttributes.styles) {
280
+ for (const key in extraRenderHTMLAttributes.styles) {
281
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
282
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
283
+ }
284
+ }
285
+ // 处理 attrs
286
+ if (extraRenderHTMLAttributes.attrs) {
287
+ for (const key in extraRenderHTMLAttributes.attrs) {
288
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
289
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
290
+ }
291
+ }
292
+ }
293
+ return rsDOM;
294
+ }
274
295
  }
275
296
  // 返回标准的 pre/code 标签结构
276
297
  return ["pre", HTMLAttributes, ["code", 0]];
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "tiptap-extension-shiki",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/zerooverture/tiptap-extension-shiki.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
8
15
  "scripts": {
9
16
  "clean": "rm -rf dist",
10
17
  "build": "npm run clean && npm run build:js && npm run build:types",
@@ -35,11 +35,8 @@ function getDecorations({
35
35
  defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>;
36
36
  defaultLanguage: StringLiteralUnion<SpecialLanguage>;
37
37
  }) {
38
- console.log("doc", doc);
39
-
40
38
  // 查找文档中所有指定类型的节点(shiki代码块)
41
39
  const decorations = findChildren(doc, (node) => {
42
- console.log("node node ", node);
43
40
  return node.type.name === name;
44
41
  }).reduce((acc, block) => {
45
42
  // 为每个代码块生成装饰
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tiptap Shiki 语法高亮扩展
3
- *
3
+ *
4
4
  * 本文件实现了一个基于 Shiki 的 Tiptap 扩展,用于在富文本编辑器中提供语法高亮功能。
5
5
  * 该扩展扩展了 Tiptap 的 CodeBlock,添加了以下功能:
6
6
  * - 支持多种编程语言的语法高亮
@@ -27,13 +27,14 @@ import type {
27
27
 
28
28
  // 导入 Shiki 轻量级插件
29
29
  import { ShikiLightPlugin } from "./ShikiLightPlugin.js";
30
+ import { mergeAttributes } from "@tiptap/core";
30
31
 
31
32
  /**
32
33
  * TiptapShiki 扩展类定义
33
- *
34
+ *
34
35
  * 该类扩展了 Tiptap 的 CodeBlock,添加了语法高亮功能和相关配置选项。
35
36
  * 使用泛型类型定义扩展选项和节点视图属性。
36
- *
37
+ *
37
38
  * @template T - 扩展选项类型,包含默认主题、语言、高亮器等配置
38
39
  * @template U - 节点视图属性类型,定义 DOM 元素的类型
39
40
  */
@@ -47,6 +48,12 @@ const TiptapShiki = CodeBlock.extend<
47
48
  highlighter?: HighlighterGeneric<BundledLanguage, BundledTheme>;
48
49
  // 是否获取高亮的 HTML 字符串
49
50
  getHighlighHTML?: boolean;
51
+ // 额外渲染HTML属性
52
+ extraRenderHTMLAttributes?: {
53
+ classList?: string[];
54
+ styles?: Record<string, string>;
55
+ attrs?: Record<string, string>;
56
+ };
50
57
  // 自定义工具栏渲染函数
51
58
  renderToolbar?: (props: {
52
59
  toolbarDOM: HTMLElement;
@@ -56,285 +63,310 @@ const TiptapShiki = CodeBlock.extend<
56
63
  setTheme: (theme: string) => void;
57
64
  }) => void;
58
65
  },
59
- {
66
+ {
60
67
  // 节点视图中的 DOM 元素类型定义
61
- container: HTMLElement;
62
- shikiCode: HTMLElement;
63
- contentDOM: HTMLElement
68
+ container: HTMLElement;
69
+ shikiCode: HTMLElement;
70
+ contentDOM: HTMLElement;
64
71
  }
65
72
  >({
66
- // 扩展名称,用于标识这个自定义扩展
67
- name: "tiptapShiki",
68
-
69
- /**
70
- * 添加扩展配置选项
71
- *
72
- * 定义了 Shiki 语法高亮的默认配置,包括默认主题、编程语言等。
73
- * 使用父类的配置选项并添加本扩展特有的选项。
74
- *
75
- * @returns 配置对象,包含默认主题、语言和高亮器设置
76
- */
77
- addOptions() {
78
- return {
79
- // 继承父类的配置选项
80
- ...this.parent?.(),
81
- // 默认语法高亮主题
82
- defaultTheme: "dracula",
83
- // 默认编程语言
84
- defaultLanguage: "javascript",
85
- // 初始化时的高亮器实例(可选)
86
- highlighter: undefined,
87
- };
88
- },
89
- /**
90
- * 添加节点属性
91
- *
92
- * 定义了代码块节点的自定义属性,包括编程语言和主题。
93
- * 这些属性用于存储和渲染代码块的语法高亮配置。
94
- *
95
- * @returns 属性配置对象,包含语言和主题属性
96
- */
97
- addAttributes() {
98
- return {
99
- // 继承父类的属性
100
- ...this.parent?.(),
101
-
102
- // 编程语言属性
103
- language: {
104
- // 默认语言配置
105
- default: this.options.defaultLanguage || "javascript",
106
-
107
- /**
108
- * 从 HTML 元素解析语言属性
109
- * @param element - HTML 元素
110
- * @returns 编程语言字符串
111
- */
112
- parseHTML: (element) => {
113
- // 从 data-language 属性获取语言信息
114
- return element.getAttribute("data-language");
115
- },
116
-
117
- /**
118
- * 渲染 HTML 时设置语言属性
119
- * @param attributes - 节点属性对象
120
- * @returns HTML 属性对象
121
- */
122
- renderHTML: (attributes) => {
123
- // 将语言信息存储在 data-language 属性中
124
- return { "data-language": attributes.language };
125
- },
126
- },
127
-
128
- // 主题属性
129
- theme: {
130
- // 默认主题配置
131
- default: this.options.defaultTheme || "dracula",
132
-
133
- /**
134
- * 从 HTML 元素解析主题属性
135
- * @param element - HTML 元素
136
- * @returns 主题名称字符串
137
- */
138
- parseHTML: (element) => element.getAttribute("data-theme"),
139
-
140
- /**
141
- * 渲染 HTML 时设置主题属性
142
- * @param attributes - 节点属性对象
143
- * @returns HTML 属性对象
144
- */
145
- renderHTML: (attributes) => ({
146
- "data-theme": attributes.theme,
147
- }),
148
- },
149
- };
150
- },
151
-
152
- /**
153
- * 渲染 HTML
154
- *
155
- * 定义了代码块节点在静态 HTML 中的渲染逻辑。
156
- * 如果配置了 getHighlighHTML 和 highlighter,将使用 Shiki 生成高亮的 HTML;
157
- * 否则返回标准的 pre/code 标签结构。
158
- *
159
- * @param node - ProseMirror 节点对象
160
- * @param HTMLAttributes - HTML 属性对象
161
- * @returns HTML 数组或元素
162
- */
163
- renderHTML({ node, HTMLAttributes }) {
164
- // 如果启用了高亮 HTML 生成并且有高亮器实例
165
- if (this.options.getHighlighHTML && this.options.highlighter) {
166
- // 获取代码内容、编程语言和主题
167
- const language = node.attrs.language || this.options.defaultLanguage;
168
- const theme = node.attrs.theme || this.options.defaultTheme;
169
- const content = node.textContent;
170
-
171
- // 使用 Shiki 将代码转换为带样式的 HTML
172
- const highlightedCode = this.options.highlighter.codeToHtml(content, {
173
- lang: language,
174
- theme: theme,
175
- });
176
-
177
- // 解析生成的 HTML 字符串并返回第一个元素
178
- const container = document.createElement("div");
179
- container.innerHTML = highlightedCode;
180
- if (container.firstElementChild) return container.firstElementChild;
181
- }
182
-
183
- // 返回标准的 pre/code 标签结构
184
- return ["pre", HTMLAttributes, ["code", 0]];
73
+ // 扩展名称,用于标识这个自定义扩展
74
+ name: "tiptapShiki",
75
+
76
+ /**
77
+ * 添加扩展配置选项
78
+ *
79
+ * 定义了 Shiki 语法高亮的默认配置,包括默认主题、编程语言等。
80
+ * 使用父类的配置选项并添加本扩展特有的选项。
81
+ *
82
+ * @returns 配置对象,包含默认主题、语言和高亮器设置
83
+ */
84
+ addOptions() {
85
+ return {
86
+ // 继承父类的配置选项
87
+ ...this.parent?.(),
88
+ // 默认语法高亮主题
89
+ defaultTheme: "dracula",
90
+ // 默认编程语言
91
+ defaultLanguage: "javascript",
92
+ // 初始化时的高亮器实例(可选)
93
+ highlighter: undefined,
94
+ };
95
+ },
96
+ /**
97
+ * 添加节点属性
98
+ *
99
+ * 定义了代码块节点的自定义属性,包括编程语言和主题。
100
+ * 这些属性用于存储和渲染代码块的语法高亮配置。
101
+ *
102
+ * @returns 属性配置对象,包含语言和主题属性
103
+ */
104
+ addAttributes() {
105
+ return {
106
+ // 继承父类的属性
107
+ ...this.parent?.(),
108
+
109
+ // 编程语言属性
110
+ language: {
111
+ // 默认语言配置
112
+ default: this.options.defaultLanguage || "javascript",
113
+
114
+ /**
115
+ * 从 HTML 元素解析语言属性
116
+ * @param element - HTML 元素
117
+ * @returns 编程语言字符串
118
+ */
119
+ parseHTML: (element) => {
120
+ // 从 data-language 属性获取语言信息
121
+ return element.getAttribute("data-language");
122
+ },
123
+
124
+ /**
125
+ * 渲染 HTML 时设置语言属性
126
+ * @param attributes - 节点属性对象
127
+ * @returns HTML 属性对象
128
+ */
129
+ renderHTML: (attributes) => {
130
+ // 将语言信息存储在 data-language 属性中
131
+ return { "data-language": attributes.language };
132
+ },
185
133
  },
186
134
 
187
- /**
188
- * 添加节点视图
189
- *
190
- * 创建代码块的交互式 DOM 视图,包括工具栏和代码显示区域。
191
- * 提供了实时的语法高亮和主题切换功能。
192
- *
193
- * @returns 节点视图工厂函数
194
- */
195
- addNodeView() {
196
- // 检查是否有高亮器实例
197
- if (!this.options.highlighter) throw new Error("highlighter is required");
198
-
199
- return (props) => {
200
- // 获取视图相关参数
201
- const { view, getPos, node } = props;
202
-
203
- // 获取当前主题配置
204
- const theme = this.options.highlighter.getTheme(
205
- node.attrs.theme || this.options.defaultTheme
206
- );
207
-
208
- // 创建主容器 DOM 元素
209
- const dom = document.createElement("div");
210
- dom.classList.add(
211
- "tiptap-shiki--container",
212
- "not-prose",
213
- "shiki",
214
- "dracula"
215
- );
216
- // 应用主题颜色
217
- dom.style.backgroundColor = theme.bg;
218
- dom.style.color = theme.fg;
219
-
220
- // 创建工具栏(如果配置了自定义渲染器)
221
- let toolbarDOM: HTMLElement | undefined;
222
- if (
223
- this.options.renderToolbar &&
224
- typeof this.options.renderToolbar === "function"
225
- ) {
226
- // 创建工具栏容器
227
- toolbarDOM = document.createElement("div");
228
- toolbarDOM.classList.add("tiptap-shiki--toolbar");
229
- toolbarDOM.setAttribute("contenteditable", "false");
230
-
231
- // 创建节点标记更新函数
232
- const setNodeMarkup = (attr: Record<string, string>) => {
233
- if (typeof getPos !== "function") return;
234
- const pos = getPos();
235
- if (pos === undefined) return;
236
- const { tr } = view.state;
237
- const nowNode = view.state.doc.nodeAt(pos);
238
-
239
- // 更新节点属性
240
- tr.setNodeMarkup(pos, this.type, { ...nowNode?.attrs, ...attr });
241
- view.dispatch(tr);
242
- };
243
-
244
- // 创建语言设置函数
245
- const setLanguage = (language: string) => {
246
- setNodeMarkup({
247
- language, // 仅修改当前代码块的编程语言
248
- });
249
- };
250
-
251
- // 创建主题设置函数
252
- const setTheme = (theme: string) => {
253
- setNodeMarkup({
254
- theme, // 仅修改当前代码块的主题
255
- });
256
- };
257
-
258
- // 调用自定义工具栏渲染器
259
- this.options.renderToolbar({
260
- language: node.attrs.language,
261
- theme: node.attrs.theme,
262
- toolbarDOM,
263
- setLanguage,
264
- setTheme,
265
- });
266
-
267
- // 将工具栏添加到主容器
268
- dom.appendChild(toolbarDOM);
269
- }
270
-
271
- // 创建代码显示区域
272
- const preDOM = document.createElement("pre");
273
- const contentDOM = document.createElement("code");
274
- contentDOM.classList.add("tiptap-shiki--content");
275
-
276
- // 组装 DOM 结构
277
- preDOM.appendChild(contentDOM);
278
- dom.appendChild(preDOM);
279
-
280
- // 返回节点视图对象
281
- return {
282
- // 主 DOM 元素
283
- dom: dom,
284
- // 内容 DOM 元素
285
- contentDOM,
286
-
287
- /**
288
- * 更新节点视图
289
- * @param updatedNode - 更新后的节点
290
- * @returns 是否成功更新
291
- */
292
- update: (updatedNode) => {
293
- // 如果不是相同类型的节点,拒绝更新
294
- return updatedNode.type === node.type;
295
- },
296
-
297
- /**
298
- * 忽略某些 DOM 变化
299
- * @param mutation - DOM 变化对象
300
- * @returns 是否忽略该变化
301
- */
302
- ignoreMutation: (mutation) => {
303
- // 忽略工具栏区域的 DOM 变化
304
- return toolbarDOM?.contains(mutation.target) || false;
305
- },
306
- };
307
- };
308
- },
135
+ // 主题属性
136
+ theme: {
137
+ // 默认主题配置
138
+ default: this.options.defaultTheme || "dracula",
139
+
140
+ /**
141
+ * HTML 元素解析主题属性
142
+ * @param element - HTML 元素
143
+ * @returns 主题名称字符串
144
+ */
145
+ parseHTML: (element) => element.getAttribute("data-theme"),
146
+
147
+ /**
148
+ * 渲染 HTML 时设置主题属性
149
+ * @param attributes - 节点属性对象
150
+ * @returns HTML 属性对象
151
+ */
152
+ renderHTML: (attributes) => ({
153
+ "data-theme": attributes.theme,
154
+ }),
155
+ },
156
+ };
157
+ },
158
+
159
+ /**
160
+ * 渲染 HTML
161
+ *
162
+ * 定义了代码块节点在静态 HTML 中的渲染逻辑。
163
+ * 如果配置了 getHighlighHTML 和 highlighter,将使用 Shiki 生成高亮的 HTML;
164
+ * 否则返回标准的 pre/code 标签结构。
165
+ *
166
+ * @param node - ProseMirror 节点对象
167
+ * @param HTMLAttributes - HTML 属性对象
168
+ * @returns HTML 数组或元素
169
+ */
170
+ renderHTML({ node, HTMLAttributes }) {
171
+ const extraRenderHTMLAttributes = this.options?.extraRenderHTMLAttributes;
172
+ // 如果启用了高亮 HTML 生成并且有高亮器实例
173
+ if (this.options.getHighlighHTML && this.options.highlighter) {
174
+ // 获取代码内容、编程语言和主题
175
+ const language = node.attrs.language || this.options.defaultLanguage;
176
+ const theme = node.attrs.theme || this.options.defaultTheme;
177
+ const content = node.textContent;
178
+
179
+ // 使用 Shiki 将代码转换为带样式的 HTML
180
+ const highlightedCode = this.options.highlighter.codeToHtml(content, {
181
+ lang: language,
182
+ theme: theme,
183
+ });
184
+
185
+ // 解析生成的 HTML 字符串并返回第一个元素
186
+ const container = document.createElement("div");
187
+ container.innerHTML = highlightedCode;
188
+ if (container.firstElementChild) {
189
+ const rsDOM = container.firstElementChild as HTMLElement;
190
+ if (extraRenderHTMLAttributes) {
191
+ // 处理 classList
192
+ if (extraRenderHTMLAttributes.classList)
193
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
194
+
195
+ // 处理 styles
196
+ if (extraRenderHTMLAttributes.styles) {
197
+ for (const key in extraRenderHTMLAttributes.styles) {
198
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
199
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
200
+ }
201
+ }
202
+
203
+ // 处理 attrs
204
+ if (extraRenderHTMLAttributes.attrs) {
205
+ for (const key in extraRenderHTMLAttributes.attrs) {
206
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
207
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
208
+ }
209
+ }
210
+ }
211
+ return rsDOM;
212
+ }
213
+ }
214
+
215
+ // 返回标准的 pre/code 标签结构
216
+ return ["pre", HTMLAttributes, ["code", 0]];
217
+ },
218
+
219
+ /**
220
+ * 添加节点视图
221
+ *
222
+ * 创建代码块的交互式 DOM 视图,包括工具栏和代码显示区域。
223
+ * 提供了实时的语法高亮和主题切换功能。
224
+ *
225
+ * @returns 节点视图工厂函数
226
+ */
227
+ addNodeView() {
228
+ // 检查是否有高亮器实例
229
+ if (!this.options.highlighter) throw new Error("highlighter is required");
230
+
231
+ return (props) => {
232
+ // 获取视图相关参数
233
+ const { view, getPos, node } = props;
234
+
235
+ // 获取当前主题配置
236
+ const theme = this.options.highlighter.getTheme(
237
+ node.attrs.theme || this.options.defaultTheme
238
+ );
239
+
240
+ // 创建主容器 DOM 元素
241
+ const dom = document.createElement("div");
242
+ dom.classList.add(
243
+ "tiptap-shiki--container",
244
+ "not-prose",
245
+ "shiki",
246
+ "dracula"
247
+ );
248
+ // 应用主题颜色
249
+ dom.style.backgroundColor = theme.bg;
250
+ dom.style.color = theme.fg;
251
+
252
+ // 创建工具栏(如果配置了自定义渲染器)
253
+ let toolbarDOM: HTMLElement | undefined;
254
+ if (
255
+ this.options.renderToolbar &&
256
+ typeof this.options.renderToolbar === "function"
257
+ ) {
258
+ // 创建工具栏容器
259
+ toolbarDOM = document.createElement("div");
260
+ toolbarDOM.classList.add("tiptap-shiki--toolbar");
261
+ toolbarDOM.setAttribute("contenteditable", "false");
262
+
263
+ // 创建节点标记更新函数
264
+ const setNodeMarkup = (attr: Record<string, string>) => {
265
+ if (typeof getPos !== "function") return;
266
+ const pos = getPos();
267
+ if (pos === undefined) return;
268
+ const { tr } = view.state;
269
+ const nowNode = view.state.doc.nodeAt(pos);
270
+
271
+ // 更新节点属性
272
+ tr.setNodeMarkup(pos, this.type, { ...nowNode?.attrs, ...attr });
273
+ view.dispatch(tr);
274
+ };
275
+
276
+ // 创建语言设置函数
277
+ const setLanguage = (language: string) => {
278
+ setNodeMarkup({
279
+ language, // 仅修改当前代码块的编程语言
280
+ });
281
+ };
282
+
283
+ // 创建主题设置函数
284
+ const setTheme = (theme: string) => {
285
+ setNodeMarkup({
286
+ theme, // 仅修改当前代码块的主题
287
+ });
288
+ };
289
+
290
+ // 调用自定义工具栏渲染器
291
+ this.options.renderToolbar({
292
+ language: node.attrs.language,
293
+ theme: node.attrs.theme,
294
+ toolbarDOM,
295
+ setLanguage,
296
+ setTheme,
297
+ });
298
+
299
+ // 将工具栏添加到主容器
300
+ dom.appendChild(toolbarDOM);
301
+ }
302
+
303
+ // 创建代码显示区域
304
+ const preDOM = document.createElement("pre");
305
+ const contentDOM = document.createElement("code");
306
+ contentDOM.classList.add("tiptap-shiki--content");
307
+
308
+ // 组装 DOM 结构
309
+ preDOM.appendChild(contentDOM);
310
+ dom.appendChild(preDOM);
311
+
312
+ // 返回节点视图对象
313
+ return {
314
+ // 主 DOM 元素
315
+ dom: dom,
316
+ // 内容 DOM 元素
317
+ contentDOM,
318
+
319
+ /**
320
+ * 更新节点视图
321
+ * @param updatedNode - 更新后的节点
322
+ * @returns 是否成功更新
323
+ */
324
+ update: (updatedNode) => {
325
+ // 如果不是相同类型的节点,拒绝更新
326
+ return updatedNode.type === node.type;
327
+ },
309
328
 
310
329
  /**
311
- * 添加 ProseMirror 插件
312
- *
313
- * 注册 Shiki 轻量级语法高亮插件,实现实时语法高亮功能。
314
- * 继承父类的插件并添加自定义的高亮插件。
315
- *
316
- * @returns ProseMirror 插件数组
330
+ * 忽略某些 DOM 变化
331
+ * @param mutation - DOM 变化对象
332
+ * @returns 是否忽略该变化
317
333
  */
318
- addProseMirrorPlugins() {
319
- // 检查是否有高亮器实例
320
- if (!this.options.highlighter) throw new Error("highlighter is required");
321
-
322
- return [
323
- // 继承父类的插件
324
- ...(this.parent?.() || []),
325
- // 添加 Shiki 轻量级高亮插件
326
- ShikiLightPlugin({
327
- name: this.name,
328
- highlighter: this.options.highlighter,
329
- defaultLanguage: this.options.defaultLanguage,
330
- defaultTheme: this.options.defaultTheme,
331
- }),
332
- ];
334
+ ignoreMutation: (mutation) => {
335
+ // 忽略工具栏区域的 DOM 变化
336
+ return toolbarDOM?.contains(mutation.target) || false;
333
337
  },
334
- });
335
-
336
- // 导出命名导出
337
- export { TiptapShiki };
338
-
339
- // 导出默认导出
340
- export default TiptapShiki;
338
+ };
339
+ };
340
+ },
341
+
342
+ /**
343
+ * 添加 ProseMirror 插件
344
+ *
345
+ * 注册 Shiki 轻量级语法高亮插件,实现实时语法高亮功能。
346
+ * 继承父类的插件并添加自定义的高亮插件。
347
+ *
348
+ * @returns ProseMirror 插件数组
349
+ */
350
+ addProseMirrorPlugins() {
351
+ // 检查是否有高亮器实例
352
+ if (!this.options.highlighter) throw new Error("highlighter is required");
353
+
354
+ return [
355
+ // 继承父类的插件
356
+ ...(this.parent?.() || []),
357
+ // 添加 Shiki 轻量级高亮插件
358
+ ShikiLightPlugin({
359
+ name: this.name,
360
+ highlighter: this.options.highlighter,
361
+ defaultLanguage: this.options.defaultLanguage,
362
+ defaultTheme: this.options.defaultTheme,
363
+ }),
364
+ ];
365
+ },
366
+ });
367
+
368
+ // 导出命名导出
369
+ export { TiptapShiki };
370
+
371
+ // 导出默认导出
372
+ export default TiptapShiki;