mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-08-08 19:48:42 +08:00
Compare commits
+1
-1
@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk','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']
|
||||
|
||||
@@ -3,17 +3,25 @@ name: npm publish
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # OIDC 必須
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- run: npm install
|
||||
- run: npm publish
|
||||
|
||||
- run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
||||
@@ -1,117 +1,193 @@
|
||||
<div align="right">
|
||||
<a title="Chinese" href="/README_CN.md">中文</a>
|
||||
<a title="中文" href="/README_CN.md">中文</a>
|
||||
</div>
|
||||
|
||||
<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
|
||||
|
||||
A modern, elegant and feature-rich theme for Hexo
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
📢 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/)
|
||||
|
||||

|
||||

|
||||
|
||||
</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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```powershell
|
||||
npm i hexo-theme-butterfly
|
||||
```bash
|
||||
npm install 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
|
||||
- [x] Rounded Design/Squared Design
|
||||
- [X] Support sub-menu
|
||||
- [x] Two-column layout
|
||||
- [x] Responsive Web Design
|
||||
- [x] Dark Mode
|
||||
- [x] Pjax
|
||||
- [x] Read Mode
|
||||
- [x] Conversion between Traditional and Simplified Chinese
|
||||
- [X] TOC catalog is available for both computers and mobile phones
|
||||
- [X] Built-in Syntax Highlighting Themes (darker/pale night/light/ocean), also support customization
|
||||
- [X] Code Blocks (Display code language/close or expand Code Blocks/Copy Button/word wrap)
|
||||
- [X] Disable copy/Add a Copyright Notice to the Copied Text
|
||||
- [X] Search (Algolia Search/Local Search)
|
||||
- [x] Mathjax and Katex
|
||||
- [x] Built-in 404 page
|
||||
- [x] WordCount
|
||||
- [x] Related articles
|
||||
- [x] Displays outdated notice for a post
|
||||
- [x] Share (Sharejs/Addtoany)
|
||||
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk)
|
||||
- [x] Multiple Comment System Support
|
||||
- [x] Online Chats (Chatra/Tidio/Crisp)
|
||||
- [x] Web analytics
|
||||
- [x] Google AdSense
|
||||
- [x] Webmaster Verification
|
||||
- [x] Change website colour scheme
|
||||
- [x] Typewriter Effect: activate_power_mode
|
||||
- [x] Background effects (Canvas ribbon/canvas_ribbon_piao/canvas_nest)
|
||||
- [x] Mouse click effects (Fireworks/Heart/Text)
|
||||
- [x] Preloader/Loading Animation/pace.js
|
||||
- [x] Busuanzi visitor counter
|
||||
- [x] Medium Zoom/Fancybox
|
||||
- [x] Mermaid
|
||||
- [x] Chart.js
|
||||
- [x] Justified Gallery
|
||||
- [x] Lazyload images
|
||||
- [x] Instantpage/Snackbar notification toast/PWA......
|
||||
### 🎨 Design Style
|
||||
- [x] **Card-based Design** - Modern card-style layout
|
||||
- [x] **Rounded/Square Design** - Customizable border styles
|
||||
- [x] **Responsive Design** - Perfect adaptation to all screen sizes
|
||||
- [x] **Two-column Layout** - Optimized reading experience
|
||||
- [x] **Dark Mode** - Eye-friendly night mode
|
||||
|
||||
## ✨ 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">
|
||||
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
|
||||
</a>
|
||||
### 🔍 Search & Navigation
|
||||
- [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch
|
||||
- [x] **Built-in 404** - Beautiful 404 error page
|
||||
- [x] **Pjax Support** - Smooth page transition experience
|
||||
|
||||
## 📷 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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
### 💬 Social Interaction
|
||||
- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
|
||||
- [x] **Dual Comments Support** - Enable two comment systems simultaneously
|
||||
- [x] **Share Features** - Sharejs/Addtoany sharing components
|
||||
- [x] **Live Chat** - Chatra/Tidio/Crisp instant messaging
|
||||
|
||||
### 📊 Analytics & Statistics
|
||||
- [x] **Visit Statistics** - Busuanzi counter
|
||||
- [x] **Site Analytics** - Google Analytics/Baidu Analytics/Cloudflare Analytics/Microsoft Clarity/Umami
|
||||
- [x] **Webmaster Verification** - Major search engine verification
|
||||
- [x] **Ad Support** - Google AdSense/custom ad slots
|
||||
|
||||
### 🎪 Visual Effects
|
||||
- [x] **Typing Effects** - activate_power_mode animations
|
||||
- [x] **Background Effects** - Static ribbons/dynamic ribbons/floating ribbons/Canvas Nest
|
||||
- [x] **Mouse Effects** - Fireworks/hearts/text click effects
|
||||
- [x] **Loading Animations** - Preloader and pace.js progress bars
|
||||
- [x] **Image Effects** - Medium Zoom/Fancybox image lightbox
|
||||
- [x] **Lazy Loading** - Image lazy loading optimization
|
||||
|
||||
### 🛠️ Advanced Features
|
||||
- [x] **PWA Support** - Progressive Web App
|
||||
- [x] **Copy Protection** - Disable text copying/copyright info append
|
||||
- [x] **Theme Customization** - Custom site color schemes
|
||||
- [x] **Chart Support** - Mermaid flowcharts/Chart.js data charts
|
||||
- [x] **Music Notation** - ABCJS music notation support
|
||||
- [x] **Music Player** - APlayer/Meting music playback
|
||||
- [x] **Article Series** - Series article organization
|
||||
- [x] **Instantpage** - Page preloading acceleration
|
||||
- [x] **Snackbar** - Elegant notification messages
|
||||
|
||||
## 🤝 Contributors
|
||||
|
||||
Thanks to all the developers who have contributed to the Butterfly theme!
|
||||
|
||||
[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
|
||||
|
||||
## 🤝 Building a Better Theme Together
|
||||
|
||||
We believe **the power of open source comes from everyone's participation**! Whether you're a developer, designer, or user, you can contribute to the development of the Butterfly theme.
|
||||
|
||||
### 💬 Get Help & Support
|
||||
|
||||
- 🐛 **Found a bug?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - Let's solve it together!
|
||||
- 💡 **Have ideas?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - Share your creative ideas!
|
||||
- 📚 **Learning to use?** → [Official Documentation](https://butterfly.js.org/) - Detailed usage guide
|
||||
- 💬 **Real-time discussion?** → [Telegram Group](https://t.me/bu2fly) - Chat with community members
|
||||
|
||||
### 🎯 Contributing
|
||||
|
||||
Want to make Butterfly better? We welcome any form of contribution:
|
||||
|
||||
- **🔧 Code Contributions** - Fix bugs, add new features, optimize performance
|
||||
- **📝 Documentation** - Improve docs, translate content, write tutorials
|
||||
- **🎨 Design Suggestions** - UI/UX improvements, theme colors, icon design
|
||||
- **🧪 Testing & Feedback** - Test new features, report issues, provide user experience
|
||||
- **💰 Financial Support** - [Sponsor the Project](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - Support long-term development
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the [Apache 2.0](LICENSE) License.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
This theme is developed based on [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody). Thanks to the original author for their excellent work that provided inspiration and foundation!
|
||||
|
||||
Thanks to all friends who have contributed to the development of the Butterfly theme. Your support has made this theme continuously improve and progress.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**✨ If this theme helps you, please give us a ⭐ Star! ✨**
|
||||
</div>
|
||||
|
||||
+149
-73
@@ -4,114 +4,190 @@
|
||||
|
||||
<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 的現代化、美觀且功能豐富的主題
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
📢 預覽: [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/)
|
||||
|
||||

|
||||

|
||||
|
||||
</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
|
||||
```
|
||||
|
||||
如果想要安裝比較新的dev分支,可以
|
||||
|
||||
```powershell
|
||||
```bash
|
||||
# 安裝開發版本(搶先體驗新功能)
|
||||
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 及以上版本
|
||||
|
||||
在博客根目錄裡
|
||||
|
||||
```powershell
|
||||
npm i hexo-theme-butterfly
|
||||
```bash
|
||||
npm install hexo-theme-butterfly
|
||||
```
|
||||
|
||||
## ⚙ 應用主題
|
||||
### ⚙️ 主題配置
|
||||
|
||||
修改hexo配置文件`_config.yml`,把主題改為`Butterfly`
|
||||
1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
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/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" />
|
||||
</a>
|
||||
### 📝 內容功能
|
||||
- [x] **多級選單** - 支援二級導航選單
|
||||
- [x] **閱讀模式** - 專注的文章閱讀體驗
|
||||
- [x] **目錄導航** - 電腦和手機雙端支援 TOC
|
||||
- [x] **字數統計** - 顯示文章字數和閱讀時間
|
||||
- [x] **相關文章** - 智能推薦相關內容
|
||||
- [x] **過期提醒** - 自動提示文章更新狀態
|
||||
- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換
|
||||
- [x] **標籤外掛** - 豐富的標籤外掛支持
|
||||
|
||||
## 📷 截圖
|
||||
### 🔍 搜尋與導航
|
||||
- [x] **多種搜尋** - Algolia 搜尋 / 本地搜尋 / Docsearch
|
||||
- [x] **內建 404** - 美觀的 404 錯誤頁面
|
||||
- [x] **Pjax 支援** - 流暢的頁面切換體驗
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
### 🎨 程式碼展示
|
||||
- [x] **語法高亮** - 內建多種主題(darker/pale night/light/ocean)
|
||||
- [x] **程式碼功能** - 語言顯示/摺疊展開/複製按鈕/自動換行
|
||||
- [x] **數學公式** - 支援 Mathjax 和 Katex
|
||||
|
||||
### 💬 社交互動
|
||||
- [x] **多元評論系統** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk 等
|
||||
- [x] **雙評論支援** - 可同時啟用兩套評論系統
|
||||
- [x] **分享功能** - Sharejs/Addtoany 分享套件
|
||||
- [x] **線上客服** - Chatra/Tidio/Crisp 即時聊天
|
||||
|
||||
### 📊 數據分析
|
||||
- [x] **訪問統計** - 不蒜子計數器
|
||||
- [x] **網站分析** - Google Analytics/百度統計/Cloudflare Analytics/Microsoft Clarity/Umami
|
||||
- [x] **站長驗證** - 各大搜尋引擎驗證
|
||||
- [x] **廣告支援** - Google AdSense/自訂廣告位
|
||||
|
||||
### 🎪 視覺效果
|
||||
- [x] **打字特效** - activate_power_mode 動畫
|
||||
- [x] **背景特效** - 靜態彩帶/動態彩帶/飄帶效果/Canvas Nest
|
||||
- [x] **滑鼠特效** - 煙花/愛心/文字點擊效果
|
||||
- [x] **載入動畫** - Preloader 和 pace.js 進度條
|
||||
- [x] **圖片效果** - Medium Zoom/Fancybox 圖片燈箱
|
||||
- [x] **懶載入** - 圖片延遲載入優化
|
||||
|
||||
### 🛠️ 進階功能
|
||||
- [x] **PWA 支援** - 漸進式網頁應用
|
||||
- [x] **複製保護** - 可關閉文字複製/版權資訊追加
|
||||
- [x] **主題定製** - 自訂網站配色方案
|
||||
- [x] **圖表支援** - Mermaid 流程圖/Chart.js 數據圖表
|
||||
- [x] **音樂符號** - ABCJS 音樂記譜法支援
|
||||
- [x] **音樂播放器** - APlayer/Meting 音樂播放功能
|
||||
- [x] **系列文章** - 系列文章組織功能
|
||||
- [x] **Instantpage** - 頁面預載入加速
|
||||
- [x] **Snackbar** - 優雅的提示訊息
|
||||
|
||||
## 🤝 貢獻者
|
||||
|
||||
感謝所有為 Butterfly 主題做出貢獻的開發者們!
|
||||
|
||||
[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
|
||||
|
||||
## 📸 主題截圖
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## ⭐ Star 趨勢
|
||||
|
||||
[](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
|
||||
|
||||
## 🤝 一起構建更美好的主題
|
||||
|
||||
我們相信,**開源的力量來自於每一個人的參與**!無論您是開發者、設計師還是用戶,都可以為 Butterfly 主題的發展貢獻力量。
|
||||
|
||||
### 💬 獲取幫助與支援
|
||||
|
||||
- 🐛 **發現問題?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - 讓我們一起解決!
|
||||
- 💡 **有好想法?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - 分享您的創意想法!
|
||||
- 📚 **學習使用?** → [官方文檔](https://butterfly.js.org/) - 詳細的使用指南
|
||||
- 💬 **即時討論?** → [Telegram 群組](https://t.me/bu2fly) - 與社群成員實時交流
|
||||
|
||||
### 🎯 參與貢獻
|
||||
|
||||
想要讓 Butterfly 變得更好嗎?我們歡迎您的任何形式的貢獻:
|
||||
|
||||
- **🔧 代碼貢獻** - 修復 Bug、添加新功能、優化性能
|
||||
- **📝 文檔完善** - 改進文檔、翻譯內容、撰寫教程
|
||||
- **🎨 設計建議** - UI/UX 改進、主題配色、圖示設計
|
||||
- **🧪 測試反饋** - 測試新功能、回報問題、提供使用體驗
|
||||
- **💰 資金支援** - [贊助項目](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - 支持長期發展
|
||||
|
||||
## 📄 授權條款
|
||||
|
||||
本專案採用 [Apache 2.0](LICENSE) 授權條款。
|
||||
|
||||
## 🙏 致敬與感謝
|
||||
|
||||
本主題基於 [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody) 進行開發,感謝原作者的精彩創作為我們提供了靈感與基礎!
|
||||
|
||||
感謝所有為 Butterfly 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star!✨**
|
||||
</div>
|
||||
|
||||
+44
-10
@@ -13,6 +13,7 @@ nav:
|
||||
# Navigation bar logo image
|
||||
logo:
|
||||
display_title: true
|
||||
display_post_title: true
|
||||
# Whether to fix navigation bar
|
||||
fixed: false
|
||||
|
||||
@@ -90,7 +91,8 @@ category_per_img:
|
||||
footer_img: false
|
||||
|
||||
# Website Background
|
||||
# Can set it to color or image url
|
||||
# Can set it to color, image URL or an array containing colors and/or image URLs
|
||||
# If an array is provided, a random background will be selected from the array on each load
|
||||
background:
|
||||
|
||||
cover:
|
||||
@@ -254,12 +256,15 @@ noticeOutdate:
|
||||
# Footer Settings
|
||||
# --------------------------------------
|
||||
footer:
|
||||
nav:
|
||||
owner:
|
||||
enable: true
|
||||
since: 2019
|
||||
custom_text:
|
||||
since: 2025
|
||||
# Copyright of theme and framework
|
||||
copyright: true
|
||||
copyright:
|
||||
enable: true
|
||||
version: true
|
||||
custom_text:
|
||||
|
||||
# --------------------------------------
|
||||
# Aside Settings
|
||||
@@ -314,6 +319,7 @@ aside:
|
||||
# If set 0 will show all
|
||||
limit: 40
|
||||
color: false
|
||||
custom_colors:
|
||||
# Order of tags, random/name/length
|
||||
orderby: random
|
||||
# Sort of order. 1, asc for ascending; -1, desc for descending
|
||||
@@ -399,6 +405,9 @@ rightside_item_order:
|
||||
# Default: toc,chat,comment
|
||||
show:
|
||||
|
||||
# Animation for the bottom right config button
|
||||
rightside_config_animation: true
|
||||
|
||||
# --------------------------------------
|
||||
# Global Settings
|
||||
# --------------------------------------
|
||||
@@ -481,6 +490,11 @@ search:
|
||||
top_n_per_article: 1
|
||||
# Unescape html strings to the readable one.
|
||||
unescape: false
|
||||
# Enable pagination for search results
|
||||
pagination:
|
||||
enable: false
|
||||
# Number of search results per page
|
||||
hitsPerPage: 8
|
||||
CDN:
|
||||
|
||||
# Docsearch
|
||||
@@ -503,12 +517,12 @@ share:
|
||||
# Share.js
|
||||
# https://github.com/overtrue/share.js
|
||||
sharejs:
|
||||
sites: facebook,twitter,wechat,weibo,qq
|
||||
sites: facebook,x,wechat,weibo,qq
|
||||
|
||||
# AddToAny
|
||||
# https://www.addtoany.com/
|
||||
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
|
||||
@@ -645,7 +659,7 @@ artalk:
|
||||
# --------------------------------------
|
||||
|
||||
chat:
|
||||
# Choose: chatra/tidio/crisp
|
||||
# Choose: chatra/tidio/crisp/knocket
|
||||
# Leave it empty if you don't need chat
|
||||
use:
|
||||
# Chat Button [recommend]
|
||||
@@ -666,6 +680,10 @@ tidio:
|
||||
crisp:
|
||||
website_id:
|
||||
|
||||
# https://trtc.io/solutions/knocket
|
||||
knocket:
|
||||
identifier:
|
||||
|
||||
# --------------------------------------
|
||||
# Analysis
|
||||
# --------------------------------------
|
||||
@@ -687,6 +705,7 @@ umami_analytics:
|
||||
enable: false
|
||||
# For self-hosted setups, configure the hostname of the Umami instance
|
||||
serverURL:
|
||||
script_name: script.js
|
||||
website_id:
|
||||
option:
|
||||
UV_PV:
|
||||
@@ -696,6 +715,12 @@ umami_analytics:
|
||||
# Umami Cloud (API key) / self-hosted Umami (token)
|
||||
token:
|
||||
|
||||
# https://www.googletagmanager.com/
|
||||
google_tag_manager:
|
||||
tag_id:
|
||||
# optional
|
||||
domain:
|
||||
|
||||
# --------------------------------------
|
||||
# Advertisement
|
||||
# --------------------------------------
|
||||
@@ -919,6 +944,10 @@ mermaid:
|
||||
theme:
|
||||
light: default
|
||||
dark: dark
|
||||
# Enable "Open in New Tab" button to view diagram in a separate window
|
||||
open_in_new_tab: true
|
||||
# Enable zoom and pan interactions on diagrams
|
||||
zoom_pan: true
|
||||
|
||||
# chartjs
|
||||
# see https://www.chartjs.org/docs/latest/
|
||||
@@ -987,6 +1016,8 @@ instantpage: false
|
||||
# https://github.com/verlok/vanilla-lazyload
|
||||
lazyload:
|
||||
enable: false
|
||||
# Use browser's native lazyload instead of vanilla-lazyload
|
||||
native: false
|
||||
# Specify the field to use lazyload (site or post)
|
||||
field: site
|
||||
placeholder:
|
||||
@@ -1018,7 +1049,11 @@ Open_Graph_meta:
|
||||
|
||||
# Structured Data
|
||||
# https://developers.google.com/search/docs/guides/intro-structured-data
|
||||
structured_data: true
|
||||
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
|
||||
css_prefix: true
|
||||
@@ -1042,7 +1077,7 @@ CDN:
|
||||
third_party_provider: jsdelivr
|
||||
|
||||
# Add version number to url, true or false
|
||||
version: false
|
||||
version: true
|
||||
|
||||
# Custom format
|
||||
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
|
||||
@@ -1078,7 +1113,6 @@ CDN:
|
||||
# gitalk_css:
|
||||
# giscus:
|
||||
# instantpage:
|
||||
# instantsearch:
|
||||
# katex:
|
||||
# katex_copytex:
|
||||
# lazyload:
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
||||
recommend: Related Articles
|
||||
edit: Edit
|
||||
back_to_home: Back to Home
|
||||
|
||||
search:
|
||||
title: Search
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: Previous
|
||||
next: Next
|
||||
page_info: 'Page ${current} of ${total}'
|
||||
|
||||
comment: Comments
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
||||
recommend: Related Articles
|
||||
edit: Edit
|
||||
back_to_home: Back to Home
|
||||
|
||||
search:
|
||||
title: Search
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: Previous
|
||||
next: Next
|
||||
page_info: 'Page ${current} of ${total}'
|
||||
|
||||
comment: Comments
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: 'このブログのすべての記事は、<a href="%s">%s</a> ライセンスの下で提供されており、特に明記されていない限り、すべての権利を留保します。転載時には出典を明記してください: <a href="%s">%s</a>。'
|
||||
recommend: 関連記事
|
||||
edit: 編集
|
||||
back_to_home: ホームに戻る
|
||||
|
||||
search:
|
||||
title: 検索
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: 前へ
|
||||
next: 次へ
|
||||
page_info: '${current} ページ / 合計 ${total} ページ'
|
||||
|
||||
comment: コメント
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: '이 블로그의 모든 글은 <a href="%s">%s</a> 라이선스를 따르며, 별도로 명시되지 않는 한 모든 권리를 보유합니다. 재배포 시 출처를 명시해 주세요: <a href="%s">%s</a>.'
|
||||
recommend: 관련 글
|
||||
edit: 편집
|
||||
back_to_home: 홈으로 돌아가기
|
||||
|
||||
search:
|
||||
title: 검색
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: 이전
|
||||
next: 다음
|
||||
page_info: '${current} 페이지 / 총 ${total} 페이지'
|
||||
|
||||
comment: 댓글
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ post:
|
||||
<a href="%s" target="_blank">%s</a> 许可协议。转载请注明来源 <a href="%s" target="_blank">%s</a>!'
|
||||
recommend: 相关推荐
|
||||
edit: 编辑
|
||||
back_to_home: 返回首页
|
||||
|
||||
search:
|
||||
title: 搜索
|
||||
@@ -48,6 +49,7 @@ search:
|
||||
pagination:
|
||||
prev: 上一篇
|
||||
next: 下一篇
|
||||
page_info: '第 ${current} 页 / 共 ${total} 页'
|
||||
|
||||
comment: 评论
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: '除特別聲明外,本博客所有文章均採用<a href="%s">%s</a> 授權協議。轉載請註明出處:<a href="%s">%s</a>。'
|
||||
recommend: 相關文章
|
||||
edit: 編輯
|
||||
back_to_home: 返回首頁
|
||||
|
||||
search:
|
||||
title: 搜尋
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: 上一頁
|
||||
next: 下一頁
|
||||
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||
|
||||
comment: 評論
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ post:
|
||||
copyright_content: '本部落格所有文章除特別聲明外,均採用<a href="%s" target="_blank">%s</a> 授權協議。轉載請註明來源 <a href="%s" target="_blank">%s</a>!'
|
||||
recommend: 相關推薦
|
||||
edit: 編輯
|
||||
back_to_home: 返回首頁
|
||||
|
||||
search:
|
||||
title: 搜尋
|
||||
@@ -47,6 +48,7 @@ search:
|
||||
pagination:
|
||||
prev: 上一篇
|
||||
next: 下一篇
|
||||
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||
|
||||
comment: 評論
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ div
|
||||
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
||||
|
||||
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
||||
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
||||
script(async data-pjax src=theme.asset.busuanzi ? url_for(theme.asset.busuanzi) : '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
||||
|
||||
!= partial('includes/third-party/search/index', {}, { cache: true })
|
||||
|
||||
if theme.google_tag_manager && theme.google_tag_manager.tag_id
|
||||
noscript
|
||||
iframe(src=`${theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/ns.html?id=${theme.google_tag_manager.tag_id}` height="0" width="0" style="display:none;visibility:hidden")
|
||||
+37
-17
@@ -1,19 +1,39 @@
|
||||
#footer-wrap
|
||||
if theme.footer.owner.enable
|
||||
- const currentYear = new Date().getFullYear()
|
||||
- const sinceYear = theme.footer.owner.since
|
||||
.copyright
|
||||
if sinceYear && sinceYear != currentYear
|
||||
!= `©${sinceYear} - ${currentYear} By ${config.author}`
|
||||
else
|
||||
!= `©${currentYear} By ${config.author}`
|
||||
if theme.footer.copyright
|
||||
- const v = getVersion()
|
||||
.framework-info
|
||||
span= _p('footer.framework') + ' '
|
||||
a(href='https://hexo.io')= `Hexo ${v.hexo}`
|
||||
span.footer-separator |
|
||||
span= _p('footer.theme') + ' '
|
||||
a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly ${v.theme}`
|
||||
- const { nav, owner, copyright, custom_text } = theme.footer
|
||||
|
||||
if nav
|
||||
.footer-flex
|
||||
for block in nav
|
||||
.footer-flex-items(style=`${ block.width ? 'flex-grow:' + block.width : '' }`)
|
||||
for blockItem in block.content
|
||||
.footer-flex-item
|
||||
.footer-flex-title= blockItem.title
|
||||
.footer-flex-content
|
||||
for subitem in blockItem.item
|
||||
if subitem.html
|
||||
div!= subitem.html
|
||||
else if subitem.url
|
||||
a(href=url_for(subitem.url), target='_blank' title=subitem.title)= subitem.title
|
||||
else if subitem.title
|
||||
div!= subitem.title
|
||||
.footer-other
|
||||
.footer-copyright
|
||||
if owner.enable
|
||||
- const currentYear = new Date().getFullYear()
|
||||
- const sinceYear = owner.since
|
||||
span.copyright
|
||||
if sinceYear && sinceYear != currentYear
|
||||
!= `© ${sinceYear} - ${currentYear} By ${config.author}`
|
||||
else
|
||||
!= `© ${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
|
||||
.footer_custom_text!= theme.footer.custom_text
|
||||
@@ -32,3 +32,14 @@ if theme.microsoft_clarity
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}");
|
||||
|
||||
if (theme.google_tag_manager && theme.google_tag_manager.tag_id)
|
||||
script.
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
"!{theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','!{theme.google_tag_manager.tag_id}');
|
||||
btf.addGlobalFn('pjaxComplete', () => {
|
||||
dataLayer.push({'event': 'pjaxComplete', 'page_title': document.title, 'page_location': location.href, 'page_path': window.location.pathname})
|
||||
}, 'google_tag_manager')
|
||||
@@ -10,7 +10,6 @@
|
||||
hitsPerPage: theme.search.algolia_search.hitsPerPage,
|
||||
// search languages
|
||||
languages: {
|
||||
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
|
||||
hits_empty: _p("search.algolia_search.hits_empty"),
|
||||
hits_stats: _p("search.algolia_search.hits_stats"),
|
||||
}
|
||||
@@ -19,12 +18,16 @@
|
||||
|
||||
let localSearch = 'undefined'
|
||||
if (theme.search.use === 'local_search') {
|
||||
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search
|
||||
const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
|
||||
localSearch = JSON.stringify({
|
||||
path: CDN || config.root + config.search.path,
|
||||
preload,
|
||||
top_n_per_article,
|
||||
unescape,
|
||||
pagination: {
|
||||
enable: pagination.enable,
|
||||
hitsPerPage: pagination.hitsPerPage
|
||||
},
|
||||
languages: {
|
||||
// search languages
|
||||
hits_empty: _p("search.local_search.hits_empty"),
|
||||
@@ -69,20 +72,16 @@
|
||||
})
|
||||
}
|
||||
|
||||
let highlight = 'undefined'
|
||||
let syntaxHighlighter = config.syntax_highlighter
|
||||
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
|
||||
if (highlightEnable) {
|
||||
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
|
||||
highlight = JSON.stringify({
|
||||
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
|
||||
highlightCopy: copy,
|
||||
highlightLang: language,
|
||||
highlightHeightLimit: height_limit,
|
||||
highlightFullpage: fullpage,
|
||||
highlightMacStyle: macStyle
|
||||
})
|
||||
}
|
||||
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
|
||||
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
|
||||
let highlight = JSON.stringify({
|
||||
plugin: highlightProvider,
|
||||
highlightCopy: copy,
|
||||
highlightLang: language,
|
||||
highlightHeightLimit: height_limit,
|
||||
highlightFullpage: fullpage,
|
||||
highlightMacStyle: macStyle
|
||||
})
|
||||
|
||||
script.
|
||||
const GLOBAL_CONFIG = {
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
if theme.structured_data && page.layout === 'post'
|
||||
-
|
||||
// use json-ld to add structured data
|
||||
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 || site.url;
|
||||
const authorHref = full_url_for(authorHrefVal);
|
||||
const title = page.title
|
||||
const url = page.permalink
|
||||
const imageVal = page.cover_type === 'img' ? page.cover : theme.avatar.img
|
||||
const image = imageVal ? full_url_for(imageVal) : ''
|
||||
const datePublished = page.date.toISOString()
|
||||
const dateModified = (page.updated || page.date).toISOString()
|
||||
const author = page.copyright_author || config.author
|
||||
const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || 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
|
||||
}]
|
||||
};
|
||||
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);
|
||||
-
|
||||
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}
|
||||
|
||||
@@ -5,9 +5,13 @@ nav#nav
|
||||
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
|
||||
if theme.nav.display_title
|
||||
span.site-name=config.title
|
||||
if globalPageType === 'post'
|
||||
if globalPageType === 'post' && theme.nav.display_post_title
|
||||
a.nav-page-title(href=url_for('/'))
|
||||
span.site-name=(page.title || config.title)
|
||||
span.site-name
|
||||
i.fa-solid.fa-circle-arrow-left
|
||||
span= ' ' + _p('post.back_to_home')
|
||||
|
||||
#menus
|
||||
if theme.search.use
|
||||
#search-button
|
||||
|
||||
@@ -13,7 +13,29 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside
|
||||
!=partial('includes/loading/index', {}, {cache: true})
|
||||
|
||||
if theme.background
|
||||
#web_bg(style=getBgPath(theme.background))
|
||||
if !Array.isArray(theme.background)
|
||||
#web_bg.bg-animation(style=getBgPath(theme.background))
|
||||
else
|
||||
#web_bg.bg-animation
|
||||
- const bgStyleArr = theme.background.map(getBgPath)
|
||||
script.
|
||||
(() => {
|
||||
const arr = !{JSON.stringify(bgStyleArr)}
|
||||
const webBgDiv = document.getElementById('web_bg')
|
||||
|
||||
const setRandomBg = () => {
|
||||
webBgDiv.style = arr[Math.floor(Math.random() * arr.length)]
|
||||
requestAnimationFrame(() => webBgDiv.classList.add('bg-animation'))
|
||||
}
|
||||
|
||||
document.addEventListener('pjax:send', () => {
|
||||
webBgDiv.style = ''
|
||||
webBgDiv.classList.remove('bg-animation')
|
||||
})
|
||||
|
||||
document.addEventListener('pjax:complete', setRandomBg)
|
||||
document.addEventListener('DOMContentLoaded', setRandomBg)
|
||||
})()
|
||||
|
||||
!=partial('includes/sidebar', {}, {cache: true})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ script.
|
||||
const $body = document.body
|
||||
const preloader = {
|
||||
endLoading: () => {
|
||||
if ($loadingBox.classList.contains('loaded')) return
|
||||
$body.style.overflow = ''
|
||||
$loadingBox.classList.add('loaded')
|
||||
},
|
||||
@@ -24,7 +25,15 @@ script.
|
||||
}
|
||||
|
||||
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}) {
|
||||
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mixin indexPostUI()
|
||||
- const indexLayout = theme.index_layout
|
||||
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : ''
|
||||
- const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
|
||||
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
||||
.recent-post-items
|
||||
each article, index in page.posts.data
|
||||
@@ -8,17 +8,17 @@ mixin indexPostUI()
|
||||
- const link = article.link || article.path
|
||||
- const title = article.title || _p('no_title')
|
||||
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
||||
- const post_cover = article.cover
|
||||
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
||||
- const postCover = article.cover
|
||||
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
||||
|
||||
if post_cover && theme.cover.index_enable
|
||||
if postCover && theme.cover.index_enable
|
||||
.post_cover(class=leftOrRight)
|
||||
a(href=url_for(link) title=title)
|
||||
if article.cover_type === 'img'
|
||||
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
||||
img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
||||
else
|
||||
div.post-bg(style=`background: ${post_cover}`)
|
||||
.recent-post-info(class=no_cover)
|
||||
div.post-bg(style=`background: ${postCover}`)
|
||||
.recent-post-info(class=noCover)
|
||||
a.article-title(href=url_for(link) title=title)
|
||||
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
||||
i.fas.fa-thumbtack.sticky
|
||||
@@ -35,13 +35,13 @@ mixin indexPostUI()
|
||||
span.article-meta-label=_p('post.updated')
|
||||
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
|
||||
else
|
||||
- const data_type_updated = theme.post_meta.page.date_type === 'updated'
|
||||
- const date_type = data_type_updated ? 'updated' : 'date'
|
||||
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
|
||||
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
|
||||
i(class=date_icon)
|
||||
span.article-meta-label= date_title
|
||||
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format)
|
||||
- const isUpdatedType = theme.post_meta.page.date_type === 'updated'
|
||||
- const dateType = isUpdatedType ? 'updated' : 'date'
|
||||
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
|
||||
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
|
||||
i(class=dateIcon)
|
||||
span.article-meta-label= dateTitle
|
||||
time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format)
|
||||
if theme.post_meta.page.categories && article.categories.data.length > 0
|
||||
span.article-meta
|
||||
span.article-meta-separator |
|
||||
@@ -69,7 +69,10 @@ mixin indexPostUI()
|
||||
span.article-meta-label= ' ' + _p('card_post_count')
|
||||
|
||||
if theme.comments.card_post_count && theme.comments.use
|
||||
case theme.comments.use[0]
|
||||
- const commentSystem = theme.comments.use[0]
|
||||
- const commentLink = url_for(link) + '#post-comment'
|
||||
|
||||
case commentSystem
|
||||
when 'Disqus'
|
||||
when 'Disqusjs'
|
||||
+countBlockInIndex
|
||||
@@ -77,30 +80,30 @@ mixin indexPostUI()
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
when 'Valine'
|
||||
+countBlockInIndex
|
||||
a(href=url_for(link) + '#post-comment')
|
||||
a(href=commentLink)
|
||||
span.valine-comment-count(data-xid=url_for(link))
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
when 'Waline'
|
||||
+countBlockInIndex
|
||||
a(href=url_for(link) + '#post-comment')
|
||||
a(href=commentLink)
|
||||
span.waline-comment-count(data-path=url_for(link))
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
when 'Twikoo'
|
||||
+countBlockInIndex
|
||||
a.twikoo-count(href=url_for(link) + '#post-comment')
|
||||
a.twikoo-count(href=commentLink)
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
when 'Facebook Comments'
|
||||
+countBlockInIndex
|
||||
a(href=url_for(link) + '#post-comment')
|
||||
a(href=commentLink)
|
||||
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
||||
when 'Remark42'
|
||||
+countBlockInIndex
|
||||
a(href=url_for(link) + '#post-comment')
|
||||
a(href=commentLink)
|
||||
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
when 'Artalk'
|
||||
+countBlockInIndex
|
||||
a(href=url_for(link) + '#post-comment')
|
||||
a(href=commentLink)
|
||||
span.artalk-count(data-page-key=url_for(link))
|
||||
i.fa-solid.fa-spinner.fa-spin
|
||||
|
||||
|
||||
+251
-130
@@ -9,44 +9,53 @@
|
||||
- page.toc = false
|
||||
|
||||
#article-container
|
||||
if page.comments !== false && theme.comments.use
|
||||
- commentsJsLoad = true
|
||||
if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
|
||||
if page.comments !== false && theme.comments.use
|
||||
- commentsJsLoad = true
|
||||
|
||||
script.
|
||||
(() => {
|
||||
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
||||
script.
|
||||
(() => {
|
||||
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
||||
|
||||
const runDestroy = (shuoshuoComment) => {
|
||||
if (!shuoshuoComment) return
|
||||
const runDestroy = (shuoshuoComment) => {
|
||||
if (!shuoshuoComment) return
|
||||
|
||||
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
||||
if (key.startsWith('destroy')) fn()
|
||||
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
||||
if (key.startsWith('destroy')) fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addCommentToShuoshuo = e => {
|
||||
const btn = e.target.closest('.shuoshuo-comment-btn')
|
||||
if (!btn) return
|
||||
window.addCommentToShuoshuo = e => {
|
||||
const btn = e.target.closest('.shuoshuo-comment-btn')
|
||||
if (!btn) return
|
||||
|
||||
const ele = btn.closest('.container').nextElementSibling
|
||||
const { shuoshuoComment } = window
|
||||
const isInclude = ele.classList.contains('no-comment')
|
||||
runDestroy(shuoshuoComment)
|
||||
if (isInclude) {
|
||||
ele.classList.remove('no-comment')
|
||||
ele.innerHTML = commentDiv
|
||||
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
||||
btf.switchComments(ele, key)
|
||||
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
||||
const ele = btn.closest('.container').nextElementSibling
|
||||
const { shuoshuoComment } = window
|
||||
const isInclude = ele.classList.contains('no-comment')
|
||||
runDestroy(shuoshuoComment)
|
||||
if (isInclude) {
|
||||
ele.classList.remove('no-comment')
|
||||
ele.innerHTML = commentDiv
|
||||
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
||||
btf.switchComments(ele, key)
|
||||
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
})()
|
||||
|
||||
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.
|
||||
(() => {
|
||||
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
||||
|
||||
const escapeHtml = str => String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||
|
||||
const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
|
||||
const filterDataByLimit = (data, limit) => {
|
||||
@@ -57,132 +66,244 @@
|
||||
return data.filter(item => new Date(item.date) >= limitDate)
|
||||
}
|
||||
return data
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: !{JSON.stringify(config.timezone || '')} || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
|
||||
const formatToTimeZone = (date) => {
|
||||
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
||||
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const options = {
|
||||
timeZone: visitorTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}
|
||||
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
|
||||
.format(new Date(fullDate))
|
||||
.match(/\d+/g)
|
||||
const [day, month, year, hour, minute, second] = dateFormatter.format(new Date(fullDate)).match(/\d+/g)
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
const loadShuoshuo = async () => {
|
||||
try {
|
||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
||||
let data = await response.json()
|
||||
const addLazyload = str => {
|
||||
const lazyConfig = {
|
||||
enable: !{Boolean(enable)},
|
||||
native: !{Boolean(native)},
|
||||
field: !{JSON.stringify(field || '')},
|
||||
placeholder: !{JSON.stringify(url_for(placeholder))},
|
||||
}
|
||||
|
||||
data = filterDataByLimit(sortDataByDate(data), limitConfig)
|
||||
if (!lazyConfig.enable || lazyConfig.field !== 'site' || str.indexOf('<img') === -1) return str
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(str, 'text/html')
|
||||
const images = doc.querySelectorAll('img')
|
||||
|
||||
const container = document.getElementById('article-container')
|
||||
let start = 0
|
||||
images.forEach(img => {
|
||||
if (lazyConfig.native) {
|
||||
img.setAttribute('loading', 'lazy')
|
||||
} else {
|
||||
const src = img.getAttribute('src')
|
||||
img.setAttribute('data-lazy-src', src)
|
||||
|
||||
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>`
|
||||
: ''
|
||||
if (lazyConfig.placeholder) {
|
||||
img.setAttribute('src', lazyConfig.placeholder)
|
||||
} else {
|
||||
img.removeAttribute('src')
|
||||
}
|
||||
}
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
|
||||
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>
|
||||
let currentPage = 1
|
||||
const itemsPerPage = 8
|
||||
let totalPages = 0
|
||||
let data = []
|
||||
|
||||
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">${escapeHtml(tag)}</span>`).join('') || ''
|
||||
const commentButton = item.key && !{commentsJsLoad || false}
|
||||
? `<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="${escapeHtml(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 ? escapeHtml(item.avatar) : !{JSON.stringify(url_for(theme.avatar.img))}}">
|
||||
</div>
|
||||
${commentContainer}
|
||||
</div>`
|
||||
}).join('')
|
||||
<div class="shuoshuo-info">
|
||||
<div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(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('')
|
||||
|
||||
container.insertAdjacentHTML('beforeend', content)
|
||||
const container = document.getElementById('article-container')
|
||||
container.innerHTML = content
|
||||
|
||||
window.lazyLoadInstance.update()
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
window.lazyLoadInstance && window.lazyLoadInstance.update()
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
}
|
||||
|
||||
const setupNavEvents = (nav) => {
|
||||
nav.querySelector('.shuoshuo-prev-btn').addEventListener('click', () => {
|
||||
if (currentPage > 1) { currentPage--; renderPage(currentPage) }
|
||||
})
|
||||
nav.querySelector('.shuoshuo-next-btn').addEventListener('click', () => {
|
||||
if (currentPage < totalPages) { currentPage++; renderPage(currentPage) }
|
||||
})
|
||||
|
||||
const input = nav.querySelector('.shuoshuo-page-input')
|
||||
|
||||
input.addEventListener('focus', e => { e.target.placeholder = '' })
|
||||
input.addEventListener('blur', e => {
|
||||
if (!e.target.value.trim()) e.target.placeholder = currentPage
|
||||
})
|
||||
|
||||
input.addEventListener('input', e => {
|
||||
const value = parseInt(e.target.value) || 0
|
||||
let wasInvalid = false
|
||||
|
||||
if (value > totalPages) { e.target.value = totalPages; wasInvalid = true }
|
||||
else if (value < 1 && e.target.value !== '') { e.target.value = 1; wasInvalid = true }
|
||||
|
||||
if (wasInvalid) {
|
||||
e.target.classList.add('invalid')
|
||||
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||
}
|
||||
})
|
||||
|
||||
input.addEventListener('keydown', e => {
|
||||
const value = e.target.value + e.key
|
||||
|
||||
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete' ||
|
||||
e.key === 'ArrowLeft' || e.key === 'ArrowRight' ||
|
||||
e.key === 'Tab' || e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'Enter') {
|
||||
const inputValue = e.target.value.trim()
|
||||
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
|
||||
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
|
||||
currentPage = inputPage
|
||||
renderPage(currentPage)
|
||||
} else if (inputValue === '') {
|
||||
renderPage(currentPage)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const handleIntersection = (entries) => {
|
||||
if (!entries[0].isIntersecting) return
|
||||
observer.unobserve(entries[0].target)
|
||||
if (!/^\d$/.test(e.key)) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const slice = data.slice(start, start + 10)
|
||||
renderData(slice)
|
||||
start += 10
|
||||
const newValue = parseInt(value) || 0
|
||||
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||
e.preventDefault()
|
||||
e.target.classList.add('invalid')
|
||||
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (start < data.length) {
|
||||
setTimeout(() => observer.observe(container.lastElementChild), 100)
|
||||
} else {
|
||||
observer.disconnect()
|
||||
}
|
||||
};
|
||||
const renderNavigation = () => {
|
||||
const container = document.getElementById('article-container')
|
||||
let nav = container.nextElementSibling
|
||||
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
||||
const pageInfoText = pageInfoTemplate
|
||||
.replace(/\$\{current}/g, currentPage)
|
||||
.replace(/\$\{total}/g, totalPages)
|
||||
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 1.0
|
||||
})
|
||||
if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
|
||||
nav = document.createElement('div')
|
||||
nav.className = 'shuoshuo-navigation'
|
||||
nav.innerHTML = `
|
||||
<button class="shuoshuo-prev-btn"><i class="fa-solid fa-chevron-left"></i></button>
|
||||
<span class="shuoshuo-page-info"></span>
|
||||
<input type="number" class="shuoshuo-page-input" min="1">
|
||||
<button class="shuoshuo-next-btn"><i class="fa-solid fa-chevron-right"></i></button>
|
||||
`
|
||||
container.insertAdjacentElement('afterend', nav)
|
||||
setupNavEvents(nav)
|
||||
}
|
||||
|
||||
renderData(data.slice(start, 10))
|
||||
start += 10
|
||||
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
|
||||
nav.querySelector('.shuoshuo-prev-btn').disabled = currentPage === 1
|
||||
nav.querySelector('.shuoshuo-next-btn').disabled = currentPage === totalPages
|
||||
const input = nav.querySelector('.shuoshuo-page-input')
|
||||
input.max = totalPages
|
||||
input.placeholder = currentPage
|
||||
}
|
||||
|
||||
if (container.lastElementChild) observer.observe(container.lastElementChild)
|
||||
const renderPage = (page) => {
|
||||
const start = (page - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
const pageData = data.slice(start, end)
|
||||
renderData(pageData)
|
||||
renderNavigation()
|
||||
}
|
||||
|
||||
const loadShuoshuo = async () => {
|
||||
const container = document.getElementById('article-container')
|
||||
try {
|
||||
let originData = []
|
||||
if (!{Boolean(page.shuoshuo_url)}) {
|
||||
container.innerHTML = '<div class="shuoshuo-loading"><i class="fa-solid fa-circle-notch fa-spin"></i></div>'
|
||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
originData = await response.json()
|
||||
} else {
|
||||
const dataElement = document.getElementById('shuoshuo-data')
|
||||
originData = dataElement ? JSON.parse(dataElement.textContent) : []
|
||||
}
|
||||
|
||||
data = filterDataByLimit(sortDataByDate(originData), limitConfig)
|
||||
totalPages = Math.ceil(data.length / itemsPerPage)
|
||||
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = '<div class="shuoshuo-empty"></div>'
|
||||
return
|
||||
}
|
||||
|
||||
renderPage(currentPage)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
container.innerHTML = '<div class="shuoshuo-error"><i class="fa-solid fa-circle-exclamation"></i></div>'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo)
|
||||
const pjaxCleanup = () => {
|
||||
const container = document.getElementById('article-container')
|
||||
if (container) {
|
||||
const nav = container.nextElementSibling
|
||||
if (nav && nav.classList.contains('shuoshuo-navigation')) nav.remove()
|
||||
}
|
||||
document.removeEventListener('pjax:send', pjaxCleanup)
|
||||
}
|
||||
|
||||
if (window.pjax) {
|
||||
document.addEventListener('pjax:send', pjaxCleanup)
|
||||
loadShuoshuo()
|
||||
} else {
|
||||
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)
|
||||
@@ -1,2 +1,2 @@
|
||||
.tag-cloud-list.text-center
|
||||
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'})
|
||||
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors})
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
-
|
||||
var options = {
|
||||
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
|
||||
next_text: '<i class="fas fa-chevron-right fa-fw"></i>',
|
||||
mid_size: 1,
|
||||
escape: false
|
||||
}
|
||||
if page.total !== 1
|
||||
-
|
||||
var options = {
|
||||
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
|
||||
next_text: '<i class="fas fa-chevron-right fa-fw"></i>',
|
||||
mid_size: 1,
|
||||
escape: false
|
||||
}
|
||||
|
||||
if globalPageType === 'post'
|
||||
- let paginationOrder = theme.post_pagination === 1 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
|
||||
if globalPageType === 'post'
|
||||
- let paginationOrder = theme.post_pagination === 2 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
|
||||
|
||||
nav#pagination.pagination-post
|
||||
each direction, key in paginationOrder
|
||||
if direction
|
||||
- const getPostDesc = direction.postDesc || postDesc(direction)
|
||||
- let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width')
|
||||
- className = getPostDesc ? className : className + ' no-desc'
|
||||
nav#pagination.pagination-post
|
||||
each direction, key in paginationOrder
|
||||
if direction
|
||||
- const getPostDesc = direction.postDesc || postDesc(direction)
|
||||
- let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width')
|
||||
- className = getPostDesc ? className : className + ' no-desc'
|
||||
|
||||
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
||||
if direction.cover_type === 'img'
|
||||
img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
||||
else
|
||||
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
||||
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
||||
if direction.cover_type === 'img'
|
||||
img.cover(src=url_for(direction.pagination_cover || direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
||||
else
|
||||
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
||||
|
||||
.info(class=key === 'prev' ? '' : 'text-right')
|
||||
.info-1
|
||||
.info-item-1=_p(`pagination.${key}`)
|
||||
.info-item-2!=direction.title
|
||||
if getPostDesc
|
||||
.info-2
|
||||
.info-item-1!=getPostDesc
|
||||
else
|
||||
nav#pagination
|
||||
.pagination
|
||||
if globalPageType === 'home'
|
||||
- options.format = 'page/%d/#content-inner'
|
||||
!=paginator(options)
|
||||
.info(class=key === 'prev' ? '' : 'text-right')
|
||||
.info-1
|
||||
.info-item-1=_p(`pagination.${key}`)
|
||||
.info-item-2!=direction.title
|
||||
if getPostDesc
|
||||
.info-2
|
||||
.info-item-1!=getPostDesc
|
||||
else
|
||||
nav#pagination
|
||||
.pagination
|
||||
if globalPageType === 'home'
|
||||
- options.format = 'page/%d/#content-inner'
|
||||
!=paginator(options)
|
||||
@@ -1,4 +1,5 @@
|
||||
- const { readmode, translate, darkmode, aside, chat } = theme
|
||||
|
||||
mixin rightsideItem(array)
|
||||
each item in array
|
||||
case item
|
||||
@@ -30,30 +31,22 @@ mixin rightsideItem(array)
|
||||
a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))
|
||||
i.fas.fa-comments
|
||||
|
||||
- const { enable, hide, show } = theme.rightside_item_order
|
||||
- const hideArray = enable && hide ? hide.split(',') : ['readmode','translate','darkmode','hideAside']
|
||||
- const showArray = enable && show ? show.split(',') : ['toc','chat','comment']
|
||||
- const needCogBtn = (enable && hide) || (!enable && ((globalPageType === 'post' && (readmode || translate.enable || (darkmode.enable && darkmode.button))) || (translate.enable || (darkmode.enable && darkmode.button))))
|
||||
|
||||
#rightside
|
||||
- const { enable, hide, show } = theme.rightside_item_order
|
||||
- const hideArray = enable ? hide && hide.split(',') : ['readmode','translate','darkmode','hideAside']
|
||||
- const showArray = enable ? show && show.split(',') : ['toc','chat','comment']
|
||||
|
||||
|
||||
#rightside-config-hide
|
||||
if hideArray
|
||||
if hideArray.length
|
||||
+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 globalPageType === 'post'
|
||||
if (readmode || translate.enable || (darkmode.enable && darkmode.button))
|
||||
button#rightside-config(type="button" title=_p("rightside.setting"))
|
||||
i.fas.fa-cog.fa-spin
|
||||
else if translate.enable || (darkmode.enable && darkmode.button)
|
||||
button#rightside-config(type="button" title=_p("rightside.setting"))
|
||||
i.fas.fa-cog.fa-spin
|
||||
|
||||
if showArray
|
||||
#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)
|
||||
|
||||
button#go-up(type="button" title=_p("rightside.back_to_top"))
|
||||
|
||||
+10
-7
@@ -1,9 +1,12 @@
|
||||
script.
|
||||
(function() {
|
||||
const abcjsInit = function() {
|
||||
const abcjsFn = function() {
|
||||
setTimeout(function() {
|
||||
const sheets = document.querySelectorAll(".abc-music-sheet")
|
||||
(() => {
|
||||
const abcjsInit = () => {
|
||||
const abcjsFn = () => {
|
||||
const $article = document.getElementById("article-container")
|
||||
if (!$article || $article.querySelector(".hbe-container")) return
|
||||
|
||||
setTimeout(() => {
|
||||
const sheets = $article.querySelectorAll(".abc-music-sheet")
|
||||
for (let i = 0; i < sheets.length; i++) {
|
||||
const ele = sheets[i]
|
||||
if (ele.children.length > 0) continue
|
||||
@@ -24,12 +27,12 @@ script.
|
||||
const options = { ...params, responsive: "resize" }
|
||||
|
||||
// Render the music score using ABCJS.renderAbc
|
||||
ABCJS.renderAbc(ele, ele.innerHTML, options)
|
||||
ABCJS.renderAbc(ele, ele.textContent, options)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
if (typeof ABCJS === "object") {
|
||||
if (typeof ABCJS === "object" && ABCJS !== null) {
|
||||
abcjsFn()
|
||||
} else {
|
||||
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
||||
|
||||
+3
-1
@@ -18,7 +18,9 @@ script.
|
||||
includeReply: false
|
||||
}).then(function (res) {
|
||||
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) {
|
||||
console.log(err)
|
||||
|
||||
+13
-7
@@ -6,10 +6,14 @@ script.
|
||||
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
||||
}
|
||||
|
||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
|
||||
if (isChatBtn) {
|
||||
window.ChatraSetup = { startHidden: true }
|
||||
}
|
||||
|
||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||
if (isChatBtn) {
|
||||
const close = () => {
|
||||
Chatra('minimizeWidget')
|
||||
@@ -21,11 +25,13 @@ script.
|
||||
Chatra('show')
|
||||
}
|
||||
|
||||
window.ChatraSetup = { startHidden: true }
|
||||
window.chatBtnFn = () => {
|
||||
const el = document.getElementById('chatra')
|
||||
return el && el.classList.contains('chatra--expanded') ? close() : open()
|
||||
}
|
||||
|
||||
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => Chatra('hide'),
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ script.
|
||||
|
||||
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => $crisp.push(["do", "chat:hide"]),
|
||||
|
||||
+2
@@ -5,3 +5,5 @@ case theme.chat.use
|
||||
include ./tidio.pug
|
||||
when 'crisp'
|
||||
include ./crisp.pug
|
||||
when 'knocket'
|
||||
include ./knocket.pug
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//- Knocket live chat — https://trtc.io/solutions/knocket
|
||||
script.
|
||||
(() => {
|
||||
const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/</g, '\\u003c')}
|
||||
if (!identifier) return
|
||||
|
||||
btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => {
|
||||
const isChatBtn = !{theme.chat.rightside_button}
|
||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||
|
||||
let widget = null
|
||||
const getWidget = () => widget || (widget = document.getElementById('contact-widget-auto'))
|
||||
|
||||
if (isChatBtn) {
|
||||
const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' }
|
||||
const show = () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||
|
||||
// Hide the native Knocket floating button
|
||||
const observer = new MutationObserver(() => {
|
||||
if (getWidget()) { hide(); observer.disconnect() }
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
hide()
|
||||
|
||||
window.chatBtnFn = () => {
|
||||
const w = getWidget()
|
||||
if (!w) return
|
||||
w.style.display = w.style.display === 'none' ? '' : 'none'
|
||||
}
|
||||
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => { const w = getWidget(); if (w) w.style.display = 'none' },
|
||||
show: () => { const w = getWidget(); if (w) w.style.display = '' }
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn('[Knocket] Failed to load the chat SDK.', error)
|
||||
})
|
||||
})()
|
||||
+2
-2
@@ -32,8 +32,8 @@ script.
|
||||
isShow ? close() : open()
|
||||
}
|
||||
|
||||
document.getElementById('chat-btn').style.display = 'block'
|
||||
|
||||
const chatBtn = document.getElementById('chat-btn')
|
||||
if (chatBtn) chatBtn.style.display = 'block'
|
||||
} else if (isChatHideShow) {
|
||||
window.chatBtn = {
|
||||
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ script.
|
||||
const loadArtalk = async (el, pageKey) => {
|
||||
if (typeof Artalk === 'object') initArtalk(el, pageKey)
|
||||
else {
|
||||
await btf.getCSS('!{theme.asset.artalk_css}')
|
||||
await btf.getScript('!{theme.asset.artalk_js}')
|
||||
await btf.getCSS('!{url_for(theme.asset.artalk_css)}')
|
||||
await btf.getScript('!{url_for(theme.asset.artalk_js)}')
|
||||
initArtalk(el, pageKey)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
script.
|
||||
(() => {
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||
const dqOption = !{JSON.stringify(dqOption)}
|
||||
|
||||
const destroyDisqusjs = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
script.
|
||||
(()=>{
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||
|
||||
const loadFBComment = (el = document, path) => {
|
||||
if (isShuoshuo) {
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
script.
|
||||
(() => {
|
||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||
const option = !{JSON.stringify(option)}
|
||||
const options = !{JSON.stringify(option)}
|
||||
|
||||
const loadScript = src => {
|
||||
const script = document.createElement('script')
|
||||
@@ -42,8 +42,8 @@ script.
|
||||
host: '!{host}',
|
||||
site_id: '!{siteId}',
|
||||
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
|
||||
...option,
|
||||
url: isShuoshuo ? window.location.origin + path : (option && option.url) || window.location.origin + window.location.pathname
|
||||
...options,
|
||||
url: isShuoshuo ? window.location.origin + path : (options && options.url) || window.location.origin + window.location.pathname
|
||||
}
|
||||
|
||||
if (window.REMARK42) {
|
||||
|
||||
+51
-17
@@ -1,32 +1,67 @@
|
||||
//- Mathjax 3
|
||||
//- Mathjax 4/5
|
||||
- const { tags, enableMenu } = theme.math.mathjax
|
||||
script.
|
||||
(() => {
|
||||
const changeScriptToMath = article => {
|
||||
article.querySelectorAll('script[type^="math/tex"]').forEach(el => {
|
||||
const display = /mode=display/.test(el.type)
|
||||
const node = document.createElement(display ? 'div' : 'span')
|
||||
node.textContent = display
|
||||
? `$$${el.textContent}$$`
|
||||
: `$${el.textContent}$`
|
||||
el.parentNode.replaceChild(node, el)
|
||||
})
|
||||
}
|
||||
|
||||
const loadMathjax = () => {
|
||||
const article = document.getElementById('article-container')
|
||||
if (!article || article.querySelector('.hbe-container')) return
|
||||
changeScriptToMath(article)
|
||||
|
||||
if (!window.MathJax) {
|
||||
window.MathJax = {
|
||||
loader: {
|
||||
load: [
|
||||
// Four font extension packages (optional)
|
||||
//- '[tex]/bbm',
|
||||
//- '[tex]/bboldx',
|
||||
//- '[tex]/dsfont',
|
||||
'[tex]/mhchem',
|
||||
'ui/lazy'
|
||||
],
|
||||
paths: {
|
||||
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
|
||||
|
||||
//- // Four font extension packages (optional)
|
||||
//- 'mathjax-bbm-extension': '[mathjax]/../@mathjax/mathjax-bbm-font-extension',
|
||||
//- 'mathjax-bboldx-extension': '[mathjax]/../@mathjax/mathjax-bboldx-font-extension',
|
||||
//- 'mathjax-dsfont-extension': '[mathjax]/../@mathjax/mathjax-dsfont-font-extension',
|
||||
'mathjax-mhchem-extension': '[mathjax]/../@mathjax/mathjax-mhchem-font-extension'
|
||||
}
|
||||
},
|
||||
output: {
|
||||
font: 'mathjax-newcm',
|
||||
},
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||
tags: '!{tags}',
|
||||
packages: {
|
||||
'[+]': [
|
||||
'mhchem'
|
||||
]
|
||||
}
|
||||
},
|
||||
chtml: {
|
||||
scale: 1.1
|
||||
},
|
||||
options: {
|
||||
lazyMargin: '200px',
|
||||
enableMenu: !{enableMenu},
|
||||
renderActions: {
|
||||
findScript: [10, doc => {
|
||||
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
||||
const display = !!node.type.match(/; *mode=display/)
|
||||
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display)
|
||||
const text = document.createTextNode('')
|
||||
node.parentNode.replaceChild(text, node)
|
||||
math.start = {node: text, delim: '', n: 0}
|
||||
math.end = {node: text, delim: '', n: 0}
|
||||
doc.math.push(math)
|
||||
}
|
||||
}, '']
|
||||
}
|
||||
menuOptions: {
|
||||
settings: {
|
||||
enrich: false // Turn off Braille and voice narration text automatic generation
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +71,8 @@ script.
|
||||
script.async = true
|
||||
document.head.appendChild(script)
|
||||
} else {
|
||||
MathJax.startup.document.state(0)
|
||||
MathJax.texReset()
|
||||
MathJax.typesetPromise()
|
||||
MathJax.typesetClear()
|
||||
MathJax.typesetPromise([ article ])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+322
-9
@@ -1,27 +1,337 @@
|
||||
script.
|
||||
(() => {
|
||||
const parseViewBox = viewBox => {
|
||||
if (!viewBox) return null
|
||||
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
|
||||
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
|
||||
return parts
|
||||
}
|
||||
|
||||
const getSvgViewBox = svg => {
|
||||
const attr = parseViewBox(svg.getAttribute('viewBox'))
|
||||
if (attr) return attr
|
||||
|
||||
// Fallback: use bbox to build a viewBox
|
||||
try {
|
||||
const bbox = svg.getBBox()
|
||||
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
|
||||
} catch (e) {
|
||||
// getBBox may fail on some edge cases; ignore
|
||||
}
|
||||
|
||||
const w = Number(svg.getAttribute('width')) || 0
|
||||
const h = Number(svg.getAttribute('height')) || 0
|
||||
if (w > 0 && h > 0) return [0, 0, w, h]
|
||||
return [0, 0, 100, 100]
|
||||
}
|
||||
|
||||
const setSvgViewBox = (svg, vb) => {
|
||||
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
|
||||
}
|
||||
|
||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
|
||||
|
||||
const openSvgInNewTab = ({ source, initViewBox }) => {
|
||||
const getClonedSvg = () => {
|
||||
if (typeof source === 'string') {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = source.trim()
|
||||
const svg = template.content.querySelector('svg')
|
||||
return svg ? svg.cloneNode(true) : null
|
||||
}
|
||||
if (source && typeof source.cloneNode === 'function') {
|
||||
return source.cloneNode(true)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const clone = getClonedSvg()
|
||||
if (!clone) return
|
||||
if (initViewBox && initViewBox.length === 4) {
|
||||
clone.setAttribute('viewBox', initViewBox.join(' '))
|
||||
}
|
||||
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
||||
if (!clone.getAttribute('xmlns:xlink') && clone.innerHTML.includes('xlink:')) {
|
||||
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
||||
}
|
||||
// inject background to match current theme
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff')
|
||||
if (!clone.style.background) clone.style.background = bg
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
const svgSource = serializer.serializeToString(clone)
|
||||
const htmlSource = `<!doctype html><html><head><meta charset="utf-8" />
|
||||
<style>
|
||||
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
|
||||
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
|
||||
</style>
|
||||
</head><body>${svgSource}</body></html>`
|
||||
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener')
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000)
|
||||
}
|
||||
|
||||
const attachMermaidViewerButton = wrap => {
|
||||
let btn = wrap.querySelector('.mermaid-open-btn')
|
||||
if (!btn) {
|
||||
btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.className = 'mermaid-open-btn'
|
||||
wrap.appendChild(btn)
|
||||
}
|
||||
|
||||
btn.innerHTML = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
|
||||
|
||||
if (!btn.__mermaidViewerBound) {
|
||||
btn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
|
||||
if (!svg) return
|
||||
const initViewBox = wrap.__mermaidInitViewBox
|
||||
openSvgInNewTab({ source: svg, initViewBox })
|
||||
})
|
||||
btn.__mermaidViewerBound = true
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates)
|
||||
const zoomAtPoint = (vb, factor, px, py) => {
|
||||
const w = vb[2] * factor
|
||||
const h = vb[3] * factor
|
||||
const nx = px - (px - vb[0]) * factor
|
||||
const ny = py - (py - vb[1]) * factor
|
||||
return [nx, ny, w, h]
|
||||
}
|
||||
|
||||
const initMermaidGestures = wrap => {
|
||||
// Clean up previous event listeners and pending frames
|
||||
if (wrap.__mermaidAbortController) {
|
||||
wrap.__mermaidAbortController.abort()
|
||||
if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId)
|
||||
}
|
||||
const ac = new AbortController()
|
||||
wrap.__mermaidAbortController = ac
|
||||
const svg = wrap.querySelector('svg')
|
||||
if (!svg) return
|
||||
|
||||
// Ensure viewBox exists so gestures always work
|
||||
const initVb = getSvgViewBox(svg)
|
||||
wrap.__mermaidInitViewBox = initVb
|
||||
wrap.__mermaidCurViewBox = initVb.slice()
|
||||
setSvgViewBox(svg, initVb)
|
||||
// Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chrome
|
||||
svg.style.touchAction = 'none'
|
||||
|
||||
// Cache BoundingClientRect, throttled on scroll to reduce reflow
|
||||
let cachedRect = svg.getBoundingClientRect()
|
||||
let rectDirty = false
|
||||
const markRectDirty = () => { rectDirty = true }
|
||||
window.addEventListener('resize', markRectDirty, { signal: ac.signal })
|
||||
window.addEventListener('scroll', markRectDirty, { signal: ac.signal, capture: true })
|
||||
const getRect = () => {
|
||||
if (rectDirty) {
|
||||
cachedRect = svg.getBoundingClientRect()
|
||||
rectDirty = false
|
||||
}
|
||||
return cachedRect
|
||||
}
|
||||
|
||||
// Precompute clamp bounds from initial viewBox
|
||||
const minW = initVb[2] * 0.1
|
||||
const maxW = initVb[2] * 10
|
||||
const minH = initVb[3] * 0.1
|
||||
const maxH = initVb[3] * 10
|
||||
const clampVb = vb => {
|
||||
const out = vb.slice()
|
||||
out[2] = clamp(out[2], minW, maxW)
|
||||
out[3] = clamp(out[3], minH, maxH)
|
||||
return out
|
||||
}
|
||||
|
||||
// Throttle DOM updates using requestAnimationFrame
|
||||
let pendingVb = null
|
||||
let rafId = null
|
||||
const applyVb = () => {
|
||||
if (pendingVb) {
|
||||
wrap.__mermaidCurViewBox = pendingVb
|
||||
setSvgViewBox(svg, pendingVb)
|
||||
pendingVb = null
|
||||
}
|
||||
rafId = null
|
||||
wrap.__mermaidRafId = null
|
||||
}
|
||||
const setCurVb = vb => {
|
||||
pendingVb = clampVb(vb)
|
||||
if (!rafId) {
|
||||
rafId = requestAnimationFrame(applyVb)
|
||||
wrap.__mermaidRafId = rafId
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
pointers: new Map(),
|
||||
startVb: null,
|
||||
startDist: 0,
|
||||
lastPointerX: 0,
|
||||
lastPointerY: 0
|
||||
}
|
||||
|
||||
const onPointerDown = e => {
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||||
svg.setPointerCapture(e.pointerId)
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||
if (state.pointers.size === 1) {
|
||||
state.startVb = curVb.slice()
|
||||
state.lastPointerX = e.clientX
|
||||
state.lastPointerY = e.clientY
|
||||
} else if (state.pointers.size === 2) {
|
||||
const pts = [...state.pointers.values()]
|
||||
const dx = pts[0].x - pts[1].x
|
||||
const dy = pts[0].y - pts[1].y
|
||||
state.startDist = Math.hypot(dx, dy)
|
||||
state.startVb = curVb.slice()
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerMove = e => {
|
||||
if (!state.pointers.has(e.pointerId)) return
|
||||
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
const rect = getRect()
|
||||
if (state.pointers.size === 1 && state.startVb) {
|
||||
const p = state.pointers.values().next().value
|
||||
const dxClient = p.x - state.lastPointerX
|
||||
const dyClient = p.y - state.lastPointerY
|
||||
state.lastPointerX = p.x
|
||||
state.lastPointerY = p.y
|
||||
const dx = dxClient * (curVb[2] / rect.width)
|
||||
const dy = dyClient * (curVb[3] / rect.height)
|
||||
setCurVb([curVb[0] - dx, curVb[1] - dy, curVb[2], curVb[3]])
|
||||
return
|
||||
}
|
||||
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
|
||||
const pts = [...state.pointers.values()]
|
||||
const dx = pts[0].x - pts[1].x
|
||||
const dy = pts[0].y - pts[1].y
|
||||
const dist = Math.hypot(dx, dy)
|
||||
if (!dist) return
|
||||
const factor = state.startDist / dist
|
||||
const cx = (pts[0].x + pts[1].x) / 2
|
||||
const cy = (pts[0].y + pts[1].y) / 2
|
||||
const px = curVb[0] + (cx - rect.left) * (curVb[2] / rect.width)
|
||||
const py = curVb[1] + (cy - rect.top) * (curVb[3] / rect.height)
|
||||
setCurVb(zoomAtPoint(state.startVb, factor, px, py))
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerUpOrCancel = e => {
|
||||
// Release PointerCapture to avoid event capture anomalies
|
||||
if (svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) {
|
||||
svg.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
state.pointers.delete(e.pointerId)
|
||||
if (state.pointers.size === 0) {
|
||||
state.startVb = null
|
||||
state.startDist = 0
|
||||
} else if (state.pointers.size === 1) {
|
||||
const p = state.pointers.values().next().value
|
||||
state.lastPointerX = p.x
|
||||
state.lastPointerY = p.y
|
||||
}
|
||||
}
|
||||
|
||||
const onWheel = e => {
|
||||
// Prevent event bubbling from triggering external scroll
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
// Normalize deltaY across deltaMode (Chrome uses pixels, Safari uses lines)
|
||||
let delta = e.deltaY
|
||||
if (e.deltaMode === 1) delta *= 16
|
||||
else if (e.deltaMode === 2) delta *= 400
|
||||
|
||||
// Continuous zoom factor: smoother than fixed 1.1/0.9 steps
|
||||
const zoomFactor = Math.pow(1.001, delta)
|
||||
|
||||
const curVb = wrap.__mermaidCurViewBox
|
||||
const rect = getRect()
|
||||
const px = curVb[0] + (e.clientX - rect.left) * (curVb[2] / rect.width)
|
||||
const py = curVb[1] + (e.clientY - rect.top) * (curVb[3] / rect.height)
|
||||
setCurVb(zoomAtPoint(curVb, zoomFactor, px, py))
|
||||
}
|
||||
|
||||
const onDblClick = () => {
|
||||
const init = wrap.__mermaidInitViewBox
|
||||
if (init) setCurVb(init)
|
||||
}
|
||||
|
||||
svg.addEventListener('pointerdown', onPointerDown, { signal: ac.signal })
|
||||
svg.addEventListener('pointermove', onPointerMove, { signal: ac.signal })
|
||||
svg.addEventListener('pointerup', onPointerUpOrCancel, { signal: ac.signal })
|
||||
svg.addEventListener('pointercancel', onPointerUpOrCancel, { signal: ac.signal })
|
||||
svg.addEventListener('wheel', onWheel, { passive: false, signal: ac.signal })
|
||||
svg.addEventListener('dblclick', onDblClick, { signal: ac.signal })
|
||||
}
|
||||
|
||||
const runMermaid = ele => {
|
||||
window.loadMermaid = true
|
||||
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
||||
|
||||
ele.forEach((item, index) => {
|
||||
const mermaidSrc = item.firstElementChild
|
||||
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
|
||||
// Clean up event listeners before removing old SVG
|
||||
if (item.__mermaidAbortController) {
|
||||
item.__mermaidAbortController.abort()
|
||||
}
|
||||
const oldSvg = item.querySelector('svg')
|
||||
if (oldSvg) oldSvg.remove()
|
||||
let config = {}
|
||||
try {
|
||||
config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
||||
} catch (e) {
|
||||
console.warn('[mermaid] failed to parse dataset.config:', e)
|
||||
}
|
||||
if (!config.theme) {
|
||||
config.theme = theme
|
||||
}
|
||||
const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`
|
||||
const mermaidID = `mermaid-${index}`
|
||||
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
||||
|
||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||
const renderMermaid = svg => {
|
||||
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
||||
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
|
||||
item.__mermaidOriginalSvg = svg
|
||||
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
|
||||
}
|
||||
|
||||
// mermaid v9 and v10 compatibility
|
||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
||||
const handleError = err => {
|
||||
console.error(`[mermaid] render failed for block #${index}:`, err)
|
||||
const errorEl = document.createElement('div')
|
||||
errorEl.className = 'mermaid-error'
|
||||
errorEl.textContent = `Mermaid render error: ${err.message || err}`
|
||||
mermaidSrc.insertAdjacentElement('afterend', errorEl)
|
||||
}
|
||||
|
||||
try {
|
||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||
// mermaid v9 and v10 compatibility
|
||||
if (typeof renderFn === 'string') {
|
||||
renderMermaid(renderFn)
|
||||
} else {
|
||||
renderFn.then(({ svg }) => renderMermaid(svg)).catch(handleError)
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const codeToMermaid = () => {
|
||||
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid')
|
||||
const codeToMermaid = $article => {
|
||||
const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
|
||||
if (codeMermaidEle.length === 0) return
|
||||
|
||||
codeMermaidEle.forEach(ele => {
|
||||
@@ -37,8 +347,11 @@ script.
|
||||
}
|
||||
|
||||
const loadMermaid = () => {
|
||||
if (!{theme.mermaid.code_write}) codeToMermaid()
|
||||
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap')
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article) return
|
||||
|
||||
if (!{theme.mermaid.code_write}) codeToMermaid($article)
|
||||
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
|
||||
if ($mermaid.length === 0) return
|
||||
|
||||
const runMermaidFn = () => runMermaid($mermaid)
|
||||
|
||||
Vendored
+18
-9
@@ -14,9 +14,9 @@ if choose
|
||||
else
|
||||
- pjaxSelectors.unshift('meta[name="description"]')
|
||||
|
||||
script(src=url_for(theme.asset.pjax))
|
||||
script(src=url_for(theme.asset.pjax) defer)
|
||||
script.
|
||||
(() => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
|
||||
|
||||
window.pjax = new Pjax({
|
||||
@@ -29,7 +29,13 @@ script.
|
||||
|
||||
const triggerPjaxFn = (val) => {
|
||||
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', () => {
|
||||
@@ -47,8 +53,9 @@ script.
|
||||
document.addEventListener('pjax:complete', () => {
|
||||
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
||||
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
||||
if (!item.parentNode) return
|
||||
const newScript = document.createElement('script')
|
||||
const content = item.text || item.textContent || item.innerHTML || ""
|
||||
const content = item.text || item.textContent || ''
|
||||
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
||||
newScript.appendChild(document.createTextNode(content))
|
||||
item.parentNode.replaceChild(newScript, item)
|
||||
@@ -58,11 +65,13 @@ script.
|
||||
})
|
||||
|
||||
document.addEventListener('pjax:error', e => {
|
||||
if (e.request.status === 404) {
|
||||
const usePjax = !{theme.pjax && theme.pjax.enable}
|
||||
const responseURL = e.request && e.request.responseURL
|
||||
if (e.request && e.request.status === 404) {
|
||||
!{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
|
||||
? pjax.loadUrl('!{url_for("/404.html")}')
|
||||
: responseURL && (window.location.href = responseURL)
|
||||
} else if (responseURL) {
|
||||
window.location.href = responseURL
|
||||
}
|
||||
})
|
||||
})()
|
||||
})
|
||||
+8
-4
@@ -5,11 +5,15 @@
|
||||
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
||||
script.
|
||||
(() => {
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article || $article.querySelector('.hbe-container')) return
|
||||
|
||||
window.Prism = window.Prism || {}
|
||||
window.Prism.manual = true
|
||||
|
||||
const highlightAll = () => {
|
||||
window.Prism.highlightAll()
|
||||
// window.Prism.highlightAll()
|
||||
window.Prism.highlightAllUnder($article, true)
|
||||
}
|
||||
|
||||
window.addEventListener('load', highlightAll)
|
||||
@@ -17,7 +21,7 @@ if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
||||
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
|
||||
})()
|
||||
|
||||
script(src=url_for(prismjs_js))
|
||||
script(src=url_for(prismjs_autoloader))
|
||||
script(src=url_for(prismjs_js) defer)
|
||||
script(src=url_for(prismjs_autoloader) defer)
|
||||
if (line_number)
|
||||
script(src=url_for(prismjs_lineNumber_js))
|
||||
script(src=url_for(prismjs_lineNumber_js) defer)
|
||||
+18
-6
@@ -2,21 +2,33 @@
|
||||
.search-dialog
|
||||
nav.search-nav
|
||||
span.search-dialog-title= _p('search.title')
|
||||
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||
button.search-close-button
|
||||
i.fas.fa-times
|
||||
|
||||
.search-wrap
|
||||
#algolia-search-input
|
||||
#algolia-search-input
|
||||
.ais-SearchBox
|
||||
form.ais-SearchBox-form(action="" role="search" novalidate="")
|
||||
input.ais-SearchBox-input(type="search" placeholder=theme.search.placeholder || _p("search.input_placeholder") autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" maxlength="512" aria-label="Search")
|
||||
button.ais-SearchBox-submit(type="submit" title="Submit the search query" style="display:none;")
|
||||
svg.ais-SearchBox-submitIcon(width="10" height="10" viewBox="0 0 40 40" aria-hidden="true")
|
||||
path(d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z")
|
||||
hr
|
||||
#algolia-search-results
|
||||
#algolia-hits
|
||||
#algolia-pagination
|
||||
#algolia-hits-empty(style="display:none;")
|
||||
.ais-Hits(style="display:none;")
|
||||
ol.ais-Hits-list
|
||||
#algolia-pagination.ais-Pagination(style="display:none;")
|
||||
ul.ais-Pagination-list
|
||||
#algolia-info
|
||||
.algolia-stats
|
||||
.algolia-poweredBy
|
||||
span.ais-Stats-text
|
||||
a.algolia-poweredBy(href="https://www.algolia.com/?utm_source=algoliasearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby" target="_blank" aria-label="Search by Algolia" rel="noopener noreferrer")
|
||||
svg.ais-PoweredBy-logo(height="1.2em" viewBox="0 0 572 64" style="width: auto;")
|
||||
path(fill="#36395A" d="M16 48.3c-3.4 0-6.3-.6-8.7-1.7A12.4 12.4 0 0 1 1.9 42C.6 40 0 38 0 35.4h6.5a6.7 6.7 0 0 0 3.9 6c1.4.7 3.3 1.1 5.6 1.1 2.2 0 4-.3 5.4-1a7 7 0 0 0 3-2.4 6 6 0 0 0 1-3.4c0-1.5-.6-2.8-1.9-3.7-1.3-1-3.3-1.6-5.9-1.8l-4-.4c-3.7-.3-6.6-1.4-8.8-3.4a10 10 0 0 1-3.3-7.9c0-2.4.6-4.6 1.8-6.4a12 12 0 0 1 5-4.3c2.2-1 4.7-1.6 7.5-1.6s5.5.5 7.6 1.6a12 12 0 0 1 5 4.4c1.2 1.8 1.8 4 1.8 6.7h-6.5a6.4 6.4 0 0 0-3.5-5.9c-1-.6-2.6-1-4.4-1s-3.2.3-4.4 1c-1.1.6-2 1.4-2.6 2.4-.5 1-.8 2-.8 3.1a5 5 0 0 0 1.5 3.6c1 1 2.6 1.7 4.7 1.9l4 .3c2.8.2 5.2.8 7.2 1.8 2.1 1 3.7 2.2 4.9 3.8a9.7 9.7 0 0 1 1.7 5.8c0 2.5-.7 4.7-2 6.6a13 13 0 0 1-5.6 4.4c-2.4 1-5.2 1.6-8.4 1.6Zm35.6 0c-2.6 0-4.8-.4-6.7-1.3a13 13 0 0 1-4.7-3.5 17.1 17.1 0 0 1-3.6-10.4v-1c0-2 .3-3.8 1-5.6a13 13 0 0 1 7.3-8.3 15 15 0 0 1 6.3-1.4A13.2 13.2 0 0 1 64 24.3c1 2.2 1.6 4.6 1.6 7.2V34H39.4v-4.3h21.8l-1.8 2.2c0-2-.3-3.7-.9-5.1a7.3 7.3 0 0 0-2.7-3.4c-1.2-.7-2.7-1.1-4.6-1.1s-3.4.4-4.7 1.3a8 8 0 0 0-2.9 3.6c-.6 1.5-.9 3.3-.9 5.4 0 2 .3 3.7 1 5.3a7.9 7.9 0 0 0 2.8 3.7c1.3.8 3 1.3 5 1.3s3.8-.5 5.1-1.3c1.3-1 2.1-2 2.4-3.2h6a11.8 11.8 0 0 1-7 8.7 16 16 0 0 1-6.4 1.2ZM80 48c-2.2 0-4-.3-5.7-1a8.4 8.4 0 0 1-3.7-3.3 9.7 9.7 0 0 1-1.3-5.2c0-2 .5-3.8 1.5-5.2a9 9 0 0 1 4.3-3.1c1.8-.7 4-1 6.7-1H89v4.1h-7.5c-2 0-3.4.5-4.4 1.4-1 1-1.6 2.1-1.6 3.6s.5 2.7 1.6 3.6c1 1 2.5 1.4 4.4 1.4 1.1 0 2.2-.2 3.2-.7 1-.4 1.9-1 2.6-2 .6-1 1-2.4 1-4.2l1.7 2.1c-.2 2-.7 3.8-1.5 5.2a9 9 0 0 1-3.4 3.3 12 12 0 0 1-5.3 1Zm9.5-.7v-8.8h-1v-10c0-1.8-.5-3.2-1.4-4.1-1-1-2.4-1.4-4.2-1.4a142.9 142.9 0 0 0-10.2.4v-5.6a74.8 74.8 0 0 1 8.6-.4c3 0 5.5.4 7.5 1.2s3.4 2 4.4 3.6c1 1.7 1.4 4 1.4 6.7v18.4h-5Zm12.9 0V17.8h5v12.3h-.2c0-4.2 1-7.4 2.8-9.5a11 11 0 0 1 8.3-3.1h1v5.6h-2a9 9 0 0 0-6.3 2.2c-1.5 1.5-2.2 3.6-2.2 6.4v15.6h-6.4Zm34.4 1a15 15 0 0 1-6.6-1.3c-1.9-.9-3.4-2-4.7-3.5a15.5 15.5 0 0 1-2.7-5c-.6-1.7-1-3.6-1-5.4v-1c0-2 .4-3.8 1-5.6a15 15 0 0 1 2.8-4.9c1.3-1.5 2.8-2.6 4.6-3.5a16.4 16.4 0 0 1 13.3.2c2 1 3.5 2.3 4.8 4a12 12 0 0 1 2 6H144c-.2-1.6-1-3-2.2-4.1a7.5 7.5 0 0 0-5.2-1.7 8 8 0 0 0-4.7 1.3 8 8 0 0 0-2.8 3.6 13.8 13.8 0 0 0 0 10.3c.6 1.5 1.5 2.7 2.8 3.6s2.8 1.3 4.8 1.3c1.5 0 2.7-.2 3.8-.8a7 7 0 0 0 2.6-2c.7-1 1-2 1.2-3.2h6.2a11 11 0 0 1-2 6.2 15.1 15.1 0 0 1-11.8 5.5Zm19.7-1v-40h6.4V31h-1.3c0-3 .4-5.5 1.1-7.6a9.7 9.7 0 0 1 3.5-4.8A9.9 9.9 0 0 1 172 17h.3c3.5 0 6 1.1 7.9 3.5 1.7 2.3 2.6 5.7 2.6 10v16.8h-6.4V29.6c0-2.1-.6-3.8-1.8-5a6.4 6.4 0 0 0-4.8-1.8c-2 0-3.7.7-5 2a7.8 7.8 0 0 0-1.9 5.5v17h-6.4Zm63.8 1a12.2 12.2 0 0 1-10.9-6.2 19 19 0 0 1-1.8-7.3h1.4v12.5h-5.1v-40h6.4v19.8l-2 3.5c.2-3.1.8-5.7 1.9-7.7a11 11 0 0 1 4.4-4.5c1.8-1 3.9-1.5 6.1-1.5a13.4 13.4 0 0 1 12.8 9.1c.7 1.9 1 3.8 1 6v1c0 2.2-.3 4.1-1 6a13.6 13.6 0 0 1-13.2 9.4Zm-1.2-5.5a8.4 8.4 0 0 0 7.9-5c.7-1.5 1.1-3.3 1.1-5.3s-.4-3.8-1.1-5.3a8.7 8.7 0 0 0-3.2-3.6 9.6 9.6 0 0 0-9.2-.2 8.5 8.5 0 0 0-3.3 3.2c-.8 1.4-1.3 3-1.3 5v2.3a9 9 0 0 0 1.3 4.8 9 9 0 0 0 3.4 3c1.4.7 2.8 1 4.4 1Zm27.3 3.9-10-28.9h6.5l9.5 28.9h-6Zm-7.5 12.2v-5.7h4.9c1 0 2-.1 2.9-.4a4 4 0 0 0 2-1.4c.4-.7.9-1.6 1.2-2.7l8.6-30.9h6.2l-9.3 32.4a14 14 0 0 1-2.5 5 8.9 8.9 0 0 1-4 2.8c-1.5.6-3.4.9-5.6.9h-4.4Zm9-12.2v-5.2h6.4v5.2H248Z")
|
||||
path(fill="#003DFF" d="M534.4 9.1H528a.8.8 0 0 1-.7-.7V1.8c0-.4.2-.7.6-.8l6.5-1c.4 0 .8.2.9.6v7.8c0 .4-.4.7-.8.7zM428 35.2V.8c0-.5-.3-.8-.7-.8h-.2l-6.4 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.5 0 .8-.4.8-.8V43c0-.4-.3-.7-.6-.8-4.5-.5-4.5-6-4.5-7zm106.5-21.8H528c-.4 0-.7.4-.7.8v34c0 .4.3.8.7.8h6.5c.4 0 .8-.4.8-.8v-34c0-.5-.4-.8-.8-.8zm-17.7 21.8V.8c0-.5-.3-.8-.8-.8l-6.5 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.4 0 .8-.4.8-.8V43c0-.4-.3-.7-.7-.8-4.4-.5-4.4-6-4.4-7zm-22.2-20.6a16.5 16.5 0 0 1 8.6 9.3c.8 2.2 1.3 4.8 1.3 7.5a19.4 19.4 0 0 1-4.6 12.6 14.8 14.8 0 0 1-5.2 3.6c-2 .9-5.2 1.4-6.8 1.4a21 21 0 0 1-6.7-1.4 15.4 15.4 0 0 1-8.6-9.3 21.3 21.3 0 0 1 0-14.4 15.2 15.2 0 0 1 8.6-9.3c2-.8 4.3-1.2 6.7-1.2s4.6.4 6.7 1.2zm-6.7 27.6c2.7 0 4.7-1 6.2-3s2.2-4.3 2.2-7.8-.7-6.3-2.2-8.3-3.5-3-6.2-3-4.7 1-6.1 3c-1.5 2-2.2 4.8-2.2 8.3s.7 5.8 2.2 7.8 3.5 3 6.2 3zm-88.8-28.8c-6.2 0-11.7 3.3-14.8 8.2a18.6 18.6 0 0 0 4.8 25.2c1.8 1.2 4 1.8 6.2 1.7s.1 0 .1 0h.9c4.2-.7 8-4 9.1-8.1v7.4c0 .4.3.7.8.7h6.4a.7.7 0 0 0 .7-.7V14.2c0-.5-.3-.8-.7-.8h-13.5zm6.3 26.5a9.8 9.8 0 0 1-5.7 2h-.5a10 10 0 0 1-9.2-14c1.4-3.7 5-6.3 9-6.3h6.4v18.3zm152.3-26.5h13.5c.5 0 .8.3.8.7v33.7c0 .4-.3.7-.8.7h-6.4a.7.7 0 0 1-.8-.7v-7.4c-1.2 4-4.8 7.4-9 8h-.1a4.2 4.2 0 0 1-.5.1h-.9a10.3 10.3 0 0 1-7-2.6c-4-3.3-6.5-8.4-6.5-14.2 0-3.7 1-7.2 3-10 3-5 8.5-8.3 14.7-8.3zm.6 28.4c2.2-.1 4.2-.6 5.7-2V21.7h-6.3a9.8 9.8 0 0 0-9 6.4 10.2 10.2 0 0 0 9.1 13.9h.5zM452.8 13.4c-6.2 0-11.7 3.3-14.8 8.2a18.5 18.5 0 0 0 3.6 24.3 10.4 10.4 0 0 0 13 .6c2.2-1.5 3.8-3.7 4.5-6.1v7.8c0 2.8-.8 5-2.2 6.3-1.5 1.5-4 2.2-7.5 2.2l-6-.3c-.3 0-.7.2-.8.5l-1.6 5.5c-.1.4.1.8.5 1h.1c2.8.4 5.5.6 7 .6 6.3 0 11-1.4 14-4.1 2.7-2.5 4.2-6.3 4.5-11.4V14.2c0-.5-.4-.8-.8-.8h-13.5zm6.3 8.2v18.3a9.6 9.6 0 0 1-5.6 2h-1a10.3 10.3 0 0 1-8.8-14c1.4-3.7 5-6.3 9-6.3h6.4zM291 31.5A32 32 0 0 1 322.8 0h30.8c.6 0 1.2.5 1.2 1.2v61.5c0 1.1-1.3 1.7-2.2 1l-19.2-17a18 18 0 0 1-11 3.4 18.1 18.1 0 1 1 18.2-14.8c-.1.4-.5.7-.9.6-.1 0-.3 0-.4-.2l-3.8-3.4c-.4-.3-.6-.8-.7-1.4a12 12 0 1 0-2.4 8.3c.4-.4 1-.5 1.6-.2l14.7 13.1v-46H323a26 26 0 1 0 10 49.7c.8-.4 1.6-.2 2.3.3l3 2.7c.3.2.3.7 0 1l-.2.2a32 32 0 0 1-47.2-28.6z")
|
||||
|
||||
#search-mask
|
||||
|
||||
script(src=url_for(theme.asset.algolia_search))
|
||||
script(src=url_for(theme.asset.instantsearch))
|
||||
script(src=url_for(theme.asset.algolia_js))
|
||||
+11
-9
@@ -2,7 +2,7 @@
|
||||
.search-dialog
|
||||
nav.search-nav
|
||||
span.search-dialog-title= _p('search.title')
|
||||
span#loading-status
|
||||
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||
button.search-close-button
|
||||
i.fas.fa-times
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
i.fas.fa-spinner.fa-pulse
|
||||
span= ' ' + _p("search.load_data")
|
||||
|
||||
.search-wrap
|
||||
#local-search-input
|
||||
.local-search-box
|
||||
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input
|
||||
hr
|
||||
#local-search-results
|
||||
#local-search-stats-wrap
|
||||
#search-mask
|
||||
.local-search-input
|
||||
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text")
|
||||
|
||||
hr
|
||||
|
||||
#local-search-results
|
||||
#local-search-pagination.ais-Pagination(style="display:none;")
|
||||
ul.ais-Pagination-list
|
||||
#local-search-stats
|
||||
|
||||
#search-mask
|
||||
script(src=url_for(theme.asset.local_search))
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
- const { effect, source, sub, typed_option } = theme.subtitle
|
||||
- let subContent = sub || new Array()
|
||||
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
|
||||
|
||||
script.
|
||||
window.typedJSFn = {
|
||||
@@ -44,7 +44,7 @@ script.
|
||||
}
|
||||
}
|
||||
}
|
||||
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
|
||||
btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
|
||||
|
||||
case source
|
||||
when 1
|
||||
@@ -53,8 +53,8 @@ case source
|
||||
fetch('https://v1.hitokoto.cn')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const from = '出自 ' + data.from
|
||||
typedJSFn.processSubtitle(data.hitokoto, [from])
|
||||
const from = data.from ? '出自 ' + data.from : ''
|
||||
typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Hitokoto API:', err)
|
||||
@@ -79,7 +79,7 @@ case source
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Yiyan API:', err)
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||
})
|
||||
}
|
||||
typedJSFn.run(subtitleType)
|
||||
@@ -99,7 +99,7 @@ case source
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to get the Jinrishici API:', err)
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||
})
|
||||
}
|
||||
typedJSFn.run(subtitleType)
|
||||
|
||||
+83
-32
@@ -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 baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
||||
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
||||
@@ -9,51 +9,97 @@ script.
|
||||
const config = !{JSON.stringify(UV_PV)}
|
||||
|
||||
const runTrack = () => {
|
||||
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
||||
if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
|
||||
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
||||
} else {
|
||||
console.warn('Umami Analytics: umami.track is not available')
|
||||
}
|
||||
}
|
||||
|
||||
const loadUmamiJS = () => {
|
||||
btf.getScript('!{baseURL}/script.js', {
|
||||
btf.getScript('!{baseURL}/!{script_name}', {
|
||||
'data-website-id': '!{website_id}',
|
||||
'data-auto-track': 'false',
|
||||
...option
|
||||
}).then(runTrack)
|
||||
}).then(() => {
|
||||
runTrack()
|
||||
}).catch(error => {
|
||||
console.error('Umami Analytics: Error loading script', error)
|
||||
})
|
||||
}
|
||||
|
||||
const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
|
||||
|
||||
const getData = async (isPost) => {
|
||||
const now = Date.now()
|
||||
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
||||
const headerList = { 'Accept': 'application/json' }
|
||||
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
|
||||
else headerList['x-umami-api-key'] = config.token
|
||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
|
||||
method: "GET",
|
||||
headers: headerList
|
||||
})
|
||||
return await res.json()
|
||||
try {
|
||||
const now = Date.now()
|
||||
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
[!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
|
||||
}
|
||||
|
||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
})
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const tasks = []
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||
const pagePV = document.getElementById('umamiPV')
|
||||
if (pagePV) {
|
||||
const data = await getData(true)
|
||||
pagePV.textContent = data.pageviews.value
|
||||
}
|
||||
} else {
|
||||
const data = (config.site_uv || config.site_pv) && await getData()
|
||||
if (config.site_uv) {
|
||||
const siteUV = document.getElementById('umami-site-uv')
|
||||
if (siteUV) siteUV.textContent = data.visitors.value
|
||||
}
|
||||
if (config.site_pv) {
|
||||
const sitePV = document.getElementById('umami-site-pv')
|
||||
if (sitePV) sitePV.textContent = data.pageviews.value
|
||||
}
|
||||
tasks.push((async () => {
|
||||
const pagePV = document.getElementById('umamiPV')
|
||||
if (pagePV) {
|
||||
const data = await getData(true)
|
||||
if (data && data.pageviews) {
|
||||
pagePV.textContent = extractValue(data.pageviews)
|
||||
} else {
|
||||
console.warn('Umami Analytics: Invalid page view data received')
|
||||
}
|
||||
}
|
||||
})())
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load Umami Analytics:', e)
|
||||
|
||||
if (config.site_uv || config.site_pv) {
|
||||
tasks.push((async () => {
|
||||
const data = await getData(false)
|
||||
|
||||
if (config.site_uv) {
|
||||
const siteUV = document.getElementById('umami-site-uv')
|
||||
if (siteUV && data && data.visitors) {
|
||||
siteUV.textContent = extractValue(data.visitors)
|
||||
} 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) {
|
||||
sitePV.textContent = extractValue(data.pageviews)
|
||||
} else if (sitePV) {
|
||||
console.warn('Umami Analytics: Invalid site PV data received')
|
||||
}
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
} catch (error) {
|
||||
console.error('Umami Analytics: Failed to insert data', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +107,10 @@ script.
|
||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||
|
||||
loadUmamiJS()
|
||||
insertData()
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', insertData)
|
||||
} else {
|
||||
setTimeout(insertData, 100)
|
||||
}
|
||||
})()
|
||||
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
|
||||
i.fas.fa-tags
|
||||
span= _p('aside.card_tags')
|
||||
|
||||
- let { limit, orderby, order } = theme.aside.card_tags
|
||||
- let { limit, orderby, order, custom_colors } = theme.aside.card_tags
|
||||
- limit = limit === 0 ? 0 : limit || 40
|
||||
|
||||
if theme.aside.card_tags.color
|
||||
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em'})
|
||||
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
|
||||
else
|
||||
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
||||
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hexo-theme-butterfly",
|
||||
"version": "5.3.5",
|
||||
"version": "5.6.0",
|
||||
"description": "A Simple and Card UI Design theme for Hexo",
|
||||
"main": "package.json",
|
||||
"scripts": {
|
||||
@@ -15,16 +15,18 @@
|
||||
"hexo-theme-butterfly"
|
||||
],
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/jerryc127/hexo-theme-butterfly.git"
|
||||
"type": "git",
|
||||
"url": "https://github.com/jerryc127/hexo-theme-butterfly.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/jerryc127/hexo-theme-butterfly/issues",
|
||||
"email": "my@crazywong.com"
|
||||
},
|
||||
"dependencies": {
|
||||
"hexo-renderer-pug": "^3.0.0",
|
||||
"hexo-renderer-stylus": "^3.0.1",
|
||||
"hexo-renderer-pug": "^3.0.0"
|
||||
"hexo-util": "^4.0.0",
|
||||
"moment-timezone": "^0.6.2"
|
||||
},
|
||||
"homepage": "https://butterfly.js.org/",
|
||||
"author": "Jerry <my@crazywong.com>",
|
||||
|
||||
+33
-37
@@ -1,15 +1,15 @@
|
||||
abcjs_basic_js:
|
||||
name: abcjs
|
||||
file: dist/abcjs-basic-min.js
|
||||
version: 6.4.4
|
||||
version: 6.6.4
|
||||
activate_power_mode:
|
||||
name: butterfly-extsrc
|
||||
file: dist/activate-power-mode.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
algolia_search:
|
||||
name: algoliasearch
|
||||
file: dist/lite/builds/browser.umd.js
|
||||
version: 5.20.3
|
||||
version: 5.56.0
|
||||
aplayer_css:
|
||||
name: aplayer
|
||||
file: dist/APlayer.min.css
|
||||
@@ -33,69 +33,69 @@ blueimp_md5:
|
||||
canvas_fluttering_ribbon:
|
||||
name: butterfly-extsrc
|
||||
file: dist/canvas-fluttering-ribbon.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
canvas_nest:
|
||||
name: butterfly-extsrc
|
||||
file: dist/canvas-nest.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
canvas_ribbon:
|
||||
name: butterfly-extsrc
|
||||
file: dist/canvas-ribbon.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
chartjs:
|
||||
name: chart.js
|
||||
file: dist/chart.umd.js
|
||||
version: 4.4.8
|
||||
version: 4.5.1
|
||||
clickShowText:
|
||||
name: butterfly-extsrc
|
||||
file: dist/click-show-text.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
click_heart:
|
||||
name: butterfly-extsrc
|
||||
file: dist/click-heart.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
disqusjs:
|
||||
name: disqusjs
|
||||
file: dist/browser/disqusjs.es2015.umd.min.js
|
||||
version: 3.0.2
|
||||
version: 3.2.1
|
||||
disqusjs_css:
|
||||
name: disqusjs
|
||||
file: dist/browser/styles/disqusjs.css
|
||||
version: 3.0.2
|
||||
version: 3.2.1
|
||||
docsearch_css:
|
||||
name: '@docsearch/css'
|
||||
other_name: docsearch-css
|
||||
file: dist/style.css
|
||||
version: 3.9.0
|
||||
version: 4.6.3
|
||||
docsearch_js:
|
||||
name: '@docsearch/js'
|
||||
other_name: docsearch-js
|
||||
file: dist/umd/index.js
|
||||
version: 3.9.0
|
||||
version: 4.6.3
|
||||
egjs_infinitegrid:
|
||||
name: '@egjs/infinitegrid'
|
||||
other_name: egjs-infinitegrid
|
||||
file: dist/infinitegrid.min.js
|
||||
version: 4.12.0
|
||||
version: 4.13.0
|
||||
fancybox:
|
||||
name: '@fancyapps/ui'
|
||||
file: dist/fancybox/fancybox.umd.js
|
||||
version: 5.0.36
|
||||
version: 6.1.14
|
||||
other_name: fancyapps-ui
|
||||
fancybox_css:
|
||||
name: '@fancyapps/ui'
|
||||
file: dist/fancybox/fancybox.css
|
||||
version: 5.0.36
|
||||
version: 6.1.14
|
||||
other_name: fancyapps-ui
|
||||
fireworks:
|
||||
name: butterfly-extsrc
|
||||
file: dist/fireworks.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
fontawesome:
|
||||
name: '@fortawesome/fontawesome-free'
|
||||
file: css/all.min.css
|
||||
other_name: font-awesome
|
||||
version: 6.7.2
|
||||
version: 7.3.1
|
||||
gitalk:
|
||||
name: gitalk
|
||||
file: dist/gitalk.min.js
|
||||
@@ -108,28 +108,24 @@ instantpage:
|
||||
name: instant.page
|
||||
file: instantpage.js
|
||||
version: 5.2.0
|
||||
instantsearch:
|
||||
name: instantsearch.js
|
||||
file: dist/instantsearch.production.min.js
|
||||
version: 4.77.3
|
||||
katex:
|
||||
name: katex
|
||||
file: dist/katex.min.css
|
||||
other_name: KaTeX
|
||||
version: 0.16.21
|
||||
version: 0.17.0
|
||||
katex_copytex:
|
||||
name: katex
|
||||
file: dist/contrib/copy-tex.min.js
|
||||
other_name: KaTeX
|
||||
version: 0.16.21
|
||||
version: 0.17.0
|
||||
lazyload:
|
||||
name: vanilla-lazyload
|
||||
file: dist/lazyload.iife.min.js
|
||||
version: 19.1.3
|
||||
mathjax:
|
||||
name: mathjax
|
||||
file: es5/tex-mml-chtml.js
|
||||
version: 3.2.2
|
||||
file: tex-mml-chtml.js
|
||||
version: 4.1.3
|
||||
medium_zoom:
|
||||
name: medium-zoom
|
||||
file: dist/medium-zoom.min.js
|
||||
@@ -137,11 +133,11 @@ medium_zoom:
|
||||
mermaid:
|
||||
name: mermaid
|
||||
file: dist/mermaid.min.js
|
||||
version: 11.4.1
|
||||
version: 11.16.0
|
||||
meting_js:
|
||||
name: butterfly-extsrc
|
||||
file: metingjs/dist/Meting.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
pace_default_css:
|
||||
name: pace-js
|
||||
other_name: pace
|
||||
@@ -160,25 +156,25 @@ prismjs_autoloader:
|
||||
name: prismjs
|
||||
file: plugins/autoloader/prism-autoloader.min.js
|
||||
other_name: prism
|
||||
version: 1.29.0
|
||||
version: 1.30.0
|
||||
prismjs_js:
|
||||
name: prismjs
|
||||
file: prism.js
|
||||
other_name: prism
|
||||
version: 1.29.0
|
||||
version: 1.30.0
|
||||
prismjs_lineNumber_js:
|
||||
name: prismjs
|
||||
file: plugins/line-numbers/prism-line-numbers.min.js
|
||||
other_name: prism
|
||||
version: 1.29.0
|
||||
version: 1.30.0
|
||||
sharejs:
|
||||
name: butterfly-extsrc
|
||||
file: sharejs/dist/js/social-share.min.js
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
sharejs_css:
|
||||
name: butterfly-extsrc
|
||||
file: sharejs/dist/css/share.min.css
|
||||
version: 1.1.4
|
||||
version: 1.1.6
|
||||
snackbar:
|
||||
name: node-snackbar
|
||||
file: dist/snackbar.min.js
|
||||
@@ -190,11 +186,11 @@ snackbar_css:
|
||||
twikoo:
|
||||
name: twikoo
|
||||
file: dist/twikoo.all.min.js
|
||||
version: 1.6.41
|
||||
version: 1.7.14
|
||||
typed:
|
||||
name: typed.js
|
||||
file: dist/typed.umd.js
|
||||
version: 2.1.0
|
||||
version: 3.0.0
|
||||
valine:
|
||||
name: valine
|
||||
file: dist/Valine.min.js
|
||||
@@ -203,9 +199,9 @@ waline_css:
|
||||
name: '@waline/client'
|
||||
file: dist/waline.css
|
||||
other_name: waline
|
||||
version: 3.5.5
|
||||
version: 3.15.2
|
||||
waline_js:
|
||||
name: '@waline/client'
|
||||
file: dist/waline.js
|
||||
other_name: waline
|
||||
version: 3.5.5
|
||||
version: 3.15.2
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
// Butterfly 主題默認配置
|
||||
// Default configuration for Butterfly theme
|
||||
|
||||
module.exports = {
|
||||
nav: {
|
||||
logo: null,
|
||||
display_title: true,
|
||||
display_post_title: true,
|
||||
fixed: false
|
||||
},
|
||||
menu: null,
|
||||
code_blocks: {
|
||||
theme: 'light',
|
||||
macStyle: false,
|
||||
height_limit: false,
|
||||
word_wrap: false,
|
||||
copy: true,
|
||||
language: true,
|
||||
shrink: false,
|
||||
fullpage: false
|
||||
},
|
||||
social: null,
|
||||
favicon: '/img/favicon.png',
|
||||
avatar: {
|
||||
img: '/img/butterfly-icon.png',
|
||||
effect: false
|
||||
},
|
||||
disable_top_img: false,
|
||||
default_top_img: null,
|
||||
index_img: null,
|
||||
archive_img: null,
|
||||
tag_img: null,
|
||||
tag_per_img: null,
|
||||
category_img: null,
|
||||
category_per_img: null,
|
||||
footer_img: false,
|
||||
background: null,
|
||||
cover: {
|
||||
index_enable: true,
|
||||
aside_enable: true,
|
||||
archives_enable: true,
|
||||
default_cover: null
|
||||
},
|
||||
error_img: {
|
||||
flink: '/img/friend_404.gif',
|
||||
post_page: '/img/404.jpg'
|
||||
},
|
||||
error_404: {
|
||||
enable: false,
|
||||
subtitle: 'Page Not Found',
|
||||
background: '/img/error-page.png'
|
||||
},
|
||||
post_meta: {
|
||||
page: {
|
||||
date_type: 'created',
|
||||
date_format: 'date',
|
||||
categories: true,
|
||||
tags: false,
|
||||
label: true
|
||||
},
|
||||
post: {
|
||||
position: 'left',
|
||||
date_type: 'both',
|
||||
date_format: 'date',
|
||||
categories: true,
|
||||
tags: true,
|
||||
label: true
|
||||
}
|
||||
},
|
||||
index_site_info_top: null,
|
||||
index_top_img_height: null,
|
||||
subtitle: {
|
||||
enable: false,
|
||||
effect: true,
|
||||
typed_option: null,
|
||||
source: false,
|
||||
sub: null
|
||||
},
|
||||
index_layout: 3,
|
||||
index_post_content: {
|
||||
method: 3,
|
||||
length: 500
|
||||
},
|
||||
toc: {
|
||||
post: true,
|
||||
page: false,
|
||||
number: true,
|
||||
expand: false,
|
||||
style_simple: false,
|
||||
scroll_percent: true
|
||||
},
|
||||
post_copyright: {
|
||||
enable: true,
|
||||
decode: false,
|
||||
author_href: null,
|
||||
license: 'CC BY-NC-SA 4.0',
|
||||
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
|
||||
},
|
||||
reward: {
|
||||
enable: false,
|
||||
text: null,
|
||||
QR_code: null
|
||||
},
|
||||
post_edit: {
|
||||
enable: false,
|
||||
url: null
|
||||
},
|
||||
related_post: {
|
||||
enable: true,
|
||||
limit: 6,
|
||||
date_type: 'created'
|
||||
},
|
||||
post_pagination: 1,
|
||||
noticeOutdate: {
|
||||
enable: false,
|
||||
style: 'flat',
|
||||
limit_day: 365,
|
||||
position: 'top',
|
||||
message_prev: 'It has been',
|
||||
message_next: 'days since the last update, the content of the article may be outdated.'
|
||||
},
|
||||
footer: {
|
||||
nav: null,
|
||||
owner: {
|
||||
enable: true,
|
||||
since: 2025
|
||||
},
|
||||
copyright: {
|
||||
enable: true,
|
||||
version: true
|
||||
},
|
||||
custom_text: null
|
||||
},
|
||||
aside: {
|
||||
enable: true,
|
||||
hide: false,
|
||||
button: true,
|
||||
mobile: true,
|
||||
position: 'right',
|
||||
display: {
|
||||
archive: true,
|
||||
tag: true,
|
||||
category: true
|
||||
},
|
||||
card_author: {
|
||||
enable: true,
|
||||
description: null,
|
||||
button: {
|
||||
enable: true,
|
||||
icon: 'fab fa-github',
|
||||
text: 'Follow Me',
|
||||
link: 'https://github.com/xxxxxx'
|
||||
}
|
||||
},
|
||||
card_announcement: {
|
||||
enable: true,
|
||||
content: 'This is my Blog'
|
||||
},
|
||||
card_recent_post: {
|
||||
enable: true,
|
||||
limit: 5,
|
||||
sort: 'date',
|
||||
sort_order: null
|
||||
},
|
||||
card_newest_comments: {
|
||||
enable: false,
|
||||
sort_order: null,
|
||||
limit: 6,
|
||||
storage: 10,
|
||||
avatar: true
|
||||
},
|
||||
card_categories: {
|
||||
enable: true,
|
||||
limit: 8,
|
||||
expand: 'none',
|
||||
sort_order: null
|
||||
},
|
||||
card_tags: {
|
||||
enable: true,
|
||||
limit: 40,
|
||||
color: false,
|
||||
custom_colors: null,
|
||||
orderby: 'random',
|
||||
order: 1,
|
||||
sort_order: null
|
||||
},
|
||||
card_archives: {
|
||||
enable: true,
|
||||
type: 'monthly',
|
||||
format: 'MMMM YYYY',
|
||||
order: -1,
|
||||
limit: 8,
|
||||
sort_order: null
|
||||
},
|
||||
card_post_series: {
|
||||
enable: true,
|
||||
series_title: false,
|
||||
orderBy: 'date',
|
||||
order: -1
|
||||
},
|
||||
card_webinfo: {
|
||||
enable: true,
|
||||
post_count: true,
|
||||
last_push_date: true,
|
||||
sort_order: null,
|
||||
runtime_date: null
|
||||
}
|
||||
},
|
||||
rightside_bottom: null,
|
||||
translate: {
|
||||
enable: false,
|
||||
default: '繁',
|
||||
defaultEncoding: 2,
|
||||
translateDelay: 0,
|
||||
msgToTraditionalChinese: '繁',
|
||||
msgToSimplifiedChinese: '簡'
|
||||
},
|
||||
readmode: true,
|
||||
darkmode: {
|
||||
enable: true,
|
||||
button: true,
|
||||
autoChangeMode: false,
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
rightside_scroll_percent: false,
|
||||
rightside_item_order: {
|
||||
enable: false,
|
||||
hide: null,
|
||||
show: null
|
||||
},
|
||||
rightside_config_animation: true,
|
||||
anchor: {
|
||||
auto_update: false,
|
||||
click_to_scroll: false
|
||||
},
|
||||
photofigcaption: false,
|
||||
copy: {
|
||||
enable: true,
|
||||
copyright: {
|
||||
enable: false,
|
||||
limit_count: 150
|
||||
}
|
||||
},
|
||||
wordcount: {
|
||||
enable: false,
|
||||
post_wordcount: true,
|
||||
min2read: true,
|
||||
total_wordcount: true
|
||||
},
|
||||
busuanzi: {
|
||||
site_uv: true,
|
||||
site_pv: true,
|
||||
page_pv: true
|
||||
},
|
||||
math: {
|
||||
use: null,
|
||||
per_page: true,
|
||||
hide_scrollbar: false,
|
||||
mathjax: {
|
||||
enableMenu: true,
|
||||
tags: 'none'
|
||||
},
|
||||
katex: {
|
||||
copy_tex: false
|
||||
}
|
||||
},
|
||||
search: {
|
||||
use: null,
|
||||
placeholder: null,
|
||||
algolia_search: {
|
||||
hitsPerPage: 6
|
||||
},
|
||||
local_search: {
|
||||
preload: false,
|
||||
top_n_per_article: 1,
|
||||
unescape: false,
|
||||
pagination: {
|
||||
enable: false,
|
||||
hitsPerPage: 8
|
||||
},
|
||||
CDN: null
|
||||
},
|
||||
docsearch: {
|
||||
appId: null,
|
||||
apiKey: null,
|
||||
indexName: null,
|
||||
option: null
|
||||
}
|
||||
},
|
||||
share: {
|
||||
use: 'sharejs',
|
||||
sharejs: {
|
||||
sites: 'facebook,x,wechat,weibo,qq'
|
||||
},
|
||||
addtoany: {
|
||||
item: 'facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link'
|
||||
}
|
||||
},
|
||||
comments: {
|
||||
use: null,
|
||||
text: true,
|
||||
lazyload: false,
|
||||
count: false,
|
||||
card_post_count: false
|
||||
},
|
||||
disqus: {
|
||||
shortname: null,
|
||||
apikey: null
|
||||
},
|
||||
disqusjs: {
|
||||
shortname: null,
|
||||
apikey: null,
|
||||
option: null
|
||||
},
|
||||
livere: {
|
||||
uid: null
|
||||
},
|
||||
gitalk: {
|
||||
client_id: null,
|
||||
client_secret: null,
|
||||
repo: null,
|
||||
owner: null,
|
||||
admin: null,
|
||||
option: null
|
||||
},
|
||||
valine: {
|
||||
appId: null,
|
||||
appKey: null,
|
||||
avatar: 'monsterid',
|
||||
serverURLs: null,
|
||||
bg: null,
|
||||
visitor: false,
|
||||
option: null
|
||||
},
|
||||
waline: {
|
||||
serverURL: null,
|
||||
bg: null,
|
||||
pageview: false,
|
||||
option: null
|
||||
},
|
||||
utterances: {
|
||||
repo: null,
|
||||
issue_term: 'pathname',
|
||||
light_theme: 'github-light',
|
||||
dark_theme: 'photon-dark',
|
||||
js: null,
|
||||
option: null
|
||||
},
|
||||
facebook_comments: {
|
||||
app_id: null,
|
||||
user_id: null,
|
||||
pageSize: 10,
|
||||
order_by: 'social',
|
||||
lang: 'en_US'
|
||||
},
|
||||
twikoo: {
|
||||
envId: null,
|
||||
region: null,
|
||||
visitor: false,
|
||||
option: null
|
||||
},
|
||||
giscus: {
|
||||
repo: null,
|
||||
repo_id: null,
|
||||
category_id: null,
|
||||
light_theme: 'light',
|
||||
dark_theme: 'dark',
|
||||
js: null,
|
||||
option: null
|
||||
},
|
||||
remark42: {
|
||||
host: null,
|
||||
siteId: null,
|
||||
option: null
|
||||
},
|
||||
artalk: {
|
||||
server: null,
|
||||
site: null,
|
||||
visitor: false,
|
||||
option: null
|
||||
},
|
||||
chat: {
|
||||
use: null,
|
||||
rightside_button: false,
|
||||
button_hide_show: false
|
||||
},
|
||||
chatra: {
|
||||
id: null
|
||||
},
|
||||
tidio: {
|
||||
public_key: null
|
||||
},
|
||||
crisp: {
|
||||
website_id: null
|
||||
},
|
||||
knocket: {
|
||||
identifier: null
|
||||
},
|
||||
google_tag_manager: {
|
||||
tag_id: null,
|
||||
domain: 'https://www.googletagmanager.com'
|
||||
},
|
||||
baidu_analytics: null,
|
||||
google_analytics: null,
|
||||
cloudflare_analytics: null,
|
||||
microsoft_clarity: null,
|
||||
umami_analytics: {
|
||||
enable: false,
|
||||
serverURL: null,
|
||||
script_name: 'script.js',
|
||||
website_id: null,
|
||||
option: null,
|
||||
UV_PV: {
|
||||
site_uv: false,
|
||||
site_pv: false,
|
||||
page_pv: false,
|
||||
token: null
|
||||
}
|
||||
},
|
||||
google_adsense: {
|
||||
enable: false,
|
||||
auto_ads: true,
|
||||
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
|
||||
client: null,
|
||||
enable_page_level_ads: true
|
||||
},
|
||||
ad: {
|
||||
index: null,
|
||||
aside: null,
|
||||
post: null
|
||||
},
|
||||
site_verification: null,
|
||||
category_ui: null,
|
||||
tag_ui: null,
|
||||
rounded_corners_ui: true,
|
||||
text_align_justify: false,
|
||||
mask: {
|
||||
header: true,
|
||||
footer: true
|
||||
},
|
||||
preloader: {
|
||||
enable: false,
|
||||
source: 1,
|
||||
pace_css_url: null
|
||||
},
|
||||
enter_transitions: true,
|
||||
display_mode: 'light',
|
||||
beautify: {
|
||||
enable: false,
|
||||
field: 'post',
|
||||
title_prefix_icon: null,
|
||||
title_prefix_icon_color: null
|
||||
},
|
||||
font: {
|
||||
global_font_size: null,
|
||||
code_font_size: null,
|
||||
font_family: null,
|
||||
code_font_family: null
|
||||
},
|
||||
blog_title_font: {
|
||||
font_link: null,
|
||||
font_family: null
|
||||
},
|
||||
hr_icon: {
|
||||
enable: true,
|
||||
icon: null,
|
||||
icon_top: null
|
||||
},
|
||||
activate_power_mode: {
|
||||
enable: false,
|
||||
colorful: true,
|
||||
shake: true,
|
||||
mobile: false
|
||||
},
|
||||
canvas_ribbon: {
|
||||
enable: false,
|
||||
size: 150,
|
||||
alpha: 0.6,
|
||||
zIndex: -1,
|
||||
click_to_change: false,
|
||||
mobile: false
|
||||
},
|
||||
canvas_fluttering_ribbon: {
|
||||
enable: false,
|
||||
mobile: false
|
||||
},
|
||||
canvas_nest: {
|
||||
enable: false,
|
||||
color: '0,0,255',
|
||||
opacity: 0.7,
|
||||
zIndex: -1,
|
||||
count: 99,
|
||||
mobile: false
|
||||
},
|
||||
fireworks: {
|
||||
enable: false,
|
||||
zIndex: 9999,
|
||||
mobile: false
|
||||
},
|
||||
click_heart: {
|
||||
enable: false,
|
||||
mobile: false
|
||||
},
|
||||
clickShowText: {
|
||||
enable: false,
|
||||
text: null,
|
||||
fontSize: '15px',
|
||||
random: false,
|
||||
mobile: false
|
||||
},
|
||||
lightbox: null,
|
||||
series: {
|
||||
enable: false,
|
||||
orderBy: 'title',
|
||||
order: 1,
|
||||
number: true
|
||||
},
|
||||
abcjs: {
|
||||
enable: false,
|
||||
per_page: true
|
||||
},
|
||||
mermaid: {
|
||||
enable: false,
|
||||
code_write: false,
|
||||
theme: {
|
||||
light: 'default',
|
||||
dark: 'dark'
|
||||
},
|
||||
open_in_new_tab: true,
|
||||
zoom_pan: true
|
||||
},
|
||||
chartjs: {
|
||||
enable: false,
|
||||
fontColor: {
|
||||
light: 'rgba(0, 0, 0, 0.8)',
|
||||
dark: 'rgba(255, 255, 255, 0.8)'
|
||||
},
|
||||
borderColor: {
|
||||
light: 'rgba(0, 0, 0, 0.1)',
|
||||
dark: 'rgba(255, 255, 255, 0.2)'
|
||||
},
|
||||
scale_ticks_backdropColor: {
|
||||
light: 'transparent',
|
||||
dark: 'transparent'
|
||||
}
|
||||
},
|
||||
note: {
|
||||
style: 'flat',
|
||||
icons: true,
|
||||
border_radius: 3,
|
||||
light_bg_offset: 0
|
||||
},
|
||||
pjax: {
|
||||
enable: false,
|
||||
exclude: null
|
||||
},
|
||||
aplayerInject: {
|
||||
enable: false,
|
||||
per_page: true
|
||||
},
|
||||
snackbar: {
|
||||
enable: false,
|
||||
position: 'bottom-left',
|
||||
bg_light: '#49b1f5',
|
||||
bg_dark: '#1f1f1f'
|
||||
},
|
||||
instantpage: false,
|
||||
lazyload: {
|
||||
enable: false,
|
||||
native: false,
|
||||
field: 'site',
|
||||
placeholder: null,
|
||||
blur: false
|
||||
},
|
||||
pwa: {
|
||||
enable: false,
|
||||
manifest: null,
|
||||
apple_touch_icon: null,
|
||||
favicon_32_32: null,
|
||||
favicon_16_16: null,
|
||||
mask_icon: null
|
||||
},
|
||||
Open_Graph_meta: {
|
||||
enable: true,
|
||||
option: null
|
||||
},
|
||||
structured_data: {
|
||||
enable: false,
|
||||
alternate_name: null
|
||||
},
|
||||
css_prefix: true,
|
||||
inject: {
|
||||
head: null,
|
||||
bottom: null
|
||||
},
|
||||
CDN: {
|
||||
internal_provider: 'local',
|
||||
third_party_provider: 'jsdelivr',
|
||||
version: true,
|
||||
custom_format: null,
|
||||
option: null
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@
|
||||
const { stripHTML, truncate } = require('hexo-util')
|
||||
|
||||
// Truncates the given content to a specified length, removing HTML tags and replacing newlines with spaces.
|
||||
const truncateContent = (content, length) => {
|
||||
return truncate(stripHTML(content), { length, separator: ' ' }).replace(/\n/g, ' ')
|
||||
const truncateContent = (content, length, encrypt = false) => {
|
||||
if (!content || encrypt) return ''
|
||||
return truncate(stripHTML(content).replace(/\n/g, ' '), { length })
|
||||
}
|
||||
|
||||
// Generates a post description based on the provided data and theme configuration.
|
||||
const postDesc = (data, hexo) => {
|
||||
const { description, content, postDesc } = data
|
||||
const { description, content, postDesc, encrypt } = data
|
||||
|
||||
if (postDesc) return postDesc
|
||||
|
||||
@@ -23,10 +24,10 @@ const postDesc = (data, hexo) => {
|
||||
result = description
|
||||
break
|
||||
case 2:
|
||||
result = description || truncateContent(content, length)
|
||||
result = description || truncateContent(content, length, encrypt)
|
||||
break
|
||||
default:
|
||||
result = truncateContent(content, length)
|
||||
result = truncateContent(content, length, encrypt)
|
||||
}
|
||||
|
||||
data.postDesc = result
|
||||
|
||||
+18
-20
@@ -46,45 +46,43 @@ hexo.extend.filter.register('before_generate', () => {
|
||||
}
|
||||
|
||||
const createCDNLink = (data, type, cond = '') => {
|
||||
Object.keys(data).forEach(key => {
|
||||
let { name, version, file, other_name } = data[key]
|
||||
const cdnjs_name = other_name || name
|
||||
const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
||||
const min_cdnjs_file = minFile(cdnjs_file)
|
||||
return Object.keys(data).reduce((result, key) => {
|
||||
let { name, version, file, other_name: otherName } = data[key]
|
||||
const cdnjsName = otherName || name
|
||||
const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
||||
const minCdnjsFile = minFile(cdnjsFile)
|
||||
if (cond === 'internal') file = `source/${file}`
|
||||
const min_file = minFile(file)
|
||||
const minFilePath = minFile(file)
|
||||
const verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
|
||||
|
||||
const value = {
|
||||
version,
|
||||
name,
|
||||
file,
|
||||
cdnjs_file,
|
||||
min_file,
|
||||
min_cdnjs_file,
|
||||
cdnjs_name
|
||||
cdnjs_file: cdnjsFile,
|
||||
min_file: minFilePath,
|
||||
min_cdnjs_file: minCdnjsFile,
|
||||
cdnjs_name: cdnjsName
|
||||
}
|
||||
|
||||
const cdnSource = {
|
||||
local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
||||
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
|
||||
local: cond === 'internal' ? `${cdnjsFile + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
||||
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${minFilePath}`,
|
||||
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
|
||||
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjs_name}/${version}/${min_cdnjs_file}`,
|
||||
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjsName}/${version}/${minCdnjsFile}`,
|
||||
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
|
||||
}
|
||||
|
||||
data[key] = cdnSource[type]
|
||||
})
|
||||
|
||||
if (cond === 'internal') data.main_css = 'css/index.css' + (CDN.version ? `?v=${version}` : '')
|
||||
return data
|
||||
result[key] = cdnSource[type]
|
||||
return result
|
||||
}, cond === 'internal' ? { main_css: 'css/index.css' + (CDN.version ? `?v=${version}` : '') } : {})
|
||||
}
|
||||
|
||||
// delete null value
|
||||
const deleteNullValue = obj => {
|
||||
if (!obj) return
|
||||
if (!obj) return {}
|
||||
for (const i in obj) {
|
||||
obj[i] === null && delete obj[i]
|
||||
if (obj[i] === null) delete obj[i]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
+74
-8
@@ -1,20 +1,86 @@
|
||||
hexo.extend.filter.register('before_generate', () => {
|
||||
// Get first two digits of the Hexo version number
|
||||
const { version, log, locals } = hexo
|
||||
const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
|
||||
const { deepMerge } = require('hexo-util')
|
||||
const path = require('path')
|
||||
|
||||
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('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
||||
process.exit(-1)
|
||||
throw new Error('Hexo version too old')
|
||||
}
|
||||
|
||||
// Check for deprecated configuration file
|
||||
if (locals.get) {
|
||||
const data = locals.get('data')
|
||||
if (data && data.butterfly) {
|
||||
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
||||
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
||||
process.exit(-1)
|
||||
throw new Error('Deprecated configuration file')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load default configuration
|
||||
*/
|
||||
function loadDefaultConfig () {
|
||||
if (cachedDefaultConfig) {
|
||||
return cachedDefaultConfig
|
||||
}
|
||||
|
||||
const configPath = path.join(__dirname, '../common/default_config.js')
|
||||
cachedDefaultConfig = require(configPath)
|
||||
return cachedDefaultConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Process comment system configuration
|
||||
*/
|
||||
function processCommentConfig (themeConfig) {
|
||||
const { comments } = themeConfig
|
||||
if (!comments || !comments.use) {
|
||||
return
|
||||
}
|
||||
|
||||
let { use } = comments
|
||||
|
||||
if (!Array.isArray(use)) {
|
||||
use = typeof use === 'string' ? use.split(',') : [use]
|
||||
}
|
||||
|
||||
use = use
|
||||
.map(item => {
|
||||
if (typeof item !== 'string') return item
|
||||
return item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
// Handle Disqus and Disqusjs conflict
|
||||
if (use.includes('Disqus') && use.includes('Disqusjs')) {
|
||||
hexo.log.warn('Disqus and Disqusjs conflict detected, keeping only the first one')
|
||||
hexo.log.warn('檢測到 Disqus 和 Disqusjs 衝突,只保留第一個')
|
||||
use = [use[0]]
|
||||
}
|
||||
|
||||
themeConfig.comments.use = use
|
||||
}
|
||||
|
||||
hexo.extend.filter.register('before_generate', () => {
|
||||
checkHexoEnvironment(hexo)
|
||||
|
||||
const defaultConfig = loadDefaultConfig()
|
||||
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
|
||||
|
||||
processCommentConfig(hexo.theme.config)
|
||||
}, 1)
|
||||
|
||||
@@ -1,583 +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,
|
||||
lazyload: {
|
||||
enable: false,
|
||||
native: false,
|
||||
field: 'site',
|
||||
placeholder: null,
|
||||
blur: false
|
||||
},
|
||||
pwa: {
|
||||
enable: false,
|
||||
manifest: null,
|
||||
apple_touch_icon: null,
|
||||
favicon_32_32: null,
|
||||
favicon_16_16: null,
|
||||
mask_icon: null
|
||||
},
|
||||
Open_Graph_meta: {
|
||||
enable: true,
|
||||
option: null
|
||||
},
|
||||
structured_data: true,
|
||||
css_prefix: true,
|
||||
inject: {
|
||||
head: null,
|
||||
bottom: null
|
||||
},
|
||||
CDN: {
|
||||
internal_provider: 'local',
|
||||
third_party_provider: 'jsdelivr',
|
||||
version: false,
|
||||
custom_format: null,
|
||||
option: null
|
||||
}
|
||||
}
|
||||
|
||||
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
|
||||
}, 1)
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Butterfly
|
||||
* lazyload
|
||||
* replace src to data-lazy-src
|
||||
* Lazyload filter
|
||||
* Replace src with data-lazy-src for lazy loading
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
@@ -10,11 +10,20 @@ const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
|
||||
const lazyload = htmlContent => {
|
||||
if (hexo.theme.config.lazyload.native) {
|
||||
return htmlContent.replace(/(<img.*?)(>)/ig, '$1 loading=\'lazy\'$2')
|
||||
// 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'
|
||||
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 => {
|
||||
|
||||
@@ -5,78 +5,105 @@
|
||||
'use strict'
|
||||
|
||||
hexo.extend.generator.register('post', locals => {
|
||||
const previousIndexes = []
|
||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||
const remoteImgReg = /^(?:https?:)?\/\//i
|
||||
const dataImgReg = /^data:image\//i
|
||||
|
||||
const getRandomCover = defaultCover => {
|
||||
if (!defaultCover) return false
|
||||
if (!Array.isArray(defaultCover)) return defaultCover
|
||||
const { post_asset_folder: postAssetFolder } = hexo.config
|
||||
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
||||
|
||||
const isImage = value => {
|
||||
return typeof value === 'string' &&
|
||||
(remoteImgReg.test(value) || dataImgReg.test(value) || imgTestReg.test(value))
|
||||
}
|
||||
|
||||
function * createCoverGenerator () {
|
||||
if (!defaultCover || (Array.isArray(defaultCover) && defaultCover.length === 0)) {
|
||||
while (true) yield false
|
||||
}
|
||||
|
||||
if (!Array.isArray(defaultCover)) {
|
||||
while (true) yield defaultCover
|
||||
}
|
||||
|
||||
if (defaultCover.length === 1) {
|
||||
while (true) yield defaultCover[0]
|
||||
}
|
||||
|
||||
const coverCount = defaultCover.length
|
||||
const maxHistory = Math.min(3, coverCount - 1)
|
||||
const history = []
|
||||
|
||||
if (coverCount === 1) {
|
||||
return defaultCover[0]
|
||||
while (true) {
|
||||
let index
|
||||
|
||||
do {
|
||||
index = Math.floor(Math.random() * coverCount)
|
||||
} while (history.includes(index))
|
||||
|
||||
history.push(index)
|
||||
|
||||
if (history.length > maxHistory) {
|
||||
history.shift()
|
||||
}
|
||||
|
||||
yield defaultCover[index]
|
||||
}
|
||||
}
|
||||
|
||||
const coverGenerator = createCoverGenerator()
|
||||
|
||||
const resolvePostAsset = (value, postPath) => {
|
||||
if (
|
||||
!postAssetFolder ||
|
||||
typeof value !== 'string' ||
|
||||
value.includes('/') ||
|
||||
!imgTestReg.test(value)
|
||||
) {
|
||||
return value
|
||||
}
|
||||
|
||||
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]
|
||||
return `${postPath}${value}`
|
||||
}
|
||||
|
||||
const handleImg = data => {
|
||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||
let { cover: coverVal, top_img: topImg } = data
|
||||
data.top_img = resolvePostAsset(data.top_img, data.path)
|
||||
data.cover = resolvePostAsset(data.cover, data.path)
|
||||
data.pagination_cover = resolvePostAsset(data.pagination_cover, data.path)
|
||||
|
||||
// Add path to top_img and cover if post_asset_folder is enabled
|
||||
if (hexo.config.post_asset_folder) {
|
||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
|
||||
data.top_img = `${data.path}${topImg}`
|
||||
}
|
||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
|
||||
data.cover = `${data.path}${coverVal}`
|
||||
}
|
||||
if (data.cover === false) return data
|
||||
|
||||
if (!data.cover) {
|
||||
data.cover = coverGenerator.next().value
|
||||
}
|
||||
|
||||
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))) {
|
||||
if (isImage(data.cover)) {
|
||||
data.cover_type = 'img'
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
|
||||
const posts = locals.posts.sort('date').toArray()
|
||||
const { length } = posts
|
||||
|
||||
return posts.map((post, i) => {
|
||||
if (i) post.prev = posts[i - 1]
|
||||
if (i < length - 1) post.next = posts[i + 1]
|
||||
return posts.map((post, index) => {
|
||||
const data = post
|
||||
|
||||
post.__post = true
|
||||
if (index > 0) {
|
||||
data.prev = posts[index - 1]
|
||||
}
|
||||
|
||||
if (index < length - 1) {
|
||||
data.next = posts[index + 1]
|
||||
}
|
||||
|
||||
data.__post = true
|
||||
|
||||
return {
|
||||
data: handleImg(post),
|
||||
data: handleImg(data),
|
||||
layout: 'post',
|
||||
path: post.path
|
||||
path: data.path
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
const { config, page, site, url_for, _p } = this
|
||||
const {
|
||||
archive_dir: archiveDir,
|
||||
timezone,
|
||||
language
|
||||
} = config
|
||||
const { config, page, site, url_for: urlFor, _p } = this
|
||||
const { archive_dir: archiveDir, timezone, language } = config
|
||||
|
||||
// Destructure and set default options with object destructuring
|
||||
const {
|
||||
@@ -21,46 +17,43 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
// Optimize locale handling
|
||||
const lang = toMomentLocale(page.lang || page.language || language)
|
||||
|
||||
// Memoize comparison function to improve performance
|
||||
const compareFunc = type === 'monthly'
|
||||
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
|
||||
: (yearA, yearB) => yearA === yearB
|
||||
|
||||
// Early return if no posts
|
||||
if (!site.posts.length) return ''
|
||||
|
||||
// Use reduce for more efficient data processing
|
||||
const data = site.posts
|
||||
.sort('date', order)
|
||||
.reduce((acc, post) => {
|
||||
let date = post.date.clone()
|
||||
if (timezone) date = date.tz(timezone)
|
||||
const archives = new Map()
|
||||
site.posts.forEach(post => {
|
||||
const date = post.date
|
||||
const year = date.year()
|
||||
const month = date.month() + 1
|
||||
const key = type === 'yearly' ? year : `${year}-${month}`
|
||||
|
||||
const year = date.year()
|
||||
const month = date.month() + 1
|
||||
|
||||
if (lang) date = date.locale(lang)
|
||||
|
||||
// Find or create archive entry
|
||||
const lastEntry = acc[acc.length - 1]
|
||||
if (!lastEntry || !compareFunc(
|
||||
lastEntry.year,
|
||||
lastEntry.month,
|
||||
if (archives.has(key)) {
|
||||
archives.get(key).count++
|
||||
} else {
|
||||
archives.set(key, {
|
||||
year,
|
||||
month
|
||||
)) {
|
||||
acc.push({
|
||||
name: date.format(format),
|
||||
year,
|
||||
month,
|
||||
count: 1
|
||||
})
|
||||
} else {
|
||||
lastEntry.count++
|
||||
}
|
||||
month,
|
||||
count: 1,
|
||||
date // Store date object for later formatting
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
const data = Array.from(archives.values()).sort((a, b) => {
|
||||
if (order === -1) {
|
||||
return b.year - a.year || b.month - a.month
|
||||
}
|
||||
return a.year - b.year || a.month - b.month
|
||||
})
|
||||
|
||||
// Format names after aggregation
|
||||
data.forEach(item => {
|
||||
let date = item.date.clone()
|
||||
if (timezone) date = date.tz(timezone)
|
||||
if (lang) date = date.locale(lang)
|
||||
item.name = date.format(format)
|
||||
delete item.date // Clean up
|
||||
})
|
||||
|
||||
// Create link generator function
|
||||
const createArchiveLink = item => {
|
||||
@@ -68,43 +61,45 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
if (type === 'monthly') {
|
||||
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
||||
}
|
||||
return url_for(url)
|
||||
return urlFor(url)
|
||||
}
|
||||
|
||||
// Limit results efficiently
|
||||
const limitedData = limit > 0
|
||||
? data.slice(0, Math.min(data.length, limit))
|
||||
: data
|
||||
const limitedData = limit > 0 ? data.slice(0, Math.min(data.length, limit)) : data
|
||||
|
||||
// Use template literal for better readability
|
||||
const archiveHeader = `
|
||||
<div class="item-headline">
|
||||
<i class="fas fa-archive"></i>
|
||||
<span>${_p('aside.card_archives')}</span>
|
||||
${data.length > limitedData.length
|
||||
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
|
||||
${
|
||||
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>
|
||||
`
|
||||
|
||||
// Use map for generating list items, join for performance
|
||||
const archiveList = `
|
||||
<ul class="card-archive-list">
|
||||
${limitedData.map(item => `
|
||||
${limitedData
|
||||
.map(
|
||||
item => `
|
||||
<li class="card-archive-list-item">
|
||||
<a class="card-archive-list-link" href="${createArchiveLink(item)}">
|
||||
<span class="card-archive-list-date">
|
||||
${transform ? transform(item.name) : item.name}
|
||||
</span>
|
||||
${showCount
|
||||
? `<span class="card-archive-list-count">${item.count}</span>`
|
||||
: ''}
|
||||
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''}
|
||||
</a>
|
||||
</li>
|
||||
`).join('')}
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</ul>
|
||||
`
|
||||
|
||||
|
||||
@@ -19,15 +19,33 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
||||
const expandClass = isExpand && options.expand === true ? 'expand' : ''
|
||||
const buttonLabel = this._p('aside.more_button')
|
||||
|
||||
const prepareQuery = parent => {
|
||||
const query = parent ? { parent } : { parent: { $exists: false } }
|
||||
return categories.find(query).sort(orderby, order).filter(cat => cat.length)
|
||||
const categoryMap = new Map()
|
||||
categories.forEach(cat => {
|
||||
if (cat.length) {
|
||||
const parentId = cat.parent || 'root'
|
||||
if (!categoryMap.has(parentId)) {
|
||||
categoryMap.set(parentId, [])
|
||||
}
|
||||
categoryMap.get(parentId).push(cat)
|
||||
}
|
||||
})
|
||||
|
||||
const sortFn = (a, b) => {
|
||||
const valA = a[orderby]
|
||||
const valB = b[orderby]
|
||||
if (valA < valB) return -order
|
||||
if (valA > valB) return order
|
||||
return 0
|
||||
}
|
||||
|
||||
const hierarchicalList = (remaining, level = 0, parent) => {
|
||||
for (const list of categoryMap.values()) {
|
||||
list.sort(sortFn)
|
||||
}
|
||||
|
||||
const hierarchicalList = (remaining, level = 0, parentId = 'root') => {
|
||||
let result = ''
|
||||
if (remaining > 0) {
|
||||
prepareQuery(parent).forEach(cat => {
|
||||
if (remaining > 0 && categoryMap.has(parentId)) {
|
||||
categoryMap.get(parentId).forEach(cat => {
|
||||
if (remaining > 0) {
|
||||
remaining -= 1
|
||||
let child = ''
|
||||
@@ -37,7 +55,8 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
||||
remaining = childList.remaining
|
||||
}
|
||||
|
||||
const parentClass = isExpand && !parent && child ? 'parent' : ''
|
||||
const isTopLevel = parentId === 'root'
|
||||
const parentClass = isExpand && isTopLevel && child ? 'parent' : ''
|
||||
result += `<li class="card-category-list-item ${parentClass}">`
|
||||
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
|
||||
result += `<span class="card-category-list-name">${cat.name}</span>`
|
||||
@@ -46,7 +65,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
||||
result += `<span class="card-category-list-count">${cat.length}</span>`
|
||||
}
|
||||
|
||||
if (isExpand && !parent && child) {
|
||||
if (isExpand && isTopLevel && child) {
|
||||
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,36 +8,36 @@ hexo.extend.helper.register('getArchiveLength', function () {
|
||||
// Archives Page
|
||||
if (!year) return posts.length
|
||||
|
||||
// Function to generate a unique key based on the granularity
|
||||
const getKey = (post, type) => {
|
||||
const date = post.date.clone()
|
||||
const y = date.year()
|
||||
const m = date.month() + 1
|
||||
const d = date.date()
|
||||
if (type === 'year') return `${y}`
|
||||
if (type === 'month') return `${y}-${m}`
|
||||
if (type === 'day') return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
// Create a map to count posts per period
|
||||
const mapData = this.fragment_cache('createArchiveObj', () => {
|
||||
const map = new Map()
|
||||
posts.forEach(post => {
|
||||
const keyYear = getKey(post, 'year')
|
||||
const keyMonth = getKey(post, 'month')
|
||||
const keyDay = getKey(post, 'day')
|
||||
const date = post.date
|
||||
const y = date.year()
|
||||
const m = date.month() + 1
|
||||
const d = date.date()
|
||||
|
||||
if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
||||
if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
||||
if (daily) map.set(keyDay, (map.get(keyDay) || 0) + 1)
|
||||
// Always track year so year archive pages can be counted
|
||||
const keyYear = `${y}`
|
||||
map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
||||
|
||||
if (monthly || daily) {
|
||||
const keyMonth = `${y}-${m}`
|
||||
map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
||||
}
|
||||
|
||||
if (daily) {
|
||||
const keyDay = `${y}-${m}-${d}`
|
||||
map.set(keyDay, (map.get(keyDay) || 0) + 1)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
// Determine the appropriate key to fetch based on current page context
|
||||
let key
|
||||
if (yearly && year) key = `${year}`
|
||||
if (monthly && month) key = `${year}-${month}`
|
||||
if (yearly || monthly || daily) key = `${year}`
|
||||
if ((monthly || daily) && month) key = `${year}-${month}`
|
||||
if (daily && day) key = `${year}-${month}-${day}`
|
||||
|
||||
// Return the count for the current period or default to the total posts
|
||||
|
||||
@@ -4,53 +4,94 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
const { darkmode, aside, pjax } = this.theme
|
||||
const start = darkmode.start || 6
|
||||
const end = darkmode.end || 18
|
||||
const { theme_color } = hexo.theme.config
|
||||
const themeColorLight = theme_color && theme_color.enable ? theme_color.meta_theme_color_light : '#ffffff'
|
||||
const themeColorDark = theme_color && theme_color.enable ? theme_color.meta_theme_color_dark : '#0d0d0d'
|
||||
const { theme_color: themeColor } = hexo.theme.config
|
||||
const themeColorLight = themeColor && themeColor.enable ? themeColor.meta_theme_color_light : '#ffffff'
|
||||
const themeColorDark = themeColor && themeColor.enable ? themeColor.meta_theme_color_dark : '#0d0d0d'
|
||||
|
||||
const createCustomJs = () => `
|
||||
const saveToLocal = {
|
||||
set: (key, value, ttl) => {
|
||||
if (!ttl) return
|
||||
const expiry = Date.now() + ttl * 86400000
|
||||
localStorage.setItem(key, JSON.stringify({ value, expiry }))
|
||||
const data = { value }
|
||||
|
||||
if (ttl != null) {
|
||||
data.expiry = Date.now() + ttl * 86400000
|
||||
}
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
},
|
||||
get: key => {
|
||||
const itemStr = localStorage.getItem(key)
|
||||
if (!itemStr) return undefined
|
||||
const { value, expiry } = JSON.parse(itemStr)
|
||||
if (Date.now() > expiry) {
|
||||
if (!itemStr) return
|
||||
|
||||
try {
|
||||
const data = JSON.parse(itemStr)
|
||||
|
||||
if (data.expiry && Date.now() > data.expiry) {
|
||||
localStorage.removeItem(key)
|
||||
return
|
||||
}
|
||||
|
||||
return data.value
|
||||
} catch {
|
||||
localStorage.removeItem(key)
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const scriptCache = new Map()
|
||||
const cssCache = new Map()
|
||||
window.btf = {
|
||||
saveToLocal,
|
||||
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = url
|
||||
script.async = true
|
||||
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
|
||||
script.onload = script.onreadystatechange = () => {
|
||||
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
|
||||
getScript: (url, attr = {}) => {
|
||||
if (scriptCache.has(url)) {
|
||||
return scriptCache.get(url)
|
||||
}
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
}),
|
||||
getCSS: (url, id) => new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = url
|
||||
if (id) link.id = id
|
||||
link.onload = link.onreadystatechange = () => {
|
||||
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.src = url
|
||||
script.async = true
|
||||
|
||||
for (const key in attr) {
|
||||
script.setAttribute(key, attr[key])
|
||||
}
|
||||
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
scriptCache.set(url, promise)
|
||||
|
||||
return promise
|
||||
},
|
||||
getCSS: (url, id) => {
|
||||
if (cssCache.has(url)) {
|
||||
return cssCache.get(url)
|
||||
}
|
||||
link.onerror = reject
|
||||
document.head.appendChild(link)
|
||||
}),
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.href = url
|
||||
|
||||
if (id) {
|
||||
link.id = id
|
||||
}
|
||||
|
||||
link.onload = resolve
|
||||
link.onerror = reject
|
||||
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
|
||||
cssCache.set(url, promise)
|
||||
|
||||
return promise
|
||||
},
|
||||
addGlobalFn: (key, fn, name = false, parent = window) => {
|
||||
if (!${pjax.enable} && key.startsWith('pjax')) return
|
||||
const globalFn = parent.globalFn || {}
|
||||
@@ -65,16 +106,17 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
if (!darkmode.enable) return ''
|
||||
|
||||
let darkmodeJs = `
|
||||
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
||||
const activateDarkMode = () => {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}')
|
||||
document.documentElement.dataset.theme = 'dark'
|
||||
if (metaThemeColor !== null) {
|
||||
metaThemeColor.setAttribute('content', '${themeColorDark}')
|
||||
}
|
||||
}
|
||||
const activateLightMode = () => {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}')
|
||||
document.documentElement.dataset.theme = 'light'
|
||||
if (metaThemeColor !== null) {
|
||||
metaThemeColor.setAttribute('content', '${themeColorLight}')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +137,15 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
else if (mediaQueryDark.matches) activateDarkMode()
|
||||
else {
|
||||
const hour = new Date().getHours()
|
||||
const isNight = hour <= ${start} || hour >= ${end}
|
||||
const start = ${start}
|
||||
const end = ${end}
|
||||
const isNight =
|
||||
start < end
|
||||
? hour < start || hour >= end
|
||||
: hour >= start || hour < end
|
||||
isNight ? activateDarkMode() : activateLightMode()
|
||||
}
|
||||
mediaQueryDark.addEventListener('change', () => {
|
||||
mediaQueryDark.addEventListener('change', e => {
|
||||
if (saveToLocal.get('theme') === undefined) {
|
||||
e.matches ? activateDarkMode() : activateLightMode()
|
||||
}
|
||||
@@ -111,7 +158,12 @@ hexo.extend.helper.register('inject_head_js', function () {
|
||||
case 2:
|
||||
darkmodeJs += `
|
||||
const hour = new Date().getHours()
|
||||
const isNight = hour <= ${start} || hour >= ${end}
|
||||
const start = ${start}
|
||||
const end = ${end}
|
||||
const isNight =
|
||||
start < end
|
||||
? hour < start || hour >= end
|
||||
: hour >= start || hour < end
|
||||
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
|
||||
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
||||
`
|
||||
|
||||
+87
-47
@@ -5,6 +5,14 @@ const { prettyUrls } = require('hexo-util')
|
||||
const crypto = require('crypto')
|
||||
const moment = require('moment-timezone')
|
||||
|
||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
||||
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
|
||||
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
||||
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
|
||||
const archiveRegex = /\/archives\//
|
||||
|
||||
const { version: themeVersion } = require('../../package.json')
|
||||
|
||||
hexo.extend.helper.register('truncate', truncateContent)
|
||||
|
||||
hexo.extend.helper.register('postDesc', data => {
|
||||
@@ -13,34 +21,64 @@ hexo.extend.helper.register('postDesc', data => {
|
||||
|
||||
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||
const env = this
|
||||
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order } = options
|
||||
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
|
||||
|
||||
if (limit > 0) {
|
||||
source = source.limit(limit)
|
||||
}
|
||||
|
||||
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
|
||||
const sizeMap = new Map(sizes.map((size, index) => [size, index]))
|
||||
const length = sizes.length - 1
|
||||
|
||||
const getRandomColor = () => {
|
||||
const randomColor = () => Math.floor(Math.random() * 201)
|
||||
const r = randomColor()
|
||||
const g = randomColor()
|
||||
const b = randomColor()
|
||||
const r = Math.floor(Math.random() * 201)
|
||||
const g = Math.floor(Math.random() * 201)
|
||||
const b = Math.floor(Math.random() * 201)
|
||||
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
||||
}
|
||||
|
||||
const generateStyle = (size, unit) =>
|
||||
`font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
|
||||
const normalizeColors = input => {
|
||||
if (!input) return null
|
||||
if (typeof input === 'string') {
|
||||
const color = input.trim()
|
||||
return color ? [color] : null
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
const result = []
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const value = input[i]
|
||||
if (value === null || value === undefined) continue
|
||||
const color = String(value).trim()
|
||||
if (!color) continue
|
||||
result.push(color)
|
||||
}
|
||||
return result.length ? result : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const length = sizes.length - 1
|
||||
const result = source.sort(orderby, order).map(tag => {
|
||||
const ratio = length ? sizes.indexOf(tag.length) / length : 0
|
||||
const userColors = normalizeColors(custom_colors)
|
||||
|
||||
const generateStyle = (size, unit, page, color) => {
|
||||
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
|
||||
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
|
||||
}
|
||||
|
||||
return source.sort(orderby, order).map((tag, idx) => {
|
||||
const ratio = length ? sizeMap.get(tag.length) / length : 0
|
||||
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
||||
const style = generateStyle(size, unit)
|
||||
|
||||
if (userColors && userColors.length) {
|
||||
const color = userColors[idx % userColors.length]
|
||||
const style = generateStyle(size, unit, page, color)
|
||||
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item" style="${style}">${tag.name}</a>`
|
||||
}
|
||||
|
||||
const color = getRandomColor()
|
||||
const style = generateStyle(size, unit, page, color)
|
||||
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
||||
}).join('')
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
|
||||
@@ -48,7 +86,7 @@ hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex =
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('md5', function (path) {
|
||||
return crypto.createHash('md5').update(decodeURI(this.url_for(path))).digest('hex')
|
||||
return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex')
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('injectHtml', data => {
|
||||
@@ -65,14 +103,14 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
||||
const defaultTitle = this._p('page.archives')
|
||||
if (!menu) return defaultTitle
|
||||
|
||||
const loop = (m) => {
|
||||
for (const key in m) {
|
||||
if (typeof m[key] === 'object') {
|
||||
const result = loop(m[key])
|
||||
const loop = m => {
|
||||
for (const [key, value] of Object.entries(m)) {
|
||||
if (value && typeof value === 'object') {
|
||||
const result = loop(value)
|
||||
if (result) return result
|
||||
}
|
||||
|
||||
if (/\/archives\//.test(m[key])) {
|
||||
if (typeof value === 'string' && archiveRegex.test(value)) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
@@ -81,17 +119,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
||||
return loop(menu) || defaultTitle
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('getBgPath', path => {
|
||||
hexo.extend.helper.register('getBgPath', function (path) {
|
||||
if (!path) return ''
|
||||
|
||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
||||
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
|
||||
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
||||
|
||||
if (colorPattern.test(path)) {
|
||||
return `background-color: ${path};`
|
||||
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) {
|
||||
return `background-image: url(${path});`
|
||||
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path) || simpleFilePattern.test(path)) {
|
||||
return `background-image: url(${this.url_for(path)});`
|
||||
} else {
|
||||
return `background: ${path};`
|
||||
}
|
||||
@@ -99,37 +133,35 @@ hexo.extend.helper.register('getBgPath', path => {
|
||||
|
||||
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
||||
const { limit } = page
|
||||
let finalResult = ''
|
||||
|
||||
// Shallow copy to avoid mutating original data
|
||||
let processedData = data.map(item => ({ ...item }))
|
||||
|
||||
// Check if limit.value is a valid date
|
||||
const isValidDate = date => !isNaN(Date.parse(date))
|
||||
|
||||
// order by date
|
||||
const orderByDate = data => data.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
|
||||
processedData.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
|
||||
|
||||
// Apply number limit or time limit conditionally
|
||||
const limitData = data => {
|
||||
if (limit && limit.type === 'num' && limit.value > 0) {
|
||||
return data.slice(0, limit.value)
|
||||
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
||||
const limitDate = Date.parse(limit.value)
|
||||
return data.filter(item => Date.parse(item.date) >= limitDate)
|
||||
}
|
||||
|
||||
return data
|
||||
if (limit && limit.type === 'num' && limit.value > 0) {
|
||||
processedData = processedData.slice(0, limit.value)
|
||||
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
||||
const limitDate = Date.parse(limit.value)
|
||||
processedData = processedData.filter(item => Date.parse(item.date) >= limitDate)
|
||||
}
|
||||
|
||||
orderByDate(data)
|
||||
finalResult = limitData(data)
|
||||
|
||||
// This is a hack method, because hexo treats time as UTC time
|
||||
// so you need to manually convert the time zone
|
||||
finalResult.forEach(item => {
|
||||
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
|
||||
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
|
||||
const timezone = hexo.config.timezone
|
||||
processedData.forEach(item => {
|
||||
const parsed = moment.utc(item.date)
|
||||
item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), timezone).format('YYYY-MM-DD HH:mm:ss')
|
||||
// markdown
|
||||
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||
})
|
||||
|
||||
return finalResult
|
||||
return processedData
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||
@@ -139,7 +171,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||
if (category) return 'category'
|
||||
if (archive) return 'archive'
|
||||
if (type) {
|
||||
if (type === 'tags' || type === 'categories') return type
|
||||
if (type === 'tags' || type === 'categories' || type === '404') return type
|
||||
else return 'page'
|
||||
}
|
||||
if (isHome) return 'home'
|
||||
@@ -147,6 +179,14 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||
})
|
||||
|
||||
hexo.extend.helper.register('getVersion', () => {
|
||||
const { version } = require('../../package.json')
|
||||
return { hexo: hexo.version, theme: version }
|
||||
return { hexo: hexo.version, theme: themeVersion }
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
@@ -9,14 +9,21 @@
|
||||
|
||||
const { postDesc } = require('../common/postDesc')
|
||||
|
||||
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
||||
let relatedPosts = []
|
||||
hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||
const relatedPosts = new Map()
|
||||
const tagsData = currentPost.tags
|
||||
tagsData.length && tagsData.forEach(function (tag) {
|
||||
allPosts.forEach(function (post) {
|
||||
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
|
||||
const getPostDesc = post.postDesc || postDesc(post, hexo)
|
||||
const relatedPost = {
|
||||
|
||||
if (!tagsData || !tagsData.length) return ''
|
||||
|
||||
tagsData.forEach(tag => {
|
||||
const posts = tag.posts
|
||||
posts.forEach(post => {
|
||||
if (currentPost.path === post.path) return
|
||||
|
||||
if (relatedPosts.has(post.path)) {
|
||||
relatedPosts.get(post.path).weight += 1
|
||||
} else {
|
||||
relatedPosts.set(post.path, {
|
||||
title: post.title,
|
||||
path: post.path,
|
||||
cover: post.cover,
|
||||
@@ -24,22 +31,17 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
||||
weight: 1,
|
||||
updated: post.updated,
|
||||
created: post.date,
|
||||
postDesc: getPostDesc
|
||||
}
|
||||
const index = findItem(relatedPosts, 'path', post.path)
|
||||
if (index !== -1) {
|
||||
relatedPosts[index].weight += 1
|
||||
} else {
|
||||
relatedPosts.push(relatedPost)
|
||||
}
|
||||
post,
|
||||
random: Math.random()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (relatedPosts.length === 0) {
|
||||
if (relatedPosts.size === 0) {
|
||||
return ''
|
||||
}
|
||||
let result = ''
|
||||
|
||||
const hexoConfig = hexo.config
|
||||
const config = hexo.theme.config
|
||||
|
||||
@@ -47,51 +49,45 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
||||
const dateType = config.related_post.date_type || 'created'
|
||||
const headlineLang = this._p('post.recommend')
|
||||
|
||||
relatedPosts = relatedPosts.sort(compare('weight'))
|
||||
|
||||
if (relatedPosts.length > 0) {
|
||||
result += '<div class="relatedPosts">'
|
||||
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
||||
result += '<div class="relatedPosts-list">'
|
||||
|
||||
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
|
||||
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i]
|
||||
const { escape_html, url_for, date } = this
|
||||
cover = cover || 'var(--default-bg-color)'
|
||||
title = escape_html(title)
|
||||
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
|
||||
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
||||
if (cover_type === 'img') {
|
||||
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
||||
} else {
|
||||
result += `<div class="cover" style="background: ${cover}"></div>`
|
||||
}
|
||||
if (dateType === 'created') {
|
||||
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>`
|
||||
} else {
|
||||
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>`
|
||||
}
|
||||
result += `<div class="info-item-2">${title}</div></div>`
|
||||
|
||||
if (postDesc) {
|
||||
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
|
||||
}
|
||||
result += '</div></a>'
|
||||
const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => {
|
||||
if (b.weight !== a.weight) {
|
||||
return b.weight - a.weight
|
||||
}
|
||||
return b.random - a.random
|
||||
})
|
||||
|
||||
result += '</div></div>'
|
||||
return result
|
||||
let result = '<div class="relatedPosts">'
|
||||
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
||||
result += '<div class="relatedPosts-list">'
|
||||
|
||||
const max = Math.min(relatedPostsList.length, limitNum)
|
||||
for (let i = 0; i < max; i++) {
|
||||
const item = relatedPostsList[i]
|
||||
let { cover, title, path, cover_type, created, updated, post } = item
|
||||
const { escape_html, url_for, date } = this
|
||||
cover = cover || 'var(--default-bg-color)'
|
||||
title = escape_html(title)
|
||||
const desc = post.postDesc || postDesc(post, hexo)
|
||||
const className = desc ? 'pagination-related' : 'pagination-related no-desc'
|
||||
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
||||
if (cover_type === 'img') {
|
||||
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
||||
} else {
|
||||
result += `<div class="cover" style="background: ${cover}"></div>`
|
||||
}
|
||||
if (dateType === 'created') {
|
||||
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>`
|
||||
} else {
|
||||
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>`
|
||||
}
|
||||
result += `<div class="info-item-2">${title}</div></div>`
|
||||
|
||||
if (desc) {
|
||||
result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
|
||||
}
|
||||
result += '</div></a>'
|
||||
}
|
||||
|
||||
result += '</div></div>'
|
||||
return result
|
||||
})
|
||||
|
||||
function isTagRelated (tagName, tags) {
|
||||
return tags.some(tag => tag.name === tagName)
|
||||
}
|
||||
|
||||
function findItem (arrayToSearch, attr, val) {
|
||||
return arrayToSearch.findIndex(item => item[attr] === val)
|
||||
}
|
||||
|
||||
function compare (attr) {
|
||||
return (a, b) => b[attr] - a[attr]
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const { url_for, escapeHTML } = require('hexo-util')
|
||||
const urlFor = url_for.bind(hexo)
|
||||
|
||||
const btn = args => {
|
||||
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
||||
|
||||
const iconHTML = icon ? `<i class="${icon}"></i>` : ''
|
||||
const textHTML = text ? `<span>${text}</span>` : ''
|
||||
const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
|
||||
const textHTML = text ? `<span>${escapeHTML(text)}</span>` : ''
|
||||
|
||||
return `<a class="btn-beautify ${option}" href="${urlFor(url)}" title="${text}">${iconHTML}${textHTML}</a>`
|
||||
return `<a class="btn-beautify ${escapeHTML(option)}" href="${urlFor(url)}" title="${escapeHTML(text)}">${iconHTML}${textHTML}</a>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('btn', btn, { ends: false })
|
||||
|
||||
@@ -4,24 +4,25 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const { url_for, escapeHTML } = require('hexo-util')
|
||||
const urlFor = url_for.bind(hexo)
|
||||
|
||||
const flinkFn = (args, content) => {
|
||||
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
||||
let result = ''
|
||||
|
||||
data.forEach(item => {
|
||||
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : ''
|
||||
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : ''
|
||||
const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
|
||||
const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
|
||||
|
||||
const listResult = item.link_list.map(link => `
|
||||
<div class="flink-list-item">
|
||||
<a href="${link.link}" title="${link.name}" target="_blank">
|
||||
<a href="${escapeHTML(link.link)}" title="${escapeHTML(link.name)}" target="_blank">
|
||||
<div class="flink-item-icon">
|
||||
<img class="no-lightbox" src="${link.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${link.name}" />
|
||||
<img class="no-lightbox" src="${escapeHTML(link.avatar)}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${escapeHTML(link.name)}" />
|
||||
</div>
|
||||
<div class="flink-item-name">${link.name}</div>
|
||||
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div>
|
||||
<div class="flink-item-name">${escapeHTML(link.name)}</div>
|
||||
<div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
|
||||
</a>
|
||||
</div>`).join('')
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
|
||||
const parseImageContent = content => {
|
||||
const images = []
|
||||
let match
|
||||
IMAGE_REGEX.lastIndex = 0
|
||||
|
||||
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
||||
images.push({
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const parseArgs = args => args.join(' ').split(',')
|
||||
const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
|
||||
|
||||
const generateStyle = (bg, color) => {
|
||||
let style = 'style="'
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
const { escapeHTML } = require('hexo-util')
|
||||
|
||||
const mermaid = (args, content) => {
|
||||
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
|
||||
const config = args[0] || '{}'
|
||||
return `<div class="mermaid-wrap"><pre class="mermaid-src" data-config="${escapeHTML(config)}" hidden>
|
||||
${escapeHTML(content)}
|
||||
</pre></div>`
|
||||
}
|
||||
|
||||
+5
-15
@@ -5,20 +5,10 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const { escapeHTML } = require('hexo-util')
|
||||
|
||||
const score = (args, content) => {
|
||||
// Escape HTML tags and some special characters, including curly braces
|
||||
const escapeHtmlTags = s => {
|
||||
const lookup = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'{': '{',
|
||||
'}': '}'
|
||||
}
|
||||
return s.replace(/[&"'<>{}]/g, c => lookup[c])
|
||||
}
|
||||
const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '{' : '}')
|
||||
|
||||
const trimmed = content.trim()
|
||||
// Split content using six dashes as a delimiter
|
||||
@@ -37,7 +27,7 @@ const score = (args, content) => {
|
||||
try {
|
||||
paramsObj = JSON.parse(paramPart)
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON in score tag:", e)
|
||||
console.error('Failed to parse JSON in score tag:', e)
|
||||
}
|
||||
|
||||
// Use double quotes for data-params attribute value,
|
||||
@@ -47,4 +37,4 @@ const score = (args, content) => {
|
||||
</div>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register("score", score, { ends: true })
|
||||
hexo.extend.tag.register('score', score, { ends: true })
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
const groups = {}
|
||||
|
||||
hexo.extend.filter.register('before_generate', () => {
|
||||
Object.keys(groups).forEach(k => delete groups[k])
|
||||
})
|
||||
|
||||
hexo.extend.filter.register('before_post_render', data => {
|
||||
if (!hexo.theme.config.series.enable) return data
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.fontawesomeIcon
|
||||
display: inline-block
|
||||
font-weight: 600
|
||||
font-family: 'Font Awesome 6 Free'
|
||||
font-family: 'Font Awesome 7 Free', 'Font Awesome 6 Free'
|
||||
text-rendering: auto
|
||||
-webkit-font-smoothing: antialiased
|
||||
|
||||
@@ -159,7 +159,7 @@ if hexo-config('enter_transitions')
|
||||
animation: titleScale 1s
|
||||
|
||||
canvas:not(#ribbon-canvas),
|
||||
#web_bg
|
||||
#web_bg.bg-animation
|
||||
animation: to_show 4s
|
||||
|
||||
#ribbon-canvas
|
||||
@@ -181,6 +181,60 @@ if hexo-config('avatar.effect') == true
|
||||
.reward-main
|
||||
animation: donate_effcet .3s .1s ease both
|
||||
|
||||
.btn-effects
|
||||
position: relative
|
||||
overflow: hidden
|
||||
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||
transform: translateZ(0)
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .15)
|
||||
text-decoration: none
|
||||
transform: translateY(-1px) scale(1.02)
|
||||
|
||||
&:active
|
||||
transition-duration: .1s
|
||||
transform: translateY(0) scale(.98)
|
||||
|
||||
i
|
||||
display: inline-block
|
||||
vertical-align: middle
|
||||
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&:hover i
|
||||
animation: buttonIconBounce .6s ease-in-out
|
||||
|
||||
i + span
|
||||
margin-left: 6px
|
||||
vertical-align: middle
|
||||
transition: margin-left .3s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&:hover i + span
|
||||
margin-left: 8px
|
||||
|
||||
&::before
|
||||
position: absolute
|
||||
top: 0
|
||||
left: -100%
|
||||
z-index: 1
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .2), transparent)
|
||||
content: ''
|
||||
transition: left .5s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&:hover::before
|
||||
left: 100%
|
||||
|
||||
& > *
|
||||
position: relative
|
||||
z-index: 2
|
||||
|
||||
.btn-effects-large
|
||||
&:hover
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .2)
|
||||
transform: translateY(-2px) scale(1.03)
|
||||
|
||||
@keyframes scroll-down-effect
|
||||
0%
|
||||
opacity: .4
|
||||
@@ -278,3 +332,17 @@ if hexo-config('avatar.effect') == true
|
||||
|
||||
100%
|
||||
transform: translateX(0)
|
||||
|
||||
@keyframes buttonIconBounce
|
||||
0%,
|
||||
100%
|
||||
transform: translateY(0) scale(1)
|
||||
|
||||
25%
|
||||
transform: translateY(-3px) scale(1.1) rotateZ(-5deg)
|
||||
|
||||
50%
|
||||
transform: translateY(0) scale(1.05) rotateZ(0)
|
||||
|
||||
75%
|
||||
transform: translateY(-1px) scale(1.02) rotateZ(2deg)
|
||||
@@ -39,6 +39,65 @@
|
||||
--zoom-bg: #fff
|
||||
--mark-bg: alpha($dark-black, .3)
|
||||
|
||||
// tags plugin
|
||||
:root
|
||||
--btn-color: $btn-color
|
||||
--btn-default-color: $btn-default-color
|
||||
--tags-blue-color: $tagsP-blue-color
|
||||
--tags-blue-color-lighten: lighten($tagsP-blue-color, 85%)
|
||||
--tags-pink-color: $tagsP-pink-color
|
||||
--tags-pink-color-lighten: lighten($tagsP-pink-color, 85%)
|
||||
--tags-red-color: $tagsP-red-color
|
||||
--tags-red-color-lighten: lighten($tagsP-red-color, 85%)
|
||||
--tags-orange-color: $tagsP-orange-color
|
||||
--tags-orange-color-lighten: lighten($tagsP-orange-color, 85%)
|
||||
--tags-purple-color: $tagsP-purple-color
|
||||
--tags-purple-color-lighten: lighten($tagsP-purple-color, 85%)
|
||||
--tags-green-color: $tagsP-green-color
|
||||
--tags-green-color-lighten: lighten($tagsP-green-color, 85%)
|
||||
--note-default-border: $note-default-border
|
||||
--note-default-bg: $note-default-bg
|
||||
--note-default-text: $note-default-text
|
||||
--note-modern-default-border: $note-modern-default-border
|
||||
--note-modern-default-bg: $note-modern-default-bg
|
||||
--note-modern-default-text: $note-modern-default-text
|
||||
--note-modern-default-hover: $note-modern-default-hover
|
||||
--note-primary-border: $note-primary-border
|
||||
--note-primary-bg: $note-primary-bg
|
||||
--note-primary-text: $note-primary-text
|
||||
--note-modern-primary-border: $note-modern-primary-border
|
||||
--note-modern-primary-bg: $note-modern-primary-bg
|
||||
--note-modern-primary-text: $note-modern-primary-text
|
||||
--note-modern-primary-hover: $note-modern-primary-hover
|
||||
--note-info-border: $note-info-border
|
||||
--note-info-bg: $note-info-bg
|
||||
--note-info-text: $note-info-text
|
||||
--note-modern-info-border: $note-modern-info-border
|
||||
--note-modern-info-bg: $note-modern-info-bg
|
||||
--note-modern-info-text: $note-modern-info-text
|
||||
--note-modern-info-hover: $note-modern-info-hover
|
||||
--note-success-border: $note-success-border
|
||||
--note-success-bg: $note-success-bg
|
||||
--note-success-text: $note-success-text
|
||||
--note-modern-success-border: $note-modern-success-border
|
||||
--note-modern-success-bg: $note-modern-success-bg
|
||||
--note-modern-success-text: $note-modern-success-text
|
||||
--note-modern-success-hover: $note-modern-success-hover
|
||||
--note-warning-border: $note-warning-border
|
||||
--note-warning-bg: $note-warning-bg
|
||||
--note-warning-text: $note-warning-text
|
||||
--note-modern-warning-border: $note-modern-warning-border
|
||||
--note-modern-warning-bg: $note-modern-warning-bg
|
||||
--note-modern-warning-text: $note-modern-warning-text
|
||||
--note-modern-warning-hover: $note-modern-warning-hover
|
||||
--note-danger-border: $note-danger-border
|
||||
--note-danger-bg: $note-danger-bg
|
||||
--note-danger-text: $note-danger-text
|
||||
--note-modern-danger-border: $note-modern-danger-border
|
||||
--note-modern-danger-bg: $note-modern-danger-bg
|
||||
--note-modern-danger-text: $note-modern-danger-text
|
||||
--note-modern-danger-hover: $note-modern-danger-hover
|
||||
|
||||
body
|
||||
position: relative
|
||||
overflow-y: scroll
|
||||
@@ -222,3 +281,7 @@ blockquote
|
||||
|
||||
& > :last-child
|
||||
margin-bottom: 0 !important
|
||||
|
||||
.fa-fw
|
||||
width: 1.25em
|
||||
text-align: center
|
||||
@@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo
|
||||
--hlscrollbar-bg: lighten(#121212, 5)
|
||||
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
||||
|
||||
$scrollbar-style
|
||||
// scrollbar - firefox
|
||||
@-moz-document url-prefix()
|
||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: var(--hlscrollbar-bg)
|
||||
|
||||
if $highlight_enable
|
||||
@require 'highlight/index'
|
||||
|
||||
@@ -88,6 +96,11 @@ $code-block
|
||||
&:hover
|
||||
border-bottom-color: var(--hl-color)
|
||||
|
||||
&.default
|
||||
pre
|
||||
padding: 10px 20px
|
||||
@extend $scrollbar-style
|
||||
|
||||
&.copy-true
|
||||
user-select: all
|
||||
-webkit-user-select: all
|
||||
@@ -100,18 +113,23 @@ $code-block
|
||||
.highlight-tools
|
||||
display: flex
|
||||
align-items: center
|
||||
overflow: hidden
|
||||
padding: 0 8px
|
||||
min-height: 24px
|
||||
height: 2.15em
|
||||
background: var(--hltools-bg)
|
||||
color: var(--hltools-color)
|
||||
font-size: $code-font-size
|
||||
overflow: hidden
|
||||
|
||||
& > *
|
||||
padding: 5px
|
||||
flex: 0 0 auto
|
||||
margin: 2px
|
||||
|
||||
i
|
||||
display: inline-flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
padding: 5px
|
||||
cursor: pointer
|
||||
transition: all .3s
|
||||
|
||||
@@ -127,31 +145,28 @@ $code-block
|
||||
|
||||
if !$highlight_macstyle
|
||||
& > .macStyle
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
.code-lang
|
||||
flex: 1
|
||||
flex: 1 1 auto
|
||||
overflow: hidden
|
||||
padding-right: 10px
|
||||
text-transform: uppercase
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
font-weight: bold
|
||||
font-size: 1.15em
|
||||
user-select: none
|
||||
-webkit-user-select: none
|
||||
padding 2px
|
||||
|
||||
.copy-notice
|
||||
padding-right: 2px
|
||||
opacity: 0
|
||||
transition: opacity .4s
|
||||
|
||||
if hexo-config('code_blocks.language')
|
||||
.code-lang
|
||||
flex: 1
|
||||
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none')
|
||||
& > div:nth-child(2)
|
||||
flex: 1
|
||||
margin-right: auto
|
||||
else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
|
||||
& > :nth-child(2)
|
||||
margin-right: auto
|
||||
else
|
||||
.macStyle
|
||||
flex: 1
|
||||
& > :nth-child(1)
|
||||
margin-right: auto
|
||||
|
||||
.gutter
|
||||
user-select: none
|
||||
@@ -163,17 +178,28 @@ $code-block
|
||||
td
|
||||
border: none
|
||||
|
||||
.copy-notice
|
||||
position: absolute
|
||||
z-index: 99999
|
||||
padding: 2px 6px
|
||||
border-radius: 3px
|
||||
background: var(--hltools-bg)
|
||||
white-space: nowrap
|
||||
font-size: 12px
|
||||
pointer-events: none
|
||||
|
||||
if $highlight_macstyle
|
||||
.container
|
||||
figure.highlight
|
||||
margin: 0 0 24px
|
||||
border-radius: 7px
|
||||
border-radius: 8px
|
||||
box-shadow: 0 5px 10px 0 $highlight-mac-border
|
||||
-webkit-transform: translateZ(0)
|
||||
|
||||
.highlight-tools
|
||||
.macStyle
|
||||
display: flex
|
||||
padding: 3px
|
||||
|
||||
& > *
|
||||
margin-right: 8px
|
||||
@@ -264,8 +290,8 @@ if hexo-config('code_blocks.fullpage')
|
||||
& ~ table
|
||||
display: block
|
||||
overflow: auto
|
||||
height: calc(100vh - 2.15em)
|
||||
margin-bottom: 0
|
||||
height: calc(100vh - 2.15em)
|
||||
|
||||
@keyframes code-fullpage
|
||||
0%,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
figure.highlight
|
||||
table
|
||||
// scrollbar - firefox
|
||||
@-moz-document url-prefix()
|
||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: var(--hlscrollbar-bg)
|
||||
@extend $scrollbar-style
|
||||
|
||||
pre .deletion
|
||||
color: $highlight-deletion
|
||||
|
||||
@@ -6,12 +6,7 @@ if $highlight_theme != false
|
||||
|
||||
.container
|
||||
pre[class*='language-']
|
||||
// scrollbar - firefox
|
||||
@-moz-document url-prefix()
|
||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: var(--hlscrollbar-bg)
|
||||
@extend $scrollbar-style
|
||||
|
||||
&:not(.line-numbers)
|
||||
padding: 10px 20px
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
&:not(#card-toc)
|
||||
display: none
|
||||
|
||||
&:last-child
|
||||
margin-bottom: 0
|
||||
// &:last-child
|
||||
// margin-bottom: 0
|
||||
|
||||
.card-info
|
||||
.author-info
|
||||
@@ -60,6 +60,7 @@
|
||||
text-align: center
|
||||
line-height: 2.4
|
||||
addBorderRadius(7)
|
||||
@extend .btn-effects
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-hover-color)
|
||||
@@ -180,6 +181,11 @@
|
||||
.card-category-list
|
||||
&.child
|
||||
padding: 0 0 0 16px
|
||||
overflow: hidden
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
visibility: hidden
|
||||
transition: max-height .3s ease, opacity .3s ease
|
||||
|
||||
> .parent
|
||||
> a
|
||||
@@ -188,7 +194,9 @@
|
||||
transform: rotate(-90deg)
|
||||
|
||||
& + .child
|
||||
display: block
|
||||
max-height: 1000px
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
|
||||
.card-category-list
|
||||
&-name
|
||||
@@ -207,7 +215,9 @@
|
||||
|
||||
if hexo-config('aside.card_categories.expand') == false
|
||||
> .child
|
||||
display: none
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
visibility: hidden
|
||||
|
||||
.card-webinfo
|
||||
.webinfo
|
||||
|
||||
@@ -13,17 +13,16 @@
|
||||
background-color: var(--mark-bg)
|
||||
content: ''
|
||||
|
||||
#footer-wrap
|
||||
position: relative
|
||||
padding: 40px 20px
|
||||
color: var(--light-grey)
|
||||
text-align: center
|
||||
& > *
|
||||
position: relative
|
||||
color: var(--light-grey)
|
||||
|
||||
a
|
||||
color: var(--light-grey)
|
||||
transition: all .3s ease-in-out
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
color: $light-blue
|
||||
|
||||
.footer-separator
|
||||
margin: 0 4px
|
||||
@@ -33,3 +32,56 @@
|
||||
max-height: 1.4em
|
||||
width: auto
|
||||
vertical-align: text-bottom
|
||||
|
||||
.footer-flex
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
justify-content: space-between
|
||||
margin: 0 auto
|
||||
padding: 40px 60px
|
||||
max-width: 1200px
|
||||
width: 100%
|
||||
text-align: left
|
||||
gap: 13px
|
||||
|
||||
+maxWidth768()
|
||||
padding: 30px
|
||||
gap: 10px
|
||||
|
||||
.footer-flex-items
|
||||
flex-shrink: 0
|
||||
min-width: 100px
|
||||
text-align: left
|
||||
white-space: nowrap
|
||||
|
||||
.footer-flex-title
|
||||
margin-bottom: 5px
|
||||
white-space: nowrap
|
||||
font-weight: 600
|
||||
font-size: 1.4em
|
||||
|
||||
.footer-flex-item
|
||||
margin: 10px 0
|
||||
white-space: nowrap
|
||||
|
||||
a
|
||||
display: block
|
||||
white-space: nowrap
|
||||
|
||||
.footer-other
|
||||
padding: 40px 20px
|
||||
width: 100%
|
||||
text-align: center
|
||||
|
||||
if hexo-config('footer.nav')
|
||||
padding: 10px 8px
|
||||
background-color: rgba(0, 0, 0, .1)
|
||||
|
||||
.copyright,
|
||||
.framework-info,
|
||||
.footer_custom_text
|
||||
font-size: .9em
|
||||
else
|
||||
.framework-info
|
||||
display: block
|
||||
@@ -37,6 +37,8 @@
|
||||
margin: 0
|
||||
color: var(--white)
|
||||
font-size: 1.85em
|
||||
@extend .limit-more-line
|
||||
-webkit-line-clamp: 3
|
||||
|
||||
+minWidth768()
|
||||
font-size: 2.85em
|
||||
@@ -436,3 +438,28 @@
|
||||
&:hover
|
||||
&:after
|
||||
width: 100%
|
||||
|
||||
.nav-page-title
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
& > :first-child,
|
||||
& > :last-child
|
||||
display: inline-block
|
||||
transition: all .3s ease-in-out
|
||||
|
||||
& > :last-child
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 0
|
||||
opacity: 0
|
||||
transform: translateY(-50%) translateY(-10px)
|
||||
|
||||
&:hover
|
||||
& > :last-child
|
||||
opacity: 1
|
||||
transform: translateY(-50%) translateY(0)
|
||||
|
||||
& > :first-child
|
||||
opacity: 0
|
||||
transform: translateY(10px)
|
||||
@@ -39,7 +39,7 @@
|
||||
.pagination
|
||||
& > *
|
||||
display: inline-block
|
||||
margin: 0 6px
|
||||
margin: 6px
|
||||
width: w = 2.5em
|
||||
height: w
|
||||
line-height: w
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
color: var(--btn-color)
|
||||
cursor: pointer
|
||||
addBorderRadius()
|
||||
@extend .btn-effects
|
||||
@extend .btn-effects-large
|
||||
|
||||
i
|
||||
margin-right: 5px
|
||||
vertical-align: baseline
|
||||
|
||||
&:hover
|
||||
.reward-button
|
||||
@@ -28,19 +31,19 @@
|
||||
|
||||
.reward-main
|
||||
position: absolute
|
||||
bottom: 40px
|
||||
bottom: 50px
|
||||
left: 0
|
||||
z-index: 100
|
||||
display: none
|
||||
padding: 0 0 15px
|
||||
width: 100%
|
||||
addBorderRadius()
|
||||
|
||||
.reward-all
|
||||
display: inline-block
|
||||
margin: 0
|
||||
padding: 20px 10px
|
||||
background: var(--reward-pop)
|
||||
addBorderRadius()
|
||||
|
||||
&:before
|
||||
position: absolute
|
||||
|
||||
@@ -38,10 +38,14 @@
|
||||
font-size: 16px
|
||||
line-height: w
|
||||
addBorderRadius(5)
|
||||
@extend .btn-effects
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-hover-color)
|
||||
|
||||
i
|
||||
vertical-align: baseline
|
||||
|
||||
#mobile-toc-button
|
||||
display: none
|
||||
|
||||
@@ -54,19 +58,52 @@
|
||||
|
||||
if hexo-config('rightside_scroll_percent')
|
||||
#go-up
|
||||
position: relative
|
||||
|
||||
.scroll-percent
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
display: none
|
||||
width: 100%
|
||||
height: 100%
|
||||
opacity: 0
|
||||
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||
transform: scale(.8)
|
||||
|
||||
i
|
||||
position: relative
|
||||
z-index: 1
|
||||
width: 100%
|
||||
opacity: 1
|
||||
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||
transform: scale(1)
|
||||
|
||||
&.show-percent
|
||||
.scroll-percent
|
||||
display: block
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
animation: fadeInScale .4s ease-out
|
||||
|
||||
& + i
|
||||
display: none
|
||||
i
|
||||
opacity: 0
|
||||
transform: scale(.8)
|
||||
|
||||
&:hover
|
||||
&:hover.show-percent
|
||||
.scroll-percent
|
||||
display: none
|
||||
opacity: 0
|
||||
transform: scale(.8)
|
||||
|
||||
& + i
|
||||
display: block
|
||||
i
|
||||
opacity: 1 !important
|
||||
transform: scale(1) !important
|
||||
|
||||
@keyframes fadeInScale
|
||||
from
|
||||
opacity: 0
|
||||
transform: scale(.8)
|
||||
|
||||
to
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
@@ -52,7 +52,10 @@
|
||||
|
||||
&:hover
|
||||
background: var(--text-bg-hover)
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
|
||||
color: var(--white)
|
||||
transition: all .2s ease
|
||||
transform: translateX(3px)
|
||||
|
||||
i:first-child
|
||||
width: 15%
|
||||
@@ -70,9 +73,25 @@
|
||||
transform: rotate(90deg)
|
||||
|
||||
& + .menus_item_child
|
||||
display: none
|
||||
overflow: hidden
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
transform: scaleY(0)
|
||||
transform-origin: top
|
||||
|
||||
.menus_item_child
|
||||
margin: 0
|
||||
padding-left: 25px
|
||||
max-height: 0
|
||||
list-style: none
|
||||
opacity: 0
|
||||
transition: transform .3s ease, opacity .3s ease, max-height .3s ease
|
||||
transform: scaleY(0)
|
||||
transform-origin: top
|
||||
will-change: transform, opacity, max-height
|
||||
|
||||
// 當父元素沒有 .hide 類時,顯示子目錄
|
||||
.site-page.group:not(.hide) + .menus_item_child
|
||||
max-height: 1000px
|
||||
opacity: 1
|
||||
transform: scaleY(1)
|
||||
@@ -65,12 +65,55 @@ if hexo-config('waline.bg')
|
||||
|
||||
if hexo-config('mermaid.enable')
|
||||
.mermaid-wrap
|
||||
position: relative
|
||||
margin: 0 0 20px
|
||||
background: var(--card-bg)
|
||||
text-align: center
|
||||
|
||||
if hexo-config('mermaid.open_in_new_tab')
|
||||
.mermaid-open-btn
|
||||
position: absolute
|
||||
top: 8px
|
||||
right: 8px
|
||||
z-index: 2
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
padding: 0
|
||||
width: 34px
|
||||
height: 25px
|
||||
border: none
|
||||
border-radius: 20%
|
||||
background: #D3D3D3
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .15)
|
||||
color: #fff
|
||||
font-size: 0
|
||||
line-height: 1
|
||||
cursor: pointer
|
||||
transition: background .2s ease, transform .2s ease
|
||||
|
||||
i
|
||||
font-size: 16px
|
||||
line-height: 1
|
||||
|
||||
&:hover,
|
||||
&:focus-visible
|
||||
outline: none
|
||||
background: #C0C0C0
|
||||
transform: translateY(-1px)
|
||||
|
||||
& > svg
|
||||
max-width: 100%
|
||||
height: 100%
|
||||
|
||||
if hexo-config('mermaid.zoom_pan')
|
||||
cursor: grab
|
||||
user-select: none
|
||||
touch-action: none
|
||||
|
||||
&:active
|
||||
cursor: grabbing
|
||||
|
||||
if hexo-config('mermaid.code_write')
|
||||
pre > code.mermaid
|
||||
display: none
|
||||
@@ -183,5 +226,6 @@ if hexo-config('math.use')
|
||||
opacity: 1
|
||||
|
||||
+maxWidth768()
|
||||
.fancybox__toolbar__column.is-middle
|
||||
display: none
|
||||
.fancybox__toolbar__column.is-middle,
|
||||
.f-carousel__toolbar__column.is-middle
|
||||
visibility: hidden
|
||||
|
||||
@@ -33,6 +33,56 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
||||
--timeline-bg: lighten(#121212, 5)
|
||||
--zoom-bg: #121212
|
||||
--mark-bg: alpha($dark-black, .6)
|
||||
--btn-color: darken($btn-color, 20%)
|
||||
--btn-default-color: lighten($btn-default-color, 20%)
|
||||
--tags-blue-color: desaturate(darken($tagsP-blue-color, 20%), 25%)
|
||||
--tags-blue-color-lighten: rgba(66, 139, 202, .15)
|
||||
--tags-pink-color: desaturate(darken($tagsP-pink-color, 22%), 30%)
|
||||
--tags-pink-color-lighten: rgba(255, 105, 180, .15)
|
||||
--tags-red-color: desaturate(darken($tagsP-red-color, 25%), 28%)
|
||||
--tags-red-color-lighten: rgba(255, 0, 0, .15)
|
||||
--tags-orange-color: desaturate(darken($tagsP-orange-color, 22%), 32%)
|
||||
--tags-orange-color-lighten: rgba(255, 140, 0, .15)
|
||||
--tags-purple-color: desaturate(darken($tagsP-purple-color, 18%), 30%)
|
||||
--tags-purple-color-lighten: rgba(111, 66, 193, .15)
|
||||
--tags-green-color: desaturate(darken($tagsP-green-color, 20%), 28%)
|
||||
--tags-green-color-lighten: rgba(92, 184, 92, .15)
|
||||
--note-default-border: $note-dark-default-border
|
||||
--note-default-bg: $note-dark-default-bg
|
||||
--note-default-text: $note-dark-default-text
|
||||
--note-modern-default-border: $note-dark-modern-default-border
|
||||
--note-modern-default-bg: $note-dark-modern-default-bg
|
||||
--note-modern-default-text: $note-dark-modern-default-text
|
||||
--note-primary-border: $note-dark-primary-border
|
||||
--note-primary-bg: $note-dark-primary-bg
|
||||
--note-primary-text: $note-dark-primary-text
|
||||
--note-modern-primary-border: $note-dark-modern-primary-border
|
||||
--note-modern-primary-bg: $note-dark-modern-primary-bg
|
||||
--note-modern-primary-text: $note-dark-modern-primary-text
|
||||
--note-info-border: $note-dark-info-border
|
||||
--note-info-bg: $note-dark-info-bg
|
||||
--note-info-text: $note-dark-info-text
|
||||
--note-modern-info-border: $note-dark-modern-info-border
|
||||
--note-modern-info-bg: $note-dark-modern-info-bg
|
||||
--note-modern-info-text: $note-dark-modern-info-text
|
||||
--note-success-border: $note-dark-success-border
|
||||
--note-success-bg: $note-dark-success-bg
|
||||
--note-success-text: $note-dark-success-text
|
||||
--note-modern-success-border: $note-dark-modern-success-border
|
||||
--note-modern-success-bg: $note-dark-modern-success-bg
|
||||
--note-modern-success-text: $note-dark-modern-success-text
|
||||
--note-warning-border: $note-dark-warning-border
|
||||
--note-warning-bg: $note-dark-warning-bg
|
||||
--note-warning-text: $note-dark-warning-text
|
||||
--note-modern-warning-border: $note-dark-modern-warning-border
|
||||
--note-modern-warning-bg: $note-dark-modern-warning-bg
|
||||
--note-modern-warning-text: $note-dark-modern-warning-text
|
||||
--note-danger-border: $note-dark-danger-border
|
||||
--note-danger-bg: $note-dark-danger-bg
|
||||
--note-danger-text: $note-dark-danger-text
|
||||
--note-modern-danger-border: $note-dark-modern-danger-border
|
||||
--note-modern-danger-bg: $note-dark-modern-danger-bg
|
||||
--note-modern-danger-text: $note-dark-modern-danger-text
|
||||
|
||||
#web_bg:before
|
||||
position: absolute
|
||||
@@ -78,27 +128,22 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
||||
#switch-btn
|
||||
filter: brightness(.8)
|
||||
|
||||
// note
|
||||
if hexo-config('note.style') == 'modern' || hexo-config('note.style') == 'flat'
|
||||
.note
|
||||
filter: brightness(.8)
|
||||
|
||||
// hide-tags
|
||||
.hide-button,
|
||||
.btn-beautify,
|
||||
.hl-label,
|
||||
.toggle-button,
|
||||
#post-outdate-notice,
|
||||
.error-img,
|
||||
.container iframe,
|
||||
.gist,
|
||||
.ads-wrap
|
||||
.ads-wrap,
|
||||
.tag-cloud-list > a
|
||||
filter: brightness(.8)
|
||||
|
||||
img
|
||||
img:not(.cover)
|
||||
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
|
||||
filter: blur(0) brightness(.8)
|
||||
filter: blur(0) brightness(.88) contrast(.95)
|
||||
else
|
||||
filter: brightness(.8)
|
||||
filter: brightness(.88) contrast(.95)
|
||||
|
||||
#aside-content .aside-list > .aside-list-item:not(:last-child)
|
||||
border-bottom: 1px dashed alpha(#FFFFFF, .1)
|
||||
|
||||
@@ -42,6 +42,7 @@ if hexo-config('readmode')
|
||||
font-size: 16px
|
||||
transition: background .3s
|
||||
addBorderRadius(8)
|
||||
@extend .btn-effects
|
||||
|
||||
+maxWidth768()
|
||||
top: initial
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
padding: 20px 5px
|
||||
|
||||
+minWidth2000()
|
||||
max-width: 70%
|
||||
max-width: 60%
|
||||
|
||||
& > div:first-child:not(.nc)
|
||||
@extend .cardHover
|
||||
|
||||
@@ -172,3 +172,4 @@ $indexEnable = hexo-config('cover.index_enable')
|
||||
& > .content
|
||||
@extend .limit-more-line
|
||||
-webkit-line-clamp: 2
|
||||
word-break: break-word
|
||||
@@ -76,3 +76,110 @@
|
||||
|
||||
&.no-comment
|
||||
display: none
|
||||
|
||||
.shuoshuo-navigation
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
margin-top: 20px
|
||||
padding: 20px 0
|
||||
|
||||
button
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
width: 2.7em
|
||||
height: 2.7em
|
||||
background-color: var(--btn-bg)
|
||||
color: var(--btn-color)
|
||||
font-size: .9em
|
||||
line-height: 2.5em
|
||||
transition: all .2s ease-in-out
|
||||
addBorderRadius(6)
|
||||
|
||||
&:not(:disabled)
|
||||
@extend .btn-effects
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background-color: var(--btn-hover-color)
|
||||
|
||||
&:disabled
|
||||
background: #f5f5f5
|
||||
color: #ccc
|
||||
opacity: .5
|
||||
cursor: not-allowed
|
||||
|
||||
.shuoshuo-page-info
|
||||
margin: 0 15px
|
||||
color: #858585
|
||||
white-space: nowrap
|
||||
font-size: .9em
|
||||
|
||||
.shuoshuo-page-input
|
||||
margin-right: 12px
|
||||
padding: 0 15px
|
||||
height: 2.7em
|
||||
border: 1px solid var(--btn-bg)
|
||||
background: var(--card-bg)
|
||||
color: #858585
|
||||
text-align: center
|
||||
font-size: .9em
|
||||
transition: all .2s ease-in-out
|
||||
addBorderRadius(6)
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
border-width: 2px
|
||||
|
||||
&::placeholder
|
||||
color: transparent
|
||||
|
||||
/* 隱藏 number 輸入框的上下箭頭 */
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button
|
||||
margin: 0
|
||||
-webkit-appearance: none
|
||||
|
||||
/* Firefox */
|
||||
-moz-appearance: textfield
|
||||
|
||||
/* 當作為頁碼按鈕時的樣式 */
|
||||
&.shuoshuo-page-num
|
||||
min-width: 40px
|
||||
width: 40px
|
||||
border: none
|
||||
background: $light-blue
|
||||
color: var(--white)
|
||||
font-weight: 500
|
||||
cursor: text
|
||||
|
||||
&:focus
|
||||
border: 1px solid $light-blue
|
||||
background: var(--white)
|
||||
color: #333
|
||||
|
||||
/* 超出範圍時的紅色樣式 */
|
||||
&.invalid
|
||||
border-color: #ff4757
|
||||
background-color: #ffeaea
|
||||
color: #ff4757
|
||||
animation: shake .5s ease-in-out
|
||||
|
||||
/* 震動動畫 */
|
||||
@keyframes shake
|
||||
0%,
|
||||
100%
|
||||
transform: translateX(0)
|
||||
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90%
|
||||
transform: translateX(-2px)
|
||||
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80%
|
||||
transform: translateX(2px)
|
||||
|
||||
@@ -1,27 +1,91 @@
|
||||
.tag-cloud
|
||||
&-list
|
||||
animation: tagsFadeIn .6s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&:hover a:not(:hover)
|
||||
opacity: .7
|
||||
transform: scale(.98)
|
||||
|
||||
a
|
||||
position: relative
|
||||
display: inline-block
|
||||
margin: 2px
|
||||
padding: 2px 7px
|
||||
margin: 5px
|
||||
padding: 3px 12px
|
||||
line-height: 1.7
|
||||
transition: all .3s
|
||||
addBorderRadius(5)
|
||||
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||
addBorderRadius(7)
|
||||
overflow: hidden
|
||||
color: white
|
||||
transform: translateY(0) scale(1)
|
||||
will-change: transform, background-color, box-shadow
|
||||
|
||||
&::before
|
||||
position: absolute
|
||||
top: 0
|
||||
left: -100%
|
||||
z-index: -1
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent)
|
||||
content: ''
|
||||
transition: left .6s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&:hover
|
||||
background: var(--btn-bg) !important
|
||||
box-shadow: 2px 2px 6px rgba(0, 0, 0, .2)
|
||||
background: var(--btn-hover-color) !important
|
||||
box-shadow:
|
||||
0 6px 20px rgba(0, 0, 0, .12),
|
||||
0 4px 8px rgba(0, 0, 0, .08),
|
||||
0 0 0 1px rgba(255, 255, 255, .05)
|
||||
color: var(--btn-color) !important
|
||||
transform: translateY(-2px) scale(1.02)
|
||||
|
||||
&::before
|
||||
left: 100%
|
||||
|
||||
&:active
|
||||
box-shadow:
|
||||
0 3px 8px rgba(0, 0, 0, .15),
|
||||
0 1px 3px rgba(0, 0, 0, .1)
|
||||
transition: all .15s cubic-bezier(.4, 0, .2, 1)
|
||||
transform: translateY(-1px) scale(.98)
|
||||
|
||||
+maxWidth768()
|
||||
zoom: .85
|
||||
|
||||
&:hover
|
||||
transform: translateY(-1px) scale(1.01)
|
||||
|
||||
&:active
|
||||
transform: translateY(0) scale(.99)
|
||||
|
||||
&::before
|
||||
display: none
|
||||
|
||||
&-title
|
||||
font-size: 2.57em
|
||||
animation: titleSlideIn .8s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
+maxWidth768()
|
||||
font-size: 2em
|
||||
|
||||
@keyframes tagsFadeIn
|
||||
from
|
||||
opacity: 0
|
||||
transform: translateY(20px)
|
||||
|
||||
to
|
||||
opacity: 1
|
||||
transform: translateY(0)
|
||||
|
||||
@keyframes titleSlideIn
|
||||
from
|
||||
opacity: 0
|
||||
transform: translateX(-30px)
|
||||
|
||||
to
|
||||
opacity: 1
|
||||
transform: translateX(0)
|
||||
|
||||
.page-title
|
||||
& + .tag-cloud-list
|
||||
text-align: left
|
||||
|
||||
@@ -1,93 +1,13 @@
|
||||
#algolia-search
|
||||
.search-dialog
|
||||
.ais-SearchBox
|
||||
input
|
||||
padding: 5px 14px
|
||||
width: 100%
|
||||
outline: none
|
||||
border: 2px solid $search-color
|
||||
border-radius: 40px
|
||||
background: var(--search-bg)
|
||||
color: var(--search-input-color)
|
||||
|
||||
.ais-SearchBox-loadingIndicator
|
||||
position: absolute
|
||||
top: 18px
|
||||
left: 67px
|
||||
|
||||
.ais-Hits-list
|
||||
margin: 0
|
||||
padding: 0
|
||||
@extend .list-beauty
|
||||
|
||||
a
|
||||
color: var(--search-a-color)
|
||||
|
||||
&:hover
|
||||
color: $search-color
|
||||
|
||||
mark
|
||||
background: transparent
|
||||
color: $search-keyword-highlight
|
||||
font-weight: bold
|
||||
|
||||
.algolia-hits-item-title
|
||||
font-weight: 600
|
||||
|
||||
.algolia-hit-item-content
|
||||
margin: 0 0 8px
|
||||
word-break: break-word
|
||||
|
||||
.ais-Pagination
|
||||
margin: 15px 0 0
|
||||
padding: 0
|
||||
text-align: center
|
||||
|
||||
.ais-Pagination-list
|
||||
margin: 0
|
||||
padding: 0
|
||||
list-style: none
|
||||
|
||||
.ais-Pagination-item
|
||||
display: inline
|
||||
margin: 0 4px
|
||||
padding: 0
|
||||
|
||||
.ais-Pagination-link
|
||||
display: inline-block
|
||||
min-width: 24px
|
||||
height: 24px
|
||||
text-align: center
|
||||
line-height: 24px
|
||||
addBorderRadius()
|
||||
|
||||
.ais-Pagination-item--selected
|
||||
a
|
||||
background: $theme-paginator-color
|
||||
color: #eee
|
||||
cursor: default
|
||||
|
||||
.ais-Pagination-item--disabled
|
||||
visibility: hidden
|
||||
|
||||
#algolia-hits
|
||||
> div
|
||||
overflow-y: overlay
|
||||
margin: 0 -20px
|
||||
padding: 0 22px
|
||||
max-height: calc(80vh - 220px)
|
||||
|
||||
+maxWidth768()
|
||||
max-height: none
|
||||
height: calc(var(--search-height) - 235px)
|
||||
+maxWidth768()
|
||||
min-height: calc(var(--search-height) - 245px)
|
||||
|
||||
#algolia-info
|
||||
div
|
||||
display: inline
|
||||
|
||||
.algolia-poweredBy
|
||||
float: right
|
||||
vertical-align: text-top
|
||||
padding-top: 2px
|
||||
|
||||
svg
|
||||
height: 1.1em
|
||||
@@ -20,22 +20,174 @@
|
||||
border-radius: 0
|
||||
|
||||
.search-nav
|
||||
margin: 0 0 14px
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
margin-bottom: 14px
|
||||
color: $search-color
|
||||
font-size: 1.4em
|
||||
line-height: 1
|
||||
|
||||
.search-dialog-title
|
||||
margin-right: 10px
|
||||
margin-right: 4px
|
||||
|
||||
#loading-status
|
||||
&[hidden]
|
||||
display: none !important
|
||||
|
||||
.search-close-button
|
||||
float: right
|
||||
flex: 1
|
||||
color: $grey
|
||||
transition: color .2s ease-in-out
|
||||
text-align: right
|
||||
transition: all .2s ease
|
||||
|
||||
&:hover
|
||||
color: $search-color
|
||||
|
||||
.local-search-input,
|
||||
#algolia-search-input
|
||||
margin: 0 auto
|
||||
max-width: 100%
|
||||
width: 100%
|
||||
|
||||
input,
|
||||
.ais-SearchBox-input
|
||||
padding: 5px 14px
|
||||
width: 100%
|
||||
outline: none
|
||||
border: 2px solid $search-color
|
||||
border-radius: 40px
|
||||
background: var(--search-bg)
|
||||
color: var(--search-input-color)
|
||||
-webkit-appearance: none
|
||||
|
||||
&::placeholder
|
||||
color: var(--text-color)
|
||||
|
||||
.search-result-list,
|
||||
.ais-Hits-list
|
||||
overflow-y: overlay
|
||||
margin: 0 -20px
|
||||
padding: 0 22px
|
||||
max-height: calc(80vh - 220px)
|
||||
|
||||
.local-search-hit-item,
|
||||
.ais-Hits-item
|
||||
display: flex
|
||||
align-items: flex-start
|
||||
margin: 3px 0
|
||||
line-height: 1.8
|
||||
transition: all .2s ease-in-out
|
||||
|
||||
&:hover
|
||||
transform: translateY(-1px)
|
||||
|
||||
&:not([value])::before
|
||||
display: none
|
||||
|
||||
&[value]::before
|
||||
display: inline-flex
|
||||
flex-shrink: 0
|
||||
justify-content: center
|
||||
align-items: center
|
||||
margin-right: 6px
|
||||
margin-top: 3px
|
||||
min-width: 24px
|
||||
color: $search-color
|
||||
content: attr(value) '.'
|
||||
font-weight: bold
|
||||
font-style: italic
|
||||
font-size: .9em
|
||||
|
||||
&::marker
|
||||
content: none
|
||||
|
||||
a
|
||||
flex: 1
|
||||
color: var(--search-a-color)
|
||||
|
||||
&:hover
|
||||
color: $search-color
|
||||
|
||||
.search-result-title,
|
||||
.algolia-hits-item-title
|
||||
font-weight: 600
|
||||
|
||||
.search-result,
|
||||
.algolia-hit-item-content
|
||||
margin: 0 0 8px
|
||||
word-break: break-all
|
||||
font-size: .9em
|
||||
|
||||
.ais-Pagination
|
||||
margin: 15px 0 0
|
||||
padding: 0
|
||||
text-align: center
|
||||
|
||||
.ais-Pagination-list
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: center
|
||||
align-items: center
|
||||
margin: 0
|
||||
padding: 0
|
||||
list-style: none
|
||||
gap: 6px
|
||||
|
||||
.ais-Pagination-item
|
||||
display: flex
|
||||
padding: 0
|
||||
|
||||
&:not(.ais-Pagination-item--selected):not(.ais-Pagination-item--ellipsis):not(.ais-Pagination-item--disabled)
|
||||
.ais-Pagination-link:hover
|
||||
background: var(--btn-hover-color)
|
||||
transform: translateY(-1px)
|
||||
|
||||
.ais-Pagination-link
|
||||
display: inline-flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
padding: 4px 8px
|
||||
min-width: 28px
|
||||
height: 28px
|
||||
border-radius: 6px
|
||||
background: var(--btn-bg)
|
||||
color: var(--btn-color)
|
||||
transition: all .2s ease
|
||||
|
||||
&.ais-Pagination-link--disabled
|
||||
opacity: .3
|
||||
cursor: not-allowed
|
||||
|
||||
i
|
||||
font-size: 12px
|
||||
|
||||
.ais-Pagination-item--selected
|
||||
.ais-Pagination-link
|
||||
background: $theme-paginator-color
|
||||
font-weight: 600
|
||||
cursor: default
|
||||
|
||||
.ais-Pagination-item--ellipsis
|
||||
.ais-Pagination-link
|
||||
padding: 4px 2px
|
||||
border: none
|
||||
background: transparent
|
||||
color: var(--text-color)
|
||||
cursor: default
|
||||
|
||||
&:hover
|
||||
background: transparent
|
||||
transform: none
|
||||
|
||||
.ais-Pagination-item--disabled
|
||||
.ais-Pagination-link
|
||||
opacity: .4
|
||||
|
||||
+maxWidth768()
|
||||
.ais-Pagination-list
|
||||
gap: 4px
|
||||
|
||||
hr
|
||||
margin: 15px auto
|
||||
@extend .custom-hr
|
||||
@@ -50,6 +202,38 @@
|
||||
display: none
|
||||
background: rgba($dark-black, .6)
|
||||
|
||||
.search-result-stats,
|
||||
.ais-Stats-text
|
||||
margin: 15px 0 0
|
||||
color: var(--text-color)
|
||||
text-align: center
|
||||
font-size: .9em
|
||||
|
||||
.search-keyword
|
||||
background: transparent
|
||||
color: $search-keyword-highlight
|
||||
font-weight: 600
|
||||
|
||||
.search-loading
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
padding: 20px
|
||||
color: var(--text-color)
|
||||
|
||||
&::before
|
||||
width: 16px
|
||||
height: 16px
|
||||
border: 2px solid var(--text-color)
|
||||
border-top-color: transparent
|
||||
border-radius: 50%
|
||||
content: ''
|
||||
animation: spin 1s linear infinite
|
||||
|
||||
@keyframes spin
|
||||
to
|
||||
transform: rotate(360deg)
|
||||
|
||||
if hexo-config('search.use') == 'algolia_search'
|
||||
@require 'algolia'
|
||||
else if hexo-config('search.use') == 'local_search'
|
||||
|
||||
@@ -1,57 +1,18 @@
|
||||
#local-search
|
||||
.search-dialog
|
||||
.local-search-box
|
||||
margin: 0 auto
|
||||
max-width: 100%
|
||||
width: 100%
|
||||
|
||||
input
|
||||
padding: 5px 14px
|
||||
width: 100%
|
||||
outline: none
|
||||
border: 2px solid $search-color
|
||||
border-radius: 40px
|
||||
background: var(--search-bg)
|
||||
color: var(--search-input-color)
|
||||
-webkit-appearance: none
|
||||
|
||||
.search-wrap
|
||||
display: none
|
||||
|
||||
.local-search-hit-item
|
||||
margin-left: 24px
|
||||
padding-left: 3px
|
||||
line-height: 1.8
|
||||
|
||||
&::marker
|
||||
color: $search-color
|
||||
font-weight: bold
|
||||
font-style: italic
|
||||
|
||||
a
|
||||
color: var(--search-a-color)
|
||||
|
||||
&:hover
|
||||
color: $search-color
|
||||
|
||||
.search-result-title
|
||||
font-weight: 600
|
||||
|
||||
.search-result
|
||||
margin: 0 0 8px
|
||||
word-break: break-all
|
||||
font-size: .9em
|
||||
|
||||
.search-result-list
|
||||
overflow-y: overlay
|
||||
margin: 0 -20px
|
||||
padding: 0 22px
|
||||
max-height: calc(80vh - 180px)
|
||||
|
||||
+maxWidth768()
|
||||
max-height: calc(var(--search-height) - 190px) !important
|
||||
if hexo-config('search.local_search.pagination.enable')
|
||||
min-height: calc(var(--search-height) - 255px) !important
|
||||
else
|
||||
max-height: calc(var(--search-height) - 200px) !important
|
||||
|
||||
.search-keyword
|
||||
background: transparent
|
||||
color: $search-keyword-highlight
|
||||
font-weight: 600
|
||||
#local-search-stats
|
||||
.search-result-stats
|
||||
text-align: left
|
||||
|
||||
.search-keyword
|
||||
font-weight: 600
|
||||
|
||||
#loading-database ~ *
|
||||
visibility: hidden
|
||||
@@ -7,21 +7,20 @@
|
||||
display: inline-block
|
||||
margin: 0 4px 6px
|
||||
padding: 0 15px
|
||||
background-color: var(--btn-beautify-color, $btn-default-color)
|
||||
color: $btn-color
|
||||
background-color: var(--btn-beautify-color, var(--btn-default-color))
|
||||
color: var(--btn-color, $btn-color)
|
||||
vertical-align: top
|
||||
line-height: 2
|
||||
addBorderRadius()
|
||||
@extend .btn-effects
|
||||
|
||||
for $type in $color-types
|
||||
&.{$type}
|
||||
--btn-beautify-color: lookup('$tagsP-' + $type + '-color')
|
||||
--btn-beautify-color: unquote('var(--tags-' + $type + '-color)')
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-hover-color)
|
||||
|
||||
i + span
|
||||
margin-left: 6px
|
||||
|
||||
&:not(.block) + .btn-beautify:not(.block)
|
||||
margin: 0 4px 20px
|
||||
|
||||
@@ -39,18 +38,26 @@
|
||||
|
||||
&.larger
|
||||
padding: 6px 15px
|
||||
|
||||
&:hover
|
||||
text-decoration: none
|
||||
@extend .btn-effects-large
|
||||
|
||||
&.outline
|
||||
border: 1px solid transparent
|
||||
border-color: var(--btn-beautify-color, $btn-default-color)
|
||||
border-color: var(--btn-beautify-color, var(--btn-default-color))
|
||||
background-color: transparent
|
||||
color: var(--btn-beautify-color, $btn-default-color)
|
||||
color: var(--btn-beautify-color, var(--btn-default-color))
|
||||
|
||||
i,
|
||||
span
|
||||
transition: color .3s cubic-bezier(.4, 0, .2, 1)
|
||||
|
||||
&::before
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, .1), transparent)
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-beautify-color, $btn-default-color)
|
||||
border-color: var(--btn-beautify-color, var(--btn-default-color))
|
||||
background-color: var(--btn-beautify-color, var(--btn-default-color))
|
||||
color: var(--btn-color) !important
|
||||
|
||||
&:hover
|
||||
color: white !important
|
||||
i,
|
||||
span
|
||||
color: var(--btn-color)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
background: $tag-hide-bg
|
||||
color: var(--white)
|
||||
addBorderRadius()
|
||||
@extend .btn-effects
|
||||
|
||||
&:hover
|
||||
background-color: var(--btn-hover-color)
|
||||
@@ -38,11 +39,26 @@
|
||||
border: 1px solid $tag-hide-toggle-bg
|
||||
addBorderRadius(5, true)
|
||||
|
||||
& > .toggle-content
|
||||
margin: 30px 24px
|
||||
|
||||
& > .toggle-button
|
||||
padding: 6px 15px
|
||||
background: $tag-hide-toggle-bg
|
||||
color: #1F2D3D
|
||||
list-style: none
|
||||
cursor: pointer
|
||||
|
||||
& > .toggle-content
|
||||
margin: 30px 24px
|
||||
&::-webkit-details-marker
|
||||
display: none
|
||||
|
||||
&::before
|
||||
@extend .fontawesomeIcon
|
||||
margin-right: 8px
|
||||
content: '\f0d7'
|
||||
transition: transform .3s ease
|
||||
transform: rotate(-90deg)
|
||||
transform-origin: center center
|
||||
|
||||
&[open] summary::before
|
||||
transform: rotate(0)
|
||||
@@ -1,11 +1,11 @@
|
||||
.hl-label
|
||||
padding: 2px 4px
|
||||
color: $btn-color
|
||||
color: var(--btn-color, $btn-color)
|
||||
addBorderRadius(3)
|
||||
|
||||
&.default
|
||||
background-color: $btn-default-color
|
||||
background-color: var(--btn-default-color)
|
||||
|
||||
for $type in $color-types
|
||||
&.{$type}
|
||||
background-color: lookup('$tagsP-' + $type + '-color')
|
||||
background-color: unquote('var(--tags-' + $type + '-color)')
|
||||
|
||||
+20
-19
@@ -19,32 +19,32 @@
|
||||
for $type in $color-types
|
||||
&.{$type}
|
||||
&:not(.disabled)
|
||||
border-left-color: lookup('$tagsP-' + $type + '-color') !important
|
||||
border-left-color: unquote('var(--tags-' + $type + '-color)') !important
|
||||
|
||||
&.modern
|
||||
border-left-color: transparent !important
|
||||
color: lookup('$tagsP-' + $type + '-color')
|
||||
color: unquote('var(--tags-' + $type + '-color)')
|
||||
|
||||
&:not(.simple)
|
||||
background: lighten(lookup('$tagsP-' + $type + '-color'), 85%) !important
|
||||
background: unquote('var(--tags-' + $type + '-color-lighten)')
|
||||
|
||||
& > .note-icon
|
||||
color: lookup('$tagsP-' + $type + '-color')
|
||||
color: unquote('var(--tags-' + $type + '-color)')
|
||||
|
||||
&.simple
|
||||
border: 1px solid #EEEEEE
|
||||
border: 1px solid var(--note-default-border)
|
||||
border-left-width: 5px
|
||||
|
||||
&.modern
|
||||
border: 1px solid transparent !important
|
||||
background-color: #f5f5f5
|
||||
color: $font-black
|
||||
background-color: var(--note-modern-default-bg)
|
||||
color: var(--note-modern-default-text)
|
||||
|
||||
&.flat
|
||||
border: initial
|
||||
border-left: 5px solid #EEEEEE
|
||||
background-color: lighten(#EEEEEE, 65%)
|
||||
color: $font-black
|
||||
border-left: 5px solid var(--note-default-border)
|
||||
background-color: var(--note-default-bg)
|
||||
color: var(--note-default-text)
|
||||
|
||||
h2,
|
||||
h3,
|
||||
@@ -90,29 +90,30 @@
|
||||
for $type in $note-types
|
||||
&.{$type}
|
||||
&.flat
|
||||
background: lookup('$note-' + $type + '-bg')
|
||||
background: unquote('var(--note-' + $type + '-bg)')
|
||||
color: var(--font-color)
|
||||
|
||||
&.modern
|
||||
border-color: lookup('$note-modern-' + $type + '-border')
|
||||
background: lookup('$note-modern-' + $type + '-bg')
|
||||
color: lookup('$note-modern-' + $type + '-text')
|
||||
border-color: unquote('var(--note-modern-' + $type + '-border)') !important
|
||||
background: unquote('var(--note-modern-' + $type + '-bg)')
|
||||
color: unquote('var(--note-modern-' + $type + '-text)')
|
||||
|
||||
a
|
||||
&:not(.btn)
|
||||
color: lookup('$note-modern-' + $type + '-text')
|
||||
color: unquote('var(--note-modern-' + $type + '-text)')
|
||||
|
||||
&:hover
|
||||
color: lookup('$note-modern-' + $type + '-hover')
|
||||
color: unquote('var(--note-modern-' + $type + '-hover)')
|
||||
|
||||
&:not(.modern)
|
||||
border-left-color: lookup('$note-' + $type + '-border')
|
||||
border-left-color: unquote('var(--note-' + $type + '-border)')
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6
|
||||
color: lookup('$note-' + $type + '-text')
|
||||
color: unquote('var(--note-' + $type + '-text)')
|
||||
|
||||
if $note-icons
|
||||
&:not(.no-icon)
|
||||
@@ -121,4 +122,4 @@
|
||||
|
||||
&:not(.modern)
|
||||
&::before
|
||||
color: lookup('$note-' + $type + '-text')
|
||||
color: unquote('var(--note-' + $type + '-text)')
|
||||
|
||||
+49
-2
@@ -13,11 +13,9 @@ $code-foreground = $themeColorEnable && hexo-config('theme_color.code_foreground
|
||||
$code-background = $themeColorEnable && hexo-config('theme_color.code_background') ? convert(hexo-config('theme_color.code_background')) : rgba(27, 31, 35, .05)
|
||||
$theme-toc-color = $themeColorEnable && hexo-config('theme_color.toc_color') ? convert(hexo-config('theme_color.toc_color')) : $strong-cyan
|
||||
// font
|
||||
|
||||
$chineseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
|
||||
$default-font-family = -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', $chineseFont, sans-serif
|
||||
$default-code-font = consolas, Menlo, monospace, 'PingFang SC', $chineseFont, sans-serif
|
||||
|
||||
$font-family = hexo-config('font.font_family') ? unquote(hexo-config('font.font_family')) : $default-font-family
|
||||
$code-font-family = hexo-config('font.code_font_family') ? unquote(hexo-config('font.code_font_family')) : $default-code-font
|
||||
$site-name-font = hexo-config('blog_title_font.font_family') && unquote(hexo-config('blog_title_font.font_family'))
|
||||
@@ -184,3 +182,52 @@ $tab-to-top-color = #99a9bf
|
||||
$tab-to-top-hover-color = $theme-color
|
||||
// Tag Plugins - timeline
|
||||
$timeline-default-color = $theme-color
|
||||
// note - darkmode
|
||||
// Default
|
||||
$note-dark-default-border = #5a5a5a
|
||||
$note-dark-default-bg = #2b2b2b
|
||||
$note-dark-default-text = #b3b3b3
|
||||
$note-dark-modern-default-border = #9a9a9a
|
||||
$note-dark-modern-default-bg = #353535
|
||||
$note-dark-modern-default-text = #c4c4c4
|
||||
$note-dark-modern-default-hover = #d0d0d0
|
||||
// Primary
|
||||
$note-dark-primary-border = #5935a1
|
||||
$note-dark-primary-bg = #2e1c3e
|
||||
$note-dark-primary-text = #a47dd4
|
||||
$note-dark-modern-primary-border = #9985cc
|
||||
$note-dark-modern-primary-bg = #3c2d4c
|
||||
$note-dark-modern-primary-text = #b693e6
|
||||
$note-dark-modern-primary-hover = #c9a8f0
|
||||
// Info
|
||||
$note-dark-info-border = #346fa2
|
||||
$note-dark-info-bg = #1f2e3b
|
||||
$note-dark-info-text = #7bb3db
|
||||
$note-dark-modern-info-border = #7ca8b5
|
||||
$note-dark-modern-info-bg = #2b3c44
|
||||
$note-dark-modern-info-text = #8fc6e0
|
||||
$note-dark-modern-info-hover = #a3d4ea
|
||||
// Success
|
||||
$note-dark-success-border = #4a944a
|
||||
$note-dark-success-bg = #202e20
|
||||
$note-dark-success-text = #82c682
|
||||
$note-dark-modern-success-border = #8bb087
|
||||
$note-dark-modern-success-bg = #2c3d2c
|
||||
$note-dark-modern-success-text = #96d196
|
||||
$note-dark-modern-success-hover = #a8dca8
|
||||
// Warning
|
||||
$note-dark-warning-border = #c08a3e
|
||||
$note-dark-warning-bg = #3e301f
|
||||
$note-dark-warning-text = #e6ba6b
|
||||
$note-dark-modern-warning-border = #b8a285
|
||||
$note-dark-modern-warning-bg = #4b3c2b
|
||||
$note-dark-modern-warning-text = #d4b373
|
||||
$note-dark-modern-warning-hover = #e0c080
|
||||
// Danger
|
||||
$note-dark-danger-border = #b34440
|
||||
$note-dark-danger-bg = #3b201f
|
||||
$note-dark-danger-text = #e67572
|
||||
$note-dark-modern-danger-border = #c7898c
|
||||
$note-dark-modern-danger-bg = #4d2b2e
|
||||
$note-dark-modern-danger-text = #d98b8e
|
||||
$note-dark-modern-danger-hover = #e59fa2
|
||||
+249
-164
@@ -2,18 +2,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let headerContentWidth, $nav
|
||||
let mobileSidebarOpen = false
|
||||
|
||||
// rightsideScrollPercent
|
||||
let goUpElement = null
|
||||
let scrollPercentElement = null
|
||||
|
||||
const adjustMenu = init => {
|
||||
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
||||
let hideMenuIndex = false
|
||||
|
||||
if (init) {
|
||||
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
||||
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
||||
const blogInfoWidth = Array.from(document.querySelector('#blog-info > a').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||
const menusWidth = Array.from(document.getElementById('menus').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||
headerContentWidth = blogInfoWidth + menusWidth
|
||||
$nav = document.getElementById('nav')
|
||||
}
|
||||
|
||||
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||
hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化header
|
||||
@@ -54,21 +61,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
* 代碼
|
||||
* 只適用於Hexo默認的代碼渲染
|
||||
*/
|
||||
const addHighlightTool = () => {
|
||||
const addHighlightTool = $article => {
|
||||
const highLight = GLOBAL_CONFIG.highlight
|
||||
if (!highLight) return
|
||||
|
||||
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
||||
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
|
||||
const isNotHighlightJs = plugin !== 'highlight.js'
|
||||
const isPrismjs = plugin === 'prismjs'
|
||||
const $figureHighlight = isNotHighlightJs
|
||||
? Array.from($article.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
|
||||
: $article.querySelectorAll('figure.highlight')
|
||||
|
||||
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||
|
||||
const isPrismjs = plugin === 'prismjs'
|
||||
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
||||
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
|
||||
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
|
||||
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
|
||||
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
|
||||
|
||||
@@ -76,9 +86,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||||
btf.snackbarShow(text)
|
||||
} else {
|
||||
ele.textContent = text
|
||||
ele.style.opacity = 1
|
||||
setTimeout(() => { ele.style.opacity = 0 }, 800)
|
||||
const newEle = document.createElement('div')
|
||||
newEle.className = 'copy-notice'
|
||||
newEle.textContent = text
|
||||
document.body.appendChild(newEle)
|
||||
|
||||
const buttonRect = ele.getBoundingClientRect()
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
|
||||
|
||||
// X-axis boundary check
|
||||
const halfWidth = newEle.offsetWidth / 2
|
||||
const centerLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
|
||||
const finalLeft = Math.max(halfWidth + 10, Math.min(window.innerWidth - halfWidth - 10, centerLeft))
|
||||
|
||||
// Show tooltip below button if too close to top
|
||||
const normalTop = buttonRect.top + scrollTop - 40
|
||||
const shouldShowBelow = buttonRect.top < 60 || normalTop < 10
|
||||
|
||||
const topValue = shouldShowBelow ? buttonRect.top + scrollTop + buttonRect.height + 10 : normalTop
|
||||
|
||||
newEle.style.cssText = `
|
||||
top: ${topValue + 10}px;
|
||||
left: ${finalLeft}px;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease, top 0.3s ease;
|
||||
`
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
newEle.style.opacity = '1'
|
||||
newEle.style.top = `${topValue}px`
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
newEle.style.opacity = '0'
|
||||
newEle.style.top = `${topValue + 10}px`
|
||||
setTimeout(() => {
|
||||
newEle?.remove()
|
||||
}, 300)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +143,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const highlightCopyFn = (ele, clickEle) => {
|
||||
const $buttonParent = ele.parentNode
|
||||
$buttonParent.classList.add('copy-true')
|
||||
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
|
||||
const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
|
||||
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||
if (!codeElement) return
|
||||
copy(codeElement.innerText, clickEle.previousElementSibling)
|
||||
copy(codeElement.innerText, clickEle)
|
||||
$buttonParent.classList.remove('copy-true')
|
||||
}
|
||||
|
||||
@@ -126,33 +173,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// 獲取隱藏狀態下元素的真實高度
|
||||
const getActualHeight = item => {
|
||||
const hiddenElements = new Map()
|
||||
if (item.offsetHeight > 0) return item.offsetHeight
|
||||
|
||||
const fix = () => {
|
||||
let current = item
|
||||
while (current !== document.body && current != null) {
|
||||
if (window.getComputedStyle(current).display === 'none') {
|
||||
hiddenElements.set(current, current.getAttribute('style') || '')
|
||||
}
|
||||
current = current.parentNode
|
||||
}
|
||||
const clone = item.cloneNode(true)
|
||||
|
||||
const style = 'visibility: hidden !important; display: block !important;'
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
||||
})
|
||||
}
|
||||
clone.style.cssText = `
|
||||
position: absolute !important;
|
||||
visibility: hidden !important;
|
||||
display: block !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
pointer-events: none !important;
|
||||
z-index: -1 !important;
|
||||
margin: 0 !important;
|
||||
`
|
||||
|
||||
const restore = () => {
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
if (originalStyle === '') elem.removeAttribute('style')
|
||||
else elem.setAttribute('style', originalStyle)
|
||||
})
|
||||
}
|
||||
|
||||
fix()
|
||||
const height = item.offsetHeight
|
||||
restore()
|
||||
item.parentNode.insertBefore(clone, item)
|
||||
const height = clone.offsetHeight
|
||||
clone.remove()
|
||||
return height
|
||||
}
|
||||
|
||||
@@ -175,20 +213,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fragment.appendChild(ele)
|
||||
}
|
||||
|
||||
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||
}
|
||||
|
||||
$figureHighlight.forEach(item => {
|
||||
let langName = ''
|
||||
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
|
||||
if (isNotHighlightJs) {
|
||||
const newClassName = isPrismjs ? 'prismjs' : 'default'
|
||||
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
|
||||
}
|
||||
|
||||
if (!highlightLang) {
|
||||
createEle('', item)
|
||||
return
|
||||
}
|
||||
|
||||
if (isPrismjs) {
|
||||
langName = item.getAttribute('data-language') || 'Code'
|
||||
if (isNotHighlightJs) {
|
||||
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
|
||||
} else {
|
||||
langName = item.getAttribute('class').split(' ')[1]
|
||||
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||
@@ -200,9 +241,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* PhotoFigcaption
|
||||
*/
|
||||
const addPhotoFigcaption = () => {
|
||||
const addPhotoFigcaption = $article => {
|
||||
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||
document.querySelectorAll('#article-container img').forEach(item => {
|
||||
$article.querySelectorAll('img').forEach(item => {
|
||||
const altValue = item.title || item.alt
|
||||
if (!altValue) return
|
||||
const ele = document.createElement('div')
|
||||
@@ -215,8 +256,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* Lightbox
|
||||
*/
|
||||
const runLightbox = () => {
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
const runLightbox = $article => {
|
||||
btf.loadLightbox($article.querySelectorAll('img:not(.no-lightbox)'))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const fetchUrl = async url => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch URL:', error)
|
||||
return []
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const runJustifiedGallery = (container, data, config) => {
|
||||
const { isButton, limit, firstLimit, tabs } = config
|
||||
const { isButton, tabs } = config
|
||||
const limit = Math.max(1, Number(config.limit) || 20)
|
||||
const firstLimit = Math.max(1, Number(config.firstLimit) || limit)
|
||||
|
||||
const dataLength = data.length
|
||||
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
|
||||
const maxGroupKey = dataLength
|
||||
? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1
|
||||
: 0
|
||||
|
||||
// Gallery configuration
|
||||
const igConfig = {
|
||||
@@ -254,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let isLayoutHidden = false
|
||||
|
||||
// Utility functions
|
||||
const sanitizeString = str => (str && str.replace(/"/g, '"')) || ''
|
||||
const sanitizeString = str => String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
const createImageItem = item => {
|
||||
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
||||
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
||||
const url = item.url ? sanitizeString(item.url) : ''
|
||||
return `<div class="item">
|
||||
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||
<img src="${url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -329,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btf.setLoading.add(container)
|
||||
ig.on('renderComplete', handleRenderComplete)
|
||||
|
||||
if (isButton) {
|
||||
if (isButton && dataLength) {
|
||||
appendItems(1, firstLimit, true)
|
||||
} else {
|
||||
} else if (dataLength) {
|
||||
ig.on('requestAppend', handleRequestAppend)
|
||||
ig.renderItems()
|
||||
} else {
|
||||
btf.setLoading.remove(container)
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
||||
@@ -356,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = element.firstElementChild
|
||||
const content = container.textContent
|
||||
container.textContent = ''
|
||||
element.classList.add('loaded')
|
||||
|
||||
try {
|
||||
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
||||
if (!Array.isArray(data)) throw new TypeError('Gallery data must be an array')
|
||||
runJustifiedGallery(container, data, config)
|
||||
element.classList.add('loaded')
|
||||
} catch (error) {
|
||||
console.error('Gallery data parsing failed:', error)
|
||||
}
|
||||
@@ -380,11 +434,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
*/
|
||||
const rightsideScrollPercent = currentTop => {
|
||||
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||
const goUpElement = document.getElementById('go-up')
|
||||
|
||||
if (!goUpElement || !scrollPercentElement) return
|
||||
if (scrollPercent < 95) {
|
||||
goUpElement.classList.add('show-percent')
|
||||
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
||||
scrollPercentElement.textContent = scrollPercent
|
||||
} else {
|
||||
goUpElement.classList.remove('show-percent')
|
||||
}
|
||||
@@ -395,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
*/
|
||||
const scrollFn = () => {
|
||||
const $rightside = document.getElementById('rightside')
|
||||
const innerHeight = window.innerHeight + 56
|
||||
let initTop = 0
|
||||
const $header = document.getElementById('page-header')
|
||||
const isChatBtn = typeof chatBtn !== 'undefined'
|
||||
const isChatBtn = typeof window.chatBtn !== 'undefined'
|
||||
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||
|
||||
// 檢查文檔高度是否小於視窗高度
|
||||
const checkDocumentHeight = () => {
|
||||
if (document.body.scrollHeight <= innerHeight) {
|
||||
if (document.body.scrollHeight <= window.innerHeight + 56) {
|
||||
$rightside.classList.add('rightside-show')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果文檔高度小於視窗高度,直接返回
|
||||
if (checkDocumentHeight()) return
|
||||
|
||||
// find the scroll direction
|
||||
const scrollDirection = currentTop => {
|
||||
const result = currentTop > initTop // true is down & false is up
|
||||
@@ -421,7 +471,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
let flag = ''
|
||||
const scrollTask = btf.throttle(() => {
|
||||
const scrollTask = btf.rafThrottle(() => {
|
||||
if (checkDocumentHeight()) return
|
||||
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
const isDown = scrollDirection(currentTop)
|
||||
if (currentTop > 56) {
|
||||
@@ -452,19 +504,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
isShowPercent && rightsideScrollPercent(currentTop)
|
||||
checkDocumentHeight()
|
||||
}, 300)
|
||||
})
|
||||
|
||||
checkDocumentHeight()
|
||||
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* toc,anchor
|
||||
* toc, anchor
|
||||
*/
|
||||
const scrollFnToDo = () => {
|
||||
const scrollFnToDo = $article => {
|
||||
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||
const $article = document.getElementById('article-container')
|
||||
|
||||
if (!($article && (isToc || isAnchor))) return
|
||||
|
||||
@@ -477,7 +528,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||
isExpand = $cardToc.classList.contains('is-expand')
|
||||
|
||||
// toc元素點擊
|
||||
const tocItemClickFn = e => {
|
||||
const target = e.target.closest('.toc-link')
|
||||
if (!target) return
|
||||
@@ -504,82 +554,105 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 處理 hexo-blog-encrypt 事件
|
||||
$cardToc.style.display = 'block'
|
||||
}
|
||||
|
||||
// find head position & add active class
|
||||
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||
let detectItem = ''
|
||||
if (!$articleList.length) return
|
||||
|
||||
const findHeadPosition = top => {
|
||||
if (top === 0) return false
|
||||
let activeTocItem = null
|
||||
let activeParentItems = []
|
||||
|
||||
let currentId = ''
|
||||
let currentIndex = ''
|
||||
const updateTocUI = currentId => {
|
||||
const encodedAnchor = currentId ? '#' + encodeURI(decodeURI(currentId)) : ''
|
||||
if (isAnchor) btf.updateAnchor(encodedAnchor)
|
||||
|
||||
for (let i = 0; i < $articleList.length; i++) {
|
||||
const ele = $articleList[i]
|
||||
if (top > btf.getEleTop(ele) - 80) {
|
||||
const id = ele.id
|
||||
currentId = id ? '#' + encodeURI(id) : ''
|
||||
currentIndex = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if (!isToc) return
|
||||
|
||||
if (activeTocItem) activeTocItem.classList.remove('active')
|
||||
activeParentItems.forEach(i => i.classList.remove('active'))
|
||||
activeParentItems = []
|
||||
|
||||
if (!currentId) {
|
||||
activeTocItem = null
|
||||
return
|
||||
}
|
||||
|
||||
if (detectItem === currentIndex) return
|
||||
const targetLink = Array.from($tocLink).find(link => {
|
||||
const href = link.getAttribute('href')
|
||||
if (!href) return false
|
||||
return decodeURI(href).replace('#', '') === decodeURI(currentId)
|
||||
})
|
||||
|
||||
if (isAnchor) btf.updateAnchor(currentId)
|
||||
if (!targetLink) return
|
||||
|
||||
detectItem = currentIndex
|
||||
targetLink.classList.add('active')
|
||||
activeTocItem = targetLink
|
||||
setTimeout(() => autoScrollToc(targetLink), 0)
|
||||
|
||||
if (isToc) {
|
||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
||||
|
||||
if (currentId) {
|
||||
const currentActive = $tocLink[currentIndex]
|
||||
currentActive.classList.add('active')
|
||||
|
||||
setTimeout(() => autoScrollToc(currentActive), 0)
|
||||
|
||||
if (!isExpand) {
|
||||
let parent = currentActive.parentNode
|
||||
while (!parent.matches('.toc')) {
|
||||
if (parent.matches('li')) parent.classList.add('active')
|
||||
parent = parent.parentNode
|
||||
}
|
||||
if (!isExpand) {
|
||||
let parent = targetLink.parentNode
|
||||
while (!parent.matches('.toc')) {
|
||||
if (parent.matches('li')) {
|
||||
parent.classList.add('active')
|
||||
activeParentItems.push(parent)
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main of scroll
|
||||
const tocScrollFn = btf.throttle(() => {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-60px 0px -80% 0px',
|
||||
threshold: 0
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
updateTocUI(entry.target.id)
|
||||
}
|
||||
})
|
||||
}, observerOptions)
|
||||
|
||||
$articleList.forEach(ele => observer.observe(ele))
|
||||
|
||||
const scrollHandler = btf.rafThrottle(() => {
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
|
||||
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
||||
}
|
||||
findHeadPosition(currentTop)
|
||||
}, 100)
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
|
||||
if (currentTop === 0) {
|
||||
updateTocUI('')
|
||||
} else if (currentTop + window.innerHeight >= document.documentElement.scrollHeight - 10) {
|
||||
const lastHeader = $articleList[$articleList.length - 1]
|
||||
updateTocUI(lastHeader.id)
|
||||
}
|
||||
})
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', scrollHandler, { passive: true })
|
||||
|
||||
btf.addGlobalFn('pjaxSendOnce', () => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
|
||||
const handleThemeChange = mode => {
|
||||
const globalFn = window.globalFn || {}
|
||||
const themeChange = globalFn.themeChange || {}
|
||||
if (!themeChange) {
|
||||
return
|
||||
}
|
||||
const themeChange = globalFn.themeChange
|
||||
if (!themeChange) return
|
||||
|
||||
Object.keys(themeChange).forEach(key => {
|
||||
const themeChangeFn = themeChange[key]
|
||||
const fn = themeChange[key]
|
||||
if (typeof fn !== 'function') return
|
||||
|
||||
if (['disqus', 'disqusjs'].includes(key)) {
|
||||
setTimeout(() => themeChangeFn(mode), 300)
|
||||
setTimeout(() => fn(mode), 300)
|
||||
} else {
|
||||
themeChangeFn(mode)
|
||||
fn(mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -600,7 +673,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
$body.classList.add('read-mode')
|
||||
newEle.type = 'button'
|
||||
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
||||
newEle.className = 'exit-readmode'
|
||||
newEle.innerHTML = '<i class="fas fa-sign-out-alt"></i>'
|
||||
newEle.addEventListener('click', exitReadMode)
|
||||
$body.appendChild(newEle)
|
||||
},
|
||||
@@ -698,7 +772,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const addCopyright = () => {
|
||||
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
||||
|
||||
const handleCopy = (e) => {
|
||||
const handleCopy = e => {
|
||||
e.preventDefault()
|
||||
const copyFont = window.getSelection(0).toString()
|
||||
let textFont = copyFont
|
||||
@@ -740,8 +814,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* table overflow
|
||||
*/
|
||||
const addTableWrap = () => {
|
||||
const $table = document.querySelectorAll('#article-container table')
|
||||
const addTableWrap = $article => {
|
||||
const $table = $article.querySelectorAll('table')
|
||||
if (!$table.length) return
|
||||
|
||||
$table.forEach(item => {
|
||||
@@ -751,52 +825,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* tag-hide
|
||||
*/
|
||||
const clickFnOfTagHide = () => {
|
||||
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
||||
const clickFnOfTagHide = $article => {
|
||||
const hideButtons = $article.querySelectorAll('.hide-button')
|
||||
if (!hideButtons.length) return
|
||||
hideButtons.forEach(item => item.addEventListener('click', e => {
|
||||
const currentTarget = e.currentTarget
|
||||
currentTarget.classList.add('open')
|
||||
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||
}, { once: true }))
|
||||
|
||||
const handleClickOfTagHide = e => {
|
||||
const button = e.target.closest('.hide-button')
|
||||
if (!button) return
|
||||
button.classList.add('open')
|
||||
addJustifiedGallery(button.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||
}
|
||||
|
||||
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
|
||||
}
|
||||
|
||||
const tabsFn = () => {
|
||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
||||
if (!navTabsElements.length) return
|
||||
const tabsFn = $article => {
|
||||
if (!$article.querySelector('.tabs')) return
|
||||
|
||||
const setActiveClass = (elements, activeIndex) => {
|
||||
elements.forEach((el, index) => {
|
||||
el.classList.toggle('active', index === activeIndex)
|
||||
})
|
||||
elements.forEach((el, index) => el.classList.toggle('active', index === activeIndex))
|
||||
}
|
||||
|
||||
const handleNavClick = e => {
|
||||
const target = e.target.closest('button')
|
||||
if (!target || target.classList.contains('active')) return
|
||||
const handleClick = e => {
|
||||
const tabsRoot = e.target.closest('.tabs')
|
||||
if (!tabsRoot) return
|
||||
|
||||
const navItems = [...e.currentTarget.children]
|
||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
||||
const indexOfButton = navItems.indexOf(target)
|
||||
setActiveClass(navItems, indexOfButton)
|
||||
e.currentTarget.classList.remove('no-default')
|
||||
setActiveClass(tabContents, indexOfButton)
|
||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||
}
|
||||
const navContainer = tabsRoot.firstElementChild
|
||||
const toTopContainer = tabsRoot.lastElementChild
|
||||
|
||||
const handleToTopClick = tabElement => e => {
|
||||
if (e.target.closest('button')) {
|
||||
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
||||
if (navContainer.contains(e.target)) {
|
||||
const target = e.target.closest('button')
|
||||
if (!target || target.classList.contains('active')) return
|
||||
|
||||
const navItems = [...navContainer.children]
|
||||
const tabContents = [...navContainer.nextElementSibling.children]
|
||||
const indexOfButton = navItems.indexOf(target)
|
||||
setActiveClass(navItems, indexOfButton)
|
||||
navContainer.classList.remove('no-default')
|
||||
setActiveClass(tabContents, indexOfButton)
|
||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||
return
|
||||
}
|
||||
|
||||
if (toTopContainer.contains(e.target) && e.target.closest('button')) {
|
||||
btf.scrollToDest(btf.getEleTop(tabsRoot), 300)
|
||||
}
|
||||
}
|
||||
|
||||
navTabsElements.forEach(tabElement => {
|
||||
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
||||
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
||||
})
|
||||
btf.addEventListenerPjax($article, 'click', handleClick)
|
||||
}
|
||||
|
||||
const toggleCardCategory = () => {
|
||||
@@ -862,10 +938,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const unRefreshFn = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
const resizeHandler = btf.rafThrottle(() => {
|
||||
adjustMenu(false)
|
||||
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
||||
if (mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu'))) {
|
||||
sidebarFn.close()
|
||||
}
|
||||
})
|
||||
window.addEventListener('resize', resizeHandler, { passive: true })
|
||||
|
||||
const menuMask = document.getElementById('menu-mask')
|
||||
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||
@@ -883,18 +962,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const forPostFn = () => {
|
||||
addHighlightTool()
|
||||
addPhotoFigcaption()
|
||||
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
||||
runLightbox()
|
||||
scrollFnToDo()
|
||||
addTableWrap()
|
||||
clickFnOfTagHide()
|
||||
tabsFn()
|
||||
const $article = document.getElementById('article-container')
|
||||
if (!$article || $article.querySelector('.hbe-container')) return
|
||||
|
||||
addHighlightTool($article)
|
||||
addPhotoFigcaption($article)
|
||||
addJustifiedGallery($article.querySelectorAll('.gallery-container'))
|
||||
runLightbox($article)
|
||||
scrollFnToDo($article)
|
||||
addTableWrap($article)
|
||||
clickFnOfTagHide($article)
|
||||
tabsFn($article)
|
||||
}
|
||||
|
||||
const refreshFn = () => {
|
||||
initAdjust()
|
||||
goUpElement = document.getElementById('go-up')
|
||||
scrollPercentElement = goUpElement?.querySelector('.scroll-percent')
|
||||
|
||||
justifiedIndexPostUI()
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
||||
|
||||
+387
-90
@@ -6,8 +6,28 @@ window.addEventListener('load', () => {
|
||||
return console.error('Algolia setting is invalid!')
|
||||
}
|
||||
|
||||
const CONTENT_FIELDS = ['contentStripTruncate', 'contentStrip', 'content']
|
||||
const HIGHLIGHT_PARAMS = {
|
||||
highlightPreTag: '<mark>',
|
||||
highlightPostTag: '</mark>',
|
||||
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||
}
|
||||
|
||||
// Pre-compiled regex for tag balancing (reused across cutContent calls)
|
||||
const TAG_REGEX = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*\/?>/g
|
||||
|
||||
const $searchMask = document.getElementById('search-mask')
|
||||
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||
const $loadingStatus = document.getElementById('loading-status')
|
||||
const $hits = document.getElementById('algolia-hits')
|
||||
const $hitsEmpty = document.getElementById('algolia-hits-empty')
|
||||
const $hitsList = document.querySelector('#algolia-hits .ais-Hits-list')
|
||||
const $hitsWrapper = document.querySelector('#algolia-hits .ais-Hits')
|
||||
const $pagination = document.getElementById('algolia-pagination')
|
||||
const $paginationList = document.querySelector('#algolia-pagination .ais-Pagination-list')
|
||||
const $stats = document.querySelector('#algolia-info .ais-Stats-text')
|
||||
const $searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
||||
const $searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
||||
|
||||
const animateElements = show => {
|
||||
const action = show ? 'animateIn' : 'animateOut'
|
||||
@@ -23,27 +43,43 @@ window.addEventListener('load', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced resize to avoid layout thrashing
|
||||
let resizeTimer
|
||||
const onResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||
}
|
||||
|
||||
const handleEscape = event => {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}
|
||||
|
||||
const showLoading = show => {
|
||||
if ($loadingStatus) $loadingStatus.hidden = !show
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
btf.overflowPaddingR.add()
|
||||
animateElements(true)
|
||||
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
||||
showLoading(false)
|
||||
|
||||
const handleEscape = event => {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
if ($searchInput) $searchInput.focus()
|
||||
}, 100)
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
fixSafariHeight()
|
||||
window.addEventListener('resize', fixSafariHeight)
|
||||
window.addEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
const closeSearch = () => {
|
||||
btf.overflowPaddingR.remove()
|
||||
animateElements(false)
|
||||
window.removeEventListener('resize', fixSafariHeight)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
const searchClickFn = () => {
|
||||
@@ -55,9 +91,41 @@ window.addEventListener('load', () => {
|
||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||
}
|
||||
|
||||
const cutContent = (content) => {
|
||||
const extractContentStr = content => {
|
||||
if (!content) return ''
|
||||
const firstOccur = content.indexOf('<mark>')
|
||||
if (typeof content === 'string') return content.trim()
|
||||
if (typeof content === 'object') {
|
||||
if (content.value !== undefined) {
|
||||
const str = String(content.value).trim()
|
||||
return str || ''
|
||||
}
|
||||
if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) return ''
|
||||
try {
|
||||
const str = JSON.stringify(content).trim()
|
||||
return (str === '{}' || str === '[]' || str === '""') ? '' : str
|
||||
} catch (e) { return '' }
|
||||
}
|
||||
if (content.toString && typeof content.toString === 'function') {
|
||||
const str = content.toString().trim()
|
||||
return (str === '[object Object]' || str === '[object Array]') ? '' : str
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const extractHighlightValue = highlightObj => {
|
||||
if (!highlightObj) return ''
|
||||
if (typeof highlightObj === 'string') return highlightObj.trim()
|
||||
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
||||
return String(highlightObj.value).trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const cutContent = content => {
|
||||
const contentStr = extractContentStr(content)
|
||||
if (!contentStr) return ''
|
||||
|
||||
const firstOccur = contentStr.indexOf('<mark>')
|
||||
let start = firstOccur - 30
|
||||
let end = firstOccur + 120
|
||||
let pre = ''
|
||||
@@ -70,94 +138,329 @@ window.addEventListener('load', () => {
|
||||
pre = '...'
|
||||
}
|
||||
|
||||
if (end > content.length) {
|
||||
end = content.length
|
||||
if (end > contentStr.length) {
|
||||
end = contentStr.length
|
||||
} else {
|
||||
post = '...'
|
||||
}
|
||||
|
||||
return `${pre}${content.substring(start, end)}${post}`
|
||||
let substr = contentStr.substring(start, end)
|
||||
|
||||
// Remove incomplete tags at boundaries
|
||||
const firstCloseBracket = substr.indexOf('>')
|
||||
const firstOpenBracket = substr.indexOf('<')
|
||||
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
||||
substr = substr.substring(firstCloseBracket + 1)
|
||||
}
|
||||
|
||||
const lastOpenBracket = substr.lastIndexOf('<')
|
||||
const lastCloseBracket = substr.lastIndexOf('>')
|
||||
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
||||
substr = substr.substring(0, lastOpenBracket)
|
||||
}
|
||||
|
||||
// Balance tags using regex
|
||||
const tagStack = []
|
||||
let balancedStr = ''
|
||||
let lastIndex = 0
|
||||
let match
|
||||
|
||||
TAG_REGEX.lastIndex = 0
|
||||
while ((match = TAG_REGEX.exec(substr)) !== null) {
|
||||
const fullTag = match[0]
|
||||
const tagName = match[1]
|
||||
const tagStart = match.index
|
||||
|
||||
// Append text before this tag
|
||||
balancedStr += substr.substring(lastIndex, tagStart)
|
||||
|
||||
if (fullTag.startsWith('</')) {
|
||||
// Closing tag - remove matching opening tag from stack
|
||||
const idx = tagStack.lastIndexOf(tagName)
|
||||
if (idx !== -1) tagStack.splice(idx, 1)
|
||||
} else if (!fullTag.endsWith('/>') && !fullTag.startsWith('<!')) {
|
||||
// Opening tag (not self-closing or comment)
|
||||
tagStack.push(tagName)
|
||||
}
|
||||
balancedStr += fullTag
|
||||
lastIndex = tagStart + fullTag.length
|
||||
}
|
||||
balancedStr += substr.substring(lastIndex)
|
||||
|
||||
// Close unclosed tags
|
||||
for (let i = tagStack.length - 1; i >= 0; i--) {
|
||||
balancedStr += `</${tagStack[i]}>`
|
||||
}
|
||||
|
||||
// Check if we cut a mark tag at the beginning
|
||||
if (start > 0 || pre) {
|
||||
const checkStart = Math.max(0, start - 30)
|
||||
const actualFirstOpenBracket = contentStr.indexOf('<', checkStart)
|
||||
const actualFirstMark = contentStr.indexOf('<mark>', checkStart)
|
||||
if (actualFirstOpenBracket !== -1 && (actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
|
||||
pre = '...'
|
||||
}
|
||||
}
|
||||
|
||||
return `${pre}${balancedStr}${post}`
|
||||
}
|
||||
|
||||
const disableDiv = [
|
||||
document.getElementById('algolia-hits'),
|
||||
document.getElementById('algolia-pagination'),
|
||||
document.querySelector('#algolia-info .algolia-stats')
|
||||
]
|
||||
let searchClient
|
||||
|
||||
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
|
||||
const search = instantsearch({
|
||||
indexName,
|
||||
searchClient: searchClient(appId, apiKey),
|
||||
searchFunction (helper) {
|
||||
disableDiv.forEach(item => {
|
||||
item.style.display = helper.state.query ? '' : 'none'
|
||||
})
|
||||
if (helper.state.query) helper.search()
|
||||
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
|
||||
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
|
||||
} else if (typeof window.algoliasearch === 'function') {
|
||||
searchClient = window.algoliasearch(appId, apiKey)
|
||||
} else {
|
||||
return console.error('Algolia search client not found!')
|
||||
}
|
||||
|
||||
if (!searchClient) {
|
||||
return console.error('Failed to initialize Algolia search client')
|
||||
}
|
||||
|
||||
let currentQuery = ''
|
||||
let searchRequestId = 0 // Race condition guard
|
||||
|
||||
const toggleResultsVisibility = hasResults => {
|
||||
$pagination.style.display = hasResults ? '' : 'none'
|
||||
$stats.style.display = hasResults ? '' : 'none'
|
||||
}
|
||||
|
||||
const renderHits = (hits, query, page = 0) => {
|
||||
if (hits.length === 0 && query) {
|
||||
$hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
|
||||
$hitsEmpty.style.display = ''
|
||||
$hitsWrapper.style.display = 'none'
|
||||
$stats.style.display = 'none'
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const widgets = [
|
||||
instantsearch.widgets.configure({ hitsPerPage }),
|
||||
instantsearch.widgets.searchBox({
|
||||
container: '#algolia-search-input',
|
||||
showReset: false,
|
||||
showSubmit: false,
|
||||
placeholder: languages.input_placeholder,
|
||||
showLoadingIndicator: true
|
||||
}),
|
||||
instantsearch.widgets.hits({
|
||||
container: '#algolia-hits',
|
||||
templates: {
|
||||
item (data) {
|
||||
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
||||
const result = data._highlightResult
|
||||
const content = result.contentStripTruncate
|
||||
? cutContent(result.contentStripTruncate.value)
|
||||
: result.contentStrip
|
||||
? cutContent(result.contentStrip.value)
|
||||
: result.content
|
||||
? cutContent(result.content.value)
|
||||
: ''
|
||||
return `
|
||||
<a href="${link}" class="algolia-hit-item-link">
|
||||
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||
</a>`
|
||||
},
|
||||
empty (data) {
|
||||
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
||||
$hitsEmpty.style.display = 'none'
|
||||
|
||||
const hitsHTML = hits.map((hit, index) => {
|
||||
const itemNumber = page * hitsPerPage + index + 1
|
||||
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
|
||||
const result = hit._highlightResult || hit
|
||||
|
||||
// Content extraction - try highlight result first, then raw hit
|
||||
let content = ''
|
||||
for (const field of CONTENT_FIELDS) {
|
||||
if (result[field]) { content = cutContent(result[field]); break }
|
||||
if (hit[field]) { content = cutContent(hit[field]); break }
|
||||
}
|
||||
|
||||
// Title handling - try highlight result first, then raw hit
|
||||
let title = 'no-title'
|
||||
const titleSource = result.title || hit.title
|
||||
if (titleSource) {
|
||||
title = extractHighlightValue(titleSource) || 'no-title'
|
||||
}
|
||||
if (title === 'no-title') {
|
||||
if (typeof hit.title === 'string' && hit.title.trim()) {
|
||||
title = hit.title.trim()
|
||||
} else if (hit.title?.value) {
|
||||
title = String(hit.title.value).trim() || 'no-title'
|
||||
}
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.stats({
|
||||
container: '#algolia-info > .algolia-stats',
|
||||
templates: {
|
||||
text (data) {
|
||||
const stats = languages.hits_stats
|
||||
.replace(/\$\{hits}/, data.nbHits)
|
||||
.replace(/\$\{time}/, data.processingTimeMS)
|
||||
return `<hr>${stats}`
|
||||
}
|
||||
|
||||
return `<li class="ais-Hits-item" value="${itemNumber}">
|
||||
<a href="${link}" class="algolia-hit-item-link">
|
||||
<span class="algolia-hits-item-title">${title}</span>
|
||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||
</a>
|
||||
</li>`
|
||||
}).join('')
|
||||
|
||||
$hitsList.innerHTML = hitsHTML
|
||||
$hitsWrapper.style.display = query ? '' : 'none'
|
||||
|
||||
if (hits.length > 0) {
|
||||
$stats.style.display = ''
|
||||
}
|
||||
}
|
||||
|
||||
const renderPagination = (page, nbPages) => {
|
||||
if (nbPages <= 1) {
|
||||
$pagination.style.display = 'none'
|
||||
$paginationList.innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
const isFirstPage = page === 0
|
||||
const isLastPage = page === nbPages - 1
|
||||
|
||||
// Responsive page display
|
||||
const isMobile = window.innerWidth < 768
|
||||
const maxVisiblePages = isMobile ? 3 : 5
|
||||
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||
|
||||
// Adjust starting page to maintain max visible pages
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||
}
|
||||
|
||||
const parts = []
|
||||
|
||||
// Only add ellipsis and first page when there are many pages
|
||||
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a></li>')
|
||||
if (startPage > 1) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.poweredBy({
|
||||
container: '#algolia-info > .algolia-poweredBy'
|
||||
}),
|
||||
instantsearch.widgets.pagination({
|
||||
container: '#algolia-pagination',
|
||||
totalPages: 5,
|
||||
templates: {
|
||||
first: '<i class="fas fa-angle-double-left"></i>',
|
||||
last: '<i class="fas fa-angle-double-right"></i>',
|
||||
previous: '<i class="fas fa-angle-left"></i>',
|
||||
next: '<i class="fas fa-angle-right"></i>'
|
||||
}
|
||||
|
||||
// Add middle page numbers
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
if (i === page) {
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected"><span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span></li>`)
|
||||
} else {
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
// Only add ellipsis and last page when there are many pages
|
||||
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||
if (endPage < nbPages - 2) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||
}
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a></li>`)
|
||||
}
|
||||
|
||||
// Build prev/next links
|
||||
const prevLink = isFirstPage
|
||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||
const nextLink = isLastPage
|
||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||
|
||||
$paginationList.innerHTML = `<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">${prevLink}</li>${parts.join('')}<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">${nextLink}</li>`
|
||||
$pagination.style.display = currentQuery ? '' : 'none'
|
||||
}
|
||||
|
||||
const renderStats = (nbHits, processingTimeMS, query) => {
|
||||
if (query) {
|
||||
const stats = languages.hits_stats
|
||||
.replace(/\$\{hits}/, nbHits)
|
||||
.replace(/\$\{time}/, processingTimeMS)
|
||||
$stats.innerHTML = `<hr>${stats}`
|
||||
$stats.style.display = ''
|
||||
} else {
|
||||
$stats.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
const performSearch = async (query, page = 0) => {
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
if (!trimmedQuery) {
|
||||
currentQuery = ''
|
||||
searchRequestId++
|
||||
renderHits([], '', 0)
|
||||
renderPagination(0, 0)
|
||||
renderStats(0, 0, '')
|
||||
toggleResultsVisibility(false)
|
||||
return
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
currentQuery = trimmedQuery
|
||||
const requestId = ++searchRequestId
|
||||
|
||||
try {
|
||||
let result
|
||||
|
||||
if (searchClient && typeof searchClient.search === 'function') {
|
||||
// v5 multi-index search
|
||||
const searchResult = await searchClient.search([{
|
||||
indexName,
|
||||
query: trimmedQuery,
|
||||
params: { page, hitsPerPage, ...HIGHLIGHT_PARAMS }
|
||||
}])
|
||||
result = searchResult.results[0]
|
||||
} else if (searchClient && typeof searchClient.initIndex === 'function') {
|
||||
// v4 single-index search
|
||||
const index = searchClient.initIndex(indexName)
|
||||
result = await index.search(trimmedQuery, { page, hitsPerPage, ...HIGHLIGHT_PARAMS })
|
||||
} else {
|
||||
throw new Error('Algolia: No compatible search method available')
|
||||
}
|
||||
|
||||
// Discard stale results from superseded searches
|
||||
if (requestId !== searchRequestId) return
|
||||
|
||||
renderHits(result.hits || [], trimmedQuery, page)
|
||||
|
||||
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
|
||||
renderPagination(page, actualNbPages)
|
||||
renderStats(result.nbHits || 0, result.processingTimeMS || 0, trimmedQuery)
|
||||
|
||||
const hasResults = result.hits && result.hits.length > 0
|
||||
toggleResultsVisibility(hasResults)
|
||||
|
||||
// Refresh Pjax links
|
||||
if (window.pjax) {
|
||||
window.pjax.refresh($hits)
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== searchRequestId) return
|
||||
console.error('Algolia search error:', error)
|
||||
renderHits([], trimmedQuery, page)
|
||||
renderPagination(0, 0)
|
||||
renderStats(0, 0, trimmedQuery)
|
||||
} finally {
|
||||
if (requestId === searchRequestId) {
|
||||
showLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimeout
|
||||
const debouncedSearch = (query, delay = 300) => {
|
||||
clearTimeout(searchTimeout)
|
||||
// Empty query: clear results immediately without debounce delay
|
||||
if (!query.trim()) {
|
||||
performSearch(query)
|
||||
return
|
||||
}
|
||||
searchTimeout = setTimeout(() => performSearch(query), delay)
|
||||
}
|
||||
|
||||
const initializeSearch = () => {
|
||||
showLoading(false)
|
||||
|
||||
if ($searchInput) {
|
||||
$searchInput.addEventListener('input', e => {
|
||||
debouncedSearch(e.target.value)
|
||||
})
|
||||
}
|
||||
|
||||
if ($searchForm) {
|
||||
$searchForm.addEventListener('submit', e => {
|
||||
e.preventDefault()
|
||||
performSearch($searchInput ? $searchInput.value : '')
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination event delegation
|
||||
$pagination.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
const link = e.target.closest('a[data-page]')
|
||||
if (link) {
|
||||
const page = parseInt(link.dataset.page, 10)
|
||||
if (!isNaN(page) && currentQuery) {
|
||||
performSearch(currentQuery, page)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
search.addWidgets(widgets)
|
||||
search.start()
|
||||
// Initial state
|
||||
toggleResultsVisibility(false)
|
||||
}
|
||||
|
||||
initializeSearch()
|
||||
searchClickFn()
|
||||
searchFnOnce()
|
||||
|
||||
@@ -165,10 +468,4 @@ window.addEventListener('load', () => {
|
||||
if (!btf.isHidden($searchMask)) closeSearch()
|
||||
searchClickFn()
|
||||
})
|
||||
|
||||
if (window.pjax) {
|
||||
search.on('render', () => {
|
||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,31 +15,39 @@ class LocalSearch {
|
||||
this.top_n_per_article = top_n_per_article
|
||||
this.isfetched = false
|
||||
this.datas = null
|
||||
this._unescapeDiv = unescape ? document.createElement('div') : null
|
||||
this._processedKeywords = null
|
||||
}
|
||||
|
||||
_processKeywords (keywords) {
|
||||
if (this._processedKeywords) return this._processedKeywords
|
||||
this._processedKeywords = keywords.map(word => {
|
||||
if (this.unescape) {
|
||||
this._unescapeDiv.innerText = word
|
||||
return this._unescapeDiv.innerHTML
|
||||
}
|
||||
return word
|
||||
})
|
||||
return this._processedKeywords
|
||||
}
|
||||
|
||||
getIndexByWord (words, text, caseSensitive = false) {
|
||||
const index = []
|
||||
const included = new Set()
|
||||
const processedWords = this._processKeywords(words)
|
||||
|
||||
if (!caseSensitive) {
|
||||
text = text.toLowerCase()
|
||||
}
|
||||
words.forEach(word => {
|
||||
if (this.unescape) {
|
||||
const div = document.createElement('div')
|
||||
div.innerText = word
|
||||
word = div.innerHTML
|
||||
}
|
||||
processedWords.forEach((word, i) => {
|
||||
const wordLen = word.length
|
||||
if (wordLen === 0) return
|
||||
let startPosition = 0
|
||||
let position = -1
|
||||
if (!caseSensitive) {
|
||||
word = word.toLowerCase()
|
||||
}
|
||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
||||
const searchWord = caseSensitive ? word : word.toLowerCase()
|
||||
while ((position = text.indexOf(searchWord, startPosition)) > -1) {
|
||||
index.push({ position, word })
|
||||
included.add(word)
|
||||
included.add(words[i])
|
||||
startPosition = position + wordLen
|
||||
}
|
||||
})
|
||||
@@ -90,19 +98,22 @@ class LocalSearch {
|
||||
|
||||
// Highlight title and content
|
||||
highlightKeyword (val, slice) {
|
||||
let result = ''
|
||||
const parts = []
|
||||
let index = slice.start
|
||||
for (const { position, length } of slice.hits) {
|
||||
result += val.substring(index, position)
|
||||
parts.push(val.substring(index, position))
|
||||
index = position + length
|
||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
||||
parts.push(`<mark class="search-keyword">${val.substring(position, position + length)}</mark>`)
|
||||
}
|
||||
result += val.substring(index, slice.end)
|
||||
return result
|
||||
parts.push(val.substring(index, slice.end))
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
getResultItems (keywords) {
|
||||
const resultItems = []
|
||||
this._processedKeywords = null
|
||||
// Compute highlight param once instead of per-article
|
||||
const highlightParam = keywords.join(' ')
|
||||
this.datas.forEach(({ title, content, url }) => {
|
||||
// The number of different keywords included in the article.
|
||||
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||
@@ -147,7 +158,7 @@ class LocalSearch {
|
||||
let resultItem = ''
|
||||
|
||||
url = new URL(url, location.origin)
|
||||
url.searchParams.append('highlight', keywords.join(' '))
|
||||
url.searchParams.append('highlight', highlightParam)
|
||||
|
||||
if (slicesOfTitle.length !== 0) {
|
||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||
@@ -156,10 +167,10 @@ class LocalSearch {
|
||||
}
|
||||
|
||||
slicesOfContent.forEach(slice => {
|
||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
|
||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
|
||||
})
|
||||
|
||||
resultItem += '</li>'
|
||||
resultItem += '</a></li>'
|
||||
resultItems.push({
|
||||
item: resultItem,
|
||||
id: resultItems.length,
|
||||
@@ -173,7 +184,10 @@ class LocalSearch {
|
||||
fetchData () {
|
||||
const isXml = !this.path.endsWith('json')
|
||||
fetch(this.path)
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
return response.text()
|
||||
})
|
||||
.then(res => {
|
||||
// Get the contents from search data
|
||||
this.isfetched = true
|
||||
@@ -194,6 +208,12 @@ class LocalSearch {
|
||||
// Remove loading animation
|
||||
window.dispatchEvent(new Event('search:loaded'))
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Local search data fetch failed:', error)
|
||||
this.isfetched = true
|
||||
this.datas = []
|
||||
window.dispatchEvent(new Event('search:loaded'))
|
||||
})
|
||||
}
|
||||
|
||||
// Highlight by wrapping node in mark elements with the given class name
|
||||
@@ -206,7 +226,7 @@ class LocalSearch {
|
||||
index = position + length
|
||||
const mark = document.createElement('mark')
|
||||
mark.className = className
|
||||
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
||||
mark.appendChild(document.createTextNode(val.substring(position, position + length)))
|
||||
children.push(text, mark)
|
||||
}
|
||||
node.nodeValue = val.substring(index, slice.end)
|
||||
@@ -235,41 +255,191 @@ class LocalSearch {
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Search
|
||||
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
|
||||
// Search
|
||||
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||
const enablePagination = pagination && pagination.enable
|
||||
const localSearch = new LocalSearch({
|
||||
path,
|
||||
top_n_per_article,
|
||||
unescape
|
||||
})
|
||||
|
||||
const input = document.querySelector('#local-search-input input')
|
||||
const statsItem = document.getElementById('local-search-stats-wrap')
|
||||
const $input = document.querySelector('.local-search-input input')
|
||||
const $statsItem = document.getElementById('local-search-stats')
|
||||
const $loadingStatus = document.getElementById('loading-status')
|
||||
const $searchMask = document.getElementById('search-mask')
|
||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||
const $results = document.getElementById('local-search-results')
|
||||
const $pagination = document.getElementById('local-search-pagination')
|
||||
const $paginationList = document.querySelector('#local-search-pagination .ais-Pagination-list')
|
||||
const isXml = !path.endsWith('json')
|
||||
|
||||
// Pagination variables (only initialize if pagination is enabled)
|
||||
let currentPage = 0
|
||||
const hitsPerPage = pagination.hitsPerPage || 10
|
||||
|
||||
let currentResultItems = []
|
||||
|
||||
if (!enablePagination) {
|
||||
currentPage = undefined
|
||||
currentResultItems = undefined
|
||||
}
|
||||
|
||||
// Show/hide search results area
|
||||
const toggleResultsVisibility = hasResults => {
|
||||
$pagination.style.display = (hasResults && enablePagination) ? '' : 'none'
|
||||
}
|
||||
|
||||
// Render search results for current page
|
||||
const renderResults = (searchText, resultItems) => {
|
||||
// Determine items to display based on pagination mode
|
||||
const itemsToDisplay = enablePagination
|
||||
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||
: resultItems
|
||||
|
||||
// Handle empty page in pagination mode
|
||||
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
|
||||
currentPage = 0
|
||||
renderResults(searchText, resultItems)
|
||||
return
|
||||
}
|
||||
|
||||
// Add numbering to items
|
||||
const numberedItems = itemsToDisplay.map((result, index) => {
|
||||
const itemNumber = enablePagination
|
||||
? currentPage * hitsPerPage + index + 1
|
||||
: index + 1
|
||||
return result.item.replace(
|
||||
'<li class="local-search-hit-item">',
|
||||
`<li class="local-search-hit-item" value="${itemNumber}">`
|
||||
)
|
||||
})
|
||||
|
||||
$results.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||
|
||||
// Update stats
|
||||
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||
$statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||
|
||||
// Handle pagination
|
||||
if (enablePagination) {
|
||||
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
|
||||
renderPagination(currentPage, nbPages, searchText)
|
||||
}
|
||||
|
||||
const hasResults = resultItems.length > 0
|
||||
toggleResultsVisibility(hasResults)
|
||||
|
||||
window.pjax && window.pjax.refresh($results)
|
||||
}
|
||||
|
||||
// Render pagination
|
||||
const renderPagination = (page, nbPages) => {
|
||||
if (nbPages <= 1) {
|
||||
$pagination.style.display = 'none'
|
||||
$paginationList.innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
const isFirstPage = page === 0
|
||||
const isLastPage = page === nbPages - 1
|
||||
|
||||
// Responsive page display
|
||||
const isMobile = window.innerWidth < 768
|
||||
const maxVisiblePages = isMobile ? 3 : 5
|
||||
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||
|
||||
// Adjust starting page to maintain max visible pages
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||
}
|
||||
|
||||
const parts = []
|
||||
|
||||
// Only add ellipsis and first page when there are many pages
|
||||
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a></li>')
|
||||
if (startPage > 1) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||
}
|
||||
}
|
||||
|
||||
// Add middle page numbers
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
if (i === page) {
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected"><span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span></li>`)
|
||||
} else {
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a></li>`)
|
||||
}
|
||||
}
|
||||
|
||||
// Only add ellipsis and last page when there are many pages
|
||||
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||
if (endPage < nbPages - 2) {
|
||||
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||
}
|
||||
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a></li>`)
|
||||
}
|
||||
|
||||
// Build prev/next links
|
||||
const prevLink = isFirstPage
|
||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||
const nextLink = isLastPage
|
||||
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||
|
||||
$paginationList.innerHTML = `<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">${prevLink}</li>${parts.join('')}<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">${nextLink}</li>`
|
||||
$pagination.style.display = ''
|
||||
}
|
||||
|
||||
// Clear search results and stats
|
||||
const clearSearchResults = () => {
|
||||
$results.textContent = ''
|
||||
$statsItem.textContent = ''
|
||||
toggleResultsVisibility(false)
|
||||
if (enablePagination) {
|
||||
currentResultItems = []
|
||||
currentPage = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Show no results message
|
||||
const showNoResults = searchText => {
|
||||
$results.textContent = ''
|
||||
const statsDiv = document.createElement('div')
|
||||
statsDiv.className = 'search-result-stats'
|
||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||
$statsItem.innerHTML = statsDiv.outerHTML
|
||||
toggleResultsVisibility(false)
|
||||
if (enablePagination) {
|
||||
currentResultItems = []
|
||||
currentPage = 0
|
||||
}
|
||||
}
|
||||
|
||||
const inputEventFunction = () => {
|
||||
if (!localSearch.isfetched) return
|
||||
let searchText = input.value.trim().toLowerCase()
|
||||
let searchText = $input.value.trim().toLowerCase()
|
||||
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
|
||||
|
||||
if (searchText !== '') $loadingStatus.hidden = false
|
||||
|
||||
const keywords = searchText.split(/[-\s]+/)
|
||||
const container = document.getElementById('local-search-results')
|
||||
let resultItems = []
|
||||
|
||||
if (searchText.length > 0) {
|
||||
// Perform local searching
|
||||
resultItems = localSearch.getResultItems(keywords)
|
||||
}
|
||||
|
||||
if (keywords.length === 1 && keywords[0] === '') {
|
||||
container.textContent = ''
|
||||
statsItem.textContent = ''
|
||||
clearSearchResults()
|
||||
} else if (resultItems.length === 0) {
|
||||
container.textContent = ''
|
||||
const statsDiv = document.createElement('div')
|
||||
statsDiv.className = 'search-result-stats'
|
||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||
statsItem.innerHTML = statsDiv.outerHTML
|
||||
showNoResults(searchText)
|
||||
} else {
|
||||
// Sort results by relevance
|
||||
resultItems.sort((left, right) => {
|
||||
if (left.includedCount !== right.includedCount) {
|
||||
return right.includedCount - left.includedCount
|
||||
@@ -279,54 +449,73 @@ window.addEventListener('load', () => {
|
||||
return right.id - left.id
|
||||
})
|
||||
|
||||
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
||||
|
||||
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
|
||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||
window.pjax && window.pjax.refresh(container)
|
||||
if (enablePagination) {
|
||||
currentResultItems = resultItems
|
||||
currentPage = 0
|
||||
}
|
||||
renderResults(searchText, resultItems)
|
||||
}
|
||||
|
||||
$loadingStatus.textContent = ''
|
||||
$loadingStatus.hidden = true
|
||||
}
|
||||
|
||||
// Debounced input handler
|
||||
let searchTimeout
|
||||
const debouncedInputEvent = () => {
|
||||
clearTimeout(searchTimeout)
|
||||
// Empty input: clear results immediately without debounce delay
|
||||
if (!$input.value.trim()) {
|
||||
inputEventFunction()
|
||||
return
|
||||
}
|
||||
searchTimeout = setTimeout(inputEventFunction, 200)
|
||||
}
|
||||
|
||||
let loadFlag = false
|
||||
const $searchMask = document.getElementById('search-mask')
|
||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||
|
||||
// fix safari
|
||||
const fixSafariHeight = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced resize to avoid layout thrashing
|
||||
let resizeTimer
|
||||
const onResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||
}
|
||||
|
||||
const handleEscape = event => {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
btf.overflowPaddingR.add()
|
||||
btf.animateIn($searchMask, 'to_show 0.5s')
|
||||
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
||||
setTimeout(() => { input.focus() }, 300)
|
||||
setTimeout(() => { $input.focus() }, 300)
|
||||
if (!loadFlag) {
|
||||
!localSearch.isfetched && localSearch.fetchData()
|
||||
input.addEventListener('input', inputEventFunction)
|
||||
$input.addEventListener('input', debouncedInputEvent)
|
||||
loadFlag = true
|
||||
}
|
||||
// shortcut: ESC
|
||||
document.addEventListener('keydown', function f (event) {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', f)
|
||||
}
|
||||
})
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
|
||||
fixSafariHeight()
|
||||
window.addEventListener('resize', fixSafariHeight)
|
||||
window.addEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
const closeSearch = () => {
|
||||
btf.overflowPaddingR.remove()
|
||||
btf.animateOut($searchDialog, 'search_close .5s')
|
||||
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||
window.removeEventListener('resize', fixSafariHeight)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
const searchClickFn = () => {
|
||||
@@ -340,11 +529,29 @@ window.addEventListener('load', () => {
|
||||
localSearch.fetchData()
|
||||
}
|
||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||
|
||||
// Pagination event delegation - only add if pagination is enabled
|
||||
if (enablePagination) {
|
||||
$pagination.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
const link = e.target.closest('a[data-page]')
|
||||
if (link) {
|
||||
const page = parseInt(link.dataset.page, 10)
|
||||
if (!isNaN(page) && currentResultItems.length > 0) {
|
||||
currentPage = page
|
||||
renderResults($input.value.trim().toLowerCase(), currentResultItems)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initial state
|
||||
toggleResultsVisibility(false)
|
||||
}
|
||||
|
||||
window.addEventListener('search:loaded', () => {
|
||||
const $loadDataItem = document.getElementById('loading-database')
|
||||
$loadDataItem.nextElementSibling.style.display = 'block'
|
||||
$loadDataItem.nextElementSibling.style.visibility = 'visible'
|
||||
$loadDataItem.remove()
|
||||
})
|
||||
|
||||
|
||||
+28
-15
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user