hexo-theme-stellar 1.39.0 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.39.1
4
+
5
+ > 发布日期:2026-08-13
6
+
7
+ ### 新功能
8
+ - 置顶文章新增平铺样式配置 `article.pin_style: flat`:首页第一页不再渲染置顶轮播,置顶文章按与轮播一致的排序权重在文章列表靠前平铺展示
9
+
10
+ ### 修复
11
+ - 用户内容统一 HTML 转义并移除圆点 aria-label,修复标题含引号导致构建失败
12
+
13
+ ### 升级注意(配置变更与破坏性改动)
14
+ - 新增 `article.pin_style`(`carousel` / `flat`,默认 `carousel`):默认行为不变;设置 `flat` 后首页置顶文章改为平铺样式展示
15
+
16
+ Full Changelog: [1.39.0...1.39.1](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.39.0...1.39.1)
17
+
3
18
  ## 1.39.0
4
19
 
5
20
  > 发布日期:2026-08-13
package/_config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  ######## Stellar info ########
2
2
  stellar:
3
- version: '1.39.0'
3
+ version: '1.39.1'
4
4
  homepage: 'https://xaoxuu.com/wiki/stellar/'
5
5
  repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
6
6
  main_css: /css/main.css
@@ -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 中覆盖此参数
@@ -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;
@@ -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(&quot;qrcode-wechat&quot;)"';
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 + '&amp;'
123
- el += 'body=' + page.permalink + '"';
125
+ el += ' href="mailto:?subject=' + urlenc(page.title + ' - ' + config.title) + '&amp;'
126
+ el += 'body=' + urlenc(page.permalink) + '"';
124
127
  } else if (item == 'link') {
125
- el += ' onclick="util.copy(&quot;copy-link&quot;, &quot;' + __('message.copied') + '&quot;)"';
128
+ el += ' onclick="util.copy(&quot;copy-link&quot;, &quot;' + escape_html(__('message.copied')) + '&quot;)"';
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>
@@ -1,7 +1,10 @@
1
1
  <%
2
2
  function layoutDiv() {
3
3
  var el = '';
4
- el += partial('_partial/main/pin_slider', {type: 'post'});
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
- var label = item.title || item.name || item.id || (si + 1)
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,12 @@ 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
- // 置顶文章已由列表页 navbar 上方的置顶轮播展示,卡片内不再显示 pin 图标。
95
+ // 置顶文章由轮播(默认)或首页列表靠前展示(flat 平铺)承担,卡片内不显示 pin 图标。
96
96
  el += '</div>';
97
97
  el += '</article>';
98
98
  return el;
@@ -107,22 +107,22 @@ function div_photo() {
107
107
  position = 'bottom';
108
108
  }
109
109
  }
110
- el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + obj.image.replace(/'/g, '%27') + '\')">';
111
- el += '<img src="' + obj.image + '"/>';
110
+ el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + escape_html(obj.image.replace(/'/g, '%27')) + '\')">';
111
+ el += '<img src="' + escape_html(obj.image) + '"/>';
112
112
  if (position.length > 0) {
113
113
  el += '<div class="cover-info" position="' + position + '"';
114
114
  if (obj.color) {
115
- el += 'style="color:' + obj.color + '"';
115
+ el += 'style="color:' + escape_html(obj.color) + '"';
116
116
  }
117
117
  el += '>';
118
118
  if (obj.topic) {
119
- el += '<div class="text topic">' + obj.topic + '</div>';
119
+ el += '<div class="text topic">' + escape_html(obj.topic) + '</div>';
120
120
  }
121
121
  if (obj.headline) {
122
- el += '<div class="text headline">' + obj.headline + '</div>';
122
+ el += '<div class="text headline">' + escape_html(obj.headline) + '</div>';
123
123
  }
124
124
  if (obj.caption) {
125
- el += '<div class="text caption">' + obj.caption + '</div>';
125
+ el += '<div class="text caption">' + escape_html(obj.caption) + '</div>';
126
126
  }
127
127
  el += '</div>';
128
128
 
@@ -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 {
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 pinSliderActive = is_home_first_page()
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
- page.posts.each(function(post){
53
- if (post.indexing != false) {
54
- if (pinSliderActive) {
55
- var pinVal = post.pin != null ? post.pin : post.sticky
56
- if (pinVal !== false && pinVal != null) {
57
- return
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
- el += layout_post_card('post', post, partial(post));
61
- }
62
- })
117
+ })
118
+ }
63
119
  el += '</div>';
64
120
  return el;
65
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.39.0",
3
+ "version": "1.39.1",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {
@@ -0,0 +1,9 @@
1
+ /* global hexo */
2
+ 'use strict';
3
+
4
+ const { escapeHTML } = require('hexo-util');
5
+
6
+ // 供 EJS 模板使用的 HTML 转义辅助函数:属性值与文本内容均可安全使用。
7
+ hexo.extend.helper.register('escape_html', function(value) {
8
+ return escapeHTML(value == null ? '' : String(value));
9
+ });
@@ -6,6 +6,10 @@
6
6
 
7
7
  var util = require('hexo-util');
8
8
 
9
+ function esc(v) {
10
+ return util.escapeHTML(v == null ? '' : String(v))
11
+ }
12
+
9
13
  hexo.extend.helper.register('popular_posts_wrapper', function(args){
10
14
  const title = args.title;
11
15
  const json = args.json.json;
@@ -18,7 +22,7 @@ hexo.extend.helper.register('popular_posts_wrapper', function(args){
18
22
  var returnHTML = "";
19
23
  var div = `
20
24
  <section class='header'>
21
- <div class='title cap theme'>${title}</div>
25
+ <div class='title cap theme'>${esc(title)}</div>
22
26
  </section>
23
27
  <section class='body'>
24
28
  `;
@@ -28,14 +32,14 @@ hexo.extend.helper.register('popular_posts_wrapper', function(args){
28
32
 
29
33
  function listItem(obj){
30
34
  var el = '';
31
- el += '<a class="item" href="' + obj.path + '" title="' + obj.title + '">';
35
+ el += '<a class="item" href="' + esc(obj.path) + '" title="' + esc(obj.title) + '">';
32
36
  var p = posts.filter(p => root + p.path == obj.path)
33
37
  if (p && p.length > 0) {
34
38
  p = p.data[0];
35
39
  }
36
- el += '<span class="title">' + obj.title + '</span>';
40
+ el += '<span class="title">' + esc(obj.title) + '</span>';
37
41
  if (obj.excerpt && obj.excerpt.length > 0) {
38
- el += '<span class="excerpt">' + util.truncate(util.stripHTML(obj.excerpt), {length: 120}) + '</span>';
42
+ el += '<span class="excerpt">' + esc(util.truncate(util.stripHTML(obj.excerpt), {length: 120})) + '</span>';
39
43
  }
40
44
  el += '</a>';
41
45
  return el;
@@ -47,7 +51,7 @@ hexo.extend.helper.register('popular_posts_wrapper', function(args){
47
51
  }
48
52
  }
49
53
 
50
- if (returnHTML != "") returnHTML = "<div class=\"" + cls + "\">" + returnHTML + "</div>";
54
+ if (returnHTML != "") returnHTML = "<div class=\"" + esc(cls) + "\">" + returnHTML + "</div>";
51
55
  div += returnHTML;
52
56
  div += '</section>';
53
57
  return div;