hexo-theme-stellar 1.40.0 → 1.41.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 (106) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_config.yml +33 -10
  3. package/_data/icons.yml +250 -37
  4. package/languages/en.yml +6 -0
  5. package/languages/zh-CN.yml +6 -0
  6. package/languages/zh-TW.yml +6 -0
  7. package/layout/_partial/comments/artalk/script.ejs +25 -8
  8. package/layout/_partial/comments/beaudar/script.ejs +1 -0
  9. package/layout/_partial/comments/twikoo/script.ejs +1 -0
  10. package/layout/_partial/comments/utterances/script.ejs +1 -0
  11. package/layout/_partial/comments/waline/script.ejs +1 -0
  12. package/layout/_partial/head.ejs +43 -7
  13. package/layout/_partial/main/article/article_tags.ejs +16 -0
  14. package/layout/_partial/main/navbar/article_banner.ejs +11 -1
  15. package/layout/_partial/main/notebook/note_card.ejs +1 -1
  16. package/layout/_partial/main/notebook/paginator.ejs +2 -2
  17. package/layout/_partial/main/pin_slider.ejs +2 -2
  18. package/layout/_partial/main/post_list/paginator.ejs +2 -2
  19. package/layout/_partial/menubtn.ejs +2 -2
  20. package/layout/_partial/scripts/defines.ejs +26 -2
  21. package/layout/_partial/scripts.ejs +30 -7
  22. package/layout/_partial/sidebar/logo.ejs +1 -1
  23. package/layout/_partial/sidebar/menu.ejs +1 -1
  24. package/layout/_partial/sidebar/search.ejs +1 -1
  25. package/layout/_partial/widgets/author.ejs +1 -1
  26. package/layout/_partial/widgets/components/link.ejs +2 -2
  27. package/layout/_partial/widgets/ghuser.ejs +1 -1
  28. package/layout/_partial/widgets/related.ejs +1 -1
  29. package/layout/_partial/widgets/toc.ejs +1 -1
  30. package/layout/_partial/widgets/tree.ejs +1 -1
  31. package/layout/_plugins/fancybox.ejs +4 -2
  32. package/layout/_plugins/mermaid.ejs +2 -1
  33. package/layout/_plugins/search/local_search.ejs +4 -3
  34. package/layout/_plugins/swiper.ejs +1 -0
  35. package/layout/categories.ejs +1 -1
  36. package/layout/page.ejs +3 -0
  37. package/layout/tags.ejs +1 -0
  38. package/package.json +1 -1
  39. package/scripts/events/lib/doc_tree.js +12 -208
  40. package/scripts/events/lib/notebooks.js +8 -148
  41. package/scripts/events/lib/utils.js +10 -1
  42. package/scripts/filters/index.js +1 -0
  43. package/scripts/filters/lib/img_lazyload.js +4 -0
  44. package/scripts/filters/lib/img_onerror.js +4 -0
  45. package/scripts/filters/lib/md_table.js +46 -0
  46. package/scripts/generators/search.js +12 -18
  47. package/scripts/generators/stellar-icons.js +47 -0
  48. package/scripts/helpers/ai_label.js +29 -0
  49. package/scripts/helpers/icon.js +2 -2
  50. package/scripts/helpers/json_ld.js +10 -13
  51. package/scripts/helpers/related_posts.js +0 -7
  52. package/scripts/helpers/seo.js +9 -0
  53. package/scripts/lib/ai_label.js +55 -0
  54. package/scripts/lib/doc_tree.js +267 -0
  55. package/scripts/lib/notebooks.js +181 -0
  56. package/scripts/lib/search_index.js +82 -0
  57. package/scripts/lib/seo.js +79 -0
  58. package/scripts/tags/lib/about.js +2 -2
  59. package/scripts/tags/lib/albums.js +1 -1
  60. package/scripts/tags/lib/banner.js +1 -1
  61. package/scripts/tags/lib/chat.js +49 -49
  62. package/scripts/tags/lib/copy.js +1 -1
  63. package/scripts/tags/lib/friends.js +1 -1
  64. package/scripts/tags/lib/gallery.js +1 -1
  65. package/scripts/tags/lib/hashtag.js +1 -1
  66. package/scripts/tags/lib/image.js +3 -3
  67. package/scripts/tags/lib/posters.js +1 -1
  68. package/scripts/tags/lib/sites.js +2 -2
  69. package/source/css/_common/base.styl +5 -9
  70. package/source/css/_common/device.styl +3 -3
  71. package/source/css/_components/md-table.styl +23 -0
  72. package/source/css/_components/pages/archives.styl +2 -22
  73. package/source/css/_components/pages/article-story.styl +4 -4
  74. package/source/css/_components/partial/article-tags.styl +10 -0
  75. package/source/css/_components/partial/bread-nav.styl +14 -0
  76. package/source/css/_components/partial/navbar.styl +10 -2
  77. package/source/css/_components/sidebar/search.styl +24 -4
  78. package/source/css/_components/tag-plugins/table.styl +16 -11
  79. package/source/css/_components/widgets/components.styl +6 -0
  80. package/source/css/_components/widgets/list.styl +6 -0
  81. package/source/css/_components/widgets/toc.styl +1 -1
  82. package/source/css/_custom.styl +1 -12
  83. package/source/css/_defines/func.styl +28 -3
  84. package/source/css/_plugins/index.styl +0 -18
  85. package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
  86. package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
  87. package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
  88. package/source/css/{_plugins → plugins}/swiper.styl +5 -1
  89. package/source/js/icons.js +60 -0
  90. package/source/js/main.js +114 -0
  91. package/source/js/search/highlight.js +82 -0
  92. package/source/js/search/local-search.js +294 -103
  93. package/source/js/services/timeline.js +1 -1
  94. package/source/js/services/weibo.js +3 -3
  95. package/source/js/services.js +179 -0
  96. package/source/js/tagtree.js +29 -0
  97. package/source/js/theme.js +61 -0
  98. package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +13 -10
  99. package/layout/_partial/scripts/services.ejs +0 -178
  100. package/layout/_partial/scripts/sidebar.ejs +0 -25
  101. package/layout/_partial/scripts/tagtree.ejs +0 -29
  102. package/layout/_partial/scripts/theme.ejs +0 -67
  103. /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
  104. /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
  105. /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
  106. /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
package/languages/en.yml CHANGED
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: Page Contributors
40
40
  word_count: '%s words'
41
41
  reading_time: '%s min read'
42
+ ai_label:
43
+ manual: Written entirely by a human
44
+ reviewed: AI-reviewed
45
+ polished: AI-polished
46
+ generated: AI-generated
42
47
  date_suffix:
43
48
  just: Just
44
49
  min: minutes ago
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ": "
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "Page %s"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: 本文贡献者
40
40
  word_count: '%s 字'
41
41
  reading_time: '预计阅读 %s 分钟'
42
+ ai_label:
43
+ manual: 本文完全由人类完成
44
+ reviewed: 已 AI 审核
45
+ polished: 已 AI 润色
46
+ generated: 由 AI 生成
42
47
  date_suffix:
43
48
  just: 刚刚
44
49
  min: 分钟前
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ":"
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "第 %s 页"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: 本文貢獻者
40
40
  word_count: '%s 字'
41
41
  reading_time: '預計閱讀 %s 分鐘'
42
+ ai_label:
43
+ manual: 本文完全由人類完成
44
+ reviewed: 已 AI 審核
45
+ polished: 已 AI 潤飾
46
+ generated: 由 AI 生成
42
47
  date_suffix:
43
48
  just: 剛剛
44
49
  min: 分鐘前
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ":"
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "第 %s 頁"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -4,8 +4,13 @@
4
4
  function initArtalk() {
5
5
  const el = document.getElementById('artalk_container');
6
6
  if (!el) return;
7
+
8
+ // artalk 邮件通知链接携带 ?atk_comment=<id> / #atk-comment-<id> 定位目标,
9
+ // 命中时跳过视口懒加载立即初始化,让 Artalk 自身的 list-goto 逻辑完成定位。
10
+ const hasAtkTarget = /[?&]atk_comment=\d+/.test(window.location.search) || /#atk-comment-\d+/.test(window.location.hash);
7
11
 
8
12
  function load_artalk() {
13
+ utils.css('<%- url_for('/css/comments/artalk.css') %>?v=<%- stellar_info('version') %>');
9
14
  utils.css('<%- theme.comments.artalk.css %>');
10
15
  utils.js('<%- theme.comments.artalk.js %>', {defer: true}).then(function () {
11
16
  // 监听 Artalk 事件
@@ -13,6 +18,17 @@
13
18
  // Artalk.use((ctx) => {
14
19
  // ctx.on('list-loaded', () => window.lazyLoadInstance?.update())
15
20
  // })
21
+ // 邮件通知链接以 ?atk_comment=<id> 定位,Artalk 在 list-loaded 时才重新读取 URL,
22
+ // 固定延时清理会与之竞态(#598)。先把评论 id 改写到 hash(Artalk 读取优先级高于 query),
23
+ // 同时立即移除 atk_comment 查询参数,避免其 hash 监听干扰目录定位;
24
+ // atk_notify_key 保留到 list-loaded 完成定位与已读回执后再清理。
25
+ const atkComment = window.location.search.match(/[?&]atk_comment=(\d+)/);
26
+ if (atkComment && !/#atk-comment-\d+/.test(window.location.hash)) {
27
+ const params = new URLSearchParams(window.location.search);
28
+ const notifyKey = params.get('atk_notify_key');
29
+ const query = notifyKey ? '?atk_notify_key=' + encodeURIComponent(notifyKey) : '';
30
+ window.history.replaceState(null, '', window.location.pathname + query + '#atk-comment-' + atkComment[1]);
31
+ }
16
32
  const path = el.getAttribute('comment_id') ?? decodeURI(window.location.pathname);
17
33
  const artalk = Artalk.init({
18
34
  el: '#artalk_container',
@@ -39,18 +55,19 @@
39
55
  },
40
56
  <% } %>
41
57
  });
42
- // artalk 邮件通知链接携带 ?atk_comment=... 等查询参数,
43
- // hash 监听会与主题目录定位冲突(#598):
44
- // 初始化并完成评论定位后清理查询参数,避免后续目录/锚点跳转被干扰。
45
- if (window.location.search.indexOf('atk_') !== -1) {
46
- window.setTimeout(function () {
47
- window.history.replaceState(null, '', window.location.pathname + window.location.hash);
48
- }, 300);
58
+ // list-loaded Artalk 已读取 atk_comment / atk_notify_key 并触发定位与已读回执,
59
+ // 此时再清理残留的 ?atk_* 参数,避免固定延时与异步加载竞态(#598)。
60
+ if (hasAtkTarget && window.location.search.indexOf('atk_') !== -1) {
61
+ artalk.on('list-loaded', function () {
62
+ window.setTimeout(function () {
63
+ window.history.replaceState(null, '', window.location.pathname + window.location.hash);
64
+ }, 0);
65
+ });
49
66
  }
50
67
  });
51
68
  }
52
69
 
53
- util.viewportLazyload(el, load_artalk);
70
+ util.viewportLazyload(el, load_artalk, !hasAtkTarget);
54
71
  }
55
72
 
56
73
  // Initialize on page load
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_beaudar() {
9
+ utils.css('<%- url_for('/css/comments/beaudar.css') %>?v=<%- stellar_info('version') %>');
9
10
  try {
10
11
  el.innerHTML = '';
11
12
  } catch (error) {
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_twikoo() {
9
+ utils.css('<%- url_for('/css/comments/twikoo.css') %>?v=<%- stellar_info('version') %>');
9
10
  utils.js('<%- theme.comments.twikoo.js %>', {defer: true}).then(function () {
10
11
  const path = el.getAttribute('comment_id') ?? decodeURI(window.location.pathname);
11
12
  twikoo.init(Object.assign(<%- JSON.stringify(theme.comments.twikoo) %>, {
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_utterances() {
9
+ utils.css('<%- url_for('/css/comments/utterances.css') %>?v=<%- stellar_info('version') %>');
9
10
  try {
10
11
  el.innerHTML = '';
11
12
  } catch (error) {
@@ -7,6 +7,7 @@
7
7
  if (!el) return;
8
8
 
9
9
  function load_waline() {
10
+ utils.css('<%- url_for('/css/comments/waline.css') %>?v=<%- stellar_info('version') %>');
10
11
  utils.css('<%- theme.comments.waline.css %>');
11
12
  utils.css('<%- theme.comments.waline.meta_css %>');
12
13
 
@@ -1,13 +1,28 @@
1
1
  <%
2
+ function strip_wiki_title(title, wiki) {
3
+ if (!wiki) { return title; }
4
+ var t = String(title || '');
5
+ if (t.toLowerCase() === String(wiki).toLowerCase()) { return ''; }
6
+ // wiki 名中的空格/连字符在标题中可能写为另一种形态(如 cloud shell / cloud-shell)
7
+ var flex = String(wiki).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/[\s-]+/g, '[\\s-]*');
8
+ var m = t.match(new RegExp('^' + flex + '[\\s-]*[::\\-](.*)$', 'i'));
9
+ if (m && m[1]) {
10
+ return m[1].trim();
11
+ }
12
+ return t;
13
+ }
14
+
2
15
  function generate_title() {
3
16
  if (page.wiki) {
4
17
  const proj = theme.wiki.tree[page.wiki]
5
18
  const wiki = proj?.name || page.wiki
6
19
  if (page.title) {
7
- return wiki + __('symbol.colon') + page.title + ' - ' + config.title;
8
- } else {
9
- return wiki + ' - ' + config.title;
20
+ var stripped = strip_wiki_title(page.title, wiki)
21
+ if (stripped) {
22
+ return wiki + __('symbol.colon') + stripped + ' - ' + config.title;
23
+ }
10
24
  }
25
+ return wiki + ' - ' + config.title;
11
26
  } else {
12
27
  if (page.title) {
13
28
  return page.title + ' - ' + config.title;
@@ -17,6 +32,9 @@ function generate_title() {
17
32
  return __('btn.tag') + __('symbol.colon') + page.tag + ' - ' + config.title;
18
33
  }
19
34
  }
35
+ if (is_home() && page.current && page.current > 1) {
36
+ return config.title + ' - ' + __('symbol.page', page.current);
37
+ }
20
38
  return config.title;
21
39
  }
22
40
  function generate_description() {
@@ -75,7 +93,7 @@ function og_args() {
75
93
  args.description = proj.description;
76
94
  }
77
95
  }
78
- args.image = config.avatar || (config.email ? gravatar(config.email) : null);
96
+ args.image = page.cover || page.banner || first_content_image(page.content) || config.avatar || (config.email ? gravatar(config.email) : null);
79
97
  return Object.assign(args, page.open_graph);
80
98
  }
81
99
 
@@ -92,9 +110,6 @@ function generate_og_title() {
92
110
  }
93
111
 
94
112
  function generate_og_site_name() {
95
- if (page.layout === 'post' && page.title) {
96
- return generate_og_title();
97
- }
98
113
  return escape_meta_content(strip_html(config.title));
99
114
  }
100
115
 
@@ -207,4 +222,25 @@ function generate_canonical() {
207
222
 
208
223
  <%- json_ld() %>
209
224
  <%- custom_inject() %>
225
+
226
+ <!-- icons.yml 驱动的 CSS 装饰图标 -->
227
+ <%
228
+ const iconCssVars = (() => {
229
+ const map = {
230
+ '--icon-h3-left': 'default:arrow-left',
231
+ '--icon-h3-right': 'default:arrow-right',
232
+ '--icon-quote-left': 'quot:quote-left',
233
+ '--icon-quote-right': 'quot:quote-right'
234
+ };
235
+ let vars = [];
236
+ for (const [name, key] of Object.entries(map)) {
237
+ const raw = theme.icons && theme.icons[key];
238
+ if (raw) vars.push(` ${name}: url("data:image/svg+xml,${encodeURIComponent(raw)}");`);
239
+ }
240
+ return vars.length > 0 ? `:root {\n${vars.join('\n')}\n}` : '';
241
+ })();
242
+ %>
243
+ <% if (iconCssVars) { %>
244
+ <style><%- iconCssVars %></style>
245
+ <% } %>
210
246
  </head>
@@ -0,0 +1,16 @@
1
+ <%# 文章标签行(layout: post,位于 article-footer 之前) %>
2
+ <%
3
+ function layoutDiv() {
4
+ var el = '';
5
+ if (!page.tags || page.tags.length === 0) {
6
+ return el;
7
+ }
8
+ el += `<div class="article-tags${scrollreveal(' ')}">`
9
+ page.tags.forEach((tag, i) => {
10
+ el += `<a class="tag" href="${pretty_url(tag.path)}">${icon('default:hashtag')}${escape_html(tag.name)}</a>`
11
+ })
12
+ el += '</div>'
13
+ return el;
14
+ }
15
+ %>
16
+ <%- layoutDiv(); %>
@@ -42,12 +42,22 @@ function layoutBreadcrumb() {
42
42
  el += partial('breadcrumb/page')
43
43
  }
44
44
  // 字数和预计阅读时长,置于面包屑行右侧(#302)
45
+ var readingHtml = ''
45
46
  if (page.layout == 'post' && theme.article.reading_time) {
46
47
  var _wc = word_count(page)
47
48
  if (_wc > 0) {
48
- el += `<span class="cap reading right">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
49
+ readingHtml = `<span class="cap reading right">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
49
50
  }
50
51
  }
52
+ el += readingHtml
53
+ // AI 成分标签:置于面包屑行最右(阅读时长右侧);
54
+ // banner 含图片时不用配置色,继承默认文字色(--text-banner)
55
+ const bannerHasImage = banner.url && banner.url.length > 0
56
+ if (readingHtml.length > 0) {
57
+ el += ai_label(page.ai_label, 'cap', bannerHasImage)
58
+ } else {
59
+ el += ai_label(page.ai_label, 'cap right', bannerHasImage)
60
+ }
51
61
  // end 3.left.top
52
62
  el += `</div>`
53
63
  // 3.left.bottom
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
  <div class="meta cap">
21
21
  <% if (note.pin) { %>
22
- <span class="pin"><%- icon('solar:pin-linear') %></span>
22
+ <span class="pin"><%- icon('default:pin') %></span>
23
23
  <% } %>
24
24
  <span class="cap" id="post-meta">
25
25
  <%- icon('default:calendar') %>
@@ -1,8 +1,8 @@
1
1
  <% if ((page.total || 0) > 1) { %>
2
2
  <div class="paginator-wrap dis-select<%= scrollreveal(' ') %>">
3
3
  <%- paginator({
4
- prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
5
- next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
4
+ prev_text: icon('default:arrow-left'),
5
+ next_text: icon('default:arrow-right'),
6
6
  force_prev_next: true,
7
7
  escape: false,
8
8
  }) %>
@@ -137,8 +137,8 @@ if (pinItems.length > 0) {
137
137
  el += '</div>'
138
138
  if (dotsHtml.length > 0) {
139
139
  el += '<div class="pin-slider-dots">' + dotsHtml + '</div>'
140
- el += '<button type="button" class="pin-slider-nav prev">' + icon('solar:double-alt-arrow-left-bold-duotone') + '</button>'
141
- el += '<button type="button" class="pin-slider-nav next">' + icon('solar:double-alt-arrow-right-bold-duotone') + '</button>'
140
+ el += '<button type="button" class="pin-slider-nav prev">' + icon('default:arrow-left') + '</button>'
141
+ el += '<button type="button" class="pin-slider-nav next">' + icon('default:arrow-right') + '</button>'
142
142
  }
143
143
  el += '</div>'
144
144
  %>
@@ -1,8 +1,8 @@
1
1
  <% if ((is_home() || is_category() || is_tag()) && page.total > 1) { %>
2
2
  <div class='paginator-wrap dis-select<%- scrollreveal(' ') %>'>
3
3
  <%- paginator({
4
- prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
5
- next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
4
+ prev_text: icon('default:arrow-left'),
5
+ next_text: icon('default:arrow-right'),
6
6
  force_prev_next: true,
7
7
  escape: false
8
8
  }) %>
@@ -1,8 +1,8 @@
1
1
  <div class='float-panel'>
2
2
  <button type='button' style='display:none' class='laptop-only rightbar-toggle mobile' onclick='sidebar.rightbar()'>
3
- <%- icon('default:rightbar') %>
3
+ <%- icon('default:rightbar', '', true) %>
4
4
  </button>
5
5
  <button type='button' style='display:none' class='mobile-only leftbar-toggle mobile' onclick='sidebar.leftbar()'>
6
- <%- icon('default:leftbar') %>
6
+ <%- icon('default:leftbar', '', true) %>
7
7
  </button>
8
8
  </div>
@@ -156,7 +156,10 @@
156
156
  root : `<%- config.root %>`,
157
157
  tag_plugins: {
158
158
  chat: Object.assign(<%- JSON.stringify(theme.tag_plugins.chat) %>),
159
- }
159
+ },
160
+ // 图标由构建期生成的 /js/stellar-icons.js 填充(defer,早于 DOMContentLoaded 就绪)
161
+ icons: {},
162
+ services: <%- JSON.stringify(theme.data_services || {}) %>,
160
163
  };
161
164
 
162
165
  // required plugins (only load if needs)
@@ -171,11 +174,32 @@
171
174
  const def = {
172
175
  avatar: `<%- theme.default.avatar %>`,
173
176
  cover: `<%- theme.default.cover %>`,
174
- loading: `<%- theme.default.loading %>`,
177
+ loading: `<%- theme.default.loading || (theme.icons && theme.icons['default:loading-placeholder']) %>`,
175
178
  data_cache: <%- JSON.stringify(theme.data_cache || {}) %>,
176
179
  };
177
180
  const deps = {
178
181
  marked: `<%- url_for(theme.dependencies.marked) %>`,
179
182
  lazyload: `<%- url_for(theme.dependencies.lazyload.js) %>`
180
183
  }
184
+
185
+ // 主题切换提示文案(供 /js/theme.js 使用)
186
+ window.__STELLAR_I18N__ = {
187
+ light: `<%- __('message.theme_switched.light') %>`,
188
+ dark: `<%- __('message.theme_switched.dark') %>`,
189
+ auto: `<%- __('message.theme_switched.auto') %>`,
190
+ };
191
+
192
+ // 搜索跳转高亮:URL 带 ?kw= 时按需加载,定位正文中的匹配词
193
+ (function () {
194
+ try {
195
+ if (!window.location.search) return;
196
+ if (!new URLSearchParams(window.location.search).has('kw')) return;
197
+ document.addEventListener('DOMContentLoaded', function () {
198
+ // utils 为全局词法绑定(顶层 const),不能通过 window.utils 访问
199
+ if (typeof utils === 'object' && typeof utils.js === 'function') {
200
+ utils.js('<%- url_for('/js/search/highlight.js') %>', { defer: true }).catch(function () {});
201
+ }
202
+ });
203
+ } catch (e) {}
204
+ })();
181
205
  </script>
@@ -14,22 +14,45 @@ function custom_inject() {
14
14
  return el;
15
15
  }
16
16
 
17
+ // tagtree 脚本仅在与 tagtree 小部件渲染相同的条件下加载(见 _partial/sidebar/index_leftbar.ejs)
18
+ function tagtreeWidgetOnPage() {
19
+ const leftbar = page && page.leftbar;
20
+ if (!Array.isArray(leftbar)) {
21
+ return false;
22
+ }
23
+ return leftbar.some(function (w) {
24
+ if (typeof w === 'string') {
25
+ return w === 'tagtree';
26
+ }
27
+ if (w && typeof w === 'object') {
28
+ return w.override === 'tagtree' || w.layout === 'tagtree';
29
+ }
30
+ return false;
31
+ });
32
+ }
33
+
17
34
  %>
18
35
  <%- partial('scripts/defines') %>
19
- <%- partial('scripts/utils') %>
20
- <%- partial('scripts/sidebar') %>
21
- <%- partial('scripts/tagtree') %>
22
- <%- partial('scripts/lazyload') %>
23
36
 
24
- <!-- required -->
37
+ <!-- core: utils 必须同步加载(页尾内联插件片段在解析期即调用 utils.initPlugin) -->
38
+ <script src="<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>"></script>
39
+ <script defer src="<%- url_for('/js/stellar-icons.js') %>?v=<%- stellar_info('version') %>"></script>
40
+ <!-- 非首屏图标异步加载:占位符替换依赖构建期生成的 js/icons/{ns}.json -->
41
+ <script defer src="<%- url_for('/js/icons.js') %>?v=<%- stellar_info('version') %>" data-version="<%- stellar_info('version') %>"></script>
25
42
  <script src="<%- url_for(`${theme.stellar.main_js}?v=${stellar_info('version')}`) %>" defer></script>
43
+ <script defer src="<%- url_for('/js/theme.js') %>?v=<%- stellar_info('version') %>"></script>
44
+
45
+ <% if (tagtreeWidgetOnPage()) { %>
46
+ <script>window.__STELLAR_TAGTREE__ = { expand_active: <%- JSON.stringify(!!(theme.widgets.tagtree && theme.widgets.tagtree.expand_active)) %> };</script>
47
+ <script defer src="<%- url_for('/js/tagtree.js') %>?v=<%- stellar_info('version') %>"></script>
48
+ <% } %>
26
49
 
27
- <%- partial('scripts/theme') %>
50
+ <%- partial('scripts/lazyload') %>
28
51
 
29
52
  <!-- optional -->
30
53
  <%- partial('comments/script') %>
31
54
 
32
- <%- partial('scripts/services') %>
55
+ <script defer src="<%- url_for('/js/services.js') %>?v=<%- stellar_info('version') %>"></script>
33
56
  <%- partial('../_plugins/index') %>
34
57
 
35
58
  <!-- inject -->
@@ -50,7 +50,7 @@ function layoutBack() {
50
50
  var el = ''
51
51
  el += `<div class="wiki-home-wrap">`
52
52
  el += `<a class="wiki-home cap" href="${pretty_url(theme.site_tree.index_wiki.base_dir)}">`
53
- el += icon('default:goback')
53
+ el += icon('default:goback', '', true)
54
54
  el += `<span>${__('btn.all_wiki')}</span>`
55
55
  el += `</a>`
56
56
  el += `</div>`
@@ -15,7 +15,7 @@ function layoutDiv() {
15
15
  el += ` style="${style}"`
16
16
  el += ` title="${item.title}" href="${pretty_url(item.url)}">`
17
17
  if (item.icon?.length > 0) {
18
- el += icon(item.icon, 'no-lazy')
18
+ el += icon(item.icon, 'no-lazy', true)
19
19
  }
20
20
  el += `<span>${__(item.title)}</span>`
21
21
  if (active) {
@@ -32,7 +32,7 @@ function layoutDiv() {
32
32
  }
33
33
  }
34
34
  el += `<a class="search-button" onclick="document.getElementById(&quot;search-input&quot;).focus();">`
35
- el += icon('default:search')
35
+ el += icon('default:search', '', true)
36
36
  el += `</a>`
37
37
  el += `<input type="text" class="search-input" id="search-input"`
38
38
  if (filter.length > 0) {
@@ -26,7 +26,7 @@ function layoutDiv() {
26
26
 
27
27
  // homepage
28
28
  el += `<a class="follow" href="${url_for(author.path)}">`
29
- el += icon('solar:documents-bold-duotone')
29
+ el += icon('default:documents')
30
30
  el += __('btn.all_posts')
31
31
  el += `</a>`
32
32
  el += `</div>`
@@ -12,10 +12,10 @@ function layoutDiv() {
12
12
  }
13
13
  el += `</div>`
14
14
  if (columns == 1 && isActive) {
15
- el += icon('default:bookmark.active')
15
+ el += '<span class="active-dot"></span>'
16
16
  }
17
17
  el += `</a>`
18
18
  return el
19
19
  }
20
20
  %>
21
- <%- layoutDiv() %>
21
+ <%- layoutDiv() %>
@@ -30,7 +30,7 @@ function layoutDiv() {
30
30
  if (item.header) {
31
31
  el += `<div class="widget-header">`
32
32
  el += `<a class="gh-url" href="https://github.com/${item.username}">
33
- <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from TDesign Icons by TDesign - https://github.com/Tencent/tdesign-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 .999c-6.074 0-11 5.05-11 11.278c0 4.983 3.152 9.21 7.523 10.702c.55.104.727-.246.727-.543v-2.1c-3.06.683-3.697-1.33-3.697-1.33c-.5-1.304-1.222-1.65-1.222-1.65c-.998-.7.076-.686.076-.686c1.105.08 1.686 1.163 1.686 1.163c.98 1.724 2.573 1.226 3.201.937c.098-.728.383-1.226.698-1.508c-2.442-.286-5.01-1.253-5.01-5.574c0-1.232.429-2.237 1.132-3.027c-.114-.285-.49-1.432.107-2.985c0 0 .924-.303 3.026 1.156c.877-.25 1.818-.375 2.753-.38c.935.005 1.876.13 2.755.38c2.1-1.459 3.023-1.156 3.023-1.156c.598 1.554.222 2.701.108 2.985c.706.79 1.132 1.796 1.132 3.027c0 4.332-2.573 5.286-5.022 5.565c.394.35.754 1.036.754 2.088v3.095c0 .3.176.652.734.542C19.852 21.484 23 17.258 23 12.277C23 6.048 18.075.999 12 .999"/></svg>
33
+ ${icon('github:logo-alt')}
34
34
  <span>github/${item.username}</span>
35
35
  </a>`
36
36
  el += `</div>`
@@ -18,7 +18,7 @@ function relatedPostsInTopic() {
18
18
  el += `<a class="item${isActive ? ' active' : ''}" href="${pretty_url(post.path)}">`
19
19
  el += `<span class="title">${post.title}</span>`
20
20
  if (isActive) {
21
- el += icon('default:bookmark.active')
21
+ el += '<span class="active-dot"></span>'
22
22
  }
23
23
  el += `</a>`
24
24
  }
@@ -15,7 +15,7 @@ function layoutTocHeader(title) {
15
15
  var el = ''
16
16
  el += `<div class="widget-header dis-select">`
17
17
  el += `<span class="name">${title || __("meta.toc")}</span>`
18
- el += `<a class="cap-action" onclick="sidebar.toggleTOC()" >${icon('default:rightbar')}</a>`
18
+ el += `<a class="cap-action" onclick="sidebar.toggleTOC()" >${icon('default:rightbar', '', true)}</a>`
19
19
  el += `</div>`
20
20
  return el
21
21
  }
@@ -27,7 +27,7 @@ function layoutDocTree(pages) {
27
27
  el += `<a class="link${isActive}" href="${href}">`
28
28
  el += `<span class="toc-text">${p.title}</span>`
29
29
  if (isActive.length > 0) {
30
- el += icon('default:bookmark.active')
30
+ el += '<span class="active-dot"></span>'
31
31
  }
32
32
  el += `</a>`
33
33
  }
@@ -7,8 +7,9 @@
7
7
  };
8
8
  // auto=按需:页面存在可弹窗图片才加载;global=全局:所有页面加载
9
9
  const isGlobal = ctx.fancybox.mode === 'global';
10
- // 评论区图片(artalk/twikoo/waline)显示较小,自动支持放大
11
- var selector = '[data-fancybox]:not(.error), .with-fancybox img:not([atk-emoticon]):not([class*="emo"])';
10
+ // 评论区图片(artalk/twikoo/waline)显示较小,自动支持放大;
11
+ // 仅绑定各评论服务的评论内容区,编辑器、表情面板等非内容区图片不弹窗
12
+ var selector = '[data-fancybox]:not(.error), .with-fancybox .atk-content img:not([atk-emoticon]):not([class*="emo"]), .with-fancybox .tk-content img:not([atk-emoticon]):not([class*="emo"]), .with-fancybox .wl-content img:not([atk-emoticon]):not([class*="emo"])';
12
13
  if (ctx.fancybox.selector) {
13
14
  selector += `, ${ctx.fancybox.selector}`
14
15
  }
@@ -31,6 +32,7 @@
31
32
  }
32
33
  }
33
34
  if (needFancybox) {
35
+ utils.css(`<%- url_for('/css/plugins/fancybox.css') %>?v=<%- stellar_info('version') %>`);
34
36
  utils.css(ctx.fancybox.css);
35
37
  utils.js(ctx.fancybox.js, { defer: true }).then(function () {
36
38
  if (isGlobal) {
@@ -3,6 +3,7 @@
3
3
  utils.initPlugin(() => {
4
4
  const els = document.querySelectorAll('.mermaid');
5
5
  if (els.length > 0) {
6
+ utils.css(`<%- url_for('/css/plugins/mermaid.css') %>?v=<%- stellar_info('version') %>`);
6
7
  const mermaid_config = {
7
8
  startOnLoad: false,
8
9
  theme:
@@ -30,4 +31,4 @@
30
31
  mermaid.run();
31
32
  }
32
33
  }, 'mermaid');
33
- </script>
34
+ </script>
@@ -1,8 +1,9 @@
1
1
  <script>
2
2
  window.addEventListener('DOMContentLoaded', (event) => {
3
- ctx.search = {
3
+ ctx.search = Object.assign({}, ctx.search, {
4
4
  path: `<%- conf.path || '/search.json' %>`,
5
- }
5
+ local_search: <%- JSON.stringify(conf) %>,
6
+ });
6
7
  utils.js('/js/search/local-search.js', { defer: true });
7
8
  });
8
- </script>
9
+ </script>
@@ -2,6 +2,7 @@
2
2
  utils.initPlugin(() => {
3
3
  const swiper_api = document.getElementById('swiper-api');
4
4
  if (swiper_api != undefined) {
5
+ utils.css(`<%- url_for('/css/plugins/swiper.css') %>?v=<%- stellar_info('version') %>`);
5
6
  utils.css(`<%- conf.css %>`);
6
7
  utils.js(`<%- conf.js %>`, { defer: true }).then(function () {
7
8
  const effect = swiper_api.getAttribute('effect') || '';
@@ -16,7 +16,7 @@ if (page.menu_id == undefined) {
16
16
  nodes.forEach(function(node) {
17
17
  el += `<div class="cat-item">`
18
18
  el += `<a class="cat${node.parentId ? ' child' : ''}" href="${pretty_url(node.path)}">`
19
- el += `<span class="name">${icon('default:category')}${escape_html(node.name)}</span>`
19
+ el += `<span class="name">${icon(node.children && node.children.length > 0 ? 'default:category-open' : 'default:category')}${escape_html(node.name)}</span>`
20
20
  el += `<span class="badge">(${node.count})</span>`
21
21
  el += `</a>`
22
22
  if (node.children && node.children.length > 0) {
package/layout/page.ejs CHANGED
@@ -56,6 +56,9 @@ function layoutDiv() {
56
56
  }
57
57
  el += `</article>`
58
58
  if (layout === 'post' || page.wiki || notebook) {
59
+ if (layout === 'post' && theme.article.tags) {
60
+ el += partial('_partial/main/article/article_tags')
61
+ }
59
62
  el += partial('_partial/main/article/article_footer')
60
63
  }
61
64
  if (layout === 'post' || page.wiki) {
package/layout/tags.ejs CHANGED
@@ -12,6 +12,7 @@ if (page.menu_id == undefined) {
12
12
  <article class='<%- scrollreveal("") %>' id='tags'>
13
13
  <% site.tags.sort('length', -1).each(function(tag){ %>
14
14
  <a class='tag' href="<%= pretty_url(tag.path) %>">
15
+ <%- icon('default:hashtag') %>
15
16
  <span class='name'><%= tag.name %></span>
16
17
  </a>
17
18
  <% }) %>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {