tiptap-extension-shiki 1.0.1 → 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.
- package/README.md +2 -0
- package/README_CN.md +1 -0
- package/dist/index.cjs.js +25 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ShikiLightPlugin.ts +0 -3
- package/src/index.ts +308 -276
package/README.md
CHANGED
package/README_CN.md
CHANGED
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
|
-
|
|
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]];
|
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.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
|
-
|
|
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
package/src/ShikiLightPlugin.ts
CHANGED
|
@@ -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
|
-
|
|
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]];
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* 继承父类的插件并添加自定义的高亮插件。
|
|
315
|
-
*
|
|
316
|
-
* @returns ProseMirror 插件数组
|
|
330
|
+
* 忽略某些 DOM 变化
|
|
331
|
+
* @param mutation - DOM 变化对象
|
|
332
|
+
* @returns 是否忽略该变化
|
|
317
333
|
*/
|
|
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
|
-
];
|
|
334
|
+
ignoreMutation: (mutation) => {
|
|
335
|
+
// 忽略工具栏区域的 DOM 变化
|
|
336
|
+
return toolbarDOM?.contains(mutation.target) || false;
|
|
333
337
|
},
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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;
|