Compare commits

..

89 Commits
5.2.2 ... 5.5.2

214 changed files with 18079 additions and 15981 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 issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
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'] custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

2
.gitignore vendored Normal file
View File

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

226
README.md
View File

@@ -1,117 +1,193 @@
<div align="right"> <div align="right">
<a title="Chinese" href="/README_CN.md">中文</a> <a title="中文" href="/README_CN.md">中文</a>
</div> </div>
<div align="center"> <div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" /> <img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
# hexo-theme-butterfly # 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) ![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) ![dev 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) ![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+-0e83c) ![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) ![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](https://butterfly.js.org/) / [CrazyWong](https://blog.crazywong.com/) 📢 **Demo**: [Butterfly Official](https://butterfly.js.org/) | [CrazyWong's Blog](https://blog.crazywong.com/)
📖 Docs: [English](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) / [Chinese](https://butterfly.js.org/posts/21cfbf15/) 📖 **Documentation**: [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) | [中文文档](https://butterfly.js.org/posts/21cfbf15/)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png) ![Butterfly Theme Preview](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div> </div>
--- ---
## 💻 Installation ## 🚀 Quick Start
### GIT ### 💾 Installation
> If you are in Mainland China, you can download in [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git) #### Method 1: Git Installation (Recommended)
Stable branch [recommend]: > 💡 **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 git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
``` ```
Dev branch: ```bash
# Install development version (early access to new features)
```
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
``` ```
### NPM #### Method 2: NPM Installation
> It supports Hexo 5.0.0 or later > ⚠️ **Note**: NPM installation only supports Hexo 5.0.0 and above
In Hexo site root directory ```bash
npm install hexo-theme-butterfly
```powershell
npm i hexo-theme-butterfly
``` ```
## ⚙ Configuration ### Theme Configuration
Set theme in the hexo work folder's root config file `_config.yml`: 1. **Enable Theme**: Modify your Hexo configuration file `_config.yml`:
> theme: butterfly ```yaml
theme: butterfly
```
If you don't have pug & stylus renderer, try this: 2. **Install Dependencies**: If you haven't installed pug and stylus renderers, please run:
> npm install hexo-renderer-pug hexo-renderer-stylus ```bash
npm install hexo-renderer-pug hexo-renderer-stylus --save
```
## 🎉 Features ## ✨ Theme Features
- [x] Card UI Design ### 🎨 Design Style
- [x] Rounded Design/Squared Design - [x] **Card-based Design** - Modern card-style layout
- [X] Support sub-menu - [x] **Rounded/Square Design** - Customizable border styles
- [x] Two-column layout - [x] **Responsive Design** - Perfect adaptation to all screen sizes
- [x] Responsive Web Design - [x] **Two-column Layout** - Optimized reading experience
- [x] Dark Mode - [x] **Dark Mode** - Eye-friendly night 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/Pangu/Snackbar notification toast/PWA......
## Contributors ### 📝 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
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors"> ### 🔍 Search & Navigation
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" /> - [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch
</a> - [x] **Built-in 404** - Beautiful 404 error page
- [x] **Pjax Support** - Smooth page transition experience
## 📷 Screenshots ### 🎨 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
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg) ### 💬 Social Interaction
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg) - [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg) - [x] **Dual Comments Support** - Enable two comment systems simultaneously
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg) - [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>

View File

@@ -4,114 +4,190 @@
<div align="center"> <div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" /> <img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
# hexo-theme-butterfly # hexo-theme-butterfly
一個適用於 Hexo 的現代化、美觀且功能豐富的主題
![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/master?color=%231ab1ad&label=master)
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev) ![dev 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) ![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+-0e83c) ![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) ![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](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) 📖 **完整文檔**: [中文文檔](https://butterfly.js.org/posts/21cfbf15/) | [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png) ![Butterfly 主題預覽](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div> </div>
--- ---
## 💻 安裝 ## 🚀 快速開始
### Git 安裝 ### 💾 安裝方式
> 本倉庫同時上傳到 [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git),如果你訪問 Github 緩慢,可從 Gitee 中下載。 #### 方式一Git 安裝(推薦)
在博客根目錄裡安裝穩定版【推薦】 > 💡 **提示**: 如果您在中國大陸訪問 GitHub 速度較慢,可以使用 [Gitee 鏡像](https://gitee.com/immyw/hexo-theme-butterfly.git)
```powershell 在您的 Hexo 博客根目錄下執行:
```bash
# 安裝穩定版本(推薦)
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
``` ```
如果想要安裝比較新的dev分支可以 ```bash
# 安裝開發版本(搶先體驗新功能)
```powershell
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
``` ```
### npm 安裝 #### 方式二NPM 安裝
> 此方法只支持Hexo 5.0.0以上版本 > ⚠️ **注意**: NPM 安裝方式僅支援 Hexo 5.0.0以上版本
在博客根目錄裡 ```bash
npm install hexo-theme-butterfly
```powershell
npm i hexo-theme-butterfly
``` ```
## ⚙ 應用主題 ### 主題配置
修改hexo配置文件`_config.yml`,把主題改為`Butterfly` 1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`
``` ```yaml
theme: butterfly theme: butterfly
``` ```
>如果你沒有pug以及stylus渲染器,請下載安裝: npm install hexo-renderer-pug hexo-renderer-stylus --save 2. **安裝依賴**: 如果您尚未安裝 pug 和 stylus 渲染器,請執行:
## 🎉 特色 ```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/Pangu/Snackbar彈窗/PWA......
## ✨ 貢獻者 ### 🎨 設計風格
- [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" /> - [x] **多級選單** - 支援二級導航選單
</a> - [x] **閱讀模式** - 專注的文章閱讀體驗
- [x] **目錄導航** - 電腦和手機雙端支援 TOC
- [x] **字數統計** - 顯示文章字數和閱讀時間
- [x] **相關文章** - 智能推薦相關內容
- [x] **過期提醒** - 自動提示文章更新狀態
- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換
- [x] **標籤外掛** - 豐富的標籤外掛支持
## 📷 截圖 ### 🔍 搜尋與導航
- [x] **多種搜尋** - Algolia 搜尋 / 本地搜尋 / Docsearch
- [x] **內建 404** - 美觀的 404 錯誤頁面
- [x] **Pjax 支援** - 流暢的頁面切換體驗
![](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) - [x] **語法高亮** - 內建多種主題darker/pale night/light/ocean
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg) - [x] **程式碼功能** - 語言顯示/摺疊展開/複製按鈕/自動換行
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg) - [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>

View File

@@ -13,6 +13,7 @@ nav:
# Navigation bar logo image # Navigation bar logo image
logo: logo:
display_title: true display_title: true
display_post_title: true
# Whether to fix navigation bar # Whether to fix navigation bar
fixed: false fixed: false
@@ -158,7 +159,7 @@ subtitle:
# Choose: false/1/2/3 # Choose: false/1/2/3
# false - disable the function # false - disable the function
# 1 - hitokoto.cn # 1 - hitokoto.cn
# 2 - yijuzhan.com # 2 - https://api.aa1.cn/doc/yiyan.html
# 3 - jinrishici.com # 3 - jinrishici.com
source: false source: false
# If you close the typewriter effect, the subtitle will only show the first line of sub # If you close the typewriter effect, the subtitle will only show the first line of sub
@@ -254,12 +255,15 @@ noticeOutdate:
# Footer Settings # Footer Settings
# -------------------------------------- # --------------------------------------
footer: footer:
nav:
owner: owner:
enable: true enable: true
since: 2019 since: 2025
custom_text:
# Copyright of theme and framework # Copyright of theme and framework
copyright: true copyright:
enable: true
version: true
custom_text:
# -------------------------------------- # --------------------------------------
# Aside Settings # Aside Settings
@@ -399,6 +403,9 @@ rightside_item_order:
# Default: toc,chat,comment # Default: toc,chat,comment
show: show:
# Animation for the bottom right config button
rightside_config_animation: true
# -------------------------------------- # --------------------------------------
# Global Settings # Global Settings
# -------------------------------------- # --------------------------------------
@@ -481,6 +488,11 @@ search:
top_n_per_article: 1 top_n_per_article: 1
# Unescape html strings to the readable one. # Unescape html strings to the readable one.
unescape: false unescape: false
# Enable pagination for search results
pagination:
enable: false
# Number of search results per page
hitsPerPage: 8
CDN: CDN:
# Docsearch # Docsearch
@@ -503,12 +515,12 @@ share:
# Share.js # Share.js
# https://github.com/overtrue/share.js # https://github.com/overtrue/share.js
sharejs: sharejs:
sites: facebook,twitter,wechat,weibo,qq sites: facebook,x,wechat,weibo,qq
# AddToAny # AddToAny
# https://www.addtoany.com/ # https://www.addtoany.com/
addtoany: addtoany:
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
# -------------------------------------- # --------------------------------------
# Comments System # Comments System
@@ -687,6 +699,7 @@ umami_analytics:
enable: false enable: false
# For self-hosted setups, configure the hostname of the Umami instance # For self-hosted setups, configure the hostname of the Umami instance
serverURL: serverURL:
script_name: script.js
website_id: website_id:
option: option:
UV_PV: UV_PV:
@@ -696,6 +709,12 @@ umami_analytics:
# Umami Cloud (API key) / self-hosted Umami (token) # Umami Cloud (API key) / self-hosted Umami (token)
token: token:
# https://www.googletagmanager.com/
google_tag_manager:
tag_id:
# optional
domain:
# -------------------------------------- # --------------------------------------
# Advertisement # Advertisement
# -------------------------------------- # --------------------------------------
@@ -983,17 +1002,12 @@ snackbar:
# https://instant.page/ # https://instant.page/
instantpage: false instantpage: false
# Pangu - Insert a space between Chinese character and English character
# https://github.com/vinta/pangu.js
pangu:
enable: false
# Specify the field to use pangu (site or post)
field: site
# Lazyload # Lazyload
# https://github.com/verlok/vanilla-lazyload # https://github.com/verlok/vanilla-lazyload
lazyload: lazyload:
enable: false enable: false
# Use browser's native lazyload instead of vanilla-lazyload
native: false
# Specify the field to use lazyload (site or post) # Specify the field to use lazyload (site or post)
field: site field: site
placeholder: placeholder:
@@ -1023,6 +1037,14 @@ Open_Graph_meta:
# fb_admins: # fb_admins:
# fb_app_id: # fb_app_id:
# 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:
# Add the vendor prefixes to ensure compatibility # Add the vendor prefixes to ensure compatibility
css_prefix: true css_prefix: true
@@ -1045,7 +1067,7 @@ CDN:
third_party_provider: jsdelivr third_party_provider: jsdelivr
# Add version number to url, true or false # Add version number to url, true or false
version: false version: true
# Custom format # Custom format
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file} # For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
@@ -1081,7 +1103,6 @@ CDN:
# gitalk_css: # gitalk_css:
# giscus: # giscus:
# instantpage: # instantpage:
# instantsearch:
# katex: # katex:
# katex_copytex: # katex_copytex:
# lazyload: # lazyload:
@@ -1092,7 +1113,6 @@ CDN:
# medium_zoom: # medium_zoom:
# mermaid: # mermaid:
# meting_js: # meting_js:
# pangu:
# prismjs_autoloader: # prismjs_autoloader:
# prismjs_js: # prismjs_js:
# prismjs_lineNumber_js: # prismjs_lineNumber_js:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,15 +11,12 @@ div
if theme.instantpage if theme.instantpage
script(src=url_for(theme.asset.instantpage), type='module') script(src=url_for(theme.asset.instantpage), type='module')
if theme.lazyload.enable if theme.lazyload.enable && !theme.lazyload.native
script(src=url_for(theme.asset.lazyload)) script(src=url_for(theme.asset.lazyload))
if theme.snackbar.enable if theme.snackbar.enable
script(src=url_for(theme.asset.snackbar)) script(src=url_for(theme.asset.snackbar))
if theme.pangu.enable
!= partial("includes/third-party/pangu.pug", {}, { cache: true })
.js-pjax .js-pjax
if needLoadCountJs if needLoadCountJs
!= partial("includes/third-party/card-post-count/index", {}, { cache: true }) != partial("includes/third-party/card-post-count/index", {}, { cache: true })
@@ -36,7 +33,7 @@ div
!= partial("includes/third-party/prismjs", {}, { cache: true }) != partial("includes/third-party/prismjs", {}, { cache: true })
if theme.aside.enable && theme.aside.card_newest_comments.enable if theme.aside.enable && theme.aside.card_newest_comments.enable
if theme.pjax.enable || (!is_post() && page.aside !== false) if theme.pjax.enable || (globalPageType !== 'post' && page.aside !== false)
!= partial("includes/third-party/newest-comments/index", {}, { cache: true }) != partial("includes/third-party/newest-comments/index", {}, { cache: true })
!= fragment_cache('injectBottom', function(){return injectHtml(theme.inject.bottom)}) != fragment_cache('injectBottom', function(){return injectHtml(theme.inject.bottom)})
@@ -55,6 +52,10 @@ div
!= partial("includes/third-party/umami_analytics", {}, { cache: true }) != partial("includes/third-party/umami_analytics", {}, { cache: true })
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js') script(async data-pjax src=url_for(theme.asset.busuanzi) || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
!= partial('includes/third-party/search/index', {}, { cache: true }) != 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")

View File

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

View File

@@ -1,12 +1,18 @@
- var pageTitle - var pageTitle
- is_archive() ? page.title = findArchivesTitle(page, theme.menu, date) : '' - globalPageType === 'archive' ? page.title = findArchivesTitle(page, theme.menu, date) : ''
- if (is_tag()) pageTitle = _p('page.tag') + ': ' + page.tag case globalPageType
- else if (is_category()) pageTitle = _p('page.category') + ': ' + page.category when 'tag'
- else if (is_current('/404.html', [strict])) pageTitle = _p('error404') - pageTitle = _p('page.tag') + ': ' + page.tag
- else pageTitle = page.title || config.title || '' when 'category'
- pageTitle = _p('page.category') + ': ' + page.category
when '404'
- pageTitle = _p('error404')
default
- pageTitle = page.title || config.title || ''
- var isSubtitle = config.subtitle ? ' - ' + config.subtitle : '' - var isSubtitle = config.subtitle ? ' - ' + config.subtitle : ''
- var tabTitle = is_home() || !pageTitle ? config.title + isSubtitle : pageTitle + ' | ' + config.title - var tabTitle = globalPageType === 'home' || !pageTitle ? config.title + isSubtitle : pageTitle + ' | ' + config.title
- var pageAuthor = config.email ? config.author + ',' + config.email : config.author - var pageAuthor = config.email ? config.author + ',' + config.email : config.author
- var pageCopyright = config.copyright || config.author - var pageCopyright = config.copyright || config.author
- var themeColorLight = theme.theme_color && theme.theme_color.enable && theme.theme_color.meta_theme_color_light || '#ffffff' - var themeColorLight = theme.theme_color && theme.theme_color.enable && theme.theme_color.meta_theme_color_light || '#ffffff'
@@ -25,6 +31,9 @@ meta(name="theme-color" content=themeColor)
//- Open_Graph //- Open_Graph
include ./head/Open_Graph.pug include ./head/Open_Graph.pug
//- Structured Data
include ./head/structured_data.pug
!=favicon_tag(theme.favicon || config.favicon) !=favicon_tag(theme.favicon || config.favicon)
link(rel="canonical" href=urlNoIndex(null,config.pretty_urls.trailing_index,config.pretty_urls.trailing_html)) link(rel="canonical" href=urlNoIndex(null,config.pretty_urls.trailing_index,config.pretty_urls.trailing_html))

View File

@@ -2,7 +2,7 @@ if theme.Open_Graph_meta.enable
- -
const coverVal = page.cover_type === 'img' ? page.cover : theme.avatar.img const coverVal = page.cover_type === 'img' ? page.cover : theme.avatar.img
let ogOption = Object.assign({ let ogOption = Object.assign({
type: is_post() ? 'article' : 'website', type: globalPageType === 'post' ? 'article' : 'website',
image: coverVal ? full_url_for(coverVal) : '', image: coverVal ? full_url_for(coverVal) : '',
fb_admins: theme.facebook_comments.user_id || '', fb_admins: theme.facebook_comments.user_id || '',
fb_app_id: theme.facebook_comments.app_id || '', fb_app_id: theme.facebook_comments.app_id || '',

View File

@@ -32,3 +32,14 @@ if theme.microsoft_clarity
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; 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); y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}"); })(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')

View File

@@ -10,7 +10,6 @@
hitsPerPage: theme.search.algolia_search.hitsPerPage, hitsPerPage: theme.search.algolia_search.hitsPerPage,
// search languages // search languages
languages: { languages: {
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
hits_empty: _p("search.algolia_search.hits_empty"), hits_empty: _p("search.algolia_search.hits_empty"),
hits_stats: _p("search.algolia_search.hits_stats"), hits_stats: _p("search.algolia_search.hits_stats"),
} }
@@ -19,12 +18,16 @@
let localSearch = 'undefined' let localSearch = 'undefined'
if (theme.search.use === 'local_search') { if (theme.search.use === 'local_search') {
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
localSearch = JSON.stringify({ localSearch = JSON.stringify({
path: CDN || config.root + config.search.path, path: CDN || config.root + config.search.path,
preload, preload,
top_n_per_article, top_n_per_article,
unescape, unescape,
pagination: {
enable: pagination.enable,
hitsPerPage: pagination.hitsPerPage
},
languages: { languages: {
// search languages // search languages
hits_empty: _p("search.local_search.hits_empty"), hits_empty: _p("search.local_search.hits_empty"),
@@ -116,7 +119,7 @@ script.
buttonText: '!{_p("load_more")}' buttonText: '!{_p("load_more")}'
}, },
isPhotoFigcaption: !{theme.photofigcaption}, isPhotoFigcaption: !{theme.photofigcaption},
islazyload: !{theme.lazyload.enable}, islazyloadPlugin: !{theme.lazyload.enable && !theme.lazyload.native},
isAnchor: !{theme.anchor.auto_update || false}, isAnchor: !{theme.anchor.auto_update || false},
percent: { percent: {
toc: !{theme.toc.scroll_percent}, toc: !{theme.toc.scroll_percent},

View File

@@ -9,8 +9,8 @@
var showToc = false var showToc = false
if (theme.aside.enable && page.aside !== false) { if (theme.aside.enable && page.aside !== false) {
let tocEnable = false let tocEnable = false
if (is_post() && theme.toc.post) tocEnable = true if (globalPageType === 'post' && theme.toc.post) tocEnable = true
else if (is_page() && theme.toc.page) tocEnable = true else if (globalPageType === 'page' && theme.toc.page) tocEnable = true
const pageToc = typeof page.toc === 'boolean' ? page.toc : tocEnable const pageToc = typeof page.toc === 'boolean' ? page.toc : tocEnable
showToc = pageToc && (toc(page.content) !== '' || page.encrypt === true) showToc = pageToc && (toc(page.content) !== '' || page.encrypt === true)
} }
@@ -19,9 +19,7 @@
script#config-diff. script#config-diff.
var GLOBAL_CONFIG_SITE = { var GLOBAL_CONFIG_SITE = {
title: '!{titleVal}', title: '!{titleVal}',
isPost: !{is_post()},
isHome: !{is_home()},
isHighlightShrink: !{isHighlightShrink}, isHighlightShrink: !{isHighlightShrink},
isToc: !{showToc}, isToc: !{showToc},
isShuoshuo: !{page.type == 'shuoshuo'} pageType: '!{page.type == 'shuoshuo' ? 'shuoshuo' : globalPageType}'
} }

View File

@@ -0,0 +1,67 @@
if theme.structured_data
if page.layout === 'post'
-
// https://developers.google.com/search/docs/appearance/structured-data/article
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 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)
}
-
script(type="application/ld+json").
!{jsonLdScript}

View File

@@ -6,33 +6,32 @@
var bg_img = '' var bg_img = ''
if !theme.disable_top_img && page.top_img !== false if !theme.disable_top_img && page.top_img !== false
if is_post() case globalPageType
- top_img = page.top_img || page.cover || theme.default_top_img when 'post'
else if is_page() - top_img = page.top_img || page.cover || theme.default_top_img
- top_img = page.top_img || theme.default_top_img when 'page'
else if is_tag() - top_img = page.top_img || theme.default_top_img
- top_img = theme.tag_per_img && theme.tag_per_img[page.tag] when 'tag'
- top_img = top_img || returnTopImg(theme.tag_img) - top_img = theme.tag_per_img && theme.tag_per_img[page.tag] || returnTopImg(theme.tag_img)
else if is_category() when 'category'
- top_img = theme.category_per_img && theme.category_per_img[page.category] - top_img = theme.category_per_img && theme.category_per_img[page.category] || returnTopImg(theme.category_img)
- top_img = top_img || returnTopImg(theme.category_img) when 'home'
else if is_home() - top_img = returnTopImg(theme.index_img)
- top_img = returnTopImg(theme.index_img) when 'archive'
else if is_archive() - top_img = returnTopImg(theme.archive_img)
- top_img = returnTopImg(theme.archive_img) default
else - top_img = page.top_img || theme.default_top_img
- top_img = page.top_img || theme.default_top_img
if top_img !== false if top_img !== false
- bg_img = getBgPath(top_img) - bg_img = getBgPath(top_img)
- headerClassName = is_home() ? 'full_page' : is_post() ? 'post-bg' : 'not-home-page' - headerClassName = globalPageType === 'home' ? 'full_page' : globalPageType === 'post' ? 'post-bg' : 'not-home-page'
header#page-header(class=`${headerClassName + isFixedClass}` style=bg_img) header#page-header(class=`${headerClassName + isFixedClass}` style=bg_img)
include ./nav.pug include ./nav.pug
if top_img !== false if top_img !== false
if is_post() if globalPageType === 'post'
include ./post-info.pug include ./post-info.pug
else if is_home() else if globalPageType === 'home'
#site-info #site-info
h1#site-title=config.title h1#site-title=config.title
if theme.subtitle.enable if theme.subtitle.enable
@@ -48,6 +47,6 @@ header#page-header(class=`${headerClassName + isFixedClass}` style=bg_img)
#page-site-info #page-site-info
h1#site-title=page.title || page.tag || page.category h1#site-title=page.title || page.tag || page.category
else else
//- improvement seo //- improve seo
if !is_post() if globalPageType !== 'post'
h1.title-seo=page.title || page.tag || page.category || config.title h1.title-seo=page.title || page.tag || page.category || config.title

View File

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

View File

@@ -1,7 +1,8 @@
- var globalPageType = getPageType(page, is_home)
- var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : '' - var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : ''
- page.aside = is_archive() ? theme.aside.display.archive: is_category() ? theme.aside.display.category : is_tag() ? theme.aside.display.tag : page.aside - page.aside = globalPageType === 'archive' ? theme.aside.display.archive: globalPageType === 'category' ? theme.aside.display.category : globalPageType === 'tag' ? theme.aside.display.tag : page.aside
- var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : '' - var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : ''
- var pageType = is_post() ? 'post' : 'page' - var pageType = globalPageType === 'post' ? 'post' : 'page'
- pageType = page.type ? pageType + ' type-' + page.type : pageType - pageType = page.type ? pageType + ' type-' + page.type : pageType
doctype html doctype html

View File

@@ -14,6 +14,7 @@ script.
const $body = document.body const $body = document.body
const preloader = { const preloader = {
endLoading: () => { endLoading: () => {
if ($loadingBox.classList.contains('loaded')) return
$body.style.overflow = '' $body.style.overflow = ''
$loadingBox.classList.add('loaded') $loadingBox.classList.add('loaded')
}, },
@@ -24,7 +25,15 @@ script.
} }
preloader.initLoading() preloader.initLoading()
window.addEventListener('load', preloader.endLoading)
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)
}
if (!{theme.pjax && theme.pjax.enable}) { if (!{theme.pjax && theme.pjax.enable}) {
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init') btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')

View File

@@ -20,7 +20,7 @@ mixin indexPostUI()
div.post-bg(style=`background: ${post_cover}`) div.post-bg(style=`background: ${post_cover}`)
.recent-post-info(class=no_cover) .recent-post-info(class=no_cover)
a.article-title(href=url_for(link) title=title) a.article-title(href=url_for(link) title=title)
if is_home() && (article.top || article.sticky > 0) if globalPageType === 'home' && (article.top || article.sticky > 0)
i.fas.fa-thumbtack.sticky i.fas.fa-thumbtack.sticky
= title = title
.article-meta-wrap .article-meta-wrap

View File

@@ -9,40 +9,47 @@
- page.toc = false - page.toc = false
#article-container #article-container
if page.comments !== false && theme.comments.use if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
- commentsJsLoad = true if page.comments !== false && theme.comments.use
- commentsJsLoad = true
script. script.
(() => { (() => {
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}` const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
const runDestroy = (shuoshuoComment) => { const runDestroy = (shuoshuoComment) => {
if (!shuoshuoComment) return if (!shuoshuoComment) return
for (const [key, fn] of Object.entries(shuoshuoComment)) { for (const [key, fn] of Object.entries(shuoshuoComment)) {
if (key.startsWith('destroy')) fn() if (key.startsWith('destroy')) fn()
}
} }
}
window.addCommentToShuoshuo = e => { window.addCommentToShuoshuo = e => {
const btn = e.target.closest('.shuoshuo-comment-btn') const btn = e.target.closest('.shuoshuo-comment-btn')
if (!btn) return if (!btn) return
const ele = btn.closest('.container').nextElementSibling const ele = btn.closest('.container').nextElementSibling
const { shuoshuoComment } = window const { shuoshuoComment } = window
const isInclude = ele.classList.contains('no-comment') const isInclude = ele.classList.contains('no-comment')
runDestroy(shuoshuoComment) runDestroy(shuoshuoComment)
if (isInclude) { if (isInclude) {
ele.classList.remove('no-comment') ele.classList.remove('no-comment')
ele.innerHTML = commentDiv ele.innerHTML = commentDiv
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}` const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
btf.switchComments(ele, key) btf.switchComments(ele, key)
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key) shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
}
} }
} })()
})()
if page.shuoshuo_url
- 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
script. script.
(() => { (() => {
const limitConfig = !{ JSON.stringify(page.limit || {}) } const limitConfig = !{ JSON.stringify(page.limit || {}) }
@@ -78,84 +85,244 @@
return `${year}-${month}-${day} ${hour}:${minute}:${second}` 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 () => { const loadShuoshuo = async () => {
try { try {
const response = await fetch('!{url_for(page.shuoshuo_url)}') let originData = []
let data = await response.json() if (!{Boolean(page.shuoshuo_url)}) {
const response = await fetch('!{url_for(page.shuoshuo_url)}')
data = filterDataByLimit(sortDataByDate(data), limitConfig) originData = await response.json()
} else {
const container = document.getElementById('article-container') const dataElement = document.getElementById('shuoshuo-data')
let start = 0 originData = dataElement ? JSON.parse(dataElement.textContent) : []
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)'))
} }
const handleIntersection = (entries) => { data = filterDataByLimit(sortDataByDate(originData), limitConfig)
if (!entries[0].isIntersecting) return
observer.unobserve(entries[0].target)
const slice = data.slice(start, start + 10) totalPages = Math.ceil(data.length / itemsPerPage)
renderData(slice)
start += 10
if (start < data.length) { renderPage(currentPage)
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) { } catch (error) {
console.error(error) console.error(error)
} }
@@ -163,26 +330,3 @@
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo) 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,37 +1,38 @@
- if page.total !== 1
var options = { -
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>', var options = {
next_text: '<i class="fas fa-chevron-right fa-fw"></i>', prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
mid_size: 1, next_text: '<i class="fas fa-chevron-right fa-fw"></i>',
escape: false mid_size: 1,
} escape: false
}
if is_post() if globalPageType === 'post'
- let paginationOrder = theme.post_pagination === 1 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev } - let paginationOrder = theme.post_pagination === 2 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
nav#pagination.pagination-post nav#pagination.pagination-post
each direction, key in paginationOrder each direction, key in paginationOrder
if direction if direction
- const getPostDesc = direction.postDesc || postDesc(direction) - const getPostDesc = direction.postDesc || postDesc(direction)
- let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width') - let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width')
- className = getPostDesc ? className : className + ' no-desc' - className = getPostDesc ? className : className + ' no-desc'
a.pagination-related(class=className href=url_for(direction.path) title=direction.title) a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
if direction.cover_type === 'img' 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`) 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 else
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`) .cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
.info(class=key === 'prev' ? '' : 'text-right') .info(class=key === 'prev' ? '' : 'text-right')
.info-1 .info-1
.info-item-1=_p(`pagination.${key}`) .info-item-1=_p(`pagination.${key}`)
.info-item-2!=direction.title .info-item-2!=direction.title
if getPostDesc if getPostDesc
.info-2 .info-2
.info-item-1!=getPostDesc .info-item-1!=getPostDesc
else else
nav#pagination nav#pagination
.pagination .pagination
if is_home() if globalPageType === 'home'
- options.format = 'page/%d/#content-inner' - options.format = 'page/%d/#content-inner'
!=paginator(options) !=paginator(options)

View File

@@ -1,9 +1,10 @@
- const { readmode, translate, darkmode, aside, chat } = theme - const { readmode, translate, darkmode, aside, chat } = theme
mixin rightsideItem(array) mixin rightsideItem(array)
each item in array each item in array
case item case item
when 'readmode' when 'readmode'
if is_post() && readmode if globalPageType === 'post' && readmode
button#readmode(type="button" title=_p('rightside.readmode_title')) button#readmode(type="button" title=_p('rightside.readmode_title'))
i.fas.fa-book-open i.fas.fa-book-open
when 'translate' when 'translate'
@@ -23,37 +24,29 @@ mixin rightsideItem(array)
i.fas.fa-list-ul i.fas.fa-list-ul
when 'chat' when 'chat'
if chat.rightside_button && chat.use if chat.rightside_button && chat.use
button#chat-btn(type="button" title=_p("rightside.chat")) button#chat-btn(type="button" title=_p("rightside.chat") style="display:none")
i.fas.fa-message i.fas.fa-message
when 'comment' when 'comment'
if commentsJsLoad if commentsJsLoad
a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment")) a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))
i.fas.fa-comments 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 #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 #rightside-config-hide
if hideArray if hideArray.length
+rightsideItem(hideArray) +rightsideItem(hideArray)
#rightside-config-show
if enable
if hide
button#rightside-config(type="button" title=_p("rightside.setting"))
i.fas.fa-cog.fa-spin
else
if is_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 #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 showArray.length
+rightsideItem(showArray) +rightsideItem(showArray)
button#go-up(type="button" title=_p("rightside.back_to_top")) button#go-up(type="button" title=_p("rightside.back_to_top"))

View File

@@ -3,16 +3,16 @@ if theme.menu
#menu-mask #menu-mask
#sidebar-menus #sidebar-menus
.avatar-img.text-center .avatar-img.text-center
img(src=url_for(theme.avatar.img) onerror=`onerror=null;src='${theme.error_img.flink}'` alt="avatar") img(src=url_for(theme.avatar.img) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.flink)}'` alt="avatar")
.site-data.text-center .site-data.text-center
a(href=url_for(config.archive_dir) + '/') a(href=`${url_for(config.archive_dir)}/`)
.headline= _p('aside.articles') .headline= _p('aside.articles')
.length-num= site.posts.length .length-num= site.posts.length
a(href=url_for(config.tag_dir) + '/' ) a(href=`${url_for(config.tag_dir)}/`)
.headline= _p('aside.tags') .headline= _p('aside.tags')
.length-num= site.tags.length .length-num= site.tags.length
a(href=url_for(config.category_dir) + '/') a(href=`${url_for(config.category_dir)}/`)
.headline= _p('aside.categories') .headline= _p('aside.categories')
.length-num= site.categories.length .length-num= site.categories.length
!=partial('includes/header/menu_item', {}, {cache: true}) != partial('includes/header/menu_item', {}, { cache: true })

View File

@@ -1,17 +1,46 @@
script. script.
(() => { (() => {
const abcjsInit = () => { const abcjsInit = () => {
const abcjsFn = () => setTimeout(() => { const abcjsFn = () => {
document.querySelectorAll(".abc-music-sheet").forEach(ele => { setTimeout(() => {
if (ele.children.length > 0) return const sheets = document.querySelectorAll(".abc-music-sheet")
ABCJS.renderAbc(ele, ele.innerHTML, {responsive: 'resize'}) for (let i = 0; i < sheets.length; i++) {
}) const ele = sheets[i]
}, 100) if (ele.children.length > 0) continue
typeof ABCJS === 'object' ? abcjsFn() // Parse parameters from data-params attribute
: btf.getScript('!{url_for(theme.asset.abcjs_basic_js)}').then(abcjsFn) 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.innerHTML, options)
}
}, 100)
}
if (typeof ABCJS === "object") {
abcjsFn()
} else {
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
}
} }
window.pjax ? abcjsInit() : window.addEventListener('load', abcjsInit) if (window.pjax) {
btf.addGlobalFn('encrypt', abcjsInit, 'abcjs') abcjsInit()
} else {
window.addEventListener("load", abcjsInit)
}
btf.addGlobalFn("encrypt", abcjsInit, "abcjs")
})() })()

View File

@@ -1,6 +1,3 @@
if theme.abcjs.enable if theme.abcjs.enable
if theme.abcjs.per_page if theme.abcjs.per_page && (['post','page'].includes(globalPageType)) || page.abcjs
if is_post() || is_page()
include ./abcjs.pug
else if page.abcjs
include ./abcjs.pug include ./abcjs.pug

View File

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

View File

@@ -1,42 +1,38 @@
//- https://chatra.io/help/api/ //- https://chatra.io/help/api/
script. script.
(() => { (() => {
const isChatBtn = !{theme.chat.rightside_button} window.ChatraID = '#{theme.chatra.id}'
const isChatHideShow = !{theme.chat.button_hide_show} window.Chatra = window.Chatra || function() {
(window.Chatra.q = window.Chatra.q || []).push(arguments)
if (isChatBtn) {
const close = () => {
Chatra('minimizeWidget')
Chatra('hide')
}
const open = () => {
Chatra('openChat', true)
Chatra('show')
}
window.ChatraSetup = { startHidden: true }
window.chatBtnFn = () => {
document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
}
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => Chatra('hide'),
show: () => Chatra('show')
}
} }
(function(d, w, c) { btf.getScript('https://call.chatra.io/chatra.js').then(() => {
w.ChatraID = '#{theme.chatra.id}' const isChatBtn = !{theme.chat.rightside_button}
var s = d.createElement('script') const isChatHideShow = !{theme.chat.button_hide_show}
w[c] = w[c] || function() {
(w[c].q = w[c].q || []).push(arguments) if (isChatBtn) {
const close = () => {
Chatra('minimizeWidget')
Chatra('hide')
}
const open = () => {
Chatra('openChat', true)
Chatra('show')
}
window.ChatraSetup = { startHidden: true }
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
document.getElementById('chat-btn').style.display = 'block'
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => Chatra('hide'),
show: () => Chatra('show')
}
} }
s.async = true })
s.src = 'https://call.chatra.io/chatra.js'
if (d.head) d.head.appendChild(s)
})(document, window, 'Chatra')
})() })()

View File

@@ -1,37 +1,32 @@
script. script.
(() => { (() => {
window.$crisp = []; window.$crisp = ['safe', true]
window.CRISP_WEBSITE_ID = "!{theme.crisp.website_id}"; window.CRISP_WEBSITE_ID = "!{theme.crisp.website_id}"
(function () {
d = document;
s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
$crisp.push(["safe", true])
const isChatBtn = !{theme.chat.rightside_button} btf.getScript('https://client.crisp.chat/l.js').then(() => {
const isChatHideShow = !{theme.chat.button_hide_show} const isChatBtn = !{theme.chat.rightside_button}
const isChatHideShow = !{theme.chat.button_hide_show}
if (isChatBtn) { if (isChatBtn) {
const open = () => { const open = () => {
$crisp.push(["do", "chat:show"]) $crisp.push(["do", "chat:show"])
$crisp.push(["do", "chat:open"]) $crisp.push(["do", "chat:open"])
}
const close = () => $crisp.push(["do", "chat:hide"])
close()
$crisp.push(["on", "chat:closed", close])
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
document.getElementById('chat-btn').style.display = 'block'
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => $crisp.push(["do", "chat:hide"]),
show: () => $crisp.push(["do", "chat:show"])
}
} }
})
const close = () => $crisp.push(["do", "chat:hide"])
close()
$crisp.push(["on", "chat:closed", close])
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => $crisp.push(["do", "chat:hide"]),
show: () => $crisp.push(["do", "chat:show"])
}
}
})() })()

View File

@@ -1,41 +1,45 @@
script(src=`//code.tidio.co/${theme.tidio.public_key}.js` async)
script. script.
(() => { (() => {
const isChatBtn = !{theme.chat.rightside_button} btf.getScript('//code.tidio.co/!{theme.tidio.public_key}.js').then(() => {
const isChatHideShow = !{theme.chat.button_hide_show} const isChatBtn = !{theme.chat.rightside_button}
const isChatHideShow = !{theme.chat.button_hide_show}
if (isChatBtn) { if (isChatBtn) {
let isShow = false let isShow = false
const close = () => { const close = () => {
window.tidioChatApi.hide() window.tidioChatApi.hide()
isShow = false isShow = false
} }
const open = () => { const open = () => {
window.tidioChatApi.open() window.tidioChatApi.open()
window.tidioChatApi.show() window.tidioChatApi.show()
isShow = true isShow = true
} }
const onTidioChatApiReady = () => { const onTidioChatApiReady = () => {
window.tidioChatApi.hide() window.tidioChatApi.hide()
window.tidioChatApi.on("close", close) window.tidioChatApi.on("close", close)
} }
if (window.tidioChatApi) { if (window.tidioChatApi) {
window.tidioChatApi.on("ready", onTidioChatApiReady) window.tidioChatApi.on("ready", onTidioChatApiReady)
} else { } else {
document.addEventListener("tidioChat-ready", onTidioChatApiReady) document.addEventListener("tidioChat-ready", onTidioChatApiReady)
} }
window.chatBtnFn = () => { window.chatBtnFn = () => {
if (!window.tidioChatApi) return if (!window.tidioChatApi) return
isShow ? close() : open() isShow ? close() : open()
}
document.getElementById('chat-btn').style.display = 'block'
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
show: () => window.tidioChatApi && window.tidioChatApi.show()
}
} }
} else if (isChatHideShow) { })
window.chatBtn = {
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
show: () => window.tidioChatApi && window.tidioChatApi.show()
}
}
})() })()

View File

@@ -5,7 +5,7 @@ script.
(() => { (() => {
let artalkItem = null let artalkItem = null
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const destroyArtalk = () => { const destroyArtalk = () => {
if (artalkItem) { if (artalkItem) {
@@ -51,8 +51,8 @@ script.
const loadArtalk = async (el, pageKey) => { const loadArtalk = async (el, pageKey) => {
if (typeof Artalk === 'object') initArtalk(el, pageKey) if (typeof Artalk === 'object') initArtalk(el, pageKey)
else { else {
await btf.getCSS('!{theme.asset.artalk_css}') await btf.getCSS('!{url_for(theme.asset.artalk_css)}')
await btf.getScript('!{theme.asset.artalk_js}') await btf.getScript('!{url_for(theme.asset.artalk_js)}')
initArtalk(el, pageKey) initArtalk(el, pageKey)
} }
} }

View File

@@ -4,7 +4,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const disqusReset = conf => { const disqusReset = conf => {
window.DISQUS && window.DISQUS.reset({ window.DISQUS && window.DISQUS.reset({
@@ -72,7 +72,7 @@ script.
if (!{lazyload}) btf.loadComment(document.getElementById('disqus_thread'), loadDisqus) if (!{lazyload}) btf.loadComment(document.getElementById('disqus_thread'), loadDisqus)
else { else {
loadDisqus() loadDisqus()
!{ count ? 'GLOBAL_CONFIG_SITE.isPost && getCount()' : '' } !{ count ? `GLOBAL_CONFIG_SITE.pageType === 'post' && getCount()` : '' }
} }
} else { } else {
window.loadOtherComment = loadDisqus window.loadOtherComment = loadDisqus

View File

@@ -3,7 +3,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
const dqOption = !{JSON.stringify(dqOption)} const dqOption = !{JSON.stringify(dqOption)}
const destroyDisqusjs = () => { const destroyDisqusjs = () => {
@@ -79,7 +79,7 @@ script.
if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('disqusjs-wrap'), loadDisqusjs) if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('disqusjs-wrap'), loadDisqusjs)
else { else {
loadDisqusjs() loadDisqusjs()
!{ theme.comments.count ? 'GLOBAL_CONFIG_SITE.isPost && getCount()' : '' } !{ theme.comments.count ? `GLOBAL_CONFIG_SITE.pageType === 'post' && getCount()` : '' }
} }
} else { } else {
window.loadOtherComment = loadDisqusjs window.loadOtherComment = loadDisqusjs

View File

@@ -3,7 +3,7 @@
script. script.
(()=>{ (()=>{
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
const loadFBComment = (el = document, path) => { const loadFBComment = (el = document, path) => {
if (isShuoshuo) { if (isShuoshuo) {

View File

@@ -5,7 +5,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const getGiscusTheme = theme => theme === 'dark' ? '!{dark_theme}' : '!{light_theme}' const getGiscusTheme = theme => theme === 'dark' ? '!{dark_theme}' : '!{light_theme}'

View File

@@ -2,7 +2,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const commentCount = n => { const commentCount = n => {

View File

@@ -2,7 +2,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const loadLivere = (el, path) => { const loadLivere = (el, path) => {
window.livereOptions = { window.livereOptions = {

View File

@@ -2,8 +2,8 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const options = !{JSON.stringify(option)}
const loadScript = src => { const loadScript = src => {
const script = document.createElement('script') const script = document.createElement('script')
@@ -42,8 +42,8 @@ script.
host: '!{host}', host: '!{host}',
site_id: '!{siteId}', site_id: '!{siteId}',
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light', theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
...option, ...options,
url: isShuoshuo ? window.location.origin + path : (option && option.url) || window.location.origin + window.location.pathname url: isShuoshuo ? window.location.origin + path : (options && options.url) || window.location.origin + window.location.pathname
} }
if (window.REMARK42) { if (window.REMARK42) {

View File

@@ -3,7 +3,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const getCount = () => { const getCount = () => {
@@ -33,7 +33,7 @@ script.
path: isShuoshuo ? path : (option && option.path) || path path: isShuoshuo ? path : (option && option.path) || path
}) })
!{count ? 'GLOBAL_CONFIG_SITE.isPost && getCount()' : ''} !{count ? `GLOBAL_CONFIG_SITE.pageType === 'post' && getCount()` : ''}
isShuoshuo && (window.shuoshuoComment.destroyTwikoo = () => { isShuoshuo && (window.shuoshuoComment.destroyTwikoo = () => {
if (el.children.length) { if (el.children.length) {

View File

@@ -5,7 +5,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const getUtterancesTheme = theme => theme === 'dark' ? '#{dark_theme}' : '#{light_theme}' const getUtterancesTheme = theme => theme === 'dark' ? '#{dark_theme}' : '#{light_theme}'

View File

@@ -7,7 +7,7 @@ if site.data.valine
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const initValine = (el, path) => { const initValine = (el, path) => {

View File

@@ -4,7 +4,7 @@
script. script.
(() => { (() => {
let initFn = window.walineFn || null let initFn = window.walineFn || null
const isShuoshuo = GLOBAL_CONFIG_SITE.isShuoshuo const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const destroyWaline = ele => ele.destroy() const destroyWaline = ele => ele.destroy()

View File

@@ -2,9 +2,6 @@
script. script.
(() => { (() => {
const $chartjs = document.querySelectorAll('#article-container .chartjs-container')
if ($chartjs.length === 0) return
const applyThemeDefaultsConfig = theme => { const applyThemeDefaultsConfig = theme => {
if (theme === 'dark-mode') { if (theme === 'dark-mode') {
Chart.defaults.color = "!{fontColor.dark}" Chart.defaults.color = "!{fontColor.dark}"
@@ -35,10 +32,10 @@ script.
}) })
} }
const runChartJS = () => { const runChartJS = ele => {
window.loadChartJS = true window.loadChartJS = true
Array.from($chartjs).forEach((item, index) => { Array.from(ele).forEach((item, index) => {
const chartSrc = item.firstElementChild const chartSrc = item.firstElementChild
const chartID = item.getAttribute('data-chartjs-id') || ('chartjs-' + index) // Use custom ID or default ID const chartID = item.getAttribute('data-chartjs-id') || ('chartjs-' + index) // Use custom ID or default ID
const width = item.getAttribute('data-width') const width = item.getAttribute('data-width')
@@ -80,12 +77,15 @@ script.
} }
const loadChartJS = () => { const loadChartJS = () => {
window.loadChartJS ? runChartJS() : btf.getScript('!{url_for(theme.asset.chartjs)}').then(runChartJS) const chartJSEle = document.querySelectorAll('#article-container .chartjs-container')
if (chartJSEle.length === 0) return
window.loadChartJS ? runChartJS(chartJSEle) : btf.getScript('!{url_for(theme.asset.chartjs)}').then(() => runChartJS(chartJSEle))
} }
// Listen for theme change events // Listen for theme change events
btf.addGlobalFn('themeChange', runChartJS, 'chartjs') btf.addGlobalFn('themeChange', loadChartJS, 'chartjs')
btf.addGlobalFn('encrypt', runChartJS, 'chartjs') btf.addGlobalFn('encrypt', loadChartJS, 'chartjs')
window.pjax ? loadChartJS() : document.addEventListener('DOMContentLoaded', loadChartJS) window.pjax ? loadChartJS() : document.addEventListener('DOMContentLoaded', loadChartJS)
})() })()

View File

@@ -1,10 +1,10 @@
case theme.math.use case theme.math.use
when 'mathjax' when 'mathjax'
if (theme.math.per_page && (is_post() || is_page())) || page.mathjax if (theme.math.per_page && (['post','page'].includes(globalPageType))) || page.mathjax
include ./mathjax.pug include ./mathjax.pug
when 'katex' when 'katex'
if (theme.math.per_page && (is_post() || is_page())) || page.katex if (theme.math.per_page && (['post','page'].includes(globalPageType))) || page.katex
include ./katex.pug include ./katex.pug
if theme.mermaid.enable if theme.mermaid.enable

View File

@@ -1,19 +1,50 @@
//- Mathjax 3 //- Mathjax 4/5
- const { tags, enableMenu } = theme.math.mathjax - const { tags, enableMenu } = theme.math.mathjax
script. script.
(() => { (() => {
const loadMathjax = () => { const loadMathjax = () => {
if (!window.MathJax) { if (!window.MathJax) {
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: { tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']], inlineMath: [['$', '$'], ['\\(', '\\)']],
tags: '!{tags}', tags: '!{tags}',
packages: {
'[+]': [
'mhchem'
]
}
}, },
chtml: { chtml: {
scale: 1.1 scale: 1.1
}, },
options: { options: {
enableMenu: !{enableMenu}, enableMenu: !{enableMenu},
menuOptions: {
settings: {
enrich: false // Turn off Braille and voice narration text automatic generation
}
},
renderActions: { renderActions: {
findScript: [10, doc => { findScript: [10, doc => {
for (const node of document.querySelectorAll('script[type^="math/tex"]')) { for (const node of document.querySelectorAll('script[type^="math/tex"]')) {

View File

@@ -6,7 +6,11 @@ script.
ele.forEach((item, index) => { ele.forEach((item, index) => {
const mermaidSrc = item.firstElementChild const mermaidSrc = item.firstElementChild
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n` const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
if (!config.theme) {
config.theme = theme
}
const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`
const mermaidID = `mermaid-${index}` const mermaidID = `mermaid-${index}`
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent

View File

@@ -34,7 +34,7 @@ script.
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
'site_name': '!{site}', 'site_name': '!{site}',
'limit': '!{theme.aside.card_newest_comments.limit}', 'limit': '!{newestCommentsLimit * 2}', // Fetch more comments to filter pending comments
}) })
const getComment = async (ele) => { const getComment = async (ele) => {
@@ -42,16 +42,19 @@ script.
const res = await fetch(`!{server}/api/v2/stats/latest_comments?${searchParams}`) const res = await fetch(`!{server}/api/v2/stats/latest_comments?${searchParams}`)
const result = await res.json() const result = await res.json()
const { avatarCdn, avatarDefault } = await getAvatarValue() const { avatarCdn, avatarDefault } = await getAvatarValue()
const artalk = result.data.map(e => { const artalk = result.data
const avatar = avatarCdn && e.email_encrypted ? `${avatarCdn}${e.email_encrypted}?${avatarDefault}` : '' .filter(e => !e.is_pending) // Filter pending comments
return { .slice(0, !{newestCommentsLimit}) // Limit the number of comments
'avatar': avatar, .map(e => {
'content': changeContent(e.content_marked), const avatar = avatarCdn && e.email_encrypted ? `${avatarCdn}${e.email_encrypted}?${avatarDefault}` : ''
'nick': e.nick, return {
'url': e.page_url, 'avatar': avatar,
'date': e.date, 'content': changeContent(e.content_marked),
} 'nick': e.nick,
}) 'url': e.page_url,
'date': e.date,
}
})
btf.saveToLocal.set(keyName, JSON.stringify(artalk), !{theme.aside.card_newest_comments.storage}/(60*24)) btf.saveToLocal.set(keyName, JSON.stringify(artalk), !{theme.aside.card_newest_comments.storage}/(60*24))
generateHtml(artalk, ele) generateHtml(artalk, ele)
} catch (e) { } catch (e) {

View File

@@ -23,8 +23,9 @@ script.
result += '<div class="aside-list-item">' result += '<div class="aside-list-item">'
if (!{theme.aside.card_newest_comments.avatar} && array[i].avatar) { if (!{theme.aside.card_newest_comments.avatar} && array[i].avatar) {
const imgAttr = '!{theme.lazyload.enable ? "data-lazy-src" : "src"}' const imgAttr = '!{theme.lazyload.enable && !theme.lazyload.native ? "data-lazy-src" : "src"}'
result += `<a href="${array[i].url}" class="thumbnail"><img ${imgAttr}="${array[i].avatar}" alt="${array[i].nick}"></a>` const lazyloadNative = '!{theme.lazyload.enable && theme.lazyload.native ? "loading=\"lazy\"" : ""}'
result += `<a href="${array[i].url}" class="thumbnail"><img ${imgAttr}="${array[i].avatar}" alt="${array[i].nick}" ${lazyloadNative}></a>`
} }
result += `<div class="content"> result += `<div class="content">

View File

@@ -6,7 +6,7 @@ script.
const { changeContent, generateHtml, run } = window.newestComments const { changeContent, generateHtml, run } = window.newestComments
const getComment = ele => { const getComment = ele => {
fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{theme.aside.card_newest_comments.limit}&api_key=!{apiKey}') fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{newestCommentsLimit}&api_key=!{apiKey}')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const disqusArray = data.response.map(item => { const disqusArray = data.response.map(item => {

View File

@@ -32,7 +32,7 @@ script.
} }
const getComment = ele => { const getComment = ele => {
fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{theme.aside.card_newest_comments.limit}&page=1',{ fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{newestCommentsLimit}&page=1',{
"headers": { "headers": {
Accept: 'application/vnd.github.v3.html+json' Accept: 'application/vnd.github.v3.html+json'
} }

View File

@@ -1,7 +1,11 @@
- let { use } = theme.comments - let { use } = theme.comments
if use if use
- let forum,apiKey,userRepo -
let forum,apiKey,userRepo
let { limit:newestCommentsLimit } = theme.aside.card_newest_comments
if (newestCommentsLimit > 10 || newestCommentsLimit < 1) newestCommentsLimit = 6
case use[0] case use[0]
when 'Valine' when 'Valine'
include ./valine.pug include ./valine.pug

View File

@@ -7,7 +7,7 @@ script.
const { changeContent, generateHtml, run } = window.newestComments const { changeContent, generateHtml, run } = window.newestComments
const getComment = ele => { const getComment = ele => {
fetch('!{host}/api/v1/last/!{theme.aside.card_newest_comments.limit}?site=!{siteId}') fetch('!{host}/api/v1/last/!{newestCommentsLimit}?site=!{siteId}')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const remark42 = data.map(e => { const remark42 = data.map(e => {

View File

@@ -10,7 +10,7 @@ script.
twikoo.getRecentComments({ twikoo.getRecentComments({
envId: '!{theme.twikoo.envId}', envId: '!{theme.twikoo.envId}',
region: '!{theme.twikoo.region}', region: '!{theme.twikoo.region}',
pageSize: !{theme.aside.card_newest_comments.limit}, pageSize: !{newestCommentsLimit},
includeReply: true includeReply: true
}).then(res => { }).then(res => {
const twikooArray = res.map(e => { const twikooArray = res.map(e => {

View File

@@ -27,7 +27,7 @@ script.
}, },
} }
fetch(`${serverURL}/1.1/classes/Comment?limit=!{theme.aside.card_newest_comments.limit}&order=-createdAt`,settings) fetch(`${serverURL}/1.1/classes/Comment?limit=!{newestCommentsLimit}&order=-createdAt`,settings)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const valineArray = data.results.map(e => { const valineArray = data.results.map(e => {

View File

@@ -9,7 +9,7 @@ script.
const getComment = async (ele) => { const getComment = async (ele) => {
try { try {
const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{theme.aside.card_newest_comments.limit}', { method: 'GET' }) const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{newestCommentsLimit}')
const result = await res.json() const result = await res.json()
const walineArray = result.data.map(e => { const walineArray = result.data.map(e => {
return { return {

View File

@@ -1,23 +0,0 @@
script.
(() => {
const panguFn = () => {
if (typeof pangu === 'object') pangu.autoSpacingPage()
else {
btf.getScript('!{url_for(theme.asset.pangu)}')
.then(() => {
pangu.autoSpacingPage()
})
}
}
const panguInit = () => {
if (!{theme.pangu.field === 'post'}){
GLOBAL_CONFIG_SITE.isPost && panguFn()
} else {
panguFn()
}
}
btf.addGlobalFn('pjaxComplete', panguInit, 'pangu')
document.addEventListener('DOMContentLoaded', panguInit)
})()

View File

@@ -7,10 +7,12 @@ if theme.pjax.exclude
- let choose = theme.comments.use - let choose = theme.comments.use
if choose if choose
if theme.Open_Graph_meta.enable && (choose.includes('Livere') || choose.includes('Utterances') || choose.includes('Giscus')) if choose.includes('Livere') || choose.includes('Utterances') || choose.includes('Giscus')
- pjaxSelectors.unshift('meta[property="og:image"]', 'meta[property="og:title"]', 'meta[property="og:url"]')
if choose.includes('Utterances') || choose.includes('Giscus')
- pjaxSelectors.unshift('link[rel="canonical"]') - pjaxSelectors.unshift('link[rel="canonical"]')
if theme.Open_Graph_meta.enable
- pjaxSelectors.unshift('meta[property="og:image"]', 'meta[property="og:title"]', 'meta[property="og:url"]', 'meta[property="og:description"]')
else
- pjaxSelectors.unshift('meta[name="description"]')
script(src=url_for(theme.asset.pjax)) script(src=url_for(theme.asset.pjax))
script. script.
@@ -27,7 +29,13 @@ script.
const triggerPjaxFn = (val) => { const triggerPjaxFn = (val) => {
if (!val) return if (!val) return
Object.values(val).forEach(fn => fn()) Object.values(val).forEach(fn => {
try {
fn()
} catch (err) {
console.debug('Pjax callback failed:', err)
}
})
} }
document.addEventListener('pjax:send', () => { document.addEventListener('pjax:send', () => {
@@ -57,7 +65,10 @@ script.
document.addEventListener('pjax:error', e => { document.addEventListener('pjax:error', e => {
if (e.request.status === 404) { if (e.request.status === 404) {
pjax.loadUrl('!{url_for("/404.html")}') const usePjax = !{theme.pjax && theme.pjax.enable}
!{theme.error_404 && theme.error_404.enable}
? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{url_for("/404.html")}')
: window.location.href = e.request.responseURL
} }
}) })
})() })()

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
.addtoany .addtoany
.a2a_kit.a2a_kit_size_32.a2a_default_style .a2a_kit.a2a_kit_size_32.a2a_default_style
- let addtoanyItem = theme.addtoany.item.split(',') - let addtoanyItem = theme.share.addtoany.item.split(',')
each name in addtoanyItem each name in addtoanyItem
a(class="a2a_button_" + name) a(class="a2a_button_" + name)

View File

@@ -1,4 +1,4 @@
- const { effect,source,sub,typed_option } = theme.subtitle - const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = sub || new Array() - let subContent = sub || new Array()
script. script.
@@ -22,6 +22,26 @@ script.
} else { } else {
subtitleType() 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') btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
@@ -33,14 +53,12 @@ case source
fetch('https://v1.hitokoto.cn') fetch('https://v1.hitokoto.cn')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (!{effect}) { const from = '出自 ' + data.from
const from = '出自 ' + data.from typedJSFn.processSubtitle(data.hitokoto, [from])
const sub = !{JSON.stringify(subContent)} })
sub.unshift(data.hitokoto, from) .catch(err => {
typedJSFn.init(sub) console.error('Failed to get the Hitokoto API:', err)
} else { typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
document.getElementById('subtitle').textContent = data.hitokoto
}
}) })
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
@@ -48,46 +66,48 @@ case source
when 2 when 2
script. script.
function subtitleType () { function subtitleType () {
btf.getScript('https://yijuzhan.com/api/word.php?m=js').then(() => { fetch('https://v.api.aa1.cn/api/yiyan/index.php')
const con = str[0] .then(response => response.text())
if (!{effect}) { .then(data => {
const from = '出自 ' + str[1] const reg = /<p>(.*?)<\/p>/g
const sub = !{JSON.stringify(subContent)} const result = reg.exec(data)
sub.unshift(con, from) if (result && result[1]) {
typedJSFn.init(sub) typedJSFn.processSubtitle(result[1])
} else { } else {
document.getElementById('subtitle').textContent = con 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)})
})
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
when 3 when 3
script. script.
function subtitleType () { function subtitleType () {
btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js').then(() => { btf.getScript('https://sdk.jinrishici.com/v2/browser/jinrishici.js')
jinrishici.load(result =>{ .then(() => {
if (!{effect}) { jinrishici.load(result => {
const sub = !{JSON.stringify(subContent)} if (result && result.data && result.data.content) {
const content = result.data.content typedJSFn.processSubtitle(result.data.content)
sub.unshift(content) } else {
typedJSFn.init(sub) throw new Error('Failed to parse the return value of Jinrishici API')
} else { }
document.getElementById('subtitle').textContent = result.data.content })
} })
.catch(err => {
console.error('Failed to get the Jinrishici API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
}) })
})
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
default default
- subContent = subContent.length ? subContent : new Array(config.subtitle) if subContent.length > 0
script. script.
function subtitleType () { function subtitleType () {
if (!{effect}) { typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
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, website_id, option, UV_PV } = theme.umami_analytics - let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
- const isServerURL = !!serverURL - const isServerURL = !!serverURL
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is' - const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1' - const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
@@ -9,57 +9,102 @@ script.
const config = !{JSON.stringify(UV_PV)} const config = !{JSON.stringify(UV_PV)}
const runTrack = () => { const runTrack = () => {
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
} else {
console.warn('Umami Analytics: umami.track is not available')
}
} }
const loadUmamiJS = () => { const loadUmamiJS = () => {
btf.getScript('!{baseURL}/script.js', { btf.getScript('!{baseURL}/!{script_name}', {
'data-website-id': '!{website_id}', 'data-website-id': '!{website_id}',
'data-auto-track': 'false', 'data-auto-track': 'false',
...option ...option
}).then(runTrack) }).then(() => {
runTrack()
}).catch(error => {
console.error('Umami Analytics: Error loading script', error)
})
} }
const getData = async (isPost) => { const getData = async (isPost) => {
const now = Date.now() try {
const keyUrl = isPost ? `&url=${window.location.pathname}` : '' const now = Date.now()
const headerList = { 'Accept': 'application/json' } const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}` const headerList = { 'Accept': 'application/json' }
else headerList['x-umami-api-key'] = config.token
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { if (!{isServerURL}) {
method: "GET", headerList['Authorization'] = `Bearer ${config.token}`
headers: headerList } else {
}) headerList['x-umami-api-key'] = config.token
return await res.json() }
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
method: "GET",
headers: headerList
})
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return await res.json()
} catch (error) {
console.error('Umami Analytics: Failed to fetch data', error)
throw error
}
} }
const insertData = async () => { const insertData = async () => {
try { try {
if (GLOBAL_CONFIG_SITE.isPost && config.page_pv) { if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
const pagePV = document.getElementById('umamiPV') const pagePV = document.getElementById('umamiPV')
if (pagePV) { if (pagePV) {
const data = await getData(true) const data = await getData(true)
pagePV.textContent = data.pageviews.value if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
} pagePV.textContent = data.pageviews.value
} else { } else {
const data = (config.site_uv || config.site_pv) && await getData() console.warn('Umami Analytics: Invalid page view data received')
if (config.site_uv) { }
const siteUV = document.getElementById('umami-site-uv')
if (siteUV) siteUV.textContent = data.visitors.value
}
if (config.site_pv) {
const sitePV = document.getElementById('umami-site-pv')
if (sitePV) sitePV.textContent = data.pageviews.value
} }
} }
} catch (e) {
console.error('Failed to load Umami Analytics:', e) if (config.site_uv || config.site_pv) {
const data = await getData(false)
if (config.site_uv) {
const siteUV = document.getElementById('umami-site-uv')
if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') {
siteUV.textContent = data.visitors.value
} else if (siteUV) {
console.warn('Umami Analytics: Invalid site UV data received')
}
}
if (config.site_pv) {
const sitePV = document.getElementById('umami-site-pv')
if (sitePV && data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
sitePV.textContent = data.pageviews.value
} else if (sitePV) {
console.warn('Umami Analytics: Invalid site PV data received')
}
}
}
} catch (error) {
console.error('Umami Analytics: Failed to insert data', error)
} }
} }
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track') btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
loadUmamiJS() loadUmamiJS()
insertData()
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData)
} else {
setTimeout(insertData, 100)
}
})() })()

View File

@@ -12,4 +12,3 @@
.toc-content.toc-div-class(class=tocExpandClass style="display:none")!=toc(page.origin, {list_number: tocNumber}) .toc-content.toc-div-class(class=tocExpandClass style="display:none")!=toc(page.origin, {list_number: tocNumber})
else else
.toc-content(class=tocExpandClass)!=toc(page.content, {list_number: tocNumber}) .toc-content(class=tocExpandClass)!=toc(page.content, {list_number: tocNumber})

View File

@@ -9,6 +9,6 @@ if theme.aside.card_tags.enable
- limit = limit === 0 ? 0 : limit || 40 - limit = limit === 0 ? 0 : limit || 40
if theme.aside.card_tags.color 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'}) .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'})
else 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

@@ -6,40 +6,39 @@ if theme.aside.card_webinfo.enable
.webinfo .webinfo
if theme.aside.card_webinfo.post_count if theme.aside.card_webinfo.post_count
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.article_name') + " :" .item-name= `${_p('aside.card_webinfo.article_name')} :`
.item-count= site.posts.length .item-count= site.posts.length
if theme.aside.card_webinfo.runtime_date if theme.aside.card_webinfo.runtime_date
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.runtime.name') + " :" .item-name= `${_p('aside.card_webinfo.runtime.name')} :`
.item-count#runtimeshow(data-publishDate=date_xml(theme.aside.card_webinfo.runtime_date)) .item-count#runtimeshow(data-publishDate=date_xml(theme.aside.card_webinfo.runtime_date))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
if theme.wordcount.enable && theme.wordcount.total_wordcount if theme.wordcount.enable && theme.wordcount.total_wordcount
.webinfo-item .webinfo-item
.item-name=_p('aside.card_webinfo.site_wordcount') + " :" .item-name= `${_p('aside.card_webinfo.site_wordcount')} :`
.item-count=totalcount(site) .item-count= totalcount(site)
if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_uv if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_uv
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.site_uv_name') + " :" .item-name= `${_p('aside.card_webinfo.site_uv_name')} :`
.item-count#umami-site-uv .item-count#umami-site-uv
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
else if theme.busuanzi.site_uv else if theme.busuanzi.site_uv
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.site_uv_name') + " :" .item-name= `${_p('aside.card_webinfo.site_uv_name')} :`
.item-count#busuanzi_value_site_uv .item-count#busuanzi_value_site_uv
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_pv if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_pv
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.site_pv_name') + " :" .item-name= `${_p('aside.card_webinfo.site_pv_name')} :`
.item-count#umami-site-pv .item-count#umami-site-pv
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
else if theme.busuanzi.site_pv else if theme.busuanzi.site_pv
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.site_pv_name') + " :" .item-name= `${_p('aside.card_webinfo.site_pv_name')} :`
.item-count#busuanzi_value_site_pv .item-count#busuanzi_value_site_pv
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
if theme.aside.card_webinfo.last_push_date if theme.aside.card_webinfo.last_push_date
.webinfo-item .webinfo-item
.item-name= _p('aside.card_webinfo.last_push_date.name') + " :" .item-name= `${_p('aside.card_webinfo.last_push_date.name')} :`
.item-count#last-push-date(data-lastPushDate=date_xml(Date.now())) .item-count#last-push-date(data-lastPushDate=date_xml(Date.now()))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin

View File

@@ -1,6 +1,6 @@
#aside-content.aside-content #aside-content.aside-content
//- post //- post
if is_post() if globalPageType === 'post'
- const tocStyle = page.toc_style_simple - const tocStyle = page.toc_style_simple
- const tocStyleVal = tocStyle === true || tocStyle === false ? tocStyle : theme.toc.style_simple - const tocStyleVal = tocStyle === true || tocStyle === false ? tocStyle : theme.toc.style_simple
if showToc && tocStyleVal if showToc && tocStyleVal

View File

@@ -33,4 +33,3 @@ block content
if page.comments !== false && theme.comments.use if page.comments !== false && theme.comments.use
- var commentsJsLoad = true - var commentsJsLoad = true
!=partial('includes/third-party/comments/index', {}, {cache: true}) !=partial('includes/third-party/comments/index', {}, {cache: true})

View File

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

View File

@@ -1,15 +1,15 @@
abcjs_basic_js: abcjs_basic_js:
name: abcjs name: abcjs
file: dist/abcjs-basic-min.js file: dist/abcjs-basic-min.js
version: 6.4.4 version: 6.5.2
activate_power_mode: activate_power_mode:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/activate-power-mode.min.js file: dist/activate-power-mode.min.js
version: 1.1.4 version: 1.1.6
algolia_search: algolia_search:
name: algoliasearch name: algoliasearch
file: dist/lite/builds/browser.umd.js file: dist/lite/builds/browser.umd.js
version: 5.12.0 version: 5.43.0
aplayer_css: aplayer_css:
name: aplayer name: aplayer
file: dist/APlayer.min.css file: dist/APlayer.min.css
@@ -33,45 +33,45 @@ blueimp_md5:
canvas_fluttering_ribbon: canvas_fluttering_ribbon:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/canvas-fluttering-ribbon.min.js file: dist/canvas-fluttering-ribbon.min.js
version: 1.1.4 version: 1.1.6
canvas_nest: canvas_nest:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/canvas-nest.min.js file: dist/canvas-nest.min.js
version: 1.1.4 version: 1.1.6
canvas_ribbon: canvas_ribbon:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/canvas-ribbon.min.js file: dist/canvas-ribbon.min.js
version: 1.1.4 version: 1.1.6
chartjs: chartjs:
name: chart.js name: chart.js
file: dist/chart.umd.js file: dist/chart.umd.js
version: 4.4.6 version: 4.5.1
clickShowText: clickShowText:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/click-show-text.min.js file: dist/click-show-text.min.js
version: 1.1.4 version: 1.1.6
click_heart: click_heart:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/click-heart.min.js file: dist/click-heart.min.js
version: 1.1.4 version: 1.1.6
disqusjs: disqusjs:
name: disqusjs name: disqusjs
file: dist/browser/disqusjs.es2015.umd.min.js file: dist/browser/disqusjs.es2015.umd.min.js
version: 3.0.2 version: 3.2.1
disqusjs_css: disqusjs_css:
name: disqusjs name: disqusjs
file: dist/browser/styles/disqusjs.css file: dist/browser/styles/disqusjs.css
version: 3.0.2 version: 3.2.1
docsearch_css: docsearch_css:
name: '@docsearch/css' name: '@docsearch/css'
other_name: docsearch-css other_name: docsearch-css
file: dist/style.css file: dist/style.css
version: 3.6.3 version: 4.3.1
docsearch_js: docsearch_js:
name: '@docsearch/js' name: '@docsearch/js'
other_name: docsearch-js other_name: docsearch-js
file: dist/umd/index.js file: dist/umd/index.js
version: 3.6.3 version: 4.3.1
egjs_infinitegrid: egjs_infinitegrid:
name: '@egjs/infinitegrid' name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid other_name: egjs-infinitegrid
@@ -80,22 +80,22 @@ egjs_infinitegrid:
fancybox: fancybox:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
version: 5.0.36 version: 6.1.4
other_name: fancyapps-ui other_name: fancyapps-ui
fancybox_css: fancybox_css:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css file: dist/fancybox/fancybox.css
version: 5.0.36 version: 6.1.4
other_name: fancyapps-ui other_name: fancyapps-ui
fireworks: fireworks:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/fireworks.min.js file: dist/fireworks.min.js
version: 1.1.4 version: 1.1.6
fontawesome: fontawesome:
name: '@fortawesome/fontawesome-free' name: '@fortawesome/fontawesome-free'
file: css/all.min.css file: css/all.min.css
other_name: font-awesome other_name: font-awesome
version: 6.6.0 version: 7.1.0
gitalk: gitalk:
name: gitalk name: gitalk
file: dist/gitalk.min.js file: dist/gitalk.min.js
@@ -108,28 +108,24 @@ instantpage:
name: instant.page name: instant.page
file: instantpage.js file: instantpage.js
version: 5.2.0 version: 5.2.0
instantsearch:
name: instantsearch.js
file: dist/instantsearch.production.min.js
version: 4.75.3
katex: katex:
name: katex name: katex
file: dist/katex.min.css file: dist/katex.min.css
other_name: KaTeX other_name: KaTeX
version: 0.16.11 version: 0.16.25
katex_copytex: katex_copytex:
name: katex name: katex
file: dist/contrib/copy-tex.min.js file: dist/contrib/copy-tex.min.js
other_name: KaTeX other_name: KaTeX
version: 0.16.11 version: 0.16.25
lazyload: lazyload:
name: vanilla-lazyload name: vanilla-lazyload
file: dist/lazyload.iife.min.js file: dist/lazyload.iife.min.js
version: 19.1.3 version: 19.1.3
mathjax: mathjax:
name: mathjax name: mathjax
file: es5/tex-mml-chtml.js file: tex-mml-chtml.js
version: 3.2.2 version: 4.0.0
medium_zoom: medium_zoom:
name: medium-zoom name: medium-zoom
file: dist/medium-zoom.min.js file: dist/medium-zoom.min.js
@@ -137,11 +133,11 @@ medium_zoom:
mermaid: mermaid:
name: mermaid name: mermaid
file: dist/mermaid.min.js file: dist/mermaid.min.js
version: 11.4.0 version: 11.12.1
meting_js: meting_js:
name: butterfly-extsrc name: butterfly-extsrc
file: metingjs/dist/Meting.min.js file: metingjs/dist/Meting.min.js
version: 1.1.4 version: 1.1.6
pace_default_css: pace_default_css:
name: pace-js name: pace-js
other_name: pace other_name: pace
@@ -152,10 +148,6 @@ pace_js:
other_name: pace other_name: pace
file: pace.min.js file: pace.min.js
version: 1.2.4 version: 1.2.4
pangu:
name: pangu
file: dist/browser/pangu.min.js
version: 4.0.7
pjax: pjax:
name: pjax name: pjax
file: pjax.min.js file: pjax.min.js
@@ -164,25 +156,25 @@ prismjs_autoloader:
name: prismjs name: prismjs
file: plugins/autoloader/prism-autoloader.min.js file: plugins/autoloader/prism-autoloader.min.js
other_name: prism other_name: prism
version: 1.29.0 version: 1.30.0
prismjs_js: prismjs_js:
name: prismjs name: prismjs
file: prism.js file: prism.js
other_name: prism other_name: prism
version: 1.29.0 version: 1.30.0
prismjs_lineNumber_js: prismjs_lineNumber_js:
name: prismjs name: prismjs
file: plugins/line-numbers/prism-line-numbers.min.js file: plugins/line-numbers/prism-line-numbers.min.js
other_name: prism other_name: prism
version: 1.29.0 version: 1.30.0
sharejs: sharejs:
name: butterfly-extsrc name: butterfly-extsrc
file: sharejs/dist/js/social-share.min.js file: sharejs/dist/js/social-share.min.js
version: 1.1.4 version: 1.1.6
sharejs_css: sharejs_css:
name: butterfly-extsrc name: butterfly-extsrc
file: sharejs/dist/css/share.min.css file: sharejs/dist/css/share.min.css
version: 1.1.4 version: 1.1.6
snackbar: snackbar:
name: node-snackbar name: node-snackbar
file: dist/snackbar.min.js file: dist/snackbar.min.js
@@ -194,7 +186,7 @@ snackbar_css:
twikoo: twikoo:
name: twikoo name: twikoo
file: dist/twikoo.all.min.js file: dist/twikoo.all.min.js
version: 1.6.39 version: 1.6.44
typed: typed:
name: typed.js name: typed.js
file: dist/typed.umd.js file: dist/typed.umd.js
@@ -202,14 +194,14 @@ typed:
valine: valine:
name: valine name: valine
file: dist/Valine.min.js file: dist/Valine.min.js
version: 1.5.2 version: 1.5.3
waline_css: waline_css:
name: '@waline/client' name: '@waline/client'
file: dist/waline.css file: dist/waline.css
other_name: waline other_name: waline
version: 3.3.2 version: 3.7.1
waline_js: waline_js:
name: '@waline/client' name: '@waline/client'
file: dist/waline.js file: dist/waline.js
other_name: waline other_name: waline
version: 3.3.2 version: 3.7.1

View File

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

View File

@@ -1,24 +0,0 @@
/**
* 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,14 +1,26 @@
hexo.extend.filter.register('before_generate', () => { const { deepMerge } = require('hexo-util')
// Get first two digits of the Hexo version number const path = require('path')
const { version, log, locals } = hexo
const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
if (hexoVer < 5.3) { // 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('Please update Hexo to V5.3.0 or higher!')
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!') log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
process.exit(-1) process.exit(-1)
} }
// Check for deprecated configuration file
if (locals.get) { if (locals.get) {
const data = locals.get('data') const data = locals.get('data')
if (data && data.butterfly) { if (data && data.butterfly) {
@@ -17,4 +29,58 @@ hexo.extend.filter.register('before_generate', () => {
process.exit(-1) process.exit(-1)
} }
} }
}) }
/**
* 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)

View File

@@ -1,585 +0,0 @@
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,
pangu: {
enable: false,
field: 'site'
},
lazyload: {
enable: 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
},
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 * Butterfly
* lazyload * Lazyload filter
* replace src to data-lazy-src * Replace src with data-lazy-src for lazy loading
*/ */
'use strict' 'use strict'
@@ -9,8 +9,21 @@
const urlFor = require('hexo-util').url_for.bind(hexo) const urlFor = require('hexo-util').url_for.bind(hexo)
const lazyload = htmlContent => { 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\'>')
})
}
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`)
// 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}"`
})
} }
hexo.extend.filter.register('after_render:html', data => { hexo.extend.filter.register('after_render:html', data => {

View File

@@ -1,40 +1,82 @@
/** /**
* Butterfly * Random cover for posts
* ramdom cover
*/ */
'use strict' 'use strict'
hexo.extend.filter.register('before_post_render', data => { hexo.extend.generator.register('post', locals => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/i const previousIndexes = []
let { cover: coverVal, top_img: topImg } = data
// Add path to top_img and cover if post_asset_folder is enabled const getRandomCover = defaultCover => {
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}`
}
const randomCoverFn = () => {
const { cover: { default_cover: defaultCover } } = hexo.theme.config
if (!defaultCover) return false if (!defaultCover) return false
if (!Array.isArray(defaultCover)) return defaultCover if (!Array.isArray(defaultCover)) return defaultCover
const num = Math.floor(Math.random() * defaultCover.length)
return defaultCover[num] const coverCount = defaultCover.length
if (coverCount === 1) {
return defaultCover[0]
}
const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
let index
do {
index = Math.floor(Math.random() * coverCount)
} while (previousIndexes.includes(index) && previousIndexes.length < coverCount)
previousIndexes.push(index)
if (previousIndexes.length > maxPreviousIndexes) {
previousIndexes.shift()
}
return defaultCover[index]
} }
if (coverVal === false) return data const handleImg = data => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
let { cover: coverVal, top_img: topImg } = data
// If cover is not set, use random cover // Add path to top_img and cover if post_asset_folder is enabled
if (!coverVal) { if (hexo.config.post_asset_folder) {
const randomCover = randomCoverFn() if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
data.cover = randomCover data.top_img = `${data.path}${topImg}`
coverVal = randomCover // update coverVal }
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
data.cover = `${data.path}${coverVal}`
}
}
if (coverVal === false) return data
// If cover is not set, use random cover
if (!coverVal) {
const { cover: { default_cover: defaultCover } } = hexo.theme.config
const randomCover = getRandomCover(defaultCover)
data.cover = randomCover
coverVal = randomCover // update coverVal
}
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
data.cover_type = 'img'
}
return data
} }
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) { // https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
data.cover_type = 'img' const posts = locals.posts.sort('date').toArray()
} const { length } = posts
return data return posts.map((post, i) => {
if (i) post.prev = posts[i - 1]
if (i < length - 1) post.next = posts[i + 1]
post.__post = true
return {
data: handleImg(post),
layout: 'post',
path: post.path
}
})
}) })

View File

@@ -1,77 +1,126 @@
'use strict' 'use strict'
hexo.extend.helper.register('aside_archives', function (options = {}) { hexo.extend.helper.register('aside_archives', function (options = {}) {
const { config, page, site, url_for, _p } = this const { config, page, site, url_for: urlFor, _p } = this
const archiveDir = config.archive_dir const { archive_dir: archiveDir, timezone, language } = config
const { timezone } = config
const lang = toMomentLocale(page.lang || page.language || config.language)
const type = options.type || 'monthly'
const format = options.format || (type === 'monthly' ? 'MMMM YYYY' : 'YYYY')
const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true
const order = options.order || -1
const limit = options.limit
const compareFunc = type === 'monthly'
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
: (yearA, monthA, yearB, monthB) => yearA === yearB
const posts = site.posts.sort('date', order) // Destructure and set default options with object destructuring
if (!posts.length) return '' const {
type = 'monthly',
format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY',
show_count: showCount = true,
order = -1,
limit,
transform
} = options
const data = [] // Optimize locale handling
posts.forEach(post => { 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 ''
// Use reduce for more efficient data processing
const data = site.posts.sort('date', order).reduce((acc, post) => {
let date = post.date.clone() let date = post.date.clone()
if (timezone) date = date.tz(timezone) if (timezone) date = date.tz(timezone)
const year = date.year() const year = date.year()
const month = date.month() + 1 const month = date.month() + 1
if (!data.length || !compareFunc(data[data.length - 1].year, data[data.length - 1].month, year, month)) { if (lang) date = date.locale(lang)
if (lang) date = date.locale(lang)
data.push({ name: date.format(format), year, month, count: 1 })
} else {
data[data.length - 1].count++
}
})
const link = item => { // Find or create archive entry
const lastEntry = acc[acc.length - 1]
if (type === 'yearly') {
const existingYearIndex = acc.findIndex(entry => entry.year === year)
if (existingYearIndex !== -1) {
acc[existingYearIndex].count++
} else {
// 否則創建新條目
acc.push({
name: date.format(format),
year,
month,
count: 1
})
}
} else {
if (!lastEntry || !compareFunc(lastEntry.year, lastEntry.month, year, month)) {
acc.push({
name: date.format(format),
year,
month,
count: 1
})
} else {
lastEntry.count++
}
}
return acc
}, [])
// Create link generator function
const createArchiveLink = item => {
let url = `${archiveDir}/${item.year}/` let url = `${archiveDir}/${item.year}/`
if (type === 'monthly') { if (type === 'monthly') {
url += item.month < 10 ? `0${item.month}/` : `${item.month}/` url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
} }
return url_for(url) return urlFor(url)
} }
const len = data.length // Limit results efficiently
const limitLength = limit === 0 ? len : Math.min(len, limit) const limitedData = limit > 0 ? data.slice(0, Math.min(data.length, limit)) : data
let result = ` // Use template literal for better readability
const archiveHeader = `
<div class="item-headline"> <div class="item-headline">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i>
<span>${_p('aside.card_archives')}</span> <span>${_p('aside.card_archives')}</span>
${len > limitLength ? `<a class="card-more-btn" href="${url_for(archiveDir)}/" title="${_p('aside.more_button')}"><i class="fas fa-angle-right"></i></a>` : ''} ${
data.length > limitedData.length
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
title="${_p('aside.more_button')}">
<i class="fas fa-angle-right"></i>
</a>`
: ''
}
</div> </div>
<ul class="card-archive-list">
` `
for (let i = 0; i < limitLength; i++) { // Use map for generating list items, join for performance
const item = data[i] const archiveList = `
result += ` <ul class="card-archive-list">
<li class="card-archive-list-item"> ${limitedData
<a class="card-archive-list-link" href="${link(item)}"> .map(
<span class="card-archive-list-date">${options.transform ? options.transform(item.name) : item.name}</span> item => `
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''} <li class="card-archive-list-item">
</a> <a class="card-archive-list-link" href="${createArchiveLink(item)}">
</li> <span class="card-archive-list-date">
` ${transform ? transform(item.name) : item.name}
} </span>
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''}
</a>
</li>
`
)
.join('')}
</ul>
`
result += '</ul>' return archiveHeader + archiveList
return result
}) })
const toMomentLocale = function (lang) { // Improved locale conversion function
if (!lang || lang === 'en' || lang === 'default') { const toMomentLocale = lang => {
return 'en' if (!lang || ['en', 'default'].includes(lang)) return 'en'
}
return lang.toLowerCase().replace('_', '-') return lang.toLowerCase().replace('_', '-')
} }

View File

@@ -4,9 +4,9 @@ hexo.extend.helper.register('inject_head_js', function () {
const { darkmode, aside, pjax } = this.theme const { darkmode, aside, pjax } = this.theme
const start = darkmode.start || 6 const start = darkmode.start || 6
const end = darkmode.end || 18 const end = darkmode.end || 18
const { theme_color } = hexo.theme.config const { theme_color: themeColor } = hexo.theme.config
const themeColorLight = theme_color && theme_color.enable ? theme_color.meta_theme_color_light : '#ffffff' const themeColorLight = themeColor && themeColor.enable ? themeColor.meta_theme_color_light : '#ffffff'
const themeColorDark = theme_color && theme_color.enable ? theme_color.meta_theme_color_dark : '#0d0d0d' const themeColorDark = themeColor && themeColor.enable ? themeColor.meta_theme_color_dark : '#0d0d0d'
const createCustomJs = () => ` const createCustomJs = () => `
const saveToLocal = { const saveToLocal = {

View File

@@ -13,7 +13,7 @@ hexo.extend.helper.register('postDesc', data => {
hexo.extend.helper.register('cloudTags', function (options = {}) { hexo.extend.helper.register('cloudTags', function (options = {}) {
const env = this const env = this
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order } = options let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options
if (limit > 0) { if (limit > 0) {
source = source.limit(limit) source = source.limit(limit)
@@ -29,14 +29,19 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})` return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
} }
const generateStyle = (size, unit) => const generateStyle = (size, unit, page) => {
`font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};` if (page === 'tags') {
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; background-color: ${getRandomColor()};`
} else {
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
}
}
const length = sizes.length - 1 const length = sizes.length - 1
const result = source.sort(orderby, order).map(tag => { const result = source.sort(orderby, order).map(tag => {
const ratio = length ? sizes.indexOf(tag.length) / length : 0 const ratio = length ? sizes.indexOf(tag.length) / length : 0
const size = minfontsize + ((maxfontsize - minfontsize) * ratio) const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
const style = generateStyle(size, unit) const style = generateStyle(size, unit, page)
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>` return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
}).join('') }).join('')
@@ -48,7 +53,7 @@ hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex =
}) })
hexo.extend.helper.register('md5', function (path) { hexo.extend.helper.register('md5', function (path) {
return crypto.createHash('md5').update(decodeURI(this.url_for(path))).digest('hex') return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex')
}) })
hexo.extend.helper.register('injectHtml', data => { hexo.extend.helper.register('injectHtml', data => {
@@ -65,7 +70,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
const defaultTitle = this._p('page.archives') const defaultTitle = this._p('page.archives')
if (!menu) return defaultTitle if (!menu) return defaultTitle
const loop = (m) => { const loop = m => {
for (const key in m) { for (const key in m) {
if (typeof m[key] === 'object') { if (typeof m[key] === 'object') {
const result = loop(m[key]) const result = loop(m[key])
@@ -81,7 +86,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
return loop(menu) || defaultTitle return loop(menu) || defaultTitle
}) })
hexo.extend.helper.register('getBgPath', path => { hexo.extend.helper.register('getBgPath', function (path) {
if (!path) return '' if (!path) return ''
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
@@ -91,7 +96,7 @@ hexo.extend.helper.register('getBgPath', path => {
if (colorPattern.test(path)) { if (colorPattern.test(path)) {
return `background-color: ${path};` return `background-color: ${path};`
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) { } else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) {
return `background-image: url(${path});` return `background-image: url(${this.url_for(path)});`
} else { } else {
return `background: ${path};` return `background: ${path};`
} }
@@ -127,7 +132,37 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => {
finalResult.forEach(item => { finalResult.forEach(item => {
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') 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') item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
// markdown
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
}) })
return finalResult return finalResult
}) })
hexo.extend.helper.register('getPageType', (page, isHome) => {
const { layout, tag, category, type, archive } = page
if (layout) return layout
if (tag) return 'tag'
if (category) return 'category'
if (archive) return 'archive'
if (type) {
if (type === 'tags' || type === 'categories') return type
else return 'page'
}
if (isHome) return 'home'
return 'post'
})
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

@@ -3,7 +3,7 @@
* galleryGroup and gallery * galleryGroup and gallery
* {% galleryGroup [name] [descr] [url] [img] %} * {% galleryGroup [name] [descr] [url] [img] %}
* *
* {% gallery [button] %} * {% gallery [button],[limit],[firstLimit] %}
* {% gallery url,[url],[button] %} * {% gallery url,[url],[button] %}
*/ */
@@ -11,54 +11,66 @@
const urlFor = require('hexo-util').url_for.bind(hexo) const urlFor = require('hexo-util').url_for.bind(hexo)
const gallery = (args, content) => { const DEFAULT_LIMIT = 10
args = args.join(' ').split(',') const DEFAULT_FIRST_LIMIT = 10
let button = false const IMAGE_REGEX = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g
let type = 'data'
let dataStr = ''
if (args[0] === 'url') { // Helper functions
[type, dataStr, button] = args // url,[link],[lazyload] const parseGalleryArgs = args => {
dataStr = urlFor(dataStr) const [type, ...rest] = args.join(' ').split(',').map(arg => arg.trim())
} else { return {
[button] = args // [lazyload] isUrl: type === 'url',
const regex = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g params: type === 'url' ? rest : [type, ...rest]
let m }
const arr = [] }
while ((m = regex.exec(content)) !== null) {
if (m.index === regex.lastIndex) {
regex.lastIndex++
}
arr.push({
url: m[2],
alt: m[1],
title: m[3]
})
}
dataStr = JSON.stringify(arr) const parseImageContent = content => {
const images = []
let match
while ((match = IMAGE_REGEX.exec(content)) !== null) {
images.push({
url: match[2],
alt: match[1] || '',
title: match[3] || ''
})
} }
return `<div class="gallery-container" data-type="${type}" data-button="${button}"> return images
<div class="gallery-items">${dataStr}</div> }
</div>`
const createGalleryHTML = (type, dataStr, button, limit, firstLimit) => {
return `<div class="gallery-container" data-type="${type}" data-button="${button}" data-limit="${limit}" data-first="${firstLimit}">
<div class="gallery-items">${dataStr}</div>
</div>`
}
const gallery = (args, content) => {
const { isUrl, params } = parseGalleryArgs(args)
if (isUrl) {
const [dataStr, button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
return createGalleryHTML('url', urlFor(dataStr), button, limit, firstLimit)
}
const [button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
const images = parseImageContent(content)
return createGalleryHTML('data', JSON.stringify(images), button, limit, firstLimit)
} }
const galleryGroup = args => { const galleryGroup = args => {
const [name, descr, url, img] = args const [name = '', descr = '', url = '', img = ''] = args.map(arg => arg.trim())
const imgUrl = urlFor(img)
const urlLink = urlFor(url)
return `<figure class="gallery-group"> return `<figure class="gallery-group">
<img class="gallery-group-img no-lightbox" src='${imgUrl}' alt="Group Image Gallery"> <img class="gallery-group-img no-lightbox" src='${urlFor(img)}' alt="Group Image Gallery">
<figcaption> <figcaption>
<div class="gallery-group-name">${name}</div> <div class="gallery-group-name">${name}</div>
<p>${descr}</p> <p>${descr}</p>
<a href='${urlLink}'></a> <a href='${urlFor(url)}'></a>
</figcaption> </figcaption>
</figure> </figure>`
`
} }
// Register tags
hexo.extend.tag.register('gallery', gallery, { ends: true }) hexo.extend.tag.register('gallery', gallery, { ends: true })
hexo.extend.tag.register('galleryGroup', galleryGroup) hexo.extend.tag.register('galleryGroup', galleryGroup)

View File

@@ -16,48 +16,35 @@
'use strict' 'use strict'
const parseArgs = args => { const parseArgs = args => args.join(' ').split(',')
return args.join(' ').split(',')
}
const generateStyle = (bg, color) => { const generateStyle = (bg, color) => {
let style = 'style="' let style = 'style="'
if (bg) { if (bg) style += `background-color: ${bg};`
style += `background-color: ${bg};` if (color) style += `color: ${color}`
}
if (color) {
style += `color: ${color}`
}
style += '"' style += '"'
return style return style
} }
const hideInline = args => { const hideInline = args => {
const [content, display = 'Click', bg = false, color = false] = parseArgs(args) const [content, display = 'Click', bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color) const style = generateStyle(bg, color)
return `<span class="hide-inline"><button type="button" class="hide-button" ${style}>${display}</button><span class="hide-content">${content}</span></span>`
return `<span class="hide-inline"><button type="button" class="hide-button" ${group}>${display}
</button><span class="hide-content">${content}</span></span>`
} }
const hideBlock = (args, content) => { const hideBlock = (args, content) => {
const [display = 'Click', bg = false, color = false] = parseArgs(args) const [display = 'Click', bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color) const style = generateStyle(bg, color)
const renderedContent = hexo.render.renderSync({ text: content, engine: 'markdown' })
return `<div class="hide-block"><button type="button" class="hide-button" ${group}>${display} return `<div class="hide-block"><button type="button" class="hide-button" ${style}>${display}</button><div class="hide-content">${renderedContent}</div></div>`
</button><div class="hide-content">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></div>`
} }
const hideToggle = (args, content) => { const hideToggle = (args, content) => {
const [display, bg = false, color = false] = parseArgs(args) const [display, bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color) const style = generateStyle(bg, color)
let border = '' const border = bg ? `style="border: 1px solid ${bg}"` : ''
const renderedContent = hexo.render.renderSync({ text: content, engine: 'markdown' })
if (bg) { return `<details class="toggle" ${border}><summary class="toggle-button" ${style}>${display}</summary><div class="toggle-content">${renderedContent}</div></details>`
border = `style="border: 1px solid ${bg}"`
}
return `<details class="toggle" ${border}><summary class="toggle-button" ${group}>${display}</summary><div class="toggle-content">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></details>`
} }
hexo.extend.tag.register('hideInline', hideInline) hexo.extend.tag.register('hideInline', hideInline)

View File

@@ -1,9 +1,9 @@
/** /**
* inlineImg 圖片 * inlineImg
* @param {Array} args 圖片名稱和高度 * @param {Array} args - Image name and height
* @param {string} args[0] 圖片名稱 * @param {string} args[0] - Image name
* @param {number} args[1] 圖片高度 * @param {number} args[1] - Image height
* @returns {string} 圖片標籤 * @returns {string} - Image tag
*/ */
'use strict' 'use strict'

View File

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

View File

@@ -6,17 +6,45 @@
'use strict' 'use strict'
const score = (args, content) => { const score = (args, content) => {
// Escape HTML tags and some special characters, including curly braces
const escapeHtmlTags = s => { const escapeHtmlTags = s => {
const lookup = { const lookup = {
'&': '&amp;', '&': '&amp;',
'"': '&quot;', '"': '&quot;',
'\'': '&apos;', "'": '&apos;',
'<': '&lt;', '<': '&lt;',
'>': '&gt;' '>': '&gt;',
'{': '&#123;',
'}': '&#125;'
} }
return s.replace(/[&"'<>]/g, c => lookup[c]) return s.replace(/[&"'<>{}]/g, c => lookup[c])
} }
return `<div class="abc-music-sheet">${escapeHtmlTags(content)}</div>`
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>`
} }
hexo.extend.tag.register('score', score, { ends: true }) hexo.extend.tag.register('score', score, { ends: true })

View File

@@ -1,41 +1,50 @@
/** /**
* timeline * Timeline tag for Hexo
* by Jerry * Syntax:
* {% timeline [headline],[color] %}
* <!-- timeline [title] -->
* [content]
* <!-- endtimeline -->
* <!-- timeline [title] -->
* [content]
* <!-- endtimeline -->
* {% endtimeline %}
*/ */
'use strict' 'use strict'
const timeLineFn = (args, content) => { const timeLineFn = (args, content) => {
const tlBlock = /<!--\s*timeline (.*?)\s*-->\n([\w\W\s\S]*?)<!--\s*endtimeline\s*-->/g // Use named capture groups for better readability
const tlBlock = /<!--\s*timeline\s*(?<title>.*?)\s*-->\n(?<content>[\s\S]*?)<!--\s*endtimeline\s*-->/g
let result = '' // Pre-compile markdown render function
let color = '' const renderMd = text => hexo.render.renderSync({ text, engine: 'markdown' })
let text = ''
if (args.length) {
[text, color] = args.join(' ').split(',')
const mdContent = hexo.render.renderSync({ text, engine: 'markdown' })
result += `<div class='timeline-item headline'><div class='timeline-item-title'><div class='item-circle'>${mdContent}</div></div></div>`
}
const matches = [] // Parse arguments more efficiently
let match const [text, color = ''] = args.length ? args.join(' ').split(',') : []
while ((match = tlBlock.exec(content)) !== null) { // Build initial headline if text exists
matches.push(match[1]) const headline = text
matches.push(match[2]) ? `<div class='timeline-item headline'>
} <div class='timeline-item-title'>
<div class='item-circle'>${renderMd(text)}</div>
</div>
</div>`
: ''
for (let i = 0; i < matches.length; i += 2) { // Match all timeline blocks in one pass and transform
const tlChildTitle = hexo.render.renderSync({ text: matches[i], engine: 'markdown' }) const items = Array.from(content.matchAll(tlBlock))
const tlChildContent = hexo.render.renderSync({ text: matches[i + 1], engine: 'markdown' }) .map(({ groups: { title, content } }) =>
`<div class='timeline-item'>
<div class='timeline-item-title'>
<div class='item-circle'>${renderMd(title)}</div>
</div>
<div class='timeline-item-content'>${renderMd(content)}</div>
</div>`
)
.join('')
const tlTitleHtml = `<div class='timeline-item-title'><div class='item-circle'>${tlChildTitle}</div></div>` return `<div class="timeline ${color}">${headline}${items}</div>`
const tlContentHtml = `<div class='timeline-item-content'>${tlChildContent}</div>`
result += `<div class='timeline-item'>${tlTitleHtml + tlContentHtml}</div>`
}
return `<div class="timeline ${color || ''}">${result}</div>`
} }
hexo.extend.tag.register('timeline', timeLineFn, { ends: true }) hexo.extend.tag.register('timeline', timeLineFn, { ends: true })

View File

@@ -11,7 +11,7 @@
.fontawesomeIcon .fontawesomeIcon
display: inline-block display: inline-block
font-weight: 600 font-weight: 600
font-family: 'Font Awesome 6 Free' font-family: 'Font Awesome 7 Free', 'Font Awesome 6 Free'
text-rendering: auto text-rendering: auto
-webkit-font-smoothing: antialiased -webkit-font-smoothing: antialiased
@@ -181,6 +181,60 @@ if hexo-config('avatar.effect') == true
.reward-main .reward-main
animation: donate_effcet .3s .1s ease both 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 @keyframes scroll-down-effect
0% 0%
opacity: .4 opacity: .4
@@ -278,3 +332,17 @@ if hexo-config('avatar.effect') == true
100% 100%
transform: translateX(0) 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-bg: $preloader-bg
--preloader-color: $preloader-word-color --preloader-color: $preloader-word-color
--tab-border-color: $tab-border-color --tab-border-color: $tab-border-color
--tab-botton-bg: $tab-botton-bg --tab-button-bg: $tab-button-bg
--tab-botton-color: $tab-botton-color --tab-button-color: $tab-button-color
--tab-button-hover-bg: $tab-button-hover-bg --tab-button-hover-bg: $tab-button-hover-bg
--tab-button-active-bg: $tab-button-active-bg --tab-button-active-bg: $tab-button-active-bg
--card-bg: $card-bg --card-bg: $card-bg
@@ -39,6 +39,65 @@
--zoom-bg: #fff --zoom-bg: #fff
--mark-bg: alpha($dark-black, .3) --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 body
position: relative position: relative
overflow-y: scroll overflow-y: scroll
@@ -222,3 +281,7 @@ blockquote
& > :last-child & > :last-child
margin-bottom: 0 !important margin-bottom: 0 !important
.fa-fw
width: 1.25em
text-align: center

View File

@@ -100,16 +100,17 @@ $code-block
.highlight-tools .highlight-tools
display: flex display: flex
align-items: center align-items: center
overflow: hidden
padding: 0 8px padding: 0 8px
min-height: 24px min-height: 24px
height: 2.15em height: 2.15em
background: var(--hltools-bg) background: var(--hltools-bg)
color: var(--hltools-color) color: var(--hltools-color)
font-size: $code-font-size font-size: $code-font-size
overflow: hidden
& > * & > *
padding: 5px flex: 0 0 auto
margin: 2px
i i
cursor: pointer cursor: pointer
@@ -130,28 +131,25 @@ $code-block
padding: 0 padding: 0
.code-lang .code-lang
flex: 1 flex: 1 1 auto
overflow: hidden
padding-right: 10px
text-transform: uppercase text-transform: uppercase
text-overflow: ellipsis
white-space: nowrap
font-weight: bold font-weight: bold
font-size: 1.15em font-size: 1.15em
user-select: none user-select: none
-webkit-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') if hexo-config('code_blocks.language')
.code-lang margin-right: auto
flex: 1 else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none') & > :nth-child(2)
& > div:nth-child(2) margin-right: auto
flex: 1
else else
.macStyle & > :nth-child(1)
flex: 1 margin-right: auto
.gutter .gutter
user-select: none user-select: none
@@ -163,11 +161,21 @@ $code-block
td td
border: none 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 if $highlight_macstyle
.container .container
figure.highlight figure.highlight
margin: 0 0 24px margin: 0 0 24px
border-radius: 7px border-radius: 8px
box-shadow: 0 5px 10px 0 $highlight-mac-border box-shadow: 0 5px 10px 0 $highlight-mac-border
-webkit-transform: translateZ(0) -webkit-transform: translateZ(0)
@@ -264,8 +272,8 @@ if hexo-config('code_blocks.fullpage')
& ~ table & ~ table
display: block display: block
overflow: auto overflow: auto
height: calc(100vh - 2.15em)
margin-bottom: 0 margin-bottom: 0
height: calc(100vh - 2.15em)
@keyframes code-fullpage @keyframes code-fullpage
0%, 0%,

View File

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

View File

@@ -13,17 +13,16 @@
background-color: var(--mark-bg) background-color: var(--mark-bg)
content: '' content: ''
#footer-wrap & > *
position: relative position: relative
padding: 40px 20px color: var(--light-grey)
color: var(--light-grey)
text-align: center
a a
color: var(--light-grey) color: var(--light-grey)
transition: all .3s ease-in-out
&:hover &:hover
text-decoration: underline color: $light-blue
.footer-separator .footer-separator
margin: 0 4px margin: 0 4px
@@ -33,3 +32,56 @@
max-height: 1.4em max-height: 1.4em
width: auto width: auto
vertical-align: text-bottom 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

@@ -436,3 +436,28 @@
&:hover &:hover
&:after &:after
width: 100% 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)

View File

@@ -12,11 +12,10 @@
width: 100% !important width: 100% !important
.pagination-related .pagination-related
width: 50%
height: 150px height: 150px
+maxWidth768() +minWidth768()
width: 100% flex: 1
.info-1 .info-1
.info-item-2 .info-item-2
@@ -31,6 +30,10 @@
margin-top: 40px margin-top: 40px
width: 100% width: 100%
addBorderRadius() addBorderRadius()
display: flex
+maxWidth768()
flex-direction: column
.layout .layout
.pagination .pagination

View File

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

View File

@@ -38,10 +38,14 @@
font-size: 16px font-size: 16px
line-height: w line-height: w
addBorderRadius(5) addBorderRadius(5)
@extend .btn-effects
&:hover &:hover
background-color: var(--btn-hover-color) background-color: var(--btn-hover-color)
i
vertical-align: baseline
#mobile-toc-button #mobile-toc-button
display: none display: none
@@ -54,19 +58,52 @@
if hexo-config('rightside_scroll_percent') if hexo-config('rightside_scroll_percent')
#go-up #go-up
position: relative
.scroll-percent .scroll-percent
position: absolute
top: 0
left: 0
display: none 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 &.show-percent
.scroll-percent .scroll-percent
display: block display: block
opacity: 1
transform: scale(1)
animation: fadeInScale .4s ease-out
& + i i
display: none opacity: 0
transform: scale(.8)
&:hover &:hover.show-percent
.scroll-percent .scroll-percent
display: none opacity: 0
transform: scale(.8)
& + i i
display: block opacity: 1 !important
transform: scale(1) !important
@keyframes fadeInScale
from
opacity: 0
transform: scale(.8)
to
opacity: 1
transform: scale(1)

View File

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

Some files were not shown because too many files have changed in this diff Show More