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 @@
+
+
+
+
+

+
+# hexo-theme-butterfly
+
+A modern, elegant and feature-rich theme for Hexo
+
+
+
+
+
+
+
+
+📢 **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/)
+
+
+
+
+
+---
+
+## 🚀 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!
+
+[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
+
+## 📸 Screenshots
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## ⭐ Star History
+
+[](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 @@
+
+
+
+
+

+
+# hexo-theme-butterfly
+
+一個適用於 Hexo 的現代化、美觀且功能豐富的主題
+
+
+
+
+
+
+
+
+📢 **在線預覽**: [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/)
+
+
+
+
+
+---
+
+## 🚀 快速開始
+
+### 💾 安裝方式
+
+#### 方式一: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 主題做出貢獻的開發者們!
+
+[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
+
+## 📸 主題截圖
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## ⭐ Star 趨勢
+
+[](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 += `
+ `
+ if (startPage > 1) {
+ pagesHTML += `
+ `
+ }
+ }
+
+ // Add middle page numbers
+ for (let i = startPage; i <= endPage; i++) {
+ const isSelected = i === page
+ if (isSelected) {
+ pagesHTML += `
+ `
+ } else {
+ pagesHTML += `
+ `
+ }
+ }
+
+ // Only add ellipsis and last page when there are many pages
+ if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
+ if (endPage < nbPages - 2) {
+ pagesHTML += `
+ `
+ }
+ pagesHTML += `
+ `
+ }
+
+ if (nbPages > 1) {
+ elements.paginationList.innerHTML = `
+
+ ${pagesHTML}
+ `
+ } 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()
+ })
+})