feat: add wordcount helpers, waline lightbox, and extensive bug fixes

- wordcount/min2read/totalcount built-in helpers (replaces hexo-wordcount)
- cloudTags random sort support
- waline lightbox via MutationObserver
- subtitle: seq guard prevents stale API response race
- newest-comments: escapeHtml XSS fix, unified fetchAndRender with cache fallback
- katex: try/catch asset load, encrypt re-render
- chartjs: destroy charts on pjax, fix theme property check
- mathjax: DOMContentLoaded instead of load
- mermaid: clean stale error elements
- medium-zoom: single instance, detach on pjax
- getScrollPercent: WeakMap cache with resize invalidation
- overflowPaddingR: avoid stale DOM refs
- readmode: dedupe button, pjax cleanup
- hexo-blog-decrypt: null-guard translateFn and encryptFn
- tw_cn: expand skipped translate tags
- localStorage: try/catch setItem
- cdn.js: fix [lib|dist]* character class regex bug
- findArchivesTitle: support custom archive_dir
- shuoshuoFN: guard moment.tz when timezone empty
- umami: no-store cache, error handling, path param
- artalk: vote config option
- bump version 5.7.1.260810
This commit is contained in:
myw
2026-08-10 18:02:27 +08:00 Unverified
parent 51d36ef83a
commit 79e16a5cdd
25 changed files with 726 additions and 624 deletions
+120 -113
View File
@@ -1,113 +1,120 @@
- const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
script.
window.typedJSFn = {
init: str => {
window.typed = new Typed('#subtitle', Object.assign({
strings: str,
startDelay: 300,
typeSpeed: 150,
loop: true,
backSpeed: 50,
}, !{JSON.stringify(typed_option)}))
},
run: subtitleType => {
if (!{effect}) {
if (typeof Typed === 'function') {
subtitleType()
} else {
btf.getScript('!{url_for(theme.asset.typed)}').then(subtitleType)
}
} else {
subtitleType()
}
},
processSubtitle: (content, extraContents = []) => {
if (!{effect}) {
const sub = !{JSON.stringify(subContent)}.slice()
if (extraContents.length > 0) {
sub.unshift(...extraContents)
}
if (typeof content === 'string') {
sub.unshift(content)
} else if (Array.isArray(content)) {
sub.unshift(...content)
}
sub.length > 0 && typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = typeof content === 'string' ? content :
(Array.isArray(content) && content.length > 0 ? content[0] : '')
}
}
}
btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
case source
when 1
script.
function subtitleType () {
fetch('https://v1.hitokoto.cn')
.then(response => response.json())
.then(data => {
const from = data.from ? '出自 ' + data.from : ''
typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
})
.catch(err => {
console.error('Failed to get the Hitokoto API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
})
}
typedJSFn.run(subtitleType)
when 2
script.
function subtitleType () {
fetch('https://v.api.aa1.cn/api/yiyan/index.php')
.then(response => response.text())
.then(data => {
const reg = /<p>(.*?)<\/p>/g
const result = reg.exec(data)
if (result && result[1]) {
typedJSFn.processSubtitle(result[1])
} else {
throw new Error('Failed to parse the return value of the Yiyan API')
}
})
.catch(err => {
console.error('Failed to get the Yiyan API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
})
}
typedJSFn.run(subtitleType)
when 3
script.
function subtitleType () {
btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js')
.then(() => {
jinrishici.load(result => {
if (result && result.data && result.data.content) {
typedJSFn.processSubtitle(result.data.content)
} else {
throw new Error('Failed to parse the return value of Jinrishici API')
}
})
})
.catch(err => {
console.error('Failed to get the Jinrishici API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
})
}
typedJSFn.run(subtitleType)
default
if subContent.length > 0
script.
function subtitleType () {
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}
typedJSFn.run(subtitleType)
- const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
script.
window.typedJSFn = {
_seq: 0,
init: str => {
window.typed = new Typed('#subtitle', Object.assign({
strings: str,
startDelay: 300,
typeSpeed: 150,
loop: true,
backSpeed: 50,
}, !{JSON.stringify(typed_option)}))
},
run: subtitleType => {
const seq = ++window.typedJSFn._seq
const invoke = () => {
if (seq === window.typedJSFn._seq) subtitleType()
}
if (!{effect}) {
if (typeof Typed === 'function') invoke()
else btf.getScript('!{url_for(theme.asset.typed)}').then(invoke)
} else {
invoke()
}
},
fetchSubtitle: (fetcher, parser) => {
const seq = window.typedJSFn._seq
Promise.resolve()
.then(fetcher)
.then(parser)
.then(({ content, extra = [] }) => {
if (seq !== window.typedJSFn._seq) return
typedJSFn.processSubtitle(content, extra)
})
.catch(err => {
if (seq !== window.typedJSFn._seq) return
console.error('Failed to get the subtitle API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
})
},
processSubtitle: (content, extraContents = []) => {
if (!{effect}) {
const sub = !{JSON.stringify(subContent)}.slice()
if (extraContents.length > 0) {
sub.unshift(...extraContents)
}
if (typeof content === 'string') {
sub.unshift(content)
} else if (Array.isArray(content)) {
sub.unshift(...content)
}
sub.length > 0 && typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = typeof content === 'string' ? content :
(Array.isArray(content) && content.length > 0 ? content[0] : '')
}
}
}
btf.addGlobalFn('pjaxSendOnce', () => {
window.typedJSFn._seq++
typed && typed.destroy()
window.typed = null
}, 'typedDestroy')
case source
when 1
script.
function subtitleType () {
typedJSFn.fetchSubtitle(
() => fetch('https://v1.hitokoto.cn').then(response => response.json()),
data => ({ content: data.hitokoto, extra: data.from ? ['出自 ' + data.from] : [] })
)
}
typedJSFn.run(subtitleType)
when 2
script.
function subtitleType () {
typedJSFn.fetchSubtitle(
() => fetch('https://v.api.aa1.cn/api/yiyan/index.php').then(response => response.text()),
data => {
const reg = /<p>(.*?)<\/p>/g
const result = reg.exec(data)
if (result && result[1]) return { content: result[1] }
throw new Error('Failed to parse the return value of the Yiyan API')
}
)
}
typedJSFn.run(subtitleType)
when 3
script.
function subtitleType () {
typedJSFn.fetchSubtitle(
() => btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js').then(() =>
new Promise((resolve, reject) => {
jinrishici.load(result => {
if (result && result.data && result.data.content) resolve(result.data.content)
else reject(new Error('Failed to parse the return value of Jinrishici API'))
})
})
),
content => ({ content })
)
}
typedJSFn.run(subtitleType)
default
if subContent.length > 0
script.
function subtitleType () {
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}
typedJSFn.run(subtitleType)