hexo-theme-stellar 1.40.0 → 1.41.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/CHANGELOG.md +41 -0
- package/_config.yml +33 -10
- package/_data/icons.yml +250 -37
- package/languages/en.yml +6 -0
- package/languages/zh-CN.yml +6 -0
- package/languages/zh-TW.yml +6 -0
- package/layout/_partial/comments/artalk/script.ejs +25 -8
- package/layout/_partial/comments/beaudar/script.ejs +1 -0
- package/layout/_partial/comments/twikoo/script.ejs +1 -0
- package/layout/_partial/comments/utterances/script.ejs +1 -0
- package/layout/_partial/comments/waline/script.ejs +1 -0
- package/layout/_partial/head.ejs +43 -7
- package/layout/_partial/main/article/article_tags.ejs +16 -0
- package/layout/_partial/main/navbar/article_banner.ejs +11 -1
- package/layout/_partial/main/notebook/note_card.ejs +1 -1
- package/layout/_partial/main/notebook/paginator.ejs +2 -2
- package/layout/_partial/main/pin_slider.ejs +2 -2
- package/layout/_partial/main/post_list/paginator.ejs +2 -2
- package/layout/_partial/menubtn.ejs +2 -2
- package/layout/_partial/scripts/defines.ejs +26 -2
- package/layout/_partial/scripts.ejs +30 -7
- package/layout/_partial/sidebar/logo.ejs +1 -1
- package/layout/_partial/sidebar/menu.ejs +1 -1
- package/layout/_partial/sidebar/search.ejs +1 -1
- package/layout/_partial/widgets/author.ejs +1 -1
- package/layout/_partial/widgets/components/link.ejs +2 -2
- package/layout/_partial/widgets/ghuser.ejs +1 -1
- package/layout/_partial/widgets/related.ejs +1 -1
- package/layout/_partial/widgets/toc.ejs +1 -1
- package/layout/_partial/widgets/tree.ejs +1 -1
- package/layout/_plugins/fancybox.ejs +4 -2
- package/layout/_plugins/mermaid.ejs +2 -1
- package/layout/_plugins/search/local_search.ejs +4 -3
- package/layout/_plugins/swiper.ejs +1 -0
- package/layout/categories.ejs +1 -1
- package/layout/page.ejs +3 -0
- package/layout/tags.ejs +1 -0
- package/package.json +1 -1
- package/scripts/events/lib/doc_tree.js +12 -208
- package/scripts/events/lib/notebooks.js +8 -148
- package/scripts/events/lib/utils.js +10 -1
- package/scripts/filters/index.js +1 -0
- package/scripts/filters/lib/img_lazyload.js +4 -0
- package/scripts/filters/lib/img_onerror.js +4 -0
- package/scripts/filters/lib/md_table.js +46 -0
- package/scripts/generators/search.js +12 -18
- package/scripts/generators/stellar-icons.js +47 -0
- package/scripts/helpers/ai_label.js +29 -0
- package/scripts/helpers/icon.js +2 -2
- package/scripts/helpers/json_ld.js +10 -13
- package/scripts/helpers/related_posts.js +0 -7
- package/scripts/helpers/seo.js +9 -0
- package/scripts/lib/ai_label.js +55 -0
- package/scripts/lib/doc_tree.js +267 -0
- package/scripts/lib/notebooks.js +181 -0
- package/scripts/lib/search_index.js +82 -0
- package/scripts/lib/seo.js +79 -0
- package/scripts/tags/lib/about.js +2 -2
- package/scripts/tags/lib/albums.js +1 -1
- package/scripts/tags/lib/banner.js +1 -1
- package/scripts/tags/lib/chat.js +49 -49
- package/scripts/tags/lib/copy.js +1 -1
- package/scripts/tags/lib/friends.js +1 -1
- package/scripts/tags/lib/gallery.js +1 -1
- package/scripts/tags/lib/hashtag.js +1 -1
- package/scripts/tags/lib/image.js +3 -3
- package/scripts/tags/lib/posters.js +1 -1
- package/scripts/tags/lib/sites.js +2 -2
- package/source/css/_common/base.styl +5 -9
- package/source/css/_common/device.styl +3 -3
- package/source/css/_components/md-table.styl +23 -0
- package/source/css/_components/pages/archives.styl +2 -22
- package/source/css/_components/pages/article-story.styl +4 -4
- package/source/css/_components/partial/article-tags.styl +10 -0
- package/source/css/_components/partial/bread-nav.styl +14 -0
- package/source/css/_components/partial/navbar.styl +10 -2
- package/source/css/_components/sidebar/search.styl +24 -4
- package/source/css/_components/tag-plugins/table.styl +16 -11
- package/source/css/_components/widgets/components.styl +6 -0
- package/source/css/_components/widgets/list.styl +6 -0
- package/source/css/_components/widgets/toc.styl +1 -1
- package/source/css/_custom.styl +1 -12
- package/source/css/_defines/func.styl +28 -3
- package/source/css/_plugins/index.styl +0 -18
- package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
- package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
- package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
- package/source/css/{_plugins → plugins}/swiper.styl +5 -1
- package/source/js/icons.js +60 -0
- package/source/js/main.js +114 -0
- package/source/js/search/highlight.js +82 -0
- package/source/js/search/local-search.js +294 -103
- package/source/js/services/timeline.js +1 -1
- package/source/js/services/weibo.js +3 -3
- package/source/js/services.js +179 -0
- package/source/js/tagtree.js +29 -0
- package/source/js/theme.js +61 -0
- package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +13 -10
- package/layout/_partial/scripts/services.ejs +0 -178
- package/layout/_partial/scripts/sidebar.ejs +0 -25
- package/layout/_partial/scripts/tagtree.ejs +0 -29
- package/layout/_partial/scripts/theme.ejs +0 -67
- /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
- /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
- /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
- /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
|
@@ -1,216 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* doc_tree.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
*
|
|
4
|
+
* Wiki 文档树构建入口:委托 scripts/lib/doc_tree.js 的纯函数,
|
|
5
|
+
* 保持 ctx.theme.config.wiki 输出结构与旧实现一致。
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
'use strict';
|
|
6
9
|
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
class WikiPage {
|
|
10
|
-
constructor(page) {
|
|
11
|
-
this.id = page._id
|
|
12
|
-
this.wiki = page.wiki
|
|
13
|
-
this.title = page.title
|
|
14
|
-
this.path = page.path
|
|
15
|
-
this.path_key = normalize_path(page.path)
|
|
16
|
-
this.layout = page.layout
|
|
17
|
-
this.updated = page.updated
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getWikiObject(ctx) {
|
|
22
|
-
var wiki = { tree:{} }
|
|
23
|
-
const data = ctx.locals.get('data')
|
|
24
|
-
var list = []
|
|
25
|
-
for (let key of Object.keys(data)) {
|
|
26
|
-
if (key.endsWith('.DS_Store')) {
|
|
27
|
-
continue
|
|
28
|
-
}
|
|
29
|
-
if (key.includes('wiki/') && key.length > 5) {
|
|
30
|
-
let newKey = key.replace('wiki/', '')
|
|
31
|
-
let obj = data[key]
|
|
32
|
-
if ((typeof obj.tags == 'string') && obj.tags.constructor == String) {
|
|
33
|
-
obj.tags = [obj.tags]
|
|
34
|
-
}
|
|
35
|
-
if ((typeof obj.tree == 'object') && obj.tree.constructor == Array) {
|
|
36
|
-
obj.tree = { '': obj.tree }
|
|
37
|
-
}
|
|
38
|
-
obj.id = newKey
|
|
39
|
-
if (obj.sort == null) {
|
|
40
|
-
obj.sort = 0
|
|
41
|
-
}
|
|
42
|
-
if (obj.base_dir) {
|
|
43
|
-
if (obj.base_dir.startsWith('/')) {
|
|
44
|
-
obj.base_dir = obj.base_dir.substring(1)
|
|
45
|
-
}
|
|
46
|
-
if (obj.base_dir.length > 1 && obj.base_dir.endsWith('/') == false) {
|
|
47
|
-
obj.base_dir = obj.base_dir + '/'
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
obj.base_dir = ''
|
|
51
|
-
}
|
|
52
|
-
list.push(obj)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
list = list.sort((p1, p2) => p2.sort - p1.sort)
|
|
56
|
-
for (let item of list) {
|
|
57
|
-
wiki.tree[item.id] = item
|
|
58
|
-
}
|
|
59
|
-
return wiki
|
|
60
|
-
}
|
|
10
|
+
const { buildWikiTree } = require('../../lib/doc_tree');
|
|
61
11
|
|
|
62
12
|
module.exports = ctx => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// 数据整合:项目标签
|
|
73
|
-
var all_tag_name = []
|
|
74
|
-
for (let id of wiki_list) {
|
|
75
|
-
let item = wiki.tree[id]
|
|
76
|
-
let tags = item.tags
|
|
77
|
-
if (tags) {
|
|
78
|
-
tags.forEach((tag, i) => {
|
|
79
|
-
if (all_tag_name.includes(tag) === false) {
|
|
80
|
-
all_tag_name.push(tag)
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
wiki.tree[id].tags = tags
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 补充项目名称和首页
|
|
88
|
-
for (let id of wiki_list) {
|
|
89
|
-
let item = wiki.tree[id]
|
|
90
|
-
item.id = id
|
|
91
|
-
if (item.title == undefined || item.title.length === 0) {
|
|
92
|
-
item.title = id
|
|
93
|
-
}
|
|
94
|
-
if (item.name == undefined || item.name.length == 0) {
|
|
95
|
-
item.name = id
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 数据整合:每个项目的子页面
|
|
100
|
-
for (let i = 0; i < wiki_list.length; i++) {
|
|
101
|
-
let id = wiki_list[i];
|
|
102
|
-
let item = wiki.tree[id]
|
|
103
|
-
let sub_pages = wiki_pages.filter(p => p.wiki === id)
|
|
104
|
-
|
|
105
|
-
// 首页
|
|
106
|
-
// 未特别指定首页时,获取TOC第一页作为首页
|
|
107
|
-
var homepage = item.homepage
|
|
108
|
-
if (homepage == null && item.tree != null) {
|
|
109
|
-
for (let id of Object.keys(item.tree)) {
|
|
110
|
-
const sec = item.tree[id]
|
|
111
|
-
for (let key of sec) {
|
|
112
|
-
let hs = sub_pages.filter(p => p.path_key == normalize_path(item.base_dir + key))
|
|
113
|
-
if (hs.length > 0) {
|
|
114
|
-
homepage = hs[0]
|
|
115
|
-
break
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (homepage != null) {
|
|
119
|
-
break
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (homepage == null) {
|
|
124
|
-
homepage = sub_pages[0]
|
|
125
|
-
}
|
|
126
|
-
if (typeof homepage == 'string') {
|
|
127
|
-
homepage = {path: homepage}
|
|
128
|
-
}
|
|
129
|
-
homepage.is_homepage = true
|
|
130
|
-
item.homepage = homepage
|
|
131
|
-
// 内页分组
|
|
132
|
-
var sections = []
|
|
133
|
-
var others = sub_pages
|
|
134
|
-
if (item.tree) {
|
|
135
|
-
// 根据配置设置顺序
|
|
136
|
-
for (let title of Object.keys(item.tree)) {
|
|
137
|
-
var sec = { title: title, pages: []}
|
|
138
|
-
for (let key of item.tree[title]) {
|
|
139
|
-
const pagePathKey = normalize_path(item.base_dir + key)
|
|
140
|
-
sec.pages = sec.pages.concat(sub_pages.filter(p => p.path_key == pagePathKey))
|
|
141
|
-
others = others.filter(p => p.path_key != pagePathKey)
|
|
142
|
-
}
|
|
143
|
-
sections.push(sec)
|
|
144
|
-
}
|
|
145
|
-
if (others.length > 0 && others.filter(p => p.title?.length > 0).length > 0) {
|
|
146
|
-
sections.push({
|
|
147
|
-
title: '...',
|
|
148
|
-
pages: others.sort((p1, p2) => p1.title - p2.title)
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
// 自动设置顺序
|
|
153
|
-
sections.push({
|
|
154
|
-
pages: sub_pages.sort((p1, p2) => p1.title - p2.title)
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// page number
|
|
159
|
-
var page_number = 0
|
|
160
|
-
for (let sec of sections) {
|
|
161
|
-
for (let page of sec.pages) {
|
|
162
|
-
page.page_number = page_number++
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
item.sections = sections
|
|
166
|
-
item.pages = sub_pages
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 全站所有的项目标签
|
|
170
|
-
var all_tags = {}
|
|
171
|
-
all_tag_name.forEach((tag_name, i) => {
|
|
172
|
-
var items = []
|
|
173
|
-
for (let id of wiki_list) {
|
|
174
|
-
let item = wiki.tree[id]
|
|
175
|
-
// 过滤掉找不到页面的项目
|
|
176
|
-
if (item.homepage == null) {
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
// 过滤掉未上架的项目
|
|
180
|
-
if (!wiki.shelf.includes(item.id)) {
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
if (item.tags && item.tags.includes(tag_name) === true && items.includes(tag_name) === false) {
|
|
184
|
-
items.push(item.id)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
all_tags[tag_name] = {
|
|
188
|
-
name: tag_name,
|
|
189
|
-
path: (ctx.theme.config.site_tree.index_wiki.base_dir) + '/tags/' + tag_name + '/index.html',
|
|
190
|
-
items: items
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
// 关联相似项目
|
|
195
|
-
for (let id of wiki_list) {
|
|
196
|
-
let item = wiki.tree[id]
|
|
197
|
-
if (item.tags) {
|
|
198
|
-
var relatedItems = []
|
|
199
|
-
item.tags.forEach((tag_name, i) => {
|
|
200
|
-
let relatedOtherItems = all_tags[tag_name].items.filter(name => name != item.id)
|
|
201
|
-
if (relatedOtherItems.length > 0) {
|
|
202
|
-
relatedItems.push({
|
|
203
|
-
name: tag_name,
|
|
204
|
-
items: relatedOtherItems
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
item.relatedItems = relatedItems
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
wiki.all_tags = all_tags
|
|
213
|
-
wiki.all_pages = wiki_pages
|
|
214
|
-
ctx.theme.config.wiki = wiki
|
|
215
|
-
|
|
216
|
-
}
|
|
13
|
+
const wiki = buildWikiTree({
|
|
14
|
+
data: ctx.locals.get('data'),
|
|
15
|
+
pages: ctx.locals.get('pages'),
|
|
16
|
+
shelf: ctx.locals.get('data').wiki || [],
|
|
17
|
+
siteTree: ctx.theme.config.site_tree
|
|
18
|
+
});
|
|
19
|
+
ctx.theme.config.wiki = wiki;
|
|
20
|
+
};
|
|
@@ -1,155 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* notebooks.js v1
|
|
3
|
+
*
|
|
4
|
+
* 笔记本系统构建入口:委托 scripts/lib/notebooks.js 的纯函数,
|
|
5
|
+
* 保持 ctx.theme.config.notebooks 输出结构与旧实现一致。
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
'use strict'
|
|
8
|
+
'use strict';
|
|
6
9
|
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
class NotePage {
|
|
10
|
-
constructor(page) {
|
|
11
|
-
this.id = page._id
|
|
12
|
-
this.notebook = page.notebook
|
|
13
|
-
this.title = page.title
|
|
14
|
-
this.tags = page.tags
|
|
15
|
-
this.path = page.path
|
|
16
|
-
this.path_key = normalize_path(page.path)
|
|
17
|
-
this.layout = page.layout
|
|
18
|
-
this.date = page.date
|
|
19
|
-
this.updated = page.updated || page.date
|
|
20
|
-
|
|
21
|
-
const pin = page.pin ?? page.sticky ?? 0
|
|
22
|
-
if (pin === true) {
|
|
23
|
-
this.pin = 1
|
|
24
|
-
} else if (pin === false) {
|
|
25
|
-
this.pin = 0
|
|
26
|
-
} else {
|
|
27
|
-
this.pin = pin
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function splitTag(tag) {
|
|
33
|
-
return tag.split('/').filter(t => t.length > 0)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function prepareNotebook(id, info, ctx) {
|
|
37
|
-
const notebook = info
|
|
38
|
-
notebook.id = id
|
|
39
|
-
|
|
40
|
-
if (notebook.base_dir) {
|
|
41
|
-
if (notebook.base_dir.startsWith('/')) {
|
|
42
|
-
notebook.base_dir = notebook.base_dir.substring(1)
|
|
43
|
-
}
|
|
44
|
-
if (notebook.base_dir.length > 1 && !notebook.base_dir.endsWith('/')) {
|
|
45
|
-
notebook.base_dir = notebook.base_dir + '/'
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
const notebooksBaseDir = ctx.theme.config.site_tree.notebooks.base_dir
|
|
49
|
-
notebook.base_dir = notebooksBaseDir ? `${notebooksBaseDir}/${id}` : id
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
notebook.sort ||= 0
|
|
53
|
-
notebook.auto_excerpt ||= ctx.theme.config.notebook.auto_excerpt || 0
|
|
54
|
-
notebook.per_page ??= ctx.theme.config.notebook.per_page ?? ctx.config.per_page ?? 10
|
|
55
|
-
notebook.order_by ||= ctx.theme.config.notebook.order_by || '-updated'
|
|
56
|
-
notebook.menu_id ??= ctx.theme.config.site_tree.notes.menu_id
|
|
57
|
-
notebook.license ??= ctx.theme.config.notebook.license
|
|
58
|
-
notebook.share ??= ctx.theme.config.notebook.share
|
|
59
|
-
|
|
60
|
-
notebook.leftbar ??= ctx.theme.config.site_tree.notes.leftbar
|
|
61
|
-
notebook.rightbar ??= ctx.theme.config.site_tree.notes.rightbar
|
|
62
|
-
notebook.note_leftbar ??= ctx.theme.config.site_tree.note.leftbar
|
|
63
|
-
notebook.note_rightbar ??= ctx.theme.config.site_tree.note.rightbar
|
|
64
|
-
|
|
65
|
-
const tagMap = new Map() // tagId: tagInfo
|
|
66
|
-
notebook.tagTree = tagMap
|
|
67
|
-
|
|
68
|
-
const rootTag = {
|
|
69
|
-
id: '',
|
|
70
|
-
name: '',
|
|
71
|
-
part: '',
|
|
72
|
-
path: notebook.base_dir,
|
|
73
|
-
parent: null, // parent tag id
|
|
74
|
-
childSet: new Set(), // child tag ids
|
|
75
|
-
noteSet: new Set(), // note ids
|
|
76
|
-
}
|
|
77
|
-
tagMap.set(rootTag.id, rootTag)
|
|
78
|
-
|
|
79
|
-
// Iterate through all notes in the notebook, build the tag tree.
|
|
80
|
-
const allPages = ctx.locals.get('pages')
|
|
81
|
-
const pages = allPages.filter(p => p.notebook === notebook.id)
|
|
82
|
-
for (const page of pages.data) {
|
|
83
|
-
rootTag.noteSet.add(page._id)
|
|
84
|
-
|
|
85
|
-
if (!page.tags) {
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const hierarchyTag of page.tags) {
|
|
90
|
-
const parts = splitTag(hierarchyTag)
|
|
91
|
-
let parent = rootTag
|
|
92
|
-
for (const part of parts) {
|
|
93
|
-
const tagName = parent.name ? `${parent.name}/${part}` : part
|
|
94
|
-
const tagId = tagName.toLowerCase()
|
|
95
|
-
let tag = tagMap.get(tagId)
|
|
96
|
-
if (tag == null) {
|
|
97
|
-
tag = {
|
|
98
|
-
id: tagId,
|
|
99
|
-
name: tagName,
|
|
100
|
-
part: part,
|
|
101
|
-
path: `${notebook.base_dir}/tags/${tagId}`,
|
|
102
|
-
parent: parent.id,
|
|
103
|
-
childSet: new Set(),
|
|
104
|
-
noteSet: new Set(),
|
|
105
|
-
}
|
|
106
|
-
tagMap.set(tagId, tag)
|
|
107
|
-
parent.childSet.add(tagId)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
tag.noteSet.add(page._id)
|
|
111
|
-
parent = tag
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
notebook.noteMap = pages.map(p => new NotePage(p)).reduce((map, note) => {
|
|
117
|
-
map.set(note.id, note)
|
|
118
|
-
return map
|
|
119
|
-
}, new Map())
|
|
120
|
-
|
|
121
|
-
// Sort child tags for each tag.
|
|
122
|
-
for (const [_, tag] of tagMap) {
|
|
123
|
-
tag.children = Array.from(tag.childSet)
|
|
124
|
-
tag.children.sort()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return notebook
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getNotebooksObject(ctx) {
|
|
131
|
-
const notebooks = {
|
|
132
|
-
tree: {},
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const data = ctx.locals.get('data')
|
|
136
|
-
const list = []
|
|
137
|
-
for (const [key, info] of Object.entries(data)) {
|
|
138
|
-
if (!key.startsWith('notebooks/') || key.endsWith('.DS_Store')) {
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
const id = key.substring(10)
|
|
142
|
-
list.push(prepareNotebook(id, info, ctx))
|
|
143
|
-
}
|
|
144
|
-
list.sort((a, b) => a.sort - b.sort)
|
|
145
|
-
for (const info of list) {
|
|
146
|
-
notebooks.tree[info.id] = info
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return notebooks
|
|
150
|
-
}
|
|
10
|
+
const { getNotebooksObject } = require('../../lib/notebooks');
|
|
151
11
|
|
|
152
12
|
module.exports = ctx => {
|
|
153
|
-
const notebooks = getNotebooksObject(ctx)
|
|
154
|
-
ctx.theme.config.notebooks = notebooks
|
|
155
|
-
}
|
|
13
|
+
const notebooks = getNotebooksObject(ctx);
|
|
14
|
+
ctx.theme.config.notebooks = notebooks;
|
|
15
|
+
};
|
|
@@ -71,7 +71,7 @@ module.exports = hexo => {
|
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
hexo.utils = {
|
|
74
|
-
icon: (key, args) => {
|
|
74
|
+
icon: (key, args, inline) => {
|
|
75
75
|
const { icons } = hexo.theme.config
|
|
76
76
|
var result = ''
|
|
77
77
|
if (icons[key]) {
|
|
@@ -81,9 +81,18 @@ module.exports = hexo => {
|
|
|
81
81
|
}
|
|
82
82
|
if (result.startsWith('/') || result.startsWith('https://') || result.startsWith('http://')) {
|
|
83
83
|
return `<img ${args?.length > 0 ? args : ''} src="${result}" />`
|
|
84
|
+
} else if (inline === true) {
|
|
85
|
+
return result
|
|
86
|
+
} else if (result.startsWith('<svg')) {
|
|
87
|
+
// 非首屏图标:输出占位符,由 /js/icons.js 异步拉取 js/icons/{ns}.json 后原位替换
|
|
88
|
+
return `<svg class="icon" data-icon="${key}" aria-hidden="true"></svg>`
|
|
84
89
|
} else {
|
|
85
90
|
return result
|
|
86
91
|
}
|
|
92
|
+
},
|
|
93
|
+
iconData: (key) => {
|
|
94
|
+
const { icons } = hexo.theme.config
|
|
95
|
+
return icons && icons[key] ? icons[key] : ''
|
|
87
96
|
}
|
|
88
97
|
};
|
|
89
98
|
};
|
package/scripts/filters/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
hexo.extend.filter.register('after_render:html', require('./lib/img_lazyload').processSite);
|
|
4
4
|
hexo.extend.filter.register('after_render:html', require('./lib/img_onerror').processSite);
|
|
5
|
+
hexo.extend.filter.register('after_post_render', require('./lib/md_table').processPost);
|
|
5
6
|
|
|
6
7
|
function change_image(data) {
|
|
7
8
|
if (this.theme.config.tag_plugins.image.parse_markdown) {
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
// 懒加载强制开启:不再读取 enable 配置,no-lazy 是唯一例外
|
|
9
9
|
function lazyProcess(htmlContent) {
|
|
10
|
+
// 短路:无 <img 的页面(如 404)无需正则扫描
|
|
11
|
+
if (typeof htmlContent !== 'string' || !/<img/i.test(htmlContent)) {
|
|
12
|
+
return htmlContent;
|
|
13
|
+
}
|
|
10
14
|
return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function(imgTag, src_before, src_value, src_after) {
|
|
11
15
|
// 已由 tag 插件输出懒加载标记(data-src / data-srcset)的图片不重复处理
|
|
12
16
|
if (/data-src/gi.test(imgTag)) {
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
module.exports.processSite = function(htmlContent) {
|
|
9
|
+
// 短路:无 <img 的页面(如 404)无需正则扫描
|
|
10
|
+
if (typeof htmlContent !== 'string' || !/<img/i.test(htmlContent)) {
|
|
11
|
+
return htmlContent;
|
|
12
|
+
}
|
|
9
13
|
const default_image = this.theme.config.default.image;
|
|
10
14
|
return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function(imgTag) {
|
|
11
15
|
if (/="data:image(.*?)/gi.test(imgTag)) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* md_table.js v1 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
* 普通 Markdown 表格包一层 .md-table-scroll 横向滚动容器,
|
|
4
|
+
* 宽度足够时铺满容器,内容超出容器宽度时横向滚动。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const cheerio = require('cheerio');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 将内容中的普通 `<table>` 包一层 `.md-table-scroll` 容器。
|
|
13
|
+
* 排除代码高亮表格(.highlight)与 {% table %} 标签容器(.tag-plugin.table)。
|
|
14
|
+
*/
|
|
15
|
+
function wrapMdTables(htmlContent) {
|
|
16
|
+
// 短路:不含 <table 时无需解析,避免 cheerio 全量 parse + serialize
|
|
17
|
+
if (typeof htmlContent === 'string' && !htmlContent.includes('<table')) {
|
|
18
|
+
return htmlContent;
|
|
19
|
+
}
|
|
20
|
+
const $ = cheerio.load(htmlContent, null, false);
|
|
21
|
+
$('table').each(function () {
|
|
22
|
+
const $table = $(this);
|
|
23
|
+
if ($table.parent().is('.md-table-scroll')) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if ($table.closest('.highlight').length) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if ($table.closest('.tag-plugin.table').length) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
$table.wrap('<div class="md-table-scroll"></div>');
|
|
33
|
+
});
|
|
34
|
+
return $.html();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports.wrapMdTables = wrapMdTables;
|
|
38
|
+
|
|
39
|
+
function processPost(data) {
|
|
40
|
+
if (data.content) {
|
|
41
|
+
data.content = wrapMdTables(data.content);
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports.processPost = processPost;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* https://github.com/wzpan/hexo-generator-search
|
|
3
3
|
*/
|
|
4
|
-
const { stripHTML } = require('hexo-util')
|
|
5
4
|
const { normalize_path } = require('../lib/path_utils')
|
|
5
|
+
const { buildSearchIndex } = require('../lib/search_index')
|
|
6
6
|
|
|
7
7
|
hexo.extend.generator.register('search_json_generator', function (locals) {
|
|
8
8
|
if (this.theme.config.search.service != 'local_search') { return {} }
|
|
@@ -33,19 +33,11 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
|
|
|
33
33
|
temp_post.path = path === '/' ? '/' : path + '/'
|
|
34
34
|
}
|
|
35
35
|
if (cfg.content != false && post.content) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// 去除HTML实体
|
|
42
|
-
content = content.replace(/&[^\s;]+;/g, "")
|
|
43
|
-
// 换行符换成空格
|
|
44
|
-
content = content.replace(/\\n/g, ' ')
|
|
45
|
-
content = content.replace(/\n/g, ' ')
|
|
46
|
-
// 多个连续空格换成单个空格
|
|
47
|
-
content = content.replace(/[\s]{2,}/g, ' ')
|
|
48
|
-
temp_post.content = content.trim()
|
|
36
|
+
const { content, anchors } = buildSearchIndex(post.content)
|
|
37
|
+
temp_post.content = content
|
|
38
|
+
if (anchors.length > 0) {
|
|
39
|
+
temp_post.anchors = anchors
|
|
40
|
+
}
|
|
49
41
|
}
|
|
50
42
|
if (post.tags && post.tags.length > 0) {
|
|
51
43
|
var tags = []
|
|
@@ -64,10 +56,12 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
|
|
|
64
56
|
return temp_post
|
|
65
57
|
}
|
|
66
58
|
|
|
59
|
+
// 循环外编译一次 skip_search 正则,避免每个 post/page 重复 new RegExp
|
|
60
|
+
const skipSearchPatterns = (cfg.skip_search || []).map(pattern => new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'));
|
|
61
|
+
|
|
67
62
|
function matchAndExit(path, patterns) {
|
|
68
63
|
for (let pattern of patterns) {
|
|
69
|
-
|
|
70
|
-
if (path.match(regexPattern)) {
|
|
64
|
+
if (path.match(pattern)) {
|
|
71
65
|
// console.log("Matched pattern:", pattern);
|
|
72
66
|
return true;
|
|
73
67
|
}
|
|
@@ -79,7 +73,7 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
|
|
|
79
73
|
posts.each(function(post) {
|
|
80
74
|
var layout_list = ["post"]
|
|
81
75
|
if (!layout_list.includes(post.layout)) return
|
|
82
|
-
if (cfg.skip_search && matchAndExit(post.path,
|
|
76
|
+
if (cfg.skip_search && matchAndExit(post.path, skipSearchPatterns)) return
|
|
83
77
|
if (post.indexing == false) return
|
|
84
78
|
let temp_post = generateJson(post)
|
|
85
79
|
res.push(temp_post)
|
|
@@ -89,7 +83,7 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
|
|
|
89
83
|
pages.each(function(page) {
|
|
90
84
|
var layout_list = ["page", "wiki"]
|
|
91
85
|
if (!layout_list.includes(page.layout)) return
|
|
92
|
-
if (cfg.skip_search && matchAndExit(page.path,
|
|
86
|
+
if (cfg.skip_search && matchAndExit(page.path, skipSearchPatterns)) return
|
|
93
87
|
if (page.indexing == false) return
|
|
94
88
|
let temp_post = generateJson(page)
|
|
95
89
|
res.push(temp_post)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// 构建期生成客户端图标白名单文件:避免把 SVG 图标数据内联进每个页面的 defines。
|
|
5
|
+
// 与 layout/_partial/scripts/defines.ejs 中旧的白名单保持一致(客户端通过 ctx.icons 读取)。
|
|
6
|
+
hexo.extend.generator.register('stellar_icons', function () {
|
|
7
|
+
const icons = (this.theme.config && this.theme.config.icons) || {};
|
|
8
|
+
const keys = ['weibo:comment', 'default:loading-spinner', 'default:warning', 'weibo:repeat', 'weibo:like'];
|
|
9
|
+
const out = {};
|
|
10
|
+
// 去除 SVG 注释(避免 <!-- / </script> 解析风险),再转义 <
|
|
11
|
+
for (const k of keys) {
|
|
12
|
+
out[k] = ((icons[k]) || '').replace(/<!--[\s\S]*?-->/g, '');
|
|
13
|
+
}
|
|
14
|
+
const json = JSON.stringify(out).replace(/</g, '\\u003c');
|
|
15
|
+
return {
|
|
16
|
+
path: 'js/stellar-icons.js',
|
|
17
|
+
data: 'window.stellarIcons = ' + json + ';\nif (typeof ctx !== "undefined" && ctx) { ctx.icons = window.stellarIcons; }\n'
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 构建期生成按命名空间拆分的图标数据文件(js/icons/{ns}.json):
|
|
22
|
+
// 供客户端加载器 /js/icons.js 对非首屏图标占位符(svg.icon[data-icon])做异步原位替换。
|
|
23
|
+
// 数据源为合并后的 theme.config.icons(主题 _data/icons.yml + 站点 source/_data/icons.yml 覆盖),
|
|
24
|
+
// 仅收集内联 SVG 值(URL 值由服务端直接输出 <img>,无需下发)。
|
|
25
|
+
hexo.extend.generator.register('stellar_icon_sets', function () {
|
|
26
|
+
const icons = (this.theme.config && this.theme.config.icons) || {};
|
|
27
|
+
const sets = {};
|
|
28
|
+
for (const key of Object.keys(icons)) {
|
|
29
|
+
const value = icons[key];
|
|
30
|
+
if (typeof value !== 'string' || !value.trim().startsWith('<svg')) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const ns = key.split(':')[0];
|
|
34
|
+
if (ns.length === 0) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!sets[ns]) {
|
|
38
|
+
sets[ns] = {};
|
|
39
|
+
}
|
|
40
|
+
// 去除 SVG 注释,避免注释内容在注入 HTML 时引发解析问题
|
|
41
|
+
sets[ns][key] = value.replace(/<!--[\s\S]*?-->/g, '');
|
|
42
|
+
}
|
|
43
|
+
return Object.keys(sets).map((ns) => ({
|
|
44
|
+
path: `js/icons/${ns}.json`,
|
|
45
|
+
data: JSON.stringify({ [ns]: sets[ns] })
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* global hexo */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { buildAiLabel, resolveAiKey } = require('../lib/ai_label');
|
|
5
|
+
|
|
6
|
+
// 文章 AI 成分标签:front-matter 用 ai_label: manual / reviewed / polished / generated 标记,
|
|
7
|
+
// 未设置时取配置 article.ai_label.default(为空则不渲染);
|
|
8
|
+
// 文案由多语言系统提供(languages/*.yml 的 meta.ai_label.*,缺失时不渲染),
|
|
9
|
+
// 颜色与可选图标由配置 article.ai_label 提供(默认在主题 _config.yml,站点可覆盖)。
|
|
10
|
+
hexo.extend.helper.register('ai_label', function(value, extraClass, noColor) {
|
|
11
|
+
const article = hexo.theme.config.article;
|
|
12
|
+
const labelConfig = article && article.ai_label;
|
|
13
|
+
const key = resolveAiKey(value, labelConfig);
|
|
14
|
+
if (key && (!labelConfig || !labelConfig[key])) {
|
|
15
|
+
console.warn('[ai_label] 未知的 ai_label 值: ' + key);
|
|
16
|
+
}
|
|
17
|
+
if (!key) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
const def = labelConfig && labelConfig[key];
|
|
21
|
+
const iconHtml = (def && def.icon) ? hexo.utils.icon(def.icon) : '';
|
|
22
|
+
const __ = (this && typeof this.__ === 'function') ? this.__ : hexo.theme.i18n.__(hexo.config.language);
|
|
23
|
+
const i18nKey = 'meta.ai_label.' + key;
|
|
24
|
+
const text = __(i18nKey);
|
|
25
|
+
if (!text || text === i18nKey) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return buildAiLabel(key, labelConfig, text, extraClass, iconHtml, noColor);
|
|
29
|
+
});
|
package/scripts/helpers/icon.js
CHANGED