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.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/include/hexo/encrypt.js +42 -0
  3. package/include/hexo/feed.js +329 -0
  4. package/include/util/common.js +7 -9
  5. package/languages/en.yml +6 -3
  6. package/languages/zh-CN.yml +6 -3
  7. package/layout/archive.jsx +86 -65
  8. package/layout/comment/twikoo.jsx +2 -11
  9. package/layout/comment/waline.jsx +2 -2
  10. package/layout/common/article.jsx +5 -8
  11. package/layout/common/article_cover.jsx +11 -1
  12. package/layout/common/article_media.jsx +2 -4
  13. package/layout/common/footer.jsx +11 -31
  14. package/layout/common/head.jsx +6 -14
  15. package/layout/common/navbar.jsx +4 -4
  16. package/layout/common/scripts.jsx +6 -6
  17. package/layout/common/theme_selector.jsx +5 -6
  18. package/layout/common/toc.jsx +8 -14
  19. package/layout/index.jsx +2 -4
  20. package/layout/misc/article_licensing.jsx +4 -2
  21. package/layout/misc/open_graph.jsx +4 -4
  22. package/layout/misc/paginator.jsx +10 -4
  23. package/layout/misc/structured_data.jsx +3 -4
  24. package/layout/plugin/busuanzi.jsx +1 -1
  25. package/layout/plugin/cookie_consent.jsx +40 -31
  26. package/layout/plugin/swup.jsx +2 -22
  27. package/layout/search/insight.jsx +16 -3
  28. package/package.json +12 -8
  29. package/scripts/hot-reload.js +92 -0
  30. package/scripts/index.js +2 -0
  31. package/source/css/archive.css +251 -0
  32. package/source/css/default.css +250 -284
  33. package/source/css/encrypt.css +55 -0
  34. package/source/css/responsive/desktop.css +0 -119
  35. package/source/css/responsive/mobile.css +7 -23
  36. package/source/css/responsive/touch.css +9 -103
  37. package/source/css/shiki/shiki.css +7 -22
  38. package/source/css/twikoo.css +290 -830
  39. package/source/img/og_image.webp +0 -0
  40. package/source/js/archive-breadcrumb.js +132 -0
  41. package/source/js/busuanzi.js +1 -12
  42. package/source/js/components/accordion.js +192 -0
  43. package/source/js/components/chat.js +239 -0
  44. package/source/js/components/device-carousel.js +260 -0
  45. package/source/js/components/image-carousel.js +410 -0
  46. package/source/js/components/text-image-section.js +180 -0
  47. package/source/js/components/theme-stacked.js +526 -0
  48. package/source/js/components/tree.js +437 -0
  49. package/source/js/decrypt.js +112 -0
  50. package/source/js/insight.js +75 -65
  51. package/source/js/main.js +192 -99
  52. package/source/js/mdit/mermaid.js +12 -4
  53. package/source/js/swup.bundle.js +1 -0
  54. package/source/js/theme-selector.js +94 -113
  55. package/source/img/og_image.png +0 -0
  56. package/source/js/host/swup/Swup.umd.min.js +0 -1
  57. package/source/js/host/swup/head-plugin.umd.min.js +0 -1
  58. package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
  59. 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
+ };
@@ -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&nbsp;%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(󰘳 + K)"
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"
@@ -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... (󰘳 + K)"
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: "此文章已加密"
@@ -1,71 +1,11 @@
1
- const { format, isValid, parseISO } = require("date-fns");
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 && isValid(time)) {
87
- title = format(time, "LLLL 'yy");
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
- <style dangerouslySetInnerHTML={{ __html: inlineCSS }}></style>
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, Fragment, cacheComponent, lazy_load_css } = require("../../include/util/common");
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
- const configJs = `window.twikooConfig = { envId: '${envId}', ${region ? `region: ${JSON.stringify(region)},` : ""} ${lang ? `lang: ${JSON.stringify(lang)},` : ""} };`;
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: helper.cdn("@waline/client", "3.3.0", "dist/waline.js"),
150
- cssUrl: helper.cdn("@waline/client", "3.3.0", "dist/waline.css"),
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