This commit is contained in:
myw
2026-07-16 16:53:38 +08:00 Unverified
parent 6b44647d4e
commit d8942c236a
22 changed files with 263 additions and 240 deletions
+5 -5
View File
@@ -659,7 +659,7 @@ artalk:
# -------------------------------------- # --------------------------------------
chat: chat:
# Choose: chatra/tidio/crisp/knocket # Choose: chatra/tidio/crisp/knocket
# Leave it empty if you don't need chat # Leave it empty if you don't need chat
use: use:
# Chat Button [recommend] # Chat Button [recommend]
@@ -680,10 +680,10 @@ tidio:
crisp: crisp:
website_id: website_id:
# https://trtc.io/solutions/knocket # https://trtc.io/solutions/knocket
knocket: knocket:
identifier: identifier:
# -------------------------------------- # --------------------------------------
# Analysis # Analysis
# -------------------------------------- # --------------------------------------
+5 -2
View File
@@ -2,8 +2,11 @@ script.
(() => { (() => {
const abcjsInit = () => { const abcjsInit = () => {
const abcjsFn = () => { const abcjsFn = () => {
const $article = document.getElementById("article-container")
if (!$article || $article.querySelector(".hbe-container")) return
setTimeout(() => { setTimeout(() => {
const sheets = document.querySelectorAll(".abc-music-sheet") const sheets = $article.querySelectorAll(".abc-music-sheet")
for (let i = 0; i < sheets.length; i++) { for (let i = 0; i < sheets.length; i++) {
const ele = sheets[i] const ele = sheets[i]
if (ele.children.length > 0) continue if (ele.children.length > 0) continue
@@ -29,7 +32,7 @@ script.
}, 100) }, 100)
} }
if (typeof ABCJS === "object") { if (typeof ABCJS === "object" && ABCJS !== null) {
abcjsFn() abcjsFn()
} else { } else {
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn) btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
+4 -4
View File
@@ -3,7 +3,7 @@ case theme.chat.use
include ./chatra.pug include ./chatra.pug
when 'tidio' when 'tidio'
include ./tidio.pug include ./tidio.pug
when 'crisp' when 'crisp'
include ./crisp.pug include ./crisp.pug
when 'knocket' when 'knocket'
include ./knocket.pug include ./knocket.pug
+10 -12
View File
@@ -1,14 +1,15 @@
//- Knocket live chat — https://trtc.io/solutions/knocket //- Knocket live chat — https://trtc.io/solutions/knocket
script. script.
(() => { (() => {
const id = '#{theme.knocket.identifier}' const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/</g, '\\u003c')}
if (!id) return if (!identifier) return
btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${id}`).then(() => { btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => {
const isChatBtn = !{theme.chat.rightside_button} const isChatBtn = !{theme.chat.rightside_button}
const isChatHideShow = !{theme.chat.button_hide_show} const isChatHideShow = !{theme.chat.button_hide_show}
const getWidget = () => document.getElementById('contact-widget-auto') let widget = null
const getWidget = () => widget || (widget = document.getElementById('contact-widget-auto'))
if (isChatBtn) { if (isChatBtn) {
const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' } const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' }
@@ -24,21 +25,18 @@ script.
window.chatBtnFn = () => { window.chatBtnFn = () => {
const w = getWidget() const w = getWidget()
if (!w) return if (!w) return
if (w.style.display === 'none') show() w.style.display = w.style.display === 'none' ? '' : 'none'
else hide()
} }
document.getElementById('chat-btn').style.display = 'block' const chatBtn = document.getElementById('chat-btn')
if (chatBtn) chatBtn.style.display = 'block'
} else if (isChatHideShow) { } else if (isChatHideShow) {
const observer = new MutationObserver(() => {
if (getWidget()) observer.disconnect()
})
observer.observe(document.body, { childList: true, subtree: true })
window.chatBtn = { window.chatBtn = {
hide: () => { const w = getWidget(); if (w) w.style.display = 'none' }, hide: () => { const w = getWidget(); if (w) w.style.display = 'none' },
show: () => { const w = getWidget(); if (w) w.style.display = '' } show: () => { const w = getWidget(); if (w) w.style.display = '' }
} }
} }
}).catch(error => {
console.warn('[Knocket] Failed to load the chat SDK.', error)
}) })
})() })()
+1 -1
View File
@@ -15,7 +15,7 @@ script.
const loadMathjax = () => { const loadMathjax = () => {
const article = document.getElementById('article-container') const article = document.getElementById('article-container')
if (!article) return if (!article || article.querySelector('.hbe-container')) return
changeScriptToMath(article) changeScriptToMath(article)
if (!window.MathJax) { if (!window.MathJax) {
+7 -4
View File
@@ -330,8 +330,8 @@ script.
}) })
} }
const codeToMermaid = () => { const codeToMermaid = $article => {
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid') const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
if (codeMermaidEle.length === 0) return if (codeMermaidEle.length === 0) return
codeMermaidEle.forEach(ele => { codeMermaidEle.forEach(ele => {
@@ -347,8 +347,11 @@ script.
} }
const loadMermaid = () => { const loadMermaid = () => {
if (!{theme.mermaid.code_write}) codeToMermaid() const $article = document.getElementById('article-container')
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap') if (!$article) return
if (!{theme.mermaid.code_write}) codeToMermaid($article)
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
if ($mermaid.length === 0) return if ($mermaid.length === 0) return
const runMermaidFn = () => runMermaid($mermaid) const runMermaidFn = () => runMermaid($mermaid)
+5 -4
View File
@@ -65,12 +65,13 @@ script.
}) })
document.addEventListener('pjax:error', e => { document.addEventListener('pjax:error', e => {
if (e.request.status === 404) { const responseURL = e.request && e.request.responseURL
if (e.request && e.request.status === 404) {
!{theme.error_404 && theme.error_404.enable} !{theme.error_404 && theme.error_404.enable}
? pjax.loadUrl('!{url_for("/404.html")}') ? pjax.loadUrl('!{url_for("/404.html")}')
: window.location.href = e.request.responseURL : responseURL && (window.location.href = responseURL)
} else { } else if (responseURL) {
window.location.href = e.request.responseURL window.location.href = responseURL
} }
}) })
}) })
+5 -1
View File
@@ -5,11 +5,15 @@
if (syntax_highlighter === 'prismjs' || enable) && !preprocess if (syntax_highlighter === 'prismjs' || enable) && !preprocess
script. script.
(() => { (() => {
const $article = document.getElementById('article-container')
if (!$article || $article.querySelector('.hbe-container')) return
window.Prism = window.Prism || {} window.Prism = window.Prism || {}
window.Prism.manual = true window.Prism.manual = true
const highlightAll = () => { const highlightAll = () => {
window.Prism.highlightAll() // window.Prism.highlightAll()
window.Prism.highlightAllUnder($article, true)
} }
window.addEventListener('load', highlightAll) window.addEventListener('load', highlightAll)
+5 -5
View File
@@ -44,7 +44,7 @@ script.
} }
} }
} }
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy') btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
case source case source
when 1 when 1
@@ -53,8 +53,8 @@ case source
fetch('https://v1.hitokoto.cn') fetch('https://v1.hitokoto.cn')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const from = '出自 ' + data.from const from = data.from ? '出自 ' + data.from : ''
typedJSFn.processSubtitle(data.hitokoto, [from]) typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Hitokoto API:', err) console.error('Failed to get the Hitokoto API:', err)
@@ -79,7 +79,7 @@ case source
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Yiyan API:', err) console.error('Failed to get the Yiyan API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}) })
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
@@ -99,7 +99,7 @@ case source
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Jinrishici API:', err) console.error('Failed to get the Jinrishici API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}) })
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
+116 -110
View File
@@ -1,110 +1,116 @@
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics - let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
- const isServerURL = !!serverURL - const isServerURL = !!serverURL
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is' - const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1' - const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
script. script.
(() => { (() => {
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const config = !{JSON.stringify(UV_PV)} const config = !{JSON.stringify(UV_PV)}
const runTrack = () => { const runTrack = () => {
if (typeof umami !== 'undefined' && typeof umami.track === 'function') { if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
} else { } else {
console.warn('Umami Analytics: umami.track is not available') console.warn('Umami Analytics: umami.track is not available')
} }
} }
const loadUmamiJS = () => { const loadUmamiJS = () => {
btf.getScript('!{baseURL}/!{script_name}', { btf.getScript('!{baseURL}/!{script_name}', {
'data-website-id': '!{website_id}', 'data-website-id': '!{website_id}',
'data-auto-track': 'false', 'data-auto-track': 'false',
...option ...option
}).then(() => { }).then(() => {
runTrack() runTrack()
}).catch(error => { }).catch(error => {
console.error('Umami Analytics: Error loading script', error) console.error('Umami Analytics: Error loading script', error)
}) })
} }
const getData = async (isPost) => { const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
try {
const now = Date.now() const getData = async (isPost) => {
const keyUrl = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : '' try {
const headerList = { 'Accept': 'application/json' } const now = Date.now()
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
if (!{isServerURL}) { const headers = {
headerList['Authorization'] = `Bearer ${config.token}` 'Accept': 'application/json',
} else { [!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
headerList['x-umami-api-key'] = config.token }
}
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { method: 'GET',
method: "GET", headers
headers: headerList })
})
if (!res.ok) {
if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`)
throw new Error(`HTTP error! status: ${res.status}`) }
}
return await res.json()
return await res.json() } catch (error) {
} catch (error) { console.error('Umami Analytics: Failed to fetch data', error)
console.error('Umami Analytics: Failed to fetch data', error) throw error
throw error }
} }
}
const insertData = async () => {
const insertData = async () => { try {
try { const tasks = []
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
const pagePV = document.getElementById('umamiPV') if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
if (pagePV) { tasks.push((async () => {
const data = await getData(true) const pagePV = document.getElementById('umamiPV')
if (data && data.pageviews) { if (pagePV) {
pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews const data = await getData(true)
} else { if (data && data.pageviews) {
console.warn('Umami Analytics: Invalid page view data received') pagePV.textContent = extractValue(data.pageviews)
} } else {
} console.warn('Umami Analytics: Invalid page view data received')
} }
}
if (config.site_uv || config.site_pv) { })())
const data = await getData(false) }
if (config.site_uv) { if (config.site_uv || config.site_pv) {
const siteUV = document.getElementById('umami-site-uv') tasks.push((async () => {
if (siteUV && data && data.visitors) { const data = await getData(false)
siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors
} else if (siteUV) { if (config.site_uv) {
console.warn('Umami Analytics: Invalid site UV data received') const siteUV = document.getElementById('umami-site-uv')
} if (siteUV && data && data.visitors) {
} siteUV.textContent = extractValue(data.visitors)
} else if (siteUV) {
if (config.site_pv) { console.warn('Umami Analytics: Invalid site UV data received')
const sitePV = document.getElementById('umami-site-pv') }
if (sitePV && data && data.pageviews) { }
sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else if (sitePV) { if (config.site_pv) {
console.warn('Umami Analytics: Invalid site PV data received') const sitePV = document.getElementById('umami-site-pv')
} if (sitePV && data && data.pageviews) {
} sitePV.textContent = extractValue(data.pageviews)
} } else if (sitePV) {
} catch (error) { console.warn('Umami Analytics: Invalid site PV data received')
console.error('Umami Analytics: Failed to insert data', error) }
} }
} })())
}
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') await Promise.all(tasks)
} catch (error) {
console.error('Umami Analytics: Failed to insert data', error)
loadUmamiJS() }
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData) btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
} else { btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
setTimeout(insertData, 100)
} loadUmamiJS()
})()
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData)
} else {
setTimeout(insertData, 100)
}
})()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hexo-theme-butterfly", "name": "hexo-theme-butterfly",
"version": "5.5.5", "version": "5.6.0",
"description": "A Simple and Card UI Design theme for Hexo", "description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json", "main": "package.json",
"scripts": { "scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
abcjs_basic_js: abcjs_basic_js:
name: abcjs name: abcjs
file: dist/abcjs-basic-min.js file: dist/abcjs-basic-min.js
version: 6.6.3 version: 6.6.4
activate_power_mode: activate_power_mode:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/activate-power-mode.min.js file: dist/activate-power-mode.min.js
@@ -9,7 +9,7 @@ activate_power_mode:
algolia_search: algolia_search:
name: algoliasearch name: algoliasearch
file: dist/lite/builds/browser.umd.js file: dist/lite/builds/browser.umd.js
version: 5.53.0 version: 5.56.0
aplayer_css: aplayer_css:
name: aplayer name: aplayer
file: dist/APlayer.min.css file: dist/APlayer.min.css
@@ -95,7 +95,7 @@ fontawesome:
name: '@fortawesome/fontawesome-free' name: '@fortawesome/fontawesome-free'
file: css/all.min.css file: css/all.min.css
other_name: font-awesome other_name: font-awesome
version: 7.2.0 version: 7.3.1
gitalk: gitalk:
name: gitalk name: gitalk
file: dist/gitalk.min.js file: dist/gitalk.min.js
@@ -125,7 +125,7 @@ lazyload:
mathjax: mathjax:
name: mathjax name: mathjax
file: tex-mml-chtml.js file: tex-mml-chtml.js
version: 4.1.2 version: 4.1.3
medium_zoom: medium_zoom:
name: medium-zoom name: medium-zoom
file: dist/medium-zoom.min.js file: dist/medium-zoom.min.js
@@ -133,7 +133,7 @@ medium_zoom:
mermaid: mermaid:
name: mermaid name: mermaid
file: dist/mermaid.min.js file: dist/mermaid.min.js
version: 11.15.0 version: 11.16.0
meting_js: meting_js:
name: butterfly-extsrc name: butterfly-extsrc
file: metingjs/dist/Meting.min.js file: metingjs/dist/Meting.min.js
@@ -186,7 +186,7 @@ snackbar_css:
twikoo: twikoo:
name: twikoo name: twikoo
file: dist/twikoo.all.min.js file: dist/twikoo.all.min.js
version: 1.7.11 version: 1.7.14
typed: typed:
name: typed.js name: typed.js
file: dist/typed.umd.js file: dist/typed.umd.js
+6 -7
View File
@@ -17,12 +17,11 @@ hexo.extend.helper.register('getArchiveLength', function () {
const m = date.month() + 1 const m = date.month() + 1
const d = date.date() const d = date.date()
if (yearly) { // Always track year so year archive pages can be counted
const keyYear = `${y}` const keyYear = `${y}`
map.set(keyYear, (map.get(keyYear) || 0) + 1) map.set(keyYear, (map.get(keyYear) || 0) + 1)
}
if (monthly) { if (monthly || daily) {
const keyMonth = `${y}-${m}` const keyMonth = `${y}-${m}`
map.set(keyMonth, (map.get(keyMonth) || 0) + 1) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
} }
@@ -37,8 +36,8 @@ hexo.extend.helper.register('getArchiveLength', function () {
// Determine the appropriate key to fetch based on current page context // Determine the appropriate key to fetch based on current page context
let key let key
if (yearly && year) key = `${year}` if (yearly || monthly || daily) key = `${year}`
if (monthly && month) key = `${year}-${month}` if ((monthly || daily) && month) key = `${year}-${month}`
if (daily && day) key = `${year}-${month}-${day}` if (daily && day) key = `${year}-${month}-${day}`
// Return the count for the current period or default to the total posts // Return the count for the current period or default to the total posts
+11 -12
View File
@@ -11,6 +11,8 @@ const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
const archiveRegex = /\/archives\// const archiveRegex = /\/archives\//
const { version: themeVersion } = require('../../package.json')
hexo.extend.helper.register('truncate', truncateContent) hexo.extend.helper.register('truncate', truncateContent)
hexo.extend.helper.register('postDesc', data => { hexo.extend.helper.register('postDesc', data => {
@@ -58,8 +60,6 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
const userColors = normalizeColors(custom_colors) const userColors = normalizeColors(custom_colors)
const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}`
const generateStyle = (size, unit, page, color) => { const generateStyle = (size, unit, page, color) => {
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};` const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}` return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
@@ -70,10 +70,9 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
const size = minfontsize + ((maxfontsize - minfontsize) * ratio) const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
if (userColors && userColors.length) { if (userColors && userColors.length) {
const colorClass = resolveColorClass(idx)
const color = userColors[idx % userColors.length] const color = userColors[idx % userColors.length]
const style = generateStyle(size, unit, page, color) const style = generateStyle(size, unit, page, color)
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item ${colorClass}" style="${style}">${tag.name}</a>` return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item" style="${style}">${tag.name}</a>`
} }
const color = getRandomColor() const color = getRandomColor()
@@ -105,13 +104,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
if (!menu) return defaultTitle if (!menu) return defaultTitle
const loop = m => { const loop = m => {
for (const key in m) { for (const [key, value] of Object.entries(m)) {
if (typeof m[key] === 'object') { if (value && typeof value === 'object') {
const result = loop(m[key]) const result = loop(value)
if (result) return result if (result) return result
} }
if (archiveRegex.test(m[key])) { if (typeof value === 'string' && archiveRegex.test(value)) {
return key return key
} }
} }
@@ -154,9 +153,10 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => {
// This is a hack method, because hexo treats time as UTC time // This is a hack method, because hexo treats time as UTC time
// so you need to manually convert the time zone // so you need to manually convert the time zone
const timezone = hexo.config.timezone
processedData.forEach(item => { processedData.forEach(item => {
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') const parsed = moment.utc(item.date)
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss') item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss')
// markdown // markdown
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' }) item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
}) })
@@ -179,8 +179,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
}) })
hexo.extend.helper.register('getVersion', () => { hexo.extend.helper.register('getVersion', () => {
const { version } = require('../../package.json') return { hexo: hexo.version, theme: themeVersion }
return { hexo: hexo.version, theme: version }
}) })
hexo.extend.helper.register('safeJSON', data => { hexo.extend.helper.register('safeJSON', data => {
+9 -7
View File
@@ -23,7 +23,6 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
if (relatedPosts.has(post.path)) { if (relatedPosts.has(post.path)) {
relatedPosts.get(post.path).weight += 1 relatedPosts.get(post.path).weight += 1
} else { } else {
const getPostDesc = post.postDesc || postDesc(post, hexo)
relatedPosts.set(post.path, { relatedPosts.set(post.path, {
title: post.title, title: post.title,
path: post.path, path: post.path,
@@ -32,7 +31,7 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
weight: 1, weight: 1,
updated: post.updated, updated: post.updated,
created: post.date, created: post.date,
postDesc: getPostDesc, post,
random: Math.random() random: Math.random()
}) })
} }
@@ -61,12 +60,15 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>` result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">' result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) { const max = Math.min(relatedPostsList.length, limitNum)
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i] for (let i = 0; i < max; i++) {
const item = relatedPostsList[i]
let { cover, title, path, cover_type, created, updated, post } = item
const { escape_html, url_for, date } = this const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)' cover = cover || 'var(--default-bg-color)'
title = escape_html(title) title = escape_html(title)
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc' const desc = post.postDesc || postDesc(post, hexo)
const className = desc ? 'pagination-related' : 'pagination-related no-desc'
result += `<a class="${className}" href="${url_for(path)}" title="${title}">` result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
if (cover_type === 'img') { if (cover_type === 'img') {
result += `<img class="cover" src="${url_for(cover)}" alt="cover">` result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
@@ -80,8 +82,8 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
} }
result += `<div class="info-item-2">${title}</div></div>` result += `<div class="info-item-2">${title}</div></div>`
if (postDesc) { if (desc) {
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>` result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
} }
result += '</div></a>' result += '</div></a>'
} }
+5 -4
View File
@@ -7,15 +7,16 @@
'use strict' 'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo) const { url_for, escapeHTML } = require('hexo-util')
const urlFor = url_for.bind(hexo)
const btn = args => { const btn = args => {
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim()) const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
const iconHTML = icon ? `<i class="${icon}"></i>` : '' const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
const textHTML = text ? `<span>${text}</span>` : '' const textHTML = text ? `<span>${escapeHTML(text)}</span>` : ''
return `<a class="btn-beautify ${option}" href="${urlFor(url)}" title="${text}">${iconHTML}${textHTML}</a>` return `<a class="btn-beautify ${escapeHTML(option)}" href="${urlFor(url)}" title="${escapeHTML(text)}">${iconHTML}${textHTML}</a>`
} }
hexo.extend.tag.register('btn', btn, { ends: false }) hexo.extend.tag.register('btn', btn, { ends: false })
+8 -7
View File
@@ -4,24 +4,25 @@
'use strict' 'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo) const { url_for, escapeHTML } = require('hexo-util')
const urlFor = url_for.bind(hexo)
const flinkFn = (args, content) => { const flinkFn = (args, content) => {
const data = hexo.render.renderSync({ text: content, engine: 'yaml' }) const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
let result = '' let result = ''
data.forEach(item => { data.forEach(item => {
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : '' const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : '' const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
const listResult = item.link_list.map(link => ` const listResult = item.link_list.map(link => `
<div class="flink-list-item"> <div class="flink-list-item">
<a href="${link.link}" title="${link.name}" target="_blank"> <a href="${escapeHTML(link.link)}" title="${escapeHTML(link.name)}" target="_blank">
<div class="flink-item-icon"> <div class="flink-item-icon">
<img class="no-lightbox" src="${link.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${link.name}" /> <img class="no-lightbox" src="${escapeHTML(link.avatar)}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${escapeHTML(link.name)}" />
</div> </div>
<div class="flink-item-name">${link.name}</div> <div class="flink-item-name">${escapeHTML(link.name)}</div>
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div> <div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
</a> </a>
</div>`).join('') </div>`).join('')
+1
View File
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
const parseImageContent = content => { const parseImageContent = content => {
const images = [] const images = []
let match let match
IMAGE_REGEX.lastIndex = 0
while ((match = IMAGE_REGEX.exec(content)) !== null) { while ((match = IMAGE_REGEX.exec(content)) !== null) {
images.push({ images.push({
+1 -1
View File
@@ -16,7 +16,7 @@
'use strict' 'use strict'
const parseArgs = args => args.join(' ').split(',') const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
const generateStyle = (bg, color) => { const generateStyle = (bg, color) => {
let style = 'style="' let style = 'style="'
+3 -13
View File
@@ -5,20 +5,10 @@
'use strict' 'use strict'
const { escapeHTML } = require('hexo-util')
const score = (args, content) => { const score = (args, content) => {
// Escape HTML tags and some special characters, including curly braces const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '&#123;' : '&#125;')
const escapeHtmlTags = s => {
const lookup = {
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'<': '&lt;',
'>': '&gt;',
'{': '&#123;',
'}': '&#125;'
}
return s.replace(/[&"'<>{}]/g, c => lookup[c])
}
const trimmed = content.trim() const trimmed = content.trim()
// Split content using six dashes as a delimiter // Split content using six dashes as a delimiter
+4
View File
@@ -12,6 +12,10 @@
const urlFor = require('hexo-util').url_for.bind(hexo) const urlFor = require('hexo-util').url_for.bind(hexo)
const groups = {} const groups = {}
hexo.extend.filter.register('before_generate', () => {
Object.keys(groups).forEach(k => delete groups[k])
})
hexo.extend.filter.register('before_post_render', data => { hexo.extend.filter.register('before_post_render', data => {
if (!hexo.theme.config.series.enable) return data if (!hexo.theme.config.series.enable) return data
+45 -34
View File
@@ -267,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
const fetchUrl = async url => { const fetchUrl = async url => {
try { try {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return await response.json() return await response.json()
} catch (error) { } catch (error) {
console.error('Failed to fetch URL:', error) console.error('Failed to fetch URL:', error)
return [] throw error
} }
} }
const runJustifiedGallery = (container, data, config) => { const runJustifiedGallery = (container, data, config) => {
const { isButton, limit, firstLimit, tabs } = config const { isButton, tabs } = config
const limit = Math.max(1, Number(config.limit) || 20)
const firstLimit = Math.max(1, Number(config.firstLimit) || limit)
const dataLength = data.length const dataLength = data.length
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1) const maxGroupKey = dataLength
? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1
: 0
// Gallery configuration // Gallery configuration
const igConfig = { const igConfig = {
@@ -295,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
let isLayoutHidden = false let isLayoutHidden = false
// Utility functions // Utility functions
const sanitizeString = str => (str && str.replace(/"/g, '&quot;')) || '' const sanitizeString = str => String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
const createImageItem = item => { const createImageItem = item => {
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : '' const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
const title = item.title ? `title="${sanitizeString(item.title)}"` : '' const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
const url = item.url ? sanitizeString(item.url) : ''
return `<div class="item"> return `<div class="item">
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} /> <img src="${url}" data-grid-maintained-target="true" ${alt} ${title} />
</div>` </div>`
} }
@@ -370,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
btf.setLoading.add(container) btf.setLoading.add(container)
ig.on('renderComplete', handleRenderComplete) ig.on('renderComplete', handleRenderComplete)
if (isButton) { if (isButton && dataLength) {
appendItems(1, firstLimit, true) appendItems(1, firstLimit, true)
} else { } else if (dataLength) {
ig.on('requestAppend', handleRequestAppend) ig.on('requestAppend', handleRequestAppend)
ig.renderItems() ig.renderItems()
} else {
btf.setLoading.remove(container)
} }
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy()) btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
@@ -397,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
const container = element.firstElementChild const container = element.firstElementChild
const content = container.textContent const content = container.textContent
container.textContent = '' container.textContent = ''
element.classList.add('loaded')
try { try {
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content) const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
if (!Array.isArray(data)) throw new TypeError('Gallery data must be an array')
runJustifiedGallery(container, data, config) runJustifiedGallery(container, data, config)
element.classList.add('loaded')
} catch (error) { } catch (error) {
console.error('Gallery data parsing failed:', error) console.error('Gallery data parsing failed:', error)
} }
@@ -436,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
*/ */
const scrollFn = () => { const scrollFn = () => {
const $rightside = document.getElementById('rightside') const $rightside = document.getElementById('rightside')
const innerHeight = window.innerHeight + 56
let initTop = 0 let initTop = 0
const $header = document.getElementById('page-header') const $header = document.getElementById('page-header')
const isChatBtn = typeof chatBtn !== 'undefined' const isChatBtn = typeof window.chatBtn !== 'undefined'
const isShowPercent = GLOBAL_CONFIG.percent.rightside const isShowPercent = GLOBAL_CONFIG.percent.rightside
// 檢查文檔高度是否小於視窗高度 // 檢查文檔高度是否小於視窗高度
const checkDocumentHeight = () => { const checkDocumentHeight = () => {
if (document.body.scrollHeight <= innerHeight) { if (document.body.scrollHeight <= window.innerHeight + 56) {
$rightside.classList.add('rightside-show') $rightside.classList.add('rightside-show')
return true return true
} }
return false return false
} }
// 如果文檔高度小於視窗高度,直接返回
if (checkDocumentHeight()) return
// find the scroll direction // find the scroll direction
const scrollDirection = currentTop => { const scrollDirection = currentTop => {
const result = currentTop > initTop // true is down & false is up const result = currentTop > initTop // true is down & false is up
@@ -463,6 +472,8 @@ document.addEventListener('DOMContentLoaded', () => {
let flag = '' let flag = ''
const scrollTask = btf.rafThrottle(() => { const scrollTask = btf.rafThrottle(() => {
if (checkDocumentHeight()) return
const currentTop = window.scrollY || document.documentElement.scrollTop const currentTop = window.scrollY || document.documentElement.scrollTop
const isDown = scrollDirection(currentTop) const isDown = scrollDirection(currentTop)
if (currentTop > 56) { if (currentTop > 56) {
@@ -490,14 +501,14 @@ document.addEventListener('DOMContentLoaded', () => {
$header.classList.remove('nav-fixed', 'nav-visible') $header.classList.remove('nav-fixed', 'nav-visible')
} }
$rightside.classList.remove('rightside-show') $rightside.classList.remove('rightside-show')
} }
isShowPercent && rightsideScrollPercent(currentTop) isShowPercent && rightsideScrollPercent(currentTop)
checkDocumentHeight() })
})
checkDocumentHeight()
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true }) btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
} }
/** /**
* toc, anchor * toc, anchor
@@ -631,17 +642,17 @@ document.addEventListener('DOMContentLoaded', () => {
const handleThemeChange = mode => { const handleThemeChange = mode => {
const globalFn = window.globalFn || {} const globalFn = window.globalFn || {}
const themeChange = globalFn.themeChange || {} const themeChange = globalFn.themeChange
if (!themeChange) { if (!themeChange) return
return
}
Object.keys(themeChange).forEach(key => { Object.keys(themeChange).forEach(key => {
const themeChangeFn = themeChange[key] const fn = themeChange[key]
if (typeof fn !== 'function') return
if (['disqus', 'disqusjs'].includes(key)) { if (['disqus', 'disqusjs'].includes(key)) {
setTimeout(() => themeChangeFn(mode), 300) setTimeout(() => fn(mode), 300)
} else { } else {
themeChangeFn(mode) fn(mode)
} }
}) })
} }
@@ -952,7 +963,7 @@ document.addEventListener('DOMContentLoaded', () => {
const forPostFn = () => { const forPostFn = () => {
const $article = document.getElementById('article-container') const $article = document.getElementById('article-container')
if (!$article) return if (!$article || $article.querySelector('.hbe-container')) return
addHighlightTool($article) addHighlightTool($article)
addPhotoFigcaption($article) addPhotoFigcaption($article)