hexo-theme-stellar 1.40.0 → 1.42.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 (146) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +1 -2
  2. package/CHANGELOG.md +90 -0
  3. package/CONTRIBUTING.md +2 -1
  4. package/README.md +50 -21
  5. package/README_EN.md +113 -0
  6. package/_config.yml +40 -12
  7. package/_data/icons.yml +248 -37
  8. package/languages/en.yml +6 -0
  9. package/languages/zh-CN.yml +6 -0
  10. package/languages/zh-TW.yml +6 -0
  11. package/layout/_partial/comments/artalk/script.ejs +25 -8
  12. package/layout/_partial/comments/beaudar/script.ejs +1 -0
  13. package/layout/_partial/comments/twikoo/script.ejs +1 -0
  14. package/layout/_partial/comments/utterances/script.ejs +1 -0
  15. package/layout/_partial/comments/waline/script.ejs +1 -0
  16. package/layout/_partial/cover/index.ejs +0 -3
  17. package/layout/_partial/head.ejs +43 -7
  18. package/layout/_partial/main/article/article_tags.ejs +16 -0
  19. package/layout/_partial/main/navbar/article_banner.ejs +33 -20
  20. package/layout/_partial/main/notebook/note_card.ejs +1 -1
  21. package/layout/_partial/main/notebook/paginator.ejs +2 -2
  22. package/layout/_partial/main/pin_slider.ejs +61 -30
  23. package/layout/_partial/main/post_list/latest_post_card.ejs +25 -0
  24. package/layout/_partial/main/post_list/paginator.ejs +2 -2
  25. package/layout/_partial/main/post_list/post_card.ejs +16 -64
  26. package/layout/_partial/main/post_list/topic_card.ejs +26 -5
  27. package/layout/_partial/menubtn.ejs +2 -2
  28. package/layout/_partial/scripts/bootstrap.ejs +35 -0
  29. package/layout/_partial/scripts/defines.ejs +27 -2
  30. package/layout/_partial/scripts.ejs +39 -7
  31. package/layout/_partial/sidebar/logo.ejs +1 -1
  32. package/layout/_partial/sidebar/menu.ejs +1 -1
  33. package/layout/_partial/sidebar/search.ejs +1 -1
  34. package/layout/_partial/widgets/author.ejs +1 -1
  35. package/layout/_partial/widgets/components/link.ejs +2 -2
  36. package/layout/_partial/widgets/ghuser.ejs +1 -1
  37. package/layout/_partial/widgets/related.ejs +1 -1
  38. package/layout/_partial/widgets/toc.ejs +3 -3
  39. package/layout/_partial/widgets/tree.ejs +1 -1
  40. package/layout/_plugins/adaptive_text.ejs +11 -0
  41. package/layout/_plugins/copycode.ejs +2 -2
  42. package/layout/_plugins/fancybox.ejs +4 -2
  43. package/layout/_plugins/mermaid.ejs +3 -2
  44. package/layout/_plugins/scrollreveal.ejs +22 -16
  45. package/layout/_plugins/search/local_search.ejs +4 -3
  46. package/layout/_plugins/swiper.ejs +2 -1
  47. package/layout/categories.ejs +1 -1
  48. package/layout/index.ejs +10 -2
  49. package/layout/index_topic.ejs +4 -4
  50. package/layout/page.ejs +3 -0
  51. package/layout/tags.ejs +1 -0
  52. package/package.json +3 -3
  53. package/scripts/events/lib/doc_tree.js +12 -208
  54. package/scripts/events/lib/notebooks.js +8 -148
  55. package/scripts/events/lib/utils.js +10 -1
  56. package/scripts/filters/index.js +1 -0
  57. package/scripts/filters/lib/img_lazyload.js +4 -0
  58. package/scripts/filters/lib/img_onerror.js +4 -0
  59. package/scripts/filters/lib/md_table.js +46 -0
  60. package/scripts/generators/search.js +12 -18
  61. package/scripts/generators/stellar-icons.js +47 -0
  62. package/scripts/helpers/ai_label.js +29 -0
  63. package/scripts/helpers/icon.js +2 -2
  64. package/scripts/helpers/json_ld.js +10 -13
  65. package/scripts/helpers/related_posts.js +0 -7
  66. package/scripts/helpers/seo.js +9 -0
  67. package/scripts/helpers/subtitle.js +8 -0
  68. package/scripts/lib/ai_label.js +55 -0
  69. package/scripts/lib/doc_tree.js +267 -0
  70. package/scripts/lib/notebooks.js +181 -0
  71. package/scripts/lib/search_index.js +82 -0
  72. package/scripts/lib/seo.js +79 -0
  73. package/scripts/lib/subtitle.js +24 -0
  74. package/scripts/tags/lib/about.js +2 -2
  75. package/scripts/tags/lib/albums.js +1 -1
  76. package/scripts/tags/lib/banner.js +5 -4
  77. package/scripts/tags/lib/chat.js +49 -49
  78. package/scripts/tags/lib/copy.js +1 -1
  79. package/scripts/tags/lib/friends.js +1 -1
  80. package/scripts/tags/lib/gallery.js +1 -1
  81. package/scripts/tags/lib/hashtag.js +1 -1
  82. package/scripts/tags/lib/image.js +3 -3
  83. package/scripts/tags/lib/posters.js +1 -1
  84. package/scripts/tags/lib/sites.js +2 -2
  85. package/source/css/_common/base.styl +5 -9
  86. package/source/css/_common/cover-overlay.styl +111 -0
  87. package/source/css/_common/device.styl +3 -3
  88. package/source/css/_common/title.styl +17 -0
  89. package/source/css/_components/layout.styl +9 -13
  90. package/source/css/_components/list.styl +135 -64
  91. package/source/css/_components/main.styl +2 -2
  92. package/source/css/_components/md-table.styl +23 -0
  93. package/source/css/_components/pages/archives.styl +5 -22
  94. package/source/css/_components/pages/article-story.styl +5 -14
  95. package/source/css/_components/partial/article-banner.styl +61 -37
  96. package/source/css/_components/partial/article-tags.styl +13 -0
  97. package/source/css/_components/partial/bread-nav.styl +19 -12
  98. package/source/css/_components/partial/navbar.styl +11 -3
  99. package/source/css/_components/partial/post-panel.styl +76 -0
  100. package/source/css/_components/pin-slider.styl +38 -41
  101. package/source/css/_components/sidebar/footer.styl +1 -1
  102. package/source/css/_components/sidebar/logo.styl +2 -2
  103. package/source/css/_components/sidebar/search.styl +25 -5
  104. package/source/css/_components/sidebar/sidebar.styl +7 -7
  105. package/source/css/_components/tag-plugins/banner.styl +53 -11
  106. package/source/css/_components/tag-plugins/friends_posts.styl +5 -86
  107. package/source/css/_components/tag-plugins/gallery.styl +3 -0
  108. package/source/css/_components/tag-plugins/read/paper.styl +3 -3
  109. package/source/css/_components/tag-plugins/read/reel.styl +4 -4
  110. package/source/css/_components/tag-plugins/table.styl +16 -11
  111. package/source/css/_components/tag-plugins/timeline.styl +4 -0
  112. package/source/css/_components/widgets/components.styl +6 -0
  113. package/source/css/_components/widgets/list.styl +6 -0
  114. package/source/css/_components/widgets/timeline.styl +2 -2
  115. package/source/css/_components/widgets/toc.styl +13 -7
  116. package/source/css/_components/widgets/widgets.styl +8 -7
  117. package/source/css/_custom.styl +9 -21
  118. package/source/css/_defines/func.styl +28 -3
  119. package/source/css/_plugins/index.styl +0 -18
  120. package/source/css/_plugins/lazyload.styl +1 -0
  121. package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
  122. package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
  123. package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
  124. package/source/css/{_plugins → plugins}/swiper.styl +5 -1
  125. package/source/js/color.js +325 -0
  126. package/source/js/icons.js +60 -0
  127. package/source/js/main.js +115 -1
  128. package/source/js/plugins/adaptive-text.js +146 -0
  129. package/source/js/search/highlight.js +82 -0
  130. package/source/js/search/local-search.js +294 -103
  131. package/source/js/services/friends_and_posts.js +1 -1
  132. package/source/js/services/timeline.js +1 -1
  133. package/source/js/services/weibo.js +3 -3
  134. package/source/js/services.js +179 -0
  135. package/source/js/tagtree.js +29 -0
  136. package/source/js/theme.js +61 -0
  137. package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +27 -11
  138. package/layout/_partial/cover/post_cover.ejs +0 -37
  139. package/layout/_partial/scripts/services.ejs +0 -178
  140. package/layout/_partial/scripts/sidebar.ejs +0 -25
  141. package/layout/_partial/scripts/tagtree.ejs +0 -29
  142. package/layout/_partial/scripts/theme.ejs +0 -67
  143. /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
  144. /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
  145. /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
  146. /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
@@ -0,0 +1,179 @@
1
+ const initServices = () => {
2
+ // 用于存储需要清理的资源
3
+ let timers = [];
4
+
5
+ for (let id of Object.keys(ctx.services)) {
6
+ const js = ctx.services[id].js;
7
+ if (id == 'siteinfo') {
8
+ const cardlinks = document.querySelectorAll('a.link-card[cardlink]');
9
+ if (cardlinks?.length > 0) {
10
+ utils.js(js, { defer: true }).then(function () {
11
+ setCardLink(cardlinks);
12
+ });
13
+ }
14
+ } else if (id == 'ghinfo') {
15
+ const els = document.querySelectorAll('.ds-ghinfo');
16
+ if (els.length > 0) {
17
+ utils.js(js, { defer: true });
18
+ }
19
+ } else if (id == 'voice') {
20
+ const voiceAudios = document.querySelectorAll('.voice>audio');
21
+ if (voiceAudios?.length > 0) {
22
+ utils.js(js, { defer: true }).then(function () {
23
+ createVoiceDom(voiceAudios);
24
+ });
25
+ }
26
+ } else if (id == 'video') {
27
+ const videos = document.querySelectorAll('.video>video');
28
+ if (videos?.length > 0) {
29
+ utils.js(js, { defer: true }).then(function () {
30
+ videoEvents(videos);
31
+ });
32
+ }
33
+ } else if (id == 'download-file') {
34
+ const files = document.querySelectorAll('.chat-file');
35
+ if (files?.length > 0) {
36
+ utils.js(js, { defer: true }).then(function () {
37
+ downloadFileEvent(files);
38
+ });
39
+ }
40
+ } else {
41
+ const els = document.getElementsByClassName(`ds-${id}`);
42
+ if (els?.length > 0) {
43
+ if (id == 'timeline' || id == 'memos' || id == 'marked' || id == 'mdrender') {
44
+ utils.js(deps.marked).then(function () {
45
+ utils.js(js, { defer: true });
46
+ });
47
+ } else {
48
+ utils.js(js, { defer: true });
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ // search
55
+ if (ctx.search && typeof searchFunc === 'function') {
56
+ const searchCfg = ctx.search.local_search;
57
+ const lazyLoad = searchCfg ? searchCfg.lazy_load !== false : true;
58
+ // 懒加载模式下由 local-search.js 的聚焦事件负责初始化,此处跳过
59
+ if (!lazyLoad) {
60
+ const inputArea = document.querySelector("input#search-input");
61
+ if (inputArea && !inputArea._searchInitialized) {
62
+ const path = ctx.search.path.startsWith('/') ? ctx.root + ctx.search.path.substring(1) : ctx.root + ctx.search.path;
63
+ const filter = inputArea.getAttribute('data-filter') || '';
64
+ searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
65
+ // searchFunc 内部管理 _searchInitialized(pending → true),此处不再覆盖
66
+ }
67
+ }
68
+ }
69
+
70
+ // chat iphone time
71
+ let phoneTimes = document.querySelectorAll('.chat .status-bar .time');
72
+ let firstAdjustInterval = null;
73
+ let mainInterval = null;
74
+
75
+ if (phoneTimes.length > 0) {
76
+ NowTime();
77
+ const date = new Date();
78
+ const sec = date.getSeconds();
79
+ firstAdjustInterval = setInterval(firstAdjustTime, 1000 * (60 - sec));
80
+ timers.push(firstAdjustInterval);
81
+
82
+ function firstAdjustTime() {
83
+ NowTime();
84
+ if (firstAdjustInterval) {
85
+ clearInterval(firstAdjustInterval);
86
+ firstAdjustInterval = null;
87
+ }
88
+ mainInterval = setInterval(NowTime, 1000 * 60);
89
+ timers.push(mainInterval);
90
+ }
91
+
92
+ function NowTime() {
93
+ for (let i = 0; i < phoneTimes.length; ++i) {
94
+ const timeSpan = phoneTimes[i];
95
+ const date = new Date();
96
+ const hour = date.getHours();
97
+ const min = date.getMinutes();
98
+ timeSpan.innerHTML = check(hour) + ":" + check(min);
99
+ }
100
+ };
101
+
102
+ function check(val) {
103
+ if (val < 10) {
104
+ return ("0" + val);
105
+ }
106
+ return (val);
107
+ }
108
+ }
109
+
110
+ // chat quote - 存储事件监听器以便清理
111
+ const quoteClickHandlers = new Map();
112
+ const chat_quote_obverser = new IntersectionObserver((entries, observer) => {
113
+ entries.filter((entry) => { return entry.isIntersecting }).sort((a, b) => a.intersectionRect.y !== b.intersectionRect.y ? a.intersectionRect.y - b.intersectionRect.y : a.intersectionRect.x - b.intersectionRect.x).forEach((entry, index) => {
114
+ observer.unobserve(entry.target);
115
+ setTimeout(() => {
116
+ entry.target.classList.add('quote-blink');
117
+ setTimeout(() => {
118
+ entry.target.classList.remove('quote-blink');
119
+ }, 1000);
120
+ }, Math.max(100, 16) * (index + 1));
121
+ });
122
+ });
123
+
124
+ var chatQuotes = document.querySelectorAll(".chat .talk .quote");
125
+ chatQuotes.forEach((quote) => {
126
+ const handler = function () {
127
+ var chatCellDom = document.getElementById("quote-" + quote.getAttribute("quotedCellTag"));
128
+ if (chatCellDom) {
129
+ var chatDiv = chatCellDom.parentElement;
130
+ var mid = chatDiv.clientHeight / 2;
131
+ var offsetTop = chatCellDom.offsetTop;
132
+ if (offsetTop > mid - chatCellDom.clientHeight / 2) {
133
+ chatDiv.scrollTo({
134
+ top: chatCellDom.offsetTop - mid + chatCellDom.clientHeight / 2,
135
+ behavior: "smooth"
136
+ });
137
+ } else {
138
+ chatDiv.scrollTo({
139
+ top: 0,
140
+ behavior: "smooth"
141
+ });
142
+ }
143
+ chat_quote_obverser.observe(chatCellDom);
144
+ }
145
+ };
146
+ quote.addEventListener('click', handler);
147
+ quoteClickHandlers.set(quote, handler); // 保存处理器引用
148
+ });
149
+
150
+ // 返回清理函数,用于清理定时器和观察器
151
+ return () => {
152
+ // 清理所有定时器(包括可能未完成的 firstAdjustInterval)
153
+ timers.forEach(timer => {
154
+ if (timer) clearInterval(timer);
155
+ });
156
+ timers = [];
157
+ if (firstAdjustInterval) {
158
+ clearInterval(firstAdjustInterval);
159
+ firstAdjustInterval = null;
160
+ }
161
+ if (mainInterval) {
162
+ clearInterval(mainInterval);
163
+ mainInterval = null;
164
+ }
165
+
166
+ // 断开观察器
167
+ if (chat_quote_obverser) {
168
+ chat_quote_obverser.disconnect();
169
+ }
170
+
171
+ // 移除所有 click 事件监听器
172
+ quoteClickHandlers.forEach((handler, quote) => {
173
+ quote.removeEventListener('click', handler);
174
+ });
175
+ quoteClickHandlers.clear();
176
+ };
177
+ };
178
+
179
+ stellar.initPlugin(initServices, 'services');
@@ -0,0 +1,29 @@
1
+ (() => {
2
+ const tagtreeConf = window.__STELLAR_TAGTREE__ || {};
3
+
4
+ const tagSwitchers = document.querySelectorAll('.tag-subtree.parent-tag > a > .tag-switcher-wrapper')
5
+ for (const tagSwitcher of tagSwitchers) {
6
+ tagSwitcher.addEventListener('click', (e) => {
7
+ const parent = e.target.closest('.tag-subtree.parent-tag')
8
+ parent.classList.toggle('expanded')
9
+ e.preventDefault()
10
+ })
11
+ }
12
+
13
+ // Get active tag from query string, then activate it.
14
+ const urlParams = new URLSearchParams(window.location.search)
15
+ const activeTag = urlParams.get('tag')
16
+ if (activeTag) {
17
+ let tag = document.querySelector(`.tag-subtree[data-tag="${activeTag}"]`)
18
+ if (tag) {
19
+ tag.querySelector('a').classList.add('active')
20
+ if (!tagtreeConf.expand_active) {
21
+ tag = tag.parentElement.closest('.tag-subtree.parent-tag')
22
+ }
23
+ while (tag) {
24
+ tag.classList.add('expanded')
25
+ tag = tag.parentElement.closest('.tag-subtree.parent-tag')
26
+ }
27
+ }
28
+ }
29
+ })()
@@ -0,0 +1,61 @@
1
+ const applyTheme = (theme) => {
2
+ if (theme === 'auto') {
3
+ document.documentElement.removeAttribute('data-theme')
4
+ } else {
5
+ document.documentElement.setAttribute('data-theme', theme)
6
+ }
7
+
8
+ // applyThemeToGiscus(theme)
9
+ }
10
+
11
+ // FIXME: 这会导致无法使用 preferred_color_scheme 以外的主题
12
+ const applyThemeToGiscus = (theme) => {
13
+ // theme = theme === 'auto' ? 'preferred_color_scheme' : theme
14
+ const cmt = document.getElementById('giscus')
15
+ if (cmt) {
16
+ // This works before giscus load.
17
+ cmt.setAttribute('data-theme', theme)
18
+ }
19
+
20
+ const iframe = document.querySelector('#comments > section.giscus > iframe')
21
+ if (iframe) {
22
+ // This works after giscus loaded.
23
+ const src = iframe.src
24
+ const newSrc = src.replace(/theme=[\w]+/, `theme=${theme}`)
25
+ iframe.src = newSrc
26
+ }
27
+ }
28
+
29
+ const switchTheme = () => {
30
+ // light -> dark -> auto -> light -> ...
31
+ const currentTheme = document.documentElement.getAttribute('data-theme')
32
+ let newTheme;
33
+ switch (currentTheme) {
34
+ case 'light':
35
+ newTheme = 'dark'
36
+ break
37
+ case 'dark':
38
+ newTheme = 'auto'
39
+ break
40
+ default:
41
+ newTheme = 'light'
42
+ }
43
+ applyTheme(newTheme)
44
+ window.localStorage.setItem('Stellar.theme', newTheme)
45
+ utils.dark.mode = newTheme === 'auto' ? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : newTheme;
46
+ utils.dark.method.toggle.start();
47
+
48
+ const messages = window.__STELLAR_I18N__ || {};
49
+ hud?.toast?.(messages[newTheme])
50
+ }
51
+
52
+ (() => {
53
+ // Apply user's preferred theme, if any.
54
+ const theme = window.localStorage.getItem('Stellar.theme')
55
+ if (theme !== null) {
56
+ applyTheme(theme)
57
+ } else {
58
+ utils.dark.mode = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
59
+ }
60
+ utils.dark.method.toggle.start();
61
+ })()
@@ -1,5 +1,8 @@
1
- <script type="text/javascript">
2
-
1
+ (function () {
2
+ // 防重复执行:utils.js 可能被重复加载,二次执行直接跳过,避免重复声明报错。
3
+ if (window.__stellarUtilsLoaded) return;
4
+ window.__stellarUtilsLoaded = true;
5
+
3
6
  function RunItem() {
4
7
  this.list = []; // 存放回调函数
5
8
  this.start = () => {
@@ -63,7 +66,7 @@
63
66
  var sheets = doc.styleSheets;
64
67
  if (attributes) {
65
68
  for (var attributeName in attributes) {
66
- if (attributes.hasOwnProperty(attributeName)) {
69
+ if (Object.prototype.hasOwnProperty.call(attributes, attributeName)) {
67
70
  ss.setAttribute(attributeName, attributes[attributeName]);
68
71
  }
69
72
  }
@@ -296,7 +299,7 @@
296
299
  onLoading: (el) => {
297
300
  if (el) {
298
301
  if (el.querySelector('.loading-wrap') === null) {
299
- el.insertAdjacentHTML('beforeend', '<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
302
+ el.insertAdjacentHTML('beforeend', `<div class="loading-wrap">${ctx.icons['default:loading-spinner']}</div>`);
300
303
  }
301
304
  }
302
305
  },
@@ -312,7 +315,7 @@
312
315
  if (wrap) {
313
316
  var svg = wrap.querySelector('svg');
314
317
  if (svg) svg.remove();
315
- wrap.insertAdjacentHTML('beforeend', '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
318
+ wrap.insertAdjacentHTML('beforeend', ctx.icons['default:warning']);
316
319
  wrap.classList.add('error');
317
320
  }
318
321
  }
@@ -533,11 +536,13 @@
533
536
  if (timedOut) return;
534
537
  // 标记元素实例为已加载
535
538
  if (el) utils._loadedElements.add(el);
536
- // 写入缓存(克隆响应,避免影响回调读取;延迟执行,避免阻塞渲染)
539
+ // 写入缓存:回调会立即消费 data 的 body,必须先同步 clone 再延迟读取
537
540
  if (cacheable && data.ok) {
541
+ const cacheClone = data.clone();
542
+ const contentType = data.headers.get('Content-Type') || 'application/json';
538
543
  utils._defer(() => {
539
- data.clone().text().then(text => {
540
- utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
544
+ cacheClone.text().then(text => {
545
+ utils.cache.set(url, text, contentType, ttl);
541
546
  }).catch(() => {});
542
547
  });
543
548
  }
@@ -609,9 +614,12 @@
609
614
  clearTimeout(timer);
610
615
  if (timedOut) return;
611
616
  if (cacheable) {
617
+ // 调用方会消费 resp 的 body,必须先同步 clone 再延迟读取
618
+ const cacheClone = resp.clone();
619
+ const contentType = resp.headers.get('Content-Type') || 'application/json';
612
620
  utils._defer(() => {
613
- resp.clone().text().then(text => {
614
- utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
621
+ cacheClone.text().then(text => {
622
+ utils.cache.set(url, text, contentType, ttl);
615
623
  }).catch(() => {});
616
624
  });
617
625
  }
@@ -722,4 +730,12 @@
722
730
  utils.dark = Object.assign(utils.dark, {
723
731
  push: utils.dark.method.toggle.push,
724
732
  });
725
- </script>
733
+
734
+ // 暴露到 window:页尾插件片段经裸标识符 utils 即可使用
735
+ window.utils = utils;
736
+
737
+ // 补跑解析期注册的插件队列
738
+ if (window.stellar && typeof window.stellar._flushPlugins === 'function') {
739
+ window.stellar._flushPlugins();
740
+ }
741
+ })();
@@ -1,37 +0,0 @@
1
- <%
2
- function div() {
3
- var el = '';
4
- if ((page.banner == undefined || page.banner == false) && !theme.article.auto_banner) {
5
- return el;
6
- }
7
- var url;
8
- if (page.banner != undefined) {
9
- if (page.banner.includes('/')) {
10
- url = page.banner;
11
- } else {
12
- url = 'https://source.unsplash.com/2000x400/?' + page.banner;
13
- }
14
- } else {
15
- // 自动以 tags 作为关键词搜索封面
16
- if (page.tags) {
17
- var params = '';
18
- page.tags.reverse().forEach((tag, i) => {
19
- if (i > 0) {
20
- params += ',';
21
- }
22
- params += tag.name;
23
- });
24
- url = 'https://source.unsplash.com/2000x400/?' + params;
25
- } else {
26
- url = 'https://source.unsplash.com/random/2000x400';
27
- }
28
- }
29
- el += '<div class="l_cover post' + scrollreveal(' ') + '">';
30
- el += '<div class="cover">';
31
- el += '<div class="lazy img bg" data-bg="' + escape_html(url) + '"></div>';
32
- el += '</div>';
33
- el += '</div>';
34
- return el;
35
- }
36
- %>
37
- <%- div() %>
@@ -1,178 +0,0 @@
1
- <script defer>
2
- const initServices = () => {
3
- // 用于存储需要清理的资源
4
- let timers = [];
5
-
6
- ctx.services = Object.assign({}, JSON.parse(`<%- JSON.stringify(theme.data_services) %>`));
7
- for (let id of Object.keys(ctx.services)) {
8
- const js = ctx.services[id].js;
9
- if (id == 'siteinfo') {
10
- const cardlinks = document.querySelectorAll('a.link-card[cardlink]');
11
- if (cardlinks?.length > 0) {
12
- utils.js(js, { defer: true }).then(function () {
13
- setCardLink(cardlinks);
14
- });
15
- }
16
- } else if (id == 'ghinfo') {
17
- const els = document.querySelectorAll('.ds-ghinfo');
18
- if (els.length > 0) {
19
- utils.js(js, { defer: true });
20
- }
21
- } else if (id == 'voice') {
22
- const voiceAudios = document.querySelectorAll('.voice>audio');
23
- if (voiceAudios?.length > 0) {
24
- utils.js(js, { defer: true }).then(function () {
25
- createVoiceDom(voiceAudios);
26
- });
27
- }
28
- } else if (id == 'video') {
29
- const videos = document.querySelectorAll('.video>video');
30
- if (videos?.length > 0) {
31
- utils.js(js, { defer: true }).then(function () {
32
- videoEvents(videos);
33
- });
34
- }
35
- } else if (id == 'download-file') {
36
- const files = document.querySelectorAll('.chat-file');
37
- if (files?.length > 0) {
38
- utils.js(js, { defer: true }).then(function () {
39
- downloadFileEvent(files);
40
- });
41
- }
42
- } else {
43
- const els = document.getElementsByClassName(`ds-${id}`);
44
- if (els?.length > 0) {
45
- if (id == 'timeline' || id == 'memos' || id == 'marked' || id == 'mdrender') {
46
- utils.js(deps.marked).then(function () {
47
- utils.js(js, { defer: true });
48
- });
49
- } else {
50
- utils.js(js, { defer: true });
51
- }
52
- }
53
- }
54
- }
55
-
56
- // search
57
- if (ctx.search && typeof searchFunc === 'function') {
58
- const inputArea = document.querySelector("input#search-input");
59
- if (inputArea && !inputArea._searchInitialized) {
60
- const path = ctx.search.path.startsWith('/') ? ctx.root + ctx.search.path.substring(1) : ctx.root + ctx.search.path;
61
- const filter = inputArea.getAttribute('data-filter') || '';
62
- searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
63
- // 标记为已初始化,防止重复绑定
64
- inputArea._searchInitialized = true;
65
- }
66
- }
67
-
68
- // chat iphone time
69
- let phoneTimes = document.querySelectorAll('.chat .status-bar .time');
70
- let firstAdjustInterval = null;
71
- let mainInterval = null;
72
-
73
- if (phoneTimes.length > 0) {
74
- NowTime();
75
- const date = new Date();
76
- const sec = date.getSeconds();
77
- firstAdjustInterval = setInterval(firstAdjustTime, 1000 * (60 - sec));
78
- timers.push(firstAdjustInterval);
79
-
80
- function firstAdjustTime() {
81
- NowTime();
82
- if (firstAdjustInterval) {
83
- clearInterval(firstAdjustInterval);
84
- firstAdjustInterval = null;
85
- }
86
- mainInterval = setInterval(NowTime, 1000 * 60);
87
- timers.push(mainInterval);
88
- }
89
-
90
- function NowTime() {
91
- for (let i = 0; i < phoneTimes.length; ++i) {
92
- const timeSpan = phoneTimes[i];
93
- const date = new Date();
94
- const hour = date.getHours();
95
- const min = date.getMinutes();
96
- timeSpan.innerHTML = check(hour) + ":" + check(min);
97
- }
98
- };
99
-
100
- function check(val) {
101
- if (val < 10) {
102
- return ("0" + val);
103
- }
104
- return (val);
105
- }
106
- }
107
-
108
- // chat quote - 存储事件监听器以便清理
109
- const quoteClickHandlers = new Map();
110
- const chat_quote_obverser = new IntersectionObserver((entries, observer) => {
111
- entries.filter((entry) => { return entry.isIntersecting }).sort((a, b) => a.intersectionRect.y !== b.intersectionRect.y ? a.intersectionRect.y - b.intersectionRect.y : a.intersectionRect.x - b.intersectionRect.x).forEach((entry, index) => {
112
- observer.unobserve(entry.target);
113
- setTimeout(() => {
114
- entry.target.classList.add('quote-blink');
115
- setTimeout(() => {
116
- entry.target.classList.remove('quote-blink');
117
- }, 1000);
118
- }, Math.max(100, 16) * (index + 1));
119
- });
120
- });
121
-
122
- var chatQuotes = document.querySelectorAll(".chat .talk .quote");
123
- chatQuotes.forEach((quote) => {
124
- const handler = function () {
125
- var chatCellDom = document.getElementById("quote-" + quote.getAttribute("quotedCellTag"));
126
- if (chatCellDom) {
127
- var chatDiv = chatCellDom.parentElement;
128
- var mid = chatDiv.clientHeight / 2;
129
- var offsetTop = chatCellDom.offsetTop;
130
- if (offsetTop > mid - chatCellDom.clientHeight / 2) {
131
- chatDiv.scrollTo({
132
- top: chatCellDom.offsetTop - mid + chatCellDom.clientHeight / 2,
133
- behavior: "smooth"
134
- });
135
- } else {
136
- chatDiv.scrollTo({
137
- top: 0,
138
- behavior: "smooth"
139
- });
140
- }
141
- chat_quote_obverser.observe(chatCellDom);
142
- }
143
- };
144
- quote.addEventListener('click', handler);
145
- quoteClickHandlers.set(quote, handler); // 保存处理器引用
146
- });
147
-
148
- // 返回清理函数,用于清理定时器和观察器
149
- return () => {
150
- // 清理所有定时器(包括可能未完成的 firstAdjustInterval)
151
- timers.forEach(timer => {
152
- if (timer) clearInterval(timer);
153
- });
154
- timers = [];
155
- if (firstAdjustInterval) {
156
- clearInterval(firstAdjustInterval);
157
- firstAdjustInterval = null;
158
- }
159
- if (mainInterval) {
160
- clearInterval(mainInterval);
161
- mainInterval = null;
162
- }
163
-
164
- // 断开观察器
165
- if (chat_quote_obverser) {
166
- chat_quote_obverser.disconnect();
167
- }
168
-
169
- // 移除所有 click 事件监听器
170
- quoteClickHandlers.forEach((handler, quote) => {
171
- quote.removeEventListener('click', handler);
172
- });
173
- quoteClickHandlers.clear();
174
- };
175
- };
176
-
177
- utils.initPlugin(initServices, 'services');
178
- </script>
@@ -1,25 +0,0 @@
1
- <script>
2
- const sidebar = {
3
- leftbar: () => {
4
- if (l_body) {
5
- l_body.toggleAttribute('leftbar');
6
- l_body.removeAttribute('rightbar');
7
- }
8
- },
9
- rightbar: () => {
10
- if (l_body) {
11
- l_body.toggleAttribute('rightbar');
12
- l_body.removeAttribute('leftbar');
13
- }
14
- },
15
- dismiss: () => {
16
- if (l_body) {
17
- l_body.removeAttribute('leftbar');
18
- l_body.removeAttribute('rightbar');
19
- }
20
- },
21
- toggleTOC: () => {
22
- document.querySelector('#data-toc').classList.toggle('collapse');
23
- }
24
- }
25
- </script>
@@ -1,29 +0,0 @@
1
- <script type="text/javascript">
2
- (() => {
3
- const tagSwitchers = document.querySelectorAll('.tag-subtree.parent-tag > a > .tag-switcher-wrapper')
4
- for (const tagSwitcher of tagSwitchers) {
5
- tagSwitcher.addEventListener('click', (e) => {
6
- const parent = e.target.closest('.tag-subtree.parent-tag')
7
- parent.classList.toggle('expanded')
8
- e.preventDefault()
9
- })
10
- }
11
-
12
- // Get active tag from query string, then activate it.
13
- const urlParams = new URLSearchParams(window.location.search)
14
- const activeTag = urlParams.get('tag')
15
- if (activeTag) {
16
- let tag = document.querySelector(`.tag-subtree[data-tag="${activeTag}"]`)
17
- if (tag) {
18
- tag.querySelector('a').classList.add('active')
19
- <% if (!theme.widgets.tagtree.expand_active) { %>
20
- tag = tag.parentElement.closest('.tag-subtree.parent-tag')
21
- <% } %>
22
- while (tag) {
23
- tag.classList.add('expanded')
24
- tag = tag.parentElement.closest('.tag-subtree.parent-tag')
25
- }
26
- }
27
- }
28
- })()
29
- </script>