main
1"use strict";
2import { bundledLanguages, bundledThemes, createHighlighter, ShikiTransformer } from "shiki";
3import type Hexo from "hexo";
4import { HighlightOptions } from "hexo/dist/extend/syntax_highlight";
5import { stripIndent, htmlTag } from "hexo-util";
6import { readFileSync } from "fs";
7import {
8 transformerCompactLineOptions,
9 transformerMetaHighlight,
10 transformerMetaWordHighlight,
11 transformerNotationDiff,
12 transformerNotationErrorLevel,
13 transformerNotationFocus,
14 transformerNotationHighlight,
15 transformerNotationWordHighlight,
16 transformerRemoveLineBreak,
17 transformerRemoveNotationEscape,
18 transformerRenderWhitespace,
19} from "@shikijs/transformers";
20
21const supported_transformers = {
22 transformerCompactLineOptions,
23 transformerMetaHighlight,
24 transformerMetaWordHighlight,
25 transformerNotationDiff,
26 transformerNotationErrorLevel,
27 transformerNotationFocus,
28 transformerNotationHighlight,
29 transformerNotationWordHighlight,
30 transformerRemoveLineBreak,
31 transformerRemoveNotationEscape,
32 transformerRenderWhitespace,
33};
34
35type SupportedTransformers = keyof typeof supported_transformers;
36
37interface TransformerConfig {
38 name: SupportedTransformers;
39 option?: Record<string, any> | undefined;
40}
41
42interface OriginalLangNameConfig {
43 exclude?: boolean;
44 langs?: string[];
45 change_origin?: Record<string, string>;
46}
47
48interface AdditionalConfig {
49 themes?: string[];
50 langs?: string[];
51 lang_alias?: Record<string, string>;
52}
53
54interface HighlighterConfig {
55 theme?: string | Record<string, string>;
56 line_number?: boolean;
57 strip_indent?: boolean;
58 tab_replace?: string;
59 original_lang_name?: boolean | OriginalLangNameConfig;
60 pre_style?: boolean;
61 default_color?: string;
62 css_variable_prefix?: string;
63 transformers?: (SupportedTransformers | TransformerConfig)[];
64 additional?: AdditionalConfig;
65}
66
67export async function init(hexo: Hexo) {
68 const config: HighlighterConfig = Object.assign(
69 {
70 theme: "one-dark-pro",
71 line_number: false,
72 strip_indent: true,
73 tab_replace: " ",
74 original_lang_name: false,
75 pre_style: true,
76 default_color: "light",
77 css_variable_prefix: "--shiki-",
78 transformers: [],
79 // TODO: 增加 `wrap` 选项,与 Hexo 的 highlight.js 渲染行为一致
80 // TODO: 增加 `beautify` 选项以适配更多主题
81 additional: {
82 themes: [],
83 langs: [],
84 lang_alias: {},
85 },
86 },
87 hexo.config.shiki || {}
88 );
89
90 // 处理自定义语言 json
91 let additional_langs = [];
92 if (config.additional.langs)
93 for (const extra_lang of [].concat(config.additional.langs)) {
94 additional_langs.push(JSON.parse(readFileSync(extra_lang, "utf8")));
95 }
96 const langs = [...Object.keys(bundledLanguages), ...additional_langs];
97
98 // 处理自定义主题 json
99 let additional_themes = [];
100 if (config.additional.themes)
101 for (const extra_theme of [].concat(config.additional.themes)) {
102 additional_themes.push(JSON.parse(readFileSync(extra_theme, "utf8")));
103 }
104 // 处理主题
105 const themes = additional_themes.concat(
106 (typeof config.theme === "string" ? [config.theme] : Object.values(config.theme)).filter(
107 (theme) => theme in bundledThemes
108 )
109 );
110
111 // 处理 transformers
112 const transformers = config.transformers
113 .map((transformer) => {
114 if (typeof transformer === "string") {
115 let tfm = supported_transformers[transformer];
116 if (!tfm) return null;
117 return tfm();
118 }
119 let tfm = supported_transformers[transformer["name"]];
120 if (!tfm) return null;
121 let option = transformer["option"];
122 if (!option) return tfm();
123 // @ts-ignore
124 return tfm(option);
125 })
126 .filter((tfm) => tfm !== null);
127
128 // 创建 highlighter
129 let highlighter_options = {
130 langs: langs,
131 themes: themes,
132 };
133 if (config.additional.lang_alias && Object.keys(config.additional.lang_alias).length > 0) {
134 highlighter_options["langAlias"] = config.additional.lang_alias;
135 }
136 const highlighter = await createHighlighter(highlighter_options);
137 const supported_languages = highlighter.getLoadedLanguages().reduce(
138 (acc, lang) => {
139 acc[lang] = true;
140 return acc;
141 },
142 {
143 text: true,
144 plain: true,
145 ansi: true,
146 }
147 );
148
149 // 处理语言转换原名
150 const useOringinalLangName = (lang: string) => {
151 if (lang === "text") return false;
152 if (typeof config.original_lang_name === "boolean") return config.original_lang_name;
153 const exclude = config.original_lang_name.exclude || false;
154 const langs = config.original_lang_name.langs || [];
155 return exclude ? !langs.includes(lang) : langs.includes(lang);
156 };
157
158 const originalLangName = (lang: string) => {
159 const name = highlighter.getLanguage(lang).name;
160 if (typeof config.original_lang_name === "boolean") return name;
161 const origin = config.original_lang_name.change_origin || {};
162 if (name in origin) return origin[name];
163 return name;
164 };
165
166 const hexoHighlighter = (code: string, options: HighlightOptions) => {
167 var code = config.strip_indent ? (stripIndent(code) as string) : code;
168 code = config.tab_replace ? code.replace(/\t/g, config.tab_replace) : code;
169
170 // 处理代码语言
171 let lang = options.lang;
172 if (!lang || !supported_languages[lang]) {
173 lang = "text";
174 }
175
176 // 处理代码块语法高亮
177 let pre = "";
178 const transformerMarkedLine = (): ShikiTransformer => {
179 return {
180 line(node, line) {
181 if (options.mark && options.mark.includes(line)) {
182 this.addClassToHast(node, "marked");
183 }
184 },
185 };
186 };
187 try {
188 if (typeof config.theme === "string")
189 pre = highlighter.codeToHtml(code, {
190 lang,
191 theme: config.theme,
192 transformers: [transformerMarkedLine()].concat(transformers),
193 });
194 else
195 pre = highlighter.codeToHtml(code, {
196 lang,
197 themes: config.theme,
198 transformers: [transformerMarkedLine()].concat(transformers),
199 defaultColor: config.default_color,
200 cssVariablePrefix: config.css_variable_prefix,
201 });
202 // 删除多余内容
203 pre = pre.replace(/<pre[^>]*>/, (match) => {
204 if (config.pre_style) return match.replace(/\s*tabindex="0"/, "");
205 return match.replace(/\s*style\s*=\s*"[^"]*"\s*tabindex="0"/, "");
206 });
207 pre = pre.replace(/<\/?code>/, "");
208 } catch (error) {
209 console.warn(error);
210 pre = htmlTag("pre", {}, code);
211 }
212
213 // 处理行号
214 let numbers = "";
215 const show_line_number =
216 config.line_number && // 设置中显示行号
217 (options.line_number || true) && // 代码块中未设置不显示行号
218 (options.line_threshold || 0) < options.lines_length; // 代码行数超过阈值
219 if (show_line_number) {
220 const firstLine = options.firstLine || 1;
221 for (let i = firstLine, len = code.split("\n").length + firstLine; i < len; i++) {
222 numbers += htmlTag("span", { class: "line" }, `${i}`, false) + "<br>";
223 }
224 numbers = htmlTag("pre", {}, numbers, false);
225 }
226
227 // 处理标题与链接
228 const caption = options.caption ? htmlTag("figcaption", {}, options.caption, false) : "";
229
230 // 处理包裹标签
231 const td_code = htmlTag("td", { class: "code" }, pre, false);
232 const td_gutter = numbers.length > 0 ? htmlTag("td", { class: "gutter" }, numbers, false) : "";
233
234 // 合并标签
235 const html = htmlTag(
236 "figure",
237 {
238 class: `highlight ${useOringinalLangName(lang) ? originalLangName(lang) : lang}`,
239 },
240 caption +
241 htmlTag(
242 "table",
243 {},
244 htmlTag("tbody", {}, htmlTag("tr", {}, td_gutter + td_code, false), false),
245 false
246 ),
247 false
248 );
249 return html;
250 };
251 hexo.extend.highlight.register("shiki", hexoHighlighter);
252}