hexo-theme-linen 1.0.21 → 1.0.23

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 (46) hide show
  1. package/languages/en.yml +3 -0
  2. package/languages/zh-CN.yml +4 -1
  3. package/layout/footer.ejs +62 -0
  4. package/layout/layout.ejs +22 -9
  5. package/layout/partials/header.ejs +1 -1
  6. package/layout/partials/post-item.ejs +5 -5
  7. package/layout/partials/related-posts.ejs +1 -1
  8. package/layout/post.ejs +362 -24
  9. package/layout/series.ejs +277 -4
  10. package/package.json +1 -1
  11. package/scripts/font-version.js +5 -3
  12. package/scripts/helpers/tools.js +187 -191
  13. package/scripts/lazyload/client.js +11 -2
  14. package/scripts/lazyload/index.js +7 -5
  15. package/scripts/lazyload/inject.js +8 -0
  16. package/scripts/lazyload/process.js +202 -163
  17. package/scripts/markdown-it-image-grid.js +74 -10
  18. package/source/linen-theme/css/{mediaquery.scss → _mediaquery.scss} +183 -12
  19. package/source/linen-theme/css/atom.xsl +37 -0
  20. package/source/linen-theme/css/font-version.scss +1 -1
  21. package/source/linen-theme/css/images/layers-2x.png +0 -0
  22. package/source/linen-theme/css/images/layers.png +0 -0
  23. package/source/linen-theme/css/images/marker-icon-2x.png +0 -0
  24. package/source/linen-theme/css/images/marker-icon.png +0 -0
  25. package/source/linen-theme/css/images/marker-shadow.png +0 -0
  26. package/source/linen-theme/css/leaflet.css +661 -0
  27. package/source/linen-theme/css/linen.scss +1198 -114
  28. package/source/linen-theme/css/linkcard.scss +53 -11
  29. package/source/linen-theme/css/noscript.css +70 -0
  30. package/source/linen-theme/css/photoswipe-dynamic-caption-plugin.css +1 -1
  31. package/source/linen-theme/css/photoswipe.css +166 -1
  32. package/source/linen-theme/css/rss-en.xsl +1 -1
  33. package/source/linen-theme/css/rss-zh.xsl +1 -1
  34. package/source/linen-theme/css/vars.scss +1 -1
  35. package/source/linen-theme/img/pattern-randomized.svg +1 -0
  36. package/source/linen-theme/img/travel-map-fallback.svg +30 -0
  37. package/source/linen-theme/js/hdr.js +42 -12
  38. package/source/linen-theme/js/lazyload.js +11 -2
  39. package/source/linen-theme/js/leaflet.js +6 -0
  40. package/source/linen-theme/js/linen.js +114 -83
  41. package/source/linen-theme/js/photoswipe/photoswipe.esm.min.js +13 -0
  42. package/source/linen-theme/js/replace-emoji.js +2 -4
  43. package/source/linen-theme/js/toc.js +71 -121
  44. package/layout/partials/footer.ejs +0 -12
  45. package/source/linen-theme/img/icons/backref.svg +0 -1
  46. package/source/linen-theme/img/icons/more.svg +0 -4
@@ -2,6 +2,45 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const moment = require("moment");
4
4
 
5
+ hexo.extend.helper.register("take", function (arr, n = 1) {
6
+ return arr.slice(0, Math.max(0, n));
7
+ });
8
+
9
+ hexo.extend.helper.register("takeLast", function (arr, n = 1) {
10
+ return (arr, n = 1) => arr.slice(-Math.max(0, n));
11
+ });
12
+
13
+ hexo.extend.helper.register("getPageTitle", function () {
14
+ const page = this.page;
15
+ const config = this.config;
16
+ let pageTitle = page.title || config.title || "";
17
+ if (this.is_archive()) {
18
+ pageTitle = this.__("common.archives");
19
+ }
20
+ if (this.is_tag()) {
21
+ pageTitle = this.__("common.tag") + ": " + page.tag;
22
+ }
23
+ if (this.is_category()) {
24
+ pageTitle = this.__("common.category") + ": " + page.category;
25
+ }
26
+ if (this.is_month()) {
27
+ pageTitle += ": " + page.month + "/" + page.year;
28
+ }
29
+ if (this.is_year()) {
30
+ pageTitle += ": " + page.year;
31
+ }
32
+ if (this.is_home()) {
33
+ pageTitle += this.is_home_first_page() ? "" : " - " + page.current;
34
+ }
35
+ if (!this.is_post() && !!page.series) {
36
+ pageTitle = this.__("common.series") + ": " + page.series;
37
+ }
38
+ if (page.subTitle) {
39
+ pageTitle += `(${page.subTitle})`;
40
+ }
41
+ return pageTitle;
42
+ });
43
+
5
44
  function getFile(filePath, defaultValue) {
6
45
  try {
7
46
  return JSON.parse(fs.readFileSync(filePath));
@@ -10,217 +49,174 @@ function getFile(filePath, defaultValue) {
10
49
  }
11
50
  }
12
51
 
13
- hexo.extend.helper.register("take", function (arr, n = 1) {
14
- return arr.slice(0, Math.max(0, n));
15
- });
16
-
17
- hexo.extend.helper.register("takeLast", function (arr, n = 1) {
18
- return (arr, n = 1) => arr.slice(-Math.max(0, n));
19
- });
52
+ hexo.extend.helper.register("getSeriesInfo", function (seriesName) {
53
+ const seriesInfo = getFile(
54
+ path.join(hexo.base_dir, "assets-db/series.json"),
55
+ {},
56
+ );
57
+ return seriesInfo?.[seriesName] || {};
58
+ });
20
59
 
21
- hexo.extend.helper.register("getPageTitle", function () {
60
+ hexo.extend.helper.register("getTranslationPage", function () {
61
+ try {
22
62
  const page = this.page;
23
63
  const config = this.config;
24
- let pageTitle = page.title || config.title || "";
25
- if (this.is_archive()) {
26
- pageTitle = this.__("common.archives");
27
- }
28
- if (this.is_tag()) {
29
- pageTitle = this.__("common.tag") + ": " + page.tag;
30
- }
31
- if (this.is_category()) {
32
- pageTitle = this.__("common.category") + ": " + page.category;
33
- }
34
- if (this.is_month()) {
35
- pageTitle += ": " + page.month + "/" + page.year;
36
- }
37
- if (this.is_year()) {
38
- pageTitle += ": " + page.year;
39
- }
40
- if (this.is_home()) {
41
- pageTitle += this.is_home_first_page() ? "" : " - " + page.current;
42
- }
43
- if (!this.is_post() && !!page.series) {
44
- pageTitle = this.__("common.series") + ": " + page.series;
45
- }
46
- if (page.subTitle) {
47
- pageTitle += `(${page.subTitle})`;
48
- }
49
- return pageTitle;
50
- });
51
-
52
- hexo.extend.helper.register("getSeriesInfo", function (seriesName) {
53
- const seriesInfo = getFile(
54
- path.join(hexo.base_dir, "assets-db/series.json"),
64
+ const translations = page.translations || [];
65
+ const currentLanguage = page.language || config.language;
66
+ const targetLanguage =
67
+ translations.length >= 1
68
+ ? translations[0]
69
+ : currentLanguage === "en"
70
+ ? "zh-CN"
71
+ : "en";
72
+ const categoriesInfo = getFile(
73
+ path.join(hexo.base_dir, "assets-db/categories.json"),
55
74
  {},
56
75
  );
57
- return seriesInfo?.[seriesName] || {};
58
- });
59
-
60
- hexo.extend.helper.register("getTranslationPage", function () {
61
- try {
62
- const page = this.page;
63
- const config = this.config;
64
- const rootPath = config.root || "/";
65
- const translations = page.translations || [];
66
- const currentLanguage = page.language || config.language;
67
- const targetLanguage =
68
- translations.length >= 1
69
- ? translations[0]
70
- : currentLanguage === "en"
71
- ? "zh-CN"
72
- : "en";
73
- const categoriesInfo = getFile(
74
- path.join(hexo.base_dir, "assets-db/categories.json"),
75
- {},
76
- );
77
- const tagsInfo = getFile(
78
- path.join(hexo.base_dir, "assets-db/tags.json"),
79
- {},
80
- );
81
- let hasTranslatation = Object.keys(
82
- config?.theme_config?.siteOriginLocaleMap || {},
83
- ).length
84
- ? this.is_post()
85
- ? translations.length >= 1
86
- : !page?.series || page?.seriesInfo?.mappingName
87
- : false;
88
- let origin =
89
- config?.theme_config?.siteOriginLocaleMap?.[targetLanguage] ||
90
- config.url;
91
- let targetPath = this.url_for(page.path);
92
- if (page?.seriesInfo?.mappingName) {
93
- targetPath = this.url_for(`/series/${page?.seriesInfo?.mappingName}/`);
94
- }
95
- if (this.is_category()) {
96
- if (categoriesInfo?.[page.category]?.mappingName) {
97
- targetPath = this.url_for(`/categories/${
98
- categoriesInfo?.[page.category]?.mappingName
99
- }/`);
100
- } else {
101
- hasTranslatation = false;
102
- }
76
+ const tagsInfo = getFile(
77
+ path.join(hexo.base_dir, "assets-db/tags.json"),
78
+ {},
79
+ );
80
+ let hasTranslatation = Object.keys(
81
+ config?.theme_config?.siteOriginLocaleMap || {},
82
+ ).length
83
+ ? this.is_post()
84
+ ? translations.length >= 1
85
+ : !page?.series || page?.seriesInfo?.mappingName
86
+ : false;
87
+ let origin =
88
+ config?.theme_config?.siteOriginLocaleMap?.[targetLanguage] || config.url;
89
+ let targetLanguagePath = `${origin}${this.url_for(page.path)}`;
90
+ if (page?.seriesInfo?.mappingName) {
91
+ targetLanguagePath = `${origin}/series/${page?.seriesInfo?.mappingName}/`;
92
+ }
93
+ if (this.is_category()) {
94
+ if (categoriesInfo?.[page.category]?.mappingName) {
95
+ targetLanguagePath = `${origin}/categories/${
96
+ categoriesInfo?.[page.category]?.mappingName
97
+ }/`;
98
+ } else {
99
+ hasTranslatation = false;
103
100
  }
104
- if (this.is_tag()) {
105
- if (tagsInfo?.[page.tag]?.mappingName) {
106
- targetPath = this.url_for(`/tags/${tagsInfo?.[page.tag]?.mappingName}/`);
107
- } else {
108
- hasTranslatation = false;
109
- }
101
+ }
102
+ if (this.is_tag()) {
103
+ if (tagsInfo?.[page.tag]?.mappingName) {
104
+ targetLanguagePath = `${origin}/tags/${tagsInfo?.[page.tag]?.mappingName}/`;
105
+ } else {
106
+ hasTranslatation = false;
110
107
  }
111
- const targetLanguagePath = `${origin}${targetPath.replace(rootPath, "")}`;
112
- return {
113
- hasTranslatation,
114
- targetLanguage,
115
- targetLanguagePath,
116
- };
117
- } catch (error) {
118
- console.log(error);
119
- return {
120
- hasTranslatation: false,
121
- targetLanguage: "",
122
- targetLanguagePath: "",
123
- };
124
108
  }
125
- });
109
+ return {
110
+ hasTranslatation,
111
+ targetLanguage,
112
+ targetLanguagePath,
113
+ };
114
+ } catch (error) {
115
+ console.log(error);
116
+ return {
117
+ hasTranslatation: false,
118
+ targetLanguage: "",
119
+ targetLanguagePath: "",
120
+ };
121
+ }
122
+ });
123
+
124
+ hexo.extend.helper.register("getPostDate", function (post, lang) {
125
+ const p = post || this.page;
126
+ if (!p || !p.date) return "";
126
127
 
127
- hexo.extend.helper.register("getPostDate", function (post, lang) {
128
- const p = post || this.page;
129
- if (!p || !p.date) return "";
128
+ // 统一转成 moment,避免 generator 阶段是原生 Date
129
+ const date = moment(p.date);
130
130
 
131
- // 统一转成 moment,避免 generator 阶段是原生 Date
132
- const date = moment(p.date);
131
+ const language =
132
+ lang ||
133
+ p.lang ||
134
+ p.language ||
135
+ this?.page?.lang ||
136
+ this?.page?.language ||
137
+ this?.config?.language;
133
138
 
134
- const language =
135
- lang ||
136
- p.lang ||
137
- p.language ||
138
- this?.page?.lang ||
139
- this?.page?.language ||
140
- this?.config?.language;
139
+ if (language?.startsWith("zh")) {
140
+ return date.format("M 月 D 日,YYYY");
141
+ }
141
142
 
142
- if (language?.startsWith("zh")) {
143
- return date.format("M 月 D 日,YYYY");
143
+ return date.format("MMM D, YYYY");
144
+ });
145
+
146
+ hexo.extend.helper.register(
147
+ "estimateReadingTime",
148
+ function (text, photos, content, options = {}) {
149
+ const {
150
+ chineseCPM = 260,
151
+ englishWPM = 200,
152
+ codeWPM = 100,
153
+ imgSec = 2,
154
+ } = options;
155
+
156
+ // 移除 HTML 标签,保留文本
157
+
158
+ // 匹配代码块内容 <pre><code>...</code></pre>
159
+ const codeBlocks = Array.from(content.matchAll(/<pre>[\s\S]*?<\/pre>/gi));
160
+ let codeWords = 0;
161
+ if (codeBlocks.length) {
162
+ codeBlocks.forEach((block) => {
163
+ const codeText = block[0].replace(/<[^>]+>/g, " ");
164
+ codeWords += codeText.trim().split(/\s+/).length;
165
+ });
144
166
  }
145
167
 
146
- return date.format("MMM D, YYYY");
147
- });
148
-
149
- hexo.extend.helper.register(
150
- "estimateReadingTime",
151
- function (text, photos, content, options = {}) {
152
- const {
153
- chineseCPM = 260,
154
- englishWPM = 200,
155
- codeWPM = 100,
156
- imgSec = 2,
157
- } = options;
158
-
159
- // 移除 HTML 标签,保留文本
160
-
161
- // 匹配代码块内容 <pre><code>...</code></pre>
162
- const codeBlocks = Array.from(content.matchAll(/<pre>[\s\S]*?<\/pre>/gi));
163
- let codeWords = 0;
164
- if (codeBlocks.length) {
165
- codeBlocks.forEach((block) => {
166
- const codeText = block[0].replace(/<[^>]+>/g, " ");
167
- codeWords += codeText.trim().split(/\s+/).length;
168
- });
169
- }
168
+ // 文本内容(去掉无关元素)
169
+ const englishWords = (text.match(/[a-zA-Z]+/g) || []).length;
170
170
 
171
- // 文本内容(去掉无关元素)
172
- const englishWords = (text.match(/[a-zA-Z]+/g) || []).length;
171
+ // 匹配中文字符
172
+ const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
173
173
 
174
- // 匹配中文字符
175
- const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
174
+ // 匹配图片 <img>
175
+ const imgTime = (photos * imgSec) / 60; // 转成分钟
176
176
 
177
- // 匹配图片 <img>
178
- const imgTime = (photos * imgSec) / 60; // 转成分钟
177
+ // 计算阅读时间
178
+ const timeEnglish = englishWords / englishWPM;
179
+ const timeChinese = chineseChars / chineseCPM;
180
+ const timeCode = codeWords / codeWPM;
179
181
 
180
- // 计算阅读时间
181
- const timeEnglish = englishWords / englishWPM;
182
- const timeChinese = chineseChars / chineseCPM;
183
- const timeCode = codeWords / codeWPM;
182
+ const totalTime = timeEnglish + timeChinese + timeCode + imgTime;
184
183
 
185
- const totalTime = timeEnglish + timeChinese + timeCode + imgTime;
184
+ return Math.max(1, Math.round(totalTime)); // 最少 1 分钟
185
+ },
186
+ );
186
187
 
187
- return Math.max(1, Math.round(totalTime)); // 最少 1 分钟
188
- },
189
- );
188
+ hexo.extend.helper.register("extractTocFromHtml", function (htmlString) {
189
+ // 匹配 h2 和 h3 的正则表达式,增加捕获 id 处理
190
+ const headingRegex = /<(h2|h3)(?:[^>]*?\s+id="([^"]*)")?[^>]*>(.*?)<\/\1>/gi;
190
191
 
191
- hexo.extend.helper.register("extractTocFromHtml", function (htmlString) {
192
- // 匹配 h2 和 h3 的正则表达式,增加捕获 id 处理
193
- const headingRegex =
194
- /<(h2|h3)(?:[^>]*?\s+id="([^"]*)")?[^>]*>(.*?)<\/\1>/gi;
195
-
196
- const toc = [];
197
- let currentH2 = null;
198
-
199
- let match;
200
- while ((match = headingRegex.exec(htmlString)) !== null) {
201
- const tag = match[1]; // h2 或 h3
202
- let id = match[2]; // 已存在的 id
203
- let content = match[3].trim(); // 提取标签内容
204
-
205
- // 去除所有 HTML 标签,提取纯文本内容
206
- content = content.replace(/<[^>]*>/g, "");
207
-
208
- if (tag === "h2") {
209
- // 创建新的 h2 结构
210
- currentH2 = {
211
- title: content,
212
- id: id,
213
- children: [],
214
- };
215
- toc.push(currentH2);
216
- } else if (tag === "h3" && id && currentH2) {
217
- // 将 h3 添加到当前 h2 的 children 数组中
218
- currentH2.children.push({
219
- title: content,
220
- id: id,
221
- });
222
- }
192
+ const toc = [];
193
+ let currentH2 = null;
194
+
195
+ let match;
196
+ while ((match = headingRegex.exec(htmlString)) !== null) {
197
+ const tag = match[1]; // h2 或 h3
198
+ let id = match[2]; // 已存在的 id
199
+ let content = match[3].trim(); // 提取标签内容
200
+
201
+ // 去除所有 HTML 标签,提取纯文本内容
202
+ content = content.replace(/<[^>]*>/g, "");
203
+
204
+ if (tag === "h2") {
205
+ // 创建新的 h2 结构
206
+ currentH2 = {
207
+ title: content,
208
+ id: id,
209
+ children: [],
210
+ };
211
+ toc.push(currentH2);
212
+ } else if (tag === "h3" && id && currentH2) {
213
+ // 将 h3 添加到当前 h2 的 children 数组中
214
+ currentH2.children.push({
215
+ title: content,
216
+ id: id,
217
+ });
223
218
  }
219
+ }
224
220
 
225
- return toc;
226
- });
221
+ return toc;
222
+ });
@@ -114,6 +114,16 @@
114
114
  }
115
115
  var lazyloadItems = document.querySelectorAll(".lazyload-wrap");
116
116
  var loadingQueue = [];
117
+ var appendStyles = function appendStyles(element, styles) {
118
+ var currentStyle = element.getAttribute("style");
119
+ var newStyles = "";
120
+ if (currentStyle) {
121
+ var hasSemicolon = currentStyle.endsWith(";");
122
+ newStyles = currentStyle + (hasSemicolon ? "" : ";");
123
+ }
124
+ newStyles += styles;
125
+ element.setAttribute("style", newStyles);
126
+ };
117
127
  var createResourceElement = function createResourceElement(contentStr) {
118
128
  var nElement = document.createElement("div");
119
129
  nElement.innerHTML = contentStr;
@@ -324,8 +334,7 @@
324
334
  }
325
335
 
326
336
  var createObserver = function createObserver(element) {
327
- const observer = observeWithStableVisible(element);
328
- observer.observe(element);
337
+ observeWithStableVisible(element);
329
338
  };
330
339
  if (!window.IntersectionObserver) {
331
340
  for (var i = 0; i < lazyloadItems.length; i++) {
@@ -1,13 +1,15 @@
1
- if (hexo?.config?.theme_config?.lazyload?.enable === false) {
1
+ if (!hexo?.config?.theme_config?.lazyload?.enable) {
2
2
  return;
3
3
  }
4
4
  hexo.extend.filter.register(
5
5
  "after_post_render",
6
- require("./process").processPost,
6
+ require("./process").processPost
7
7
  );
8
8
  hexo.extend.filter.register(
9
9
  "after_render:html",
10
10
  require("./process").processCovers,
11
- 3,
12
- );
13
- hexo.extend.filter.register("after_render:html", require("./inject")(hexo));
11
+ 3);
12
+ hexo.extend.filter.register(
13
+ "after_render:html",
14
+ require("./inject")(hexo)
15
+ );
@@ -162,6 +162,14 @@ module.exports = function (hexo) {
162
162
  };
163
163
  if (/<\/body>/gi.test(htmlContent)) {
164
164
  htmlContent = appendToHead(settingsContent, htmlContent);
165
+ htmlContent = appendToHead(
166
+ `<link rel="preload" as="image" href="${loadingImage}" />`,
167
+ htmlContent,
168
+ );
169
+ htmlContent = appendToHead(
170
+ `<link rel="preload" as="image" href="${errorTipImage}" />`,
171
+ htmlContent,
172
+ );
165
173
  injectExtraScript(lazyLoadPath);
166
174
  injectExtraStyles(injectedStyles);
167
175
  htmlContent = htmlContent.replace(