Compare commits

..

94 Commits

91 changed files with 1934 additions and 4317 deletions

2
.github/FUNDING.yml vendored
View File

@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk','https://jsd.012700.xyz/gh/jerryc127/CDN/Photo/wechat.jpg'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
.DS_Store
node_modules/
.DS_Store

310
README.md
View File

@@ -1,193 +1,117 @@
<div align="right">
<a title="中文" href="/README_CN.md">中文</a>
</div>
<div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="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)
</div>
---
## 🚀 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
<div align="center">
![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)
</div>
## ⭐ 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.
---
<div align="center">
**✨ If this theme helps you, please give us a ⭐ Star! ✨**
</div>
<div align="right">
<a title="Chinese" href="/README_CN.md">中文</a>
</div>
<div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
# hexo-theme-butterfly
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/master?color=%231ab1ad&label=master)
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83c)
![license](https://img.shields.io/github/license/jerryc127/hexo-theme-butterfly?color=FF5531)
📢 Demo: [Butterfly](https://butterfly.js.org/) / [CrazyWong](https://blog.crazywong.com/)
📖 Docs: [English](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) / [Chinese](https://butterfly.js.org/posts/21cfbf15/)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div>
---
## 💻 Installation
### GIT
> If you are in Mainland China, you can download in [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git)
Stable branch [recommend]:
```
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
Dev branch:
```
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
### NPM
> It supports Hexo 5.0.0 or later
In Hexo site root directory
```powershell
npm i hexo-theme-butterfly
```
## ⚙ Configuration
Set theme in the hexo work folder's root config file `_config.yml`:
> theme: butterfly
If you don't have pug & stylus renderer, try this:
> npm install hexo-renderer-pug hexo-renderer-stylus
## 🎉 Features
- [x] Card UI Design
- [x] Rounded Design/Squared Design
- [X] Support sub-menu
- [x] Two-column layout
- [x] Responsive Web Design
- [x] Dark Mode
- [x] Pjax
- [x] Read Mode
- [x] Conversion between Traditional and Simplified Chinese
- [X] TOC catalog is available for both computers and mobile phones
- [X] Built-in Syntax Highlighting Themes (darker/pale night/light/ocean), also support customization
- [X] Code Blocks (Display code language/close or expand Code Blocks/Copy Button/word wrap)
- [X] Disable copy/Add a Copyright Notice to the Copied Text
- [X] Search (Algolia Search/Local Search)
- [x] Mathjax and Katex
- [x] Built-in 404 page
- [x] WordCount
- [x] Related articles
- [x] Displays outdated notice for a post
- [x] Share (Sharejs/Addtoany)
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk)
- [x] Multiple Comment System Support
- [x] Online Chats (Chatra/Tidio/Crisp)
- [x] Web analytics
- [x] Google AdSense
- [x] Webmaster Verification
- [x] Change website colour scheme
- [x] Typewriter Effect: activate_power_mode
- [x] Background effects (Canvas ribbon/canvas_ribbon_piao/canvas_nest)
- [x] Mouse click effects (Fireworks/Heart/Text)
- [x] Preloader/Loading Animation/pace.js
- [x] Busuanzi visitor counter
- [x] Medium Zoom/Fancybox
- [x] Mermaid
- [x] Chart.js
- [x] Justified Gallery
- [x] Lazyload images
- [x] Instantpage/Snackbar notification toast/PWA......
## ✨ Contributors
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
</a>
## 📷 Screenshots
![](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)

View File

@@ -4,190 +4,114 @@
<div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
# 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)
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83c)
![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/)
📢 預覽: [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/)
📖 文檔: [中文](https://butterfly.js.org/posts/21cfbf15/) / [English](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)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div>
---
## 🚀 快速開始
## 💻 安裝
### 💾 安裝方式
### Git 安裝
#### 方式一Git 安裝(推薦)
> 本倉庫同時上傳到 [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git),如果你訪問 Github 緩慢,可從 Gitee 中下載。
> 💡 **提示**: 如果您在中國大陸訪問 GitHub 速度較慢,可以使用 [Gitee 鏡像](https://gitee.com/immyw/hexo-theme-butterfly.git)
在博客根目錄裡安裝穩定版【推薦】
在您的 Hexo 博客根目錄下執行:
```bash
# 安裝穩定版本(推薦)
```powershell
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
```bash
# 安裝開發版本(搶先體驗新功能)
如果想要安裝比較新的dev分支可以
```powershell
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
#### 方式二NPM 安裝
### npm 安裝
> ⚠️ **注意**: NPM 安裝方式僅支援 Hexo 5.0.0以上版本
> 此方法只支持Hexo 5.0.0以上版本
```bash
npm install hexo-theme-butterfly
在博客根目錄裡
```powershell
npm i hexo-theme-butterfly
```
### 主題配置
## ⚙ 應用主題
1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`
修改hexo配置文件`_config.yml`,把主題改為`Butterfly`
```yaml
```
theme: butterfly
```
2. **安裝依賴**: 如果您尚未安裝 pug 和 stylus 渲染器,請執行:
>如果你沒有pug以及stylus渲染器,請下載安裝: npm install hexo-renderer-pug hexo-renderer-stylus --save
```bash
npm install hexo-renderer-pug hexo-renderer-stylus --save
```
## 🎉 特色
## ✨ 主題特色
- [x] 卡片化設計
- [x] 圓角化設計/直角化設計
- [X] 支持二級目錄
- [x] 雙欄設計
- [x] 響應式主題
- [x] 夜間模式
- [x] Pjax
- [x] 文章閲讀模式
- [x] 簡體和繁體轉換
- [X] 電腦和手機都可查看TOC目錄
- [X] 內置多種代碼配色darker/pale night/light/ocean可自定義代碼配色
- [X] 代碼塊顯示代碼語言/關閉或展開代碼塊/代碼複製/代碼自動換行
- [X] 可關閉文字複製/可開啟內容複製增加版權信息)
- [X] 兩種搜索( Algolia 搜索和本地搜索)
- [x] Mathjax 和 Katex
- [x] 內置404頁面
- [x] 顯示字數統計
- [x] 顯示相關文章
- [x] 過期文章提醒
- [x] 多種分享系統Sharejs/Addtoany
- [X] 多種評論系統Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk
- [x] 支持雙評論部署
- [x] 多種在線聊天Chatra/Tidio/Crisp
- [x] 多種分析系統
- [x] 谷歌廣告/手動廣告位置
- [x] 各種站長驗證
- [x] 修改網站配色
- [x] 打字特效 activate_power_mode
- [x] 多種背景特效(靜止彩帶/動態彩帶/Canvas Nest
- [x] 多種鼠標點擊特效(煙花/文字/愛心)
- [x] 內置一種 Preloader 加載動畫和 pace.js 加載動畫條
- [x] 不蒜子訪問統計
- [x] 兩種大圖模式Medium Zoom/Fancybox
- [x] Mermaid 圖表顯示
- [x] Chart.js 圖表顯示
- [x] 照片牆
- [x] 圖片懶加載
- [x] Instantpage/Snackbar彈窗/PWA......
### 🎨 設計風格
- [x] **卡片化設計** - 現代化的卡片式佈局
- [x] **圓角/直角設計** - 支援自訂邊框樣式
- [x] **響應式設計** - 完美適配各種螢幕尺寸
- [x] **雙欄佈局** - 優化的閱讀體驗
- [x] **深色模式** - 護眼的夜間模式
## ✨ 貢獻者
### 📝 內容功能
- [x] **多級選單** - 支援二級導航選單
- [x] **閱讀模式** - 專注的文章閱讀體驗
- [x] **目錄導航** - 電腦和手機雙端支援 TOC
- [x] **字數統計** - 顯示文章字數和閱讀時間
- [x] **相關文章** - 智能推薦相關內容
- [x] **過期提醒** - 自動提示文章更新狀態
- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換
- [x] **標籤外掛** - 豐富的標籤外掛支持
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
</a>
### 🔍 搜尋與導航
- [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)
## 📸 主題截圖
<div align="center">
![主題展示](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)
</div>
## ⭐ 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 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。
---
<div align="center">
**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star✨**
</div>
![](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)

View File

@@ -13,7 +13,6 @@ nav:
# Navigation bar logo image
logo:
display_title: true
display_post_title: true
# Whether to fix navigation bar
fixed: false
@@ -91,8 +90,7 @@ category_per_img:
footer_img: false
# Website Background
# Can set it to color, image URL or an array containing colors and/or image URLs
# If an array is provided, a random background will be selected from the array on each load
# Can set it to color or image url
background:
cover:
@@ -160,7 +158,7 @@ subtitle:
# Choose: false/1/2/3
# false - disable the function
# 1 - hitokoto.cn
# 2 - https://api.aa1.cn/doc/yiyan.html
# 2 - yijuzhan.com
# 3 - jinrishici.com
source: false
# If you close the typewriter effect, the subtitle will only show the first line of sub
@@ -256,15 +254,12 @@ noticeOutdate:
# Footer Settings
# --------------------------------------
footer:
nav:
owner:
enable: true
since: 2025
# Copyright of theme and framework
copyright:
enable: true
version: true
since: 2019
custom_text:
# Copyright of theme and framework
copyright: true
# --------------------------------------
# Aside Settings
@@ -319,7 +314,6 @@ 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
@@ -405,9 +399,6 @@ rightside_item_order:
# Default: toc,chat,comment
show:
# Animation for the bottom right config button
rightside_config_animation: true
# --------------------------------------
# Global Settings
# --------------------------------------
@@ -490,11 +481,6 @@ search:
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Enable pagination for search results
pagination:
enable: false
# Number of search results per page
hitsPerPage: 8
CDN:
# Docsearch
@@ -517,12 +503,12 @@ share:
# Share.js
# https://github.com/overtrue/share.js
sharejs:
sites: facebook,x,wechat,weibo,qq
sites: facebook,twitter,wechat,weibo,qq
# AddToAny
# https://www.addtoany.com/
addtoany:
item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link
# --------------------------------------
# Comments System
@@ -701,7 +687,6 @@ umami_analytics:
enable: false
# For self-hosted setups, configure the hostname of the Umami instance
serverURL:
script_name: script.js
website_id:
option:
UV_PV:
@@ -711,12 +696,6 @@ umami_analytics:
# Umami Cloud (API key) / self-hosted Umami (token)
token:
# https://www.googletagmanager.com/
google_tag_manager:
tag_id:
# optional
domain:
# --------------------------------------
# Advertisement
# --------------------------------------
@@ -940,10 +919,6 @@ 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/
@@ -1012,8 +987,6 @@ instantpage: false
# https://github.com/verlok/vanilla-lazyload
lazyload:
enable: false
# Use browser's native lazyload instead of vanilla-lazyload
native: false
# Specify the field to use lazyload (site or post)
field: site
placeholder:
@@ -1045,11 +1018,7 @@ Open_Graph_meta:
# Structured Data
# https://developers.google.com/search/docs/guides/intro-structured-data
structured_data:
enable: false
# Alternate name for the site, used in structured data
# Format: ['name1', 'name2']
alternate_name:
structured_data: true
# Add the vendor prefixes to ensure compatibility
css_prefix: true
@@ -1073,7 +1042,7 @@ CDN:
third_party_provider: jsdelivr
# Add version number to url, true or false
version: true
version: false
# Custom format
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
@@ -1109,6 +1078,7 @@ CDN:
# gitalk_css:
# giscus:
# instantpage:
# instantsearch:
# katex:
# katex_copytex:
# lazyload:

View File

@@ -32,7 +32,6 @@ post:
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
recommend: Related Articles
edit: Edit
back_to_home: Back to Home
search:
title: Search
@@ -48,7 +47,6 @@ search:
pagination:
prev: Previous
next: Next
page_info: 'Page ${current} of ${total}'
comment: Comments

View File

@@ -32,7 +32,6 @@ post:
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
recommend: Related Articles
edit: Edit
back_to_home: Back to Home
search:
title: Search
@@ -48,7 +47,6 @@ search:
pagination:
prev: Previous
next: Next
page_info: 'Page ${current} of ${total}'
comment: Comments

View File

@@ -32,7 +32,6 @@ post:
copyright_content: 'このブログのすべての記事は、<a href="%s">%s</a> ライセンスの下で提供されており、特に明記されていない限り、すべての権利を留保します。転載時には出典を明記してください: <a href="%s">%s</a>。'
recommend: 関連記事
edit: 編集
back_to_home: ホームに戻る
search:
title: 検索
@@ -48,7 +47,6 @@ search:
pagination:
prev: 前へ
next: 次へ
page_info: '${current} ページ / 合計 ${total} ページ'
comment: コメント

View File

@@ -32,7 +32,6 @@ post:
copyright_content: '이 블로그의 모든 글은 <a href="%s">%s</a> 라이선스를 따르며, 별도로 명시되지 않는 한 모든 권리를 보유합니다. 재배포 시 출처를 명시해 주세요: <a href="%s">%s</a>.'
recommend: 관련 글
edit: 편집
back_to_home: 홈으로 돌아가기
search:
title: 검색
@@ -48,7 +47,6 @@ search:
pagination:
prev: 이전
next: 다음
page_info: '${current} 페이지 / 총 ${total} 페이지'
comment: 댓글

View File

@@ -33,7 +33,6 @@ post:
<a href="%s" target="_blank">%s</a> 许可协议。转载请注明来源 <a href="%s" target="_blank">%s</a>'
recommend: 相关推荐
edit: 编辑
back_to_home: 返回首页
search:
title: 搜索
@@ -49,7 +48,6 @@ search:
pagination:
prev: 上一篇
next: 下一篇
page_info: '第 ${current} 页 / 共 ${total} 页'
comment: 评论

View File

@@ -32,7 +32,6 @@ post:
copyright_content: '除特別聲明外,本博客所有文章均採用<a href="%s">%s</a> 授權協議。轉載請註明出處:<a href="%s">%s</a>。'
recommend: 相關文章
edit: 編輯
back_to_home: 返回首頁
search:
title: 搜尋
@@ -48,7 +47,6 @@ search:
pagination:
prev: 上一頁
next: 下一頁
page_info: '第 ${current} 頁 / 共 ${total} 頁'
comment: 評論

View File

@@ -32,7 +32,6 @@ post:
copyright_content: '本部落格所有文章除特別聲明外,均採用<a href="%s" target="_blank">%s</a> 授權協議。轉載請註明來源 <a href="%s" target="_blank">%s</a>'
recommend: 相關推薦
edit: 編輯
back_to_home: 返回首頁
search:
title: 搜尋
@@ -48,7 +47,6 @@ search:
pagination:
prev: 上一篇
next: 下一篇
page_info: '第 ${current} 頁 / 共 ${total} 頁'
comment: 評論

View File

@@ -52,10 +52,6 @@ div
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
script(async data-pjax src=theme.asset.busuanzi ? url_for(theme.asset.busuanzi) : '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
!= partial('includes/third-party/search/index', {}, { cache: true })
if theme.google_tag_manager && theme.google_tag_manager.tag_id
noscript
iframe(src=`${theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/ns.html?id=${theme.google_tag_manager.tag_id}` height="0" width="0" style="display:none;visibility:hidden")
!= partial('includes/third-party/search/index', {}, { cache: true })

View File

@@ -1,39 +1,19 @@
- const { nav, owner, copyright, custom_text } = theme.footer
if nav
.footer-flex
for block in nav
.footer-flex-items(style=`${ block.width ? 'flex-grow:' + block.width : '' }`)
for blockItem in block.content
.footer-flex-item
.footer-flex-title= blockItem.title
.footer-flex-content
for subitem in blockItem.item
if subitem.html
div!= subitem.html
else if subitem.url
a(href=url_for(subitem.url), target='_blank' title=subitem.title)= subitem.title
else if subitem.title
div!= subitem.title
.footer-other
.footer-copyright
if owner.enable
- const currentYear = new Date().getFullYear()
- const sinceYear = owner.since
span.copyright
if sinceYear && sinceYear != currentYear
!= `&copy;&nbsp;${sinceYear} - ${currentYear} By ${config.author}`
else
!= `&copy;&nbsp;${currentYear} By ${config.author}`
if copyright.enable
- const v = copyright.version ? getVersion() : false
span.framework-info
if owner.enable && nav
span.footer-separator |
span= _p('footer.framework') + ' '
a(href='https://hexo.io')= `Hexo${ v ? ' ' + v.hexo : '' }`
span.footer-separator |
span= _p('footer.theme') + ' '
a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly${ v ? ' ' + v.theme : '' }`
#footer-wrap
if theme.footer.owner.enable
- const currentYear = new Date().getFullYear()
- const sinceYear = theme.footer.owner.since
.copyright
if sinceYear && sinceYear != currentYear
!= `&copy;${sinceYear} - ${currentYear} By ${config.author}`
else
!= `&copy;${currentYear} By ${config.author}`
if theme.footer.copyright
- const v = getVersion()
.framework-info
span= _p('footer.framework') + ' '
a(href='https://hexo.io')= `Hexo ${v.hexo}`
span.footer-separator |
span= _p('footer.theme') + ' '
a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly ${v.theme}`
if theme.footer.custom_text
.footer_custom_text!= theme.footer.custom_text
.footer_custom_text!= theme.footer.custom_text

View File

@@ -31,15 +31,4 @@ if theme.microsoft_clarity
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}");
if (theme.google_tag_manager && theme.google_tag_manager.tag_id)
script.
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
"!{theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','!{theme.google_tag_manager.tag_id}');
btf.addGlobalFn('pjaxComplete', () => {
dataLayer.push({'event': 'pjaxComplete', 'page_title': document.title, 'page_location': location.href, 'page_path': window.location.pathname})
}, 'google_tag_manager')
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}");

View File

@@ -10,6 +10,7 @@
hitsPerPage: theme.search.algolia_search.hitsPerPage,
// search languages
languages: {
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
hits_empty: _p("search.algolia_search.hits_empty"),
hits_stats: _p("search.algolia_search.hits_stats"),
}
@@ -18,16 +19,12 @@
let localSearch = 'undefined'
if (theme.search.use === 'local_search') {
const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search
localSearch = JSON.stringify({
path: CDN || config.root + config.search.path,
preload,
top_n_per_article,
unescape,
pagination: {
enable: pagination.enable,
hitsPerPage: pagination.hitsPerPage
},
languages: {
// search languages
hits_empty: _p("search.local_search.hits_empty"),
@@ -72,16 +69,20 @@
})
}
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
})
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
})
}
script.
const GLOBAL_CONFIG = {

View File

@@ -1,67 +1,34 @@
if theme.structured_data
if page.layout === 'post'
-
// https://developers.google.com/search/docs/appearance/structured-data/article
if theme.structured_data && page.layout === 'post'
-
// use json-ld to add structured data
const title = page.title
const url = page.permalink
const imageVal = page.cover_type === 'img' ? page.cover : theme.avatar.img
const image = imageVal ? full_url_for(imageVal) : ''
const datePublished = page.date.toISOString()
const dateModified = (page.updated || page.date).toISOString()
const author = page.copyright_author || config.author
const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || config.url
const authorHref = full_url_for(authorHrefVal)
const title = page.title
const url = page.permalink
const imageVal = page.cover_type === 'img' ? page.cover : theme.avatar.img
const image = imageVal ? full_url_for(imageVal) : ''
const datePublished = page.date.toISOString()
const dateModified = (page.updated || page.date).toISOString()
const author = page.copyright_author || config.author
const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || site.url;
const authorHref = full_url_for(authorHrefVal);
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"url": url,
"image": image,
"datePublished": datePublished,
"dateModified": dateModified,
"author": [{
"@type": "Person",
"name": author,
"url": authorHref
}]
}
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"url": url,
"image": image,
"datePublished": datePublished,
"dateModified": dateModified,
"author": [{
"@type": "Person",
"name": author,
"url": authorHref
}]
};
jsonLdScript = JSON.stringify(jsonLd, null, 2)
-
else if is_home() && (!page.current || page.current === 1)
-
// https://developers.google.com/search/docs/appearance/site-names#website
const baseUrl = config.url;
const currentPath = url_for('/');
const isRootOrSubdomain = currentPath.split('/').filter(Boolean).length === 0;
if (isRootOrSubdomain) {
const domain = new URL(config.url).hostname;
const alternateNames = theme.structured_data.alternate_name || [];
if (config.subtitle) {
alternateNames.push(config.subtitle);
}
if (domain) {
alternateNames.push(domain);
}
const jsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": config.title,
"alternateName": alternateNames,
"url": full_url_for('/'),
}
jsonLdScript = JSON.stringify(jsonLd, null, 2)
}
-
jsonLdScript = JSON.stringify(jsonLd, null, 2);
-
script(type="application/ld+json").
!{jsonLdScript}

View File

@@ -5,13 +5,9 @@ nav#nav
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
if theme.nav.display_title
span.site-name=config.title
if globalPageType === 'post' && theme.nav.display_post_title
if globalPageType === 'post'
a.nav-page-title(href=url_for('/'))
span.site-name=(page.title || config.title)
span.site-name
i.fa-solid.fa-circle-arrow-left
span= ' ' + _p('post.back_to_home')
#menus
if theme.search.use
#search-button

View File

@@ -13,29 +13,7 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside
!=partial('includes/loading/index', {}, {cache: true})
if theme.background
if !Array.isArray(theme.background)
#web_bg.bg-animation(style=getBgPath(theme.background))
else
#web_bg.bg-animation
- const bgStyleArr = theme.background.map(getBgPath)
script.
(() => {
const arr = !{JSON.stringify(bgStyleArr)}
const webBgDiv = document.getElementById('web_bg')
const setRandomBg = () => {
webBgDiv.style = arr[Math.floor(Math.random() * arr.length)]
requestAnimationFrame(() => webBgDiv.classList.add('bg-animation'))
}
document.addEventListener('pjax:send', () => {
webBgDiv.style = ''
webBgDiv.classList.remove('bg-animation')
})
document.addEventListener('pjax:complete', setRandomBg)
document.addEventListener('DOMContentLoaded', setRandomBg)
})()
#web_bg(style=getBgPath(theme.background))
!=partial('includes/sidebar', {}, {cache: true})

View File

@@ -14,7 +14,6 @@ script.
const $body = document.body
const preloader = {
endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = ''
$loadingBox.classList.add('loaded')
},
@@ -25,15 +24,7 @@ script.
}
preloader.initLoading()
if (document.readyState === 'complete') {
preloader.endLoading()
} else {
window.addEventListener('load', preloader.endLoading)
document.addEventListener('DOMContentLoaded', preloader.endLoading)
// Add timeout protection: force end after 7 seconds
setTimeout(preloader.endLoading, 7000)
}
window.addEventListener('load', preloader.endLoading)
if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')

View File

@@ -1,6 +1,6 @@
mixin indexPostUI()
- const indexLayout = theme.index_layout
- const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? '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 postCover = article.cover
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
- const post_cover = article.cover
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
if postCover && theme.cover.index_enable
if post_cover && 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(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
else
div.post-bg(style=`background: ${postCover}`)
.recent-post-info(class=noCover)
div.post-bg(style=`background: ${post_cover}`)
.recent-post-info(class=no_cover)
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 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)
- 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)
if theme.post_meta.page.categories && article.categories.data.length > 0
span.article-meta
span.article-meta-separator |
@@ -69,10 +69,7 @@ mixin indexPostUI()
span.article-meta-label= ' ' + _p('card_post_count')
if theme.comments.card_post_count && theme.comments.use
- const commentSystem = theme.comments.use[0]
- const commentLink = url_for(link) + '#post-comment'
case commentSystem
case theme.comments.use[0]
when 'Disqus'
when 'Disqusjs'
+countBlockInIndex
@@ -80,30 +77,30 @@ mixin indexPostUI()
i.fa-solid.fa-spinner.fa-spin
when 'Valine'
+countBlockInIndex
a(href=commentLink)
a(href=url_for(link) + '#post-comment')
span.valine-comment-count(data-xid=url_for(link))
i.fa-solid.fa-spinner.fa-spin
when 'Waline'
+countBlockInIndex
a(href=commentLink)
a(href=url_for(link) + '#post-comment')
span.waline-comment-count(data-path=url_for(link))
i.fa-solid.fa-spinner.fa-spin
when 'Twikoo'
+countBlockInIndex
a.twikoo-count(href=commentLink)
a.twikoo-count(href=url_for(link) + '#post-comment')
i.fa-solid.fa-spinner.fa-spin
when 'Facebook Comments'
+countBlockInIndex
a(href=commentLink)
a(href=url_for(link) + '#post-comment')
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
when 'Remark42'
+countBlockInIndex
a(href=commentLink)
a(href=url_for(link) + '#post-comment')
span.remark42__counter(data-url=urlNoIndex(article.permalink))
i.fa-solid.fa-spinner.fa-spin
when 'Artalk'
+countBlockInIndex
a(href=commentLink)
a(href=url_for(link) + '#post-comment')
span.artalk-count(data-page-key=url_for(link))
i.fa-solid.fa-spinner.fa-spin

View File

@@ -9,47 +9,40 @@
- page.toc = false
#article-container
if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
if page.comments !== false && theme.comments.use
- commentsJsLoad = true
if page.comments !== false && theme.comments.use
- commentsJsLoad = true
script.
(() => {
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
script.
(() => {
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
const runDestroy = (shuoshuoComment) => {
if (!shuoshuoComment) return
const runDestroy = (shuoshuoComment) => {
if (!shuoshuoComment) return
for (const [key, fn] of Object.entries(shuoshuoComment)) {
if (key.startsWith('destroy')) fn()
}
for (const [key, fn] of Object.entries(shuoshuoComment)) {
if (key.startsWith('destroy')) fn()
}
}
window.addCommentToShuoshuo = e => {
const btn = e.target.closest('.shuoshuo-comment-btn')
if (!btn) return
window.addCommentToShuoshuo = e => {
const btn = e.target.closest('.shuoshuo-comment-btn')
if (!btn) return
const ele = btn.closest('.container').nextElementSibling
const { shuoshuoComment } = window
const isInclude = ele.classList.contains('no-comment')
runDestroy(shuoshuoComment)
if (isInclude) {
ele.classList.remove('no-comment')
ele.innerHTML = commentDiv
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
btf.switchComments(ele, key)
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
}
const ele = btn.closest('.container').nextElementSibling
const { shuoshuoComment } = window
const isInclude = ele.classList.contains('no-comment')
runDestroy(shuoshuoComment)
if (isInclude) {
ele.classList.remove('no-comment')
ele.innerHTML = commentDiv
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
btf.switchComments(ele, key)
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
}
})()
}
})()
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
if !page.shuoshuo_url
script(type='application/json' id='shuoshuo-data')!= safeJSON(localDate)
- const { enable, native, placeholder, field } = theme.lazyload
if page.shuoshuo_url
script.
(() => {
const limitConfig = !{ JSON.stringify(page.limit || {}) }
@@ -85,248 +78,111 @@
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
const addLazyload = str => {
const config = {
enable: !{Boolean(enable)},
native: !{Boolean(native)},
field: '!{field}',
placeholder: '!{url_for(placeholder)}',
}
if (!config.enable || config.field !== 'site') return str
const parser = new DOMParser()
const doc = parser.parseFromString(str, 'text/html')
const images = doc.querySelectorAll('img')
images.forEach(img => {
if (config.native) {
img.setAttribute('loading', 'lazy')
} else {
const src = img.getAttribute('src')
img.setAttribute('data-lazy-src', src)
if (config.placeholder) {
img.setAttribute('src', config.placeholder)
} else {
img.removeAttribute('src')
}
}
})
return doc.body.innerHTML
}
let currentPage = 1
const itemsPerPage = 8
let totalPages = 0
let data = []
let inputEventsAttached = false // Flag to mark if input event listeners have been added
const renderData = (dataSlice) => {
const content = dataSlice.map(item => {
const formattedDate = formatToTimeZone(item.date)
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
const commentButton = item.key && !{commentsJsLoad}
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
<i class="fa-solid fa-comments"></i>
</div>`
: ''
const commentContainer = item.key
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
: ''
return `
<div class="shuoshuo-item">
<div class="container">
<div class="shuoshuo-item-header">
<div class="shuoshuo-avatar">
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
</div>
<div class="shuoshuo-info">
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
<time class="shuoshuo-date" title="${formattedDate}">
${btf.diffDate(formattedDate, true)}
</time>
</div>
</div>
<div class="shuoshuo-content">${addLazyload(item.content)}</div>
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
${commentButton}
</div>
</div>
${commentContainer}
</div>`
}).join('')
const container = document.getElementById('article-container')
container.innerHTML = content
window.lazyLoadInstance && window.lazyLoadInstance.update()
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
}
const renderNavigation = () => {
const container = document.getElementById('article-container')
const existingNav = container.nextElementSibling
if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) {
existingNav.remove()
}
const pageInfoTemplate = '#{__('pagination.page_info')}'
const pageInfoText = pageInfoTemplate
.replace(/\$\{current}/g, currentPage)
.replace(/\$\{total}/g, totalPages)
const navHtml = `
<div class="shuoshuo-navigation">
<button onclick="window.shuoshuoPrevPage()" ${currentPage === 1 ? 'disabled' : ''}><i class="fa-solid fa-chevron-left"></i></button>
<span class="shuoshuo-page-info">${pageInfoText}</span>
<input type="number" class="shuoshuo-page-input" min="1" max="${totalPages}" placeholder="${currentPage}" onkeydown="window.shuoshuoHandleKeyDown(event)">
<button onclick="window.shuoshuoNextPage()" ${currentPage === totalPages ? 'disabled' : ''}><i class="fa-solid fa-chevron-right"></i></button>
</div>
`
container.insertAdjacentHTML('afterend', navHtml)
// Add input validation event listeners (only once)
if (!inputEventsAttached) {
setTimeout(() => {
const input = document.querySelector('.shuoshuo-page-input')
if (input) {
// Clear placeholder when clicking the input box
input.addEventListener('focus', (event) => {
event.target.placeholder = ''
})
// Restore placeholder if no content when losing focus
input.addEventListener('blur', (event) => {
if (!event.target.value.trim()) {
event.target.placeholder = currentPage
}
})
input.addEventListener('input', (event) => {
const value = parseInt(event.target.value) || 0
let wasInvalid = false
if (value > totalPages) {
event.target.value = totalPages
wasInvalid = true
} else if (value < 1 && event.target.value !== '') {
event.target.value = 1
wasInvalid = true
}
// If value is corrected, show red and shake effect
if (wasInvalid) {
event.target.classList.add('invalid')
setTimeout(() => {
event.target.classList.remove('invalid')
}, 500)
}
})
inputEventsAttached = true // Mark that event listeners have been added
}
}, 0)
}
}
const renderPage = (page) => {
const start = (page - 1) * itemsPerPage
const end = start + itemsPerPage
const pageData = data.slice(start, end)
renderData(pageData)
renderNavigation()
}
window.shuoshuoPrevPage = () => {
if (currentPage > 1) {
currentPage--
renderPage(currentPage)
}
}
window.shuoshuoNextPage = () => {
if (currentPage < totalPages) {
currentPage++
renderPage(currentPage)
}
}
window.shuoshuoGoToPage = (page) => {
if (typeof page === 'number') {
// Directly jump to the specified page
if (page >= 1 && page <= totalPages && page !== currentPage) {
currentPage = page
renderPage(currentPage)
}
} else {
// Get page from input box
const input = document.querySelector('.shuoshuo-page-input')
const inputValue = input.value.trim()
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
currentPage = inputPage
renderPage(currentPage)
} else if (inputValue === '') {
// If input box is empty, re-render current page (update placeholder)
renderPage(currentPage)
}
}
}
window.shuoshuoHandleKeyDown = (event) => {
const input = event.target
const value = input.value + event.key
// Allow delete, arrow keys, backspace, etc.
if (event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' ||
event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
event.key === 'Tab' || event.ctrlKey || event.metaKey) {
if (event.key === 'Enter') {
window.shuoshuoGoToPage()
}
return
}
// Only allow numbers
if (!/^\d$/.test(event.key)) {
event.preventDefault()
return
}
// Check if the value after input exceeds the range
const newValue = parseInt(value) || 0
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
event.preventDefault()
// Add red and shake effect
input.classList.add('invalid')
setTimeout(() => {
input.classList.remove('invalid')
}, 500)
}
}
const loadShuoshuo = async () => {
try {
let originData = []
if (!{Boolean(page.shuoshuo_url)}) {
const response = await fetch('!{url_for(page.shuoshuo_url)}')
originData = await response.json()
} else {
const dataElement = document.getElementById('shuoshuo-data')
originData = dataElement ? JSON.parse(dataElement.textContent) : []
const response = await fetch('!{url_for(page.shuoshuo_url)}')
let data = await response.json()
data = filterDataByLimit(sortDataByDate(data), limitConfig)
const container = document.getElementById('article-container')
let start = 0
const renderData = (dataSlice) => {
const content = dataSlice.map(item => {
const formattedDate = formatToTimeZone(item.date)
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
const commentButton = item.key && !{commentsJsLoad}
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
<i class="fa-solid fa-comments"></i>
</div>`
: ''
const commentContainer = item.key
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
: ''
return `
<div class="shuoshuo-item">
<div class="container">
<div class="shuoshuo-item-header">
<div class="shuoshuo-avatar">
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
</div>
<div class="shuoshuo-info">
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
<time class="shuoshuo-date" title="${formattedDate}">
${btf.diffDate(formattedDate, true)}
</time>
</div>
</div>
<div class="shuoshuo-content">${item.content}</div>
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
${commentButton}
</div>
</div>
${commentContainer}
</div>`
}).join('')
container.insertAdjacentHTML('beforeend', content)
window.lazyLoadInstance.update()
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
}
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
const handleIntersection = (entries) => {
if (!entries[0].isIntersecting) return
observer.unobserve(entries[0].target)
totalPages = Math.ceil(data.length / itemsPerPage)
const slice = data.slice(start, start + 10)
renderData(slice)
start += 10
renderPage(currentPage)
if (start < data.length) {
setTimeout(() => observer.observe(container.lastElementChild), 100)
} else {
observer.disconnect()
}
};
const observer = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: '0px',
threshold: 1.0
})
renderData(data.slice(start, 10))
start += 10
if (container.lastElementChild) observer.observe(container.lastElementChild)
} catch (error) {
console.error(error)
}
};
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
})()
})()
else
if site.data.shuoshuo
each i in shuoshuoFN(site.data.shuoshuo, page)
.shuoshuo-item
.container
.shuoshuo-item-header
.shuoshuo-avatar
img.no-lightbox(src=i.avatar || url_for(theme.avatar.img))
.shuoshuo-info
.shuoshuo-author=i.author || config.author
time.shuoshuo-date(title=i.date)=i.date
.shuoshuo-content
!=markdown(i.content)
.shuoshuo-footer(class=i.tags && i.tags.length ? 'flex-between' : 'flex-end')
if i.tags
.shuoshuo-tags
each tag in i.tags
span.shuoshuo-tag=tag
if i.key && commentsJsLoad
.shuoshuo-comment-btn(onclick='addCommentToShuoshuo(event)')
i.fa-solid.fa-comments
if i.key && commentsJsLoad
.shuoshuo-comment.no-comment(data-key=i.key)

View File

@@ -1,2 +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', custom_colors: page.custom_colors})
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'})

View File

@@ -1,38 +1,37 @@
if page.total !== 1
-
var options = {
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
next_text: '<i class="fas fa-chevron-right fa-fw"></i>',
mid_size: 1,
escape: false
}
-
var options = {
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
next_text: '<i class="fas fa-chevron-right fa-fw"></i>',
mid_size: 1,
escape: false
}
if globalPageType === 'post'
- let paginationOrder = theme.post_pagination === 2 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
if globalPageType === 'post'
- let paginationOrder = theme.post_pagination === 1 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
nav#pagination.pagination-post
each direction, key in paginationOrder
if direction
- const getPostDesc = direction.postDesc || postDesc(direction)
- let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width')
- className = getPostDesc ? className : className + ' no-desc'
nav#pagination.pagination-post
each direction, key in paginationOrder
if direction
- const getPostDesc = direction.postDesc || postDesc(direction)
- let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width')
- className = getPostDesc ? className : className + ' no-desc'
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
if direction.cover_type === 'img'
img.cover(src=url_for(direction.pagination_cover || direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
else
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
if direction.cover_type === 'img'
img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
else
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
.info(class=key === 'prev' ? '' : 'text-right')
.info-1
.info-item-1=_p(`pagination.${key}`)
.info-item-2!=direction.title
if getPostDesc
.info-2
.info-item-1!=getPostDesc
else
nav#pagination
.pagination
if globalPageType === 'home'
- options.format = 'page/%d/#content-inner'
!=paginator(options)
.info(class=key === 'prev' ? '' : 'text-right')
.info-1
.info-item-1=_p(`pagination.${key}`)
.info-item-2!=direction.title
if getPostDesc
.info-2
.info-item-1!=getPostDesc
else
nav#pagination
.pagination
if globalPageType === 'home'
- options.format = 'page/%d/#content-inner'
!=paginator(options)

View File

@@ -1,5 +1,4 @@
- const { readmode, translate, darkmode, aside, chat } = theme
mixin rightsideItem(array)
each item in array
case item
@@ -31,22 +30,30 @@ mixin rightsideItem(array)
a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))
i.fas.fa-comments
- const { enable, hide, show } = theme.rightside_item_order
- const hideArray = enable && hide ? hide.split(',') : ['readmode','translate','darkmode','hideAside']
- const showArray = enable && show ? show.split(',') : ['toc','chat','comment']
- const needCogBtn = (enable && hide) || (!enable && ((globalPageType === 'post' && (readmode || translate.enable || (darkmode.enable && darkmode.button))) || (translate.enable || (darkmode.enable && darkmode.button))))
#rightside
- const { enable, hide, show } = theme.rightside_item_order
- const hideArray = enable ? hide && hide.split(',') : ['readmode','translate','darkmode','hideAside']
- const showArray = enable ? show && show.split(',') : ['toc','chat','comment']
#rightside-config-hide
if hideArray.length
if hideArray
+rightsideItem(hideArray)
#rightside-config-show
if needCogBtn
button#rightside-config(type="button" title=_p("rightside.setting"))
i.fas.fa-cog(class=theme.rightside_config_animation ? 'fa-spin' : '')
if enable
if hide
button#rightside-config(type="button" title=_p("rightside.setting"))
i.fas.fa-cog.fa-spin
else
if globalPageType === 'post'
if (readmode || translate.enable || (darkmode.enable && darkmode.button))
button#rightside-config(type="button" title=_p("rightside.setting"))
i.fas.fa-cog.fa-spin
else if translate.enable || (darkmode.enable && darkmode.button)
button#rightside-config(type="button" title=_p("rightside.setting"))
i.fas.fa-cog.fa-spin
if showArray.length
if showArray
+rightsideItem(showArray)
button#go-up(type="button" title=_p("rightside.back_to_top"))

View File

@@ -1,46 +1,17 @@
script.
(() => {
const abcjsInit = () => {
const abcjsFn = () => {
setTimeout(() => {
const sheets = document.querySelectorAll(".abc-music-sheet")
for (let i = 0; i < sheets.length; i++) {
const ele = sheets[i]
if (ele.children.length > 0) continue
// Parse parameters from data-params attribute
let params = {}
const dp = ele.getAttribute("data-params")
if (dp) {
try {
params = JSON.parse(dp)
} catch (e) {
console.error("Failed to parse data-params:", e)
}
}
// Merge parsed parameters with the responsive option
// Ensures params content appears before responsive
const options = { ...params, responsive: "resize" }
// Render the music score using ABCJS.renderAbc
ABCJS.renderAbc(ele, ele.textContent, options)
}
}, 100)
}
if (typeof ABCJS === "object") {
abcjsFn()
} else {
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
}
const abcjsFn = () => setTimeout(() => {
document.querySelectorAll(".abc-music-sheet").forEach(ele => {
if (ele.children.length > 0) return
ABCJS.renderAbc(ele, ele.innerHTML, {responsive: 'resize'})
})
}, 100)
typeof ABCJS === 'object' ? abcjsFn()
: btf.getScript('!{url_for(theme.asset.abcjs_basic_js)}').then(abcjsFn)
}
if (window.pjax) {
abcjsInit()
} else {
window.addEventListener("load", abcjsInit)
}
btf.addGlobalFn("encrypt", abcjsInit, "abcjs")
})()
window.pjax ? abcjsInit() : window.addEventListener('load', abcjsInit)
btf.addGlobalFn('encrypt', abcjsInit, 'abcjs')
})()

View File

@@ -1,5 +1,5 @@
script.
(() => {
(() => {
const getCommentUrl = () => {
const eleGroup = document.querySelectorAll('#recent-posts .article-title')
let urlArray = []
@@ -18,9 +18,7 @@ script.
includeReply: false
}).then(function (res) {
document.querySelectorAll('#recent-posts .twikoo-count').forEach((item,index) => {
if (res[index]) {
item.textContent = res[index].count
}
item.textContent = res[index].count
})
}).catch(function (err) {
console.log(err)

View File

@@ -51,8 +51,8 @@ script.
const loadArtalk = async (el, pageKey) => {
if (typeof Artalk === 'object') initArtalk(el, pageKey)
else {
await btf.getCSS('!{url_for(theme.asset.artalk_css)}')
await btf.getScript('!{url_for(theme.asset.artalk_js)}')
await btf.getCSS('!{theme.asset.artalk_css}')
await btf.getScript('!{theme.asset.artalk_js}')
initArtalk(el, pageKey)
}
}

View File

@@ -5,7 +5,7 @@ hr.custom-hr
.comment-headline
i.fas.fa-comments.fa-fw
span= ' ' + _p('comment')
if theme.comments.use.length > 1
.comment-switch
span.first-comment=defaultComment

View File

@@ -37,7 +37,7 @@ script.
: window.loadOtherComment = loadLivere
return
}
if ('!{use[0]}' === 'Livere' || !!{lazyload}) {
if (!{lazyload}) btf.loadComment(document.getElementById('lv-container'), loadLivere)
else loadLivere()

View File

@@ -3,7 +3,7 @@
script.
(() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const options = !{JSON.stringify(option)}
const option = !{JSON.stringify(option)}
const loadScript = src => {
const script = document.createElement('script')
@@ -42,8 +42,8 @@ script.
host: '!{host}',
site_id: '!{siteId}',
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
...options,
url: isShuoshuo ? window.location.origin + path : (options && options.url) || window.location.origin + window.location.pathname
...option,
url: isShuoshuo ? window.location.origin + path : (option && option.url) || window.location.origin + window.location.pathname
}
if (window.REMARK42) {
@@ -68,7 +68,7 @@ script.
: window.loadOtherComment = loadRemark42
return
}
if ('!{theme.comments.use[0]}' === 'Remark42' || !!{theme.comments.lazyload}) {
if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('remark42'), loadRemark42)
else loadRemark42()

View File

@@ -1,50 +1,19 @@
//- Mathjax 4/5
//- Mathjax 3
- const { tags, enableMenu } = theme.math.mathjax
script.
(() => {
const loadMathjax = () => {
if (!window.MathJax) {
window.MathJax = {
loader: {
load: [
// Four font extension packages (optional)
//- '[tex]/bbm',
//- '[tex]/bboldx',
//- '[tex]/dsfont',
'[tex]/mhchem'
],
paths: {
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
//- // Four font extension packages (optional)
//- 'mathjax-bbm-extension': '[mathjax]/../@mathjax/mathjax-bbm-font-extension',
//- 'mathjax-bboldx-extension': '[mathjax]/../@mathjax/mathjax-bboldx-font-extension',
//- 'mathjax-dsfont-extension': '[mathjax]/../@mathjax/mathjax-dsfont-font-extension',
'mathjax-mhchem-extension': '[mathjax]/../@mathjax/mathjax-mhchem-font-extension'
}
},
output: {
font: 'mathjax-newcm',
},
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
tags: '!{tags}',
packages: {
'[+]': [
'mhchem'
]
}
},
chtml: {
scale: 1.1
},
options: {
enableMenu: !{enableMenu},
menuOptions: {
settings: {
enrich: false // Turn off Braille and voice narration text automatic generation
}
},
renderActions: {
findScript: [10, doc => {
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {

View File

@@ -1,292 +1,20 @@
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 = `<!doctype html><html><head><meta charset="utf-8" />
<style>
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
</style>
</head><body>${svgSource}</body></html>`
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 = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
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
}
const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
const mermaidID = `mermaid-${index}`
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
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))
})
@@ -320,4 +48,4 @@ script.
btf.addGlobalFn('encrypt', loadMermaid, 'mermaid')
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)
})()
})()

View File

@@ -14,9 +14,9 @@ if choose
else
- pjaxSelectors.unshift('meta[name="description"]')
script(src=url_for(theme.asset.pjax) defer)
script(src=url_for(theme.asset.pjax))
script.
document.addEventListener('DOMContentLoaded', () => {
(() => {
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
window.pjax = new Pjax({
@@ -29,13 +29,7 @@ script.
const triggerPjaxFn = (val) => {
if (!val) return
Object.values(val).forEach(fn => {
try {
fn()
} catch (err) {
console.debug('Pjax callback failed:', err)
}
})
Object.values(val).forEach(fn => fn())
}
document.addEventListener('pjax:send', () => {
@@ -65,9 +59,7 @@ script.
document.addEventListener('pjax:error', e => {
if (e.request.status === 404) {
!{theme.error_404 && theme.error_404.enable}
? pjax.loadUrl('!{url_for("/404.html")}')
: window.location.href = e.request.responseURL
window.location.href = e.request.responseURL
}
})
})
})()

View File

@@ -17,7 +17,7 @@ if (syntax_highlighter === 'prismjs' || enable) && !preprocess
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
})()
script(src=url_for(prismjs_js) defer)
script(src=url_for(prismjs_autoloader) defer)
script(src=url_for(prismjs_js))
script(src=url_for(prismjs_autoloader))
if (line_number)
script(src=url_for(prismjs_lineNumber_js) defer)
script(src=url_for(prismjs_lineNumber_js))

View File

@@ -2,33 +2,21 @@
.search-dialog
nav.search-nav
span.search-dialog-title= _p('search.title')
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
button.search-close-button
i.fas.fa-times
#algolia-search-input
.ais-SearchBox
form.ais-SearchBox-form(action="" role="search" novalidate="")
input.ais-SearchBox-input(type="search" placeholder=theme.search.placeholder || _p("search.input_placeholder") autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" maxlength="512" aria-label="Search")
button.ais-SearchBox-submit(type="submit" title="Submit the search query" style="display:none;")
svg.ais-SearchBox-submitIcon(width="10" height="10" viewBox="0 0 40 40" aria-hidden="true")
path(d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z")
.search-wrap
#algolia-search-input
hr
#algolia-search-results
#algolia-hits
#algolia-hits-empty(style="display:none;")
.ais-Hits(style="display:none;")
ol.ais-Hits-list
#algolia-pagination.ais-Pagination(style="display:none;")
ul.ais-Pagination-list
#algolia-pagination
#algolia-info
span.ais-Stats-text
a.algolia-poweredBy(href="https://www.algolia.com/?utm_source=algoliasearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby" target="_blank" aria-label="Search by Algolia" rel="noopener noreferrer")
svg.ais-PoweredBy-logo(height="1.2em" viewBox="0 0 572 64" style="width: auto;")
path(fill="#36395A" d="M16 48.3c-3.4 0-6.3-.6-8.7-1.7A12.4 12.4 0 0 1 1.9 42C.6 40 0 38 0 35.4h6.5a6.7 6.7 0 0 0 3.9 6c1.4.7 3.3 1.1 5.6 1.1 2.2 0 4-.3 5.4-1a7 7 0 0 0 3-2.4 6 6 0 0 0 1-3.4c0-1.5-.6-2.8-1.9-3.7-1.3-1-3.3-1.6-5.9-1.8l-4-.4c-3.7-.3-6.6-1.4-8.8-3.4a10 10 0 0 1-3.3-7.9c0-2.4.6-4.6 1.8-6.4a12 12 0 0 1 5-4.3c2.2-1 4.7-1.6 7.5-1.6s5.5.5 7.6 1.6a12 12 0 0 1 5 4.4c1.2 1.8 1.8 4 1.8 6.7h-6.5a6.4 6.4 0 0 0-3.5-5.9c-1-.6-2.6-1-4.4-1s-3.2.3-4.4 1c-1.1.6-2 1.4-2.6 2.4-.5 1-.8 2-.8 3.1a5 5 0 0 0 1.5 3.6c1 1 2.6 1.7 4.7 1.9l4 .3c2.8.2 5.2.8 7.2 1.8 2.1 1 3.7 2.2 4.9 3.8a9.7 9.7 0 0 1 1.7 5.8c0 2.5-.7 4.7-2 6.6a13 13 0 0 1-5.6 4.4c-2.4 1-5.2 1.6-8.4 1.6Zm35.6 0c-2.6 0-4.8-.4-6.7-1.3a13 13 0 0 1-4.7-3.5 17.1 17.1 0 0 1-3.6-10.4v-1c0-2 .3-3.8 1-5.6a13 13 0 0 1 7.3-8.3 15 15 0 0 1 6.3-1.4A13.2 13.2 0 0 1 64 24.3c1 2.2 1.6 4.6 1.6 7.2V34H39.4v-4.3h21.8l-1.8 2.2c0-2-.3-3.7-.9-5.1a7.3 7.3 0 0 0-2.7-3.4c-1.2-.7-2.7-1.1-4.6-1.1s-3.4.4-4.7 1.3a8 8 0 0 0-2.9 3.6c-.6 1.5-.9 3.3-.9 5.4 0 2 .3 3.7 1 5.3a7.9 7.9 0 0 0 2.8 3.7c1.3.8 3 1.3 5 1.3s3.8-.5 5.1-1.3c1.3-1 2.1-2 2.4-3.2h6a11.8 11.8 0 0 1-7 8.7 16 16 0 0 1-6.4 1.2ZM80 48c-2.2 0-4-.3-5.7-1a8.4 8.4 0 0 1-3.7-3.3 9.7 9.7 0 0 1-1.3-5.2c0-2 .5-3.8 1.5-5.2a9 9 0 0 1 4.3-3.1c1.8-.7 4-1 6.7-1H89v4.1h-7.5c-2 0-3.4.5-4.4 1.4-1 1-1.6 2.1-1.6 3.6s.5 2.7 1.6 3.6c1 1 2.5 1.4 4.4 1.4 1.1 0 2.2-.2 3.2-.7 1-.4 1.9-1 2.6-2 .6-1 1-2.4 1-4.2l1.7 2.1c-.2 2-.7 3.8-1.5 5.2a9 9 0 0 1-3.4 3.3 12 12 0 0 1-5.3 1Zm9.5-.7v-8.8h-1v-10c0-1.8-.5-3.2-1.4-4.1-1-1-2.4-1.4-4.2-1.4a142.9 142.9 0 0 0-10.2.4v-5.6a74.8 74.8 0 0 1 8.6-.4c3 0 5.5.4 7.5 1.2s3.4 2 4.4 3.6c1 1.7 1.4 4 1.4 6.7v18.4h-5Zm12.9 0V17.8h5v12.3h-.2c0-4.2 1-7.4 2.8-9.5a11 11 0 0 1 8.3-3.1h1v5.6h-2a9 9 0 0 0-6.3 2.2c-1.5 1.5-2.2 3.6-2.2 6.4v15.6h-6.4Zm34.4 1a15 15 0 0 1-6.6-1.3c-1.9-.9-3.4-2-4.7-3.5a15.5 15.5 0 0 1-2.7-5c-.6-1.7-1-3.6-1-5.4v-1c0-2 .4-3.8 1-5.6a15 15 0 0 1 2.8-4.9c1.3-1.5 2.8-2.6 4.6-3.5a16.4 16.4 0 0 1 13.3.2c2 1 3.5 2.3 4.8 4a12 12 0 0 1 2 6H144c-.2-1.6-1-3-2.2-4.1a7.5 7.5 0 0 0-5.2-1.7 8 8 0 0 0-4.7 1.3 8 8 0 0 0-2.8 3.6 13.8 13.8 0 0 0 0 10.3c.6 1.5 1.5 2.7 2.8 3.6s2.8 1.3 4.8 1.3c1.5 0 2.7-.2 3.8-.8a7 7 0 0 0 2.6-2c.7-1 1-2 1.2-3.2h6.2a11 11 0 0 1-2 6.2 15.1 15.1 0 0 1-11.8 5.5Zm19.7-1v-40h6.4V31h-1.3c0-3 .4-5.5 1.1-7.6a9.7 9.7 0 0 1 3.5-4.8A9.9 9.9 0 0 1 172 17h.3c3.5 0 6 1.1 7.9 3.5 1.7 2.3 2.6 5.7 2.6 10v16.8h-6.4V29.6c0-2.1-.6-3.8-1.8-5a6.4 6.4 0 0 0-4.8-1.8c-2 0-3.7.7-5 2a7.8 7.8 0 0 0-1.9 5.5v17h-6.4Zm63.8 1a12.2 12.2 0 0 1-10.9-6.2 19 19 0 0 1-1.8-7.3h1.4v12.5h-5.1v-40h6.4v19.8l-2 3.5c.2-3.1.8-5.7 1.9-7.7a11 11 0 0 1 4.4-4.5c1.8-1 3.9-1.5 6.1-1.5a13.4 13.4 0 0 1 12.8 9.1c.7 1.9 1 3.8 1 6v1c0 2.2-.3 4.1-1 6a13.6 13.6 0 0 1-13.2 9.4Zm-1.2-5.5a8.4 8.4 0 0 0 7.9-5c.7-1.5 1.1-3.3 1.1-5.3s-.4-3.8-1.1-5.3a8.7 8.7 0 0 0-3.2-3.6 9.6 9.6 0 0 0-9.2-.2 8.5 8.5 0 0 0-3.3 3.2c-.8 1.4-1.3 3-1.3 5v2.3a9 9 0 0 0 1.3 4.8 9 9 0 0 0 3.4 3c1.4.7 2.8 1 4.4 1Zm27.3 3.9-10-28.9h6.5l9.5 28.9h-6Zm-7.5 12.2v-5.7h4.9c1 0 2-.1 2.9-.4a4 4 0 0 0 2-1.4c.4-.7.9-1.6 1.2-2.7l8.6-30.9h6.2l-9.3 32.4a14 14 0 0 1-2.5 5 8.9 8.9 0 0 1-4 2.8c-1.5.6-3.4.9-5.6.9h-4.4Zm9-12.2v-5.2h6.4v5.2H248Z")
path(fill="#003DFF" d="M534.4 9.1H528a.8.8 0 0 1-.7-.7V1.8c0-.4.2-.7.6-.8l6.5-1c.4 0 .8.2.9.6v7.8c0 .4-.4.7-.8.7zM428 35.2V.8c0-.5-.3-.8-.7-.8h-.2l-6.4 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.5 0 .8-.4.8-.8V43c0-.4-.3-.7-.6-.8-4.5-.5-4.5-6-4.5-7zm106.5-21.8H528c-.4 0-.7.4-.7.8v34c0 .4.3.8.7.8h6.5c.4 0 .8-.4.8-.8v-34c0-.5-.4-.8-.8-.8zm-17.7 21.8V.8c0-.5-.3-.8-.8-.8l-6.5 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.4 0 .8-.4.8-.8V43c0-.4-.3-.7-.7-.8-4.4-.5-4.4-6-4.4-7zm-22.2-20.6a16.5 16.5 0 0 1 8.6 9.3c.8 2.2 1.3 4.8 1.3 7.5a19.4 19.4 0 0 1-4.6 12.6 14.8 14.8 0 0 1-5.2 3.6c-2 .9-5.2 1.4-6.8 1.4a21 21 0 0 1-6.7-1.4 15.4 15.4 0 0 1-8.6-9.3 21.3 21.3 0 0 1 0-14.4 15.2 15.2 0 0 1 8.6-9.3c2-.8 4.3-1.2 6.7-1.2s4.6.4 6.7 1.2zm-6.7 27.6c2.7 0 4.7-1 6.2-3s2.2-4.3 2.2-7.8-.7-6.3-2.2-8.3-3.5-3-6.2-3-4.7 1-6.1 3c-1.5 2-2.2 4.8-2.2 8.3s.7 5.8 2.2 7.8 3.5 3 6.2 3zm-88.8-28.8c-6.2 0-11.7 3.3-14.8 8.2a18.6 18.6 0 0 0 4.8 25.2c1.8 1.2 4 1.8 6.2 1.7s.1 0 .1 0h.9c4.2-.7 8-4 9.1-8.1v7.4c0 .4.3.7.8.7h6.4a.7.7 0 0 0 .7-.7V14.2c0-.5-.3-.8-.7-.8h-13.5zm6.3 26.5a9.8 9.8 0 0 1-5.7 2h-.5a10 10 0 0 1-9.2-14c1.4-3.7 5-6.3 9-6.3h6.4v18.3zm152.3-26.5h13.5c.5 0 .8.3.8.7v33.7c0 .4-.3.7-.8.7h-6.4a.7.7 0 0 1-.8-.7v-7.4c-1.2 4-4.8 7.4-9 8h-.1a4.2 4.2 0 0 1-.5.1h-.9a10.3 10.3 0 0 1-7-2.6c-4-3.3-6.5-8.4-6.5-14.2 0-3.7 1-7.2 3-10 3-5 8.5-8.3 14.7-8.3zm.6 28.4c2.2-.1 4.2-.6 5.7-2V21.7h-6.3a9.8 9.8 0 0 0-9 6.4 10.2 10.2 0 0 0 9.1 13.9h.5zM452.8 13.4c-6.2 0-11.7 3.3-14.8 8.2a18.5 18.5 0 0 0 3.6 24.3 10.4 10.4 0 0 0 13 .6c2.2-1.5 3.8-3.7 4.5-6.1v7.8c0 2.8-.8 5-2.2 6.3-1.5 1.5-4 2.2-7.5 2.2l-6-.3c-.3 0-.7.2-.8.5l-1.6 5.5c-.1.4.1.8.5 1h.1c2.8.4 5.5.6 7 .6 6.3 0 11-1.4 14-4.1 2.7-2.5 4.2-6.3 4.5-11.4V14.2c0-.5-.4-.8-.8-.8h-13.5zm6.3 8.2v18.3a9.6 9.6 0 0 1-5.6 2h-1a10.3 10.3 0 0 1-8.8-14c1.4-3.7 5-6.3 9-6.3h6.4zM291 31.5A32 32 0 0 1 322.8 0h30.8c.6 0 1.2.5 1.2 1.2v61.5c0 1.1-1.3 1.7-2.2 1l-19.2-17a18 18 0 0 1-11 3.4 18.1 18.1 0 1 1 18.2-14.8c-.1.4-.5.7-.9.6-.1 0-.3 0-.4-.2l-3.8-3.4c-.4-.3-.6-.8-.7-1.4a12 12 0 1 0-2.4 8.3c.4-.4 1-.5 1.6-.2l14.7 13.1v-46H323a26 26 0 1 0 10 49.7c.8-.4 1.6-.2 2.3.3l3 2.7c.3.2.3.7 0 1l-.2.2a32 32 0 0 1-47.2-28.6z")
.algolia-stats
.algolia-poweredBy
#search-mask
script(src=url_for(theme.asset.algolia_search))
script(src=url_for(theme.asset.instantsearch))
script(src=url_for(theme.asset.algolia_js))

View File

@@ -2,7 +2,7 @@
.search-dialog
nav.search-nav
span.search-dialog-title= _p('search.title')
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
span#loading-status
button.search-close-button
i.fas.fa-times
@@ -10,15 +10,13 @@
i.fas.fa-spinner.fa-pulse
span= ' ' + _p("search.load_data")
.local-search-input
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text")
hr
#local-search-results
#local-search-pagination.ais-Pagination(style="display:none;")
ul.ais-Pagination-list
#local-search-stats
.search-wrap
#local-search-input
.local-search-box
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input
hr
#local-search-results
#local-search-stats-wrap
#search-mask
script(src=url_for(theme.asset.local_search))

View File

@@ -1,5 +1,5 @@
- const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
- const { effect,source,sub,typed_option } = theme.subtitle
- let subContent = sub || new Array()
script.
window.typedJSFn = {
@@ -22,26 +22,6 @@ script.
} else {
subtitleType()
}
},
processSubtitle: (content, extraContents = []) => {
if (!{effect}) {
const sub = !{JSON.stringify(subContent)}.slice()
if (extraContents.length > 0) {
sub.unshift(...extraContents)
}
if (typeof content === 'string') {
sub.unshift(content)
} else if (Array.isArray(content)) {
sub.unshift(...content)
}
sub.length > 0 && typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = typeof content === 'string' ? content :
(Array.isArray(content) && content.length > 0 ? content[0] : '')
}
}
}
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
@@ -53,12 +33,14 @@ case source
fetch('https://v1.hitokoto.cn')
.then(response => response.json())
.then(data => {
const from = '出自 ' + data.from
typedJSFn.processSubtitle(data.hitokoto, [from])
})
.catch(err => {
console.error('Failed to get the Hitokoto API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
if (!{effect}) {
const from = '出自 ' + data.from
const sub = !{JSON.stringify(subContent)}
sub.unshift(data.hitokoto, from)
typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = data.hitokoto
}
})
}
typedJSFn.run(subtitleType)
@@ -66,48 +48,46 @@ case source
when 2
script.
function subtitleType () {
fetch('https://v.api.aa1.cn/api/yiyan/index.php')
.then(response => response.text())
.then(data => {
const reg = /<p>(.*?)<\/p>/g
const result = reg.exec(data)
if (result && result[1]) {
typedJSFn.processSubtitle(result[1])
} else {
throw new Error('Failed to parse the return value of the Yiyan API')
}
})
.catch(err => {
console.error('Failed to get the Yiyan API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
})
btf.getScript('https://yijuzhan.com/api/word.php?m=js').then(() => {
const con = str[0]
if (!{effect}) {
const from = '出自 ' + str[1]
const sub = !{JSON.stringify(subContent)}
sub.unshift(con, from)
typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = con
}
})
}
typedJSFn.run(subtitleType)
when 3
script.
function subtitleType () {
btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js')
.then(() => {
jinrishici.load(result => {
if (result && result.data && result.data.content) {
typedJSFn.processSubtitle(result.data.content)
} else {
throw new Error('Failed to parse the return value of Jinrishici API')
}
})
})
.catch(err => {
console.error('Failed to get the Jinrishici API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js').then(() => {
jinrishici.load(result =>{
if (!{effect}) {
const sub = !{JSON.stringify(subContent)}
const content = result.data.content
sub.unshift(content)
typedJSFn.init(sub)
} else {
document.getElementById('subtitle').textContent = result.data.content
}
})
})
}
typedJSFn.run(subtitleType)
default
if subContent.length > 0
script.
function subtitleType () {
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
- subContent = subContent.length ? subContent : new Array(config.subtitle)
script.
function subtitleType () {
if (!{effect}) {
typedJSFn.init(!{JSON.stringify(subContent)})
} else {
document.getElementById("subtitle").textContent = !{JSON.stringify(subContent[0])}
}
typedJSFn.run(subtitleType)
}
typedJSFn.run(subtitleType)

View File

@@ -1,4 +1,4 @@
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
- let { serverURL, website_id, option, UV_PV } = theme.umami_analytics
- const isServerURL = !!serverURL
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
@@ -9,51 +9,28 @@ script.
const config = !{JSON.stringify(UV_PV)}
const runTrack = () => {
if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
} else {
console.warn('Umami Analytics: umami.track is not available')
}
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
}
const loadUmamiJS = () => {
btf.getScript('!{baseURL}/!{script_name}', {
btf.getScript('!{baseURL}/script.js', {
'data-website-id': '!{website_id}',
'data-auto-track': 'false',
...option
}).then(() => {
runTrack()
}).catch(error => {
console.error('Umami Analytics: Error loading script', error)
})
}).then(runTrack)
}
const getData = async (isPost) => {
try {
const now = Date.now()
const keyUrl = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
const headerList = { 'Accept': 'application/json' }
if (!{isServerURL}) {
headerList['Authorization'] = `Bearer ${config.token}`
} else {
headerList['x-umami-api-key'] = config.token
}
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
method: "GET",
headers: headerList
})
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return await res.json()
} catch (error) {
console.error('Umami Analytics: Failed to fetch data', error)
throw error
}
const now = Date.now()
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
const headerList = { 'Accept': 'application/json' }
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
else headerList['x-umami-api-key'] = config.token
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
method: "GET",
headers: headerList
})
return await res.json()
}
const insertData = async () => {
@@ -62,49 +39,27 @@ script.
const pagePV = document.getElementById('umamiPV')
if (pagePV) {
const data = await getData(true)
if (data && data.pageviews) {
pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else {
console.warn('Umami Analytics: Invalid page view data received')
}
pagePV.textContent = data.pageviews.value
}
}
if (config.site_uv || config.site_pv) {
const data = await getData(false)
} else {
const data = (config.site_uv || config.site_pv) && await getData()
if (config.site_uv) {
const siteUV = document.getElementById('umami-site-uv')
if (siteUV && data && data.visitors) {
siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors
} else if (siteUV) {
console.warn('Umami Analytics: Invalid site UV data received')
}
if (siteUV) siteUV.textContent = data.visitors.value
}
if (config.site_pv) {
const sitePV = document.getElementById('umami-site-pv')
if (sitePV && data && data.pageviews) {
sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else if (sitePV) {
console.warn('Umami Analytics: Invalid site PV data received')
}
if (sitePV) sitePV.textContent = data.pageviews.value
}
}
} catch (error) {
console.error('Umami Analytics: Failed to insert data', error)
} catch (e) {
console.error('Failed to load Umami Analytics:', e)
}
}
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
loadUmamiJS()
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData)
} else {
setTimeout(insertData, 100)
}
insertData()
})()

View File

@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
i.fas.fa-tags
span= _p('aside.card_tags')
- let { limit, orderby, order, custom_colors } = theme.aside.card_tags
- let { limit, orderby, order } = 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', custom_colors: custom_colors})
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em'})
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'})

View File

@@ -1,6 +1,6 @@
{
"name": "hexo-theme-butterfly",
"version": "5.5.5-b1",
"version": "5.3.3",
"description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json",
"scripts": {
@@ -15,18 +15,16 @@
"hexo-theme-butterfly"
],
"repository": {
"type": "git",
"url": "https://github.com/jerryc127/hexo-theme-butterfly.git"
"type" : "git",
"url" : "https://github.com/jerryc127/hexo-theme-butterfly.git"
},
"bugs": {
"url": "https://github.com/jerryc127/hexo-theme-butterfly/issues",
"email": "my@crazywong.com"
},
"dependencies": {
"hexo-renderer-pug": "^3.0.0",
"hexo-renderer-stylus": "^3.0.1",
"hexo-util": "^4.0.0",
"moment-timezone": "^0.6.0"
"hexo-renderer-pug": "^3.0.0"
},
"homepage": "https://butterfly.js.org/",
"author": "Jerry <my@crazywong.com>",

View File

@@ -1,15 +1,15 @@
abcjs_basic_js:
name: abcjs
file: dist/abcjs-basic-min.js
version: 6.6.0
version: 6.4.4
activate_power_mode:
name: butterfly-extsrc
file: dist/activate-power-mode.min.js
version: 1.1.6
version: 1.1.4
algolia_search:
name: algoliasearch
file: dist/lite/builds/browser.umd.js
version: 5.47.0
version: 5.20.2
aplayer_css:
name: aplayer
file: dist/APlayer.min.css
@@ -33,69 +33,69 @@ blueimp_md5:
canvas_fluttering_ribbon:
name: butterfly-extsrc
file: dist/canvas-fluttering-ribbon.min.js
version: 1.1.6
version: 1.1.4
canvas_nest:
name: butterfly-extsrc
file: dist/canvas-nest.min.js
version: 1.1.6
version: 1.1.4
canvas_ribbon:
name: butterfly-extsrc
file: dist/canvas-ribbon.min.js
version: 1.1.6
version: 1.1.4
chartjs:
name: chart.js
file: dist/chart.umd.js
version: 4.5.1
version: 4.4.7
clickShowText:
name: butterfly-extsrc
file: dist/click-show-text.min.js
version: 1.1.6
version: 1.1.4
click_heart:
name: butterfly-extsrc
file: dist/click-heart.min.js
version: 1.1.6
version: 1.1.4
disqusjs:
name: disqusjs
file: dist/browser/disqusjs.es2015.umd.min.js
version: 3.2.1
version: 3.0.2
disqusjs_css:
name: disqusjs
file: dist/browser/styles/disqusjs.css
version: 3.2.1
version: 3.0.2
docsearch_css:
name: '@docsearch/css'
other_name: docsearch-css
file: dist/style.css
version: 4.5.3
version: 3.8.3
docsearch_js:
name: '@docsearch/js'
other_name: docsearch-js
file: dist/umd/index.js
version: 4.5.3
version: 3.8.3
egjs_infinitegrid:
name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid
file: dist/infinitegrid.min.js
version: 4.13.0
version: 4.12.0
fancybox:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js
version: 6.1.9
version: 5.0.36
other_name: fancyapps-ui
fancybox_css:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css
version: 6.1.9
version: 5.0.36
other_name: fancyapps-ui
fireworks:
name: butterfly-extsrc
file: dist/fireworks.min.js
version: 1.1.6
version: 1.1.4
fontawesome:
name: '@fortawesome/fontawesome-free'
file: css/all.min.css
other_name: font-awesome
version: 7.1.0
version: 6.7.2
gitalk:
name: gitalk
file: dist/gitalk.min.js
@@ -108,24 +108,28 @@ instantpage:
name: instant.page
file: instantpage.js
version: 5.2.0
instantsearch:
name: instantsearch.js
file: dist/instantsearch.production.min.js
version: 4.77.3
katex:
name: katex
file: dist/katex.min.css
other_name: KaTeX
version: 0.16.28
version: 0.16.21
katex_copytex:
name: katex
file: dist/contrib/copy-tex.min.js
other_name: KaTeX
version: 0.16.28
version: 0.16.21
lazyload:
name: vanilla-lazyload
file: dist/lazyload.iife.min.js
version: 19.1.3
mathjax:
name: mathjax
file: tex-mml-chtml.js
version: 4.1.0
file: es5/tex-mml-chtml.js
version: 3.2.2
medium_zoom:
name: medium-zoom
file: dist/medium-zoom.min.js
@@ -133,11 +137,11 @@ medium_zoom:
mermaid:
name: mermaid
file: dist/mermaid.min.js
version: 11.12.2
version: 11.4.1
meting_js:
name: butterfly-extsrc
file: metingjs/dist/Meting.min.js
version: 1.1.6
version: 1.1.4
pace_default_css:
name: pace-js
other_name: pace
@@ -156,25 +160,25 @@ prismjs_autoloader:
name: prismjs
file: plugins/autoloader/prism-autoloader.min.js
other_name: prism
version: 1.30.0
version: 1.29.0
prismjs_js:
name: prismjs
file: prism.js
other_name: prism
version: 1.30.0
version: 1.29.0
prismjs_lineNumber_js:
name: prismjs
file: plugins/line-numbers/prism-line-numbers.min.js
other_name: prism
version: 1.30.0
version: 1.29.0
sharejs:
name: butterfly-extsrc
file: sharejs/dist/js/social-share.min.js
version: 1.1.6
version: 1.1.4
sharejs_css:
name: butterfly-extsrc
file: sharejs/dist/css/share.min.css
version: 1.1.6
version: 1.1.4
snackbar:
name: node-snackbar
file: dist/snackbar.min.js
@@ -186,11 +190,11 @@ snackbar_css:
twikoo:
name: twikoo
file: dist/twikoo.all.min.js
version: 1.6.44
version: 1.6.41
typed:
name: typed.js
file: dist/typed.umd.js
version: 3.0.0
version: 2.1.0
valine:
name: valine
file: dist/Valine.min.js
@@ -199,9 +203,9 @@ waline_css:
name: '@waline/client'
file: dist/waline.css
other_name: waline
version: 3.8.0
version: 3.5.2
waline_js:
name: '@waline/client'
file: dist/waline.js
other_name: waline
version: 3.8.0
version: 3.5.2

View File

@@ -1,601 +0,0 @@
// Butterfly 主題默認配置
// Default configuration for Butterfly theme
module.exports = {
nav: {
logo: null,
display_title: true,
display_post_title: true,
fixed: false
},
menu: null,
code_blocks: {
theme: 'light',
macStyle: false,
height_limit: false,
word_wrap: false,
copy: true,
language: true,
shrink: false,
fullpage: false
},
social: null,
favicon: '/img/favicon.png',
avatar: {
img: '/img/butterfly-icon.png',
effect: false
},
disable_top_img: false,
default_top_img: null,
index_img: null,
archive_img: null,
tag_img: null,
tag_per_img: null,
category_img: null,
category_per_img: null,
footer_img: false,
background: null,
cover: {
index_enable: true,
aside_enable: true,
archives_enable: true,
default_cover: null
},
error_img: {
flink: '/img/friend_404.gif',
post_page: '/img/404.jpg'
},
error_404: {
enable: false,
subtitle: 'Page Not Found',
background: '/img/error-page.png'
},
post_meta: {
page: {
date_type: 'created',
date_format: 'date',
categories: true,
tags: false,
label: true
},
post: {
position: 'left',
date_type: 'both',
date_format: 'date',
categories: true,
tags: true,
label: true
}
},
index_site_info_top: null,
index_top_img_height: null,
subtitle: {
enable: false,
effect: true,
typed_option: null,
source: false,
sub: null
},
index_layout: 3,
index_post_content: {
method: 3,
length: 500
},
toc: {
post: true,
page: false,
number: true,
expand: false,
style_simple: false,
scroll_percent: true
},
post_copyright: {
enable: true,
decode: false,
author_href: null,
license: 'CC BY-NC-SA 4.0',
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
},
reward: {
enable: false,
text: null,
QR_code: null
},
post_edit: {
enable: false,
url: null
},
related_post: {
enable: true,
limit: 6,
date_type: 'created'
},
post_pagination: 1,
noticeOutdate: {
enable: false,
style: 'flat',
limit_day: 365,
position: 'top',
message_prev: 'It has been',
message_next: 'days since the last update, the content of the article may be outdated.'
},
footer: {
nav: null,
owner: {
enable: true,
since: 2025
},
copyright: {
enable: true,
version: true
},
custom_text: null
},
aside: {
enable: true,
hide: false,
button: true,
mobile: true,
position: 'right',
display: {
archive: true,
tag: true,
category: true
},
card_author: {
enable: true,
description: null,
button: {
enable: true,
icon: 'fab fa-github',
text: 'Follow Me',
link: 'https://github.com/xxxxxx'
}
},
card_announcement: {
enable: true,
content: 'This is my Blog'
},
card_recent_post: {
enable: true,
limit: 5,
sort: 'date',
sort_order: null
},
card_newest_comments: {
enable: false,
sort_order: null,
limit: 6,
storage: 10,
avatar: true
},
card_categories: {
enable: true,
limit: 8,
expand: 'none',
sort_order: null
},
card_tags: {
enable: true,
limit: 40,
color: false,
custom_colors: null,
orderby: 'random',
order: 1,
sort_order: null
},
card_archives: {
enable: true,
type: 'monthly',
format: 'MMMM YYYY',
order: -1,
limit: 8,
sort_order: null
},
card_post_series: {
enable: true,
series_title: false,
orderBy: 'date',
order: -1
},
card_webinfo: {
enable: true,
post_count: true,
last_push_date: true,
sort_order: null,
runtime_date: null
}
},
rightside_bottom: null,
translate: {
enable: false,
default: '繁',
defaultEncoding: 2,
translateDelay: 0,
msgToTraditionalChinese: '繁',
msgToSimplifiedChinese: '簡'
},
readmode: true,
darkmode: {
enable: true,
button: true,
autoChangeMode: false,
start: null,
end: null
},
rightside_scroll_percent: false,
rightside_item_order: {
enable: false,
hide: null,
show: null
},
rightside_config_animation: true,
anchor: {
auto_update: false,
click_to_scroll: false
},
photofigcaption: false,
copy: {
enable: true,
copyright: {
enable: false,
limit_count: 150
}
},
wordcount: {
enable: false,
post_wordcount: true,
min2read: true,
total_wordcount: true
},
busuanzi: {
site_uv: true,
site_pv: true,
page_pv: true
},
math: {
use: null,
per_page: true,
hide_scrollbar: false,
mathjax: {
enableMenu: true,
tags: 'none'
},
katex: {
copy_tex: false
}
},
search: {
use: null,
placeholder: null,
algolia_search: {
hitsPerPage: 6
},
local_search: {
preload: false,
top_n_per_article: 1,
unescape: false,
pagination: {
enable: false,
hitsPerPage: 8
},
CDN: null
},
docsearch: {
appId: null,
apiKey: null,
indexName: null,
option: null
}
},
share: {
use: 'sharejs',
sharejs: {
sites: 'facebook,x,wechat,weibo,qq'
},
addtoany: {
item: 'facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link'
}
},
comments: {
use: null,
text: true,
lazyload: false,
count: false,
card_post_count: false
},
disqus: {
shortname: null,
apikey: null
},
disqusjs: {
shortname: null,
apikey: null,
option: null
},
livere: {
uid: null
},
gitalk: {
client_id: null,
client_secret: null,
repo: null,
owner: null,
admin: null,
option: null
},
valine: {
appId: null,
appKey: null,
avatar: 'monsterid',
serverURLs: null,
bg: null,
visitor: false,
option: null
},
waline: {
serverURL: null,
bg: null,
pageview: false,
option: null
},
utterances: {
repo: null,
issue_term: 'pathname',
light_theme: 'github-light',
dark_theme: 'photon-dark',
js: null,
option: null
},
facebook_comments: {
app_id: null,
user_id: null,
pageSize: 10,
order_by: 'social',
lang: 'en_US'
},
twikoo: {
envId: null,
region: null,
visitor: false,
option: null
},
giscus: {
repo: null,
repo_id: null,
category_id: null,
light_theme: 'light',
dark_theme: 'dark',
js: null,
option: null
},
remark42: {
host: null,
siteId: null,
option: null
},
artalk: {
server: null,
site: null,
visitor: false,
option: null
},
chat: {
use: null,
rightside_button: false,
button_hide_show: false
},
chatra: {
id: null
},
tidio: {
public_key: null
},
crisp: {
website_id: null
},
google_tag_manager: {
tag_id: null,
domain: 'https://www.googletagmanager.com'
},
baidu_analytics: null,
google_analytics: null,
cloudflare_analytics: null,
microsoft_clarity: null,
umami_analytics: {
enable: false,
serverURL: null,
script_name: 'script.js',
website_id: null,
option: null,
UV_PV: {
site_uv: false,
site_pv: false,
page_pv: false,
token: null
}
},
google_adsense: {
enable: false,
auto_ads: true,
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
client: null,
enable_page_level_ads: true
},
ad: {
index: null,
aside: null,
post: null
},
site_verification: null,
category_ui: null,
tag_ui: null,
rounded_corners_ui: true,
text_align_justify: false,
mask: {
header: true,
footer: true
},
preloader: {
enable: false,
source: 1,
pace_css_url: null
},
enter_transitions: true,
display_mode: 'light',
beautify: {
enable: false,
field: 'post',
title_prefix_icon: null,
title_prefix_icon_color: null
},
font: {
global_font_size: null,
code_font_size: null,
font_family: null,
code_font_family: null
},
blog_title_font: {
font_link: null,
font_family: null
},
hr_icon: {
enable: true,
icon: null,
icon_top: null
},
activate_power_mode: {
enable: false,
colorful: true,
shake: true,
mobile: false
},
canvas_ribbon: {
enable: false,
size: 150,
alpha: 0.6,
zIndex: -1,
click_to_change: false,
mobile: false
},
canvas_fluttering_ribbon: {
enable: false,
mobile: false
},
canvas_nest: {
enable: false,
color: '0,0,255',
opacity: 0.7,
zIndex: -1,
count: 99,
mobile: false
},
fireworks: {
enable: false,
zIndex: 9999,
mobile: false
},
click_heart: {
enable: false,
mobile: false
},
clickShowText: {
enable: false,
text: null,
fontSize: '15px',
random: false,
mobile: false
},
lightbox: null,
series: {
enable: false,
orderBy: 'title',
order: 1,
number: true
},
abcjs: {
enable: false,
per_page: true
},
mermaid: {
enable: false,
code_write: false,
theme: {
light: 'default',
dark: 'dark'
},
open_in_new_tab: true,
zoom_pan: true
},
chartjs: {
enable: false,
fontColor: {
light: 'rgba(0, 0, 0, 0.8)',
dark: 'rgba(255, 255, 255, 0.8)'
},
borderColor: {
light: 'rgba(0, 0, 0, 0.1)',
dark: 'rgba(255, 255, 255, 0.2)'
},
scale_ticks_backdropColor: {
light: 'transparent',
dark: 'transparent'
}
},
note: {
style: 'flat',
icons: true,
border_radius: 3,
light_bg_offset: 0
},
pjax: {
enable: false,
exclude: null
},
aplayerInject: {
enable: false,
per_page: true
},
snackbar: {
enable: false,
position: 'bottom-left',
bg_light: '#49b1f5',
bg_dark: '#1f1f1f'
},
instantpage: false,
lazyload: {
enable: false,
native: false,
field: 'site',
placeholder: null,
blur: false
},
pwa: {
enable: false,
manifest: null,
apple_touch_icon: null,
favicon_32_32: null,
favicon_16_16: null,
mask_icon: null
},
Open_Graph_meta: {
enable: true,
option: null
},
structured_data: {
enable: false,
alternate_name: null
},
css_prefix: true,
inject: {
head: null,
bottom: null
},
CDN: {
internal_provider: 'local',
third_party_provider: 'jsdelivr',
version: true,
custom_format: null,
option: null
}
}

View File

@@ -3,14 +3,13 @@
const { stripHTML, truncate } = require('hexo-util')
// Truncates the given content to a specified length, removing HTML tags and replacing newlines with spaces.
const truncateContent = (content, length, encrypt = false) => {
if (!content || encrypt) return ''
return truncate(stripHTML(content).replace(/\n/g, ' '), { length })
const truncateContent = (content, length) => {
return truncate(stripHTML(content), { length, separator: ' ' }).replace(/\n/g, ' ')
}
// Generates a post description based on the provided data and theme configuration.
const postDesc = (data, hexo) => {
const { description, content, postDesc, encrypt } = data
const { description, content, postDesc } = data
if (postDesc) return postDesc
@@ -24,10 +23,10 @@ const postDesc = (data, hexo) => {
result = description
break
case 2:
result = description || truncateContent(content, length, encrypt)
result = description || truncateContent(content, length)
break
default:
result = truncateContent(content, length, encrypt)
result = truncateContent(content, length)
}
data.postDesc = result

View File

@@ -46,43 +46,45 @@ hexo.extend.filter.register('before_generate', () => {
}
const createCDNLink = (data, type, cond = '') => {
return Object.keys(data).reduce((result, key) => {
let { name, version, file, other_name: otherName } = data[key]
const cdnjsName = otherName || name
const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '')
const minCdnjsFile = minFile(cdnjsFile)
Object.keys(data).forEach(key => {
let { name, version, file, other_name } = data[key]
const cdnjs_name = other_name || name
const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
const min_cdnjs_file = minFile(cdnjs_file)
if (cond === 'internal') file = `source/${file}`
const minFilePath = minFile(file)
const min_file = minFile(file)
const verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
const value = {
version,
name,
file,
cdnjs_file: cdnjsFile,
min_file: minFilePath,
min_cdnjs_file: minCdnjsFile,
cdnjs_name: cdnjsName
cdnjs_file,
min_file,
min_cdnjs_file,
cdnjs_name
}
const cdnSource = {
local: cond === 'internal' ? `${cdnjsFile + verType}` : `/pluginsSrc/${name}/${file + verType}`,
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${minFilePath}`,
local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjsName}/${version}/${minCdnjsFile}`,
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjs_name}/${version}/${min_cdnjs_file}`,
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
}
result[key] = cdnSource[type]
return result
}, cond === 'internal' ? { main_css: 'css/index.css' + (CDN.version ? `?v=${version}` : '') } : {})
data[key] = cdnSource[type]
})
if (cond === 'internal') data.main_css = 'css/index.css' + (CDN.version ? `?v=${version}` : '')
return data
}
// delete null value
const deleteNullValue = obj => {
if (!obj) return {}
if (!obj) return
for (const i in obj) {
if (obj[i] === null) delete obj[i]
obj[i] === null && delete obj[i]
}
return obj
}

24
scripts/events/comment.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* Capitalize the first letter of comment name
*/
hexo.extend.filter.register('before_generate', () => {
const themeConfig = hexo.theme.config
let { use } = themeConfig.comments
if (!use) return
// Make sure use is an array
use = Array.isArray(use) ? use : use.split(',')
// Capitalize the first letter of each comment name
use = use.map(item =>
item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
)
// Disqus and Disqusjs conflict, only keep the first one
if (use.includes('Disqus') && use.includes('Disqusjs')) {
use = [use[0]]
}
themeConfig.comments.use = use
})

View File

@@ -1,86 +1,20 @@
const { deepMerge } = require('hexo-util')
const path = require('path')
// Cache default config to avoid repeated file reads
let cachedDefaultConfig = null
/**
* Check Hexo version and configuration
*/
function checkHexoEnvironment (hexo) {
const { version, log, locals } = hexo
const [major, minor] = version.split('.').map(Number)
const requiredMajor = 5
const requiredMinor = 3
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
log.error('Please update Hexo to V5.3.0 or higher!')
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
throw new Error('Hexo version too old')
}
// Check for deprecated configuration file
if (locals.get) {
const data = locals.get('data')
if (data && data.butterfly) {
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
throw new Error('Deprecated configuration file')
}
}
}
/**
* Load default configuration
*/
function loadDefaultConfig () {
if (cachedDefaultConfig) {
return cachedDefaultConfig
}
const configPath = path.join(__dirname, '../common/default_config.js')
cachedDefaultConfig = require(configPath)
return cachedDefaultConfig
}
/**
* Process comment system configuration
*/
function processCommentConfig (themeConfig) {
const { comments } = themeConfig
if (!comments || !comments.use) {
return
}
let { use } = comments
if (!Array.isArray(use)) {
use = typeof use === 'string' ? use.split(',') : [use]
}
use = use
.map(item => {
if (typeof item !== 'string') return item
return item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
})
.filter(Boolean)
// Handle Disqus and Disqusjs conflict
if (use.includes('Disqus') && use.includes('Disqusjs')) {
hexo.log.warn('Disqus and Disqusjs conflict detected, keeping only the first one')
hexo.log.warn('檢測到 Disqus 和 Disqusjs 衝突,只保留第一個')
use = [use[0]]
}
themeConfig.comments.use = use
}
hexo.extend.filter.register('before_generate', () => {
checkHexoEnvironment(hexo)
const defaultConfig = loadDefaultConfig()
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
processCommentConfig(hexo.theme.config)
}, 1)
hexo.extend.filter.register('before_generate', () => {
// Get first two digits of the Hexo version number
const { version, log, locals } = hexo
const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
if (hexoVer < 5.3) {
log.error('Please update Hexo to V5.3.0 or higher!')
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
process.exit(-1)
}
if (locals.get) {
const data = locals.get('data')
if (data && data.butterfly) {
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
process.exit(-1)
}
}
})

View File

@@ -0,0 +1,583 @@
const { deepMerge } = require('hexo-util')
hexo.extend.filter.register('before_generate', () => {
const defaultConfig = {
nav: {
logo: null,
display_title: true,
fixed: false
},
menu: null,
code_blocks: {
theme: 'light',
macStyle: false,
height_limit: false,
word_wrap: false,
copy: true,
language: true,
shrink: false,
fullpage: false
},
social: null,
favicon: '/img/favicon.png',
avatar: {
img: '/img/butterfly-icon.png',
effect: false
},
disable_top_img: false,
default_top_img: null,
index_img: null,
archive_img: null,
tag_img: null,
tag_per_img: null,
category_img: null,
category_per_img: null,
footer_img: false,
background: null,
cover: {
index_enable: true,
aside_enable: true,
archives_enable: true,
default_cover: null
},
error_img: {
flink: '/img/friend_404.gif',
post_page: '/img/404.jpg'
},
error_404: {
enable: false,
subtitle: 'Page Not Found',
background: '/img/error-page.png'
},
post_meta: {
page: {
date_type: 'created',
date_format: 'date',
categories: true,
tags: false,
label: true
},
post: {
position: 'left',
date_type: 'both',
date_format: 'date',
categories: true,
tags: true,
label: true
}
},
index_site_info_top: null,
index_top_img_height: null,
subtitle: {
enable: false,
effect: true,
typed_option: null,
source: false,
sub: null
},
index_layout: 3,
index_post_content: {
method: 3,
length: 500
},
toc: {
post: true,
page: false,
number: true,
expand: false,
style_simple: false,
scroll_percent: true
},
post_copyright: {
enable: true,
decode: false,
author_href: null,
license: 'CC BY-NC-SA 4.0',
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
},
reward: {
enable: false,
text: null,
QR_code: null
},
post_edit: {
enable: false,
url: null
},
related_post: {
enable: true,
limit: 6,
date_type: 'created'
},
post_pagination: 1,
noticeOutdate: {
enable: false,
style: 'flat',
limit_day: 365,
position: 'top',
message_prev: 'It has been',
message_next: 'days since the last update, the content of the article may be outdated.'
},
footer: {
owner: {
enable: true,
since: 2019
},
custom_text: null,
copyright: true
},
aside: {
enable: true,
hide: false,
button: true,
mobile: true,
position: 'right',
display: {
archive: true,
tag: true,
category: true
},
card_author: {
enable: true,
description: null,
button: {
enable: true,
icon: 'fab fa-github',
text: 'Follow Me',
link: 'https://github.com/xxxxxx'
}
},
card_announcement: {
enable: true,
content: 'This is my Blog'
},
card_recent_post: {
enable: true,
limit: 5,
sort: 'date',
sort_order: null
},
card_newest_comments: {
enable: false,
sort_order: null,
limit: 6,
storage: 10,
avatar: true
},
card_categories: {
enable: true,
limit: 8,
expand: 'none',
sort_order: null
},
card_tags: {
enable: true,
limit: 40,
color: false,
orderby: 'random',
order: 1,
sort_order: null
},
card_archives: {
enable: true,
type: 'monthly',
format: 'MMMM YYYY',
order: -1,
limit: 8,
sort_order: null
},
card_post_series: {
enable: true,
series_title: false,
orderBy: 'date',
order: -1
},
card_webinfo: {
enable: true,
post_count: true,
last_push_date: true,
sort_order: null,
runtime_date: null
}
},
rightside_bottom: null,
translate: {
enable: false,
default: '繁',
defaultEncoding: 2,
translateDelay: 0,
msgToTraditionalChinese: '繁',
msgToSimplifiedChinese: '簡'
},
readmode: true,
darkmode: {
enable: true,
button: true,
autoChangeMode: false,
start: null,
end: null
},
rightside_scroll_percent: false,
rightside_item_order: {
enable: false,
hide: null,
show: null
},
anchor: {
auto_update: false,
click_to_scroll: false
},
photofigcaption: false,
copy: {
enable: true,
copyright: {
enable: false,
limit_count: 150
}
},
wordcount: {
enable: false,
post_wordcount: true,
min2read: true,
total_wordcount: true
},
busuanzi: {
site_uv: true,
site_pv: true,
page_pv: true
},
math: {
use: null,
per_page: true,
hide_scrollbar: false,
mathjax: {
enableMenu: true,
tags: 'none'
},
katex: {
copy_tex: false
}
},
search: {
use: null,
placeholder: null,
algolia_search: {
hitsPerPage: 6
},
local_search: {
preload: false,
top_n_per_article: 1,
unescape: false,
CDN: null
},
docsearch: {
appId: null,
apiKey: null,
indexName: null,
option: null
}
},
share: {
use: 'sharejs',
sharejs: {
sites: 'facebook,twitter,wechat,weibo,qq'
},
addtoany: {
item: 'facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link'
}
},
comments: {
use: null,
text: true,
lazyload: false,
count: false,
card_post_count: false
},
disqus: {
shortname: null,
apikey: null
},
disqusjs: {
shortname: null,
apikey: null,
option: null
},
livere: {
uid: null
},
gitalk: {
client_id: null,
client_secret: null,
repo: null,
owner: null,
admin: null,
option: null
},
valine: {
appId: null,
appKey: null,
avatar: 'monsterid',
serverURLs: null,
bg: null,
visitor: false,
option: null
},
waline: {
serverURL: null,
bg: null,
pageview: false,
option: null
},
utterances: {
repo: null,
issue_term: 'pathname',
light_theme: 'github-light',
dark_theme: 'photon-dark',
js: null,
option: null
},
facebook_comments: {
app_id: null,
user_id: null,
pageSize: 10,
order_by: 'social',
lang: 'en_US'
},
twikoo: {
envId: null,
region: null,
visitor: false,
option: null
},
giscus: {
repo: null,
repo_id: null,
category_id: null,
light_theme: 'light',
dark_theme: 'dark',
js: null,
option: null
},
remark42: {
host: null,
siteId: null,
option: null
},
artalk: {
server: null,
site: null,
visitor: false,
option: null
},
chat: {
use: null,
rightside_button: false,
button_hide_show: false
},
chatra: {
id: null
},
tidio: {
public_key: null
},
crisp: {
website_id: null
},
baidu_analytics: null,
google_analytics: null,
cloudflare_analytics: null,
microsoft_clarity: null,
umami_analytics: {
enable: false,
serverURL: null,
website_id: null,
option: null,
UV_PV: {
site_uv: false,
site_pv: false,
page_pv: false,
token: null
}
},
google_adsense: {
enable: false,
auto_ads: true,
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
client: null,
enable_page_level_ads: true
},
ad: {
index: null,
aside: null,
post: null
},
site_verification: null,
category_ui: null,
tag_ui: null,
rounded_corners_ui: true,
text_align_justify: false,
mask: {
header: true,
footer: true
},
preloader: {
enable: false,
source: 1,
pace_css_url: null
},
enter_transitions: true,
display_mode: 'light',
beautify: {
enable: false,
field: 'post',
title_prefix_icon: null,
title_prefix_icon_color: null
},
font: {
global_font_size: null,
code_font_size: null,
font_family: null,
code_font_family: null
},
blog_title_font: {
font_link: null,
font_family: null
},
hr_icon: {
enable: true,
icon: null,
icon_top: null
},
activate_power_mode: {
enable: false,
colorful: true,
shake: true,
mobile: false
},
canvas_ribbon: {
enable: false,
size: 150,
alpha: 0.6,
zIndex: -1,
click_to_change: false,
mobile: false
},
canvas_fluttering_ribbon: {
enable: false,
mobile: false
},
canvas_nest: {
enable: false,
color: '0,0,255',
opacity: 0.7,
zIndex: -1,
count: 99,
mobile: false
},
fireworks: {
enable: false,
zIndex: 9999,
mobile: false
},
click_heart: {
enable: false,
mobile: false
},
clickShowText: {
enable: false,
text: null,
fontSize: '15px',
random: false,
mobile: false
},
lightbox: null,
series: {
enable: false,
orderBy: 'title',
order: 1,
number: true
},
abcjs: {
enable: false,
per_page: true
},
mermaid: {
enable: false,
code_write: false,
theme: {
light: 'default',
dark: 'dark'
}
},
chartjs: {
enable: false,
fontColor: {
light: 'rgba(0, 0, 0, 0.8)',
dark: 'rgba(255, 255, 255, 0.8)'
},
borderColor: {
light: 'rgba(0, 0, 0, 0.1)',
dark: 'rgba(255, 255, 255, 0.2)'
},
scale_ticks_backdropColor: {
light: 'transparent',
dark: 'transparent'
}
},
note: {
style: 'flat',
icons: true,
border_radius: 3,
light_bg_offset: 0
},
pjax: {
enable: false,
exclude: null
},
aplayerInject: {
enable: false,
per_page: true
},
snackbar: {
enable: false,
position: 'bottom-left',
bg_light: '#49b1f5',
bg_dark: '#1f1f1f'
},
instantpage: false,
lazyload: {
enable: false,
native: false,
field: 'site',
placeholder: null,
blur: false
},
pwa: {
enable: false,
manifest: null,
apple_touch_icon: null,
favicon_32_32: null,
favicon_16_16: null,
mask_icon: null
},
Open_Graph_meta: {
enable: true,
option: null
},
structured_data: true,
css_prefix: true,
inject: {
head: null,
bottom: null
},
CDN: {
internal_provider: 'local',
third_party_provider: 'jsdelivr',
version: false,
custom_format: null,
option: null
}
}
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
}, 1)

View File

@@ -1,7 +1,7 @@
/**
* Butterfly
* Lazyload filter
* Replace src with data-lazy-src for lazy loading
* lazyload
* replace src to data-lazy-src
*/
'use strict'
@@ -10,20 +10,11 @@ const urlFor = require('hexo-util').url_for.bind(hexo)
const lazyload = htmlContent => {
if (hexo.theme.config.lazyload.native) {
// Use more precise replacement: only replace img tags in HTML, not content inside script tags
return htmlContent.replace(/(<img(?![^>]*?\bloading=)(?:\s[^>]*?)?>)(?![^<]*<\/script>)/gi, match => {
return match.replace(/>$/, ' loading=\'lazy\'>')
})
return htmlContent.replace(/(<img.*?)(>)/ig, '$1 loading=\'lazy\'$2')
}
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
// Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
// Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify)
return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => {
const src = srcDoubleQuote || srcSingleQuote || srcNoQuote
return `${prefix}"${bg}" data-lazy-src="${src}"`
})
return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`)
}
hexo.extend.filter.register('after_render:html', data => {

View File

@@ -5,64 +5,55 @@
'use strict'
hexo.extend.generator.register('post', locals => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
const { post_asset_folder: postAssetFolder } = hexo.config
const { cover: { default_cover: defaultCover } } = hexo.theme.config
const previousIndexes = []
function * createCoverGenerator () {
if (!defaultCover) {
while (true) yield false
}
if (!Array.isArray(defaultCover)) {
while (true) yield defaultCover
}
const getRandomCover = defaultCover => {
if (!defaultCover) return false
if (!Array.isArray(defaultCover)) return defaultCover
const coverCount = defaultCover.length
if (coverCount === 1) {
while (true) yield defaultCover[0]
return defaultCover[0]
}
const maxHistory = Math.min(3, coverCount - 1)
const history = []
const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
while (true) {
let index
do {
index = Math.floor(Math.random() * coverCount)
} while (history.includes(index))
let index
do {
index = Math.floor(Math.random() * coverCount)
} while (previousIndexes.includes(index) && previousIndexes.length < coverCount)
history.push(index)
if (history.length > maxHistory) history.shift()
yield defaultCover[index]
previousIndexes.push(index)
if (previousIndexes.length > maxPreviousIndexes) {
previousIndexes.shift()
}
return defaultCover[index]
}
const coverGenerator = createCoverGenerator()
const handleImg = data => {
let { cover: coverVal, top_img: topImg, pagination_cover: paginationCover } = data
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
let { cover: coverVal, top_img: topImg } = data
// Add path to top_img and cover if post_asset_folder is enabled
if (postAssetFolder) {
if (hexo.config.post_asset_folder) {
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
data.top_img = `${data.path}${topImg}`
}
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
data.cover = `${data.path}${coverVal}`
}
if (paginationCover && paginationCover.indexOf('/') === -1 && imgTestReg.test(paginationCover)) {
data.pagination_cover = `${data.path}${paginationCover}`
}
}
if (coverVal === false) return data
// If cover is not set, use random cover
if (!coverVal) {
const randomCover = coverGenerator.next().value
const { cover: { default_cover: defaultCover } } = hexo.theme.config
const randomCover = getRandomCover(defaultCover)
data.cover = randomCover
coverVal = randomCover
coverVal = randomCover // update coverVal
}
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
@@ -72,6 +63,7 @@ hexo.extend.generator.register('post', locals => {
return data
}
// https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
const posts = locals.posts.sort('date').toArray()
const { length } = posts

View File

@@ -1,8 +1,12 @@
'use strict'
hexo.extend.helper.register('aside_archives', function (options = {}) {
const { config, page, site, url_for: urlFor, _p } = this
const { archive_dir: archiveDir, timezone, language } = config
const { config, page, site, url_for, _p } = this
const {
archive_dir: archiveDir,
timezone,
language
} = config
// Destructure and set default options with object destructuring
const {
@@ -17,43 +21,46 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
// Optimize locale handling
const lang = toMomentLocale(page.lang || page.language || language)
// Memoize comparison function to improve performance
const compareFunc = type === 'monthly'
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
: (yearA, yearB) => yearA === yearB
// Early return if no posts
if (!site.posts.length) return ''
const archives = new Map()
site.posts.forEach(post => {
const date = post.date
const year = date.year()
const month = date.month() + 1
const key = type === 'yearly' ? year : `${year}-${month}`
// Use reduce for more efficient data processing
const data = site.posts
.sort('date', order)
.reduce((acc, post) => {
let date = post.date.clone()
if (timezone) date = date.tz(timezone)
if (archives.has(key)) {
archives.get(key).count++
} else {
archives.set(key, {
const year = date.year()
const month = date.month() + 1
if (lang) date = date.locale(lang)
// Find or create archive entry
const lastEntry = acc[acc.length - 1]
if (!lastEntry || !compareFunc(
lastEntry.year,
lastEntry.month,
year,
month,
count: 1,
date // Store date object for later formatting
})
}
})
month
)) {
acc.push({
name: date.format(format),
year,
month,
count: 1
})
} else {
lastEntry.count++
}
const data = Array.from(archives.values()).sort((a, b) => {
if (order === -1) {
return b.year - a.year || b.month - a.month
}
return a.year - b.year || a.month - b.month
})
// Format names after aggregation
data.forEach(item => {
let date = item.date.clone()
if (timezone) date = date.tz(timezone)
if (lang) date = date.locale(lang)
item.name = date.format(format)
delete item.date // Clean up
})
return acc
}, [])
// Create link generator function
const createArchiveLink = item => {
@@ -61,45 +68,43 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
if (type === 'monthly') {
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
}
return urlFor(url)
return url_for(url)
}
// Limit results efficiently
const limitedData = limit > 0 ? data.slice(0, Math.min(data.length, limit)) : data
const limitedData = limit > 0
? data.slice(0, Math.min(data.length, limit))
: data
// Use template literal for better readability
const archiveHeader = `
<div class="item-headline">
<i class="fas fa-archive"></i>
<span>${_p('aside.card_archives')}</span>
${
data.length > limitedData.length
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
${data.length > limitedData.length
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
title="${_p('aside.more_button')}">
<i class="fas fa-angle-right"></i>
</a>`
: ''
}
: ''}
</div>
`
// Use map for generating list items, join for performance
const archiveList = `
<ul class="card-archive-list">
${limitedData
.map(
item => `
${limitedData.map(item => `
<li class="card-archive-list-item">
<a class="card-archive-list-link" href="${createArchiveLink(item)}">
<span class="card-archive-list-date">
${transform ? transform(item.name) : item.name}
</span>
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''}
${showCount
? `<span class="card-archive-list-count">${item.count}</span>`
: ''}
</a>
</li>
`
)
.join('')}
`).join('')}
</ul>
`

View File

@@ -19,33 +19,15 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
const expandClass = isExpand && options.expand === true ? 'expand' : ''
const buttonLabel = this._p('aside.more_button')
const categoryMap = new Map()
categories.forEach(cat => {
if (cat.length) {
const parentId = cat.parent || 'root'
if (!categoryMap.has(parentId)) {
categoryMap.set(parentId, [])
}
categoryMap.get(parentId).push(cat)
}
})
const sortFn = (a, b) => {
const valA = a[orderby]
const valB = b[orderby]
if (valA < valB) return -order
if (valA > valB) return order
return 0
const prepareQuery = parent => {
const query = parent ? { parent } : { parent: { $exists: false } }
return categories.find(query).sort(orderby, order).filter(cat => cat.length)
}
for (const list of categoryMap.values()) {
list.sort(sortFn)
}
const hierarchicalList = (remaining, level = 0, parentId = 'root') => {
const hierarchicalList = (remaining, level = 0, parent) => {
let result = ''
if (remaining > 0 && categoryMap.has(parentId)) {
categoryMap.get(parentId).forEach(cat => {
if (remaining > 0) {
prepareQuery(parent).forEach(cat => {
if (remaining > 0) {
remaining -= 1
let child = ''
@@ -55,8 +37,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
remaining = childList.remaining
}
const isTopLevel = parentId === 'root'
const parentClass = isExpand && isTopLevel && child ? 'parent' : ''
const parentClass = isExpand && !parent && child ? 'parent' : ''
result += `<li class="card-category-list-item ${parentClass}">`
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
result += `<span class="card-category-list-name">${cat.name}</span>`
@@ -65,7 +46,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
result += `<span class="card-category-list-count">${cat.length}</span>`
}
if (isExpand && isTopLevel && child) {
if (isExpand && !parent && child) {
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
}

View File

@@ -8,29 +8,28 @@ hexo.extend.helper.register('getArchiveLength', function () {
// Archives Page
if (!year) return posts.length
// Function to generate a unique key based on the granularity
const getKey = (post, type) => {
const date = post.date.clone()
const y = date.year()
const m = date.month() + 1
const d = date.date()
if (type === 'year') return `${y}`
if (type === 'month') return `${y}-${m}`
if (type === 'day') return `${y}-${m}-${d}`
}
// Create a map to count posts per period
const mapData = this.fragment_cache('createArchiveObj', () => {
const map = new Map()
posts.forEach(post => {
const date = post.date
const y = date.year()
const m = date.month() + 1
const d = date.date()
const keyYear = getKey(post, 'year')
const keyMonth = getKey(post, 'month')
const keyDay = getKey(post, 'day')
if (yearly) {
const keyYear = `${y}`
map.set(keyYear, (map.get(keyYear) || 0) + 1)
}
if (monthly) {
const keyMonth = `${y}-${m}`
map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
}
if (daily) {
const keyDay = `${y}-${m}-${d}`
map.set(keyDay, (map.get(keyDay) || 0) + 1)
}
if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
if (daily) map.set(keyDay, (map.get(keyDay) || 0) + 1)
})
return map
})

View File

@@ -4,9 +4,9 @@ hexo.extend.helper.register('inject_head_js', function () {
const { darkmode, aside, pjax } = this.theme
const start = darkmode.start || 6
const end = darkmode.end || 18
const { theme_color: themeColor } = hexo.theme.config
const themeColorLight = themeColor && themeColor.enable ? themeColor.meta_theme_color_light : '#ffffff'
const themeColorDark = themeColor && themeColor.enable ? themeColor.meta_theme_color_dark : '#0d0d0d'
const { theme_color } = hexo.theme.config
const themeColorLight = theme_color && theme_color.enable ? theme_color.meta_theme_color_light : '#ffffff'
const themeColorDark = theme_color && theme_color.enable ? theme_color.meta_theme_color_dark : '#0d0d0d'
const createCustomJs = () => `
const saveToLocal = {
@@ -89,7 +89,7 @@ hexo.extend.helper.register('inject_head_js', function () {
darkmodeJs += `
const mediaQueryDark = window.matchMedia('(prefers-color-scheme: dark)')
const mediaQueryLight = window.matchMedia('(prefers-color-scheme: light)')
if (theme === undefined) {
if (mediaQueryLight.matches) activateLightMode()
else if (mediaQueryDark.matches) activateDarkMode()

View File

@@ -5,12 +5,6 @@ const { prettyUrls } = require('hexo-util')
const crypto = require('crypto')
const moment = require('moment-timezone')
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
const archiveRegex = /\/archives\//
hexo.extend.helper.register('truncate', truncateContent)
hexo.extend.helper.register('postDesc', data => {
@@ -19,67 +13,34 @@ 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', custom_colors } = options
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order } = options
if (limit > 0) {
source = source.limit(limit)
}
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
const sizeMap = new Map(sizes.map((size, index) => [size, index]))
const length = sizes.length - 1
const getRandomColor = () => {
const r = Math.floor(Math.random() * 201)
const g = Math.floor(Math.random() * 201)
const b = Math.floor(Math.random() * 201)
const randomColor = () => Math.floor(Math.random() * 201)
const r = randomColor()
const g = randomColor()
const b = randomColor()
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
}
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 generateStyle = (size, unit) =>
`font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
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, idx) => {
const ratio = length ? sizeMap.get(tag.length) / length : 0
const length = sizes.length - 1
const result = source.sort(orderby, order).map(tag => {
const ratio = length ? sizes.indexOf(tag.length) / length : 0
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
if (userColors && userColors.length) {
const colorClass = resolveColorClass(idx)
const color = userColors[idx % userColors.length]
const style = generateStyle(size, unit, page, color)
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item ${colorClass}" style="${style}">${tag.name}</a>`
}
const color = getRandomColor()
const style = generateStyle(size, unit, page, color)
const style = generateStyle(size, unit)
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
}).join('')
return result
})
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
@@ -87,7 +48,7 @@ hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex =
})
hexo.extend.helper.register('md5', function (path) {
return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex')
return crypto.createHash('md5').update(decodeURI(this.url_for(path))).digest('hex')
})
hexo.extend.helper.register('injectHtml', data => {
@@ -104,14 +65,14 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
const defaultTitle = this._p('page.archives')
if (!menu) return defaultTitle
const loop = m => {
const loop = (m) => {
for (const key in m) {
if (typeof m[key] === 'object') {
const result = loop(m[key])
if (result) return result
}
if (archiveRegex.test(m[key])) {
if (/\/archives\//.test(m[key])) {
return key
}
}
@@ -120,13 +81,17 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
return loop(menu) || defaultTitle
})
hexo.extend.helper.register('getBgPath', function (path) {
hexo.extend.helper.register('getBgPath', path => {
if (!path) return ''
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
if (colorPattern.test(path)) {
return `background-color: ${path};`
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path) || simpleFilePattern.test(path)) {
return `background-image: url(${this.url_for(path)});`
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) {
return `background-image: url(${path});`
} else {
return `background: ${path};`
}
@@ -134,34 +99,37 @@ hexo.extend.helper.register('getBgPath', function (path) {
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
const { limit } = page
// Shallow copy to avoid mutating original data
let processedData = data.map(item => ({ ...item }))
let finalResult = ''
// Check if limit.value is a valid date
const isValidDate = date => !isNaN(Date.parse(date))
// order by date
processedData.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
const orderByDate = data => data.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
// Apply number limit or time limit conditionally
if (limit && limit.type === 'num' && limit.value > 0) {
processedData = processedData.slice(0, limit.value)
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
const limitDate = Date.parse(limit.value)
processedData = processedData.filter(item => Date.parse(item.date) >= limitDate)
const limitData = data => {
if (limit && limit.type === 'num' && limit.value > 0) {
return data.slice(0, limit.value)
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
const limitDate = Date.parse(limit.value)
return data.filter(item => Date.parse(item.date) >= limitDate)
}
return data
}
orderByDate(data)
finalResult = limitData(data)
// This is a hack method, because hexo treats time as UTC time
// so you need to manually convert the time zone
processedData.forEach(item => {
finalResult.forEach(item => {
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
// markdown
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
})
return processedData
return finalResult
})
hexo.extend.helper.register('getPageType', (page, isHome) => {
@@ -182,12 +150,3 @@ hexo.extend.helper.register('getVersion', () => {
const { version } = require('../../package.json')
return { hexo: hexo.version, theme: version }
})
hexo.extend.helper.register('safeJSON', data => {
// Safely serialize JSON for embedding in <script> tags
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
})

View File

@@ -9,22 +9,14 @@
const { postDesc } = require('../common/postDesc')
hexo.extend.helper.register('related_posts', function (currentPost) {
const relatedPosts = new Map()
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
let relatedPosts = []
const tagsData = currentPost.tags
if (!tagsData || !tagsData.length) return ''
tagsData.forEach(tag => {
const posts = tag.posts
posts.forEach(post => {
if (currentPost.path === post.path) return
if (relatedPosts.has(post.path)) {
relatedPosts.get(post.path).weight += 1
} else {
tagsData.length && tagsData.forEach(function (tag) {
allPosts.forEach(function (post) {
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
const getPostDesc = post.postDesc || postDesc(post, hexo)
relatedPosts.set(post.path, {
const relatedPost = {
title: post.title,
path: post.path,
cover: post.cover,
@@ -32,17 +24,22 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
weight: 1,
updated: post.updated,
created: post.date,
postDesc: getPostDesc,
random: Math.random()
})
postDesc: getPostDesc
}
const index = findItem(relatedPosts, 'path', post.path)
if (index !== -1) {
relatedPosts[index].weight += 1
} else {
relatedPosts.push(relatedPost)
}
}
})
})
if (relatedPosts.size === 0) {
if (relatedPosts.length === 0) {
return ''
}
let result = ''
const hexoConfig = hexo.config
const config = hexo.theme.config
@@ -50,42 +47,51 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
const dateType = config.related_post.date_type || 'created'
const headlineLang = this._p('post.recommend')
const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => {
if (b.weight !== a.weight) {
return b.weight - a.weight
}
return b.random - a.random
})
relatedPosts = relatedPosts.sort(compare('weight'))
let result = '<div class="relatedPosts">'
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">'
if (relatedPosts.length > 0) {
result += '<div class="relatedPosts">'
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i]
const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)'
title = escape_html(title)
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
if (cover_type === 'img') {
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
} else {
result += `<div class="cover" style="background: ${cover}"></div>`
}
if (dateType === 'created') {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>`
} else {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>`
}
result += `<div class="info-item-2">${title}</div></div>`
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i]
const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)'
title = escape_html(title)
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
if (cover_type === 'img') {
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
} else {
result += `<div class="cover" style="background: ${cover}"></div>`
}
if (dateType === 'created') {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>`
} else {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>`
}
result += `<div class="info-item-2">${title}</div></div>`
if (postDesc) {
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
if (postDesc) {
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
}
result += '</div></a>'
}
result += '</div></a>'
result += '</div></div>'
return result
}
result += '</div></div>'
return result
})
function isTagRelated (tagName, tags) {
return tags.some(tag => tag.name === tagName)
}
function findItem (arrayToSearch, attr, val) {
return arrayToSearch.findIndex(item => item[attr] === val)
}
function compare (attr) {
return (a, b) => b[attr] - a[attr]
}

View File

@@ -49,4 +49,4 @@ const hideToggle = (args, content) => {
hexo.extend.tag.register('hideInline', hideInline)
hexo.extend.tag.register('hideBlock', hideBlock, { ends: true })
hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })
hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })

View File

@@ -9,8 +9,7 @@
const { escapeHTML } = require('hexo-util')
const mermaid = (args, content) => {
const config = args[0] || '{}'
return `<div class="mermaid-wrap"><pre class="mermaid-src" data-config="${escapeHTML(config)}" hidden>
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
${escapeHTML(content)}
</pre></div>`
}

View File

@@ -6,45 +6,17 @@
'use strict'
const score = (args, content) => {
// Escape HTML tags and some special characters, including curly braces
const escapeHtmlTags = s => {
const lookup = {
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'\'': '&apos;',
'<': '&lt;',
'>': '&gt;',
'{': '&#123;',
'}': '&#125;'
'>': '&gt;'
}
return s.replace(/[&"'<>{}]/g, c => lookup[c])
return s.replace(/[&"'<>]/g, c => lookup[c])
}
const trimmed = content.trim()
// Split content using six dashes as a delimiter
const parts = trimmed.split('------')
if (parts.length < 2) {
// If no delimiter is found, treat the entire content as the score
return `<div class="abc-music-sheet">${escapeHtmlTags(trimmed)}</div>`
}
// First part is parameters (JSON string), the rest is the score content
const paramPart = parts[0].trim()
const scorePart = parts.slice(1).join('------').trim()
let paramsObj = {}
try {
paramsObj = JSON.parse(paramPart)
} catch (e) {
console.error('Failed to parse JSON in score tag:', e)
}
// Use double quotes for data-params attribute value,
// ensuring JSON internal double quotes are escaped
return `<div class="abc-music-sheet" data-params="${escapeHtmlTags(JSON.stringify(paramsObj))}">
${escapeHtmlTags(scorePart)}
</div>`
return `<div class="abc-music-sheet">${escapeHtmlTags(content)}</div>`
}
hexo.extend.tag.register('score', score, { ends: true })

View File

@@ -11,7 +11,7 @@
.fontawesomeIcon
display: inline-block
font-weight: 600
font-family: 'Font Awesome 7 Free', 'Font Awesome 6 Free'
font-family: 'Font Awesome 6 Free'
text-rendering: auto
-webkit-font-smoothing: antialiased
@@ -159,7 +159,7 @@ if hexo-config('enter_transitions')
animation: titleScale 1s
canvas:not(#ribbon-canvas),
#web_bg.bg-animation
#web_bg
animation: to_show 4s
#ribbon-canvas
@@ -181,60 +181,6 @@ if hexo-config('avatar.effect') == true
.reward-main
animation: donate_effcet .3s .1s ease both
.btn-effects
position: relative
overflow: hidden
transition: all .3s cubic-bezier(.4, 0, .2, 1)
transform: translateZ(0)
&:hover
box-shadow: 0 4px 12px rgba(0, 0, 0, .15)
text-decoration: none
transform: translateY(-1px) scale(1.02)
&:active
transition-duration: .1s
transform: translateY(0) scale(.98)
i
display: inline-block
vertical-align: middle
transition: all .3s cubic-bezier(.4, 0, .2, 1)
&:hover i
animation: buttonIconBounce .6s ease-in-out
i + span
margin-left: 6px
vertical-align: middle
transition: margin-left .3s cubic-bezier(.4, 0, .2, 1)
&:hover i + span
margin-left: 8px
&::before
position: absolute
top: 0
left: -100%
z-index: 1
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .2), transparent)
content: ''
transition: left .5s cubic-bezier(.4, 0, .2, 1)
&:hover::before
left: 100%
& > *
position: relative
z-index: 2
.btn-effects-large
&:hover
box-shadow: 0 6px 16px rgba(0, 0, 0, .2)
transform: translateY(-2px) scale(1.03)
@keyframes scroll-down-effect
0%
opacity: .4
@@ -332,17 +278,3 @@ if hexo-config('avatar.effect') == true
100%
transform: translateX(0)
@keyframes buttonIconBounce
0%,
100%
transform: translateY(0) scale(1)
25%
transform: translateY(-3px) scale(1.1) rotateZ(-5deg)
50%
transform: translateY(0) scale(1.05) rotateZ(0)
75%
transform: translateY(-1px) scale(1.02) rotateZ(2deg)

View File

@@ -10,8 +10,8 @@
--preloader-bg: $preloader-bg
--preloader-color: $preloader-word-color
--tab-border-color: $tab-border-color
--tab-button-bg: $tab-button-bg
--tab-button-color: $tab-button-color
--tab-botton-bg: $tab-botton-bg
--tab-botton-color: $tab-botton-color
--tab-button-hover-bg: $tab-button-hover-bg
--tab-button-active-bg: $tab-button-active-bg
--card-bg: $card-bg
@@ -39,65 +39,6 @@
--zoom-bg: #fff
--mark-bg: alpha($dark-black, .3)
// tags plugin
:root
--btn-color: $btn-color
--btn-default-color: $btn-default-color
--tags-blue-color: $tagsP-blue-color
--tags-blue-color-lighten: lighten($tagsP-blue-color, 85%)
--tags-pink-color: $tagsP-pink-color
--tags-pink-color-lighten: lighten($tagsP-pink-color, 85%)
--tags-red-color: $tagsP-red-color
--tags-red-color-lighten: lighten($tagsP-red-color, 85%)
--tags-orange-color: $tagsP-orange-color
--tags-orange-color-lighten: lighten($tagsP-orange-color, 85%)
--tags-purple-color: $tagsP-purple-color
--tags-purple-color-lighten: lighten($tagsP-purple-color, 85%)
--tags-green-color: $tagsP-green-color
--tags-green-color-lighten: lighten($tagsP-green-color, 85%)
--note-default-border: $note-default-border
--note-default-bg: $note-default-bg
--note-default-text: $note-default-text
--note-modern-default-border: $note-modern-default-border
--note-modern-default-bg: $note-modern-default-bg
--note-modern-default-text: $note-modern-default-text
--note-modern-default-hover: $note-modern-default-hover
--note-primary-border: $note-primary-border
--note-primary-bg: $note-primary-bg
--note-primary-text: $note-primary-text
--note-modern-primary-border: $note-modern-primary-border
--note-modern-primary-bg: $note-modern-primary-bg
--note-modern-primary-text: $note-modern-primary-text
--note-modern-primary-hover: $note-modern-primary-hover
--note-info-border: $note-info-border
--note-info-bg: $note-info-bg
--note-info-text: $note-info-text
--note-modern-info-border: $note-modern-info-border
--note-modern-info-bg: $note-modern-info-bg
--note-modern-info-text: $note-modern-info-text
--note-modern-info-hover: $note-modern-info-hover
--note-success-border: $note-success-border
--note-success-bg: $note-success-bg
--note-success-text: $note-success-text
--note-modern-success-border: $note-modern-success-border
--note-modern-success-bg: $note-modern-success-bg
--note-modern-success-text: $note-modern-success-text
--note-modern-success-hover: $note-modern-success-hover
--note-warning-border: $note-warning-border
--note-warning-bg: $note-warning-bg
--note-warning-text: $note-warning-text
--note-modern-warning-border: $note-modern-warning-border
--note-modern-warning-bg: $note-modern-warning-bg
--note-modern-warning-text: $note-modern-warning-text
--note-modern-warning-hover: $note-modern-warning-hover
--note-danger-border: $note-danger-border
--note-danger-bg: $note-danger-bg
--note-danger-text: $note-danger-text
--note-modern-danger-border: $note-modern-danger-border
--note-modern-danger-bg: $note-modern-danger-bg
--note-modern-danger-text: $note-modern-danger-text
--note-modern-danger-hover: $note-modern-danger-hover
body
position: relative
overflow-y: scroll
@@ -281,7 +222,3 @@ blockquote
& > :last-child
margin-bottom: 0 !important
.fa-fw
width: 1.25em
text-align: center

View File

@@ -24,14 +24,6 @@ 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'
@@ -96,11 +88,6 @@ $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
@@ -113,23 +100,18 @@ $code-block
.highlight-tools
display: flex
align-items: center
overflow: hidden
padding: 0 8px
min-height: 24px
height: 2.15em
background: var(--hltools-bg)
color: var(--hltools-color)
font-size: $code-font-size
overflow: hidden
& > *
flex: 0 0 auto
margin: 2px
padding: 5px
i
display: inline-flex
justify-content: center
align-items: center
padding: 5px
cursor: pointer
transition: all .3s
@@ -145,28 +127,31 @@ $code-block
if !$highlight_macstyle
& > .macStyle
margin: 0
padding: 0
.code-lang
flex: 1 1 auto
overflow: hidden
padding-right: 10px
flex: 1
text-transform: uppercase
text-overflow: ellipsis
white-space: nowrap
font-weight: bold
font-size: 1.15em
user-select: none
-webkit-user-select: none
padding 2px
.copy-notice
padding-right: 2px
opacity: 0
transition: opacity .4s
if hexo-config('code_blocks.language')
margin-right: auto
else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
& > :nth-child(2)
margin-right: auto
.code-lang
flex: 1
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none')
& > div:nth-child(2)
flex: 1
else
& > :nth-child(1)
margin-right: auto
.macStyle
flex: 1
.gutter
user-select: none
@@ -178,28 +163,17 @@ $code-block
td
border: none
.copy-notice
position: absolute
z-index: 99999
padding: 2px 6px
border-radius: 3px
background: var(--hltools-bg)
white-space: nowrap
font-size: 12px
pointer-events: none
if $highlight_macstyle
.container
figure.highlight
margin: 0 0 24px
border-radius: 8px
border-radius: 7px
box-shadow: 0 5px 10px 0 $highlight-mac-border
-webkit-transform: translateZ(0)
.highlight-tools
.macStyle
display: flex
padding: 3px
& > *
margin-right: 8px
@@ -257,7 +231,7 @@ if hexo-config('code_blocks.height_limit')
& ~ pre
overflow: hidden
height: unit(hexo-config('code_blocks.height_limit'), px)
@keyframes code-expand-key
0%
opacity: .6
@@ -290,8 +264,8 @@ if hexo-config('code_blocks.fullpage')
& ~ table
display: block
overflow: auto
margin-bottom: 0
height: calc(100vh - 2.15em)
margin-bottom: 0
@keyframes code-fullpage
0%,

View File

@@ -1,6 +1,11 @@
figure.highlight
table
@extend $scrollbar-style
// scrollbar - firefox
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
pre .deletion
color: $highlight-deletion

View File

@@ -6,7 +6,12 @@ if $highlight_theme != false
.container
pre[class*='language-']
@extend $scrollbar-style
// scrollbar - firefox
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
&:not(.line-numbers)
padding: 10px 20px

View File

@@ -23,8 +23,8 @@
&:not(#card-toc)
display: none
// &:last-child
// margin-bottom: 0
&:last-child
margin-bottom: 0
.card-info
.author-info
@@ -60,7 +60,6 @@
text-align: center
line-height: 2.4
addBorderRadius(7)
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)
@@ -181,11 +180,6 @@
.card-category-list
&.child
padding: 0 0 0 16px
overflow: hidden
max-height: 0
opacity: 0
visibility: hidden
transition: max-height .3s ease, opacity .3s ease
> .parent
> a
@@ -194,9 +188,7 @@
transform: rotate(-90deg)
& + .child
max-height: 1000px
opacity: 1
visibility: visible
display: block
.card-category-list
&-name
@@ -215,9 +207,7 @@
if hexo-config('aside.card_categories.expand') == false
> .child
max-height: 0
opacity: 0
visibility: hidden
display: none
.card-webinfo
.webinfo

View File

@@ -13,16 +13,17 @@
background-color: var(--mark-bg)
content: ''
& > *
position: relative
color: var(--light-grey)
#footer-wrap
position: relative
padding: 40px 20px
color: var(--light-grey)
text-align: center
a
color: var(--light-grey)
transition: all .3s ease-in-out
&:hover
color: $light-blue
text-decoration: underline
.footer-separator
margin: 0 4px
@@ -32,56 +33,3 @@
max-height: 1.4em
width: auto
vertical-align: text-bottom
.footer-flex
display: flex
flex-direction: row
flex-wrap: wrap
justify-content: space-between
margin: 0 auto
padding: 40px 60px
max-width: 1200px
width: 100%
text-align: left
gap: 13px
+maxWidth768()
padding: 30px
gap: 10px
.footer-flex-items
flex-shrink: 0
min-width: 100px
text-align: left
white-space: nowrap
.footer-flex-title
margin-bottom: 5px
white-space: nowrap
font-weight: 600
font-size: 1.4em
.footer-flex-item
margin: 10px 0
white-space: nowrap
a
display: block
white-space: nowrap
.footer-other
padding: 40px 20px
width: 100%
text-align: center
if hexo-config('footer.nav')
padding: 10px 8px
background-color: rgba(0, 0, 0, .1)
.copyright,
.framework-info,
.footer_custom_text
font-size: .9em
else
.framework-info
display: block

View File

@@ -37,8 +37,6 @@
margin: 0
color: var(--white)
font-size: 1.85em
@extend .limit-more-line
-webkit-line-clamp: 3
+minWidth768()
font-size: 2.85em
@@ -437,29 +435,4 @@
&:hover
&:after
width: 100%
.nav-page-title
position: relative
overflow: hidden
& > :first-child,
& > :last-child
display: inline-block
transition: all .3s ease-in-out
& > :last-child
position: absolute
top: 50%
left: 0
opacity: 0
transform: translateY(-50%) translateY(-10px)
&:hover
& > :last-child
opacity: 1
transform: translateY(-50%) translateY(0)
& > :first-child
opacity: 0
transform: translateY(10px)
width: 100%

View File

@@ -15,12 +15,9 @@
color: var(--btn-color)
cursor: pointer
addBorderRadius()
@extend .btn-effects
@extend .btn-effects-large
i
margin-right: 5px
vertical-align: baseline
&:hover
.reward-button
@@ -31,19 +28,19 @@
.reward-main
position: absolute
bottom: 50px
bottom: 40px
left: 0
z-index: 100
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

View File

@@ -38,13 +38,9 @@
font-size: 16px
line-height: w
addBorderRadius(5)
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)
i
vertical-align: baseline
#mobile-toc-button
display: none
@@ -58,52 +54,19 @@
if hexo-config('rightside_scroll_percent')
#go-up
position: relative
.scroll-percent
position: absolute
top: 0
left: 0
display: none
width: 100%
height: 100%
opacity: 0
transition: all .4s cubic-bezier(.4, 0, .2, 1)
transform: scale(.8)
i
position: relative
z-index: 1
width: 100%
opacity: 1
transition: all .4s cubic-bezier(.4, 0, .2, 1)
transform: scale(1)
&.show-percent
.scroll-percent
display: block
opacity: 1
transform: scale(1)
animation: fadeInScale .4s ease-out
i
opacity: 0
transform: scale(.8)
& + i
display: none
&:hover.show-percent
&:hover
.scroll-percent
opacity: 0
transform: scale(.8)
display: none
i
opacity: 1 !important
transform: scale(1) !important
@keyframes fadeInScale
from
opacity: 0
transform: scale(.8)
to
opacity: 1
transform: scale(1)
& + i
display: block

View File

@@ -52,10 +52,7 @@
&:hover
background: var(--text-bg-hover)
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
color: var(--white)
transition: all .2s ease
transform: translateX(3px)
i:first-child
width: 15%
@@ -73,25 +70,9 @@
transform: rotate(90deg)
& + .menus_item_child
overflow: hidden
max-height: 0
opacity: 0
transform: scaleY(0)
transform-origin: top
display: none
.menus_item_child
margin: 0
padding-left: 25px
max-height: 0
list-style: none
opacity: 0
transition: transform .3s ease, opacity .3s ease, max-height .3s ease
transform: scaleY(0)
transform-origin: top
will-change: transform, opacity, max-height
// .hide
.site-page.group:not(.hide) + .menus_item_child
max-height: 1000px
opacity: 1
transform: scaleY(1)
list-style: none

View File

@@ -65,55 +65,12 @@ 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
@@ -226,6 +183,5 @@ if hexo-config('math.use')
opacity: 1
+maxWidth768()
.fancybox__toolbar__column.is-middle,
.f-carousel__toolbar__column.is-middle
visibility: hidden
.fancybox__toolbar__column.is-middle
display: none

View File

@@ -10,8 +10,8 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
--preloader-bg: darken(#121212, 2)
--preloader-color: alpha(#FFFFFF, .7)
--tab-border-color: #2c2c2c
--tab-button-bg: #2c2c2c
--tab-button-color: alpha(#FFFFFF, .7)
--tab-botton-bg: #2c2c2c
--tab-botton-color: alpha(#FFFFFF, .7)
--tab-button-hover-bg: lighten(#121212, 15)
--tab-button-active-bg: #121212
--card-bg: #121212
@@ -33,56 +33,6 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
--timeline-bg: lighten(#121212, 5)
--zoom-bg: #121212
--mark-bg: alpha($dark-black, .6)
--btn-color: darken($btn-color, 20%)
--btn-default-color: lighten($btn-default-color, 20%)
--tags-blue-color: desaturate(darken($tagsP-blue-color, 20%), 25%)
--tags-blue-color-lighten: rgba(66, 139, 202, .15)
--tags-pink-color: desaturate(darken($tagsP-pink-color, 22%), 30%)
--tags-pink-color-lighten: rgba(255, 105, 180, .15)
--tags-red-color: desaturate(darken($tagsP-red-color, 25%), 28%)
--tags-red-color-lighten: rgba(255, 0, 0, .15)
--tags-orange-color: desaturate(darken($tagsP-orange-color, 22%), 32%)
--tags-orange-color-lighten: rgba(255, 140, 0, .15)
--tags-purple-color: desaturate(darken($tagsP-purple-color, 18%), 30%)
--tags-purple-color-lighten: rgba(111, 66, 193, .15)
--tags-green-color: desaturate(darken($tagsP-green-color, 20%), 28%)
--tags-green-color-lighten: rgba(92, 184, 92, .15)
--note-default-border: $note-dark-default-border
--note-default-bg: $note-dark-default-bg
--note-default-text: $note-dark-default-text
--note-modern-default-border: $note-dark-modern-default-border
--note-modern-default-bg: $note-dark-modern-default-bg
--note-modern-default-text: $note-dark-modern-default-text
--note-primary-border: $note-dark-primary-border
--note-primary-bg: $note-dark-primary-bg
--note-primary-text: $note-dark-primary-text
--note-modern-primary-border: $note-dark-modern-primary-border
--note-modern-primary-bg: $note-dark-modern-primary-bg
--note-modern-primary-text: $note-dark-modern-primary-text
--note-info-border: $note-dark-info-border
--note-info-bg: $note-dark-info-bg
--note-info-text: $note-dark-info-text
--note-modern-info-border: $note-dark-modern-info-border
--note-modern-info-bg: $note-dark-modern-info-bg
--note-modern-info-text: $note-dark-modern-info-text
--note-success-border: $note-dark-success-border
--note-success-bg: $note-dark-success-bg
--note-success-text: $note-dark-success-text
--note-modern-success-border: $note-dark-modern-success-border
--note-modern-success-bg: $note-dark-modern-success-bg
--note-modern-success-text: $note-dark-modern-success-text
--note-warning-border: $note-dark-warning-border
--note-warning-bg: $note-dark-warning-bg
--note-warning-text: $note-dark-warning-text
--note-modern-warning-border: $note-dark-modern-warning-border
--note-modern-warning-bg: $note-dark-modern-warning-bg
--note-modern-warning-text: $note-dark-modern-warning-text
--note-danger-border: $note-dark-danger-border
--note-danger-bg: $note-dark-danger-bg
--note-danger-text: $note-dark-danger-text
--note-modern-danger-border: $note-dark-modern-danger-border
--note-modern-danger-bg: $note-dark-modern-danger-bg
--note-modern-danger-text: $note-dark-modern-danger-text
#web_bg:before
position: absolute
@@ -128,22 +78,27 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
#switch-btn
filter: brightness(.8)
// note
if hexo-config('note.style') == 'modern' || hexo-config('note.style') == 'flat'
.note
filter: brightness(.8)
// hide-tags
.hide-button,
.toggle-button,
.btn-beautify,
.hl-label,
#post-outdate-notice,
.error-img,
.container iframe,
.gist,
.ads-wrap,
.tag-cloud-list > a
.ads-wrap
filter: brightness(.8)
img:not(.cover)
img
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
filter: blur(0) brightness(.88) contrast(.95)
filter: blur(0) brightness(.8)
else
filter: brightness(.88) contrast(.95)
filter: brightness(.8)
#aside-content .aside-list > .aside-list-item:not(:last-child)
border-bottom: 1px dashed alpha(#FFFFFF, .1)
@@ -193,7 +148,7 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
--at-color-font: alpha(#FFFFFF, .7)
--at-color-meta: alpha(#FFFFFF, .7)
--at-color-grey: alpha(#FFFFFF, .7)
.atk-send-btn,
.atk-badge
color: alpha(#FFFFFF, .7) !important

View File

@@ -42,7 +42,6 @@ if hexo-config('readmode')
font-size: 16px
transition: background .3s
addBorderRadius(8)
@extend .btn-effects
+maxWidth768()
top: initial

View File

@@ -18,7 +18,7 @@
padding: 20px 5px
+minWidth2000()
max-width: 60%
max-width: 70%
& > div:first-child:not(.nc)
@extend .cardHover

View File

@@ -171,5 +171,4 @@ $indexEnable = hexo-config('cover.index_enable')
& > .content
@extend .limit-more-line
-webkit-line-clamp: 2
word-break: break-word
-webkit-line-clamp: 2

View File

@@ -76,110 +76,3 @@
&.no-comment
display: none
.shuoshuo-navigation
display: flex
justify-content: center
align-items: center
margin-top: 20px
padding: 20px 0
button
display: flex
justify-content: center
align-items: center
width: 2.7em
height: 2.7em
background-color: var(--btn-bg)
color: var(--btn-color)
font-size: .9em
line-height: 2.5em
transition: all .2s ease-in-out
addBorderRadius(6)
&:not(:disabled)
@extend .btn-effects
&:hover:not(:disabled)
background-color: var(--btn-hover-color)
&:disabled
background: #f5f5f5
color: #ccc
opacity: .5
cursor: not-allowed
.shuoshuo-page-info
margin: 0 15px
color: #858585
white-space: nowrap
font-size: .9em
.shuoshuo-page-input
margin-right: 12px
padding: 0 15px
height: 2.7em
border: 1px solid var(--btn-bg)
background: var(--card-bg)
color: #858585
text-align: center
font-size: .9em
transition: all .2s ease-in-out
addBorderRadius(6)
&:focus
outline: none
border-width: 2px
&::placeholder
color: transparent
/* number */
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button
margin: 0
-webkit-appearance: none
/* Firefox */
-moz-appearance: textfield
/* */
&.shuoshuo-page-num
min-width: 40px
width: 40px
border: none
background: $light-blue
color: var(--white)
font-weight: 500
cursor: text
&:focus
border: 1px solid $light-blue
background: var(--white)
color: #333
/* */
&.invalid
border-color: #ff4757
background-color: #ffeaea
color: #ff4757
animation: shake .5s ease-in-out
/* */
@keyframes shake
0%,
100%
transform: translateX(0)
10%,
30%,
50%,
70%,
90%
transform: translateX(-2px)
20%,
40%,
60%,
80%
transform: translateX(2px)

View File

@@ -1,91 +1,27 @@
.tag-cloud
&-list
animation: tagsFadeIn .6s cubic-bezier(.4, 0, .2, 1)
&:hover a:not(:hover)
opacity: .7
transform: scale(.98)
a
position: relative
display: inline-block
margin: 5px
padding: 3px 12px
margin: 2px
padding: 2px 7px
line-height: 1.7
transition: all .3s cubic-bezier(.4, 0, .2, 1)
addBorderRadius(7)
overflow: hidden
color: white
transform: translateY(0) scale(1)
will-change: transform, background-color, box-shadow
&::before
position: absolute
top: 0
left: -100%
z-index: -1
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent)
content: ''
transition: left .6s cubic-bezier(.4, 0, .2, 1)
transition: all .3s
addBorderRadius(5)
&:hover
background: var(--btn-hover-color) !important
box-shadow:
0 6px 20px rgba(0, 0, 0, .12),
0 4px 8px rgba(0, 0, 0, .08),
0 0 0 1px rgba(255, 255, 255, .05)
background: var(--btn-bg) !important
box-shadow: 2px 2px 6px rgba(0, 0, 0, .2)
color: var(--btn-color) !important
transform: translateY(-2px) scale(1.02)
&::before
left: 100%
&:active
box-shadow:
0 3px 8px rgba(0, 0, 0, .15),
0 1px 3px rgba(0, 0, 0, .1)
transition: all .15s cubic-bezier(.4, 0, .2, 1)
transform: translateY(-1px) scale(.98)
+maxWidth768()
zoom: .85
&:hover
transform: translateY(-1px) scale(1.01)
&:active
transform: translateY(0) scale(.99)
&::before
display: none
&-title
font-size: 2.57em
animation: titleSlideIn .8s cubic-bezier(.4, 0, .2, 1)
+maxWidth768()
font-size: 2em
@keyframes tagsFadeIn
from
opacity: 0
transform: translateY(20px)
to
opacity: 1
transform: translateY(0)
@keyframes titleSlideIn
from
opacity: 0
transform: translateX(-30px)
to
opacity: 1
transform: translateX(0)
.page-title
& + .tag-cloud-list
text-align: left

View File

@@ -1,13 +1,93 @@
#algolia-search
.search-dialog
.ais-SearchBox
input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
.ais-SearchBox-loadingIndicator
position: absolute
top: 18px
left: 67px
.ais-Hits-list
+maxWidth768()
min-height: calc(var(--search-height) - 245px)
margin: 0
padding: 0
@extend .list-beauty
a
color: var(--search-a-color)
&:hover
color: $search-color
mark
background: transparent
color: $search-keyword-highlight
font-weight: bold
.algolia-hits-item-title
font-weight: 600
.algolia-hit-item-content
margin: 0 0 8px
word-break: break-word
.ais-Pagination
margin: 15px 0 0
padding: 0
text-align: center
.ais-Pagination-list
margin: 0
padding: 0
list-style: none
.ais-Pagination-item
display: inline
margin: 0 4px
padding: 0
.ais-Pagination-link
display: inline-block
min-width: 24px
height: 24px
text-align: center
line-height: 24px
addBorderRadius()
.ais-Pagination-item--selected
a
background: $theme-paginator-color
color: #eee
cursor: default
.ais-Pagination-item--disabled
visibility: hidden
#algolia-hits
> div
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 220px)
+maxWidth768()
max-height: none
height: calc(var(--search-height) - 235px)
#algolia-info
div
display: inline
.algolia-poweredBy
float: right
padding-top: 2px
vertical-align: text-top
svg
height: 1.1em

View File

@@ -20,174 +20,22 @@
border-radius: 0
.search-nav
display: flex
justify-content: space-between
align-items: center
margin-bottom: 14px
margin: 0 0 14px
color: $search-color
font-size: 1.4em
line-height: 1
.search-dialog-title
margin-right: 4px
#loading-status
&[hidden]
display: none !important
margin-right: 10px
.search-close-button
flex: 1
float: right
color: $grey
text-align: right
transition: all .2s ease
transition: color .2s ease-in-out
&:hover
color: $search-color
.local-search-input,
#algolia-search-input
margin: 0 auto
max-width: 100%
width: 100%
input,
.ais-SearchBox-input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
-webkit-appearance: none
&::placeholder
color: var(--text-color)
.search-result-list,
.ais-Hits-list
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 220px)
.local-search-hit-item,
.ais-Hits-item
display: flex
align-items: flex-start
margin: 3px 0
line-height: 1.8
transition: all .2s ease-in-out
&:hover
transform: translateY(-1px)
&:not([value])::before
display: none
&[value]::before
display: inline-flex
flex-shrink: 0
justify-content: center
align-items: center
margin-right: 6px
margin-top: 3px
min-width: 24px
color: $search-color
content: attr(value) '.'
font-weight: bold
font-style: italic
font-size: .9em
&::marker
content: none
a
flex: 1
color: var(--search-a-color)
&:hover
color: $search-color
.search-result-title,
.algolia-hits-item-title
font-weight: 600
.search-result,
.algolia-hit-item-content
margin: 0 0 8px
word-break: break-all
font-size: .9em
.ais-Pagination
margin: 15px 0 0
padding: 0
text-align: center
.ais-Pagination-list
display: flex
flex-wrap: wrap
justify-content: center
align-items: center
margin: 0
padding: 0
list-style: none
gap: 6px
.ais-Pagination-item
display: flex
padding: 0
&:not(.ais-Pagination-item--selected):not(.ais-Pagination-item--ellipsis):not(.ais-Pagination-item--disabled)
.ais-Pagination-link:hover
background: var(--btn-hover-color)
transform: translateY(-1px)
.ais-Pagination-link
display: inline-flex
justify-content: center
align-items: center
padding: 4px 8px
min-width: 28px
height: 28px
border-radius: 6px
background: var(--btn-bg)
color: var(--btn-color)
transition: all .2s ease
&.ais-Pagination-link--disabled
opacity: .3
cursor: not-allowed
i
font-size: 12px
.ais-Pagination-item--selected
.ais-Pagination-link
background: $theme-paginator-color
font-weight: 600
cursor: default
.ais-Pagination-item--ellipsis
.ais-Pagination-link
padding: 4px 2px
border: none
background: transparent
color: var(--text-color)
cursor: default
&:hover
background: transparent
transform: none
.ais-Pagination-item--disabled
.ais-Pagination-link
opacity: .4
+maxWidth768()
.ais-Pagination-list
gap: 4px
hr
margin: 15px auto
@extend .custom-hr
@@ -202,38 +50,6 @@
display: none
background: rgba($dark-black, .6)
.search-result-stats,
.ais-Stats-text
margin: 15px 0 0
color: var(--text-color)
text-align: center
font-size: .9em
.search-keyword
background: transparent
color: $search-keyword-highlight
font-weight: 600
.search-loading
display: flex
justify-content: center
align-items: center
padding: 20px
color: var(--text-color)
&::before
width: 16px
height: 16px
border: 2px solid var(--text-color)
border-top-color: transparent
border-radius: 50%
content: ''
animation: spin 1s linear infinite
@keyframes spin
to
transform: rotate(360deg)
if hexo-config('search.use') == 'algolia_search'
@require 'algolia'
else if hexo-config('search.use') == 'local_search'

View File

@@ -1,18 +1,57 @@
#local-search
.search-dialog
.local-search-box
margin: 0 auto
max-width: 100%
width: 100%
input
padding: 5px 14px
width: 100%
outline: none
border: 2px solid $search-color
border-radius: 40px
background: var(--search-bg)
color: var(--search-input-color)
-webkit-appearance: none
.search-wrap
display: none
.local-search-hit-item
margin-left: 24px
padding-left: 3px
line-height: 1.8
&::marker
color: $search-color
font-weight: bold
font-style: italic
a
color: var(--search-a-color)
&:hover
color: $search-color
.search-result-title
font-weight: 600
.search-result
margin: 0 0 8px
word-break: break-all
font-size: .9em
.search-result-list
overflow-y: overlay
margin: 0 -20px
padding: 0 22px
max-height: calc(80vh - 180px)
+maxWidth768()
if hexo-config('search.local_search.pagination.enable')
min-height: calc(var(--search-height) - 255px) !important
else
max-height: calc(var(--search-height) - 200px) !important
max-height: calc(var(--search-height) - 190px) !important
#local-search-stats
.search-result-stats
text-align: left
.search-keyword
font-weight: 600
#loading-database ~ *
visibility: hidden
.search-keyword
background: transparent
color: $search-keyword-highlight
font-weight: 600

View File

@@ -7,20 +7,21 @@
display: inline-block
margin: 0 4px 6px
padding: 0 15px
background-color: var(--btn-beautify-color, var(--btn-default-color))
color: var(--btn-color, $btn-color)
vertical-align: top
background-color: var(--btn-beautify-color, $btn-default-color)
color: $btn-color
line-height: 2
addBorderRadius()
@extend .btn-effects
for $type in $color-types
&.{$type}
--btn-beautify-color: unquote('var(--tags-' + $type + '-color)')
--btn-beautify-color: lookup('$tagsP-' + $type + '-color')
&:hover
background-color: var(--btn-hover-color)
i + span
margin-left: 6px
&:not(.block) + .btn-beautify:not(.block)
margin: 0 4px 20px
@@ -38,26 +39,18 @@
&.larger
padding: 6px 15px
@extend .btn-effects-large
&:hover
text-decoration: none
&.outline
border: 1px solid transparent
border-color: var(--btn-beautify-color, var(--btn-default-color))
border-color: var(--btn-beautify-color, $btn-default-color)
background-color: transparent
color: var(--btn-beautify-color, var(--btn-default-color))
i,
span
transition: color .3s cubic-bezier(.4, 0, .2, 1)
&::before
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, .1), transparent)
color: var(--btn-beautify-color, $btn-default-color)
&:hover
border-color: var(--btn-beautify-color, var(--btn-default-color))
background-color: var(--btn-beautify-color, var(--btn-default-color))
color: var(--btn-color) !important
background-color: var(--btn-beautify-color, $btn-default-color)
i,
span
color: var(--btn-color)
&:hover
color: white !important

View File

@@ -7,7 +7,6 @@
background: $tag-hide-bg
color: var(--white)
addBorderRadius()
@extend .btn-effects
&:hover
background-color: var(--btn-hover-color)
@@ -39,26 +38,11 @@
border: 1px solid $tag-hide-toggle-bg
addBorderRadius(5, true)
& > .toggle-content
margin: 30px 24px
& > .toggle-button
padding: 6px 15px
background: $tag-hide-toggle-bg
color: #1F2D3D
list-style: none
cursor: pointer
&::-webkit-details-marker
display: none
&::before
@extend .fontawesomeIcon
margin-right: 8px
content: '\f0d7'
transition: transform .3s ease
transform: rotate(-90deg)
transform-origin: center center
&[open] summary::before
transform: rotate(0)
& > .toggle-content
margin: 30px 24px

View File

@@ -1,11 +1,11 @@
.hl-label
padding: 2px 4px
color: var(--btn-color, $btn-color)
color: $btn-color
addBorderRadius(3)
&.default
background-color: var(--btn-default-color)
background-color: $btn-default-color
for $type in $color-types
&.{$type}
background-color: unquote('var(--tags-' + $type + '-color)')
background-color: lookup('$tagsP-' + $type + '-color')

View File

@@ -19,32 +19,32 @@
for $type in $color-types
&.{$type}
&:not(.disabled)
border-left-color: unquote('var(--tags-' + $type + '-color)') !important
border-left-color: lookup('$tagsP-' + $type + '-color') !important
&.modern
border-left-color: transparent !important
color: unquote('var(--tags-' + $type + '-color)')
color: lookup('$tagsP-' + $type + '-color')
&:not(.simple)
background: unquote('var(--tags-' + $type + '-color-lighten)')
background: lighten(lookup('$tagsP-' + $type + '-color'), 85%) !important
& > .note-icon
color: unquote('var(--tags-' + $type + '-color)')
color: lookup('$tagsP-' + $type + '-color')
&.simple
border: 1px solid var(--note-default-border)
border: 1px solid #EEEEEE
border-left-width: 5px
&.modern
border: 1px solid transparent !important
background-color: var(--note-modern-default-bg)
color: var(--note-modern-default-text)
background-color: #f5f5f5
color: $font-black
&.flat
border: initial
border-left: 5px solid var(--note-default-border)
background-color: var(--note-default-bg)
color: var(--note-default-text)
border-left: 5px solid #EEEEEE
background-color: lighten(#EEEEEE, 65%)
color: $font-black
h2,
h3,
@@ -90,30 +90,29 @@
for $type in $note-types
&.{$type}
&.flat
background: unquote('var(--note-' + $type + '-bg)')
color: var(--font-color)
background: lookup('$note-' + $type + '-bg')
&.modern
border-color: unquote('var(--note-modern-' + $type + '-border)') !important
background: unquote('var(--note-modern-' + $type + '-bg)')
color: unquote('var(--note-modern-' + $type + '-text)')
border-color: lookup('$note-modern-' + $type + '-border')
background: lookup('$note-modern-' + $type + '-bg')
color: lookup('$note-modern-' + $type + '-text')
a
&:not(.btn)
color: unquote('var(--note-modern-' + $type + '-text)')
color: lookup('$note-modern-' + $type + '-text')
&:hover
color: unquote('var(--note-modern-' + $type + '-hover)')
color: lookup('$note-modern-' + $type + '-hover')
&:not(.modern)
border-left-color: unquote('var(--note-' + $type + '-border)')
border-left-color: lookup('$note-' + $type + '-border')
h2,
h3,
h4,
h5,
h6
color: unquote('var(--note-' + $type + '-text)')
color: lookup('$note-' + $type + '-text')
if $note-icons
&:not(.no-icon)
@@ -122,4 +121,4 @@
&:not(.modern)
&::before
color: unquote('var(--note-' + $type + '-text)')
color: lookup('$note-' + $type + '-text')

View File

@@ -14,14 +14,14 @@
flex-wrap: wrap
margin: 0
padding: 0
background: var(--tab-button-bg)
background: var(--tab-botton-bg)
> .tab
flex-grow: 1
padding: 8px 18px
border-top: 2px solid var(--tab-border-color)
background: var(--tab-button-bg)
color: var(--tab-button-color)
background: var(--tab-botton-bg)
color: var(--tab-botton-color)
line-height: 2
transition: all .4s

View File

@@ -13,17 +13,19 @@ $code-foreground = $themeColorEnable && hexo-config('theme_color.code_foreground
$code-background = $themeColorEnable && hexo-config('theme_color.code_background') ? convert(hexo-config('theme_color.code_background')) : rgba(27, 31, 35, .05)
$theme-toc-color = $themeColorEnable && hexo-config('theme_color.toc_color') ? convert(hexo-config('theme_color.toc_color')) : $strong-cyan
// font
$chineseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
$default-font-family = -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', $chineseFont, sans-serif
$default-code-font = consolas, Menlo, monospace, 'PingFang SC', $chineseFont, sans-serif
$font-family = hexo-config('font.font_family') ? unquote(hexo-config('font.font_family')) : $default-font-family
$code-font-family = hexo-config('font.code_font_family') ? unquote(hexo-config('font.code_font_family')) : $default-code-font
$chinseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
$dafault-font-family = -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', $chinseFont, sans-serif
$dafault-code-font = consolas, Menlo, monospace, 'PingFang SC', $chinseFont, sans-serif
$font-family = hexo-config('font.font_family') ? unquote(hexo-config('font.font_family')) : $dafault-font-family
$code-font-family = hexo-config('font.code_font_family') ? unquote(hexo-config('font.code_font_family')) : $dafault-code-font
$site-name-font = hexo-config('blog_title_font.font_family') && unquote(hexo-config('blog_title_font.font_family'))
// hr
$hrEnable = hexo-config('hr_icon') && hexo-config('hr_icon.enable')
$hr-icon = $hrEnable && hexo-config('hr_icon.icon') ? hexo-config('hr_icon.icon') : '\f0c4'
$hr-icon-top = $hrEnable && hexo-config('hr_icon.icon_top') ? convert(hexo-config('hr_icon.icon_top')) : -10px
// page beautify
// page beatutify
$beautifyEnable = hexo-config('beautify.enable')
$title-prefix-icon = $beautifyEnable && hexo-config('beautify.title_prefix_icon') ? hexo-config('beautify.title_prefix_icon') : '\f0c1'
$title-prefix-icon-color = $beautifyEnable && hexo-config('beautify.title_prefix_icon_color') ? convert(hexo-config('beautify.title_prefix_icon_color')) : $light-red
@@ -173,8 +175,8 @@ $tagsP-purple-color = #6f42c1
$tagsP-green-color = #5cb85c
// Tag Plugins - Tab
$tab-border-color = #f0f0f0
$tab-button-bg = #f0f0f0
$tab-button-color = $font-color
$tab-botton-bg = #f0f0f0
$tab-botton-color = $font-color
$tab-button-hover-bg = darken($tab-border-color, 8)
$tab-active-border-color = $theme-color
$tab-button-active-bg = $card-bg
@@ -182,52 +184,3 @@ $tab-to-top-color = #99a9bf
$tab-to-top-hover-color = $theme-color
// Tag Plugins - timeline
$timeline-default-color = $theme-color
// note - darkmode
// Default
$note-dark-default-border = #5a5a5a
$note-dark-default-bg = #2b2b2b
$note-dark-default-text = #b3b3b3
$note-dark-modern-default-border = #9a9a9a
$note-dark-modern-default-bg = #353535
$note-dark-modern-default-text = #c4c4c4
$note-dark-modern-default-hover = #d0d0d0
// Primary
$note-dark-primary-border = #5935a1
$note-dark-primary-bg = #2e1c3e
$note-dark-primary-text = #a47dd4
$note-dark-modern-primary-border = #9985cc
$note-dark-modern-primary-bg = #3c2d4c
$note-dark-modern-primary-text = #b693e6
$note-dark-modern-primary-hover = #c9a8f0
// Info
$note-dark-info-border = #346fa2
$note-dark-info-bg = #1f2e3b
$note-dark-info-text = #7bb3db
$note-dark-modern-info-border = #7ca8b5
$note-dark-modern-info-bg = #2b3c44
$note-dark-modern-info-text = #8fc6e0
$note-dark-modern-info-hover = #a3d4ea
// Success
$note-dark-success-border = #4a944a
$note-dark-success-bg = #202e20
$note-dark-success-text = #82c682
$note-dark-modern-success-border = #8bb087
$note-dark-modern-success-bg = #2c3d2c
$note-dark-modern-success-text = #96d196
$note-dark-modern-success-hover = #a8dca8
// Warning
$note-dark-warning-border = #c08a3e
$note-dark-warning-bg = #3e301f
$note-dark-warning-text = #e6ba6b
$note-dark-modern-warning-border = #b8a285
$note-dark-modern-warning-bg = #4b3c2b
$note-dark-modern-warning-text = #d4b373
$note-dark-modern-warning-hover = #e0c080
// Danger
$note-dark-danger-border = #b34440
$note-dark-danger-bg = #3b201f
$note-dark-danger-text = #e67572
$note-dark-modern-danger-border = #c7898c
$note-dark-modern-danger-bg = #4d2b2e
$note-dark-modern-danger-text = #d98b8e
$note-dark-modern-danger-hover = #e59fa2

View File

@@ -61,17 +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 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')
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
const isPrismjs = plugin === 'prismjs'
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
@@ -79,46 +76,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (GLOBAL_CONFIG.Snackbar !== undefined) {
btf.snackbarShow(text)
} else {
const newEle = document.createElement('div')
newEle.className = 'copy-notice'
newEle.textContent = text
document.body.appendChild(newEle)
const buttonRect = ele.getBoundingClientRect()
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
// X-axis boundary check
const halfWidth = newEle.offsetWidth / 2
const centerLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
const finalLeft = Math.max(halfWidth + 10, Math.min(window.innerWidth - halfWidth - 10, centerLeft))
// Show tooltip below button if too close to top
const normalTop = buttonRect.top + scrollTop - 40
const shouldShowBelow = buttonRect.top < 60 || normalTop < 10
const topValue = shouldShowBelow ? buttonRect.top + scrollTop + buttonRect.height + 10 : normalTop
newEle.style.cssText = `
top: ${topValue + 10}px;
left: ${finalLeft}px;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s ease, top 0.3s ease;
`
requestAnimationFrame(() => {
newEle.style.opacity = '1'
newEle.style.top = `${topValue}px`
})
setTimeout(() => {
newEle.style.opacity = '0'
newEle.style.top = `${topValue + 10}px`
setTimeout(() => {
newEle?.remove()
}, 300)
}, 800)
ele.textContent = text
ele.style.opacity = 1
setTimeout(() => { ele.style.opacity = 0 }, 800)
}
}
@@ -136,10 +96,10 @@ document.addEventListener('DOMContentLoaded', () => {
const highlightCopyFn = (ele, clickEle) => {
const $buttonParent = ele.parentNode
$buttonParent.classList.add('copy-true')
const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
const codeElement = $buttonParent.querySelector(preCodeSelector)
if (!codeElement) return
copy(codeElement.innerText, clickEle)
copy(codeElement.innerText, clickEle.previousElementSibling)
$buttonParent.classList.remove('copy-true')
}
@@ -166,7 +126,6 @@ document.addEventListener('DOMContentLoaded', () => {
// 獲取隱藏狀態下元素的真實高度
const getActualHeight = item => {
if (item.offsetHeight > 0) return item.offsetHeight
const hiddenElements = new Map()
const fix = () => {
@@ -216,23 +175,20 @@ document.addEventListener('DOMContentLoaded', () => {
fragment.appendChild(ele)
}
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
}
$figureHighlight.forEach(item => {
let langName = ''
if (isNotHighlightJs) {
const newClassName = isPrismjs ? 'prismjs' : 'default'
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
}
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
if (!highlightLang) {
createEle('', item)
return
}
if (isNotHighlightJs) {
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
if (isPrismjs) {
langName = item.getAttribute('data-language') || 'Code'
} else {
langName = item.getAttribute('class').split(' ')[1]
if (langName === 'plain' || langName === undefined) langName = 'Code'
@@ -556,29 +512,17 @@ document.addEventListener('DOMContentLoaded', () => {
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
let detectItem = ''
// Optimization: Cache header positions
let headerList = []
const updateHeaderPositions = () => {
headerList = Array.from($articleList).map(ele => ({
ele,
top: btf.getEleTop(ele),
id: ele.id
}))
}
updateHeaderPositions()
btf.addEventListenerPjax(window, 'resize', btf.throttle(updateHeaderPositions, 200))
const findHeadPosition = top => {
if (top === 0) return false
let currentId = ''
let currentIndex = ''
for (let i = 0; i < headerList.length; i++) {
const item = headerList[i]
if (top > item.top - 80) {
currentId = item.id ? '#' + encodeURI(item.id) : ''
for (let i = 0; i < $articleList.length; i++) {
const ele = $articleList[i]
if (top > btf.getEleTop(ele) - 80) {
const id = ele.id
currentId = id ? '#' + encodeURI(id) : ''
currentIndex = i
} else {
break
@@ -656,8 +600,7 @@ document.addEventListener('DOMContentLoaded', () => {
$body.classList.add('read-mode')
newEle.type = 'button'
newEle.className = 'exit-readmode'
newEle.innerHTML = '<i class="fas fa-sign-out-alt"></i>'
newEle.className = 'fas fa-sign-out-alt exit-readmode'
newEle.addEventListener('click', exitReadMode)
$body.appendChild(newEle)
},
@@ -755,7 +698,7 @@ document.addEventListener('DOMContentLoaded', () => {
const addCopyright = () => {
const { limitCount, languages } = GLOBAL_CONFIG.copyright
const handleCopy = e => {
const handleCopy = (e) => {
e.preventDefault()
const copyFont = window.getSelection(0).toString()
let textFont = copyFont

View File

@@ -26,12 +26,7 @@ window.addEventListener('load', () => {
const openSearch = () => {
btf.overflowPaddingR.add()
animateElements(true)
showLoading(false)
setTimeout(() => {
const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
if (searchInput) searchInput.focus()
}, 100)
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
const handleEscape = event => {
if (event.code === 'Escape') {
@@ -60,38 +55,9 @@ window.addEventListener('load', () => {
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
}
const cutContent = content => {
const cutContent = (content) => {
if (!content) return ''
let contentStr = ''
if (typeof content === 'string') {
contentStr = content.trim()
} else if (typeof content === 'object') {
if (content.value !== undefined) {
contentStr = String(content.value).trim()
if (!contentStr) return ''
} else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) {
return ''
} else {
try {
contentStr = JSON.stringify(content).trim()
if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') {
return ''
}
} catch (e) {
return ''
}
}
} else if (content.toString && typeof content.toString === 'function') {
contentStr = content.toString().trim()
if (contentStr === '[object Object]' || contentStr === '[object Array]') {
return ''
}
} else {
return ''
}
const firstOccur = contentStr.indexOf('<mark>')
const firstOccur = content.indexOf('<mark>')
let start = firstOccur - 30
let end = firstOccur + 120
let pre = ''
@@ -104,454 +70,94 @@ window.addEventListener('load', () => {
pre = '...'
}
if (end > contentStr.length) {
end = contentStr.length
if (end > content.length) {
end = content.length
} else {
post = '...'
}
// Ensure we don't cut off HTML tags in the middle
let substr = contentStr.substring(start, end)
// Handle tag completeness
// Check for incomplete opening tags at the beginning
const firstCloseBracket = substr.indexOf('>')
const firstOpenBracket = substr.indexOf('<')
// If there's a closing bracket but no opening bracket before it, we've cut a tag
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
substr = substr.substring(firstCloseBracket + 1)
}
// Check for incomplete closing tags at the end
const lastOpenBracket = substr.lastIndexOf('<')
const lastCloseBracket = substr.lastIndexOf('>')
// If there's an opening bracket after the last closing bracket, we've cut a tag
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
substr = substr.substring(0, lastOpenBracket)
}
// Balance tags in the substring
const tagStack = []
let balancedStr = ''
let i = 0
while (i < substr.length) {
if (substr[i] === '<') {
// Check if it's a closing tag
if (substr[i + 1] === '/') {
const closeTagEnd = substr.indexOf('>', i)
if (closeTagEnd !== -1) {
const closeTagName = substr.substring(i + 2, closeTagEnd)
// Remove matching opening tag from stack
for (let j = tagStack.length - 1; j >= 0; j--) {
if (tagStack[j] === closeTagName) {
tagStack.splice(j, 1)
break
}
}
balancedStr += substr.substring(i, closeTagEnd + 1)
i = closeTagEnd + 1
continue
}
} else if (substr.substr(i, 2) === '<!' || (substr.indexOf('/>', i) !== -1 && substr.indexOf('/>', i) < substr.indexOf('>', i))) {
const tagEnd = substr.indexOf('>', i)
if (tagEnd !== -1) {
balancedStr += substr.substring(i, tagEnd + 1)
i = tagEnd + 1
continue
}
} else {
const tagEnd = substr.indexOf('>', i)
if (tagEnd !== -1) {
const tagName = substr.substring(i + 1, (substr.indexOf(' ', i) > -1 && substr.indexOf(' ', i) < tagEnd)
? substr.indexOf(' ', i)
: tagEnd).split(/\s/)[0]
tagStack.push(tagName)
balancedStr += substr.substring(i, tagEnd + 1)
i = tagEnd + 1
continue
}
}
}
balancedStr += substr[i]
i++
}
// Close any unclosed tags
while (tagStack.length > 0) {
const tagName = tagStack.pop()
balancedStr += `</${tagName}>`
}
// If we removed content from the beginning, add prefix
if (start > 0 || pre) {
const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0)
const actualFirstMark = contentStr.indexOf('<mark>', start > 0 ? start - 30 : 0)
if (actualFirstOpenBracket !== -1 &&
(actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
pre = '...'
}
}
substr = balancedStr
return `${pre}${substr}${post}`
return `${pre}${content.substring(start, end)}${post}`
}
// Helper function to handle Algolia highlight results
const extractHighlightValue = highlightObj => {
if (!highlightObj) return ''
const disableDiv = [
document.getElementById('algolia-hits'),
document.getElementById('algolia-pagination'),
document.querySelector('#algolia-info .algolia-stats')
]
if (typeof highlightObj === 'string') {
return highlightObj.trim()
}
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
return String(highlightObj.value).trim()
}
return ''
}
// Initialize Algolia client
let searchClient
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
} else if (typeof window.algoliasearch === 'function') {
searchClient = window.algoliasearch(appId, apiKey)
} else {
return console.error('Algolia search client not found!')
}
if (!searchClient) {
return console.error('Failed to initialize Algolia search client')
}
// Search state
let currentQuery = ''
// Show loading state
const showLoading = show => {
const loadingIndicator = document.getElementById('loading-status')
if (loadingIndicator) {
loadingIndicator.hidden = !show
}
}
// Cache frequently used elements
const elements = {
get searchInput () { return document.querySelector('#algolia-search-input .ais-SearchBox-input') },
get hits () { return document.getElementById('algolia-hits') },
get hitsEmpty () { return document.getElementById('algolia-hits-empty') },
get hitsList () { return document.querySelector('#algolia-hits .ais-Hits-list') },
get hitsWrapper () { return document.querySelector('#algolia-hits .ais-Hits') },
get pagination () { return document.getElementById('algolia-pagination') },
get paginationList () { return document.querySelector('#algolia-pagination .ais-Pagination-list') },
get stats () { return document.querySelector('#algolia-info .ais-Stats-text') },
}
// Show/hide search results area
const toggleResultsVisibility = hasResults => {
elements.pagination.style.display = hasResults ? '' : 'none'
elements.stats.style.display = hasResults ? '' : 'none'
}
// Render search results
const renderHits = (hits, query, page = 0) => {
if (hits.length === 0 && query) {
elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
elements.hitsEmpty.style.display = ''
elements.hitsWrapper.style.display = 'none'
elements.stats.style.display = 'none'
return
}
elements.hitsEmpty.style.display = 'none'
const hitsHTML = hits.map((hit, index) => {
const itemNumber = page * hitsPerPage + index + 1
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
const result = hit._highlightResult || hit
// Content extraction
let content = ''
try {
if (result.contentStripTruncate) {
content = cutContent(result.contentStripTruncate)
} else if (result.contentStrip) {
content = cutContent(result.contentStrip)
} else if (result.content) {
content = cutContent(result.content)
} else if (hit.contentStripTruncate) {
content = cutContent(hit.contentStripTruncate)
} else if (hit.contentStrip) {
content = cutContent(hit.contentStrip)
} else if (hit.content) {
content = cutContent(hit.content)
}
} catch (error) {
content = ''
}
// Title handling
let title = 'no-title'
try {
if (result.title) {
title = extractHighlightValue(result.title) || 'no-title'
} else if (hit.title) {
title = extractHighlightValue(hit.title) || 'no-title'
}
if (!title || title === 'no-title') {
if (typeof hit.title === 'string' && hit.title.trim()) {
title = hit.title.trim()
} else if (hit.title && typeof hit.title === 'object' && hit.title.value) {
title = String(hit.title.value).trim() || 'no-title'
} else {
title = 'no-title'
}
}
} catch (error) {
title = 'no-title'
}
return `
<li class="ais-Hits-item" value="${itemNumber}">
<a href="${link}" class="algolia-hit-item-link">
<span class="algolia-hits-item-title">${title}</span>
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
</a>
</li>`
}).join('')
elements.hitsList.innerHTML = hitsHTML
elements.hitsWrapper.style.display = query ? '' : 'none'
if (hits.length > 0) {
elements.stats.style.display = ''
}
}
// Render pagination
const renderPagination = (page, nbPages) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
</li>`
if (startPage > 1) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
</li>`
} else {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
</li>`
}
}
// Only add ellipsis and last page when there are many pages
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
</li>`
}
if (nbPages > 1) {
elements.paginationList.innerHTML = `
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
${isFirstPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</li>
${pagesHTML}
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
${isLastPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
elements.pagination.style.display = currentQuery ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render statistics
const renderStats = (nbHits, processingTimeMS, query) => {
if (query) {
const stats = languages.hits_stats
.replace(/\$\{hits}/, nbHits)
.replace(/\$\{time}/, processingTimeMS)
elements.stats.innerHTML = `<hr>${stats}`
elements.stats.style.display = ''
} else {
elements.stats.style.display = 'none'
}
}
// Perform search
const performSearch = async (query, page = 0) => {
if (!query.trim()) {
currentQuery = ''
renderHits([], '', 0)
renderPagination(0, 0)
renderStats(0, 0, '')
toggleResultsVisibility(false)
return
}
showLoading(true)
currentQuery = query
try {
let result
if (searchClient && typeof searchClient.search === 'function') {
// v5 multi-index search
const searchResult = await searchClient.search([{
indexName,
query,
params: {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
}
}])
result = searchResult.results[0]
} else if (searchClient && typeof searchClient.initIndex === 'function') {
// v4 single-index search
const index = searchClient.initIndex(indexName)
result = await index.search(query, {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
})
} else {
throw new Error('Algolia: No compatible search method available')
}
renderHits(result.hits || [], query, page)
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
renderPagination(page, actualNbPages)
renderStats(result.nbHits || 0, result.processingTimeMS || 0, query)
const hasResults = result.hits && result.hits.length > 0
toggleResultsVisibility(hasResults)
// Refresh Pjax links
if (window.pjax) {
window.pjax.refresh(document.getElementById('algolia-hits'))
}
} catch (error) {
console.error('Algolia search error:', error)
renderHits([], query, page)
renderPagination(0, 0)
renderStats(0, 0, query)
} finally {
showLoading(false)
}
}
// Debounced search
let searchTimeout
const debouncedSearch = (query, delay = 300) => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => performSearch(query), delay)
}
// Initialize search box and events
const initializeSearch = () => {
showLoading(false)
if (elements.searchInput) {
elements.searchInput.addEventListener('input', e => {
const query = e.target.value
debouncedSearch(query)
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
const search = instantsearch({
indexName,
searchClient: searchClient(appId, apiKey),
searchFunction (helper) {
disableDiv.forEach(item => {
item.style.display = helper.state.query ? '' : 'none'
})
if (helper.state.query) helper.search()
}
})
const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
if (searchForm) {
searchForm.addEventListener('submit', e => {
e.preventDefault()
const query = elements.searchInput.value
performSearch(query)
})
}
// Pagination event delegation
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentQuery) {
performSearch(currentQuery, page)
const widgets = [
instantsearch.widgets.configure({ hitsPerPage }),
instantsearch.widgets.searchBox({
container: '#algolia-search-input',
showReset: false,
showSubmit: false,
placeholder: languages.input_placeholder,
showLoadingIndicator: true
}),
instantsearch.widgets.hits({
container: '#algolia-hits',
templates: {
item (data) {
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
const result = data._highlightResult
const content = result.contentStripTruncate
? cutContent(result.contentStripTruncate.value)
: result.contentStrip
? cutContent(result.contentStrip.value)
: result.content
? cutContent(result.content.value)
: ''
return `
<a href="${link}" class="algolia-hit-item-link">
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
</a>`
},
empty (data) {
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
}
}
}),
instantsearch.widgets.stats({
container: '#algolia-info > .algolia-stats',
templates: {
text (data) {
const stats = languages.hits_stats
.replace(/\$\{hits}/, data.nbHits)
.replace(/\$\{time}/, data.processingTimeMS)
return `<hr>${stats}`
}
}
}),
instantsearch.widgets.poweredBy({
container: '#algolia-info > .algolia-poweredBy'
}),
instantsearch.widgets.pagination({
container: '#algolia-pagination',
totalPages: 5,
templates: {
first: '<i class="fas fa-angle-double-left"></i>',
last: '<i class="fas fa-angle-double-right"></i>',
previous: '<i class="fas fa-angle-left"></i>',
next: '<i class="fas fa-angle-right"></i>'
}
})
]
// Initial state
toggleResultsVisibility(false)
}
// Initialize
initializeSearch()
search.addWidgets(widgets)
search.start()
searchClickFn()
searchFnOnce()
@@ -559,4 +165,10 @@ window.addEventListener('load', () => {
if (!btf.isHidden($searchMask)) closeSearch()
searchClickFn()
})
if (window.pjax) {
search.on('render', () => {
window.pjax.refresh(document.getElementById('algolia-hits'))
})
}
})

View File

@@ -156,10 +156,10 @@ class LocalSearch {
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
})
resultItem += '</a></li>'
resultItem += '</li>'
resultItems.push({
item: resultItem,
id: resultItems.length,
@@ -236,229 +236,40 @@ class LocalSearch {
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
const enablePagination = pagination && pagination.enable
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
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 input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
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(
'<li class="local-search-hit-item">',
`<li class="local-search-hit-item" value="${itemNumber}">`
)
})
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
// Update stats
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
// 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 += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
</li>`
if (startPage > 1) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
</li>`
} else {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
</li>`
}
}
// Only add ellipsis and last page when there are many pages
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
</li>`
}
if (nbPages > 1) {
elements.paginationList.innerHTML = `
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
${isFirstPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</li>
${pagesHTML}
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
${isLastPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
} 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, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.hidden = false
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
clearSearchResults()
container.textContent = ''
statsItem.textContent = ''
} else if (resultItems.length === 0) {
showNoResults(searchText)
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
} else {
// Sort results by relevance
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
@@ -468,14 +279,14 @@ window.addEventListener('load', () => {
return right.id - left.id
})
if (enablePagination) {
currentResultItems = resultItems
currentPage = 0
}
renderResults(searchText, resultItems)
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
}
$loadingStatus.hidden = true
$loadingStatus.textContent = ''
}
let loadFlag = false
@@ -529,29 +340,11 @@ window.addEventListener('load', () => {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
// Pagination event delegation - only add if pagination is enabled
if (enablePagination) {
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentResultItems.length > 0) {
currentPage = page
renderResults(input.value.trim().toLowerCase(), currentResultItems)
}
}
})
}
// Initial state
toggleResultsVisibility(false)
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.visibility = 'visible'
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.remove()
})

File diff suppressed because one or more lines are too long

View File

@@ -14,35 +14,37 @@
}
},
throttle: (func, wait, options = {}) => {
let timeout, args
throttle: function (func, wait, options = {}) {
let timeout, context, args
let previous = 0
const later = () => {
previous = options.leading === false ? 0 : new Date().getTime()
timeout = null
func(...args)
if (!timeout) args = null
func.apply(context, args)
if (!timeout) context = args = null
}
return (...params) => {
const throttled = (...params) => {
const now = new Date().getTime()
if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous)
context = this
args = params
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func(...args)
if (!timeout) args = null
func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}
return throttled
},
overflowPaddingR: {
@@ -104,7 +106,7 @@
loadComment: (dom, callback) => {
if ('IntersectionObserver' in window) {
const observerItem = new IntersectionObserver(entries => {
const observerItem = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
callback()
observerItem.disconnect()
@@ -167,18 +169,27 @@
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY,
getEleTop: ele => {
let actualTop = ele.offsetTop
let current = ele.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
},
loadLightbox: ele => {
const service = GLOBAL_CONFIG.lightbox
if (service === 'medium_zoom') {
mediumZoom(ele, { background: 'var(--zoom-bg)' })
return
}
if (service === 'fancybox') {
ele.forEach(i => {
Array.from(ele).forEach(i => {
if (i.parentNode.tagName !== 'A') {
const dataSrc = i.dataset.lazySrc || i.src
const dataCaption = i.title || i.alt || ''
@@ -187,71 +198,35 @@
})
if (!window.fancyboxRun) {
let options = ''
if (Fancybox.version < '6') {
options = {
Hash: false,
Thumbs: {
showOnStart: false
},
Images: {
Panzoom: {
maxScale: 4
}
},
Carousel: {
transition: 'slide'
},
Toolbar: {
display: {
left: ['infobar'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY'
],
right: ['slideshow', 'thumbs', 'close']
}
Fancybox.bind('[data-fancybox]', {
Hash: false,
Thumbs: {
showOnStart: false
},
Images: {
Panzoom: {
maxScale: 4
}
},
Carousel: {
transition: 'slide'
},
Toolbar: {
display: {
left: ['infobar'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY'
],
right: ['slideshow', 'thumbs', 'close']
}
}
} else {
options = {
Hash: false,
Carousel: {
transition: 'slide',
Thumbs: {
showOnStart: false
},
Toolbar: {
display: {
left: ['counter'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY',
'reset'
],
right: ['autoplay', 'thumbs', 'close']
}
},
Zoomable: {
Panzoom: {
maxScale: 4
}
}
}
}
}
Fancybox.bind('[data-fancybox]', options)
})
window.fancyboxRun = true
}
}