From d8942c236a015416459f05a790d5b7f7578a0e97 Mon Sep 17 00:00:00 2001 From: myw Date: Thu, 16 Jul 2026 16:53:38 +0800 Subject: [PATCH] update --- _config.yml | 10 +- layout/includes/third-party/abcjs/abcjs.pug | 7 +- layout/includes/third-party/chat/index.pug | 8 +- layout/includes/third-party/chat/knocket.pug | 22 +- layout/includes/third-party/math/mathjax.pug | 2 +- layout/includes/third-party/math/mermaid.pug | 11 +- layout/includes/third-party/pjax.pug | 9 +- layout/includes/third-party/prismjs.pug | 6 +- layout/includes/third-party/subtitle.pug | 10 +- .../includes/third-party/umami_analytics.pug | 226 +++++++++--------- package.json | 2 +- plugins.yml | 12 +- scripts/helpers/getArchiveLength.js | 13 +- scripts/helpers/page.js | 23 +- scripts/helpers/related_post.js | 16 +- scripts/tag/button.js | 9 +- scripts/tag/flink.js | 15 +- scripts/tag/gallery.js | 1 + scripts/tag/hide.js | 2 +- scripts/tag/score.js | 16 +- scripts/tag/series.js | 4 + source/js/main.js | 79 +++--- 22 files changed, 263 insertions(+), 240 deletions(-) diff --git a/_config.yml b/_config.yml index 496616e..36c9211 100644 --- a/_config.yml +++ b/_config.yml @@ -659,7 +659,7 @@ artalk: # -------------------------------------- chat: - # Choose: chatra/tidio/crisp/knocket + # Choose: chatra/tidio/crisp/knocket # Leave it empty if you don't need chat use: # Chat Button [recommend] @@ -680,10 +680,10 @@ tidio: crisp: website_id: -# https://trtc.io/solutions/knocket -knocket: - identifier: - +# https://trtc.io/solutions/knocket +knocket: + identifier: + # -------------------------------------- # Analysis # -------------------------------------- diff --git a/layout/includes/third-party/abcjs/abcjs.pug b/layout/includes/third-party/abcjs/abcjs.pug index 22a98b8..56408f2 100644 --- a/layout/includes/third-party/abcjs/abcjs.pug +++ b/layout/includes/third-party/abcjs/abcjs.pug @@ -2,8 +2,11 @@ script. (() => { const abcjsInit = () => { const abcjsFn = () => { + const $article = document.getElementById("article-container") + if (!$article || $article.querySelector(".hbe-container")) return + setTimeout(() => { - const sheets = document.querySelectorAll(".abc-music-sheet") + const sheets = $article.querySelectorAll(".abc-music-sheet") for (let i = 0; i < sheets.length; i++) { const ele = sheets[i] if (ele.children.length > 0) continue @@ -29,7 +32,7 @@ script. }, 100) } - if (typeof ABCJS === "object") { + if (typeof ABCJS === "object" && ABCJS !== null) { abcjsFn() } else { btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn) diff --git a/layout/includes/third-party/chat/index.pug b/layout/includes/third-party/chat/index.pug index ace9b3b..4d6a942 100644 --- a/layout/includes/third-party/chat/index.pug +++ b/layout/includes/third-party/chat/index.pug @@ -3,7 +3,7 @@ case theme.chat.use include ./chatra.pug when 'tidio' include ./tidio.pug - when 'crisp' - include ./crisp.pug - when 'knocket' - include ./knocket.pug \ No newline at end of file + when 'crisp' + include ./crisp.pug + when 'knocket' + include ./knocket.pug diff --git a/layout/includes/third-party/chat/knocket.pug b/layout/includes/third-party/chat/knocket.pug index 0f76439..039055e 100644 --- a/layout/includes/third-party/chat/knocket.pug +++ b/layout/includes/third-party/chat/knocket.pug @@ -1,14 +1,15 @@ //- Knocket live chat — https://trtc.io/solutions/knocket script. (() => { - const id = '#{theme.knocket.identifier}' - if (!id) return + const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/ { + btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => { const isChatBtn = !{theme.chat.rightside_button} 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) { const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' } @@ -24,21 +25,18 @@ script. window.chatBtnFn = () => { const w = getWidget() if (!w) return - if (w.style.display === 'none') show() - else hide() + w.style.display = w.style.display === 'none' ? '' : 'none' } - document.getElementById('chat-btn').style.display = 'block' + const chatBtn = document.getElementById('chat-btn') + if (chatBtn) chatBtn.style.display = 'block' } else if (isChatHideShow) { - const observer = new MutationObserver(() => { - if (getWidget()) observer.disconnect() - }) - observer.observe(document.body, { childList: true, subtree: true }) - window.chatBtn = { hide: () => { const w = getWidget(); if (w) w.style.display = 'none' }, show: () => { const w = getWidget(); if (w) w.style.display = '' } } } + }).catch(error => { + console.warn('[Knocket] Failed to load the chat SDK.', error) }) })() diff --git a/layout/includes/third-party/math/mathjax.pug b/layout/includes/third-party/math/mathjax.pug index 1e85bd6..660eeca 100644 --- a/layout/includes/third-party/math/mathjax.pug +++ b/layout/includes/third-party/math/mathjax.pug @@ -15,7 +15,7 @@ script. const loadMathjax = () => { const article = document.getElementById('article-container') - if (!article) return + if (!article || article.querySelector('.hbe-container')) return changeScriptToMath(article) if (!window.MathJax) { diff --git a/layout/includes/third-party/math/mermaid.pug b/layout/includes/third-party/math/mermaid.pug index 475d619..dd04714 100644 --- a/layout/includes/third-party/math/mermaid.pug +++ b/layout/includes/third-party/math/mermaid.pug @@ -330,8 +330,8 @@ script. }) } - const codeToMermaid = () => { - const codeMermaidEle = document.querySelectorAll('pre > code.mermaid') + const codeToMermaid = $article => { + const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid') if (codeMermaidEle.length === 0) return codeMermaidEle.forEach(ele => { @@ -347,8 +347,11 @@ script. } const loadMermaid = () => { - if (!{theme.mermaid.code_write}) codeToMermaid() - const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap') + const $article = document.getElementById('article-container') + if (!$article) return + + if (!{theme.mermaid.code_write}) codeToMermaid($article) + const $mermaid = $article.querySelectorAll('.mermaid-wrap') if ($mermaid.length === 0) return const runMermaidFn = () => runMermaid($mermaid) diff --git a/layout/includes/third-party/pjax.pug b/layout/includes/third-party/pjax.pug index 75d1964..38e0504 100644 --- a/layout/includes/third-party/pjax.pug +++ b/layout/includes/third-party/pjax.pug @@ -65,12 +65,13 @@ script. }) 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} ? pjax.loadUrl('!{url_for("/404.html")}') - : window.location.href = e.request.responseURL - } else { - window.location.href = e.request.responseURL + : responseURL && (window.location.href = responseURL) + } else if (responseURL) { + window.location.href = responseURL } }) }) \ No newline at end of file diff --git a/layout/includes/third-party/prismjs.pug b/layout/includes/third-party/prismjs.pug index 0c866ab..d1a98b7 100644 --- a/layout/includes/third-party/prismjs.pug +++ b/layout/includes/third-party/prismjs.pug @@ -5,11 +5,15 @@ if (syntax_highlighter === 'prismjs' || enable) && !preprocess script. (() => { + const $article = document.getElementById('article-container') + if (!$article || $article.querySelector('.hbe-container')) return + window.Prism = window.Prism || {} window.Prism.manual = true const highlightAll = () => { - window.Prism.highlightAll() + // window.Prism.highlightAll() + window.Prism.highlightAllUnder($article, true) } window.addEventListener('load', highlightAll) diff --git a/layout/includes/third-party/subtitle.pug b/layout/includes/third-party/subtitle.pug index 4e04e54..ebe027a 100644 --- a/layout/includes/third-party/subtitle.pug +++ b/layout/includes/third-party/subtitle.pug @@ -44,7 +44,7 @@ script. } } } - btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy') + btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy') case source when 1 @@ -53,8 +53,8 @@ case source fetch('https://v1.hitokoto.cn') .then(response => response.json()) .then(data => { - const from = '出自 ' + data.from - typedJSFn.processSubtitle(data.hitokoto, [from]) + const from = data.from ? '出自 ' + data.from : '' + typedJSFn.processSubtitle(data.hitokoto, from ? [from] : []) }) .catch(err => { console.error('Failed to get the Hitokoto API:', err) @@ -79,7 +79,7 @@ case source }) .catch(err => { console.error('Failed to get the Yiyan API:', err) - typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) + typedJSFn.processSubtitle(!{JSON.stringify(subContent)}) }) } typedJSFn.run(subtitleType) @@ -99,7 +99,7 @@ case source }) .catch(err => { console.error('Failed to get the Jinrishici API:', err) - typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) + typedJSFn.processSubtitle(!{JSON.stringify(subContent)}) }) } typedJSFn.run(subtitleType) diff --git a/layout/includes/third-party/umami_analytics.pug b/layout/includes/third-party/umami_analytics.pug index 14f1fd6..96c1f69 100644 --- a/layout/includes/third-party/umami_analytics.pug +++ b/layout/includes/third-party/umami_analytics.pug @@ -1,110 +1,116 @@ -- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics -- const isServerURL = !!serverURL -- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is' -- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1' - -script. - (() => { - const option = !{JSON.stringify(option)} - const config = !{JSON.stringify(UV_PV)} - - const runTrack = () => { - if (typeof umami !== 'undefined' && typeof umami.track === 'function') { - umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) - } else { - console.warn('Umami Analytics: umami.track is not available') - } - } - - const loadUmamiJS = () => { - btf.getScript('!{baseURL}/!{script_name}', { - 'data-website-id': '!{website_id}', - 'data-auto-track': 'false', - ...option - }).then(() => { - runTrack() - }).catch(error => { - console.error('Umami Analytics: Error loading script', error) - }) - } - - const getData = async (isPost) => { - try { - const now = Date.now() - const keyUrl = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : '' - const headerList = { 'Accept': 'application/json' } - - if (!{isServerURL}) { - headerList['Authorization'] = `Bearer ${config.token}` - } else { - headerList['x-umami-api-key'] = config.token - } - - const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { - method: "GET", - headers: headerList - }) - - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`) - } - - return await res.json() - } catch (error) { - console.error('Umami Analytics: Failed to fetch data', error) - throw error - } - } - - const insertData = async () => { - try { - if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) { - const pagePV = document.getElementById('umamiPV') - if (pagePV) { - const data = await getData(true) - if (data && data.pageviews) { - pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : 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) { - const siteUV = document.getElementById('umami-site-uv') - if (siteUV && data && data.visitors) { - siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors - } else if (siteUV) { - console.warn('Umami Analytics: Invalid site UV data received') - } - } - - if (config.site_pv) { - 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) { - console.warn('Umami Analytics: Invalid site PV data received') - } - } - } - } catch (error) { - console.error('Umami Analytics: Failed to insert data', error) - } - } - - btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track') - btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') - - - loadUmamiJS() - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', insertData) - } else { - setTimeout(insertData, 100) - } - })() \ No newline at end of file +- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics +- const isServerURL = !!serverURL +- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is' +- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1' + +script. + (() => { + const option = !{JSON.stringify(option)} + const config = !{JSON.stringify(UV_PV)} + + const runTrack = () => { + if (typeof umami !== 'undefined' && typeof umami.track === 'function') { + umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) + } else { + console.warn('Umami Analytics: umami.track is not available') + } + } + + const loadUmamiJS = () => { + btf.getScript('!{baseURL}/!{script_name}', { + 'data-website-id': '!{website_id}', + 'data-auto-track': 'false', + ...option + }).then(() => { + runTrack() + }).catch(error => { + console.error('Umami Analytics: Error loading script', error) + }) + } + + const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat + + const getData = async (isPost) => { + try { + const now = Date.now() + const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : '' + const headers = { + 'Accept': 'application/json', + [!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token + } + + const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, { + method: 'GET', + headers + }) + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`) + } + + return await res.json() + } catch (error) { + console.error('Umami Analytics: Failed to fetch data', error) + throw error + } + } + + const insertData = async () => { + try { + const tasks = [] + + if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) { + tasks.push((async () => { + const pagePV = document.getElementById('umamiPV') + if (pagePV) { + const data = await getData(true) + if (data && data.pageviews) { + pagePV.textContent = extractValue(data.pageviews) + } else { + console.warn('Umami Analytics: Invalid page view data received') + } + } + })()) + } + + if (config.site_uv || config.site_pv) { + tasks.push((async () => { + const data = await getData(false) + + if (config.site_uv) { + const siteUV = document.getElementById('umami-site-uv') + if (siteUV && data && data.visitors) { + siteUV.textContent = extractValue(data.visitors) + } else if (siteUV) { + console.warn('Umami Analytics: Invalid site UV data received') + } + } + + if (config.site_pv) { + const sitePV = document.getElementById('umami-site-pv') + if (sitePV && data && data.pageviews) { + sitePV.textContent = extractValue(data.pageviews) + } else if (sitePV) { + console.warn('Umami Analytics: Invalid site PV data received') + } + } + })()) + } + + await Promise.all(tasks) + } catch (error) { + console.error('Umami Analytics: Failed to insert data', error) + } + } + + btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track') + btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') + + loadUmamiJS() + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', insertData) + } else { + setTimeout(insertData, 100) + } + })() diff --git a/package.json b/package.json index f6b86eb..5debe44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.5.5", + "version": "5.6.0", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/plugins.yml b/plugins.yml index ee0f167..ae5f39c 100644 --- a/plugins.yml +++ b/plugins.yml @@ -1,7 +1,7 @@ abcjs_basic_js: name: abcjs file: dist/abcjs-basic-min.js - version: 6.6.3 + version: 6.6.4 activate_power_mode: name: butterfly-extsrc file: dist/activate-power-mode.min.js @@ -9,7 +9,7 @@ activate_power_mode: algolia_search: name: algoliasearch file: dist/lite/builds/browser.umd.js - version: 5.53.0 + version: 5.56.0 aplayer_css: name: aplayer file: dist/APlayer.min.css @@ -95,7 +95,7 @@ fontawesome: name: '@fortawesome/fontawesome-free' file: css/all.min.css other_name: font-awesome - version: 7.2.0 + version: 7.3.1 gitalk: name: gitalk file: dist/gitalk.min.js @@ -125,7 +125,7 @@ lazyload: mathjax: name: mathjax file: tex-mml-chtml.js - version: 4.1.2 + version: 4.1.3 medium_zoom: name: medium-zoom file: dist/medium-zoom.min.js @@ -133,7 +133,7 @@ medium_zoom: mermaid: name: mermaid file: dist/mermaid.min.js - version: 11.15.0 + version: 11.16.0 meting_js: name: butterfly-extsrc file: metingjs/dist/Meting.min.js @@ -186,7 +186,7 @@ snackbar_css: twikoo: name: twikoo file: dist/twikoo.all.min.js - version: 1.7.11 + version: 1.7.14 typed: name: typed.js file: dist/typed.umd.js diff --git a/scripts/helpers/getArchiveLength.js b/scripts/helpers/getArchiveLength.js index 5f7d812..76e573e 100644 --- a/scripts/helpers/getArchiveLength.js +++ b/scripts/helpers/getArchiveLength.js @@ -17,12 +17,11 @@ hexo.extend.helper.register('getArchiveLength', function () { const m = date.month() + 1 const d = date.date() - if (yearly) { - const keyYear = `${y}` - map.set(keyYear, (map.get(keyYear) || 0) + 1) - } + // Always track year so year archive pages can be counted + const keyYear = `${y}` + map.set(keyYear, (map.get(keyYear) || 0) + 1) - if (monthly) { + if (monthly || daily) { const keyMonth = `${y}-${m}` 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 let key - if (yearly && year) key = `${year}` - if (monthly && month) key = `${year}-${month}` + if (yearly || monthly || daily) key = `${year}` + if ((monthly || daily) && month) key = `${year}-${month}` if (daily && day) key = `${year}-${month}-${day}` // Return the count for the current period or default to the total posts diff --git a/scripts/helpers/page.js b/scripts/helpers/page.js index 45d4b35..a9cf5bb 100644 --- a/scripts/helpers/page.js +++ b/scripts/helpers/page.js @@ -11,6 +11,8 @@ const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i const archiveRegex = /\/archives\// +const { version: themeVersion } = require('../../package.json') + hexo.extend.helper.register('truncate', truncateContent) hexo.extend.helper.register('postDesc', data => { @@ -58,8 +60,6 @@ hexo.extend.helper.register('cloudTags', function (options = {}) { const userColors = normalizeColors(custom_colors) - const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}` - const generateStyle = (size, unit, page, color) => { const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};` 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) if (userColors && userColors.length) { - const colorClass = resolveColorClass(idx) const color = userColors[idx % userColors.length] const style = generateStyle(size, unit, page, color) - return `${tag.name}` + return `${tag.name}` } const color = getRandomColor() @@ -105,13 +104,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) { if (!menu) return defaultTitle const loop = m => { - for (const key in m) { - if (typeof m[key] === 'object') { - const result = loop(m[key]) + for (const [key, value] of Object.entries(m)) { + if (value && typeof value === 'object') { + const result = loop(value) if (result) return result } - if (archiveRegex.test(m[key])) { + if (typeof value === 'string' && archiveRegex.test(value)) { 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 // so you need to manually convert the time zone + const timezone = hexo.config.timezone processedData.forEach(item => { - const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') - item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss') + const parsed = moment.utc(item.date) + item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss') // 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', () => { - const { version } = require('../../package.json') - return { hexo: hexo.version, theme: version } + return { hexo: hexo.version, theme: themeVersion } }) hexo.extend.helper.register('safeJSON', data => { diff --git a/scripts/helpers/related_post.js b/scripts/helpers/related_post.js index 2aa3148..23b17d1 100644 --- a/scripts/helpers/related_post.js +++ b/scripts/helpers/related_post.js @@ -23,7 +23,6 @@ hexo.extend.helper.register('related_posts', function (currentPost) { if (relatedPosts.has(post.path)) { relatedPosts.get(post.path).weight += 1 } else { - const getPostDesc = post.postDesc || postDesc(post, hexo) relatedPosts.set(post.path, { title: post.title, path: post.path, @@ -32,7 +31,7 @@ hexo.extend.helper.register('related_posts', function (currentPost) { weight: 1, updated: post.updated, created: post.date, - postDesc: getPostDesc, + post, random: Math.random() }) } @@ -61,12 +60,15 @@ hexo.extend.helper.register('related_posts', function (currentPost) { result += `
${headlineLang}
` result += '` - if (postDesc) { - result += `
${postDesc}
` + if (desc) { + result += `
${desc}
` } result += '
' } diff --git a/scripts/tag/button.js b/scripts/tag/button.js index c8303c2..ef72206 100644 --- a/scripts/tag/button.js +++ b/scripts/tag/button.js @@ -7,15 +7,16 @@ '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 [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim()) - const iconHTML = icon ? `` : '' - const textHTML = text ? `${text}` : '' + const iconHTML = icon ? `` : '' + const textHTML = text ? `${escapeHTML(text)}` : '' - return `${iconHTML}${textHTML}` + return `${iconHTML}${textHTML}` } hexo.extend.tag.register('btn', btn, { ends: false }) diff --git a/scripts/tag/flink.js b/scripts/tag/flink.js index 1d77841..81802ff 100644 --- a/scripts/tag/flink.js +++ b/scripts/tag/flink.js @@ -4,24 +4,25 @@ '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 data = hexo.render.renderSync({ text: content, engine: 'yaml' }) let result = '' data.forEach(item => { - const className = item.class_name ? `` : '' - const classDesc = item.class_desc ? `` : '' + const className = item.class_name ? `` : '' + const classDesc = item.class_desc ? `` : '' const listResult = item.link_list.map(link => ` `).join('') diff --git a/scripts/tag/gallery.js b/scripts/tag/gallery.js index e50bf70..bd8d556 100644 --- a/scripts/tag/gallery.js +++ b/scripts/tag/gallery.js @@ -27,6 +27,7 @@ const parseGalleryArgs = args => { const parseImageContent = content => { const images = [] let match + IMAGE_REGEX.lastIndex = 0 while ((match = IMAGE_REGEX.exec(content)) !== null) { images.push({ diff --git a/scripts/tag/hide.js b/scripts/tag/hide.js index c244ddb..11d2e7f 100644 --- a/scripts/tag/hide.js +++ b/scripts/tag/hide.js @@ -16,7 +16,7 @@ 'use strict' -const parseArgs = args => args.join(' ').split(',') +const parseArgs = args => args.join(' ').split(',').map(s => s.trim()) const generateStyle = (bg, color) => { let style = 'style="' diff --git a/scripts/tag/score.js b/scripts/tag/score.js index a356325..15dd964 100644 --- a/scripts/tag/score.js +++ b/scripts/tag/score.js @@ -5,20 +5,10 @@ 'use strict' +const { escapeHTML } = require('hexo-util') + const score = (args, content) => { - // Escape HTML tags and some special characters, including curly braces - const escapeHtmlTags = s => { - const lookup = { - '&': '&', - '"': '"', - "'": ''', - '<': '<', - '>': '>', - '{': '{', - '}': '}' - } - return s.replace(/[&"'<>{}]/g, c => lookup[c]) - } + const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '{' : '}') const trimmed = content.trim() // Split content using six dashes as a delimiter diff --git a/scripts/tag/series.js b/scripts/tag/series.js index 77f87c7..cadd2d4 100644 --- a/scripts/tag/series.js +++ b/scripts/tag/series.js @@ -12,6 +12,10 @@ const urlFor = require('hexo-util').url_for.bind(hexo) const groups = {} +hexo.extend.filter.register('before_generate', () => { + Object.keys(groups).forEach(k => delete groups[k]) +}) + hexo.extend.filter.register('before_post_render', data => { if (!hexo.theme.config.series.enable) return data diff --git a/source/js/main.js b/source/js/main.js index 539e747..5fc6dca 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -267,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => { const fetchUrl = async url => { try { const response = await fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) return await response.json() - } catch (error) { - console.error('Failed to fetch URL:', error) - return [] - } + } catch (error) { + console.error('Failed to fetch URL:', error) + throw error + } } 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 maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1) + const maxGroupKey = dataLength + ? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1 + : 0 // Gallery configuration const igConfig = { @@ -295,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => { let isLayoutHidden = false // Utility functions - const sanitizeString = str => (str && str.replace(/"/g, '"')) || '' + const sanitizeString = str => String(str ?? '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>') const createImageItem = item => { const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : '' const title = item.title ? `title="${sanitizeString(item.title)}"` : '' + const url = item.url ? sanitizeString(item.url) : '' return `
- +
` } @@ -370,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => { btf.setLoading.add(container) ig.on('renderComplete', handleRenderComplete) - if (isButton) { + if (isButton && dataLength) { appendItems(1, firstLimit, true) - } else { + } else if (dataLength) { ig.on('requestAppend', handleRequestAppend) ig.renderItems() + } else { + btf.setLoading.remove(container) } btf.addGlobalFn('pjaxSendOnce', () => ig.destroy()) @@ -397,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => { const container = element.firstElementChild const content = container.textContent container.textContent = '' - element.classList.add('loaded') - try { 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) + element.classList.add('loaded') } catch (error) { console.error('Gallery data parsing failed:', error) } @@ -436,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => { */ const scrollFn = () => { const $rightside = document.getElementById('rightside') - const innerHeight = window.innerHeight + 56 let initTop = 0 const $header = document.getElementById('page-header') - const isChatBtn = typeof chatBtn !== 'undefined' + const isChatBtn = typeof window.chatBtn !== 'undefined' const isShowPercent = GLOBAL_CONFIG.percent.rightside // 檢查文檔高度是否小於視窗高度 const checkDocumentHeight = () => { - if (document.body.scrollHeight <= innerHeight) { + if (document.body.scrollHeight <= window.innerHeight + 56) { $rightside.classList.add('rightside-show') return true } return false } - // 如果文檔高度小於視窗高度,直接返回 - if (checkDocumentHeight()) return - // find the scroll direction const scrollDirection = currentTop => { const result = currentTop > initTop // true is down & false is up @@ -463,6 +472,8 @@ document.addEventListener('DOMContentLoaded', () => { let flag = '' const scrollTask = btf.rafThrottle(() => { + if (checkDocumentHeight()) return + const currentTop = window.scrollY || document.documentElement.scrollTop const isDown = scrollDirection(currentTop) if (currentTop > 56) { @@ -490,14 +501,14 @@ document.addEventListener('DOMContentLoaded', () => { $header.classList.remove('nav-fixed', 'nav-visible') } $rightside.classList.remove('rightside-show') - } - - isShowPercent && rightsideScrollPercent(currentTop) - checkDocumentHeight() - }) - - btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true }) - } + } + + isShowPercent && rightsideScrollPercent(currentTop) + }) + + checkDocumentHeight() + btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true }) + } /** * toc, anchor @@ -631,17 +642,17 @@ document.addEventListener('DOMContentLoaded', () => { const handleThemeChange = mode => { const globalFn = window.globalFn || {} - const themeChange = globalFn.themeChange || {} - if (!themeChange) { - return - } + const themeChange = globalFn.themeChange + if (!themeChange) return Object.keys(themeChange).forEach(key => { - const themeChangeFn = themeChange[key] + const fn = themeChange[key] + if (typeof fn !== 'function') return + if (['disqus', 'disqusjs'].includes(key)) { - setTimeout(() => themeChangeFn(mode), 300) + setTimeout(() => fn(mode), 300) } else { - themeChangeFn(mode) + fn(mode) } }) } @@ -952,7 +963,7 @@ document.addEventListener('DOMContentLoaded', () => { const forPostFn = () => { const $article = document.getElementById('article-container') - if (!$article) return + if (!$article || $article.querySelector('.hbe-container')) return addHighlightTool($article) addPhotoFigcaption($article)