hexo-theme-gnix 9.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +4 -2
  2. package/include/hexo/feed.js +5 -5
  3. package/include/hexo/filter.js +25 -1
  4. package/include/hexo/generator/archive.js +116 -0
  5. package/include/hexo/generator/home.js +64 -0
  6. package/include/hexo/generator/index.js +82 -0
  7. package/include/hexo/generator/md_generator.js +87 -0
  8. package/include/hexo/generator/page.js +55 -0
  9. package/include/hexo/generator/tag.js +84 -0
  10. package/include/hexo/helper.js +38 -0
  11. package/include/hexo/i18n.js +183 -0
  12. package/include/util/article_font.js +132 -0
  13. package/include/util/i18n.js +280 -0
  14. package/include/util/theme.js +84 -0
  15. package/languages/en.yml +28 -0
  16. package/languages/zh-CN.yml +28 -0
  17. package/layout/archive.jsx +131 -127
  18. package/layout/common/article.jsx +283 -16
  19. package/layout/common/article_info.jsx +339 -0
  20. package/layout/common/article_media.jsx +11 -4
  21. package/layout/common/comment.jsx +15 -7
  22. package/layout/common/footer.jsx +6 -5
  23. package/layout/common/head.jsx +121 -32
  24. package/layout/common/navbar.jsx +195 -65
  25. package/layout/common/theme_selector.jsx +16 -14
  26. package/layout/layout.jsx +43 -5
  27. package/layout/misc/open_graph.jsx +162 -66
  28. package/layout/misc/paginator.jsx +2 -8
  29. package/layout/plugin/cookie_consent.jsx +252 -53
  30. package/layout/plugin/swup.jsx +1 -1
  31. package/layout/search/insight.jsx +1 -1
  32. package/layout/tag.jsx +3 -2
  33. package/layout/tags.jsx +81 -73
  34. package/package.json +5 -5
  35. package/scripts/index.js +1 -0
  36. package/source/css/archive.css +225 -180
  37. package/source/css/default.css +1162 -98
  38. package/source/css/responsive.css +426 -0
  39. package/source/css/shiki/shiki.css +12 -2081
  40. package/source/css/tags.css +183 -0
  41. package/source/css/twikoo.css +1049 -1045
  42. package/source/img/favicon.svg +1 -6
  43. package/source/img/og_image.webp +0 -0
  44. package/source/js/article-font-utils.js +99 -0
  45. package/source/js/busuanzi.js +91 -24
  46. package/source/js/components/chat.js +169 -50
  47. package/source/js/components/image-carousel.js +152 -108
  48. package/source/js/components/sidenote.js +210 -0
  49. package/source/js/components/text-image-section.js +78 -90
  50. package/source/js/components/theme-stacked.js +65 -33
  51. package/source/js/components/tree.js +30 -16
  52. package/source/js/decrypt.js +7 -2
  53. package/source/js/main.js +428 -5
  54. package/source/js/swup.js +39 -0
  55. package/source/js/theme-selector.js +26 -16
  56. package/include/hexo/generator.js +0 -53
  57. package/layout/misc/article_licensing.jsx +0 -99
  58. package/source/css/responsive/desktop.css +0 -36
  59. package/source/css/responsive/mobile.css +0 -29
  60. package/source/css/responsive/tablet.css +0 -43
  61. package/source/css/responsive/touch.css +0 -155
  62. package/source/img/logo.svg +0 -9
  63. package/source/js/archive-breadcrumb.js +0 -132
  64. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +0 -6
  65. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +0 -1
  66. package/source/js/swup.bundle.js +0 -1
package/source/js/main.js CHANGED
@@ -17,6 +17,7 @@ function tableWrapFix() {
17
17
  function twikoo_handler() {
18
18
  const el = document.getElementById("tko");
19
19
  if (!el) return;
20
+ if (el.dataset.initialized === "true" || el.dataset.initializing === "true") return;
20
21
 
21
22
  const { envId, region, lang, jsUrl, cssUrl } = el.dataset;
22
23
 
@@ -26,10 +27,20 @@ function twikoo_handler() {
26
27
 
27
28
  if (typeof window.twikoo?.init === "function") {
28
29
  window.twikoo.init(config);
30
+ el.dataset.initialized = "true";
29
31
  return;
30
32
  }
31
33
 
32
- loadScriptOnce(jsUrl, () => window.twikoo.init(config));
34
+ el.dataset.initializing = "true";
35
+ loadScriptOnce(jsUrl, () => {
36
+ if (el.dataset.initialized === "true") {
37
+ delete el.dataset.initializing;
38
+ return;
39
+ }
40
+ window.twikoo.init(config);
41
+ el.dataset.initialized = "true";
42
+ delete el.dataset.initializing;
43
+ });
33
44
  }
34
45
  // #region mdit@tab-plugin
35
46
  /**
@@ -288,25 +299,435 @@ function handleMermaid() {
288
299
 
289
300
  // #endregion
290
301
 
302
+ const articleFontConfig = window.__GNIX_ARTICLE_FONT_CONFIG__ || {};
303
+ const ARTICLE_FONT_STORAGE_KEY = articleFontConfig.storageKey || "gnix-article-font";
304
+ const ARTICLE_FONT_DEFAULT_SETTINGS = articleFontConfig.defaultSettings || { size: "medium", type: "serif", lineHeight: 1.7, weight: "regular" };
305
+ const ARTICLE_SIZE_OPTIONS = new Set(articleFontConfig.sizeOptions || ["small", "medium-small", "medium", "medium-large", "large"]);
306
+ const ARTICLE_FONT_OPTIONS = new Set(articleFontConfig.fontOptions || ["serif", "sans-serif", "mono", "handwriting"]);
307
+ const ARTICLE_WEIGHT_OPTIONS = new Set(articleFontConfig.weightOptions || ["light", "regular", "medium"]);
308
+ const ARTICLE_LINE_HEIGHT_MIN = articleFontConfig.lineHeight?.min ?? 1.45;
309
+ const ARTICLE_LINE_HEIGHT_MAX = articleFontConfig.lineHeight?.max ?? 1.9;
310
+ const ARTICLE_CUSTOM_FONT_OPTIONS = articleFontConfig.customFonts?.familyOptions || {
311
+ serif: "--font-serif",
312
+ "sans-serif": "--font-sans-serif",
313
+ mono: "--font-mono",
314
+ handwriting: "--font-handwriting",
315
+ };
316
+ const ARTICLE_CUSTOM_FONT_IMPORT_LIMIT = articleFontConfig.customFonts?.importLimit ?? 6;
317
+ const ARTICLE_CUSTOM_FONT_LINK_SELECTOR = 'link[data-gnix-custom-font="true"]';
318
+
319
+ function getCssVariableValue(name, fallback = "") {
320
+ if (typeof document === "undefined") return fallback;
321
+
322
+ const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
323
+ return value || fallback;
324
+ }
325
+
326
+ // Reads `--font-*` from default.css. Custom fonts are applied as inline
327
+ // styles on <html>, which would override the stylesheet value; we strip
328
+ // those overrides while reading and restore them after.
329
+ function getDefaultCustomFontFamilies() {
330
+ if (typeof document === "undefined") return {};
331
+
332
+ const html = document.documentElement;
333
+ const saved = {};
334
+ Object.values(ARTICLE_CUSTOM_FONT_OPTIONS).forEach((cssVar) => {
335
+ const inline = html.style.getPropertyValue(cssVar);
336
+ if (inline) {
337
+ saved[cssVar] = inline;
338
+ html.style.removeProperty(cssVar);
339
+ }
340
+ });
341
+
342
+ const defaults = {
343
+ serif: getCssVariableValue(ARTICLE_CUSTOM_FONT_OPTIONS.serif),
344
+ "sans-serif": getCssVariableValue(ARTICLE_CUSTOM_FONT_OPTIONS["sans-serif"]),
345
+ mono: getCssVariableValue(ARTICLE_CUSTOM_FONT_OPTIONS.mono),
346
+ handwriting: getCssVariableValue(ARTICLE_CUSTOM_FONT_OPTIONS.handwriting),
347
+ };
348
+
349
+ Object.keys(saved).forEach((cssVar) => {
350
+ html.style.setProperty(cssVar, saved[cssVar]);
351
+ });
352
+
353
+ return defaults;
354
+ }
355
+
356
+ const ARTICLE_FONT_UTILS = window.__GNIX_ARTICLE_FONT_UTILS__ || {};
357
+
358
+ function normalizeCustomFonts(value = {}) {
359
+ const normalized = ARTICLE_FONT_UTILS.normalizeCustomFonts
360
+ ? ARTICLE_FONT_UTILS.normalizeCustomFonts(value, ARTICLE_CUSTOM_FONT_OPTIONS, ARTICLE_CUSTOM_FONT_IMPORT_LIMIT)
361
+ : { imports: [], families: {} };
362
+ const defaultFamilies = getDefaultCustomFontFamilies();
363
+
364
+ Object.keys(ARTICLE_CUSTOM_FONT_OPTIONS).forEach((key) => {
365
+ if (!normalized.families[key]) {
366
+ normalized.families[key] = defaultFamilies[key] || "";
367
+ }
368
+ });
369
+
370
+ return normalized;
371
+ }
372
+
373
+ function applyCustomFonts(customFonts = {}) {
374
+ const normalized = normalizeCustomFonts(customFonts);
375
+ if (ARTICLE_FONT_UTILS.applyCustomFontImports) {
376
+ ARTICLE_FONT_UTILS.applyCustomFontImports(normalized.imports, ARTICLE_CUSTOM_FONT_LINK_SELECTOR);
377
+ }
378
+ if (ARTICLE_FONT_UTILS.applyCustomFontFamilies) {
379
+ ARTICLE_FONT_UTILS.applyCustomFontFamilies(document.documentElement, normalized.families, ARTICLE_CUSTOM_FONT_OPTIONS);
380
+ }
381
+ return normalized;
382
+ }
383
+
384
+ function initHoverPopover(trigger, popover) {
385
+ if (!trigger || !popover || typeof popover.showPopover !== "function" || typeof popover.hidePopover !== "function") {
386
+ return;
387
+ }
388
+
389
+ let hideTimer = null;
390
+
391
+ function clearHideTimer() {
392
+ if (hideTimer) {
393
+ clearTimeout(hideTimer);
394
+ hideTimer = null;
395
+ }
396
+ }
397
+
398
+ function openPopover() {
399
+ clearHideTimer();
400
+ if (!popover.matches(":popover-open")) {
401
+ popover.showPopover();
402
+ }
403
+ const rect = trigger.getBoundingClientRect();
404
+ const popoverWidth = popover.offsetWidth || 0;
405
+ const left = Math.min(Math.max(16, rect.left), Math.max(16, window.innerWidth - popoverWidth - 16));
406
+ popover.style.left = `${left}px`;
407
+ popover.style.top = `${rect.bottom + 8}px`;
408
+ }
409
+
410
+ function scheduleClose() {
411
+ clearHideTimer();
412
+ hideTimer = window.setTimeout(() => {
413
+ const hasPointer = trigger.matches(":hover") || popover.matches(":hover");
414
+ const hasFocus = trigger.matches(":focus-visible") || popover.contains(document.activeElement);
415
+ if (!hasPointer && !hasFocus && popover.matches(":popover-open")) {
416
+ popover.hidePopover();
417
+ }
418
+ }, 80);
419
+ }
420
+
421
+ trigger.addEventListener("pointerenter", openPopover);
422
+ trigger.addEventListener("focus", openPopover);
423
+ trigger.addEventListener("pointerleave", scheduleClose);
424
+ trigger.addEventListener("blur", scheduleClose);
425
+ trigger.addEventListener("click", openPopover);
426
+
427
+ popover.addEventListener("pointerenter", openPopover);
428
+ popover.addEventListener("pointerleave", scheduleClose);
429
+ popover.addEventListener("focusin", openPopover);
430
+ popover.addEventListener("focusout", scheduleClose);
431
+ popover.addEventListener("toggle", (event) => {
432
+ if (event.newState === "closed") clearHideTimer();
433
+ });
434
+ }
435
+
436
+ function normalizeArticleLineHeight(value) {
437
+ if (value === "compact") return 1.55;
438
+ if (value === "normal") return 1.7;
439
+ if (value === "relaxed") return 1.85;
440
+
441
+ const parsed = Number(value);
442
+ if (!Number.isFinite(parsed)) return ARTICLE_FONT_DEFAULT_SETTINGS.lineHeight;
443
+ return Math.min(ARTICLE_LINE_HEIGHT_MAX, Math.max(ARTICLE_LINE_HEIGHT_MIN, parsed));
444
+ }
445
+
446
+ function normalizeArticleFontSettings(value = {}) {
447
+ const candidate = value || {};
448
+ return {
449
+ size: ARTICLE_SIZE_OPTIONS.has(candidate.size) ? candidate.size : ARTICLE_FONT_DEFAULT_SETTINGS.size,
450
+ type: ARTICLE_FONT_OPTIONS.has(candidate.type) ? candidate.type : ARTICLE_FONT_DEFAULT_SETTINGS.type,
451
+ lineHeight: normalizeArticleLineHeight(candidate.lineHeight),
452
+ weight: ARTICLE_WEIGHT_OPTIONS.has(candidate.weight) ? candidate.weight : ARTICLE_FONT_DEFAULT_SETTINGS.weight,
453
+ customFonts: normalizeCustomFonts(candidate.customFonts),
454
+ };
455
+ }
456
+
457
+ function getArticleFontSettings() {
458
+ let settings = { ...ARTICLE_FONT_DEFAULT_SETTINGS };
459
+ try {
460
+ const stored = localStorage.getItem(ARTICLE_FONT_STORAGE_KEY);
461
+ if (stored) settings = normalizeArticleFontSettings({ ...ARTICLE_FONT_DEFAULT_SETTINGS, ...JSON.parse(stored) });
462
+ } catch {
463
+ settings = { ...ARTICLE_FONT_DEFAULT_SETTINGS };
464
+ }
465
+ return settings;
466
+ }
467
+
468
+ function applyArticleFontSettings(settings = getArticleFontSettings()) {
469
+ const normalized = normalizeArticleFontSettings(settings);
470
+ const html = document.documentElement;
471
+
472
+ applyCustomFonts(normalized.customFonts);
473
+ html.dataset.articleFontSize = normalized.size;
474
+ html.dataset.articleFontFamily = normalized.type;
475
+ html.dataset.articleLineHeight = String(normalized.lineHeight);
476
+ html.dataset.articleFontWeight = normalized.weight;
477
+ html.style.setProperty("--article-line-height", String(normalized.lineHeight));
478
+ }
479
+
480
+ function saveArticleFontSettings(settings) {
481
+ try {
482
+ localStorage.setItem(ARTICLE_FONT_STORAGE_KEY, JSON.stringify(normalizeArticleFontSettings(settings)));
483
+ } catch {
484
+ // Keep the current page responsive even when storage is unavailable.
485
+ }
486
+ }
487
+
488
+ function initArticleSettings() {
489
+ const settings = getArticleFontSettings();
490
+ applyArticleFontSettings(settings);
491
+
492
+ const fontSettingsPopover = document.getElementById("article-font-settings");
493
+ if (!fontSettingsPopover) return;
494
+
495
+ function updateButtonStates(selector, isActive) {
496
+ fontSettingsPopover.querySelectorAll(selector).forEach((btn) => {
497
+ const active = isActive(btn);
498
+ btn.classList.toggle("is-active", active);
499
+ btn.setAttribute("aria-pressed", String(active));
500
+ });
501
+ }
502
+
503
+ const lineHeightSlider = fontSettingsPopover.querySelector(".font-line-height-slider");
504
+ const lineHeightValue = fontSettingsPopover.querySelector(".font-line-height-value");
505
+ const customFontForm = fontSettingsPopover.querySelector(".font-custom-form");
506
+ const customFontImportInput = fontSettingsPopover.querySelector(".font-custom-imports");
507
+ const customFontResetButton = fontSettingsPopover.querySelector(".font-custom-reset");
508
+ const customFontFamilyInputs = fontSettingsPopover.querySelectorAll(".font-custom-family-input");
509
+ const customFontHelpButton = fontSettingsPopover.querySelector(".font-custom-help-btn");
510
+ const customFontHelpPopover = document.getElementById("font-custom-help-popover");
511
+ const customFontToggleButton = fontSettingsPopover.querySelector(".font-custom-toggle");
512
+ const customFontPanel = fontSettingsPopover.querySelector(".font-custom-panel");
513
+
514
+ initHoverPopover(customFontHelpButton, customFontHelpPopover);
515
+
516
+ if (customFontToggleButton && customFontPanel) {
517
+ const syncCustomFontPanelState = (expanded) => {
518
+ customFontToggleButton.setAttribute("aria-expanded", String(expanded));
519
+ customFontPanel.dataset.expanded = String(expanded);
520
+ customFontPanel.setAttribute("aria-hidden", String(!expanded));
521
+ };
522
+
523
+ syncCustomFontPanelState(false);
524
+ customFontToggleButton.addEventListener("click", () => {
525
+ const expanded = customFontToggleButton.getAttribute("aria-expanded") === "true";
526
+ syncCustomFontPanelState(!expanded);
527
+ });
528
+ }
529
+
530
+ function updateLineHeightUI() {
531
+ if (lineHeightSlider) {
532
+ lineHeightSlider.value = String(settings.lineHeight);
533
+ }
534
+ if (lineHeightValue) {
535
+ lineHeightValue.textContent = settings.lineHeight.toFixed(2);
536
+ }
537
+ }
538
+
539
+ function updateActiveStates() {
540
+ updateButtonStates(".font-size-btn", (btn) => btn.dataset.size === settings.size);
541
+ updateButtonStates(".font-type-btn", (btn) => btn.dataset.font === settings.type);
542
+ updateButtonStates(".font-weight-btn", (btn) => btn.dataset.weight === settings.weight);
543
+ updateLineHeightUI();
544
+ }
545
+
546
+ function updateCustomFontUI() {
547
+ const customFonts = normalizeCustomFonts(settings.customFonts);
548
+ if (customFontImportInput) {
549
+ customFontImportInput.value = customFonts.imports.join("\n");
550
+ }
551
+
552
+ customFontFamilyInputs.forEach((input) => {
553
+ input.value = customFonts.families[input.dataset.fontFamily] || "";
554
+ });
555
+ }
556
+
557
+ function readCustomFontsFromUI() {
558
+ const families = {};
559
+
560
+ customFontFamilyInputs.forEach((input) => {
561
+ families[input.dataset.fontFamily] = input.value;
562
+ });
563
+
564
+ return normalizeCustomFonts({
565
+ imports: customFontImportInput?.value || "",
566
+ families,
567
+ });
568
+ }
569
+
570
+ fontSettingsPopover.querySelectorAll(".font-size-btn").forEach((btn) => {
571
+ btn.addEventListener("click", () => {
572
+ if (!ARTICLE_SIZE_OPTIONS.has(btn.dataset.size)) return;
573
+ settings.size = btn.dataset.size;
574
+ saveArticleFontSettings(settings);
575
+ updateActiveStates();
576
+ applyArticleFontSettings(settings);
577
+ });
578
+ });
579
+
580
+ fontSettingsPopover.querySelectorAll(".font-type-btn").forEach((btn) => {
581
+ btn.addEventListener("click", () => {
582
+ if (!ARTICLE_FONT_OPTIONS.has(btn.dataset.font)) return;
583
+ settings.type = btn.dataset.font;
584
+ saveArticleFontSettings(settings);
585
+ updateActiveStates();
586
+ applyArticleFontSettings(settings);
587
+ });
588
+ });
589
+
590
+ if (lineHeightSlider) {
591
+ lineHeightSlider.min = String(ARTICLE_LINE_HEIGHT_MIN);
592
+ lineHeightSlider.max = String(ARTICLE_LINE_HEIGHT_MAX);
593
+ lineHeightSlider.step = "0.05";
594
+ lineHeightSlider.addEventListener("input", () => {
595
+ settings.lineHeight = normalizeArticleLineHeight(lineHeightSlider.value);
596
+ saveArticleFontSettings(settings);
597
+ updateActiveStates();
598
+ applyArticleFontSettings(settings);
599
+ });
600
+ }
601
+
602
+ fontSettingsPopover.querySelectorAll(".font-weight-btn").forEach((btn) => {
603
+ btn.addEventListener("click", () => {
604
+ if (!ARTICLE_WEIGHT_OPTIONS.has(btn.dataset.weight)) return;
605
+ settings.weight = btn.dataset.weight;
606
+ saveArticleFontSettings(settings);
607
+ updateActiveStates();
608
+ applyArticleFontSettings(settings);
609
+ });
610
+ });
611
+
612
+ if (customFontForm) {
613
+ customFontForm.addEventListener("submit", (event) => {
614
+ event.preventDefault();
615
+ settings.customFonts = readCustomFontsFromUI();
616
+ saveArticleFontSettings(settings);
617
+ applyArticleFontSettings(settings);
618
+ updateCustomFontUI();
619
+ });
620
+ }
621
+
622
+ if (customFontResetButton) {
623
+ customFontResetButton.addEventListener("click", () => {
624
+ settings.customFonts = normalizeCustomFonts();
625
+ saveArticleFontSettings(settings);
626
+ applyArticleFontSettings(settings);
627
+
628
+ const defaults = getDefaultCustomFontFamilies();
629
+ if (customFontImportInput) {
630
+ customFontImportInput.value = "";
631
+ }
632
+ customFontFamilyInputs.forEach((input) => {
633
+ const key = input.dataset.fontFamily;
634
+ input.value = defaults[key] || "";
635
+ });
636
+ });
637
+ }
638
+
639
+ updateActiveStates();
640
+ updateCustomFontUI();
641
+ }
642
+
643
+ function initArticleCommentPopover() {
644
+ const commentPopover = document.getElementById("article-comment-popover");
645
+ if (!commentPopover) {
646
+ twikoo_handler();
647
+ return;
648
+ }
649
+
650
+ // Preload twikoo JS during idle time so comments render faster on click
651
+ const tko = document.getElementById("tko");
652
+ if (tko && tko.dataset.jsUrl) {
653
+ const preload = () => loadScriptOnce(tko.dataset.jsUrl, () => {});
654
+ if (typeof requestIdleCallback === "function") {
655
+ requestIdleCallback(preload, { timeout: 3000 });
656
+ } else {
657
+ setTimeout(preload, 200);
658
+ }
659
+ }
660
+
661
+ const initializeComments = () => twikoo_handler();
662
+ if (commentPopover.matches(":popover-open")) {
663
+ initializeComments();
664
+ }
665
+
666
+ if (commentPopover.dataset.bound === "true") return;
667
+ commentPopover.dataset.bound = "true";
668
+ commentPopover.addEventListener("toggle", (event) => {
669
+ if (event.newState === "open") initializeComments();
670
+ });
671
+ }
672
+
673
+ function getComparablePath(url) {
674
+ const path = new URL(url, window.location.href).pathname.replace(/\/index\.html$/, "/").replace(/\/+$/, "");
675
+ return path || "/";
676
+ }
677
+
678
+ function updateNavbarCurrentPage() {
679
+ const currentPath = getComparablePath(window.location.href);
680
+ document.querySelectorAll(".navbar-start .navbar-item").forEach((item) => {
681
+ const itemPath = getComparablePath(item.href);
682
+ const active = itemPath === currentPath || (itemPath !== "/" && currentPath.startsWith(`${itemPath}/`));
683
+
684
+ item.classList.toggle("is-active", active);
685
+ if (active) {
686
+ item.setAttribute("aria-current", "page");
687
+ } else {
688
+ item.removeAttribute("aria-current");
689
+ }
690
+ });
691
+ }
692
+
693
+ function refreshNavbarIcons() {
694
+ document.querySelectorAll(".navbar-end iconify-icon").forEach((el) => {
695
+ if (!el.getAttribute("icon")) return;
696
+ if (el.shadowRoot?.querySelector("svg")) return;
697
+ const parent = el.parentNode;
698
+ if (!parent) return;
699
+ const next = el.nextSibling;
700
+ parent.removeChild(el);
701
+ parent.insertBefore(el, next);
702
+ });
703
+ }
704
+
291
705
  function initPage() {
292
706
  tableWrapFix();
293
707
  initializeTabs();
294
708
  handleMermaid();
295
709
  addHighlightTool();
710
+ initArticleSettings();
296
711
  const zoomOpts = { background: "hsla(from var(--mantle) / 0.9)" };
297
712
  const zoomImgs = new Set();
298
713
  document.querySelectorAll(".content img").forEach((img) => zoomImgs.add(img));
299
714
  mediumZoom([...zoomImgs], zoomOpts);
300
- twikoo_handler();
715
+ initArticleCommentPopover();
716
+ updateNavbarCurrentPage();
717
+ refreshNavbarIcons();
301
718
  }
302
719
 
303
720
  document.addEventListener("DOMContentLoaded", initPage, { once: true });
304
721
 
305
- // Re-initialize on page changes when using swup
306
- if (typeof swup !== "undefined") {
307
- swup.hooks.on("page:view", initPage);
722
+ function bindSwupPageHook(swupInstance) {
723
+ if (!swupInstance || swupInstance.gnixMainPageHookBound) return;
724
+ swupInstance.gnixMainPageHookBound = true;
725
+ swupInstance.hooks.on("page:view", initPage);
308
726
  }
309
727
 
728
+ bindSwupPageHook(window.swup);
729
+ document.addEventListener("gnix:swup-ready", (event) => bindSwupPageHook(event.detail?.swup), { once: true });
730
+
310
731
  document.addEventListener("keydown", handleKeyDown, {
311
732
  capture: true, // 捕获阶段监听,优先于浏览器默认处理
312
733
  passive: false, // 允许调用 preventDefault
@@ -326,3 +747,5 @@ function toggleNav(event) {
326
747
  menu.classList.remove("is-active");
327
748
  }
328
749
  }
750
+
751
+ window.toggleNav = toggleNav;
@@ -0,0 +1,39 @@
1
+ import SwupHeadPlugin from "https://unpkg.com/@swup/head-plugin@2?module";
2
+ import SwupScriptsPlugin from "https://unpkg.com/@swup/scripts-plugin@2?module";
3
+ import Swup from "https://unpkg.com/swup@4?module";
4
+
5
+ const swup = new Swup({
6
+ containers: ["#swup"],
7
+ cache: true,
8
+ native: false,
9
+ animationSelector: false,
10
+ plugins: [
11
+ new SwupHeadPlugin({
12
+ persistTags: true,
13
+ }),
14
+ new SwupScriptsPlugin({
15
+ optin: true,
16
+ }),
17
+ ],
18
+ });
19
+
20
+ document.addEventListener(
21
+ "click",
22
+ (event) => {
23
+ if (!swup.navigating || event.defaultPrevented) return;
24
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
25
+
26
+ const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
27
+ if (!link || link.matches('[download], [target="_blank"]')) return;
28
+
29
+ const url = new URL(link.href, window.location.href);
30
+ if (url.origin !== window.location.origin) return;
31
+
32
+ event.preventDefault();
33
+ event.stopImmediatePropagation();
34
+ },
35
+ { capture: true },
36
+ );
37
+
38
+ window.swup = swup;
39
+ document.dispatchEvent(new CustomEvent("gnix:swup-ready", { detail: { swup } }));
@@ -1,15 +1,14 @@
1
1
  ((window, document, localStorage) => {
2
- const STORAGE_KEY = "themePreference";
3
- const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2
+ const themeConfig = window.__GNIX_THEME_CONFIG__;
4
3
 
5
- const THEME_MAP = {
6
- mocha: "night",
7
- rose_pine: "night",
8
- nord: "light",
9
- nord_night: "night",
10
- tokyo_night: "night",
11
- latte: "light",
12
- };
4
+ if (!themeConfig) return;
5
+
6
+ const STORAGE_KEY = themeConfig.storageKey;
7
+ const DEFAULT_THEME = themeConfig.defaultTheme;
8
+ const SYSTEM_THEME = themeConfig.systemTheme;
9
+ const THEME_MAP = themeConfig.themeClassMap || {};
10
+ const THEME_CLASSES = [...new Set(Object.values(THEME_MAP))];
11
+ const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
13
12
 
14
13
  let currentIndex = 0;
15
14
  let previewTheme = null;
@@ -19,18 +18,28 @@
19
18
  let previousFocus = null;
20
19
  let popoverEl = null;
21
20
 
21
+ function isValidTheme(theme) {
22
+ return theme === DEFAULT_THEME || Object.hasOwn(THEME_MAP, theme);
23
+ }
24
+
25
+ function resolveTheme(theme) {
26
+ return theme === DEFAULT_THEME ? (colorSchemeMediaQuery.matches ? SYSTEM_THEME.dark : SYSTEM_THEME.light) : theme;
27
+ }
28
+
22
29
  function getThemePreference() {
23
30
  const stored = localStorage.getItem(STORAGE_KEY);
24
- return stored && stored in THEME_MAP ? stored : "system";
31
+ return isValidTheme(stored) ? stored : DEFAULT_THEME;
25
32
  }
26
33
 
27
34
  function applyTheme(theme, persist = false) {
28
- const resolved = theme === "system" ? (colorSchemeMediaQuery.matches ? "mocha" : "nord") : theme;
35
+ const preference = isValidTheme(theme) ? theme : DEFAULT_THEME;
36
+ const resolved = resolveTheme(preference);
37
+ const themeClass = THEME_MAP[resolved];
29
38
  const html = document.documentElement;
30
39
  html.setAttribute("data-theme", resolved);
31
- html.classList.remove("night", "light");
32
- html.classList.add(THEME_MAP[resolved]);
33
- if (persist) localStorage.setItem(STORAGE_KEY, theme);
40
+ html.classList.remove(...THEME_CLASSES);
41
+ if (themeClass) html.classList.add(themeClass);
42
+ if (persist) localStorage.setItem(STORAGE_KEY, preference);
34
43
  }
35
44
 
36
45
  function updateFocus() {
@@ -54,6 +63,7 @@
54
63
  originalTheme = getThemePreference();
55
64
  previewTheme = null;
56
65
  shouldApply = false;
66
+ currentIndex = 0;
57
67
  themeOptions = el.querySelectorAll(".theme-option");
58
68
 
59
69
  themeOptions.forEach((option, index) => {
@@ -143,7 +153,7 @@
143
153
  }
144
154
 
145
155
  colorSchemeMediaQuery.addEventListener("change", () => {
146
- if (getThemePreference() === "system") applyTheme("system", true);
156
+ if (getThemePreference() === DEFAULT_THEME) applyTheme(DEFAULT_THEME, true);
147
157
  });
148
158
 
149
159
  window.selectThemeOption = (index) => {
@@ -1,53 +0,0 @@
1
- const util = require("hexo-util");
2
-
3
- module.exports = (hexo) => {
4
- hexo.extend.generator.register("insight", function (locals) {
5
- const url_for = hexo.extend.helper.get("url_for").bind(this);
6
- function minify(str) {
7
- return util
8
- .stripHTML(str)
9
- .trim()
10
- .replace(/\n/g, " ")
11
- .replace(/\s+/g, " ")
12
- .replace(/&#x([\da-fA-F]+);/g, (_, hex) => {
13
- return String.fromCharCode(parseInt(hex, 16));
14
- })
15
- .replace(/&#([\d]+);/g, (_, dec) => {
16
- return String.fromCharCode(dec);
17
- });
18
- }
19
- function mapPost(post) {
20
- return {
21
- title: util.escapeHTML(post.title).trim(),
22
- text: post.password ? "该文章需要密码" : minify(post.content),
23
- link: url_for(post.path),
24
- };
25
- }
26
- function mapTag(tag) {
27
- return {
28
- name: util.escapeHTML(tag.name).trim(),
29
- slug: minify(tag.slug),
30
- link: url_for(tag.path),
31
- };
32
- }
33
- const site = {
34
- posts: locals.posts.map(mapPost),
35
- tags: locals.tags.map(mapTag),
36
- };
37
-
38
- return {
39
- path: "/content.json",
40
- data: JSON.stringify(site),
41
- };
42
- });
43
- // Generate "<root>/tags/" page
44
- hexo.extend.generator.register("tags", (locals) => {
45
- return {
46
- path: "tags/",
47
- layout: ["tags"],
48
- data: Object.assign({}, locals, {
49
- __tags: true,
50
- }),
51
- };
52
- });
53
- };