From 79e16a5cdd29df6e35ce8eec092db62e493b9095 Mon Sep 17 00:00:00 2001 From: myw Date: Mon, 10 Aug 2026 17:54:17 +0800 Subject: [PATCH] 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 --- _config.yml | 12 +- layout/includes/header/post-info.pug | 4 +- .../includes/third-party/comments/waline.pug | 23 +- layout/includes/third-party/math/chartjs.pug | 17 +- layout/includes/third-party/math/katex.pug | 37 +-- layout/includes/third-party/math/mathjax.pug | 4 +- layout/includes/third-party/math/mermaid.pug | 4 +- .../third-party/newest-comments/artalk.pug | 127 +++++----- .../third-party/newest-comments/common.pug | 158 +++++++----- .../newest-comments/disqus-comment.pug | 57 ++--- .../newest-comments/github-issues.pug | 111 ++++----- .../third-party/newest-comments/remark42.pug | 55 ++--- .../newest-comments/twikoo-comment.pug | 77 +++--- .../third-party/newest-comments/valine.pug | 95 ++++--- .../third-party/newest-comments/waline.pug | 57 ++--- layout/includes/third-party/subtitle.pug | 233 +++++++++--------- .../includes/third-party/umami_analytics.pug | 70 +++--- package.json | 2 +- scripts/events/cdn.js | 2 +- scripts/helpers/inject_head_js.js | 14 +- scripts/helpers/page.js | 71 +++++- scripts/tag/chartjs.js | 2 +- source/js/main.js | 12 +- source/js/tw_cn.js | 6 +- source/js/utils.js | 100 ++++---- 25 files changed, 726 insertions(+), 624 deletions(-) diff --git a/_config.yml b/_config.yml index 932f048..05d6b88 100644 --- a/_config.yml +++ b/_config.yml @@ -427,7 +427,6 @@ copy: enable: false limit_count: 150 -# Need to install the hexo-wordcount plugin wordcount: enable: false # Display the word count of the article in post meta @@ -652,6 +651,7 @@ artalk: site: # Use Artalk visitor count as the page view count visitor: false + vote: false option: # -------------------------------------- @@ -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/header/post-info.pug b/layout/includes/header/post-info.pug index 2e5100c..5a3ed84 100644 --- a/layout/includes/header/post-info.pug +++ b/layout/includes/header/post-info.pug @@ -43,13 +43,13 @@ if theme.wordcount.post_wordcount i.far.fa-file-word.fa-fw.post-meta-icon span.post-meta-label= _p('post.wordcount') + ':' - span.word-count= wordcount(page.content) + span.word-count= wordcount(page) if theme.wordcount.min2read span.post-meta-separator | if theme.wordcount.min2read i.far.fa-clock.fa-fw.post-meta-icon span.post-meta-label= _p('post.min2read') + ':' - span= min2read(page.content, {cn: 350, en: 160}) + _p('post.min2read_unit') + span= min2read(page) + ' ' + _p('post.min2read_unit') //- for pv and count mixin pvBlock(parent_id, parent_class, parent_title) diff --git a/layout/includes/third-party/comments/waline.pug b/layout/includes/third-party/comments/waline.pug index a39c5dc..06eb04e 100644 --- a/layout/includes/third-party/comments/waline.pug +++ b/layout/includes/third-party/comments/waline.pug @@ -20,8 +20,29 @@ script. path: isShuoshuo ? path : (option && option.path) || path }) + let lightboxObserver = null + if (GLOBAL_CONFIG.lightbox !== 'null') { + const wrap = el.querySelector('#waline-wrap') + if (wrap) { + lightboxObserver = new MutationObserver(mutations => { + const imgs = [] + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType !== 1) return + const nodeImgs = node.matches && node.matches('img') ? [node] : node.querySelectorAll('img') + nodeImgs.forEach(img => imgs.push(img)) + }) + }) + const lightboxImgs = imgs.filter(img => img.closest('.wl-content') && !img.classList.contains('wl-emoji')) + if (lightboxImgs.length) btf.loadLightbox(lightboxImgs) + }) + lightboxObserver.observe(wrap, { childList: true, subtree: true }) + } + } + if (isShuoshuo) { window.shuoshuoComment.destroyWaline = () => { + lightboxObserver && lightboxObserver.disconnect() destroyWaline(waline) if (el.children.length) { el.innerHTML = '' @@ -46,7 +67,7 @@ script. if (isShuoshuo) { '!{use[0]}' === 'Waline' - ? window.shuoshuoComment = { loadComment: loadWaline } + ? window.shuoshuoComment = { loadComment: loadWaline } : window.loadOtherComment = loadWaline return } diff --git a/layout/includes/third-party/math/chartjs.pug b/layout/includes/third-party/math/chartjs.pug index e51c621..a9023c9 100644 --- a/layout/includes/third-party/math/chartjs.pug +++ b/layout/includes/third-party/math/chartjs.pug @@ -22,7 +22,7 @@ script. const value = obj[key] // If the property is an object and has theme-specific options, apply them if (typeof value === 'object' && value !== null) { - if (value[theme]) { + if (theme in value) { obj[key] = value[theme] // Apply the value for the current theme } else { // Recursively process child objects @@ -32,6 +32,11 @@ script. }) } + const destroyChart = canvas => { + const chart = canvas && window.Chart && Chart.getChart(canvas) + if (chart) chart.destroy() + } + const runChartJS = ele => { window.loadChartJS = true @@ -41,9 +46,10 @@ script. const width = item.getAttribute('data-width') const existingCanvas = document.getElementById(chartID) - // If a canvas already exists, remove it to avoid rendering duplicates + // If a canvas already exists, destroy its chart and remove it to avoid rendering duplicates if (existingCanvas) { - existingCanvas.parentNode.remove() + destroyChart(existingCanvas) + existingCanvas.parentNode.remove() } const chartDefinition = chartSrc.textContent @@ -83,6 +89,11 @@ script. window.loadChartJS ? runChartJS(chartJSEle) : btf.getScript('!{url_for(theme.asset.chartjs)}').then(() => runChartJS(chartJSEle)) } + // Destroy all charts before navigating away to avoid leaks + btf.addGlobalFn('pjaxSendOnce', () => { + document.querySelectorAll('.chartjs-wrap canvas').forEach(destroyChart) + }, 'chartjs') + // Listen for theme change events btf.addGlobalFn('themeChange', loadChartJS, 'chartjs') btf.addGlobalFn('encrypt', loadChartJS, 'chartjs') diff --git a/layout/includes/third-party/math/katex.pug b/layout/includes/third-party/math/katex.pug index 6e429a5..56153e0 100644 --- a/layout/includes/third-party/math/katex.pug +++ b/layout/includes/third-party/math/katex.pug @@ -1,16 +1,21 @@ -script. - (async () => { - const showKatex = () => { - document.querySelectorAll('#article-container .katex').forEach(el => el.classList.add('katex-show')) - } - - if (!window.katex_js_css) { - window.katex_js_css = true - await btf.getCSS('!{url_for(theme.asset.katex)}') - if (!{theme.math.katex.copy_tex}) { - await btf.getScript('!{url_for(theme.asset.katex_copytex)}') - } - } - - showKatex() - })() \ No newline at end of file +script. + (async () => { + const showKatex = () => { + document.querySelectorAll('#article-container .katex').forEach(el => el.classList.add('katex-show')) + } + + if (!window.katex_js_css) { + window.katex_js_css = true + try { + await btf.getCSS('!{url_for(theme.asset.katex)}') + if (!{theme.math.katex.copy_tex}) { + await btf.getScript('!{url_for(theme.asset.katex_copytex)}') + } + } catch (e) { + console.error('[katex] failed to load assets, showing formulas anyway:', e) + } + } + + showKatex() + btf.addGlobalFn('encrypt', showKatex, 'katex') + })() diff --git a/layout/includes/third-party/math/mathjax.pug b/layout/includes/third-party/math/mathjax.pug index 660eeca..f910dbc 100644 --- a/layout/includes/third-party/math/mathjax.pug +++ b/layout/includes/third-party/math/mathjax.pug @@ -77,5 +77,7 @@ script. } btf.addGlobalFn('encrypt', loadMathjax, 'mathjax') - window.pjax ? loadMathjax() : window.addEventListener('load', loadMathjax) + if (window.pjax) loadMathjax() + else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', loadMathjax) + else loadMathjax() })() \ No newline at end of file diff --git a/layout/includes/third-party/math/mermaid.pug b/layout/includes/third-party/math/mermaid.pug index dd04714..9746996 100644 --- a/layout/includes/third-party/math/mermaid.pug +++ b/layout/includes/third-party/math/mermaid.pug @@ -282,12 +282,14 @@ script. const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' ele.forEach((item, index) => { const mermaidSrc = item.firstElementChild - // Clean up event listeners before removing old SVG + // Clean up event listeners and stale content before re-rendering if (item.__mermaidAbortController) { item.__mermaidAbortController.abort() } const oldSvg = item.querySelector('svg') if (oldSvg) oldSvg.remove() + const oldError = item.querySelector('.mermaid-error') + if (oldError) oldError.remove() let config = {} try { config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {} diff --git a/layout/includes/third-party/newest-comments/artalk.pug b/layout/includes/third-party/newest-comments/artalk.pug index cf51638..b6be6e6 100644 --- a/layout/includes/third-party/newest-comments/artalk.pug +++ b/layout/includes/third-party/newest-comments/artalk.pug @@ -1,67 +1,60 @@ -- const { server, site, option } = theme.artalk -- const avatarCdn = (option !== null && option.gravatar && option.gravatar.mirror) || '' -- const avatarDefault = (option !== null && option.gravatar && (option.gravatar.params || option.gravatar.default)) || '' - -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'artalk-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getAvatarValue = async () => { - const predefinedAvatarCdn = '!{avatarCdn}' - const predefinedAvatarDefault = '!{avatarDefault}' - - const avatarDefaultFormat = e => e.startsWith('d=') ? e : `d=${e}` - - if (predefinedAvatarCdn && predefinedAvatarDefault) { - return { avatarCdn: predefinedAvatarCdn, avatarDefault: avatarDefaultFormat(predefinedAvatarDefault) } - } - - try { - const res = await fetch('!{server}/api/v2/conf') - const result = await res.json() - const { mirror, params, default: defaults } = result.frontend_conf.gravatar - const avatarCdn = predefinedAvatarCdn || mirror - let avatarDefault = avatarDefaultFormat(predefinedAvatarDefault || params || defaults) - return { avatarCdn, avatarDefault} - } catch (e) { - console.error(e) - return { avatarCdn: predefinedAvatarCdn, avatarDefault: avatarDefaultFormat(predefinedAvatarDefault) } - } - } - - const searchParams = new URLSearchParams({ - 'site_name': '!{site}', - 'limit': '!{newestCommentsLimit * 2}', // Fetch more comments to filter pending comments - }) - - const getComment = async (ele) => { - try { - const res = await fetch(`!{server}/api/v2/stats/latest_comments?${searchParams}`) - const result = await res.json() - const { avatarCdn, avatarDefault } = await getAvatarValue() - const artalk = result.data - .filter(e => !e.is_pending) // Filter pending comments - .slice(0, !{newestCommentsLimit}) // Limit the number of comments - .map(e => { - const avatar = avatarCdn && e.email_encrypted ? `${avatarCdn}${e.email_encrypted}?${avatarDefault}` : '' - return { - 'avatar': avatar, - 'content': changeContent(e.content_marked), - 'nick': e.nick, - 'url': e.page_url, - 'date': e.date, - } - }) - btf.saveToLocal.set(keyName, JSON.stringify(artalk), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(artalk, ele) - } catch (e) { - console.log(e) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - } - } - - run(keyName, getComment) - }) \ No newline at end of file +- const { server, site, option } = theme.artalk +- const avatarCdn = (option !== null && option.gravatar && option.gravatar.mirror) || '' +- const avatarDefault = (option !== null && option.gravatar && (option.gravatar.params || option.gravatar.default)) || '' + +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'artalk-newest-comments' + const { changeContent, run } = window.newestComments + + const getAvatarValue = async () => { + const predefinedAvatarCdn = '!{avatarCdn}' + const predefinedAvatarDefault = '!{avatarDefault}' + + const avatarDefaultFormat = e => e.startsWith('d=') ? e : `d=${e}` + + if (predefinedAvatarCdn && predefinedAvatarDefault) { + return { avatarCdn: predefinedAvatarCdn, avatarDefault: avatarDefaultFormat(predefinedAvatarDefault) } + } + + try { + const res = await fetch('!{server}/api/v2/conf') + const result = await res.json() + const { mirror, params, default: defaults } = result.frontend_conf.gravatar + const avatarCdn = predefinedAvatarCdn || mirror + const avatarDefault = avatarDefaultFormat(predefinedAvatarDefault || params || defaults) + return { avatarCdn, avatarDefault } + } catch (e) { + console.error(e) + return { avatarCdn: predefinedAvatarCdn, avatarDefault: avatarDefaultFormat(predefinedAvatarDefault) } + } + } + + const searchParams = new URLSearchParams({ + 'site_name': '!{site}', + 'limit': '!{newestCommentsLimit * 2}', // Fetch more comments to filter pending comments + }) + + const getComment = async () => { + const res = await fetch(`!{server}/api/v2/stats/latest_comments?${searchParams}`) + const result = await res.json() + const { avatarCdn, avatarDefault } = await getAvatarValue() + return result.data + .filter(e => !e.is_pending) // Filter pending comments + .slice(0, !{newestCommentsLimit}) // Limit the number of comments + .map(e => { + const avatar = avatarCdn && e.email_encrypted ? `${avatarCdn}${e.email_encrypted}?${avatarDefault}` : '' + return { + 'avatar': avatar, + 'content': changeContent(e.content_marked), + 'nick': e.nick, + 'url': e.page_url, + 'date': e.date, + } + }) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/common.pug b/layout/includes/third-party/newest-comments/common.pug index 9a0814d..977febe 100644 --- a/layout/includes/third-party/newest-comments/common.pug +++ b/layout/includes/third-party/newest-comments/common.pug @@ -1,61 +1,97 @@ -script. - window.newestComments = { - changeContent: content => { - if (content === '') return content - - content = content.replace(/]+>/ig, '[!{_p("aside.card_newest_comments.image")}]') // replace image link - content = content.replace(/]+?href=["']?([^"']+)["']?[^>]*>([^<]+)<\/a>/gi, '[!{_p("aside.card_newest_comments.link")}]') // replace url - content = content.replace(/
.*?<\/pre>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code
-      content = content.replace(/.*?<\/code>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code
-      content = content.replace(/<[^>]+>/g, "") // remove html tag
-
-      if (content.length > 150) {
-        content = content.substring(0, 150) + '...'
-      }
-      return content
-    },
-
-    generateHtml: (array, ele) => {
-      let result = ''
-
-      if (array.length) {
-        for (let i = 0; i < array.length; i++) {
-          result += '
' - - if (!{theme.aside.card_newest_comments.avatar} && array[i].avatar) { - const imgAttr = '!{theme.lazyload.enable && !theme.lazyload.native ? "data-lazy-src" : "src"}' - const lazyloadNative = '!{theme.lazyload.enable && theme.lazyload.native ? "loading=\"lazy\"" : ""}' - result += `${array[i].nick}` - } - - result += `
- ${array[i].content} -
${array[i].nick} /
-
` - } - } else { - result += '!{_p("aside.card_newest_comments.zero")}' - } - - ele.innerHTML = result - window.lazyLoadInstance && window.lazyLoadInstance.update() - window.pjax && window.pjax.refresh(ele) - }, - - newestCommentInit: (name, getComment) => { - const $dom = document.querySelector('#card-newest-comments .aside-list') - if ($dom) { - const data = btf.saveToLocal.get(name) - if (data) { - newestComments.generateHtml(JSON.parse(data), $dom) - } else { - getComment($dom) - } - } - }, - - run: (name, getComment) => { - newestComments.newestCommentInit(name, getComment) - btf.addGlobalFn('pjaxComplete', () => newestComments.newestCommentInit(name, getComment), name) - } - } \ No newline at end of file +script. + window.newestComments = { + ready: fn => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fn, { once: true }) + } else { + fn() + } + }, + + escapeHtml: str => { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, + + changeContent: content => { + if (!content) return '' + + content = content.replace(/]*>/gis, '[!{_p("aside.card_newest_comments.image")}]') // replace image link + content = content.replace(/
[\s\S]*?<\/pre>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code block
+      content = content.replace(/]+?>[\s\S]*?<\/a>/gi, '[!{_p("aside.card_newest_comments.link")}]') // replace url
+      content = content.replace(/]*?>[\s\S]*?<\/code>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code
+      content = content.replace(/<[^>]+>/g, '') // remove html tag
+
+      if (content.length > 150) {
+        content = Array.from(content).slice(0, 150).join('') + '...'
+      }
+
+      return content
+    },
+
+    generateHtml: (array, ele) => {
+      const { escapeHtml } = window.newestComments
+      let result = ''
+
+      if (array.length) {
+        result = array.map(item => {
+          let html = '
' + + if (!{theme.aside.card_newest_comments.avatar} && item.avatar) { + const imgAttr = '!{theme.lazyload.enable && !theme.lazyload.native ? "data-lazy-src" : "src"}' + const lazyloadNative = '!{theme.lazyload.enable && theme.lazyload.native ? "loading=\"lazy\"" : ""}' + html += `${escapeHtml(item.nick)}` + } + + html += `
+ ${escapeHtml(item.content)} +
${escapeHtml(item.nick)} /
+
` + + return html + }).join('') + } else { + result += '!{_p("aside.card_newest_comments.zero")}' + } + + ele.innerHTML = result + window.lazyLoadInstance && window.lazyLoadInstance.update() + window.pjax && window.pjax.refresh(ele) + }, + + fetchAndRender: (keyName, ele, getComment) => { + Promise.resolve(getComment(ele)).then(array => { + btf.saveToLocal.set(keyName, JSON.stringify(array), !{theme.aside.card_newest_comments.storage}/(60*24)) + newestComments.generateHtml(array, ele) + }).catch(e => { + console.error(e) + const data = btf.saveToLocal.get(keyName) + if (data) { + newestComments.generateHtml(JSON.parse(data), ele) + } else { + ele.textContent = '!{_p("aside.card_newest_comments.error")}' + } + }) + }, + + newestCommentInit: (name, getComment) => { + const $dom = document.querySelector('#card-newest-comments .aside-list') + if ($dom) { + const data = btf.saveToLocal.get(name) + if (data) { + newestComments.generateHtml(JSON.parse(data), $dom) + } else { + newestComments.fetchAndRender(name, $dom, getComment) + } + } + }, + + run: (name, getComment) => { + newestComments.newestCommentInit(name, getComment) + btf.addGlobalFn('pjaxComplete', () => newestComments.newestCommentInit(name, getComment), name) + } + } diff --git a/layout/includes/third-party/newest-comments/disqus-comment.pug b/layout/includes/third-party/newest-comments/disqus-comment.pug index 040d5d0..11ba9b7 100644 --- a/layout/includes/third-party/newest-comments/disqus-comment.pug +++ b/layout/includes/third-party/newest-comments/disqus-comment.pug @@ -1,34 +1,23 @@ -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'disqus-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getComment = ele => { - fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{newestCommentsLimit}&api_key=!{apiKey}') - .then(response => response.json()) - .then(data => { - const disqusArray = data.response.map(item => { - return { - 'avatar': item.author.avatar.cache, - 'content': changeContent(item.message), - 'nick': item.author.name, - 'url': item.url, - 'date': item.createdAt - } - }) - - btf.saveToLocal.set(keyName, JSON.stringify(disqusArray), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(disqusArray, ele) - }).catch(e => { - console.error(e) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - }) - } - - run(keyName, getComment) - }) - - - +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'disqus-newest-comments' + const { changeContent, run } = window.newestComments + + const getComment = () => { + return fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{newestCommentsLimit}&api_key=!{apiKey}') + .then(response => response.json()) + .then(data => data.response.map(item => { + return { + 'avatar': item.author.avatar.cache, + 'content': changeContent(item.message), + 'nick': item.author.name, + 'url': item.url, + 'date': item.createdAt + } + })) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/github-issues.pug b/layout/includes/third-party/newest-comments/github-issues.pug index 5612c8a..b2a0db7 100644 --- a/layout/includes/third-party/newest-comments/github-issues.pug +++ b/layout/includes/third-party/newest-comments/github-issues.pug @@ -1,62 +1,49 @@ -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'github-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const findTrueUrl = (array, ele) => { - Promise.all(array.map(item => - fetch(item.url).then(resp => resp.json()).then(data => { - let urlArray = data.body ? data.body.match(/(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?/ig) : [] - if (!Array.isArray(urlArray) || urlArray.length === 0) { - urlArray = [`${data.html_url}`] - } - if (data.user.login === 'utterances-bot') { - return urlArray.pop() - } else { - return urlArray.shift() - } - }) - )).then(res => { - array = array.map((i,index)=> { - return { - ...i, - url: res[index] - } - }) - - btf.saveToLocal.set(keyName, JSON.stringify(array), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(array, ele) - }); - } - - const getComment = ele => { - fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{newestCommentsLimit}&page=1',{ - "headers": { - Accept: 'application/vnd.github.v3.html+json' - } - }) - .then(response => response.json()) - .then(data => { - const githubArray = data.map(item => { - return { - 'avatar': item.user.avatar_url, - 'content': changeContent(item.body_html || item.body), - 'nick': item.user.login, - 'url': item.issue_url, - 'date': item.updated_at - } - }) - findTrueUrl(githubArray, ele) - }).catch(e => { - console.error(e) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - }) - } - run(keyName, getComment) - }) - - - - +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'github-newest-comments' + const { changeContent, run } = window.newestComments + + const findTrueUrl = array => { + return Promise.all(array.map(item => + fetch(item.url) + .then(resp => resp.json()) + .then(data => { + let urlArray = data.body ? data.body.match(/(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?/ig) : [] + if (!Array.isArray(urlArray) || urlArray.length === 0) { + urlArray = [item.html_url] + } + const isBot = data.user && data.user.login === 'utterances-bot' + return isBot ? urlArray.pop() : urlArray.shift() + }) + .catch(() => item.html_url) + )).then(res => array.map((item, index) => { + return { + ...item, + url: res[index] + } + })) + } + + const getComment = () => { + return fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{newestCommentsLimit}&page=1', { + "headers": { + Accept: 'application/vnd.github.v3.html+json' + } + }) + .then(response => response.json()) + .then(data => data.map(item => { + return { + 'avatar': item.user.avatar_url, + 'content': changeContent(item.body_html || item.body), + 'nick': item.user.login, + 'url': item.html_url, + 'date': item.updated_at + } + })) + .then(findTrueUrl) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/remark42.pug b/layout/includes/third-party/newest-comments/remark42.pug index a335968..63b1158 100644 --- a/layout/includes/third-party/newest-comments/remark42.pug +++ b/layout/includes/third-party/newest-comments/remark42.pug @@ -1,31 +1,24 @@ -- const { host, siteId } = theme.remark42 -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'remark42-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getComment = ele => { - fetch('!{host}/api/v1/last/!{newestCommentsLimit}?site=!{siteId}') - .then(response => response.json()) - .then(data => { - const remark42 = data.map(e => { - return { - 'avatar': e.user.picture, - 'content': changeContent(e.text), - 'nick': e.user.name, - 'url': e.locator.url, - 'date': e.time, - } - }) - btf.saveToLocal.set(keyName, JSON.stringify(remark42), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(remark42, ele) - }).catch(e => { - console.error(e) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - }) - } - - run(keyName, getComment) - }) +- const { host, siteId } = theme.remark42 +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'remark42-newest-comments' + const { changeContent, run } = window.newestComments + + const getComment = () => { + return fetch('!{host}/api/v1/last/!{newestCommentsLimit}?site=!{siteId}') + .then(response => response.json()) + .then(data => data.map(e => { + return { + 'avatar': e.user.picture, + 'content': changeContent(e.text), + 'nick': e.user.name, + 'url': e.locator.url, + 'date': e.time, + } + })) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/twikoo-comment.pug b/layout/includes/third-party/newest-comments/twikoo-comment.pug index 250efa8..1839db8 100644 --- a/layout/includes/third-party/newest-comments/twikoo-comment.pug +++ b/layout/includes/third-party/newest-comments/twikoo-comment.pug @@ -1,45 +1,32 @@ -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'twikoo-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getComment = ele => { - const runTwikoo = () => { - twikoo.getRecentComments({ - envId: '!{theme.twikoo.envId}', - region: '!{theme.twikoo.region}', - pageSize: !{newestCommentsLimit}, - includeReply: true - }).then(res => { - const twikooArray = res.map(e => { - return { - 'content': changeContent(e.comment), - 'avatar': e.avatar, - 'nick': e.nick, - 'url': e.url + '#' + e.id, - 'date': new Date(e.created).toISOString() - } - }) - - btf.saveToLocal.set(keyName, JSON.stringify(twikooArray), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(twikooArray, ele) - }).catch(err => { - console.error(err) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - }) - } - - if (typeof twikoo === 'object') { - runTwikoo() - } else { - btf.getScript('!{url_for(theme.asset.twikoo)}').then(runTwikoo) - } - } - - run(keyName, getComment) - }) - - - +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'twikoo-newest-comments' + const { changeContent, run } = window.newestComments + + const getComment = async () => { + const runTwikoo = () => twikoo.getRecentComments({ + envId: '!{theme.twikoo.envId}', + region: '!{theme.twikoo.region}', + pageSize: !{newestCommentsLimit}, + includeReply: true + }).then(res => res.map(e => { + return { + 'content': changeContent(e.comment), + 'avatar': e.avatar, + 'nick': e.nick, + 'url': (e.url || '') + '#' + e.id, + 'date': new Date(e.created).toISOString() + } + })) + + if (typeof twikoo === 'undefined') { + await btf.getScript('!{url_for(theme.asset.twikoo)}') + } + + return runTwikoo() + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/valine.pug b/layout/includes/third-party/newest-comments/valine.pug index 6d7742c..1af23c2 100644 --- a/layout/includes/third-party/newest-comments/valine.pug +++ b/layout/includes/third-party/newest-comments/valine.pug @@ -1,51 +1,44 @@ -- let default_avatar = theme.valine.avatar - -script(src=url_for(theme.asset.blueimp_md5)) -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'valine-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getIcon = (icon, mail) => { - if (icon) return icon - let defaultIcon = '!{ default_avatar ? `?d=${default_avatar}` : ''}' - let iconUrl = `https://gravatar.loli.net/avatar/${md5(mail.toLowerCase()) + defaultIcon}` - return iconUrl - } - - const getComment = ele => { - const serverURL = '!{theme.valine.serverURLs || `https://${theme.valine.appId.substring(0,8)}.api.lncldglobal.com` }' - - var settings = { - "method": "GET", - "headers": { - "X-LC-Id": '!{theme.valine.appId}', - "X-LC-Key": '!{theme.valine.appKey}', - "Content-Type": "application/json" - }, - } - - fetch(`${serverURL}/1.1/classes/Comment?limit=!{newestCommentsLimit}&order=-createdAt`,settings) - .then(response => response.json()) - .then(data => { - const valineArray = data.results.map(e => { - return { - 'avatar': getIcon(e.QQAvatar, e.mail), - 'content': changeContent(e.comment), - 'nick': e.nick, - 'url': e.url + '#' + e.objectId, - 'date': e.updatedAt, - } - }) - btf.saveToLocal.set(keyName, JSON.stringify(valineArray), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(valineArray, ele) - }).catch(e => { - console.error(e) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - }) - } - - run(keyName, getComment) - }) +- let default_avatar = theme.valine.avatar + +script(src=url_for(theme.asset.blueimp_md5)) +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'valine-newest-comments' + const { changeContent, run } = window.newestComments + + const getIcon = (icon, mail) => { + if (icon) return icon + let defaultIcon = '!{ default_avatar ? `?d=${default_avatar}` : ''}' + let iconUrl = `https://gravatar.loli.net/avatar/${md5(mail.toLowerCase()) + defaultIcon}` + return iconUrl + } + + const getComment = async () => { + const serverURL = '!{theme.valine.serverURLs || `https://${theme.valine.appId.substring(0,8)}.api.lncldglobal.com` }' + + const settings = { + "method": "GET", + "headers": { + "X-LC-Id": '!{theme.valine.appId}', + "X-LC-Key": '!{theme.valine.appKey}', + "Content-Type": "application/json" + }, + } + + const res = await fetch(`${serverURL}/1.1/classes/Comment?limit=!{newestCommentsLimit}&order=-createdAt`, settings) + const data = await res.json() + return data.results.map(e => { + return { + 'avatar': getIcon(e.QQAvatar, e.mail), + 'content': changeContent(e.comment), + 'nick': e.nick, + 'url': (e.url || '') + '#' + e.objectId, + 'date': e.updatedAt, + } + }) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/newest-comments/waline.pug b/layout/includes/third-party/newest-comments/waline.pug index b8117ef..370a500 100644 --- a/layout/includes/third-party/newest-comments/waline.pug +++ b/layout/includes/third-party/newest-comments/waline.pug @@ -1,32 +1,25 @@ -- const serverURL = theme.waline.serverURL.replace(/\/$/, '') - -!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) - -script. - window.addEventListener('load', () => { - const keyName = 'waline-newest-comments' - const { changeContent, generateHtml, run } = window.newestComments - - const getComment = async (ele) => { - try { - const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{newestCommentsLimit}') - const result = await res.json() - const walineArray = result.data.map(e => { - return { - 'content': changeContent(e.comment), - 'avatar': e.avatar, - 'nick': e.nick, - 'url': e.url + '#' + e.objectId, - 'date': e.time || e.insertedAt - } - }) - btf.saveToLocal.set(keyName, JSON.stringify(walineArray), !{theme.aside.card_newest_comments.storage}/(60*24)) - generateHtml(walineArray, ele) - } catch (err) { - console.error(err) - ele.textContent= "!{_p('aside.card_newest_comments.error')}" - } - } - - run(keyName, getComment) - }) +- const serverURL = theme.waline.serverURL ? theme.waline.serverURL.replace(/\/$/, '') : '' + +!= partial("includes/third-party/newest-comments/common.pug", {}, { cache: true }) + +script. + window.newestComments.ready(() => { + const keyName = 'waline-newest-comments' + const { changeContent, run } = window.newestComments + + const getComment = async () => { + const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{newestCommentsLimit}') + const result = await res.json() + return result.data.map(e => { + return { + 'content': changeContent(e.comment), + 'avatar': e.avatar, + 'nick': e.nick, + 'url': (e.url || '') + '#' + e.objectId, + 'date': e.time || e.insertedAt + } + }) + } + + run(keyName, getComment) + }) diff --git a/layout/includes/third-party/subtitle.pug b/layout/includes/third-party/subtitle.pug index ebe027a..059eea5 100644 --- a/layout/includes/third-party/subtitle.pug +++ b/layout/includes/third-party/subtitle.pug @@ -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>/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) \ No newline at end of file +- 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>/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) diff --git a/layout/includes/third-party/umami_analytics.pug b/layout/includes/third-party/umami_analytics.pug index 96c1f69..5362468 100644 --- a/layout/includes/third-party/umami_analytics.pug +++ b/layout/includes/third-party/umami_analytics.pug @@ -30,29 +30,25 @@ script. 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 getData = async (isPost, path = window.location.pathname) => { + const now = Date.now() + const pathQuery = isPost ? `&url=${path}&path=${path}` : '' + 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, + cache: 'no-store' + }) + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`) + } + + return await res.json() } const insertData = async () => { @@ -63,7 +59,8 @@ script. tasks.push((async () => { const pagePV = document.getElementById('umamiPV') if (pagePV) { - const data = await getData(true) + const path = pagePV.dataset.path || window.location.pathname + const data = await getData(true, path) if (data && data.pageviews) { pagePV.textContent = extractValue(data.pageviews) } else { @@ -73,26 +70,23 @@ script. })()) } - if (config.site_uv || config.site_pv) { + const siteUV = config.site_uv ? document.getElementById('umami-site-uv') : null + const sitePV = config.site_pv ? document.getElementById('umami-site-pv') : null + + if (siteUV || sitePV) { 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 (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') - } + if (sitePV && data && data.pageviews) { + sitePV.textContent = extractValue(data.pageviews) + } else if (sitePV) { + console.warn('Umami Analytics: Invalid site PV data received') } })()) } diff --git a/package.json b/package.json index db03c50..6e40048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.7.0", + "version": "5.7.1.260810", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/scripts/events/cdn.js b/scripts/events/cdn.js index 04ec649..dcc6aa5 100644 --- a/scripts/events/cdn.js +++ b/scripts/events/cdn.js @@ -57,7 +57,7 @@ hexo.extend.filter.register('before_generate', () => { return Object.keys(data).reduce((result, key) => { let { name, version, file, other_name: otherName } = data[key] const cdnjsName = otherName || name - const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '') + const cdnjsFile = file.replace(/^(?:lib|dist)\/|^browser\//, '') const minCdnjsFile = minFile(cdnjsFile) if (cond === 'internal') file = `source/${file}` const minFilePath = minFile(file) diff --git a/scripts/helpers/inject_head_js.js b/scripts/helpers/inject_head_js.js index 1f3ccb9..49f6130 100644 --- a/scripts/helpers/inject_head_js.js +++ b/scripts/helpers/inject_head_js.js @@ -11,13 +11,17 @@ hexo.extend.helper.register('inject_head_js', function () { const createCustomJs = () => ` const saveToLocal = { set: (key, value, ttl) => { - const data = { value } + try { + const data = { value } - if (ttl != null) { - data.expiry = Date.now() + ttl * 86400000 + if (ttl != null) { + data.expiry = Date.now() + ttl * 86400000 + } + + localStorage.setItem(key, JSON.stringify(data)) + } catch (e) { + console.error(e) } - - localStorage.setItem(key, JSON.stringify(data)) }, get: key => { const itemStr = localStorage.getItem(key) diff --git a/scripts/helpers/page.js b/scripts/helpers/page.js index f0d72e4..26438cf 100644 --- a/scripts/helpers/page.js +++ b/scripts/helpers/page.js @@ -1,7 +1,7 @@ 'use strict' const { truncateContent, postDesc } = require('../common/postDesc') -const { prettyUrls } = require('hexo-util') +const { prettyUrls, stripHTML } = require('hexo-util') const crypto = require('crypto') const moment = require('moment-timezone') @@ -9,7 +9,7 @@ const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/ const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i -const archiveRegex = /\/archives\// +const escapeRegex = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const { version: themeVersion } = require('../../package.json') @@ -27,6 +27,16 @@ hexo.extend.helper.register('cloudTags', function (options = {}) { source = source.limit(limit) } + const shuffle = list => { + for (let i = list.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + const temp = list[i] + list[i] = list[j] + list[j] = temp + } + return list + } + const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))] const sizeMap = new Map(sizes.map((size, index) => [size, index])) const length = sizes.length - 1 @@ -65,7 +75,11 @@ hexo.extend.helper.register('cloudTags', function (options = {}) { return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}` } - return source.sort(orderby, order).map((tag, idx) => { + const sortedSource = orderby === 'random' + ? shuffle(typeof source.toArray === 'function' ? source.toArray() : Array.from(source)) + : source.sort(orderby, order) + + return sortedSource.map((tag, idx) => { const ratio = length ? sizeMap.get(tag.length) / length : 0 const size = minfontsize + ((maxfontsize - minfontsize) * ratio) @@ -102,6 +116,11 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) { const defaultTitle = this._p('page.archives') if (!menu) return defaultTitle + const archiveDir = String(hexo.config.archive_dir || 'archives').replace(/^\/+|\/+$/g, '') + const archivePath = this.url_for(archiveDir) + const normalizedArchivePath = archivePath.endsWith('/') ? archivePath : `${archivePath}/` + const archivePathRegex = new RegExp(`${escapeRegex(normalizedArchivePath)}(?:$|[?#])`) + const archiveDirRegex = new RegExp(`/${escapeRegex(archiveDir)}/(?:$|[?#])`) const loop = m => { for (const [key, value] of Object.entries(m)) { @@ -110,7 +129,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) { if (result) return result } - if (typeof value === 'string' && archiveRegex.test(value)) { + if (typeof value === 'string' && (archivePathRegex.test(value) || archiveDirRegex.test(value))) { return key } } @@ -156,7 +175,9 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => { const timezone = hexo.config.timezone processedData.forEach(item => { 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') + item.date = timezone + ? moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss') + : parsed.format('YYYY-MM-DD HH:mm:ss') // Render the content using Hexo's rendering engine to process any tags or markdown const mockPost = { @@ -222,3 +243,43 @@ hexo.extend.helper.register('safeJSON', data => { .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') }) + +const charBasedRange = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff]/g + +const countWords = text => { + if (!text) return 0 + const charCount = (text.match(charBasedRange) || []).length + const remaining = text.replace(charBasedRange, ' ').trim() + const wordCount = remaining ? remaining.split(/\s+/).length : 0 + return charCount + wordCount +} + +const formatCount = num => { + if (num >= 100000) { + return Math.round(num / 1000) + 'k' + } + return num +} + +hexo.extend.helper.register('wordcount', page => { + return formatCount(countWords(stripHTML(page.encrypt ? page.origin : page.content || ''))) +}) + +hexo.extend.helper.register('min2read', (page, options = {}) => { + const { cn = 300, en = 160 } = options + const text = stripHTML(page.encrypt ? page.origin : page.content || '') + const charCount = (text.match(charBasedRange) || []).length + const remaining = text.replace(charBasedRange, ' ').trim() + const wordCount = remaining ? remaining.split(/\s+/).length : 0 + const minutes = Math.ceil(charCount / cn + wordCount / en) + return minutes < 1 ? 1 : minutes +}) + +hexo.extend.helper.register('totalcount', site => { + if (!site || !site.posts) return 0 + let total = 0 + site.posts.forEach(post => { + total += countWords(stripHTML(post.encrypt ? post.origin : post.content || '')) + }) + return formatCount(total) +}) diff --git a/scripts/tag/chartjs.js b/scripts/tag/chartjs.js index b59902a..8fdb089 100644 --- a/scripts/tag/chartjs.js +++ b/scripts/tag/chartjs.js @@ -31,7 +31,7 @@ const chartjs = (args, content) => { return } - const chartConfig = chartMatch && chartMatch[1] ? chartMatch[1] : '' + const chartConfig = chartMatch[1] || '' const descContent = descMatch && descMatch[1] ? descMatch[1] : '' const renderedDesc = descContent ? hexo.render.renderSync({ text: descContent, engine: 'markdown' }).trim() : '' diff --git a/source/js/main.js b/source/js/main.js index 4cca9aa..877bf45 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -622,7 +622,7 @@ document.addEventListener('DOMContentLoaded', () => { const currentTop = window.scrollY || document.documentElement.scrollTop if (isToc && GLOBAL_CONFIG.percent.toc) { - $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article) + $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article, false) } if (currentTop === 0) { @@ -662,6 +662,7 @@ document.addEventListener('DOMContentLoaded', () => { */ const rightSideFn = { readmode: () => { // read mode + if (document.querySelector('.exit-readmode')) return const $body = document.body const newEle = document.createElement('button') @@ -677,6 +678,7 @@ document.addEventListener('DOMContentLoaded', () => { newEle.innerHTML = '' newEle.addEventListener('click', exitReadMode) $body.appendChild(newEle) + btf.addGlobalFn('pjaxSendOnce', exitReadMode, 'exitReadMode') }, darkmode: () => { // switch between light and dark mode const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark' @@ -1010,8 +1012,12 @@ document.addEventListener('DOMContentLoaded', () => { // 處理 hexo-blog-encrypt 事件 window.addEventListener('hexo-blog-decrypt', e => { forPostFn() - window.translateFn.translateInitialization() - Object.values(window.globalFn.encrypt).forEach(fn => { + if (window.translateFn && typeof window.translateFn.translateInitialization === 'function') { + window.translateFn.translateInitialization() + } + + const encryptFn = window.globalFn && window.globalFn.encrypt ? window.globalFn.encrypt : {} + Object.values(encryptFn).forEach(fn => { fn() }) }) diff --git a/source/js/tw_cn.js b/source/js/tw_cn.js index d43ae2a..3760bbc 100644 --- a/source/js/tw_cn.js +++ b/source/js/tw_cn.js @@ -19,12 +19,14 @@ document.addEventListener('DOMContentLoaded', () => { return txt } + const skippedTranslateTags = ['BR', 'HR', 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'CODE', 'PRE'] + const translateBody = fobj => { const nodes = typeof fobj === 'object' ? fobj.childNodes : document.body.childNodes for (const node of nodes) { - // Skip BR, HR tags, or the translate button object - if (['BR', 'HR'].includes(node.tagName) || node === translateButtonObject) continue + // Skip non-translatable tags or the translate button object + if (skippedTranslateTags.includes(node.tagName) || node === translateButtonObject) continue if (node.nodeType === Node.ELEMENT_NODE) { const { tagName, title, alt, placeholder, value, type } = node diff --git a/source/js/utils.js b/source/js/utils.js index 37dffde..52558c1 100644 --- a/source/js/utils.js +++ b/source/js/utils.js @@ -56,43 +56,30 @@ } }, - overflowPaddingR: (() => { - let headerElement = null - let menuElement = null + overflowPaddingR: { + add: () => { + const paddingRight = window.innerWidth - document.body.clientWidth - const getElements = () => { - if (!headerElement) { - headerElement = document.getElementById('page-header') - } - if (!menuElement) { - menuElement = document.getElementById('menus') - } - return { headerElement, menuElement } - } - - return { - add: () => { - const paddingRight = window.innerWidth - document.body.clientWidth - - if (paddingRight > 0) { - document.body.style.paddingRight = `${paddingRight}px` - document.body.style.overflow = 'hidden' - const { headerElement: header, menuElement: menu } = getElements() - if (header && menu && header.classList.contains('nav-fixed')) { - menu.style.paddingRight = `${paddingRight}px` - } - } - }, - remove: () => { - document.body.style.paddingRight = '' - document.body.style.overflow = '' - const { headerElement: header, menuElement: menu } = getElements() + if (paddingRight > 0) { + document.body.style.paddingRight = `${paddingRight}px` + document.body.style.overflow = 'hidden' + const header = document.getElementById('page-header') + const menu = document.getElementById('menus') if (header && menu && header.classList.contains('nav-fixed')) { - menu.style.paddingRight = '' + menu.style.paddingRight = `${paddingRight}px` } } + }, + remove: () => { + document.body.style.paddingRight = '' + document.body.style.overflow = '' + const header = document.getElementById('page-header') + const menu = document.getElementById('menus') + if (header && menu && header.classList.contains('nav-fixed')) { + menu.style.paddingRight = '' + } } - })(), + }, snackbarShow: (text, showAction = false, duration = 2000) => { const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar @@ -200,7 +187,12 @@ const service = GLOBAL_CONFIG.lightbox if (service === 'medium_zoom') { - mediumZoom(ele, { background: 'var(--zoom-bg)' }) + const zoom = window.mediumZoomInstance || (window.mediumZoomInstance = mediumZoom({ background: 'var(--zoom-bg)' })) + zoom.attach(ele) + + btf.addGlobalFn('pjaxSendOnce', () => { + window.mediumZoomInstance && window.mediumZoomInstance.detach() + }, 'mediumZoom') return } @@ -246,7 +238,7 @@ }, hideScrollbar: false } - } else { + } else { options = { Hash: false, Carousel: { @@ -314,17 +306,41 @@ }, getScrollPercent: (() => { - let docHeight, winHeight, headerHeight, contentMath + let cache = new WeakMap() - return (currentTop, ele) => { - if (!docHeight || ele.clientHeight !== docHeight) { - docHeight = ele.clientHeight - winHeight = window.innerHeight - headerHeight = ele.offsetTop - contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight) + window.addEventListener('resize', () => { + cache = new WeakMap() + }) + + return (currentTop, ele, useDocHeight = ele === document.body) => { + const eleHeight = ele.clientHeight + const winHeight = window.innerHeight + const cacheData = cache.get(ele) + let data = cacheData + + if ( + !cacheData || + cacheData.docHeight !== eleHeight || + cacheData.winHeight !== winHeight || + cacheData.useDocHeight !== useDocHeight + ) { + const headerHeight = ele.offsetTop + const contentMath = useDocHeight + ? Math.max(eleHeight - winHeight, document.documentElement.scrollHeight - winHeight) + : Math.max(eleHeight - winHeight, 1) + + data = { + docHeight: eleHeight, + winHeight, + useDocHeight, + headerHeight, + contentMath + } + + cache.set(ele, data) } - const scrollPercent = (currentTop - headerHeight) / contentMath + const scrollPercent = (currentTop - data.headerHeight) / data.contentMath return Math.max(0, Math.min(100, Math.round(scrollPercent * 100))) } })(),