hexo-theme-stellar 1.39.0 → 1.40.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.
- package/.claude/skills/stellar-theme-dev/SKILL.md +2 -2
- package/CHANGELOG.md +44 -0
- package/_config.yml +8 -2
- package/languages/en.yml +2 -0
- package/languages/zh-CN.yml +2 -0
- package/languages/zh-TW.yml +2 -0
- package/layout/_partial/comments/artalk/script.ejs +8 -0
- package/layout/_partial/cover/post_cover.ejs +1 -1
- package/layout/_partial/head.ejs +11 -2
- package/layout/_partial/main/article/article_footer.ejs +13 -10
- package/layout/_partial/main/navbar/article_banner.ejs +7 -0
- package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
- package/layout/_partial/main/navbar/nav_tabs_blog.ejs +6 -3
- package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +1 -1
- package/layout/_partial/main/pin_slider.ejs +11 -12
- package/layout/_partial/main/post_list/post_card.ejs +24 -13
- package/layout/_partial/sidebar/index_leftbar.ejs +2 -2
- package/layout/_partial/widgets/markdown.ejs +1 -1
- package/layout/_partial/widgets/toc.ejs +8 -1
- package/layout/categories.ejs +18 -8
- package/layout/index.ejs +67 -11
- package/layout/page.ejs +5 -1
- package/package.json +5 -3
- package/scripts/commands/new-note.js +2 -2
- package/scripts/helpers/category_tree.js +11 -0
- package/scripts/helpers/escape_html.js +9 -0
- package/scripts/helpers/json_ld.js +5 -0
- package/scripts/helpers/mdrender.js +29 -0
- package/scripts/helpers/reading_time.js +12 -0
- package/scripts/helpers/related_posts.js +9 -5
- package/scripts/lib/category_tree.js +26 -0
- package/scripts/lib/mdrender_html.js +97 -0
- package/scripts/lib/reading_time.js +16 -0
- package/scripts/lib/wiki_readme.js +57 -0
- package/scripts/tags/index.js +2 -0
- package/scripts/tags/lib/md.js +17 -9
- package/scripts/tags/lib/table.js +22 -0
- package/scripts/tags/lib/timeline.js +9 -9
- package/scripts/tags/lib/tip.js +23 -0
- package/source/css/_common/device.styl +3 -0
- package/source/css/_components/list.styl +11 -5
- package/source/css/_components/pages/archives.styl +7 -3
- package/source/css/_components/partial/bread-nav.styl +9 -1
- package/source/css/_components/sidebar/search.styl +6 -4
- package/source/css/_components/sidebar/sidebar.styl +10 -7
- package/source/css/_components/tag-plugins/table.styl +37 -0
- package/source/css/_components/tag-plugins/tip.styl +27 -0
- package/source/css/_custom.styl +1 -1
- package/source/css/_defines/func.styl +19 -13
- package/source/css/_defines/theme_base.styl +3 -0
- package/source/js/main.js +92 -1
- package/source/js/services/mdrender.js +116 -7
- package/source/js/services/memos.js +60 -8
- package/source/js/services/waline_latest_comment.js +3 -1
|
@@ -29,11 +29,11 @@ description: 主题仓库(hexo-theme-stellar)开发全流程:方案 →
|
|
|
29
29
|
命令按需执行:
|
|
30
30
|
|
|
31
31
|
- `scripts/` 有改动 → 必须在主工程(xaoxuu.com)执行 `npm run g` 全量验证(已含 `hexo clean && hexo generate && npx gulp minify`);`npm run s` 是按需渲染,不能替代。
|
|
32
|
-
- 新增/修改纯函数 → 补充单测并跑 `npm run check`(lint + 单测 + 知识库硬事实核查)。
|
|
32
|
+
- 新增/修改纯函数 → 补充单测并跑 `npm run check`(lint + 单测 + 依赖声明检查 + 知识库硬事实核查)。
|
|
33
33
|
- 知识库有改动 → `python3 docs/knowledge/tools/verify.py` 硬事实核查。
|
|
34
34
|
- UI 方面(样式、模板、前端交互等)改动量不大时无需自检流程,除非用户明确要求。
|
|
35
35
|
- 检查所有受影响页面类型(首页、文章页、Wiki 页等),验证结果记录在方案目录 `checklist.md`。
|
|
36
|
-
-
|
|
36
|
+
- 完成条件:应执行的命令全部通过;新增 `require` 均已声明或为 Node 内置模块(test/ 禁止幽灵依赖);首页、文章页、Wiki 页等受影响页面类型均检查通过。
|
|
37
37
|
|
|
38
38
|
### 4. 提交
|
|
39
39
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.40.0
|
|
4
|
+
|
|
5
|
+
> 发布日期:2026-08-14
|
|
6
|
+
|
|
7
|
+
### 新功能
|
|
8
|
+
- 文章页新增字数与预计阅读时长显示(`article.reading_time`,默认关闭)
|
|
9
|
+
- 文章卡片新增标签展示(最多 5 个,`article.card_tags`,默认关闭)
|
|
10
|
+
- 新增表格标签 `{% table style:scroll|wrap|compact %}`,支持滚动/换行/紧凑三种表格样式
|
|
11
|
+
- 新增气泡注解标签 `{% tip text:... %}词句{% endtip %}`,桌面 hover、移动端点击显示
|
|
12
|
+
- 远程 Markdown 渲染抽象:`{% md %}` 标签新增可选 `wrap` 参数;wiki 项目配置 `repo` 后可直接以仓库 README 作为首页正文
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
- wiki 项目描述作为页面描述(OG / JSON-LD)的兜底,修复 SEO 描述缺失的问题
|
|
16
|
+
- 声明 hexo-pagination / hexo-util 依赖并新增依赖声明检查门禁,修复幽灵依赖导致的构建问题
|
|
17
|
+
- 时间线节点标题(header)不再渲染 markdown 的问题(#401)
|
|
18
|
+
- 分类页三级及以上分类无法正确展示的问题,改为基于 parent 递归构建嵌套树(#564)
|
|
19
|
+
- 亮暗切换后 topbar 与左栏颜色不同步的问题:navbar/sidebar 暗色样式统一跟随 `data-theme` 开关(#593、#663)
|
|
20
|
+
- iOS Safari 下右下角悬浮控件在玻璃椭圆框中不对齐的问题(#599)
|
|
21
|
+
- artalk 邮件通知链接携带 `?atk_*` 参数导致目录定位失效的问题(#598)
|
|
22
|
+
- waline 最近评论接口返回结构兼容(数组 / `{ data: [...] }`)(#630)
|
|
23
|
+
- memos 新版 v1 接口识别与渲染适配,识别失败仍回退 feature 兜底(#668)
|
|
24
|
+
- new-note 命令生成 front-matter 的 date 对齐为 `YYYY-MM-DD HH:mm`(#594)
|
|
25
|
+
|
|
26
|
+
### 升级注意(配置变更与破坏性改动)
|
|
27
|
+
- 新增 `article.reading_time`(默认 `false`)与 `article.card_tags`(默认 `false`),需要时设为 `true`
|
|
28
|
+
- 新增 `table`、`tip` 两个标签插件;`table` 默认使用 `scroll` 样式
|
|
29
|
+
|
|
30
|
+
Full Changelog: [1.39.1...1.40.0](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.39.1...1.40.0)
|
|
31
|
+
|
|
32
|
+
## 1.39.1
|
|
33
|
+
|
|
34
|
+
> 发布日期:2026-08-13
|
|
35
|
+
|
|
36
|
+
### 新功能
|
|
37
|
+
- 置顶文章新增平铺样式配置 `article.pin_style: flat`:首页第一页不再渲染置顶轮播,置顶文章按与轮播一致的排序权重在文章列表靠前平铺展示
|
|
38
|
+
|
|
39
|
+
### 修复
|
|
40
|
+
- 用户内容统一 HTML 转义并移除圆点 aria-label,修复标题含引号导致构建失败
|
|
41
|
+
|
|
42
|
+
### 升级注意(配置变更与破坏性改动)
|
|
43
|
+
- 新增 `article.pin_style`(`carousel` / `flat`,默认 `carousel`):默认行为不变;设置 `flat` 后首页置顶文章改为平铺样式展示
|
|
44
|
+
|
|
45
|
+
Full Changelog: [1.39.0...1.39.1](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.39.0...1.39.1)
|
|
46
|
+
|
|
3
47
|
## 1.39.0
|
|
4
48
|
|
|
5
49
|
> 发布日期:2026-08-13
|
package/_config.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
######## Stellar info ########
|
|
2
2
|
stellar:
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.40.0'
|
|
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
|
|
@@ -180,6 +180,8 @@ notebook:
|
|
|
180
180
|
|
|
181
181
|
######## Article ########
|
|
182
182
|
article:
|
|
183
|
+
# 置顶文章样式:carousel = 轮播(默认), flat = 平铺(不渲染轮播,置顶文章在首页文章列表靠前展示,排序规则与轮播一致)
|
|
184
|
+
pin_style: carousel
|
|
183
185
|
# 文章类型,决定布局风格,可以在 front-matter/topic/wiki 中覆盖此参数
|
|
184
186
|
type: tech # tech: 默认技术类文章, story: 图文类文章,文字和段落间增距大
|
|
185
187
|
# 缩进两个文字宽度,适合文字类博客,可以在 front-matter/topic/wiki 中覆盖此参数
|
|
@@ -204,6 +206,10 @@ article:
|
|
|
204
206
|
related_posts:
|
|
205
207
|
enable: false
|
|
206
208
|
max_count: 5
|
|
209
|
+
# 文章页是否显示字数和预计阅读时长(#302)
|
|
210
|
+
reading_time: false
|
|
211
|
+
# 文章卡片是否显示标签(最多 5 个,#580)
|
|
212
|
+
card_tags: false
|
|
207
213
|
|
|
208
214
|
search:
|
|
209
215
|
service: local_search # local_search, algolia_search, todo...
|
|
@@ -674,7 +680,7 @@ style:
|
|
|
674
680
|
image-l: 24px # 非技术类文章插图圆角
|
|
675
681
|
image: 16px # 技术类文章插图圆角
|
|
676
682
|
image-s: 8px # 评论区图片圆角(部分评论支持)
|
|
677
|
-
corner-shape: superellipse(1.
|
|
683
|
+
corner-shape: superellipse(1.25) # 连续曲率圆角(仅 Chromium 139+ 支持,其余浏览器自动回退普通圆角):superellipse(1.x) 开启,round 关闭
|
|
678
684
|
color:
|
|
679
685
|
# 主题色配置(不会根据明暗动态调整,请设置为通用的颜色)
|
|
680
686
|
theme: 'hsl(192 98% 55%)' # 主题色
|
package/languages/en.yml
CHANGED
package/languages/zh-CN.yml
CHANGED
package/languages/zh-TW.yml
CHANGED
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
<% } %>
|
|
41
41
|
});
|
|
42
|
+
// artalk 邮件通知链接携带 ?atk_comment=... 等查询参数,
|
|
43
|
+
// 其 hash 监听会与主题目录定位冲突(#598):
|
|
44
|
+
// 初始化并完成评论定位后清理查询参数,避免后续目录/锚点跳转被干扰。
|
|
45
|
+
if (window.location.search.indexOf('atk_') !== -1) {
|
|
46
|
+
window.setTimeout(function () {
|
|
47
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.hash);
|
|
48
|
+
}, 300);
|
|
49
|
+
}
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -28,7 +28,7 @@ function div() {
|
|
|
28
28
|
}
|
|
29
29
|
el += '<div class="l_cover post' + scrollreveal(' ') + '">';
|
|
30
30
|
el += '<div class="cover">';
|
|
31
|
-
el += '<div class="lazy img bg" data-bg="' + url + '"></div>';
|
|
31
|
+
el += '<div class="lazy img bg" data-bg="' + escape_html(url) + '"></div>';
|
|
32
32
|
el += '</div>';
|
|
33
33
|
el += '</div>';
|
|
34
34
|
return el;
|
package/layout/_partial/head.ejs
CHANGED
|
@@ -23,14 +23,17 @@ function generate_description() {
|
|
|
23
23
|
if (theme.open_graph && theme.open_graph.enable) {
|
|
24
24
|
return '';
|
|
25
25
|
}
|
|
26
|
+
if (page.description) {
|
|
27
|
+
return '<meta name="description" content="' + truncate(strip_html(page.description), {length: 150}) + '">';
|
|
28
|
+
}
|
|
26
29
|
if (page.wiki) {
|
|
27
30
|
const proj = theme.wiki.tree[page.wiki]
|
|
28
31
|
if (proj?.description) {
|
|
29
32
|
return '<meta name="description" content="' + proj.description + '">'
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
|
-
if (page.
|
|
33
|
-
return '<meta name="description" content="' + truncate(strip_html(page.
|
|
35
|
+
if (page.excerpt || page.content) {
|
|
36
|
+
return '<meta name="description" content="' + truncate(strip_html(page.excerpt || page.content), {length: 150}) + '">';
|
|
34
37
|
}
|
|
35
38
|
return '<meta name="description" content="' + config.description + '">';
|
|
36
39
|
}
|
|
@@ -66,6 +69,12 @@ function og_args() {
|
|
|
66
69
|
if (page.layout == 'post' && page.cover) {
|
|
67
70
|
args.twitter_card = 'summary_large_image';
|
|
68
71
|
}
|
|
72
|
+
if (page.wiki && !page.description) {
|
|
73
|
+
const proj = theme.wiki.tree[page.wiki];
|
|
74
|
+
if (proj && proj.description) {
|
|
75
|
+
args.description = proj.description;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
args.image = config.avatar || (config.email ? gravatar(config.email) : null);
|
|
70
79
|
return Object.assign(args, page.open_graph);
|
|
71
80
|
}
|
|
@@ -98,6 +98,9 @@ function layoutDiv() {
|
|
|
98
98
|
showSharePlugin = true
|
|
99
99
|
}
|
|
100
100
|
if (theme.article.share && showSharePlugin) {
|
|
101
|
+
function urlenc(v) {
|
|
102
|
+
return encodeURIComponent(v == null ? '' : String(v))
|
|
103
|
+
}
|
|
101
104
|
function socialButtons() {
|
|
102
105
|
var el = ''
|
|
103
106
|
theme.article.share.forEach((item, i) => {
|
|
@@ -109,20 +112,20 @@ function layoutDiv() {
|
|
|
109
112
|
if (item == 'wechat') {
|
|
110
113
|
el += ' onclick="util.toggle("qrcode-wechat")"';
|
|
111
114
|
} else if (item == 'weibo') {
|
|
112
|
-
el += ' href="https://service.weibo.com/share/share.php?url=' + page.permalink;
|
|
113
|
-
el += '&title=' + page.title + ' - ' + config.title;
|
|
115
|
+
el += ' href="https://service.weibo.com/share/share.php?url=' + urlenc(page.permalink);
|
|
116
|
+
el += '&title=' + urlenc(page.title + ' - ' + config.title);
|
|
114
117
|
if (page.layout == 'post' && page.cover) {
|
|
115
|
-
el += '&pics=' + page.cover;
|
|
118
|
+
el += '&pics=' + urlenc(page.cover);
|
|
116
119
|
} else if (page.wiki && page.icon) {
|
|
117
|
-
el += '&pics=' + page.icon;
|
|
120
|
+
el += '&pics=' + urlenc(page.icon);
|
|
118
121
|
}
|
|
119
|
-
el += '&summary=' + truncate(page.description || strip_html(page.excerpt || page.content), {length: 120});
|
|
122
|
+
el += '&summary=' + urlenc(truncate(page.description || strip_html(page.excerpt || page.content), {length: 120}));
|
|
120
123
|
el += '"';
|
|
121
124
|
} else if (item == 'email') {
|
|
122
|
-
el += ' href="mailto:?subject=' + page.title + ' - ' + config.title + '&'
|
|
123
|
-
el += 'body=' + page.permalink + '"';
|
|
125
|
+
el += ' href="mailto:?subject=' + urlenc(page.title + ' - ' + config.title) + '&'
|
|
126
|
+
el += 'body=' + urlenc(page.permalink) + '"';
|
|
124
127
|
} else if (item == 'link') {
|
|
125
|
-
el += ' onclick="util.copy("copy-link", "' + __('message.copied') + '")"';
|
|
128
|
+
el += ' onclick="util.copy("copy-link", "' + escape_html(__('message.copied')) + '")"';
|
|
126
129
|
}
|
|
127
130
|
el += '>';
|
|
128
131
|
el += icon(`share:${item}`)
|
|
@@ -135,7 +138,7 @@ function layoutDiv() {
|
|
|
135
138
|
if (theme.article.share.includes('wechat')) {
|
|
136
139
|
return `
|
|
137
140
|
<div class="qrcode" id="qrcode-wechat" style="opacity:0;height:0">
|
|
138
|
-
<img src="https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${page.permalink}"/>
|
|
141
|
+
<img src="https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${urlenc(page.permalink)}"/>
|
|
139
142
|
</div>
|
|
140
143
|
`
|
|
141
144
|
} else {
|
|
@@ -146,7 +149,7 @@ function layoutDiv() {
|
|
|
146
149
|
<section id="share">
|
|
147
150
|
<div class="header"><span>${__('meta.share')}</span></div>
|
|
148
151
|
<div class="body">
|
|
149
|
-
<div class="link"><input class="copy-area" readonly="true" id="copy-link" value="${page.permalink}" /></div>
|
|
152
|
+
<div class="link"><input class="copy-area" readonly="true" id="copy-link" value="${escape_html(page.permalink)}" /></div>
|
|
150
153
|
<div class="social-wrap dis-select">${socialButtons()}</div>
|
|
151
154
|
${qrcode()}
|
|
152
155
|
</div>
|
|
@@ -41,6 +41,13 @@ function layoutBreadcrumb() {
|
|
|
41
41
|
} else {
|
|
42
42
|
el += partial('breadcrumb/page')
|
|
43
43
|
}
|
|
44
|
+
// 字数和预计阅读时长,置于面包屑行右侧(#302)
|
|
45
|
+
if (page.layout == 'post' && theme.article.reading_time) {
|
|
46
|
+
var _wc = word_count(page)
|
|
47
|
+
if (_wc > 0) {
|
|
48
|
+
el += `<span class="cap reading right">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
44
51
|
// end 3.left.top
|
|
45
52
|
el += `</div>`
|
|
46
53
|
// 3.left.bottom
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<%
|
|
2
2
|
function layoutDiv() {
|
|
3
3
|
var el = '';
|
|
4
|
-
|
|
4
|
+
// 置顶文章样式为平铺时不渲染轮播,置顶文章改由首页文章列表靠前展示
|
|
5
|
+
if (theme.article?.pin_style !== 'flat') {
|
|
6
|
+
el += partial('_partial/main/pin_slider', {type: 'post'});
|
|
7
|
+
}
|
|
5
8
|
el += '<div class="navbar top">';
|
|
6
9
|
el += '<div class="navbar-blur">';
|
|
7
10
|
el += '<div class="navbar-container">';
|
|
@@ -14,7 +17,7 @@ function layoutDiv() {
|
|
|
14
17
|
|
|
15
18
|
if (site.categories && site.categories.length > 0) {
|
|
16
19
|
if (page.category) {
|
|
17
|
-
el += '<a class="active" href="' + pretty_url(config.category_dir) + '">' + __("btn.category") + __("symbol.colon") + page.category + '</a>';
|
|
20
|
+
el += '<a class="active" href="' + pretty_url(config.category_dir) + '">' + __("btn.category") + __("symbol.colon") + escape_html(page.category) + '</a>';
|
|
18
21
|
} else if (page.layout == "categories") {
|
|
19
22
|
el += '<a class="active" href="' + pretty_url(config.category_dir) + '">' + __("btn.categories") + '</a>';
|
|
20
23
|
} else {
|
|
@@ -24,7 +27,7 @@ function layoutDiv() {
|
|
|
24
27
|
|
|
25
28
|
if (site.tags && site.tags.length > 0) {
|
|
26
29
|
if (page.tag) {
|
|
27
|
-
el += '<a class="active" href="' + pretty_url(config.tag_dir) + '">' + __("btn.tag") + __("symbol.colon") + page.tag + '</a>';
|
|
30
|
+
el += '<a class="active" href="' + pretty_url(config.tag_dir) + '">' + __("btn.tag") + __("symbol.colon") + escape_html(page.tag) + '</a>';
|
|
28
31
|
} else if (page.layout == "tags") {
|
|
29
32
|
el += '<a class="active" href="' + pretty_url(config.tag_dir) + '">' + __("btn.tags") + '</a>';
|
|
30
33
|
} else {
|
|
@@ -23,7 +23,7 @@ function layoutDiv() {
|
|
|
23
23
|
if (tag.name && tag.name.length > 0 && page.tagName === tag.name) {
|
|
24
24
|
el += ' class="active"';
|
|
25
25
|
}
|
|
26
|
-
el += ' href="' + pretty_url(tag.path) + '">' + tag.name + '</a>';
|
|
26
|
+
el += ' href="' + escape_html(pretty_url(tag.path)) + '">' + escape_html(tag.name) + '</a>';
|
|
27
27
|
el += '</a>';
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -69,18 +69,18 @@ function postSlide(post) {
|
|
|
69
69
|
var headline = (post.poster != null && post.poster.headline != null && post.poster.headline.length > 0) ? post.poster.headline : post.title
|
|
70
70
|
var subtitle = postSubtitle(post)
|
|
71
71
|
var el = ''
|
|
72
|
-
el += '<a class="pin-slide post-slide' + (noCover ? ' no-cover' : '') + '" href="' + pretty_url(post.link || post.path) + '"'
|
|
72
|
+
el += '<a class="pin-slide post-slide' + (noCover ? ' no-cover' : '') + '" href="' + escape_html(pretty_url(post.link || post.path)) + '"'
|
|
73
73
|
if (!noCover) {
|
|
74
|
-
el += ' style="--pin-cover-url:url(\'' + post.cover.replace(/'/g, '%27') + '\')"'
|
|
74
|
+
el += ' style="--pin-cover-url:url(\'' + escape_html(post.cover.replace(/'/g, '%27')) + '\')"'
|
|
75
75
|
}
|
|
76
76
|
el += '>'
|
|
77
77
|
if (!noCover) {
|
|
78
|
-
el += '<img class="pin-slide-bg" src="' + post.cover + '" alt=""/>'
|
|
78
|
+
el += '<img class="pin-slide-bg" src="' + escape_html(post.cover) + '" alt=""/>'
|
|
79
79
|
}
|
|
80
80
|
el += '<div class="pin-slide-text">'
|
|
81
|
-
el += '<div class="pin-slide-headline">' + headline + '</div>'
|
|
81
|
+
el += '<div class="pin-slide-headline">' + escape_html(headline) + '</div>'
|
|
82
82
|
if (subtitle.length > 0) {
|
|
83
|
-
el += '<div class="pin-slide-caption">' + subtitle + '</div>'
|
|
83
|
+
el += '<div class="pin-slide-caption">' + escape_html(subtitle) + '</div>'
|
|
84
84
|
}
|
|
85
85
|
el += '</div>'
|
|
86
86
|
el += '</a>'
|
|
@@ -90,7 +90,7 @@ function postSlide(post) {
|
|
|
90
90
|
function projectChips(tags) {
|
|
91
91
|
var caps = ''
|
|
92
92
|
if (tags && tags.length > 0) {
|
|
93
|
-
caps += '<span class="pin-slide-chip">' + tags.join('</span><span class="pin-slide-chip">') + '</span>'
|
|
93
|
+
caps += '<span class="pin-slide-chip">' + tags.map(function(t) { return escape_html(t) }).join('</span><span class="pin-slide-chip">') + '</span>'
|
|
94
94
|
}
|
|
95
95
|
return caps
|
|
96
96
|
}
|
|
@@ -100,14 +100,14 @@ function wikiSlide(proj) {
|
|
|
100
100
|
var title = proj.title || proj.name || proj.id
|
|
101
101
|
var excerpt = proj.description || ''
|
|
102
102
|
var el = ''
|
|
103
|
-
el += '<a class="pin-slide" href="' + pretty_url(proj.homepage?.path || '/') + '">'
|
|
104
|
-
el += '<img class="pin-slide-bg" src="' + img + '" alt=""/>'
|
|
103
|
+
el += '<a class="pin-slide" href="' + escape_html(pretty_url(proj.homepage?.path || '/')) + '">'
|
|
104
|
+
el += '<img class="pin-slide-bg" src="' + escape_html(img) + '" alt=""/>'
|
|
105
105
|
el += '<div class="pin-slide-mask"></div>'
|
|
106
106
|
el += '<div class="pin-slide-info">'
|
|
107
107
|
el += '<div class="pin-slide-caps">' + projectChips(proj.tags) + '</div>'
|
|
108
|
-
el += '<div class="pin-slide-title">' + title + '</div>'
|
|
108
|
+
el += '<div class="pin-slide-title">' + escape_html(title) + '</div>'
|
|
109
109
|
if (excerpt.length > 0) {
|
|
110
|
-
el += '<div class="pin-slide-excerpt">' + excerpt + '</div>'
|
|
110
|
+
el += '<div class="pin-slide-excerpt">' + escape_html(excerpt) + '</div>'
|
|
111
111
|
}
|
|
112
112
|
el += '</div>'
|
|
113
113
|
el += '</a>'
|
|
@@ -125,8 +125,7 @@ if (pinItems.length > 0) {
|
|
|
125
125
|
slidesHtml += postSlide(item)
|
|
126
126
|
}
|
|
127
127
|
if (pinItems.length > 1) {
|
|
128
|
-
|
|
129
|
-
dotsHtml += '<button type="button" class="pin-slider-dot' + (si == 0 ? ' active' : '') + '" data-index="' + si + '" aria-label="' + label + '"></button>'
|
|
128
|
+
dotsHtml += '<button type="button" class="pin-slider-dot' + (si == 0 ? ' active' : '') + '" data-index="' + si + '"></button>'
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
var groupKey = pinType == 'wiki' ? 'wiki' : 'post'
|
|
@@ -39,14 +39,14 @@ function div_default() {
|
|
|
39
39
|
}
|
|
40
40
|
if (cover_url) {
|
|
41
41
|
el += '<div class="post-cover">';
|
|
42
|
-
el += '<img src="' + cover_url + '"/>';
|
|
42
|
+
el += '<img src="' + escape_html(cover_url) + '"/>';
|
|
43
43
|
el += '</div>';
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// 标题
|
|
48
48
|
el += '<h2 class="post-title">';
|
|
49
|
-
el += post.title ? post.title : date(post.date, config.date_format);
|
|
49
|
+
el += post.title ? escape_html(post.title) : date(post.date, config.date_format);
|
|
50
50
|
el += '</h2>';
|
|
51
51
|
|
|
52
52
|
// 摘要
|
|
@@ -57,11 +57,11 @@ function div_default() {
|
|
|
57
57
|
el += '">';
|
|
58
58
|
el += '<p>';
|
|
59
59
|
if (post.excerpt) {
|
|
60
|
-
el += strip_html(post.excerpt);
|
|
60
|
+
el += escape_html(strip_html(post.excerpt));
|
|
61
61
|
} else if (post.description) {
|
|
62
|
-
el += post.description;
|
|
62
|
+
el += escape_html(post.description);
|
|
63
63
|
} else if (post.content && theme.article.auto_excerpt > 0) {
|
|
64
|
-
el += truncate(strip_html(post.content), {length: theme.article.auto_excerpt});
|
|
64
|
+
el += escape_html(truncate(strip_html(post.content), {length: theme.article.auto_excerpt}));
|
|
65
65
|
}
|
|
66
66
|
el += '</p>';
|
|
67
67
|
el += '</div>';
|
|
@@ -87,12 +87,23 @@ function div_default() {
|
|
|
87
87
|
let lastCat = cats.pop();
|
|
88
88
|
el += '<span class="cap breadcrumb"' + category_color(lastCat) + '>';
|
|
89
89
|
el += icon('default:category')
|
|
90
|
-
el += `<span>${str}</span>`
|
|
90
|
+
el += `<span>${escape_html(str)}</span>`
|
|
91
91
|
el += '</span>';
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
//
|
|
95
|
+
// 置顶文章由轮播(默认)或首页列表靠前展示(flat 平铺)承担,卡片内不显示 pin 图标。
|
|
96
|
+
// 文章标签(最多 5 个,#580)
|
|
97
|
+
if (theme.article.card_tags && post.tags && post.tags.length > 0) {
|
|
98
|
+
el += '<span class="card-tags">';
|
|
99
|
+
post.tags.forEach((tag, i) => {
|
|
100
|
+
if (i >= 5) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
el += `<span class="cap tag-chip">#${escape_html(tag.name)}</span>`;
|
|
104
|
+
});
|
|
105
|
+
el += '</span>';
|
|
106
|
+
}
|
|
96
107
|
el += '</div>';
|
|
97
108
|
el += '</article>';
|
|
98
109
|
return el;
|
|
@@ -107,22 +118,22 @@ function div_photo() {
|
|
|
107
118
|
position = 'bottom';
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
|
-
el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + obj.image.replace(/'/g, '%27') + '\')">';
|
|
111
|
-
el += '<img src="' + obj.image + '"/>';
|
|
121
|
+
el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + escape_html(obj.image.replace(/'/g, '%27')) + '\')">';
|
|
122
|
+
el += '<img src="' + escape_html(obj.image) + '"/>';
|
|
112
123
|
if (position.length > 0) {
|
|
113
124
|
el += '<div class="cover-info" position="' + position + '"';
|
|
114
125
|
if (obj.color) {
|
|
115
|
-
el += 'style="color:' + obj.color + '"';
|
|
126
|
+
el += 'style="color:' + escape_html(obj.color) + '"';
|
|
116
127
|
}
|
|
117
128
|
el += '>';
|
|
118
129
|
if (obj.topic) {
|
|
119
|
-
el += '<div class="text topic">' + obj.topic + '</div>';
|
|
130
|
+
el += '<div class="text topic">' + escape_html(obj.topic) + '</div>';
|
|
120
131
|
}
|
|
121
132
|
if (obj.headline) {
|
|
122
|
-
el += '<div class="text headline">' + obj.headline + '</div>';
|
|
133
|
+
el += '<div class="text headline">' + escape_html(obj.headline) + '</div>';
|
|
123
134
|
}
|
|
124
135
|
if (obj.caption) {
|
|
125
|
-
el += '<div class="text caption">' + obj.caption + '</div>';
|
|
136
|
+
el += '<div class="text caption">' + escape_html(obj.caption) + '</div>';
|
|
126
137
|
}
|
|
127
138
|
el += '</div>';
|
|
128
139
|
|
|
@@ -131,10 +131,10 @@ function layoutFooterDiv() {
|
|
|
131
131
|
if (item.icon && (item.url || item.onclick)) {
|
|
132
132
|
el += '<a class="social"';
|
|
133
133
|
if (item.title) {
|
|
134
|
-
el += ' title="' + item.title + '"';
|
|
134
|
+
el += ' title="' + escape_html(item.title) + '"';
|
|
135
135
|
}
|
|
136
136
|
if (item.url) {
|
|
137
|
-
el += ' href="' + url_for(item.url) + '"';
|
|
137
|
+
el += ' href="' + escape_html(url_for(item.url)) + '"';
|
|
138
138
|
if (item.url.includes('://')) {
|
|
139
139
|
el += ' target="_blank" rel="external nofollow noopener noreferrer"';
|
|
140
140
|
} else {
|
|
@@ -13,7 +13,7 @@ function layoutDiv() {
|
|
|
13
13
|
el += markdown(item.content);
|
|
14
14
|
}
|
|
15
15
|
if (item.src) {
|
|
16
|
-
el +=
|
|
16
|
+
el += mdrender_html(item.src)
|
|
17
17
|
}
|
|
18
18
|
if (item.linklist) {
|
|
19
19
|
el += partial('components/linklist', {item: item.linklist})
|
|
@@ -38,9 +38,12 @@ function layoutToc(fallback) {
|
|
|
38
38
|
function layoutDiv(fallback) {
|
|
39
39
|
const tocBody = layoutTocBody()
|
|
40
40
|
const hasToc = tocBody.trim().length > 0
|
|
41
|
+
// 远程 md 内容由客户端渲染,构建期正文无标题:
|
|
42
|
+
// 预留空 TOC 容器,渲染完成后由 mdrender.js 填充
|
|
43
|
+
const hasRemoteMd = has_remote_md(page)
|
|
41
44
|
// 页面是否实际渲染评论区,由 _partial/comments/layout.ejs 标记
|
|
42
45
|
const hasComments = page.cmt_rendered === true
|
|
43
|
-
if (!hasToc && !hasComments) {
|
|
46
|
+
if (!hasToc && !hasRemoteMd && !hasComments) {
|
|
44
47
|
return ''
|
|
45
48
|
}
|
|
46
49
|
var el = ''
|
|
@@ -50,6 +53,10 @@ function layoutDiv(fallback) {
|
|
|
50
53
|
el += `<div class="widget-body">`
|
|
51
54
|
el += tocBody
|
|
52
55
|
el += `</div>`
|
|
56
|
+
} else if (hasRemoteMd) {
|
|
57
|
+
el += layoutTocHeader()
|
|
58
|
+
el += `<div class="widget-body">`
|
|
59
|
+
el += `</div>`
|
|
53
60
|
}
|
|
54
61
|
el += `<div class="widget-footer">`
|
|
55
62
|
el += `<a class="top" onclick="util.scrollTop()">`
|
package/layout/categories.ejs
CHANGED
|
@@ -10,14 +10,24 @@ if (page.menu_id == undefined) {
|
|
|
10
10
|
<%- partial('_partial/main/navbar/nav_tabs_blog') %>
|
|
11
11
|
<div class='post-list'>
|
|
12
12
|
<article class='<%- scrollreveal("") %>' id='cats'>
|
|
13
|
-
<%
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
<%
|
|
14
|
+
function renderCategoryTree(nodes) {
|
|
15
|
+
var el = ''
|
|
16
|
+
nodes.forEach(function(node) {
|
|
17
|
+
el += `<div class="cat-item">`
|
|
18
|
+
el += `<a class="cat${node.parentId ? ' child' : ''}" href="${pretty_url(node.path)}">`
|
|
19
|
+
el += `<span class="name">${icon('default:category')}${escape_html(node.name)}</span>`
|
|
20
|
+
el += `<span class="badge">(${node.count})</span>`
|
|
21
|
+
el += `</a>`
|
|
22
|
+
if (node.children && node.children.length > 0) {
|
|
23
|
+
el += `<div class="cat-children">${renderCategoryTree(node.children)}</div>`
|
|
24
|
+
}
|
|
25
|
+
el += `</div>`
|
|
26
|
+
})
|
|
27
|
+
return el
|
|
28
|
+
}
|
|
29
|
+
%>
|
|
30
|
+
<%- renderCategoryTree(category_tree(site.categories)) %>
|
|
21
31
|
</article>
|
|
22
32
|
</div>
|
|
23
33
|
<% } %>
|
package/layout/index.ejs
CHANGED
|
@@ -29,8 +29,49 @@ if (homeCmt && homeCmt !== false && is_home_first_page()) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
var
|
|
32
|
+
// 置顶文章样式:carousel = 轮播(默认),flat = 平铺(首页列表靠前展示)
|
|
33
|
+
var pinStyle = theme.article?.pin_style || 'carousel'
|
|
34
|
+
var pinFlat = pinStyle === 'flat'
|
|
35
|
+
|
|
36
|
+
// 与 pin_slider.ejs 保持一致的置顶判定与排序权重
|
|
37
|
+
function pinValue(post) {
|
|
38
|
+
return post.pin != null ? post.pin : post.sticky
|
|
39
|
+
}
|
|
40
|
+
function isPinned(post) {
|
|
41
|
+
var v = pinValue(post)
|
|
42
|
+
return v !== false && v != null
|
|
43
|
+
}
|
|
44
|
+
function pinWeight(post) {
|
|
45
|
+
var v = pinValue(post)
|
|
46
|
+
if (v === true) {
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
var n = Number(v)
|
|
50
|
+
return isNaN(n) ? 0 : n
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 首页第一页渲染置顶轮播时,列表不再重复展示置顶文章(carousel 模式)
|
|
54
|
+
var pinSliderActive = is_home_first_page() && !pinFlat
|
|
55
|
+
|
|
56
|
+
// flat 模式:首页第一页收集全部置顶文章,按轮播顺序置顶展示
|
|
57
|
+
var flatPins = []
|
|
58
|
+
if (pinFlat && is_home_first_page()) {
|
|
59
|
+
var allPosts = site.posts.toArray() || []
|
|
60
|
+
for (var fi = 0; fi < allPosts.length; fi++) {
|
|
61
|
+
var fpost = allPosts[fi]
|
|
62
|
+
if (fpost.indexing === false) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (isPinned(fpost)) {
|
|
66
|
+
flatPins.push({post: fpost, index: fi})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
flatPins.sort(function(a, b) {
|
|
70
|
+
var d = pinWeight(b.post) - pinWeight(a.post)
|
|
71
|
+
// 权重相同保持原顺序(与轮播一致)
|
|
72
|
+
return d !== 0 ? d : a.index - b.index
|
|
73
|
+
})
|
|
74
|
+
}
|
|
34
75
|
|
|
35
76
|
function layout_post_card(layout, post, content) {
|
|
36
77
|
var el = '';
|
|
@@ -49,17 +90,32 @@ function layout_post_card(layout, post, content) {
|
|
|
49
90
|
function layout_post_list(partial) {
|
|
50
91
|
var el = '';
|
|
51
92
|
el += `<div class="post-list post">`;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
if (flatPins.length > 0) {
|
|
94
|
+
var pinnedPaths = {}
|
|
95
|
+
flatPins.forEach(function(item) {
|
|
96
|
+
pinnedPaths[item.post.path] = true
|
|
97
|
+
})
|
|
98
|
+
flatPins.forEach(function(item) {
|
|
99
|
+
el += layout_post_card('post', item.post, partial(item.post));
|
|
100
|
+
})
|
|
101
|
+
page.posts.each(function(post){
|
|
102
|
+
if (post.indexing != false && !pinnedPaths[post.path]) {
|
|
103
|
+
el += layout_post_card('post', post, partial(post));
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
} else {
|
|
107
|
+
page.posts.each(function(post){
|
|
108
|
+
if (post.indexing != false) {
|
|
109
|
+
if (pinSliderActive) {
|
|
110
|
+
var pinVal = pinValue(post)
|
|
111
|
+
if (pinVal !== false && pinVal != null) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
58
114
|
}
|
|
115
|
+
el += layout_post_card('post', post, partial(post));
|
|
59
116
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
63
119
|
el += '</div>';
|
|
64
120
|
return el;
|
|
65
121
|
}
|