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,45 @@
1
+ <div class="page-layout archive-layout">
2
+ <div class="archive-wrapper">
3
+ <header class="archive-header">
4
+ <h1 class="archive-title">
5
+ <% if (is_archive()) { %>
6
+ 归档
7
+ <% } else if (is_category()) { %>
8
+ 分类: <%= page.category %>
9
+ <% } else if (is_tag()) { %>
10
+ 标签: <%= page.tag %>
11
+ <% } %>
12
+ </h1>
13
+ <p class="archive-count">共 <%= page.posts.length %> 篇文章</p>
14
+ </header>
15
+
16
+ <div class="archive-content">
17
+ <%
18
+ let currentYear = null;
19
+ page.posts.each(function(post) {
20
+ const postYear = date(post.date, 'YYYY');
21
+ %>
22
+ <% if (currentYear !== postYear) { %>
23
+ <% if (currentYear !== null) { %>
24
+ </ul>
25
+ <% } %>
26
+ <% currentYear = postYear; %>
27
+ <h2 class="archive-year"><%= currentYear %></h2>
28
+ <ul class="archive-list">
29
+ <% } %>
30
+
31
+ <li class="archive-item">
32
+ <span class="archive-date"><%= date(post.date, 'MM-DD') %></span>
33
+ <a href="<%- url_for(post.path) %>" class="archive-link"><%= post.title %></a>
34
+ </li>
35
+ <% }) %>
36
+
37
+ <% if (currentYear !== null) { %>
38
+ </ul>
39
+ <% } %>
40
+ </div>
41
+
42
+ <!-- 分页 -->
43
+ <%- partial('_partial/pagination') %>
44
+ </div>
45
+ </div>
@@ -0,0 +1,53 @@
1
+ <div class="page-layout blog-layout">
2
+ <div class="content-wrapper">
3
+ <!-- 文章列表 -->
4
+ <div class="post-list">
5
+ <% page.posts.sort('date', -1).each(function(post) { %>
6
+ <article class="post-card">
7
+ <!-- 封面图 -->
8
+ <% if (post.cover) { %>
9
+ <div class="post-cover">
10
+ <a href="<%- url_for(post.path) %>">
11
+ <img src="<%- url_for(post.cover) %>" alt="<%= post.title %>" loading="lazy">
12
+ </a>
13
+ </div>
14
+ <% } %>
15
+
16
+ <div class="post-card-content">
17
+ <!-- 标题 -->
18
+ <h2 class="post-title">
19
+ <a href="<%- url_for(post.path) %>"><%= post.title %></a>
20
+ </h2>
21
+
22
+ <!-- 元信息 -->
23
+ <%- partial('_partial/post-meta', { post: post }) %>
24
+
25
+ <!-- 摘要 -->
26
+ <div class="post-excerpt">
27
+ <% if (post.excerpt) { %>
28
+ <%- post.excerpt %>
29
+ <% } else { %>
30
+ <%- strip_html(post.content).substring(0, 200) %>...
31
+ <% } %>
32
+ </div>
33
+
34
+ <!-- 阅读更多 -->
35
+ <a href="<%- url_for(post.path) %>" class="read-more">
36
+ 阅读全文
37
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+ <path d="M5 12h14"></path>
39
+ <path d="m12 5 7 7-7 7"></path>
40
+ </svg>
41
+ </a>
42
+ </div>
43
+ </article>
44
+ <% }) %>
45
+ </div>
46
+
47
+ <!-- 分页 -->
48
+ <%- partial('_partial/pagination') %>
49
+ </div>
50
+
51
+ <!-- 侧边栏 -->
52
+ <%- partial('_partial/sidebar') %>
53
+ </div>
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= config.language || 'zh-CN' %>" data-theme="light">
3
+ <head>
4
+ <%- partial('_partial/head') %>
5
+ </head>
6
+ <body>
7
+ <%- partial('_partial/header') %>
8
+
9
+ <main class="main-content">
10
+ <div class="container">
11
+ <%- body %>
12
+ </div>
13
+ </main>
14
+
15
+ <%- partial('_partial/footer') %>
16
+
17
+ <!-- 搜索模态框 -->
18
+ <% if (theme.search.enable) { %>
19
+ <%- partial('_partial/search') %>
20
+ <% } %>
21
+
22
+ <!-- 站点配置 -->
23
+ <script>
24
+ window.STARTER_CONFIG = {
25
+ root: '<%- config.root %>',
26
+ searchEnabled: <%= theme.search.enable %>
27
+ };
28
+ </script>
29
+
30
+ <!-- 主脚本 -->
31
+ <%- js('js/main.js') %>
32
+
33
+ <% if (theme.search.enable) { %>
34
+ <%- js('js/search.js') %>
35
+ <% } %>
36
+ </body>
37
+ </html>
@@ -0,0 +1,13 @@
1
+ <div class="page-layout page-single">
2
+ <article class="page-article">
3
+ <!-- 页面头部 -->
4
+ <header class="page-header">
5
+ <h1 class="page-title"><%= page.title %></h1>
6
+ </header>
7
+
8
+ <!-- 页面内容 -->
9
+ <div class="page-content markdown-body">
10
+ <%- page.content %>
11
+ </div>
12
+ </article>
13
+ </div>
@@ -0,0 +1,78 @@
1
+ <div class="page-layout post-layout">
2
+ <article class="post-article">
3
+ <!-- 文章头部 -->
4
+ <header class="post-header">
5
+ <h1 class="post-title"><%= page.title %></h1>
6
+
7
+ <!-- 元信息 -->
8
+ <%- partial('_partial/post-meta', { post: page, showFull: true }) %>
9
+
10
+ <!-- 封面图 -->
11
+ <% if (page.cover) { %>
12
+ <div class="post-cover">
13
+ <img src="<%- url_for(page.cover) %>" alt="<%= page.title %>">
14
+ </div>
15
+ <% } %>
16
+ </header>
17
+
18
+ <!-- 文章内容 -->
19
+ <div class="post-content markdown-body">
20
+ <%- page.content %>
21
+ </div>
22
+
23
+ <!-- 标签 -->
24
+ <% if (page.tags && page.tags.length) { %>
25
+ <div class="post-tags">
26
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
27
+ <path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"></path>
28
+ <path d="M7 7h.01"></path>
29
+ </svg>
30
+ <% page.tags.each(function(tag) { %>
31
+ <a href="<%- url_for(tag.path) %>" class="tag-link"><%= tag.name %></a>
32
+ <% }) %>
33
+ </div>
34
+ <% } %>
35
+
36
+ <!-- 版权信息 -->
37
+ <% if (theme.post.copyright) { %>
38
+ <div class="post-copyright">
39
+ <p><strong>本文作者:</strong><%= config.author %></p>
40
+ <p><strong>本文链接:</strong><a href="<%= page.permalink %>"><%= page.permalink %></a></p>
41
+ <p><strong>版权声明:</strong>本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noopener noreferrer">CC BY-NC-SA 4.0</a> 许可协议。转载请注明出处!</p>
42
+ </div>
43
+ <% } %>
44
+
45
+ <!-- 上一篇 / 下一篇 -->
46
+ <nav class="post-nav">
47
+ <% if (page.prev) { %>
48
+ <a href="<%- url_for(page.prev.path) %>" class="post-nav-item prev">
49
+ <span class="post-nav-label">上一篇</span>
50
+ <span class="post-nav-title"><%= page.prev.title %></span>
51
+ </a>
52
+ <% } else { %>
53
+ <div class="post-nav-item prev disabled"></div>
54
+ <% } %>
55
+
56
+ <% if (page.next) { %>
57
+ <a href="<%- url_for(page.next.path) %>" class="post-nav-item next">
58
+ <span class="post-nav-label">下一篇</span>
59
+ <span class="post-nav-title"><%= page.next.title %></span>
60
+ </a>
61
+ <% } else { %>
62
+ <div class="post-nav-item next disabled"></div>
63
+ <% } %>
64
+ </nav>
65
+ </article>
66
+
67
+ <!-- TOC 目录 -->
68
+ <% if (theme.toc.enable && page.toc !== false) { %>
69
+ <aside class="post-toc">
70
+ <div class="toc-wrapper">
71
+ <h3 class="toc-title">目录</h3>
72
+ <div class="toc-content">
73
+ <%- toc(page.content, { list_number: theme.toc.number, max_depth: theme.toc.max_depth }) %>
74
+ </div>
75
+ </div>
76
+ </aside>
77
+ <% } %>
78
+ </div>
package/layout/tag.ejs ADDED
@@ -0,0 +1,28 @@
1
+ <div class="page-layout archive-layout">
2
+ <div class="archive-wrapper">
3
+ <header class="archive-header">
4
+ <h1 class="archive-title">
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6
+ <path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"></path>
7
+ <path d="M7 7h.01"></path>
8
+ </svg>
9
+ <%= page.tag %>
10
+ </h1>
11
+ <p class="archive-count">共 <%= page.posts.length %> 篇文章</p>
12
+ </header>
13
+
14
+ <div class="archive-content">
15
+ <ul class="archive-list">
16
+ <% page.posts.each(function(post) { %>
17
+ <li class="archive-item">
18
+ <span class="archive-date"><%= date(post.date, 'YYYY-MM-DD') %></span>
19
+ <a href="<%- url_for(post.path) %>" class="archive-link"><%= post.title %></a>
20
+ </li>
21
+ <% }) %>
22
+ </ul>
23
+ </div>
24
+
25
+ <!-- 分页 -->
26
+ <%- partial('_partial/pagination') %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="page-layout tags-layout">
2
+ <div class="tags-wrapper">
3
+ <header class="tags-header">
4
+ <h1 class="tags-title">标签</h1>
5
+ <p class="tags-count">共 <%= site.tags.length %> 个标签</p>
6
+ </header>
7
+
8
+ <div class="tags-cloud">
9
+ <%
10
+ // 按文章数量降序排序
11
+ const sortedTags = site.tags.toArray().sort((a, b) => b.posts.length - a.posts.length);
12
+ sortedTags.forEach(function(tag) {
13
+ %>
14
+ <a href="<%- url_for(tag.path) %>" class="tag-cloud-item" data-count="<%= tag.posts.length %>">
15
+ <%= tag.name %>
16
+ <span class="tag-count">(<%= tag.posts.length %>)</span>
17
+ </a>
18
+ <% }) %>
19
+ </div>
20
+ </div>
21
+ </div>
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hexo-theme-hydrogen",
3
+ "version": "1.1.0",
4
+ "license": "MIT",
5
+ "description": "Hexo Theme Hydrogen",
6
+ "repository": "https://github.com/CoderFrish/hexo-theme-hydrogen",
7
+ "keywords": [
8
+ "hexo",
9
+ "theme",
10
+ "hydrogen"
11
+ ]
12
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * 搜索索引生成器
5
+ * 在 Hexo 构建时生成 search.json 文件
6
+ */
7
+ hexo.extend.generator.register("search", function (locals) {
8
+ const config = this.theme.config.search || {};
9
+
10
+ if (!config.enable) {
11
+ return;
12
+ }
13
+
14
+ const fields = config.fields || ["title", "content", "tags"];
15
+ const excerptLength = config.excerpt_length || 100;
16
+
17
+ // 收集所有文章和页面
18
+ const posts = locals.posts.sort("-date").toArray();
19
+ const pages = locals.pages.toArray();
20
+
21
+ const data = [];
22
+
23
+ // 处理文章
24
+ posts.forEach(function (post) {
25
+ const item = {
26
+ title: post.title || "",
27
+ url: post.path,
28
+ date: post.date ? post.date.format("YYYY-MM-DD") : "",
29
+ };
30
+
31
+ // 内容
32
+ if (fields.includes("content")) {
33
+ let content = post.content || "";
34
+ // 移除 HTML 标签
35
+ content = content.replace(/<[^>]+>/g, "");
36
+ // 移除多余空白
37
+ content = content.replace(/\s+/g, " ").trim();
38
+ // 截取摘要
39
+ item.content = content.substring(0, excerptLength * 3);
40
+ item.excerpt = content.substring(0, excerptLength);
41
+ }
42
+
43
+ // 标签
44
+ if (fields.includes("tags") && post.tags) {
45
+ item.tags = post.tags.map(function (tag) {
46
+ return tag.name;
47
+ });
48
+ }
49
+
50
+ // 分类
51
+ if (fields.includes("categories") && post.categories) {
52
+ item.categories = post.categories.map(function (cat) {
53
+ return cat.name;
54
+ });
55
+ }
56
+
57
+ data.push(item);
58
+ });
59
+
60
+ // 处理页面(包括文档页面)
61
+ pages.forEach(function (page) {
62
+ // 跳过没有标题的页面
63
+ if (!page.title) return;
64
+
65
+ const item = {
66
+ title: page.title,
67
+ url: page.path,
68
+ isPage: true,
69
+ };
70
+
71
+ // 内容
72
+ if (fields.includes("content")) {
73
+ let content = page.content || "";
74
+ content = content.replace(/<[^>]+>/g, "");
75
+ content = content.replace(/\s+/g, " ").trim();
76
+ item.content = content.substring(0, excerptLength * 3);
77
+ item.excerpt = content.substring(0, excerptLength);
78
+ }
79
+
80
+ data.push(item);
81
+ });
82
+
83
+ return {
84
+ path: "search.json",
85
+ data: JSON.stringify(data),
86
+ };
87
+ });
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * 字数统计
5
+ * 用法: <%- wordCount(post.content) %>
6
+ */
7
+ hexo.extend.helper.register("wordCount", function (content) {
8
+ if (!content) return 0;
9
+
10
+ // 移除 HTML 标签
11
+ const text = content.replace(/<[^>]+>/g, "");
12
+ // 移除多余空白
13
+ const cleanText = text.replace(/\s+/g, "").trim();
14
+
15
+ return cleanText.length;
16
+ });
17
+
18
+ /**
19
+ * 阅读时间估算
20
+ * 用法: <%- readingTime(post.content, 300) %>
21
+ */
22
+ hexo.extend.helper.register("readingTime", function (content, wordsPerMinute) {
23
+ if (!content) return 0;
24
+
25
+ const wpm = wordsPerMinute || 300;
26
+ const text = content.replace(/<[^>]+>/g, "");
27
+ const cleanText = text.replace(/\s+/g, "").trim();
28
+ return Math.ceil(cleanText.length / wpm);
29
+ });
30
+
31
+ /**
32
+ * 格式化字数显示
33
+ * 用法: <%- formatWordCount(wordCount) %>
34
+ */
35
+ hexo.extend.helper.register("formatWordCount", function (count) {
36
+ if (count >= 10000) {
37
+ return (count / 10000).toFixed(1) + "万";
38
+ } else if (count >= 1000) {
39
+ return (count / 1000).toFixed(1) + "k";
40
+ }
41
+ return count.toString();
42
+ });
43
+
44
+ /**
45
+ * 判断当前路径是否匹配
46
+ * 用法: <%- is_current('/about') %>
47
+ */
48
+ hexo.extend.helper.register("is_current", function (path) {
49
+ const currentPath = this.page.path || "";
50
+ const targetPath = path.replace(/^\//, "").replace(/\/$/, "");
51
+ const current = currentPath
52
+ .replace(/^\//, "")
53
+ .replace(/\/$/, "")
54
+ .replace(/index\.html$/, "");
55
+
56
+ if (targetPath === "") {
57
+ return current === "" || current === "index.html";
58
+ }
59
+
60
+ return current.startsWith(targetPath);
61
+ });
62
+
63
+ /**
64
+ * 获取文档导航(上一篇/下一篇)
65
+ * 用法: <% const nav = getDocNav(theme.docs, page.path) %>
66
+ */
67
+ hexo.extend.helper.register("getDocNav", function (docs, currentPath) {
68
+ if (!docs || !docs.length) {
69
+ return { prev: null, next: null };
70
+ }
71
+
72
+ // 扁平化文档列表
73
+ const flatDocs = [];
74
+
75
+ function flatten(items) {
76
+ items.forEach(function (item) {
77
+ if (item.path) {
78
+ flatDocs.push(item);
79
+ }
80
+ if (item.children) {
81
+ flatten(item.children);
82
+ }
83
+ });
84
+ }
85
+
86
+ flatten(docs);
87
+
88
+ // 查找当前文档位置
89
+ const currentIndex = flatDocs.findIndex(function (doc) {
90
+ return currentPath.includes(doc.path);
91
+ });
92
+
93
+ if (currentIndex === -1) {
94
+ return { prev: null, next: null };
95
+ }
96
+
97
+ return {
98
+ prev: currentIndex > 0 ? flatDocs[currentIndex - 1] : null,
99
+ next:
100
+ currentIndex < flatDocs.length - 1 ? flatDocs[currentIndex + 1] : null,
101
+ };
102
+ });
103
+
104
+ /**
105
+ * 截取摘要
106
+ * 用法: <%- excerpt(post.content, 200) %>
107
+ */
108
+ hexo.extend.helper.register("excerpt", function (content, length) {
109
+ if (!content) return "";
110
+
111
+ const maxLength = length || 200;
112
+ // 移除 HTML 标签
113
+ const text = content.replace(/<[^>]+>/g, "");
114
+ // 移除多余空白
115
+ const cleanText = text.replace(/\s+/g, " ").trim();
116
+
117
+ if (cleanText.length <= maxLength) {
118
+ return cleanText;
119
+ }
120
+
121
+ return cleanText.substring(0, maxLength) + "...";
122
+ });
123
+
124
+ /**
125
+ * 相对时间
126
+ * 用法: <%- timeAgo(post.date) %>
127
+ */
128
+ hexo.extend.helper.register("timeAgo", function (date) {
129
+ const now = new Date();
130
+ const past = new Date(date);
131
+ const diffMs = now - past;
132
+ const diffSec = Math.floor(diffMs / 1000);
133
+ const diffMin = Math.floor(diffSec / 60);
134
+ const diffHour = Math.floor(diffMin / 60);
135
+ const diffDay = Math.floor(diffHour / 24);
136
+ const diffMonth = Math.floor(diffDay / 30);
137
+ const diffYear = Math.floor(diffDay / 365);
138
+
139
+ if (diffYear > 0) {
140
+ return diffYear + " 年前";
141
+ } else if (diffMonth > 0) {
142
+ return diffMonth + " 个月前";
143
+ } else if (diffDay > 0) {
144
+ return diffDay + " 天前";
145
+ } else if (diffHour > 0) {
146
+ return diffHour + " 小时前";
147
+ } else if (diffMin > 0) {
148
+ return diffMin + " 分钟前";
149
+ } else {
150
+ return "刚刚";
151
+ }
152
+ });
153
+
154
+ /**
155
+ * 生成唯一 ID
156
+ * 用法: <%- uniqueId('prefix') %>
157
+ */
158
+ hexo.extend.helper.register("uniqueId", function (prefix) {
159
+ const random = Math.random().toString(36).substring(2, 9);
160
+ return (prefix || "id") + "-" + random;
161
+ });
162
+
163
+ /**
164
+ * JSON 序列化(用于内联脚本)
165
+ * 用法: <%- jsonStringify(data) %>
166
+ */
167
+ hexo.extend.helper.register("jsonStringify", function (data) {
168
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
169
+ });
170
+
171
+ /**
172
+ * 获取文章所属的分类路径
173
+ * 用法: <%- getCategoryPath(post.categories) %>
174
+ */
175
+ hexo.extend.helper.register("getCategoryPath", function (categories) {
176
+ if (!categories || !categories.length) {
177
+ return [];
178
+ }
179
+
180
+ return categories.map(function (cat) {
181
+ return {
182
+ name: cat.name,
183
+ path: cat.path,
184
+ };
185
+ });
186
+ });
187
+
188
+ /**
189
+ * 检查是否有封面图
190
+ * 用法: <% if (hasCover(post)) { %>
191
+ */
192
+ hexo.extend.helper.register("hasCover", function (post) {
193
+ return post && post.cover && post.cover.trim() !== "";
194
+ });
195
+
196
+ /**
197
+ * 获取随机颜色(用于标签等)
198
+ * 用法: <%- randomColor(tag.name) %>
199
+ */
200
+ hexo.extend.helper.register("randomColor", function (str) {
201
+ const colors = [
202
+ "#ef4444",
203
+ "#f97316",
204
+ "#f59e0b",
205
+ "#eab308",
206
+ "#84cc16",
207
+ "#22c55e",
208
+ "#10b981",
209
+ "#14b8a6",
210
+ "#06b6d4",
211
+ "#0ea5e9",
212
+ "#3b82f6",
213
+ "#6366f1",
214
+ "#8b5cf6",
215
+ "#a855f7",
216
+ "#d946ef",
217
+ "#ec4899",
218
+ "#f43f5e",
219
+ ];
220
+
221
+ // 基于字符串生成固定的颜色索引
222
+ let hash = 0;
223
+ for (let i = 0; i < str.length; i++) {
224
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
225
+ }
226
+
227
+ const index = Math.abs(hash) % colors.length;
228
+ return colors[index];
229
+ });