hexo-theme-gnix 6.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/include/hexo/encrypt.js +42 -0
  3. package/include/hexo/feed.js +329 -0
  4. package/include/util/common.js +7 -9
  5. package/languages/en.yml +6 -3
  6. package/languages/zh-CN.yml +6 -3
  7. package/layout/archive.jsx +86 -65
  8. package/layout/comment/twikoo.jsx +2 -11
  9. package/layout/comment/waline.jsx +2 -2
  10. package/layout/common/article.jsx +5 -8
  11. package/layout/common/article_cover.jsx +11 -1
  12. package/layout/common/article_media.jsx +2 -4
  13. package/layout/common/footer.jsx +11 -31
  14. package/layout/common/head.jsx +6 -14
  15. package/layout/common/navbar.jsx +4 -4
  16. package/layout/common/scripts.jsx +6 -6
  17. package/layout/common/theme_selector.jsx +5 -6
  18. package/layout/common/toc.jsx +8 -14
  19. package/layout/index.jsx +2 -4
  20. package/layout/misc/article_licensing.jsx +4 -2
  21. package/layout/misc/open_graph.jsx +4 -4
  22. package/layout/misc/paginator.jsx +10 -4
  23. package/layout/misc/structured_data.jsx +3 -4
  24. package/layout/plugin/busuanzi.jsx +1 -1
  25. package/layout/plugin/cookie_consent.jsx +40 -31
  26. package/layout/plugin/swup.jsx +2 -22
  27. package/layout/search/insight.jsx +16 -3
  28. package/package.json +12 -8
  29. package/scripts/hot-reload.js +92 -0
  30. package/scripts/index.js +2 -0
  31. package/source/css/archive.css +251 -0
  32. package/source/css/default.css +250 -284
  33. package/source/css/encrypt.css +55 -0
  34. package/source/css/responsive/desktop.css +0 -119
  35. package/source/css/responsive/mobile.css +7 -23
  36. package/source/css/responsive/touch.css +9 -103
  37. package/source/css/shiki/shiki.css +7 -22
  38. package/source/css/twikoo.css +290 -830
  39. package/source/img/og_image.webp +0 -0
  40. package/source/js/archive-breadcrumb.js +132 -0
  41. package/source/js/busuanzi.js +1 -12
  42. package/source/js/components/accordion.js +192 -0
  43. package/source/js/components/chat.js +239 -0
  44. package/source/js/components/device-carousel.js +260 -0
  45. package/source/js/components/image-carousel.js +410 -0
  46. package/source/js/components/text-image-section.js +180 -0
  47. package/source/js/components/theme-stacked.js +526 -0
  48. package/source/js/components/tree.js +437 -0
  49. package/source/js/decrypt.js +112 -0
  50. package/source/js/insight.js +75 -65
  51. package/source/js/main.js +192 -99
  52. package/source/js/mdit/mermaid.js +12 -4
  53. package/source/js/swup.bundle.js +1 -0
  54. package/source/js/theme-selector.js +94 -113
  55. package/source/img/og_image.png +0 -0
  56. package/source/js/host/swup/Swup.umd.min.js +0 -1
  57. package/source/js/host/swup/head-plugin.umd.min.js +0 -1
  58. package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
  59. package/source/js/mdit/shiki.js +0 -158
@@ -1,6 +1,6 @@
1
1
  // biome-ignore lint/correctness/noUnusedVariables: Called in other files
2
2
  function loadInsight(config, translation) {
3
- const main = document.querySelector(".searchbox");
3
+ const main = document.querySelector("#searchbox");
4
4
  if (!main) return;
5
5
 
6
6
  const input = main.querySelector(".searchbox-input");
@@ -10,6 +10,7 @@ function loadInsight(config, translation) {
10
10
  let dataset = null; // 缓存 JSON 数据
11
11
  let isLoading = false; // 加载锁
12
12
  let searchTimer = null; // 防抖定时器
13
+ const keywordRegexCache = new Map();
13
14
 
14
15
  // 辅助:创建 DOM
15
16
  function createElement(tag, className, text) {
@@ -29,17 +30,15 @@ function loadInsight(config, translation) {
29
30
  // --- 核心逻辑优化区 ---
30
31
 
31
32
  // HTML 转义函数,防止 XSS 攻击和标签渲染异常
33
+ const _escapeMap = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
34
+ const _escapeRe = /[&<>"']/g;
32
35
  function escapeHTML(str) {
33
- const map = {
34
- "&": "&amp;",
35
- "<": "&lt;",
36
- ">": "&gt;",
37
- '"': "&quot;",
38
- "'": "&#039;",
39
- };
40
- return str.replace(/[&<>"']/g, (m) => map[m]);
36
+ return str.replace(_escapeRe, (m) => _escapeMap[m]);
41
37
  }
42
38
 
39
+ // 字段名 → 预计算小写字段名的映射
40
+ const _lowerFields = { title: "_lowerTitle", text: "_lowerText", name: "_lowerName", slug: "_lowerSlug" };
41
+
43
42
  // 优化点:合并 ranges 的逻辑保持不变,这是高亮的核心算法
44
43
  function merge(ranges) {
45
44
  let last;
@@ -74,7 +73,7 @@ function loadInsight(config, translation) {
74
73
 
75
74
  if (!indices.length) return maxlen ? escapeHTML(text.slice(0, maxlen)) : escapeHTML(text);
76
75
 
77
- let result = "";
76
+ const parts = [];
78
77
  let last = 0;
79
78
  const ranges = merge(indices);
80
79
  const sumRange = [ranges[0][0], ranges[ranges.length - 1][1]];
@@ -85,21 +84,21 @@ function loadInsight(config, translation) {
85
84
 
86
85
  for (let i = 0; i < ranges.length; i++) {
87
86
  const range = ranges[i];
88
- result += escapeHTML(text.slice(last, Math.min(range[0], sumRange[0] + maxlen)));
87
+ parts.push(escapeHTML(text.slice(last, Math.min(range[0], sumRange[0] + maxlen))));
89
88
  if (maxlen && range[0] >= sumRange[0] + maxlen) break;
90
89
 
91
- result += `<span style="color: var(--mauve)">${escapeHTML(text.slice(range[0], range[1]))}</span>`;
90
+ parts.push(`<span style="color: var(--mauve)">${escapeHTML(text.slice(range[0], range[1]))}</span>`);
92
91
  last = range[1];
93
92
 
94
93
  if (i === ranges.length - 1) {
95
94
  if (maxlen) {
96
- result += escapeHTML(text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1)));
95
+ parts.push(escapeHTML(text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1))));
97
96
  } else {
98
- result += escapeHTML(text.slice(range[1]));
97
+ parts.push(escapeHTML(text.slice(range[1])));
99
98
  }
100
99
  }
101
100
  }
102
- return result;
101
+ return parts.join("");
103
102
  }
104
103
 
105
104
  function searchItem(title, preview, url) {
@@ -156,9 +155,14 @@ function loadInsight(config, translation) {
156
155
  const keywords = parseKeywords(keywordsStr);
157
156
  if (keywords.length === 0) return {};
158
157
 
159
- // keywords中的特殊字符转义, 将转移后的关键词编译为正则表达式(忽略大小写,全局,多行)
160
- // 后续在文章内容匹配时使用
161
- const keywordRegexes = keywords.map((k) => new RegExp(k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "img"));
158
+ const keywordRegexes = keywords.map((k) => {
159
+ let regex = keywordRegexCache.get(k);
160
+ if (!regex) {
161
+ regex = new RegExp(k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "img");
162
+ keywordRegexCache.set(k, regex);
163
+ }
164
+ return regex;
165
+ });
162
166
 
163
167
  const calculateWeight = (obj, fields, weights) => {
164
168
  let value = 0;
@@ -174,7 +178,8 @@ function loadInsight(config, translation) {
174
178
  if (!obj[field]) continue;
175
179
 
176
180
  // 1. 快速检查:如果都不包含这个词,直接跳过正则
177
- if (obj[field].toLowerCase().indexOf(keyword) === -1) continue;
181
+ const lowerVal = obj[_lowerFields[field]] ?? obj[field].toLowerCase();
182
+ if (lowerVal.indexOf(keyword) === -1) continue;
178
183
 
179
184
  // 2. 权重计算
180
185
  const matches = obj[field].match(regex);
@@ -221,6 +226,18 @@ function loadInsight(config, translation) {
221
226
  }
222
227
  }
223
228
  container.appendChild(fragment);
229
+
230
+ // 为动态生成的结果项补充 ARIA 属性
231
+ const items = container.querySelectorAll(".searchbox-result-item");
232
+ items.forEach((item, i) => {
233
+ item.id = `searchbox-result-${i}`;
234
+ item.role = "option";
235
+ item.setAttribute("aria-selected", "false");
236
+ item.tabIndex = -1;
237
+ });
238
+
239
+ input.removeAttribute("aria-activedescendant");
240
+ input.setAttribute("aria-expanded", items.length > 0 ? "true" : "false");
224
241
  }
225
242
 
226
243
  function scrollTo(item) {
@@ -252,10 +269,15 @@ function loadInsight(config, translation) {
252
269
  const nextPosition = (items.length + prevPosition + value) % items.length;
253
270
  const finalPosition = nextPosition < 0 ? nextPosition + items.length : nextPosition;
254
271
 
255
- if (prevPosition !== -1) items[prevPosition].classList.remove("active");
272
+ if (prevPosition !== -1) {
273
+ items[prevPosition].classList.remove("active");
274
+ items[prevPosition].setAttribute("aria-selected", "false");
275
+ }
256
276
 
257
277
  const nextItem = items[finalPosition];
258
278
  nextItem.classList.add("active");
279
+ nextItem.setAttribute("aria-selected", "true");
280
+ input.setAttribute("aria-activedescendant", nextItem.id);
259
281
  scrollTo(nextItem);
260
282
  }
261
283
 
@@ -269,6 +291,14 @@ function loadInsight(config, translation) {
269
291
  fetch(config.contentUrl)
270
292
  .then((response) => response.json())
271
293
  .then((json) => {
294
+ for (const post of json.posts) {
295
+ post._lowerTitle = post.title ? post.title.toLowerCase() : "";
296
+ post._lowerText = post.text ? post.text.toLowerCase() : "";
297
+ }
298
+ for (const tag of json.tags) {
299
+ tag._lowerName = tag.name ? tag.name.toLowerCase() : "";
300
+ tag._lowerSlug = tag.slug ? tag.slug.toLowerCase() : "";
301
+ }
272
302
  dataset = json;
273
303
  isLoading = false;
274
304
  // 如果加载完之后输入框里有字,立即触发一次搜索
@@ -294,64 +324,44 @@ function loadInsight(config, translation) {
294
324
  return;
295
325
  }
296
326
 
297
- // 优化点:防抖 (Debounce) 300ms
298
327
  searchTimer = setTimeout(() => {
299
328
  searchResultToDOM(keywords, search(dataset, keywords));
300
- }, 200);
329
+ }, 150);
301
330
  });
302
331
 
303
- main.addEventListener("focusout", (e) => {
304
- if (main.contains(e.relatedTarget)) {
305
- return;
332
+ // 键盘导航
333
+ input.addEventListener("keydown", (e) => {
334
+ if (e.key === "ArrowDown") {
335
+ e.preventDefault();
336
+ selectItemByDiff(1);
337
+ } else if (e.key === "ArrowUp") {
338
+ e.preventDefault();
339
+ selectItemByDiff(-1);
340
+ } else if (e.key === "Enter") {
341
+ const active = container.querySelector(".searchbox-result-item.active");
342
+ if (active) {
343
+ active.click();
344
+ }
306
345
  }
307
- main.classList.remove("show");
308
346
  });
309
347
 
310
348
  main.addEventListener("click", (e) => {
311
- const resultItem = e.target.closest(".searchbox-result-item");
312
- if (resultItem) {
313
- main.classList.remove("show");
349
+ if (e.target === main || e.target.closest(".searchbox-result-item")) {
350
+ main.hidePopover();
314
351
  }
315
352
  });
316
353
 
317
- document.addEventListener("click", (e) => {
318
- if (e.target.closest(".navbar-main .search")) {
319
- main.classList.add("show");
320
- const inp = main.querySelector(".searchbox-input");
321
- inp.focus();
354
+ if (location.hash.trim() === "#insight-search") {
355
+ main.showPopover();
356
+ }
322
357
 
323
- // Lazy Load - 打开时再请求数据
358
+ // Popover 打开时 focus input 并预加载数据
359
+ main.addEventListener("toggle", (e) => {
360
+ if (e.newState === "open") {
324
361
  fetchData();
362
+ input.focus();
363
+ } else {
364
+ input.setAttribute("aria-expanded", "false");
325
365
  }
326
366
  });
327
-
328
- document.addEventListener("keydown", (e) => {
329
- if (!main.classList.contains("show")) return;
330
- switch (e.key) {
331
- case "ArrowUp":
332
- selectItemByDiff(-1);
333
- break;
334
- case "ArrowDown":
335
- selectItemByDiff(1);
336
- break;
337
- case "Enter": {
338
- const activeItem = container.querySelector(".searchbox-result-item.active");
339
- if (activeItem) location.href = activeItem.getAttribute("href");
340
- break;
341
- }
342
- }
343
- });
344
-
345
- document.addEventListener("touchstart", () => {
346
- touch = true;
347
- });
348
- document.addEventListener("touchmove", () => {
349
- touch = false;
350
- });
351
-
352
- // 处理 location.hash 自动打开的情况
353
- if (location.hash.trim() === "#insight-search") {
354
- main.classList.add("show");
355
- fetchData();
356
- }
357
367
  }
package/source/js/main.js CHANGED
@@ -1,23 +1,51 @@
1
+ function tableWrapFix() {
2
+ document.querySelectorAll(".content table").forEach((table) => {
3
+ if (table.hasAttribute("data-nowrap") || table.parentElement.classList.contains("table-wrapper")) {
4
+ return;
5
+ }
6
+ // if width exceeds container, wrap it
7
+ const wrapper = document.createElement("div");
8
+ Object.assign(wrapper.style, {
9
+ width: "100%",
10
+ overflowX: "auto",
11
+ });
12
+ table.parentNode.insertBefore(wrapper, table);
13
+ wrapper.appendChild(table);
14
+ });
15
+ }
16
+
17
+ function twikoo_handler() {
18
+ const el = document.getElementById("tko");
19
+ if (!el) return;
20
+
21
+ const { envId, region, lang, jsUrl, cssUrl } = el.dataset;
22
+
23
+ if (cssUrl) loadCSSOnce(cssUrl);
24
+
25
+ const config = { envId, region, lang, el: "#tko" };
26
+
27
+ if (typeof window.twikoo?.init === "function") {
28
+ window.twikoo.init(config);
29
+ return;
30
+ }
31
+
32
+ loadScriptOnce(jsUrl, () => window.twikoo.init(config));
33
+ }
1
34
  // #region mdit@tab-plugin
2
35
  /**
3
36
  * 初始化页面上所有的 Tab 组件
4
37
  */
5
- function initializeTabs() {
6
- // 选取页面上所有的 Tab 容器
7
- const tabContainers = document.querySelectorAll(".tabs-tabs-wrapper");
8
38
 
9
- tabContainers.forEach((container) => {
10
- // 先移除已有事件(防止PJAX重复绑定)
39
+ function initializeTabs() {
40
+ document.querySelectorAll(".tabs-tabs-wrapper").forEach((container) => {
11
41
  const buttons = container.querySelectorAll(".tabs-tab-button");
12
42
  buttons.forEach((button) => {
13
- // 移除旧事件,避免叠加
14
43
  button.removeEventListener("click", handleTabClick);
15
44
  button.addEventListener("click", handleTabClick);
16
45
  });
17
46
  });
18
47
  }
19
48
 
20
- // 抽离Tab点击处理函数,方便移除事件
21
49
  function handleTabClick() {
22
50
  const tabContainer = this.closest(".tabs-tabs-wrapper");
23
51
  const targetIndex = this.getAttribute("data-tab");
@@ -84,6 +112,98 @@ function syncRelatedTabs(syncId) {
84
112
 
85
113
  // #endregion
86
114
 
115
+ // #region markdown-exit shiki
116
+ const SELECTORS = {
117
+ figure: "figure.shiki",
118
+ pre: "pre.shiki",
119
+ code: "pre.shiki code",
120
+ expandBtn: ".code-expand-btn",
121
+ };
122
+
123
+ const CLS = {
124
+ copy: "copy-true",
125
+ wrap: "wrap-active",
126
+ expanded: "expanded",
127
+ expandDone: "expand-done",
128
+ };
129
+
130
+ function addHighlightTool() {
131
+ const figures = document.querySelectorAll(SELECTORS.figure);
132
+ if (!figures.length) return;
133
+
134
+ figures.forEach((figure) => {
135
+ if (figure.hasAttribute("data-initialized")) return;
136
+ figure.setAttribute("data-initialized", "true");
137
+
138
+ const pre = figure.querySelector(SELECTORS.pre);
139
+ const toolbar = figure.querySelector(".shiki-tools");
140
+ const expandBtn = figure.querySelector(SELECTORS.expandBtn);
141
+
142
+ // Copy button handler
143
+ if (toolbar) {
144
+ toolbar.addEventListener("click", (e) => {
145
+ const target = e.target;
146
+ if (target.closest(".copy-button")) {
147
+ const btn = target.closest(".copy-button");
148
+ const notice = btn.previousElementSibling;
149
+ const code = figure.querySelector(SELECTORS.code);
150
+
151
+ navigator.clipboard.writeText(code.innerText);
152
+ notice.textContent = "Copied";
153
+ notice.classList.add("show");
154
+ setTimeout(() => notice.classList.remove("show"), 800);
155
+ } else if (target.closest(".toggle-wrap")) {
156
+ // Toggle wrap
157
+ const code = figure.querySelector(SELECTORS.code);
158
+ const enabled = code.style.whiteSpace !== "pre-wrap";
159
+ code.style.whiteSpace = enabled ? "pre-wrap" : "pre";
160
+ code.style.wordBreak = enabled ? "break-all" : "normal";
161
+ target.closest(".toggle-wrap").classList.toggle(CLS.wrap, enabled);
162
+ }
163
+ });
164
+ }
165
+
166
+ // Expand button handler
167
+ if (expandBtn) {
168
+ expandBtn.addEventListener("click", (e) => {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+
172
+ const showLines = parseInt(figure.dataset.maxLines || "10", 10);
173
+ const isExpanded = figure.classList.contains(CLS.expanded);
174
+
175
+ if (isExpanded) {
176
+ const computed = getComputedStyle(pre);
177
+ const lineHeight = parseFloat(computed.lineHeight) || 20;
178
+ const padding = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.paddingBottom) || 0);
179
+ figure.classList.remove(CLS.expanded);
180
+ pre.style.maxHeight = `${showLines * lineHeight + padding}px`;
181
+ expandBtn.classList.remove(CLS.expandDone);
182
+ } else {
183
+ figure.classList.add(CLS.expanded);
184
+ pre.style.maxHeight = `${pre.scrollHeight}px`;
185
+ expandBtn.classList.add(CLS.expandDone);
186
+
187
+ setTimeout(() => {
188
+ pre.style.maxHeight = "none";
189
+ }, 300);
190
+ }
191
+ });
192
+ }
193
+
194
+ // Initialize collapsed state
195
+ if (figure.dataset.collapsible === "true" && pre) {
196
+ requestAnimationFrame(() => {
197
+ const lineHeight = parseFloat(getComputedStyle(pre).lineHeight) || 20;
198
+ const showLines = parseInt(figure.dataset.maxLines || "10", 10);
199
+ pre.style.maxHeight = `${showLines * lineHeight}px`;
200
+ pre.style.overflow = "hidden";
201
+ });
202
+ }
203
+ });
204
+ }
205
+ // #endregion
206
+
87
207
  // #region Keyboard Shortcuts
88
208
 
89
209
  function handleKeyDown(e) {
@@ -91,17 +211,47 @@ function handleKeyDown(e) {
91
211
  if (!isModifier) return;
92
212
 
93
213
  const tag = e.target.tagName;
94
- if (tag === "INPUT" || tag === "TEXTAREA" || e.target.isContentEditable) {
95
- return;
214
+ if (["INPUT", "TEXTAREA"].includes(tag) || e.target.isContentEditable) return;
215
+
216
+ switch (e.code) {
217
+ case "KeyT":
218
+ e.preventDefault();
219
+ document.getElementById("toc-body")?.togglePopover();
220
+ break;
221
+ case "KeyK":
222
+ e.preventDefault();
223
+ document.querySelector("#searchbox")?.showPopover();
224
+ break;
225
+ case "KeyP":
226
+ if (!e.shiftKey) {
227
+ e.preventDefault();
228
+ document.querySelector("#theme-selector-popover")?.showPopover();
229
+ }
230
+ break;
231
+ }
232
+ }
233
+
234
+ function loadCSSOnce(url) {
235
+ if (!document.querySelector(`link[href="${url}"]`)) {
236
+ const link = document.createElement("link");
237
+ link.rel = "stylesheet";
238
+ link.href = url;
239
+ document.head.appendChild(link);
96
240
  }
241
+ }
97
242
 
98
- if (e.code === "KeyK") {
99
- e.preventDefault();
100
- const searchBtn = document.querySelector(".navbar-main .search");
101
- if (searchBtn) searchBtn.click();
102
- } else if ((e.shiftKey && e.code === "KeyP") || e.code === "KeyP") {
103
- e.preventDefault();
104
- window.openThemeModal?.();
243
+ /**
244
+ * 加载脚本一次,如果已存在则监听 load 事件
245
+ */
246
+ function loadScriptOnce(url, onLoad) {
247
+ const existingScript = document.querySelector(`script[src="${url}"]`);
248
+ if (existingScript) {
249
+ existingScript.addEventListener("load", onLoad);
250
+ } else {
251
+ const script = document.createElement("script");
252
+ script.src = url;
253
+ script.onload = onLoad;
254
+ document.head.appendChild(script);
105
255
  }
106
256
  }
107
257
 
@@ -112,12 +262,7 @@ function handleMermaid() {
112
262
  const cssUrl = "/css/optional/mermaid.css";
113
263
  const adapterUrl = "/js/mdit/mermaid.js";
114
264
 
115
- if (!document.querySelector(`link[href="${cssUrl}"]`)) {
116
- const link = document.createElement("link");
117
- link.rel = "stylesheet";
118
- link.href = cssUrl;
119
- document.head.appendChild(link);
120
- }
265
+ loadCSSOnce(cssUrl);
121
266
 
122
267
  const runInit = () => {
123
268
  const isNight = document.documentElement.classList.contains("night");
@@ -137,99 +282,47 @@ function handleMermaid() {
137
282
  if (window.initMermaidDiagram) {
138
283
  runInit();
139
284
  } else {
140
- const existingScript = document.querySelector(`script[src="${adapterUrl}"]`);
141
- if (existingScript) {
142
- existingScript.addEventListener("load", runInit);
143
- } else {
144
- const script = document.createElement("script");
145
- script.src = adapterUrl;
146
- script.onload = runInit;
147
- document.head.appendChild(script);
148
- }
285
+ loadScriptOnce(adapterUrl, runInit);
149
286
  }
150
287
  }
151
288
 
152
289
  // #endregion
153
290
 
154
- function initLogic() {
291
+ function initPage() {
292
+ tableWrapFix();
155
293
  initializeTabs();
156
294
  handleMermaid();
157
- mediumZoom(".article img", {
158
- background: "hsla(from var(--mantle) / 0.9)",
159
- });
160
- if (document.getElementById("twikoo")) {
161
- const initTwikoo = () => {
162
- if (window.twikoo && window.twikooConfig) {
163
- window.twikoo.init(window.twikooConfig);
164
- }
165
- };
166
-
167
- if (typeof twikoo !== "undefined") {
168
- initTwikoo();
169
- } else {
170
- const script = document.querySelector('script[src*="twikoo.all.min.js"]');
171
- if (script) {
172
- script.addEventListener("load", initTwikoo);
173
- }
174
- }
175
- }
295
+ addHighlightTool();
296
+ const zoomOpts = { background: "hsla(from var(--mantle) / 0.9)" };
297
+ const zoomImgs = new Set();
298
+ document.querySelectorAll(".content img").forEach((img) => zoomImgs.add(img));
299
+ mediumZoom([...zoomImgs], zoomOpts);
300
+ twikoo_handler();
176
301
  }
177
302
 
178
- document.addEventListener("DOMContentLoaded", () => {
179
- initLogic();
180
- });
303
+ document.addEventListener("DOMContentLoaded", initPage, { once: true });
181
304
 
182
305
  // Re-initialize on page changes when using swup
183
306
  if (typeof swup !== "undefined") {
184
- swup.hooks.on("page:view", (visit) => {
185
- console.log("New page loaded:", visit.to.url);
186
- initLogic();
187
- });
307
+ swup.hooks.on("page:view", initPage);
188
308
  }
189
309
 
190
- // Global functions
191
- // biome-ignore lint/correctness/noUnusedVariables: used in <nav click="handleNavbarClick(event)">
192
- function handleNavbarClick(e) {
193
- const target = e.target;
194
- const navbarBurger = document.querySelector(".navbar-burger");
195
- const navbarMenu = document.querySelector(".navbar-menu");
310
+ document.addEventListener("keydown", handleKeyDown, {
311
+ capture: true, // 捕获阶段监听,优先于浏览器默认处理
312
+ passive: false, // 允许调用 preventDefault
313
+ });
196
314
 
197
- if (!navbarBurger || !navbarMenu) return;
315
+ function toggleNav(event) {
316
+ const container = event.currentTarget;
317
+ const burger = container.querySelector(".navbar-burger");
318
+ const menu = container.querySelector(".navbar-menu");
319
+ const target = event.target;
198
320
 
199
- // 处理 burger 点击
200
321
  if (target.closest(".navbar-burger")) {
201
- navbarBurger.classList.toggle("is-active");
202
- navbarMenu.classList.toggle("is-active");
203
- return;
322
+ burger.classList.toggle("is-active");
323
+ menu.classList.toggle("is-active");
324
+ } else if (target.closest(".navbar-item")) {
325
+ burger.classList.remove("is-active");
326
+ menu.classList.remove("is-active");
204
327
  }
205
-
206
- // 处理 item 点击
207
- if (target.closest(".navbar-item")) {
208
- if (navbarBurger.classList.contains("is-active")) {
209
- navbarBurger.classList.remove("is-active");
210
- navbarMenu.classList.remove("is-active");
211
- }
212
- }
213
- }
214
-
215
- function tableWrapFix() {
216
- document.querySelectorAll(".content table").forEach((table) => {
217
- if (table.hasAttribute("data-nowrap") || table.parentElement.classList.contains("table-wrapper")) {
218
- return;
219
- }
220
- // if width exceeds container, wrap it
221
- const wrapper = document.createElement("div");
222
- Object.assign(wrapper.style, {
223
- width: "100%",
224
- overflowX: "auto",
225
- });
226
- table.parentNode.insertBefore(wrapper, table);
227
- wrapper.appendChild(table);
228
- });
229
328
  }
230
-
231
- tableWrapFix();
232
- document.addEventListener("keydown", handleKeyDown, {
233
- capture: true, // 捕获阶段监听,优先于浏览器默认处理
234
- passive: false, // 允许调用 preventDefault
235
- });
@@ -1,6 +1,8 @@
1
1
  (() => {
2
2
  const instances = new Map();
3
3
  let mermaidPromise = null;
4
+ let renderSeq = 0;
5
+
4
6
  const loadMermaid = (jsUrl) => {
5
7
  if (mermaidPromise) return mermaidPromise;
6
8
  if (window.mermaid) {
@@ -21,11 +23,15 @@
21
23
  const mermaid = await mermaidPromise;
22
24
  if (!content || !mermaid) return;
23
25
 
24
- // Initialize with current theme
26
+ const instance = instances.get(id);
27
+ if (!instance) return;
28
+ const version = (instance.renderVersion = ++renderSeq);
29
+
30
+ const isNight = document.documentElement.classList.contains("night");
25
31
  mermaid.initialize({
26
32
  startOnLoad: false,
27
- theme: document.documentElement.classList.contains("night") ? "dark" : "default",
28
- darkMode: document.documentElement.classList.contains("night"),
33
+ theme: isNight ? "dark" : "default",
34
+ darkMode: isNight,
29
35
  themeVariables,
30
36
  securityLevel: "strict",
31
37
  fontSize: 16,
@@ -33,9 +39,11 @@
33
39
 
34
40
  try {
35
41
  content.innerHTML = "";
36
- const { svg } = await mermaid.render(`${id}-svg`, code);
42
+ const { svg } = await mermaid.render(`${id}-svg-${version}`, code);
43
+ if (instance.renderVersion !== version) return;
37
44
  content.insertAdjacentHTML("beforeend", svg);
38
45
  } catch (error) {
46
+ if (instance.renderVersion !== version) return;
39
47
  console.error("Mermaid rendering error:", error);
40
48
  content.innerHTML = `<p style="color: red;">Failed to render diagram: ${error.message}</p>`;
41
49
  }