hexo-theme-stellar 1.42.0 → 1.42.1
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.
- package/.claude/skills/stellar-theme-dev/SKILL.md +1 -0
- package/CHANGELOG.md +11 -0
- package/_config.yml +4 -4
- package/layout/_partial/scripts/bootstrap.ejs +3 -1
- package/layout/_plugins/index.ejs +25 -0
- package/package.json +1 -1
- package/scripts/filters/lib/img_lazyload.js +184 -40
- package/source/css/_components/widgets/ghrepo.styl +4 -0
- package/source/css/comments/waline.styl +24 -0
|
@@ -38,6 +38,7 @@ description: 主题仓库(hexo-theme-stellar)开发全流程:方案 →
|
|
|
38
38
|
- 任务完成后**不自动提交**:改动保留在工作区供用户审查。
|
|
39
39
|
- 仅在用户明确要求提交时执行;提交前须通过第 3 步中应执行的验证。
|
|
40
40
|
- 使用 Conventional Commits,一次提交对应一个需求点;如果几个需求任务逻辑相似,可以合并为一次提交;类型与格式见 `AGENTS.md`「Git 规范」(§7)。
|
|
41
|
+
- 合并代码时,把合并提交 / PR 标题改为 Conventional Commits 格式(见 `AGENTS.md`「Git 规范」§7),不保留默认的 `Merge branch ...` / `Merge pull request ...` 标题。
|
|
41
42
|
- 改动只提交在本仓库(stellar);仅当用户明确要求时 push。
|
|
42
43
|
- 完成条件:改动已通过应执行的验证并保留在工作区;如用户要求提交,提交符合规范;如用户要求 push,已推送且远端为最新。
|
|
43
44
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.42.1
|
|
4
|
+
|
|
5
|
+
> 发布日期:2026-08-17
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- 图片懒加载过滤器在 HTML 压缩器(如 hexo-minify `removeAttributeQuotes`)去除属性引号后,原正则跨标签越界,把页尾 bootstrap 内联脚本里的 `s.src = "/js/utils.js?v=..."` 误改写为「1×1 PNG 占位 + `data-src`」,导致 bootstrap 语法错误、`stellar is not defined` 连锁报错、scrollreveal 不初始化、`.slide-up` 文章列表延迟约 3 秒才显示;改为属性感知标签扫描并跳过 `<script>`/`<style>`/注释区域,同时兼容无引号 `src` 的图片懒加载
|
|
9
|
+
- bootstrap 的 utils 补载改用 `setAttribute('src')` 赋值,避免被基于 `s.src = "..."` 的朴素正则(图片懒加载 / 脚本延迟优化器)改写
|
|
10
|
+
- `layout/_plugins/index.ejs` 增加 `stellar.initPlugin` 兜底注册点:bootstrap 被第三方优化器改写/移除时不再产生 `stellar is not defined` 连锁报错,插件仍可注册运行
|
|
11
|
+
|
|
12
|
+
Full Changelog: [1.42.0...1.42.1](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.42.0...1.42.1)
|
|
13
|
+
|
|
3
14
|
## 1.42.0
|
|
4
15
|
|
|
5
16
|
> 发布日期:2026-08-16
|
package/_config.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
######## Stellar info ########
|
|
2
2
|
stellar:
|
|
3
|
-
version: '1.42.
|
|
3
|
+
version: '1.42.1'
|
|
4
4
|
homepage: 'https://xaoxuu.com/wiki/stellar/'
|
|
5
5
|
repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
|
|
6
6
|
main_css: /css/main.css
|
|
@@ -299,9 +299,9 @@ comments:
|
|
|
299
299
|
# Waline
|
|
300
300
|
# https://waline.js.org/
|
|
301
301
|
waline:
|
|
302
|
-
js: https://gcore.jsdelivr.net/npm/@waline/client@3.
|
|
303
|
-
css: https://gcore.jsdelivr.net/npm/@waline/client@3.
|
|
304
|
-
meta_css: https://gcore.jsdelivr.net/npm/@waline/client@3.
|
|
302
|
+
js: https://gcore.jsdelivr.net/npm/@waline/client@3.15.2/dist/waline.js
|
|
303
|
+
css: https://gcore.jsdelivr.net/npm/@waline/client@3.15.2/dist/waline.css
|
|
304
|
+
meta_css: https://gcore.jsdelivr.net/npm/@waline/client@3.15.2/dist/waline-meta.css
|
|
305
305
|
# Waline server address url, you should set this to your own link
|
|
306
306
|
serverURL: https://waline.vercel.app
|
|
307
307
|
# If false, comment count will only be displayed in post page, not in home page
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
if (typeof utils === 'object') return;
|
|
25
25
|
// utils 未就绪时补载;加载成功后由 utils.js 内部补跑插件队列
|
|
26
26
|
var s = document.createElement('script');
|
|
27
|
-
s.src =
|
|
27
|
+
// 用 setAttribute 赋值 src,避免被基于 `s.src = "..."` 的正则
|
|
28
|
+
// (图片懒加载 / 脚本延迟优化器)误改写
|
|
29
|
+
s.setAttribute('src', '<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>');
|
|
28
30
|
s.async = false;
|
|
29
31
|
s.onerror = function () {
|
|
30
32
|
console.error('[Stellar] 无法加载 /js/utils.js,已启用内容兜底显示(sr-fallback)');
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// 防御:bootstrap 被第三方优化器改写/移除导致 window.stellar.initPlugin 缺失时,
|
|
3
|
+
// 在此补一个等价注册点:utils 就绪时直接委托,未就绪时入队,utils 加载后自动补跑。
|
|
4
|
+
if (typeof window.stellar === 'undefined' || typeof window.stellar.initPlugin !== 'function') {
|
|
5
|
+
window.stellar = window.stellar || {};
|
|
6
|
+
window.stellar._pluginQueue = window.stellar._pluginQueue || [];
|
|
7
|
+
window.stellar.initPlugin = function (fn, name, options) {
|
|
8
|
+
if (typeof utils === 'object' && typeof utils.initPlugin === 'function') {
|
|
9
|
+
return utils.initPlugin(fn, name, options);
|
|
10
|
+
}
|
|
11
|
+
window.stellar._pluginQueue.push({ fn: fn, name: name, options: options || {} });
|
|
12
|
+
};
|
|
13
|
+
window.stellar._flushPlugins = function () {
|
|
14
|
+
var pending = window.stellar._pluginQueue.splice(0);
|
|
15
|
+
if (typeof utils !== 'object' || typeof utils.initPlugin !== 'function') return;
|
|
16
|
+
for (var i = 0; i < pending.length; i++) {
|
|
17
|
+
try {
|
|
18
|
+
utils.initPlugin(pending[i].fn, pending[i].name, pending[i].options);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error('[Stellar] 插件补注册失败:', e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
1
26
|
<%
|
|
2
27
|
function loadPlugins() {
|
|
3
28
|
var el = ''
|
package/package.json
CHANGED
|
@@ -1,56 +1,200 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* img_lazyload.js
|
|
2
|
+
* img_lazyload.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
3
|
*
|
|
4
|
+
* 懒加载强制开启:不再读取 enable 配置,no-lazy 是唯一例外。
|
|
5
|
+
* v2:改为属性感知的 <img> 标签解析,仅在标签自身范围内读取 src,
|
|
6
|
+
* 并跳过 <script>/<style>/<!-- --> 区域,避免与「去属性引号」的 HTML
|
|
7
|
+
* 压缩器(如 hexo-minify removeAttributeQuotes)组合时正则跨标签越界,
|
|
8
|
+
* 误改写页内内联脚本中的 `s.src = "..."`。
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
"use strict";
|
|
7
12
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// 1×1 透明 PNG:懒加载占位图
|
|
14
|
+
const LOADING_IMG =
|
|
15
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=";
|
|
16
|
+
|
|
17
|
+
// 解析 <img ...> 标签内部属性,返回 [{name, value, raw, start, end}]
|
|
18
|
+
function parseAttrs(inner) {
|
|
19
|
+
const attrs = [];
|
|
20
|
+
const re = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
21
|
+
let m;
|
|
22
|
+
while ((m = re.exec(inner)) !== null) {
|
|
23
|
+
attrs.push({
|
|
24
|
+
name: m[1].toLowerCase(),
|
|
25
|
+
value: m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4] !== undefined ? m[4] : null,
|
|
26
|
+
raw: m[0],
|
|
27
|
+
start: m.index,
|
|
28
|
+
end: m.index + m[0].length,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return attrs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 处理单个 <img> 标签:加 lazy 类并替换 src 为占位图 + data-src
|
|
35
|
+
function processImgTag(imgTag) {
|
|
36
|
+
// 快速跳过:整标签已含懒加载标记 / 原生 srcset / no-lazy(兼容压缩前后写法)
|
|
37
|
+
if (/data-src/gi.test(imgTag)) {
|
|
38
|
+
return imgTag;
|
|
39
|
+
}
|
|
40
|
+
if (/srcset=/gi.test(imgTag)) {
|
|
41
|
+
return imgTag;
|
|
42
|
+
}
|
|
43
|
+
if (/\bno-lazy\b/gi.test(imgTag)) {
|
|
44
|
+
return imgTag;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selfClosing = /\/>$/.test(imgTag);
|
|
48
|
+
const inner = imgTag.slice(4, selfClosing ? imgTag.length - 2 : imgTag.length - 1);
|
|
49
|
+
const attrs = parseAttrs(inner);
|
|
50
|
+
const src = attrs.find((a) => a.name === "src");
|
|
51
|
+
// 无 src / 空 src / 已是占位图(data:image)时不处理
|
|
52
|
+
if (!src || src.value == null || src.value === "" || /^data:image/i.test(src.value)) {
|
|
53
|
+
return imgTag;
|
|
13
54
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
55
|
+
|
|
56
|
+
const classAttr = attrs.find((a) => a.name === "class");
|
|
57
|
+
const oldClass = classAttr && classAttr.value != null ? classAttr.value : "";
|
|
58
|
+
const newClass = (oldClass ? oldClass + " " : "") + "lazy";
|
|
59
|
+
|
|
60
|
+
let out = "<img";
|
|
61
|
+
if (!classAttr) {
|
|
62
|
+
out += ' class="' + newClass + '"';
|
|
63
|
+
}
|
|
64
|
+
let pos = 0;
|
|
65
|
+
for (const a of attrs) {
|
|
66
|
+
out += inner.slice(pos, a.start);
|
|
67
|
+
if (a.name === "src") {
|
|
68
|
+
out += 'src="' + LOADING_IMG + '" data-src="' + src.value + '"';
|
|
69
|
+
} else if (a.name === "class") {
|
|
70
|
+
out += 'class="' + newClass + '"';
|
|
71
|
+
} else {
|
|
72
|
+
out += a.raw;
|
|
18
73
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
74
|
+
pos = a.end;
|
|
75
|
+
}
|
|
76
|
+
out += inner.slice(pos);
|
|
77
|
+
out += selfClosing ? "/>" : ">";
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 查找标签结束位置(尊重属性值内的引号,避免把 > 截断进属性值)
|
|
82
|
+
function findTagEnd(html, start) {
|
|
83
|
+
let j = start;
|
|
84
|
+
let quote = null;
|
|
85
|
+
while (j < html.length) {
|
|
86
|
+
const ch = html[j];
|
|
87
|
+
if (quote) {
|
|
88
|
+
if (ch === quote) {
|
|
89
|
+
quote = null;
|
|
90
|
+
}
|
|
91
|
+
} else if (ch === '"' || ch === "'") {
|
|
92
|
+
quote = ch;
|
|
93
|
+
} else if (ch === ">") {
|
|
94
|
+
return j;
|
|
22
95
|
}
|
|
23
|
-
|
|
24
|
-
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isTagBoundary(html, pos) {
|
|
102
|
+
if (pos >= html.length) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const ch = html[pos];
|
|
106
|
+
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === ">" || ch === "/";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 全页扫描:只在真实 <img> 标签上做懒加载,跳过脚本/样式/注释内容
|
|
110
|
+
function processHtml(html) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
let i = 0;
|
|
113
|
+
const n = html.length;
|
|
114
|
+
const lower = html.toLowerCase();
|
|
115
|
+
while (i < n) {
|
|
116
|
+
const ch = html[i];
|
|
117
|
+
if (ch !== "<") {
|
|
118
|
+
parts.push(ch);
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
25
121
|
}
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
28
|
-
|
|
122
|
+
// HTML 注释
|
|
123
|
+
if (html.startsWith("<!--", i)) {
|
|
124
|
+
const end = html.indexOf("-->", i);
|
|
125
|
+
if (end === -1) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
parts.push(html.slice(i, end + 3));
|
|
129
|
+
i = end + 3;
|
|
130
|
+
continue;
|
|
29
131
|
}
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
132
|
+
// <script>…</script>:整段跳过,避免误处理模板字符串/字符串里的 <img
|
|
133
|
+
if (lower.startsWith("<script", i) && isTagBoundary(html, i + 7)) {
|
|
134
|
+
const end = findTagEnd(html, i);
|
|
135
|
+
if (end === -1) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
parts.push(html.slice(i, end + 1));
|
|
139
|
+
i = end + 1;
|
|
140
|
+
const close = lower.indexOf("</script", i);
|
|
141
|
+
if (close === -1) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const closeEnd = html.indexOf(">", close);
|
|
145
|
+
if (closeEnd === -1) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
parts.push(html.slice(i, closeEnd + 1));
|
|
149
|
+
i = closeEnd + 1;
|
|
150
|
+
continue;
|
|
33
151
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
newClass += 'lazy';
|
|
41
|
-
if (value) {
|
|
42
|
-
return ori.replace('class="' + value, 'class="' + newClass);
|
|
43
|
-
} else {
|
|
44
|
-
return ori.replace('class="', 'class="' + newClass);
|
|
152
|
+
// <style>…</style>:整段跳过
|
|
153
|
+
if (lower.startsWith("<style", i) && isTagBoundary(html, i + 6)) {
|
|
154
|
+
const end = findTagEnd(html, i);
|
|
155
|
+
if (end === -1) {
|
|
156
|
+
break;
|
|
45
157
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
158
|
+
parts.push(html.slice(i, end + 1));
|
|
159
|
+
i = end + 1;
|
|
160
|
+
const close = lower.indexOf("</style", i);
|
|
161
|
+
if (close === -1) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const closeEnd = html.indexOf(">", close);
|
|
165
|
+
if (closeEnd === -1) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
parts.push(html.slice(i, closeEnd + 1));
|
|
169
|
+
i = closeEnd + 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// <img …>
|
|
173
|
+
if (lower.startsWith("<img", i) && isTagBoundary(html, i + 4)) {
|
|
174
|
+
const end = findTagEnd(html, i);
|
|
175
|
+
if (end === -1) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
parts.push(processImgTag(html.slice(i, end + 1)));
|
|
179
|
+
i = end + 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
parts.push(ch);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
return parts.join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function lazyProcess(htmlContent) {
|
|
189
|
+
// 短路:无 <img 的页面(如 404)无需扫描
|
|
190
|
+
if (typeof htmlContent !== "string" || !/<img/i.test(htmlContent)) {
|
|
191
|
+
return htmlContent;
|
|
192
|
+
}
|
|
193
|
+
return processHtml(htmlContent);
|
|
52
194
|
}
|
|
53
195
|
|
|
54
|
-
module.exports.processSite = function(htmlContent) {
|
|
196
|
+
module.exports.processSite = function (htmlContent) {
|
|
55
197
|
return lazyProcess.call(this, htmlContent);
|
|
56
198
|
};
|
|
199
|
+
|
|
200
|
+
module.exports.lazyProcess = lazyProcess;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
@import '../_defines/const'
|
|
2
|
+
@import '../_custom'
|
|
3
|
+
@import '../_defines/func'
|
|
4
|
+
|
|
1
5
|
.cmt-body
|
|
2
6
|
.wl-panel
|
|
3
7
|
margin: 0.5em 0 !important
|
|
@@ -66,3 +70,23 @@
|
|
|
66
70
|
:root:not([data-theme]) &
|
|
67
71
|
@media (prefers-color-scheme: dark)
|
|
68
72
|
ondark()
|
|
73
|
+
|
|
74
|
+
// override
|
|
75
|
+
.md-text .cmt-body.waline
|
|
76
|
+
p
|
|
77
|
+
font-size: var(--waline-font-size)
|
|
78
|
+
line-height: 1.5
|
|
79
|
+
margin: 0
|
|
80
|
+
.wl-reply-to *
|
|
81
|
+
line-height: 1.5
|
|
82
|
+
.wl-header label, .wl-input
|
|
83
|
+
font-size: 0.875em
|
|
84
|
+
.wl-editor
|
|
85
|
+
font-size: var(--waline-font-size)
|
|
86
|
+
font-family: $ff-body
|
|
87
|
+
.wl-content
|
|
88
|
+
.wl-emoji, .vemoji
|
|
89
|
+
height: 2.5em
|
|
90
|
+
img:not(.wl-emoji):not(.vemoji)
|
|
91
|
+
margin-top: 0.5em
|
|
92
|
+
margin-bottom: 0.5em
|