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}