hexo-theme-gnix 6.2.0 → 8.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 +6 -2
- package/include/hexo/encrypt.js +42 -0
- package/include/hexo/feed.js +329 -0
- package/include/util/common.js +7 -9
- package/languages/en.yml +6 -3
- package/languages/zh-CN.yml +6 -3
- package/layout/archive.jsx +86 -65
- package/layout/comment/twikoo.jsx +2 -11
- package/layout/comment/waline.jsx +2 -2
- package/layout/common/article.jsx +5 -8
- package/layout/common/article_cover.jsx +11 -1
- package/layout/common/article_media.jsx +2 -4
- package/layout/common/footer.jsx +11 -31
- package/layout/common/head.jsx +6 -14
- package/layout/common/navbar.jsx +4 -4
- package/layout/common/scripts.jsx +6 -6
- package/layout/common/theme_selector.jsx +5 -6
- package/layout/common/toc.jsx +8 -14
- package/layout/index.jsx +2 -4
- package/layout/misc/article_licensing.jsx +4 -2
- package/layout/misc/open_graph.jsx +4 -4
- package/layout/misc/paginator.jsx +10 -4
- package/layout/misc/structured_data.jsx +3 -4
- package/layout/plugin/busuanzi.jsx +1 -1
- package/layout/plugin/cookie_consent.jsx +40 -31
- package/layout/plugin/swup.jsx +2 -22
- package/layout/search/insight.jsx +16 -3
- package/package.json +12 -8
- package/scripts/hot-reload.js +92 -0
- package/scripts/index.js +2 -0
- package/source/css/archive.css +251 -0
- package/source/css/default.css +250 -284
- package/source/css/encrypt.css +55 -0
- package/source/css/responsive/desktop.css +0 -119
- package/source/css/responsive/mobile.css +7 -23
- package/source/css/responsive/touch.css +9 -103
- package/source/css/shiki/shiki.css +7 -22
- package/source/css/twikoo.css +290 -830
- package/source/img/og_image.webp +0 -0
- package/source/js/archive-breadcrumb.js +132 -0
- package/source/js/busuanzi.js +1 -12
- package/source/js/components/accordion.js +192 -0
- package/source/js/components/chat.js +239 -0
- package/source/js/components/device-carousel.js +260 -0
- package/source/js/components/image-carousel.js +410 -0
- package/source/js/components/text-image-section.js +180 -0
- package/source/js/components/theme-stacked.js +526 -0
- package/source/js/components/tree.js +437 -0
- package/source/js/decrypt.js +112 -0
- package/source/js/insight.js +75 -65
- package/source/js/main.js +192 -99
- package/source/js/mdit/mermaid.js +12 -4
- package/source/js/swup.bundle.js +1 -0
- package/source/js/theme-selector.js +94 -113
- package/source/img/og_image.png +0 -0
- package/source/js/host/swup/Swup.umd.min.js +0 -1
- package/source/js/host/swup/head-plugin.umd.min.js +0 -1
- package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
- package/source/js/mdit/shiki.js +0 -158
package/README.md
CHANGED
|
@@ -54,9 +54,13 @@ bun i hexo-renderer-markdown-exit
|
|
|
54
54
|
## Links
|
|
55
55
|
|
|
56
56
|
- Change log: http://vluv.space/change
|
|
57
|
-
- Live Preview: http://vluv.space/test_markdown/
|
|
57
|
+
- Live Preview for Markdown: http://vluv.space/test_markdown/
|
|
58
|
+
- Live Preview for Components: https://vluv.space/test_components/
|
|
59
|
+
- Live Preview for Flavoured Markdown & HTML Element: https://vluv.space/test_flavored_md/
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
## Credit
|
|
61
63
|
|
|
62
|
-
[ppoffice/hexo-theme-icarus: A simple, delicate, and modern theme for the static site generator Hexo.](https://github.com/ppoffice/hexo-theme-icarus)
|
|
64
|
+
- [ppoffice/hexo-theme-icarus: A simple, delicate, and modern theme for the static site generator Hexo.](https://github.com/ppoffice/hexo-theme-icarus)
|
|
65
|
+
- [D0n9X1n/hexo-blog-encrypt: Yet, just another hexo plugin for security.](https://github.com/D0n9X1n/hexo-blog-encrypt)
|
|
66
|
+
- [hexojs/hexo-generator-feed: Feed generator for Hexo.](https://github.com/hexojs/hexo-generator-feed)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
const ITERATIONS = 100_000;
|
|
4
|
+
|
|
5
|
+
module.exports = (hexo) => {
|
|
6
|
+
hexo.extend.filter.register(
|
|
7
|
+
"after_post_render",
|
|
8
|
+
(data) => {
|
|
9
|
+
const password = data.password;
|
|
10
|
+
if (!password && password !== 0) return data;
|
|
11
|
+
|
|
12
|
+
const passphrase = String(password);
|
|
13
|
+
const __ = hexo.theme.i18n.__(hexo.config.language);
|
|
14
|
+
|
|
15
|
+
const salt = crypto.randomBytes(16);
|
|
16
|
+
const iv = crypto.randomBytes(12);
|
|
17
|
+
const key = crypto.pbkdf2Sync(passphrase, salt, ITERATIONS, 32, "sha256");
|
|
18
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
19
|
+
const encrypted = Buffer.concat([cipher.update(data.content, "utf8"), cipher.final()]);
|
|
20
|
+
const tag = cipher.getAuthTag();
|
|
21
|
+
const base64 = Buffer.concat([salt, iv, encrypted, tag]).toString("base64");
|
|
22
|
+
|
|
23
|
+
data.content = `
|
|
24
|
+
<div class="encrypted-content" id="encrypted-article">
|
|
25
|
+
<div class="encrypted-data" style="display:none">${base64}</div>
|
|
26
|
+
<form class="encrypt-form" id="encrypt-form">
|
|
27
|
+
<p class="encrypt-message">${__("encrypt.message")}</p>
|
|
28
|
+
<div class="encrypt-input-wrap">
|
|
29
|
+
<input type="password" id="encrypt-pass" placeholder="${__("encrypt.placeholder")}" enterkeyhint="done" autocomplete="off" />
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
</div>`;
|
|
33
|
+
|
|
34
|
+
data.excerpt = `<p>${__("encrypt.abstract")}</p>`;
|
|
35
|
+
data.more = "";
|
|
36
|
+
data.encrypt = true;
|
|
37
|
+
|
|
38
|
+
return data;
|
|
39
|
+
},
|
|
40
|
+
1000,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const { extname } = require("node:path");
|
|
2
|
+
const { generateRssFeed, generateAtomFeed } = require("feedsmith");
|
|
3
|
+
const { gravatar, full_url_for, encodeURL, url_for } = require("hexo-util");
|
|
4
|
+
|
|
5
|
+
const VALID_FEED_TYPES = new Set(["atom", "rss2"]);
|
|
6
|
+
const EXISTING_FEED_LINK_RE = /type=['|"]?application\/(atom|rss)\+xml['|"]?/i;
|
|
7
|
+
const HEAD_RE = /<head>(?!<\/head>).+?<\/head>/s;
|
|
8
|
+
|
|
9
|
+
function stripControlChars(value) {
|
|
10
|
+
let sanitizedParts;
|
|
11
|
+
let start = 0;
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
14
|
+
const code = value.charCodeAt(index);
|
|
15
|
+
if (code < 0x20 || code === 0x7f) {
|
|
16
|
+
sanitizedParts ||= [];
|
|
17
|
+
if (start < index) {
|
|
18
|
+
sanitizedParts.push(value.slice(start, index));
|
|
19
|
+
}
|
|
20
|
+
start = index + 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!sanitizedParts) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (start < value.length) {
|
|
29
|
+
sanitizedParts.push(value.slice(start));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return sanitizedParts.join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = (hexo) => {
|
|
36
|
+
hexo.config.feed = Object.assign(
|
|
37
|
+
{
|
|
38
|
+
enable: true,
|
|
39
|
+
type: "atom",
|
|
40
|
+
limit: 20,
|
|
41
|
+
hub: "",
|
|
42
|
+
content: true,
|
|
43
|
+
content_limit: 140,
|
|
44
|
+
content_limit_delim: "",
|
|
45
|
+
order_by: "-date",
|
|
46
|
+
autodiscovery: true,
|
|
47
|
+
},
|
|
48
|
+
hexo.config.feed,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const config = hexo.config.feed;
|
|
52
|
+
|
|
53
|
+
if (!config.enable) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let type = config.type;
|
|
58
|
+
let path = config.path;
|
|
59
|
+
|
|
60
|
+
if (!type || (typeof type !== "string" && !Array.isArray(type))) {
|
|
61
|
+
type = "atom";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(type)) {
|
|
65
|
+
type = [...new Set(type.filter((item) => VALID_FEED_TYPES.has(item)))];
|
|
66
|
+
if (type.length === 0) {
|
|
67
|
+
type = "atom";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof type === "string") {
|
|
72
|
+
if (!VALID_FEED_TYPES.has(type)) type = "atom";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!path || typeof path !== typeof type) {
|
|
76
|
+
if (typeof type === "string") path = type.concat(".xml");
|
|
77
|
+
else path = type.map((str) => str.concat(".xml"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(path)) {
|
|
81
|
+
if (path.length !== type.length) {
|
|
82
|
+
if (path.length > type.length) path = path.slice(0, type.length);
|
|
83
|
+
else if (path.length === 0) path = type.map((str) => str.concat(".xml"));
|
|
84
|
+
else path.push(type[1].concat(".xml"));
|
|
85
|
+
}
|
|
86
|
+
path = path.map((str) => {
|
|
87
|
+
if (!extname(str)) return str.concat(".xml");
|
|
88
|
+
return str;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof path === "string") {
|
|
93
|
+
if (!extname(path)) path += ".xml";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
config.type = type;
|
|
97
|
+
config.path = path;
|
|
98
|
+
const feedEntries = typeof type === "string" ? [{ type, path }] : type.map((feedType, index) => ({ type: feedType, path: path[index] }));
|
|
99
|
+
|
|
100
|
+
function composePosts(posts, feedConfig) {
|
|
101
|
+
const { limit, order_by } = feedConfig;
|
|
102
|
+
let processedPosts = posts.sort(order_by || "-date");
|
|
103
|
+
processedPosts = processedPosts.filter((post) => post.draft !== true);
|
|
104
|
+
if (limit) processedPosts = processedPosts.limit(limit);
|
|
105
|
+
return processedPosts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function composeFeed(feedConfig, urlConfig, email, feedPath, context, posts) {
|
|
109
|
+
const { icon: iconConfig, hub } = feedConfig;
|
|
110
|
+
const latestPost = posts.first();
|
|
111
|
+
let url = urlConfig;
|
|
112
|
+
if (url[url.length - 1] !== "/") url += "/";
|
|
113
|
+
|
|
114
|
+
let icon = "";
|
|
115
|
+
if (iconConfig) icon = full_url_for.call(context, iconConfig);
|
|
116
|
+
else if (email) icon = gravatar(email);
|
|
117
|
+
|
|
118
|
+
const feedUrl = full_url_for.call(context, feedPath);
|
|
119
|
+
const currentYear = new Date().getFullYear();
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
title: context.title,
|
|
123
|
+
description: context.subtitle || context.description,
|
|
124
|
+
url,
|
|
125
|
+
feedUrl,
|
|
126
|
+
icon,
|
|
127
|
+
hub,
|
|
128
|
+
language: context.language,
|
|
129
|
+
author: { name: context.author, email: context.email },
|
|
130
|
+
copyright: context.author && `All rights reserved ${currentYear}, ${context.author}`,
|
|
131
|
+
updated: latestPost.updated ? latestPost.updated.toDate() : latestPost.date.toDate(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function composeFeedLinks(feedUrl, hub, feedType) {
|
|
136
|
+
const links = [{ href: encodeURL(feedUrl), rel: "self", type: feedType }];
|
|
137
|
+
if (hub) links.push({ href: encodeURL(hub), rel: "hub" });
|
|
138
|
+
return links;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function composeItemDescription(post, feedConfig) {
|
|
142
|
+
const { content_limit, content_limit_delim } = feedConfig;
|
|
143
|
+
if (post.description) return post.description;
|
|
144
|
+
if (post.intro) return post.intro;
|
|
145
|
+
if (post.excerpt) return post.excerpt;
|
|
146
|
+
if (post.content) {
|
|
147
|
+
const shortContent = post.content.substring(0, content_limit || 140);
|
|
148
|
+
if (content_limit_delim) {
|
|
149
|
+
const delimPos = shortContent.lastIndexOf(content_limit_delim);
|
|
150
|
+
if (delimPos > -1) {
|
|
151
|
+
return shortContent.substring(0, delimPos);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return shortContent;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function composeItemContent(post, feedConfig) {
|
|
161
|
+
const { content } = feedConfig;
|
|
162
|
+
if (content && post.content) {
|
|
163
|
+
return stripControlChars(post.content);
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function composeItemCategories(post) {
|
|
169
|
+
const categories = [];
|
|
170
|
+
|
|
171
|
+
if (post.categories) {
|
|
172
|
+
for (const item of post.categories.toArray()) {
|
|
173
|
+
categories.push({ name: item.name, domain: item.permalink });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (post.tags) {
|
|
178
|
+
for (const item of post.tags.toArray()) {
|
|
179
|
+
categories.push({ name: item.name, domain: item.permalink });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return categories;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function composeItem(post, feedConfig, context) {
|
|
187
|
+
const published = post.date.toDate();
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
title: post.title,
|
|
191
|
+
link: encodeURL(full_url_for.call(context, post.permalink)),
|
|
192
|
+
description: composeItemDescription(post, feedConfig),
|
|
193
|
+
published,
|
|
194
|
+
updated: post.updated ? post.updated.toDate() : published,
|
|
195
|
+
content: composeItemContent(post, feedConfig),
|
|
196
|
+
enclosures: post.image && [{ url: full_url_for.call(context, post.image) }],
|
|
197
|
+
categories: composeItemCategories(post),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function composeRssItem(feed, item) {
|
|
202
|
+
return {
|
|
203
|
+
title: item.title,
|
|
204
|
+
link: item.link,
|
|
205
|
+
guid: item.link,
|
|
206
|
+
description: item.description,
|
|
207
|
+
pubDate: item.published,
|
|
208
|
+
authors: [feed.author],
|
|
209
|
+
content: { encoded: item.content },
|
|
210
|
+
enclosures: item.enclosures,
|
|
211
|
+
categories: item.categories,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function composeAtomEntry(feed, item) {
|
|
216
|
+
const entryLinks = [{ href: item.link }, ...(item.enclosures || []).map((enclosure) => ({ href: enclosure.url, rel: "enclosure" }))];
|
|
217
|
+
return {
|
|
218
|
+
title: item.title,
|
|
219
|
+
id: item.link,
|
|
220
|
+
links: entryLinks,
|
|
221
|
+
summary: item.description,
|
|
222
|
+
content: item.content,
|
|
223
|
+
published: item.published,
|
|
224
|
+
updated: item.updated || item.published,
|
|
225
|
+
authors: feed.author.name && [feed.author],
|
|
226
|
+
categories: item.categories.map((cat) => ({ term: cat.name, scheme: cat.domain })),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function generateRss(feed, items) {
|
|
231
|
+
const links = composeFeedLinks(feed.feedUrl, feed.hub, "application/rss+xml");
|
|
232
|
+
const siteUrl = encodeURL(feed.url);
|
|
233
|
+
|
|
234
|
+
return generateRssFeed(
|
|
235
|
+
{
|
|
236
|
+
title: feed.title,
|
|
237
|
+
description: feed.description,
|
|
238
|
+
link: siteUrl,
|
|
239
|
+
language: feed.language,
|
|
240
|
+
copyright: feed.copyright,
|
|
241
|
+
generator: "Hexo",
|
|
242
|
+
lastBuildDate: feed.updated,
|
|
243
|
+
image: feed.icon && {
|
|
244
|
+
url: feed.icon,
|
|
245
|
+
title: feed.title,
|
|
246
|
+
link: siteUrl,
|
|
247
|
+
},
|
|
248
|
+
atom: { links },
|
|
249
|
+
items: items.map((item) => composeRssItem(feed, item)),
|
|
250
|
+
},
|
|
251
|
+
{ lenient: true },
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateAtom(feed, items) {
|
|
256
|
+
const siteUrl = encodeURL(feed.url);
|
|
257
|
+
const links = [{ href: siteUrl, rel: "alternate" }, ...composeFeedLinks(feed.feedUrl, feed.hub)];
|
|
258
|
+
|
|
259
|
+
return generateAtomFeed(
|
|
260
|
+
{
|
|
261
|
+
title: feed.title,
|
|
262
|
+
id: siteUrl,
|
|
263
|
+
subtitle: feed.description,
|
|
264
|
+
updated: feed.updated,
|
|
265
|
+
links,
|
|
266
|
+
generator: { text: "Hexo", uri: "https://hexo.io/" },
|
|
267
|
+
icon: feed.icon,
|
|
268
|
+
rights: feed.copyright,
|
|
269
|
+
authors: feed.author.name && [feed.author],
|
|
270
|
+
entries: items.map((item) => composeAtomEntry(feed, item)),
|
|
271
|
+
language: feed.language,
|
|
272
|
+
},
|
|
273
|
+
{ lenient: true },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createFeedGenerator(feedType, feedPath) {
|
|
278
|
+
return function (locals) {
|
|
279
|
+
const { config } = this;
|
|
280
|
+
const { feed: feedConfig } = config;
|
|
281
|
+
const posts = composePosts(locals.posts, feedConfig);
|
|
282
|
+
|
|
283
|
+
if (posts.length <= 0) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const feed = composeFeed(feedConfig, config.url, config.email, feedPath, this, posts);
|
|
288
|
+
const items = posts.toArray().map((post) => composeItem(post, feedConfig, this));
|
|
289
|
+
|
|
290
|
+
let data;
|
|
291
|
+
switch (feedType) {
|
|
292
|
+
case "rss2":
|
|
293
|
+
data = generateRss(feed, items);
|
|
294
|
+
break;
|
|
295
|
+
default:
|
|
296
|
+
data = generateAtom(feed, items);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
path: feedPath,
|
|
301
|
+
data,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (typeof type === "string") {
|
|
307
|
+
hexo.extend.generator.register(type, createFeedGenerator(type, path));
|
|
308
|
+
} else {
|
|
309
|
+
for (const entry of feedEntries) {
|
|
310
|
+
hexo.extend.generator.register(entry.type, createFeedGenerator(entry.type, entry.path));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof config.autodiscovery !== "boolean") config.autodiscovery = true;
|
|
315
|
+
|
|
316
|
+
if (config.autodiscovery === true) {
|
|
317
|
+
hexo.extend.filter.register("after_render:html", function (data) {
|
|
318
|
+
if (EXISTING_FEED_LINK_RE.test(data) || hexo.config.feed.autodiscovery === false) {
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const autodiscoveryTag = feedEntries
|
|
323
|
+
.map((entry) => `<link rel="alternate" href="${url_for.call(this, entry.path)}" title="${hexo.config.title}" type="application/${entry.type.replace(/2$/, "")}+xml">\n`)
|
|
324
|
+
.join("");
|
|
325
|
+
|
|
326
|
+
return data.replace(HEAD_RE, (str) => str.replace("</head>", `${autodiscoveryTag}</head>`));
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
};
|
package/include/util/common.js
CHANGED
|
@@ -26,20 +26,11 @@ function cacheComponent(type, prefix, transform) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function lazy_load_css(href) {
|
|
30
|
-
script_str = `var link = document.createElement('link');
|
|
31
|
-
link.rel = 'stylesheet';
|
|
32
|
-
link.href = '${href}';
|
|
33
|
-
document.getElementsByTagName('head')[0].appendChild(link);`;
|
|
34
|
-
return script_str;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
module.exports = {
|
|
38
30
|
// 导出常用的依赖
|
|
39
31
|
Component,
|
|
40
32
|
Fragment,
|
|
41
33
|
view,
|
|
42
|
-
lazy_load_css,
|
|
43
34
|
cacheComponent,
|
|
44
35
|
|
|
45
36
|
// 通用的组件加载函数
|
|
@@ -51,4 +42,11 @@ module.exports = {
|
|
|
51
42
|
return fallback;
|
|
52
43
|
}
|
|
53
44
|
},
|
|
45
|
+
|
|
46
|
+
isValidDate: (val) => val instanceof Date && !Number.isNaN(val.getTime()),
|
|
47
|
+
parseISO: (str) => new Date(str),
|
|
48
|
+
dateFormatters: {
|
|
49
|
+
shortDay: new Intl.DateTimeFormat("en", { month: "short", day: "2-digit" }),
|
|
50
|
+
longMonth: new Intl.DateTimeFormat("en", { month: "long" }),
|
|
51
|
+
},
|
|
54
52
|
};
|
package/languages/en.yml
CHANGED
|
@@ -14,8 +14,6 @@ common:
|
|
|
14
14
|
page:
|
|
15
15
|
one: "Page"
|
|
16
16
|
other: "Pages"
|
|
17
|
-
prev: "Previous"
|
|
18
|
-
next: "Next"
|
|
19
17
|
article:
|
|
20
18
|
created_at: "Posted %s"
|
|
21
19
|
more: "Read more"
|
|
@@ -41,7 +39,12 @@ plugin:
|
|
|
41
39
|
policy: Cookie Policy
|
|
42
40
|
search:
|
|
43
41
|
search: "Search"
|
|
44
|
-
hint: "Search
|
|
42
|
+
hint: "Search Something..."
|
|
45
43
|
no_result: "No results for"
|
|
46
44
|
untitled: "(Untitled)"
|
|
47
45
|
empty_preview: "(No preview)"
|
|
46
|
+
encrypt:
|
|
47
|
+
message: "This article is encrypted, please enter the password to view"
|
|
48
|
+
placeholder: "Enter password..."
|
|
49
|
+
wrong_pass: "Wrong password, please try again"
|
|
50
|
+
abstract: "This article is encrypted"
|
package/languages/zh-CN.yml
CHANGED
|
@@ -14,8 +14,6 @@ common:
|
|
|
14
14
|
page:
|
|
15
15
|
one: "PAGE"
|
|
16
16
|
other: "PAGE"
|
|
17
|
-
prev: "Previous"
|
|
18
|
-
next: "Next"
|
|
19
17
|
article:
|
|
20
18
|
created_at: "%s"
|
|
21
19
|
more: "Read"
|
|
@@ -41,7 +39,12 @@ plugin:
|
|
|
41
39
|
policy: Cookie政策
|
|
42
40
|
search:
|
|
43
41
|
search: "搜索"
|
|
44
|
-
hint: "Search something...
|
|
42
|
+
hint: "Search something..."
|
|
45
43
|
no_result: "未找到搜索结果"
|
|
46
44
|
untitled: "(无标题)"
|
|
47
45
|
empty_preview: "(无内容预览)"
|
|
46
|
+
encrypt:
|
|
47
|
+
message: "此文章已加密,请输入密码查看"
|
|
48
|
+
placeholder: "输入密码..."
|
|
49
|
+
wrong_pass: "密码错误,请重试"
|
|
50
|
+
abstract: "此文章已加密"
|
package/layout/archive.jsx
CHANGED
|
@@ -1,71 +1,11 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { Component, Fragment } = require("../include/util/common");
|
|
1
|
+
const { Component, Fragment, isValidDate, parseISO, dateFormatters } = require("../include/util/common");
|
|
3
2
|
const ArticleMedia = require("./common/article_media");
|
|
4
3
|
|
|
5
4
|
module.exports = class extends Component {
|
|
6
5
|
render() {
|
|
7
|
-
const { page, helper } = this.props;
|
|
6
|
+
const { page, helper, site, config } = this.props;
|
|
8
7
|
const { url_for, date_xml, date } = helper;
|
|
9
8
|
|
|
10
|
-
const inlineCSS = `
|
|
11
|
-
.winter {
|
|
12
|
-
--accent: var(--blue);
|
|
13
|
-
}
|
|
14
|
-
.autumn {
|
|
15
|
-
--accent: var(--red);
|
|
16
|
-
}
|
|
17
|
-
.summer {
|
|
18
|
-
--accent: var(--green);
|
|
19
|
-
}
|
|
20
|
-
.spring {
|
|
21
|
-
--accent: var(--peach);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.article-meta {
|
|
25
|
-
font-size: 0.8rem;
|
|
26
|
-
color: var(--subtext1);
|
|
27
|
-
font-family: var(--font-handwriting);
|
|
28
|
-
}
|
|
29
|
-
a.archive-title {
|
|
30
|
-
font-family: var(--font-sans-serif);
|
|
31
|
-
font-weight: 400;
|
|
32
|
-
color: var(--text);
|
|
33
|
-
|
|
34
|
-
&:hover {
|
|
35
|
-
color: var(--accent);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
span.year {
|
|
40
|
-
position: absolute;
|
|
41
|
-
top: 1.5rem;
|
|
42
|
-
right: 1.5rem;
|
|
43
|
-
z-index: 0;
|
|
44
|
-
font-weight: bolder;
|
|
45
|
-
font-family: var(--font-handwriting);
|
|
46
|
-
font-size: 4em;
|
|
47
|
-
font-style: italic;
|
|
48
|
-
color: hsl(from var(--accent) h s l / 0.3);
|
|
49
|
-
line-height: 1;
|
|
50
|
-
user-select: none;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.archive-item {
|
|
54
|
-
display: flex;
|
|
55
|
-
text-align: left;
|
|
56
|
-
align-items: flex-start;
|
|
57
|
-
}
|
|
58
|
-
.archive-item > div {
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: baseline; /* 时间和标题的文字基线对齐 */
|
|
61
|
-
gap: 0.75rem;
|
|
62
|
-
}
|
|
63
|
-
.archive-item + .archive-item {
|
|
64
|
-
border: none;
|
|
65
|
-
margin-top: 0;
|
|
66
|
-
padding-top: 1em;
|
|
67
|
-
}
|
|
68
|
-
`;
|
|
69
9
|
function getSeason(month) {
|
|
70
10
|
if (month >= 2 && month <= 4) return "Spring";
|
|
71
11
|
if (month >= 5 && month <= 7) return "Summer";
|
|
@@ -83,8 +23,8 @@ module.exports = class extends Component {
|
|
|
83
23
|
let title = `'${String(year).slice(-2)}`;
|
|
84
24
|
if (sectionTitle) {
|
|
85
25
|
title = sectionTitle;
|
|
86
|
-
} else if (month !== null &&
|
|
87
|
-
title = format(time
|
|
26
|
+
} else if (month !== null && isValidDate(time)) {
|
|
27
|
+
title = dateFormatters.longMonth.format(time);
|
|
88
28
|
}
|
|
89
29
|
|
|
90
30
|
return (
|
|
@@ -130,9 +70,90 @@ module.exports = class extends Component {
|
|
|
130
70
|
articleList = renderArticleList(page.posts, page.year, page.month, null, season);
|
|
131
71
|
}
|
|
132
72
|
|
|
73
|
+
const archiveDir = config?.archive_dir || "archives";
|
|
74
|
+
const archiveBasePath = url_for(`/${archiveDir}/`);
|
|
75
|
+
const currentYear = page.year ? Number(page.year) : null;
|
|
76
|
+
const currentMonth = page.month ? Number(page.month) : null;
|
|
77
|
+
|
|
78
|
+
const yearToMonths = new Map();
|
|
79
|
+
const allPosts = site?.posts ? site.posts.sort("date", -1) : page.posts;
|
|
80
|
+
if (allPosts?.each) {
|
|
81
|
+
allPosts.each((p) => {
|
|
82
|
+
const y = p.date.year();
|
|
83
|
+
const m = p.date.month() + 1;
|
|
84
|
+
const existing = yearToMonths.get(y);
|
|
85
|
+
if (existing) {
|
|
86
|
+
existing.add(m);
|
|
87
|
+
} else {
|
|
88
|
+
yearToMonths.set(y, new Set([m]));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const years = Array.from(yearToMonths.keys()).sort((a, b) => b - a);
|
|
93
|
+
const monthsForCurrentYear = currentYear && yearToMonths.get(currentYear) ? Array.from(yearToMonths.get(currentYear)).sort((a, b) => a - b) : [];
|
|
94
|
+
|
|
133
95
|
return (
|
|
134
96
|
<Fragment>
|
|
135
|
-
<
|
|
97
|
+
<script data-swup-reload-script defer src="/js/archive-breadcrumb.js"></script>
|
|
98
|
+
<link rel="stylesheet" href="/css/archive.css" />
|
|
99
|
+
<nav class="archive-breadcrumb" aria-label="archive breadcrumb" data-archive-breadcrumb data-archive-dir={archiveDir}>
|
|
100
|
+
<span class="prompt">$</span>{" "}
|
|
101
|
+
<span class="archive-breadcrumb__cmd" style="color: var(--blue)">
|
|
102
|
+
ls
|
|
103
|
+
</span>{" "}
|
|
104
|
+
<span class="archive-breadcrumb__cmd" style="color: var(--yellow)">
|
|
105
|
+
posts
|
|
106
|
+
</span>
|
|
107
|
+
<span class="archive-breadcrumb__cmd" style="padding: 0">
|
|
108
|
+
/
|
|
109
|
+
</span>
|
|
110
|
+
<span class="archive-breadcrumb__picker" data-picker="year">
|
|
111
|
+
<button type="button" class="archive-breadcrumb__trigger" aria-haspopup="listbox" aria-expanded="false" aria-label="Select year">
|
|
112
|
+
<span data-label="year">{currentYear ? String(currentYear) : "*"}</span>
|
|
113
|
+
</button>
|
|
114
|
+
<div class="archive-breadcrumb__menu" role="listbox" tabindex="-1" data-menu="year">
|
|
115
|
+
<button type="button" class="archive-breadcrumb__option" role="option" data-href={archiveBasePath} aria-selected={!currentYear ? "true" : "false"}>
|
|
116
|
+
*
|
|
117
|
+
</button>
|
|
118
|
+
{years.map((y) => (
|
|
119
|
+
<button type="button" class="archive-breadcrumb__option" role="option" data-href={url_for(`/${archiveDir}/${y}/`)} aria-selected={currentYear === y ? "true" : "false"}>
|
|
120
|
+
{y}
|
|
121
|
+
</button>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</span>
|
|
125
|
+
<span class="archive-breadcrumb__cmd" style="padding: 0">
|
|
126
|
+
/
|
|
127
|
+
</span>
|
|
128
|
+
<span class="archive-breadcrumb__picker" data-picker="month">
|
|
129
|
+
<button type="button" class="archive-breadcrumb__trigger" aria-haspopup="listbox" aria-expanded="false" aria-label="Select month" disabled={!currentYear}>
|
|
130
|
+
<span data-label="month">{currentMonth ? String(String(currentMonth).padStart(2, "0")) : "*"}</span>
|
|
131
|
+
</button>
|
|
132
|
+
<div class="archive-breadcrumb__menu" role="listbox" tabindex="-1" data-menu="month">
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
class="archive-breadcrumb__option"
|
|
136
|
+
role="option"
|
|
137
|
+
data-href={currentYear ? url_for(`/${archiveDir}/${currentYear}/`) : archiveBasePath}
|
|
138
|
+
aria-selected={!currentMonth ? "true" : "false"}
|
|
139
|
+
>
|
|
140
|
+
*
|
|
141
|
+
</button>
|
|
142
|
+
{monthsForCurrentYear.map((m) => (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="archive-breadcrumb__option"
|
|
146
|
+
role="option"
|
|
147
|
+
data-href={url_for(`/${archiveDir}/${currentYear}/${String(m).padStart(2, "0")}/`)}
|
|
148
|
+
aria-selected={currentMonth === m ? "true" : "false"}
|
|
149
|
+
>
|
|
150
|
+
{String(m).padStart(2, "0")}
|
|
151
|
+
</button>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</span>
|
|
155
|
+
<span class="cursor">_</span>
|
|
156
|
+
</nav>
|
|
136
157
|
{articleList}
|
|
137
158
|
</Fragment>
|
|
138
159
|
);
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
const { Component,
|
|
1
|
+
const { Component, cacheComponent } = require("../../include/util/common");
|
|
2
2
|
|
|
3
3
|
class Twikoo extends Component {
|
|
4
4
|
render() {
|
|
5
5
|
const { envId, region, lang, jsUrl } = this.props;
|
|
6
|
-
|
|
7
|
-
const lazy_load_css_script = lazy_load_css("/css/twikoo.css");
|
|
8
|
-
return (
|
|
9
|
-
<Fragment>
|
|
10
|
-
<div id="twikoo" class="content twikoo"></div>
|
|
11
|
-
<script dangerouslySetInnerHTML={{ __html: lazy_load_css_script }}></script>
|
|
12
|
-
<script dangerouslySetInnerHTML={{ __html: configJs }}></script>
|
|
13
|
-
<script defer src={jsUrl}></script>
|
|
14
|
-
</Fragment>
|
|
15
|
-
);
|
|
6
|
+
return <div id="tko" class="content twikoo" data-env-id={envId} data-region={JSON.stringify(region)} data-lang={JSON.stringify(lang)} data-js-url={jsUrl} data-css-url="/css/twikoo.css"></div>;
|
|
16
7
|
}
|
|
17
8
|
}
|
|
18
9
|
|
|
@@ -146,8 +146,8 @@ Waline.Cacheable = cacheComponent(Waline, "comment.waline", (props) => {
|
|
|
146
146
|
pageview: comment.pageview,
|
|
147
147
|
comment: comment.comment,
|
|
148
148
|
copyright: comment.copyright,
|
|
149
|
-
jsUrl:
|
|
150
|
-
cssUrl:
|
|
149
|
+
jsUrl: "https://unpkg.com/@waline/client@v3/dist/waline.js",
|
|
150
|
+
cssUrl: "https://unpkg.com/@waline/client@v3/dist/waline.css",
|
|
151
151
|
};
|
|
152
152
|
});
|
|
153
153
|
|