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.
Files changed (106) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_config.yml +33 -10
  3. package/_data/icons.yml +250 -37
  4. package/languages/en.yml +6 -0
  5. package/languages/zh-CN.yml +6 -0
  6. package/languages/zh-TW.yml +6 -0
  7. package/layout/_partial/comments/artalk/script.ejs +25 -8
  8. package/layout/_partial/comments/beaudar/script.ejs +1 -0
  9. package/layout/_partial/comments/twikoo/script.ejs +1 -0
  10. package/layout/_partial/comments/utterances/script.ejs +1 -0
  11. package/layout/_partial/comments/waline/script.ejs +1 -0
  12. package/layout/_partial/head.ejs +43 -7
  13. package/layout/_partial/main/article/article_tags.ejs +16 -0
  14. package/layout/_partial/main/navbar/article_banner.ejs +11 -1
  15. package/layout/_partial/main/notebook/note_card.ejs +1 -1
  16. package/layout/_partial/main/notebook/paginator.ejs +2 -2
  17. package/layout/_partial/main/pin_slider.ejs +2 -2
  18. package/layout/_partial/main/post_list/paginator.ejs +2 -2
  19. package/layout/_partial/menubtn.ejs +2 -2
  20. package/layout/_partial/scripts/defines.ejs +26 -2
  21. package/layout/_partial/scripts.ejs +30 -7
  22. package/layout/_partial/sidebar/logo.ejs +1 -1
  23. package/layout/_partial/sidebar/menu.ejs +1 -1
  24. package/layout/_partial/sidebar/search.ejs +1 -1
  25. package/layout/_partial/widgets/author.ejs +1 -1
  26. package/layout/_partial/widgets/components/link.ejs +2 -2
  27. package/layout/_partial/widgets/ghuser.ejs +1 -1
  28. package/layout/_partial/widgets/related.ejs +1 -1
  29. package/layout/_partial/widgets/toc.ejs +1 -1
  30. package/layout/_partial/widgets/tree.ejs +1 -1
  31. package/layout/_plugins/fancybox.ejs +4 -2
  32. package/layout/_plugins/mermaid.ejs +2 -1
  33. package/layout/_plugins/search/local_search.ejs +4 -3
  34. package/layout/_plugins/swiper.ejs +1 -0
  35. package/layout/categories.ejs +1 -1
  36. package/layout/page.ejs +3 -0
  37. package/layout/tags.ejs +1 -0
  38. package/package.json +1 -1
  39. package/scripts/events/lib/doc_tree.js +12 -208
  40. package/scripts/events/lib/notebooks.js +8 -148
  41. package/scripts/events/lib/utils.js +10 -1
  42. package/scripts/filters/index.js +1 -0
  43. package/scripts/filters/lib/img_lazyload.js +4 -0
  44. package/scripts/filters/lib/img_onerror.js +4 -0
  45. package/scripts/filters/lib/md_table.js +46 -0
  46. package/scripts/generators/search.js +12 -18
  47. package/scripts/generators/stellar-icons.js +47 -0
  48. package/scripts/helpers/ai_label.js +29 -0
  49. package/scripts/helpers/icon.js +2 -2
  50. package/scripts/helpers/json_ld.js +10 -13
  51. package/scripts/helpers/related_posts.js +0 -7
  52. package/scripts/helpers/seo.js +9 -0
  53. package/scripts/lib/ai_label.js +55 -0
  54. package/scripts/lib/doc_tree.js +267 -0
  55. package/scripts/lib/notebooks.js +181 -0
  56. package/scripts/lib/search_index.js +82 -0
  57. package/scripts/lib/seo.js +79 -0
  58. package/scripts/tags/lib/about.js +2 -2
  59. package/scripts/tags/lib/albums.js +1 -1
  60. package/scripts/tags/lib/banner.js +1 -1
  61. package/scripts/tags/lib/chat.js +49 -49
  62. package/scripts/tags/lib/copy.js +1 -1
  63. package/scripts/tags/lib/friends.js +1 -1
  64. package/scripts/tags/lib/gallery.js +1 -1
  65. package/scripts/tags/lib/hashtag.js +1 -1
  66. package/scripts/tags/lib/image.js +3 -3
  67. package/scripts/tags/lib/posters.js +1 -1
  68. package/scripts/tags/lib/sites.js +2 -2
  69. package/source/css/_common/base.styl +5 -9
  70. package/source/css/_common/device.styl +3 -3
  71. package/source/css/_components/md-table.styl +23 -0
  72. package/source/css/_components/pages/archives.styl +2 -22
  73. package/source/css/_components/pages/article-story.styl +4 -4
  74. package/source/css/_components/partial/article-tags.styl +10 -0
  75. package/source/css/_components/partial/bread-nav.styl +14 -0
  76. package/source/css/_components/partial/navbar.styl +10 -2
  77. package/source/css/_components/sidebar/search.styl +24 -4
  78. package/source/css/_components/tag-plugins/table.styl +16 -11
  79. package/source/css/_components/widgets/components.styl +6 -0
  80. package/source/css/_components/widgets/list.styl +6 -0
  81. package/source/css/_components/widgets/toc.styl +1 -1
  82. package/source/css/_custom.styl +1 -12
  83. package/source/css/_defines/func.styl +28 -3
  84. package/source/css/_plugins/index.styl +0 -18
  85. package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
  86. package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
  87. package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
  88. package/source/css/{_plugins → plugins}/swiper.styl +5 -1
  89. package/source/js/icons.js +60 -0
  90. package/source/js/main.js +114 -0
  91. package/source/js/search/highlight.js +82 -0
  92. package/source/js/search/local-search.js +294 -103
  93. package/source/js/services/timeline.js +1 -1
  94. package/source/js/services/weibo.js +3 -3
  95. package/source/js/services.js +179 -0
  96. package/source/js/tagtree.js +29 -0
  97. package/source/js/theme.js +61 -0
  98. package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +13 -10
  99. package/layout/_partial/scripts/services.ejs +0 -178
  100. package/layout/_partial/scripts/sidebar.ejs +0 -25
  101. package/layout/_partial/scripts/tagtree.ejs +0 -29
  102. package/layout/_partial/scripts/theme.ejs +0 -67
  103. /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
  104. /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
  105. /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
  106. /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 { normalize_path } = require('../../lib/path_utils');
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
- // wiki 配置
64
- var wiki = getWikiObject(ctx)
65
- const pages = ctx.locals.get('pages')
66
- // wiki 所有页面
67
- const wiki_pages = pages.filter(p => (p.wiki != null)).map(p => new WikiPage(p))
68
- const wiki_list = Object.keys(wiki.tree).filter(id => wiki_pages.some(p => p.wiki === id))
69
- // 上架的项目列表
70
- wiki.shelf = ctx.locals.get('data').wiki || []
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 { normalize_path } = require('../../lib/path_utils');
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
  };
@@ -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
- var content = stripHTML(post.content.replace(/<span class="line">\d+<\/span>/g, '')).trim()
37
- // 部分HTML标签
38
- content = content.replace(/<iframe[\s|\S]+iframe>/g, '')
39
- content = content.replace(/<hr>/g, '')
40
- content = content.replace(/<br>/g, '')
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
- const regexPattern = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
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, cfg.skip_search)) return
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, cfg.skip_search)) return
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
+ });
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
2
 
3
- hexo.extend.helper.register('icon', function(key, args) {
4
- return hexo.utils.icon(key, args)
3
+ hexo.extend.helper.register('icon', function(key, args, inline) {
4
+ return hexo.utils.icon(key, args, inline)
5
5
  })