hexo-theme-stellar 2.0.0-rc.2 → 2.0.0-rc.4

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 (223) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/_config.yml +110 -10
  3. package/languages/en.yml +1 -0
  4. package/languages/zh-CN.yml +1 -0
  5. package/languages/zh-TW.yml +1 -0
  6. package/layout/_partial/comments/beaudar/layout.ejs +1 -1
  7. package/layout/_partial/comments/giscus/layout.ejs +1 -1
  8. package/layout/_partial/comments/utterances/layout.ejs +1 -1
  9. package/layout/_partial/cover/wiki_cover.ejs +2 -1
  10. package/layout/_partial/head.ejs +16 -10
  11. package/layout/_partial/main/article/post_footer.ejs +1 -1
  12. package/layout/_partial/main/article/post_read_next.ejs +4 -1
  13. package/layout/_partial/main/footer.ejs +8 -8
  14. package/layout/_partial/main/listing_nav/blog.ejs +2 -1
  15. package/layout/_partial/main/navbar/article_banner.ejs +3 -1
  16. package/layout/_partial/main/pin_slider.ejs +33 -20
  17. package/layout/_partial/menubtn.ejs +1 -1
  18. package/layout/_partial/primitives/region.ejs +3 -3
  19. package/layout/_partial/primitives/shell.ejs +1 -1
  20. package/layout/_partial/scripts/defines.ejs +20 -119
  21. package/layout/_partial/scripts/runtime.ejs +6 -4
  22. package/layout/_partial/scripts.ejs +7 -27
  23. package/layout/_partial/search/dialog.ejs +1 -1
  24. package/layout/_partial/sidebar/brand.ejs +2 -2
  25. package/layout/_partial/widgets/related.ejs +1 -1
  26. package/layout/_partial/widgets/tagtree.ejs +1 -1
  27. package/layout/archive.ejs +14 -23
  28. package/layout/index.ejs +4 -0
  29. package/layout/layout.ejs +2 -2
  30. package/layout/page.ejs +1 -1
  31. package/package.json +7 -4
  32. package/scripts/commands/stellar.js +15 -3
  33. package/scripts/events/index.js +10 -23
  34. package/scripts/events/lib/authors.js +1 -0
  35. package/scripts/events/lib/doc_tree.js +8 -7
  36. package/scripts/events/lib/notebooks.js +6 -21
  37. package/scripts/filters/index.js +8 -3
  38. package/scripts/filters/lib/img.js +15 -0
  39. package/scripts/filters/lib/img_lazyload.js +1 -0
  40. package/scripts/filters/lib/img_onerror.js +21 -12
  41. package/scripts/filters/lib/md_link.js +26 -0
  42. package/scripts/filters/lib/page-view-model.js +36 -16
  43. package/scripts/generators/author.js +0 -1
  44. package/scripts/generators/search.js +3 -3
  45. package/scripts/generators/stellar-icons.js +1 -0
  46. package/scripts/helpers/brand.js +0 -5
  47. package/scripts/helpers/collection.js +3 -3
  48. package/scripts/helpers/comments_model.js +2 -2
  49. package/scripts/helpers/extension_assets.js +16 -2
  50. package/scripts/helpers/json_ld.js +5 -4
  51. package/scripts/helpers/md_link.js +7 -0
  52. package/scripts/helpers/page_meta.js +2 -0
  53. package/scripts/helpers/post_view_model.js +15 -12
  54. package/scripts/helpers/script_json.js +3 -0
  55. package/scripts/lib/browser-runtime.js +4 -11
  56. package/scripts/lib/collection-pipeline/adapters/notebook.js +4 -6
  57. package/scripts/lib/collection-pipeline/adapters/post.js +5 -2
  58. package/scripts/lib/collection-pipeline/adapters/topic.js +15 -10
  59. package/scripts/lib/collection-pipeline/index.js +13 -8
  60. package/scripts/lib/comments.js +20 -2
  61. package/scripts/lib/config-schema.js +13 -3
  62. package/scripts/lib/content-config.js +35 -23
  63. package/scripts/lib/contribution-contract.js +8 -16
  64. package/scripts/lib/contribution-registry.js +102 -51
  65. package/scripts/lib/doctor.js +14 -10
  66. package/scripts/lib/hero-effect-registry.js +0 -8
  67. package/scripts/lib/html-images.js +7 -6
  68. package/scripts/lib/image-metadata.js +308 -0
  69. package/scripts/lib/internal-constants.js +3 -29
  70. package/scripts/lib/models/index.js +21 -1537
  71. package/scripts/lib/models/notebook.js +292 -0
  72. package/scripts/lib/models/post.js +72 -0
  73. package/scripts/lib/models/shared.js +571 -0
  74. package/scripts/lib/models/topic.js +201 -0
  75. package/scripts/lib/models/wiki.js +413 -0
  76. package/scripts/lib/page-view-model-registry.js +126 -82
  77. package/scripts/lib/partial-navigation.js +64 -0
  78. package/scripts/lib/resource-assets.js +19 -0
  79. package/scripts/lib/script-json.js +13 -0
  80. package/scripts/lib/source-config.js +26 -16
  81. package/scripts/schema/config-rules.js +21 -15
  82. package/scripts/schema/content-config-rules.js +2 -1
  83. package/scripts/schema/content-config-schema.js +6 -4
  84. package/scripts/schema/model-schema.js +8 -5
  85. package/scripts/tags/lib/albums.js +1 -1
  86. package/scripts/tags/lib/friends.js +1 -1
  87. package/scripts/tags/lib/ghcard.js +1 -1
  88. package/scripts/tags/lib/posters.js +1 -1
  89. package/scripts/tags/lib/sites.js +2 -2
  90. package/source/css/_appearances/_mixins.styl +14 -16
  91. package/source/css/_appearances/minimal.styl +86 -46
  92. package/source/css/_common/button.styl +2 -2
  93. package/source/css/_common/cap.styl +1 -1
  94. package/source/css/_common/highlight.styl +2 -3
  95. package/source/css/_common/html.styl +5 -1
  96. package/source/css/_common/input.styl +1 -1
  97. package/source/css/_common/loading.styl +2 -0
  98. package/source/css/_common/md-link.styl +10 -0
  99. package/source/css/_common/span.styl +1 -1
  100. package/source/css/_common/title.styl +2 -2
  101. package/source/css/_common/toast.styl +1 -1
  102. package/source/css/_components/collection.styl +17 -9
  103. package/source/css/_components/layout.styl +42 -8
  104. package/source/css/_components/list.styl +6 -3
  105. package/source/css/_components/md.styl +1 -2
  106. package/source/css/_components/pages/archives.styl +4 -4
  107. package/source/css/_components/pages/article-story.styl +1 -1
  108. package/source/css/_components/pages/article-tech.styl +2 -2
  109. package/source/css/_components/partial/article-footer.styl +1 -1
  110. package/source/css/_components/partial/article-share.styl +3 -0
  111. package/source/css/_components/partial/bread-nav.styl +2 -2
  112. package/source/css/_components/partial/cover.styl +4 -4
  113. package/source/css/_components/partial/footer.styl +2 -2
  114. package/source/css/_components/partial/paginator.styl +1 -1
  115. package/source/css/_components/partial/post-panel.styl +3 -3
  116. package/source/css/_components/partial/related.styl +14 -3
  117. package/source/css/_components/pin-slider.styl +7 -4
  118. package/source/css/_components/sidebar/brand.styl +3 -3
  119. package/source/css/_components/sidebar/nav-area.styl +1 -1
  120. package/source/css/_components/sidebar/search.styl +44 -21
  121. package/source/css/_components/tag-plugins/banner.styl +1 -1
  122. package/source/css/_components/tag-plugins/chat.styl +2 -2
  123. package/source/css/_components/tag-plugins/copy.styl +1 -1
  124. package/source/css/_components/tag-plugins/emoji.styl +1 -0
  125. package/source/css/_components/tag-plugins/folders.styl +2 -2
  126. package/source/css/_components/tag-plugins/folding.styl +2 -2
  127. package/source/css/_components/tag-plugins/friends.styl +2 -2
  128. package/source/css/_components/tag-plugins/friends_posts.styl +4 -2
  129. package/source/css/_components/tag-plugins/gallery.styl +4 -3
  130. package/source/css/_components/tag-plugins/hashtag.styl +1 -1
  131. package/source/css/_components/tag-plugins/image.styl +2 -0
  132. package/source/css/_components/tag-plugins/inline-labels.styl +2 -2
  133. package/source/css/_components/tag-plugins/link.styl +1 -1
  134. package/source/css/_components/tag-plugins/mark.styl +2 -2
  135. package/source/css/_components/tag-plugins/mbti.styl +6 -4
  136. package/source/css/_components/tag-plugins/mdrender.styl +1 -1
  137. package/source/css/_components/tag-plugins/navbar.styl +1 -1
  138. package/source/css/_components/tag-plugins/note.styl +1 -1
  139. package/source/css/_components/tag-plugins/okr.styl +5 -3
  140. package/source/css/_components/tag-plugins/poetry.styl +2 -2
  141. package/source/css/_components/tag-plugins/quot.styl +4 -2
  142. package/source/css/_components/tag-plugins/rating.styl +2 -2
  143. package/source/css/_components/tag-plugins/read/paper.styl +3 -3
  144. package/source/css/_components/tag-plugins/read/reel.styl +3 -3
  145. package/source/css/_components/tag-plugins/sites.styl +2 -2
  146. package/source/css/_components/tag-plugins/tabs.styl +1 -1
  147. package/source/css/_components/tag-plugins/timeline.styl +6 -6
  148. package/source/css/_components/tag-plugins/toc.styl +2 -2
  149. package/source/css/_components/tag-plugins/vote.styl +2 -2
  150. package/source/css/_components/widgets/ghrepo.styl +1 -1
  151. package/source/css/_components/widgets/ghuser.styl +4 -4
  152. package/source/css/_components/widgets/markdown.styl +3 -0
  153. package/source/css/_components/widgets/settings.styl +2 -2
  154. package/source/css/_components/widgets/toc.styl +24 -3
  155. package/source/css/_components/widgets/widgets.styl +1 -1
  156. package/source/css/_components/wiki-card.styl +3 -3
  157. package/source/css/_custom.styl +2 -1
  158. package/source/css/_defines/func.styl +2 -2
  159. package/source/css/_defines/theme_base.styl +0 -2
  160. package/source/css/_defines/typography.styl +10 -0
  161. package/source/css/_plugins/card-hover.styl +46 -43
  162. package/source/css/_plugins/index.styl +1 -1
  163. package/source/css/comments/artalk.styl +1 -1
  164. package/source/css/comments/twikoo.styl +2 -2
  165. package/source/css/comments/waline.styl +1 -1
  166. package/source/css/plugins/swiper.styl +2 -1
  167. package/source/js/color.js +21 -85
  168. package/source/js/main.js +63 -56
  169. package/source/js/navigation-init.js +58 -0
  170. package/source/js/plugins/adaptive-text.js +51 -25
  171. package/source/js/plugins/card-hover.js +5 -3
  172. package/source/js/plugins/download-file.js +26 -28
  173. package/source/js/plugins/video.js +22 -63
  174. package/source/js/profile.js +2 -0
  175. package/source/js/runtime/asset-loader.js +43 -34
  176. package/source/js/runtime/extension-registry.js +78 -57
  177. package/source/js/runtime/extensions/adaptive-text.js +14 -0
  178. package/source/js/runtime/extensions/card-hover.js +3 -2
  179. package/source/js/runtime/extensions/code-copy.js +19 -0
  180. package/source/js/runtime/extensions/comments.js +4 -3
  181. package/source/js/runtime/extensions/deferred-icons.js +13 -7
  182. package/source/js/runtime/extensions/diagrams.js +20 -0
  183. package/source/js/runtime/extensions/hero-effect.js +7 -3
  184. package/source/js/runtime/extensions/heti.js +12 -0
  185. package/source/js/runtime/extensions/lazy-loading.js +74 -0
  186. package/source/js/runtime/extensions/lightbox.js +28 -0
  187. package/source/js/runtime/extensions/link-prefetch.js +5 -0
  188. package/source/js/runtime/extensions/mathjax.js +24 -0
  189. package/source/js/runtime/extensions/page-controls.js +14 -0
  190. package/source/js/runtime/extensions/partial-navigation.js +172 -0
  191. package/source/js/runtime/extensions/reveal.js +29 -8
  192. package/source/js/runtime/extensions/search.js +4 -1
  193. package/source/js/runtime/extensions/services.js +113 -16
  194. package/source/js/runtime/extensions/settings.js +14 -10
  195. package/source/js/runtime/extensions/swiper.js +22 -0
  196. package/source/js/runtime/image-color.js +211 -0
  197. package/source/js/runtime/index.js +60 -43
  198. package/source/js/runtime/legacy-request-adapter.js +2 -0
  199. package/source/js/runtime/request-cache.js +26 -0
  200. package/source/js/search/algolia-search.js +1 -1
  201. package/source/js/search/highlight.js +6 -9
  202. package/source/js/search/local-search.js +128 -81
  203. package/source/js/services/artalk_latest_comment.js +6 -4
  204. package/source/js/services/contributors.js +6 -3
  205. package/source/js/services/friends.js +7 -4
  206. package/source/js/services/friends_and_posts.js +7 -4
  207. package/source/js/services/ghinfo.js +6 -3
  208. package/source/js/services/giscus_latest_comment.js +6 -3
  209. package/source/js/services/mdrender.js +10 -9
  210. package/source/js/services/memos.js +9 -3
  211. package/source/js/services/rating.js +13 -10
  212. package/source/js/services/rss.js +6 -3
  213. package/source/js/services/siteinfo.js +37 -14
  214. package/source/js/services/sites.js +9 -6
  215. package/source/js/services/timeline.js +6 -3
  216. package/source/js/services/twikoo_latest_comment.js +10 -5
  217. package/source/js/services/vote.js +11 -8
  218. package/source/js/services/waline_latest_comment.js +6 -3
  219. package/source/js/services/weibo.js +6 -3
  220. package/source/js/tagtree.js +7 -7
  221. package/scripts/events/lib/fix_image_tags.js +0 -44
  222. package/scripts/events/lib/get_image_ratios.js +0 -110
  223. package/source/js/runtime/extensions/feature.js +0 -200
@@ -1,4 +1,4 @@
1
- <div class="site-dock" role="group" aria-label="Layout panels">
1
+ <div data-stellar-runtime-region class="site-dock" role="group" aria-label="Layout panels">
2
2
  <button type="button" class="<%- ui_classes('site-dock__button site-dock__button--rightbar', 'interactive') %>" aria-label="Open rightbar" aria-controls="rightbar-region" aria-expanded="false" data-shell-action="toggle-rightbar-drawer">
3
3
  <%- icon('default:rightbar', '', true) %>
4
4
  </button>
@@ -6,11 +6,11 @@ if (!allowedSlots.includes(slot)) {
6
6
  var regionBody = typeof body === 'string' ? body : ''
7
7
  var html = ''
8
8
  if (slot === 'topbar') {
9
- html = `<header id="topbar-region" class="site-region site-region--topbar ui-surface" data-region="topbar"><div class="site-region__viewport">${regionBody}</div></header>`
9
+ html = `<header data-stellar-runtime-region id="topbar-region" class="site-region site-region--topbar ui-surface" data-region="topbar"><div class="site-region__viewport">${regionBody}</div></header>`
10
10
  } else if (slot === 'leftbar') {
11
- html = `<aside id="leftbar-region" class="site-region site-region--leftbar" data-region="leftbar" aria-label="Leftbar"><div class="site-region__surface ui-surface"><div class="site-region__decoration" aria-hidden="true"></div><div class="site-region__viewport">${regionBody}</div></div></aside>`
11
+ html = `<aside data-stellar-runtime-region id="leftbar-region" class="site-region site-region--leftbar" data-region="leftbar" aria-label="Leftbar"><div class="site-region__surface ui-surface"><div class="site-region__decoration" aria-hidden="true"></div><div class="site-region__viewport">${regionBody}</div></div></aside>`
12
12
  } else if (slot === 'rightbar') {
13
- html = `<aside id="rightbar-region" class="site-region site-region--rightbar" data-region="rightbar" aria-label="Rightbar"><div class="site-region__surface ui-drawer-surface"><div class="site-region__viewport">${regionBody}</div></div></aside>`
13
+ html = `<aside data-stellar-runtime-region id="rightbar-region" class="site-region site-region--rightbar" data-region="rightbar" aria-label="Rightbar"><div class="site-region__surface ui-drawer-surface"><div class="site-region__viewport">${regionBody}</div></div></aside>`
14
14
  }
15
15
  %>
16
16
  <%- html %>
@@ -22,7 +22,7 @@ html += `<div class="site-shell" id="start" data-regions="${presentRegions.join(
22
22
  html += shellRegions.topbar || ''
23
23
  html += '<div class="site-workspace">'
24
24
  html += shellRegions.leftbar || ''
25
- html += `<main class="site-main" id="main">${main || ''}</main>`
25
+ html += `<main data-stellar-runtime-region class="site-main" id="main">${main || ''}</main>`
26
26
  html += shellRegions.rightbar || ''
27
27
  html += '</div>'
28
28
  html += '<button type="button" class="site-scrim" data-shell-action="dismiss-drawer" aria-label="Close navigation" tabindex="-1"></button>'
@@ -21,121 +21,35 @@
21
21
  var searchProvider = extensions.search.provider === 'local' ? 'local_search' : extensions.search.provider
22
22
  %>
23
23
 
24
- <script type="text/javascript">
25
- window.canonical = <%- JSON.stringify(canonicalWithEncoded) %>;
26
- window.canonical["param"] = <%- JSON.stringify({permalink, checklink}) %>;
27
- // Fix: Ensure page is at the correct top position on data load
28
- if (history.scrollRestoration) {
29
- history.scrollRestoration = 'auto';
30
- }
31
- (function() {
32
- const scrollHandler = () => {
33
- // 存在有效锚点时,等待布局稳定后滚动到目标
34
- // (浏览器原生锚点滚动发生在布局完成前,长文章的懒加载图片/异步组件会把目标顶偏)
35
- const hash = window.location.hash;
36
- if (hash.length > 1) {
37
- let anchorId = hash.slice(1);
38
- try {
39
- anchorId = decodeURIComponent(anchorId);
40
- } catch (e) {
41
- // 片段含非法编码时按原样查找
42
- }
43
- const anchor = document.getElementById(anchorId);
44
- if (anchor) {
45
- // #start 锚点贴顶,不预留 offset;其余锚点与 TOC 点击滚动保持一致
46
- const offset = anchorId === 'start' ? 0 : 32;
47
- const targetY = () => anchor.getBoundingClientRect().top + window.scrollY - offset;
48
- let timer = null;
49
- let lastY = null;
50
- let quiet = 0;
51
- let attempts = 0;
52
- // 记录最近一次资源加载完成时间,判断异步数据/图片是否仍在改变布局
53
- let lastResourceTime = performance.now();
54
- let observer = null;
55
- try {
56
- observer = new PerformanceObserver(function (list) {
57
- lastResourceTime = performance.now();
58
- });
59
- observer.observe({ entryTypes: ['resource'] });
60
- } catch (e) {}
61
- // 只跟踪主题 request/cache 客户端,不替换浏览器原生 fetch/XHR。
62
- const pendingRequests = new Set();
63
- const onRequestStart = event => pendingRequests.add(event.detail.key);
64
- const onRequestEnd = event => pendingRequests.delete(event.detail.key);
65
- document.addEventListener('stellar:request-start', onRequestStart);
66
- document.addEventListener('stellar:request-end', onRequestEnd);
67
- const cancel = () => {
68
- if (timer !== null) {
69
- clearInterval(timer);
70
- timer = null;
71
- }
72
- if (observer !== null) {
73
- try {
74
- observer.disconnect();
75
- } catch (e) {}
76
- }
77
- document.removeEventListener('stellar:request-start', onRequestStart);
78
- document.removeEventListener('stellar:request-end', onRequestEnd);
79
- };
80
- const adjust = () => {
81
- const y = targetY();
82
- const atTarget = Math.abs(window.scrollY - y) <= 2;
83
- if (!atTarget || lastY === null || Math.abs(y - lastY) > 2) {
84
- window.scrollTo({ top: y, behavior: 'instant' });
85
- lastY = y;
86
- quiet = 0;
87
- } else {
88
- quiet++;
89
- }
90
- attempts++;
91
- const resourceQuiet = performance.now() - lastResourceTime > 3000;
92
- // 位置连续 1.8 秒无变化、无进行中的请求且最近 3 秒无资源加载才视为稳定;最长等待 30 秒
93
- if ((quiet >= 3 && pendingRequests.size === 0 && resourceQuiet) || attempts >= 50) {
94
- cancel();
95
- }
96
- };
97
- requestAnimationFrame(() => {
98
- // 锚点链接打开时直接定位,不做动画;用户主动点击(如 TOC)仍走平滑滚动
99
- window.scrollTo({ top: targetY(), behavior: 'instant' });
100
- timer = setInterval(adjust, 600);
101
- });
102
- // 用户主动滚动时停止自动校正
103
- window.addEventListener('wheel', cancel, { passive: true });
104
- window.addEventListener('touchstart', cancel, { passive: true });
105
- window.addEventListener('mousedown', cancel, { passive: true });
106
- window.addEventListener('keydown', cancel, { passive: true });
107
- return;
108
- }
109
- }
110
- // 没有锚点时始终保持页面顶部;Wiki Hero 只在用户显式访问 #start 时定位正文。
111
- window.scrollTo(0, 0);
112
- };
113
- if (document.readyState !== 'loading') scrollHandler();
114
- else document.addEventListener('DOMContentLoaded', scrollHandler);
115
- })();
24
+ <script data-stellar-script="defines" type="text/javascript">
25
+ window.canonical = <%- script_json(canonicalWithEncoded) %>;
26
+ window.canonical["param"] = <%- script_json({permalink, checklink}) %>;
116
27
  const ctx = {
117
28
  date_suffix: {
118
- just: `<%- __('meta.date_suffix.just') %>`,
119
- min: `<%- __('meta.date_suffix.min') %>`,
120
- hour: `<%- __('meta.date_suffix.hour') %>`,
121
- day: `<%- __('meta.date_suffix.day') %>`,
29
+ just: <%- script_json(__('meta.date_suffix.just')) %>,
30
+ min: <%- script_json(__('meta.date_suffix.min')) %>,
31
+ hour: <%- script_json(__('meta.date_suffix.hour')) %>,
32
+ day: <%- script_json(__('meta.date_suffix.day')) %>,
122
33
  },
123
- root : `<%- config.root %>`,
34
+ root : <%- script_json(config.root) %>,
124
35
  tag_plugins: {
125
- chat: {api: <%- JSON.stringify(assets.services.chat.endpoint) %>},
36
+ chat: {api: <%- script_json(assets.services.chat.endpoint) %>},
126
37
  },
127
38
  // 图标由构建期生成的 /js/stellar-icons.js 填充(defer,早于 DOMContentLoaded 就绪)
128
39
  icons: {},
129
- ui: { classes: <%- JSON.stringify(ui_capabilities()) %> },
130
- services: <%- JSON.stringify(clientServices) %>,
40
+ ui: { classes: <%- script_json(ui_capabilities()) %> },
41
+ services: <%- script_json(clientServices) %>,
131
42
  };
132
43
 
133
44
  // required plugins (only load if needs)
134
- if (`<%- searchProvider || '' %>`) {
135
- ctx.search = {};
136
- ctx.search.service = `<%- searchProvider || '' %>`;
45
+ if (<%- script_json(searchProvider || '') %>) {
46
+ ctx.search = {
47
+ more: <%- script_json(__('meta.more')) %>,
48
+ no_more: <%- script_json(__('search.no_more')) %>
49
+ };
50
+ ctx.search.service = <%- script_json(searchProvider || '') %>;
137
51
  if (ctx.search.service == 'local_search') {
138
- let service_obj = <%- JSON.stringify({
52
+ let service_obj = <%- script_json({
139
53
  field: localSearch.scope,
140
54
  path: '/search.json',
141
55
  content: localSearch.includeContent,
@@ -147,24 +61,11 @@
147
61
  }
148
62
  }
149
63
  const def = {
150
- avatar: `<%- stellar_config('fallbacks.avatar') %>`,
151
- cover: `<%- stellar_config('fallbacks.cover') %>`,
64
+ avatar: <%- script_json(stellar_config('fallbacks.avatar')) %>,
65
+ cover: <%- script_json(stellar_config('fallbacks.cover')) %>,
152
66
  };
153
67
  // 经典脚本仍消费这些只读投影。
154
68
  window.ctx = ctx;
155
69
  window.def = def;
156
70
 
157
- // 搜索跳转高亮:URL 带 ?kw= 时按需加载,定位正文中的匹配词
158
- (function () {
159
- try {
160
- if (!window.location.search) return;
161
- if (!new URLSearchParams(window.location.search).has('kw')) return;
162
- document.addEventListener('DOMContentLoaded', function () {
163
- // utils 由前置 /js/utils.js 同步定义(同时暴露 window.utils)。
164
- if (typeof utils === 'object' && typeof utils.js === 'function') {
165
- utils.js('<%- url_for('/js/search/highlight.js') %>', { defer: true }).catch(function () {});
166
- }
167
- });
168
- } catch (e) {}
169
- })();
170
71
  </script>
@@ -48,8 +48,10 @@ var runtimeJson = browser_runtime_manifest({
48
48
  messages: runtimeMessages
49
49
  })
50
50
  %>
51
- <% if (runtimeMathProvider === 'katex') { %>
52
- <link rel="stylesheet" href="<%- url_for(runtimeAssets.features.katexCss) %>" integrity="sha384-//SZkxyB7axjCAopkAL1E1rve+ZSPKapD89Lo/lLhcsXR+zOYl5z6zJZEFXil+q0" crossorigin="anonymous">
51
+ <% if (runtimeMathProvider === 'katex') {
52
+ var katexStyle = katex_stylesheet()
53
+ %>
54
+ <link data-stellar-page-style rel="stylesheet" href="<%= url_for(katexStyle.css) %>"<% if (katexStyle.integrity) { %> integrity="<%= katexStyle.integrity %>" crossorigin="anonymous"<% } %>>
53
55
  <% } %>
54
- <script type="application/json" id="stellar-runtime-config"><%- runtimeJson %></script>
55
- <script type="module" src="<%- url_for(runtimeAssets.runtime.bootstrap) %>?v=<%- stellar_info('version') %>&runtime=1"></script>
56
+ <script data-stellar-script="core" type="application/json" id="stellar-runtime-config"><%- runtimeJson %></script>
57
+ <script data-stellar-script="core" type="module" src="<%- url_for(runtimeAssets.runtime.bootstrap) %>?v=<%- stellar_info('version') %>&runtime=1"></script>
@@ -5,41 +5,21 @@ function custom_inject() {
5
5
  return stellar_inject('bodyEnd', renderViewModel ? renderViewModel.render.document.bodyEndInject : content_config(page).inject?.bodyEnd)
6
6
  }
7
7
 
8
- // tagtree 脚本仅在最终 Region 实例包含 tagtree 时加载。
9
- function tagtreeWidgetOnPage() {
10
- var layout = renderViewModel?.render.layout
11
- || page.notebookIndex?.collection?.layout
12
- || {};
13
- var regions = [layout.topbar, layout.leftbar, layout.rightbar];
14
- var widgets = regions.flatMap(function (region) {
15
- return Array.isArray(region?.widgets) ? region.widgets : []
16
- })
17
- if (!Array.isArray(widgets)) {
18
- return false;
19
- }
20
- return widgets.some(function (w) {
21
- if (w && typeof w === 'object') {
22
- return w.id === 'tagtree' || w.layout === 'tagtree' || w.override === 'tagtree';
23
- }
24
- return false;
25
- });
26
- }
27
8
 
28
9
  %>
29
10
  <%- partial('scripts/defines') %>
30
11
 
31
12
  <!-- core: 迁移期 DOM/资源兼容工具;Extension 注册与生命周期由 ESM runtime 所有 -->
32
- <script src="<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>"></script>
33
- <script defer src="<%- url_for('/js/stellar-icons.js') %>?v=<%- stellar_info('version') %>"></script>
34
- <script src="<%- url_for(`${stellar_info('main_js')}?v=${stellar_info('version')}`) %>" defer></script>
35
- <script defer src="<%- url_for('/js/profile.js') %>?v=<%- stellar_info('version') %>"></script>
13
+ <script data-stellar-script="core" src="<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>"></script>
14
+ <script data-stellar-script="core" defer src="<%- url_for('/js/stellar-icons.js') %>?v=<%- stellar_info('version') %>"></script>
15
+ <script data-stellar-script="core" src="<%- url_for(`${stellar_info('main_js')}?v=${stellar_info('version')}`) %>" defer></script>
16
+ <script data-stellar-script="core" defer src="<%- url_for('/js/profile.js') %>?v=<%- stellar_info('version') %>"></script>
17
+
36
18
 
37
- <% if (tagtreeWidgetOnPage()) { %>
38
- <script>window.__STELLAR_TAGTREE__ = { expand_active: <%- JSON.stringify(!!(stellar_data('widgets').tagtree && stellar_data('widgets').tagtree.expand_active)) %> };</script>
39
- <script defer src="<%- url_for('/js/tagtree.js') %>?v=<%- stellar_info('version') %>"></script>
40
- <% } %>
41
19
 
42
20
  <%- partial('scripts/runtime', {viewModel: typeof viewModel !== 'undefined' ? viewModel : null}) %>
43
21
 
44
22
  <!-- inject -->
45
23
  <%- custom_inject() %>
24
+
25
+ <script data-stellar-script="core" defer src="<%- url_for('/js/navigation-init.js') %>?v=<%- stellar_info('version') %>"></script>
@@ -1,7 +1,7 @@
1
1
  <%
2
2
  var localSearch = stellar_config('search.provider') === 'local'
3
3
  %>
4
- <dialog class="search-dialog" id="site-search-dialog" aria-labelledby="site-search-title">
4
+ <dialog data-stellar-runtime-region class="search-dialog" id="site-search-dialog" aria-labelledby="site-search-title">
5
5
  <div class="search-dialog__panel search-wrapper" id="site-search-wrapper" searching="false">
6
6
  <div class="search-dialog__header">
7
7
  <div class="search-form" role="search">
@@ -4,7 +4,7 @@ var brandPlacement = typeof placement !== 'undefined' ? placement : 'leftbar'
4
4
  var activeViewModel = typeof viewModel !== 'undefined' ? viewModel : null
5
5
  var brandStyle = brandPlacement === 'topbar'
6
6
  ? 'topbar'
7
- : brand.style === 'compact' ? 'compact' : 'regular'
7
+ : brandPlacement === 'main' || brand.style === 'compact' ? 'compact' : 'regular'
8
8
 
9
9
  function layoutBrandNavigation() {
10
10
  if (brandPlacement !== 'leftbar' || brand.source !== 'collection' || brand.backButton !== true) return ''
@@ -82,7 +82,7 @@ function layoutCollectionStats(apiBase) {
82
82
  }
83
83
 
84
84
  function layoutSiteStats(apiBase) {
85
- var username = typeof brandGithubUsername === 'function' ? brandGithubUsername() : ''
85
+ var username = typeof brand.ghuser === 'string' ? brand.ghuser.trim() : ''
86
86
  if (!username) return ''
87
87
  var encodedUsername = encodeURIComponent(username)
88
88
  var githubHref = `https://github.com/${encodedUsername}`
@@ -8,7 +8,7 @@ function relatedPostsInTopic() {
8
8
  return {
9
9
  href: pretty_url(post.path),
10
10
  title: post.title,
11
- active: post.current
11
+ active: post.id === topicViewModel.item.id
12
12
  }
13
13
  })
14
14
  var body = partial('_partial/components/collection', {options: {
@@ -75,7 +75,7 @@ function layoutDiv() {
75
75
  className: 'tag-tree-widget',
76
76
  title: __('meta.tag_tree'),
77
77
  body: body,
78
- attrs: {'data-notebook': notebookId}
78
+ attrs: {'data-notebook': notebookId, 'data-expand-active': !!item.expand_active}
79
79
  })
80
80
  }
81
81
  %>
@@ -9,11 +9,6 @@ function layoutArchiveList() {
9
9
  var cls = ''
10
10
  if (page.author) {
11
11
  page.title = page.author.name
12
- page.banner = {
13
- image: page.author.banner,
14
- avatar: page.author.avatar,
15
- tagline: page.author.description
16
- }
17
12
  el += partial('_partial/main/navbar/article_banner')
18
13
  cls += ' author'
19
14
  } else {
@@ -21,7 +16,7 @@ function layoutArchiveList() {
21
16
  el += partial('_partial/main/listing_nav/blog')
22
17
  }
23
18
  el += `<div class="post-list${cls} archives">`
24
- var years = []
19
+ var groups = new Map()
25
20
  function requirePostViewModel(post) {
26
21
  var viewModel = post_view_model(post)
27
22
  if (!['post', 'topic'].includes(viewModel?.collection?.profile) || !viewModel.render?.listing) {
@@ -29,26 +24,22 @@ function layoutArchiveList() {
29
24
  }
30
25
  return viewModel
31
26
  }
32
- var posts = site.posts.filter(function(post) {
33
- var listing = requirePostViewModel(post).render.listing
34
- if (!listing.listed) return false
35
- return page.author == null || listing.authorId == page.author.id
36
- })
37
- posts.sort('date', -1).each(function(post) {
38
- post.year = date(post.date, 'YYYY')
39
- if (post.year && (years.includes(post.year) == false) && (post.title || post.date)) {
40
- years.push(post.year)
41
- }
27
+ site.posts.sort('date', -1).each(function(post) {
28
+ var viewModel = requirePostViewModel(post)
29
+ var listing = viewModel.render.listing
30
+ if (!listing.listed) return
31
+ if (page.author != null && listing.authorId != page.author.id) return
32
+ var year = date(post.date, 'YYYY')
33
+ if (!year || !(post.title || post.date)) return
34
+ if (!groups.has(year)) groups.set(year, [])
35
+ groups.get(year).push({viewModel: viewModel})
42
36
  })
43
- for (var year of years) {
37
+ for (var [year, records] of groups) {
44
38
  el += `<article class="${scrollreveal('')}" id="archive">`
45
39
  el += `<div class='archive-header h4'>${year}</div>`
46
- posts.sort('date', -1).filter(function (post) {
47
- post.year = date(post.date, 'YYYY')
48
- return post.year == year
49
- }).each(function(post) {
50
- el += partial('_partial/main/post_list/archive_item', {viewModel: requirePostViewModel(post)})
51
- })
40
+ for (var record of records) {
41
+ el += partial('_partial/main/post_list/archive_item', record)
42
+ }
52
43
  el += `</article>`
53
44
  }
54
45
  el += `</div>`
package/layout/index.ejs CHANGED
@@ -14,16 +14,20 @@ if (homeCmt && homeCmt !== false && is_home_first_page()) {
14
14
  var articleDefaults = stellar_config('article')
15
15
  var pinStyle = articleDefaults.listing.pinnedLayout
16
16
  var pinFlat = pinStyle === 'flat'
17
+ var postViewModels = new Map()
17
18
 
18
19
  // 与 pin_slider.ejs 保持一致的置顶判定与排序权重
19
20
  function pinValue(post) {
20
21
  return postListing(post).priority
21
22
  }
22
23
  function requirePostViewModel(post) {
24
+ var key = post.source || post.path || post._id || post
25
+ if (postViewModels.has(key)) return postViewModels.get(key)
23
26
  var viewModel = post_view_model(post)
24
27
  if (!['post', 'topic'].includes(viewModel?.collection?.profile) || !viewModel.render?.listing) {
25
28
  throw new Error(`Stellar v2: Post ${post?.source || post?.path || '<unknown>'} 缺少列表 ViewModel`)
26
29
  }
30
+ postViewModels.set(key, viewModel)
27
31
  return viewModel
28
32
  }
29
33
  function postListing(post) {
package/layout/layout.ejs CHANGED
@@ -2,7 +2,7 @@
2
2
  var pageConfig = content_config(page)
3
3
  var isOrdinaryPost = page.layout === 'post' && pageConfig.collection?.profile !== 'topic'
4
4
  var activeViewModel = (isOrdinaryPost || pageConfig.collection?.profile === 'topic')
5
- ? post_view_model(page)
5
+ ? (page.viewModel || post_view_model(page))
6
6
  : page.viewModel
7
7
  var activeProfile = activeViewModel?.collection?.profile
8
8
  if (isOrdinaryPost && (activeProfile !== 'post' || activeViewModel?.render == null)) {
@@ -30,7 +30,7 @@ function siteBackground(enabled) {
30
30
 
31
31
  function coverSlot(viewModel) {
32
32
  var coverBody = viewModel ? partial('_partial/cover/index', {viewModel: viewModel}).trim() : ''
33
- return `<div id="site-cover" class="site-cover">${coverBody}</div>`
33
+ return `<div data-stellar-runtime-region id="site-cover" class="site-cover">${coverBody}</div>`
34
34
  }
35
35
 
36
36
  function regionSlot(slot, state, viewModel) {
package/layout/page.ejs CHANGED
@@ -2,7 +2,7 @@
2
2
  var pageConfig = content_config(page)
3
3
  var registeredProfile = pageConfig.collection?.profile
4
4
  var postViewModel = page.layout === 'post' && (registeredProfile == null || registeredProfile === 'topic')
5
- ? post_view_model(page)
5
+ ? (page.viewModel || post_view_model(page))
6
6
  : null
7
7
  var wikiViewModel = page.viewModel?.collection?.profile === 'wiki' ? page.viewModel : null
8
8
  var notebookViewModel = page.viewModel?.collection?.profile === 'notebook' ? page.viewModel : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.4",
4
4
  "description": "An all-in-one Hexo theme for blogs, project documentation, knowledge bases, series, and notebooks.",
5
5
  "main": "package.json",
6
6
  "scripts": {
@@ -46,17 +46,20 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "cheerio": "^1.1.0",
49
- "glob": "^10.4.0",
50
49
  "hexo-front-matter": "^4.2.1",
50
+ "hexo-pagination": "^3.0.0",
51
51
  "hexo-renderer-ejs": "^2.0.0",
52
52
  "hexo-renderer-stylus": "^3.0.1",
53
- "hexo-pagination": "^3.0.0",
54
53
  "js-yaml": "^4.1.0",
55
- "probe-image-size": "^7.2.3"
54
+ "probe-image-size": "^7.2.3",
55
+ "sharp": "^0.34.5"
56
56
  },
57
57
  "homepage": "https://xaoxuu.com/wiki/stellar/",
58
58
  "devDependencies": {
59
+ "@eslint/js": "^9.39.5",
59
60
  "eslint": "^9.39.5",
61
+ "glob": "^10.4.0",
62
+ "globals": "^14.0.0",
60
63
  "hexo-util": "^4.0.0"
61
64
  }
62
65
  }
@@ -14,13 +14,16 @@ function hexoVersion(ctx) {
14
14
  }
15
15
 
16
16
  hexo.extend.console.register("stellar", "Diagnose and author a Stellar v2 site.", {
17
- usage: "<doctor|new note> [options]",
17
+ usage: "<doctor|new note|images> [options]",
18
18
  commands: [
19
+ { name: "images", desc: "Incrementally prepare persistent image dimensions and colors without changing Markdown." },
19
20
  { name: "doctor", desc: "Validate the environment and v2 configuration." },
20
21
  { name: "new note", desc: "Create a Note in a known Notebook." }
21
22
  ],
22
23
  options: [
23
- { name: "--dry-run", desc: "Print the new note plan without writing files." },
24
+ { name: "--dry-run", desc: "Preview note creation or missing image metadata without writing files or downloading images." },
25
+ { name: "--page <route>", desc: "Limit image discovery to one generated HTML page." },
26
+ { name: "--refresh <url>", desc: "Refresh one image even when metadata already exists." },
24
27
  { name: "--format <text|json>", desc: "Doctor output format; use Hexo global --silent with JSON." },
25
28
  { name: "--notebook <id>", desc: "Notebook id for stellar new note." },
26
29
  { name: "--title <title>", desc: "Note title and filename for stellar new note." },
@@ -28,6 +31,15 @@ hexo.extend.console.register("stellar", "Diagnose and author a Stellar v2 site."
28
31
  ]
29
32
  }, async function (args) {
30
33
  const subcommand = args._[0];
34
+ if (subcommand === "images") {
35
+ this.log?.info?.("Image metadata: rendering pages for image discovery…");
36
+ await this.load();
37
+ const report = await require("../lib/image-metadata").prepareImages(this, {
38
+ dryRun: args.dryRun, page: args.page, refresh: args.refresh
39
+ });
40
+ console.log(JSON.stringify(report, null, 2));
41
+ return report;
42
+ }
31
43
  if (subcommand === "doctor") {
32
44
  const format = args.format || "text";
33
45
  if (!["text", "json"].includes(format)) throw new Error("--format 必须是 text 或 json");
@@ -51,5 +63,5 @@ hexo.extend.console.register("stellar", "Diagnose and author a Stellar v2 site."
51
63
  }
52
64
  return plan;
53
65
  }
54
- throw new Error("Usage: hexo stellar <doctor|new note>");
66
+ throw new Error("Usage: hexo stellar <doctor|new note|images>");
55
67
  });
@@ -4,6 +4,10 @@
4
4
 
5
5
  require('./lib/config-hot-reload').registerConfigHotReload(hexo);
6
6
 
7
+ hexo.on('ready', () => {
8
+ require('../lib/image-metadata').excludeMetadataFromSource(hexo);
9
+ });
10
+
7
11
  hexo.on('generateBefore', () => {
8
12
  // 页面路径归一化:xxx.html → xxx/,必须先于所有读取 page.path 的逻辑
9
13
  require('./lib/path_normalize')(hexo);
@@ -47,27 +51,10 @@ hexo.on('ready', () => {
47
51
  checkVersion(hexo, { useCache: true });
48
52
  });
49
53
 
50
- // 防止重复注册
51
- let hasRun = false;
52
-
53
- hexo.extend.filter.register('before_generate', async () => {
54
- const isDev = ['s', 'server', 'serve'].some(arg => process.argv.includes(arg));
55
- if (!isDev) {
56
- return;
57
- }
58
-
59
- if (hasRun) return;
60
- hasRun = true;
61
-
62
- // 读取主题配置开关
63
- const enabled = hexo.stellar.config.features.lazyLoading.autoAspectRatio === true;
64
-
65
- const generateImageRatios = require('./lib/get_image_ratios');
66
- const fixMarkdownImages = require('./lib/fix_image_tags');
67
-
68
- if (enabled) {
69
- // 构建前:生成缓存 + 写回 Markdown
70
- await generateImageRatios(hexo);
71
- fixMarkdownImages(hexo); // 不用 await,因为是同步的
72
- }
54
+ // Complete missing metadata before generate writes routes or server exposes them.
55
+ // The explicit CLI owns its own preprocessing pass and must not recurse here.
56
+ hexo.extend.filter.register('after_generate', async () => {
57
+ if (!['generate', 'g', 'server', 's', 'serve'].includes(hexo.env?.cmd)) return;
58
+ if (hexo.stellar?.config?.features?.imageOptimization?.enabled !== true) return;
59
+ await require('../lib/image-metadata').prepareBuildImages(hexo);
73
60
  });
@@ -13,6 +13,7 @@ module.exports = ctx => {
13
13
  // url
14
14
  for (let key of Object.keys(authors)) {
15
15
  let author = authors[key]
16
+ author.id = key
16
17
  author.path = `${basePath}/${key}/index.html`
17
18
  }
18
19
  // default author
@@ -11,16 +11,14 @@ const { getCollectionId } = require("../../lib/content-config");
11
11
  const { buildWikiTree } = require("../../lib/doc_tree");
12
12
  const { requireLayoutProfiles } = require("../../lib/layout-config");
13
13
  const {
14
+ buildWikiCollectionModel,
14
15
  buildWikiListingRender,
15
16
  buildWikiPageViewModelBase,
16
17
  buildWikiRelated,
17
18
  completeWikiPageViewModel
18
19
  } = require("../../lib/models");
19
20
  const { ensureRuntimeData } = require("../../lib/runtime-data");
20
- const {
21
- setProfileViewModelBase,
22
- setProfileViewModelInput
23
- } = require("../../lib/page-view-model-registry");
21
+ const { pageViewModelsFor } = require("../../lib/page-view-model-registry");
24
22
  const { sourcePathForData } = require("../../lib/source-config");
25
23
 
26
24
  function cloneConfig(value) {
@@ -49,6 +47,7 @@ module.exports = (ctx, pipeline) => {
49
47
  const runtimeData = ensureRuntimeData(ctx);
50
48
  runtimeData.wiki = wiki;
51
49
 
50
+ const collectionModels = new Map();
52
51
  const entries = [];
53
52
  const homepageEntries = new Map();
54
53
  for (const record of records) {
@@ -66,6 +65,8 @@ module.exports = (ctx, pipeline) => {
66
65
  collectionListed: wiki.shelf.includes(collectionId),
67
66
  isBackup: process.env.IS_BACKUP === "true"
68
67
  };
68
+ if (!collectionModels.has(collectionId)) collectionModels.set(collectionId, buildWikiCollectionModel(input, collectionId));
69
+ input.collectionModel = collectionModels.get(collectionId);
69
70
  const base = buildWikiPageViewModelBase(input);
70
71
  const entry = { page, collectionId, input, base };
71
72
  entries.push(entry);
@@ -92,9 +93,9 @@ module.exports = (ctx, pipeline) => {
92
93
  related,
93
94
  listing: listings.get(entry.collectionId)
94
95
  });
95
- entry.page.viewModel = completeWikiPageViewModel(completeInput, entry.base);
96
- setProfileViewModelInput("wiki", entry.page, completeInput);
97
- setProfileViewModelBase("wiki", entry.page, entry.base);
96
+ pageViewModelsFor(ctx).setPageViewModel(entry.page, completeWikiPageViewModel(completeInput, entry.base));
97
+ pageViewModelsFor(ctx).setProfileViewModelInput("wiki", entry.page, completeInput);
98
+ pageViewModelsFor(ctx).setProfileViewModelBase("wiki", entry.page, entry.base);
98
99
  }
99
100
 
100
101
  wiki.index = {