hexo-theme-gnix 12.0.0 → 13.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.
package/README.md CHANGED
@@ -60,5 +60,7 @@ Support multiple light and dark themes:
60
60
  - [ppoffice/hexo-theme-icarus: A simple, delicate, and modern theme for the static site generator Hexo.](https://github.com/ppoffice/hexo-theme-icarus)
61
61
  - [D0n9X1n/hexo-blog-encrypt: Yet, just another hexo plugin for security.](https://github.com/D0n9X1n/hexo-blog-encrypt)
62
62
  - [hexojs/hexo-generator-feed: Feed generator for Hexo.](https://github.com/hexojs/hexo-generator-feed)
63
+ - [hexojs/hexo-generator-sitemap: Sitemap generator for Hexo.](https://github.com/hexojs/hexo-generator-sitemap)
63
64
  - [hexojs/hexo-generator-tag: Tag generator for Hexo.](https://github.com/hexojs/hexo-generator-tag)
64
65
  - [hexojs/hexo-generator-archive: Archive generator for Hexo.](https://github.com/hexojs/hexo-generator-archive)
66
+ - [ebullient/markdown-it-obsidian-callouts: markdown-it plugin for GitHub and Obsidian callouts.](https://github.com/ebullient/markdown-it-obsidian-callouts?tab=Apache-2.0-1-ov-file)
@@ -13,8 +13,18 @@ const {
13
13
  trimSlashes,
14
14
  } = require("../util/i18n");
15
15
 
16
+ let cachedConfig = null;
17
+ let cachedHexo = null;
18
+
16
19
  function getConfig(hexo, locals = {}) {
17
- return Object.assign({}, hexo.config, hexo.config.theme_config, hexo.theme.config, locals.theme);
20
+ if (hexo !== cachedHexo || !cachedConfig) {
21
+ cachedHexo = hexo;
22
+ cachedConfig = Object.assign({}, hexo.config, hexo.config.theme_config, hexo.theme.config);
23
+ }
24
+ if (locals.theme) {
25
+ return Object.assign({}, cachedConfig, locals.theme);
26
+ }
27
+ return cachedConfig;
18
28
  }
19
29
 
20
30
  function getLocalizedPostParams(hexo, sourcePath) {
@@ -0,0 +1,210 @@
1
+ // Adapted from markdown-it-obsidian-callouts (Apache-2.0, (c) Erin Schnabel)
2
+ // https://github.com/ebullient/markdown-it-obsidian-callouts
3
+
4
+ const DEFAULT_ICONS = {
5
+ abstract: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard-list"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg>',
6
+ bug: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>',
7
+ danger: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
8
+ example: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>',
9
+ failure: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
10
+ info: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
11
+ note: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>',
12
+ question: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-help-circle"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',
13
+ quote: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-quote"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>',
14
+ success: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>',
15
+ tip: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>',
16
+ todo: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-circle-2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
17
+ warning: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
18
+ };
19
+
20
+ DEFAULT_ICONS.attention = DEFAULT_ICONS.warning;
21
+ DEFAULT_ICONS.caution = DEFAULT_ICONS.warning;
22
+ DEFAULT_ICONS.check = DEFAULT_ICONS.success;
23
+ DEFAULT_ICONS.cite = DEFAULT_ICONS.quote;
24
+ DEFAULT_ICONS.done = DEFAULT_ICONS.success;
25
+ DEFAULT_ICONS.error = DEFAULT_ICONS.danger;
26
+ DEFAULT_ICONS.fail = DEFAULT_ICONS.failure;
27
+ DEFAULT_ICONS.faq = DEFAULT_ICONS.question;
28
+ DEFAULT_ICONS.help = DEFAULT_ICONS.question;
29
+ DEFAULT_ICONS.hint = DEFAULT_ICONS.tip;
30
+ DEFAULT_ICONS.important = DEFAULT_ICONS.tip;
31
+ DEFAULT_ICONS.missing = DEFAULT_ICONS.failure;
32
+ DEFAULT_ICONS.summary = DEFAULT_ICONS.abstract;
33
+ DEFAULT_ICONS.tldr = DEFAULT_ICONS.abstract;
34
+
35
+ const CALLOUT_RE = /^\[!([^\]]+)\](\+|-|) *(.*)? */;
36
+ const ADMONITION_RE = /^ad-([^\s]+) */;
37
+ const ADMONITION_HEADER_RE = /^(title|collapse|icon|color):(.*)/;
38
+ const HEADER_TO_ATTR = {
39
+ title: "data-callout-title",
40
+ icon: "data-callout-icon",
41
+ color: "data-callout-color",
42
+ };
43
+
44
+ function resolveIcon(token, options) {
45
+ const explicit = token.attrGet("data-callout-icon");
46
+ if (explicit) return explicit.trim();
47
+ const type = token.attrGet("data-callout");
48
+ if (!type) return "";
49
+ return options.icons?.[type] || DEFAULT_ICONS[type] || DEFAULT_ICONS.note;
50
+ }
51
+
52
+ function toTitleCase(str) {
53
+ return str
54
+ .split(" ")
55
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
56
+ .join(" ");
57
+ }
58
+
59
+ async function resolveTitle(token, md) {
60
+ const title = token.attrGet("data-callout-title");
61
+ if (title) return md.renderInlineAsync(title.trim());
62
+ const type = token.attrGet("data-callout");
63
+ return type ? toTitleCase(type) : "";
64
+ }
65
+
66
+ function escapeAttr(str) {
67
+ return String(str).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
68
+ }
69
+
70
+ function colorStyle(token) {
71
+ const color = token.attrGet("data-callout-color");
72
+ return color ? ` style="--callout-color: ${escapeAttr(color)}"` : "";
73
+ }
74
+
75
+ async function renderCalloutPrefix(token, md, options) {
76
+ const type = token.attrGet("data-callout");
77
+ if (!type) return "";
78
+ const fold = token.attrGet("data-callout-fold");
79
+ const icon = resolveIcon(token, options);
80
+ const title = await resolveTitle(token, md);
81
+ const style = colorStyle(token);
82
+
83
+ if (fold) {
84
+ return `
85
+ <details class="callout" data-callout="${type}" data-callout-fold="${fold}"${fold === "+" ? " open" : ""}${style}>
86
+ <summary class="callout-title">
87
+ <div class="callout-title-icon">
88
+ ${icon}
89
+ </div>
90
+ <div class="callout-title-inner">${title}</div>
91
+ <div class="callout-fold"></div>
92
+ </summary>
93
+ <div class="callout-content">`;
94
+ }
95
+
96
+ return `
97
+ <div class="callout" data-callout="${type}"${style}>
98
+ <div class="callout-title">
99
+ <div class="callout-title-icon">
100
+ ${icon}
101
+ </div>
102
+ <div class="callout-title-inner">${title}</div>
103
+ </div>
104
+ <div class="callout-content">`;
105
+ }
106
+
107
+ function renderCalloutPostfix(token) {
108
+ const type = token.attrGet("data-callout");
109
+ if (!type) return "";
110
+ const fold = token.attrGet("data-callout-fold");
111
+ return fold ? "</div></details>" : "</div></div>";
112
+ }
113
+
114
+ function inspectBlockquote(tokens, startIdx) {
115
+ let content = "";
116
+ let depth = 0;
117
+ let endIdx = startIdx;
118
+ let contentIdx = startIdx;
119
+
120
+ for (let i = startIdx; i < tokens.length; i++) {
121
+ const token = tokens[i];
122
+
123
+ if (token.type === "blockquote_open") depth++;
124
+ else if (token.type === "blockquote_close") {
125
+ endIdx = i;
126
+ depth--;
127
+ }
128
+
129
+ if (depth === 0) break;
130
+ if (depth > 1) continue;
131
+
132
+ if (token.type === "inline") {
133
+ if (contentIdx === startIdx && token.content.match(CALLOUT_RE)) {
134
+ contentIdx = i;
135
+ }
136
+ content += token.content;
137
+ } else if (token.type === "paragraph_close") {
138
+ content += "\n";
139
+ }
140
+ }
141
+
142
+ const match = content.match(CALLOUT_RE);
143
+ if (!match || startIdx === endIdx) return;
144
+
145
+ const type = match[1].toLowerCase();
146
+ const fold = match[2];
147
+ const title = match[3];
148
+
149
+ tokens[startIdx].type = "callout_open";
150
+ tokens[startIdx].attrPush(["class", "callout"]);
151
+ tokens[startIdx].attrPush(["data-callout", type]);
152
+ tokens[startIdx].attrPush(["data-callout-fold", fold]);
153
+ if (title) tokens[startIdx].attrPush(["data-callout-title", title]);
154
+
155
+ tokens[endIdx].type = "callout_close";
156
+ tokens[endIdx].attrPush(["data-callout", type]);
157
+ tokens[endIdx].attrPush(["data-callout-fold", fold]);
158
+
159
+ if (contentIdx !== startIdx && tokens[contentIdx]?.children) {
160
+ tokens[contentIdx].content = tokens[contentIdx].content.replace(CALLOUT_RE, "").trim();
161
+ }
162
+ }
163
+
164
+ function inspectFence(tokens, startIdx, options) {
165
+ const token = tokens[startIdx];
166
+ if (!token.info) return;
167
+
168
+ const match = token.info.replace(options.langPrefix || "", "").match(ADMONITION_RE);
169
+ if (!match) return;
170
+
171
+ token.type = "admonition_block";
172
+ token.attrPush(["class", "callout"]);
173
+ token.attrPush(["data-callout", match[1].toLowerCase()]);
174
+
175
+ let lines = token.content.split("\n");
176
+ while (lines.length > 0 && ADMONITION_HEADER_RE.test(lines[0])) {
177
+ const headerMatch = lines[0].match(ADMONITION_HEADER_RE);
178
+ if (!headerMatch) break;
179
+ const attrName = HEADER_TO_ATTR[headerMatch[1].trim().toLowerCase()];
180
+ if (attrName) token.attrPush([attrName, headerMatch[2].trim()]);
181
+ lines = lines.slice(1);
182
+ }
183
+
184
+ token.content = lines.join("\n");
185
+ }
186
+
187
+ function obsidianCallouts(md, options = {}) {
188
+ md.core.ruler.after("block", "obsidian-callouts", (state) => {
189
+ const tokens = state.tokens;
190
+ for (let i = 0; i < tokens.length; i++) {
191
+ const token = tokens[i];
192
+ if (token.type === "blockquote_open") inspectBlockquote(tokens, i);
193
+ if (token.type === "fence") inspectFence(tokens, i, options);
194
+ }
195
+ });
196
+
197
+ md.renderer.rules.callout_open = async (tokens, idx) => renderCalloutPrefix(tokens[idx], md, options);
198
+
199
+ md.renderer.rules.admonition_block = async (tokens, idx) => {
200
+ const token = tokens[idx];
201
+ const prefix = await renderCalloutPrefix(token, md, options);
202
+ const body = await md.renderAsync(token.content);
203
+ return `${prefix}${body}\n</div>\n</div>`;
204
+ };
205
+
206
+ md.renderer.rules.callout_close = (tokens, idx) => renderCalloutPostfix(tokens[idx]);
207
+ }
208
+
209
+ module.exports = obsidianCallouts;
210
+ module.exports.DEFAULT_ICONS = DEFAULT_ICONS;
@@ -2,14 +2,11 @@ const path = require("node:path");
2
2
  const { createMarkdownExit } = require("markdown-exit");
3
3
  const mermaidDiagram = require("markdown-exit-mermaid");
4
4
  const ratex = require("markdown-exit-ratex");
5
- const code = require("markdown-exit-shiki");
6
- const abbr = require("markdown-it-abbr");
5
+ const code = require("./shiki");
6
+ const obsidianCallouts = require("./obsidian-callouts");
7
7
  const anchor = require("markdown-it-anchor");
8
8
  const footnote = require("markdown-it-footnote");
9
- const ins = require("markdown-it-ins");
10
9
  const mark = require("markdown-it-mark");
11
- const sub = require("markdown-it-sub");
12
- const sup = require("markdown-it-sup");
13
10
  const taskLists = require("markdown-it-task-lists");
14
11
 
15
12
  function resolveDefault(module) {
@@ -222,11 +219,7 @@ class MarkdownRenderer {
222
219
  typographer: true,
223
220
  xhtmlOut: false,
224
221
  },
225
- code_options: {
226
- themes: {
227
- light: "catppuccin-latte",
228
- },
229
- },
222
+
230
223
  mermaid_options: {
231
224
  theme: "default",
232
225
  },
@@ -243,14 +236,11 @@ class MarkdownRenderer {
243
236
  this.md
244
237
  .use(resolveDefault(footnote))
245
238
  .use(resolveDefault(mark))
246
- .use(resolveDefault(sub))
247
- .use(resolveDefault(sup))
248
- .use(resolveDefault(abbr))
249
- .use(resolveDefault(ins))
250
239
  .use(resolveDefault(taskLists))
251
240
  .use(resolveDefault(code), this.config.code_options)
252
241
  .use(resolveDefault(mermaidDiagram), this.config.mermaid_options)
253
242
  .use(resolveDefault(ratex), this.config.ratex_options)
243
+ .use(obsidianCallouts, this.config.callout_options)
254
244
  .use(customTabs)
255
245
  .use(wrapMarkdownItTable)
256
246
  .use(resolveDefault(anchor), {
@@ -0,0 +1,191 @@
1
+ const { codeToHtml } = require("shiki");
2
+ const t = require("@shikijs/transformers");
3
+ const { transformerColorizedBrackets } = require("@shikijs/colorized-brackets");
4
+ const { mkdir, writeFile } = require("node:fs/promises");
5
+ const { dirname } = require("node:path");
6
+
7
+ const THEMES = {
8
+ light: "catppuccin-latte",
9
+ dark: "catppuccin-mocha",
10
+ song: "everforest-light",
11
+ nord: "nord",
12
+ tokyo: "tokyo-night",
13
+ rose: "rose-pine",
14
+ };
15
+
16
+ const TRANSFORMERS = [
17
+ t.transformerCompactLineOptions(),
18
+ t.transformerMetaHighlight(),
19
+ t.transformerMetaWordHighlight(),
20
+ t.transformerNotationDiff(),
21
+ t.transformerNotationErrorLevel(),
22
+ t.transformerNotationFocus(),
23
+ t.transformerNotationHighlight(),
24
+ t.transformerNotationWordHighlight(),
25
+ t.transformerRemoveLineBreak(),
26
+ t.transformerRemoveNotationEscape(),
27
+ t.transformerRenderWhitespace(),
28
+ transformerColorizedBrackets(),
29
+ ];
30
+
31
+ const SVG_WRAP =
32
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="toggle-wrap" title="Toggle Wrap"><path d="m16 16-3 3 3 3"/><path d="M3 12h14.5a1 1 0 0 1 0 7H13"/><path d="M3 19h6"/><path d="M3 5h18"/></svg>';
33
+ const SVG_COPY =
34
+ '<div class="copy-notice"></div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="copy-button"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/><path d="M16 4h2a2 2 0 0 1 2 2v4"/><path d="M21 14H11"/><path d="m15 10-4 4 4 4"/></svg>';
35
+ const SVG_EXPAND =
36
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="expand-icon"><path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3 3-3-3"/><path d="m15 5-3-3-3 3"/></svg>';
37
+ const SVG_COLLAPSE =
38
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="collapse-icon"><path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3-3-3 3"/><path d="m15 5 3 3-3 3"/></svg>';
39
+
40
+ const RE_LINE = /<span class="line/g;
41
+
42
+ function escapeHtml(code) {
43
+ return code.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
44
+ }
45
+
46
+ function createShikiTools(lang, title, { lang: showLang, title: showTitle, wrapToggle, copyButton }) {
47
+ let left = '<div class="left"><div class="traffic-lights"> <span class="traffic-light red"></span> <span class="traffic-light yellow"></span> <span class="traffic-light green"></span> </div>';
48
+ if (showLang) left += `<div class="code-lang">${lang.toUpperCase()}</div>`;
49
+ left += "</div>";
50
+
51
+ let center = '<div class="center">';
52
+ if (showTitle && title) center += `\n<div class="code-title">${title}</div>`;
53
+ center += "\n</div>";
54
+
55
+ let right = '<div class="right">';
56
+ if (wrapToggle) right += `\n${SVG_WRAP}`;
57
+ if (copyButton) right += `\n${SVG_COPY}`;
58
+ right += "\n</div>";
59
+
60
+ return `<div class="shiki-tools">${left}${center}${right}</div>`;
61
+ }
62
+
63
+ async function writeCssAsync(cssGetter, cssOutputPath) {
64
+ if (!cssGetter || !cssOutputPath) return;
65
+ const css = cssGetter();
66
+ await mkdir(dirname(cssOutputPath), { recursive: true });
67
+ await writeFile(cssOutputPath, css, "utf8");
68
+ }
69
+
70
+ function computeCollapseAttributes(cfg, codeHtml) {
71
+ const codeLines = (codeHtml.match(RE_LINE) || []).length;
72
+ const shouldCollapse = cfg.collapseConfig.enable && codeLines > cfg.collapseConfig.maxLines;
73
+ return {
74
+ expandButton: shouldCollapse ? `<div class="code-expand-btn">${SVG_EXPAND}${SVG_COLLAPSE}</div>` : "",
75
+ collapseAttrs: shouldCollapse ? ` data-collapsible="true" data-max-lines="${cfg.collapseConfig.maxLines}" data-total-lines="${codeLines}"` : "",
76
+ };
77
+ }
78
+
79
+ function parseConfig(renderOptions) {
80
+ const options = renderOptions || {};
81
+ const { toolbar_items: ti = {}, style_to_class: stc } = options;
82
+
83
+ let enabledTransformers;
84
+ if (!options.transformers || options.transformers.includes("all")) {
85
+ enabledTransformers = [...TRANSFORMERS];
86
+ } else {
87
+ enabledTransformers = options.transformers.map((name) => TRANSFORMERS.find((tr) => tr.name === name)).filter(Boolean);
88
+ }
89
+
90
+ let toClass = null;
91
+ if (stc && stc.enable) {
92
+ toClass = t.transformerStyleToClass({ classPrefix: stc.class_prefix || "_sk_" });
93
+ enabledTransformers.push(toClass);
94
+ }
95
+
96
+ const maxLines = options.code_collapse != null ? options.code_collapse : 30;
97
+
98
+ return {
99
+ themes: THEMES,
100
+ excludes: options.exclude_languages || ["mermaid"],
101
+ aliases: options.language_aliases || {},
102
+ collapseConfig: { enable: maxLines > 0, maxLines },
103
+ styleToClass: {
104
+ enable: !!(stc && stc.enable),
105
+ cssGetter: toClass ? toClass.getCSS : undefined,
106
+ css_output_path: stc ? stc.css_output_path : undefined,
107
+ },
108
+ transformers: enabledTransformers,
109
+ toolbarItems: {
110
+ lang: ti.lang != null ? ti.lang : true,
111
+ title: ti.title != null ? ti.title : true,
112
+ wrapToggle: ti.wrapToggle != null ? ti.wrapToggle : true,
113
+ copyButton: ti.copyButton != null ? ti.copyButton : true,
114
+ },
115
+ };
116
+ }
117
+
118
+ function renderCode(md, renderOptions) {
119
+ const cfg = parseConfig(renderOptions);
120
+
121
+ md.renderer.rules.fence = async (tokens, idx) => {
122
+ const token = tokens[idx];
123
+ if (!token) return "";
124
+
125
+ const code = token.content;
126
+ const lang = token.info.split(/\s+/)[0] || "";
127
+ const attrs = token.info.split(/\s+/).slice(1).join(" ");
128
+
129
+ if (cfg.excludes.includes(lang)) {
130
+ const escaped = escapeHtml(code);
131
+ return `<pre><code class="${lang}">${escaped}</code></pre>`;
132
+ }
133
+ const normalizedCode = code.replace(/\r?\n$/, "");
134
+ const mappedLang = cfg.aliases[lang] || lang;
135
+ let codeHtml;
136
+ try {
137
+ codeHtml = await codeToHtml(normalizedCode, {
138
+ lang: mappedLang,
139
+ themes: cfg.themes,
140
+ transformers: cfg.transformers,
141
+ });
142
+ } catch (err) {
143
+ console.warn(`[shiki] Language \`${mappedLang}\` is not supported, falling back to \`txt\`.`);
144
+ codeHtml = await codeToHtml(normalizedCode, {
145
+ lang: "txt",
146
+ themes: cfg.themes,
147
+ transformers: cfg.transformers,
148
+ });
149
+ }
150
+ await writeCssAsync(cfg.styleToClass.cssGetter, cfg.styleToClass.css_output_path);
151
+ codeHtml = codeHtml.replace(/<pre[^>]*>/, (match) => match.replace(/\s*style\s*=\s*"[^"]*"\s*tabindex="0"/, ""));
152
+
153
+ const title = attrs || "";
154
+ const shikiToolsHtml = createShikiTools(lang || "", title, cfg.toolbarItems);
155
+ const { expandButton, collapseAttrs } = computeCollapseAttributes(cfg, codeHtml);
156
+ return `<figure class="shiki" ${collapseAttrs}> ${shikiToolsHtml} ${codeHtml}${expandButton} </figure>`;
157
+ };
158
+
159
+ md.renderer.rules.code_inline = async (tokens, idx, _options, _env, self) => {
160
+ const token = tokens[idx];
161
+ if (!token) return "";
162
+
163
+ const content = token.content.trim();
164
+ const match = content.match(/^\{(\w+)\}\s+(.+)$/);
165
+ if (match === null) {
166
+ return `<code${self.renderAttrs(token)}>${escapeHtml(content)}</code>`;
167
+ }
168
+ const [, lang, code] = match;
169
+ if (!lang || !code) return `<code>${content}</code>`;
170
+ let highlighted;
171
+ try {
172
+ highlighted = await codeToHtml(code, {
173
+ lang: lang,
174
+ themes: cfg.themes,
175
+ structure: "inline",
176
+ });
177
+ } catch (err) {
178
+ console.warn(`[shiki] Language \`${lang}\` is not supported, falling back to \`txt\`.`);
179
+ highlighted = await codeToHtml(code, {
180
+ lang: "txt",
181
+ themes: cfg.themes,
182
+ structure: "inline",
183
+ });
184
+ }
185
+ return `<code${self.renderAttrs(token)}>${highlighted}</code>`;
186
+ };
187
+ }
188
+
189
+ module.exports = renderCode;
190
+ module.exports.default = renderCode;
191
+
@@ -0,0 +1,184 @@
1
+ // Adapted from hexo-generator-sitemap (MIT, (c) Tommy Chen)
2
+ // https://github.com/hexojs/hexo-generator-sitemap
3
+
4
+ const { extname } = require("node:path");
5
+ const { encodeURL, url_for } = require("hexo-util");
6
+
7
+ const DEFAULT_CONFIG = {
8
+ path: ["sitemap.xml", "sitemap.txt"],
9
+ rel: false,
10
+ tags: true,
11
+ categories: true,
12
+ };
13
+
14
+ const DEFAULT_SKIP_PATTERNS = ["**/*.js", "**/*.css"];
15
+ const REL_SITEMAP_RE = /rel=['|"]?sitemap['|"]?/i;
16
+ const HEAD_RE = /<head>(?!<\/head>).+?<\/head>/s;
17
+ const REGEX_ESCAPE_CHARS = "\\^$.+()[]{}|";
18
+
19
+ function globToRegExp(pattern) {
20
+ let regex = "";
21
+ for (let i = 0; i < pattern.length; i++) {
22
+ const ch = pattern[i];
23
+ if (ch === "*") {
24
+ if (pattern[i + 1] === "*") {
25
+ regex += ".*";
26
+ i++;
27
+ if (pattern[i + 1] === "/") i++;
28
+ } else {
29
+ regex += "[^/]*";
30
+ }
31
+ } else if (ch === "?") {
32
+ regex += "[^/]";
33
+ } else if (REGEX_ESCAPE_CHARS.includes(ch)) {
34
+ regex += `\\${ch}`;
35
+ } else {
36
+ regex += ch;
37
+ }
38
+ }
39
+ return new RegExp(`^${regex}$`);
40
+ }
41
+
42
+ function buildMatcher(patterns) {
43
+ const compiled = patterns.map(globToRegExp);
44
+ return (value) => compiled.some((re) => re.test(value));
45
+ }
46
+
47
+ function normalizePaths(rawPath) {
48
+ const paths = Array.isArray(rawPath) ? rawPath : typeof rawPath === "string" ? [rawPath] : DEFAULT_CONFIG.path;
49
+ return paths.filter((p) => typeof p === "string" && p.trim()).map((p) => (extname(p) ? p : `${p}.xml`));
50
+ }
51
+
52
+ function formatDate(date) {
53
+ return date.toISOString().substring(0, 10);
54
+ }
55
+
56
+ function getLastMod(post) {
57
+ const value = post.updated || post.date;
58
+ if (!value) return null;
59
+ if (typeof value.toDate === "function") return value.toDate();
60
+ return value instanceof Date ? value : null;
61
+ }
62
+
63
+ function getSortKey(post) {
64
+ const value = post.updated;
65
+ if (!value) return 0;
66
+ if (typeof value.valueOf === "function") return value.valueOf();
67
+ return value instanceof Date ? value.getTime() : 0;
68
+ }
69
+
70
+ function renderXml({ posts, tags, categories, siteUrl, now }) {
71
+ const formattedNow = formatDate(now);
72
+
73
+ const postEntries = posts
74
+ .map((post) => {
75
+ const lastMod = getLastMod(post);
76
+ const lastModLine = lastMod ? `\n <lastmod>${formatDate(lastMod)}</lastmod>` : "";
77
+ return ` <url>
78
+ <loc>${encodeURL(post.permalink)}</loc>${lastModLine}
79
+ <changefreq>monthly</changefreq>
80
+ <priority>0.6</priority>
81
+ </url>`;
82
+ })
83
+ .join("\n");
84
+
85
+ const taxonomyEntries = (items, freq, priority) =>
86
+ items
87
+ .map(
88
+ (item) => ` <url>
89
+ <loc>${encodeURL(item.permalink)}</loc>
90
+ <lastmod>${formattedNow}</lastmod>
91
+ <changefreq>${freq}</changefreq>
92
+ <priority>${priority}</priority>
93
+ </url>`,
94
+ )
95
+ .join("\n");
96
+
97
+ const sections = [
98
+ postEntries,
99
+ ` <url>
100
+ <loc>${encodeURL(siteUrl)}</loc>
101
+ <lastmod>${formattedNow}</lastmod>
102
+ <changefreq>daily</changefreq>
103
+ <priority>1.0</priority>
104
+ </url>`,
105
+ taxonomyEntries(tags, "weekly", "0.2"),
106
+ taxonomyEntries(categories, "weekly", "0.2"),
107
+ ].filter(Boolean);
108
+
109
+ return `<?xml version="1.0" encoding="UTF-8"?>
110
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
111
+ ${sections.join("\n\n")}
112
+ </urlset>
113
+ `;
114
+ }
115
+
116
+ function renderTxt({ posts, tags, categories, siteUrl }) {
117
+ const lines = [
118
+ ...posts.map((post) => encodeURL(post.permalink)),
119
+ encodeURL(siteUrl),
120
+ ...tags.map((tag) => encodeURL(tag.permalink)),
121
+ ...categories.map((cat) => encodeURL(cat.permalink)),
122
+ ];
123
+ return `${lines.join("\n")}\n`;
124
+ }
125
+
126
+ const RENDERERS = {
127
+ ".xml": renderXml,
128
+ ".txt": renderTxt,
129
+ };
130
+
131
+ module.exports = (hexo) => {
132
+ const config = Object.assign({}, DEFAULT_CONFIG, hexo.config.sitemap);
133
+ config.path = normalizePaths(config.path);
134
+ hexo.config.sitemap = config;
135
+
136
+ hexo.extend.generator.register("sitemap", function (locals) {
137
+ const skipPatterns = [...DEFAULT_SKIP_PATTERNS];
138
+ const userSkip = this.config.skip_render;
139
+ if (Array.isArray(userSkip)) {
140
+ skipPatterns.push(...userSkip);
141
+ } else if (typeof userSkip === "string" && userSkip.length > 0) {
142
+ skipPatterns.push(userSkip);
143
+ }
144
+
145
+ const isSkipped = buildMatcher(skipPatterns);
146
+ const posts = []
147
+ .concat(locals.posts.toArray(), locals.pages.toArray())
148
+ .filter((post) => post.sitemap !== false && !isSkipped(post.source))
149
+ .sort((a, b) => getSortKey(b) - getSortKey(a));
150
+
151
+ if (posts.length === 0) {
152
+ config.rel = false;
153
+ return;
154
+ }
155
+
156
+ const context = {
157
+ posts,
158
+ tags: config.tags ? locals.tags.toArray() : [],
159
+ categories: config.categories ? locals.categories.toArray() : [],
160
+ siteUrl: this.config.url,
161
+ now: new Date(),
162
+ };
163
+
164
+ return config.path
165
+ .map((p) => {
166
+ const renderer = RENDERERS[extname(p)];
167
+ return renderer ? { path: p, data: renderer(context) } : null;
168
+ })
169
+ .filter(Boolean);
170
+ });
171
+
172
+ if (config.rel === true) {
173
+ hexo.extend.filter.register("after_render:html", function (data) {
174
+ const sitemapConfig = hexo.config.sitemap;
175
+ if (!sitemapConfig.rel || REL_SITEMAP_RE.test(data)) return data;
176
+
177
+ const xmlPath = sitemapConfig.path.find((p) => extname(p) === ".xml");
178
+ if (!xmlPath) return data;
179
+
180
+ const tag = `<link rel="sitemap" type="application/xml" title="Sitemap" href="${url_for.call(this, xmlPath)}">`;
181
+ return data.replace(HEAD_RE, (str) => str.replace("</head>", `${tag}</head>`));
182
+ });
183
+ }
184
+ };