hexo-theme-gnix 12.0.0 → 14.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 +2 -0
- package/include/hexo/generator/archive.js +14 -1
- package/include/hexo/generator/index.js +0 -5
- package/include/hexo/generator/page.js +18 -4
- package/include/hexo/generator/tag.js +1 -1
- package/include/hexo/helper.js +0 -4
- package/include/hexo/i18n.js +31 -136
- package/include/hexo/obsidian-callouts.js +210 -0
- package/include/hexo/renderer.js +4 -14
- package/include/hexo/shiki.js +191 -0
- package/include/hexo/sitemap.js +184 -0
- package/include/util/i18n.js +92 -106
- package/languages/en.yml +4 -10
- package/languages/zh-CN.yml +4 -10
- package/layout/archive.jsx +155 -78
- package/layout/common/article.jsx +94 -108
- package/layout/common/article_cover.jsx +3 -3
- package/layout/common/article_info.jsx +11 -48
- package/layout/common/article_media.jsx +9 -2
- package/layout/common/footer.jsx +17 -106
- package/layout/common/head.jsx +3 -15
- package/layout/common/navbar.jsx +24 -87
- package/layout/common/scripts.jsx +1 -1
- package/layout/layout.jsx +37 -19
- package/layout/plugin/goatcounter.jsx +25 -0
- package/layout/tag.jsx +3 -70
- package/layout/tags.jsx +26 -23
- package/package.json +7 -13
- package/scripts/index.js +1 -0
- package/source/css/archive.css +287 -168
- package/source/css/callout_blocks.css +41 -21
- package/source/css/default.css +154 -132
- package/source/css/optional/mermaid.css +12 -6
- package/source/css/responsive.css +1 -45
- package/source/css/shiki/shiki.css +5 -4
- package/source/css/tags.css +53 -59
- package/source/js/components/archive-popup.js +313 -0
- package/source/js/components/friends-list.js +270 -0
- package/source/js/components/x-info-card.js +297 -0
- package/source/js/main.js +38 -34
- package/source/js/mdit/mermaid.js +10 -0
- package/include/hexo/generator/home.js +0 -64
- package/layout/index.jsx +0 -19
- package/layout/misc/paginator.jsx +0 -69
- package/source/js/host/iconify-icon/3.0.2/iconify-icon.min.js +0 -12
|
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
+
};
|
package/include/util/i18n.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
cn: {
|
|
5
|
-
label: "Chinese",
|
|
6
|
-
locale: "zh-CN",
|
|
7
|
-
prefix: "cn",
|
|
8
|
-
},
|
|
9
|
-
en: {
|
|
10
|
-
label: "English",
|
|
11
|
-
locale: "en",
|
|
12
|
-
prefix: "en",
|
|
13
|
-
},
|
|
14
|
-
};
|
|
3
|
+
// ─── 基础字符串工具 ────────────────────────────────────────────────
|
|
15
4
|
|
|
16
5
|
function trimSlashes(value) {
|
|
17
6
|
return String(value || "").replace(/^\/+|\/+$/g, "");
|
|
@@ -34,6 +23,8 @@ function isExternalUrl(value) {
|
|
|
34
23
|
return /^(?:[a-z][a-z\d+.-]*:)?\/\//i.test(value) || /^(?:mailto|tel|data):/i.test(value) || value.startsWith("#");
|
|
35
24
|
}
|
|
36
25
|
|
|
26
|
+
// ─── i18n 配置解析(带缓存)─────────────────────────────────────────
|
|
27
|
+
|
|
37
28
|
const i18nConfigCache = new WeakMap();
|
|
38
29
|
|
|
39
30
|
function getI18nConfig(config = {}) {
|
|
@@ -41,27 +32,23 @@ function getI18nConfig(config = {}) {
|
|
|
41
32
|
return i18nConfigCache.get(config);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const raw = configI18n.enabled === true || configI18n.languages ? configI18n : themeI18n.enabled === true || themeI18n.languages ? themeI18n : configI18n;
|
|
47
|
-
const rawLanguages = raw.languages || DEFAULT_LANGUAGES;
|
|
35
|
+
const raw = config.i18n || {};
|
|
36
|
+
const rawLanguages = raw.languages || {};
|
|
48
37
|
const languages = {};
|
|
49
38
|
|
|
50
39
|
Object.keys(rawLanguages).forEach((key) => {
|
|
51
40
|
const normalizedKey = normalizeLanguageKey(key);
|
|
52
|
-
const value =
|
|
53
|
-
const locale = normalizeLocale(value.locale || value.lang || value.language || normalizedKey);
|
|
54
|
-
const prefix = value.prefix ?? value.path ?? value.url_prefix;
|
|
41
|
+
const value = rawLanguages[key] || {};
|
|
55
42
|
languages[normalizedKey] = {
|
|
56
43
|
key: normalizedKey,
|
|
57
|
-
label: value.label ||
|
|
58
|
-
locale,
|
|
59
|
-
prefix: normalizePrefix(prefix, normalizedKey),
|
|
44
|
+
label: value.label || normalizedKey,
|
|
45
|
+
locale: normalizeLocale(value.locale || normalizedKey),
|
|
46
|
+
prefix: normalizePrefix(value.prefix, normalizedKey),
|
|
60
47
|
};
|
|
61
48
|
});
|
|
62
49
|
|
|
63
50
|
const keys = Object.keys(languages);
|
|
64
|
-
const configuredDefault = normalizeLanguageKey(raw.default ||
|
|
51
|
+
const configuredDefault = normalizeLanguageKey(raw.default || keys[0]);
|
|
65
52
|
const defaultLanguage = languages[configuredDefault] ? configuredDefault : keys[0];
|
|
66
53
|
|
|
67
54
|
const result = {
|
|
@@ -92,22 +79,23 @@ function getLanguage(config = {}, key) {
|
|
|
92
79
|
return i18n.languages[normalizedKey] || i18n.languages[i18n.defaultLanguage];
|
|
93
80
|
}
|
|
94
81
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
function getLanguageBasePath(config, key) {
|
|
83
|
+
return getLanguage(config, key)?.prefix || "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── 语言键查询(私有辅助)─────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function getLanguageKeyFromLocale(config, locale) {
|
|
89
|
+
const normalized = normalizeLocale(locale).toLowerCase();
|
|
90
|
+
if (!normalized) return null;
|
|
98
91
|
|
|
99
92
|
const i18n = getI18nConfig(config);
|
|
100
|
-
if (i18n.languages[
|
|
93
|
+
if (i18n.languages[normalized]) return normalized;
|
|
101
94
|
|
|
102
|
-
return (
|
|
103
|
-
Object.keys(i18n.languages).find((key) => {
|
|
104
|
-
const language = i18n.languages[key];
|
|
105
|
-
return language.locale.toLowerCase() === normalizedLocale;
|
|
106
|
-
}) || null
|
|
107
|
-
);
|
|
95
|
+
return Object.keys(i18n.languages).find((key) => i18n.languages[key].locale.toLowerCase() === normalized) || null;
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
function getLanguageKeyFromPath(value, config
|
|
98
|
+
function getLanguageKeyFromPath(value, config) {
|
|
111
99
|
const normalized = trimSlashes(value);
|
|
112
100
|
if (!normalized) return null;
|
|
113
101
|
|
|
@@ -120,30 +108,69 @@ function getLanguageKeyFromPath(value, config = {}) {
|
|
|
120
108
|
);
|
|
121
109
|
}
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
const normalized = trimSlashes(value).replace(/\\/g, "/");
|
|
125
|
-
if (!normalized) return null;
|
|
111
|
+
// ─── 源路径解析:从文件名识别 __<lang> 后缀 ──────────────────────────
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
function parseLocalizedSource(source) {
|
|
114
|
+
if (typeof source !== "string" || !source) {
|
|
115
|
+
return { langKey: null, baseSource: "" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalized = source.replace(/\\/g, "/");
|
|
119
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
120
|
+
const dir = lastSlash >= 0 ? normalized.slice(0, lastSlash + 1) : "";
|
|
121
|
+
const filename = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
|
122
|
+
const ext = path.posix.extname(filename);
|
|
123
|
+
const stem = ext ? filename.slice(0, -ext.length) : filename;
|
|
124
|
+
|
|
125
|
+
const match = stem.match(/^(.+?)__([a-zA-Z][\w-]*)$/);
|
|
126
|
+
if (!match) return { langKey: null, baseSource: normalized };
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
langKey: match[2],
|
|
130
|
+
baseSource: dir + match[1] + ext,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function inferI18nKeyFromSource(source) {
|
|
135
|
+
if (typeof source !== "string") return "";
|
|
136
|
+
const { baseSource } = parseLocalizedSource(source);
|
|
137
|
+
const normalized = trimSlashes(baseSource || source).replace(/\\/g, "/");
|
|
138
|
+
const ext = path.posix.extname(normalized);
|
|
139
|
+
const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
|
|
140
|
+
const parts = withoutExt.split("/").filter(Boolean);
|
|
141
|
+
if (!parts.length) return "";
|
|
142
|
+
|
|
143
|
+
const last = parts[parts.length - 1];
|
|
144
|
+
if (last === "index" && parts.length >= 2) {
|
|
145
|
+
return parts[parts.length - 2];
|
|
146
|
+
}
|
|
147
|
+
return last;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getI18nKey(item = {}) {
|
|
151
|
+
if (item.i18n_key) return item.i18n_key;
|
|
152
|
+
|
|
153
|
+
const fromSource = inferI18nKeyFromSource(item.source);
|
|
154
|
+
if (fromSource) return fromSource;
|
|
155
|
+
|
|
156
|
+
return item.slug || "";
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
// ─── 单个 post / page 的语言归属查询 ────────────────────────────────
|
|
160
|
+
|
|
132
161
|
function getPageLanguageKey(page = {}, config = {}) {
|
|
133
162
|
if (!isI18nEnabled(config)) {
|
|
134
|
-
const
|
|
135
|
-
return getLanguageKeyFromLocale(config, page.lang || page.language ||
|
|
163
|
+
const fallback = Array.isArray(config.language) ? config.language[0] : config.language;
|
|
164
|
+
return getLanguageKeyFromLocale(config, page.lang || page.language || fallback) || getDefaultLanguageKey(config);
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
const explicit = normalizeLanguageKey(page.i18n_lang
|
|
167
|
+
const explicit = normalizeLanguageKey(page.i18n_lang);
|
|
139
168
|
if (explicit && getLanguage(config, explicit)?.key === explicit) return explicit;
|
|
140
169
|
|
|
141
|
-
const fromSource =
|
|
170
|
+
const fromSource = parseLocalizedSource(page.source || page.full_source || "").langKey;
|
|
142
171
|
if (fromSource) return fromSource;
|
|
143
172
|
|
|
144
|
-
const
|
|
145
|
-
const pathValue = pathDescriptor && typeof pathDescriptor.get !== "function" ? pathDescriptor.value : "";
|
|
146
|
-
const fromPath = getLanguageKeyFromPath(pathValue || page.canonical_path || "", config);
|
|
173
|
+
const fromPath = getLanguageKeyFromPath(page.path || page.canonical_path || "", config);
|
|
147
174
|
if (fromPath) return fromPath;
|
|
148
175
|
|
|
149
176
|
const fromLocale = getLanguageKeyFromLocale(config, page.lang || page.language);
|
|
@@ -154,30 +181,22 @@ function getPageLanguageKey(page = {}, config = {}) {
|
|
|
154
181
|
|
|
155
182
|
function getPageLocale(page = {}, config = {}) {
|
|
156
183
|
if (!isI18nEnabled(config)) {
|
|
157
|
-
const
|
|
158
|
-
return normalizeLocale(page.lang || page.language ||
|
|
184
|
+
const fallback = Array.isArray(config.language) ? config.language[0] : config.language;
|
|
185
|
+
return normalizeLocale(page.lang || page.language || fallback || "");
|
|
159
186
|
}
|
|
160
187
|
|
|
161
188
|
const language = getLanguage(config, getPageLanguageKey(page, config));
|
|
162
189
|
return language?.locale || normalizeLocale(page.lang || page.language) || "";
|
|
163
190
|
}
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
const key = getLanguage(config, keyOrLocale)?.key === normalizeLanguageKey(keyOrLocale) ? normalizeLanguageKey(keyOrLocale) : getLanguageKeyFromLocale(config, keyOrLocale);
|
|
167
|
-
const language = getLanguage(config, key || getDefaultLanguageKey(config));
|
|
168
|
-
return language?.label || keyOrLocale;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getLanguageBasePath(config = {}, key) {
|
|
172
|
-
return getLanguage(config, key)?.prefix || "";
|
|
173
|
-
}
|
|
192
|
+
// ─── 路径生成 ──────────────────────────────────────────────────────
|
|
174
193
|
|
|
175
194
|
function joinRoute(...parts) {
|
|
176
195
|
const joined = parts.map(trimSlashes).filter(Boolean).join("/");
|
|
177
196
|
return joined ? `${joined}/` : "";
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
function stripLanguagePrefix(value, config
|
|
199
|
+
function stripLanguagePrefix(value, config) {
|
|
181
200
|
const normalized = trimSlashes(value);
|
|
182
201
|
const key = getLanguageKeyFromPath(normalized, config);
|
|
183
202
|
if (!key) return normalized;
|
|
@@ -193,35 +212,12 @@ function localizePath(value, key, config = {}) {
|
|
|
193
212
|
|
|
194
213
|
const [pathAndQuery, hash = ""] = value.split("#");
|
|
195
214
|
const [pathname, query = ""] = pathAndQuery.split("?");
|
|
196
|
-
const route = stripLanguagePrefix(pathname, config);
|
|
215
|
+
const route = trimSlashes(stripLanguagePrefix(pathname, config));
|
|
197
216
|
const base = trimSlashes(getLanguageBasePath(config, key || getDefaultLanguageKey(config)));
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const hasFileExtension = /\.[^/]+$/.test(normalizedRoute);
|
|
217
|
+
const joined = [base, route].filter(Boolean).join("/");
|
|
218
|
+
const hasFileExtension = /\.[^/]+$/.test(route);
|
|
201
219
|
const localized = `/${joined}${joined && !hasFileExtension ? "/" : ""}`;
|
|
202
|
-
|
|
203
|
-
const hashPart = hash ? `#${hash}` : "";
|
|
204
|
-
return `${localized}${queryPart}${hashPart}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function toArray(collection) {
|
|
208
|
-
if (!collection) return [];
|
|
209
|
-
if (typeof collection.toArray === "function") return collection.toArray();
|
|
210
|
-
if (Array.isArray(collection.data)) return collection.data;
|
|
211
|
-
if (Array.isArray(collection)) return collection;
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function filterByLanguage(collection, key, config = {}) {
|
|
216
|
-
if (!isI18nEnabled(config)) return collection;
|
|
217
|
-
if (typeof collection?.filter === "function" && !Array.isArray(collection)) {
|
|
218
|
-
return collection.filter((item) => getPageLanguageKey(item, config) === key);
|
|
219
|
-
}
|
|
220
|
-
return toArray(collection).filter((item) => getPageLanguageKey(item, config) === key);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function getI18nKey(item = {}) {
|
|
224
|
-
return item.i18n_key || item.i18n?.key || item.translation_key || item.slug || inferI18nKeyFromSource(item.source);
|
|
220
|
+
return `${localized}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
function getLocalizedTagPath(tag, key, config = {}) {
|
|
@@ -237,44 +233,34 @@ function getLocalizedTagPath(tag, key, config = {}) {
|
|
|
237
233
|
return joinRoute(getLanguageBasePath(config, key), tagDir, slug);
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
|
|
241
|
-
if (typeof source !== "string") return "";
|
|
242
|
-
const normalized = trimSlashes(source).replace(/\\/g, "/");
|
|
243
|
-
const ext = path.posix.extname(normalized);
|
|
244
|
-
const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
|
|
245
|
-
const parts = withoutExt.split("/").filter(Boolean);
|
|
246
|
-
if (!parts.length) return "";
|
|
236
|
+
// ─── 集合按语言筛选 ────────────────────────────────────────────────
|
|
247
237
|
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
238
|
+
function filterByLanguage(collection, key, config = {}) {
|
|
239
|
+
if (!isI18nEnabled(config)) return collection;
|
|
240
|
+
// Hexo Query 对象:保留链式能力
|
|
241
|
+
if (typeof collection?.filter === "function" && !Array.isArray(collection)) {
|
|
242
|
+
return collection.filter((item) => getPageLanguageKey(item, config) === key);
|
|
251
243
|
}
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
// 普通数组 / 含 toArray 的对象
|
|
245
|
+
const arr = Array.isArray(collection) ? collection : typeof collection?.toArray === "function" ? collection.toArray() : [];
|
|
246
|
+
return arr.filter((item) => getPageLanguageKey(item, config) === key);
|
|
254
247
|
}
|
|
255
248
|
|
|
256
249
|
module.exports = {
|
|
257
250
|
filterByLanguage,
|
|
258
251
|
getDefaultLanguageKey,
|
|
259
|
-
getI18nConfig,
|
|
260
252
|
getI18nKey,
|
|
261
253
|
getLanguage,
|
|
262
254
|
getLanguageBasePath,
|
|
263
|
-
getLanguageKeyFromLocale,
|
|
264
|
-
getLanguageKeyFromPath,
|
|
265
|
-
getLanguageKeyFromSource,
|
|
266
255
|
getLanguageKeys,
|
|
267
|
-
getLanguageLabel,
|
|
268
256
|
getLocalizedTagPath,
|
|
269
257
|
getPageLanguageKey,
|
|
270
258
|
getPageLocale,
|
|
271
259
|
inferI18nKeyFromSource,
|
|
272
260
|
isExternalUrl,
|
|
273
261
|
isI18nEnabled,
|
|
274
|
-
joinRoute,
|
|
275
262
|
localizePath,
|
|
276
263
|
normalizeLocale,
|
|
277
|
-
|
|
278
|
-
toArray,
|
|
264
|
+
parseLocalizedSource,
|
|
279
265
|
trimSlashes,
|
|
280
266
|
};
|