hexo-theme-gnix 6.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/include/hexo/encrypt.js +42 -0
  3. package/include/hexo/feed.js +329 -0
  4. package/include/util/common.js +7 -9
  5. package/languages/en.yml +6 -3
  6. package/languages/zh-CN.yml +6 -3
  7. package/layout/archive.jsx +86 -65
  8. package/layout/comment/twikoo.jsx +2 -11
  9. package/layout/comment/waline.jsx +2 -2
  10. package/layout/common/article.jsx +5 -8
  11. package/layout/common/article_cover.jsx +11 -1
  12. package/layout/common/article_media.jsx +2 -4
  13. package/layout/common/footer.jsx +11 -31
  14. package/layout/common/head.jsx +6 -14
  15. package/layout/common/navbar.jsx +4 -4
  16. package/layout/common/scripts.jsx +6 -6
  17. package/layout/common/theme_selector.jsx +5 -6
  18. package/layout/common/toc.jsx +8 -14
  19. package/layout/index.jsx +2 -4
  20. package/layout/misc/article_licensing.jsx +4 -2
  21. package/layout/misc/open_graph.jsx +4 -4
  22. package/layout/misc/paginator.jsx +10 -4
  23. package/layout/misc/structured_data.jsx +3 -4
  24. package/layout/plugin/busuanzi.jsx +1 -1
  25. package/layout/plugin/cookie_consent.jsx +40 -31
  26. package/layout/plugin/swup.jsx +2 -22
  27. package/layout/search/insight.jsx +16 -3
  28. package/package.json +12 -8
  29. package/scripts/hot-reload.js +92 -0
  30. package/scripts/index.js +2 -0
  31. package/source/css/archive.css +251 -0
  32. package/source/css/default.css +250 -284
  33. package/source/css/encrypt.css +55 -0
  34. package/source/css/responsive/desktop.css +0 -119
  35. package/source/css/responsive/mobile.css +7 -23
  36. package/source/css/responsive/touch.css +9 -103
  37. package/source/css/shiki/shiki.css +7 -22
  38. package/source/css/twikoo.css +290 -830
  39. package/source/img/og_image.webp +0 -0
  40. package/source/js/archive-breadcrumb.js +132 -0
  41. package/source/js/busuanzi.js +1 -12
  42. package/source/js/components/accordion.js +192 -0
  43. package/source/js/components/chat.js +239 -0
  44. package/source/js/components/device-carousel.js +260 -0
  45. package/source/js/components/image-carousel.js +410 -0
  46. package/source/js/components/text-image-section.js +180 -0
  47. package/source/js/components/theme-stacked.js +526 -0
  48. package/source/js/components/tree.js +437 -0
  49. package/source/js/decrypt.js +112 -0
  50. package/source/js/insight.js +75 -65
  51. package/source/js/main.js +192 -99
  52. package/source/js/mdit/mermaid.js +12 -4
  53. package/source/js/swup.bundle.js +1 -0
  54. package/source/js/theme-selector.js +94 -113
  55. package/source/img/og_image.png +0 -0
  56. package/source/js/host/swup/Swup.umd.min.js +0 -1
  57. package/source/js/host/swup/head-plugin.umd.min.js +0 -1
  58. package/source/js/host/swup/scripts-plugin.umd.min.js +0 -2
  59. package/source/js/mdit/shiki.js +0 -158
Binary file
@@ -0,0 +1,132 @@
1
+ (() => {
2
+ function parseArchiveLocation(pathname, archiveDir) {
3
+ const segments = String(pathname || "")
4
+ .replace(/\/+$/, "")
5
+ .split("/")
6
+ .filter(Boolean);
7
+
8
+ const index = segments.lastIndexOf(archiveDir);
9
+ if (index === -1) return { year: null, month: null };
10
+
11
+ const yearRaw = segments[index + 1] || null;
12
+ const monthRaw = segments[index + 2] || null;
13
+
14
+ const year = yearRaw && /^\d{4}$/.test(yearRaw) ? Number(yearRaw) : null;
15
+ const month = monthRaw && /^\d{1,2}$/.test(monthRaw) ? Number(monthRaw) : null;
16
+
17
+ if (month && (month < 1 || month > 12)) return { year, month: null };
18
+ return { year, month };
19
+ }
20
+
21
+ function getMenuForTrigger(trigger) {
22
+ const menu = trigger?.nextElementSibling;
23
+ if (!menu?.classList.contains("archive-breadcrumb__menu")) return null;
24
+ return menu;
25
+ }
26
+
27
+ function getOptions(menu) {
28
+ return Array.from(menu.querySelectorAll(".archive-breadcrumb__option"));
29
+ }
30
+
31
+ function setSelected(menu, isSelectedFn) {
32
+ const options = getOptions(menu);
33
+ for (const option of options) {
34
+ option.setAttribute("aria-selected", isSelectedFn(option) ? "true" : "false");
35
+ }
36
+ }
37
+
38
+ function syncFromLocation(root) {
39
+ const archiveDir = root.dataset.archiveDir || "archives";
40
+ const { year, month } = parseArchiveLocation(window.location.pathname, archiveDir);
41
+
42
+ const yearLabel = root.querySelector('[data-label="year"]');
43
+ const monthLabel = root.querySelector('[data-label="month"]');
44
+ if (yearLabel) yearLabel.textContent = year ? String(year) : "*";
45
+ if (monthLabel) monthLabel.textContent = month ? String(month).padStart(2, "0") : "*";
46
+
47
+ const yearTrigger = root.querySelector('.archive-breadcrumb__picker[data-picker="year"] .archive-breadcrumb__trigger');
48
+ const yearMenu = yearTrigger ? getMenuForTrigger(yearTrigger) : null;
49
+ if (yearMenu) {
50
+ setSelected(yearMenu, (opt) => {
51
+ const text = opt.textContent?.trim();
52
+ if (!year) return text === "*";
53
+ return text === String(year);
54
+ });
55
+ }
56
+
57
+ const monthTrigger = root.querySelector('.archive-breadcrumb__picker[data-picker="month"] .archive-breadcrumb__trigger');
58
+ const monthMenu = monthTrigger ? getMenuForTrigger(monthTrigger) : null;
59
+ if (monthTrigger) monthTrigger.disabled = !year;
60
+ if (monthMenu) {
61
+ setSelected(monthMenu, (opt) => {
62
+ const text = opt.textContent?.trim();
63
+ if (!year) return text === "*";
64
+ if (!month) return text === "*";
65
+ return text === String(month).padStart(2, "0");
66
+ });
67
+ }
68
+
69
+ closeAll(root);
70
+ }
71
+
72
+ function closeAll(root) {
73
+ const triggers = root.querySelectorAll(".archive-breadcrumb__trigger");
74
+ for (const trigger of triggers) {
75
+ trigger.setAttribute("aria-expanded", "false");
76
+ }
77
+ }
78
+
79
+ function openMenu(trigger) {
80
+ if (trigger.disabled) return;
81
+ trigger.setAttribute("aria-expanded", "true");
82
+ }
83
+
84
+ function toggleMenu(trigger, root) {
85
+ if (trigger.disabled) return;
86
+ const expanded = trigger.getAttribute("aria-expanded") === "true";
87
+ closeAll(root);
88
+ if (!expanded) {
89
+ openMenu(trigger);
90
+ }
91
+ }
92
+
93
+ function navigateToOption(option) {
94
+ const href = option?.dataset?.href;
95
+ if (!href) return;
96
+ window.location.assign(href);
97
+ }
98
+
99
+ function init(root) {
100
+ root.addEventListener("click", (e) => {
101
+ const target = e.target.closest?.(".archive-breadcrumb__option, .archive-breadcrumb__trigger");
102
+ if (!target) {
103
+ closeAll(root);
104
+ return;
105
+ }
106
+
107
+ if (target.classList.contains("archive-breadcrumb__option")) {
108
+ e.preventDefault();
109
+ navigateToOption(target);
110
+ return;
111
+ }
112
+
113
+ if (target.classList.contains("archive-breadcrumb__trigger")) {
114
+ e.preventDefault();
115
+ toggleMenu(target, root);
116
+ }
117
+ });
118
+
119
+ syncFromLocation(root);
120
+ }
121
+
122
+ function initAll() {
123
+ const roots = document.querySelectorAll("[data-archive-breadcrumb]");
124
+ for (const root of roots) {
125
+ if (root._gnixArchiveBreadcrumbInited) continue;
126
+ root._gnixArchiveBreadcrumbInited = true;
127
+ init(root);
128
+ }
129
+ }
130
+
131
+ initAll();
132
+ })();
@@ -1,13 +1,11 @@
1
1
  !(() => {
2
2
  const TYPES = ["site_pv", "site_uv", "page_pv", "page_uv"];
3
- const script = document.currentScript;
4
- const api = script.getAttribute("data-api") || "https://bsz.dusays.com:9001/api";
5
3
  const STORAGE_KEY = "bsz-id";
6
4
  const BASE = { site_pv: 12801, site_uv: 2450 };
7
5
 
8
6
  const update = () => {
9
7
  const xhr = new XMLHttpRequest();
10
- xhr.open("POST", api, true);
8
+ xhr.open("POST", "https://bsz.dusays.com:9001/api", true);
11
9
 
12
10
  const token = localStorage.getItem(STORAGE_KEY);
13
11
  token && xhr.setRequestHeader("Authorization", `Bearer ${token}`);
@@ -33,13 +31,4 @@
33
31
  };
34
32
 
35
33
  update();
36
-
37
- if (script.hasAttribute("pjax")) {
38
- const pushState = history.pushState;
39
- history.pushState = function (...args) {
40
- pushState.apply(this, ...args);
41
- update();
42
- };
43
- addEventListener("popstate", update);
44
- }
45
34
  })();
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Accordion Custom Element
3
+ *
4
+ * Usage:
5
+ * <x-accordion>
6
+ * <accordion-item title="Introduction">
7
+ * Content here...
8
+ * </accordion-item>
9
+ * <accordion-item title="Design Patterns">
10
+ * More content...
11
+ * </accordion-item>
12
+ * </x-accordion>
13
+ */
14
+
15
+ let styleSheetInjected = false;
16
+
17
+ class Accordion extends HTMLElement {
18
+ connectedCallback() {
19
+ this.injectStyles();
20
+ this.render();
21
+ this.setupListeners();
22
+ }
23
+
24
+ injectStyles() {
25
+ if (styleSheetInjected) return;
26
+
27
+ const style = `
28
+ x-accordion {
29
+ display: block;
30
+ margin: 1em 0;
31
+ }
32
+
33
+ .accordion-item {
34
+ border-bottom: 1px solid var(--surface0);
35
+ }
36
+
37
+ .accordion-item:last-child {
38
+ border-bottom: none;
39
+ }
40
+
41
+ .accordion-header {
42
+ width: 100%;
43
+ padding: 16px 0;
44
+ border: none;
45
+ color: var(--text);
46
+ background: transparent;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 12px;
50
+ cursor: pointer;
51
+ transition: color 0.2s;
52
+ text-align: left;
53
+ & :hover {
54
+ color: var(--subtext0);
55
+ }
56
+ }
57
+
58
+ .accordion-icon {
59
+ font-size: 16px;
60
+ font-weight: 300;
61
+ color: var(--subtext0);
62
+ transition: transform 0.2s ease;
63
+ flex-shrink: 0;
64
+ display: inline-flex;
65
+ width: 16px;
66
+ justify-content: center;
67
+ }
68
+
69
+ .accordion-item.active .accordion-icon {
70
+ transform: rotate(45deg);
71
+ }
72
+
73
+ .accordion-content {
74
+ max-height: 0;
75
+ overflow: hidden;
76
+ transition: max-height 0.3s ease;
77
+ }
78
+
79
+ .accordion-content-inner {
80
+ padding: 0 0 16px 28px;
81
+ color: var(--subtext1);
82
+ font-size: 14px;
83
+ line-height: 1.6;
84
+ }
85
+ `;
86
+
87
+ const styleEl = document.createElement("style");
88
+ styleEl.textContent = style;
89
+ document.head.appendChild(styleEl);
90
+ styleSheetInjected = true;
91
+ }
92
+
93
+ render() {
94
+ const items = this.querySelectorAll("accordion-item");
95
+ if (items.length === 0) return; // Already rendered or empty
96
+
97
+ const itemsHTML = Array.from(items)
98
+ .map((item) => {
99
+ const title = item.getAttribute("title") || "Item";
100
+
101
+ // Filter out nested accordion-item elements to handle malformed HTML
102
+ const contentNodes = Array.from(item.childNodes).filter((node) => {
103
+ return node.nodeType !== Node.ELEMENT_NODE || node.tagName.toLowerCase() !== "accordion-item";
104
+ });
105
+
106
+ const content = contentNodes
107
+ .map((node) => {
108
+ return node.nodeType === Node.TEXT_NODE ? node.textContent : node.outerHTML;
109
+ })
110
+ .join("");
111
+
112
+ return `
113
+ <div class="accordion-item">
114
+ <button class="accordion-header" aria-expanded="false">
115
+ <span class="accordion-icon">+</span>
116
+ <span>${title}</span>
117
+ </button>
118
+ <div class="accordion-content">
119
+ <div class="accordion-content-inner content">${content}</div>
120
+ </div>
121
+ </div>
122
+ `;
123
+ })
124
+ .join("");
125
+
126
+ this.innerHTML = itemsHTML;
127
+ }
128
+
129
+ setupListeners() {
130
+ const headers = this.querySelectorAll(".accordion-header");
131
+
132
+ headers.forEach((header) => {
133
+ header.addEventListener("click", () => {
134
+ const item = header.closest(".accordion-item");
135
+ const isActive = item.classList.contains("active");
136
+ const content = item.querySelector(".accordion-content");
137
+
138
+ // Close all items (accordion mode - single expansion)
139
+ this.querySelectorAll(".accordion-item").forEach((i) => {
140
+ i.classList.remove("active");
141
+ i.querySelector(".accordion-header").setAttribute("aria-expanded", "false");
142
+ const c = i.querySelector(".accordion-content");
143
+ if (c) c.style.maxHeight = null;
144
+ });
145
+
146
+ // Open clicked item if it wasn't active
147
+ if (!isActive) {
148
+ item.classList.add("active");
149
+ header.setAttribute("aria-expanded", "true");
150
+ if (content) {
151
+ content.style.maxHeight = `${content.scrollHeight}px`;
152
+ }
153
+ }
154
+ });
155
+ });
156
+ }
157
+
158
+ // Public method to expand a specific item by index
159
+ expandItem(index) {
160
+ const items = this.querySelectorAll(".accordion-item");
161
+ if (items[index]) {
162
+ items[index].querySelector(".accordion-header").click();
163
+ }
164
+ }
165
+
166
+ // Public method to collapse all items
167
+ collapseAll() {
168
+ this.querySelectorAll(".accordion-item").forEach((item) => {
169
+ item.classList.remove("active");
170
+ item.querySelector(".accordion-header").setAttribute("aria-expanded", "false");
171
+ const content = item.querySelector(".accordion-content");
172
+ if (content) content.style.maxHeight = null;
173
+ });
174
+ }
175
+
176
+ static get observedAttributes() {
177
+ return ["single-expand"];
178
+ }
179
+
180
+ attributeChangedCallback() {
181
+ // Future: support multi-expand mode
182
+ }
183
+ }
184
+
185
+ // Define accordion-item as a placeholder for slot content
186
+ class AccordionItem extends HTMLElement {}
187
+
188
+ // Register custom elements
189
+ customElements.define("x-accordion", Accordion);
190
+ customElements.define("accordion-item", AccordionItem);
191
+
192
+ export { Accordion, AccordionItem };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Chat Component Custom Element
3
+ * Displays group chat conversations with avatars, names, timestamps, and messages
4
+ *
5
+ * Usage:
6
+ * <x-chat>
7
+ * <chat-message
8
+ * name="User Name"
9
+ * avatar="/path/to/avatar.png"
10
+ * timestamp="2024-01-15 10:30"
11
+ * is-me
12
+ * >
13
+ * Message content here...
14
+ * </chat-message>
15
+ * <chat-message name="Other User" avatar="/path/to/avatar.png" timestamp="2024-01-15 10:32">
16
+ * Another message...
17
+ * </chat-message>
18
+ * </x-chat>
19
+ *
20
+ * Or with data attribute:
21
+ * <x-chat messages='[{"name": "User", "content": "Hello", "timestamp": "10:30"}]'></x-chat>
22
+ */
23
+
24
+ const CHAT_STYLES = `
25
+ :host {
26
+ display: block;
27
+ font-family: var(--font-sans, sans-serif);
28
+ }
29
+
30
+ .chat-container {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 16px;
34
+ padding: 16px;
35
+ background: var(--base, #1e1e2e);
36
+ border-radius: 12px;
37
+ max-height: 500px;
38
+ overflow-y: auto;
39
+ scrollbar-width: none;
40
+ -ms-overflow-style: none;
41
+ }
42
+
43
+ .chat-container::-webkit-scrollbar {
44
+ display: none;
45
+ }
46
+
47
+ .chat-message {
48
+ display: flex;
49
+ gap: 12px;
50
+ align-items: flex-start;
51
+ }
52
+
53
+ .chat-message.is-me {
54
+ flex-direction: row-reverse;
55
+ }
56
+
57
+ .avatar {
58
+ width: 40px;
59
+ height: 40px;
60
+ border-radius: 50%;
61
+ object-fit: cover;
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ .avatar-placeholder {
66
+ width: 40px;
67
+ height: 40px;
68
+ border-radius: 50%;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ font-size: 18px;
73
+ font-weight: 600;
74
+ color: var(--text, #cdd6f4);
75
+ flex-shrink: 0;
76
+ background: var(--surface1, #45475a);
77
+ }
78
+
79
+ .message-content {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 4px;
83
+ max-width: 70%;
84
+ }
85
+
86
+ .chat-message.is-me .message-content {
87
+ align-items: flex-end;
88
+ }
89
+
90
+ .message-header {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 8px;
94
+ font-size: 12px;
95
+ }
96
+
97
+ .chat-message.is-me .message-header {
98
+ flex-direction: row-reverse;
99
+ }
100
+
101
+ .sender-name {
102
+ font-weight: 600;
103
+ color: var(--subtext1, #bac2de);
104
+ }
105
+
106
+ .timestamp {
107
+ color: var(--subtext0, #a6adc8);
108
+ font-size: 11px;
109
+ }
110
+
111
+ .message-bubble {
112
+ padding: 10px 14px;
113
+ border-radius: 16px;
114
+ background: var(--surface1, #45475a);
115
+ color: var(--text, #cdd6f4);
116
+ line-height: 1.5;
117
+ font-size: 14px;
118
+ word-wrap: break-word;
119
+ }
120
+
121
+ .chat-message.is-me .message-bubble {
122
+ background: var(--blue, #89b4fa);
123
+ color: var(--base, #1e1e2e);
124
+ }
125
+
126
+ .message-bubble a {
127
+ color: var(--blue, #89b4fa);
128
+ text-decoration: underline;
129
+ }
130
+
131
+ .message-bubble code {
132
+ font-family: var(--font-mono, 'Maple Mono', 'Fira Code', monospace);
133
+ font-size: 13px;
134
+ background: var(--mantle, #181825);
135
+ padding: 2px 6px;
136
+ border-radius: 4px;
137
+ }
138
+
139
+ .empty-state {
140
+ text-align: center;
141
+ padding: 40px 20px;
142
+ color: var(--subtext0, #a6adc8);
143
+ }
144
+ `;
145
+
146
+ class Chat extends HTMLElement {
147
+ constructor() {
148
+ super();
149
+ this.attachShadow({ mode: "open" });
150
+ }
151
+
152
+ connectedCallback() {
153
+ this.render();
154
+ }
155
+
156
+ getMessagesFromSlots() {
157
+ const messages = this.querySelectorAll("chat-message");
158
+ return Array.from(messages).map((msg) => ({
159
+ name: msg.getAttribute("name") || "Anonymous",
160
+ avatar: msg.getAttribute("avatar") || "",
161
+ timestamp: msg.getAttribute("timestamp") || "",
162
+ isMe: msg.hasAttribute("is-me"),
163
+ content: msg.innerHTML.trim(),
164
+ }));
165
+ }
166
+
167
+ getMessagesFromAttribute() {
168
+ const data = this.getAttribute("messages");
169
+ if (!data) return [];
170
+ try {
171
+ return JSON.parse(data);
172
+ } catch (e) {
173
+ console.warn("Invalid messages JSON:", e);
174
+ return [];
175
+ }
176
+ }
177
+
178
+ render() {
179
+ const messages = this.getMessagesFromSlots();
180
+
181
+ const messagesHTML = messages.map((msg) => this.renderMessage(msg)).join("");
182
+
183
+ this.shadowRoot.innerHTML = `
184
+ <style>${CHAT_STYLES}</style>
185
+ <div class="chat-container">
186
+ ${messagesHTML}
187
+ </div>
188
+ `;
189
+ }
190
+
191
+ renderMessage(msg) {
192
+ const initial = msg.name.charAt(0).toUpperCase();
193
+ const avatarHTML = msg.avatar ? `<img src="${msg.avatar}" alt="${msg.name}" class="avatar" loading="lazy"/>` : `<div class="avatar-placeholder">${initial}</div>`;
194
+
195
+ return `
196
+ <div class="chat-message ${msg.isMe ? "is-me" : ""}">
197
+ ${avatarHTML}
198
+ <div class="message-content">
199
+ <div class="message-header">
200
+ <span class="sender-name">${msg.name}</span>
201
+ <span class="timestamp">${msg.timestamp}</span>
202
+ </div>
203
+ <div class="message-bubble">${msg.content}</div>
204
+ </div>
205
+ </div>
206
+ `;
207
+ }
208
+
209
+ static get observedAttributes() {
210
+ return ["messages"];
211
+ }
212
+
213
+ attributeChangedCallback(name, oldValue, newValue) {
214
+ if (name === "messages" && oldValue !== newValue) {
215
+ this.render();
216
+ }
217
+ }
218
+
219
+ // Public API
220
+ addMessage(msg) {
221
+ const messages = this.getMessagesFromAttribute();
222
+ messages.push(msg);
223
+ this.setAttribute("messages", JSON.stringify(messages));
224
+ }
225
+
226
+ clear() {
227
+ this.innerHTML = "";
228
+ this.render();
229
+ }
230
+ }
231
+
232
+ // Placeholder for slot content
233
+ class ChatMessage extends HTMLElement {}
234
+
235
+ // Register custom elements
236
+ customElements.define("x-chat", Chat);
237
+ customElements.define("chat-message", ChatMessage);
238
+
239
+ export { Chat, ChatMessage };