hexo-theme-stellar 1.39.1 → 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 +29 -0
- package/_config.yml +6 -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/head.ejs +11 -2
- package/layout/_partial/main/navbar/article_banner.ejs +7 -0
- package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
- package/layout/_partial/main/post_list/post_card.ejs +11 -0
- package/layout/_partial/widgets/markdown.ejs +1 -1
- package/layout/_partial/widgets/toc.ejs +8 -1
- package/layout/categories.ejs +18 -8
- 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/json_ld.js +5 -0
- package/scripts/helpers/mdrender.js +29 -0
- package/scripts/helpers/reading_time.js +12 -0
- 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,34 @@
|
|
|
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
|
+
|
|
3
32
|
## 1.39.1
|
|
4
33
|
|
|
5
34
|
> 发布日期: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
|
|
@@ -206,6 +206,10 @@ article:
|
|
|
206
206
|
related_posts:
|
|
207
207
|
enable: false
|
|
208
208
|
max_count: 5
|
|
209
|
+
# 文章页是否显示字数和预计阅读时长(#302)
|
|
210
|
+
reading_time: false
|
|
211
|
+
# 文章卡片是否显示标签(最多 5 个,#580)
|
|
212
|
+
card_tags: false
|
|
209
213
|
|
|
210
214
|
search:
|
|
211
215
|
service: local_search # local_search, algolia_search, todo...
|
|
@@ -676,7 +680,7 @@ style:
|
|
|
676
680
|
image-l: 24px # 非技术类文章插图圆角
|
|
677
681
|
image: 16px # 技术类文章插图圆角
|
|
678
682
|
image-s: 8px # 评论区图片圆角(部分评论支持)
|
|
679
|
-
corner-shape: superellipse(1.
|
|
683
|
+
corner-shape: superellipse(1.25) # 连续曲率圆角(仅 Chromium 139+ 支持,其余浏览器自动回退普通圆角):superellipse(1.x) 开启,round 关闭
|
|
680
684
|
color:
|
|
681
685
|
# 主题色配置(不会根据明暗动态调整,请设置为通用的颜色)
|
|
682
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
|
|
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
|
}
|
|
@@ -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
|
|
@@ -93,6 +93,17 @@ function div_default() {
|
|
|
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;
|
|
@@ -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/page.ejs
CHANGED
|
@@ -44,8 +44,12 @@ function layoutDiv() {
|
|
|
44
44
|
el += partial('_partial/main/navbar/article_banner')
|
|
45
45
|
}
|
|
46
46
|
el += `<article class="${articleClass()}">`
|
|
47
|
-
if (page.content && page.content.length > 0) {
|
|
47
|
+
if (page.content && String(page.content).trim().length > 0) {
|
|
48
48
|
el += page.content
|
|
49
|
+
} else {
|
|
50
|
+
// wiki 项目首页正文为空(剪裁空白后)且配置了 repo 时,
|
|
51
|
+
// 以该仓库 README.md 作为主页正文(应用层 helper,复用底层远程 md 渲染组件)
|
|
52
|
+
el += wiki_readme_html(theme.wiki.tree[page.wiki], page)
|
|
49
53
|
}
|
|
50
54
|
if (notebook) {
|
|
51
55
|
el += partial('_partial/main/notebook/note_tags', { notebook: notebook })
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hexo-theme-stellar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Elegant and powerful theme for Hexo.",
|
|
5
5
|
"main": "package.json",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "eslint .",
|
|
8
|
-
"test": "node --test",
|
|
8
|
+
"test": "node ci/check-require-decls.js && node --test",
|
|
9
9
|
"check": "npm run lint && npm test && python3 docs/knowledge/tools/verify.py",
|
|
10
10
|
"release": "node release.js",
|
|
11
11
|
"release:dry": "node release.js --dry-run"
|
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
"glob": "^10.4.0",
|
|
30
30
|
"hexo-renderer-ejs": "^2.0.0",
|
|
31
31
|
"hexo-renderer-stylus": "^3.0.1",
|
|
32
|
+
"hexo-pagination": "^3.0.0",
|
|
32
33
|
"probe-image-size": "^7.2.3"
|
|
33
34
|
},
|
|
34
35
|
"homepage": "https://xaoxuu.com/wiki/stellar/",
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"eslint": "^9.39.5"
|
|
37
|
+
"eslint": "^9.39.5",
|
|
38
|
+
"hexo-util": "^4.0.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -36,7 +36,7 @@ hexo.extend.console.register('new-note', 'Create a new note', {
|
|
|
36
36
|
const frontMatter = [
|
|
37
37
|
'---',
|
|
38
38
|
`title: ${name}`,
|
|
39
|
-
`date: ${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')} ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}
|
|
39
|
+
`date: ${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')} ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`,
|
|
40
40
|
`tags: [${tags.map(t => '"'+t+'"').join(', ')}]`,
|
|
41
41
|
`notebook: ${book}`,
|
|
42
42
|
'---',
|
|
@@ -46,4 +46,4 @@ hexo.extend.console.register('new-note', 'Create a new note', {
|
|
|
46
46
|
fs.writeFileSync(filePath, frontMatter);
|
|
47
47
|
console.log(`Created: ${filePath}`);
|
|
48
48
|
callback();
|
|
49
|
-
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { buildCategoryTree } = require('../lib/category_tree');
|
|
5
|
+
|
|
6
|
+
hexo.extend.helper.register('category_tree', function(categories) {
|
|
7
|
+
const list = categories && typeof categories.toArray === 'function'
|
|
8
|
+
? categories.toArray()
|
|
9
|
+
: (categories || []);
|
|
10
|
+
return buildCategoryTree(list);
|
|
11
|
+
});
|
|
@@ -98,6 +98,11 @@ hexo.extend.helper.register('json_ld', function(args) {
|
|
|
98
98
|
|
|
99
99
|
if (page.excerpt || page.description) {
|
|
100
100
|
schema.description = this.strip_html(page.description || page.excerpt);
|
|
101
|
+
} else if (page.wiki) {
|
|
102
|
+
const proj = this.theme.wiki.tree[page.wiki];
|
|
103
|
+
if (proj && proj.description) {
|
|
104
|
+
schema.description = proj.description;
|
|
105
|
+
}
|
|
101
106
|
} else {
|
|
102
107
|
schema.description = util.truncate(this.strip_html(page.content), {length: 200});
|
|
103
108
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { mdrenderHtml, PLACEHOLDER_CLASS } = require('../lib/mdrender_html');
|
|
5
|
+
const { wikiReadmeHtml, isWikiReadmePage } = require('../lib/wiki_readme');
|
|
6
|
+
|
|
7
|
+
// 远程 Markdown 渲染占位标记(底层组件服务端入口,供 EJS 模板复用)
|
|
8
|
+
hexo.extend.helper.register('mdrender_html', function (src, options = {}) {
|
|
9
|
+
if (options.ghraw == null) {
|
|
10
|
+
options.ghraw = hexo.theme.config.api_host.ghraw;
|
|
11
|
+
}
|
|
12
|
+
return mdrenderHtml(src, options);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// wiki 项目 README 主页占位(首页正文为空且配置 repo 时输出,否则空串)
|
|
16
|
+
hexo.extend.helper.register('wiki_readme_html', function (proj, page) {
|
|
17
|
+
return wikiReadmeHtml(proj, page, { ghraw: hexo.theme.config.api_host.ghraw });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// 页面是否包含客户端渲染的远程 md(供 TOC 预留空容器)
|
|
21
|
+
hexo.extend.helper.register('has_remote_md', function (page) {
|
|
22
|
+
if (!page) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (page.content && page.content.indexOf(PLACEHOLDER_CLASS) >= 0) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return isWikiReadmePage(hexo.theme.config.wiki.tree[page.wiki], page);
|
|
29
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { countWords, readingMinutes } = require('../lib/reading_time');
|
|
5
|
+
|
|
6
|
+
hexo.extend.helper.register('word_count', function(page) {
|
|
7
|
+
return countWords(page && (page.content || page._content || ''));
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
hexo.extend.helper.register('reading_time', function(page) {
|
|
11
|
+
return readingMinutes(page && (page.content || page._content || ''));
|
|
12
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 分类嵌套树构建(#564):基于 parent 关系递归组装,兼容 2/3/4 级
|
|
4
|
+
function buildCategoryTree(list) {
|
|
5
|
+
const items = (list || []).map(c => ({
|
|
6
|
+
_id: c._id,
|
|
7
|
+
name: c.name,
|
|
8
|
+
path: c.path,
|
|
9
|
+
parentId: c.parent ? (typeof c.parent === 'object' ? c.parent._id : c.parent) : null,
|
|
10
|
+
count: (c.posts && c.posts.length) || 0,
|
|
11
|
+
children: []
|
|
12
|
+
}));
|
|
13
|
+
const byId = {};
|
|
14
|
+
items.forEach(item => { byId[item._id] = item; });
|
|
15
|
+
const roots = [];
|
|
16
|
+
items.forEach(item => {
|
|
17
|
+
if (item.parentId != null && byId[item.parentId]) {
|
|
18
|
+
byId[item.parentId].children.push(item);
|
|
19
|
+
} else {
|
|
20
|
+
roots.push(item);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return roots;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { buildCategoryTree };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdrender_html.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
*
|
|
4
|
+
* 远程 Markdown 渲染底层组件(通用能力,不包含应用场景逻辑):
|
|
5
|
+
* 统一生成 .data-service.ds-mdrender 占位标记。
|
|
6
|
+
*
|
|
7
|
+
* 行为:
|
|
8
|
+
* - src 为 GitHub raw 类地址时,host 替换为调用方传入的 ghraw
|
|
9
|
+
* (主题配置 api_host.ghraw,代码不设默认值);未传 ghraw 时保留 src 原 host;
|
|
10
|
+
* - 识别后输出 data-base(该 md 所在 raw 目录);
|
|
11
|
+
* - 非 GitHub src 原样输出、不带 data-base;
|
|
12
|
+
* - replace=true 输出 data-replace(客户端原地替换,无外部容器);
|
|
13
|
+
* - heading 缺省 true,输出 data-heading(客户端标题适配本地文章格式)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// GitHub raw 规范 host:用于识别用户 src 中的规范 raw 地址(GitHub 固有事实,非配置默认值)
|
|
19
|
+
const GITHUB_RAW_HOST = 'raw.githubusercontent.com';
|
|
20
|
+
|
|
21
|
+
// 占位标记类名(组件契约:服务端生成、services 按需加载、客户端渲染共用)
|
|
22
|
+
const PLACEHOLDER_CLASS = 'ds-mdrender';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析 GitHub raw 地址(规范 raw host 或调用方传入的 ghraw 镜像 host)。
|
|
26
|
+
* 返回 { host, owner, repo, ref, rest };无法识别时返回 null。
|
|
27
|
+
*/
|
|
28
|
+
function parseGitHubRaw(url, ghrawHost) {
|
|
29
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
let u;
|
|
33
|
+
try {
|
|
34
|
+
u = new URL(url);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const host = u.hostname || '';
|
|
39
|
+
if (host !== GITHUB_RAW_HOST && host !== ghrawHost) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const segs = u.pathname.split('/').filter(s => s.length > 0);
|
|
43
|
+
if (segs.length < 3) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
host: host,
|
|
48
|
+
owner: segs[0],
|
|
49
|
+
repo: segs[1],
|
|
50
|
+
ref: segs[2],
|
|
51
|
+
rest: segs.slice(3).join('/')
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeAttr(value) {
|
|
56
|
+
return String(value).replace(/&/g, '&').replace(/"/g, '"');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 生成远程 Markdown 渲染占位标记。
|
|
61
|
+
* options: { id, replace, heading, ghraw }
|
|
62
|
+
*/
|
|
63
|
+
function mdrenderHtml(src, options = {}) {
|
|
64
|
+
const parsed = parseGitHubRaw(src, options.ghraw);
|
|
65
|
+
let url = src;
|
|
66
|
+
let base = null;
|
|
67
|
+
if (parsed) {
|
|
68
|
+
// 未传 ghraw 时保留 src 原 host,不硬编码默认值
|
|
69
|
+
const ghraw = options.ghraw || parsed.host;
|
|
70
|
+
const dir = `${parsed.owner}/${parsed.repo}/${parsed.ref}/`;
|
|
71
|
+
url = `https://${ghraw}/${dir}${parsed.rest}`;
|
|
72
|
+
base = `https://${ghraw}/${dir}`;
|
|
73
|
+
}
|
|
74
|
+
let el = '<div class="data-service ds-mdrender"';
|
|
75
|
+
el += ` src="${escapeAttr(url)}"`;
|
|
76
|
+
if (base) {
|
|
77
|
+
el += ` data-base="${escapeAttr(base)}"`;
|
|
78
|
+
}
|
|
79
|
+
if (options.replace === true) {
|
|
80
|
+
el += ' data-replace="true"';
|
|
81
|
+
}
|
|
82
|
+
if (options.heading !== false) {
|
|
83
|
+
el += ' data-heading="true"';
|
|
84
|
+
}
|
|
85
|
+
if (options.id) {
|
|
86
|
+
el += ` id="${escapeAttr(options.id)}"`;
|
|
87
|
+
}
|
|
88
|
+
el += '></div>';
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
GITHUB_RAW_HOST,
|
|
94
|
+
PLACEHOLDER_CLASS,
|
|
95
|
+
parseGitHubRaw,
|
|
96
|
+
mdrenderHtml
|
|
97
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 阅读信息统计(#302):中文字符按 1 字计,英文/数字按空格分词计词
|
|
4
|
+
function countWords(content) {
|
|
5
|
+
const text = String(content || '').replace(/<[^>]*>/g, ' ');
|
|
6
|
+
const cjk = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
7
|
+
const latin = (text.replace(/[\u4e00-\u9fff]/g, ' ').match(/[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*/g) || []).length;
|
|
8
|
+
return cjk + latin;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 混合阅读速度约 300 字/分钟,最少 1 分钟
|
|
12
|
+
function readingMinutes(content, speed = 300) {
|
|
13
|
+
return Math.max(1, Math.ceil(countWords(content) / speed));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { countWords, readingMinutes };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wiki_readme.js v1 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
*
|
|
4
|
+
* Wiki 应用层:项目首页正文为空(剪裁空白后)且配置 repo 时,
|
|
5
|
+
* 以该仓库 README.md 作为主页正文(复用底层远程 md 渲染组件)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { mdrenderHtml } = require('./mdrender_html');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* README 地址。host 由调用方从主题配置 api_host.ghraw 传入;
|
|
14
|
+
* branch 缺省用 GitHub HEAD(自动指向默认分支)。
|
|
15
|
+
*/
|
|
16
|
+
function readmeUrl(repo, branch, host) {
|
|
17
|
+
return `https://${host}/${repo}/${branch || 'HEAD'}/README.md`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 页面正文是否为空(剪裁多余空行、空格后判断)
|
|
21
|
+
function isEmptyContent(page) {
|
|
22
|
+
return !page || !String(page.content || '').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 判定是否 README 主页:配置了 repo 的项目首页且正文为空。
|
|
27
|
+
*/
|
|
28
|
+
function isWikiReadmePage(proj, page) {
|
|
29
|
+
if (!proj || !proj.repo || !page || !proj.homepage) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!isEmptyContent(page)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return proj.homepage.path === page.path;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 生成 README 主页占位(原地替换模式);不适用时返回空串。
|
|
40
|
+
* options: { ghraw }
|
|
41
|
+
*/
|
|
42
|
+
function wikiReadmeHtml(proj, page, options = {}) {
|
|
43
|
+
if (!isWikiReadmePage(proj, page)) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
return mdrenderHtml(readmeUrl(proj.repo, proj.branch, options.ghraw), {
|
|
47
|
+
ghraw: options.ghraw,
|
|
48
|
+
replace: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
readmeUrl,
|
|
54
|
+
isEmptyContent,
|
|
55
|
+
isWikiReadmePage,
|
|
56
|
+
wikiReadmeHtml
|
|
57
|
+
};
|
package/scripts/tags/index.js
CHANGED
|
@@ -48,6 +48,8 @@ hexo.extend.tag.register('audio', require('./lib/audio')(hexo))
|
|
|
48
48
|
hexo.extend.tag.register('video', require('./lib/video')(hexo))
|
|
49
49
|
hexo.extend.tag.register('rating', require('./lib/rating')(hexo))
|
|
50
50
|
hexo.extend.tag.register('vote', require('./lib/vote')(hexo))
|
|
51
|
+
hexo.extend.tag.register('table', require('./lib/table')(hexo), {ends: true})
|
|
52
|
+
hexo.extend.tag.register('tip', require('./lib/tip')(hexo), {ends: true})
|
|
51
53
|
|
|
52
54
|
// read 阅读类
|
|
53
55
|
hexo.extend.tag.register('reel', require('./lib/read/reel')(hexo), true)
|
package/scripts/tags/lib/md.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* md.js
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* md.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
*
|
|
5
4
|
* 格式与官方标签插件一致使用空格分隔,中括号内的是可选参数(中括号不需要写出来)
|
|
6
5
|
*
|
|
7
|
-
* {% md src %}
|
|
6
|
+
* {% md src [wrap:true|false] %}
|
|
7
|
+
*
|
|
8
|
+
* wrap 默认 true:渲染结果保留在 .data-service.ds-mdrender 容器内(现状);
|
|
9
|
+
* wrap:false:渲染后占位元素被原地替换,无外部容器(复用底层 data-replace)。
|
|
8
10
|
*
|
|
11
|
+
* 复用底层组件(scripts/lib/mdrender_html.js)输出渲染占位;
|
|
12
|
+
* 容器结构与现状一致,标题默认适配本地文章格式(id + headerlink,h1 隐藏)。
|
|
9
13
|
*/
|
|
10
14
|
'use strict'
|
|
11
15
|
|
|
16
|
+
const { mdrenderHtml } = require('../../lib/mdrender_html')
|
|
17
|
+
|
|
12
18
|
var md_index = 0
|
|
13
19
|
|
|
14
20
|
module.exports = ctx => function(args) {
|
|
15
|
-
args = ctx.args.map(args, [''], ['src'])
|
|
21
|
+
args = ctx.args.map(args, ['wrap'], ['src'])
|
|
16
22
|
const md_id = "md_" + ++md_index
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
return mdrenderHtml(args.src, {
|
|
24
|
+
id: md_id,
|
|
25
|
+
ghraw: ctx.theme.config.api_host.ghraw,
|
|
26
|
+
replace: args.wrap === 'false'
|
|
27
|
+
})
|
|
28
|
+
}
|