tiptap-extension-shiki 1.0.0

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.
@@ -0,0 +1,412 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var CodeBlock = require('@tiptap/extension-code-block');
6
+ var core = require('@tiptap/core');
7
+ var state = require('@tiptap/pm/state');
8
+ var view = require('@tiptap/pm/view');
9
+
10
+ /**
11
+ * 为整个文档计算语法高亮装饰
12
+ * 该函数遍历文档中的所有自定义节点,为每个节点生成相应的装饰样式
13
+ *
14
+ * @param doc - ProseMirror文档节点
15
+ * @param name - 插件名称,用于识别目标节点类型
16
+ * @param highlighter - Shiki语法高亮器实例
17
+ * @param defaultTheme - 默认主题
18
+ * @param defaultLanguage - 默认编程语言
19
+ * @returns 装饰器集合,用于渲染语法高亮
20
+ */
21
+ function getDecorations({ doc, name, highlighter, defaultTheme, defaultLanguage, }) {
22
+ console.log("doc", doc);
23
+ // 查找文档中所有指定类型的节点(shiki代码块)
24
+ const decorations = core.findChildren(doc, (node) => {
25
+ console.log("node node ", node);
26
+ return node.type.name === name;
27
+ }).reduce((acc, block) => {
28
+ // 为每个代码块生成装饰
29
+ const nodeDecorations = getSingleNodeDecorations(block.node, block.pos, highlighter, defaultTheme, defaultLanguage);
30
+ return acc.concat(nodeDecorations);
31
+ }, []);
32
+ // 创建装饰器集合
33
+ return view.DecorationSet.create(doc, decorations);
34
+ }
35
+ /**
36
+ * 为单个代码块节点生成语法高亮装饰
37
+ * 使用Shiki对代码进行分词,然后为每个token生成相应的装饰样式
38
+ *
39
+ * @param node - 代码块节点
40
+ * @param pos - 节点在文档中的位置
41
+ * @param highlighter - Shiki语法高亮器
42
+ * @param defaultTheme - 默认主题
43
+ * @param defaultLanguage - 默认语言
44
+ * @returns 该节点的所有装饰器数组
45
+ */
46
+ function getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage) {
47
+ const decorations = [];
48
+ // 获取代码块的语言和主题属性,如果没有则使用默认值
49
+ const language = node.attrs.language || defaultLanguage;
50
+ const theme = node.attrs.theme || defaultTheme;
51
+ // 计算文本内容的起始位置(跳过节点标记)
52
+ let startPos = pos + 1;
53
+ // 使用Shiki对代码进行分词,返回每个token的颜色信息
54
+ const lines = highlighter.codeToTokensBase(node.textContent, {
55
+ lang: language,
56
+ theme: theme,
57
+ });
58
+ // 遍历每一行的token,为有颜色的token创建装饰
59
+ lines.forEach((line) => {
60
+ line.forEach((token) => {
61
+ const endPos = startPos + token.content.length;
62
+ // 如果token有颜色信息,创建内联装饰器设置文本颜色
63
+ if (token.color) {
64
+ decorations.push(view.Decoration.inline(startPos, endPos, {
65
+ style: `color: ${token.color}`,
66
+ }));
67
+ }
68
+ // 更新下一个token的起始位置
69
+ startPos = endPos;
70
+ });
71
+ // 处理换行符(每个line之间的分隔符)
72
+ startPos += 1;
73
+ });
74
+ return decorations;
75
+ }
76
+ /**
77
+ * 创建Shiki轻量级语法高亮插件
78
+ * 该插件负责在ProseMirror编辑器中为代码块提供实时的语法高亮显示
79
+ * 通过ProseMirror的装饰器系统实现高性能的语法高亮渲染
80
+ *
81
+ * @param name - 插件名称
82
+ * @param highlighter - Shiki语法高亮器实例
83
+ * @param defaultTheme - 默认主题
84
+ * @param defaultLanguage - 默认编程语言
85
+ * @returns ProseMirror插件实例
86
+ */
87
+ function ShikiLightPlugin({ name, highlighter, defaultTheme, defaultLanguage, }) {
88
+ const shikiLightPlugin = new state.Plugin({
89
+ key: new state.PluginKey("shiki"),
90
+ // 插件状态管理
91
+ state: {
92
+ /**
93
+ * 初始化装饰器
94
+ * 在插件首次加载时为整个文档计算初始的语法高亮装饰
95
+ */
96
+ init: (_, { doc }) => {
97
+ return getDecorations({
98
+ doc,
99
+ name,
100
+ highlighter,
101
+ defaultTheme,
102
+ defaultLanguage,
103
+ });
104
+ },
105
+ /**
106
+ * 应用事务变化
107
+ * 当文档发生变化时,智能更新受影响的代码块的语法高亮
108
+ * 实现了性能优化:只重新计算变化范围内的装饰
109
+ */
110
+ apply: (transaction, decorationSet, oldState, newState) => {
111
+ // 1. 如果文档内容没有变化,直接映射现有装饰(最高性能)
112
+ if (!transaction.docChanged) {
113
+ return decorationSet.map(transaction.mapping, transaction.doc);
114
+ }
115
+ // 2. 重新计算受影响的装饰器
116
+ let newDecorationSet = decorationSet.map(transaction.mapping, transaction.doc);
117
+ // 3. 遍历事务中的每个步骤映射,精确更新受影响的节点
118
+ transaction.mapping.maps.forEach((stepMap) => {
119
+ stepMap.forEach((fromA, toA, fromB, toB) => {
120
+ // 限制计算范围在文档边界内,避免越界错误
121
+ const docSize = newState.doc.content.size;
122
+ const start = Math.max(0, Math.min(fromB, docSize));
123
+ const end = Math.max(0, Math.min(toB, docSize));
124
+ // 检查范围内的节点是否为代码块类型
125
+ newState.doc.nodesBetween(start, end, (node, pos) => {
126
+ if (node.type.name === "text")
127
+ return;
128
+ // 先移除这个节点旧的高亮
129
+ const nodeEnd = pos + node.nodeSize;
130
+ // 移除该节点范围内现有的所有装饰
131
+ const oldDecos = newDecorationSet.find(pos, nodeEnd);
132
+ newDecorationSet = newDecorationSet.remove(oldDecos);
133
+ if (node.type.name === name) {
134
+ // 重新计算该节点的语法高亮装饰
135
+ const newSpecs = getSingleNodeDecorations(node, pos, highlighter, defaultTheme, defaultLanguage);
136
+ newDecorationSet = newDecorationSet.add(newState.doc, newSpecs);
137
+ }
138
+ });
139
+ });
140
+ });
141
+ return newDecorationSet;
142
+ },
143
+ },
144
+ // 插件属性:将装饰器提供给编辑器视图
145
+ props: {
146
+ decorations(state) {
147
+ return shikiLightPlugin.getState(state);
148
+ },
149
+ },
150
+ });
151
+ return shikiLightPlugin;
152
+ }
153
+
154
+ /**
155
+ * Tiptap Shiki 语法高亮扩展
156
+ *
157
+ * 本文件实现了一个基于 Shiki 的 Tiptap 扩展,用于在富文本编辑器中提供语法高亮功能。
158
+ * 该扩展扩展了 Tiptap 的 CodeBlock,添加了以下功能:
159
+ * - 支持多种编程语言的语法高亮
160
+ * - 支持多种主题切换
161
+ * - 自定义工具栏界面
162
+ * - 键盘快捷键支持
163
+ * - 实时语法高亮渲染
164
+ */
165
+ // 导入 Tiptap 核心组件
166
+ /**
167
+ * TiptapShiki 扩展类定义
168
+ *
169
+ * 该类扩展了 Tiptap 的 CodeBlock,添加了语法高亮功能和相关配置选项。
170
+ * 使用泛型类型定义扩展选项和节点视图属性。
171
+ *
172
+ * @template T - 扩展选项类型,包含默认主题、语言、高亮器等配置
173
+ * @template U - 节点视图属性类型,定义 DOM 元素的类型
174
+ */
175
+ const TiptapShiki = CodeBlock.extend({
176
+ // 扩展名称,用于标识这个自定义扩展
177
+ name: "tiptapShiki",
178
+ /**
179
+ * 添加扩展配置选项
180
+ *
181
+ * 定义了 Shiki 语法高亮的默认配置,包括默认主题、编程语言等。
182
+ * 使用父类的配置选项并添加本扩展特有的选项。
183
+ *
184
+ * @returns 配置对象,包含默认主题、语言和高亮器设置
185
+ */
186
+ addOptions() {
187
+ var _a;
188
+ return Object.assign(Object.assign({}, (_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)), {
189
+ // 默认语法高亮主题
190
+ defaultTheme: "dracula",
191
+ // 默认编程语言
192
+ defaultLanguage: "javascript",
193
+ // 初始化时的高亮器实例(可选)
194
+ highlighter: undefined });
195
+ },
196
+ /**
197
+ * 添加节点属性
198
+ *
199
+ * 定义了代码块节点的自定义属性,包括编程语言和主题。
200
+ * 这些属性用于存储和渲染代码块的语法高亮配置。
201
+ *
202
+ * @returns 属性配置对象,包含语言和主题属性
203
+ */
204
+ addAttributes() {
205
+ var _a;
206
+ return Object.assign(Object.assign({}, (_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)), {
207
+ // 编程语言属性
208
+ language: {
209
+ // 默认语言配置
210
+ default: this.options.defaultLanguage || "javascript",
211
+ /**
212
+ * 从 HTML 元素解析语言属性
213
+ * @param element - HTML 元素
214
+ * @returns 编程语言字符串
215
+ */
216
+ parseHTML: (element) => {
217
+ // 从 data-language 属性获取语言信息
218
+ return element.getAttribute("data-language");
219
+ },
220
+ /**
221
+ * 渲染 HTML 时设置语言属性
222
+ * @param attributes - 节点属性对象
223
+ * @returns HTML 属性对象
224
+ */
225
+ renderHTML: (attributes) => {
226
+ // 将语言信息存储在 data-language 属性中
227
+ return { "data-language": attributes.language };
228
+ },
229
+ },
230
+ // 主题属性
231
+ theme: {
232
+ // 默认主题配置
233
+ default: this.options.defaultTheme || "dracula",
234
+ /**
235
+ * 从 HTML 元素解析主题属性
236
+ * @param element - HTML 元素
237
+ * @returns 主题名称字符串
238
+ */
239
+ parseHTML: (element) => element.getAttribute("data-theme"),
240
+ /**
241
+ * 渲染 HTML 时设置主题属性
242
+ * @param attributes - 节点属性对象
243
+ * @returns HTML 属性对象
244
+ */
245
+ renderHTML: (attributes) => ({
246
+ "data-theme": attributes.theme,
247
+ }),
248
+ } });
249
+ },
250
+ /**
251
+ * 渲染 HTML
252
+ *
253
+ * 定义了代码块节点在静态 HTML 中的渲染逻辑。
254
+ * 如果配置了 getHighlighHTML 和 highlighter,将使用 Shiki 生成高亮的 HTML;
255
+ * 否则返回标准的 pre/code 标签结构。
256
+ *
257
+ * @param node - ProseMirror 节点对象
258
+ * @param HTMLAttributes - HTML 属性对象
259
+ * @returns HTML 数组或元素
260
+ */
261
+ renderHTML({ node, HTMLAttributes }) {
262
+ // 如果启用了高亮 HTML 生成并且有高亮器实例
263
+ if (this.options.getHighlighHTML && this.options.highlighter) {
264
+ // 获取代码内容、编程语言和主题
265
+ const language = node.attrs.language || this.options.defaultLanguage;
266
+ const theme = node.attrs.theme || this.options.defaultTheme;
267
+ const content = node.textContent;
268
+ // 使用 Shiki 将代码转换为带样式的 HTML
269
+ const highlightedCode = this.options.highlighter.codeToHtml(content, {
270
+ lang: language,
271
+ theme: theme,
272
+ });
273
+ // 解析生成的 HTML 字符串并返回第一个元素
274
+ const container = document.createElement("div");
275
+ container.innerHTML = highlightedCode;
276
+ if (container.firstElementChild)
277
+ return container.firstElementChild;
278
+ }
279
+ // 返回标准的 pre/code 标签结构
280
+ return ["pre", HTMLAttributes, ["code", 0]];
281
+ },
282
+ /**
283
+ * 添加节点视图
284
+ *
285
+ * 创建代码块的交互式 DOM 视图,包括工具栏和代码显示区域。
286
+ * 提供了实时的语法高亮和主题切换功能。
287
+ *
288
+ * @returns 节点视图工厂函数
289
+ */
290
+ addNodeView() {
291
+ // 检查是否有高亮器实例
292
+ if (!this.options.highlighter)
293
+ throw new Error("highlighter is required");
294
+ return (props) => {
295
+ // 获取视图相关参数
296
+ const { view, getPos, node } = props;
297
+ // 获取当前主题配置
298
+ const theme = this.options.highlighter.getTheme(node.attrs.theme || this.options.defaultTheme);
299
+ // 创建主容器 DOM 元素
300
+ const dom = document.createElement("div");
301
+ dom.classList.add("tiptap-shiki--container", "not-prose", "shiki", "dracula");
302
+ // 应用主题颜色
303
+ dom.style.backgroundColor = theme.bg;
304
+ dom.style.color = theme.fg;
305
+ // 创建工具栏(如果配置了自定义渲染器)
306
+ let toolbarDOM;
307
+ if (this.options.renderToolbar &&
308
+ typeof this.options.renderToolbar === "function") {
309
+ // 创建工具栏容器
310
+ toolbarDOM = document.createElement("div");
311
+ toolbarDOM.classList.add("tiptap-shiki--toolbar");
312
+ toolbarDOM.setAttribute("contenteditable", "false");
313
+ // 创建节点标记更新函数
314
+ const setNodeMarkup = (attr) => {
315
+ if (typeof getPos !== "function")
316
+ return;
317
+ const pos = getPos();
318
+ if (pos === undefined)
319
+ return;
320
+ const { tr } = view.state;
321
+ const nowNode = view.state.doc.nodeAt(pos);
322
+ // 更新节点属性
323
+ tr.setNodeMarkup(pos, this.type, Object.assign(Object.assign({}, nowNode === null || nowNode === void 0 ? void 0 : nowNode.attrs), attr));
324
+ view.dispatch(tr);
325
+ };
326
+ // 创建语言设置函数
327
+ const setLanguage = (language) => {
328
+ setNodeMarkup({
329
+ language, // 仅修改当前代码块的编程语言
330
+ });
331
+ };
332
+ // 创建主题设置函数
333
+ const setTheme = (theme) => {
334
+ setNodeMarkup({
335
+ theme, // 仅修改当前代码块的主题
336
+ });
337
+ };
338
+ // 调用自定义工具栏渲染器
339
+ this.options.renderToolbar({
340
+ language: node.attrs.language,
341
+ theme: node.attrs.theme,
342
+ toolbarDOM,
343
+ setLanguage,
344
+ setTheme,
345
+ });
346
+ // 将工具栏添加到主容器
347
+ dom.appendChild(toolbarDOM);
348
+ }
349
+ // 创建代码显示区域
350
+ const preDOM = document.createElement("pre");
351
+ const contentDOM = document.createElement("code");
352
+ contentDOM.classList.add("tiptap-shiki--content");
353
+ // 组装 DOM 结构
354
+ preDOM.appendChild(contentDOM);
355
+ dom.appendChild(preDOM);
356
+ // 返回节点视图对象
357
+ return {
358
+ // 主 DOM 元素
359
+ dom: dom,
360
+ // 内容 DOM 元素
361
+ contentDOM,
362
+ /**
363
+ * 更新节点视图
364
+ * @param updatedNode - 更新后的节点
365
+ * @returns 是否成功更新
366
+ */
367
+ update: (updatedNode) => {
368
+ // 如果不是相同类型的节点,拒绝更新
369
+ return updatedNode.type === node.type;
370
+ },
371
+ /**
372
+ * 忽略某些 DOM 变化
373
+ * @param mutation - DOM 变化对象
374
+ * @returns 是否忽略该变化
375
+ */
376
+ ignoreMutation: (mutation) => {
377
+ // 忽略工具栏区域的 DOM 变化
378
+ return (toolbarDOM === null || toolbarDOM === void 0 ? void 0 : toolbarDOM.contains(mutation.target)) || false;
379
+ },
380
+ };
381
+ };
382
+ },
383
+ /**
384
+ * 添加 ProseMirror 插件
385
+ *
386
+ * 注册 Shiki 轻量级语法高亮插件,实现实时语法高亮功能。
387
+ * 继承父类的插件并添加自定义的高亮插件。
388
+ *
389
+ * @returns ProseMirror 插件数组
390
+ */
391
+ addProseMirrorPlugins() {
392
+ var _a;
393
+ // 检查是否有高亮器实例
394
+ if (!this.options.highlighter)
395
+ throw new Error("highlighter is required");
396
+ return [
397
+ // 继承父类的插件
398
+ ...(((_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)) || []),
399
+ // 添加 Shiki 轻量级高亮插件
400
+ ShikiLightPlugin({
401
+ name: this.name,
402
+ highlighter: this.options.highlighter,
403
+ defaultLanguage: this.options.defaultLanguage,
404
+ defaultTheme: this.options.defaultTheme,
405
+ }),
406
+ ];
407
+ },
408
+ });
409
+
410
+ exports.TiptapShiki = TiptapShiki;
411
+ exports.default = TiptapShiki;
412
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.css ADDED
@@ -0,0 +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}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tiptap Shiki 语法高亮扩展
3
+ *
4
+ * 本文件实现了一个基于 Shiki 的 Tiptap 扩展,用于在富文本编辑器中提供语法高亮功能。
5
+ * 该扩展扩展了 Tiptap 的 CodeBlock,添加了以下功能:
6
+ * - 支持多种编程语言的语法高亮
7
+ * - 支持多种主题切换
8
+ * - 自定义工具栏界面
9
+ * - 键盘快捷键支持
10
+ * - 实时语法高亮渲染
11
+ */
12
+ import "./index.css";
13
+ import type { BundledLanguage, BundledTheme, HighlighterGeneric, SpecialLanguage, StringLiteralUnion, ThemeRegistrationAny } from "shiki";
14
+ /**
15
+ * TiptapShiki 扩展类定义
16
+ *
17
+ * 该类扩展了 Tiptap 的 CodeBlock,添加了语法高亮功能和相关配置选项。
18
+ * 使用泛型类型定义扩展选项和节点视图属性。
19
+ *
20
+ * @template T - 扩展选项类型,包含默认主题、语言、高亮器等配置
21
+ * @template U - 节点视图属性类型,定义 DOM 元素的类型
22
+ */
23
+ declare const TiptapShiki: import("@tiptap/core").Node<{
24
+ defaultTheme: ThemeRegistrationAny | StringLiteralUnion<string>;
25
+ defaultLanguage: StringLiteralUnion<SpecialLanguage>;
26
+ highlighter?: HighlighterGeneric<BundledLanguage, BundledTheme>;
27
+ getHighlighHTML?: boolean;
28
+ renderToolbar?: (props: {
29
+ toolbarDOM: HTMLElement;
30
+ language: StringLiteralUnion<SpecialLanguage>;
31
+ theme: ThemeRegistrationAny | StringLiteralUnion<string>;
32
+ setLanguage: (lang: string) => void;
33
+ setTheme: (theme: string) => void;
34
+ }) => void;
35
+ }, {
36
+ container: HTMLElement;
37
+ shikiCode: HTMLElement;
38
+ contentDOM: HTMLElement;
39
+ }>;
40
+ export { TiptapShiki };
41
+ export default TiptapShiki;