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
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)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const pagination = require("hexo-pagination");
|
|
2
|
-
const { filterByLanguage, getLanguage, getLanguageBasePath, getLanguageKeys, isI18nEnabled } = require("../../util/i18n");
|
|
2
|
+
const { filterByLanguage, getDefaultLanguageKey, getLanguage, getLanguageBasePath, getLanguageKeys, isI18nEnabled } = require("../../util/i18n");
|
|
3
3
|
|
|
4
4
|
const fmtNum = (num) => num.toString().padStart(2, "0");
|
|
5
5
|
|
|
6
|
+
function redirectTo(path) {
|
|
7
|
+
const target = path.startsWith("/") ? path : `/${path}`;
|
|
8
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="robots" content="noindex"><meta http-equiv="refresh" content="0;url=${target}"><link rel="canonical" href="${target}"></head><body><a href="${target}">Continue</a></body></html>`;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
module.exports = (hexo) => {
|
|
7
12
|
hexo.extend.generator.register("archive", function (locals) {
|
|
8
13
|
const { config } = this;
|
|
@@ -44,6 +49,7 @@ module.exports = (hexo) => {
|
|
|
44
49
|
: {};
|
|
45
50
|
|
|
46
51
|
generate(baseArchiveDir, allPosts, languageData);
|
|
52
|
+
generate(languageBase || "", allPosts, languageData);
|
|
47
53
|
|
|
48
54
|
const yearly = themeConfig.yearly ?? true;
|
|
49
55
|
const monthly = themeConfig.monthly ?? true;
|
|
@@ -106,6 +112,13 @@ module.exports = (hexo) => {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
if (isI18nEnabled(fullConfig)) {
|
|
115
|
+
const defaultLanguageBase = getLanguageBasePath(fullConfig, getDefaultLanguageKey(fullConfig));
|
|
116
|
+
if (defaultLanguageBase) {
|
|
117
|
+
result.push({
|
|
118
|
+
path: "index.html",
|
|
119
|
+
data: redirectTo(defaultLanguageBase),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
109
122
|
getLanguageKeys(fullConfig).forEach((langKey) => generateLanguageArchives(langKey));
|
|
110
123
|
return result;
|
|
111
124
|
}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
getI18nKey,
|
|
3
|
+
getLanguage,
|
|
4
|
+
getLanguageBasePath,
|
|
5
|
+
getPageLanguageKey,
|
|
6
|
+
inferI18nKeyFromSource,
|
|
7
|
+
isI18nEnabled,
|
|
8
|
+
localizePath,
|
|
9
|
+
parseLocalizedSource,
|
|
10
|
+
trimSlashes,
|
|
11
|
+
} = require("../../util/i18n");
|
|
2
12
|
|
|
3
13
|
function getLocalizedPagePath(page, langKey, config) {
|
|
4
|
-
const sourcePrefix = `${trimSlashes(langKey)}/`;
|
|
5
14
|
let route = page.path || "";
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
const parsed = parseLocalizedSource(page.source || "");
|
|
17
|
+
if (parsed.langKey) {
|
|
18
|
+
const suffix = `__${parsed.langKey}`;
|
|
19
|
+
const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
// Strip the __<lang> suffix when followed by '/', '.', or end of string
|
|
21
|
+
// e.g. "about__en/index.html" → "about/index.html"; "about__en.html" → "about.html"
|
|
22
|
+
route = route.replace(new RegExp(`${escapedSuffix}(?=[/.]|$)`), "");
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
return trimSlashes(localizePath(`/${route}`, langKey, config));
|
|
@@ -6,7 +6,7 @@ module.exports = (hexo) => {
|
|
|
6
6
|
const config = this.config;
|
|
7
7
|
const fullConfig = Object.assign({}, config, config.theme_config, hexo.theme.config);
|
|
8
8
|
const themeConfig = hexo.theme.config.tag_generator || {};
|
|
9
|
-
const perPage = themeConfig.per_page ??
|
|
9
|
+
const perPage = themeConfig.per_page ?? 0;
|
|
10
10
|
const orderBy = themeConfig.order_by ?? "-date";
|
|
11
11
|
const paginationDir = config.pagination_dir || "page";
|
|
12
12
|
const tags = locals.tags;
|
package/include/hexo/helper.js
CHANGED
|
@@ -14,7 +14,6 @@ function getCDN(cdn, pkg, version, filename) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const {
|
|
17
|
-
getLanguageLabel,
|
|
18
17
|
getPageLanguageKey,
|
|
19
18
|
getPageLocale,
|
|
20
19
|
getLocalizedTagPath,
|
|
@@ -40,9 +39,6 @@ module.exports = (hexo) => {
|
|
|
40
39
|
hexo.extend.helper.register("language_locale", function (page = null) {
|
|
41
40
|
return getPageLocale(page || this.page, this.config);
|
|
42
41
|
});
|
|
43
|
-
hexo.extend.helper.register("language_label", function (keyOrLocale) {
|
|
44
|
-
return getLanguageLabel(this.config, keyOrLocale);
|
|
45
|
-
});
|
|
46
42
|
hexo.extend.helper.register("localized_path", function (targetPath, langKey = null) {
|
|
47
43
|
return localizePath(targetPath, langKey || getPageLanguageKey(this.page, this.config), this.config);
|
|
48
44
|
});
|
package/include/hexo/i18n.js
CHANGED
|
@@ -1,144 +1,44 @@
|
|
|
1
|
-
const { extname } = require("node:path");
|
|
2
|
-
const createPostProcessor = require("hexo/dist/plugins/processor/post");
|
|
3
|
-
const { isHiddenFile, isMatch, isTmpFile } = require("hexo/dist/plugins/processor/common");
|
|
4
1
|
const {
|
|
5
2
|
getI18nKey,
|
|
6
|
-
getLanguageBasePath,
|
|
7
3
|
getLanguage,
|
|
8
|
-
|
|
4
|
+
getLanguageBasePath,
|
|
9
5
|
getPageLanguageKey,
|
|
10
6
|
inferI18nKeyFromSource,
|
|
11
7
|
isI18nEnabled,
|
|
12
|
-
|
|
13
|
-
trimSlashes,
|
|
8
|
+
parseLocalizedSource,
|
|
14
9
|
} = require("../util/i18n");
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getLocalizedPostParams(hexo, sourcePath) {
|
|
21
|
-
const config = getConfig(hexo);
|
|
22
|
-
if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
|
|
23
|
-
|
|
24
|
-
const match = sourcePath.replace(/\\/g, "/").match(/^([^/]+)\/(_posts|_drafts)\/(.+)$/);
|
|
25
|
-
if (!match) return null;
|
|
26
|
-
|
|
27
|
-
const langKey = match[1];
|
|
28
|
-
if (!getLanguageKeys(config).includes(langKey)) return null;
|
|
29
|
-
|
|
30
|
-
const postPath = match[3];
|
|
31
|
-
if (isHiddenFile(postPath)) return null;
|
|
32
|
-
|
|
33
|
-
let renderable = hexo.render.isRenderable(sourcePath) && !isMatch(sourcePath, hexo.config.skip_render);
|
|
34
|
-
if (renderable && hexo.config.post_asset_folder) {
|
|
35
|
-
renderable = extname(hexo.config.new_post_name) === extname(sourcePath);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
i18n_lang: langKey,
|
|
40
|
-
path: postPath,
|
|
41
|
-
published: match[2] === "_posts",
|
|
42
|
-
renderable,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getLocalizedPageParams(hexo, sourcePath) {
|
|
47
|
-
const config = getConfig(hexo);
|
|
48
|
-
if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
|
|
49
|
-
|
|
50
|
-
const normalized = sourcePath.replace(/\\/g, "/");
|
|
51
|
-
const match = normalized.match(/^([^/]+)\/(.+)$/);
|
|
52
|
-
if (!match) return null;
|
|
53
|
-
|
|
54
|
-
const langKey = match[1];
|
|
55
|
-
if (!getLanguageKeys(config).includes(langKey)) return null;
|
|
11
|
+
let cachedConfig = null;
|
|
12
|
+
let cachedHexo = null;
|
|
56
13
|
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!renderable) return null;
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
i18n_lang: langKey,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildPostI18nUpdate(post, langKey, config) {
|
|
69
|
-
const language = getLanguage(config, langKey);
|
|
70
|
-
const update = {
|
|
71
|
-
i18n_path: getLanguageBasePath(config, langKey),
|
|
72
|
-
i18n_lang: langKey,
|
|
73
|
-
lang: language.locale,
|
|
74
|
-
language: language.locale,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (!post.i18n_key) {
|
|
78
|
-
update.i18n_key = getI18nKey(post) || inferI18nKeyFromSource(post.source);
|
|
14
|
+
function getConfig(hexo, locals = {}) {
|
|
15
|
+
if (hexo !== cachedHexo || !cachedConfig) {
|
|
16
|
+
cachedHexo = hexo;
|
|
17
|
+
cachedConfig = Object.assign({}, hexo.config, hexo.config.theme_config, hexo.theme.config);
|
|
79
18
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getLocalizedPagePath(page, langKey, config) {
|
|
85
|
-
const sourcePrefix = `${trimSlashes(langKey)}/`;
|
|
86
|
-
let route = page.path || "";
|
|
87
|
-
|
|
88
|
-
if (route.startsWith(sourcePrefix)) {
|
|
89
|
-
route = route.slice(sourcePrefix.length);
|
|
19
|
+
if (locals.theme) {
|
|
20
|
+
return Object.assign({}, cachedConfig, locals.theme);
|
|
90
21
|
}
|
|
91
|
-
|
|
92
|
-
return trimSlashes(localizePath(`/${route}`, langKey, config));
|
|
22
|
+
return cachedConfig;
|
|
93
23
|
}
|
|
94
24
|
|
|
95
|
-
function
|
|
25
|
+
function applyI18nFields(item, config) {
|
|
26
|
+
const langKey = getPageLanguageKey(item, config);
|
|
96
27
|
const language = getLanguage(config, langKey);
|
|
97
|
-
const update = {
|
|
98
|
-
i18n_path: getLanguageBasePath(config, langKey),
|
|
99
|
-
i18n_lang: langKey,
|
|
100
|
-
lang: language.locale,
|
|
101
|
-
language: language.locale,
|
|
102
|
-
path: getLocalizedPagePath(page, langKey, config),
|
|
103
|
-
};
|
|
104
28
|
|
|
105
|
-
|
|
106
|
-
|
|
29
|
+
item.i18n_lang = item.i18n_lang || langKey;
|
|
30
|
+
item.i18n_path = getLanguageBasePath(config, langKey);
|
|
31
|
+
item.lang = language.locale;
|
|
32
|
+
item.language = language.locale;
|
|
33
|
+
|
|
34
|
+
if (!item.i18n_key) {
|
|
35
|
+
item.i18n_key = getI18nKey(item) || inferI18nKeyFromSource(item.source);
|
|
107
36
|
}
|
|
108
37
|
|
|
109
|
-
return
|
|
38
|
+
return langKey;
|
|
110
39
|
}
|
|
111
40
|
|
|
112
41
|
module.exports = (hexo) => {
|
|
113
|
-
const defaultPostProcessor = createPostProcessor(hexo);
|
|
114
|
-
|
|
115
|
-
hexo.extend.processor.register(
|
|
116
|
-
(sourcePath) => getLocalizedPostParams(hexo, sourcePath),
|
|
117
|
-
(file) =>
|
|
118
|
-
Promise.resolve(defaultPostProcessor.process(file)).then(() => {
|
|
119
|
-
if (!isI18nEnabled(getConfig(hexo))) return null;
|
|
120
|
-
if (!file.params.renderable) return null;
|
|
121
|
-
|
|
122
|
-
const post = hexo.model("Post").findOne({ source: file.path });
|
|
123
|
-
if (!post) return null;
|
|
124
|
-
|
|
125
|
-
return post.update(buildPostI18nUpdate(post, file.params.i18n_lang, getConfig(hexo)));
|
|
126
|
-
}),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
hexo.extend.processor.register(
|
|
130
|
-
(sourcePath) => getLocalizedPageParams(hexo, sourcePath),
|
|
131
|
-
(file) => {
|
|
132
|
-
if (!isI18nEnabled(getConfig(hexo))) return null;
|
|
133
|
-
if (file.type === "delete") return null;
|
|
134
|
-
|
|
135
|
-
const page = hexo.model("Page").findOne({ source: file.path });
|
|
136
|
-
if (!page) return null;
|
|
137
|
-
|
|
138
|
-
return page.update(buildPageI18nUpdate(page, file.params.i18n_lang, getConfig(hexo)));
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
|
|
142
42
|
hexo.extend.filter.register(
|
|
143
43
|
"post_permalink",
|
|
144
44
|
(post) => {
|
|
@@ -146,14 +46,16 @@ module.exports = (hexo) => {
|
|
|
146
46
|
const activeConfig = getConfig(hexo);
|
|
147
47
|
if (!isI18nEnabled(activeConfig)) return post;
|
|
148
48
|
|
|
149
|
-
|
|
150
|
-
const language = getLanguage(activeConfig, langKey);
|
|
49
|
+
applyI18nFields(post, activeConfig);
|
|
151
50
|
|
|
152
|
-
|
|
153
|
-
post.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
51
|
+
// 让 permalink 模板里的 :name 取到 baseName 而非 foo__en
|
|
52
|
+
if (post.source) {
|
|
53
|
+
const parsed = parseLocalizedSource(post.source);
|
|
54
|
+
if (parsed.langKey) {
|
|
55
|
+
const baseName = inferI18nKeyFromSource(post.source);
|
|
56
|
+
if (baseName) post.slug = baseName;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
157
59
|
|
|
158
60
|
return post;
|
|
159
61
|
},
|
|
@@ -167,14 +69,7 @@ module.exports = (hexo) => {
|
|
|
167
69
|
const activeConfig = getConfig(hexo, locals);
|
|
168
70
|
if (!page || !isI18nEnabled(activeConfig)) return locals;
|
|
169
71
|
|
|
170
|
-
|
|
171
|
-
const language = getLanguage(activeConfig, langKey);
|
|
172
|
-
|
|
173
|
-
page.i18n_lang = page.i18n_lang || langKey;
|
|
174
|
-
page.i18n_path = getLanguageBasePath(activeConfig, langKey);
|
|
175
|
-
page.lang = language.locale;
|
|
176
|
-
page.language = language.locale;
|
|
177
|
-
page.i18n_key = page.i18n_key || getI18nKey(page) || inferI18nKeyFromSource(page.source);
|
|
72
|
+
applyI18nFields(page, activeConfig);
|
|
178
73
|
|
|
179
74
|
return locals;
|
|
180
75
|
},
|
|
@@ -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("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
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;
|
package/include/hexo/renderer.js
CHANGED
|
@@ -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("
|
|
6
|
-
const
|
|
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
|
-
|
|
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), {
|