From 67c95cee0c6fcb6bad8e1890b28f8b35686e0d3b Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 9 Sep 2025 15:40:08 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8D=20=E6=90=9C=E7=B4=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=94=B9=E9=80=B2=EF=BC=9A=20-=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=90=9C=E7=B4=A2=E5=88=86=E9=A0=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20(enablePagination,=20hitsPerPage)=20-=20=E9=87=8D?= =?UTF-8?q?=E6=A7=8B=20Algolia=20=E6=90=9C=E7=B4=A2=E9=82=8F=E8=BC=AF?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8F=B4=E5=A4=9A=E7=B4=A2=E5=BC=95=E5=92=8C?= =?UTF-8?q?=E6=9B=B4=E5=A5=BD=E7=9A=84=E9=8C=AF=E8=AA=A4=E8=99=95=E7=90=86?= =?UTF-8?q?=20-=20=E5=84=AA=E5=8C=96=E6=90=9C=E7=B4=A2=20UI=20=E6=A8=A3?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E5=8C=85=E6=8B=AC=E5=88=86=E9=A0=81=E6=8C=89?= =?UTF-8?q?=E9=88=95=E5=92=8C=E9=9F=BF=E6=87=89=E5=BC=8F=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=20-=20=E6=94=B9=E9=80=B2=E6=90=9C=E7=B4=A2=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=B7=A8=E8=99=9F?= =?UTF-8?q?=E5=92=8C=E6=9B=B4=E5=A5=BD=E7=9A=84=E9=AB=98=E4=BA=AE=E8=99=95?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 依賴項更新: - 更新 plugins.yml 中的多個插件版本 (abcjs, algolia, aplayer 等) - 更新 package.json 版本號為 5.5.0 🎨 UI/UX 優化: - 改進側邊欄和目錄的動畫效果 - 優化樣式佈局,調整寬度百分比 - 新增說說頁面的分頁導航組件 - 改進右側邊欄按鈕樣式 🐛 錯誤處理和代碼優化: - 修復 Umami Analytics 的錯誤處理和數據驗證 - 改進懶加載圖片的正則表達式,避免匹配腳本標籤 - 移除未使用的變數和改進代碼結構 - 新增說說內容的 Markdown 渲染支援 🔧 其他改進: - 更新翻譯功能,移除箭頭函數語法以提升相容性 --- _config.yml | 6 +- languages/default.yml | 1 + languages/en.yml | 1 + languages/ja.yml | 1 + languages/ko.yml | 1 + languages/zh-CN.yml | 1 + languages/zh-HK.yml | 1 + languages/zh-TW.yml | 1 + layout/includes/head/config.pug | 7 +- layout/includes/page/shuoshuo.pug | 353 +++++++---- layout/includes/third-party/math/mathjax.pug | 33 +- .../includes/third-party/search/algolia.pug | 24 +- .../third-party/search/local-search.pug | 20 +- .../includes/third-party/umami_analytics.pug | 97 ++- package.json | 2 +- plugins.yml | 40 +- scripts/common/default_config.js | 4 + scripts/events/init.js | 1 - scripts/filters/post_lazyload.js | 25 +- scripts/helpers/page.js | 8 +- source/css/_layout/aside.styl | 13 +- source/css/_layout/rightside.styl | 4 + source/css/_layout/sidebar.styl | 23 +- source/css/_page/common.styl | 2 +- source/css/_page/shuoshuo.styl | 107 ++++ source/css/_search/algolia.styl | 86 +-- source/css/_search/index.styl | 193 +++++- source/css/_search/local-search.styl | 65 +-- source/js/main.js | 2 +- source/js/search/algolia.js | 550 +++++++++++++++--- source/js/search/local-search.js | 247 +++++++- source/js/tw_cn.js | 8 +- source/js/utils.js | 2 +- 33 files changed, 1479 insertions(+), 450 deletions(-) diff --git a/_config.yml b/_config.yml index 77d358a..61ad8c4 100644 --- a/_config.yml +++ b/_config.yml @@ -488,6 +488,11 @@ search: top_n_per_article: 1 # Unescape html strings to the readable one. unescape: false + # Enable pagination for search results + pagination: + enable: false + # Number of search results per page + hitsPerPage: 8 CDN: # Docsearch @@ -1097,7 +1102,6 @@ CDN: # gitalk_css: # giscus: # instantpage: - # instantsearch: # katex: # katex_copytex: # lazyload: diff --git a/languages/default.yml b/languages/default.yml index 6e222ab..193e89a 100644 --- a/languages/default.yml +++ b/languages/default.yml @@ -48,6 +48,7 @@ search: pagination: prev: Previous next: Next + page_info: 'Page ${current} of ${total}' comment: Comments diff --git a/languages/en.yml b/languages/en.yml index 6e222ab..03b2712 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -48,6 +48,7 @@ search: pagination: prev: Previous next: Next + page_info: 'Page ${current} of ${total}' comment: Comments diff --git a/languages/ja.yml b/languages/ja.yml index 3144f05..0b7ebf2 100644 --- a/languages/ja.yml +++ b/languages/ja.yml @@ -48,6 +48,7 @@ search: pagination: prev: 前へ next: 次へ + page_info: '${current} ページ / 合計 ${total} ページ' comment: コメント diff --git a/languages/ko.yml b/languages/ko.yml index 1efaf7d..3c10d76 100644 --- a/languages/ko.yml +++ b/languages/ko.yml @@ -48,6 +48,7 @@ search: pagination: prev: 이전 next: 다음 + page_info: '${current} 페이지 / 총 ${total} 페이지' comment: 댓글 diff --git a/languages/zh-CN.yml b/languages/zh-CN.yml index 14c8615..782e013 100644 --- a/languages/zh-CN.yml +++ b/languages/zh-CN.yml @@ -49,6 +49,7 @@ search: pagination: prev: 上一篇 next: 下一篇 + page_info: '第 ${current} 頁 / 共 ${total} 頁' comment: 评论 diff --git a/languages/zh-HK.yml b/languages/zh-HK.yml index fb0bae1..68132fe 100644 --- a/languages/zh-HK.yml +++ b/languages/zh-HK.yml @@ -48,6 +48,7 @@ search: pagination: prev: 上一頁 next: 下一頁 + page_info: '第 ${current} 頁 / 共 ${total} 頁' comment: 評論 diff --git a/languages/zh-TW.yml b/languages/zh-TW.yml index 333a483..ed663be 100644 --- a/languages/zh-TW.yml +++ b/languages/zh-TW.yml @@ -48,6 +48,7 @@ search: pagination: prev: 上一篇 next: 下一篇 + page_info: '第 ${current} 頁 / 共 ${total} 頁' comment: 評論 diff --git a/layout/includes/head/config.pug b/layout/includes/head/config.pug index b50ecfe..99e1305 100644 --- a/layout/includes/head/config.pug +++ b/layout/includes/head/config.pug @@ -10,7 +10,6 @@ hitsPerPage: theme.search.algolia_search.hitsPerPage, // search languages languages: { - input_placeholder: theme.search.placeholder || _p("search.input_placeholder"), hits_empty: _p("search.algolia_search.hits_empty"), hits_stats: _p("search.algolia_search.hits_stats"), } @@ -19,12 +18,16 @@ let localSearch = 'undefined' if (theme.search.use === 'local_search') { - const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search + const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search localSearch = JSON.stringify({ path: CDN || config.root + config.search.path, preload, top_n_per_article, unescape, + pagination: { + enable: pagination.enable, + hitsPerPage: pagination.hitsPerPage + }, languages: { // search languages hits_empty: _p("search.local_search.hits_empty"), diff --git a/layout/includes/page/shuoshuo.pug b/layout/includes/page/shuoshuo.pug index c107d5c..81a32ee 100644 --- a/layout/includes/page/shuoshuo.pug +++ b/layout/includes/page/shuoshuo.pug @@ -9,40 +9,43 @@ - page.toc = false #article-container - if page.comments !== false && theme.comments.use - - commentsJsLoad = true + if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length) + if page.comments !== false && theme.comments.use + - commentsJsLoad = true - script. - (() => { - const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}` + script. + (() => { + const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}` - const runDestroy = (shuoshuoComment) => { - if (!shuoshuoComment) return + const runDestroy = (shuoshuoComment) => { + if (!shuoshuoComment) return - for (const [key, fn] of Object.entries(shuoshuoComment)) { - if (key.startsWith('destroy')) fn() + for (const [key, fn] of Object.entries(shuoshuoComment)) { + if (key.startsWith('destroy')) fn() + } } - } - window.addCommentToShuoshuo = e => { - const btn = e.target.closest('.shuoshuo-comment-btn') - if (!btn) return + window.addCommentToShuoshuo = e => { + const btn = e.target.closest('.shuoshuo-comment-btn') + if (!btn) return - const ele = btn.closest('.container').nextElementSibling - const { shuoshuoComment } = window - const isInclude = ele.classList.contains('no-comment') - runDestroy(shuoshuoComment) - if (isInclude) { - ele.classList.remove('no-comment') - ele.innerHTML = commentDiv - const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}` - btf.switchComments(ele, key) - shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key) + const ele = btn.closest('.container').nextElementSibling + const { shuoshuoComment } = window + const isInclude = ele.classList.contains('no-comment') + runDestroy(shuoshuoComment) + if (isInclude) { + ele.classList.remove('no-comment') + ele.innerHTML = commentDiv + const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}` + btf.switchComments(ele, key) + shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key) + } } - } - })() + })() + + + - const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page) - if page.shuoshuo_url script. (() => { const limitConfig = !{ JSON.stringify(page.limit || {}) } @@ -78,111 +81,217 @@ return `${year}-${month}-${day} ${hour}:${minute}:${second}` } + let currentPage = 1 + const itemsPerPage = 8 + let totalPages = 0 + let data = [] + let inputEventsAttached = false // Flag to mark if input event listeners have been added + + const renderData = (dataSlice) => { + const content = dataSlice.map(item => { + const formattedDate = formatToTimeZone(item.date) + const tags = item.tags && item.tags.map(tag => `${tag}`).join('') || '' + const commentButton = item.key && !{commentsJsLoad} + ? `
+ +
` + : '' + const commentContainer = item.key + ? `
` + : '' + + return ` +
+
+
+
+ +
+
+
${item.author || '!{config.author}'}
+ +
+
+
${item.content}
+ +
+ ${commentContainer} +
` + }).join('') + + const container = document.getElementById('article-container') + container.innerHTML = content + + window.lazyLoadInstance && window.lazyLoadInstance.update() + btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) + } + + const renderNavigation = () => { + const container = document.getElementById('article-container') + const existingNav = container.nextElementSibling + if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) { + existingNav.remove() + } + + const pageInfoTemplate = '#{__('pagination.page_info')}' + const pageInfoText = pageInfoTemplate + .replace(/\$\{current}/g, currentPage) + .replace(/\$\{total}/g, totalPages) + + const navHtml = ` +
+ + ${pageInfoText} + + +
+ ` + container.insertAdjacentHTML('afterend', navHtml) + + // Add input validation event listeners (only once) + if (!inputEventsAttached) { + setTimeout(() => { + const input = document.querySelector('.shuoshuo-page-input') + if (input) { + // Clear placeholder when clicking the input box + input.addEventListener('focus', (event) => { + event.target.placeholder = '' + }) + + // Restore placeholder if no content when losing focus + input.addEventListener('blur', (event) => { + if (!event.target.value.trim()) { + event.target.placeholder = currentPage + } + }) + + input.addEventListener('input', (event) => { + const value = parseInt(event.target.value) || 0 + let wasInvalid = false + + if (value > totalPages) { + event.target.value = totalPages + wasInvalid = true + } else if (value < 1 && event.target.value !== '') { + event.target.value = 1 + wasInvalid = true + } + + // If value is corrected, show red and shake effect + if (wasInvalid) { + event.target.classList.add('invalid') + setTimeout(() => { + event.target.classList.remove('invalid') + }, 500) + } + }) + + inputEventsAttached = true // Mark that event listeners have been added + } + }, 0) + } + } + + const renderPage = (page) => { + const start = (page - 1) * itemsPerPage + const end = start + itemsPerPage + const pageData = data.slice(start, end) + renderData(pageData) + renderNavigation() + } + + window.shuoshuoPrevPage = () => { + if (currentPage > 1) { + currentPage-- + renderPage(currentPage) + } + } + + window.shuoshuoNextPage = () => { + if (currentPage < totalPages) { + currentPage++ + renderPage(currentPage) + } + } + + window.shuoshuoGoToPage = (page) => { + if (typeof page === 'number') { + // Directly jump to the specified page + if (page >= 1 && page <= totalPages && page !== currentPage) { + currentPage = page + renderPage(currentPage) + } + } else { + // Get page from input box + const input = document.querySelector('.shuoshuo-page-input') + const inputValue = input.value.trim() + const inputPage = inputValue === '' ? currentPage : parseInt(inputValue) + if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) { + currentPage = inputPage + renderPage(currentPage) + } else if (inputValue === '') { + // If input box is empty, re-render current page (update placeholder) + renderPage(currentPage) + } + } + } + + window.shuoshuoHandleKeyDown = (event) => { + const input = event.target + const value = input.value + event.key + + // Allow delete, arrow keys, backspace, etc. + if (event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' || + event.key === 'ArrowLeft' || event.key === 'ArrowRight' || + event.key === 'Tab' || event.ctrlKey || event.metaKey) { + if (event.key === 'Enter') { + window.shuoshuoGoToPage() + } + return + } + + // Only allow numbers + if (!/^\d$/.test(event.key)) { + event.preventDefault() + return + } + + // Check if the value after input exceeds the range + const newValue = parseInt(value) || 0 + if (newValue > totalPages || (value.length > 1 && newValue === 0)) { + event.preventDefault() + // Add red and shake effect + input.classList.add('invalid') + setTimeout(() => { + input.classList.remove('invalid') + }, 500) + } + } + const loadShuoshuo = async () => { try { - const response = await fetch('!{url_for(page.shuoshuo_url)}') - let data = await response.json() - - data = filterDataByLimit(sortDataByDate(data), limitConfig) - - const container = document.getElementById('article-container') - let start = 0 - - const renderData = (dataSlice) => { - const content = dataSlice.map(item => { - const formattedDate = formatToTimeZone(item.date) - const tags = item.tags && item.tags.map(tag => `${tag}`).join('') || '' - const commentButton = item.key && !{commentsJsLoad} - ? `
- -
` - : '' - const commentContainer = item.key - ? `
` - : '' - - return ` -
-
-
-
- -
-
-
${item.author || '!{config.author}'}
- -
-
-
${item.content}
- -
- ${commentContainer} -
` - }).join('') - - container.insertAdjacentHTML('beforeend', content) - - window.lazyLoadInstance.update() - btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) + let originData = [] + if (!{Boolean(page.shuoshuo_url)}) { + const response = await fetch('!{url_for(page.shuoshuo_url)}') + originData = await response.json() + } else { + originData = !{JSON.stringify(localDate)} } - const handleIntersection = (entries) => { - if (!entries[0].isIntersecting) return - observer.unobserve(entries[0].target) + data = filterDataByLimit(sortDataByDate(originData), limitConfig) - const slice = data.slice(start, start + 10) - renderData(slice) - start += 10 + totalPages = Math.ceil(data.length / itemsPerPage) - if (start < data.length) { - setTimeout(() => observer.observe(container.lastElementChild), 100) - } else { - observer.disconnect() - } - }; - - const observer = new IntersectionObserver(handleIntersection, { - root: null, - rootMargin: '0px', - threshold: 1.0 - }) - - renderData(data.slice(start, 10)) - start += 10 - - if (container.lastElementChild) observer.observe(container.lastElementChild) + renderPage(currentPage) } catch (error) { console.error(error) } }; window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo) - })() - else - if site.data.shuoshuo - each i in shuoshuoFN(site.data.shuoshuo, page) - .shuoshuo-item - .container - .shuoshuo-item-header - .shuoshuo-avatar - img.no-lightbox(src=i.avatar || url_for(theme.avatar.img)) - .shuoshuo-info - .shuoshuo-author=i.author || config.author - time.shuoshuo-date(title=i.date)=i.date - .shuoshuo-content - !=markdown(i.content) - .shuoshuo-footer(class=i.tags && i.tags.length ? 'flex-between' : 'flex-end') - if i.tags - .shuoshuo-tags - each tag in i.tags - span.shuoshuo-tag=tag - if i.key && commentsJsLoad - .shuoshuo-comment-btn(onclick='addCommentToShuoshuo(event)') - i.fa-solid.fa-comments - if i.key && commentsJsLoad - .shuoshuo-comment.no-comment(data-key=i.key) \ No newline at end of file + })() \ No newline at end of file diff --git a/layout/includes/third-party/math/mathjax.pug b/layout/includes/third-party/math/mathjax.pug index e937dbc..b22d993 100644 --- a/layout/includes/third-party/math/mathjax.pug +++ b/layout/includes/third-party/math/mathjax.pug @@ -1,19 +1,50 @@ -//- Mathjax 3 +//- Mathjax 4/5 - const { tags, enableMenu } = theme.math.mathjax script. (() => { const loadMathjax = () => { if (!window.MathJax) { window.MathJax = { + loader: { + load: [ + // Four font extension packages (optional) + //- '[tex]/bbm', + //- '[tex]/bboldx', + //- '[tex]/dsfont', + '[tex]/mhchem' + ], + paths: { + 'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font', + + //- // Four font extension packages (optional) + //- 'mathjax-bbm-extension': '[mathjax]/../@mathjax/mathjax-bbm-font-extension', + //- 'mathjax-bboldx-extension': '[mathjax]/../@mathjax/mathjax-bboldx-font-extension', + //- 'mathjax-dsfont-extension': '[mathjax]/../@mathjax/mathjax-dsfont-font-extension', + 'mathjax-mhchem-extension': '[mathjax]/../@mathjax/mathjax-mhchem-font-extension' + } + }, + output: { + font: 'mathjax-newcm', + }, tex: { inlineMath: [['$', '$'], ['\\(', '\\)']], tags: '!{tags}', + packages: { + '[+]': [ + 'mhchem' + ] + } }, chtml: { scale: 1.1 }, options: { enableMenu: !{enableMenu}, + menuOptions: { + settings: { + enrich: false // Turn off Braille and voice narration text automatic generation + } + }, renderActions: { findScript: [10, doc => { for (const node of document.querySelectorAll('script[type^="math/tex"]')) { diff --git a/layout/includes/third-party/search/algolia.pug b/layout/includes/third-party/search/algolia.pug index c0dd215..19dca66 100644 --- a/layout/includes/third-party/search/algolia.pug +++ b/layout/includes/third-party/search/algolia.pug @@ -2,21 +2,33 @@ .search-dialog nav.search-nav span.search-dialog-title= _p('search.title') + i.fas.fa-spinner.fa-pulse#loading-status(hidden) button.search-close-button i.fas.fa-times - .search-wrap - #algolia-search-input + #algolia-search-input + .ais-SearchBox + form.ais-SearchBox-form(action="" role="search" novalidate="") + input.ais-SearchBox-input(type="search" placeholder=theme.search.placeholder || _p("search.input_placeholder") autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" maxlength="512" aria-label="Search") + button.ais-SearchBox-submit(type="submit" title="Submit the search query" style="display:none;") + svg.ais-SearchBox-submitIcon(width="10" height="10" viewBox="0 0 40 40" aria-hidden="true") + path(d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z") hr #algolia-search-results #algolia-hits - #algolia-pagination + #algolia-hits-empty(style="display:none;") + .ais-Hits(style="display:none;") + ol.ais-Hits-list + #algolia-pagination.ais-Pagination(style="display:none;") + ul.ais-Pagination-list #algolia-info - .algolia-stats - .algolia-poweredBy + span.ais-Stats-text + a.algolia-poweredBy(href="https://www.algolia.com/?utm_source=algoliasearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby" target="_blank" aria-label="Search by Algolia" rel="noopener noreferrer") + svg.ais-PoweredBy-logo(height="1.2em" viewBox="0 0 572 64" style="width: auto;") + path(fill="#36395A" d="M16 48.3c-3.4 0-6.3-.6-8.7-1.7A12.4 12.4 0 0 1 1.9 42C.6 40 0 38 0 35.4h6.5a6.7 6.7 0 0 0 3.9 6c1.4.7 3.3 1.1 5.6 1.1 2.2 0 4-.3 5.4-1a7 7 0 0 0 3-2.4 6 6 0 0 0 1-3.4c0-1.5-.6-2.8-1.9-3.7-1.3-1-3.3-1.6-5.9-1.8l-4-.4c-3.7-.3-6.6-1.4-8.8-3.4a10 10 0 0 1-3.3-7.9c0-2.4.6-4.6 1.8-6.4a12 12 0 0 1 5-4.3c2.2-1 4.7-1.6 7.5-1.6s5.5.5 7.6 1.6a12 12 0 0 1 5 4.4c1.2 1.8 1.8 4 1.8 6.7h-6.5a6.4 6.4 0 0 0-3.5-5.9c-1-.6-2.6-1-4.4-1s-3.2.3-4.4 1c-1.1.6-2 1.4-2.6 2.4-.5 1-.8 2-.8 3.1a5 5 0 0 0 1.5 3.6c1 1 2.6 1.7 4.7 1.9l4 .3c2.8.2 5.2.8 7.2 1.8 2.1 1 3.7 2.2 4.9 3.8a9.7 9.7 0 0 1 1.7 5.8c0 2.5-.7 4.7-2 6.6a13 13 0 0 1-5.6 4.4c-2.4 1-5.2 1.6-8.4 1.6Zm35.6 0c-2.6 0-4.8-.4-6.7-1.3a13 13 0 0 1-4.7-3.5 17.1 17.1 0 0 1-3.6-10.4v-1c0-2 .3-3.8 1-5.6a13 13 0 0 1 7.3-8.3 15 15 0 0 1 6.3-1.4A13.2 13.2 0 0 1 64 24.3c1 2.2 1.6 4.6 1.6 7.2V34H39.4v-4.3h21.8l-1.8 2.2c0-2-.3-3.7-.9-5.1a7.3 7.3 0 0 0-2.7-3.4c-1.2-.7-2.7-1.1-4.6-1.1s-3.4.4-4.7 1.3a8 8 0 0 0-2.9 3.6c-.6 1.5-.9 3.3-.9 5.4 0 2 .3 3.7 1 5.3a7.9 7.9 0 0 0 2.8 3.7c1.3.8 3 1.3 5 1.3s3.8-.5 5.1-1.3c1.3-1 2.1-2 2.4-3.2h6a11.8 11.8 0 0 1-7 8.7 16 16 0 0 1-6.4 1.2ZM80 48c-2.2 0-4-.3-5.7-1a8.4 8.4 0 0 1-3.7-3.3 9.7 9.7 0 0 1-1.3-5.2c0-2 .5-3.8 1.5-5.2a9 9 0 0 1 4.3-3.1c1.8-.7 4-1 6.7-1H89v4.1h-7.5c-2 0-3.4.5-4.4 1.4-1 1-1.6 2.1-1.6 3.6s.5 2.7 1.6 3.6c1 1 2.5 1.4 4.4 1.4 1.1 0 2.2-.2 3.2-.7 1-.4 1.9-1 2.6-2 .6-1 1-2.4 1-4.2l1.7 2.1c-.2 2-.7 3.8-1.5 5.2a9 9 0 0 1-3.4 3.3 12 12 0 0 1-5.3 1Zm9.5-.7v-8.8h-1v-10c0-1.8-.5-3.2-1.4-4.1-1-1-2.4-1.4-4.2-1.4a142.9 142.9 0 0 0-10.2.4v-5.6a74.8 74.8 0 0 1 8.6-.4c3 0 5.5.4 7.5 1.2s3.4 2 4.4 3.6c1 1.7 1.4 4 1.4 6.7v18.4h-5Zm12.9 0V17.8h5v12.3h-.2c0-4.2 1-7.4 2.8-9.5a11 11 0 0 1 8.3-3.1h1v5.6h-2a9 9 0 0 0-6.3 2.2c-1.5 1.5-2.2 3.6-2.2 6.4v15.6h-6.4Zm34.4 1a15 15 0 0 1-6.6-1.3c-1.9-.9-3.4-2-4.7-3.5a15.5 15.5 0 0 1-2.7-5c-.6-1.7-1-3.6-1-5.4v-1c0-2 .4-3.8 1-5.6a15 15 0 0 1 2.8-4.9c1.3-1.5 2.8-2.6 4.6-3.5a16.4 16.4 0 0 1 13.3.2c2 1 3.5 2.3 4.8 4a12 12 0 0 1 2 6H144c-.2-1.6-1-3-2.2-4.1a7.5 7.5 0 0 0-5.2-1.7 8 8 0 0 0-4.7 1.3 8 8 0 0 0-2.8 3.6 13.8 13.8 0 0 0 0 10.3c.6 1.5 1.5 2.7 2.8 3.6s2.8 1.3 4.8 1.3c1.5 0 2.7-.2 3.8-.8a7 7 0 0 0 2.6-2c.7-1 1-2 1.2-3.2h6.2a11 11 0 0 1-2 6.2 15.1 15.1 0 0 1-11.8 5.5Zm19.7-1v-40h6.4V31h-1.3c0-3 .4-5.5 1.1-7.6a9.7 9.7 0 0 1 3.5-4.8A9.9 9.9 0 0 1 172 17h.3c3.5 0 6 1.1 7.9 3.5 1.7 2.3 2.6 5.7 2.6 10v16.8h-6.4V29.6c0-2.1-.6-3.8-1.8-5a6.4 6.4 0 0 0-4.8-1.8c-2 0-3.7.7-5 2a7.8 7.8 0 0 0-1.9 5.5v17h-6.4Zm63.8 1a12.2 12.2 0 0 1-10.9-6.2 19 19 0 0 1-1.8-7.3h1.4v12.5h-5.1v-40h6.4v19.8l-2 3.5c.2-3.1.8-5.7 1.9-7.7a11 11 0 0 1 4.4-4.5c1.8-1 3.9-1.5 6.1-1.5a13.4 13.4 0 0 1 12.8 9.1c.7 1.9 1 3.8 1 6v1c0 2.2-.3 4.1-1 6a13.6 13.6 0 0 1-13.2 9.4Zm-1.2-5.5a8.4 8.4 0 0 0 7.9-5c.7-1.5 1.1-3.3 1.1-5.3s-.4-3.8-1.1-5.3a8.7 8.7 0 0 0-3.2-3.6 9.6 9.6 0 0 0-9.2-.2 8.5 8.5 0 0 0-3.3 3.2c-.8 1.4-1.3 3-1.3 5v2.3a9 9 0 0 0 1.3 4.8 9 9 0 0 0 3.4 3c1.4.7 2.8 1 4.4 1Zm27.3 3.9-10-28.9h6.5l9.5 28.9h-6Zm-7.5 12.2v-5.7h4.9c1 0 2-.1 2.9-.4a4 4 0 0 0 2-1.4c.4-.7.9-1.6 1.2-2.7l8.6-30.9h6.2l-9.3 32.4a14 14 0 0 1-2.5 5 8.9 8.9 0 0 1-4 2.8c-1.5.6-3.4.9-5.6.9h-4.4Zm9-12.2v-5.2h6.4v5.2H248Z") + path(fill="#003DFF" d="M534.4 9.1H528a.8.8 0 0 1-.7-.7V1.8c0-.4.2-.7.6-.8l6.5-1c.4 0 .8.2.9.6v7.8c0 .4-.4.7-.8.7zM428 35.2V.8c0-.5-.3-.8-.7-.8h-.2l-6.4 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.5 0 .8-.4.8-.8V43c0-.4-.3-.7-.6-.8-4.5-.5-4.5-6-4.5-7zm106.5-21.8H528c-.4 0-.7.4-.7.8v34c0 .4.3.8.7.8h6.5c.4 0 .8-.4.8-.8v-34c0-.5-.4-.8-.8-.8zm-17.7 21.8V.8c0-.5-.3-.8-.8-.8l-6.5 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.4 0 .8-.4.8-.8V43c0-.4-.3-.7-.7-.8-4.4-.5-4.4-6-4.4-7zm-22.2-20.6a16.5 16.5 0 0 1 8.6 9.3c.8 2.2 1.3 4.8 1.3 7.5a19.4 19.4 0 0 1-4.6 12.6 14.8 14.8 0 0 1-5.2 3.6c-2 .9-5.2 1.4-6.8 1.4a21 21 0 0 1-6.7-1.4 15.4 15.4 0 0 1-8.6-9.3 21.3 21.3 0 0 1 0-14.4 15.2 15.2 0 0 1 8.6-9.3c2-.8 4.3-1.2 6.7-1.2s4.6.4 6.7 1.2zm-6.7 27.6c2.7 0 4.7-1 6.2-3s2.2-4.3 2.2-7.8-.7-6.3-2.2-8.3-3.5-3-6.2-3-4.7 1-6.1 3c-1.5 2-2.2 4.8-2.2 8.3s.7 5.8 2.2 7.8 3.5 3 6.2 3zm-88.8-28.8c-6.2 0-11.7 3.3-14.8 8.2a18.6 18.6 0 0 0 4.8 25.2c1.8 1.2 4 1.8 6.2 1.7s.1 0 .1 0h.9c4.2-.7 8-4 9.1-8.1v7.4c0 .4.3.7.8.7h6.4a.7.7 0 0 0 .7-.7V14.2c0-.5-.3-.8-.7-.8h-13.5zm6.3 26.5a9.8 9.8 0 0 1-5.7 2h-.5a10 10 0 0 1-9.2-14c1.4-3.7 5-6.3 9-6.3h6.4v18.3zm152.3-26.5h13.5c.5 0 .8.3.8.7v33.7c0 .4-.3.7-.8.7h-6.4a.7.7 0 0 1-.8-.7v-7.4c-1.2 4-4.8 7.4-9 8h-.1a4.2 4.2 0 0 1-.5.1h-.9a10.3 10.3 0 0 1-7-2.6c-4-3.3-6.5-8.4-6.5-14.2 0-3.7 1-7.2 3-10 3-5 8.5-8.3 14.7-8.3zm.6 28.4c2.2-.1 4.2-.6 5.7-2V21.7h-6.3a9.8 9.8 0 0 0-9 6.4 10.2 10.2 0 0 0 9.1 13.9h.5zM452.8 13.4c-6.2 0-11.7 3.3-14.8 8.2a18.5 18.5 0 0 0 3.6 24.3 10.4 10.4 0 0 0 13 .6c2.2-1.5 3.8-3.7 4.5-6.1v7.8c0 2.8-.8 5-2.2 6.3-1.5 1.5-4 2.2-7.5 2.2l-6-.3c-.3 0-.7.2-.8.5l-1.6 5.5c-.1.4.1.8.5 1h.1c2.8.4 5.5.6 7 .6 6.3 0 11-1.4 14-4.1 2.7-2.5 4.2-6.3 4.5-11.4V14.2c0-.5-.4-.8-.8-.8h-13.5zm6.3 8.2v18.3a9.6 9.6 0 0 1-5.6 2h-1a10.3 10.3 0 0 1-8.8-14c1.4-3.7 5-6.3 9-6.3h6.4zM291 31.5A32 32 0 0 1 322.8 0h30.8c.6 0 1.2.5 1.2 1.2v61.5c0 1.1-1.3 1.7-2.2 1l-19.2-17a18 18 0 0 1-11 3.4 18.1 18.1 0 1 1 18.2-14.8c-.1.4-.5.7-.9.6-.1 0-.3 0-.4-.2l-3.8-3.4c-.4-.3-.6-.8-.7-1.4a12 12 0 1 0-2.4 8.3c.4-.4 1-.5 1.6-.2l14.7 13.1v-46H323a26 26 0 1 0 10 49.7c.8-.4 1.6-.2 2.3.3l3 2.7c.3.2.3.7 0 1l-.2.2a32 32 0 0 1-47.2-28.6z") #search-mask script(src=url_for(theme.asset.algolia_search)) - script(src=url_for(theme.asset.instantsearch)) script(src=url_for(theme.asset.algolia_js)) \ No newline at end of file diff --git a/layout/includes/third-party/search/local-search.pug b/layout/includes/third-party/search/local-search.pug index 6c28dfd..dd154ec 100644 --- a/layout/includes/third-party/search/local-search.pug +++ b/layout/includes/third-party/search/local-search.pug @@ -2,7 +2,7 @@ .search-dialog nav.search-nav span.search-dialog-title= _p('search.title') - span#loading-status + i.fas.fa-spinner.fa-pulse#loading-status(hidden) button.search-close-button i.fas.fa-times @@ -10,13 +10,15 @@ i.fas.fa-spinner.fa-pulse span= ' ' + _p("search.load_data") - .search-wrap - #local-search-input - .local-search-box - input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input - hr - #local-search-results - #local-search-stats-wrap - #search-mask + .local-search-input + input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text") + hr + + #local-search-results + #local-search-pagination.ais-Pagination(style="display:none;") + ul.ais-Pagination-list + #local-search-stats + + #search-mask script(src=url_for(theme.asset.local_search)) \ No newline at end of file diff --git a/layout/includes/third-party/umami_analytics.pug b/layout/includes/third-party/umami_analytics.pug index 7487425..a344a7a 100644 --- a/layout/includes/third-party/umami_analytics.pug +++ b/layout/includes/third-party/umami_analytics.pug @@ -9,7 +9,11 @@ script. const config = !{JSON.stringify(UV_PV)} const runTrack = () => { - umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) + if (typeof umami !== 'undefined' && typeof umami.track === 'function') { + umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) + } else { + console.warn('Umami Analytics: umami.track is not available') + } } const loadUmamiJS = () => { @@ -17,20 +21,39 @@ script. 'data-website-id': '!{website_id}', 'data-auto-track': 'false', ...option - }).then(runTrack) + }).then(() => { + runTrack() + }).catch(error => { + console.error('Umami Analytics: Error loading script', error) + }) } const getData = async (isPost) => { - const now = Date.now() - const keyUrl = isPost ? `&url=${window.location.pathname}` : '' - const headerList = { 'Accept': 'application/json' } - if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}` - else headerList['x-umami-api-key'] = config.token - const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { - method: "GET", - headers: headerList - }) - return await res.json() + try { + const now = Date.now() + const keyUrl = isPost ? `&url=${window.location.pathname}` : '' + const headerList = { 'Accept': 'application/json' } + + if (!{isServerURL}) { + headerList['Authorization'] = `Bearer ${config.token}` + } else { + headerList['x-umami-api-key'] = config.token + } + + const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { + method: "GET", + headers: headerList + }) + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`) + } + + return await res.json() + } catch (error) { + console.error('Umami Analytics: Failed to fetch data', error) + throw error + } } const insertData = async () => { @@ -39,27 +62,49 @@ script. const pagePV = document.getElementById('umamiPV') if (pagePV) { const data = await getData(true) - pagePV.textContent = data.pageviews.value - } - } else { - const data = (config.site_uv || config.site_pv) && await getData() - if (config.site_uv) { - const siteUV = document.getElementById('umami-site-uv') - if (siteUV) siteUV.textContent = data.visitors.value - } - if (config.site_pv) { - const sitePV = document.getElementById('umami-site-pv') - if (sitePV) sitePV.textContent = data.pageviews.value + if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') { + pagePV.textContent = data.pageviews.value + } else { + console.warn('Umami Analytics: Invalid page view data received') + } } } - } catch (e) { - console.error('Failed to load Umami Analytics:', e) + + if (config.site_uv || config.site_pv) { + const data = await getData(false) + + if (config.site_uv) { + const siteUV = document.getElementById('umami-site-uv') + if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') { + siteUV.textContent = data.visitors.value + } 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 && typeof data.pageviews.value !== 'undefined') { + sitePV.textContent = data.pageviews.value + } else if (sitePV) { + console.warn('Umami Analytics: Invalid site PV data received') + } + } + } + } catch (error) { + console.error('Umami Analytics: Failed to insert data', error) } } btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track') btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') + loadUmamiJS() - insertData() + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', insertData) + } else { + setTimeout(insertData, 100) + } })() \ No newline at end of file diff --git a/package.json b/package.json index d94368c..8749c12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.5.0-b1", + "version": "5.5.0", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/plugins.yml b/plugins.yml index 9984a9a..8724ac0 100644 --- a/plugins.yml +++ b/plugins.yml @@ -1,15 +1,15 @@ abcjs_basic_js: name: abcjs file: dist/abcjs-basic-min.js - version: 6.5.1 + version: 6.5.2 activate_power_mode: name: butterfly-extsrc file: dist/activate-power-mode.min.js - version: 1.1.4 + version: 1.1.6 algolia_search: name: algoliasearch file: dist/lite/builds/browser.umd.js - version: 5.34.1 + version: 5.37.0 aplayer_css: name: aplayer file: dist/APlayer.min.css @@ -33,15 +33,15 @@ blueimp_md5: canvas_fluttering_ribbon: name: butterfly-extsrc file: dist/canvas-fluttering-ribbon.min.js - version: 1.1.4 + version: 1.1.6 canvas_nest: name: butterfly-extsrc file: dist/canvas-nest.min.js - version: 1.1.4 + version: 1.1.6 canvas_ribbon: name: butterfly-extsrc file: dist/canvas-ribbon.min.js - version: 1.1.4 + version: 1.1.6 chartjs: name: chart.js file: dist/chart.umd.js @@ -49,11 +49,11 @@ chartjs: clickShowText: name: butterfly-extsrc file: dist/click-show-text.min.js - version: 1.1.4 + version: 1.1.6 click_heart: name: butterfly-extsrc file: dist/click-heart.min.js - version: 1.1.4 + version: 1.1.6 disqusjs: name: disqusjs file: dist/browser/disqusjs.es2015.umd.min.js @@ -80,22 +80,22 @@ egjs_infinitegrid: fancybox: name: '@fancyapps/ui' file: dist/fancybox/fancybox.umd.js - version: 6.0.17 + version: 6.0.29 other_name: fancyapps-ui fancybox_css: name: '@fancyapps/ui' file: dist/fancybox/fancybox.css - version: 6.0.17 + version: 6.0.29 other_name: fancyapps-ui fireworks: name: butterfly-extsrc file: dist/fireworks.min.js - version: 1.1.4 + version: 1.1.6 fontawesome: name: '@fortawesome/fontawesome-free' file: css/all.min.css other_name: font-awesome - version: 6.7.2 + version: 7.0.1 gitalk: name: gitalk file: dist/gitalk.min.js @@ -108,10 +108,6 @@ instantpage: name: instant.page file: instantpage.js version: 5.2.0 -instantsearch: - name: instantsearch.js - file: dist/instantsearch.production.min.js - version: 4.79.2 katex: name: katex file: dist/katex.min.css @@ -128,8 +124,8 @@ lazyload: version: 19.1.3 mathjax: name: mathjax - file: es5/tex-mml-chtml.js - version: 3.2.2 + file: tex-mml-chtml.js + version: 4.0.0 medium_zoom: name: medium-zoom file: dist/medium-zoom.min.js @@ -137,11 +133,11 @@ medium_zoom: mermaid: name: mermaid file: dist/mermaid.min.js - version: 11.9.0 + version: 11.11.0 meting_js: name: butterfly-extsrc file: metingjs/dist/Meting.min.js - version: 1.1.4 + version: 1.1.6 pace_default_css: name: pace-js other_name: pace @@ -174,11 +170,11 @@ prismjs_lineNumber_js: sharejs: name: butterfly-extsrc file: sharejs/dist/js/social-share.min.js - version: 1.1.4 + version: 1.1.6 sharejs_css: name: butterfly-extsrc file: sharejs/dist/css/share.min.css - version: 1.1.4 + version: 1.1.6 snackbar: name: node-snackbar file: dist/snackbar.min.js diff --git a/scripts/common/default_config.js b/scripts/common/default_config.js index 87238b7..b25dd02 100644 --- a/scripts/common/default_config.js +++ b/scripts/common/default_config.js @@ -274,6 +274,10 @@ module.exports = { preload: false, top_n_per_article: 1, unescape: false, + pagination: { + enable: false, + hitsPerPage: 8 + }, CDN: null }, docsearch: { diff --git a/scripts/events/init.js b/scripts/events/init.js index b7d6e63..e8dfadf 100644 --- a/scripts/events/init.js +++ b/scripts/events/init.js @@ -1,6 +1,5 @@ const { deepMerge } = require('hexo-util') const path = require('path') -const fs = require('fs') // Cache default config to avoid repeated file reads let cachedDefaultConfig = null diff --git a/scripts/filters/post_lazyload.js b/scripts/filters/post_lazyload.js index 98aa681..8687c86 100644 --- a/scripts/filters/post_lazyload.js +++ b/scripts/filters/post_lazyload.js @@ -1,7 +1,7 @@ /** * Butterfly - * lazyload - * replace src to data-lazy-src + * Lazyload filter + * Replace src with data-lazy-src for lazy loading */ 'use strict' @@ -10,11 +10,28 @@ const urlFor = require('hexo-util').url_for.bind(hexo) const lazyload = htmlContent => { if (hexo.theme.config.lazyload.native) { - return htmlContent.replace(/()/ig, '$1 loading=\'lazy\'$2') + // Use more precise replacement: only replace img tags in HTML, not content inside script tags + return htmlContent.replace(/(]*?\bloading=)(?:\s[^>]*?)?>)(?![^<]*<\/script>)/gi, match => { + return match.replace(/>$/, ' loading=\'lazy\'>') + }) } const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' - return htmlContent.replace(/(]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc="([^"]+)")(?![^<]*<\/script>)/gi, (match, tag, src) => { + return tag.replace(`src="${src}"`, `src="${bg}" data-lazy-src="${src}"`) + }) + + // Handle src attributes with single quotes + result = result.replace(/(]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc='([^']+)')(?![^<]*<\/script>)/gi, (match, tag, src) => { + return tag.replace(`src='${src}'`, `src='${bg}' data-lazy-src='${src}'`) + }) + + return result } hexo.extend.filter.register('after_render:html', data => { diff --git a/scripts/helpers/page.js b/scripts/helpers/page.js index 15a29c8..bf5e800 100644 --- a/scripts/helpers/page.js +++ b/scripts/helpers/page.js @@ -53,7 +53,7 @@ hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = }) hexo.extend.helper.register('md5', function (path) { - return crypto.createHash('md5').update(decodeURI(this.url_for(path, {relative: false}))).digest('hex') + return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex') }) hexo.extend.helper.register('injectHtml', data => { @@ -70,7 +70,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) { const defaultTitle = this._p('page.archives') if (!menu) return defaultTitle - const loop = (m) => { + const loop = m => { for (const key in m) { if (typeof m[key] === 'object') { const result = loop(m[key]) @@ -86,7 +86,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) { return loop(menu) || defaultTitle }) -hexo.extend.helper.register('getBgPath', function(path) { +hexo.extend.helper.register('getBgPath', function (path) { if (!path) return '' const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i @@ -132,6 +132,8 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => { finalResult.forEach(item => { const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss') + // markdown + item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' }) }) return finalResult diff --git a/source/css/_layout/aside.styl b/source/css/_layout/aside.styl index abb779d..4e534c4 100644 --- a/source/css/_layout/aside.styl +++ b/source/css/_layout/aside.styl @@ -181,6 +181,11 @@ .card-category-list &.child padding: 0 0 0 16px + overflow: hidden + max-height: 0 + opacity: 0 + visibility: hidden + transition: max-height .3s ease, opacity .3s ease > .parent > a @@ -189,7 +194,9 @@ transform: rotate(-90deg) & + .child - display: block + max-height: 1000px + opacity: 1 + visibility: visible .card-category-list &-name @@ -208,7 +215,9 @@ if hexo-config('aside.card_categories.expand') == false > .child - display: none + max-height: 0 + opacity: 0 + visibility: hidden .card-webinfo .webinfo diff --git a/source/css/_layout/rightside.styl b/source/css/_layout/rightside.styl index 9ebb741..7b54840 100644 --- a/source/css/_layout/rightside.styl +++ b/source/css/_layout/rightside.styl @@ -38,9 +38,13 @@ font-size: 16px line-height: w addBorderRadius(5) + @extend .btn-effects &:hover background-color: var(--btn-hover-color) + + i + vertical-align: baseline #mobile-toc-button display: none diff --git a/source/css/_layout/sidebar.styl b/source/css/_layout/sidebar.styl index e96faa0..3e730fb 100644 --- a/source/css/_layout/sidebar.styl +++ b/source/css/_layout/sidebar.styl @@ -52,7 +52,10 @@ &:hover background: var(--text-bg-hover) + box-shadow: 0 2px 8px rgba(0, 0, 0, .1) color: var(--white) + transition: all .2s ease + transform: translateX(3px) i:first-child width: 15% @@ -70,9 +73,25 @@ transform: rotate(90deg) & + .menus_item_child - display: none + overflow: hidden + max-height: 0 + opacity: 0 + transform: scaleY(0) + transform-origin: top .menus_item_child margin: 0 padding-left: 25px - list-style: none \ No newline at end of file + max-height: 0 + list-style: none + opacity: 0 + transition: transform .3s ease, opacity .3s ease, max-height .3s ease + transform: scaleY(0) + transform-origin: top + will-change: transform, opacity, max-height + + // 當父元素沒有 .hide 類時,顯示子目錄 + .site-page.group:not(.hide) + .menus_item_child + max-height: 1000px + opacity: 1 + transform: scaleY(1) \ No newline at end of file diff --git a/source/css/_page/common.styl b/source/css/_page/common.styl index 008270e..d0bb7f5 100644 --- a/source/css/_page/common.styl +++ b/source/css/_page/common.styl @@ -18,7 +18,7 @@ padding: 20px 5px +minWidth2000() - max-width: 70% + max-width: 60% & > div:first-child:not(.nc) @extend .cardHover diff --git a/source/css/_page/shuoshuo.styl b/source/css/_page/shuoshuo.styl index 7370739..71ea948 100644 --- a/source/css/_page/shuoshuo.styl +++ b/source/css/_page/shuoshuo.styl @@ -76,3 +76,110 @@ &.no-comment display: none + +.shuoshuo-navigation + display: flex + justify-content: center + align-items: center + margin-top: 20px + padding: 20px 0 + + button + display: flex + justify-content: center + align-items: center + width: 2.7em + height: 2.7em + background-color: var(--btn-bg) + color: var(--btn-color) + font-size: .9em + line-height: 2.5em + transition: all .2s ease-in-out + addBorderRadius(6) + + &:not(:disabled) + @extend .btn-effects + + &:hover:not(:disabled) + background-color: var(--btn-hover-color) + + &:disabled + background: #f5f5f5 + color: #ccc + opacity: .5 + cursor: not-allowed + + .shuoshuo-page-info + margin: 0 15px + color: #858585 + white-space: nowrap + font-size: .9em + + .shuoshuo-page-input + margin-right: 12px + padding: 0 15px + height: 2.7em + border: 1px solid var(--btn-bg) + background: var(--card-bg) + color: #858585 + text-align: center + font-size: .9em + transition: all .2s ease-in-out + addBorderRadius(6) + + &:focus + outline: none + border-width: 2px + + &::placeholder + color: transparent + + /* 隱藏 number 輸入框的上下箭頭 */ + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button + margin: 0 + -webkit-appearance: none + + /* Firefox */ + -moz-appearance: textfield + + /* 當作為頁碼按鈕時的樣式 */ + &.shuoshuo-page-num + min-width: 40px + width: 40px + border: none + background: $light-blue + color: var(--white) + font-weight: 500 + cursor: text + + &:focus + border: 1px solid $light-blue + background: var(--white) + color: #333 + + /* 超出範圍時的紅色樣式 */ + &.invalid + border-color: #ff4757 + background-color: #ffeaea + color: #ff4757 + animation: shake .5s ease-in-out + + /* 震動動畫 */ + @keyframes shake + 0%, + 100% + transform: translateX(0) + + 10%, + 30%, + 50%, + 70%, + 90% + transform: translateX(-2px) + + 20%, + 40%, + 60%, + 80% + transform: translateX(2px) diff --git a/source/css/_search/algolia.styl b/source/css/_search/algolia.styl index 3c27f7f..8f45471 100644 --- a/source/css/_search/algolia.styl +++ b/source/css/_search/algolia.styl @@ -1,93 +1,13 @@ #algolia-search .search-dialog - .ais-SearchBox - input - padding: 5px 14px - width: 100% - outline: none - border: 2px solid $search-color - border-radius: 40px - background: var(--search-bg) - color: var(--search-input-color) - - .ais-SearchBox-loadingIndicator - position: absolute - top: 18px - left: 67px - .ais-Hits-list - margin: 0 - padding: 0 - @extend .list-beauty - - a - color: var(--search-a-color) - - &:hover - color: $search-color - - mark - background: transparent - color: $search-keyword-highlight - font-weight: bold - - .algolia-hits-item-title - font-weight: 600 - - .algolia-hit-item-content - margin: 0 0 8px - word-break: break-word - - .ais-Pagination - margin: 15px 0 0 - padding: 0 - text-align: center - - .ais-Pagination-list - margin: 0 - padding: 0 - list-style: none - - .ais-Pagination-item - display: inline - margin: 0 4px - padding: 0 - - .ais-Pagination-link - display: inline-block - min-width: 24px - height: 24px - text-align: center - line-height: 24px - addBorderRadius() - - .ais-Pagination-item--selected - a - background: $theme-paginator-color - color: #eee - cursor: default - - .ais-Pagination-item--disabled - visibility: hidden - - #algolia-hits - > div - overflow-y: overlay - margin: 0 -20px - padding: 0 22px - max-height: calc(80vh - 220px) - - +maxWidth768() - max-height: none - height: calc(var(--search-height) - 235px) + +maxWidth768() + min-height: calc(var(--search-height) - 245px) #algolia-info - div - display: inline - .algolia-poweredBy float: right - vertical-align: text-top + padding-top: 2px svg height: 1.1em \ No newline at end of file diff --git a/source/css/_search/index.styl b/source/css/_search/index.styl index 9dba10e..eb45f90 100644 --- a/source/css/_search/index.styl +++ b/source/css/_search/index.styl @@ -20,22 +20,173 @@ border-radius: 0 .search-nav - margin: 0 0 14px + display: flex + justify-content: space-between + align-items: center + margin-bottom: 14px color: $search-color font-size: 1.4em line-height: 1 .search-dialog-title - margin-right: 10px + margin-right: 4px + + #loading-status + &[hidden] + display: none !important .search-close-button - float: right + flex: 1 color: $grey - transition: color .2s ease-in-out + text-align: right + transition: all .2s ease &:hover color: $search-color - + + .local-search-input, + #algolia-search-input + margin: 0 auto + max-width: 100% + width: 100% + + input, + .ais-SearchBox-input + padding: 5px 14px + width: 100% + outline: none + border: 2px solid $search-color + border-radius: 40px + background: var(--search-bg) + color: var(--search-input-color) + -webkit-appearance: none + + &::placeholder + color: var(--text-color) + + .search-result-list, + .ais-Hits-list + overflow-y: overlay + margin: 0 -20px + padding: 0 22px + max-height: calc(80vh - 220px) + + .local-search-hit-item, + .ais-Hits-item + display: flex + align-items: flex-start + margin: 3px 0 + line-height: 1.8 + transition: all .2s ease-in-out + + &:hover + transform: translateY(-1px) + + &:not([value])::before + display: none + + &[value]::before + display: inline-flex + flex-shrink: 0 + justify-content: center + align-items: center + margin-right: 6px + min-width: 24px + color: $search-color + content: attr(value) '.' + font-weight: bold + font-style: italic + font-size: .9em + + &::marker + content: none + + a + flex: 1 + color: var(--search-a-color) + + &:hover + color: $search-color + + .search-result-title, + .algolia-hits-item-title + font-weight: 600 + + .search-result, + .algolia-hit-item-content + margin: 0 0 8px + word-break: break-all + font-size: .9em + + .ais-Pagination + margin: 15px 0 0 + padding: 0 + text-align: center + + .ais-Pagination-list + display: flex + flex-wrap: wrap + justify-content: center + align-items: center + margin: 0 + padding: 0 + list-style: none + gap: 6px + + .ais-Pagination-item + display: flex + padding: 0 + + &:not(.ais-Pagination-item--selected):not(.ais-Pagination-item--ellipsis):not(.ais-Pagination-item--disabled) + .ais-Pagination-link:hover + background: var(--btn-hover-color) + transform: translateY(-1px) + + .ais-Pagination-link + display: inline-flex + justify-content: center + align-items: center + padding: 4px 8px + min-width: 28px + height: 28px + border-radius: 6px + background: var(--btn-bg) + color: var(--btn-color) + transition: all .2s ease + + &.ais-Pagination-link--disabled + opacity: .3 + cursor: not-allowed + + i + font-size: 12px + + .ais-Pagination-item--selected + .ais-Pagination-link + background: $theme-paginator-color + font-weight: 600 + cursor: default + + .ais-Pagination-item--ellipsis + .ais-Pagination-link + padding: 4px 2px + border: none + background: transparent + color: var(--text-color) + cursor: default + + &:hover + background: transparent + transform: none + + .ais-Pagination-item--disabled + .ais-Pagination-link + opacity: .4 + + +maxWidth768() + .ais-Pagination-list + gap: 4px + hr margin: 15px auto @extend .custom-hr @@ -50,6 +201,38 @@ display: none background: rgba($dark-black, .6) +.search-result-stats, +.ais-Stats-text + margin: 15px 0 0 + color: var(--text-color) + text-align: center + font-size: .9em + +.search-keyword + background: transparent + color: $search-keyword-highlight + font-weight: 600 + +.search-loading + display: flex + justify-content: center + align-items: center + padding: 20px + color: var(--text-color) + + &::before + width: 16px + height: 16px + border: 2px solid var(--text-color) + border-top-color: transparent + border-radius: 50% + content: '' + animation: spin 1s linear infinite + +@keyframes spin + to + transform: rotate(360deg) + if hexo-config('search.use') == 'algolia_search' @require 'algolia' else if hexo-config('search.use') == 'local_search' diff --git a/source/css/_search/local-search.styl b/source/css/_search/local-search.styl index 3e82c15..618f251 100644 --- a/source/css/_search/local-search.styl +++ b/source/css/_search/local-search.styl @@ -1,57 +1,18 @@ #local-search .search-dialog - .local-search-box - margin: 0 auto - max-width: 100% - width: 100% - - input - padding: 5px 14px - width: 100% - outline: none - border: 2px solid $search-color - border-radius: 40px - background: var(--search-bg) - color: var(--search-input-color) - -webkit-appearance: none - - .search-wrap - display: none - - .local-search-hit-item - margin-left: 24px - padding-left: 3px - line-height: 1.8 - - &::marker - color: $search-color - font-weight: bold - font-style: italic - - a - color: var(--search-a-color) - - &:hover - color: $search-color - - .search-result-title - font-weight: 600 - - .search-result - margin: 0 0 8px - word-break: break-all - font-size: .9em - .search-result-list - overflow-y: overlay - margin: 0 -20px - padding: 0 22px - max-height: calc(80vh - 180px) - +maxWidth768() - max-height: calc(var(--search-height) - 190px) !important + if hexo-config('search.local_search.pagination.enable') + min-height: calc(var(--search-height) - 255px) !important + else + max-height: calc(var(--search-height) - 200px) !important -.search-keyword - background: transparent - color: $search-keyword-highlight - font-weight: 600 \ No newline at end of file + #local-search-stats + .search-result-stats + text-align: left + + .search-keyword + font-weight: 600 + + #loading-database ~ * + visibility: hidden \ No newline at end of file diff --git a/source/js/main.js b/source/js/main.js index 66482d0..ec9d3e5 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -698,7 +698,7 @@ document.addEventListener('DOMContentLoaded', () => { const addCopyright = () => { const { limitCount, languages } = GLOBAL_CONFIG.copyright - const handleCopy = (e) => { + const handleCopy = e => { e.preventDefault() const copyFont = window.getSelection(0).toString() let textFont = copyFont diff --git a/source/js/search/algolia.js b/source/js/search/algolia.js index af4e0df..684e25f 100644 --- a/source/js/search/algolia.js +++ b/source/js/search/algolia.js @@ -26,7 +26,12 @@ window.addEventListener('load', () => { const openSearch = () => { btf.overflowPaddingR.add() animateElements(true) - setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100) + showLoading(false) + + setTimeout(() => { + const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input') + if (searchInput) searchInput.focus() + }, 100) const handleEscape = event => { if (event.code === 'Escape') { @@ -55,9 +60,38 @@ window.addEventListener('load', () => { document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch) } - const cutContent = (content) => { + const cutContent = content => { if (!content) return '' - const firstOccur = content.indexOf('') + + let contentStr = '' + if (typeof content === 'string') { + contentStr = content.trim() + } else if (typeof content === 'object') { + if (content.value !== undefined) { + contentStr = String(content.value).trim() + if (!contentStr) return '' + } else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) { + return '' + } else { + try { + contentStr = JSON.stringify(content).trim() + if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') { + return '' + } + } catch (e) { + return '' + } + } + } else if (content.toString && typeof content.toString === 'function') { + contentStr = content.toString().trim() + if (contentStr === '[object Object]' || contentStr === '[object Array]') { + return '' + } + } else { + return '' + } + + const firstOccur = contentStr.indexOf('') let start = firstOccur - 30 let end = firstOccur + 120 let pre = '' @@ -70,94 +104,454 @@ window.addEventListener('load', () => { pre = '...' } - if (end > content.length) { - end = content.length + if (end > contentStr.length) { + end = contentStr.length } else { post = '...' } - return `${pre}${content.substring(start, end)}${post}` + // Ensure we don't cut off HTML tags in the middle + let substr = contentStr.substring(start, end) + + // Handle tag completeness + // Check for incomplete opening tags at the beginning + const firstCloseBracket = substr.indexOf('>') + const firstOpenBracket = substr.indexOf('<') + + // If there's a closing bracket but no opening bracket before it, we've cut a tag + if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) { + substr = substr.substring(firstCloseBracket + 1) + } + + // Check for incomplete closing tags at the end + const lastOpenBracket = substr.lastIndexOf('<') + const lastCloseBracket = substr.lastIndexOf('>') + + // If there's an opening bracket after the last closing bracket, we've cut a tag + if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) { + substr = substr.substring(0, lastOpenBracket) + } + + // Balance tags in the substring + const tagStack = [] + let balancedStr = '' + let i = 0 + + while (i < substr.length) { + if (substr[i] === '<') { + // Check if it's a closing tag + if (substr[i + 1] === '/') { + const closeTagEnd = substr.indexOf('>', i) + if (closeTagEnd !== -1) { + const closeTagName = substr.substring(i + 2, closeTagEnd) + // Remove matching opening tag from stack + for (let j = tagStack.length - 1; j >= 0; j--) { + if (tagStack[j] === closeTagName) { + tagStack.splice(j, 1) + break + } + } + balancedStr += substr.substring(i, closeTagEnd + 1) + i = closeTagEnd + 1 + continue + } + } else if (substr.substr(i, 2) === '', i) !== -1 && substr.indexOf('/>', i) < substr.indexOf('>', i))) { + const tagEnd = substr.indexOf('>', i) + if (tagEnd !== -1) { + balancedStr += substr.substring(i, tagEnd + 1) + i = tagEnd + 1 + continue + } + } else { + const tagEnd = substr.indexOf('>', i) + if (tagEnd !== -1) { + const tagName = substr.substring(i + 1, (substr.indexOf(' ', i) > -1 && substr.indexOf(' ', i) < tagEnd) + ? substr.indexOf(' ', i) + : tagEnd).split(/\s/)[0] + tagStack.push(tagName) + balancedStr += substr.substring(i, tagEnd + 1) + i = tagEnd + 1 + continue + } + } + } + balancedStr += substr[i] + i++ + } + + // Close any unclosed tags + while (tagStack.length > 0) { + const tagName = tagStack.pop() + balancedStr += `` + } + + // If we removed content from the beginning, add prefix + if (start > 0 || pre) { + const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0) + const actualFirstMark = contentStr.indexOf('', start > 0 ? start - 30 : 0) + + if (actualFirstOpenBracket !== -1 && + (actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) { + pre = '...' + } + } + + substr = balancedStr + return `${pre}${substr}${post}` } - const disableDiv = [ - document.getElementById('algolia-hits'), - document.getElementById('algolia-pagination'), - document.querySelector('#algolia-info .algolia-stats') - ] + // Helper function to handle Algolia highlight results + const extractHighlightValue = highlightObj => { + if (!highlightObj) return '' - const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient - const search = instantsearch({ - indexName, - searchClient: searchClient(appId, apiKey), - searchFunction (helper) { - disableDiv.forEach(item => { - item.style.display = helper.state.query ? '' : 'none' - }) - if (helper.state.query) helper.search() + if (typeof highlightObj === 'string') { + return highlightObj.trim() } - }) - const widgets = [ - instantsearch.widgets.configure({ hitsPerPage }), - instantsearch.widgets.searchBox({ - container: '#algolia-search-input', - showReset: false, - showSubmit: false, - placeholder: languages.input_placeholder, - showLoadingIndicator: true - }), - instantsearch.widgets.hits({ - container: '#algolia-hits', - templates: { - item (data) { - const link = data.permalink || (GLOBAL_CONFIG.root + data.path) - const result = data._highlightResult - const content = result.contentStripTruncate - ? cutContent(result.contentStripTruncate.value) - : result.contentStrip - ? cutContent(result.contentStrip.value) - : result.content - ? cutContent(result.content.value) - : '' - return ` - - ${result.title.value || 'no-title'} - ${content ? `
${content}
` : ''} -
` - }, - empty (data) { - return `
${languages.hits_empty.replace(/\$\{query}/, data.query)}
` + if (typeof highlightObj === 'object' && highlightObj.value !== undefined) { + return String(highlightObj.value).trim() + } + + return '' + } + + // Initialize Algolia client + let searchClient + + if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') { + searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey) + } else if (typeof window.algoliasearch === 'function') { + searchClient = window.algoliasearch(appId, apiKey) + } else { + return console.error('Algolia search client not found!') + } + + if (!searchClient) { + return console.error('Failed to initialize Algolia search client') + } + + // Search state + let currentQuery = '' + + // Show loading state + const showLoading = show => { + const loadingIndicator = document.getElementById('loading-status') + if (loadingIndicator) { + loadingIndicator.hidden = !show + } + } + + // Cache frequently used elements + const elements = { + get searchInput () { return document.querySelector('#algolia-search-input .ais-SearchBox-input') }, + get hits () { return document.getElementById('algolia-hits') }, + get hitsEmpty () { return document.getElementById('algolia-hits-empty') }, + get hitsList () { return document.querySelector('#algolia-hits .ais-Hits-list') }, + get hitsWrapper () { return document.querySelector('#algolia-hits .ais-Hits') }, + get pagination () { return document.getElementById('algolia-pagination') }, + get paginationList () { return document.querySelector('#algolia-pagination .ais-Pagination-list') }, + get stats () { return document.querySelector('#algolia-info .ais-Stats-text') }, + } + + // Show/hide search results area + const toggleResultsVisibility = hasResults => { + elements.pagination.style.display = hasResults ? '' : 'none' + elements.stats.style.display = hasResults ? '' : 'none' + } + + // Render search results + const renderHits = (hits, query, page = 0) => { + if (hits.length === 0 && query) { + elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query) + elements.hitsEmpty.style.display = '' + elements.hitsWrapper.style.display = 'none' + elements.stats.style.display = 'none' + return + } + + elements.hitsEmpty.style.display = 'none' + + const hitsHTML = hits.map((hit, index) => { + const itemNumber = page * hitsPerPage + index + 1 + const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path) + const result = hit._highlightResult || hit + + // Content extraction + let content = '' + try { + if (result.contentStripTruncate) { + content = cutContent(result.contentStripTruncate) + } else if (result.contentStrip) { + content = cutContent(result.contentStrip) + } else if (result.content) { + content = cutContent(result.content) + } else if (hit.contentStripTruncate) { + content = cutContent(hit.contentStripTruncate) + } else if (hit.contentStrip) { + content = cutContent(hit.contentStrip) + } else if (hit.content) { + content = cutContent(hit.content) } + } catch (error) { + content = '' } - }), - instantsearch.widgets.stats({ - container: '#algolia-info > .algolia-stats', - templates: { - text (data) { - const stats = languages.hits_stats - .replace(/\$\{hits}/, data.nbHits) - .replace(/\$\{time}/, data.processingTimeMS) - return `
${stats}` + + // Title handling + let title = 'no-title' + try { + if (result.title) { + title = extractHighlightValue(result.title) || 'no-title' + } else if (hit.title) { + title = extractHighlightValue(hit.title) || 'no-title' } + + if (!title || title === 'no-title') { + if (typeof hit.title === 'string' && hit.title.trim()) { + title = hit.title.trim() + } else if (hit.title && typeof hit.title === 'object' && hit.title.value) { + title = String(hit.title.value).trim() || 'no-title' + } else { + title = 'no-title' + } + } + } catch (error) { + title = 'no-title' } - }), - instantsearch.widgets.poweredBy({ - container: '#algolia-info > .algolia-poweredBy' - }), - instantsearch.widgets.pagination({ - container: '#algolia-pagination', - totalPages: 5, - templates: { - first: '', - last: '', - previous: '', - next: '' + + return ` +
  • + + ${title} + ${content ? `
    ${content}
    ` : ''} +
    +
  • ` + }).join('') + + elements.hitsList.innerHTML = hitsHTML + elements.hitsWrapper.style.display = query ? '' : 'none' + + if (hits.length > 0) { + elements.stats.style.display = '' + } + } + + // Render pagination + const renderPagination = (page, nbPages) => { + if (nbPages <= 1) { + elements.pagination.style.display = 'none' + elements.paginationList.innerHTML = '' + return + } + + elements.pagination.style.display = 'block' + + const isFirstPage = page === 0 + const isLastPage = page === nbPages - 1 + + // Responsive page display + const isMobile = window.innerWidth < 768 + const maxVisiblePages = isMobile ? 3 : 5 + let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2)) + const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1) + + // Adjust starting page to maintain max visible pages + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(0, endPage - maxVisiblePages + 1) + } + + let pagesHTML = '' + + // Only add ellipsis and first page when there are many pages + if (nbPages > maxVisiblePages && startPage > 0) { + pagesHTML += ` +
  • + 1 +
  • ` + if (startPage > 1) { + pagesHTML += ` +
  • + ... +
  • ` + } + } + + // Add middle page numbers + for (let i = startPage; i <= endPage; i++) { + const isSelected = i === page + if (isSelected) { + pagesHTML += ` +
  • + ${i + 1} +
  • ` + } else { + pagesHTML += ` +
  • + ${i + 1} +
  • ` + } + } + + // Only add ellipsis and last page when there are many pages + if (nbPages > maxVisiblePages && endPage < nbPages - 1) { + if (endPage < nbPages - 2) { + pagesHTML += ` +
  • + ... +
  • ` + } + pagesHTML += ` +
  • + ${nbPages} +
  • ` + } + + if (nbPages > 1) { + elements.paginationList.innerHTML = ` +
  • + ${isFirstPage + ? '' + : `` + } +
  • + ${pagesHTML} +
  • + ${isLastPage + ? '' + : `` + } +
  • ` + elements.pagination.style.display = currentQuery ? '' : 'none' + } else { + elements.pagination.style.display = 'none' + } + } + + // Render statistics + const renderStats = (nbHits, processingTimeMS, query) => { + if (query) { + const stats = languages.hits_stats + .replace(/\$\{hits}/, nbHits) + .replace(/\$\{time}/, processingTimeMS) + elements.stats.innerHTML = `
    ${stats}` + elements.stats.style.display = '' + } else { + elements.stats.style.display = 'none' + } + } + + // Perform search + const performSearch = async (query, page = 0) => { + if (!query.trim()) { + currentQuery = '' + renderHits([], '', 0) + renderPagination(0, 0) + renderStats(0, 0, '') + toggleResultsVisibility(false) + return + } + + showLoading(true) + currentQuery = query + + try { + let result + + if (searchClient && typeof searchClient.search === 'function') { + // v5 multi-index search + const searchResult = await searchClient.search([{ + indexName, + query, + params: { + page, + hitsPerPage, + highlightPreTag: '', + highlightPostTag: '', + attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate'] + } + }]) + result = searchResult.results[0] + } else if (searchClient && typeof searchClient.initIndex === 'function') { + // v4 single-index search + const index = searchClient.initIndex(indexName) + result = await index.search(query, { + page, + hitsPerPage, + highlightPreTag: '', + highlightPostTag: '', + attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate'] + }) + } else { + throw new Error('Algolia: No compatible search method available') + } + + renderHits(result.hits || [], query, page) + + const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0) + renderPagination(page, actualNbPages) + renderStats(result.nbHits || 0, result.processingTimeMS || 0, query) + + const hasResults = result.hits && result.hits.length > 0 + toggleResultsVisibility(hasResults) + + // Refresh Pjax links + if (window.pjax) { + window.pjax.refresh(document.getElementById('algolia-hits')) + } + } catch (error) { + console.error('Algolia search error:', error) + renderHits([], query, page) + renderPagination(0, 0) + renderStats(0, 0, query) + } finally { + showLoading(false) + } + } + + // Debounced search + let searchTimeout + const debouncedSearch = (query, delay = 300) => { + clearTimeout(searchTimeout) + searchTimeout = setTimeout(() => performSearch(query), delay) + } + + // Initialize search box and events + const initializeSearch = () => { + showLoading(false) + + if (elements.searchInput) { + elements.searchInput.addEventListener('input', e => { + const query = e.target.value + debouncedSearch(query) + }) + } + + const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form') + if (searchForm) { + searchForm.addEventListener('submit', e => { + e.preventDefault() + const query = elements.searchInput.value + performSearch(query) + }) + } + + // Pagination event delegation + elements.pagination.addEventListener('click', e => { + e.preventDefault() + const link = e.target.closest('a[data-page]') + if (link) { + const page = parseInt(link.dataset.page, 10) + if (!isNaN(page) && currentQuery) { + performSearch(currentQuery, page) + } } }) - ] - search.addWidgets(widgets) - search.start() + // Initial state + toggleResultsVisibility(false) + } + + // Initialize + initializeSearch() searchClickFn() searchFnOnce() @@ -165,10 +559,4 @@ window.addEventListener('load', () => { if (!btf.isHidden($searchMask)) closeSearch() searchClickFn() }) - - if (window.pjax) { - search.on('render', () => { - window.pjax.refresh(document.getElementById('algolia-hits')) - }) - } }) diff --git a/source/js/search/local-search.js b/source/js/search/local-search.js index 08575e6..68d3654 100644 --- a/source/js/search/local-search.js +++ b/source/js/search/local-search.js @@ -236,40 +236,229 @@ class LocalSearch { window.addEventListener('load', () => { // Search - const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch + const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch + const enablePagination = pagination && pagination.enable const localSearch = new LocalSearch({ path, top_n_per_article, unescape }) - const input = document.querySelector('#local-search-input input') - const statsItem = document.getElementById('local-search-stats-wrap') + const input = document.querySelector('.local-search-input input') + const statsItem = document.getElementById('local-search-stats') const $loadingStatus = document.getElementById('loading-status') const isXml = !path.endsWith('json') + // Pagination variables (only initialize if pagination is enabled) + let currentPage = 0 + const hitsPerPage = pagination.hitsPerPage || 10 + + let currentResultItems = [] + + if (!enablePagination) { + // If pagination is disabled, we don't need these variables + currentPage = undefined + currentResultItems = undefined + } + + // Cache frequently used elements + const elements = { + get pagination () { return document.getElementById('local-search-pagination') }, + get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') } + } + + // Show/hide search results area + const toggleResultsVisibility = hasResults => { + if (enablePagination) { + elements.pagination.style.display = hasResults ? '' : 'none' + } else { + elements.pagination.style.display = 'none' + } + } + + // Render search results for current page + const renderResults = (searchText, resultItems) => { + const container = document.getElementById('local-search-results') + + // Determine items to display based on pagination mode + const itemsToDisplay = enablePagination + ? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage) + : resultItems + + // Handle empty page in pagination mode + if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) { + currentPage = 0 + renderResults(searchText, resultItems) + return + } + + // Add numbering to items + const numberedItems = itemsToDisplay.map((result, index) => { + const itemNumber = enablePagination + ? currentPage * hitsPerPage + index + 1 + : index + 1 + return result.item.replace( + '
  • ', + `
  • ` + ) + }) + + container.innerHTML = `
      ${numberedItems.join('')}
    ` + + // Update stats + const displayCount = enablePagination ? currentResultItems.length : resultItems.length + const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount) + statsItem.innerHTML = `
    ${stats}
    ` + + // Handle pagination + if (enablePagination) { + const nbPages = Math.ceil(currentResultItems.length / hitsPerPage) + renderPagination(currentPage, nbPages, searchText) + } + + const hasResults = resultItems.length > 0 + toggleResultsVisibility(hasResults) + + window.pjax && window.pjax.refresh(container) + } + + // Render pagination + const renderPagination = (page, nbPages, query) => { + if (nbPages <= 1) { + elements.pagination.style.display = 'none' + elements.paginationList.innerHTML = '' + return + } + + elements.pagination.style.display = 'block' + + const isFirstPage = page === 0 + const isLastPage = page === nbPages - 1 + + // Responsive page display + const isMobile = window.innerWidth < 768 + const maxVisiblePages = isMobile ? 3 : 5 + let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2)) + const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1) + + // Adjust starting page to maintain max visible pages + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(0, endPage - maxVisiblePages + 1) + } + + let pagesHTML = '' + + // Only add ellipsis and first page when there are many pages + if (nbPages > maxVisiblePages && startPage > 0) { + pagesHTML += ` +
  • + 1 +
  • ` + if (startPage > 1) { + pagesHTML += ` +
  • + ... +
  • ` + } + } + + // Add middle page numbers + for (let i = startPage; i <= endPage; i++) { + const isSelected = i === page + if (isSelected) { + pagesHTML += ` +
  • + ${i + 1} +
  • ` + } else { + pagesHTML += ` +
  • + ${i + 1} +
  • ` + } + } + + // Only add ellipsis and last page when there are many pages + if (nbPages > maxVisiblePages && endPage < nbPages - 1) { + if (endPage < nbPages - 2) { + pagesHTML += ` +
  • + ... +
  • ` + } + pagesHTML += ` +
  • + ${nbPages} +
  • ` + } + + if (nbPages > 1) { + elements.paginationList.innerHTML = ` +
  • + ${isFirstPage + ? '' + : `` + } +
  • + ${pagesHTML} +
  • + ${isLastPage + ? '' + : `` + } +
  • ` + } else { + elements.pagination.style.display = 'none' + } + } + + // Clear search results and stats + const clearSearchResults = () => { + const container = document.getElementById('local-search-results') + container.textContent = '' + statsItem.textContent = '' + toggleResultsVisibility(false) + if (enablePagination) { + currentResultItems = [] + currentPage = 0 + } + } + + // Show no results message + const showNoResults = searchText => { + const container = document.getElementById('local-search-results') + container.textContent = '' + const statsDiv = document.createElement('div') + statsDiv.className = 'search-result-stats' + statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText) + statsItem.innerHTML = statsDiv.outerHTML + toggleResultsVisibility(false) + if (enablePagination) { + currentResultItems = [] + currentPage = 0 + } + } + const inputEventFunction = () => { if (!localSearch.isfetched) return let searchText = input.value.trim().toLowerCase() isXml && (searchText = searchText.replace(//g, '>')) - if (searchText !== '') $loadingStatus.innerHTML = '' + + if (searchText !== '') $loadingStatus.hidden = false + const keywords = searchText.split(/[-\s]+/) - const container = document.getElementById('local-search-results') let resultItems = [] + if (searchText.length > 0) { - // Perform local searching resultItems = localSearch.getResultItems(keywords) } + if (keywords.length === 1 && keywords[0] === '') { - container.textContent = '' - statsItem.textContent = '' + clearSearchResults() } else if (resultItems.length === 0) { - container.textContent = '' - const statsDiv = document.createElement('div') - statsDiv.className = 'search-result-stats' - statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText) - statsItem.innerHTML = statsDiv.outerHTML + showNoResults(searchText) } else { + // Sort results by relevance resultItems.sort((left, right) => { if (left.includedCount !== right.includedCount) { return right.includedCount - left.includedCount @@ -279,14 +468,14 @@ window.addEventListener('load', () => { return right.id - left.id }) - const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length) - - container.innerHTML = `
      ${resultItems.map(result => result.item).join('')}
    ` - statsItem.innerHTML = `
    ${stats}
    ` - window.pjax && window.pjax.refresh(container) + if (enablePagination) { + currentResultItems = resultItems + currentPage = 0 + } + renderResults(searchText, resultItems) } - $loadingStatus.textContent = '' + $loadingStatus.hidden = true } let loadFlag = false @@ -340,11 +529,29 @@ window.addEventListener('load', () => { localSearch.fetchData() } localSearch.highlightSearchWords(document.getElementById('article-container')) + + // Pagination event delegation - only add if pagination is enabled + if (enablePagination) { + elements.pagination.addEventListener('click', e => { + e.preventDefault() + const link = e.target.closest('a[data-page]') + if (link) { + const page = parseInt(link.dataset.page, 10) + if (!isNaN(page) && currentResultItems.length > 0) { + currentPage = page + renderResults(input.value.trim().toLowerCase(), currentResultItems) + } + } + }) + } + + // Initial state + toggleResultsVisibility(false) } window.addEventListener('search:loaded', () => { const $loadDataItem = document.getElementById('loading-database') - $loadDataItem.nextElementSibling.style.display = 'block' + $loadDataItem.nextElementSibling.style.visibility = 'visible' $loadDataItem.remove() }) diff --git a/source/js/tw_cn.js b/source/js/tw_cn.js index 223015c..3a7cb32 100644 --- a/source/js/tw_cn.js +++ b/source/js/tw_cn.js @@ -12,14 +12,14 @@ document.addEventListener('DOMContentLoaded', () => { document.documentElement.lang = targetEncoding === 1 ? 'zh-TW' : 'zh-CN' } - const translateText = (txt) => { + const translateText = txt => { if (!txt) return '' if (currentEncoding === 1 && targetEncoding === 2) return Simplized(txt) if (currentEncoding === 2 && targetEncoding === 1) return Traditionalized(txt) return txt } - const translateBody = (fobj) => { + const translateBody = fobj => { const nodes = typeof fobj === 'object' ? fobj.childNodes : document.body.childNodes for (const node of nodes) { @@ -68,7 +68,7 @@ document.addEventListener('DOMContentLoaded', () => { const JTPYStr = () => '万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤唿啧啬啭啮啰啴啸喷喽喾嗫呵嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳熘爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬着睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾龙历志制一台皋准复猛钟注范签' const FTPYStr = () => '萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閑閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽龍歷誌製壹臺臯準復勐鐘註範籤' - const Traditionalized = (cc) => { + const Traditionalized = cc => { let str = '' const ss = JTPYStr() const tt = FTPYStr() @@ -80,7 +80,7 @@ document.addEventListener('DOMContentLoaded', () => { return str } - const Simplized = (cc) => { + const Simplized = cc => { let str = '' const ss = JTPYStr() const tt = FTPYStr() diff --git a/source/js/utils.js b/source/js/utils.js index 0ba1c38..d9367d0 100644 --- a/source/js/utils.js +++ b/source/js/utils.js @@ -106,7 +106,7 @@ loadComment: (dom, callback) => { if ('IntersectionObserver' in window) { - const observerItem = new IntersectionObserver((entries) => { + const observerItem = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { callback() observerItem.disconnect()