hexo-theme-stellar 1.42.0 → 1.43.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 -0
- package/CHANGELOG.md +42 -0
- package/THIRD-PARTY-NOTICES.md +28 -0
- package/_config.yml +20 -10
- package/_data/icons.yml +10 -9
- package/languages/en.yml +11 -0
- package/languages/zh-CN.yml +11 -0
- package/languages/zh-TW.yml +11 -0
- package/layout/_partial/comments/artalk/layout.ejs +1 -1
- package/layout/_partial/comments/beaudar/layout.ejs +1 -1
- package/layout/_partial/comments/giscus/layout.ejs +1 -1
- package/layout/_partial/comments/twikoo/layout.ejs +1 -1
- package/layout/_partial/comments/utterances/layout.ejs +1 -1
- package/layout/_partial/comments/waline/layout.ejs +1 -1
- package/layout/_partial/components/collection-item.ejs +44 -0
- package/layout/_partial/components/collection.ejs +26 -0
- package/layout/_partial/components/widget-frame.ejs +35 -0
- package/layout/_partial/cover/wiki_cover.ejs +82 -26
- package/layout/_partial/dropdown.ejs +62 -0
- package/layout/_partial/head.ejs +9 -0
- package/layout/_partial/main/navbar/breadcrumb/blog.ejs +1 -1
- package/layout/_partial/main/post_list/archive_item.ejs +12 -0
- package/layout/_partial/main/post_list/topic_card.ejs +21 -9
- package/layout/_partial/main/post_list/wiki_card.ejs +49 -10
- package/layout/_partial/scripts/bootstrap.ejs +3 -1
- package/layout/_partial/scripts/defines.ejs +2 -8
- package/layout/_partial/scripts/lazyload.ejs +0 -3
- package/layout/_partial/scripts.ejs +1 -0
- package/layout/_partial/sidebar/index_leftbar.ejs +12 -2
- package/layout/_partial/sidebar/logo.ejs +1 -1
- package/layout/_partial/sidebar/menu.ejs +21 -23
- package/layout/_partial/widgets/components/linklist.ejs +23 -21
- package/layout/_partial/widgets/linklist.ejs +7 -12
- package/layout/_partial/widgets/recent.ejs +52 -41
- package/layout/_partial/widgets/related.ejs +47 -36
- package/layout/_partial/widgets/tagtree.ejs +70 -54
- package/layout/_partial/widgets/toc.ejs +37 -62
- package/layout/_partial/widgets/tree.ejs +38 -51
- package/layout/_plugins/index.ejs +25 -0
- package/layout/_plugins/mermaid.ejs +3 -4
- package/layout/archive.ejs +11 -20
- package/layout/collection-preview.ejs +60 -0
- package/layout/index_topic.ejs +12 -6
- package/layout/index_wiki.ejs +49 -3
- package/layout/layout.ejs +4 -3
- package/package.json +1 -1
- package/scripts/events/index.js +0 -2
- package/scripts/filters/lib/img_lazyload.js +184 -40
- package/scripts/generators/stellar-icons.js +1 -1
- package/scripts/lib/subtitle.js +5 -1
- package/scripts/tags/index.js +1 -0
- package/scripts/tags/lib/about.js +1 -1
- package/scripts/tags/lib/albums.js +1 -1
- package/scripts/tags/lib/banner.js +1 -1
- package/scripts/tags/lib/dropdown.js +64 -0
- package/scripts/tags/lib/friends.js +1 -1
- package/scripts/tags/lib/gallery.js +3 -3
- package/scripts/tags/lib/image.js +1 -1
- package/scripts/tags/lib/posters.js +1 -1
- package/scripts/tags/lib/sites.js +8 -4
- package/source/css/_common/base.styl +4 -4
- package/source/css/_common/blockquote.styl +2 -2
- package/source/css/_common/dropdown.styl +132 -0
- package/source/css/_common/html.styl +1 -1
- package/source/css/_common/loading.styl +5 -2
- package/source/css/_common/title.styl +8 -8
- package/source/css/_components/collection.styl +188 -0
- package/source/css/_components/layout.styl +1 -1
- package/source/css/_components/list.styl +44 -46
- package/source/css/_components/md.styl +3 -3
- package/source/css/_components/page-transition.styl +22 -0
- package/source/css/_components/pages/archives.styl +19 -18
- package/source/css/_components/pages/article-indent.styl +2 -2
- package/source/css/_components/pages/article-story.styl +9 -5
- package/source/css/_components/pages/collection-preview.styl +68 -0
- package/source/css/_components/pages/notebook.styl +11 -9
- package/source/css/_components/partial/article-footer.styl +7 -4
- package/source/css/_components/partial/cover.styl +280 -1
- package/source/css/_components/partial/related.styl +10 -6
- package/source/css/_components/sidebar/footer.styl +65 -12
- package/source/css/_components/sidebar/nav-area.styl +6 -1
- package/source/css/_components/sidebar/search.styl +6 -2
- package/source/css/_components/tag-plugins/chat.styl +1 -1
- package/source/css/_components/tag-plugins/folders.styl +4 -4
- package/source/css/_components/tag-plugins/folding.styl +3 -3
- package/source/css/_components/tag-plugins/hashtag.styl +2 -3
- package/source/css/_components/tag-plugins/mdrender.styl +1 -1
- package/source/css/_components/tag-plugins/note.styl +4 -4
- package/source/css/_components/tag-plugins/poetry.styl +5 -5
- package/source/css/_components/tag-plugins/quot.styl +1 -2
- package/source/css/_components/tag-plugins/quote.styl +1 -1
- package/source/css/_components/tag-plugins/tabs.styl +2 -2
- package/source/css/_components/tag-plugins/timeline.styl +4 -4
- package/source/css/_components/widgets/ghrepo.styl +4 -0
- package/source/css/_components/widgets/markdown.styl +1 -14
- package/source/css/_components/widgets/timeline.styl +2 -2
- package/source/css/_components/widgets/toc.styl +22 -20
- package/source/css/_components/widgets/widgets.styl +13 -1
- package/source/css/_components/wiki-card.styl +209 -0
- package/source/css/_custom.styl +22 -20
- package/source/css/_plugins/lazyload.styl +8 -5
- package/source/css/comments/utterances.styl +2 -2
- package/source/css/comments/waline.styl +24 -0
- package/source/css/plugins/mermaid.styl +31 -12
- package/source/js/icons.js +1 -1
- package/source/js/main.js +62 -6
- package/source/js/plugins/adaptive-text.js +46 -2
- package/source/js/plugins/dropdown.js +435 -0
- package/source/js/plugins/galaxy.js +423 -0
- package/source/js/search/algolia-search.js +1 -1
- package/source/js/search/local-search.js +1 -1
- package/source/js/services/ghinfo.js +31 -3
- package/source/js/services/siteinfo.js +26 -0
- package/source/js/services/sites.js +6 -2
- package/source/js/services.js +5 -2
- package/source/js/tagtree.js +7 -7
- package/source/js/utils.js +1 -1
- package/layout/_partial/widgets/components/link.ejs +0 -21
- package/source/css/_components/sidebar/menu.styl +0 -77
- package/source/css/_components/widgets/components.styl +0 -68
- package/source/css/_components/widgets/list.styl +0 -46
- package/source/css/_components/widgets/related.styl +0 -40
package/layout/archive.ejs
CHANGED
|
@@ -24,34 +24,25 @@ function layoutArchiveList() {
|
|
|
24
24
|
}
|
|
25
25
|
el += `<div class="post-list${cls} archives">`
|
|
26
26
|
var years = []
|
|
27
|
-
|
|
27
|
+
var posts = page.author != null ? site.posts.filter(function(post) {
|
|
28
|
+
return (post.author || theme.default_author.id) == page.author.id
|
|
29
|
+
}) : site.posts
|
|
28
30
|
posts.sort('date', -1).each(function(post) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
post.year = date(post.date, 'YYYY')
|
|
32
|
+
if (post.year && (years.includes(post.year) == false) && (post.title || post.date)) {
|
|
33
|
+
years.push(post.year)
|
|
32
34
|
}
|
|
33
35
|
})
|
|
34
|
-
for (
|
|
36
|
+
for (var year of years) {
|
|
35
37
|
el += `<article class="${scrollreveal('')}" id="archive">`
|
|
36
|
-
|
|
38
|
+
el += `<div class='archive-header h4'>${year}</div>`
|
|
37
39
|
posts.sort('date', -1).filter(function (post) {
|
|
38
40
|
post.year = date(post.date, 'YYYY')
|
|
39
41
|
return post.year == year
|
|
40
42
|
}).each(function(post) {
|
|
41
|
-
el +=
|
|
42
|
-
el +=`<a class='post fs14' href="${pretty_url(post.link || post.path)}">`
|
|
43
|
-
el += `<time>${date(post.date, 'MM-DD')}</time>`
|
|
44
|
-
el += `<span>`
|
|
45
|
-
if (post.title) {
|
|
46
|
-
el += post.title
|
|
47
|
-
} else if (post.date) {
|
|
48
|
-
el += date(post.date, config.date_format)
|
|
49
|
-
}
|
|
50
|
-
el += `</span>`
|
|
51
|
-
el += `</a>`
|
|
52
|
-
el += `</div>`
|
|
43
|
+
el += partial('_partial/main/post_list/archive_item', {post: post})
|
|
53
44
|
})
|
|
54
|
-
|
|
45
|
+
el += `</article>`
|
|
55
46
|
}
|
|
56
47
|
el += `</div>`
|
|
57
48
|
return el
|
|
@@ -65,4 +56,4 @@ function layoutDiv() {
|
|
|
65
56
|
}
|
|
66
57
|
%>
|
|
67
58
|
|
|
68
|
-
<%- layoutDiv() %>
|
|
59
|
+
<%- layoutDiv() %>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<%
|
|
2
|
+
function renderCollection(options) {
|
|
3
|
+
return partial('_partial/components/collection', {options: options})
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function renderWidget(title, options) {
|
|
7
|
+
return partial('_partial/components/widget-frame', {
|
|
8
|
+
title: title,
|
|
9
|
+
body: renderCollection(options),
|
|
10
|
+
reveal: false
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderSurface(name, surface, width) {
|
|
15
|
+
var navItems = [
|
|
16
|
+
{href: '#active', title: '当前页面', icon: 'default:documents', active: true},
|
|
17
|
+
{href: '#long', title: '这是一个用于检查单行省略和容器适应性的很长标题', icon: 'default:rightbar'},
|
|
18
|
+
{href: '#plain', title: '无图标条目'}
|
|
19
|
+
]
|
|
20
|
+
var gridItems = [
|
|
21
|
+
{href: '#one', title: '项目一', icon: 'default:documents', active: true},
|
|
22
|
+
{href: '#two', title: '项目二', icon: 'default:rightbar'},
|
|
23
|
+
{href: '#three', title: '项目三', icon: 'default:github'},
|
|
24
|
+
{href: '#four', title: '项目四', icon: 'default:rss'}
|
|
25
|
+
]
|
|
26
|
+
var summaryItems = [
|
|
27
|
+
{href: '#summary-one', title: '摘要条目', description: '两行说明用于检查文字层级、截断和不同 surface 下的卡片填充。'},
|
|
28
|
+
{href: '#summary-two', title: '第二个摘要条目', description: '正文宽度下仍复用同一 DOM 与状态变量。'}
|
|
29
|
+
]
|
|
30
|
+
var treeItems = [
|
|
31
|
+
{href: '#root', title: '一级节点', active: true},
|
|
32
|
+
{href: '#child', title: '二级节点', depth: 1, indicator: false},
|
|
33
|
+
{href: '#leaf', title: '三级节点', depth: 2, indicator: false}
|
|
34
|
+
]
|
|
35
|
+
var html = '<section class="collection-preview__surface" data-ui-surface="' + surface + '" data-preview-width="' + width + '">'
|
|
36
|
+
html += '<header><strong>' + escape_html(name) + '</strong><code>' + escape_html(surface) + ' · ' + width + 'px</code></header>'
|
|
37
|
+
html += renderWidget('紧凑列表', {layout: 'list', variant: 'nav', density: 'compact', items: navItems})
|
|
38
|
+
html += renderWidget('响应式网格', {layout: 'grid', variant: 'nav', columns: 2, items: gridItems})
|
|
39
|
+
html += renderWidget('摘要列表', {layout: 'list', variant: 'summary', items: summaryItems})
|
|
40
|
+
html += renderWidget('层级列表', {layout: 'list', variant: 'nav', density: 'compact', items: treeItems})
|
|
41
|
+
html += '</section>'
|
|
42
|
+
return html
|
|
43
|
+
}
|
|
44
|
+
%>
|
|
45
|
+
<article class="md-text collection-preview">
|
|
46
|
+
<h1>侧边栏集合组件预览</h1>
|
|
47
|
+
<p>此开发页面用于同时检查列表、网格和摘要条目在四种 surface、不同容器宽度及明暗主题下的表现。</p>
|
|
48
|
+
<div class="collection-preview__theme" aria-label="预览主题">
|
|
49
|
+
<span>预览主题</span>
|
|
50
|
+
<button type="button" onclick="document.documentElement.setAttribute('data-theme','light')">Light</button>
|
|
51
|
+
<button type="button" onclick="document.documentElement.setAttribute('data-theme','dark')">Dark</button>
|
|
52
|
+
<button type="button" onclick="document.documentElement.removeAttribute('data-theme')">Auto</button>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="collection-preview__grid">
|
|
55
|
+
<%- renderSurface('左栏 Glass', 'glass', 240) %>
|
|
56
|
+
<%- renderSurface('左栏 Card', 'card', 240) %>
|
|
57
|
+
<%- renderSurface('右侧栏', 'sidebar', 320) %>
|
|
58
|
+
<%- renderSurface('正文区域', 'content', 720) %>
|
|
59
|
+
</div>
|
|
60
|
+
</article>
|
package/layout/index_topic.ejs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<%
|
|
2
2
|
function layout_topic_list(partial) {
|
|
3
3
|
var el = ''
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
var publish_list = theme.topic.publish_list
|
|
5
|
+
var tree = theme.topic.tree
|
|
6
|
+
var topics = publish_list.map(function(id) {
|
|
7
|
+
return tree[id]
|
|
8
|
+
}).filter(function(topic) {
|
|
9
|
+
return topic != null
|
|
10
|
+
}).sort(function(a, b) {
|
|
11
|
+
var aDate = a.homepage && a.homepage.date ? new Date(a.homepage.date).getTime() : 0
|
|
12
|
+
var bDate = b.homepage && b.homepage.date ? new Date(b.homepage.date).getTime() : 0
|
|
13
|
+
return bDate - aDate
|
|
14
|
+
})
|
|
15
|
+
for (var topic of topics) {
|
|
10
16
|
el += `<div class="post-card-wrap${scrollreveal(' ')}">`
|
|
11
17
|
el += `<div class="post-card topic">`
|
|
12
18
|
el += partial(topic)
|
package/layout/index_wiki.ejs
CHANGED
|
@@ -13,14 +13,22 @@ function layout_wiki_list(partial) {
|
|
|
13
13
|
if (page.filter === false) {
|
|
14
14
|
// wikiList
|
|
15
15
|
el += `<div class="post-card-wrap${scrollreveal(' ')}">`
|
|
16
|
-
|
|
16
|
+
var coverStyle = ''
|
|
17
|
+
if (proj.cover) {
|
|
18
|
+
coverStyle = ` style="--cover-url:url('${escape_html(proj.cover.replace(/'/g, '%27'))}')"`
|
|
19
|
+
}
|
|
20
|
+
el += `<a class="wiki-card${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
|
|
17
21
|
el += partial(proj)
|
|
18
22
|
el += `</a>`
|
|
19
23
|
el += `</div>`
|
|
20
24
|
} else if (proj.tags && proj.tags.includes(page.tagName) === true) {
|
|
21
25
|
// filtered wikiList
|
|
22
26
|
el += `<div class="post-card-wrap${scrollreveal(' ')} filter">`
|
|
23
|
-
|
|
27
|
+
var coverStyle = ''
|
|
28
|
+
if (proj.cover) {
|
|
29
|
+
coverStyle = ` style="--cover-url:url('${escape_html(proj.cover.replace(/'/g, '%27'))}')"`
|
|
30
|
+
}
|
|
31
|
+
el += `<a class="wiki-card${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
|
|
24
32
|
el += partial(proj)
|
|
25
33
|
el += `</a>`
|
|
26
34
|
el += `</div>`
|
|
@@ -41,4 +49,42 @@ function layoutDiv() {
|
|
|
41
49
|
}
|
|
42
50
|
%>
|
|
43
51
|
|
|
44
|
-
<%- layoutDiv() %>
|
|
52
|
+
<%- layoutDiv() %>
|
|
53
|
+
<script>
|
|
54
|
+
// Wiki 覆盖层须同时等待封面原图和平均主题色完成,避免默认主题色短暂闪现。
|
|
55
|
+
function revealWikiCover(card) {
|
|
56
|
+
if (card._wikiImageReady && card._wikiOverlayReady && !card.classList.contains('no-cover')) {
|
|
57
|
+
card.classList.add('cover-loaded');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
document.addEventListener('wiki-overlay-ready', function(event) {
|
|
61
|
+
var card = event.target.closest('.wiki-card');
|
|
62
|
+
if (!card) return;
|
|
63
|
+
card._wikiOverlayReady = true;
|
|
64
|
+
revealWikiCover(card);
|
|
65
|
+
});
|
|
66
|
+
document.querySelectorAll('.wiki-card-cover:not(.no-cover) > img').forEach(function(img) {
|
|
67
|
+
var cover = img.parentElement;
|
|
68
|
+
var card = img.closest('.wiki-card');
|
|
69
|
+
if (!cover || !card) return;
|
|
70
|
+
var loaded = function() {
|
|
71
|
+
card._wikiImageReady = true;
|
|
72
|
+
revealWikiCover(card);
|
|
73
|
+
};
|
|
74
|
+
var failed = function() {
|
|
75
|
+
card._wikiImageReady = false;
|
|
76
|
+
card.classList.remove('cover-loaded');
|
|
77
|
+
card.classList.add('no-cover');
|
|
78
|
+
cover.classList.add('no-cover', 'cover-error');
|
|
79
|
+
};
|
|
80
|
+
img.addEventListener('load', loaded, { once: true });
|
|
81
|
+
img.addEventListener('error', failed, { once: true });
|
|
82
|
+
if (img.complete) {
|
|
83
|
+
if (img.naturalWidth > 0) {
|
|
84
|
+
loaded();
|
|
85
|
+
} else {
|
|
86
|
+
failed();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
</script>
|
package/layout/layout.ejs
CHANGED
|
@@ -49,19 +49,20 @@ if (theme.style.prefers_theme === 'auto') {
|
|
|
49
49
|
html += site_background
|
|
50
50
|
html += `<div id="l_cover">${partial('_partial/cover/index').trim()}</div>`
|
|
51
51
|
html += `<div class="l_body ${page_type}" id="start" layout="${page.layout}" type="${article_type}" ${indent ? 'text-indent' : ''}>`
|
|
52
|
-
|
|
52
|
+
var leftbarSurface = theme.style.leftbar?.['ui-style'] === 'card' ? 'card' : 'glass'
|
|
53
|
+
html += `<aside class="l_left${leftbarSurface === 'card' ? ' leftbar-card' : ''}" data-ui-surface="${leftbarSurface}">`
|
|
53
54
|
html += `<div class="sidebg"></div>`
|
|
54
55
|
html += `<div class="leftbar-container${theme.style.leftbar?.blur ? ' leftbar-blur' : ''}">`
|
|
55
56
|
html += partial('_partial/sidebar/index_leftbar')
|
|
56
57
|
html += `</div>`
|
|
57
58
|
html += `</aside>`
|
|
58
|
-
html += `<div class="l_main" id="main">`
|
|
59
|
+
html += `<div class="l_main" id="main" data-ui-surface="content">`
|
|
59
60
|
html += partial('_partial/sidebar/logo', {where: 'main'})
|
|
60
61
|
html += body
|
|
61
62
|
html += partial('_partial/main/footer')
|
|
62
63
|
html += `<div class="main-mask" onclick="sidebar.dismiss()"></div>`
|
|
63
64
|
html += `</div>`
|
|
64
|
-
html += `<aside class="l_right">`
|
|
65
|
+
html += `<aside class="l_right" data-ui-surface="sidebar">`
|
|
65
66
|
html += partial('_partial/sidebar/index_rightbar').trim()
|
|
66
67
|
html += `</aside>`
|
|
67
68
|
html += partial('_partial/menubtn')
|
package/package.json
CHANGED
package/scripts/events/index.js
CHANGED
|
@@ -41,7 +41,6 @@ hexo.on('ready', () => {
|
|
|
41
41
|
checkVersion(hexo, { useCache: true });
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
// 防止重复注册
|
|
46
45
|
let hasRun = false;
|
|
47
46
|
|
|
@@ -66,4 +65,3 @@ hexo.extend.filter.register('before_generate', async () => {
|
|
|
66
65
|
fixMarkdownImages(hexo); // 不用 await,因为是同步的
|
|
67
66
|
}
|
|
68
67
|
});
|
|
69
|
-
|
|
@@ -1,56 +1,200 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* img_lazyload.js
|
|
2
|
+
* img_lazyload.js v2 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
3
|
*
|
|
4
|
+
* 懒加载强制开启:不再读取 enable 配置,no-lazy 是唯一例外。
|
|
5
|
+
* v2:改为属性感知的 <img> 标签解析,仅在标签自身范围内读取 src,
|
|
6
|
+
* 并跳过 <script>/<style>/<!-- --> 区域,避免与「去属性引号」的 HTML
|
|
7
|
+
* 压缩器(如 hexo-minify removeAttributeQuotes)组合时正则跨标签越界,
|
|
8
|
+
* 误改写页内内联脚本中的 `s.src = "..."`。
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
"use strict";
|
|
7
12
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// 1×1 透明 PNG:懒加载占位图
|
|
14
|
+
const LOADING_IMG =
|
|
15
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=";
|
|
16
|
+
|
|
17
|
+
// 解析 <img ...> 标签内部属性,返回 [{name, value, raw, start, end}]
|
|
18
|
+
function parseAttrs(inner) {
|
|
19
|
+
const attrs = [];
|
|
20
|
+
const re = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
21
|
+
let m;
|
|
22
|
+
while ((m = re.exec(inner)) !== null) {
|
|
23
|
+
attrs.push({
|
|
24
|
+
name: m[1].toLowerCase(),
|
|
25
|
+
value: m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4] !== undefined ? m[4] : null,
|
|
26
|
+
raw: m[0],
|
|
27
|
+
start: m.index,
|
|
28
|
+
end: m.index + m[0].length,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return attrs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 处理单个 <img> 标签:加 lazy 类并替换 src 为占位图 + data-src
|
|
35
|
+
function processImgTag(imgTag) {
|
|
36
|
+
// 快速跳过:整标签已含懒加载标记 / 原生 srcset / no-lazy(兼容压缩前后写法)
|
|
37
|
+
if (/data-src/gi.test(imgTag)) {
|
|
38
|
+
return imgTag;
|
|
39
|
+
}
|
|
40
|
+
if (/srcset=/gi.test(imgTag)) {
|
|
41
|
+
return imgTag;
|
|
42
|
+
}
|
|
43
|
+
if (/\bno-lazy\b/gi.test(imgTag)) {
|
|
44
|
+
return imgTag;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selfClosing = /\/>$/.test(imgTag);
|
|
48
|
+
const inner = imgTag.slice(4, selfClosing ? imgTag.length - 2 : imgTag.length - 1);
|
|
49
|
+
const attrs = parseAttrs(inner);
|
|
50
|
+
const src = attrs.find((a) => a.name === "src");
|
|
51
|
+
// 无 src / 空 src / 已是占位图(data:image)时不处理
|
|
52
|
+
if (!src || src.value == null || src.value === "" || /^data:image/i.test(src.value)) {
|
|
53
|
+
return imgTag;
|
|
13
54
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
55
|
+
|
|
56
|
+
const classAttr = attrs.find((a) => a.name === "class");
|
|
57
|
+
const oldClass = classAttr && classAttr.value != null ? classAttr.value : "";
|
|
58
|
+
const newClass = (oldClass ? oldClass + " " : "") + "lazy";
|
|
59
|
+
|
|
60
|
+
let out = "<img";
|
|
61
|
+
if (!classAttr) {
|
|
62
|
+
out += ' class="' + newClass + '"';
|
|
63
|
+
}
|
|
64
|
+
let pos = 0;
|
|
65
|
+
for (const a of attrs) {
|
|
66
|
+
out += inner.slice(pos, a.start);
|
|
67
|
+
if (a.name === "src") {
|
|
68
|
+
out += 'src="' + LOADING_IMG + '" data-src="' + src.value + '"';
|
|
69
|
+
} else if (a.name === "class") {
|
|
70
|
+
out += 'class="' + newClass + '"';
|
|
71
|
+
} else {
|
|
72
|
+
out += a.raw;
|
|
18
73
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
74
|
+
pos = a.end;
|
|
75
|
+
}
|
|
76
|
+
out += inner.slice(pos);
|
|
77
|
+
out += selfClosing ? "/>" : ">";
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 查找标签结束位置(尊重属性值内的引号,避免把 > 截断进属性值)
|
|
82
|
+
function findTagEnd(html, start) {
|
|
83
|
+
let j = start;
|
|
84
|
+
let quote = null;
|
|
85
|
+
while (j < html.length) {
|
|
86
|
+
const ch = html[j];
|
|
87
|
+
if (quote) {
|
|
88
|
+
if (ch === quote) {
|
|
89
|
+
quote = null;
|
|
90
|
+
}
|
|
91
|
+
} else if (ch === '"' || ch === "'") {
|
|
92
|
+
quote = ch;
|
|
93
|
+
} else if (ch === ">") {
|
|
94
|
+
return j;
|
|
22
95
|
}
|
|
23
|
-
|
|
24
|
-
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isTagBoundary(html, pos) {
|
|
102
|
+
if (pos >= html.length) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const ch = html[pos];
|
|
106
|
+
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === ">" || ch === "/";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 全页扫描:只在真实 <img> 标签上做懒加载,跳过脚本/样式/注释内容
|
|
110
|
+
function processHtml(html) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
let i = 0;
|
|
113
|
+
const n = html.length;
|
|
114
|
+
const lower = html.toLowerCase();
|
|
115
|
+
while (i < n) {
|
|
116
|
+
const ch = html[i];
|
|
117
|
+
if (ch !== "<") {
|
|
118
|
+
parts.push(ch);
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
25
121
|
}
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
28
|
-
|
|
122
|
+
// HTML 注释
|
|
123
|
+
if (html.startsWith("<!--", i)) {
|
|
124
|
+
const end = html.indexOf("-->", i);
|
|
125
|
+
if (end === -1) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
parts.push(html.slice(i, end + 3));
|
|
129
|
+
i = end + 3;
|
|
130
|
+
continue;
|
|
29
131
|
}
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
132
|
+
// <script>…</script>:整段跳过,避免误处理模板字符串/字符串里的 <img
|
|
133
|
+
if (lower.startsWith("<script", i) && isTagBoundary(html, i + 7)) {
|
|
134
|
+
const end = findTagEnd(html, i);
|
|
135
|
+
if (end === -1) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
parts.push(html.slice(i, end + 1));
|
|
139
|
+
i = end + 1;
|
|
140
|
+
const close = lower.indexOf("</script", i);
|
|
141
|
+
if (close === -1) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const closeEnd = html.indexOf(">", close);
|
|
145
|
+
if (closeEnd === -1) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
parts.push(html.slice(i, closeEnd + 1));
|
|
149
|
+
i = closeEnd + 1;
|
|
150
|
+
continue;
|
|
33
151
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
newClass += 'lazy';
|
|
41
|
-
if (value) {
|
|
42
|
-
return ori.replace('class="' + value, 'class="' + newClass);
|
|
43
|
-
} else {
|
|
44
|
-
return ori.replace('class="', 'class="' + newClass);
|
|
152
|
+
// <style>…</style>:整段跳过
|
|
153
|
+
if (lower.startsWith("<style", i) && isTagBoundary(html, i + 6)) {
|
|
154
|
+
const end = findTagEnd(html, i);
|
|
155
|
+
if (end === -1) {
|
|
156
|
+
break;
|
|
45
157
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
158
|
+
parts.push(html.slice(i, end + 1));
|
|
159
|
+
i = end + 1;
|
|
160
|
+
const close = lower.indexOf("</style", i);
|
|
161
|
+
if (close === -1) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const closeEnd = html.indexOf(">", close);
|
|
165
|
+
if (closeEnd === -1) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
parts.push(html.slice(i, closeEnd + 1));
|
|
169
|
+
i = closeEnd + 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// <img …>
|
|
173
|
+
if (lower.startsWith("<img", i) && isTagBoundary(html, i + 4)) {
|
|
174
|
+
const end = findTagEnd(html, i);
|
|
175
|
+
if (end === -1) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
parts.push(processImgTag(html.slice(i, end + 1)));
|
|
179
|
+
i = end + 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
parts.push(ch);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
return parts.join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function lazyProcess(htmlContent) {
|
|
189
|
+
// 短路:无 <img 的页面(如 404)无需扫描
|
|
190
|
+
if (typeof htmlContent !== "string" || !/<img/i.test(htmlContent)) {
|
|
191
|
+
return htmlContent;
|
|
192
|
+
}
|
|
193
|
+
return processHtml(htmlContent);
|
|
52
194
|
}
|
|
53
195
|
|
|
54
|
-
module.exports.processSite = function(htmlContent) {
|
|
196
|
+
module.exports.processSite = function (htmlContent) {
|
|
55
197
|
return lazyProcess.call(this, htmlContent);
|
|
56
198
|
};
|
|
199
|
+
|
|
200
|
+
module.exports.lazyProcess = lazyProcess;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 与 layout/_partial/scripts/defines.ejs 中旧的白名单保持一致(客户端通过 ctx.icons 读取)。
|
|
6
6
|
hexo.extend.generator.register('stellar_icons', function () {
|
|
7
7
|
const icons = (this.theme.config && this.theme.config.icons) || {};
|
|
8
|
-
const keys = ['default:tocomment', 'default:
|
|
8
|
+
const keys = ['default:tocomment', 'default:warning', 'weibo:repeat', 'weibo:like'];
|
|
9
9
|
const out = {};
|
|
10
10
|
// 去除 SVG 注释(避免 <!-- / </script> 解析风险),再转义 <
|
|
11
11
|
for (const k of keys) {
|
package/scripts/lib/subtitle.js
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
const { stripHTML } = require('hexo-util');
|
|
4
4
|
|
|
5
5
|
// 文章一行小字取值(hero 卡片与置顶轮播共用,未来可复用于其它场景):
|
|
6
|
-
// subtitle
|
|
6
|
+
// subtitle(含 " | " 时取非空左侧)> description > excerpt/content 前 length 字(去 HTML、压缩空白、截断;省略号由 CSS 单行处理),空则返回空串。
|
|
7
7
|
function subtitle(post, length = 50) {
|
|
8
8
|
if (post == null) {
|
|
9
9
|
return '';
|
|
10
10
|
}
|
|
11
11
|
if (post.subtitle != null && String(post.subtitle).length > 0) {
|
|
12
|
+
const separator = String(post.subtitle).indexOf(' | ');
|
|
13
|
+
if (separator > 0) {
|
|
14
|
+
return String(post.subtitle).slice(0, separator);
|
|
15
|
+
}
|
|
12
16
|
return post.subtitle;
|
|
13
17
|
}
|
|
14
18
|
if (post.description != null && String(post.description).length > 0) {
|
package/scripts/tags/index.js
CHANGED
|
@@ -12,6 +12,7 @@ hexo.extend.tag.register('grid', require('./lib/grid')(hexo), true)
|
|
|
12
12
|
hexo.extend.tag.register('swiper', require('./lib/swiper')(hexo), true)
|
|
13
13
|
hexo.extend.tag.register('gallery', require('./lib/gallery')(hexo), {ends: true})
|
|
14
14
|
hexo.extend.tag.register('banner', require('./lib/banner')(hexo), true)
|
|
15
|
+
hexo.extend.tag.register('dropdown', require('./lib/dropdown')(hexo), true)
|
|
15
16
|
|
|
16
17
|
// data
|
|
17
18
|
hexo.extend.tag.register('users', require('./lib/friends')(hexo))
|
|
@@ -18,7 +18,7 @@ module.exports = ctx => function(args, content) {
|
|
|
18
18
|
el += '<div class="tag-plugin about">'
|
|
19
19
|
if (args.back) {
|
|
20
20
|
el += '<a class="nav-back cap" href="' + url_for(ctx.config.root) + '">'
|
|
21
|
-
el += ctx.utils.icon('default:
|
|
21
|
+
el += ctx.utils.icon('default:arrow-left')
|
|
22
22
|
el += '</a>'
|
|
23
23
|
}
|
|
24
24
|
// avatar
|
|
@@ -32,7 +32,7 @@ module.exports = ctx => function(args) {
|
|
|
32
32
|
el += `<div class="grid-cell album-card">`
|
|
33
33
|
el += `<a class="card-link lazy-box" target="_blank" rel="external nofollow noopener noreferrer" href="${item.url}">`
|
|
34
34
|
el += `<img class="lazy" data-src="${item.cover || item.icon || item.avatar || ctx.theme.config.default.cover}" onerror="javascript:this.removeAttribute("data-src");this.src="${ctx.theme.config.default.cover}";"/>`
|
|
35
|
-
el += `<div class="lazy-icon"
|
|
35
|
+
el += `<div class="lazy-icon"></div>`
|
|
36
36
|
el += `<div class="image-meta">`
|
|
37
37
|
if (item.title) {
|
|
38
38
|
el += `<span class="image-caption">${item.title}</span>`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dropdown.js v1 | https://github.com/xaoxuu/hexo-theme-stellar/
|
|
3
|
+
*
|
|
4
|
+
* {% dropdown [direction:up/down] [align:left/right] title %}
|
|
5
|
+
* - icon:key [title](url)
|
|
6
|
+
* {% enddropdown %}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict'
|
|
10
|
+
|
|
11
|
+
function parseItem(line) {
|
|
12
|
+
const matches = line.match(/^\s*(?:[-*]\s+)?(?:icon:([^\s]+)\s+)?\[([^\]]+)\]\(([^)\s]+)\)(?:\s+icon:([^\s]+))?\s*$/)
|
|
13
|
+
if (!matches) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
icon: matches[1] || matches[4],
|
|
18
|
+
title: matches[2],
|
|
19
|
+
url: matches[3]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = ctx => function(args, content = '') {
|
|
24
|
+
const { escapeHTML, url_for } = require('hexo-util')
|
|
25
|
+
args = ctx.args.map(args, ['icon', 'direction', 'align', 'open'], ['title'])
|
|
26
|
+
if (!args.title) {
|
|
27
|
+
return ''
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const items = content.split(/\r?\n/).map(parseItem).filter(Boolean)
|
|
31
|
+
if (items.length === 0) {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const direction = ['up', 'down'].includes(args.direction) ? args.direction : 'auto'
|
|
36
|
+
const align = ['left', 'right'].includes(args.align) ? args.align : 'auto'
|
|
37
|
+
const title = escapeHTML(args.title)
|
|
38
|
+
const toUrl = url_for.bind(ctx)
|
|
39
|
+
var el = `<details class="tag-plugin dropdown" direction="${direction}" align="${align}"`
|
|
40
|
+
if (args.open === 'true') {
|
|
41
|
+
el += ' open'
|
|
42
|
+
}
|
|
43
|
+
el += '>'
|
|
44
|
+
el += `<summary class="dropdown-trigger" title="${title}" aria-label="${title}">`
|
|
45
|
+
el += `<span>${title}</span>`
|
|
46
|
+
el += '<svg class="dropdown-arrow" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
|
47
|
+
el += '</summary>'
|
|
48
|
+
el += '<div class="dropdown-menu ui-collection" data-ui-surface="glass" data-layout="list" data-variant="nav" data-density="compact">'
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const url = String(item.url)
|
|
51
|
+
el += `<a class="dropdown-item ui-collection__item" href="${escapeHTML(toUrl(url))}`
|
|
52
|
+
if (url.includes('://')) {
|
|
53
|
+
el += '" target="_blank" rel="external nofollow noopener noreferrer">'
|
|
54
|
+
} else {
|
|
55
|
+
el += '" rel="noopener noreferrer">'
|
|
56
|
+
}
|
|
57
|
+
if (item.icon) {
|
|
58
|
+
el += `<span class="ui-collection__leading">${ctx.utils.icon(item.icon, 'no-lazy', true)}</span>`
|
|
59
|
+
}
|
|
60
|
+
el += `<span class="ui-collection__content"><span class="ui-collection__title">${escapeHTML(item.title)}</span></span></a>`
|
|
61
|
+
}
|
|
62
|
+
el += '</div></details>'
|
|
63
|
+
return el
|
|
64
|
+
}
|
|
@@ -29,7 +29,7 @@ module.exports = ctx => function(args) {
|
|
|
29
29
|
el += `<a class="card-link" target="_blank" rel="external nofollow noopener noreferrer" href="${item.url}">`
|
|
30
30
|
el += `<div class="lazy-box icon">`
|
|
31
31
|
el += `<img class="lazy" data-src="${item.icon || item.avatar || ctx.theme.config.default.avatar}" onerror="javascript:this.removeAttribute("data-src");this.src="${ctx.theme.config.default.avatar}";"/>`
|
|
32
|
-
el += `<div class="lazy-icon"
|
|
32
|
+
el += `<div class="lazy-icon"></div>`
|
|
33
33
|
el += `</div>`
|
|
34
34
|
el += `<div class="name">`
|
|
35
35
|
el += `<span>${item.title}</span>`
|