From 0d0001c8085ce792ed65d309ce63e6c1e5bd8792 Mon Sep 17 00:00:00 2001 From: myw Date: Tue, 10 Dec 2024 20:35:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20plugins.yml=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=BE=9D=E8=B3=B4=E7=89=88=E6=9C=AC=E8=87=B3?= =?UTF-8?q?=E6=9C=80=E6=96=B0=20feat:=20=E5=84=AA=E5=8C=96=20aside=5Farchi?= =?UTF-8?q?ves=20=EF=BC=8C=E6=94=B9=E9=80=B2=E6=80=A7=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E8=AE=80=E6=80=A7=20feat:=20=E6=94=B9=E5=96=84=20inli?= =?UTF-8?q?neImg=20=E5=92=8C=20timeline=20=E6=A8=99=E7=B1=A4=E7=9A=84?= =?UTF-8?q?=E6=96=87=E6=AA=94=EF=BC=8C=E5=84=AA=E5=8C=96=E6=99=82=E9=96=93?= =?UTF-8?q?=E7=B7=9A=E9=82=8F=E8=BC=AF=20feat:=20=E6=9B=B4=E6=96=B0=20gall?= =?UTF-8?q?ery=20=E6=A8=99=E7=B1=A4=E4=BB=A5=E6=94=AF=E6=8C=81=E9=A1=8D?= =?UTF-8?q?=E5=A4=96=E5=8F=83=E6=95=B8=EF=BC=8C=E5=84=AA=E5=8C=96=E5=9C=96?= =?UTF-8?q?=E7=89=87=E9=A1=AF=E7=A4=BA=E9=82=8F=E8=BC=AF=20improvement:=20?= =?UTF-8?q?=E5=84=AA=E5=8C=96=E9=9A=A8=E6=A9=9F=E5=B0=81=E9=9D=A2=E9=81=8E?= =?UTF-8?q?=E6=BF=BE=E5=99=A8=E9=82=8F=E8=BC=AF,=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=80=A3=E7=BA=8C=E9=87=8D=E8=A4=87=20feat:=20=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E8=A9=95=E8=AB=96=E9=99=90=E5=88=B6=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?=201-10=20=E6=A2=9D=E4=B9=8B=E9=96=93=20fix:=20artalk=20?= =?UTF-8?q?=E7=9A=84=E6=9C=80=E6=96=B0=E8=A9=95=E8=AB=96=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?=E5=BE=85=E5=AE=9A=E6=88=96=E8=80=85=E5=B0=81=E7=A6=81=E7=9A=84?= =?UTF-8?q?=E8=A9=95=E8=AB=96=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- layout/includes/sidebar.pug | 10 +- .../third-party/newest-comments/artalk.pug | 25 +-- .../newest-comments/disqus-comment.pug | 2 +- .../newest-comments/github-issues.pug | 2 +- .../third-party/newest-comments/index.pug | 6 +- .../third-party/newest-comments/remark42.pug | 2 +- .../newest-comments/twikoo-comment.pug | 2 +- .../third-party/newest-comments/valine.pug | 2 +- .../third-party/newest-comments/waline.pug | 2 +- layout/includes/widget/card_webinfo.pug | 21 ++- layout/page.pug | 2 +- layout/post.pug | 2 +- package.json | 2 +- plugins.yml | 24 +-- scripts/filters/random_cover.js | 66 ++++--- scripts/helpers/aside_archives.js | 133 ++++++++++----- scripts/tag/gallery.js | 90 +++++----- scripts/tag/inlineImg.js | 10 +- scripts/tag/mermaid.js | 2 +- scripts/tag/timeline.js | 73 ++++---- source/js/main.js | 161 +++++++++--------- 21 files changed, 367 insertions(+), 272 deletions(-) diff --git a/layout/includes/sidebar.pug b/layout/includes/sidebar.pug index ef64d2b14..178c420fa 100644 --- a/layout/includes/sidebar.pug +++ b/layout/includes/sidebar.pug @@ -3,16 +3,16 @@ if theme.menu #menu-mask #sidebar-menus .avatar-img.text-center - img(src=url_for(theme.avatar.img) onerror=`onerror=null;src='${theme.error_img.flink}'` alt="avatar") + img(src=url_for(theme.avatar.img) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.flink)}'` alt="avatar") .site-data.text-center - a(href=url_for(config.archive_dir) + '/') + a(href=`${url_for(config.archive_dir)}/`) .headline= _p('aside.articles') .length-num= site.posts.length - a(href=url_for(config.tag_dir) + '/' ) + a(href=`${url_for(config.tag_dir)}/`) .headline= _p('aside.tags') .length-num= site.tags.length - a(href=url_for(config.category_dir) + '/') + a(href=`${url_for(config.category_dir)}/`) .headline= _p('aside.categories') .length-num= site.categories.length - !=partial('includes/header/menu_item', {}, {cache: true}) + != partial('includes/header/menu_item', {}, { cache: true }) \ No newline at end of file diff --git a/layout/includes/third-party/newest-comments/artalk.pug b/layout/includes/third-party/newest-comments/artalk.pug index 89ca3adbc..4e703b2f6 100644 --- a/layout/includes/third-party/newest-comments/artalk.pug +++ b/layout/includes/third-party/newest-comments/artalk.pug @@ -34,7 +34,7 @@ script. const searchParams = new URLSearchParams({ 'site_name': '!{site}', - 'limit': '!{theme.aside.card_newest_comments.limit}', + 'limit': '!{newestCommentsLimit * 2}', // Fetch more comments to filter pending comments }) const getComment = async (ele) => { @@ -42,16 +42,19 @@ script. 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.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, - } - }) + 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) { diff --git a/layout/includes/third-party/newest-comments/disqus-comment.pug b/layout/includes/third-party/newest-comments/disqus-comment.pug index 96eaa023b..e26bb418e 100644 --- a/layout/includes/third-party/newest-comments/disqus-comment.pug +++ b/layout/includes/third-party/newest-comments/disqus-comment.pug @@ -6,7 +6,7 @@ script. const { changeContent, generateHtml, run } = window.newestComments const getComment = ele => { - fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{theme.aside.card_newest_comments.limit}&api_key=!{apiKey}') + 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 => { diff --git a/layout/includes/third-party/newest-comments/github-issues.pug b/layout/includes/third-party/newest-comments/github-issues.pug index d36f1b2de..7dc418c40 100644 --- a/layout/includes/third-party/newest-comments/github-issues.pug +++ b/layout/includes/third-party/newest-comments/github-issues.pug @@ -32,7 +32,7 @@ script. } const getComment = ele => { - fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{theme.aside.card_newest_comments.limit}&page=1',{ + 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' } diff --git a/layout/includes/third-party/newest-comments/index.pug b/layout/includes/third-party/newest-comments/index.pug index 8ceaccf8a..a2f2c0d0a 100644 --- a/layout/includes/third-party/newest-comments/index.pug +++ b/layout/includes/third-party/newest-comments/index.pug @@ -1,7 +1,11 @@ - let { use } = theme.comments if use - - let forum,apiKey,userRepo + - + let forum,apiKey,userRepo + let { limit:newestCommentsLimit } = theme.aside.card_newest_comments + if (newestCommentsLimit > 10 || newestCommentsLimit < 1) newestCommentsLimit = 6 + case use[0] when 'Valine' include ./valine.pug diff --git a/layout/includes/third-party/newest-comments/remark42.pug b/layout/includes/third-party/newest-comments/remark42.pug index be3b2fc6d..4321b3b3f 100644 --- a/layout/includes/third-party/newest-comments/remark42.pug +++ b/layout/includes/third-party/newest-comments/remark42.pug @@ -7,7 +7,7 @@ script. const { changeContent, generateHtml, run } = window.newestComments const getComment = ele => { - fetch('!{host}/api/v1/last/!{theme.aside.card_newest_comments.limit}?site=!{siteId}') + fetch('!{host}/api/v1/last/!{newestCommentsLimit}?site=!{siteId}') .then(response => response.json()) .then(data => { const remark42 = data.map(e => { diff --git a/layout/includes/third-party/newest-comments/twikoo-comment.pug b/layout/includes/third-party/newest-comments/twikoo-comment.pug index 941f42b04..772c8ec26 100644 --- a/layout/includes/third-party/newest-comments/twikoo-comment.pug +++ b/layout/includes/third-party/newest-comments/twikoo-comment.pug @@ -10,7 +10,7 @@ script. twikoo.getRecentComments({ envId: '!{theme.twikoo.envId}', region: '!{theme.twikoo.region}', - pageSize: !{theme.aside.card_newest_comments.limit}, + pageSize: !{newestCommentsLimit}, includeReply: true }).then(res => { const twikooArray = res.map(e => { diff --git a/layout/includes/third-party/newest-comments/valine.pug b/layout/includes/third-party/newest-comments/valine.pug index 6b87683b3..08af70c29 100644 --- a/layout/includes/third-party/newest-comments/valine.pug +++ b/layout/includes/third-party/newest-comments/valine.pug @@ -27,7 +27,7 @@ script. }, } - fetch(`${serverURL}/1.1/classes/Comment?limit=!{theme.aside.card_newest_comments.limit}&order=-createdAt`,settings) + fetch(`${serverURL}/1.1/classes/Comment?limit=!{newestCommentsLimit}&order=-createdAt`,settings) .then(response => response.json()) .then(data => { const valineArray = data.results.map(e => { diff --git a/layout/includes/third-party/newest-comments/waline.pug b/layout/includes/third-party/newest-comments/waline.pug index 4031d3420..72743ea9a 100644 --- a/layout/includes/third-party/newest-comments/waline.pug +++ b/layout/includes/third-party/newest-comments/waline.pug @@ -9,7 +9,7 @@ script. const getComment = async (ele) => { try { - const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{theme.aside.card_newest_comments.limit}', { method: 'GET' }) + const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{newestCommentsLimit}') const result = await res.json() const walineArray = result.data.map(e => { return { diff --git a/layout/includes/widget/card_webinfo.pug b/layout/includes/widget/card_webinfo.pug index 09ea9c41c..7ea4d984f 100644 --- a/layout/includes/widget/card_webinfo.pug +++ b/layout/includes/widget/card_webinfo.pug @@ -6,40 +6,39 @@ if theme.aside.card_webinfo.enable .webinfo if theme.aside.card_webinfo.post_count .webinfo-item - .item-name= _p('aside.card_webinfo.article_name') + " :" + .item-name= `${_p('aside.card_webinfo.article_name')} :` .item-count= site.posts.length if theme.aside.card_webinfo.runtime_date .webinfo-item - .item-name= _p('aside.card_webinfo.runtime.name') + " :" + .item-name= `${_p('aside.card_webinfo.runtime.name')} :` .item-count#runtimeshow(data-publishDate=date_xml(theme.aside.card_webinfo.runtime_date)) i.fa-solid.fa-spinner.fa-spin if theme.wordcount.enable && theme.wordcount.total_wordcount .webinfo-item - .item-name=_p('aside.card_webinfo.site_wordcount') + " :" - .item-count=totalcount(site) + .item-name= `${_p('aside.card_webinfo.site_wordcount')} :` + .item-count= totalcount(site) if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_uv .webinfo-item - .item-name= _p('aside.card_webinfo.site_uv_name') + " :" + .item-name= `${_p('aside.card_webinfo.site_uv_name')} :` .item-count#umami-site-uv i.fa-solid.fa-spinner.fa-spin else if theme.busuanzi.site_uv .webinfo-item - .item-name= _p('aside.card_webinfo.site_uv_name') + " :" + .item-name= `${_p('aside.card_webinfo.site_uv_name')} :` .item-count#busuanzi_value_site_uv i.fa-solid.fa-spinner.fa-spin if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_pv .webinfo-item - .item-name= _p('aside.card_webinfo.site_pv_name') + " :" + .item-name= `${_p('aside.card_webinfo.site_pv_name')} :` .item-count#umami-site-pv i.fa-solid.fa-spinner.fa-spin else if theme.busuanzi.site_pv .webinfo-item - .item-name= _p('aside.card_webinfo.site_pv_name') + " :" + .item-name= `${_p('aside.card_webinfo.site_pv_name')} :` .item-count#busuanzi_value_site_pv i.fa-solid.fa-spinner.fa-spin if theme.aside.card_webinfo.last_push_date .webinfo-item - .item-name= _p('aside.card_webinfo.last_push_date.name') + " :" + .item-name= `${_p('aside.card_webinfo.last_push_date.name')} :` .item-count#last-push-date(data-lastPushDate=date_xml(Date.now())) - i.fa-solid.fa-spinner.fa-spin - + i.fa-solid.fa-spinner.fa-spin \ No newline at end of file diff --git a/layout/page.pug b/layout/page.pug index 5fdbbbd37..23746812e 100644 --- a/layout/page.pug +++ b/layout/page.pug @@ -2,7 +2,7 @@ extends includes/layout.pug block content - const noCardLayout = ['shuoshuo', '404'].includes(page.type) ? 'nc' : '' - - var commentsJsLoad = false + - var commentsJsLoad = false mixin commentLoad if page.comments !== false && theme.comments.use diff --git a/layout/post.pug b/layout/post.pug index 947b3793b..9a44282a6 100644 --- a/layout/post.pug +++ b/layout/post.pug @@ -6,7 +6,7 @@ block content include includes/header/post-info.pug article#article-container.container.post-content - if theme.noticeOutdate.enable && page.noticeOutdate !== false + if theme.noticeOutdate.enable && page.noticeOutdate !== false include includes/post/outdate-notice.pug else !=page.content diff --git a/package.json b/package.json index 5eff91acb..389317c72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.3.0-b2", + "version": "5.3.0-b3", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/plugins.yml b/plugins.yml index bcc4f9aee..0f10ffb0f 100644 --- a/plugins.yml +++ b/plugins.yml @@ -9,7 +9,7 @@ activate_power_mode: algolia_search: name: algoliasearch file: dist/lite/builds/browser.umd.js - version: 5.12.0 + version: 5.16.0 aplayer_css: name: aplayer file: dist/APlayer.min.css @@ -45,7 +45,7 @@ canvas_ribbon: chartjs: name: chart.js file: dist/chart.umd.js - version: 4.4.6 + version: 4.4.7 clickShowText: name: butterfly-extsrc file: dist/click-show-text.min.js @@ -66,12 +66,12 @@ docsearch_css: name: '@docsearch/css' other_name: docsearch-css file: dist/style.css - version: 3.6.3 + version: 3.8.0 docsearch_js: name: '@docsearch/js' other_name: docsearch-js file: dist/umd/index.js - version: 3.6.3 + version: 3.8.0 egjs_infinitegrid: name: '@egjs/infinitegrid' other_name: egjs-infinitegrid @@ -95,7 +95,7 @@ fontawesome: name: '@fortawesome/fontawesome-free' file: css/all.min.css other_name: font-awesome - version: 6.6.0 + version: 6.7.1 gitalk: name: gitalk file: dist/gitalk.min.js @@ -111,17 +111,17 @@ instantpage: instantsearch: name: instantsearch.js file: dist/instantsearch.production.min.js - version: 4.75.3 + version: 4.75.6 katex: name: katex file: dist/katex.min.css other_name: KaTeX - version: 0.16.11 + version: 0.16.15 katex_copytex: name: katex file: dist/contrib/copy-tex.min.js other_name: KaTeX - version: 0.16.11 + version: 0.16.15 lazyload: name: vanilla-lazyload file: dist/lazyload.iife.min.js @@ -137,7 +137,7 @@ medium_zoom: mermaid: name: mermaid file: dist/mermaid.min.js - version: 11.4.0 + version: 11.4.1 meting_js: name: butterfly-extsrc file: metingjs/dist/Meting.min.js @@ -190,7 +190,7 @@ snackbar_css: twikoo: name: twikoo file: dist/twikoo.all.min.js - version: 1.6.39 + version: 1.6.40 typed: name: typed.js file: dist/typed.umd.js @@ -203,9 +203,9 @@ waline_css: name: '@waline/client' file: dist/waline.css other_name: waline - version: 3.3.2 + version: 3.4.1 waline_js: name: '@waline/client' file: dist/waline.js other_name: waline - version: 3.3.2 + version: 3.4.1 diff --git a/scripts/filters/random_cover.js b/scripts/filters/random_cover.js index 2cce14860..f9cc13c97 100644 --- a/scripts/filters/random_cover.js +++ b/scripts/filters/random_cover.js @@ -1,40 +1,60 @@ /** - * Butterfly - * ramdom cover + * Random cover for posts */ 'use strict' -hexo.extend.filter.register('before_post_render', data => { - const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i - let { cover: coverVal, top_img: topImg } = data - - // Add path to top_img and cover if post_asset_folder is enabled - if (hexo.config.post_asset_folder) { - if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) data.top_img = `${data.path}${topImg}` - if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) data.cover = `${data.path}${coverVal}` - } - +hexo.extend.generator.register('post', locals => { + const recentCovers = [] const randomCoverFn = () => { const { cover: { default_cover: defaultCover } } = hexo.theme.config if (!defaultCover) return false if (!Array.isArray(defaultCover)) return defaultCover - const num = Math.floor(Math.random() * defaultCover.length) + const defaultCoverLen = defaultCover.length + const limit = 3 + + let num + do { + num = Math.floor(Math.random() * defaultCoverLen) + } while (recentCovers.includes(num)) + + recentCovers.push(num) + if (recentCovers.length > limit) recentCovers.shift() + return defaultCover[num] } - if (coverVal === false) return data + const handleImg = data => { + const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i + let { cover: coverVal, top_img: topImg } = data - // If cover is not set, use random cover - if (!coverVal) { - const randomCover = randomCoverFn() - data.cover = randomCover - coverVal = randomCover // update coverVal - } + // Add path to top_img and cover if post_asset_folder is enabled + if (hexo.config.post_asset_folder) { + if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) data.top_img = `${data.path}${topImg}` + if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) data.cover = `${data.path}${coverVal}` + } + + if (coverVal === false) return data + + // If cover is not set, use random cover + if (!coverVal) { + const randomCover = randomCoverFn() + data.cover = randomCover + coverVal = randomCover // update coverVal + } + + if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) { + data.cover_type = 'img' + } - if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) { - data.cover_type = 'img' + return data } - return data + return locals.posts.sort('date').map(post => { + return { + data: handleImg(post), + layout: 'post', + path: post.path + } + }) }) diff --git a/scripts/helpers/aside_archives.js b/scripts/helpers/aside_archives.js index 1cf752181..df2431752 100644 --- a/scripts/helpers/aside_archives.js +++ b/scripts/helpers/aside_archives.js @@ -2,38 +2,68 @@ hexo.extend.helper.register('aside_archives', function (options = {}) { const { config, page, site, url_for, _p } = this - const archiveDir = config.archive_dir - const { timezone } = config - const lang = toMomentLocale(page.lang || page.language || config.language) - const type = options.type || 'monthly' - const format = options.format || (type === 'monthly' ? 'MMMM YYYY' : 'YYYY') - const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true - const order = options.order || -1 - const limit = options.limit + const { + archive_dir: archiveDir, + timezone, + language + } = config + + // Destructure and set default options with object destructuring + const { + type = 'monthly', + format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY', + show_count: showCount = true, + order = -1, + limit, + transform + } = options + + // Optimize locale handling + const lang = toMomentLocale(page.lang || page.language || language) + + // Memoize comparison function to improve performance const compareFunc = type === 'monthly' ? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB - : (yearA, monthA, yearB, monthB) => yearA === yearB + : (yearA, yearB) => yearA === yearB - const posts = site.posts.sort('date', order) - if (!posts.length) return '' + // Early return if no posts + if (!site.posts.length) return '' - const data = [] - posts.forEach(post => { - let date = post.date.clone() - if (timezone) date = date.tz(timezone) + // Use reduce for more efficient data processing + const data = site.posts + .sort('date', order) + .reduce((acc, post) => { + let date = post.date.clone() + if (timezone) date = date.tz(timezone) - const year = date.year() - const month = date.month() + 1 + const year = date.year() + const month = date.month() + 1 - if (!data.length || !compareFunc(data[data.length - 1].year, data[data.length - 1].month, year, month)) { if (lang) date = date.locale(lang) - data.push({ name: date.format(format), year, month, count: 1 }) - } else { - data[data.length - 1].count++ - } - }) - const link = item => { + // Find or create archive entry + const lastEntry = acc[acc.length - 1] + if (!lastEntry || !compareFunc( + lastEntry.year, + lastEntry.month, + year, + month + )) { + acc.push({ + name: date.format(format), + year, + month, + count: 1 + }) + } else { + lastEntry.count++ + } + + return acc + }, []) + + // Create link generator function + const createArchiveLink = item => { let url = `${archiveDir}/${item.year}/` if (type === 'monthly') { url += item.month < 10 ? `0${item.month}/` : `${item.month}/` @@ -41,37 +71,48 @@ hexo.extend.helper.register('aside_archives', function (options = {}) { return url_for(url) } - const len = data.length - const limitLength = limit === 0 ? len : Math.min(len, limit) + // Limit results efficiently + const limitedData = limit > 0 + ? data.slice(0, Math.min(data.length, limit)) + : data - let result = ` + // Use template literal for better readability + const archiveHeader = `
${_p('aside.card_archives')} - ${len > limitLength ? `` : ''} + ${data.length > limitedData.length + ? ` + + ` + : ''}
- ' - return result + return archiveHeader + archiveList }) -const toMomentLocale = function (lang) { - if (!lang || lang === 'en' || lang === 'default') { - return 'en' - } +// Improved locale conversion function +const toMomentLocale = lang => { + if (!lang || ['en', 'default'].includes(lang)) return 'en' return lang.toLowerCase().replace('_', '-') } diff --git a/scripts/tag/gallery.js b/scripts/tag/gallery.js index 5d6949b02..f4bfab126 100644 --- a/scripts/tag/gallery.js +++ b/scripts/tag/gallery.js @@ -3,7 +3,7 @@ * galleryGroup and gallery * {% galleryGroup [name] [descr] [url] [img] %} * - * {% gallery [button] %} + * {% gallery [button],[limit],[firstLimit] %} * {% gallery url,[url],[button] %} */ @@ -11,54 +11,66 @@ const urlFor = require('hexo-util').url_for.bind(hexo) -const gallery = (args, content) => { - args = args.join(' ').split(',') - let button = false - let type = 'data' - let dataStr = '' +const DEFAULT_LIMIT = 10 +const DEFAULT_FIRST_LIMIT = 10 +const IMAGE_REGEX = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g + +// Helper functions +const parseGalleryArgs = args => { + const [type, ...rest] = args.join(' ').split(',').map(arg => arg.trim()) + return { + isUrl: type === 'url', + params: type === 'url' ? rest : [type, ...rest] + } +} + +const parseImageContent = content => { + const images = [] + let match + + while ((match = IMAGE_REGEX.exec(content)) !== null) { + images.push({ + url: match[2], + alt: match[1] || '', + title: match[3] || '' + }) + } + + return images +} - if (args[0] === 'url') { - [type, dataStr, button] = args // url,[link],[lazyload] - dataStr = urlFor(dataStr) - } else { - [button] = args // [lazyload] - const regex = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g - let m - const arr = [] - while ((m = regex.exec(content)) !== null) { - if (m.index === regex.lastIndex) { - regex.lastIndex++ - } - arr.push({ - url: m[2], - alt: m[1], - title: m[3] - }) - } +const createGalleryHTML = (type, dataStr, button, limit, firstLimit) => { + return `` +} + +const gallery = (args, content) => { + const { isUrl, params } = parseGalleryArgs(args) - dataStr = JSON.stringify(arr) + if (isUrl) { + const [dataStr, button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params + return createGalleryHTML('url', urlFor(dataStr), button, limit, firstLimit) } - return `` + const [button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params + const images = parseImageContent(content) + return createGalleryHTML('data', JSON.stringify(images), button, limit, firstLimit) } const galleryGroup = args => { - const [name, descr, url, img] = args - const imgUrl = urlFor(img) - const urlLink = urlFor(url) + const [name = '', descr = '', url = '', img = ''] = args.map(arg => arg.trim()) return ` - ` + Group Image Gallery +
+ +

${descr}

+ +
+ ` } +// Register tags hexo.extend.tag.register('gallery', gallery, { ends: true }) hexo.extend.tag.register('galleryGroup', galleryGroup) diff --git a/scripts/tag/inlineImg.js b/scripts/tag/inlineImg.js index 753add0f3..a0357ebd0 100644 --- a/scripts/tag/inlineImg.js +++ b/scripts/tag/inlineImg.js @@ -1,9 +1,9 @@ /** - * inlineImg 圖片 - * @param {Array} args 圖片名稱和高度 - * @param {string} args[0] 圖片名稱 - * @param {number} args[1] 圖片高度 - * @returns {string} 圖片標籤 + * inlineImg + * @param {Array} args - Image name and height + * @param {string} args[0] - Image name + * @param {number} args[1] - Image height + * @returns {string} - Image tag */ 'use strict' diff --git a/scripts/tag/mermaid.js b/scripts/tag/mermaid.js index ba25c0fbe..bf6b1e582 100644 --- a/scripts/tag/mermaid.js +++ b/scripts/tag/mermaid.js @@ -10,7 +10,7 @@ const { escapeHTML } = require('hexo-util') const mermaid = (args, content) => { return `
` } diff --git a/scripts/tag/timeline.js b/scripts/tag/timeline.js index 12579406c..945454c7f 100644 --- a/scripts/tag/timeline.js +++ b/scripts/tag/timeline.js @@ -1,41 +1,50 @@ /** - * timeline - * by Jerry + * Timeline tag for Hexo + * Syntax: + * {% timeline [headline],[color] %} + * + * [content] + * + * + * [content] + * + * {% endtimeline %} */ 'use strict' const timeLineFn = (args, content) => { - const tlBlock = /\n([\w\W\s\S]*?)/g - - let result = '' - let color = '' - let text = '' - if (args.length) { - [text, color] = args.join(' ').split(',') - const mdContent = hexo.render.renderSync({ text, engine: 'markdown' }) - result += `
${mdContent}
` - } - - const matches = [] - let match - - while ((match = tlBlock.exec(content)) !== null) { - matches.push(match[1]) - matches.push(match[2]) - } - - for (let i = 0; i < matches.length; i += 2) { - const tlChildTitle = hexo.render.renderSync({ text: matches[i], engine: 'markdown' }) - const tlChildContent = hexo.render.renderSync({ text: matches[i + 1], engine: 'markdown' }) - - const tlTitleHtml = `
${tlChildTitle}
` - const tlContentHtml = `
${tlChildContent}
` - - result += `
${tlTitleHtml + tlContentHtml}
` - } - - return `
${result}
` + // Use named capture groups for better readability + const tlBlock = /\n(?[\s\S]*?)/g + + // Pre-compile markdown render function + const renderMd = text => hexo.render.renderSync({ text, engine: 'markdown' }) + + // Parse arguments more efficiently + const [text, color = ''] = args.length ? args.join(' ').split(',') : [] + + // Build initial headline if text exists + const headline = text + ? `
+
+
${renderMd(text)}
+
+
` + : '' + + // Match all timeline blocks in one pass and transform + const items = Array.from(content.matchAll(tlBlock)) + .map(({ groups: { title, content } }) => + `
+
+
${renderMd(title)}
+
+
${renderMd(content)}
+
` + ) + .join('') + + return `
${headline}${items}
` } hexo.extend.tag.register('timeline', timeLineFn, { ends: true }) diff --git a/source/js/main.js b/source/js/main.js index db49960a7..20f509a87 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -224,14 +224,23 @@ document.addEventListener('DOMContentLoaded', () => { */ const fetchUrl = async url => { - const response = await fetch(url) - return await response.json() + try { + const response = await fetch(url) + return await response.json() + } catch (error) { + console.error('Failed to fetch URL:', error) + return [] + } } - const runJustifiedGallery = (item, data, isButton = false, tabs) => { + const runJustifiedGallery = (container, data, config) => { + const { isButton, limit, firstLimit, tabs } = config + const dataLength = data.length + const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1) - const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, { + // Gallery configuration + const igConfig = { gap: 5, isConstantSize: true, sizeRange: [150, 600], @@ -239,132 +248,130 @@ document.addEventListener('DOMContentLoaded', () => { // observeChildren: true, useTransform: true // useRecycle: false - }) - - const replaceDq = str => str.replace(/"/g, '"') // replace double quotes to " + } - const getItems = (nextGroupKey, count) => { - const nextItems = [] - const startCount = (nextGroupKey - 1) * count + const ig = new InfiniteGrid.JustifiedInfiniteGrid(container, igConfig) + let isLayoutHidden = false - for (let i = 0; i < count; ++i) { - const num = startCount + i - if (num >= dataLength) { - break - } + // Utility functions + const sanitizeString = str => (str && str.replace(/"/g, '"')) || '' - const item = data[num] - const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : '' - const title = item.title ? `title="${replaceDq(item.title)}"` : '' + const createImageItem = item => { + const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : '' + const title = item.title ? `title="${sanitizeString(item.title)}"` : '' + return `
+ +
` + } - nextItems.push(`
- -
`) - } - return nextItems + const getItems = (nextGroupKey, count, isFirst = false) => { + const startIndex = isFirst ? (nextGroupKey - 1) * count : (nextGroupKey - 2) * count + firstLimit + return data.slice(startIndex, startIndex + count).map(createImageItem) } - const buttonText = GLOBAL_CONFIG.infinitegrid.buttonText - const addButton = item => { + // Load more button + const addLoadMoreButton = container => { const button = document.createElement('button') - button.innerHTML = buttonText + '' + button.innerHTML = `${GLOBAL_CONFIG.infinitegrid.buttonText}` - button.addEventListener('click', e => { - e.target.closest('button').remove() - btf.setLoading.add(item) - appendItem(ig.getGroups().length + 1, 10) + button.addEventListener('click', () => { + button.remove() + btf.setLoading.add(container) + appendItems(ig.getGroups().length + 1, limit) }, { once: true }) - item.insertAdjacentElement('afterend', button) + container.insertAdjacentElement('afterend', button) } - const appendItem = (nextGroupKey, count) => { - ig.append(getItems(nextGroupKey, count), nextGroupKey) + const appendItems = (nextGroupKey, count, isFirst) => { + ig.append(getItems(nextGroupKey, count, isFirst), nextGroupKey) } - const maxGroupKey = Math.ceil(dataLength / 10) - let isLayoutHidden = false - - const completeFn = e => { + // Event handlers + const handleRenderComplete = e => { if (tabs) { - const parentNode = item.parentNode - + const parentNode = container.parentNode if (isLayoutHidden) { parentNode.style.visibility = 'visible' } - - if (item.offsetHeight === 0) { + if (container.offsetHeight === 0) { parentNode.style.visibility = 'hidden' isLayoutHidden = true } } const { updated, isResize, mounted } = e - if (!updated.length || !mounted.length || isResize) { - return - } + if (!updated.length || !mounted.length || isResize) return - btf.loadLightbox(item.querySelectorAll('img:not(.medium-zoom-image)')) + btf.loadLightbox(container.querySelectorAll('img:not(.medium-zoom-image)')) if (ig.getGroups().length === maxGroupKey) { - btf.setLoading.remove(item) - !tabs && ig.off('renderComplete', completeFn) + btf.setLoading.remove(container) + !tabs && ig.off('renderComplete', handleRenderComplete) return } if (isButton) { - btf.setLoading.remove(item) - addButton(item) + btf.setLoading.remove(container) + addLoadMoreButton(container) } } - const requestAppendFn = btf.debounce(e => { + const handleRequestAppend = btf.debounce(e => { const nextGroupKey = (+e.groupKey || 0) + 1 - appendItem(nextGroupKey, 10) - if (nextGroupKey === maxGroupKey) { - ig.off('requestAppend', requestAppendFn) - } + if (nextGroupKey === 1) appendItems(nextGroupKey, firstLimit, true) + else appendItems(nextGroupKey, limit) + + if (nextGroupKey === maxGroupKey) ig.off('requestAppend', handleRequestAppend) }, 300) - btf.setLoading.add(item) - ig.on('renderComplete', completeFn) + btf.setLoading.add(container) + ig.on('renderComplete', handleRenderComplete) if (isButton) { - appendItem(1, 10) + appendItems(1, firstLimit, true) } else { - ig.on('requestAppend', requestAppendFn) + ig.on('requestAppend', handleRequestAppend) ig.renderItems() } - btf.addGlobalFn('pjaxSendOnce', () => { ig.destroy() }) + btf.addGlobalFn('pjaxSendOnce', () => ig.destroy()) } - const addJustifiedGallery = async (ele, tabs = false) => { - if (!ele.length) return - const init = async () => { - for (const item of ele) { - if (btf.isHidden(item) || item.classList.contains('loaded')) continue - - const isButton = item.getAttribute('data-button') === 'true' - const children = item.firstElementChild - const text = children.textContent - children.textContent = '' - item.classList.add('loaded') + const addJustifiedGallery = async (elements, tabs = false) => { + if (!elements.length) return + + const initGallery = async () => { + for (const element of elements) { + if (btf.isHidden(element) || element.classList.contains('loaded')) continue + + const config = { + isButton: element.getAttribute('data-button') === 'true', + limit: parseInt(element.getAttribute('data-limit'), 10), + firstLimit: parseInt(element.getAttribute('data-first'), 10), + tabs + } + + const container = element.firstElementChild + const content = container.textContent + container.textContent = '' + element.classList.add('loaded') + try { - const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text) - runJustifiedGallery(children, content, isButton, tabs) - } catch (e) { - console.error('Gallery data parsing failed:', e) + const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content) + runJustifiedGallery(container, data, config) + } catch (error) { + console.error('Gallery data parsing failed:', error) } } } if (typeof InfiniteGrid === 'function') { - init() + await initGallery() } else { - await btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`) - init() + await btf.getScript(GLOBAL_CONFIG.infinitegrid.js) + await initGallery() } }