tiptap-extension-shiki 1.0.1 → 1.0.3

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.
package/README.md CHANGED
@@ -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: "",
@@ -11,9 +11,10 @@ import type { BundledLanguage, BundledTheme, HighlighterGeneric, SpecialLanguage
11
11
  * @param defaultLanguage - 默认编程语言
12
12
  * @returns ProseMirror插件实例
13
13
  */
14
- export declare function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, }: {
14
+ export declare function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, showLineNumbers, }: {
15
15
  name: string;
16
16
  highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>;
17
17
  defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>;
18
18
  defaultLanguage: StringLiteralUnion<SpecialLanguage>;
19
+ showLineNumbers?: boolean;
19
20
  }): Plugin<any>;
@@ -1 +1 @@
1
- .tiptap-shiki--container{border-radius:.4rem;font-size:.875em;pre{max-height:400px;overflow:auto;padding:20px}}.tiptap-shiki--toolbar{align-items:center;display:flex;padding:10px}
1
+ .tiptap-shiki--container{border-radius:.4rem;font-size:.875em}.tiptap-shiki--container pre{max-height:400px;overflow:auto;padding:1.25rem}.tiptap-shiki--container.show-line-numbers pre{padding-left:0}.tiptap-shiki--line-number{box-sizing:border-box;display:inline-block;padding-right:2em;text-align:right;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5em}.tiptap-shiki--toolbar{align-items:center;display:flex;padding:.75rem}
package/dist/index.cjs.js CHANGED
@@ -18,15 +18,13 @@ var view = require('@tiptap/pm/view');
18
18
  * @param defaultLanguage - 默认编程语言
19
19
  * @returns 装饰器集合,用于渲染语法高亮
20
20
  */
21
- function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, }) {
22
- console.log("doc", doc);
21
+ function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, showLineNumbers, }) {
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
  // 为每个代码块生成装饰
29
- const nodeDecorations = getSingleNodeDecorations(block.node, block.pos, highlighter, defaultTheme, defaultLanguage);
27
+ const nodeDecorations = getSingleNodeDecorations(block.node, block.pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers);
30
28
  return acc.concat(nodeDecorations);
31
29
  }, []);
32
30
  // 创建装饰器集合
@@ -43,7 +41,7 @@ function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage,
43
41
  * @param defaultLanguage - 默认语言
44
42
  * @returns 该节点的所有装饰器数组
45
43
  */
46
- function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage) {
44
+ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers) {
47
45
  const decorations = [];
48
46
  // 获取代码块的语言和主题属性,如果没有则使用默认值
49
47
  const language = node.attrs.language || defaultLanguage;
@@ -56,7 +54,45 @@ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultL
56
54
  theme: theme,
57
55
  });
58
56
  // 遍历每一行的token,为有颜色的token创建装饰
59
- lines.forEach((line) => {
57
+ lines.forEach((line, index) => {
58
+ if (showLineNumbers) {
59
+ // === 新增:在每一行开头添加行号 Widget ===
60
+ decorations.push(view.Decoration.widget(startPos, () => {
61
+ // const lineNumber = index + 1;
62
+ const lineNumberElement = document.createElement("span");
63
+ lineNumberElement.className = "tiptap-shiki--line-number";
64
+ lineNumberElement.textContent = (index + 1).toString();
65
+ return lineNumberElement;
66
+ }, {
67
+ // 设置为负数(如 -1)表示该 Widget 倾向于“依附”在左侧。
68
+ // 当位置处于 0 长度的行首时,光标会落在 Widget 的右侧(即可以输入的位置)。
69
+ side: -1,
70
+ // 这能防止光标进入 Widget 内部,并阻止某些事件冒泡
71
+ stopEvent: () => true,
72
+ // 忽略选区
73
+ // 告诉 ProseMirror 在处理点击和选区时跳过这个元素
74
+ ignoreSelection: true,
75
+ }));
76
+ }
77
+ // const lineNumber = index + 1;
78
+ // decorations.push(
79
+ // Decoration.widget(
80
+ // startPos,
81
+ // () => {
82
+ // const dom = document.createElement("span");
83
+ // dom.className = "shiki-line-number";
84
+ // dom.innerText = `${lineNumber}`;
85
+ // dom.style.userSelect = "none";
86
+ // // 关键:设置不可选中,防止干扰复制粘贴
87
+ // dom.setAttribute("unselectable", "on");
88
+ // return dom;
89
+ // },
90
+ // {
91
+ // side: -1, // 确保它在当前位置的最左侧
92
+ // ignoreSelection: true,
93
+ // }
94
+ // )
95
+ // );
60
96
  line.forEach((token) => {
61
97
  const endPos = startPos + token.content.length;
62
98
  // 如果token有颜色信息,创建内联装饰器设置文本颜色
@@ -84,7 +120,7 @@ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultL
84
120
  * @param defaultLanguage - 默认编程语言
85
121
  * @returns ProseMirror插件实例
86
122
  */
87
- function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, }) {
123
+ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, showLineNumbers, }) {
88
124
  const shikiLightPlugin = new state.Plugin({
89
125
  key: new state.PluginKey("shiki"),
90
126
  // 插件状态管理
@@ -100,6 +136,7 @@ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, })
100
136
  highlighter,
101
137
  defaultTheme,
102
138
  defaultLanguage,
139
+ showLineNumbers,
103
140
  });
104
141
  },
105
142
  /**
@@ -132,7 +169,7 @@ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, })
132
169
  newDecorationSet = newDecorationSet.remove(oldDecos);
133
170
  if (node.type.name === name) {
134
171
  // 重新计算该节点的语法高亮装饰
135
- const newSpecs = getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage);
172
+ const newSpecs = getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers);
136
173
  newDecorationSet = newDecorationSet.add(newState.doc, newSpecs);
137
174
  }
138
175
  });
@@ -259,6 +296,8 @@ const TiptapShiki = CodeBlock.extend({
259
296
  * @returns HTML 数组或元素
260
297
  */
261
298
  renderHTML({ node, HTMLAttributes }) {
299
+ var _a;
300
+ const extraRenderHTMLAttributes = (_a = this.options) === null || _a === void 0 ? void 0 : _a.extraRenderHTMLAttributes;
262
301
  // 如果启用了高亮 HTML 生成并且有高亮器实例
263
302
  if (this.options.getHighlighHTML && this.options.highlighter) {
264
303
  // 获取代码内容、编程语言和主题
@@ -273,8 +312,32 @@ const TiptapShiki = CodeBlock.extend({
273
312
  // 解析生成的 HTML 字符串并返回第一个元素
274
313
  const container = document.createElement("div");
275
314
  container.innerHTML = highlightedCode;
276
- if (container.firstElementChild)
277
- return container.firstElementChild;
315
+ if (container.firstElementChild) {
316
+ const rsDOM = container.firstElementChild;
317
+ Object.keys(HTMLAttributes).forEach((key) => {
318
+ rsDOM.setAttribute(key, HTMLAttributes[key]);
319
+ });
320
+ if (extraRenderHTMLAttributes) {
321
+ // 处理 classList
322
+ if (extraRenderHTMLAttributes.classList)
323
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
324
+ // 处理 styles
325
+ if (extraRenderHTMLAttributes.styles) {
326
+ for (const key in extraRenderHTMLAttributes.styles) {
327
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
328
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
329
+ }
330
+ }
331
+ // 处理 attrs
332
+ if (extraRenderHTMLAttributes.attrs) {
333
+ for (const key in extraRenderHTMLAttributes.attrs) {
334
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
335
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
336
+ }
337
+ }
338
+ }
339
+ return rsDOM;
340
+ }
278
341
  }
279
342
  // 返回标准的 pre/code 标签结构
280
343
  return ["pre", HTMLAttributes, ["code", 0]];
@@ -292,13 +355,20 @@ const TiptapShiki = CodeBlock.extend({
292
355
  if (!this.options.highlighter)
293
356
  throw new Error("highlighter is required");
294
357
  return (props) => {
358
+ var _a;
295
359
  // 获取视图相关参数
296
360
  const { view, getPos, node } = props;
297
361
  // 获取当前主题配置
298
362
  const theme = this.options.highlighter.getTheme(node.attrs.theme || this.options.defaultTheme);
299
363
  // 创建主容器 DOM 元素
300
364
  const dom = document.createElement("div");
301
- dom.classList.add("tiptap-shiki--container", "not-prose", "shiki", "dracula");
365
+ dom.classList.add("tiptap-shiki--container", "shiki");
366
+ if (this.options.showLineNumbers) {
367
+ dom.classList.add("show-line-numbers");
368
+ }
369
+ if ((_a = this.options.extraRenderHTMLAttributes) === null || _a === void 0 ? void 0 : _a.classList) {
370
+ dom.classList.add(...this.options.extraRenderHTMLAttributes.classList);
371
+ }
302
372
  // 应用主题颜色
303
373
  dom.style.backgroundColor = theme.bg;
304
374
  dom.style.color = theme.fg;
@@ -402,6 +472,7 @@ const TiptapShiki = CodeBlock.extend({
402
472
  highlighter: this.options.highlighter,
403
473
  defaultLanguage: this.options.defaultLanguage,
404
474
  defaultTheme: this.options.defaultTheme,
475
+ showLineNumbers: this.options.showLineNumbers,
405
476
  }),
406
477
  ];
407
478
  },
@@ -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.css CHANGED
@@ -1 +1 @@
1
- .tiptap-shiki--container{border-radius:.4rem;font-size:.875em;pre{max-height:400px;overflow:auto;padding:20px}}.tiptap-shiki--toolbar{align-items:center;display:flex;padding:10px}
1
+ .tiptap-shiki--container{border-radius:.4rem;font-size:.875em}.tiptap-shiki--container pre{max-height:400px;overflow:auto;padding:1.25rem}.tiptap-shiki--container.show-line-numbers pre{padding-left:0}.tiptap-shiki--line-number{box-sizing:border-box;display:inline-block;padding-right:2em;text-align:right;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5em}.tiptap-shiki--toolbar{align-items:center;display:flex;padding:.75rem}
package/dist/index.d.ts CHANGED
@@ -25,6 +25,12 @@ 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
+ };
33
+ showLineNumbers?: boolean;
28
34
  renderToolbar?: (props: {
29
35
  toolbarDOM: HTMLElement;
30
36
  language: StringLiteralUnion<SpecialLanguage>;
package/dist/index.js CHANGED
@@ -14,15 +14,13 @@ import { DecorationSet, Decoration } from '@tiptap/pm/view';
14
14
  * @param defaultLanguage - 默认编程语言
15
15
  * @returns 装饰器集合,用于渲染语法高亮
16
16
  */
17
- function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, }) {
18
- console.log("doc", doc);
17
+ function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, showLineNumbers, }) {
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
  // 为每个代码块生成装饰
25
- const nodeDecorations = getSingleNodeDecorations(block.node, block.pos, highlighter, defaultTheme, defaultLanguage);
23
+ const nodeDecorations = getSingleNodeDecorations(block.node, block.pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers);
26
24
  return acc.concat(nodeDecorations);
27
25
  }, []);
28
26
  // 创建装饰器集合
@@ -39,7 +37,7 @@ function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage,
39
37
  * @param defaultLanguage - 默认语言
40
38
  * @returns 该节点的所有装饰器数组
41
39
  */
42
- function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage) {
40
+ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers) {
43
41
  const decorations = [];
44
42
  // 获取代码块的语言和主题属性,如果没有则使用默认值
45
43
  const language = node.attrs.language || defaultLanguage;
@@ -52,7 +50,45 @@ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultL
52
50
  theme: theme,
53
51
  });
54
52
  // 遍历每一行的token,为有颜色的token创建装饰
55
- lines.forEach((line) => {
53
+ lines.forEach((line, index) => {
54
+ if (showLineNumbers) {
55
+ // === 新增:在每一行开头添加行号 Widget ===
56
+ decorations.push(Decoration.widget(startPos, () => {
57
+ // const lineNumber = index + 1;
58
+ const lineNumberElement = document.createElement("span");
59
+ lineNumberElement.className = "tiptap-shiki--line-number";
60
+ lineNumberElement.textContent = (index + 1).toString();
61
+ return lineNumberElement;
62
+ }, {
63
+ // 设置为负数(如 -1)表示该 Widget 倾向于“依附”在左侧。
64
+ // 当位置处于 0 长度的行首时,光标会落在 Widget 的右侧(即可以输入的位置)。
65
+ side: -1,
66
+ // 这能防止光标进入 Widget 内部,并阻止某些事件冒泡
67
+ stopEvent: () => true,
68
+ // 忽略选区
69
+ // 告诉 ProseMirror 在处理点击和选区时跳过这个元素
70
+ ignoreSelection: true,
71
+ }));
72
+ }
73
+ // const lineNumber = index + 1;
74
+ // decorations.push(
75
+ // Decoration.widget(
76
+ // startPos,
77
+ // () => {
78
+ // const dom = document.createElement("span");
79
+ // dom.className = "shiki-line-number";
80
+ // dom.innerText = `${lineNumber}`;
81
+ // dom.style.userSelect = "none";
82
+ // // 关键:设置不可选中,防止干扰复制粘贴
83
+ // dom.setAttribute("unselectable", "on");
84
+ // return dom;
85
+ // },
86
+ // {
87
+ // side: -1, // 确保它在当前位置的最左侧
88
+ // ignoreSelection: true,
89
+ // }
90
+ // )
91
+ // );
56
92
  line.forEach((token) => {
57
93
  const endPos = startPos + token.content.length;
58
94
  // 如果token有颜色信息,创建内联装饰器设置文本颜色
@@ -80,7 +116,7 @@ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultL
80
116
  * @param defaultLanguage - 默认编程语言
81
117
  * @returns ProseMirror插件实例
82
118
  */
83
- function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, }) {
119
+ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, showLineNumbers, }) {
84
120
  const shikiLightPlugin = new Plugin({
85
121
  key: new PluginKey("shiki"),
86
122
  // 插件状态管理
@@ -96,6 +132,7 @@ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, })
96
132
  highlighter,
97
133
  defaultTheme,
98
134
  defaultLanguage,
135
+ showLineNumbers,
99
136
  });
100
137
  },
101
138
  /**
@@ -128,7 +165,7 @@ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, })
128
165
  newDecorationSet = newDecorationSet.remove(oldDecos);
129
166
  if (node.type.name === name) {
130
167
  // 重新计算该节点的语法高亮装饰
131
- const newSpecs = getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage);
168
+ const newSpecs = getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage, showLineNumbers);
132
169
  newDecorationSet = newDecorationSet.add(newState.doc, newSpecs);
133
170
  }
134
171
  });
@@ -255,6 +292,8 @@ const TiptapShiki = CodeBlock.extend({
255
292
  * @returns HTML 数组或元素
256
293
  */
257
294
  renderHTML({ node, HTMLAttributes }) {
295
+ var _a;
296
+ const extraRenderHTMLAttributes = (_a = this.options) === null || _a === void 0 ? void 0 : _a.extraRenderHTMLAttributes;
258
297
  // 如果启用了高亮 HTML 生成并且有高亮器实例
259
298
  if (this.options.getHighlighHTML && this.options.highlighter) {
260
299
  // 获取代码内容、编程语言和主题
@@ -269,8 +308,32 @@ const TiptapShiki = CodeBlock.extend({
269
308
  // 解析生成的 HTML 字符串并返回第一个元素
270
309
  const container = document.createElement("div");
271
310
  container.innerHTML = highlightedCode;
272
- if (container.firstElementChild)
273
- return container.firstElementChild;
311
+ if (container.firstElementChild) {
312
+ const rsDOM = container.firstElementChild;
313
+ Object.keys(HTMLAttributes).forEach((key) => {
314
+ rsDOM.setAttribute(key, HTMLAttributes[key]);
315
+ });
316
+ if (extraRenderHTMLAttributes) {
317
+ // 处理 classList
318
+ if (extraRenderHTMLAttributes.classList)
319
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
320
+ // 处理 styles
321
+ if (extraRenderHTMLAttributes.styles) {
322
+ for (const key in extraRenderHTMLAttributes.styles) {
323
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
324
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
325
+ }
326
+ }
327
+ // 处理 attrs
328
+ if (extraRenderHTMLAttributes.attrs) {
329
+ for (const key in extraRenderHTMLAttributes.attrs) {
330
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
331
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
332
+ }
333
+ }
334
+ }
335
+ return rsDOM;
336
+ }
274
337
  }
275
338
  // 返回标准的 pre/code 标签结构
276
339
  return ["pre", HTMLAttributes, ["code", 0]];
@@ -288,13 +351,20 @@ const TiptapShiki = CodeBlock.extend({
288
351
  if (!this.options.highlighter)
289
352
  throw new Error("highlighter is required");
290
353
  return (props) => {
354
+ var _a;
291
355
  // 获取视图相关参数
292
356
  const { view, getPos, node } = props;
293
357
  // 获取当前主题配置
294
358
  const theme = this.options.highlighter.getTheme(node.attrs.theme || this.options.defaultTheme);
295
359
  // 创建主容器 DOM 元素
296
360
  const dom = document.createElement("div");
297
- dom.classList.add("tiptap-shiki--container", "not-prose", "shiki", "dracula");
361
+ dom.classList.add("tiptap-shiki--container", "shiki");
362
+ if (this.options.showLineNumbers) {
363
+ dom.classList.add("show-line-numbers");
364
+ }
365
+ if ((_a = this.options.extraRenderHTMLAttributes) === null || _a === void 0 ? void 0 : _a.classList) {
366
+ dom.classList.add(...this.options.extraRenderHTMLAttributes.classList);
367
+ }
298
368
  // 应用主题颜色
299
369
  dom.style.backgroundColor = theme.bg;
300
370
  dom.style.color = theme.fg;
@@ -398,6 +468,7 @@ const TiptapShiki = CodeBlock.extend({
398
468
  highlighter: this.options.highlighter,
399
469
  defaultLanguage: this.options.defaultLanguage,
400
470
  defaultTheme: this.options.defaultTheme,
471
+ showLineNumbers: this.options.showLineNumbers,
401
472
  }),
402
473
  ];
403
474
  },
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,6 +1,6 @@
1
1
  {
2
2
  "name": "tiptap-extension-shiki",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@tiptap/core": "^2.3.0 || ^3.0.0",
42
+ "@tiptap/extension-code-block": "^2.3.0 || ^3.14.0",
42
43
  "@tiptap/pm": "^2.3.0 || ^3.0.0",
43
44
  "shiki": "^3.0.0"
44
45
  }
package/rollup.config.js CHANGED
@@ -8,6 +8,7 @@ const typescript = require("rollup-plugin-typescript2");
8
8
  const postcss = require("rollup-plugin-postcss");
9
9
  const cssnano = require("cssnano");
10
10
  const autoprefixer = require("autoprefixer");
11
+ const pkg = require("./package.json");
11
12
 
12
13
  const config = {
13
14
  input: "src/index.ts",
@@ -26,7 +27,7 @@ const config = {
26
27
  },
27
28
  ],
28
29
  plugins: [
29
- autoExternal({
30
+ autoExternal({
30
31
  packagePath: "./package.json",
31
32
  }),
32
33
  postcss({
@@ -65,6 +66,8 @@ const config = {
65
66
  "@tiptap/extension-code-block",
66
67
  // Shiki依赖
67
68
  "shiki",
69
+ ...Object.keys(pkg.devDependencies || {}),
70
+ ...Object.keys(pkg.dependencies || {}),
68
71
  ],
69
72
  };
70
73
 
@@ -28,18 +28,17 @@ function getDecorations({
28
28
  highlighter,
29
29
  defaultTheme,
30
30
  defaultLanguage,
31
+ showLineNumbers,
31
32
  }: {
32
33
  doc: ProsemirrorNode;
33
34
  name: string;
34
35
  highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>;
35
36
  defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>;
36
37
  defaultLanguage: StringLiteralUnion<SpecialLanguage>;
38
+ showLineNumbers?: boolean;
37
39
  }) {
38
- console.log("doc", doc);
39
-
40
40
  // 查找文档中所有指定类型的节点(shiki代码块)
41
41
  const decorations = findChildren(doc, (node) => {
42
- console.log("node node ", node);
43
42
  return node.type.name === name;
44
43
  }).reduce((acc, block) => {
45
44
  // 为每个代码块生成装饰
@@ -48,7 +47,8 @@ function getDecorations({
48
47
  block.pos,
49
48
  highlighter,
50
49
  defaultTheme,
51
- defaultLanguage
50
+ defaultLanguage,
51
+ showLineNumbers
52
52
  );
53
53
  return acc.concat(nodeDecorations);
54
54
  }, [] as Decoration[]);
@@ -73,7 +73,8 @@ function getSingleNodeDecorations(
73
73
  pos: number,
74
74
  highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>,
75
75
  defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>,
76
- defaultLanguage: StringLiteralUnion<SpecialLanguage>
76
+ defaultLanguage: StringLiteralUnion<SpecialLanguage>,
77
+ showLineNumbers?: boolean
77
78
  ) {
78
79
  const decorations: Decoration[] = [];
79
80
 
@@ -91,7 +92,52 @@ function getSingleNodeDecorations(
91
92
  });
92
93
 
93
94
  // 遍历每一行的token,为有颜色的token创建装饰
94
- lines.forEach((line) => {
95
+ lines.forEach((line, index) => {
96
+ if (showLineNumbers) {
97
+ // === 新增:在每一行开头添加行号 Widget ===
98
+ decorations.push(
99
+ Decoration.widget(
100
+ startPos,
101
+ () => {
102
+ // const lineNumber = index + 1;
103
+ const lineNumberElement = document.createElement("span");
104
+ lineNumberElement.className = "tiptap-shiki--line-number";
105
+ lineNumberElement.textContent = (index + 1).toString();
106
+ return lineNumberElement;
107
+ },
108
+ {
109
+ // 设置为负数(如 -1)表示该 Widget 倾向于“依附”在左侧。
110
+ // 当位置处于 0 长度的行首时,光标会落在 Widget 的右侧(即可以输入的位置)。
111
+ side: -1,
112
+ // 这能防止光标进入 Widget 内部,并阻止某些事件冒泡
113
+ stopEvent: () => true,
114
+ // 忽略选区
115
+ // 告诉 ProseMirror 在处理点击和选区时跳过这个元素
116
+ ignoreSelection: true,
117
+ }
118
+ )
119
+ );
120
+ }
121
+
122
+ // const lineNumber = index + 1;
123
+ // decorations.push(
124
+ // Decoration.widget(
125
+ // startPos,
126
+ // () => {
127
+ // const dom = document.createElement("span");
128
+ // dom.className = "shiki-line-number";
129
+ // dom.innerText = `${lineNumber}`;
130
+ // dom.style.userSelect = "none";
131
+ // // 关键:设置不可选中,防止干扰复制粘贴
132
+ // dom.setAttribute("unselectable", "on");
133
+ // return dom;
134
+ // },
135
+ // {
136
+ // side: -1, // 确保它在当前位置的最左侧
137
+ // ignoreSelection: true,
138
+ // }
139
+ // )
140
+ // );
95
141
  line.forEach((token) => {
96
142
  const endPos = startPos + token.content.length;
97
143
 
@@ -131,11 +177,13 @@ export function ShikiLightPlugin({
131
177
  highlighter,
132
178
  defaultTheme,
133
179
  defaultLanguage,
180
+ showLineNumbers,
134
181
  }: {
135
182
  name: string;
136
183
  highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>;
137
184
  defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>;
138
185
  defaultLanguage: StringLiteralUnion<SpecialLanguage>;
186
+ showLineNumbers?: boolean;
139
187
  }) {
140
188
  const shikiLightPlugin: Plugin = new Plugin({
141
189
  key: new PluginKey("shiki"),
@@ -153,6 +201,7 @@ export function ShikiLightPlugin({
153
201
  highlighter,
154
202
  defaultTheme,
155
203
  defaultLanguage,
204
+ showLineNumbers,
156
205
  });
157
206
  },
158
207
 
@@ -197,7 +246,8 @@ export function ShikiLightPlugin({
197
246
  pos,
198
247
  highlighter,
199
248
  defaultTheme,
200
- defaultLanguage
249
+ defaultLanguage,
250
+ showLineNumbers
201
251
  );
202
252
  newDecorationSet = newDecorationSet.add(newState.doc, newSpecs);
203
253
  }
package/src/index.css CHANGED
@@ -11,31 +11,43 @@
11
11
  .tiptap-shiki--container {
12
12
  /* 设置圆角边框 */
13
13
  border-radius: 0.4rem;
14
-
14
+
15
15
  /* 设置相对较小的字体大小 */
16
16
  font-size: 0.875em;
17
+ }
18
+
19
+ .tiptap-shiki--container pre {
20
+ /* 设置内边距,提供舒适的代码阅读空间 */
21
+ padding: 1.25rem;
22
+
23
+ /* 设置最大高度,超出时显示滚动条 */
24
+ max-height: 400px;
25
+
26
+ /* 设置溢出内容为自动滚动 */
27
+ overflow: auto;
28
+ }
29
+
30
+ .tiptap-shiki--container.show-line-numbers pre {
31
+ padding-left: 0;
32
+ }
17
33
 
18
- /* 嵌套样式:代码内容区域 */
19
- pre {
20
- /* 设置内边距,提供舒适的代码阅读空间 */
21
- padding: 20px;
22
-
23
- /* 设置最大高度,超出时显示滚动条 */
24
- max-height: 400px;
25
-
26
- /* 设置溢出内容为自动滚动 */
27
- overflow: auto;
28
- }
34
+ .tiptap-shiki--line-number {
35
+ display: inline-block;
36
+ box-sizing: border-box;
37
+ width: 5em;
38
+ padding-right: 2em;
39
+ text-align: right;
40
+ user-select: none;
29
41
  }
30
42
 
31
43
  /* 工具栏样式 */
32
44
  .tiptap-shiki--toolbar {
33
45
  /* 使用弹性布局排列工具栏元素 */
34
46
  display: flex;
35
-
47
+
36
48
  /* 垂直居中对齐工具栏内容 */
37
49
  align-items: center;
38
-
50
+
39
51
  /* 设置工具栏内边距 */
40
- padding: 10px;
52
+ padding: 0.75rem;
41
53
  }
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
  * - 支持多种编程语言的语法高亮
@@ -30,10 +30,10 @@ import { ShikiLightPlugin } from "./ShikiLightPlugin.js";
30
30
 
31
31
  /**
32
32
  * TiptapShiki 扩展类定义
33
- *
33
+ *
34
34
  * 该类扩展了 Tiptap 的 CodeBlock,添加了语法高亮功能和相关配置选项。
35
35
  * 使用泛型类型定义扩展选项和节点视图属性。
36
- *
36
+ *
37
37
  * @template T - 扩展选项类型,包含默认主题、语言、高亮器等配置
38
38
  * @template U - 节点视图属性类型,定义 DOM 元素的类型
39
39
  */
@@ -47,6 +47,14 @@ const TiptapShiki = CodeBlock.extend<
47
47
  highlighter?: HighlighterGeneric<BundledLanguage, BundledTheme>;
48
48
  // 是否获取高亮的 HTML 字符串
49
49
  getHighlighHTML?: boolean;
50
+ // 额外渲染HTML属性
51
+ extraRenderHTMLAttributes?: {
52
+ classList?: string[];
53
+ styles?: Record<string, string>;
54
+ attrs?: Record<string, string>;
55
+ };
56
+ // 显示行号
57
+ showLineNumbers?: boolean;
50
58
  // 自定义工具栏渲染函数
51
59
  renderToolbar?: (props: {
52
60
  toolbarDOM: HTMLElement;
@@ -56,285 +64,319 @@ const TiptapShiki = CodeBlock.extend<
56
64
  setTheme: (theme: string) => void;
57
65
  }) => void;
58
66
  },
59
- {
67
+ {
60
68
  // 节点视图中的 DOM 元素类型定义
61
- container: HTMLElement;
62
- shikiCode: HTMLElement;
63
- contentDOM: HTMLElement
69
+ container: HTMLElement;
70
+ shikiCode: HTMLElement;
71
+ contentDOM: HTMLElement;
64
72
  }
65
73
  >({
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]];
74
+ // 扩展名称,用于标识这个自定义扩展
75
+ name: "tiptapShiki",
76
+
77
+ /**
78
+ * 添加扩展配置选项
79
+ *
80
+ * 定义了 Shiki 语法高亮的默认配置,包括默认主题、编程语言等。
81
+ * 使用父类的配置选项并添加本扩展特有的选项。
82
+ *
83
+ * @returns 配置对象,包含默认主题、语言和高亮器设置
84
+ */
85
+ addOptions() {
86
+ return {
87
+ // 继承父类的配置选项
88
+ ...this.parent?.(),
89
+ // 默认语法高亮主题
90
+ defaultTheme: "dracula",
91
+ // 默认编程语言
92
+ defaultLanguage: "javascript",
93
+ // 初始化时的高亮器实例(可选)
94
+ highlighter: undefined,
95
+ };
96
+ },
97
+ /**
98
+ * 添加节点属性
99
+ *
100
+ * 定义了代码块节点的自定义属性,包括编程语言和主题。
101
+ * 这些属性用于存储和渲染代码块的语法高亮配置。
102
+ *
103
+ * @returns 属性配置对象,包含语言和主题属性
104
+ */
105
+ addAttributes() {
106
+ return {
107
+ // 继承父类的属性
108
+ ...this.parent?.(),
109
+
110
+ // 编程语言属性
111
+ language: {
112
+ // 默认语言配置
113
+ default: this.options.defaultLanguage || "javascript",
114
+
115
+ /**
116
+ * 从 HTML 元素解析语言属性
117
+ * @param element - HTML 元素
118
+ * @returns 编程语言字符串
119
+ */
120
+ parseHTML: (element) => {
121
+ // 从 data-language 属性获取语言信息
122
+ return element.getAttribute("data-language");
123
+ },
124
+
125
+ /**
126
+ * 渲染 HTML 时设置语言属性
127
+ * @param attributes - 节点属性对象
128
+ * @returns HTML 属性对象
129
+ */
130
+ renderHTML: (attributes) => {
131
+ // 将语言信息存储在 data-language 属性中
132
+ return { "data-language": attributes.language };
133
+ },
185
134
  },
186
135
 
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
- },
136
+ // 主题属性
137
+ theme: {
138
+ // 默认主题配置
139
+ default: this.options.defaultTheme || "dracula",
140
+
141
+ /**
142
+ * HTML 元素解析主题属性
143
+ * @param element - HTML 元素
144
+ * @returns 主题名称字符串
145
+ */
146
+ parseHTML: (element) => element.getAttribute("data-theme"),
147
+
148
+ /**
149
+ * 渲染 HTML 时设置主题属性
150
+ * @param attributes - 节点属性对象
151
+ * @returns HTML 属性对象
152
+ */
153
+ renderHTML: (attributes) => ({
154
+ "data-theme": attributes.theme,
155
+ }),
156
+ },
157
+ };
158
+ },
159
+
160
+ /**
161
+ * 渲染 HTML
162
+ *
163
+ * 定义了代码块节点在静态 HTML 中的渲染逻辑。
164
+ * 如果配置了 getHighlighHTML 和 highlighter,将使用 Shiki 生成高亮的 HTML;
165
+ * 否则返回标准的 pre/code 标签结构。
166
+ *
167
+ * @param node - ProseMirror 节点对象
168
+ * @param HTMLAttributes - HTML 属性对象
169
+ * @returns HTML 数组或元素
170
+ */
171
+ renderHTML({ node, HTMLAttributes }) {
172
+ const extraRenderHTMLAttributes = this.options?.extraRenderHTMLAttributes;
173
+ // 如果启用了高亮 HTML 生成并且有高亮器实例
174
+ if (this.options.getHighlighHTML && this.options.highlighter) {
175
+ // 获取代码内容、编程语言和主题
176
+ const language = node.attrs.language || this.options.defaultLanguage;
177
+ const theme = node.attrs.theme || this.options.defaultTheme;
178
+ const content = node.textContent;
179
+
180
+ // 使用 Shiki 将代码转换为带样式的 HTML
181
+ const highlightedCode = this.options.highlighter.codeToHtml(content, {
182
+ lang: language,
183
+ theme: theme,
184
+ });
185
+
186
+ // 解析生成的 HTML 字符串并返回第一个元素
187
+ const container = document.createElement("div");
188
+ container.innerHTML = highlightedCode;
189
+ if (container.firstElementChild) {
190
+ const rsDOM = container.firstElementChild as HTMLElement;
191
+ Object.keys(HTMLAttributes).forEach((key) => {
192
+ rsDOM.setAttribute(key, HTMLAttributes[key]);
193
+ });
194
+
195
+ if (extraRenderHTMLAttributes) {
196
+ // 处理 classList
197
+ if (extraRenderHTMLAttributes.classList)
198
+ rsDOM.classList.add(...extraRenderHTMLAttributes.classList);
199
+
200
+ // 处理 styles
201
+ if (extraRenderHTMLAttributes.styles) {
202
+ for (const key in extraRenderHTMLAttributes.styles) {
203
+ if (extraRenderHTMLAttributes.styles.hasOwnProperty(key))
204
+ rsDOM.style[key] = extraRenderHTMLAttributes.styles[key];
205
+ }
206
+ }
207
+
208
+ // 处理 attrs
209
+ if (extraRenderHTMLAttributes.attrs) {
210
+ for (const key in extraRenderHTMLAttributes.attrs) {
211
+ if (extraRenderHTMLAttributes.attrs.hasOwnProperty(key))
212
+ rsDOM.setAttribute(key, extraRenderHTMLAttributes.attrs[key]);
213
+ }
214
+ }
215
+ }
216
+ return rsDOM;
217
+ }
218
+ }
219
+
220
+ // 返回标准的 pre/code 标签结构
221
+ return ["pre", HTMLAttributes, ["code", 0]];
222
+ },
223
+
224
+ /**
225
+ * 添加节点视图
226
+ *
227
+ * 创建代码块的交互式 DOM 视图,包括工具栏和代码显示区域。
228
+ * 提供了实时的语法高亮和主题切换功能。
229
+ *
230
+ * @returns 节点视图工厂函数
231
+ */
232
+ addNodeView() {
233
+ // 检查是否有高亮器实例
234
+ if (!this.options.highlighter) throw new Error("highlighter is required");
235
+
236
+ return (props) => {
237
+ // 获取视图相关参数
238
+ const { view, getPos, node } = props;
239
+
240
+ // 获取当前主题配置
241
+ const theme = this.options.highlighter.getTheme(
242
+ node.attrs.theme || this.options.defaultTheme
243
+ );
244
+
245
+ // 创建主容器 DOM 元素
246
+ const dom = document.createElement("div");
247
+ dom.classList.add("tiptap-shiki--container", "shiki");
248
+
249
+ if (this.options.showLineNumbers) {
250
+ dom.classList.add("show-line-numbers");
251
+ }
252
+
253
+ if (this.options.extraRenderHTMLAttributes?.classList) {
254
+ dom.classList.add(...this.options.extraRenderHTMLAttributes.classList);
255
+ }
256
+
257
+ // 应用主题颜色
258
+ dom.style.backgroundColor = theme.bg;
259
+ dom.style.color = theme.fg;
260
+
261
+ // 创建工具栏(如果配置了自定义渲染器)
262
+ let toolbarDOM: HTMLElement | undefined;
263
+ if (
264
+ this.options.renderToolbar &&
265
+ typeof this.options.renderToolbar === "function"
266
+ ) {
267
+ // 创建工具栏容器
268
+ toolbarDOM = document.createElement("div");
269
+ toolbarDOM.classList.add("tiptap-shiki--toolbar");
270
+ toolbarDOM.setAttribute("contenteditable", "false");
271
+
272
+ // 创建节点标记更新函数
273
+ const setNodeMarkup = (attr: Record<string, string>) => {
274
+ if (typeof getPos !== "function") return;
275
+ const pos = getPos();
276
+ if (pos === undefined) return;
277
+ const { tr } = view.state;
278
+ const nowNode = view.state.doc.nodeAt(pos);
279
+
280
+ // 更新节点属性
281
+ tr.setNodeMarkup(pos, this.type, { ...nowNode?.attrs, ...attr });
282
+ view.dispatch(tr);
283
+ };
284
+
285
+ // 创建语言设置函数
286
+ const setLanguage = (language: string) => {
287
+ setNodeMarkup({
288
+ language, // 仅修改当前代码块的编程语言
289
+ });
290
+ };
291
+
292
+ // 创建主题设置函数
293
+ const setTheme = (theme: string) => {
294
+ setNodeMarkup({
295
+ theme, // 仅修改当前代码块的主题
296
+ });
297
+ };
298
+
299
+ // 调用自定义工具栏渲染器
300
+ this.options.renderToolbar({
301
+ language: node.attrs.language,
302
+ theme: node.attrs.theme,
303
+ toolbarDOM,
304
+ setLanguage,
305
+ setTheme,
306
+ });
307
+
308
+ // 将工具栏添加到主容器
309
+ dom.appendChild(toolbarDOM);
310
+ }
311
+
312
+ // 创建代码显示区域
313
+ const preDOM = document.createElement("pre");
314
+ const contentDOM = document.createElement("code");
315
+ contentDOM.classList.add("tiptap-shiki--content");
316
+
317
+ // 组装 DOM 结构
318
+ preDOM.appendChild(contentDOM);
319
+ dom.appendChild(preDOM);
320
+
321
+ // 返回节点视图对象
322
+ return {
323
+ // 主 DOM 元素
324
+ dom: dom,
325
+ // 内容 DOM 元素
326
+ contentDOM,
327
+
328
+ /**
329
+ * 更新节点视图
330
+ * @param updatedNode - 更新后的节点
331
+ * @returns 是否成功更新
332
+ */
333
+ update: (updatedNode) => {
334
+ // 如果不是相同类型的节点,拒绝更新
335
+ return updatedNode.type === node.type;
336
+ },
309
337
 
310
338
  /**
311
- * 添加 ProseMirror 插件
312
- *
313
- * 注册 Shiki 轻量级语法高亮插件,实现实时语法高亮功能。
314
- * 继承父类的插件并添加自定义的高亮插件。
315
- *
316
- * @returns ProseMirror 插件数组
339
+ * 忽略某些 DOM 变化
340
+ * @param mutation - DOM 变化对象
341
+ * @returns 是否忽略该变化
317
342
  */
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
- ];
343
+ ignoreMutation: (mutation) => {
344
+ // 忽略工具栏区域的 DOM 变化
345
+ return toolbarDOM?.contains(mutation.target) || false;
333
346
  },
334
- });
335
-
336
- // 导出命名导出
337
- export { TiptapShiki };
338
-
339
- // 导出默认导出
340
- export default TiptapShiki;
347
+ };
348
+ };
349
+ },
350
+
351
+ /**
352
+ * 添加 ProseMirror 插件
353
+ *
354
+ * 注册 Shiki 轻量级语法高亮插件,实现实时语法高亮功能。
355
+ * 继承父类的插件并添加自定义的高亮插件。
356
+ *
357
+ * @returns ProseMirror 插件数组
358
+ */
359
+ addProseMirrorPlugins() {
360
+ // 检查是否有高亮器实例
361
+ if (!this.options.highlighter) throw new Error("highlighter is required");
362
+
363
+ return [
364
+ // 继承父类的插件
365
+ ...(this.parent?.() || []),
366
+ // 添加 Shiki 轻量级高亮插件
367
+ ShikiLightPlugin({
368
+ name: this.name,
369
+ highlighter: this.options.highlighter,
370
+ defaultLanguage: this.options.defaultLanguage,
371
+ defaultTheme: this.options.defaultTheme,
372
+ showLineNumbers: this.options.showLineNumbers,
373
+ }),
374
+ ];
375
+ },
376
+ });
377
+
378
+ // 导出命名导出
379
+ export { TiptapShiki };
380
+
381
+ // 导出默认导出
382
+ export default TiptapShiki;