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.
- package/README.md +6 -2
- package/include/hexo/encrypt.js +42 -0
- package/include/hexo/feed.js +329 -0
- package/include/util/common.js +7 -9
- package/languages/en.yml +6 -3
- package/languages/zh-CN.yml +6 -3
- package/layout/archive.jsx +86 -65
- package/layout/comment/twikoo.jsx +2 -11
- package/layout/comment/waline.jsx +2 -2
- package/layout/common/article.jsx +5 -8
- package/layout/common/article_cover.jsx +11 -1
- package/layout/common/article_media.jsx +2 -4
- package/layout/common/footer.jsx +11 -31
- package/layout/common/head.jsx +6 -14
- package/layout/common/navbar.jsx +4 -4
- package/layout/common/scripts.jsx +6 -6
- package/layout/common/theme_selector.jsx +5 -6
- package/layout/common/toc.jsx +8 -14
- package/layout/index.jsx +2 -4
- package/layout/misc/article_licensing.jsx +4 -2
- package/layout/misc/open_graph.jsx +4 -4
- package/layout/misc/paginator.jsx +10 -4
- package/layout/misc/structured_data.jsx +3 -4
- package/layout/plugin/busuanzi.jsx +1 -1
- package/layout/plugin/cookie_consent.jsx +40 -31
- package/layout/plugin/swup.jsx +2 -22
- package/layout/search/insight.jsx +16 -3
- package/package.json +12 -8
- package/scripts/hot-reload.js +92 -0
- package/scripts/index.js +2 -0
- package/source/css/archive.css +251 -0
- package/source/css/default.css +250 -284
- package/source/css/encrypt.css +55 -0
- package/source/css/responsive/desktop.css +0 -119
- package/source/css/responsive/mobile.css +7 -23
- package/source/css/responsive/touch.css +9 -103
- package/source/css/shiki/shiki.css +7 -22
- package/source/css/twikoo.css +290 -830
- package/source/img/og_image.webp +0 -0
- package/source/js/archive-breadcrumb.js +132 -0
- package/source/js/busuanzi.js +1 -12
- package/source/js/components/accordion.js +192 -0
- package/source/js/components/chat.js +239 -0
- package/source/js/components/device-carousel.js +260 -0
- package/source/js/components/image-carousel.js +410 -0
- package/source/js/components/text-image-section.js +180 -0
- package/source/js/components/theme-stacked.js +526 -0
- package/source/js/components/tree.js +437 -0
- package/source/js/decrypt.js +112 -0
- package/source/js/insight.js +75 -65
- package/source/js/main.js +192 -99
- package/source/js/mdit/mermaid.js +12 -4
- package/source/js/swup.bundle.js +1 -0
- package/source/js/theme-selector.js +94 -113
- package/source/img/og_image.png +0 -0
- package/source/js/host/swup/Swup.umd.min.js +0 -1
- package/source/js/host/swup/head-plugin.umd.min.js +0 -1
- package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
- package/source/js/mdit/shiki.js +0 -158
package/source/js/insight.js
CHANGED
|
@@ -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("
|
|
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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
34
|
+
const _escapeRe = /[&<>"']/g;
|
|
32
35
|
function escapeHTML(str) {
|
|
33
|
-
|
|
34
|
-
"&": "&",
|
|
35
|
-
"<": "<",
|
|
36
|
-
">": ">",
|
|
37
|
-
'"': """,
|
|
38
|
-
"'": "'",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
parts.push(escapeHTML(text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1))));
|
|
97
96
|
} else {
|
|
98
|
-
|
|
97
|
+
parts.push(escapeHTML(text.slice(range[1])));
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
|
-
return
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
},
|
|
329
|
+
}, 150);
|
|
301
330
|
});
|
|
302
331
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const inp = main.querySelector(".searchbox-input");
|
|
321
|
-
inp.focus();
|
|
354
|
+
if (location.hash.trim() === "#insight-search") {
|
|
355
|
+
main.showPopover();
|
|
356
|
+
}
|
|
322
357
|
|
|
323
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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 (
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
291
|
+
function initPage() {
|
|
292
|
+
tableWrapFix();
|
|
155
293
|
initializeTabs();
|
|
156
294
|
handleMermaid();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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",
|
|
185
|
-
console.log("New page loaded:", visit.to.url);
|
|
186
|
-
initLogic();
|
|
187
|
-
});
|
|
307
|
+
swup.hooks.on("page:view", initPage);
|
|
188
308
|
}
|
|
189
309
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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:
|
|
28
|
-
darkMode:
|
|
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
|
}
|