hexo-theme-stellar 1.39.1 → 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 (130) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +2 -2
  2. package/CHANGELOG.md +70 -0
  3. package/_config.yml +38 -11
  4. package/_data/icons.yml +250 -37
  5. package/languages/en.yml +8 -0
  6. package/languages/zh-CN.yml +8 -0
  7. package/languages/zh-TW.yml +8 -0
  8. package/layout/_partial/comments/artalk/script.ejs +26 -1
  9. package/layout/_partial/comments/beaudar/script.ejs +1 -0
  10. package/layout/_partial/comments/twikoo/script.ejs +1 -0
  11. package/layout/_partial/comments/utterances/script.ejs +1 -0
  12. package/layout/_partial/comments/waline/script.ejs +1 -0
  13. package/layout/_partial/head.ejs +54 -9
  14. package/layout/_partial/main/article/article_tags.ejs +16 -0
  15. package/layout/_partial/main/navbar/article_banner.ejs +17 -0
  16. package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
  17. package/layout/_partial/main/notebook/note_card.ejs +1 -1
  18. package/layout/_partial/main/notebook/paginator.ejs +2 -2
  19. package/layout/_partial/main/pin_slider.ejs +2 -2
  20. package/layout/_partial/main/post_list/paginator.ejs +2 -2
  21. package/layout/_partial/main/post_list/post_card.ejs +11 -0
  22. package/layout/_partial/menubtn.ejs +2 -2
  23. package/layout/_partial/scripts/defines.ejs +26 -2
  24. package/layout/_partial/scripts.ejs +30 -7
  25. package/layout/_partial/sidebar/logo.ejs +1 -1
  26. package/layout/_partial/sidebar/menu.ejs +1 -1
  27. package/layout/_partial/sidebar/search.ejs +1 -1
  28. package/layout/_partial/widgets/author.ejs +1 -1
  29. package/layout/_partial/widgets/components/link.ejs +2 -2
  30. package/layout/_partial/widgets/ghuser.ejs +1 -1
  31. package/layout/_partial/widgets/markdown.ejs +1 -1
  32. package/layout/_partial/widgets/related.ejs +1 -1
  33. package/layout/_partial/widgets/toc.ejs +9 -2
  34. package/layout/_partial/widgets/tree.ejs +1 -1
  35. package/layout/_plugins/fancybox.ejs +4 -2
  36. package/layout/_plugins/mermaid.ejs +2 -1
  37. package/layout/_plugins/search/local_search.ejs +4 -3
  38. package/layout/_plugins/swiper.ejs +1 -0
  39. package/layout/categories.ejs +18 -8
  40. package/layout/page.ejs +8 -1
  41. package/layout/tags.ejs +1 -0
  42. package/package.json +5 -3
  43. package/scripts/commands/new-note.js +2 -2
  44. package/scripts/events/lib/doc_tree.js +12 -208
  45. package/scripts/events/lib/notebooks.js +8 -148
  46. package/scripts/events/lib/utils.js +10 -1
  47. package/scripts/filters/index.js +1 -0
  48. package/scripts/filters/lib/img_lazyload.js +4 -0
  49. package/scripts/filters/lib/img_onerror.js +4 -0
  50. package/scripts/filters/lib/md_table.js +46 -0
  51. package/scripts/generators/search.js +12 -18
  52. package/scripts/generators/stellar-icons.js +47 -0
  53. package/scripts/helpers/ai_label.js +29 -0
  54. package/scripts/helpers/category_tree.js +11 -0
  55. package/scripts/helpers/icon.js +2 -2
  56. package/scripts/helpers/json_ld.js +15 -13
  57. package/scripts/helpers/mdrender.js +29 -0
  58. package/scripts/helpers/reading_time.js +12 -0
  59. package/scripts/helpers/related_posts.js +0 -7
  60. package/scripts/helpers/seo.js +9 -0
  61. package/scripts/lib/ai_label.js +55 -0
  62. package/scripts/lib/category_tree.js +26 -0
  63. package/scripts/lib/doc_tree.js +267 -0
  64. package/scripts/lib/mdrender_html.js +97 -0
  65. package/scripts/lib/notebooks.js +181 -0
  66. package/scripts/lib/reading_time.js +16 -0
  67. package/scripts/lib/search_index.js +82 -0
  68. package/scripts/lib/seo.js +79 -0
  69. package/scripts/lib/wiki_readme.js +57 -0
  70. package/scripts/tags/index.js +2 -0
  71. package/scripts/tags/lib/about.js +2 -2
  72. package/scripts/tags/lib/albums.js +1 -1
  73. package/scripts/tags/lib/banner.js +1 -1
  74. package/scripts/tags/lib/chat.js +49 -49
  75. package/scripts/tags/lib/copy.js +1 -1
  76. package/scripts/tags/lib/friends.js +1 -1
  77. package/scripts/tags/lib/gallery.js +1 -1
  78. package/scripts/tags/lib/hashtag.js +1 -1
  79. package/scripts/tags/lib/image.js +3 -3
  80. package/scripts/tags/lib/md.js +17 -9
  81. package/scripts/tags/lib/posters.js +1 -1
  82. package/scripts/tags/lib/sites.js +2 -2
  83. package/scripts/tags/lib/table.js +22 -0
  84. package/scripts/tags/lib/timeline.js +9 -9
  85. package/scripts/tags/lib/tip.js +23 -0
  86. package/source/css/_common/base.styl +5 -9
  87. package/source/css/_common/device.styl +6 -3
  88. package/source/css/_components/list.styl +11 -5
  89. package/source/css/_components/md-table.styl +23 -0
  90. package/source/css/_components/pages/archives.styl +9 -25
  91. package/source/css/_components/pages/article-story.styl +4 -4
  92. package/source/css/_components/partial/article-tags.styl +10 -0
  93. package/source/css/_components/partial/bread-nav.styl +23 -1
  94. package/source/css/_components/partial/navbar.styl +10 -2
  95. package/source/css/_components/sidebar/search.styl +30 -8
  96. package/source/css/_components/sidebar/sidebar.styl +10 -7
  97. package/source/css/_components/tag-plugins/table.styl +42 -0
  98. package/source/css/_components/tag-plugins/tip.styl +27 -0
  99. package/source/css/_components/widgets/components.styl +6 -0
  100. package/source/css/_components/widgets/list.styl +6 -0
  101. package/source/css/_components/widgets/toc.styl +1 -1
  102. package/source/css/_custom.styl +2 -13
  103. package/source/css/_defines/func.styl +47 -16
  104. package/source/css/_defines/theme_base.styl +3 -0
  105. package/source/css/_plugins/index.styl +0 -18
  106. package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
  107. package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
  108. package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
  109. package/source/css/{_plugins → plugins}/swiper.styl +5 -1
  110. package/source/js/icons.js +60 -0
  111. package/source/js/main.js +206 -1
  112. package/source/js/search/highlight.js +82 -0
  113. package/source/js/search/local-search.js +294 -103
  114. package/source/js/services/mdrender.js +116 -7
  115. package/source/js/services/memos.js +60 -8
  116. package/source/js/services/timeline.js +1 -1
  117. package/source/js/services/waline_latest_comment.js +3 -1
  118. package/source/js/services/weibo.js +3 -3
  119. package/source/js/services.js +179 -0
  120. package/source/js/tagtree.js +29 -0
  121. package/source/js/theme.js +61 -0
  122. package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +13 -10
  123. package/layout/_partial/scripts/services.ejs +0 -178
  124. package/layout/_partial/scripts/sidebar.ejs +0 -25
  125. package/layout/_partial/scripts/tagtree.ejs +0 -29
  126. package/layout/_partial/scripts/theme.ejs +0 -67
  127. /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
  128. /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
  129. /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
  130. /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
package/languages/en.yml CHANGED
@@ -37,6 +37,13 @@ meta:
37
37
  license: License
38
38
  share: Share
39
39
  contributors: Page Contributors
40
+ word_count: '%s words'
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
40
47
  date_suffix:
41
48
  just: Just
42
49
  min: minutes ago
@@ -68,6 +75,7 @@ symbol:
68
75
  colon: ": "
69
76
  brackets_l: "("
70
77
  brackets_r: ")"
78
+ page: "Page %s"
71
79
 
72
80
  tag_plugins:
73
81
  mbti:
@@ -37,6 +37,13 @@ meta:
37
37
  license: 许可协议
38
38
  share: 分享文章
39
39
  contributors: 本文贡献者
40
+ word_count: '%s 字'
41
+ reading_time: '预计阅读 %s 分钟'
42
+ ai_label:
43
+ manual: 本文完全由人类完成
44
+ reviewed: 已 AI 审核
45
+ polished: 已 AI 润色
46
+ generated: 由 AI 生成
40
47
  date_suffix:
41
48
  just: 刚刚
42
49
  min: 分钟前
@@ -68,6 +75,7 @@ symbol:
68
75
  colon: ":"
69
76
  brackets_l: "("
70
77
  brackets_r: ")"
78
+ page: "第 %s 页"
71
79
 
72
80
  tag_plugins:
73
81
  mbti:
@@ -37,6 +37,13 @@ meta:
37
37
  license: 授權條款
38
38
  share: 分享文章
39
39
  contributors: 本文貢獻者
40
+ word_count: '%s 字'
41
+ reading_time: '預計閱讀 %s 分鐘'
42
+ ai_label:
43
+ manual: 本文完全由人類完成
44
+ reviewed: 已 AI 審核
45
+ polished: 已 AI 潤飾
46
+ generated: 由 AI 生成
40
47
  date_suffix:
41
48
  just: 剛剛
42
49
  min: 分鐘前
@@ -68,6 +75,7 @@ symbol:
68
75
  colon: ":"
69
76
  brackets_l: "("
70
77
  brackets_r: ")"
78
+ page: "第 %s 頁"
71
79
 
72
80
  tag_plugins:
73
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,10 +55,19 @@
39
55
  },
40
56
  <% } %>
41
57
  });
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
+ });
66
+ }
42
67
  });
43
68
  }
44
69
 
45
- util.viewportLazyload(el, load_artalk);
70
+ util.viewportLazyload(el, load_artalk, !hasAtkTarget);
46
71
  }
47
72
 
48
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,20 +32,26 @@ 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() {
23
41
  if (theme.open_graph && theme.open_graph.enable) {
24
42
  return '';
25
43
  }
44
+ if (page.description) {
45
+ return '<meta name="description" content="' + truncate(strip_html(page.description), {length: 150}) + '">';
46
+ }
26
47
  if (page.wiki) {
27
48
  const proj = theme.wiki.tree[page.wiki]
28
49
  if (proj?.description) {
29
50
  return '<meta name="description" content="' + proj.description + '">'
30
51
  }
31
52
  }
32
- if (page.description || page.excerpt || page.content) {
33
- return '<meta name="description" content="' + truncate(strip_html(page.description || page.excerpt || page.content), {length: 150}) + '">';
53
+ if (page.excerpt || page.content) {
54
+ return '<meta name="description" content="' + truncate(strip_html(page.excerpt || page.content), {length: 150}) + '">';
34
55
  }
35
56
  return '<meta name="description" content="' + config.description + '">';
36
57
  }
@@ -66,7 +87,13 @@ function og_args() {
66
87
  if (page.layout == 'post' && page.cover) {
67
88
  args.twitter_card = 'summary_large_image';
68
89
  }
69
- args.image = config.avatar || (config.email ? gravatar(config.email) : null);
90
+ if (page.wiki && !page.description) {
91
+ const proj = theme.wiki.tree[page.wiki];
92
+ if (proj && proj.description) {
93
+ args.description = proj.description;
94
+ }
95
+ }
96
+ args.image = page.cover || page.banner || first_content_image(page.content) || config.avatar || (config.email ? gravatar(config.email) : null);
70
97
  return Object.assign(args, page.open_graph);
71
98
  }
72
99
 
@@ -83,9 +110,6 @@ function generate_og_title() {
83
110
  }
84
111
 
85
112
  function generate_og_site_name() {
86
- if (page.layout === 'post' && page.title) {
87
- return generate_og_title();
88
- }
89
113
  return escape_meta_content(strip_html(config.title));
90
114
  }
91
115
 
@@ -198,4 +222,25 @@ function generate_canonical() {
198
222
 
199
223
  <%- json_ld() %>
200
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
+ <% } %>
201
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(); %>
@@ -41,6 +41,23 @@ function layoutBreadcrumb() {
41
41
  } else {
42
42
  el += partial('breadcrumb/page')
43
43
  }
44
+ // 字数和预计阅读时长,置于面包屑行右侧(#302)
45
+ var readingHtml = ''
46
+ if (page.layout == 'post' && theme.article.reading_time) {
47
+ var _wc = word_count(page)
48
+ if (_wc > 0) {
49
+ readingHtml = `<span class="cap reading right">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
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
+ }
44
61
  // end 3.left.top
45
62
  el += `</div>`
46
63
  // 3.left.bottom
@@ -42,4 +42,4 @@ function layoutDiv() {
42
42
  return el
43
43
  }
44
44
  %>
45
- <%- layoutDiv() %>
45
+ <%- layoutDiv() %>
@@ -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
  }) %>
@@ -93,6 +93,17 @@ function div_default() {
93
93
  }
94
94
  }
95
95
  // 置顶文章由轮播(默认)或首页列表靠前展示(flat 平铺)承担,卡片内不显示 pin 图标。
96
+ // 文章标签(最多 5 个,#580)
97
+ if (theme.article.card_tags && post.tags && post.tags.length > 0) {
98
+ el += '<span class="card-tags">';
99
+ post.tags.forEach((tag, i) => {
100
+ if (i >= 5) {
101
+ return;
102
+ }
103
+ el += `<span class="cap tag-chip">#${escape_html(tag.name)}</span>`;
104
+ });
105
+ el += '</span>';
106
+ }
96
107
  el += '</div>';
97
108
  el += '</article>';
98
109
  return el;
@@ -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>`
@@ -13,7 +13,7 @@ function layoutDiv() {
13
13
  el += markdown(item.content);
14
14
  }
15
15
  if (item.src) {
16
- el += `<div class="data-service ds-mdrender" src="${item.src}"></div>`
16
+ el += mdrender_html(item.src)
17
17
  }
18
18
  if (item.linklist) {
19
19
  el += partial('components/linklist', {item: item.linklist})
@@ -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
  }
@@ -38,9 +38,12 @@ function layoutToc(fallback) {
38
38
  function layoutDiv(fallback) {
39
39
  const tocBody = layoutTocBody()
40
40
  const hasToc = tocBody.trim().length > 0
41
+ // 远程 md 内容由客户端渲染,构建期正文无标题:
42
+ // 预留空 TOC 容器,渲染完成后由 mdrender.js 填充
43
+ const hasRemoteMd = has_remote_md(page)
41
44
  // 页面是否实际渲染评论区,由 _partial/comments/layout.ejs 标记
42
45
  const hasComments = page.cmt_rendered === true
43
- if (!hasToc && !hasComments) {
46
+ if (!hasToc && !hasRemoteMd && !hasComments) {
44
47
  return ''
45
48
  }
46
49
  var el = ''
@@ -50,6 +53,10 @@ function layoutDiv(fallback) {
50
53
  el += `<div class="widget-body">`
51
54
  el += tocBody
52
55
  el += `</div>`
56
+ } else if (hasRemoteMd) {
57
+ el += layoutTocHeader()
58
+ el += `<div class="widget-body">`
59
+ el += `</div>`
53
60
  }
54
61
  el += `<div class="widget-footer">`
55
62
  el += `<a class="top" onclick="util.scrollTop()">`
@@ -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) {