hexo-theme-gnix 1.1.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 +106 -0
- package/include/hexo/filter/locals.js +109 -0
- package/include/hexo/generator/categories.js +12 -0
- package/include/hexo/generator/category.js +52 -0
- package/include/hexo/generator/insight.js +50 -0
- package/include/hexo/generator/manifest.js +23 -0
- package/include/hexo/generator/tags.js +12 -0
- package/include/hexo/helper/cdn.js +21 -0
- package/include/hexo/helper/page.js +27 -0
- package/include/hexo/view.js +40 -0
- package/include/register.js +11 -0
- package/include/util/common.js +33 -0
- package/languages/en.yml +47 -0
- package/languages/fr.yml +46 -0
- package/languages/ja.yml +46 -0
- package/languages/zh-CN.yml +47 -0
- package/languages/zh-TW.yml +47 -0
- package/layout/archive.jsx +118 -0
- package/layout/categories.jsx +137 -0
- package/layout/category.jsx +38 -0
- package/layout/comment/disqus.jsx +79 -0
- package/layout/comment/disqusjs.jsx +127 -0
- package/layout/comment/giscus.jsx +193 -0
- package/layout/comment/gitalk.jsx +141 -0
- package/layout/comment/twikoo.jsx +63 -0
- package/layout/comment/utterances.jsx +86 -0
- package/layout/comment/valine.jsx +143 -0
- package/layout/comment/waline.jsx +156 -0
- package/layout/common/article.jsx +131 -0
- package/layout/common/article_cover.jsx +33 -0
- package/layout/common/article_media.jsx +34 -0
- package/layout/common/comment.jsx +38 -0
- package/layout/common/footer.jsx +228 -0
- package/layout/common/head.jsx +242 -0
- package/layout/common/navbar.jsx +219 -0
- package/layout/common/plugins.jsx +39 -0
- package/layout/common/scripts.jsx +49 -0
- package/layout/common/search.jsx +22 -0
- package/layout/common/theme_selector.jsx +79 -0
- package/layout/common/toc.jsx +53 -0
- package/layout/index.jsx +29 -0
- package/layout/layout.jsx +34 -0
- package/layout/misc/article_licensing.jsx +114 -0
- package/layout/misc/meta.jsx +61 -0
- package/layout/misc/open_graph.jsx +164 -0
- package/layout/misc/paginator.jsx +90 -0
- package/layout/misc/structured_data.jsx +110 -0
- package/layout/misc/web_app.jsx +106 -0
- package/layout/page.jsx +12 -0
- package/layout/plugin/bing_webmaster.jsx +47 -0
- package/layout/plugin/busuanzi.jsx +40 -0
- package/layout/plugin/clarity.jsx +22 -0
- package/layout/plugin/cookie_consent.jsx +136 -0
- package/layout/plugin/google_analytics.jsx +66 -0
- package/layout/plugin/google_tag_mamager.jsx +41 -0
- package/layout/plugin/netlify.jsx +39 -0
- package/layout/plugin/pjax.jsx +20 -0
- package/layout/plugin/statcounter.jsx +69 -0
- package/layout/plugin/twitter_conversion_tracking.jsx +51 -0
- package/layout/post.jsx +16 -0
- package/layout/search/insight.jsx +53 -0
- package/layout/tag.jsx +29 -0
- package/layout/tags.jsx +55 -0
- package/package.json +42 -0
- package/scripts/index.js +1 -0
- package/source/css/callout_blocks.css +204 -0
- package/source/css/default.css +1590 -0
- package/source/css/font/woff2/Futura-Book.woff2 +0 -0
- package/source/css/font/woff2/Paris2024-Variable.woff2 +0 -0
- package/source/css/font/woff2/doto.woff2 +0 -0
- package/source/css/optional/chinese.css +17 -0
- package/source/css/responsive/desktop.css +164 -0
- package/source/css/responsive/mobile.css +46 -0
- package/source/css/responsive/tablet.css +46 -0
- package/source/css/responsive/touch.css +254 -0
- package/source/css/shiki/shiki.min.css +1 -0
- package/source/css/twikoo.css +2143 -0
- package/source/img/avatar.webp +0 -0
- package/source/img/background.webp +0 -0
- package/source/img/favicon.svg +6 -0
- package/source/img/logo.svg +9 -0
- package/source/img/og_image.png +0 -0
- package/source/js/busuanzi.js +46 -0
- package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +6 -0
- package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +1 -0
- package/source/js/host/iconify-icon/3.0.2/iconify-icon.min.js +12 -0
- package/source/js/host/medium-zoom/dist/medium-zoom.min.js +2 -0
- package/source/js/host/mermaid/mermaid.min.js +2811 -0
- package/source/js/host/pjax/0.2.8/pjax.min.js +1 -0
- package/source/js/host/twikoo/1.6.41/dist/twikoo.all.min.js +2 -0
- package/source/js/insight.js +330 -0
- package/source/js/instant-page.min.js +1 -0
- package/source/js/live2d_Asoul/Model/Ava/Ava.4096/texture_00.webp +0 -0
- package/source/js/live2d_Asoul/Model/Ava/Ava.moc3 +0 -0
- package/source/js/live2d_Asoul/Model/Ava/Ava.model3.json +323 -0
- package/source/js/live2d_Asoul/Model/Ava/Ava.physics3.json +1225 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_idle.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_shake01.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_shake02.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap01.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap02.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap03.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap04.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap05.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap06.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap07.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap08.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap09.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap10.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap11.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Ava/raw.ex.json +16 -0
- package/source/js/live2d_Asoul/Model/Ava/raw.model3.json +321 -0
- package/source/js/live2d_Asoul/Model/Diana/Diana.4096/texture_00.webp +0 -0
- package/source/js/live2d_Asoul/Model/Diana/Diana.moc3 +0 -0
- package/source/js/live2d_Asoul/Model/Diana/Diana.model3.json +212 -0
- package/source/js/live2d_Asoul/Model/Diana/Diana.physics3.json +764 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_idle.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap01.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap02.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap03.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap04.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap05.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap06.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap07.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap08.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap09.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap10.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap11.motion3.json +1 -0
- package/source/js/live2d_Asoul/Model/Diana/raw.ex.json +16 -0
- package/source/js/live2d_Asoul/Model/Diana/raw.model3.json +210 -0
- package/source/js/live2d_Asoul/TweenLite.js +12 -0
- package/source/js/live2d_Asoul/cubism4.min.js +2 -0
- package/source/js/live2d_Asoul/live2dcubismcore.min.js +9 -0
- package/source/js/live2d_Asoul/load.js +231 -0
- package/source/js/live2d_Asoul/pio.css +161 -0
- package/source/js/live2d_Asoul/pio.js +296 -0
- package/source/js/live2d_Asoul/pio_sdk4.js +149 -0
- package/source/js/live2d_Asoul/pixi.min.js +9 -0
- package/source/js/main.js +218 -0
- package/source/js/pjax.js +29 -0
- package/source/js/shiki/shiki.js +191 -0
- package/source/js/theme-selector.js +206 -0
- package/util/cache.js +47 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// #region mdit@tab-plugin
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 初始化页面上所有的 Tab 组件
|
|
5
|
+
*/
|
|
6
|
+
function initializeTabs() {
|
|
7
|
+
// 选取页面上所有的 Tab 容器
|
|
8
|
+
const tabContainers = document.querySelectorAll(".tabs-tabs-wrapper");
|
|
9
|
+
|
|
10
|
+
tabContainers.forEach(container => {
|
|
11
|
+
// 先移除已有事件(防止PJAX重复绑定)
|
|
12
|
+
const buttons = container.querySelectorAll(".tabs-tab-button");
|
|
13
|
+
buttons.forEach(button => {
|
|
14
|
+
// 移除旧事件,避免叠加
|
|
15
|
+
button.removeEventListener("click", handleTabClick);
|
|
16
|
+
button.addEventListener("click", handleTabClick);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 抽离Tab点击处理函数,方便移除事件
|
|
22
|
+
function handleTabClick() {
|
|
23
|
+
const tabContainer = this.closest(".tabs-tabs-wrapper");
|
|
24
|
+
const targetIndex = this.getAttribute("data-tab");
|
|
25
|
+
const syncId = this.getAttribute("data-id");
|
|
26
|
+
activateTab(tabContainer, targetIndex);
|
|
27
|
+
if (syncId) {
|
|
28
|
+
syncRelatedTabs(syncId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 激活指定容器中的特定 Tab
|
|
34
|
+
* @param {HTMLElement} container - Tab 容器元素
|
|
35
|
+
* @param {string} targetIndex - 要激活的 Tab 的 data-tab 值
|
|
36
|
+
*/
|
|
37
|
+
function activateTab(container, targetIndex) {
|
|
38
|
+
// 先重置该容器内所有 Tab 的状态
|
|
39
|
+
resetTabsState(container);
|
|
40
|
+
|
|
41
|
+
const buttonToActivate = container.querySelector(`.tabs-tab-button[data-tab="${targetIndex}"]`);
|
|
42
|
+
const contentToActivate = container.querySelector(`.tabs-tab-content[data-index="${targetIndex}"]`);
|
|
43
|
+
|
|
44
|
+
if (buttonToActivate) {
|
|
45
|
+
buttonToActivate.classList.add("active");
|
|
46
|
+
buttonToActivate.setAttribute("data-active", "");
|
|
47
|
+
}
|
|
48
|
+
if (contentToActivate) {
|
|
49
|
+
contentToActivate.classList.add("active");
|
|
50
|
+
contentToActivate.setAttribute("data-active", "");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 重置指定容器内所有 Tab 按钮和内容面板的状态
|
|
56
|
+
* @param {HTMLElement} container - Tab 容器元素
|
|
57
|
+
*/
|
|
58
|
+
function resetTabsState(container) {
|
|
59
|
+
const buttons = container.querySelectorAll(".tabs-tab-button");
|
|
60
|
+
const contents = container.querySelectorAll(".tabs-tab-content");
|
|
61
|
+
|
|
62
|
+
buttons.forEach(btn => {
|
|
63
|
+
btn.classList.remove("active");
|
|
64
|
+
btn.removeAttribute("data-active");
|
|
65
|
+
});
|
|
66
|
+
contents.forEach(content => {
|
|
67
|
+
content.classList.remove("active");
|
|
68
|
+
content.removeAttribute("data-active");
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 同步所有具有相同 data-id 的关联 Tab
|
|
74
|
+
* @param {string} syncId - 用于同步的 data-id
|
|
75
|
+
*/
|
|
76
|
+
function syncRelatedTabs(syncId) {
|
|
77
|
+
const relatedButtons = document.querySelectorAll(`.tabs-tab-button[data-id="${syncId}"]`);
|
|
78
|
+
|
|
79
|
+
relatedButtons.forEach(button => {
|
|
80
|
+
const container = button.closest(".tabs-tabs-wrapper");
|
|
81
|
+
const targetIndex = button.getAttribute("data-tab");
|
|
82
|
+
activateTab(container, targetIndex);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// #endregion
|
|
87
|
+
|
|
88
|
+
// #region Keyboard Shortcuts
|
|
89
|
+
|
|
90
|
+
function initKeyboardShortcuts() {
|
|
91
|
+
// 移除旧事件(防止PJAX重复绑定)
|
|
92
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
93
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 抽离键盘事件处理函数
|
|
97
|
+
function handleKeyDown(e) {
|
|
98
|
+
if (e.ctrlKey || e.metaKey) {
|
|
99
|
+
if (e.code === "KeyK") {
|
|
100
|
+
// ctrl/cmd + k for search
|
|
101
|
+
const searchBtn = document.querySelector(".navbar-main .search");
|
|
102
|
+
if (searchBtn) searchBtn.click();
|
|
103
|
+
} else if ((e.shiftKey && e.code === "KeyP") || e.code === "KeyP") {
|
|
104
|
+
// ctrl/cmd + shift + p for theme selector
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const themeBtn = document.querySelector(
|
|
107
|
+
"button.navbar-item.theme-selector-trigger",
|
|
108
|
+
);
|
|
109
|
+
if (themeBtn) themeBtn.click();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// #endregion
|
|
115
|
+
|
|
116
|
+
// #region TOC
|
|
117
|
+
function initializeTableOfContents() {
|
|
118
|
+
const tocContainer = document.getElementById("icarus-toc-container");
|
|
119
|
+
if (!tocContainer) return;
|
|
120
|
+
|
|
121
|
+
const tocLinks = tocContainer.querySelectorAll(".toc-link");
|
|
122
|
+
|
|
123
|
+
// Scroll Spy
|
|
124
|
+
const headers = [];
|
|
125
|
+
tocLinks.forEach((link) => {
|
|
126
|
+
const href = link.getAttribute("href") || "";
|
|
127
|
+
// 解码并移除 #
|
|
128
|
+
const id = decodeURIComponent(href.replace(/^#/, ""));
|
|
129
|
+
// 避免无效 ID 导致报错
|
|
130
|
+
if (id) {
|
|
131
|
+
const header = document.getElementById(id);
|
|
132
|
+
if (header) {
|
|
133
|
+
headers.push({ header, link });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (headers.length > 0) {
|
|
139
|
+
// 先移除旧的滚动事件(防止PJAX重复绑定)
|
|
140
|
+
window.removeEventListener("scroll", handleTocScroll, { passive: true });
|
|
141
|
+
|
|
142
|
+
function handleTocScroll() {
|
|
143
|
+
const viewportHeight = window.innerHeight;
|
|
144
|
+
let currentHeader = null;
|
|
145
|
+
|
|
146
|
+
// 查找当前视口中最后一个位于屏幕中线上方的标题
|
|
147
|
+
for (const h of headers) {
|
|
148
|
+
const rect = h.header.getBoundingClientRect();
|
|
149
|
+
if (rect.top < viewportHeight / 2) {
|
|
150
|
+
currentHeader = h;
|
|
151
|
+
} else {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 如果在页面顶部,高亮第一个
|
|
157
|
+
if (!currentHeader && window.scrollY < 100) {
|
|
158
|
+
currentHeader = headers[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 清除旧的高亮
|
|
162
|
+
for (const l of tocLinks) {
|
|
163
|
+
l.closest(".toc-item").classList.remove("is-active");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 设置新的高亮
|
|
167
|
+
if (currentHeader) {
|
|
168
|
+
currentHeader.link.closest(".toc-item").classList.add("is-active");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 使用 passive: true 提高滚动性能
|
|
173
|
+
window.addEventListener("scroll", handleTocScroll, { passive: true });
|
|
174
|
+
handleTocScroll();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// #endregion
|
|
179
|
+
|
|
180
|
+
function initLogic() {
|
|
181
|
+
initKeyboardShortcuts();
|
|
182
|
+
initializeTableOfContents();
|
|
183
|
+
initializeTabs();
|
|
184
|
+
if (typeof mediumZoom === "function") {
|
|
185
|
+
mediumZoom(".article img", {
|
|
186
|
+
background: "hsla(from var(--mantle) / 0.9)",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 初始化时执行一次
|
|
192
|
+
document.addEventListener("DOMContentLoaded", () => initLogic());
|
|
193
|
+
// PJAX完成后执行(注意:这里的函数内已经做了事件去重)
|
|
194
|
+
document.addEventListener("pjax:complete", () => initLogic());
|
|
195
|
+
|
|
196
|
+
// Global functions
|
|
197
|
+
function handleNavbarClick(e) {
|
|
198
|
+
const target = e.target;
|
|
199
|
+
const navbarBurger = document.querySelector(".navbar-burger");
|
|
200
|
+
const navbarMenu = document.querySelector(".navbar-menu");
|
|
201
|
+
|
|
202
|
+
if (!navbarBurger || !navbarMenu) return;
|
|
203
|
+
|
|
204
|
+
// 处理 burger 点击
|
|
205
|
+
if (target.closest(".navbar-burger")) {
|
|
206
|
+
navbarBurger.classList.toggle("is-active");
|
|
207
|
+
navbarMenu.classList.toggle("is-active");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 处理 item 点击
|
|
212
|
+
if (target.closest(".navbar-item")) {
|
|
213
|
+
if (navbarBurger.classList.contains("is-active")) {
|
|
214
|
+
navbarBurger.classList.remove("is-active");
|
|
215
|
+
navbarMenu.classList.remove("is-active");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// biome-ignore lint/correctness/noUnusedVariables: will be used
|
|
3
|
+
let pjax;
|
|
4
|
+
|
|
5
|
+
function initPjax() {
|
|
6
|
+
try {
|
|
7
|
+
const Pjax = window.Pjax || (() => { });
|
|
8
|
+
pjax = new Pjax({
|
|
9
|
+
selectors: [
|
|
10
|
+
"[data-pjax]",
|
|
11
|
+
".pjax-reload",
|
|
12
|
+
"head title",
|
|
13
|
+
".main-content",
|
|
14
|
+
".navbar-start",
|
|
15
|
+
".navbar-end",
|
|
16
|
+
".searchbox link",
|
|
17
|
+
".searchbox script",
|
|
18
|
+
"#comments link",
|
|
19
|
+
"#comments script",
|
|
20
|
+
],
|
|
21
|
+
cacheBust: false,
|
|
22
|
+
});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn(`PJAX error: ${e}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
document.addEventListener("DOMContentLoaded", () => initPjax());
|
|
29
|
+
})();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Constants and Configuration
|
|
2
|
+
const SELECTORS = {
|
|
3
|
+
figureHighlight: "figure.shiki",
|
|
4
|
+
preCode: "pre code",
|
|
5
|
+
preShiki: "pre.shiki",
|
|
6
|
+
expandBtn: ".code-expand-btn",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const CLASSES = {
|
|
10
|
+
copyTrue: "copy-true",
|
|
11
|
+
closed: "closed",
|
|
12
|
+
wrapActive: "wrap-active",
|
|
13
|
+
expandDone: "expand-done",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
showAlert = (element, text, duration = 800) => {
|
|
17
|
+
element.textContent = text;
|
|
18
|
+
element.style.opacity = 1;
|
|
19
|
+
element.style.visibility = "visible";
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
element.style.opacity = 0;
|
|
22
|
+
element.style.visibility = "hidden";
|
|
23
|
+
}, duration);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Feature Handlers
|
|
27
|
+
const FeatureHandlers = {
|
|
28
|
+
async copy(parentElement, clickElement) {
|
|
29
|
+
const buttonParent = parentElement.parentNode;
|
|
30
|
+
buttonParent.classList.add(CLASSES.copyTrue);
|
|
31
|
+
|
|
32
|
+
const codeElement = buttonParent.querySelector(SELECTORS.preCode);
|
|
33
|
+
await navigator.clipboard.writeText(codeElement.innerText);
|
|
34
|
+
showAlert(clickElement.previousElementSibling, "Copied");
|
|
35
|
+
|
|
36
|
+
buttonParent.classList.remove(CLASSES.copyTrue);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
toggleWrap(element) {
|
|
40
|
+
const code = element
|
|
41
|
+
.closest(SELECTORS.figureHighlight)
|
|
42
|
+
.querySelector("code");
|
|
43
|
+
|
|
44
|
+
function setWrap(enabled) {
|
|
45
|
+
Object.assign(code.style, {
|
|
46
|
+
whiteSpace: enabled ? "pre-wrap" : "pre",
|
|
47
|
+
wordBreak: enabled ? "break-all" : "normal",
|
|
48
|
+
overflowWrap: enabled ? "anywhere" : "normal",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
element.classList.toggle(CLASSES.wrapActive, enabled);
|
|
52
|
+
}
|
|
53
|
+
setWrap(code.style.whiteSpace !== "pre-wrap");
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
expandCode(figure) {
|
|
57
|
+
const expandBtn = figure.querySelector(SELECTORS.expandBtn);
|
|
58
|
+
const pre = figure.querySelector(SELECTORS.preShiki);
|
|
59
|
+
|
|
60
|
+
const isExpanded = figure.classList.contains("expanded");
|
|
61
|
+
const showLines = parseInt(figure.dataset.showLines || "10");
|
|
62
|
+
|
|
63
|
+
if (isExpanded) {
|
|
64
|
+
// 记录折叠前的状态
|
|
65
|
+
const beforeCollapseHeight = pre.scrollHeight;
|
|
66
|
+
|
|
67
|
+
// 计算折叠后的目标高度
|
|
68
|
+
const computedStyle = getComputedStyle(pre);
|
|
69
|
+
const lineHeight = parseFloat(computedStyle.lineHeight) || 20;
|
|
70
|
+
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
|
|
71
|
+
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
|
72
|
+
const targetHeight = showLines * lineHeight + paddingTop + paddingBottom;
|
|
73
|
+
|
|
74
|
+
// 首先设置当前完整高度作为起点
|
|
75
|
+
pre.style.maxHeight = `${beforeCollapseHeight}px`;
|
|
76
|
+
pre.offsetHeight; // 强制重排
|
|
77
|
+
|
|
78
|
+
// 应用折叠状态
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
figure.classList.remove("expanded");
|
|
81
|
+
pre.style.maxHeight = `${targetHeight}px`;
|
|
82
|
+
|
|
83
|
+
// 延迟箭头旋转,等待折叠动画完成
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
expandBtn.classList.remove(CLASSES.expandDone);
|
|
86
|
+
}, 300); // 与CSS transition时间同步
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
// 展开代码
|
|
90
|
+
const currentHeight = pre.offsetHeight;
|
|
91
|
+
const fullHeight = pre.scrollHeight;
|
|
92
|
+
|
|
93
|
+
// 先设置当前高度作为起点
|
|
94
|
+
pre.style.maxHeight = `${currentHeight}px`;
|
|
95
|
+
pre.offsetHeight; // 强制重排
|
|
96
|
+
|
|
97
|
+
// 应用展开状态
|
|
98
|
+
figure.classList.add("expanded");
|
|
99
|
+
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
pre.style.maxHeight = `${fullHeight}px`;
|
|
102
|
+
|
|
103
|
+
// 立即开始箭头旋转动画
|
|
104
|
+
expandBtn.classList.add(CLASSES.expandDone);
|
|
105
|
+
|
|
106
|
+
// 动画结束后清除max-height限制,允许内容自然增长
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
if (figure.classList.contains("expanded")) {
|
|
109
|
+
pre.style.maxHeight = "none";
|
|
110
|
+
}
|
|
111
|
+
}, 300);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function handleToolbarClick(event) {
|
|
118
|
+
const target = event.target;
|
|
119
|
+
const classList = target.classList;
|
|
120
|
+
|
|
121
|
+
const handlers = {
|
|
122
|
+
expand: () => FeatureHandlers.shrink(this),
|
|
123
|
+
"copy-button": () => FeatureHandlers.copy(this, target),
|
|
124
|
+
"toggle-wrap": () => FeatureHandlers.toggleWrap(this),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const [className, handler] of Object.entries(handlers)) {
|
|
128
|
+
if (classList.contains(className)) {
|
|
129
|
+
handler();
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Code expand button event handler
|
|
136
|
+
function handleExpandBtnClick(event) {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
event.stopPropagation();
|
|
139
|
+
|
|
140
|
+
const expandBtn = event.currentTarget;
|
|
141
|
+
const figure = expandBtn.closest(SELECTORS.figureHighlight);
|
|
142
|
+
|
|
143
|
+
if (figure) {
|
|
144
|
+
FeatureHandlers.expandCode(figure);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Main initialization function
|
|
149
|
+
function addHighlightTool() {
|
|
150
|
+
const figures = document.querySelectorAll(SELECTORS.figureHighlight);
|
|
151
|
+
if (!figures.length) return;
|
|
152
|
+
|
|
153
|
+
figures.forEach((figure) => {
|
|
154
|
+
if (figure.hasAttribute("data-shiki-initialized")) return;
|
|
155
|
+
figure.setAttribute("data-shiki-initialized", "true");
|
|
156
|
+
|
|
157
|
+
// Add event listener to existing shiki-tools
|
|
158
|
+
const toolbar = figure.querySelector(".shiki-tools");
|
|
159
|
+
if (toolbar) {
|
|
160
|
+
toolbar.addEventListener("click", handleToolbarClick);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add event listener to code expand button
|
|
164
|
+
const expandBtn = figure.querySelector(SELECTORS.expandBtn);
|
|
165
|
+
if (expandBtn) {
|
|
166
|
+
expandBtn.addEventListener("click", handleExpandBtnClick);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Initialize collapsed state for collapsible code blocks
|
|
170
|
+
if (figure.dataset.collapsible === "true") {
|
|
171
|
+
const pre = figure.querySelector(SELECTORS.preShiki);
|
|
172
|
+
const showLines = parseInt(figure.dataset.showLines || "10");
|
|
173
|
+
|
|
174
|
+
if (pre) {
|
|
175
|
+
// 确保元素已经渲染完成后再设置高度
|
|
176
|
+
requestAnimationFrame(() => {
|
|
177
|
+
const lineHeight = parseFloat(getComputedStyle(pre).lineHeight) || 20;
|
|
178
|
+
const maxHeight = showLines * lineHeight;
|
|
179
|
+
pre.style.maxHeight = `${maxHeight}px`;
|
|
180
|
+
pre.style.overflow = "hidden";
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
} // Event listeners
|
|
186
|
+
|
|
187
|
+
if (document.readyState === "loading") {
|
|
188
|
+
document.addEventListener("DOMContentLoaded", addHighlightTool);
|
|
189
|
+
} else {
|
|
190
|
+
addHighlightTool();
|
|
191
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
((window, document, localStorage) => {
|
|
2
|
+
const STORAGE_KEY = "themePreference";
|
|
3
|
+
const THEME_SELECTOR_ID = "theme-selector";
|
|
4
|
+
const DEFAULT_THEME = "system";
|
|
5
|
+
const colorSchemeMediaQuery = window.matchMedia(
|
|
6
|
+
"(prefers-color-scheme: dark)",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const THEME_MAP = {
|
|
10
|
+
mocha: "night",
|
|
11
|
+
macchiato: "night",
|
|
12
|
+
nord: "light",
|
|
13
|
+
nord_night: "night",
|
|
14
|
+
tokyo_night: "night",
|
|
15
|
+
latte: "light",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let currentIndex = 0;
|
|
19
|
+
let previewTheme = null;
|
|
20
|
+
let originalTheme = null;
|
|
21
|
+
let isModalOpen = false;
|
|
22
|
+
|
|
23
|
+
function getThemePreference() {
|
|
24
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
25
|
+
return stored && stored in THEME_MAP ? stored : DEFAULT_THEME;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function applyTheme(theme, persist = false) {
|
|
29
|
+
const html = document.documentElement;
|
|
30
|
+
const resolvedTheme =
|
|
31
|
+
theme === "system"
|
|
32
|
+
? colorSchemeMediaQuery.matches
|
|
33
|
+
? "mocha"
|
|
34
|
+
: "nord"
|
|
35
|
+
: theme;
|
|
36
|
+
html.setAttribute("data-theme", resolvedTheme);
|
|
37
|
+
html.classList.remove("night", "light");
|
|
38
|
+
html.classList.add(THEME_MAP[resolvedTheme]);
|
|
39
|
+
|
|
40
|
+
if (persist) {
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function openModal() {
|
|
46
|
+
const modal = document.getElementById("theme-selector-modal");
|
|
47
|
+
if (!modal || isModalOpen) return;
|
|
48
|
+
|
|
49
|
+
isModalOpen = true;
|
|
50
|
+
originalTheme = getThemePreference();
|
|
51
|
+
|
|
52
|
+
// Find current theme index
|
|
53
|
+
const themeOptions = modal.querySelectorAll(".theme-option");
|
|
54
|
+
themeOptions.forEach((option, index) => {
|
|
55
|
+
const theme = option.getAttribute("data-theme-option");
|
|
56
|
+
if (theme === originalTheme) {
|
|
57
|
+
currentIndex = index;
|
|
58
|
+
}
|
|
59
|
+
// Update active state
|
|
60
|
+
if (theme === originalTheme) {
|
|
61
|
+
option.classList.add("is-active");
|
|
62
|
+
} else {
|
|
63
|
+
option.classList.remove("is-active");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Set initial focus
|
|
68
|
+
updateFocus(themeOptions);
|
|
69
|
+
|
|
70
|
+
modal.classList.add("is-active");
|
|
71
|
+
document.body.style.overflow = "hidden";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function closeModal(apply = false) {
|
|
75
|
+
const modal = document.getElementById("theme-selector-modal");
|
|
76
|
+
if (!modal || !isModalOpen) return;
|
|
77
|
+
|
|
78
|
+
isModalOpen = false;
|
|
79
|
+
|
|
80
|
+
if (apply && previewTheme) {
|
|
81
|
+
// Apply the selected theme
|
|
82
|
+
applyTheme(previewTheme, true);
|
|
83
|
+
} else if (previewTheme && previewTheme !== originalTheme) {
|
|
84
|
+
// Restore original theme if cancelled
|
|
85
|
+
applyTheme(originalTheme);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
modal.classList.remove("is-active");
|
|
89
|
+
document.body.style.overflow = "";
|
|
90
|
+
previewTheme = null;
|
|
91
|
+
originalTheme = null;
|
|
92
|
+
|
|
93
|
+
// Clear all focus states
|
|
94
|
+
const themeOptions = modal.querySelectorAll(".theme-option");
|
|
95
|
+
for (const t of themeOptions) {
|
|
96
|
+
t.classList.remove("is-active");
|
|
97
|
+
t.classList.remove("is-focused");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateFocus(themeOptions) {
|
|
102
|
+
themeOptions.forEach((option, index) => {
|
|
103
|
+
if (index === currentIndex) {
|
|
104
|
+
option.classList.add("is-focused");
|
|
105
|
+
option.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
106
|
+
|
|
107
|
+
// Preview theme on focus
|
|
108
|
+
const theme = option.getAttribute("data-theme-option");
|
|
109
|
+
if (theme !== previewTheme) {
|
|
110
|
+
previewTheme = theme;
|
|
111
|
+
applyTheme(theme);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
option.classList.remove("is-focused");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function handleKeyboard(event) {
|
|
120
|
+
if (!isModalOpen) return;
|
|
121
|
+
|
|
122
|
+
const modal = document.getElementById("theme-selector-modal");
|
|
123
|
+
const themeOptions = modal.querySelectorAll(".theme-option");
|
|
124
|
+
const maxIndex = themeOptions.length - 1;
|
|
125
|
+
|
|
126
|
+
switch (event.key) {
|
|
127
|
+
case "ArrowDown":
|
|
128
|
+
case "Down":
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
currentIndex = currentIndex < maxIndex ? currentIndex + 1 : 0;
|
|
131
|
+
updateFocus(themeOptions);
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case "ArrowUp":
|
|
135
|
+
case "Up":
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
currentIndex = currentIndex > 0 ? currentIndex - 1 : maxIndex;
|
|
138
|
+
updateFocus(themeOptions);
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case "Enter":
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
closeModal(true);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "Escape":
|
|
147
|
+
case "Esc":
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
closeModal(false);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 初始化主题
|
|
155
|
+
applyTheme(getThemePreference());
|
|
156
|
+
|
|
157
|
+
// 监听系统主题改变
|
|
158
|
+
colorSchemeMediaQuery.addEventListener("change", () => {
|
|
159
|
+
if (getThemePreference() === "system") {
|
|
160
|
+
applyTheme("system");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 监听主题选择框变化 (legacy support)
|
|
165
|
+
document.addEventListener("change", (event) => {
|
|
166
|
+
if (event.target.id === THEME_SELECTOR_ID) {
|
|
167
|
+
applyTheme(event.target.value, true);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 监听点击事件
|
|
172
|
+
document.addEventListener("click", (event) => {
|
|
173
|
+
// Open modal when theme selector is clicked
|
|
174
|
+
if (event.target.closest(".theme-selector-trigger")) {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
openModal();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Close modal when backdrop is clicked
|
|
181
|
+
if (event.target.classList.contains("theme-selector-backdrop")) {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
closeModal(false);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Handle theme option click in modal
|
|
188
|
+
const themeOption = event.target.closest(".theme-option");
|
|
189
|
+
if (themeOption && isModalOpen) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
const modal = document.getElementById("theme-selector-modal");
|
|
192
|
+
const themeOptions = modal.querySelectorAll(".theme-option");
|
|
193
|
+
currentIndex = parseInt(themeOption.getAttribute("data-index"), 10);
|
|
194
|
+
updateFocus(themeOptions);
|
|
195
|
+
// Small delay before closing to show selection
|
|
196
|
+
setTimeout(() => closeModal(true), 150);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 监听键盘事件
|
|
202
|
+
document.addEventListener("keydown", handleKeyboard);
|
|
203
|
+
|
|
204
|
+
// Export for navbar to get current theme
|
|
205
|
+
window.getThemePreference = getThemePreference;
|
|
206
|
+
})(window, document, window.localStorage);
|
package/util/cache.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View cache utility functions.
|
|
3
|
+
* @module util/cache
|
|
4
|
+
*/
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const { createElement } = require("inferno-create-element");
|
|
7
|
+
|
|
8
|
+
const cache = {};
|
|
9
|
+
|
|
10
|
+
function computeHash(props) {
|
|
11
|
+
return crypto.createHash("md5").update(JSON.stringify(props)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
/**
|
|
16
|
+
* Create cached component from a given component class.
|
|
17
|
+
* The cache ID is caculated from the input props.
|
|
18
|
+
*
|
|
19
|
+
* @param {Component} type JSX component class
|
|
20
|
+
* @param {string} prefix Cache ID prefix
|
|
21
|
+
* @param {Function} transform Transform the input props to target props and
|
|
22
|
+
* its result is used to compute cache ID
|
|
23
|
+
* @returns The new cachable functional component, which uses the <code>transform</code> function and the
|
|
24
|
+
* component props to calculate the cache ID.
|
|
25
|
+
* <br>
|
|
26
|
+
* The cache ID will be used to determine whether the same element with the exact same
|
|
27
|
+
* props has been created and cached. If so, the cached element will be returned.
|
|
28
|
+
* If the cache ID can be computed, a new element will be created use the
|
|
29
|
+
* <code>createElement</code> function of the inferno.js, then it will be cached and returned.
|
|
30
|
+
* If the <code>transform</code>ed props is empty (<code>!transform(props)</code>), the cachable
|
|
31
|
+
* functional component will also return null element when <code>createElement</code> is called
|
|
32
|
+
* on it.
|
|
33
|
+
*/
|
|
34
|
+
cacheComponent(type, prefix, transform) {
|
|
35
|
+
return (props) => {
|
|
36
|
+
const targetProps = transform(props);
|
|
37
|
+
if (targetProps === null || typeof targetProps !== "object") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const cacheId = `${prefix}-${computeHash(targetProps)}`;
|
|
41
|
+
if (!cache[cacheId]) {
|
|
42
|
+
cache[cacheId] = createElement(type, targetProps);
|
|
43
|
+
}
|
|
44
|
+
return cache[cacheId];
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|