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 +2 -0
- package/README_CN.md +1 -0
- package/dist/ShikiLightPlugin.d.ts +2 -1
- package/dist/index.cjs.css +1 -1
- package/dist/index.cjs.js +82 -11
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +82 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/rollup.config.js +4 -1
- package/src/ShikiLightPlugin.ts +57 -7
- package/src/index.css +27 -15
- package/src/index.ts +318 -276
package/README.md
CHANGED
package/README_CN.md
CHANGED
|
@@ -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>;
|
package/dist/index.cjs.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.tiptap-shiki--container{border-radius:.4rem;font-size:.875em
|
|
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
|
-
|
|
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", "
|
|
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
|
},
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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", "
|
|
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.
|
|
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
|
|
package/src/ShikiLightPlugin.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* 继承父类的插件并添加自定义的高亮插件。
|
|
315
|
-
*
|
|
316
|
-
* @returns ProseMirror 插件数组
|
|
339
|
+
* 忽略某些 DOM 变化
|
|
340
|
+
* @param mutation - DOM 变化对象
|
|
341
|
+
* @returns 是否忽略该变化
|
|
317
342
|
*/
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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;
|