hexo-theme-stellar 1.34.0 → 1.36.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 (64) hide show
  1. package/_config.yml +25 -19
  2. package/layout/_partial/comments/artalk/layout.ejs +2 -2
  3. package/layout/_partial/comments/artalk/script.ejs +3 -9
  4. package/layout/_partial/comments/beaudar/script.ejs +2 -8
  5. package/layout/_partial/comments/giscus/script.ejs +2 -8
  6. package/layout/_partial/comments/twikoo/layout.ejs +2 -2
  7. package/layout/_partial/comments/twikoo/script.ejs +3 -9
  8. package/layout/_partial/comments/utterances/script.ejs +2 -8
  9. package/layout/_partial/comments/waline/layout.ejs +1 -1
  10. package/layout/_partial/comments/waline/script.ejs +1 -7
  11. package/layout/_partial/head.ejs +0 -3
  12. package/layout/_partial/scripts/defines.ejs +118 -1
  13. package/layout/_partial/scripts/lazyload.ejs +7 -6
  14. package/layout/_partial/scripts/services.ejs +11 -13
  15. package/layout/_partial/scripts/utils.ejs +358 -66
  16. package/layout/_partial/widgets/tagtree.ejs +1 -1
  17. package/layout/_partial/widgets/toc.ejs +1 -4
  18. package/layout/_partial/widgets/tree.ejs +1 -1
  19. package/layout/_plugins/fancybox.ejs +23 -8
  20. package/layout/_plugins/scrollreveal.ejs +77 -13
  21. package/layout/layout.ejs +0 -1
  22. package/package.json +4 -2
  23. package/scripts/events/index.js +2 -1
  24. package/scripts/events/lib/doc_tree.js +7 -4
  25. package/scripts/events/lib/merge_posts.js +3 -1
  26. package/scripts/events/lib/notebooks.js +5 -3
  27. package/scripts/events/lib/path_normalize.js +22 -0
  28. package/scripts/filters/lib/img_lazyload.js +9 -9
  29. package/scripts/generators/search.js +3 -1
  30. package/scripts/helpers/json_ld.js +3 -2
  31. package/scripts/helpers/pretty_url.js +9 -10
  32. package/scripts/lib/path_utils.js +24 -0
  33. package/scripts/tags/lib/emoji.js +17 -2
  34. package/scripts/tags/lib/image.js +13 -18
  35. package/source/css/_common/html.styl +0 -2
  36. package/source/css/_components/widgets/toc.styl +2 -2
  37. package/source/css/_plugins/index.styl +0 -3
  38. package/source/css/_plugins/lazyload.styl +40 -26
  39. package/source/css/_plugins/scrollreveal.styl +5 -1
  40. package/source/js/main.js +157 -73
  41. package/source/js/search/algolia-search.js +62 -62
  42. package/source/js/search/local-search.js +37 -36
  43. package/source/js/services/artalk_latest_comment.js +5 -7
  44. package/source/js/services/contributors.js +4 -6
  45. package/source/js/services/fcircle.js +4 -6
  46. package/source/js/services/friends.js +4 -6
  47. package/source/js/services/friends_and_posts.js +4 -6
  48. package/source/js/services/ghinfo.js +6 -8
  49. package/source/js/services/giscus_latest_comment.js +4 -6
  50. package/source/js/services/mdrender.js +2 -2
  51. package/source/js/services/memos.js +3 -4
  52. package/source/js/services/rss.js +10 -12
  53. package/source/js/services/siteinfo.js +24 -26
  54. package/source/js/services/sites.js +4 -6
  55. package/source/js/services/timeline.js +4 -6
  56. package/source/js/services/twikoo_latest_comment.js +5 -7
  57. package/source/js/services/waline_latest_comment.js +4 -6
  58. package/source/js/services/weibo.js +4 -6
  59. package/CLAUDE.md +0 -217
  60. package/docs/audits/2026-08-08-stellar-analysis.md +0 -267
  61. package/docs/release-process.md +0 -59
  62. package/layout/_plugins/pjax.ejs +0 -8
  63. package/source/css/_plugins/pjax.styl +0 -61
  64. package/source/js/plugins/pjax.js +0 -646
@@ -1,17 +1,81 @@
1
- <script defer src="<%- conf.js %>"></script>
2
1
  <script>
3
2
  utils.initPlugin(() => {
4
- const els = document.querySelectorAll('.slide-up');
5
- if (els.length > 0) {
6
- const slideUp = {
7
- distance: `<%- conf.distance %>`,
8
- duration: `<%- conf.duration %>`,
9
- interval: `<%- conf.interval %>`,
10
- scale: `<%- conf.scale %>`,
11
- opacity: 0,
12
- easing: "ease-out"
13
- };
14
- ScrollReveal().reveal('.slide-up', { ...slideUp });
15
- }
3
+ const els = Array.prototype.slice.call(document.querySelectorAll('.slide-up'));
4
+ if (els.length === 0) return;
5
+
6
+ // 吸顶/固定定位容器内的元素始终在视口内(或由容器控制显隐),
7
+ // ScrollReveal 按文档坐标判断可见性,会误判这类元素为不可见,
8
+ // 导致带锚点/恢复滚动位置打开页面时组件一直不显示。
9
+ // 解决方式:以最近的吸顶/固定祖先作为该组元素的容器,
10
+ // 使可见性判断恒成立,入场动画保留且不再依赖主文档滚动。
11
+ const findPinnedContainer = (el) => {
12
+ let node = el;
13
+ while (node && node.nodeType === 1) {
14
+ const pos = window.getComputedStyle(node).position;
15
+ if (pos === 'sticky' || pos === 'fixed') return node;
16
+ node = node.parentElement;
17
+ }
18
+ return null;
19
+ };
20
+
21
+ const pinnedGroups = new Map(); // 容器 -> 元素列表
22
+ const targets = [];
23
+ els.forEach((el) => {
24
+ const container = findPinnedContainer(el);
25
+ if (container) {
26
+ const group = pinnedGroups.get(container) || [];
27
+ group.push(el);
28
+ pinnedGroups.set(container, group);
29
+ } else {
30
+ targets.push(el);
31
+ }
32
+ });
33
+ if (pinnedGroups.size === 0 && targets.length === 0) return;
34
+
35
+ // 加载/初始化失败时兜底显示内容,避免整页空白
36
+ let fallbackApplied = false;
37
+ const revealFallback = () => {
38
+ if (fallbackApplied) return;
39
+ fallbackApplied = true;
40
+ document.documentElement.classList.add('sr-fallback');
41
+ };
42
+
43
+ const slideUp = {
44
+ distance: `<%- conf.distance %>`,
45
+ duration: `<%- conf.duration %>`,
46
+ interval: `<%- conf.interval %>`,
47
+ scale: `<%- conf.scale %>`,
48
+ opacity: 0,
49
+ easing: "ease-out"
50
+ };
51
+
52
+ // 看门狗:CDN 长时间无响应时强制显示内容
53
+ const watchdog = setTimeout(() => revealFallback(), 3000);
54
+
55
+ utils.js(`<%- conf.js %>`, { defer: true })
56
+ .then(() => {
57
+ if (fallbackApplied) return;
58
+ if (typeof ScrollReveal !== 'function') {
59
+ revealFallback();
60
+ return;
61
+ }
62
+ try {
63
+ const sr = ScrollReveal();
64
+ if (targets.length > 0) {
65
+ sr.reveal(targets, { ...slideUp });
66
+ }
67
+ pinnedGroups.forEach((group, container) => {
68
+ sr.reveal(group, { ...slideUp, container: container });
69
+ });
70
+ clearTimeout(watchdog);
71
+ } catch (err) {
72
+ console.error('[Plugin scrollreveal] 初始化失败:', err);
73
+ revealFallback();
74
+ }
75
+ })
76
+ .catch((err) => {
77
+ console.error('[Plugin scrollreveal] 加载失败:', err);
78
+ revealFallback();
79
+ });
16
80
  }, 'scrollreveal');
17
81
  </script>
package/layout/layout.ejs CHANGED
@@ -69,7 +69,6 @@ if (theme.style.prefers_theme === 'auto') {
69
69
  html += `<div class="scripts">`
70
70
  html += partial('_partial/scripts')
71
71
  html += `</div>`
72
- html += `<div class="page-loading-bar"><img src="${theme.default.loading}"></div>`
73
72
  html += `</body>`
74
73
  html += `</html>`
75
74
  %>
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {
7
- "test": "echo test"
7
+ "test": "echo test",
8
+ "release": "node release.js",
9
+ "release:dry": "node release.js --dry-run"
8
10
  },
9
11
  "repository": {
10
12
  "type": "git",
@@ -3,6 +3,8 @@
3
3
  'use strict';
4
4
 
5
5
  hexo.on('generateBefore', () => {
6
+ // 页面路径归一化:xxx.html → xxx/,必须先于所有读取 page.path 的逻辑
7
+ require('./lib/path_normalize')(hexo);
6
8
  // Merge config.
7
9
  require('./lib/config')(hexo);
8
10
  require('./lib/links')(hexo);
@@ -65,4 +67,3 @@ hexo.extend.filter.register('before_generate', async () => {
65
67
  }
66
68
  });
67
69
 
68
-
@@ -4,13 +4,15 @@
4
4
 
5
5
  'use strict';
6
6
 
7
+ const { normalize_path } = require('../../lib/path_utils');
8
+
7
9
  class WikiPage {
8
10
  constructor(page) {
9
11
  this.id = page._id
10
12
  this.wiki = page.wiki
11
13
  this.title = page.title
12
14
  this.path = page.path
13
- this.path_key = page.path.replace('.html', '')
15
+ this.path_key = normalize_path(page.path)
14
16
  this.layout = page.layout
15
17
  this.updated = page.updated
16
18
  }
@@ -107,7 +109,7 @@ module.exports = ctx => {
107
109
  for (let id of Object.keys(item.tree)) {
108
110
  const sec = item.tree[id]
109
111
  for (let key of sec) {
110
- let hs = sub_pages.filter(p => p.path_key == item.base_dir + key)
112
+ let hs = sub_pages.filter(p => p.path_key == normalize_path(item.base_dir + key))
111
113
  if (hs.length > 0) {
112
114
  homepage = hs[0]
113
115
  break
@@ -134,8 +136,9 @@ module.exports = ctx => {
134
136
  for (let title of Object.keys(item.tree)) {
135
137
  var sec = { title: title, pages: []}
136
138
  for (let key of item.tree[title]) {
137
- sec.pages = sec.pages.concat(sub_pages.filter(p => p.path_key == item.base_dir + key))
138
- others = others.filter(p => p.path_key != item.base_dir + key)
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)
139
142
  }
140
143
  sections.push(sec)
141
144
  }
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const { normalize_path } = require('../../lib/path_utils');
9
+
8
10
  class RelatedPage {
9
11
  constructor(page) {
10
12
  this.id = page._id
@@ -12,7 +14,7 @@ class RelatedPage {
12
14
  this.topic = page.topic
13
15
  this.title = page.title
14
16
  this.path = page.path
15
- this.path_key = page.path.replace('.html', '')
17
+ this.path_key = normalize_path(page.path)
16
18
  this.layout = page.layout
17
19
  this.date = page.date
18
20
  this.updated = page.updated
@@ -4,6 +4,8 @@
4
4
 
5
5
  'use strict'
6
6
 
7
+ const { normalize_path } = require('../../lib/path_utils');
8
+
7
9
  class NotePage {
8
10
  constructor(page) {
9
11
  this.id = page._id
@@ -11,7 +13,7 @@ class NotePage {
11
13
  this.title = page.title
12
14
  this.tags = page.tags
13
15
  this.path = page.path
14
- this.path_key = page.path.replace('.html', '')
16
+ this.path_key = normalize_path(page.path)
15
17
  this.layout = page.layout
16
18
  this.date = page.date
17
19
  this.updated = page.updated || page.date
@@ -39,8 +41,8 @@ function prepareNotebook(id, info, ctx) {
39
41
  if (notebook.base_dir.startsWith('/')) {
40
42
  notebook.base_dir = notebook.base_dir.substring(1)
41
43
  }
42
- if (notebook.base_dir.endsWith('/')) {
43
- notebook.base_dir = notebook.base_dir.substring(0, notebook.base_dir.length - 1)
44
+ if (notebook.base_dir.length > 1 && !notebook.base_dir.endsWith('/')) {
45
+ notebook.base_dir = notebook.base_dir + '/'
44
46
  }
45
47
  } else {
46
48
  const notebooksBaseDir = ctx.theme.config.site_tree.notebooks.base_dir
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 页面路径归一化:将 xxx.html 归一为目录形式 xxx/
5
+ * 必须在 generateBefore 最早阶段执行(先于 doc_tree / notebooks 等读取 page.path),
6
+ * 直接改写 Page model 存储,保证 page.permalink、JSON-LD、sitemap、search.json
7
+ * 与 canonical(尾斜杠格式)一致。
8
+ */
9
+ module.exports = ctx => {
10
+ const data = ctx.model('Page').data || {}
11
+ Object.values(data).forEach(page => {
12
+ if (!page || typeof page.path !== 'string') {
13
+ return
14
+ }
15
+ if (page.layout === false || page.layout === 'false') {
16
+ return
17
+ }
18
+ if (page.path.endsWith('.html') && !page.path.endsWith('/index.html')) {
19
+ page.path = page.path.replace(/\.html$/, '/')
20
+ }
21
+ })
22
+ }
@@ -5,22 +5,22 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require('hexo-fs');
9
-
8
+ // 懒加载强制开启:不再读取 enable 配置,no-lazy 是唯一例外
10
9
  function lazyProcess(htmlContent) {
11
- const cfg = this.theme.config.plugins.lazyload;
12
- if (cfg == undefined || cfg.enable != true) {
13
- return htmlContent;
14
- }
15
10
  return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function(imgTag, src_before, src_value, src_after) {
16
- // might be duplicate
17
- if (/data-srcset/gi.test(imgTag)) {
11
+ // 已由 tag 插件输出懒加载标记(data-src / data-srcset)的图片不重复处理
12
+ if (/data-src/gi.test(imgTag)) {
13
+ return imgTag;
14
+ }
15
+ // 使用 srcset 的图片交给浏览器原生处理,避免占位图与 srcset 冲突
16
+ if (/srcset=/gi.test(imgTag)) {
18
17
  return imgTag;
19
18
  }
20
19
  if (/src="data:image(.*?)/gi.test(imgTag)) {
21
20
  return imgTag;
22
21
  }
23
- if (imgTag.includes(' no-lazy ')) {
22
+ // no-lazy 兼容 `no-lazy` / `no-lazy=""` 两种写法
23
+ if (/\bno-lazy\b/gi.test(imgTag)) {
24
24
  return imgTag;
25
25
  }
26
26
  var newImgTag = imgTag;
@@ -2,6 +2,7 @@
2
2
  * https://github.com/wzpan/hexo-generator-search
3
3
  */
4
4
  const { stripHTML } = require('hexo-util')
5
+ const { normalize_path } = require('../lib/path_utils')
5
6
 
6
7
  hexo.extend.generator.register('search_json_generator', function (locals) {
7
8
  if (this.theme.config.search.service != 'local_search') { return {} }
@@ -28,7 +29,8 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
28
29
  temp_post.title = post.title.trim()
29
30
  }
30
31
  if (post.path) {
31
- temp_post.path = root + post.path
32
+ const path = normalize_path(root + post.path)
33
+ temp_post.path = path === '/' ? '/' : path + '/'
32
34
  }
33
35
  if (cfg.content != false && post.content) {
34
36
  var content = stripHTML(post.content.replace(/<span class="line">\d+<\/span>/g, '')).trim()
@@ -75,7 +75,8 @@ hexo.extend.helper.register('json_ld', function(args) {
75
75
 
76
76
  } else if (isPage || this.is_home()) {
77
77
 
78
- const url = this.is_home() ? config.url : this.pretty_url(page.permalink);
78
+ // 首页 URL 归一为带尾斜杠形式,与 canonical 保持一致
79
+ const url = this.is_home() ? config.url.replace(/\/?$/, '/') : this.pretty_url(page.permalink);
79
80
  schema = {
80
81
  '@context': 'https://schema.org',
81
82
  '@type': 'Website',
@@ -119,4 +120,4 @@ hexo.extend.helper.register('json_ld', function(args) {
119
120
  }
120
121
 
121
122
  return `<script type="application/ld+json">${JSON.stringify(schema)}</script>`;
122
- });
123
+ });
@@ -1,22 +1,21 @@
1
1
  'use strict';
2
2
 
3
+ const { normalize_path } = require('../lib/path_utils');
4
+
5
+ hexo.extend.helper.register('normalize_path', function (path = '') {
6
+ return normalize_path(path);
7
+ });
8
+
3
9
  hexo.extend.helper.register('pretty_url', function (path = '') {
4
10
  if (path.startsWith('http://') || path.startsWith('https://')) {
5
- // 如果是绝对 URL,直接返回
6
11
  return path;
7
12
  }
8
-
9
13
  let url = this.url_for(path);
10
14
 
11
- // 替换 /index.html → /
12
- url = url.replace(/\/index\.html$/, '/');
13
-
14
- // 替换 /about.html → /about/
15
- url = url.replace(/\.html$/, '/');
15
+ url = normalize_path(url);
16
16
 
17
- // 如果没有扩展名,并且不以 / 结尾,补一个 /
18
- const hasExtension = /\.[a-zA-Z0-9]+$/.test(url);
19
- if (!hasExtension && !url.endsWith('/')) {
17
+ // 添加尾 /(根路径 / 除外)
18
+ if (url !== '/') {
20
19
  url += '/';
21
20
  }
22
21
 
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 统一路径规范化:去除 .html 后缀,输出无尾斜杠的干净路径(path_key 格式)
5
+ * 用于路径比较和匹配
6
+ */
7
+ function normalize_path(path = '') {
8
+ if (path.startsWith('http://') || path.startsWith('https://')) {
9
+ return path;
10
+ }
11
+ // /index.html → /
12
+ path = path.replace(/\/index\.html$/, '/');
13
+ // 目录首页(无 .html 后缀形式,如 wiki/stellar/index)→ /
14
+ path = path.replace(/\/index$/, '/');
15
+ // /xxx.html → /xxx
16
+ path = path.replace(/\.html$/, '');
17
+ // 去除尾 /
18
+ if (path.length > 1 && path.endsWith('/')) {
19
+ path = path.slice(0, -1);
20
+ }
21
+ return path;
22
+ }
23
+
24
+ module.exports = { normalize_path };
@@ -3,6 +3,7 @@
3
3
  * 格式与官方标签插件一致使用空格分隔,中括号内的是可选参数(中括号不需要写出来)
4
4
  *
5
5
  * {% emoji [source] name [height:1.75em] %}
6
+ * {% emoji url:https://example.com/emoji.png [name:alt] [height:1.75em] %}
6
7
  *
7
8
  */
8
9
 
@@ -10,12 +11,26 @@
10
11
 
11
12
  module.exports = ctx => function(args) {
12
13
  const config = ctx.theme.config.tag_plugins.emoji
13
- args = ctx.args.map(args, ['height'], ['source', 'name'])
14
+ args = ctx.args.map(args, ['url', 'height', 'name'], ['source', 'name'])
14
15
  var el = ''
16
+ el += '<span class="tag-plugin emoji">'
17
+ if (args.url) {
18
+ // 直接引用外部图片,不再走 source 配置查找
19
+ el += '<img no-lazy="" class="inline"'
20
+ el += ' src="' + args.url + '"'
21
+ if (args.name) {
22
+ el += ' alt="' + args.name + '"'
23
+ }
24
+ if (args.height) {
25
+ el += ' style="height:' + args.height + '"'
26
+ }
27
+ el += '/>'
28
+ el += '</span>'
29
+ return el
30
+ }
15
31
  if (args.source == undefined) {
16
32
  return el
17
33
  }
18
- el += '<span class="tag-plugin emoji">'
19
34
  if (args.name == undefined) {
20
35
  // 省略了 source
21
36
  for (let id in config) {
@@ -16,32 +16,27 @@ module.exports = ctx => function(args) {
16
16
  if (args.height) {
17
17
  style += 'height:' + args.height + ';'
18
18
  }
19
- // fancybox
20
- var fancybox = false
19
+ // fancybox 默认开启(不再支持全局关闭),单图可用 fancybox:false 关闭
20
+ var fancybox = true
21
21
  var fancyboxHref = null
22
- if (ctx.theme.config.plugins.fancybox && ctx.theme.config.plugins.fancybox.enable) {
23
- // 主题配置
24
- if (ctx.theme.config.tag_plugins.image && ctx.theme.config.tag_plugins.image.fancybox) {
25
- fancybox = ctx.theme.config.tag_plugins.image.fancybox
26
- }
27
- // 覆盖配置
28
- if (args.fancybox && args.fancybox.length > 0) {
29
- if (args.fancybox == 'false') {
30
- fancybox = false
31
- } else if (args.fancybox === 'true') {
32
- fancybox = args.fancybox
33
- } else {
34
- fancybox = true
35
- fancyboxHref = args.fancybox
36
- }
22
+ if (args.fancybox && args.fancybox.length > 0) {
23
+ if (args.fancybox == 'false') {
24
+ fancybox = false
25
+ } else if (args.fancybox === 'true') {
26
+ fancybox = true
27
+ } else {
28
+ fancybox = true
29
+ fancyboxHref = args.fancybox
37
30
  }
38
31
  }
39
32
 
40
33
  var safeAlt = require('hexo-util').escapeHTML(args.alt || '')
34
+ // 懒加载占位图(1x1 透明 PNG),真实地址放在 data-src
35
+ const loadingImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII='
41
36
  function img(src, alt, style) {
42
37
  let a = '<a data-fancybox'
43
38
  let img = ''
44
- img += `<img class="lazy" src="${src}" data-src="${src}"`
39
+ img += `<img class="lazy" src="${loadingImg}" data-src="${src}"`
45
40
  if (safeAlt) {
46
41
  img += ` alt="${safeAlt}"`
47
42
  a += ` data-caption="${safeAlt}"`
@@ -10,8 +10,6 @@ html
10
10
  -webkit-text-size-adjust: 100%
11
11
  -ms-text-size-adjust: 100%
12
12
  scroll-padding-top: 8px
13
- if hexo-config('style.smooth_scroll')
14
- scroll-behavior: smooth
15
13
  body
16
14
  background: var(--background)
17
15
  margin: 0
@@ -141,7 +141,7 @@
141
141
  .widget-body
142
142
  grid-template-rows: 0fr
143
143
 
144
- // 编辑本文按钮
144
+ // 操作按钮(回到顶部 / 参与讨论)
145
145
  .widget-wrapper.toc .widget-body+.widget-footer:before
146
146
  content: ''
147
147
  position absolute
@@ -188,4 +188,4 @@
188
188
  a
189
189
  background: var(--block)
190
190
  a+a
191
- margin-top: 4px
191
+ margin-top: 4px
@@ -14,9 +14,6 @@ if hexo-config('plugins.copycode.enable')
14
14
  @import 'copycode'
15
15
  if hexo-config('plugins.tianli_gpt.enable')
16
16
  @import 'tianli_gpt'
17
- if hexo-config('plugins.pjax.enable')
18
- @import 'pjax'
19
-
20
17
  if hexo-config('plugins.katex.enable')
21
18
  @import 'katex'
22
19
 
@@ -1,9 +1,13 @@
1
- $lazyTransitionType = hexo-config('plugins.lazyload.transition')
1
+ $lazyTransitionType = hexo-config('dependencies.lazyload.transition')
2
2
  $loadingImageSize = 2rem
3
3
 
4
4
  img:not([src])
5
5
  visibility: hidden
6
6
 
7
+ .lazy
8
+ opacity: 0
9
+ z-index: 1
10
+
7
11
  .lazy-box
8
12
  position: relative
9
13
  overflow: hidden
@@ -12,26 +16,8 @@ img:not([src])
12
16
  background: var(--block)
13
17
  min-height: 4rem
14
18
 
15
- // 懒加载
16
- img[data-ll-status]
17
- z-index: 1
18
- &:not(.loaded)
19
- opacity: 0
20
- &.loaded,&.error
21
- opacity: 1
22
- if $lazyTransitionType == 'blur'
23
- trans1 all 0.5s
24
- &:not(.loaded)
25
- filter blur(20px)
26
- -webkit-filter blur(20px)
27
- &.loaded,&.error
28
- filter none
29
- -webkit-filter none
30
- else
31
- trans1 all 0.38s
32
-
33
19
  // 加载占位动画
34
- img[data-ll-status]+.lazy-icon
20
+ img.lazy.loading + .lazy-icon
35
21
  position: absolute
36
22
  width: $loadingImageSize
37
23
  height: $loadingImageSize
@@ -45,13 +31,41 @@ img[data-ll-status]+.lazy-icon
45
31
  background-position: center
46
32
 
47
33
  // 加载完成
48
- img[data-ll-status].loaded
49
- &+.lazy-icon
50
- display: none
34
+ img.lazy.loaded + .lazy-icon,
35
+ img.lazy.error + .lazy-icon
36
+ display: none
51
37
 
52
38
  // 加载失败
53
- img[data-ll-status].error
39
+ img.lazy.error
54
40
  width: $loadingImageSize
55
41
  height: $loadingImageSize
56
- &+.lazy-icon
57
- display: none
42
+
43
+ // 懒加载完成动画:使用 keyframes 而非 transition,
44
+ // 因为缓存图片可能在 loading 状态被绘制前就完成加载,transition 没有起点会直接闪现
45
+ if $lazyTransitionType == 'blur'
46
+ .lazy
47
+ filter: blur(20px)
48
+ -webkit-filter: blur(20px)
49
+ .lazy.loaded, .lazy.error
50
+ opacity: 1
51
+ filter: none
52
+ -webkit-filter: none
53
+ animation: ll-blur-in 0.5s ease-out
54
+ @keyframes ll-blur-in
55
+ from
56
+ opacity: 0
57
+ filter: blur(20px)
58
+ -webkit-filter: blur(20px)
59
+ to
60
+ opacity: 1
61
+ filter: none
62
+ -webkit-filter: none
63
+ else
64
+ .lazy.loaded, .lazy.error
65
+ opacity: 1
66
+ animation: ll-fade-in 0.5s ease-out
67
+ @keyframes ll-fade-in
68
+ from
69
+ opacity: 0
70
+ to
71
+ opacity: 1
@@ -1,2 +1,6 @@
1
1
  .slide-up
2
- visibility: hidden
2
+ visibility: hidden
3
+
4
+ // ScrollReveal 加载/初始化失败时强制显示内容,避免空白页
5
+ html.sr-fallback .slide-up
6
+ visibility: visible !important