hexo-theme-hydrogen 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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * 主脚本文件
3
+ */
4
+ (function () {
5
+ "use strict";
6
+
7
+ // ===== 主题切换 =====
8
+ function initThemeToggle() {
9
+ const toggle = document.querySelector(".theme-toggle");
10
+ if (!toggle) return;
11
+
12
+ // 获取保存的主题或系统偏好
13
+ const savedTheme = localStorage.getItem("theme");
14
+ const prefersDark = window.matchMedia(
15
+ "(prefers-color-scheme: dark)"
16
+ ).matches;
17
+ const defaultTheme =
18
+ document.documentElement.dataset.defaultTheme || "auto";
19
+
20
+ let currentTheme;
21
+ if (savedTheme) {
22
+ currentTheme = savedTheme;
23
+ } else if (defaultTheme === "auto") {
24
+ currentTheme = prefersDark ? "dark" : "light";
25
+ } else {
26
+ currentTheme = defaultTheme;
27
+ }
28
+
29
+ setTheme(currentTheme);
30
+
31
+ // 点击切换
32
+ toggle.addEventListener("click", function () {
33
+ const isDark = document.documentElement.dataset.theme === "dark";
34
+ const newTheme = isDark ? "light" : "dark";
35
+ setTheme(newTheme);
36
+ localStorage.setItem("theme", newTheme);
37
+ });
38
+
39
+ // 监听系统主题变化
40
+ window
41
+ .matchMedia("(prefers-color-scheme: dark)")
42
+ .addEventListener("change", function (e) {
43
+ if (!localStorage.getItem("theme")) {
44
+ setTheme(e.matches ? "dark" : "light");
45
+ }
46
+ });
47
+ }
48
+
49
+ function setTheme(theme) {
50
+ document.documentElement.dataset.theme = theme;
51
+ }
52
+
53
+ // ===== 移动端菜单 =====
54
+ function initMobileMenu() {
55
+ const menuToggle = document.querySelector(".menu-toggle");
56
+ const mobileNav = document.querySelector(".mobile-nav");
57
+
58
+ if (!menuToggle || !mobileNav) return;
59
+
60
+ menuToggle.addEventListener("click", function () {
61
+ mobileNav.classList.toggle("active");
62
+ menuToggle.classList.toggle("active");
63
+ });
64
+
65
+ // 点击导航链接后关闭菜单
66
+ mobileNav.querySelectorAll(".mobile-nav-link").forEach(function (link) {
67
+ link.addEventListener("click", function () {
68
+ mobileNav.classList.remove("active");
69
+ menuToggle.classList.remove("active");
70
+ });
71
+ });
72
+
73
+ // 点击外部关闭
74
+ document.addEventListener("click", function (e) {
75
+ if (!menuToggle.contains(e.target) && !mobileNav.contains(e.target)) {
76
+ mobileNav.classList.remove("active");
77
+ menuToggle.classList.remove("active");
78
+ }
79
+ });
80
+ }
81
+
82
+ // ===== TOC 高亮 =====
83
+ function initTocHighlight() {
84
+ const toc = document.querySelector(".toc-content");
85
+ if (!toc) return;
86
+
87
+ const headings = document.querySelectorAll(
88
+ ".markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4"
89
+ );
90
+ if (headings.length === 0) return;
91
+
92
+ const headerHeight =
93
+ parseInt(
94
+ getComputedStyle(document.documentElement).getPropertyValue(
95
+ "--header-height"
96
+ )
97
+ ) || 64;
98
+ const offset = headerHeight + 20;
99
+
100
+ let activeLink = null;
101
+
102
+ function updateActiveHeading() {
103
+ let current = null;
104
+
105
+ headings.forEach(function (heading) {
106
+ const rect = heading.getBoundingClientRect();
107
+ if (rect.top <= offset) {
108
+ current = heading;
109
+ }
110
+ });
111
+
112
+ if (current) {
113
+ const id = current.id;
114
+ const link = toc.querySelector('a[href="#' + id + '"]');
115
+
116
+ if (link && link !== activeLink) {
117
+ if (activeLink) {
118
+ activeLink.parentElement.classList.remove("active");
119
+ }
120
+ link.parentElement.classList.add("active");
121
+ activeLink = link;
122
+ }
123
+ }
124
+ }
125
+
126
+ window.addEventListener("scroll", throttle(updateActiveHeading, 100));
127
+ updateActiveHeading();
128
+ }
129
+
130
+ // ===== 图片灯箱 =====
131
+ function initLightbox() {
132
+ const images = document.querySelectorAll(".markdown-body img");
133
+ if (images.length === 0) return;
134
+
135
+ // 创建灯箱
136
+ const lightbox = document.createElement("div");
137
+ lightbox.className = "lightbox";
138
+ lightbox.innerHTML = '<img src="" alt="">';
139
+ document.body.appendChild(lightbox);
140
+
141
+ const lightboxImg = lightbox.querySelector("img");
142
+
143
+ images.forEach(function (img) {
144
+ img.style.cursor = "zoom-in";
145
+ img.addEventListener("click", function () {
146
+ lightboxImg.src = img.src;
147
+ lightboxImg.alt = img.alt;
148
+ lightbox.classList.add("active");
149
+ document.body.style.overflow = "hidden";
150
+ });
151
+ });
152
+
153
+ lightbox.addEventListener("click", function () {
154
+ lightbox.classList.remove("active");
155
+ document.body.style.overflow = "";
156
+ });
157
+
158
+ document.addEventListener("keydown", function (e) {
159
+ if (e.key === "Escape" && lightbox.classList.contains("active")) {
160
+ lightbox.classList.remove("active");
161
+ document.body.style.overflow = "";
162
+ }
163
+ });
164
+ }
165
+
166
+ // ===== 图片懒加载 =====
167
+ function initLazyLoad() {
168
+ const images = document.querySelectorAll(
169
+ '.markdown-body img[loading="lazy"]'
170
+ );
171
+
172
+ if ("IntersectionObserver" in window) {
173
+ const observer = new IntersectionObserver(
174
+ function (entries) {
175
+ entries.forEach(function (entry) {
176
+ if (entry.isIntersecting) {
177
+ const img = entry.target;
178
+ if (img.dataset.src) {
179
+ img.src = img.dataset.src;
180
+ img.removeAttribute("data-src");
181
+ }
182
+ observer.unobserve(img);
183
+ }
184
+ });
185
+ },
186
+ {
187
+ rootMargin: "100px",
188
+ }
189
+ );
190
+
191
+ images.forEach(function (img) {
192
+ observer.observe(img);
193
+ });
194
+ }
195
+ }
196
+
197
+ // ===== 代码复制按钮 =====
198
+ function initCodeCopy() {
199
+ const codeBlocks = document.querySelectorAll(".markdown-body pre");
200
+
201
+ codeBlocks.forEach(function (pre) {
202
+ const wrapper = document.createElement("div");
203
+ wrapper.className = "code-wrapper";
204
+ wrapper.style.position = "relative";
205
+
206
+ const button = document.createElement("button");
207
+ button.className = "code-copy-btn";
208
+ button.innerHTML =
209
+ '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>';
210
+ button.title = "复制代码";
211
+ button.style.cssText =
212
+ "position: absolute; top: 8px; right: 8px; padding: 6px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); cursor: pointer; opacity: 0; transition: opacity 0.2s; color: var(--text-secondary);";
213
+
214
+ pre.parentNode.insertBefore(wrapper, pre);
215
+ wrapper.appendChild(pre);
216
+ wrapper.appendChild(button);
217
+
218
+ wrapper.addEventListener("mouseenter", function () {
219
+ button.style.opacity = "1";
220
+ });
221
+
222
+ wrapper.addEventListener("mouseleave", function () {
223
+ button.style.opacity = "0";
224
+ });
225
+
226
+ button.addEventListener("click", function () {
227
+ const code = pre.querySelector("code");
228
+ const text = code ? code.textContent : pre.textContent;
229
+
230
+ navigator.clipboard.writeText(text).then(function () {
231
+ button.innerHTML =
232
+ '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
233
+ button.style.color = "var(--success)";
234
+
235
+ setTimeout(function () {
236
+ button.innerHTML =
237
+ '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>';
238
+ button.style.color = "var(--text-secondary)";
239
+ }, 2000);
240
+ });
241
+ });
242
+ });
243
+ }
244
+
245
+ // ===== 平滑滚动到锚点 =====
246
+ function initSmoothScroll() {
247
+ document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
248
+ anchor.addEventListener("click", function (e) {
249
+ const href = this.getAttribute("href");
250
+ if (href === "#") return;
251
+
252
+ const target = document.querySelector(href);
253
+ if (target) {
254
+ e.preventDefault();
255
+ const headerHeight =
256
+ parseInt(
257
+ getComputedStyle(document.documentElement).getPropertyValue(
258
+ "--header-height"
259
+ )
260
+ ) || 64;
261
+ const top =
262
+ target.getBoundingClientRect().top +
263
+ window.pageYOffset -
264
+ headerHeight -
265
+ 20;
266
+
267
+ window.scrollTo({
268
+ top: top,
269
+ behavior: "smooth",
270
+ });
271
+
272
+ // 更新 URL
273
+ history.pushState(null, null, href);
274
+ }
275
+ });
276
+ });
277
+ }
278
+
279
+ // ===== 回到顶部 =====
280
+ function initBackToTop() {
281
+ const button = document.createElement("button");
282
+ button.className = "back-to-top";
283
+ button.innerHTML =
284
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"></path></svg>';
285
+ button.title = "回到顶部";
286
+ button.style.cssText =
287
+ "position: fixed; bottom: 24px; right: 24px; z-index: 100; width: 48px; height: 48px; display: none; align-items: center; justify-content: center; background: var(--primary-color); color: white; border: none; border-radius: 50%; cursor: pointer; box-shadow: var(--shadow-lg); transition: all 0.3s;";
288
+ document.body.appendChild(button);
289
+
290
+ window.addEventListener(
291
+ "scroll",
292
+ throttle(function () {
293
+ if (window.pageYOffset > 300) {
294
+ button.style.display = "flex";
295
+ } else {
296
+ button.style.display = "none";
297
+ }
298
+ }, 100)
299
+ );
300
+
301
+ button.addEventListener("click", function () {
302
+ window.scrollTo({
303
+ top: 0,
304
+ behavior: "smooth",
305
+ });
306
+ });
307
+ }
308
+
309
+ // ===== 外部链接处理 =====
310
+ function initExternalLinks() {
311
+ document.querySelectorAll(".markdown-body a").forEach(function (link) {
312
+ if (link.hostname !== window.location.hostname) {
313
+ link.setAttribute("target", "_blank");
314
+ link.setAttribute("rel", "noopener noreferrer");
315
+ }
316
+ });
317
+ }
318
+
319
+ // ===== 工具函数 =====
320
+ function throttle(func, wait) {
321
+ let timeout = null;
322
+ let previous = 0;
323
+
324
+ return function () {
325
+ const now = Date.now();
326
+ const remaining = wait - (now - previous);
327
+ const context = this;
328
+ const args = arguments;
329
+
330
+ if (remaining <= 0 || remaining > wait) {
331
+ if (timeout) {
332
+ clearTimeout(timeout);
333
+ timeout = null;
334
+ }
335
+ previous = now;
336
+ func.apply(context, args);
337
+ } else if (!timeout) {
338
+ timeout = setTimeout(function () {
339
+ previous = Date.now();
340
+ timeout = null;
341
+ func.apply(context, args);
342
+ }, remaining);
343
+ }
344
+ };
345
+ }
346
+
347
+ // ===== 初始化 =====
348
+ function init() {
349
+ initThemeToggle();
350
+ initMobileMenu();
351
+ initTocHighlight();
352
+ initLightbox();
353
+ initLazyLoad();
354
+ initCodeCopy();
355
+ initSmoothScroll();
356
+ initBackToTop();
357
+ initExternalLinks();
358
+ }
359
+
360
+ // DOM 加载完成后初始化
361
+ if (document.readyState === "loading") {
362
+ document.addEventListener("DOMContentLoaded", init);
363
+ } else {
364
+ init();
365
+ }
366
+ })();
@@ -0,0 +1,267 @@
1
+ /**
2
+ * 本地搜索功能
3
+ */
4
+ (function () {
5
+ "use strict";
6
+
7
+ let searchData = null;
8
+ let searchModal = null;
9
+ let searchInput = null;
10
+ let searchResults = null;
11
+ let isLoading = false;
12
+
13
+ // 初始化搜索
14
+ function initSearch() {
15
+ searchModal = document.getElementById("search-modal");
16
+ searchInput = document.getElementById("search-input");
17
+ searchResults = document.getElementById("search-results");
18
+
19
+ if (!searchModal || !searchInput || !searchResults) {
20
+ return;
21
+ }
22
+
23
+ // 绑定搜索按钮
24
+ const searchToggle = document.querySelector(".search-toggle");
25
+ if (searchToggle) {
26
+ searchToggle.addEventListener("click", openSearch);
27
+ }
28
+
29
+ // 绑定关闭按钮
30
+ const closeBtn = searchModal.querySelector(".search-close");
31
+ if (closeBtn) {
32
+ closeBtn.addEventListener("click", closeSearch);
33
+ }
34
+
35
+ // 点击遮罩关闭
36
+ searchModal.addEventListener("click", function (e) {
37
+ if (e.target === searchModal) {
38
+ closeSearch();
39
+ }
40
+ });
41
+
42
+ // ESC 关闭
43
+ document.addEventListener("keydown", function (e) {
44
+ if (e.key === "Escape" && searchModal.classList.contains("active")) {
45
+ closeSearch();
46
+ }
47
+ // Ctrl/Cmd + K 打开搜索
48
+ if ((e.ctrlKey || e.metaKey) && e.key === "k") {
49
+ e.preventDefault();
50
+ openSearch();
51
+ }
52
+ });
53
+
54
+ // 搜索输入
55
+ let debounceTimer = null;
56
+ searchInput.addEventListener("input", function () {
57
+ clearTimeout(debounceTimer);
58
+ debounceTimer = setTimeout(function () {
59
+ performSearch(searchInput.value.trim());
60
+ }, 300);
61
+ });
62
+ }
63
+
64
+ // 打开搜索
65
+ function openSearch() {
66
+ if (!searchModal) return;
67
+
68
+ searchModal.classList.add("active");
69
+ document.body.style.overflow = "hidden";
70
+
71
+ // 加载搜索数据
72
+ loadSearchData();
73
+
74
+ // 聚焦输入框
75
+ setTimeout(function () {
76
+ searchInput.focus();
77
+ }, 100);
78
+ }
79
+
80
+ // 关闭搜索
81
+ function closeSearch() {
82
+ if (!searchModal) return;
83
+
84
+ searchModal.classList.remove("active");
85
+ document.body.style.overflow = "";
86
+ searchInput.value = "";
87
+ searchResults.innerHTML = "";
88
+ }
89
+
90
+ // 获取站点根路径
91
+ function getSiteRoot() {
92
+ return (window.STARTER_CONFIG && window.STARTER_CONFIG.root) || '/';
93
+ }
94
+
95
+ // 加载搜索数据
96
+ function loadSearchData() {
97
+ if (searchData || isLoading) return;
98
+
99
+ isLoading = true;
100
+ searchResults.innerHTML = '<div class="search-loading">加载中...</div>';
101
+
102
+ const root = getSiteRoot();
103
+ const searchUrl = root + 'search.json';
104
+ fetch(searchUrl)
105
+ .then(function (response) {
106
+ return response.json();
107
+ })
108
+ .then(function (data) {
109
+ searchData = data;
110
+ isLoading = false;
111
+ searchResults.innerHTML =
112
+ '<div class="search-tip">输入关键词开始搜索</div>';
113
+ })
114
+ .catch(function (error) {
115
+ console.error("加载搜索数据失败:", error);
116
+ isLoading = false;
117
+ searchResults.innerHTML =
118
+ '<div class="search-error">加载失败,请刷新重试</div>';
119
+ });
120
+ }
121
+
122
+ // 执行搜索
123
+ function performSearch(query) {
124
+ if (!query) {
125
+ searchResults.innerHTML =
126
+ '<div class="search-tip">输入关键词开始搜索</div>';
127
+ return;
128
+ }
129
+
130
+ if (!searchData) {
131
+ searchResults.innerHTML = '<div class="search-loading">加载中...</div>';
132
+ return;
133
+ }
134
+
135
+ const keywords = query.toLowerCase().split(/\s+/).filter(Boolean);
136
+ const results = [];
137
+
138
+ searchData.forEach(function (item) {
139
+ let score = 0;
140
+ const matchedKeywords = [];
141
+
142
+ keywords.forEach(function (keyword) {
143
+ // 标题匹配(权重最高)
144
+ if (item.title && item.title.toLowerCase().includes(keyword)) {
145
+ score += 10;
146
+ matchedKeywords.push(keyword);
147
+ }
148
+
149
+ // 标签匹配
150
+ if (
151
+ item.tags &&
152
+ item.tags.some(function (tag) {
153
+ return tag.toLowerCase().includes(keyword);
154
+ })
155
+ ) {
156
+ score += 5;
157
+ matchedKeywords.push(keyword);
158
+ }
159
+
160
+ // 内容匹配
161
+ if (item.content && item.content.toLowerCase().includes(keyword)) {
162
+ score += 1;
163
+ matchedKeywords.push(keyword);
164
+ }
165
+ });
166
+
167
+ if (score > 0) {
168
+ results.push({
169
+ item: item,
170
+ score: score,
171
+ matchedKeywords: [...new Set(matchedKeywords)],
172
+ });
173
+ }
174
+ });
175
+
176
+ // 按分数排序
177
+ results.sort(function (a, b) {
178
+ return b.score - a.score;
179
+ });
180
+
181
+ renderResults(results.slice(0, 10), keywords);
182
+ }
183
+
184
+ // 渲染搜索结果
185
+ function renderResults(results, keywords) {
186
+ if (results.length === 0) {
187
+ searchResults.innerHTML =
188
+ '<div class="search-empty">没有找到相关结果</div>';
189
+ return;
190
+ }
191
+
192
+ searchResults.innerHTML = results
193
+ .map(function (result) {
194
+ const item = result.item;
195
+
196
+ // 高亮标题
197
+ let title = escapeHtml(item.title);
198
+ keywords.forEach(function (keyword) {
199
+ const regex = new RegExp("(" + escapeRegExp(keyword) + ")", "gi");
200
+ title = title.replace(regex, "<mark>$1</mark>");
201
+ });
202
+
203
+ // 高亮摘要
204
+ let excerpt = item.excerpt || "";
205
+ excerpt = escapeHtml(excerpt);
206
+ keywords.forEach(function (keyword) {
207
+ const regex = new RegExp("(" + escapeRegExp(keyword) + ")", "gi");
208
+ excerpt = excerpt.replace(regex, "<mark>$1</mark>");
209
+ });
210
+
211
+ // 标签
212
+ let tagsHtml = "";
213
+ if (item.tags && item.tags.length) {
214
+ tagsHtml =
215
+ '<div class="search-result-tags">' +
216
+ item.tags
217
+ .slice(0, 3)
218
+ .map(function (tag) {
219
+ return (
220
+ '<span class="search-tag">' + escapeHtml(tag) + "</span>"
221
+ );
222
+ })
223
+ .join("") +
224
+ "</div>";
225
+ }
226
+
227
+ const root = getSiteRoot();
228
+ return (
229
+ '<a href="' +
230
+ root +
231
+ item.url +
232
+ '" class="search-result-item">' +
233
+ '<div class="search-result-title">' +
234
+ title +
235
+ "</div>" +
236
+ (excerpt
237
+ ? '<div class="search-result-excerpt">' + excerpt + "...</div>"
238
+ : "") +
239
+ tagsHtml +
240
+ (item.date
241
+ ? '<div class="search-result-date">' + item.date + "</div>"
242
+ : "") +
243
+ "</a>"
244
+ );
245
+ })
246
+ .join("");
247
+ }
248
+
249
+ // HTML 转义
250
+ function escapeHtml(text) {
251
+ const div = document.createElement("div");
252
+ div.textContent = text;
253
+ return div.innerHTML;
254
+ }
255
+
256
+ // 正则转义
257
+ function escapeRegExp(string) {
258
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
+ }
260
+
261
+ // DOM 加载完成后初始化
262
+ if (document.readyState === "loading") {
263
+ document.addEventListener("DOMContentLoaded", initSearch);
264
+ } else {
265
+ initSearch();
266
+ }
267
+ })();