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
@@ -113,7 +113,7 @@
113
113
  // 已加载脚本缓存
114
114
  _loadedScripts: new Set(),
115
115
 
116
- // 已加载元素缓存 (使用 WeakSet 追踪元素实例,避免 PJAX 导航时的状态混淆)
116
+ // 已加载元素缓存 (使用 WeakSet 追踪元素实例,避免重复加载)
117
117
  _loadedElements: new WeakSet(),
118
118
 
119
119
  js: (src, opt) => new Promise((resolve, reject) => {
@@ -146,71 +146,355 @@
146
146
  document.head.appendChild(script)
147
147
  }),
148
148
 
149
- // PJAX 事件监听器管理
150
- _pjaxListeners: new Set(),
151
-
152
- jq: (fn, pjax = true) => {
153
- if (typeof jQuery === 'undefined') {
154
- utils.js(deps.jquery).then(fn)
155
- } else {
156
- fn()
157
- }
158
- if (pjax && !fn._pjax_registered) {
159
- document.addEventListener('pjax:complete', fn);
160
- fn._pjax_registered = true;
161
- utils._pjaxListeners.add(fn); // 追踪监听器以便清理
149
+ // 原生 DOM 工具:querySelector / querySelectorAll 简写
150
+ qs: (sel, ctx) => (ctx || document).querySelector(sel),
151
+ qsa: (sel, ctx) => Array.prototype.slice.call((ctx || document).querySelectorAll(sel)),
152
+
153
+ // 原生 DOM 封装:常用 DOM 操作方法子集(find/append/class/attr/事件等)
154
+ dom: (selector, ctx) => {
155
+ var els = [];
156
+ if (typeof selector === 'string') {
157
+ els = utils.qsa(selector, ctx);
158
+ } else if (selector && selector.nodeType === 1) {
159
+ els = [selector];
160
+ } else if (selector && typeof selector.length === 'number') {
161
+ els = Array.prototype.slice.call(selector);
162
162
  }
163
- },
164
-
165
- // 清理所有 PJAX 事件监听器
166
- cleanupPjaxListeners: () => {
167
- utils._pjaxListeners.forEach(fn => {
168
- document.removeEventListener('pjax:complete', fn);
169
- fn._pjax_registered = false;
163
+ var api = {
164
+ length: els.length,
165
+ each: function (fn) {
166
+ els.forEach(function (el, i) {
167
+ fn.call(el, i, el);
168
+ });
169
+ return api;
170
+ },
171
+ find: function (sel) {
172
+ var result = [];
173
+ els.forEach(function (el) {
174
+ Array.prototype.push.apply(result, el.querySelectorAll(sel));
175
+ });
176
+ return utils.dom(result);
177
+ },
178
+ append: function (content) {
179
+ els.forEach(function (el) {
180
+ if (typeof content === 'string') {
181
+ el.insertAdjacentHTML('beforeend', content);
182
+ } else if (content && content.nodeType === 1) {
183
+ el.appendChild(content);
184
+ } else if (content && typeof content.length === 'number') {
185
+ Array.prototype.forEach.call(content, function (child) {
186
+ if (child && child.nodeType === 1) el.appendChild(child);
187
+ });
188
+ }
189
+ });
190
+ return api;
191
+ },
192
+ remove: function () {
193
+ els.forEach(function (el) {
194
+ el.remove();
195
+ });
196
+ },
197
+ addClass: function (cls) {
198
+ els.forEach(function (el) {
199
+ cls.trim().split(/\s+/).forEach(function (c) {
200
+ if (c) el.classList.add(c);
201
+ });
202
+ });
203
+ return api;
204
+ },
205
+ removeClass: function (cls) {
206
+ els.forEach(function (el) {
207
+ cls.trim().split(/\s+/).forEach(function (c) {
208
+ if (c) el.classList.remove(c);
209
+ });
210
+ });
211
+ return api;
212
+ },
213
+ toggleClass: function (cls, force) {
214
+ els.forEach(function (el) {
215
+ cls.trim().split(/\s+/).forEach(function (c) {
216
+ if (c) el.classList.toggle(c, force);
217
+ });
218
+ });
219
+ return api;
220
+ },
221
+ attr: function (name, value) {
222
+ if (value === undefined) {
223
+ return els[0] ? els[0].getAttribute(name) : undefined;
224
+ }
225
+ els.forEach(function (el) {
226
+ el.setAttribute(name, value);
227
+ });
228
+ return api;
229
+ },
230
+ data: function (name) {
231
+ return els[0] ? els[0].getAttribute('data-' + name) : undefined;
232
+ },
233
+ text: function (value) {
234
+ if (value === undefined) {
235
+ return els[0] ? els[0].textContent : undefined;
236
+ }
237
+ els.forEach(function (el) {
238
+ el.textContent = value;
239
+ });
240
+ return api;
241
+ },
242
+ html: function (content) {
243
+ if (content === undefined) {
244
+ return els[0] ? els[0].innerHTML : undefined;
245
+ }
246
+ els.forEach(function (el) {
247
+ if (typeof content !== 'string' && content && typeof content.length === 'number' && content[0] && content[0].nodeType === 1) {
248
+ var frag = document.createDocumentFragment();
249
+ Array.prototype.forEach.call(content, function (child) {
250
+ frag.appendChild(child);
251
+ });
252
+ el.replaceChildren(frag);
253
+ } else {
254
+ el.innerHTML = content;
255
+ }
256
+ });
257
+ return api;
258
+ },
259
+ val: function (value) {
260
+ if (value === undefined) {
261
+ return els[0] ? els[0].value : undefined;
262
+ }
263
+ els.forEach(function (el) {
264
+ el.value = value;
265
+ });
266
+ return api;
267
+ },
268
+ offset: function () {
269
+ var el = els[0];
270
+ if (!el) return { top: 0, left: 0 };
271
+ var rect = el.getBoundingClientRect();
272
+ return { top: rect.top + window.scrollY, left: rect.left + window.scrollX };
273
+ },
274
+ on: function (event, cb) {
275
+ els.forEach(function (el) {
276
+ el.addEventListener(event, cb);
277
+ });
278
+ return api;
279
+ },
280
+ click: function (cb) { return api.on('click', cb); },
281
+ focus: function (cb) { return api.on('focus', cb); },
282
+ keydown: function (cb) { return api.on('keydown', cb); },
283
+ empty: function () {
284
+ els.forEach(function (el) {
285
+ el.replaceChildren();
286
+ });
287
+ return api;
288
+ }
289
+ };
290
+ els.forEach(function (el, i) {
291
+ api[i] = el;
170
292
  });
171
- utils._pjaxListeners.clear();
293
+ return api;
172
294
  },
173
295
 
174
296
  onLoading: (el) => {
175
297
  if (el) {
176
- if ($(el).find('.loading-wrap').length === 0){
177
- $(el).append('<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
298
+ if (el.querySelector('.loading-wrap') === null) {
299
+ el.insertAdjacentHTML('beforeend', '<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
178
300
  }
179
301
  }
180
302
  },
181
303
  onLoadSuccess: (el) => {
182
304
  if (el) {
183
- $(el).find('.loading-wrap').remove();
305
+ var wrap = el.querySelector('.loading-wrap');
306
+ if (wrap) wrap.remove();
184
307
  }
185
308
  },
186
309
  onLoadFailure: (el) => {
187
310
  if (el) {
188
- $(el).find('.loading-wrap svg').remove();
189
- $(el).find('.loading-wrap').append('<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
190
- $(el).find('.loading-wrap').addClass('error');
311
+ var wrap = el.querySelector('.loading-wrap');
312
+ if (wrap) {
313
+ var svg = wrap.querySelector('svg');
314
+ if (svg) svg.remove();
315
+ wrap.insertAdjacentHTML('beforeend', '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
316
+ wrap.classList.add('error');
317
+ }
191
318
  }
192
319
  },
193
- request: (el, url, callback, onFailure) => {
320
+ /********************** data cache ********************************/
321
+ // 动态数据本地缓存:TTL 未过期直接命中;过期后先显示缓存再后台刷新(stale-while-revalidate)
322
+ _cacheEnabled: true,
323
+ _cacheMaxEntryBytes: 200 * 1024,
324
+ cache: {
325
+ prefix: 'Stellar.data_cache.v1.',
326
+ enabled: () => !!def.data_cache?.enable && utils._cacheEnabled,
327
+ serviceId: (el, options) => {
328
+ if (options && options.service) return options.service;
329
+ if (el) {
330
+ const match = String(el.className || '').match(/\bds-([\w-]+)\b/);
331
+ if (match) return match[1];
332
+ }
333
+ return null;
334
+ },
335
+ ttl: (el, options) => {
336
+ const conf = def.data_cache || {};
337
+ const id = utils.cache.serviceId(el, options);
338
+ const value = id && conf.ttl && conf.ttl[id] != null ? conf.ttl[id] : conf.default_ttl;
339
+ return typeof value === 'number' && value > 0 ? value : 0;
340
+ },
341
+ shouldCache: (url, options) => {
342
+ if (!utils.cache.enabled()) return false;
343
+ if (options && (options.cache === false || options.cache === 'no-store')) return false;
344
+ if (options && options.method && options.method !== 'GET') return false;
345
+ // 带时间戳缓存破坏参数的请求不缓存(如 mdrender 的 ?t=)
346
+ if (/[?&]t=\d{10,}/.test(url)) return false;
347
+ return true;
348
+ },
349
+ get: (url) => {
350
+ try {
351
+ const raw = localStorage.getItem(utils.cache.prefix + url);
352
+ if (!raw) return null;
353
+ const entry = JSON.parse(raw);
354
+ if (!entry || typeof entry.text !== 'string' || typeof entry.ts !== 'number') return null;
355
+ return entry;
356
+ } catch (e) {
357
+ console.warn('[cache] 读取失败:', url, e);
358
+ return null;
359
+ }
360
+ },
361
+ set: (url, text, contentType, ttl) => {
362
+ if (!(ttl > 0)) return;
363
+ if (text.length > utils._cacheMaxEntryBytes) return;
364
+ const entry = JSON.stringify({
365
+ text: text,
366
+ contentType: contentType || 'application/json',
367
+ ts: Date.now(),
368
+ ttl: ttl
369
+ });
370
+ const trySet = () => {
371
+ try {
372
+ localStorage.setItem(utils.cache.prefix + url, entry);
373
+ utils.cache.trim();
374
+ return true;
375
+ } catch (e) {
376
+ return false;
377
+ }
378
+ };
379
+ if (!trySet()) {
380
+ // 配额不足时淘汰最旧条目后重试一次,仍失败则本会话禁用缓存
381
+ utils.cache.evictOldest(url);
382
+ if (!trySet()) {
383
+ console.warn('[cache] 写入失败,本会话禁用缓存:', url);
384
+ utils._cacheEnabled = false;
385
+ }
386
+ }
387
+ },
388
+ trim: () => {
389
+ try {
390
+ const max = (def.data_cache && def.data_cache.max_entries) || 200;
391
+ const keys = [];
392
+ for (let i = 0; i < localStorage.length; i++) {
393
+ const key = localStorage.key(i);
394
+ if (key && key.indexOf(utils.cache.prefix) === 0) keys.push(key);
395
+ }
396
+ if (keys.length <= max) return;
397
+ const sorted = keys.map((key) => {
398
+ let ts = 0;
399
+ try {
400
+ const entry = JSON.parse(localStorage.getItem(key) || 'null');
401
+ if (entry && typeof entry.ts === 'number') ts = entry.ts;
402
+ } catch (e) {}
403
+ return { key, ts };
404
+ }).sort((a, b) => a.ts - b.ts);
405
+ for (let i = 0; i < sorted.length - max; i++) {
406
+ localStorage.removeItem(sorted[i].key);
407
+ }
408
+ } catch (e) {
409
+ console.warn('[cache] 清理失败:', e);
410
+ }
411
+ },
412
+ evictOldest: (exceptUrl) => {
413
+ try {
414
+ let oldestKey = null;
415
+ let oldestTs = Infinity;
416
+ const exceptKey = utils.cache.prefix + exceptUrl;
417
+ for (let i = 0; i < localStorage.length; i++) {
418
+ const key = localStorage.key(i);
419
+ if (!key || key.indexOf(utils.cache.prefix) !== 0 || key === exceptKey) continue;
420
+ let ts = 0;
421
+ try {
422
+ const entry = JSON.parse(localStorage.getItem(key) || 'null');
423
+ if (entry && typeof entry.ts === 'number') ts = entry.ts;
424
+ } catch (e) {}
425
+ if (ts < oldestTs) {
426
+ oldestTs = ts;
427
+ oldestKey = key;
428
+ }
429
+ }
430
+ if (oldestKey) localStorage.removeItem(oldestKey);
431
+ } catch (e) {}
432
+ },
433
+ isFresh: (entry) => {
434
+ if (!entry || typeof entry.ts !== 'number' || typeof entry.ttl !== 'number') return false;
435
+ if (!(entry.ttl > 0)) return false;
436
+ return Date.now() - entry.ts < entry.ttl * 1000;
437
+ },
438
+ // 用缓存文本重建 Response,兼容回调中的 resp.json() / resp.text()
439
+ toResponse: (entry) => new Response(entry.text, {
440
+ status: 200,
441
+ statusText: 'OK',
442
+ headers: { 'Content-Type': entry.contentType }
443
+ })
444
+ },
445
+ request: (el, url, callback, onFailure, options) => {
194
446
  // 检查元素实例是否已加载 (而不是检查属性)
195
447
  if (el && utils._loadedElements.has(el)) {
196
448
  return;
197
449
  }
198
450
  const maxRetry = 3;
199
451
  let retryCount = 0;
452
+ const ttl = utils.cache.ttl(el, options);
453
+ const cacheable = utils.cache.shouldCache(url, options) && ttl > 0;
454
+ const cached = cacheable ? utils.cache.get(url) : null;
455
+ // 缓存渲染前的初始结构,后台刷新渲染前恢复,避免重复渲染
456
+ let initialHTML = null;
457
+ // 缓存渲染完成后(或超时兜底)再启动后台刷新,避免异步渲染未结束就清空重绘
458
+ let cacheRenderDone = null;
459
+
460
+ if (cached) {
461
+ if (el) {
462
+ // 已有缓存时不显示加载动画
463
+ utils.onLoadSuccess?.(el);
464
+ initialHTML = el.innerHTML;
465
+ }
466
+ try {
467
+ cacheRenderDone = Promise.resolve(callback(utils.cache.toResponse(cached))).catch(e => {
468
+ console.warn('[request] 缓存渲染失败:', url, e);
469
+ });
470
+ } catch (e) {
471
+ console.warn('[request] 缓存渲染失败:', url, e);
472
+ }
473
+ // 缓存未过期:直接完成,不发请求
474
+ if (utils.cache.isFresh(cached)) {
475
+ if (el) utils._loadedElements.add(el);
476
+ return Promise.resolve(cached.text);
477
+ }
478
+ } else {
479
+ utils.onLoading?.(el);
480
+ }
200
481
 
201
482
  return new Promise((resolve, reject) => {
202
483
  const load = () => {
203
- utils.onLoading?.(el);
204
-
205
484
  let timedOut = false;
206
485
  const timeout = setTimeout(() => {
207
486
  timedOut = true;
208
487
  console.warn('[request] 超时:', url);
209
488
 
210
489
  if (++retryCount >= maxRetry) {
211
- utils.onLoadFailure?.(el);
212
- onFailure?.();
213
- reject('请求超时');
490
+ if (cached) {
491
+ // 已有缓存渲染,保留缓存内容,不显示失败
492
+ resolve(cached.text);
493
+ } else {
494
+ utils.onLoadFailure?.(el);
495
+ onFailure?.();
496
+ reject('请求超时');
497
+ }
214
498
  } else {
215
499
  setTimeout(load, 1000);
216
500
  }
@@ -226,6 +510,16 @@
226
510
  if (timedOut) return;
227
511
  // 标记元素实例为已加载
228
512
  if (el) utils._loadedElements.add(el);
513
+ // 写入缓存(克隆响应,避免影响回调读取)
514
+ if (cacheable && data.ok) {
515
+ data.clone().text().then(text => {
516
+ utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
517
+ }).catch(() => {});
518
+ }
519
+ // 后台刷新渲染前恢复初始结构,避免缓存渲染的内容重复
520
+ if (el && initialHTML !== null) {
521
+ el.innerHTML = initialHTML;
522
+ }
229
523
  utils.onLoadSuccess?.(el);
230
524
  callback(data);
231
525
  resolve(data);
@@ -234,19 +528,38 @@
234
528
  console.warn('[request] 错误:', err);
235
529
 
236
530
  if (++retryCount >= maxRetry) {
237
- utils.onLoadFailure?.(el);
238
- onFailure?.();
239
- reject(err);
531
+ if (cached) {
532
+ resolve(cached.text);
533
+ } else {
534
+ utils.onLoadFailure?.(el);
535
+ onFailure?.();
536
+ reject(err);
537
+ }
240
538
  } else {
241
539
  setTimeout(load, 1000);
242
540
  }
243
541
  });
244
542
  };
245
543
 
246
- load();
544
+ if (cacheRenderDone) {
545
+ Promise.race([
546
+ cacheRenderDone,
547
+ new Promise(resolve => setTimeout(resolve, 5000))
548
+ ]).then(load);
549
+ } else {
550
+ load();
551
+ }
247
552
  });
248
553
  },
249
554
  requestWithoutLoading: (url, options = {}, maxRetry = 2, timeout = 5000) => {
555
+ const ttl = utils.cache.ttl(null, options);
556
+ const cacheable = utils.cache.shouldCache(url, options) && ttl > 0;
557
+ const cached = cacheable ? utils.cache.get(url) : null;
558
+
559
+ if (cached && utils.cache.isFresh(cached)) {
560
+ return Promise.resolve(utils.cache.toResponse(cached));
561
+ }
562
+
250
563
  return new Promise((resolve, reject) => {
251
564
  let retryCount = 0;
252
565
 
@@ -262,6 +575,11 @@
262
575
  .then(resp => {
263
576
  clearTimeout(timer);
264
577
  if (!resp.ok) throw new Error('bad response');
578
+ if (cacheable) {
579
+ resp.clone().text().then(text => {
580
+ utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
581
+ }).catch(() => {});
582
+ }
265
583
  resolve(resp);
266
584
  })
267
585
  .catch(err => {
@@ -285,7 +603,7 @@
285
603
  },
286
604
  dark: {},
287
605
 
288
- // 插件初始化管理器 - 统一处理 DOMContentLoaded 和 pjax:complete 事件
606
+ // 插件初始化管理器 - 统一处理 DOMContentLoaded 事件
289
607
  _pluginInitializers: [],
290
608
  _pluginCleanups: new Map(), // 存储每个插件的清理函数
291
609
 
@@ -334,20 +652,6 @@
334
652
  }
335
653
  },
336
654
 
337
- // 重新初始化所有插件(用于 PJAX 等场景)
338
- reinitPlugins: () => {
339
- utils._pluginInitializers.forEach(plugin => {
340
- // 跳过标记为不需要重新初始化的插件
341
- if (plugin.options.skipReinit) return;
342
-
343
- try {
344
- plugin.fn();
345
- } catch (error) {
346
- console.error(`[Plugin ${plugin.name}] 重新初始化失败:`, error);
347
- }
348
- });
349
- },
350
-
351
655
  // 清理所有插件资源
352
656
  cleanupPlugins: () => {
353
657
  utils._pluginCleanups.forEach((cleanup, name) => {
@@ -363,21 +667,9 @@
363
667
  // 清理所有资源(用于页面卸载或重置)
364
668
  cleanupAll: () => {
365
669
  utils.cleanupPlugins();
366
- utils.cleanupPjaxListeners();
367
670
  },
368
671
  };
369
672
 
370
- // 自动处理 PJAX 事件
371
- document.addEventListener('pjax:before', () => {
372
- // 在导航前清理旧的 PJAX 监听器(如果需要重新注册)
373
- // 注意:这里不清理,因为监听器应该是持久的
374
- // utils.cleanupPjaxListeners();
375
- });
376
-
377
- document.addEventListener('pjax:complete', () => {
378
- utils.reinitPlugins();
379
- });
380
-
381
673
  // utils.dark.mode 当前模式 dark or light
382
674
  // utils.dark.toggle() 暗黑模式触发器
383
675
  // utils.dark.push(callBack[,"callBackName"]) 传入触发器回调函数
@@ -387,4 +679,4 @@
387
679
  utils.dark = Object.assign(utils.dark, {
388
680
  push: utils.dark.method.toggle.push,
389
681
  });
390
- </script>
682
+ </script>
@@ -45,7 +45,7 @@
45
45
  <% } %>
46
46
 
47
47
  <% if (tagTree) { %>
48
- <widget class="widget-wrapper<%= scrollreveal(' ') %> post-list">
48
+ <widget class="widget-wrapper<%= scrollreveal(' ') %> post-list" data-notebook="<%= page.notebook %>">
49
49
  <div class="widget-header dis-select">
50
50
  <span class="name"><%= __('meta.tag_tree') %></span>
51
51
  </div>
@@ -37,12 +37,9 @@ function layoutToc(fallback) {
37
37
 
38
38
  function layoutDiv(fallback) {
39
39
  const tocBody = layoutTocBody()
40
- if (tocBody.trim().length == 0) {
41
- return ''
42
- }
43
40
  var el = ''
44
41
  el += `<widget class="widget-wrapper${scrollreveal(' ')} toc" id="data-toc" collapse="${item.collapse}">`
45
- if (tocBody.length > 0) {
42
+ if (tocBody.trim().length > 0) {
46
43
  el += layoutTocHeader()
47
44
  el += `<div class="widget-body">`
48
45
  el += tocBody
@@ -60,7 +60,7 @@ function layoutDiv(fallback) {
60
60
  }
61
61
  }
62
62
  if (el.trim().length > 0) {
63
- return `<widget class="widget-wrapper doc-tree post-list">${el}</widget>`
63
+ return `<widget class="widget-wrapper doc-tree post-list" data-wiki="${page.wiki}">${el}</widget>`
64
64
  } else {
65
65
  return ''
66
66
  }
@@ -1,27 +1,42 @@
1
1
  <script>
2
2
  ctx.fancybox = {
3
+ mode: `<%- conf.mode || 'auto' %>`,
3
4
  selector: `<%- conf.selector %>`,
4
5
  css: `<%- conf.css %>`,
5
6
  js: `<%- conf.js %>`
6
7
  };
7
- var selector = '[data-fancybox]:not(.error), .with-fancybox .atk-content img:not([atk-emoticon])';
8
+ // auto=按需:页面存在可弹窗图片才加载;global=全局:所有页面加载
9
+ const isGlobal = ctx.fancybox.mode === 'global';
10
+ // 评论区图片(artalk/twikoo/waline)显示较小,自动支持放大
11
+ var selector = '[data-fancybox]:not(.error), .with-fancybox img:not([atk-emoticon]):not([class*="emo"])';
8
12
  if (ctx.fancybox.selector) {
9
13
  selector += `, ${ctx.fancybox.selector}`
10
14
  }
11
- var needFancybox = document.querySelectorAll(selector).length !== 0;
15
+ var needFancybox = isGlobal;
12
16
  if (!needFancybox) {
13
- const memos = document.getElementsByClassName('ds-memos');
14
- if (memos != undefined && memos.length > 0) {
15
- needFancybox = true;
17
+ needFancybox = document.querySelectorAll(selector).length !== 0;
18
+ if (!needFancybox) {
19
+ // 图片型评论系统容器:评论区图片小图需要 fancybox
20
+ const withFancybox = document.getElementsByClassName('with-fancybox');
21
+ if (withFancybox != undefined && withFancybox.length > 0) {
22
+ needFancybox = true;
23
+ }
16
24
  }
17
- const fancybox = document.getElementsByClassName('with-fancybox');
18
- if (fancybox != undefined && fancybox.length > 0) {
19
- needFancybox = true;
25
+ if (!needFancybox) {
26
+ // 动态数据服务:memos 渲染的图片需要 fancybox
27
+ const memos = document.getElementsByClassName('ds-memos');
28
+ if (memos != undefined && memos.length > 0) {
29
+ needFancybox = true;
30
+ }
20
31
  }
21
32
  }
22
33
  if (needFancybox) {
23
34
  utils.css(ctx.fancybox.css);
24
35
  utils.js(ctx.fancybox.js, { defer: true }).then(function () {
36
+ if (isGlobal) {
37
+ // 全局模式:正文所有非内联图片均可弹窗
38
+ selector += ', .md-text img:not(.inline):not([atk-emoticon])';
39
+ }
25
40
  Fancybox.bind(selector, {
26
41
  hideScrollbar: false,
27
42
  Thumbs: {