diff --git a/_config.butterfly.yml b/_config.butterfly.yml index 6f306b8..27cdef4 100644 --- a/_config.butterfly.yml +++ b/_config.butterfly.yml @@ -299,9 +299,6 @@ footer:
  • 说说 | 网站监控
  • -
  • - -
  • diff --git a/themes/butterfly/LICENSE b/themes/butterfly/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/themes/butterfly/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/themes/butterfly/README.md b/themes/butterfly/README.md new file mode 100644 index 0000000..a45b670 --- /dev/null +++ b/themes/butterfly/README.md @@ -0,0 +1,193 @@ +
    + 中文 +
    + +
    + +Butterfly Logo + +# hexo-theme-butterfly + +A modern, elegant and feature-rich theme for Hexo + +![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/master?color=%231ab1ad&label=master) +![dev version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev) +![npm version](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff) +![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83cd) +![license](https://img.shields.io/github/license/jerryc127/hexo-theme-butterfly?color=FF5531) +![GitHub stars](https://img.shields.io/github/stars/jerryc127/hexo-theme-butterfly?style=social) + +📢 **Demo**: [Butterfly Official](https://butterfly.js.org/) | [CrazyWong's Blog](https://blog.crazywong.com/) + +📖 **Documentation**: [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) | [中文文档](https://butterfly.js.org/posts/21cfbf15/) + +![Butterfly Theme Preview](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png) + +
    + +--- + +## 🚀 Quick Start + +### 💾 Installation + +#### Method 1: Git Installation (Recommended) + +> 💡 **Tip**: If GitHub access is slow in mainland China, you can use the [Gitee Mirror](https://gitee.com/immyw/hexo-theme-butterfly.git) + +Execute in your Hexo blog root directory: + +```bash +# Install stable version (recommended) +git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly +``` + +```bash +# Install development version (early access to new features) +git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly +``` + +#### Method 2: NPM Installation + +> ⚠️ **Note**: NPM installation only supports Hexo 5.0.0 and above + +```bash +npm install hexo-theme-butterfly +``` + +### ⚙️ Theme Configuration + +1. **Enable Theme**: Modify your Hexo configuration file `_config.yml`: + +```yaml +theme: butterfly +``` + +2. **Install Dependencies**: If you haven't installed pug and stylus renderers, please run: + +```bash +npm install hexo-renderer-pug hexo-renderer-stylus --save +``` + +## ✨ Theme Features + +### 🎨 Design Style +- [x] **Card-based Design** - Modern card-style layout +- [x] **Rounded/Square Design** - Customizable border styles +- [x] **Responsive Design** - Perfect adaptation to all screen sizes +- [x] **Two-column Layout** - Optimized reading experience +- [x] **Dark Mode** - Eye-friendly night mode + +### 📝 Content Features +- [x] **Multi-level Menu** - Support for secondary navigation menus +- [x] **Reading Mode** - Focused article reading experience +- [x] **TOC Navigation** - Desktop and mobile TOC support +- [x] **Word Count** - Display article word count and reading time +- [x] **Related Articles** - Smart recommendation of related content +- [x] **Outdated Reminder** - Automatic article update status alerts +- [x] **Traditional/Simplified Chinese** - Support for Traditional and Simplified Chinese switching +- [x] **Tag Plugins** - Rich tag plugin support + +### 🔍 Search & Navigation +- [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch +- [x] **Built-in 404** - Beautiful 404 error page +- [x] **Pjax Support** - Smooth page transition experience + +### 🎨 Code Display +- [x] **Syntax Highlighting** - Built-in multiple themes (darker/pale night/light/ocean) +- [x] **Code Features** - Language display/fold expand/copy button/auto-wrap +- [x] **Math Formulas** - Support for Mathjax and Katex + +### 💬 Social Interaction +- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc. +- [x] **Dual Comments Support** - Enable two comment systems simultaneously +- [x] **Share Features** - Sharejs/Addtoany sharing components +- [x] **Live Chat** - Chatra/Tidio/Crisp instant messaging + +### 📊 Analytics & Statistics +- [x] **Visit Statistics** - Busuanzi counter +- [x] **Site Analytics** - Google Analytics/Baidu Analytics/Cloudflare Analytics/Microsoft Clarity/Umami +- [x] **Webmaster Verification** - Major search engine verification +- [x] **Ad Support** - Google AdSense/custom ad slots + +### 🎪 Visual Effects +- [x] **Typing Effects** - activate_power_mode animations +- [x] **Background Effects** - Static ribbons/dynamic ribbons/floating ribbons/Canvas Nest +- [x] **Mouse Effects** - Fireworks/hearts/text click effects +- [x] **Loading Animations** - Preloader and pace.js progress bars +- [x] **Image Effects** - Medium Zoom/Fancybox image lightbox +- [x] **Lazy Loading** - Image lazy loading optimization + +### 🛠️ Advanced Features +- [x] **PWA Support** - Progressive Web App +- [x] **Copy Protection** - Disable text copying/copyright info append +- [x] **Theme Customization** - Custom site color schemes +- [x] **Chart Support** - Mermaid flowcharts/Chart.js data charts +- [x] **Music Notation** - ABCJS music notation support +- [x] **Music Player** - APlayer/Meting music playback +- [x] **Article Series** - Series article organization +- [x] **Instantpage** - Page preloading acceleration +- [x] **Snackbar** - Elegant notification messages + +## 🤝 Contributors + +Thanks to all the developers who have contributed to the Butterfly theme! + +[![Contributors](https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly)](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors) + +## 📸 Screenshots + +
    + +![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg) + +![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg) + +![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg) + +![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg) + +
    + + +## ⭐ Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=jerryc127/hexo-theme-butterfly&type=Date)](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date) + +## 🤝 Building a Better Theme Together + +We believe **the power of open source comes from everyone's participation**! Whether you're a developer, designer, or user, you can contribute to the development of the Butterfly theme. + +### 💬 Get Help & Support + +- 🐛 **Found a bug?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - Let's solve it together! +- 💡 **Have ideas?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - Share your creative ideas! +- 📚 **Learning to use?** → [Official Documentation](https://butterfly.js.org/) - Detailed usage guide +- 💬 **Real-time discussion?** → [Telegram Group](https://t.me/bu2fly) - Chat with community members + +### 🎯 Contributing + +Want to make Butterfly better? We welcome any form of contribution: + +- **🔧 Code Contributions** - Fix bugs, add new features, optimize performance +- **📝 Documentation** - Improve docs, translate content, write tutorials +- **🎨 Design Suggestions** - UI/UX improvements, theme colors, icon design +- **🧪 Testing & Feedback** - Test new features, report issues, provide user experience +- **💰 Financial Support** - [Sponsor the Project](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - Support long-term development + +## 📄 License + +This project is licensed under the [Apache 2.0](LICENSE) License. + +## 🙏 Acknowledgments + +This theme is developed based on [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody). Thanks to the original author for their excellent work that provided inspiration and foundation! + +Thanks to all friends who have contributed to the development of the Butterfly theme. Your support has made this theme continuously improve and progress. + +--- + +
    + +**✨ If this theme helps you, please give us a ⭐ Star! ✨** +
    diff --git a/themes/butterfly/README_CN.md b/themes/butterfly/README_CN.md new file mode 100644 index 0000000..2c3e143 --- /dev/null +++ b/themes/butterfly/README_CN.md @@ -0,0 +1,193 @@ +
    + English +
    + +
    + +Butterfly Logo + +# hexo-theme-butterfly + +一個適用於 Hexo 的現代化、美觀且功能豐富的主題 + +![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/master?color=%231ab1ad&label=master) +![dev version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev) +![npm version](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff) +![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83cd) +![license](https://img.shields.io/github/license/jerryc127/hexo-theme-butterfly?color=FF5531) +![GitHub stars](https://img.shields.io/github/stars/jerryc127/hexo-theme-butterfly?style=social) + +📢 **在線預覽**: [Butterfly 官方](https://butterfly.js.org/) | [CrazyWong 博客](https://blog.crazywong.com/) + +📖 **完整文檔**: [中文文檔](https://butterfly.js.org/posts/21cfbf15/) | [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) + +![Butterfly 主題預覽](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png) + +
    + +--- + +## 🚀 快速開始 + +### 💾 安裝方式 + +#### 方式一:Git 安裝(推薦) + +> 💡 **提示**: 如果您在中國大陸訪問 GitHub 速度較慢,可以使用 [Gitee 鏡像](https://gitee.com/immyw/hexo-theme-butterfly.git) + +在您的 Hexo 博客根目錄下執行: + +```bash +# 安裝穩定版本(推薦) +git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly +``` + +```bash +# 安裝開發版本(搶先體驗新功能) +git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly +``` + +#### 方式二:NPM 安裝 + +> ⚠️ **注意**: NPM 安裝方式僅支援 Hexo 5.0.0 及以上版本 + +```bash +npm install hexo-theme-butterfly +``` + +### ⚙️ 主題配置 + +1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`: + +```yaml +theme: butterfly +``` + +2. **安裝依賴**: 如果您尚未安裝 pug 和 stylus 渲染器,請執行: + +```bash +npm install hexo-renderer-pug hexo-renderer-stylus --save +``` + +## ✨ 主題特色 + +### 🎨 設計風格 +- [x] **卡片化設計** - 現代化的卡片式佈局 +- [x] **圓角/直角設計** - 支援自訂邊框樣式 +- [x] **響應式設計** - 完美適配各種螢幕尺寸 +- [x] **雙欄佈局** - 優化的閱讀體驗 +- [x] **深色模式** - 護眼的夜間模式 + +### 📝 內容功能 +- [x] **多級選單** - 支援二級導航選單 +- [x] **閱讀模式** - 專注的文章閱讀體驗 +- [x] **目錄導航** - 電腦和手機雙端支援 TOC +- [x] **字數統計** - 顯示文章字數和閱讀時間 +- [x] **相關文章** - 智能推薦相關內容 +- [x] **過期提醒** - 自動提示文章更新狀態 +- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換 +- [x] **標籤外掛** - 豐富的標籤外掛支持 + +### 🔍 搜尋與導航 +- [x] **多種搜尋** - Algolia 搜尋 / 本地搜尋 / Docsearch +- [x] **內建 404** - 美觀的 404 錯誤頁面 +- [x] **Pjax 支援** - 流暢的頁面切換體驗 + +### 🎨 程式碼展示 +- [x] **語法高亮** - 內建多種主題(darker/pale night/light/ocean) +- [x] **程式碼功能** - 語言顯示/摺疊展開/複製按鈕/自動換行 +- [x] **數學公式** - 支援 Mathjax 和 Katex + +### 💬 社交互動 +- [x] **多元評論系統** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk 等 +- [x] **雙評論支援** - 可同時啟用兩套評論系統 +- [x] **分享功能** - Sharejs/Addtoany 分享套件 +- [x] **線上客服** - Chatra/Tidio/Crisp 即時聊天 + +### 📊 數據分析 +- [x] **訪問統計** - 不蒜子計數器 +- [x] **網站分析** - Google Analytics/百度統計/Cloudflare Analytics/Microsoft Clarity/Umami +- [x] **站長驗證** - 各大搜尋引擎驗證 +- [x] **廣告支援** - Google AdSense/自訂廣告位 + +### 🎪 視覺效果 +- [x] **打字特效** - activate_power_mode 動畫 +- [x] **背景特效** - 靜態彩帶/動態彩帶/飄帶效果/Canvas Nest +- [x] **滑鼠特效** - 煙花/愛心/文字點擊效果 +- [x] **載入動畫** - Preloader 和 pace.js 進度條 +- [x] **圖片效果** - Medium Zoom/Fancybox 圖片燈箱 +- [x] **懶載入** - 圖片延遲載入優化 + +### 🛠️ 進階功能 +- [x] **PWA 支援** - 漸進式網頁應用 +- [x] **複製保護** - 可關閉文字複製/版權資訊追加 +- [x] **主題定製** - 自訂網站配色方案 +- [x] **圖表支援** - Mermaid 流程圖/Chart.js 數據圖表 +- [x] **音樂符號** - ABCJS 音樂記譜法支援 +- [x] **音樂播放器** - APlayer/Meting 音樂播放功能 +- [x] **系列文章** - 系列文章組織功能 +- [x] **Instantpage** - 頁面預載入加速 +- [x] **Snackbar** - 優雅的提示訊息 + +## 🤝 貢獻者 + +感謝所有為 Butterfly 主題做出貢獻的開發者們! + +[![Contributors](https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly)](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors) + +## 📸 主題截圖 + +
    + +![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg) + +![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg) + +![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg) + +![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg) + +
    + + +## ⭐ Star 趨勢 + +[![Star History Chart](https://api.star-history.com/svg?repos=jerryc127/hexo-theme-butterfly&type=Date)](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date) + +## 🤝 一起構建更美好的主題 + +我們相信,**開源的力量來自於每一個人的參與**!無論您是開發者、設計師還是用戶,都可以為 Butterfly 主題的發展貢獻力量。 + +### 💬 獲取幫助與支援 + +- 🐛 **發現問題?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - 讓我們一起解決! +- 💡 **有好想法?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - 分享您的創意想法! +- 📚 **學習使用?** → [官方文檔](https://butterfly.js.org/) - 詳細的使用指南 +- 💬 **即時討論?** → [Telegram 群組](https://t.me/bu2fly) - 與社群成員實時交流 + +### 🎯 參與貢獻 + +想要讓 Butterfly 變得更好嗎?我們歡迎您的任何形式的貢獻: + +- **🔧 代碼貢獻** - 修復 Bug、添加新功能、優化性能 +- **📝 文檔完善** - 改進文檔、翻譯內容、撰寫教程 +- **🎨 設計建議** - UI/UX 改進、主題配色、圖示設計 +- **🧪 測試反饋** - 測試新功能、回報問題、提供使用體驗 +- **💰 資金支援** - [贊助項目](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - 支持長期發展 + +## 📄 授權條款 + +本專案採用 [Apache 2.0](LICENSE) 授權條款。 + +## 🙏 致敬與感謝 + +本主題基於 [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody) 進行開發,感謝原作者的精彩創作為我們提供了靈感與基礎! + +感謝所有為 Butterfly 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。 + +--- + +
    + +**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star!✨** +
    diff --git a/themes/butterfly/_config.yml b/themes/butterfly/_config.yml index 39df33c..f730d80 100644 --- a/themes/butterfly/_config.yml +++ b/themes/butterfly/_config.yml @@ -319,6 +319,7 @@ aside: # If set 0 will show all limit: 40 color: false + custom_colors: # Order of tags, random/name/length orderby: random # Sort of order. 1, asc for ascending; -1, desc for descending @@ -939,6 +940,10 @@ mermaid: theme: light: default dark: dark + # Enable "Open in New Tab" button to view diagram in a separate window + open_in_new_tab: true + # Enable zoom and pan interactions on diagrams + zoom_pan: true # chartjs # see https://www.chartjs.org/docs/latest/ diff --git a/themes/butterfly/layout/archive.pug b/themes/butterfly/layout/archive.pug index b67329e..913dedc 100644 --- a/themes/butterfly/layout/archive.pug +++ b/themes/butterfly/layout/archive.pug @@ -3,7 +3,6 @@ extends includes/layout.pug block content include ./includes/mixins/article-sort.pug #archive - #posts-chart(data-start="2025-01" style="height: 300px; padding: 10px;") .article-sort-title= `${_p('page.articles')} - ${getArchiveLength()}` +articleSort(page.posts) include includes/pagination.pug \ No newline at end of file diff --git a/themes/butterfly/layout/includes/head/config.pug b/themes/butterfly/layout/includes/head/config.pug index 137c041..e5901ff 100644 --- a/themes/butterfly/layout/includes/head/config.pug +++ b/themes/butterfly/layout/includes/head/config.pug @@ -72,20 +72,16 @@ }) } - let highlight = 'undefined' - let syntaxHighlighter = config.syntax_highlighter - let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable) - if (highlightEnable) { - const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks - highlight = JSON.stringify({ - plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs', - highlightCopy: copy, - highlightLang: language, - highlightHeightLimit: height_limit, - highlightFullpage: fullpage, - highlightMacStyle: macStyle - }) - } + let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null) + const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks + let highlight = JSON.stringify({ + plugin: highlightProvider, + highlightCopy: copy, + highlightLang: language, + highlightHeightLimit: height_limit, + highlightFullpage: fullpage, + highlightMacStyle: macStyle + }) script. const GLOBAL_CONFIG = { diff --git a/themes/butterfly/layout/includes/header/nav.pug b/themes/butterfly/layout/includes/header/nav.pug index 2fef844..a6be8e2 100644 --- a/themes/butterfly/layout/includes/header/nav.pug +++ b/themes/butterfly/layout/includes/header/nav.pug @@ -10,24 +10,17 @@ nav#nav span.site-name=(page.title || config.title) span.site-name i.fa-solid.fa-circle-arrow-left - span= '' + _p('post.back_to_home') + span= ' ' + _p('post.back_to_home') #menus - //- if theme.search.use - //- #search-button - //- span.site-page.social-icon.search - //- i.fas.fa-search.fa-fw - //- span= ' ' + _p('search.title') + if theme.search.use + #search-button + span.site-page.social-icon.search + i.fas.fa-search.fa-fw + span= ' ' + _p('search.title') if theme.menu != partial('includes/header/menu_item', {}, {cache: true}) - #nav-right - if theme.search.use || true - #random-post-button - a.site-page.social-icon#random-post-link(href='javascript:void(0);' onclick='randomPost()') - i.fas.fa-solid.fa-shuffle - #search-button - a.site-page.social-icon.search-typesense-trigger - i.fas.fa-search.fa-fw + #toggle-menu span.site-page i.fas.fa-bars.fa-fw \ No newline at end of file diff --git a/themes/butterfly/layout/includes/layout.pug b/themes/butterfly/layout/includes/layout.pug index 735ad8d..8acabde 100644 --- a/themes/butterfly/layout/includes/layout.pug +++ b/themes/butterfly/layout/includes/layout.pug @@ -41,7 +41,6 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside #body-wrap(class=pageType) include ./header/index.pug - include ./others/memos_home.pug main#content-inner.layout(class=hideAside) if body @@ -57,5 +56,4 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside !=partial('includes/footer', {}, {cache: true}) include ./rightside.pug - include ./additional-js.pug - include ./rightmenu.pug \ No newline at end of file + include ./additional-js.pug \ No newline at end of file diff --git a/themes/butterfly/layout/includes/mixins/indexPostUI.pug b/themes/butterfly/layout/includes/mixins/indexPostUI.pug index 4f2c3ee..fc2e436 100644 --- a/themes/butterfly/layout/includes/mixins/indexPostUI.pug +++ b/themes/butterfly/layout/includes/mixins/indexPostUI.pug @@ -1,6 +1,6 @@ mixin indexPostUI() - const indexLayout = theme.index_layout - - const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : '' + - const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : '' #recent-posts.recent-posts.nc(class=masonryLayoutClass) .recent-post-items each article, index in page.posts.data @@ -8,17 +8,17 @@ mixin indexPostUI() - const link = article.link || article.path - const title = article.title || _p('no_title') - const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '') - - const post_cover = article.cover - - const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : '' + - const postCover = article.cover + - const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : '' - if post_cover && theme.cover.index_enable + if postCover && theme.cover.index_enable .post_cover(class=leftOrRight) a(href=url_for(link) title=title) if article.cover_type === 'img' - img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title) + img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title) else - div.post-bg(style=`background: ${post_cover}`) - .recent-post-info(class=no_cover) + div.post-bg(style=`background: ${postCover}`) + .recent-post-info(class=noCover) a.article-title(href=url_for(link) title=title) if globalPageType === 'home' && (article.top || article.sticky > 0) i.fas.fa-thumbtack.sticky @@ -35,13 +35,13 @@ mixin indexPostUI() span.article-meta-label=_p('post.updated') time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format) else - - const data_type_updated = theme.post_meta.page.date_type === 'updated' - - const date_type = data_type_updated ? 'updated' : 'date' - - const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt' - - const date_title = data_type_updated ? _p('post.updated') : _p('post.created') - i(class=date_icon) - span.article-meta-label= date_title - time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format) + - const isUpdatedType = theme.post_meta.page.date_type === 'updated' + - const dateType = isUpdatedType ? 'updated' : 'date' + - const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt' + - const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created') + i(class=dateIcon) + span.article-meta-label= dateTitle + time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format) if theme.post_meta.page.categories && article.categories.data.length > 0 span.article-meta span.article-meta-separator | @@ -69,7 +69,10 @@ mixin indexPostUI() span.article-meta-label= ' ' + _p('card_post_count') if theme.comments.card_post_count && theme.comments.use - case theme.comments.use[0] + - const commentSystem = theme.comments.use[0] + - const commentLink = url_for(link) + '#post-comment' + + case commentSystem when 'Disqus' when 'Disqusjs' +countBlockInIndex @@ -77,30 +80,30 @@ mixin indexPostUI() i.fa-solid.fa-spinner.fa-spin when 'Valine' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.valine-comment-count(data-xid=url_for(link)) i.fa-solid.fa-spinner.fa-spin when 'Waline' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.waline-comment-count(data-path=url_for(link)) i.fa-solid.fa-spinner.fa-spin when 'Twikoo' +countBlockInIndex - a.twikoo-count(href=url_for(link) + '#post-comment') + a.twikoo-count(href=commentLink) i.fa-solid.fa-spinner.fa-spin when 'Facebook Comments' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.fb-comments-count(data-href=urlNoIndex(article.permalink)) when 'Remark42' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.remark42__counter(data-url=urlNoIndex(article.permalink)) i.fa-solid.fa-spinner.fa-spin when 'Artalk' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.artalk-count(data-page-key=url_for(link)) i.fa-solid.fa-spinner.fa-spin diff --git a/themes/butterfly/layout/includes/page/categories.pug b/themes/butterfly/layout/includes/page/categories.pug index f2dd8ac..79153c8 100644 --- a/themes/butterfly/layout/includes/page/categories.pug +++ b/themes/butterfly/layout/includes/page/categories.pug @@ -1,2 +1 @@ -.category-lists!= list_categories() -
    \ No newline at end of file +.category-lists!= list_categories() \ No newline at end of file diff --git a/themes/butterfly/layout/includes/page/tags.pug b/themes/butterfly/layout/includes/page/tags.pug index 794a210..d185173 100644 --- a/themes/butterfly/layout/includes/page/tags.pug +++ b/themes/butterfly/layout/includes/page/tags.pug @@ -1,3 +1,2 @@ .tag-cloud-list.text-center - !=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'}) -
    \ No newline at end of file + !=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors}) diff --git a/themes/butterfly/layout/includes/third-party/abcjs/abcjs.pug b/themes/butterfly/layout/includes/third-party/abcjs/abcjs.pug index 51a5f20..d9e58b1 100644 --- a/themes/butterfly/layout/includes/third-party/abcjs/abcjs.pug +++ b/themes/butterfly/layout/includes/third-party/abcjs/abcjs.pug @@ -24,7 +24,7 @@ script. const options = { ...params, responsive: "resize" } // Render the music score using ABCJS.renderAbc - ABCJS.renderAbc(ele, ele.innerHTML, options) + ABCJS.renderAbc(ele, ele.textContent, options) } }, 100) } diff --git a/themes/butterfly/layout/includes/third-party/math/mermaid.pug b/themes/butterfly/layout/includes/third-party/math/mermaid.pug index 0ce5aa7..3a745db 100644 --- a/themes/butterfly/layout/includes/third-party/math/mermaid.pug +++ b/themes/butterfly/layout/includes/third-party/math/mermaid.pug @@ -1,11 +1,275 @@ script. (() => { + const parseViewBox = viewBox => { + if (!viewBox) return null + const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n)) + if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null + return parts + } + + const getSvgViewBox = svg => { + const attr = parseViewBox(svg.getAttribute('viewBox')) + if (attr) return attr + + // Fallback: use bbox to build a viewBox + try { + const bbox = svg.getBBox() + if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height] + } catch (e) { + // getBBox may fail on some edge cases; ignore + } + + const w = Number(svg.getAttribute('width')) || 0 + const h = Number(svg.getAttribute('height')) || 0 + if (w > 0 && h > 0) return [0, 0, w, h] + return [0, 0, 100, 100] + } + + const setSvgViewBox = (svg, vb) => { + svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`) + } + + const clamp = (v, min, max) => Math.max(min, Math.min(max, v)) + + const openSvgInNewTab = ({ source, initViewBox }) => { + const getClonedSvg = () => { + if (typeof source === 'string') { + const template = document.createElement('template') + template.innerHTML = source.trim() + const svg = template.content.querySelector('svg') + return svg ? svg.cloneNode(true) : null + } + if (source && typeof source.cloneNode === 'function') { + return source.cloneNode(true) + } + return null + } + + const clone = getClonedSvg() + if (!clone) return + if (initViewBox && initViewBox.length === 4) { + clone.setAttribute('viewBox', initViewBox.join(' ')) + } + if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + if (!clone.getAttribute('xmlns:xlink') && clone.outerHTML.includes('xlink:')) { + clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') + } + // inject background to match current theme + const isDark = document.documentElement.getAttribute('data-theme') === 'dark' + const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff') + if (!clone.style.background) clone.style.background = bg + + const serializer = new XMLSerializer() + const svgSource = serializer.serializeToString(clone) + const htmlSource = ` + + ${svgSource}` + const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' }) + const url = URL.createObjectURL(blob) + window.open(url, '_blank', 'noopener') + setTimeout(() => URL.revokeObjectURL(url), 30000) + } + + const attachMermaidViewerButton = wrap => { + let btn = wrap.querySelector('.mermaid-open-btn') + if (!btn) { + btn = document.createElement('button') + btn.type = 'button' + btn.className = 'mermaid-open-btn' + wrap.appendChild(btn) + } + + btn.innerHTML = '' + + if (!btn.__mermaidViewerBound) { + btn.addEventListener('click', e => { + e.preventDefault() + e.stopPropagation() + const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg') + if (!svg) return + const initViewBox = wrap.__mermaidInitViewBox + if (typeof svg === 'string') { + openSvgInNewTab({ source: svg, initViewBox }) + return + } + openSvgInNewTab({ source: svg, initViewBox }) + }) + btn.__mermaidViewerBound = true + } + } + + // Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates) + const zoomAtPoint = (vb, factor, px, py) => { + const w = vb[2] * factor + const h = vb[3] * factor + const nx = px - (px - vb[0]) * factor + const ny = py - (py - vb[1]) * factor + return [nx, ny, w, h] + } + + const initMermaidGestures = wrap => { + const svg = wrap.querySelector('svg') + if (!svg) return + + // Ensure viewBox exists so gestures always work + const initVb = getSvgViewBox(svg) + wrap.__mermaidInitViewBox = initVb + wrap.__mermaidCurViewBox = initVb.slice() + setSvgViewBox(svg, initVb) + + // Avoid binding multiple times on themeChange/pjax + if (wrap.__mermaidGestureBound) return + wrap.__mermaidGestureBound = true + + // Helper: map client (viewport) coordinate -> viewBox coordinate + const clientToViewBox = (clientX, clientY) => { + const rect = svg.getBoundingClientRect() + const vb = wrap.__mermaidCurViewBox || getSvgViewBox(svg) + const x = vb[0] + (clientX - rect.left) * (vb[2] / rect.width) + const y = vb[1] + (clientY - rect.top) * (vb[3] / rect.height) + return { x, y, rect, vb } + } + + const state = { + pointers: new Map(), + startVb: null, + startDist: 0, + startCenter: null + } + + const clampVb = vb => { + const init = wrap.__mermaidInitViewBox || vb + const minW = init[2] * 0.1 + const maxW = init[2] * 10 + const minH = init[3] * 0.1 + const maxH = init[3] * 10 + vb[2] = clamp(vb[2], minW, maxW) + vb[3] = clamp(vb[3], minH, maxH) + return vb + } + + const setCurVb = vb => { + vb = clampVb(vb) + wrap.__mermaidCurViewBox = vb + setSvgViewBox(svg, vb) + } + + const onPointerDown = e => { + // Allow only primary button for mouse + if (e.pointerType === 'mouse' && e.button !== 0) return + svg.setPointerCapture(e.pointerId) + state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) + + if (state.pointers.size === 1) { + state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + } else if (state.pointers.size === 2) { + const pts = [...state.pointers.values()] + const dx = pts[0].x - pts[1].x + const dy = pts[0].y - pts[1].y + state.startDist = Math.hypot(dx, dy) + state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + state.startCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 } + } + } + + const onPointerMove = e => { + if (!state.pointers.has(e.pointerId)) return + state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) + + // Pan with 1 pointer + if (state.pointers.size === 1 && state.startVb) { + const p = [...state.pointers.values()][0] + const prev = { x: e.clientX - e.movementX, y: e.clientY - e.movementY } + // movementX/Y unreliable on touch, compute from stored last position + const last = wrap.__mermaidLastSinglePointer || p + const dxClient = p.x - last.x + const dyClient = p.y - last.y + wrap.__mermaidLastSinglePointer = p + + const { rect } = clientToViewBox(p.x, p.y) + const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + const dx = dxClient * (vb[2] / rect.width) + const dy = dyClient * (vb[3] / rect.height) + setCurVb([vb[0] - dx, vb[1] - dy, vb[2], vb[3]]) + return + } + + // Pinch zoom with 2 pointers + if (state.pointers.size === 2 && state.startVb && state.startDist > 0) { + const pts = [...state.pointers.values()] + const dx = pts[0].x - pts[1].x + const dy = pts[0].y - pts[1].y + const dist = Math.hypot(dx, dy) + if (!dist) return + const factor = state.startDist / dist // dist bigger => zoom in (viewBox smaller) + + const cx = (pts[0].x + pts[1].x) / 2 + const cy = (pts[0].y + pts[1].y) / 2 + const centerClient = { x: cx, y: cy } + + const pxy = clientToViewBox(centerClient.x, centerClient.y) + const cpx = pxy.x + const cpy = pxy.y + + const vb = zoomAtPoint(state.startVb, factor, cpx, cpy) + setCurVb(vb) + } + } + + const onPointerUpOrCancel = e => { + state.pointers.delete(e.pointerId) + if (state.pointers.size === 0) { + state.startVb = null + state.startDist = 0 + state.startCenter = null + wrap.__mermaidLastSinglePointer = null + } else if (state.pointers.size === 1) { + // reset single pointer baseline to avoid jump + wrap.__mermaidLastSinglePointer = [...state.pointers.values()][0] + } + } + + // Wheel zoom (mouse/trackpad) + const onWheel = e => { + // ctrlKey on mac trackpad pinch; we treat both as zoom + e.preventDefault() + const delta = e.deltaY + const zoomFactor = delta > 0 ? 1.1 : 0.9 + const { x, y } = clientToViewBox(e.clientX, e.clientY) + const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + setCurVb(zoomAtPoint(vb, zoomFactor, x, y)) + } + + const onDblClick = () => { + const init = wrap.__mermaidInitViewBox + if (!init) return + wrap.__mermaidCurViewBox = init.slice() + setSvgViewBox(svg, init) + } + + svg.addEventListener('pointerdown', onPointerDown) + svg.addEventListener('pointermove', onPointerMove) + svg.addEventListener('pointerup', onPointerUpOrCancel) + svg.addEventListener('pointercancel', onPointerUpOrCancel) + svg.addEventListener('wheel', onWheel, { passive: false }) + svg.addEventListener('dblclick', onDblClick) + } + const runMermaid = ele => { window.loadMermaid = true const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' ele.forEach((item, index) => { const mermaidSrc = item.firstElementChild + + // Clear old render (themeChange/pjax will rerun) + const oldSvg = item.querySelector('svg') + if (oldSvg) oldSvg.remove() + item.__mermaidGestureBound = false + const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {} if (!config.theme) { config.theme = theme @@ -17,8 +281,12 @@ script. const renderFn = mermaid.render(mermaidID, mermaidDefinition) const renderMermaid = svg => { mermaidSrc.insertAdjacentHTML('afterend', svg) + if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item) + item.__mermaidOriginalSvg = svg + if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item) } + // mermaid v9 and v10 compatibility typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg)) }) @@ -52,4 +320,4 @@ script. btf.addGlobalFn('encrypt', loadMermaid, 'mermaid') window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid) - })() \ No newline at end of file + })() diff --git a/themes/butterfly/layout/includes/widget/card_announcement.pug b/themes/butterfly/layout/includes/widget/card_announcement.pug index 7ddbd9c..9e63627 100644 --- a/themes/butterfly/layout/includes/widget/card_announcement.pug +++ b/themes/butterfly/layout/includes/widget/card_announcement.pug @@ -3,5 +3,4 @@ if theme.aside.card_announcement.enable .item-headline i.fas.fa-bullhorn.fa-shake span= _p('aside.card_announcement') - .announcement_content!= theme.aside.card_announcement.content - #welcome-ip-location-info \ No newline at end of file + .announcement_content!= theme.aside.card_announcement.content \ No newline at end of file diff --git a/themes/butterfly/layout/includes/widget/card_tags.pug b/themes/butterfly/layout/includes/widget/card_tags.pug index 3f062d1..f76ef63 100644 --- a/themes/butterfly/layout/includes/widget/card_tags.pug +++ b/themes/butterfly/layout/includes/widget/card_tags.pug @@ -5,10 +5,10 @@ if theme.aside.card_tags.enable i.fas.fa-tags span= _p('aside.card_tags') - - let { limit, orderby, order } = theme.aside.card_tags + - let { limit, orderby, order, custom_colors } = theme.aside.card_tags - limit = limit === 0 ? 0 : limit || 40 if theme.aside.card_tags.color - .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'}) + .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors}) else - .card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'}) + .card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'}) diff --git a/themes/butterfly/layout/includes/widget/index.pug b/themes/butterfly/layout/includes/widget/index.pug index bb23caa..b298878 100644 --- a/themes/butterfly/layout/includes/widget/index.pug +++ b/themes/butterfly/layout/includes/widget/index.pug @@ -9,7 +9,6 @@ else !=partial('includes/widget/card_author', {}, {cache: true}) !=partial('includes/widget/card_announcement', {}, {cache: true}) - !=partial('includes/widget/card_poem', {}, {cache: true}) !=partial('includes/widget/card_top_self', {}, {cache: true}) .sticky_layout if showToc @@ -22,7 +21,6 @@ //- page !=partial('includes/widget/card_author', {}, {cache: true}) !=partial('includes/widget/card_announcement', {}, {cache: true}) - !=partial('includes/widget/card_poem', {}, {cache: true}) !=partial('includes/widget/card_top_self', {}, {cache: true}) .sticky_layout diff --git a/themes/butterfly/layout/post.pug b/themes/butterfly/layout/post.pug index 23a7d54..9a44282 100644 --- a/themes/butterfly/layout/post.pug +++ b/themes/butterfly/layout/post.pug @@ -6,8 +6,6 @@ block content include includes/header/post-info.pug article#article-container.container.post-content - if page.summary && theme.ai_summary.enable - include includes/post/post-summary.pug if theme.noticeOutdate.enable && page.noticeOutdate !== false include includes/post/outdate-notice.pug else diff --git a/themes/butterfly/package.json b/themes/butterfly/package.json index a3ed092..5f23a10 100644 --- a/themes/butterfly/package.json +++ b/themes/butterfly/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.5.3", + "version": "5.5.4", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/themes/butterfly/plugins.yml b/themes/butterfly/plugins.yml index 9347184..a06d76e 100644 --- a/themes/butterfly/plugins.yml +++ b/themes/butterfly/plugins.yml @@ -1,7 +1,7 @@ abcjs_basic_js: name: abcjs file: dist/abcjs-basic-min.js - version: 6.5.2 + version: 6.6.0 activate_power_mode: name: butterfly-extsrc file: dist/activate-power-mode.min.js @@ -9,7 +9,7 @@ activate_power_mode: algolia_search: name: algoliasearch file: dist/lite/builds/browser.umd.js - version: 5.46.0 + version: 5.47.0 aplayer_css: name: aplayer file: dist/APlayer.min.css @@ -66,26 +66,26 @@ docsearch_css: name: '@docsearch/css' other_name: docsearch-css file: dist/style.css - version: 4.3.2 + version: 4.5.3 docsearch_js: name: '@docsearch/js' other_name: docsearch-js file: dist/umd/index.js - version: 4.3.2 + version: 4.5.3 egjs_infinitegrid: name: '@egjs/infinitegrid' other_name: egjs-infinitegrid file: dist/infinitegrid.min.js - version: 4.12.0 + version: 4.13.0 fancybox: name: '@fancyapps/ui' file: dist/fancybox/fancybox.umd.js - version: 6.1.7 + version: 6.1.9 other_name: fancyapps-ui fancybox_css: name: '@fancyapps/ui' file: dist/fancybox/fancybox.css - version: 6.1.7 + version: 6.1.9 other_name: fancyapps-ui fireworks: name: butterfly-extsrc @@ -112,12 +112,12 @@ katex: name: katex file: dist/katex.min.css other_name: KaTeX - version: 0.16.27 + version: 0.16.28 katex_copytex: name: katex file: dist/contrib/copy-tex.min.js other_name: KaTeX - version: 0.16.27 + version: 0.16.28 lazyload: name: vanilla-lazyload file: dist/lazyload.iife.min.js @@ -125,7 +125,7 @@ lazyload: mathjax: name: mathjax file: tex-mml-chtml.js - version: 4.0.0 + version: 4.1.0 medium_zoom: name: medium-zoom file: dist/medium-zoom.min.js @@ -190,7 +190,7 @@ twikoo: typed: name: typed.js file: dist/typed.umd.js - version: 2.1.0 + version: 3.0.0 valine: name: valine file: dist/Valine.min.js diff --git a/themes/butterfly/scripts/common/default_config.js b/themes/butterfly/scripts/common/default_config.js index d8ae8c2..ab3f01f 100644 --- a/themes/butterfly/scripts/common/default_config.js +++ b/themes/butterfly/scripts/common/default_config.js @@ -179,6 +179,7 @@ module.exports = { enable: true, limit: 40, color: false, + custom_colors: null, orderby: 'random', order: 1, sort_order: null @@ -522,7 +523,9 @@ module.exports = { theme: { light: 'default', dark: 'dark' - } + }, + open_in_new_tab: true, + zoom_pan: true }, chartjs: { enable: false, diff --git a/themes/butterfly/scripts/helpers/page.js b/themes/butterfly/scripts/helpers/page.js index eea97ee..5530e61 100644 --- a/themes/butterfly/scripts/helpers/page.js +++ b/themes/butterfly/scripts/helpers/page.js @@ -19,7 +19,7 @@ hexo.extend.helper.register('postDesc', data => { hexo.extend.helper.register('cloudTags', function (options = {}) { const env = this - let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options + let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options if (limit > 0) { source = source.limit(limit) @@ -36,15 +36,48 @@ hexo.extend.helper.register('cloudTags', function (options = {}) { return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})` } - const generateStyle = (size, unit, page) => { - const colorStyle = page === 'tags' ? `background-color: ${getRandomColor()};` : `color: ${getRandomColor()};` + const normalizeColors = input => { + if (!input) return null + if (typeof input === 'string') { + const color = input.trim() + return color ? [color] : null + } + if (Array.isArray(input)) { + const result = [] + for (let i = 0; i < input.length; i++) { + const value = input[i] + if (value === null || value === undefined) continue + const color = String(value).trim() + if (!color) continue + result.push(color) + } + return result.length ? result : null + } + return null + } + + const userColors = normalizeColors(custom_colors) + + const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}` + + const generateStyle = (size, unit, page, color) => { + const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};` return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}` } - return source.sort(orderby, order).map(tag => { + return source.sort(orderby, order).map((tag, idx) => { const ratio = length ? sizeMap.get(tag.length) / length : 0 const size = minfontsize + ((maxfontsize - minfontsize) * ratio) - const style = generateStyle(size, unit, page) + + if (userColors && userColors.length) { + const colorClass = resolveColorClass(idx) + const color = userColors[idx % userColors.length] + const style = generateStyle(size, unit, page, color) + return `${tag.name}` + } + + const color = getRandomColor() + const style = generateStyle(size, unit, page, color) return `${tag.name}` }).join('') }) diff --git a/themes/butterfly/source/css/_highlight/highlight.styl b/themes/butterfly/source/css/_highlight/highlight.styl index f32329c..acdbbfd 100644 --- a/themes/butterfly/source/css/_highlight/highlight.styl +++ b/themes/butterfly/source/css/_highlight/highlight.styl @@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo --hlscrollbar-bg: lighten(#121212, 5) --hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9)) +$scrollbar-style + // scrollbar - firefox + @-moz-document url-prefix() + scrollbar-color: var(--hlscrollbar-bg) transparent + + &::-webkit-scrollbar-thumb + background: var(--hlscrollbar-bg) + if $highlight_enable @require 'highlight/index' @@ -88,6 +96,11 @@ $code-block &:hover border-bottom-color: var(--hl-color) + &.default + pre + padding: 10px 20px + @extend $scrollbar-style + &.copy-true user-select: all -webkit-user-select: all diff --git a/themes/butterfly/source/css/_highlight/highlight/diff.styl b/themes/butterfly/source/css/_highlight/highlight/diff.styl index 6d8db97..7c44b75 100644 --- a/themes/butterfly/source/css/_highlight/highlight/diff.styl +++ b/themes/butterfly/source/css/_highlight/highlight/diff.styl @@ -1,11 +1,6 @@ figure.highlight table - // scrollbar - firefox - @-moz-document url-prefix() - scrollbar-color: var(--hlscrollbar-bg) transparent - - &::-webkit-scrollbar-thumb - background: var(--hlscrollbar-bg) + @extend $scrollbar-style pre .deletion color: $highlight-deletion diff --git a/themes/butterfly/source/css/_highlight/prismjs/index.styl b/themes/butterfly/source/css/_highlight/prismjs/index.styl index b76b6ec..5dce591 100644 --- a/themes/butterfly/source/css/_highlight/prismjs/index.styl +++ b/themes/butterfly/source/css/_highlight/prismjs/index.styl @@ -6,12 +6,7 @@ if $highlight_theme != false .container pre[class*='language-'] - // scrollbar - firefox - @-moz-document url-prefix() - scrollbar-color: var(--hlscrollbar-bg) transparent - - &::-webkit-scrollbar-thumb - background: var(--hlscrollbar-bg) + @extend $scrollbar-style &:not(.line-numbers) padding: 10px 20px diff --git a/themes/butterfly/source/css/_layout/head.styl b/themes/butterfly/source/css/_layout/head.styl index 16f7d51..5346854 100644 --- a/themes/butterfly/source/css/_layout/head.styl +++ b/themes/butterfly/source/css/_layout/head.styl @@ -37,6 +37,8 @@ margin: 0 color: var(--white) font-size: 1.85em + @extend .limit-more-line + -webkit-line-clamp: 3 +minWidth768() font-size: 2.85em diff --git a/themes/butterfly/source/css/_layout/reward.styl b/themes/butterfly/source/css/_layout/reward.styl index a09b715..333ccd9 100644 --- a/themes/butterfly/source/css/_layout/reward.styl +++ b/themes/butterfly/source/css/_layout/reward.styl @@ -37,13 +37,13 @@ display: none padding: 0 0 15px width: 100% - addBorderRadius() .reward-all display: inline-block margin: 0 padding: 20px 10px background: var(--reward-pop) + addBorderRadius() &:before position: absolute diff --git a/themes/butterfly/source/css/_layout/third-party.styl b/themes/butterfly/source/css/_layout/third-party.styl index d1653f2..dc2eaca 100644 --- a/themes/butterfly/source/css/_layout/third-party.styl +++ b/themes/butterfly/source/css/_layout/third-party.styl @@ -65,12 +65,55 @@ if hexo-config('waline.bg') if hexo-config('mermaid.enable') .mermaid-wrap + position: relative margin: 0 0 20px + background: var(--card-bg) text-align: center + if hexo-config('mermaid.open_in_new_tab') + .mermaid-open-btn + position: absolute + top: 8px + right: 8px + z-index: 2 + display: flex + justify-content: center + align-items: center + padding: 0 + width: 34px + height: 25px + border: none + border-radius: 20% + background: #D3D3D3 + box-shadow: 0 4px 10px rgba(0, 0, 0, .15) + color: #fff + font-size: 0 + line-height: 1 + cursor: pointer + transition: background .2s ease, transform .2s ease + + i + font-size: 16px + line-height: 1 + + &:hover, + &:focus-visible + outline: none + background: #C0C0C0 + transform: translateY(-1px) + & > svg + max-width: 100% height: 100% + if hexo-config('mermaid.zoom_pan') + cursor: grab + user-select: none + touch-action: none + + &:active + cursor: grabbing + if hexo-config('mermaid.code_write') pre > code.mermaid display: none @@ -184,4 +227,4 @@ if hexo-config('math.use') +maxWidth768() .fancybox__toolbar__column.is-middle - display: none \ No newline at end of file + display: none diff --git a/themes/butterfly/source/js/main.js b/themes/butterfly/source/js/main.js index 9041745..046ad4e 100644 --- a/themes/butterfly/source/js/main.js +++ b/themes/butterfly/source/js/main.js @@ -61,11 +61,14 @@ document.addEventListener('DOMContentLoaded', () => { const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle - const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]') + const isNotHighlightJs = plugin !== 'highlight.js' + const isPrismjs = plugin === 'prismjs' + const $figureHighlight = isNotHighlightJs + ? Array.from(document.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement) + : document.querySelectorAll('figure.highlight') if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return - const isPrismjs = plugin === 'prismjs' const highlightShrinkClass = isHighlightShrink === true ? 'closed' : '' const highlightShrinkEle = isHighlightShrink !== undefined ? '' : '' const highlightCopyEle = highlightCopy ? '' : '' @@ -133,7 +136,7 @@ document.addEventListener('DOMContentLoaded', () => { const highlightCopyFn = (ele, clickEle) => { const $buttonParent = ele.parentNode $buttonParent.classList.add('copy-true') - const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre' + const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre' const codeElement = $buttonParent.querySelector(preCodeSelector) if (!codeElement) return copy(codeElement.innerText, clickEle) @@ -213,20 +216,23 @@ document.addEventListener('DOMContentLoaded', () => { fragment.appendChild(ele) } - isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild) + isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild) } $figureHighlight.forEach(item => { let langName = '' - if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' }) + if (isNotHighlightJs) { + const newClassName = isPrismjs ? 'prismjs' : 'default' + btf.wrap(item, 'figure', { class: `highlight ${newClassName}` }) + } if (!highlightLang) { createEle('', item) return } - if (isPrismjs) { - langName = item.getAttribute('data-language') || 'Code' + if (isNotHighlightJs) { + langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '') } else { langName = item.getAttribute('class').split(' ')[1] if (langName === 'plain' || langName === undefined) langName = 'Code' diff --git a/themes/butterfly/source/js/search/local-search.js b/themes/butterfly/source/js/search/local-search.js new file mode 100644 index 0000000..f2ccb06 --- /dev/null +++ b/themes/butterfly/source/js/search/local-search.js @@ -0,0 +1,567 @@ +/** + * Refer to hexo-generator-searchdb + * https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js + * Modified by hexo-theme-butterfly + */ + +class LocalSearch { + constructor ({ + path = '', + unescape = false, + top_n_per_article = 1 + }) { + this.path = path + this.unescape = unescape + this.top_n_per_article = top_n_per_article + this.isfetched = false + this.datas = null + } + + getIndexByWord (words, text, caseSensitive = false) { + const index = [] + const included = new Set() + + if (!caseSensitive) { + text = text.toLowerCase() + } + words.forEach(word => { + if (this.unescape) { + const div = document.createElement('div') + div.innerText = word + word = div.innerHTML + } + const wordLen = word.length + if (wordLen === 0) return + let startPosition = 0 + let position = -1 + if (!caseSensitive) { + word = word.toLowerCase() + } + while ((position = text.indexOf(word, startPosition)) > -1) { + index.push({ position, word }) + included.add(word) + startPosition = position + wordLen + } + }) + // Sort index by position of keyword + index.sort((left, right) => { + if (left.position !== right.position) { + return left.position - right.position + } + return right.word.length - left.word.length + }) + return [index, included] + } + + // Merge hits into slices + mergeIntoSlice (start, end, index) { + let item = index[0] + let { position, word } = item + const hits = [] + const count = new Set() + while (position + word.length <= end && index.length !== 0) { + count.add(word) + hits.push({ + position, + length: word.length + }) + const wordEnd = position + word.length + + // Move to next position of hit + index.shift() + while (index.length !== 0) { + item = index[0] + position = item.position + word = item.word + if (wordEnd > position) { + index.shift() + } else { + break + } + } + } + return { + hits, + start, + end, + count: count.size + } + } + + // Highlight title and content + highlightKeyword (val, slice) { + let result = '' + let index = slice.start + for (const { position, length } of slice.hits) { + result += val.substring(index, position) + index = position + length + result += `${val.substr(position, length)}` + } + result += val.substring(index, slice.end) + return result + } + + getResultItems (keywords) { + const resultItems = [] + this.datas.forEach(({ title, content, url }) => { + // The number of different keywords included in the article. + const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title) + const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content) + const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size + + // Show search results + const hitCount = indexOfTitle.length + indexOfContent.length + if (hitCount === 0) return + + const slicesOfTitle = [] + if (indexOfTitle.length !== 0) { + slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle)) + } + + let slicesOfContent = [] + while (indexOfContent.length !== 0) { + const item = indexOfContent[0] + const { position } = item + // Cut out 120 characters. The maxlength of .search-input is 80. + const start = Math.max(0, position - 20) + const end = Math.min(content.length, position + 100) + slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent)) + } + + // Sort slices in content by included keywords' count and hits' count + slicesOfContent.sort((left, right) => { + if (left.count !== right.count) { + return right.count - left.count + } else if (left.hits.length !== right.hits.length) { + return right.hits.length - left.hits.length + } + return left.start - right.start + }) + + // Select top N slices in content + const upperBound = parseInt(this.top_n_per_article, 10) + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound) + } + + let resultItem = '' + + url = new URL(url, location.origin) + url.searchParams.append('highlight', keywords.join(' ')) + + if (slicesOfTitle.length !== 0) { + resultItem += `
  • ${this.highlightKeyword(title, slicesOfTitle[0])}` + } else { + resultItem += `
  • ${title}` + } + + slicesOfContent.forEach(slice => { + resultItem += `

    ${this.highlightKeyword(content, slice)}...

    ` + }) + + resultItem += '
  • ' + resultItems.push({ + item: resultItem, + id: resultItems.length, + hitCount, + includedCount + }) + }) + return resultItems + } + + fetchData () { + const isXml = !this.path.endsWith('json') + fetch(this.path) + .then(response => response.text()) + .then(res => { + // Get the contents from search data + this.isfetched = true + this.datas = isXml + ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({ + title: element.querySelector('title').textContent, + content: element.querySelector('content').textContent, + url: element.querySelector('url').textContent + })) + : JSON.parse(res) + // Only match articles with non-empty titles + this.datas = this.datas.filter(data => data.title).map(data => { + data.title = data.title.trim() + data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '' + data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/') + return data + }) + // Remove loading animation + window.dispatchEvent(new Event('search:loaded')) + }) + } + + // Highlight by wrapping node in mark elements with the given class name + highlightText (node, slice, className) { + const val = node.nodeValue + let index = slice.start + const children = [] + for (const { position, length } of slice.hits) { + const text = document.createTextNode(val.substring(index, position)) + index = position + length + const mark = document.createElement('mark') + mark.className = className + mark.appendChild(document.createTextNode(val.substr(position, length))) + children.push(text, mark) + } + node.nodeValue = val.substring(index, slice.end) + children.forEach(element => { + node.parentNode.insertBefore(element, node) + }) + } + + // Highlight the search words provided in the url in the text + highlightSearchWords (body) { + const params = new URL(location.href).searchParams.get('highlight') + const keywords = params ? params.split(' ') : [] + if (!keywords.length || !body) return + const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null) + const allNodes = [] + while (walk.nextNode()) { + if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode) + } + allNodes.forEach(node => { + const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue) + if (!indexOfNode.length) return + const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode) + this.highlightText(node, slice, 'search-keyword') + }) + } +} + +window.addEventListener('load', () => { +// Search + 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') + 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.hidden = false + + const keywords = searchText.split(/[-\s]+/) + let resultItems = [] + + if (searchText.length > 0) { + resultItems = localSearch.getResultItems(keywords) + } + + if (keywords.length === 1 && keywords[0] === '') { + clearSearchResults() + } else if (resultItems.length === 0) { + showNoResults(searchText) + } else { + // Sort results by relevance + resultItems.sort((left, right) => { + if (left.includedCount !== right.includedCount) { + return right.includedCount - left.includedCount + } else if (left.hitCount !== right.hitCount) { + return right.hitCount - left.hitCount + } + return right.id - left.id + }) + + if (enablePagination) { + currentResultItems = resultItems + currentPage = 0 + } + renderResults(searchText, resultItems) + } + + $loadingStatus.hidden = true + } + + let loadFlag = false + const $searchMask = document.getElementById('search-mask') + const $searchDialog = document.querySelector('#local-search .search-dialog') + + // fix safari + const fixSafariHeight = () => { + if (window.innerWidth < 768) { + $searchDialog.style.setProperty('--search-height', window.innerHeight + 'px') + } + } + + const openSearch = () => { + btf.overflowPaddingR.add() + btf.animateIn($searchMask, 'to_show 0.5s') + btf.animateIn($searchDialog, 'titleScale 0.5s') + setTimeout(() => { input.focus() }, 300) + if (!loadFlag) { + !localSearch.isfetched && localSearch.fetchData() + input.addEventListener('input', inputEventFunction) + loadFlag = true + } + // shortcut: ESC + document.addEventListener('keydown', function f (event) { + if (event.code === 'Escape') { + closeSearch() + document.removeEventListener('keydown', f) + } + }) + + fixSafariHeight() + window.addEventListener('resize', fixSafariHeight) + } + + const closeSearch = () => { + btf.overflowPaddingR.remove() + btf.animateOut($searchDialog, 'search_close .5s') + btf.animateOut($searchMask, 'to_hide 0.5s') + window.removeEventListener('resize', fixSafariHeight) + } + + const searchClickFn = () => { + btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch) + } + + const searchFnOnce = () => { + document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch) + $searchMask.addEventListener('click', closeSearch) + if (GLOBAL_CONFIG.localSearch.preload) { + 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.visibility = 'visible' + $loadDataItem.remove() + }) + + searchClickFn() + searchFnOnce() + + // pjax + window.addEventListener('pjax:complete', () => { + !btf.isHidden($searchMask) && closeSearch() + localSearch.highlightSearchWords(document.getElementById('article-container')) + searchClickFn() + }) +})