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.
Files changed (143) hide show
  1. package/README.md +106 -0
  2. package/include/hexo/filter/locals.js +109 -0
  3. package/include/hexo/generator/categories.js +12 -0
  4. package/include/hexo/generator/category.js +52 -0
  5. package/include/hexo/generator/insight.js +50 -0
  6. package/include/hexo/generator/manifest.js +23 -0
  7. package/include/hexo/generator/tags.js +12 -0
  8. package/include/hexo/helper/cdn.js +21 -0
  9. package/include/hexo/helper/page.js +27 -0
  10. package/include/hexo/view.js +40 -0
  11. package/include/register.js +11 -0
  12. package/include/util/common.js +33 -0
  13. package/languages/en.yml +47 -0
  14. package/languages/fr.yml +46 -0
  15. package/languages/ja.yml +46 -0
  16. package/languages/zh-CN.yml +47 -0
  17. package/languages/zh-TW.yml +47 -0
  18. package/layout/archive.jsx +118 -0
  19. package/layout/categories.jsx +137 -0
  20. package/layout/category.jsx +38 -0
  21. package/layout/comment/disqus.jsx +79 -0
  22. package/layout/comment/disqusjs.jsx +127 -0
  23. package/layout/comment/giscus.jsx +193 -0
  24. package/layout/comment/gitalk.jsx +141 -0
  25. package/layout/comment/twikoo.jsx +63 -0
  26. package/layout/comment/utterances.jsx +86 -0
  27. package/layout/comment/valine.jsx +143 -0
  28. package/layout/comment/waline.jsx +156 -0
  29. package/layout/common/article.jsx +131 -0
  30. package/layout/common/article_cover.jsx +33 -0
  31. package/layout/common/article_media.jsx +34 -0
  32. package/layout/common/comment.jsx +38 -0
  33. package/layout/common/footer.jsx +228 -0
  34. package/layout/common/head.jsx +242 -0
  35. package/layout/common/navbar.jsx +219 -0
  36. package/layout/common/plugins.jsx +39 -0
  37. package/layout/common/scripts.jsx +49 -0
  38. package/layout/common/search.jsx +22 -0
  39. package/layout/common/theme_selector.jsx +79 -0
  40. package/layout/common/toc.jsx +53 -0
  41. package/layout/index.jsx +29 -0
  42. package/layout/layout.jsx +34 -0
  43. package/layout/misc/article_licensing.jsx +114 -0
  44. package/layout/misc/meta.jsx +61 -0
  45. package/layout/misc/open_graph.jsx +164 -0
  46. package/layout/misc/paginator.jsx +90 -0
  47. package/layout/misc/structured_data.jsx +110 -0
  48. package/layout/misc/web_app.jsx +106 -0
  49. package/layout/page.jsx +12 -0
  50. package/layout/plugin/bing_webmaster.jsx +47 -0
  51. package/layout/plugin/busuanzi.jsx +40 -0
  52. package/layout/plugin/clarity.jsx +22 -0
  53. package/layout/plugin/cookie_consent.jsx +136 -0
  54. package/layout/plugin/google_analytics.jsx +66 -0
  55. package/layout/plugin/google_tag_mamager.jsx +41 -0
  56. package/layout/plugin/netlify.jsx +39 -0
  57. package/layout/plugin/pjax.jsx +20 -0
  58. package/layout/plugin/statcounter.jsx +69 -0
  59. package/layout/plugin/twitter_conversion_tracking.jsx +51 -0
  60. package/layout/post.jsx +16 -0
  61. package/layout/search/insight.jsx +53 -0
  62. package/layout/tag.jsx +29 -0
  63. package/layout/tags.jsx +55 -0
  64. package/package.json +42 -0
  65. package/scripts/index.js +1 -0
  66. package/source/css/callout_blocks.css +204 -0
  67. package/source/css/default.css +1590 -0
  68. package/source/css/font/woff2/Futura-Book.woff2 +0 -0
  69. package/source/css/font/woff2/Paris2024-Variable.woff2 +0 -0
  70. package/source/css/font/woff2/doto.woff2 +0 -0
  71. package/source/css/optional/chinese.css +17 -0
  72. package/source/css/responsive/desktop.css +164 -0
  73. package/source/css/responsive/mobile.css +46 -0
  74. package/source/css/responsive/tablet.css +46 -0
  75. package/source/css/responsive/touch.css +254 -0
  76. package/source/css/shiki/shiki.min.css +1 -0
  77. package/source/css/twikoo.css +2143 -0
  78. package/source/img/avatar.webp +0 -0
  79. package/source/img/background.webp +0 -0
  80. package/source/img/favicon.svg +6 -0
  81. package/source/img/logo.svg +9 -0
  82. package/source/img/og_image.png +0 -0
  83. package/source/js/busuanzi.js +46 -0
  84. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +6 -0
  85. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +1 -0
  86. package/source/js/host/iconify-icon/3.0.2/iconify-icon.min.js +12 -0
  87. package/source/js/host/medium-zoom/dist/medium-zoom.min.js +2 -0
  88. package/source/js/host/mermaid/mermaid.min.js +2811 -0
  89. package/source/js/host/pjax/0.2.8/pjax.min.js +1 -0
  90. package/source/js/host/twikoo/1.6.41/dist/twikoo.all.min.js +2 -0
  91. package/source/js/insight.js +330 -0
  92. package/source/js/instant-page.min.js +1 -0
  93. package/source/js/live2d_Asoul/Model/Ava/Ava.4096/texture_00.webp +0 -0
  94. package/source/js/live2d_Asoul/Model/Ava/Ava.moc3 +0 -0
  95. package/source/js/live2d_Asoul/Model/Ava/Ava.model3.json +323 -0
  96. package/source/js/live2d_Asoul/Model/Ava/Ava.physics3.json +1225 -0
  97. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_idle.motion3.json +1 -0
  98. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_shake01.motion3.json +1 -0
  99. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_shake02.motion3.json +1 -0
  100. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap01.motion3.json +1 -0
  101. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap02.motion3.json +1 -0
  102. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap03.motion3.json +1 -0
  103. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap04.motion3.json +1 -0
  104. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap05.motion3.json +1 -0
  105. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap06.motion3.json +1 -0
  106. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap07.motion3.json +1 -0
  107. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap08.motion3.json +1 -0
  108. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap09.motion3.json +1 -0
  109. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap10.motion3.json +1 -0
  110. package/source/js/live2d_Asoul/Model/Ava/motions/Ava_tap11.motion3.json +1 -0
  111. package/source/js/live2d_Asoul/Model/Ava/raw.ex.json +16 -0
  112. package/source/js/live2d_Asoul/Model/Ava/raw.model3.json +321 -0
  113. package/source/js/live2d_Asoul/Model/Diana/Diana.4096/texture_00.webp +0 -0
  114. package/source/js/live2d_Asoul/Model/Diana/Diana.moc3 +0 -0
  115. package/source/js/live2d_Asoul/Model/Diana/Diana.model3.json +212 -0
  116. package/source/js/live2d_Asoul/Model/Diana/Diana.physics3.json +764 -0
  117. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_idle.motion3.json +1 -0
  118. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap01.motion3.json +1 -0
  119. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap02.motion3.json +1 -0
  120. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap03.motion3.json +1 -0
  121. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap04.motion3.json +1 -0
  122. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap05.motion3.json +1 -0
  123. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap06.motion3.json +1 -0
  124. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap07.motion3.json +1 -0
  125. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap08.motion3.json +1 -0
  126. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap09.motion3.json +1 -0
  127. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap10.motion3.json +1 -0
  128. package/source/js/live2d_Asoul/Model/Diana/motions/Diana_tap11.motion3.json +1 -0
  129. package/source/js/live2d_Asoul/Model/Diana/raw.ex.json +16 -0
  130. package/source/js/live2d_Asoul/Model/Diana/raw.model3.json +210 -0
  131. package/source/js/live2d_Asoul/TweenLite.js +12 -0
  132. package/source/js/live2d_Asoul/cubism4.min.js +2 -0
  133. package/source/js/live2d_Asoul/live2dcubismcore.min.js +9 -0
  134. package/source/js/live2d_Asoul/load.js +231 -0
  135. package/source/js/live2d_Asoul/pio.css +161 -0
  136. package/source/js/live2d_Asoul/pio.js +296 -0
  137. package/source/js/live2d_Asoul/pio_sdk4.js +149 -0
  138. package/source/js/live2d_Asoul/pixi.min.js +9 -0
  139. package/source/js/main.js +218 -0
  140. package/source/js/pjax.js +29 -0
  141. package/source/js/shiki/shiki.js +191 -0
  142. package/source/js/theme-selector.js +206 -0
  143. 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
+ };