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
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk','https://jsd.012700.xyz/gh/jerryc127/CDN/Photo/wechat.jpg'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
@@ -3,17 +3,25 @@ name: npm publish
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # OIDC 必須
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm publish
|
|
||||||
|
- run: npm publish --provenance --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -1,117 +1,192 @@
|
|||||||
<div align="right">
|
<div align="right">
|
||||||
<a title="Chinese" href="/README_CN.md">中文</a>
|
<a title="中文" href="/README_CN.md">中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
|
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
|
||||||
|
|
||||||
# hexo-theme-butterfly
|
# hexo-theme-butterfly
|
||||||
|
|
||||||

|
A modern, elegant and feature-rich theme for Hexo
|
||||||

|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
📢 Demo: [Butterfly](https://butterfly.js.org/) / [CrazyWong](https://blog.crazywong.com/)
|

|
||||||
|

|
||||||
📖 Docs: [English](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) / [Chinese](https://butterfly.js.org/posts/21cfbf15/)
|
|
||||||
|
📢 **Demo**: [Butterfly Official](https://butterfly.js.org/) | [CrazyWong's Blog](https://blog.crazywong.com/)
|
||||||

|
|
||||||
|
📖 **Documentation**: [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) | [中文文档](https://butterfly.js.org/posts/21cfbf15/)
|
||||||
</div>
|
|
||||||
|

|
||||||
---
|
|
||||||
|
</div>
|
||||||
## 💻 Installation
|
|
||||||
|
---
|
||||||
### GIT
|
|
||||||
|
## 🚀 Quick Start
|
||||||
> If you are in Mainland China, you can download in [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git)
|
|
||||||
|
### 💾 Installation
|
||||||
Stable branch [recommend]:
|
|
||||||
|
#### Method 1: Git Installation (Recommended)
|
||||||
```
|
|
||||||
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
> 💡 **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:
|
||||||
Dev branch:
|
|
||||||
|
```bash
|
||||||
```
|
# Install stable version (recommended)
|
||||||
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
```
|
```
|
||||||
|
|
||||||
### NPM
|
```bash
|
||||||
|
# Install development version (early access to new features)
|
||||||
> It supports Hexo 5.0.0 or later
|
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
|
```
|
||||||
In Hexo site root directory
|
|
||||||
|
#### Method 2: NPM Installation
|
||||||
```powershell
|
|
||||||
npm i hexo-theme-butterfly
|
> ⚠️ **Note**: NPM installation only supports Hexo 5.0.0 and above
|
||||||
```
|
|
||||||
|
```bash
|
||||||
## ⚙ Configuration
|
npm install hexo-theme-butterfly
|
||||||
|
```
|
||||||
Set theme in the hexo work folder's root config file `_config.yml`:
|
|
||||||
|
### ⚙️ Theme Configuration
|
||||||
> theme: butterfly
|
|
||||||
|
1. **Enable Theme**: Modify your Hexo configuration file `_config.yml`:
|
||||||
If you don't have pug & stylus renderer, try this:
|
|
||||||
|
```yaml
|
||||||
> npm install hexo-renderer-pug hexo-renderer-stylus
|
theme: butterfly
|
||||||
|
```
|
||||||
## 🎉 Features
|
|
||||||
|
2. **Install Dependencies**: If you haven't installed pug and stylus renderers, please run:
|
||||||
- [x] Card UI Design
|
|
||||||
- [x] Rounded Design/Squared Design
|
```bash
|
||||||
- [X] Support sub-menu
|
npm install hexo-renderer-pug hexo-renderer-stylus --save
|
||||||
- [x] Two-column layout
|
```
|
||||||
- [x] Responsive Web Design
|
|
||||||
- [x] Dark Mode
|
## ✨ Theme Features
|
||||||
- [x] Pjax
|
|
||||||
- [x] Read Mode
|
### 🎨 Design Style
|
||||||
- [x] Conversion between Traditional and Simplified Chinese
|
- [x] **Card-based Design** - Modern card-style layout
|
||||||
- [X] TOC catalog is available for both computers and mobile phones
|
- [x] **Rounded/Square Design** - Customizable border styles
|
||||||
- [X] Built-in Syntax Highlighting Themes (darker/pale night/light/ocean), also support customization
|
- [x] **Responsive Design** - Perfect adaptation to all screen sizes
|
||||||
- [X] Code Blocks (Display code language/close or expand Code Blocks/Copy Button/word wrap)
|
- [x] **Two-column Layout** - Optimized reading experience
|
||||||
- [X] Disable copy/Add a Copyright Notice to the Copied Text
|
- [x] **Dark Mode** - Eye-friendly night mode
|
||||||
- [X] Search (Algolia Search/Local Search)
|
|
||||||
- [x] Mathjax and Katex
|
### 📝 Content Features
|
||||||
- [x] Built-in 404 page
|
- [x] **Multi-level Menu** - Support for secondary navigation menus
|
||||||
- [x] WordCount
|
- [x] **Reading Mode** - Focused article reading experience
|
||||||
- [x] Related articles
|
- [x] **TOC Navigation** - Desktop and mobile TOC support
|
||||||
- [x] Displays outdated notice for a post
|
- [x] **Word Count** - Display article word count and reading time
|
||||||
- [x] Share (Sharejs/Addtoany)
|
- [x] **Related Articles** - Smart recommendation of related content
|
||||||
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk)
|
- [x] **Outdated Reminder** - Automatic article update status alerts
|
||||||
- [x] Multiple Comment System Support
|
- [x] **Traditional/Simplified Chinese** - Support for Traditional and Simplified Chinese switching
|
||||||
- [x] Online Chats (Chatra/Tidio/Crisp)
|
- [x] **Tag Plugins** - Rich tag plugin support
|
||||||
- [x] Web analytics
|
|
||||||
- [x] Google AdSense
|
### 🔍 Search & Navigation
|
||||||
- [x] Webmaster Verification
|
- [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch
|
||||||
- [x] Change website colour scheme
|
- [x] **Built-in 404** - Beautiful 404 error page
|
||||||
- [x] Typewriter Effect: activate_power_mode
|
- [x] **Pjax Support** - Smooth page transition experience
|
||||||
- [x] Background effects (Canvas ribbon/canvas_ribbon_piao/canvas_nest)
|
|
||||||
- [x] Mouse click effects (Fireworks/Heart/Text)
|
### 🎨 Code Display
|
||||||
- [x] Preloader/Loading Animation/pace.js
|
- [x] **Syntax Highlighting** - Built-in multiple themes (darker/pale night/light/ocean)
|
||||||
- [x] Busuanzi visitor counter
|
- [x] **Code Features** - Language display/fold expand/copy button/auto-wrap
|
||||||
- [x] Medium Zoom/Fancybox
|
- [x] **Math Formulas** - Support for Mathjax and Katex
|
||||||
- [x] Mermaid
|
|
||||||
- [x] Chart.js
|
### 💬 Social Interaction
|
||||||
- [x] Justified Gallery
|
- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
|
||||||
- [x] Lazyload images
|
- [x] **Dual Comments Support** - Enable two comment systems simultaneously
|
||||||
- [x] Instantpage/Snackbar notification toast/PWA......
|
- [x] **Share Features** - Sharejs/Addtoany sharing components
|
||||||
|
- [x] **Live Chat** - Chatra/Tidio/Crisp instant messaging
|
||||||
## ✨ Contributors
|
|
||||||
|
### 📊 Analytics & Statistics
|
||||||
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors">
|
- [x] **Visit Statistics** - Busuanzi counter
|
||||||
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
|
- [x] **Site Analytics** - Google Analytics/Baidu Analytics/Cloudflare Analytics/Microsoft Clarity/Umami
|
||||||
</a>
|
- [x] **Webmaster Verification** - Major search engine verification
|
||||||
|
- [x] **Ad Support** - Google AdSense/custom ad slots
|
||||||
## 📷 Screenshots
|
|
||||||
|
### 🎪 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] **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>
|
||||||
|
|||||||
+148
-73
@@ -4,114 +4,189 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
|
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
|
||||||
|
|
||||||
# hexo-theme-butterfly
|
# hexo-theme-butterfly
|
||||||
|
|
||||||
|
一個適用於 Hexo 的現代化、美觀且功能豐富的主題
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

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

|
### 🎨 程式碼展示
|
||||||

|
- [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] **系列文章** - 系列文章組織功能
|
||||||
|
- [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>
|
||||||
|
|||||||
+26
-15
@@ -91,7 +91,8 @@ category_per_img:
|
|||||||
footer_img: false
|
footer_img: false
|
||||||
|
|
||||||
# Website Background
|
# 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:
|
background:
|
||||||
|
|
||||||
cover:
|
cover:
|
||||||
@@ -318,6 +319,7 @@ aside:
|
|||||||
# If set 0 will show all
|
# If set 0 will show all
|
||||||
limit: 40
|
limit: 40
|
||||||
color: false
|
color: false
|
||||||
|
custom_colors:
|
||||||
# Order of tags, random/name/length
|
# Order of tags, random/name/length
|
||||||
orderby: random
|
orderby: random
|
||||||
# Sort of order. 1, asc for ascending; -1, desc for descending
|
# Sort of order. 1, asc for ascending; -1, desc for descending
|
||||||
@@ -488,6 +490,11 @@ search:
|
|||||||
top_n_per_article: 1
|
top_n_per_article: 1
|
||||||
# Unescape html strings to the readable one.
|
# Unescape html strings to the readable one.
|
||||||
unescape: false
|
unescape: false
|
||||||
|
# Enable pagination for search results
|
||||||
|
pagination:
|
||||||
|
enable: false
|
||||||
|
# Number of search results per page
|
||||||
|
hitsPerPage: 8
|
||||||
CDN:
|
CDN:
|
||||||
|
|
||||||
# Docsearch
|
# Docsearch
|
||||||
@@ -510,12 +517,12 @@ share:
|
|||||||
# Share.js
|
# Share.js
|
||||||
# https://github.com/overtrue/share.js
|
# https://github.com/overtrue/share.js
|
||||||
sharejs:
|
sharejs:
|
||||||
sites: facebook,twitter,wechat,weibo,qq
|
sites: facebook,x,wechat,weibo,qq
|
||||||
|
|
||||||
# AddToAny
|
# AddToAny
|
||||||
# https://www.addtoany.com/
|
# https://www.addtoany.com/
|
||||||
addtoany:
|
addtoany:
|
||||||
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link
|
item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Comments System
|
# Comments System
|
||||||
@@ -652,7 +659,7 @@ artalk:
|
|||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
|
|
||||||
chat:
|
chat:
|
||||||
# Choose: chatra/tidio/crisp
|
# Choose: chatra/tidio/crisp/knocket
|
||||||
# Leave it empty if you don't need chat
|
# Leave it empty if you don't need chat
|
||||||
use:
|
use:
|
||||||
# Chat Button [recommend]
|
# Chat Button [recommend]
|
||||||
@@ -673,6 +680,10 @@ tidio:
|
|||||||
crisp:
|
crisp:
|
||||||
website_id:
|
website_id:
|
||||||
|
|
||||||
|
# https://trtc.io/solutions/knocket
|
||||||
|
knocket:
|
||||||
|
identifier:
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Analysis
|
# Analysis
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
@@ -694,6 +705,7 @@ umami_analytics:
|
|||||||
enable: false
|
enable: false
|
||||||
# For self-hosted setups, configure the hostname of the Umami instance
|
# For self-hosted setups, configure the hostname of the Umami instance
|
||||||
serverURL:
|
serverURL:
|
||||||
|
script_name: script.js
|
||||||
website_id:
|
website_id:
|
||||||
option:
|
option:
|
||||||
UV_PV:
|
UV_PV:
|
||||||
@@ -932,6 +944,10 @@ mermaid:
|
|||||||
theme:
|
theme:
|
||||||
light: default
|
light: default
|
||||||
dark: dark
|
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
|
# chartjs
|
||||||
# see https://www.chartjs.org/docs/latest/
|
# see https://www.chartjs.org/docs/latest/
|
||||||
@@ -977,11 +993,6 @@ pjax:
|
|||||||
exclude:
|
exclude:
|
||||||
# - /xxxxxx/
|
# - /xxxxxx/
|
||||||
|
|
||||||
# Inject the css and script (aplayer/meting)
|
|
||||||
aplayerInject:
|
|
||||||
enable: false
|
|
||||||
per_page: true
|
|
||||||
|
|
||||||
# Snackbar - Toast Notification
|
# Snackbar - Toast Notification
|
||||||
# https://github.com/polonel/SnackBar
|
# https://github.com/polonel/SnackBar
|
||||||
# position: top-left / top-center / top-right / bottom-left / bottom-center / bottom-right
|
# position: top-left / top-center / top-right / bottom-left / bottom-center / bottom-right
|
||||||
@@ -1033,7 +1044,11 @@ Open_Graph_meta:
|
|||||||
|
|
||||||
# Structured Data
|
# Structured Data
|
||||||
# https://developers.google.com/search/docs/guides/intro-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
|
# Add the vendor prefixes to ensure compatibility
|
||||||
css_prefix: true
|
css_prefix: true
|
||||||
@@ -1057,7 +1072,7 @@ CDN:
|
|||||||
third_party_provider: jsdelivr
|
third_party_provider: jsdelivr
|
||||||
|
|
||||||
# Add version number to url, true or false
|
# Add version number to url, true or false
|
||||||
version: false
|
version: true
|
||||||
|
|
||||||
# Custom format
|
# Custom format
|
||||||
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
|
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
|
||||||
@@ -1068,8 +1083,6 @@ CDN:
|
|||||||
# activate_power_mode:
|
# activate_power_mode:
|
||||||
# algolia_js:
|
# algolia_js:
|
||||||
# algolia_search:
|
# algolia_search:
|
||||||
# aplayer_css:
|
|
||||||
# aplayer_js:
|
|
||||||
# artalk_css:
|
# artalk_css:
|
||||||
# artalk_js:
|
# artalk_js:
|
||||||
# blueimp_md5:
|
# blueimp_md5:
|
||||||
@@ -1093,7 +1106,6 @@ CDN:
|
|||||||
# gitalk_css:
|
# gitalk_css:
|
||||||
# giscus:
|
# giscus:
|
||||||
# instantpage:
|
# instantpage:
|
||||||
# instantsearch:
|
|
||||||
# katex:
|
# katex:
|
||||||
# katex_copytex:
|
# katex_copytex:
|
||||||
# lazyload:
|
# lazyload:
|
||||||
@@ -1103,7 +1115,6 @@ CDN:
|
|||||||
# mathjax:
|
# mathjax:
|
||||||
# medium_zoom:
|
# medium_zoom:
|
||||||
# mermaid:
|
# mermaid:
|
||||||
# meting_js:
|
|
||||||
# prismjs_autoloader:
|
# prismjs_autoloader:
|
||||||
# prismjs_js:
|
# prismjs_js:
|
||||||
# prismjs_lineNumber_js:
|
# prismjs_lineNumber_js:
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 前へ
|
prev: 前へ
|
||||||
next: 次へ
|
next: 次へ
|
||||||
|
page_info: '${current} ページ / 合計 ${total} ページ'
|
||||||
|
|
||||||
comment: コメント
|
comment: コメント
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 이전
|
prev: 이전
|
||||||
next: 다음
|
next: 다음
|
||||||
|
page_info: '${current} 페이지 / 총 ${total} 페이지'
|
||||||
|
|
||||||
comment: 댓글
|
comment: 댓글
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 页 / 共 ${total} 页'
|
||||||
|
|
||||||
comment: 评论
|
comment: 评论
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一頁
|
prev: 上一頁
|
||||||
next: 下一頁
|
next: 下一頁
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ div
|
|||||||
!= partial("includes/third-party/effect", {}, { cache: true })
|
!= partial("includes/third-party/effect", {}, { cache: true })
|
||||||
!= partial("includes/third-party/chat/index", {}, { cache: true })
|
!= partial("includes/third-party/chat/index", {}, { cache: true })
|
||||||
|
|
||||||
if theme.aplayerInject && theme.aplayerInject.enable
|
|
||||||
if theme.pjax.enable || theme.aplayerInject.per_page || page.aplayer
|
|
||||||
include ./third-party/aplayer.pug
|
|
||||||
|
|
||||||
if theme.pjax.enable
|
if theme.pjax.enable
|
||||||
!= partial("includes/third-party/pjax", {}, { cache: true })
|
!= partial("includes/third-party/pjax", {}, { cache: true })
|
||||||
|
|
||||||
@@ -52,7 +48,7 @@ div
|
|||||||
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
||||||
|
|
||||||
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
||||||
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
script(async data-pjax src=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 })
|
!= partial('includes/third-party/search/index', {}, { cache: true })
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ if nav
|
|||||||
- const sinceYear = owner.since
|
- const sinceYear = owner.since
|
||||||
span.copyright
|
span.copyright
|
||||||
if sinceYear && sinceYear != currentYear
|
if sinceYear && sinceYear != currentYear
|
||||||
!= `©${sinceYear} - ${currentYear} By ${config.author}`
|
!= `© ${sinceYear} - ${currentYear} By ${config.author}`
|
||||||
else
|
else
|
||||||
!= `©${currentYear} By ${config.author}`
|
!= `© ${currentYear} By ${config.author}`
|
||||||
if copyright.enable
|
if copyright.enable
|
||||||
- const v = copyright.version ? getVersion() : false
|
- const v = copyright.version ? getVersion() : false
|
||||||
span.framework-info
|
span.framework-info
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
hitsPerPage: theme.search.algolia_search.hitsPerPage,
|
hitsPerPage: theme.search.algolia_search.hitsPerPage,
|
||||||
// search languages
|
// search languages
|
||||||
languages: {
|
languages: {
|
||||||
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
|
|
||||||
hits_empty: _p("search.algolia_search.hits_empty"),
|
hits_empty: _p("search.algolia_search.hits_empty"),
|
||||||
hits_stats: _p("search.algolia_search.hits_stats"),
|
hits_stats: _p("search.algolia_search.hits_stats"),
|
||||||
}
|
}
|
||||||
@@ -19,12 +18,16 @@
|
|||||||
|
|
||||||
let localSearch = 'undefined'
|
let localSearch = 'undefined'
|
||||||
if (theme.search.use === 'local_search') {
|
if (theme.search.use === 'local_search') {
|
||||||
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search
|
const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
|
||||||
localSearch = JSON.stringify({
|
localSearch = JSON.stringify({
|
||||||
path: CDN || config.root + config.search.path,
|
path: CDN || config.root + config.search.path,
|
||||||
preload,
|
preload,
|
||||||
top_n_per_article,
|
top_n_per_article,
|
||||||
unescape,
|
unescape,
|
||||||
|
pagination: {
|
||||||
|
enable: pagination.enable,
|
||||||
|
hitsPerPage: pagination.hitsPerPage
|
||||||
|
},
|
||||||
languages: {
|
languages: {
|
||||||
// search languages
|
// search languages
|
||||||
hits_empty: _p("search.local_search.hits_empty"),
|
hits_empty: _p("search.local_search.hits_empty"),
|
||||||
@@ -69,20 +72,16 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight = 'undefined'
|
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
|
||||||
let syntaxHighlighter = config.syntax_highlighter
|
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
|
||||||
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
|
let highlight = JSON.stringify({
|
||||||
if (highlightEnable) {
|
plugin: highlightProvider,
|
||||||
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
|
highlightCopy: copy,
|
||||||
highlight = JSON.stringify({
|
highlightLang: language,
|
||||||
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
|
highlightHeightLimit: height_limit,
|
||||||
highlightCopy: copy,
|
highlightFullpage: fullpage,
|
||||||
highlightLang: language,
|
highlightMacStyle: macStyle
|
||||||
highlightHeightLimit: height_limit,
|
})
|
||||||
highlightFullpage: fullpage,
|
|
||||||
highlightMacStyle: macStyle
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
script.
|
script.
|
||||||
const GLOBAL_CONFIG = {
|
const GLOBAL_CONFIG = {
|
||||||
|
|||||||
@@ -40,10 +40,22 @@ if theme.structured_data
|
|||||||
const isRootOrSubdomain = currentPath.split('/').filter(Boolean).length === 0;
|
const isRootOrSubdomain = currentPath.split('/').filter(Boolean).length === 0;
|
||||||
|
|
||||||
if (isRootOrSubdomain) {
|
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 = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": config.title,
|
"name": config.title,
|
||||||
|
"alternateName": alternateNames,
|
||||||
"url": full_url_for('/'),
|
"url": full_url_for('/'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,29 @@ html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside
|
|||||||
!=partial('includes/loading/index', {}, {cache: true})
|
!=partial('includes/loading/index', {}, {cache: true})
|
||||||
|
|
||||||
if theme.background
|
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})
|
!=partial('includes/sidebar', {}, {cache: true})
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ script.
|
|||||||
const $body = document.body
|
const $body = document.body
|
||||||
const preloader = {
|
const preloader = {
|
||||||
endLoading: () => {
|
endLoading: () => {
|
||||||
|
if ($loadingBox.classList.contains('loaded')) return
|
||||||
$body.style.overflow = ''
|
$body.style.overflow = ''
|
||||||
$loadingBox.classList.add('loaded')
|
$loadingBox.classList.add('loaded')
|
||||||
},
|
},
|
||||||
@@ -24,7 +25,15 @@ script.
|
|||||||
}
|
}
|
||||||
|
|
||||||
preloader.initLoading()
|
preloader.initLoading()
|
||||||
window.addEventListener('load', preloader.endLoading)
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
preloader.endLoading()
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', preloader.endLoading)
|
||||||
|
document.addEventListener('DOMContentLoaded', preloader.endLoading)
|
||||||
|
// Add timeout protection: force end after 7 seconds
|
||||||
|
setTimeout(preloader.endLoading, 7000)
|
||||||
|
}
|
||||||
|
|
||||||
if (!{theme.pjax && theme.pjax.enable}) {
|
if (!{theme.pjax && theme.pjax.enable}) {
|
||||||
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
|
btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mixin indexPostUI()
|
mixin indexPostUI()
|
||||||
- const indexLayout = theme.index_layout
|
- 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-posts.recent-posts.nc(class=masonryLayoutClass)
|
||||||
.recent-post-items
|
.recent-post-items
|
||||||
each article, index in page.posts.data
|
each article, index in page.posts.data
|
||||||
@@ -8,17 +8,17 @@ mixin indexPostUI()
|
|||||||
- const link = article.link || article.path
|
- const link = article.link || article.path
|
||||||
- const title = article.title || _p('no_title')
|
- const title = article.title || _p('no_title')
|
||||||
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
||||||
- const post_cover = article.cover
|
- const postCover = article.cover
|
||||||
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-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)
|
.post_cover(class=leftOrRight)
|
||||||
a(href=url_for(link) title=title)
|
a(href=url_for(link) title=title)
|
||||||
if article.cover_type === 'img'
|
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
|
else
|
||||||
div.post-bg(style=`background: ${post_cover}`)
|
div.post-bg(style=`background: ${postCover}`)
|
||||||
.recent-post-info(class=no_cover)
|
.recent-post-info(class=noCover)
|
||||||
a.article-title(href=url_for(link) title=title)
|
a.article-title(href=url_for(link) title=title)
|
||||||
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
||||||
i.fas.fa-thumbtack.sticky
|
i.fas.fa-thumbtack.sticky
|
||||||
@@ -35,13 +35,13 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label=_p('post.updated')
|
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)
|
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
|
else
|
||||||
- const data_type_updated = theme.post_meta.page.date_type === 'updated'
|
- const isUpdatedType = theme.post_meta.page.date_type === 'updated'
|
||||||
- const date_type = data_type_updated ? 'updated' : 'date'
|
- const dateType = isUpdatedType ? 'updated' : 'date'
|
||||||
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
|
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
|
||||||
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
|
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
|
||||||
i(class=date_icon)
|
i(class=dateIcon)
|
||||||
span.article-meta-label= date_title
|
span.article-meta-label= dateTitle
|
||||||
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format)
|
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
|
if theme.post_meta.page.categories && article.categories.data.length > 0
|
||||||
span.article-meta
|
span.article-meta
|
||||||
span.article-meta-separator |
|
span.article-meta-separator |
|
||||||
@@ -69,7 +69,10 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label= ' ' + _p('card_post_count')
|
span.article-meta-label= ' ' + _p('card_post_count')
|
||||||
|
|
||||||
if theme.comments.card_post_count && theme.comments.use
|
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 'Disqus'
|
||||||
when 'Disqusjs'
|
when 'Disqusjs'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
@@ -77,30 +80,30 @@ mixin indexPostUI()
|
|||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Valine'
|
when 'Valine'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.valine-comment-count(data-xid=url_for(link))
|
span.valine-comment-count(data-xid=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Waline'
|
when 'Waline'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.waline-comment-count(data-path=url_for(link))
|
span.waline-comment-count(data-path=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Twikoo'
|
when 'Twikoo'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a.twikoo-count(href=url_for(link) + '#post-comment')
|
a.twikoo-count(href=commentLink)
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Facebook Comments'
|
when 'Facebook Comments'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
||||||
when 'Remark42'
|
when 'Remark42'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Artalk'
|
when 'Artalk'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.artalk-count(data-page-key=url_for(link))
|
span.artalk-count(data-page-key=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
|
|
||||||
|
|||||||
+251
-130
@@ -9,44 +9,53 @@
|
|||||||
- page.toc = false
|
- page.toc = false
|
||||||
|
|
||||||
#article-container
|
#article-container
|
||||||
if page.comments !== false && theme.comments.use
|
if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
|
||||||
- commentsJsLoad = true
|
if page.comments !== false && theme.comments.use
|
||||||
|
- commentsJsLoad = true
|
||||||
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
|
||||||
|
|
||||||
const runDestroy = (shuoshuoComment) => {
|
const runDestroy = (shuoshuoComment) => {
|
||||||
if (!shuoshuoComment) return
|
if (!shuoshuoComment) return
|
||||||
|
|
||||||
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
for (const [key, fn] of Object.entries(shuoshuoComment)) {
|
||||||
if (key.startsWith('destroy')) fn()
|
if (key.startsWith('destroy')) fn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.addCommentToShuoshuo = e => {
|
window.addCommentToShuoshuo = e => {
|
||||||
const btn = e.target.closest('.shuoshuo-comment-btn')
|
const btn = e.target.closest('.shuoshuo-comment-btn')
|
||||||
if (!btn) return
|
if (!btn) return
|
||||||
|
|
||||||
const ele = btn.closest('.container').nextElementSibling
|
const ele = btn.closest('.container').nextElementSibling
|
||||||
const { shuoshuoComment } = window
|
const { shuoshuoComment } = window
|
||||||
const isInclude = ele.classList.contains('no-comment')
|
const isInclude = ele.classList.contains('no-comment')
|
||||||
runDestroy(shuoshuoComment)
|
runDestroy(shuoshuoComment)
|
||||||
if (isInclude) {
|
if (isInclude) {
|
||||||
ele.classList.remove('no-comment')
|
ele.classList.remove('no-comment')
|
||||||
ele.innerHTML = commentDiv
|
ele.innerHTML = commentDiv
|
||||||
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
|
||||||
btf.switchComments(ele, key)
|
btf.switchComments(ele, key)
|
||||||
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
})()
|
|
||||||
|
|
||||||
if page.shuoshuo_url
|
|
||||||
|
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
|
||||||
|
|
||||||
|
if !page.shuoshuo_url
|
||||||
|
script(type='application/json' id='shuoshuo-data')!= safeJSON(localDate)
|
||||||
|
|
||||||
|
- const { enable, native, placeholder, field } = theme.lazyload
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
const limitConfig = !{ JSON.stringify(page.limit || {}) }
|
||||||
|
|
||||||
|
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 sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||||
|
|
||||||
const filterDataByLimit = (data, limit) => {
|
const filterDataByLimit = (data, limit) => {
|
||||||
@@ -57,132 +66,244 @@
|
|||||||
return data.filter(item => new Date(item.date) >= limitDate)
|
return data.filter(item => new Date(item.date) >= limitDate)
|
||||||
}
|
}
|
||||||
return data
|
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 formatToTimeZone = (date) => {
|
||||||
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
||||||
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
|
const [day, month, year, hour, minute, second] = dateFormatter.format(new Date(fullDate)).match(/\d+/g)
|
||||||
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)
|
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadShuoshuo = async () => {
|
const addLazyload = str => {
|
||||||
try {
|
const lazyConfig = {
|
||||||
const response = await fetch('!{url_for(page.shuoshuo_url)}')
|
enable: !{Boolean(enable)},
|
||||||
let data = await response.json()
|
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')
|
images.forEach(img => {
|
||||||
let start = 0
|
if (lazyConfig.native) {
|
||||||
|
img.setAttribute('loading', 'lazy')
|
||||||
|
} else {
|
||||||
|
const src = img.getAttribute('src')
|
||||||
|
img.setAttribute('data-lazy-src', src)
|
||||||
|
|
||||||
const renderData = (dataSlice) => {
|
if (lazyConfig.placeholder) {
|
||||||
const content = dataSlice.map(item => {
|
img.setAttribute('src', lazyConfig.placeholder)
|
||||||
const formattedDate = formatToTimeZone(item.date)
|
} else {
|
||||||
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
|
img.removeAttribute('src')
|
||||||
const commentButton = item.key && !{commentsJsLoad}
|
}
|
||||||
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
}
|
||||||
<i class="fa-solid fa-comments"></i>
|
})
|
||||||
</div>`
|
return doc.body.innerHTML
|
||||||
: ''
|
}
|
||||||
const commentContainer = item.key
|
|
||||||
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return `
|
let currentPage = 1
|
||||||
<div class="shuoshuo-item">
|
const itemsPerPage = 8
|
||||||
<div class="container">
|
let totalPages = 0
|
||||||
<div class="shuoshuo-item-header">
|
let data = []
|
||||||
<div class="shuoshuo-avatar">
|
|
||||||
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
const renderData = (dataSlice) => {
|
||||||
</div>
|
const content = dataSlice.map(item => {
|
||||||
<div class="shuoshuo-info">
|
const formattedDate = formatToTimeZone(item.date)
|
||||||
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${escapeHtml(tag)}</span>`).join('') || ''
|
||||||
<time class="shuoshuo-date" title="${formattedDate}">
|
const commentButton = item.key && !{commentsJsLoad || false}
|
||||||
${btf.diffDate(formattedDate, true)}
|
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
||||||
</time>
|
<i class="fa-solid fa-comments"></i>
|
||||||
</div>
|
</div>`
|
||||||
</div>
|
: ''
|
||||||
<div class="shuoshuo-content">${item.content}</div>
|
const commentContainer = item.key
|
||||||
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
? `<div class="shuoshuo-comment no-comment" data-key="${escapeHtml(item.key)}"></div>`
|
||||||
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
: ''
|
||||||
${commentButton}
|
|
||||||
</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>
|
</div>
|
||||||
${commentContainer}
|
<div class="shuoshuo-info">
|
||||||
</div>`
|
<div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(config.author || '')}}</div>
|
||||||
}).join('')
|
<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()
|
window.lazyLoadInstance && window.lazyLoadInstance.update()
|
||||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
document.dispatchEvent(new Event('shuoshuo:rendered'))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!/^\d$/.test(e.key)) {
|
||||||
if (!entries[0].isIntersecting) return
|
e.preventDefault()
|
||||||
observer.unobserve(entries[0].target)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const slice = data.slice(start, start + 10)
|
const newValue = parseInt(value) || 0
|
||||||
renderData(slice)
|
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||||
start += 10
|
e.preventDefault()
|
||||||
|
e.target.classList.add('invalid')
|
||||||
|
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (start < data.length) {
|
const renderNavigation = () => {
|
||||||
setTimeout(() => observer.observe(container.lastElementChild), 100)
|
const container = document.getElementById('article-container')
|
||||||
} else {
|
let nav = container.nextElementSibling
|
||||||
observer.disconnect()
|
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
||||||
}
|
const pageInfoText = pageInfoTemplate
|
||||||
};
|
.replace(/\$\{current}/g, currentPage)
|
||||||
|
.replace(/\$\{total}/g, totalPages)
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersection, {
|
if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
|
||||||
root: null,
|
nav = document.createElement('div')
|
||||||
rootMargin: '0px',
|
nav.className = 'shuoshuo-navigation'
|
||||||
threshold: 1.0
|
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))
|
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
|
||||||
start += 10
|
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) {
|
} catch (error) {
|
||||||
console.error(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
|
.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})
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if page.total !== 1
|
|||||||
|
|
||||||
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
|
||||||
if direction.cover_type === 'img'
|
if direction.cover_type === 'img'
|
||||||
img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
img.cover(src=url_for(direction.pagination_cover || direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
|
||||||
else
|
else
|
||||||
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -1,9 +1,12 @@
|
|||||||
script.
|
script.
|
||||||
(() = {
|
(() => {
|
||||||
const abcjsInit = () => {
|
const abcjsInit = () => {
|
||||||
const abcjsFn = () => {
|
const abcjsFn = () => {
|
||||||
|
const $article = document.getElementById("article-container")
|
||||||
|
if (!$article || $article.querySelector(".hbe-container")) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const sheets = document.querySelectorAll(".abc-music-sheet")
|
const sheets = $article.querySelectorAll(".abc-music-sheet")
|
||||||
for (let i = 0; i < sheets.length; i++) {
|
for (let i = 0; i < sheets.length; i++) {
|
||||||
const ele = sheets[i]
|
const ele = sheets[i]
|
||||||
if (ele.children.length > 0) continue
|
if (ele.children.length > 0) continue
|
||||||
@@ -24,12 +27,12 @@ script.
|
|||||||
const options = { ...params, responsive: "resize" }
|
const options = { ...params, responsive: "resize" }
|
||||||
|
|
||||||
// Render the music score using ABCJS.renderAbc
|
// Render the music score using ABCJS.renderAbc
|
||||||
ABCJS.renderAbc(ele, ele.innerHTML, options)
|
ABCJS.renderAbc(ele, ele.textContent, options)
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ABCJS === "object") {
|
if (typeof ABCJS === "object" && ABCJS !== null) {
|
||||||
abcjsFn()
|
abcjsFn()
|
||||||
} else {
|
} else {
|
||||||
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
|
||||||
|
|||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
link(rel='stylesheet' href=url_for(theme.asset.aplayer_css) media="print" onload="this.media='all'")
|
|
||||||
script(src=url_for(theme.asset.aplayer_js))
|
|
||||||
script(src=url_for(theme.asset.meting_js))
|
|
||||||
if theme.pjax.enable
|
|
||||||
script.
|
|
||||||
(() => {
|
|
||||||
const destroyAplayer = () => {
|
|
||||||
if (window.aplayers) {
|
|
||||||
for (let i = 0; i < window.aplayers.length; i++) {
|
|
||||||
if (!window.aplayers[i].options.fixed) {
|
|
||||||
window.aplayers[i].destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runMetingJS = () => {
|
|
||||||
typeof loadMeting === 'function' && document.getElementsByClassName('aplayer').length && loadMeting()
|
|
||||||
}
|
|
||||||
|
|
||||||
btf.addGlobalFn('pjaxSend', destroyAplayer, 'destroyAplayer')
|
|
||||||
btf.addGlobalFn('pjaxComplete', loadMeting, 'runMetingJS')
|
|
||||||
})()
|
|
||||||
+4
-2
@@ -1,5 +1,5 @@
|
|||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const getCommentUrl = () => {
|
const getCommentUrl = () => {
|
||||||
const eleGroup = document.querySelectorAll('#recent-posts .article-title')
|
const eleGroup = document.querySelectorAll('#recent-posts .article-title')
|
||||||
let urlArray = []
|
let urlArray = []
|
||||||
@@ -18,7 +18,9 @@ script.
|
|||||||
includeReply: false
|
includeReply: false
|
||||||
}).then(function (res) {
|
}).then(function (res) {
|
||||||
document.querySelectorAll('#recent-posts .twikoo-count').forEach((item,index) => {
|
document.querySelectorAll('#recent-posts .twikoo-count').forEach((item,index) => {
|
||||||
item.textContent = res[index].count
|
if (res[index]) {
|
||||||
|
item.textContent = res[index].count
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ script.
|
|||||||
|
|
||||||
const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' })
|
const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' })
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
|
|
||||||
result.data.forEach((count, index) => {
|
result.data.forEach((count, index) => {
|
||||||
eleGroup[index].textContent = count
|
eleGroup[index].textContent = count
|
||||||
})
|
})
|
||||||
|
|||||||
+13
-7
@@ -6,10 +6,14 @@ script.
|
|||||||
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
(window.Chatra.q = window.Chatra.q || []).push(arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
const isChatBtn = !{theme.chat.rightside_button}
|
||||||
const isChatBtn = !{theme.chat.rightside_button}
|
const isChatHideShow = !{theme.chat.button_hide_show}
|
||||||
const isChatHideShow = !{theme.chat.button_hide_show}
|
|
||||||
|
|
||||||
|
if (isChatBtn) {
|
||||||
|
window.ChatraSetup = { startHidden: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
|
||||||
if (isChatBtn) {
|
if (isChatBtn) {
|
||||||
const close = () => {
|
const close = () => {
|
||||||
Chatra('minimizeWidget')
|
Chatra('minimizeWidget')
|
||||||
@@ -21,11 +25,13 @@ script.
|
|||||||
Chatra('show')
|
Chatra('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ChatraSetup = { startHidden: true }
|
window.chatBtnFn = () => {
|
||||||
|
const el = document.getElementById('chatra')
|
||||||
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
|
return el && el.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) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => Chatra('hide'),
|
hide: () => Chatra('hide'),
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@ script.
|
|||||||
|
|
||||||
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
|
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) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => $crisp.push(["do", "chat:hide"]),
|
hide: () => $crisp.push(["do", "chat:hide"]),
|
||||||
|
|||||||
+4
-2
@@ -3,5 +3,7 @@ case theme.chat.use
|
|||||||
include ./chatra.pug
|
include ./chatra.pug
|
||||||
when 'tidio'
|
when 'tidio'
|
||||||
include ./tidio.pug
|
include ./tidio.pug
|
||||||
when 'crisp'
|
when 'crisp'
|
||||||
include ./crisp.pug
|
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)
|
||||||
|
})
|
||||||
|
})()
|
||||||
+3
-3
@@ -10,7 +10,7 @@ script.
|
|||||||
window.tidioChatApi.hide()
|
window.tidioChatApi.hide()
|
||||||
isShow = false
|
isShow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
window.tidioChatApi.open()
|
window.tidioChatApi.open()
|
||||||
window.tidioChatApi.show()
|
window.tidioChatApi.show()
|
||||||
@@ -32,8 +32,8 @@ script.
|
|||||||
isShow ? close() : open()
|
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) {
|
} else if (isChatHideShow) {
|
||||||
window.chatBtn = {
|
window.chatBtn = {
|
||||||
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
|
||||||
|
|||||||
+2
-2
@@ -51,8 +51,8 @@ script.
|
|||||||
const loadArtalk = async (el, pageKey) => {
|
const loadArtalk = async (el, pageKey) => {
|
||||||
if (typeof Artalk === 'object') initArtalk(el, pageKey)
|
if (typeof Artalk === 'object') initArtalk(el, pageKey)
|
||||||
else {
|
else {
|
||||||
await btf.getCSS('!{theme.asset.artalk_css}')
|
await btf.getCSS('!{url_for(theme.asset.artalk_css)}')
|
||||||
await btf.getScript('!{theme.asset.artalk_js}')
|
await btf.getScript('!{url_for(theme.asset.artalk_js)}')
|
||||||
initArtalk(el, pageKey)
|
initArtalk(el, pageKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||||
const dqOption = !{JSON.stringify(dqOption)}
|
const dqOption = !{JSON.stringify(dqOption)}
|
||||||
|
|
||||||
const destroyDisqusjs = () => {
|
const destroyDisqusjs = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
script.
|
script.
|
||||||
(()=>{
|
(()=>{
|
||||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo'
|
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||||
|
|
||||||
const loadFBComment = (el = document, path) => {
|
const loadFBComment = (el = document, path) => {
|
||||||
if (isShuoshuo) {
|
if (isShuoshuo) {
|
||||||
|
|||||||
+10
-1
@@ -5,7 +5,7 @@ hr.custom-hr
|
|||||||
.comment-headline
|
.comment-headline
|
||||||
i.fas.fa-comments.fa-fw
|
i.fas.fa-comments.fa-fw
|
||||||
span= ' ' + _p('comment')
|
span= ' ' + _p('comment')
|
||||||
|
|
||||||
if theme.comments.use.length > 1
|
if theme.comments.use.length > 1
|
||||||
.comment-switch
|
.comment-switch
|
||||||
span.first-comment=defaultComment
|
span.first-comment=defaultComment
|
||||||
@@ -43,4 +43,13 @@ hr.custom-hr
|
|||||||
when 'Remark42'
|
when 'Remark42'
|
||||||
#remark42
|
#remark42
|
||||||
when 'Artalk'
|
when 'Artalk'
|
||||||
|
if theme.artalk.vote
|
||||||
|
.artalk-vote
|
||||||
|
span.artalk-page-vote-up
|
||||||
|
i.fas.fa-thumbs-up
|
||||||
|
span.artalk-page-vote-up-count
|
||||||
|
span.artalk-page-vote-down
|
||||||
|
i.fas.fa-thumbs-down
|
||||||
|
span.artalk-page-vote-down-count
|
||||||
#artalk-wrap
|
#artalk-wrap
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ script.
|
|||||||
: window.loadOtherComment = loadLivere
|
: window.loadOtherComment = loadLivere
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('!{use[0]}' === 'Livere' || !!{lazyload}) {
|
if ('!{use[0]}' === 'Livere' || !!{lazyload}) {
|
||||||
if (!{lazyload}) btf.loadComment(document.getElementById('lv-container'), loadLivere)
|
if (!{lazyload}) btf.loadComment(document.getElementById('lv-container'), loadLivere)
|
||||||
else loadLivere()
|
else loadLivere()
|
||||||
|
|||||||
+4
-4
@@ -3,7 +3,7 @@
|
|||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
|
||||||
const option = !{JSON.stringify(option)}
|
const options = !{JSON.stringify(option)}
|
||||||
|
|
||||||
const loadScript = src => {
|
const loadScript = src => {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
@@ -42,8 +42,8 @@ script.
|
|||||||
host: '!{host}',
|
host: '!{host}',
|
||||||
site_id: '!{siteId}',
|
site_id: '!{siteId}',
|
||||||
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
|
theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
|
||||||
...option,
|
...options,
|
||||||
url: isShuoshuo ? window.location.origin + path : (option && option.url) || window.location.origin + window.location.pathname
|
url: isShuoshuo ? window.location.origin + path : (options && options.url) || window.location.origin + window.location.pathname
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.REMARK42) {
|
if (window.REMARK42) {
|
||||||
@@ -68,7 +68,7 @@ script.
|
|||||||
: window.loadOtherComment = loadRemark42
|
: window.loadOtherComment = loadRemark42
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('!{theme.comments.use[0]}' === 'Remark42' || !!{theme.comments.lazyload}) {
|
if ('!{theme.comments.use[0]}' === 'Remark42' || !!{theme.comments.lazyload}) {
|
||||||
if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('remark42'), loadRemark42)
|
if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('remark42'), loadRemark42)
|
||||||
else loadRemark42()
|
else loadRemark42()
|
||||||
|
|||||||
+51
-17
@@ -1,32 +1,67 @@
|
|||||||
//- Mathjax 3
|
//- Mathjax 4/5
|
||||||
- const { tags, enableMenu } = theme.math.mathjax
|
- const { tags, enableMenu } = theme.math.mathjax
|
||||||
script.
|
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 loadMathjax = () => {
|
||||||
|
const article = document.getElementById('article-container')
|
||||||
|
if (!article || article.querySelector('.hbe-container')) return
|
||||||
|
changeScriptToMath(article)
|
||||||
|
|
||||||
if (!window.MathJax) {
|
if (!window.MathJax) {
|
||||||
window.MathJax = {
|
window.MathJax = {
|
||||||
|
loader: {
|
||||||
|
load: [
|
||||||
|
// Four font extension packages (optional)
|
||||||
|
//- '[tex]/bbm',
|
||||||
|
//- '[tex]/bboldx',
|
||||||
|
//- '[tex]/dsfont',
|
||||||
|
'[tex]/mhchem',
|
||||||
|
'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: {
|
tex: {
|
||||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||||
tags: '!{tags}',
|
tags: '!{tags}',
|
||||||
|
packages: {
|
||||||
|
'[+]': [
|
||||||
|
'mhchem'
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
chtml: {
|
chtml: {
|
||||||
scale: 1.1
|
scale: 1.1
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
lazyMargin: '200px',
|
||||||
enableMenu: !{enableMenu},
|
enableMenu: !{enableMenu},
|
||||||
renderActions: {
|
menuOptions: {
|
||||||
findScript: [10, doc => {
|
settings: {
|
||||||
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
|
enrich: false // Turn off Braille and voice narration text automatic generation
|
||||||
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)
|
|
||||||
}
|
|
||||||
}, '']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +71,8 @@ script.
|
|||||||
script.async = true
|
script.async = true
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
} else {
|
} else {
|
||||||
MathJax.startup.document.state(0)
|
MathJax.typesetClear()
|
||||||
MathJax.texReset()
|
MathJax.typesetPromise([ article ])
|
||||||
MathJax.typesetPromise()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+323
-10
@@ -1,27 +1,337 @@
|
|||||||
script.
|
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 => {
|
const runMermaid = ele => {
|
||||||
window.loadMermaid = true
|
window.loadMermaid = true
|
||||||
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
||||||
|
|
||||||
ele.forEach((item, index) => {
|
ele.forEach((item, index) => {
|
||||||
const mermaidSrc = item.firstElementChild
|
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 mermaidID = `mermaid-${index}`
|
||||||
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
||||||
|
|
||||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
|
||||||
const renderMermaid = svg => {
|
const renderMermaid = svg => {
|
||||||
mermaidSrc.insertAdjacentHTML('afterend', 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
|
const handleError = err => {
|
||||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
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 codeToMermaid = $article => {
|
||||||
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid')
|
const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
|
||||||
if (codeMermaidEle.length === 0) return
|
if (codeMermaidEle.length === 0) return
|
||||||
|
|
||||||
codeMermaidEle.forEach(ele => {
|
codeMermaidEle.forEach(ele => {
|
||||||
@@ -37,8 +347,11 @@ script.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadMermaid = () => {
|
const loadMermaid = () => {
|
||||||
if (!{theme.mermaid.code_write}) codeToMermaid()
|
const $article = document.getElementById('article-container')
|
||||||
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap')
|
if (!$article) return
|
||||||
|
|
||||||
|
if (!{theme.mermaid.code_write}) codeToMermaid($article)
|
||||||
|
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
|
||||||
if ($mermaid.length === 0) return
|
if ($mermaid.length === 0) return
|
||||||
|
|
||||||
const runMermaidFn = () => runMermaid($mermaid)
|
const runMermaidFn = () => runMermaid($mermaid)
|
||||||
@@ -48,4 +361,4 @@ script.
|
|||||||
|
|
||||||
btf.addGlobalFn('encrypt', loadMermaid, 'mermaid')
|
btf.addGlobalFn('encrypt', loadMermaid, 'mermaid')
|
||||||
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)
|
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)
|
||||||
})()
|
})()
|
||||||
|
|||||||
Vendored
+19
-10
@@ -1,4 +1,4 @@
|
|||||||
- var pjaxExclude = 'a:not([target="_blank"])'
|
- var pjaxExclude = 'a:not([target="_blank"]):not([data-fancybox])'
|
||||||
if theme.pjax.exclude
|
if theme.pjax.exclude
|
||||||
each val in theme.pjax.exclude
|
each val in theme.pjax.exclude
|
||||||
- pjaxExclude += `:not([href="${val}"])`
|
- pjaxExclude += `:not([href="${val}"])`
|
||||||
@@ -14,9 +14,9 @@ if choose
|
|||||||
else
|
else
|
||||||
- pjaxSelectors.unshift('meta[name="description"]')
|
- pjaxSelectors.unshift('meta[name="description"]')
|
||||||
|
|
||||||
script(src=url_for(theme.asset.pjax))
|
script(src=url_for(theme.asset.pjax) defer)
|
||||||
script.
|
script.
|
||||||
(() => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
|
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
|
||||||
|
|
||||||
window.pjax = new Pjax({
|
window.pjax = new Pjax({
|
||||||
@@ -29,7 +29,13 @@ script.
|
|||||||
|
|
||||||
const triggerPjaxFn = (val) => {
|
const triggerPjaxFn = (val) => {
|
||||||
if (!val) return
|
if (!val) return
|
||||||
Object.values(val).forEach(fn => fn())
|
Object.values(val).forEach(fn => {
|
||||||
|
try {
|
||||||
|
fn()
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Pjax callback failed:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('pjax:send', () => {
|
document.addEventListener('pjax:send', () => {
|
||||||
@@ -47,8 +53,9 @@ script.
|
|||||||
document.addEventListener('pjax:complete', () => {
|
document.addEventListener('pjax:complete', () => {
|
||||||
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
||||||
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
||||||
|
if (!item.parentNode) return
|
||||||
const newScript = document.createElement('script')
|
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))
|
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
||||||
newScript.appendChild(document.createTextNode(content))
|
newScript.appendChild(document.createTextNode(content))
|
||||||
item.parentNode.replaceChild(newScript, item)
|
item.parentNode.replaceChild(newScript, item)
|
||||||
@@ -58,11 +65,13 @@ script.
|
|||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('pjax:error', e => {
|
document.addEventListener('pjax:error', e => {
|
||||||
if (e.request.status === 404) {
|
const responseURL = e.request && e.request.responseURL
|
||||||
const usePjax = !{theme.pjax && theme.pjax.enable}
|
if (e.request && e.request.status === 404) {
|
||||||
!{theme.error_404 && theme.error_404.enable}
|
!{theme.error_404 && theme.error_404.enable}
|
||||||
? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{url_for("/404.html")}')
|
? pjax.loadUrl('!{url_for("/404.html")}')
|
||||||
: window.location.href = e.request.responseURL
|
: responseURL && (window.location.href = responseURL)
|
||||||
|
} else if (responseURL) {
|
||||||
|
window.location.href = responseURL
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})()
|
})
|
||||||
+8
-4
@@ -5,11 +5,15 @@
|
|||||||
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
|
const $article = document.getElementById('article-container')
|
||||||
|
if (!$article || $article.querySelector('.hbe-container')) return
|
||||||
|
|
||||||
window.Prism = window.Prism || {}
|
window.Prism = window.Prism || {}
|
||||||
window.Prism.manual = true
|
window.Prism.manual = true
|
||||||
|
|
||||||
const highlightAll = () => {
|
const highlightAll = () => {
|
||||||
window.Prism.highlightAll()
|
// window.Prism.highlightAll()
|
||||||
|
window.Prism.highlightAllUnder($article, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', highlightAll)
|
window.addEventListener('load', highlightAll)
|
||||||
@@ -17,7 +21,7 @@ if (syntax_highlighter === 'prismjs' || enable) && !preprocess
|
|||||||
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
|
btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
|
||||||
})()
|
})()
|
||||||
|
|
||||||
script(src=url_for(prismjs_js))
|
script(src=url_for(prismjs_js) defer)
|
||||||
script(src=url_for(prismjs_autoloader))
|
script(src=url_for(prismjs_autoloader) defer)
|
||||||
if (line_number)
|
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
|
.search-dialog
|
||||||
nav.search-nav
|
nav.search-nav
|
||||||
span.search-dialog-title= _p('search.title')
|
span.search-dialog-title= _p('search.title')
|
||||||
|
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||||
button.search-close-button
|
button.search-close-button
|
||||||
i.fas.fa-times
|
i.fas.fa-times
|
||||||
|
|
||||||
.search-wrap
|
#algolia-search-input
|
||||||
#algolia-search-input
|
.ais-SearchBox
|
||||||
|
form.ais-SearchBox-form(action="" role="search" novalidate="")
|
||||||
|
input.ais-SearchBox-input(type="search" placeholder=theme.search.placeholder || _p("search.input_placeholder") autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" maxlength="512" aria-label="Search")
|
||||||
|
button.ais-SearchBox-submit(type="submit" title="Submit the search query" style="display:none;")
|
||||||
|
svg.ais-SearchBox-submitIcon(width="10" height="10" viewBox="0 0 40 40" aria-hidden="true")
|
||||||
|
path(d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z")
|
||||||
hr
|
hr
|
||||||
#algolia-search-results
|
#algolia-search-results
|
||||||
#algolia-hits
|
#algolia-hits
|
||||||
#algolia-pagination
|
#algolia-hits-empty(style="display:none;")
|
||||||
|
.ais-Hits(style="display:none;")
|
||||||
|
ol.ais-Hits-list
|
||||||
|
#algolia-pagination.ais-Pagination(style="display:none;")
|
||||||
|
ul.ais-Pagination-list
|
||||||
#algolia-info
|
#algolia-info
|
||||||
.algolia-stats
|
span.ais-Stats-text
|
||||||
.algolia-poweredBy
|
a.algolia-poweredBy(href="https://www.algolia.com/?utm_source=algoliasearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby" target="_blank" aria-label="Search by Algolia" rel="noopener noreferrer")
|
||||||
|
svg.ais-PoweredBy-logo(height="1.2em" viewBox="0 0 572 64" style="width: auto;")
|
||||||
|
path(fill="#36395A" d="M16 48.3c-3.4 0-6.3-.6-8.7-1.7A12.4 12.4 0 0 1 1.9 42C.6 40 0 38 0 35.4h6.5a6.7 6.7 0 0 0 3.9 6c1.4.7 3.3 1.1 5.6 1.1 2.2 0 4-.3 5.4-1a7 7 0 0 0 3-2.4 6 6 0 0 0 1-3.4c0-1.5-.6-2.8-1.9-3.7-1.3-1-3.3-1.6-5.9-1.8l-4-.4c-3.7-.3-6.6-1.4-8.8-3.4a10 10 0 0 1-3.3-7.9c0-2.4.6-4.6 1.8-6.4a12 12 0 0 1 5-4.3c2.2-1 4.7-1.6 7.5-1.6s5.5.5 7.6 1.6a12 12 0 0 1 5 4.4c1.2 1.8 1.8 4 1.8 6.7h-6.5a6.4 6.4 0 0 0-3.5-5.9c-1-.6-2.6-1-4.4-1s-3.2.3-4.4 1c-1.1.6-2 1.4-2.6 2.4-.5 1-.8 2-.8 3.1a5 5 0 0 0 1.5 3.6c1 1 2.6 1.7 4.7 1.9l4 .3c2.8.2 5.2.8 7.2 1.8 2.1 1 3.7 2.2 4.9 3.8a9.7 9.7 0 0 1 1.7 5.8c0 2.5-.7 4.7-2 6.6a13 13 0 0 1-5.6 4.4c-2.4 1-5.2 1.6-8.4 1.6Zm35.6 0c-2.6 0-4.8-.4-6.7-1.3a13 13 0 0 1-4.7-3.5 17.1 17.1 0 0 1-3.6-10.4v-1c0-2 .3-3.8 1-5.6a13 13 0 0 1 7.3-8.3 15 15 0 0 1 6.3-1.4A13.2 13.2 0 0 1 64 24.3c1 2.2 1.6 4.6 1.6 7.2V34H39.4v-4.3h21.8l-1.8 2.2c0-2-.3-3.7-.9-5.1a7.3 7.3 0 0 0-2.7-3.4c-1.2-.7-2.7-1.1-4.6-1.1s-3.4.4-4.7 1.3a8 8 0 0 0-2.9 3.6c-.6 1.5-.9 3.3-.9 5.4 0 2 .3 3.7 1 5.3a7.9 7.9 0 0 0 2.8 3.7c1.3.8 3 1.3 5 1.3s3.8-.5 5.1-1.3c1.3-1 2.1-2 2.4-3.2h6a11.8 11.8 0 0 1-7 8.7 16 16 0 0 1-6.4 1.2ZM80 48c-2.2 0-4-.3-5.7-1a8.4 8.4 0 0 1-3.7-3.3 9.7 9.7 0 0 1-1.3-5.2c0-2 .5-3.8 1.5-5.2a9 9 0 0 1 4.3-3.1c1.8-.7 4-1 6.7-1H89v4.1h-7.5c-2 0-3.4.5-4.4 1.4-1 1-1.6 2.1-1.6 3.6s.5 2.7 1.6 3.6c1 1 2.5 1.4 4.4 1.4 1.1 0 2.2-.2 3.2-.7 1-.4 1.9-1 2.6-2 .6-1 1-2.4 1-4.2l1.7 2.1c-.2 2-.7 3.8-1.5 5.2a9 9 0 0 1-3.4 3.3 12 12 0 0 1-5.3 1Zm9.5-.7v-8.8h-1v-10c0-1.8-.5-3.2-1.4-4.1-1-1-2.4-1.4-4.2-1.4a142.9 142.9 0 0 0-10.2.4v-5.6a74.8 74.8 0 0 1 8.6-.4c3 0 5.5.4 7.5 1.2s3.4 2 4.4 3.6c1 1.7 1.4 4 1.4 6.7v18.4h-5Zm12.9 0V17.8h5v12.3h-.2c0-4.2 1-7.4 2.8-9.5a11 11 0 0 1 8.3-3.1h1v5.6h-2a9 9 0 0 0-6.3 2.2c-1.5 1.5-2.2 3.6-2.2 6.4v15.6h-6.4Zm34.4 1a15 15 0 0 1-6.6-1.3c-1.9-.9-3.4-2-4.7-3.5a15.5 15.5 0 0 1-2.7-5c-.6-1.7-1-3.6-1-5.4v-1c0-2 .4-3.8 1-5.6a15 15 0 0 1 2.8-4.9c1.3-1.5 2.8-2.6 4.6-3.5a16.4 16.4 0 0 1 13.3.2c2 1 3.5 2.3 4.8 4a12 12 0 0 1 2 6H144c-.2-1.6-1-3-2.2-4.1a7.5 7.5 0 0 0-5.2-1.7 8 8 0 0 0-4.7 1.3 8 8 0 0 0-2.8 3.6 13.8 13.8 0 0 0 0 10.3c.6 1.5 1.5 2.7 2.8 3.6s2.8 1.3 4.8 1.3c1.5 0 2.7-.2 3.8-.8a7 7 0 0 0 2.6-2c.7-1 1-2 1.2-3.2h6.2a11 11 0 0 1-2 6.2 15.1 15.1 0 0 1-11.8 5.5Zm19.7-1v-40h6.4V31h-1.3c0-3 .4-5.5 1.1-7.6a9.7 9.7 0 0 1 3.5-4.8A9.9 9.9 0 0 1 172 17h.3c3.5 0 6 1.1 7.9 3.5 1.7 2.3 2.6 5.7 2.6 10v16.8h-6.4V29.6c0-2.1-.6-3.8-1.8-5a6.4 6.4 0 0 0-4.8-1.8c-2 0-3.7.7-5 2a7.8 7.8 0 0 0-1.9 5.5v17h-6.4Zm63.8 1a12.2 12.2 0 0 1-10.9-6.2 19 19 0 0 1-1.8-7.3h1.4v12.5h-5.1v-40h6.4v19.8l-2 3.5c.2-3.1.8-5.7 1.9-7.7a11 11 0 0 1 4.4-4.5c1.8-1 3.9-1.5 6.1-1.5a13.4 13.4 0 0 1 12.8 9.1c.7 1.9 1 3.8 1 6v1c0 2.2-.3 4.1-1 6a13.6 13.6 0 0 1-13.2 9.4Zm-1.2-5.5a8.4 8.4 0 0 0 7.9-5c.7-1.5 1.1-3.3 1.1-5.3s-.4-3.8-1.1-5.3a8.7 8.7 0 0 0-3.2-3.6 9.6 9.6 0 0 0-9.2-.2 8.5 8.5 0 0 0-3.3 3.2c-.8 1.4-1.3 3-1.3 5v2.3a9 9 0 0 0 1.3 4.8 9 9 0 0 0 3.4 3c1.4.7 2.8 1 4.4 1Zm27.3 3.9-10-28.9h6.5l9.5 28.9h-6Zm-7.5 12.2v-5.7h4.9c1 0 2-.1 2.9-.4a4 4 0 0 0 2-1.4c.4-.7.9-1.6 1.2-2.7l8.6-30.9h6.2l-9.3 32.4a14 14 0 0 1-2.5 5 8.9 8.9 0 0 1-4 2.8c-1.5.6-3.4.9-5.6.9h-4.4Zm9-12.2v-5.2h6.4v5.2H248Z")
|
||||||
|
path(fill="#003DFF" d="M534.4 9.1H528a.8.8 0 0 1-.7-.7V1.8c0-.4.2-.7.6-.8l6.5-1c.4 0 .8.2.9.6v7.8c0 .4-.4.7-.8.7zM428 35.2V.8c0-.5-.3-.8-.7-.8h-.2l-6.4 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.5 0 .8-.4.8-.8V43c0-.4-.3-.7-.6-.8-4.5-.5-4.5-6-4.5-7zm106.5-21.8H528c-.4 0-.7.4-.7.8v34c0 .4.3.8.7.8h6.5c.4 0 .8-.4.8-.8v-34c0-.5-.4-.8-.8-.8zm-17.7 21.8V.8c0-.5-.3-.8-.8-.8l-6.5 1c-.4 0-.7.4-.7.8v35c0 1.6 0 11.8 12.3 12.2.4 0 .8-.4.8-.8V43c0-.4-.3-.7-.7-.8-4.4-.5-4.4-6-4.4-7zm-22.2-20.6a16.5 16.5 0 0 1 8.6 9.3c.8 2.2 1.3 4.8 1.3 7.5a19.4 19.4 0 0 1-4.6 12.6 14.8 14.8 0 0 1-5.2 3.6c-2 .9-5.2 1.4-6.8 1.4a21 21 0 0 1-6.7-1.4 15.4 15.4 0 0 1-8.6-9.3 21.3 21.3 0 0 1 0-14.4 15.2 15.2 0 0 1 8.6-9.3c2-.8 4.3-1.2 6.7-1.2s4.6.4 6.7 1.2zm-6.7 27.6c2.7 0 4.7-1 6.2-3s2.2-4.3 2.2-7.8-.7-6.3-2.2-8.3-3.5-3-6.2-3-4.7 1-6.1 3c-1.5 2-2.2 4.8-2.2 8.3s.7 5.8 2.2 7.8 3.5 3 6.2 3zm-88.8-28.8c-6.2 0-11.7 3.3-14.8 8.2a18.6 18.6 0 0 0 4.8 25.2c1.8 1.2 4 1.8 6.2 1.7s.1 0 .1 0h.9c4.2-.7 8-4 9.1-8.1v7.4c0 .4.3.7.8.7h6.4a.7.7 0 0 0 .7-.7V14.2c0-.5-.3-.8-.7-.8h-13.5zm6.3 26.5a9.8 9.8 0 0 1-5.7 2h-.5a10 10 0 0 1-9.2-14c1.4-3.7 5-6.3 9-6.3h6.4v18.3zm152.3-26.5h13.5c.5 0 .8.3.8.7v33.7c0 .4-.3.7-.8.7h-6.4a.7.7 0 0 1-.8-.7v-7.4c-1.2 4-4.8 7.4-9 8h-.1a4.2 4.2 0 0 1-.5.1h-.9a10.3 10.3 0 0 1-7-2.6c-4-3.3-6.5-8.4-6.5-14.2 0-3.7 1-7.2 3-10 3-5 8.5-8.3 14.7-8.3zm.6 28.4c2.2-.1 4.2-.6 5.7-2V21.7h-6.3a9.8 9.8 0 0 0-9 6.4 10.2 10.2 0 0 0 9.1 13.9h.5zM452.8 13.4c-6.2 0-11.7 3.3-14.8 8.2a18.5 18.5 0 0 0 3.6 24.3 10.4 10.4 0 0 0 13 .6c2.2-1.5 3.8-3.7 4.5-6.1v7.8c0 2.8-.8 5-2.2 6.3-1.5 1.5-4 2.2-7.5 2.2l-6-.3c-.3 0-.7.2-.8.5l-1.6 5.5c-.1.4.1.8.5 1h.1c2.8.4 5.5.6 7 .6 6.3 0 11-1.4 14-4.1 2.7-2.5 4.2-6.3 4.5-11.4V14.2c0-.5-.4-.8-.8-.8h-13.5zm6.3 8.2v18.3a9.6 9.6 0 0 1-5.6 2h-1a10.3 10.3 0 0 1-8.8-14c1.4-3.7 5-6.3 9-6.3h6.4zM291 31.5A32 32 0 0 1 322.8 0h30.8c.6 0 1.2.5 1.2 1.2v61.5c0 1.1-1.3 1.7-2.2 1l-19.2-17a18 18 0 0 1-11 3.4 18.1 18.1 0 1 1 18.2-14.8c-.1.4-.5.7-.9.6-.1 0-.3 0-.4-.2l-3.8-3.4c-.4-.3-.6-.8-.7-1.4a12 12 0 1 0-2.4 8.3c.4-.4 1-.5 1.6-.2l14.7 13.1v-46H323a26 26 0 1 0 10 49.7c.8-.4 1.6-.2 2.3.3l3 2.7c.3.2.3.7 0 1l-.2.2a32 32 0 0 1-47.2-28.6z")
|
||||||
|
|
||||||
#search-mask
|
#search-mask
|
||||||
|
|
||||||
script(src=url_for(theme.asset.algolia_search))
|
script(src=url_for(theme.asset.algolia_search))
|
||||||
script(src=url_for(theme.asset.instantsearch))
|
|
||||||
script(src=url_for(theme.asset.algolia_js))
|
script(src=url_for(theme.asset.algolia_js))
|
||||||
+11
-9
@@ -2,7 +2,7 @@
|
|||||||
.search-dialog
|
.search-dialog
|
||||||
nav.search-nav
|
nav.search-nav
|
||||||
span.search-dialog-title= _p('search.title')
|
span.search-dialog-title= _p('search.title')
|
||||||
span#loading-status
|
i.fas.fa-spinner.fa-pulse#loading-status(hidden)
|
||||||
button.search-close-button
|
button.search-close-button
|
||||||
i.fas.fa-times
|
i.fas.fa-times
|
||||||
|
|
||||||
@@ -10,13 +10,15 @@
|
|||||||
i.fas.fa-spinner.fa-pulse
|
i.fas.fa-spinner.fa-pulse
|
||||||
span= ' ' + _p("search.load_data")
|
span= ' ' + _p("search.load_data")
|
||||||
|
|
||||||
.search-wrap
|
.local-search-input
|
||||||
#local-search-input
|
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text")
|
||||||
.local-search-box
|
|
||||||
input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input
|
|
||||||
hr
|
|
||||||
#local-search-results
|
|
||||||
#local-search-stats-wrap
|
|
||||||
#search-mask
|
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
#local-search-results
|
||||||
|
#local-search-pagination.ais-Pagination(style="display:none;")
|
||||||
|
ul.ais-Pagination-list
|
||||||
|
#local-search-stats
|
||||||
|
|
||||||
|
#search-mask
|
||||||
script(src=url_for(theme.asset.local_search))
|
script(src=url_for(theme.asset.local_search))
|
||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
- const { effect, source, sub, typed_option } = theme.subtitle
|
- const { effect, source, sub, typed_option } = theme.subtitle
|
||||||
- let subContent = sub || new Array()
|
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
|
||||||
|
|
||||||
script.
|
script.
|
||||||
window.typedJSFn = {
|
window.typedJSFn = {
|
||||||
@@ -44,7 +44,7 @@ script.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy')
|
btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
|
||||||
|
|
||||||
case source
|
case source
|
||||||
when 1
|
when 1
|
||||||
@@ -53,8 +53,8 @@ case source
|
|||||||
fetch('https://v1.hitokoto.cn')
|
fetch('https://v1.hitokoto.cn')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const from = '出自 ' + data.from
|
const from = data.from ? '出自 ' + data.from : ''
|
||||||
typedJSFn.processSubtitle(data.hitokoto, [from])
|
typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Hitokoto API:', err)
|
console.error('Failed to get the Hitokoto API:', err)
|
||||||
@@ -79,7 +79,7 @@ case source
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Yiyan API:', err)
|
console.error('Failed to get the Yiyan API:', err)
|
||||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
typedJSFn.run(subtitleType)
|
typedJSFn.run(subtitleType)
|
||||||
@@ -99,7 +99,7 @@ case source
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to get the Jinrishici API:', err)
|
console.error('Failed to get the Jinrishici API:', err)
|
||||||
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)})
|
typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
typedJSFn.run(subtitleType)
|
typedJSFn.run(subtitleType)
|
||||||
|
|||||||
+116
-65
@@ -1,65 +1,116 @@
|
|||||||
- let { serverURL, website_id, option, UV_PV } = theme.umami_analytics
|
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
|
||||||
- const isServerURL = !!serverURL
|
- const isServerURL = !!serverURL
|
||||||
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
||||||
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
||||||
|
|
||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
const option = !{JSON.stringify(option)}
|
const option = !{JSON.stringify(option)}
|
||||||
const config = !{JSON.stringify(UV_PV)}
|
const config = !{JSON.stringify(UV_PV)}
|
||||||
|
|
||||||
const runTrack = () => {
|
const runTrack = () => {
|
||||||
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
|
||||||
}
|
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
||||||
|
} else {
|
||||||
const loadUmamiJS = () => {
|
console.warn('Umami Analytics: umami.track is not available')
|
||||||
btf.getScript('!{baseURL}/script.js', {
|
}
|
||||||
'data-website-id': '!{website_id}',
|
}
|
||||||
'data-auto-track': 'false',
|
|
||||||
...option
|
const loadUmamiJS = () => {
|
||||||
}).then(runTrack)
|
btf.getScript('!{baseURL}/!{script_name}', {
|
||||||
}
|
'data-website-id': '!{website_id}',
|
||||||
|
'data-auto-track': 'false',
|
||||||
const getData = async (isPost) => {
|
...option
|
||||||
const now = Date.now()
|
}).then(() => {
|
||||||
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
runTrack()
|
||||||
const headerList = { 'Accept': 'application/json' }
|
}).catch(error => {
|
||||||
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
|
console.error('Umami Analytics: Error loading script', error)
|
||||||
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
|
const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
|
||||||
})
|
|
||||||
return await res.json()
|
const getData = async (isPost) => {
|
||||||
}
|
try {
|
||||||
|
const now = Date.now()
|
||||||
const insertData = async () => {
|
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||||
try {
|
const headers = {
|
||||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
'Accept': 'application/json',
|
||||||
const pagePV = document.getElementById('umamiPV')
|
[!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
|
||||||
if (pagePV) {
|
}
|
||||||
const data = await getData(true)
|
|
||||||
pagePV.textContent = data.pageviews.value
|
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
|
||||||
}
|
method: 'GET',
|
||||||
} else {
|
headers
|
||||||
const data = (config.site_uv || config.site_pv) && await getData()
|
})
|
||||||
if (config.site_uv) {
|
|
||||||
const siteUV = document.getElementById('umami-site-uv')
|
if (!res.ok) {
|
||||||
if (siteUV) siteUV.textContent = data.visitors.value
|
throw new Error(`HTTP error! status: ${res.status}`)
|
||||||
}
|
}
|
||||||
if (config.site_pv) {
|
|
||||||
const sitePV = document.getElementById('umami-site-pv')
|
return await res.json()
|
||||||
if (sitePV) sitePV.textContent = data.pageviews.value
|
} catch (error) {
|
||||||
}
|
console.error('Umami Analytics: Failed to fetch data', error)
|
||||||
}
|
throw error
|
||||||
} catch (e) {
|
}
|
||||||
console.error('Failed to load Umami Analytics:', e)
|
}
|
||||||
}
|
|
||||||
}
|
const insertData = async () => {
|
||||||
|
try {
|
||||||
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
const tasks = []
|
||||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
|
||||||
|
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||||
loadUmamiJS()
|
tasks.push((async () => {
|
||||||
insertData()
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
|
||||||
|
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||||
|
|
||||||
|
loadUmamiJS()
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', insertData)
|
||||||
|
} else {
|
||||||
|
setTimeout(insertData, 100)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
|
|||||||
i.fas.fa-tags
|
i.fas.fa-tags
|
||||||
span= _p('aside.card_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
|
- limit = limit === 0 ? 0 : limit || 40
|
||||||
|
|
||||||
if theme.aside.card_tags.color
|
if theme.aside.card_tags.color
|
||||||
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em'})
|
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
|
||||||
else
|
else
|
||||||
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hexo-theme-butterfly",
|
"name": "hexo-theme-butterfly",
|
||||||
"version": "5.4.1",
|
"version": "5.7.0",
|
||||||
"description": "A Simple and Card UI Design theme for Hexo",
|
"description": "A Simple and Card UI Design theme for Hexo",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hexo-renderer-pug": "^3.0.0",
|
"hexo-renderer-pug": "^3.0.0",
|
||||||
"hexo-renderer-stylus": "^3.0.1",
|
"hexo-renderer-stylus": "^3.0.1",
|
||||||
"hexo-util": "^3.3.0",
|
"hexo-util": "^4.0.0",
|
||||||
"moment-timezone": "^0.5.48"
|
"moment-timezone": "^0.6.3"
|
||||||
},
|
},
|
||||||
"homepage": "https://butterfly.js.org/",
|
"homepage": "https://butterfly.js.org/",
|
||||||
"author": "Jerry <my@crazywong.com>",
|
"author": "Jerry <my@crazywong.com>",
|
||||||
|
|||||||
+31
-47
@@ -1,31 +1,23 @@
|
|||||||
abcjs_basic_js:
|
abcjs_basic_js:
|
||||||
name: abcjs
|
name: abcjs
|
||||||
file: dist/abcjs-basic-min.js
|
file: dist/abcjs-basic-min.js
|
||||||
version: 6.5.1
|
version: 6.6.4
|
||||||
activate_power_mode:
|
activate_power_mode:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/activate-power-mode.min.js
|
file: dist/activate-power-mode.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
algolia_search:
|
algolia_search:
|
||||||
name: algoliasearch
|
name: algoliasearch
|
||||||
file: dist/lite/builds/browser.umd.js
|
file: dist/lite/builds/browser.umd.js
|
||||||
version: 5.30.0
|
version: 5.56.0
|
||||||
aplayer_css:
|
|
||||||
name: aplayer
|
|
||||||
file: dist/APlayer.min.css
|
|
||||||
version: 1.10.1
|
|
||||||
aplayer_js:
|
|
||||||
name: aplayer
|
|
||||||
file: dist/APlayer.min.js
|
|
||||||
version: 1.10.1
|
|
||||||
artalk_css:
|
artalk_css:
|
||||||
name: artalk
|
name: artalk
|
||||||
file: dist/Artalk.css
|
file: dist/Artalk.css
|
||||||
version: 2.9.1
|
version: 2.10.0
|
||||||
artalk_js:
|
artalk_js:
|
||||||
name: artalk
|
name: artalk
|
||||||
file: dist/Artalk.js
|
file: dist/Artalk.js
|
||||||
version: 2.9.1
|
version: 2.10.0
|
||||||
blueimp_md5:
|
blueimp_md5:
|
||||||
name: blueimp-md5
|
name: blueimp-md5
|
||||||
file: js/md5.min.js
|
file: js/md5.min.js
|
||||||
@@ -33,69 +25,69 @@ blueimp_md5:
|
|||||||
canvas_fluttering_ribbon:
|
canvas_fluttering_ribbon:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-fluttering-ribbon.min.js
|
file: dist/canvas-fluttering-ribbon.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
canvas_nest:
|
canvas_nest:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-nest.min.js
|
file: dist/canvas-nest.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
canvas_ribbon:
|
canvas_ribbon:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/canvas-ribbon.min.js
|
file: dist/canvas-ribbon.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
chartjs:
|
chartjs:
|
||||||
name: chart.js
|
name: chart.js
|
||||||
file: dist/chart.umd.js
|
file: dist/chart.umd.js
|
||||||
version: 4.5.0
|
version: 4.5.1
|
||||||
clickShowText:
|
clickShowText:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-show-text.min.js
|
file: dist/click-show-text.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
click_heart:
|
click_heart:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-heart.min.js
|
file: dist/click-heart.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
disqusjs:
|
disqusjs:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/disqusjs.es2015.umd.min.js
|
file: dist/browser/disqusjs.es2015.umd.min.js
|
||||||
version: 3.1.0
|
version: 3.2.1
|
||||||
disqusjs_css:
|
disqusjs_css:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/styles/disqusjs.css
|
file: dist/browser/styles/disqusjs.css
|
||||||
version: 3.1.0
|
version: 3.2.1
|
||||||
docsearch_css:
|
docsearch_css:
|
||||||
name: '@docsearch/css'
|
name: '@docsearch/css'
|
||||||
other_name: docsearch-css
|
other_name: docsearch-css
|
||||||
file: dist/style.css
|
file: dist/style.css
|
||||||
version: 3.9.0
|
version: 4.7.0
|
||||||
docsearch_js:
|
docsearch_js:
|
||||||
name: '@docsearch/js'
|
name: '@docsearch/js'
|
||||||
other_name: docsearch-js
|
other_name: docsearch-js
|
||||||
file: dist/umd/index.js
|
file: dist/umd/index.js
|
||||||
version: 3.9.0
|
version: 4.7.0
|
||||||
egjs_infinitegrid:
|
egjs_infinitegrid:
|
||||||
name: '@egjs/infinitegrid'
|
name: '@egjs/infinitegrid'
|
||||||
other_name: egjs-infinitegrid
|
other_name: egjs-infinitegrid
|
||||||
file: dist/infinitegrid.min.js
|
file: dist/infinitegrid.min.js
|
||||||
version: 4.12.0
|
version: 4.13.0
|
||||||
fancybox:
|
fancybox:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.umd.js
|
file: dist/fancybox/fancybox.umd.js
|
||||||
version: 6.0.7
|
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
|
version: 6.1.14
|
||||||
fancybox_css:
|
fancybox_css:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.css
|
file: dist/fancybox/fancybox.css
|
||||||
version: 6.0.7
|
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
|
version: 6.1.14
|
||||||
fireworks:
|
fireworks:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/fireworks.min.js
|
file: dist/fireworks.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
fontawesome:
|
fontawesome:
|
||||||
name: '@fortawesome/fontawesome-free'
|
name: '@fortawesome/fontawesome-free'
|
||||||
file: css/all.min.css
|
file: css/all.min.css
|
||||||
other_name: font-awesome
|
other_name: font-awesome
|
||||||
version: 6.7.2
|
version: 7.3.1
|
||||||
gitalk:
|
gitalk:
|
||||||
name: gitalk
|
name: gitalk
|
||||||
file: dist/gitalk.min.js
|
file: dist/gitalk.min.js
|
||||||
@@ -108,28 +100,24 @@ instantpage:
|
|||||||
name: instant.page
|
name: instant.page
|
||||||
file: instantpage.js
|
file: instantpage.js
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
instantsearch:
|
|
||||||
name: instantsearch.js
|
|
||||||
file: dist/instantsearch.production.min.js
|
|
||||||
version: 4.79.0
|
|
||||||
katex:
|
katex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/katex.min.css
|
file: dist/katex.min.css
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.22
|
version: 0.18.1
|
||||||
katex_copytex:
|
katex_copytex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/contrib/copy-tex.min.js
|
file: dist/contrib/copy-tex.min.js
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.22
|
version: 0.18.1
|
||||||
lazyload:
|
lazyload:
|
||||||
name: vanilla-lazyload
|
name: vanilla-lazyload
|
||||||
file: dist/lazyload.iife.min.js
|
file: dist/lazyload.iife.min.js
|
||||||
version: 19.1.3
|
version: 19.1.3
|
||||||
mathjax:
|
mathjax:
|
||||||
name: mathjax
|
name: mathjax
|
||||||
file: es5/tex-mml-chtml.js
|
file: tex-mml-chtml.js
|
||||||
version: 3.2.2
|
version: 4.1.3
|
||||||
medium_zoom:
|
medium_zoom:
|
||||||
name: medium-zoom
|
name: medium-zoom
|
||||||
file: dist/medium-zoom.min.js
|
file: dist/medium-zoom.min.js
|
||||||
@@ -137,11 +125,7 @@ medium_zoom:
|
|||||||
mermaid:
|
mermaid:
|
||||||
name: mermaid
|
name: mermaid
|
||||||
file: dist/mermaid.min.js
|
file: dist/mermaid.min.js
|
||||||
version: 11.8.0
|
version: 11.16.0
|
||||||
meting_js:
|
|
||||||
name: butterfly-extsrc
|
|
||||||
file: metingjs/dist/Meting.min.js
|
|
||||||
version: 1.1.4
|
|
||||||
pace_default_css:
|
pace_default_css:
|
||||||
name: pace-js
|
name: pace-js
|
||||||
other_name: pace
|
other_name: pace
|
||||||
@@ -174,11 +158,11 @@ prismjs_lineNumber_js:
|
|||||||
sharejs:
|
sharejs:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/js/social-share.min.js
|
file: sharejs/dist/js/social-share.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
sharejs_css:
|
sharejs_css:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/css/share.min.css
|
file: sharejs/dist/css/share.min.css
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
snackbar:
|
snackbar:
|
||||||
name: node-snackbar
|
name: node-snackbar
|
||||||
file: dist/snackbar.min.js
|
file: dist/snackbar.min.js
|
||||||
@@ -190,11 +174,11 @@ snackbar_css:
|
|||||||
twikoo:
|
twikoo:
|
||||||
name: twikoo
|
name: twikoo
|
||||||
file: dist/twikoo.all.min.js
|
file: dist/twikoo.all.min.js
|
||||||
version: 1.6.44
|
version: 1.7.15
|
||||||
typed:
|
typed:
|
||||||
name: typed.js
|
name: typed.js
|
||||||
file: dist/typed.umd.js
|
file: dist/typed.umd.js
|
||||||
version: 2.1.0
|
version: 3.0.0
|
||||||
valine:
|
valine:
|
||||||
name: valine
|
name: valine
|
||||||
file: dist/Valine.min.js
|
file: dist/Valine.min.js
|
||||||
@@ -203,9 +187,9 @@ waline_css:
|
|||||||
name: '@waline/client'
|
name: '@waline/client'
|
||||||
file: dist/waline.css
|
file: dist/waline.css
|
||||||
other_name: waline
|
other_name: waline
|
||||||
version: 3.5.7
|
version: 3.15.2
|
||||||
waline_js:
|
waline_js:
|
||||||
name: '@waline/client'
|
name: '@waline/client'
|
||||||
file: dist/waline.js
|
file: dist/waline.js
|
||||||
other_name: waline
|
other_name: waline
|
||||||
version: 3.5.7
|
version: 3.15.2
|
||||||
|
|||||||
@@ -0,0 +1,601 @@
|
|||||||
|
// 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,
|
||||||
|
vote: 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
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.generator.register('404', function (locals) {
|
hexo.extend.generator.register('404', function (locals) {
|
||||||
if (!hexo.theme.config.error_404.enable) return
|
const error404 = hexo.theme.config.error_404 || {}
|
||||||
|
if (!error404.enable) return
|
||||||
return {
|
return {
|
||||||
path: '404.html',
|
path: '404.html',
|
||||||
layout: ['page'],
|
layout: ['page'],
|
||||||
|
|||||||
+27
-21
@@ -8,11 +8,19 @@
|
|||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
// Cache plugins.yml to avoid re-rendering on every generate
|
||||||
|
let cachedPlugins = null
|
||||||
|
const loadPlugins = hexo => {
|
||||||
|
if (cachedPlugins) return cachedPlugins
|
||||||
|
cachedPlugins = hexo.render.renderSync({ path: path.join(hexo.theme_dir, '/plugins.yml'), engine: 'yaml' })
|
||||||
|
return cachedPlugins
|
||||||
|
}
|
||||||
|
|
||||||
hexo.extend.filter.register('before_generate', () => {
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
const themeConfig = hexo.theme.config
|
const themeConfig = hexo.theme.config
|
||||||
const { CDN } = themeConfig
|
const { CDN } = themeConfig
|
||||||
|
|
||||||
const thirdPartySrc = hexo.render.renderSync({ path: path.join(hexo.theme_dir, '/plugins.yml'), engine: 'yaml' })
|
const thirdPartySrc = loadPlugins(hexo)
|
||||||
const internalSrc = {
|
const internalSrc = {
|
||||||
main: {
|
main: {
|
||||||
name: 'hexo-theme-butterfly',
|
name: 'hexo-theme-butterfly',
|
||||||
@@ -46,45 +54,43 @@ hexo.extend.filter.register('before_generate', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createCDNLink = (data, type, cond = '') => {
|
const createCDNLink = (data, type, cond = '') => {
|
||||||
Object.keys(data).forEach(key => {
|
return Object.keys(data).reduce((result, key) => {
|
||||||
let { name, version, file, other_name } = data[key]
|
let { name, version, file, other_name: otherName } = data[key]
|
||||||
const cdnjs_name = other_name || name
|
const cdnjsName = otherName || name
|
||||||
const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '')
|
||||||
const min_cdnjs_file = minFile(cdnjs_file)
|
const minCdnjsFile = minFile(cdnjsFile)
|
||||||
if (cond === 'internal') file = `source/${file}`
|
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 verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
version,
|
version,
|
||||||
name,
|
name,
|
||||||
file,
|
file,
|
||||||
cdnjs_file,
|
cdnjs_file: cdnjsFile,
|
||||||
min_file,
|
min_file: minFilePath,
|
||||||
min_cdnjs_file,
|
min_cdnjs_file: minCdnjsFile,
|
||||||
cdnjs_name
|
cdnjs_name: cdnjsName
|
||||||
}
|
}
|
||||||
|
|
||||||
const cdnSource = {
|
const cdnSource = {
|
||||||
local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
local: cond === 'internal' ? `${cdnjsFile + verType}` : `/pluginsSrc/${name}/${file + verType}`,
|
||||||
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
|
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${minFilePath}`,
|
||||||
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
|
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])
|
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
|
||||||
}
|
}
|
||||||
|
|
||||||
data[key] = cdnSource[type]
|
result[key] = cdnSource[type]
|
||||||
})
|
return result
|
||||||
|
}, cond === 'internal' ? { main_css: 'css/index.css' + (CDN.version ? `?v=${version}` : '') } : {})
|
||||||
if (cond === 'internal') data.main_css = 'css/index.css' + (CDN.version ? `?v=${version}` : '')
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete null value
|
// delete null value
|
||||||
const deleteNullValue = obj => {
|
const deleteNullValue = obj => {
|
||||||
if (!obj) return
|
if (!obj) return {}
|
||||||
for (const i in obj) {
|
for (const i in obj) {
|
||||||
obj[i] === null && delete obj[i]
|
if (obj[i] === null) delete obj[i]
|
||||||
}
|
}
|
||||||
return obj
|
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
|
|
||||||
})
|
|
||||||
+86
-20
@@ -1,20 +1,86 @@
|
|||||||
hexo.extend.filter.register('before_generate', () => {
|
const { deepMerge } = require('hexo-util')
|
||||||
// Get first two digits of the Hexo version number
|
const path = require('path')
|
||||||
const { version, log, locals } = hexo
|
|
||||||
const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
|
// Cache default config to avoid repeated file reads
|
||||||
|
let cachedDefaultConfig = null
|
||||||
if (hexoVer < 5.3) {
|
|
||||||
log.error('Please update Hexo to V5.3.0 or higher!')
|
/**
|
||||||
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
* Check Hexo version and configuration
|
||||||
process.exit(-1)
|
*/
|
||||||
}
|
function checkHexoEnvironment (hexo) {
|
||||||
|
const { version, log, locals } = hexo
|
||||||
if (locals.get) {
|
|
||||||
const data = locals.get('data')
|
const [major, minor] = version.split('.').map(Number)
|
||||||
if (data && data.butterfly) {
|
const requiredMajor = 5
|
||||||
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
const requiredMinor = 3
|
||||||
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
|
||||||
process.exit(-1)
|
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
|
||||||
}
|
log.error('Please update Hexo to V5.3.0 or higher!')
|
||||||
}
|
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
||||||
})
|
throw new Error('Hexo version too old')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for deprecated configuration file
|
||||||
|
if (locals.get) {
|
||||||
|
const data = locals.get('data')
|
||||||
|
if (data && data.butterfly) {
|
||||||
|
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
||||||
|
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
||||||
|
throw new Error('Deprecated configuration file')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load default configuration
|
||||||
|
*/
|
||||||
|
function loadDefaultConfig () {
|
||||||
|
if (cachedDefaultConfig) {
|
||||||
|
return cachedDefaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(__dirname, '../common/default_config.js')
|
||||||
|
cachedDefaultConfig = require(configPath)
|
||||||
|
return cachedDefaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process comment system configuration
|
||||||
|
*/
|
||||||
|
function processCommentConfig (themeConfig) {
|
||||||
|
const { comments } = themeConfig
|
||||||
|
if (!comments || !comments.use) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { use } = comments
|
||||||
|
|
||||||
|
if (!Array.isArray(use)) {
|
||||||
|
use = typeof use === 'string' ? use.split(',') : [use]
|
||||||
|
}
|
||||||
|
|
||||||
|
use = use
|
||||||
|
.map(item => {
|
||||||
|
if (typeof item !== 'string') return item
|
||||||
|
return item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
// Handle Disqus and Disqusjs conflict
|
||||||
|
if (use.includes('Disqus') && use.includes('Disqusjs')) {
|
||||||
|
hexo.log.warn('Disqus and Disqusjs conflict detected, keeping only the first one')
|
||||||
|
hexo.log.warn('檢測到 Disqus 和 Disqusjs 衝突,只保留第一個')
|
||||||
|
use = [use[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
themeConfig.comments.use = use
|
||||||
|
}
|
||||||
|
|
||||||
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
|
checkHexoEnvironment(hexo)
|
||||||
|
|
||||||
|
const defaultConfig = loadDefaultConfig()
|
||||||
|
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
|
||||||
|
|
||||||
|
processCommentConfig(hexo.theme.config)
|
||||||
|
}, 1)
|
||||||
|
|||||||
@@ -1,593 +0,0 @@
|
|||||||
const { deepMerge } = require('hexo-util')
|
|
||||||
|
|
||||||
hexo.extend.filter.register('before_generate', () => {
|
|
||||||
const defaultConfig = {
|
|
||||||
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: 2024
|
|
||||||
},
|
|
||||||
copyright: {
|
|
||||||
enable: true,
|
|
||||||
version: true
|
|
||||||
},
|
|
||||||
custom_text: null
|
|
||||||
},
|
|
||||||
aside: {
|
|
||||||
enable: true,
|
|
||||||
hide: false,
|
|
||||||
button: true,
|
|
||||||
mobile: true,
|
|
||||||
position: 'right',
|
|
||||||
display: {
|
|
||||||
archive: true,
|
|
||||||
tag: true,
|
|
||||||
category: true
|
|
||||||
},
|
|
||||||
card_author: {
|
|
||||||
enable: true,
|
|
||||||
description: null,
|
|
||||||
button: {
|
|
||||||
enable: true,
|
|
||||||
icon: 'fab fa-github',
|
|
||||||
text: 'Follow Me',
|
|
||||||
link: 'https://github.com/xxxxxx'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
card_announcement: {
|
|
||||||
enable: true,
|
|
||||||
content: 'This is my Blog'
|
|
||||||
},
|
|
||||||
card_recent_post: {
|
|
||||||
enable: true,
|
|
||||||
limit: 5,
|
|
||||||
sort: 'date',
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_newest_comments: {
|
|
||||||
enable: false,
|
|
||||||
sort_order: null,
|
|
||||||
limit: 6,
|
|
||||||
storage: 10,
|
|
||||||
avatar: true
|
|
||||||
},
|
|
||||||
card_categories: {
|
|
||||||
enable: true,
|
|
||||||
limit: 8,
|
|
||||||
expand: 'none',
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_tags: {
|
|
||||||
enable: true,
|
|
||||||
limit: 40,
|
|
||||||
color: false,
|
|
||||||
orderby: 'random',
|
|
||||||
order: 1,
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_archives: {
|
|
||||||
enable: true,
|
|
||||||
type: 'monthly',
|
|
||||||
format: 'MMMM YYYY',
|
|
||||||
order: -1,
|
|
||||||
limit: 8,
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_post_series: {
|
|
||||||
enable: true,
|
|
||||||
series_title: false,
|
|
||||||
orderBy: 'date',
|
|
||||||
order: -1
|
|
||||||
},
|
|
||||||
card_webinfo: {
|
|
||||||
enable: true,
|
|
||||||
post_count: true,
|
|
||||||
last_push_date: true,
|
|
||||||
sort_order: null,
|
|
||||||
runtime_date: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rightside_bottom: null,
|
|
||||||
translate: {
|
|
||||||
enable: false,
|
|
||||||
default: '繁',
|
|
||||||
defaultEncoding: 2,
|
|
||||||
translateDelay: 0,
|
|
||||||
msgToTraditionalChinese: '繁',
|
|
||||||
msgToSimplifiedChinese: '簡'
|
|
||||||
},
|
|
||||||
readmode: true,
|
|
||||||
darkmode: {
|
|
||||||
enable: true,
|
|
||||||
button: true,
|
|
||||||
autoChangeMode: false,
|
|
||||||
start: null,
|
|
||||||
end: null
|
|
||||||
},
|
|
||||||
rightside_scroll_percent: false,
|
|
||||||
rightside_item_order: {
|
|
||||||
enable: false,
|
|
||||||
hide: null,
|
|
||||||
show: null
|
|
||||||
},
|
|
||||||
rightside_config_animation: true,
|
|
||||||
anchor: {
|
|
||||||
auto_update: false,
|
|
||||||
click_to_scroll: false
|
|
||||||
},
|
|
||||||
photofigcaption: false,
|
|
||||||
copy: {
|
|
||||||
enable: true,
|
|
||||||
copyright: {
|
|
||||||
enable: false,
|
|
||||||
limit_count: 150
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wordcount: {
|
|
||||||
enable: false,
|
|
||||||
post_wordcount: true,
|
|
||||||
min2read: true,
|
|
||||||
total_wordcount: true
|
|
||||||
},
|
|
||||||
busuanzi: {
|
|
||||||
site_uv: true,
|
|
||||||
site_pv: true,
|
|
||||||
page_pv: true
|
|
||||||
},
|
|
||||||
math: {
|
|
||||||
use: null,
|
|
||||||
per_page: true,
|
|
||||||
hide_scrollbar: false,
|
|
||||||
mathjax: {
|
|
||||||
enableMenu: true,
|
|
||||||
tags: 'none'
|
|
||||||
},
|
|
||||||
katex: {
|
|
||||||
copy_tex: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
use: null,
|
|
||||||
placeholder: null,
|
|
||||||
algolia_search: {
|
|
||||||
hitsPerPage: 6
|
|
||||||
},
|
|
||||||
local_search: {
|
|
||||||
preload: false,
|
|
||||||
top_n_per_article: 1,
|
|
||||||
unescape: false,
|
|
||||||
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
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
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)
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.filter.register('stylus:renderer', style => {
|
hexo.extend.filter.register('stylus:renderer', style => {
|
||||||
const { syntax_highlighter: syntaxHighlighter, highlight, prismjs } = hexo.config
|
const { syntax_highlighter: syntaxHighlighter, highlight = {}, prismjs = {} } = hexo.config
|
||||||
let { enable: highlightEnable, line_number: highlightLineNumber } = highlight
|
let { enable: highlightEnable, line_number: highlightLineNumber } = highlight
|
||||||
let { enable: prismjsEnable, line_number: prismjsLineNumber } = prismjs
|
let { enable: prismjsEnable, line_number: prismjsLineNumber } = prismjs
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Butterfly
|
* Butterfly
|
||||||
* lazyload
|
* Lazyload filter
|
||||||
* replace src to data-lazy-src
|
* Replace src with data-lazy-src for lazy loading
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
@@ -10,11 +10,20 @@ const urlFor = require('hexo-util').url_for.bind(hexo)
|
|||||||
|
|
||||||
const lazyload = htmlContent => {
|
const lazyload = htmlContent => {
|
||||||
if (hexo.theme.config.lazyload.native) {
|
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'
|
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||||
return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`)
|
|
||||||
|
// Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
|
||||||
|
// Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify)
|
||||||
|
return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => {
|
||||||
|
const src = srcDoubleQuote ?? srcSingleQuote ?? srcNoQuote
|
||||||
|
return `${prefix}"${bg}" data-lazy-src="${src}"`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.filter.register('after_render:html', data => {
|
hexo.extend.filter.register('after_render:html', data => {
|
||||||
|
|||||||
@@ -5,78 +5,105 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.generator.register('post', locals => {
|
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 => {
|
const { post_asset_folder: postAssetFolder } = hexo.config
|
||||||
if (!defaultCover) return false
|
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
||||||
if (!Array.isArray(defaultCover)) return defaultCover
|
|
||||||
|
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 coverCount = defaultCover.length
|
||||||
|
const maxHistory = Math.min(3, coverCount - 1)
|
||||||
|
const history = []
|
||||||
|
|
||||||
if (coverCount === 1) {
|
while (true) {
|
||||||
return defaultCover[0]
|
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)
|
return `${postPath}${value}`
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImg = data => {
|
const handleImg = data => {
|
||||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
data.top_img = resolvePostAsset(data.top_img, data.path)
|
||||||
let { cover: coverVal, top_img: topImg } = data
|
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 (data.cover === false) return data
|
||||||
if (hexo.config.post_asset_folder) {
|
|
||||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
|
if (!data.cover) {
|
||||||
data.top_img = `${data.path}${topImg}`
|
data.cover = coverGenerator.next().value
|
||||||
}
|
|
||||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
|
|
||||||
data.cover = `${data.path}${coverVal}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverVal === false) return data
|
if (isImage(data.cover)) {
|
||||||
|
|
||||||
// If cover is not set, use random cover
|
|
||||||
if (!coverVal) {
|
|
||||||
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
|
||||||
const randomCover = getRandomCover(defaultCover)
|
|
||||||
data.cover = randomCover
|
|
||||||
coverVal = randomCover // update coverVal
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
|
|
||||||
data.cover_type = 'img'
|
data.cover_type = 'img'
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
|
|
||||||
const posts = locals.posts.sort('date').toArray()
|
const posts = locals.posts.sort('date').toArray()
|
||||||
const { length } = posts
|
const { length } = posts
|
||||||
|
|
||||||
return posts.map((post, i) => {
|
return posts.map((post, index) => {
|
||||||
if (i) post.prev = posts[i - 1]
|
const data = post
|
||||||
if (i < length - 1) post.next = posts[i + 1]
|
|
||||||
|
|
||||||
post.__post = true
|
if (index > 0) {
|
||||||
|
data.prev = posts[index - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < length - 1) {
|
||||||
|
data.next = posts[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
data.__post = true
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: handleImg(post),
|
data: handleImg(data),
|
||||||
layout: 'post',
|
layout: 'post',
|
||||||
path: post.path
|
path: data.path
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||||
const { config, page, site, url_for, _p } = this
|
const { config, page, site, url_for: urlFor, _p } = this
|
||||||
const { archive_dir: archiveDir, timezone, language } = config
|
const { archive_dir: archiveDir, timezone, language } = config
|
||||||
|
|
||||||
// Destructure and set default options with object destructuring
|
// Destructure and set default options with object destructuring
|
||||||
@@ -17,56 +17,43 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
// Optimize locale handling
|
// Optimize locale handling
|
||||||
const lang = toMomentLocale(page.lang || page.language || language)
|
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
|
// Early return if no posts
|
||||||
if (!site.posts.length) return ''
|
if (!site.posts.length) return ''
|
||||||
|
|
||||||
// Use reduce for more efficient data processing
|
const archives = new Map()
|
||||||
const data = site.posts.sort('date', order).reduce((acc, post) => {
|
site.posts.forEach(post => {
|
||||||
let date = post.date.clone()
|
const date = post.date
|
||||||
if (timezone) date = date.tz(timezone)
|
|
||||||
|
|
||||||
const year = date.year()
|
const year = date.year()
|
||||||
const month = date.month() + 1
|
const month = date.month() + 1
|
||||||
|
const key = type === 'yearly' ? year : `${year}-${month}`
|
||||||
|
|
||||||
if (lang) date = date.locale(lang)
|
if (archives.has(key)) {
|
||||||
|
archives.get(key).count++
|
||||||
// Find or create archive entry
|
|
||||||
const lastEntry = acc[acc.length - 1]
|
|
||||||
|
|
||||||
if (type === 'yearly') {
|
|
||||||
const existingYearIndex = acc.findIndex(entry => entry.year === year)
|
|
||||||
if (existingYearIndex !== -1) {
|
|
||||||
acc[existingYearIndex].count++
|
|
||||||
} else {
|
|
||||||
// 否則創建新條目
|
|
||||||
acc.push({
|
|
||||||
name: date.format(format),
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
count: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!lastEntry || !compareFunc(lastEntry.year, lastEntry.month, year, month)) {
|
archives.set(key, {
|
||||||
acc.push({
|
year,
|
||||||
name: date.format(format),
|
month,
|
||||||
year,
|
count: 1,
|
||||||
month,
|
date // Store date object for later formatting
|
||||||
count: 1
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
lastEntry.count++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
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
|
// Create link generator function
|
||||||
const createArchiveLink = item => {
|
const createArchiveLink = item => {
|
||||||
@@ -74,7 +61,7 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
if (type === 'monthly') {
|
if (type === 'monthly') {
|
||||||
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
||||||
}
|
}
|
||||||
return url_for(url)
|
return urlFor(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit results efficiently
|
// Limit results efficiently
|
||||||
@@ -82,18 +69,14 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
|||||||
|
|
||||||
// Use template literal for better readability
|
// Use template literal for better readability
|
||||||
const archiveHeader = `
|
const archiveHeader = `
|
||||||
<div class="item-headline">
|
<div class="item-headline"><i class="fas fa-archive"></i><span>${_p('aside.card_archives')}</span>${
|
||||||
<i class="fas fa-archive"></i>
|
data.length > limitedData.length
|
||||||
<span>${_p('aside.card_archives')}</span>
|
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
|
||||||
${
|
|
||||||
data.length > limitedData.length
|
|
||||||
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
|
|
||||||
title="${_p('aside.more_button')}">
|
title="${_p('aside.more_button')}">
|
||||||
<i class="fas fa-angle-right"></i>
|
<i class="fas fa-angle-right"></i>
|
||||||
</a>`
|
</a>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}</div>
|
||||||
</div>
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Use map for generating list items, join for performance
|
// Use map for generating list items, join for performance
|
||||||
|
|||||||
@@ -19,15 +19,33 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
const expandClass = isExpand && options.expand === true ? 'expand' : ''
|
const expandClass = isExpand && options.expand === true ? 'expand' : ''
|
||||||
const buttonLabel = this._p('aside.more_button')
|
const buttonLabel = this._p('aside.more_button')
|
||||||
|
|
||||||
const prepareQuery = parent => {
|
const categoryMap = new Map()
|
||||||
const query = parent ? { parent } : { parent: { $exists: false } }
|
categories.forEach(cat => {
|
||||||
return categories.find(query).sort(orderby, order).filter(cat => cat.length)
|
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 = ''
|
let result = ''
|
||||||
if (remaining > 0) {
|
if (remaining > 0 && categoryMap.has(parentId)) {
|
||||||
prepareQuery(parent).forEach(cat => {
|
categoryMap.get(parentId).forEach(cat => {
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
remaining -= 1
|
remaining -= 1
|
||||||
let child = ''
|
let child = ''
|
||||||
@@ -37,7 +55,8 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
remaining = childList.remaining
|
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 += `<li class="card-category-list-item ${parentClass}">`
|
||||||
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
|
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
|
||||||
result += `<span class="card-category-list-name">${cat.name}</span>`
|
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>`
|
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>`
|
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +89,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
|
|||||||
<i class="fas fa-angle-right"></i></a>`
|
<i class="fas fa-angle-right"></i></a>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return `<div class="item-headline">
|
return `<div class="item-headline"><i class="fas fa-folder-open"></i><span>${this._p('aside.card_categories')}</span>${moreButton}</div>
|
||||||
<i class="fas fa-folder-open"></i>
|
|
||||||
<span>${this._p('aside.card_categories')}</span>
|
|
||||||
${moreButton}
|
|
||||||
</div>
|
|
||||||
<ul class="card-category-list${isExpand && list.result ? ' expandBtn' : ''}" id="aside-cat-list">
|
<ul class="card-category-list${isExpand && list.result ? ' expandBtn' : ''}" id="aside-cat-list">
|
||||||
${list.result}
|
${list.result}
|
||||||
</ul>`
|
</ul>`
|
||||||
|
|||||||
@@ -8,36 +8,36 @@ hexo.extend.helper.register('getArchiveLength', function () {
|
|||||||
// Archives Page
|
// Archives Page
|
||||||
if (!year) return posts.length
|
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
|
// Create a map to count posts per period
|
||||||
const mapData = this.fragment_cache('createArchiveObj', () => {
|
const mapData = this.fragment_cache('createArchiveObj', () => {
|
||||||
const map = new Map()
|
const map = new Map()
|
||||||
posts.forEach(post => {
|
posts.forEach(post => {
|
||||||
const keyYear = getKey(post, 'year')
|
const date = post.date
|
||||||
const keyMonth = getKey(post, 'month')
|
const y = date.year()
|
||||||
const keyDay = getKey(post, 'day')
|
const m = date.month() + 1
|
||||||
|
const d = date.date()
|
||||||
|
|
||||||
if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
|
// Always track year so year archive pages can be counted
|
||||||
if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
|
const keyYear = `${y}`
|
||||||
if (daily) map.set(keyDay, (map.get(keyDay) || 0) + 1)
|
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
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
// Determine the appropriate key to fetch based on current page context
|
// Determine the appropriate key to fetch based on current page context
|
||||||
let key
|
let key
|
||||||
if (yearly && year) key = `${year}`
|
if (yearly || monthly || daily) key = `${year}`
|
||||||
if (monthly && month) key = `${year}-${month}`
|
if ((monthly || daily) && month) key = `${year}-${month}`
|
||||||
if (daily && day) key = `${year}-${month}-${day}`
|
if (daily && day) key = `${year}-${month}-${day}`
|
||||||
|
|
||||||
// Return the count for the current period or default to the total posts
|
// 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 { darkmode, aside, pjax } = this.theme
|
||||||
const start = darkmode.start || 6
|
const start = darkmode.start || 6
|
||||||
const end = darkmode.end || 18
|
const end = darkmode.end || 18
|
||||||
const { theme_color } = hexo.theme.config
|
const { theme_color: themeColor } = hexo.theme.config
|
||||||
const themeColorLight = theme_color && theme_color.enable ? theme_color.meta_theme_color_light : '#ffffff'
|
const themeColorLight = themeColor && themeColor.enable ? themeColor.meta_theme_color_light : '#ffffff'
|
||||||
const themeColorDark = theme_color && theme_color.enable ? theme_color.meta_theme_color_dark : '#0d0d0d'
|
const themeColorDark = themeColor && themeColor.enable ? themeColor.meta_theme_color_dark : '#0d0d0d'
|
||||||
|
|
||||||
const createCustomJs = () => `
|
const createCustomJs = () => `
|
||||||
const saveToLocal = {
|
const saveToLocal = {
|
||||||
set: (key, value, ttl) => {
|
set: (key, value, ttl) => {
|
||||||
if (!ttl) return
|
const data = { value }
|
||||||
const expiry = Date.now() + ttl * 86400000
|
|
||||||
localStorage.setItem(key, JSON.stringify({ value, expiry }))
|
if (ttl != null) {
|
||||||
|
data.expiry = Date.now() + ttl * 86400000
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(key, JSON.stringify(data))
|
||||||
},
|
},
|
||||||
get: key => {
|
get: key => {
|
||||||
const itemStr = localStorage.getItem(key)
|
const itemStr = localStorage.getItem(key)
|
||||||
if (!itemStr) return undefined
|
if (!itemStr) return
|
||||||
const { value, expiry } = JSON.parse(itemStr)
|
|
||||||
if (Date.now() > expiry) {
|
try {
|
||||||
|
const data = JSON.parse(itemStr)
|
||||||
|
|
||||||
|
if (data.expiry && Date.now() > data.expiry) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value
|
||||||
|
} catch {
|
||||||
localStorage.removeItem(key)
|
localStorage.removeItem(key)
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scriptCache = new Map()
|
||||||
|
const cssCache = new Map()
|
||||||
window.btf = {
|
window.btf = {
|
||||||
saveToLocal,
|
saveToLocal,
|
||||||
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
|
getScript: (url, attr = {}) => {
|
||||||
const script = document.createElement('script')
|
if (scriptCache.has(url)) {
|
||||||
script.src = url
|
return scriptCache.get(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()
|
|
||||||
}
|
}
|
||||||
script.onerror = reject
|
|
||||||
document.head.appendChild(script)
|
const promise = new Promise((resolve, reject) => {
|
||||||
}),
|
const script = document.createElement('script')
|
||||||
getCSS: (url, id) => new Promise((resolve, reject) => {
|
|
||||||
const link = document.createElement('link')
|
script.src = url
|
||||||
link.rel = 'stylesheet'
|
script.async = true
|
||||||
link.href = url
|
|
||||||
if (id) link.id = id
|
for (const key in attr) {
|
||||||
link.onload = link.onreadystatechange = () => {
|
script.setAttribute(key, attr[key])
|
||||||
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
}
|
||||||
|
|
||||||
|
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) => {
|
addGlobalFn: (key, fn, name = false, parent = window) => {
|
||||||
if (!${pjax.enable} && key.startsWith('pjax')) return
|
if (!${pjax.enable} && key.startsWith('pjax')) return
|
||||||
const globalFn = parent.globalFn || {}
|
const globalFn = parent.globalFn || {}
|
||||||
@@ -65,16 +106,17 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
if (!darkmode.enable) return ''
|
if (!darkmode.enable) return ''
|
||||||
|
|
||||||
let darkmodeJs = `
|
let darkmodeJs = `
|
||||||
|
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
||||||
const activateDarkMode = () => {
|
const activateDarkMode = () => {
|
||||||
document.documentElement.setAttribute('data-theme', 'dark')
|
document.documentElement.dataset.theme = 'dark'
|
||||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
if (metaThemeColor !== null) {
|
||||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}')
|
metaThemeColor.setAttribute('content', '${themeColorDark}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const activateLightMode = () => {
|
const activateLightMode = () => {
|
||||||
document.documentElement.setAttribute('data-theme', 'light')
|
document.documentElement.dataset.theme = 'light'
|
||||||
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
if (metaThemeColor !== null) {
|
||||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}')
|
metaThemeColor.setAttribute('content', '${themeColorLight}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +137,15 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
else if (mediaQueryDark.matches) activateDarkMode()
|
else if (mediaQueryDark.matches) activateDarkMode()
|
||||||
else {
|
else {
|
||||||
const hour = new Date().getHours()
|
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()
|
isNight ? activateDarkMode() : activateLightMode()
|
||||||
}
|
}
|
||||||
mediaQueryDark.addEventListener('change', () => {
|
mediaQueryDark.addEventListener('change', e => {
|
||||||
if (saveToLocal.get('theme') === undefined) {
|
if (saveToLocal.get('theme') === undefined) {
|
||||||
e.matches ? activateDarkMode() : activateLightMode()
|
e.matches ? activateDarkMode() : activateLightMode()
|
||||||
}
|
}
|
||||||
@@ -111,7 +158,12 @@ hexo.extend.helper.register('inject_head_js', function () {
|
|||||||
case 2:
|
case 2:
|
||||||
darkmodeJs += `
|
darkmodeJs += `
|
||||||
const hour = new Date().getHours()
|
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()
|
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
|
||||||
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
else theme === 'light' ? activateLightMode() : activateDarkMode()
|
||||||
`
|
`
|
||||||
|
|||||||
+118
-46
@@ -5,6 +5,14 @@ const { prettyUrls } = require('hexo-util')
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const moment = require('moment-timezone')
|
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('truncate', truncateContent)
|
||||||
|
|
||||||
hexo.extend.helper.register('postDesc', data => {
|
hexo.extend.helper.register('postDesc', data => {
|
||||||
@@ -13,34 +21,64 @@ hexo.extend.helper.register('postDesc', data => {
|
|||||||
|
|
||||||
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||||
const env = this
|
const env = this
|
||||||
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order } = options
|
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
|
||||||
|
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
source = source.limit(limit)
|
source = source.limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
|
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 getRandomColor = () => {
|
||||||
const randomColor = () => Math.floor(Math.random() * 201)
|
const r = Math.floor(Math.random() * 201)
|
||||||
const r = randomColor()
|
const g = Math.floor(Math.random() * 201)
|
||||||
const g = randomColor()
|
const b = Math.floor(Math.random() * 201)
|
||||||
const b = randomColor()
|
|
||||||
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateStyle = (size, unit) =>
|
const normalizeColors = input => {
|
||||||
`font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
|
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 userColors = normalizeColors(custom_colors)
|
||||||
const result = source.sort(orderby, order).map(tag => {
|
|
||||||
const ratio = length ? sizes.indexOf(tag.length) / length : 0
|
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 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>`
|
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
|
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) {
|
hexo.extend.helper.register('md5', function (path) {
|
||||||
return crypto.createHash('md5').update(decodeURI(this.url_for(path))).digest('hex')
|
return crypto.createHash('md5').update(decodeURI(this.url_for(path, { relative: false }))).digest('hex')
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('injectHtml', data => {
|
hexo.extend.helper.register('injectHtml', data => {
|
||||||
@@ -65,14 +103,14 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
const defaultTitle = this._p('page.archives')
|
const defaultTitle = this._p('page.archives')
|
||||||
if (!menu) return defaultTitle
|
if (!menu) return defaultTitle
|
||||||
|
|
||||||
const loop = (m) => {
|
const loop = m => {
|
||||||
for (const key in m) {
|
for (const [key, value] of Object.entries(m)) {
|
||||||
if (typeof m[key] === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
const result = loop(m[key])
|
const result = loop(value)
|
||||||
if (result) return result
|
if (result) return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/archives\//.test(m[key])) {
|
if (typeof value === 'string' && archiveRegex.test(value)) {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,16 +119,12 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
return loop(menu) || defaultTitle
|
return loop(menu) || defaultTitle
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getBgPath', function(path) {
|
hexo.extend.helper.register('getBgPath', function (path) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
|
||||||
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
|
|
||||||
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
|
|
||||||
|
|
||||||
if (colorPattern.test(path)) {
|
if (colorPattern.test(path)) {
|
||||||
return `background-color: ${path};`
|
return `background-color: ${path};`
|
||||||
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) {
|
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path) || simpleFilePattern.test(path)) {
|
||||||
return `background-image: url(${this.url_for(path)});`
|
return `background-image: url(${this.url_for(path)});`
|
||||||
} else {
|
} else {
|
||||||
return `background: ${path};`
|
return `background: ${path};`
|
||||||
@@ -99,37 +133,67 @@ hexo.extend.helper.register('getBgPath', function(path) {
|
|||||||
|
|
||||||
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
hexo.extend.helper.register('shuoshuoFN', (data, page) => {
|
||||||
const { limit } = 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
|
// Check if limit.value is a valid date
|
||||||
const isValidDate = date => !isNaN(Date.parse(date))
|
const isValidDate = date => !isNaN(Date.parse(date))
|
||||||
|
|
||||||
// order by 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
|
// Apply number limit or time limit conditionally
|
||||||
const limitData = data => {
|
if (limit && limit.type === 'num' && limit.value > 0) {
|
||||||
if (limit && limit.type === 'num' && limit.value > 0) {
|
processedData = processedData.slice(0, limit.value)
|
||||||
return data.slice(0, limit.value)
|
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
||||||
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
|
const limitDate = Date.parse(limit.value)
|
||||||
const limitDate = Date.parse(limit.value)
|
processedData = processedData.filter(item => Date.parse(item.date) >= limitDate)
|
||||||
return data.filter(item => Date.parse(item.date) >= limitDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
orderByDate(data)
|
|
||||||
finalResult = limitData(data)
|
|
||||||
|
|
||||||
// This is a hack method, because hexo treats time as UTC time
|
// This is a hack method, because hexo treats time as UTC time
|
||||||
// so you need to manually convert the time zone
|
// so you need to manually convert the time zone
|
||||||
finalResult.forEach(item => {
|
const timezone = hexo.config.timezone
|
||||||
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
|
processedData.forEach(item => {
|
||||||
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
|
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')
|
||||||
|
|
||||||
|
// Render the content using Hexo's rendering engine to process any tags or markdown
|
||||||
|
const mockPost = {
|
||||||
|
content: item.content,
|
||||||
|
source: item.source || page.source || '',
|
||||||
|
full_source: page.full_source || '',
|
||||||
|
path: page.path || '',
|
||||||
|
layout: item.layout || page.layout || 'post',
|
||||||
|
raw: item.raw || item.content,
|
||||||
|
draft: false,
|
||||||
|
published: true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
hexo.execFilterSync('before_post_render', mockPost, { context: hexo })
|
||||||
|
} catch (e) {
|
||||||
|
// ignore third-party plugin errors, fallback to markdown-only rendering
|
||||||
|
}
|
||||||
|
item.content = mockPost.content
|
||||||
|
|
||||||
|
const codeBlocks = []
|
||||||
|
item.content = item.content.replace(/<hexoPostRenderCodeBlock>([\s\S]*?)<\/hexoPostRenderCodeBlock>/g, (_, c) => {
|
||||||
|
codeBlocks.push(c)
|
||||||
|
return `<!--CODEBLOCK_${codeBlocks.length - 1}-->`
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
item.content = hexo.extend.tag.env.renderString(item.content, {})
|
||||||
|
} catch (e) {
|
||||||
|
// ignore tag plugin errors, fallback to markdown-only rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||||
|
|
||||||
|
item.content = item.content.replace(/<!--CODEBLOCK_(\d+)-->/g, (_, i) => codeBlocks[+i])
|
||||||
})
|
})
|
||||||
|
|
||||||
return finalResult
|
return processedData
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||||
@@ -139,7 +203,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
|||||||
if (category) return 'category'
|
if (category) return 'category'
|
||||||
if (archive) return 'archive'
|
if (archive) return 'archive'
|
||||||
if (type) {
|
if (type) {
|
||||||
if (type === 'tags' || type === 'categories') return type
|
if (type === 'tags' || type === 'categories' || type === '404') return type
|
||||||
else return 'page'
|
else return 'page'
|
||||||
}
|
}
|
||||||
if (isHome) return 'home'
|
if (isHome) return 'home'
|
||||||
@@ -147,6 +211,14 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getVersion', () => {
|
hexo.extend.helper.register('getVersion', () => {
|
||||||
const { version } = require('../../package.json')
|
return { hexo: hexo.version, theme: themeVersion }
|
||||||
return { hexo: hexo.version, theme: version }
|
})
|
||||||
|
|
||||||
|
hexo.extend.helper.register('safeJSON', data => {
|
||||||
|
// Safely serialize JSON for embedding in <script> tags
|
||||||
|
return JSON.stringify(data)
|
||||||
|
.replace(/</g, '\\u003c')
|
||||||
|
.replace(/>/g, '\\u003e')
|
||||||
|
.replace(/\u2028/g, '\\u2028')
|
||||||
|
.replace(/\u2029/g, '\\u2029')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,14 +9,21 @@
|
|||||||
|
|
||||||
const { postDesc } = require('../common/postDesc')
|
const { postDesc } = require('../common/postDesc')
|
||||||
|
|
||||||
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
hexo.extend.helper.register('related_posts', function (currentPost) {
|
||||||
let relatedPosts = []
|
const relatedPosts = new Map()
|
||||||
const tagsData = currentPost.tags
|
const tagsData = currentPost.tags
|
||||||
tagsData.length && tagsData.forEach(function (tag) {
|
|
||||||
allPosts.forEach(function (post) {
|
if (!tagsData || !tagsData.length) return ''
|
||||||
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
|
|
||||||
const getPostDesc = post.postDesc || postDesc(post, hexo)
|
tagsData.forEach(tag => {
|
||||||
const relatedPost = {
|
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,
|
title: post.title,
|
||||||
path: post.path,
|
path: post.path,
|
||||||
cover: post.cover,
|
cover: post.cover,
|
||||||
@@ -24,22 +31,17 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
updated: post.updated,
|
updated: post.updated,
|
||||||
created: post.date,
|
created: post.date,
|
||||||
postDesc: getPostDesc
|
post,
|
||||||
}
|
random: Math.random()
|
||||||
const index = findItem(relatedPosts, 'path', post.path)
|
})
|
||||||
if (index !== -1) {
|
|
||||||
relatedPosts[index].weight += 1
|
|
||||||
} else {
|
|
||||||
relatedPosts.push(relatedPost)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (relatedPosts.length === 0) {
|
if (relatedPosts.size === 0) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
let result = ''
|
|
||||||
const hexoConfig = hexo.config
|
const hexoConfig = hexo.config
|
||||||
const config = hexo.theme.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 dateType = config.related_post.date_type || 'created'
|
||||||
const headlineLang = this._p('post.recommend')
|
const headlineLang = this._p('post.recommend')
|
||||||
|
|
||||||
relatedPosts = relatedPosts.sort(compare('weight'))
|
const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => {
|
||||||
|
if (b.weight !== a.weight) {
|
||||||
if (relatedPosts.length > 0) {
|
return b.weight - a.weight
|
||||||
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>'
|
|
||||||
}
|
}
|
||||||
|
return b.random - a.random
|
||||||
|
})
|
||||||
|
|
||||||
result += '</div></div>'
|
let result = '<div class="relatedPosts">'
|
||||||
return result
|
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]
|
|
||||||
}
|
|
||||||
|
|||||||
+24
-22
@@ -1,22 +1,24 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
hexo.extend.helper.register('groupPosts', function () {
|
// Read pre-built groups from tag/series.js (hexo._seriesGroups)
|
||||||
const getGroupArray = array => {
|
// and sort each group according to the sidebar card config.
|
||||||
return array.reduce((groups, item) => {
|
hexo.extend.helper.register('groupPosts', function () {
|
||||||
const key = item.series
|
const groups = hexo._seriesGroups || {}
|
||||||
if (key) {
|
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
|
||||||
groups[key] = groups[key] || []
|
|
||||||
groups[key].push(item)
|
const result = {}
|
||||||
}
|
Object.keys(groups).forEach(key => {
|
||||||
return groups
|
const arr = groups[key].slice() // copy to avoid mutating the shared data
|
||||||
}, {})
|
if (orderBy === 'title') {
|
||||||
}
|
arr.sort((a, b) => {
|
||||||
|
const cmp = a.title.toUpperCase().localeCompare(b.title.toUpperCase())
|
||||||
const sortPosts = posts => {
|
return order === 1 ? cmp : -cmp
|
||||||
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
|
})
|
||||||
if (orderBy === 'title') return posts.sort('title', order)
|
} else {
|
||||||
return posts.sort('date', order)
|
arr.sort((a, b) => order === 1 ? a.date - b.date : b.date - a.date)
|
||||||
}
|
}
|
||||||
|
result[key] = arr
|
||||||
return getGroupArray(sortPosts(this.site.posts))
|
})
|
||||||
})
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,15 +7,16 @@
|
|||||||
|
|
||||||
'use strict'
|
'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 btn = args => {
|
||||||
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
|
||||||
|
|
||||||
const iconHTML = icon ? `<i class="${icon}"></i>` : ''
|
const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
|
||||||
const textHTML = text ? `<span>${text}</span>` : ''
|
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 })
|
hexo.extend.tag.register('btn', btn, { ends: false })
|
||||||
|
|||||||
@@ -4,24 +4,25 @@
|
|||||||
|
|
||||||
'use strict'
|
'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 flinkFn = (args, content) => {
|
||||||
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
|
||||||
let result = ''
|
let result = ''
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : ''
|
const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
|
||||||
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : ''
|
const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
|
||||||
|
|
||||||
const listResult = item.link_list.map(link => `
|
const listResult = item.link_list.map(link => `
|
||||||
<div class="flink-list-item">
|
<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">
|
<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>
|
||||||
<div class="flink-item-name">${link.name}</div>
|
<div class="flink-item-name">${escapeHTML(link.name)}</div>
|
||||||
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div>
|
<div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
|
|||||||
const parseImageContent = content => {
|
const parseImageContent = content => {
|
||||||
const images = []
|
const images = []
|
||||||
let match
|
let match
|
||||||
|
IMAGE_REGEX.lastIndex = 0
|
||||||
|
|
||||||
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
||||||
images.push({
|
images.push({
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const parseArgs = args => args.join(' ').split(',')
|
const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
|
||||||
|
|
||||||
const generateStyle = (bg, color) => {
|
const generateStyle = (bg, color) => {
|
||||||
let style = 'style="'
|
let style = 'style="'
|
||||||
@@ -49,4 +49,4 @@ const hideToggle = (args, content) => {
|
|||||||
|
|
||||||
hexo.extend.tag.register('hideInline', hideInline)
|
hexo.extend.tag.register('hideInline', hideInline)
|
||||||
hexo.extend.tag.register('hideBlock', hideBlock, { ends: true })
|
hexo.extend.tag.register('hideBlock', hideBlock, { ends: true })
|
||||||
hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })
|
hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
const { escapeHTML } = require('hexo-util')
|
const { escapeHTML } = require('hexo-util')
|
||||||
|
|
||||||
const mermaid = (args, content) => {
|
const mermaid = (args, content) => {
|
||||||
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
|
const config = args[0] || '{}'
|
||||||
|
return `<div class="mermaid-wrap"><pre class="mermaid-src" data-config="${escapeHTML(config)}" hidden>
|
||||||
${escapeHTML(content)}
|
${escapeHTML(content)}
|
||||||
</pre></div>`
|
</pre></div>`
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-15
@@ -5,20 +5,10 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const { escapeHTML } = require('hexo-util')
|
||||||
|
|
||||||
const score = (args, content) => {
|
const score = (args, content) => {
|
||||||
// Escape HTML tags and some special characters, including curly braces
|
const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '{' : '}')
|
||||||
const escapeHtmlTags = s => {
|
|
||||||
const lookup = {
|
|
||||||
'&': '&',
|
|
||||||
'"': '"',
|
|
||||||
"'": ''',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'{': '{',
|
|
||||||
'}': '}'
|
|
||||||
}
|
|
||||||
return s.replace(/[&"'<>{}]/g, c => lookup[c])
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = content.trim()
|
const trimmed = content.trim()
|
||||||
// Split content using six dashes as a delimiter
|
// Split content using six dashes as a delimiter
|
||||||
@@ -37,7 +27,7 @@ const score = (args, content) => {
|
|||||||
try {
|
try {
|
||||||
paramsObj = JSON.parse(paramPart)
|
paramsObj = JSON.parse(paramPart)
|
||||||
} catch (e) {
|
} 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,
|
// Use double quotes for data-params attribute value,
|
||||||
@@ -47,4 +37,4 @@ const score = (args, content) => {
|
|||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.tag.register("score", score, { ends: true })
|
hexo.extend.tag.register('score', score, { ends: true })
|
||||||
|
|||||||
+88
-63
@@ -1,63 +1,88 @@
|
|||||||
/**
|
/**
|
||||||
* series plugin
|
* series plugin
|
||||||
* Syntax:
|
* Syntax:
|
||||||
* {% series [series name] %}
|
* {% series [series name] %}
|
||||||
* Usage:
|
* Usage:
|
||||||
* {% series %}
|
* {% series %}
|
||||||
* {% series series1 %}
|
* {% series series1 %}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||||
const groups = {}
|
|
||||||
|
// Iterate all posts once and group by series name.
|
||||||
hexo.extend.filter.register('before_post_render', data => {
|
// Result is stored on hexo._seriesGroups, shared by both tag and helper.
|
||||||
if (!hexo.theme.config.series.enable) return data
|
function buildSeriesGroups () {
|
||||||
|
const groups = {}
|
||||||
const { layout, series } = data
|
const posts = hexo.model('Post').toArray()
|
||||||
if (layout === 'post' && series) {
|
for (let i = 0, len = posts.length; i < len; i++) {
|
||||||
if (!groups[series]) groups[series] = []
|
const p = posts[i]
|
||||||
groups[series].push({
|
if (p.series) {
|
||||||
title: data.title,
|
if (!groups[p.series]) groups[p.series] = []
|
||||||
path: data.path,
|
groups[p.series].push({
|
||||||
date: data.date.unix()
|
title: p.title,
|
||||||
})
|
path: p.path,
|
||||||
}
|
date: p.date.valueOf()
|
||||||
return data
|
})
|
||||||
})
|
}
|
||||||
|
}
|
||||||
function series (args) {
|
return groups
|
||||||
const { series } = hexo.theme.config
|
}
|
||||||
if (!series.enable) {
|
|
||||||
hexo.log.warn('Series plugin is disabled in the theme config')
|
// Build once before the first render (hexo generate / hexo server initial load).
|
||||||
return ''
|
// before_generate runs after tag rendering, so we need this as a fallback.
|
||||||
}
|
hexo.extend.filter.register('before_post_render', data => {
|
||||||
|
if (!hexo.theme.config.series.enable) return data
|
||||||
const seriesArr = args.length ? groups[args[0]] : groups[this.series]
|
if (!hexo._seriesGroups) {
|
||||||
|
hexo._seriesGroups = buildSeriesGroups()
|
||||||
if (!seriesArr) {
|
}
|
||||||
hexo.log.warn(`There is no series named "${args[0]}"`)
|
return data
|
||||||
return ''
|
})
|
||||||
}
|
|
||||||
|
// Rebuild after each generate cycle in hexo server to reflect latest post data.
|
||||||
const isAsc = (series.order || 1) === 1 // 1: asc, -1: desc
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
const isSortByTitle = series.orderBy === 'title'
|
if (!hexo.theme.config.series.enable) return
|
||||||
|
hexo._seriesGroups = buildSeriesGroups()
|
||||||
const compareFn = (a, b) => {
|
})
|
||||||
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
|
|
||||||
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
|
function series (args) {
|
||||||
|
const { series } = hexo.theme.config
|
||||||
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
|
if (!series.enable) {
|
||||||
}
|
hexo.log.warn('Series plugin is disabled in the theme config')
|
||||||
|
return ''
|
||||||
seriesArr.sort(compareFn)
|
}
|
||||||
|
|
||||||
const listItems = seriesArr.map(ele =>
|
const groupName = args.length ? args[0] : this.series
|
||||||
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
|
|
||||||
).join('')
|
if (!groupName) {
|
||||||
|
hexo.log.warn('No series specified')
|
||||||
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>`
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
hexo.extend.tag.register('series', series, { ends: false })
|
const groups = hexo._seriesGroups || {}
|
||||||
|
const source = groups[groupName]
|
||||||
|
|
||||||
|
if (!source || !source.length) {
|
||||||
|
hexo.log.warn(`There is no series named "${groupName}"`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAsc = (series.order || 1) === 1
|
||||||
|
const isSortByTitle = series.orderBy === 'title'
|
||||||
|
|
||||||
|
// .slice() to copy before sorting, so we don't mutate the shared data
|
||||||
|
const seriesArr = source.slice().sort((a, b) => {
|
||||||
|
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
|
||||||
|
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
|
||||||
|
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const listItems = seriesArr.map(ele =>
|
||||||
|
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>`
|
||||||
|
}
|
||||||
|
|
||||||
|
hexo.extend.tag.register('series', series, { ends: false })
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
.fontawesomeIcon
|
.fontawesomeIcon
|
||||||
display: inline-block
|
display: inline-block
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
font-family: 'Font Awesome 6 Free'
|
font-family: 'Font Awesome 7 Free', 'Font Awesome 6 Free'
|
||||||
text-rendering: auto
|
text-rendering: auto
|
||||||
-webkit-font-smoothing: antialiased
|
-webkit-font-smoothing: antialiased
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ if hexo-config('enter_transitions')
|
|||||||
animation: titleScale 1s
|
animation: titleScale 1s
|
||||||
|
|
||||||
canvas:not(#ribbon-canvas),
|
canvas:not(#ribbon-canvas),
|
||||||
#web_bg
|
#web_bg.bg-animation
|
||||||
animation: to_show 4s
|
animation: to_show 4s
|
||||||
|
|
||||||
#ribbon-canvas
|
#ribbon-canvas
|
||||||
@@ -181,6 +181,60 @@ if hexo-config('avatar.effect') == true
|
|||||||
.reward-main
|
.reward-main
|
||||||
animation: donate_effcet .3s .1s ease both
|
animation: donate_effcet .3s .1s ease both
|
||||||
|
|
||||||
|
.btn-effects
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
transform: translateZ(0)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, .15)
|
||||||
|
text-decoration: none
|
||||||
|
transform: translateY(-1px) scale(1.02)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transition-duration: .1s
|
||||||
|
transform: translateY(0) scale(.98)
|
||||||
|
|
||||||
|
i
|
||||||
|
display: inline-block
|
||||||
|
vertical-align: middle
|
||||||
|
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
|
||||||
|
&:hover i
|
||||||
|
animation: buttonIconBounce .6s ease-in-out
|
||||||
|
|
||||||
|
i + span
|
||||||
|
margin-left: 6px
|
||||||
|
vertical-align: middle
|
||||||
|
transition: margin-left .3s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
|
||||||
|
&:hover i + span
|
||||||
|
margin-left: 8px
|
||||||
|
|
||||||
|
&::before
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: -100%
|
||||||
|
z-index: 1
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .2), transparent)
|
||||||
|
content: ''
|
||||||
|
transition: left .5s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
|
||||||
|
&:hover::before
|
||||||
|
left: 100%
|
||||||
|
|
||||||
|
& > *
|
||||||
|
position: relative
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
.btn-effects-large
|
||||||
|
&:hover
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, .2)
|
||||||
|
transform: translateY(-2px) scale(1.03)
|
||||||
|
|
||||||
@keyframes scroll-down-effect
|
@keyframes scroll-down-effect
|
||||||
0%
|
0%
|
||||||
opacity: .4
|
opacity: .4
|
||||||
@@ -278,3 +332,17 @@ if hexo-config('avatar.effect') == true
|
|||||||
|
|
||||||
100%
|
100%
|
||||||
transform: translateX(0)
|
transform: translateX(0)
|
||||||
|
|
||||||
|
@keyframes buttonIconBounce
|
||||||
|
0%,
|
||||||
|
100%
|
||||||
|
transform: translateY(0) scale(1)
|
||||||
|
|
||||||
|
25%
|
||||||
|
transform: translateY(-3px) scale(1.1) rotateZ(-5deg)
|
||||||
|
|
||||||
|
50%
|
||||||
|
transform: translateY(0) scale(1.05) rotateZ(0)
|
||||||
|
|
||||||
|
75%
|
||||||
|
transform: translateY(-1px) scale(1.02) rotateZ(2deg)
|
||||||
@@ -39,6 +39,65 @@
|
|||||||
--zoom-bg: #fff
|
--zoom-bg: #fff
|
||||||
--mark-bg: alpha($dark-black, .3)
|
--mark-bg: alpha($dark-black, .3)
|
||||||
|
|
||||||
|
// tags plugin
|
||||||
|
:root
|
||||||
|
--btn-color: $btn-color
|
||||||
|
--btn-default-color: $btn-default-color
|
||||||
|
--tags-blue-color: $tagsP-blue-color
|
||||||
|
--tags-blue-color-lighten: lighten($tagsP-blue-color, 85%)
|
||||||
|
--tags-pink-color: $tagsP-pink-color
|
||||||
|
--tags-pink-color-lighten: lighten($tagsP-pink-color, 85%)
|
||||||
|
--tags-red-color: $tagsP-red-color
|
||||||
|
--tags-red-color-lighten: lighten($tagsP-red-color, 85%)
|
||||||
|
--tags-orange-color: $tagsP-orange-color
|
||||||
|
--tags-orange-color-lighten: lighten($tagsP-orange-color, 85%)
|
||||||
|
--tags-purple-color: $tagsP-purple-color
|
||||||
|
--tags-purple-color-lighten: lighten($tagsP-purple-color, 85%)
|
||||||
|
--tags-green-color: $tagsP-green-color
|
||||||
|
--tags-green-color-lighten: lighten($tagsP-green-color, 85%)
|
||||||
|
--note-default-border: $note-default-border
|
||||||
|
--note-default-bg: $note-default-bg
|
||||||
|
--note-default-text: $note-default-text
|
||||||
|
--note-modern-default-border: $note-modern-default-border
|
||||||
|
--note-modern-default-bg: $note-modern-default-bg
|
||||||
|
--note-modern-default-text: $note-modern-default-text
|
||||||
|
--note-modern-default-hover: $note-modern-default-hover
|
||||||
|
--note-primary-border: $note-primary-border
|
||||||
|
--note-primary-bg: $note-primary-bg
|
||||||
|
--note-primary-text: $note-primary-text
|
||||||
|
--note-modern-primary-border: $note-modern-primary-border
|
||||||
|
--note-modern-primary-bg: $note-modern-primary-bg
|
||||||
|
--note-modern-primary-text: $note-modern-primary-text
|
||||||
|
--note-modern-primary-hover: $note-modern-primary-hover
|
||||||
|
--note-info-border: $note-info-border
|
||||||
|
--note-info-bg: $note-info-bg
|
||||||
|
--note-info-text: $note-info-text
|
||||||
|
--note-modern-info-border: $note-modern-info-border
|
||||||
|
--note-modern-info-bg: $note-modern-info-bg
|
||||||
|
--note-modern-info-text: $note-modern-info-text
|
||||||
|
--note-modern-info-hover: $note-modern-info-hover
|
||||||
|
--note-success-border: $note-success-border
|
||||||
|
--note-success-bg: $note-success-bg
|
||||||
|
--note-success-text: $note-success-text
|
||||||
|
--note-modern-success-border: $note-modern-success-border
|
||||||
|
--note-modern-success-bg: $note-modern-success-bg
|
||||||
|
--note-modern-success-text: $note-modern-success-text
|
||||||
|
--note-modern-success-hover: $note-modern-success-hover
|
||||||
|
--note-warning-border: $note-warning-border
|
||||||
|
--note-warning-bg: $note-warning-bg
|
||||||
|
--note-warning-text: $note-warning-text
|
||||||
|
--note-modern-warning-border: $note-modern-warning-border
|
||||||
|
--note-modern-warning-bg: $note-modern-warning-bg
|
||||||
|
--note-modern-warning-text: $note-modern-warning-text
|
||||||
|
--note-modern-warning-hover: $note-modern-warning-hover
|
||||||
|
--note-danger-border: $note-danger-border
|
||||||
|
--note-danger-bg: $note-danger-bg
|
||||||
|
--note-danger-text: $note-danger-text
|
||||||
|
--note-modern-danger-border: $note-modern-danger-border
|
||||||
|
--note-modern-danger-bg: $note-modern-danger-bg
|
||||||
|
--note-modern-danger-text: $note-modern-danger-text
|
||||||
|
--note-modern-danger-hover: $note-modern-danger-hover
|
||||||
|
|
||||||
body
|
body
|
||||||
position: relative
|
position: relative
|
||||||
overflow-y: scroll
|
overflow-y: scroll
|
||||||
@@ -72,6 +131,10 @@ body
|
|||||||
*::-webkit-scrollbar-track
|
*::-webkit-scrollbar-track
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
|
|
||||||
|
// make the scrollbar transparent when fancybox is open
|
||||||
|
html.with-fancybox
|
||||||
|
--scrollbar-color: transparent
|
||||||
|
|
||||||
input::placeholder
|
input::placeholder
|
||||||
color: var(--font-color)
|
color: var(--font-color)
|
||||||
|
|
||||||
@@ -222,3 +285,7 @@ blockquote
|
|||||||
|
|
||||||
& > :last-child
|
& > :last-child
|
||||||
margin-bottom: 0 !important
|
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)
|
--hlscrollbar-bg: lighten(#121212, 5)
|
||||||
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
--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
|
if $highlight_enable
|
||||||
@require 'highlight/index'
|
@require 'highlight/index'
|
||||||
|
|
||||||
@@ -88,6 +96,11 @@ $code-block
|
|||||||
&:hover
|
&:hover
|
||||||
border-bottom-color: var(--hl-color)
|
border-bottom-color: var(--hl-color)
|
||||||
|
|
||||||
|
&.default
|
||||||
|
pre
|
||||||
|
padding: 10px 20px
|
||||||
|
@extend $scrollbar-style
|
||||||
|
|
||||||
&.copy-true
|
&.copy-true
|
||||||
user-select: all
|
user-select: all
|
||||||
-webkit-user-select: all
|
-webkit-user-select: all
|
||||||
@@ -100,18 +113,23 @@ $code-block
|
|||||||
.highlight-tools
|
.highlight-tools
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
overflow: hidden
|
||||||
padding: 0 8px
|
padding: 0 8px
|
||||||
min-height: 24px
|
min-height: 24px
|
||||||
height: 2.15em
|
height: 2.15em
|
||||||
background: var(--hltools-bg)
|
background: var(--hltools-bg)
|
||||||
color: var(--hltools-color)
|
color: var(--hltools-color)
|
||||||
font-size: $code-font-size
|
font-size: $code-font-size
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
& > *
|
& > *
|
||||||
padding: 5px
|
flex: 0 0 auto
|
||||||
|
margin: 2px
|
||||||
|
|
||||||
i
|
i
|
||||||
|
display: inline-flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 5px
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
transition: all .3s
|
transition: all .3s
|
||||||
|
|
||||||
@@ -127,31 +145,28 @@ $code-block
|
|||||||
|
|
||||||
if !$highlight_macstyle
|
if !$highlight_macstyle
|
||||||
& > .macStyle
|
& > .macStyle
|
||||||
padding: 0
|
margin: 0
|
||||||
|
|
||||||
.code-lang
|
.code-lang
|
||||||
flex: 1
|
flex: 1 1 auto
|
||||||
|
overflow: hidden
|
||||||
|
padding-right: 10px
|
||||||
text-transform: uppercase
|
text-transform: uppercase
|
||||||
|
text-overflow: ellipsis
|
||||||
|
white-space: nowrap
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
font-size: 1.15em
|
font-size: 1.15em
|
||||||
user-select: none
|
user-select: none
|
||||||
-webkit-user-select: none
|
-webkit-user-select: none
|
||||||
padding 2px
|
|
||||||
|
|
||||||
.copy-notice
|
|
||||||
padding-right: 2px
|
|
||||||
opacity: 0
|
|
||||||
transition: opacity .4s
|
|
||||||
|
|
||||||
if hexo-config('code_blocks.language')
|
if hexo-config('code_blocks.language')
|
||||||
.code-lang
|
margin-right: auto
|
||||||
flex: 1
|
else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
|
||||||
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none')
|
& > :nth-child(2)
|
||||||
& > div:nth-child(2)
|
margin-right: auto
|
||||||
flex: 1
|
|
||||||
else
|
else
|
||||||
.macStyle
|
& > :nth-child(1)
|
||||||
flex: 1
|
margin-right: auto
|
||||||
|
|
||||||
.gutter
|
.gutter
|
||||||
user-select: none
|
user-select: none
|
||||||
@@ -163,17 +178,28 @@ $code-block
|
|||||||
td
|
td
|
||||||
border: none
|
border: none
|
||||||
|
|
||||||
|
.copy-notice
|
||||||
|
position: absolute
|
||||||
|
z-index: 99999
|
||||||
|
padding: 2px 6px
|
||||||
|
border-radius: 3px
|
||||||
|
background: var(--hltools-bg)
|
||||||
|
white-space: nowrap
|
||||||
|
font-size: 12px
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
if $highlight_macstyle
|
if $highlight_macstyle
|
||||||
.container
|
.container
|
||||||
figure.highlight
|
figure.highlight
|
||||||
margin: 0 0 24px
|
margin: 0 0 24px
|
||||||
border-radius: 7px
|
border-radius: 8px
|
||||||
box-shadow: 0 5px 10px 0 $highlight-mac-border
|
box-shadow: 0 5px 10px 0 $highlight-mac-border
|
||||||
-webkit-transform: translateZ(0)
|
-webkit-transform: translateZ(0)
|
||||||
|
|
||||||
.highlight-tools
|
.highlight-tools
|
||||||
.macStyle
|
.macStyle
|
||||||
display: flex
|
display: flex
|
||||||
|
padding: 3px
|
||||||
|
|
||||||
& > *
|
& > *
|
||||||
margin-right: 8px
|
margin-right: 8px
|
||||||
@@ -231,7 +257,7 @@ if hexo-config('code_blocks.height_limit')
|
|||||||
& ~ pre
|
& ~ pre
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
height: unit(hexo-config('code_blocks.height_limit'), px)
|
height: unit(hexo-config('code_blocks.height_limit'), px)
|
||||||
|
|
||||||
@keyframes code-expand-key
|
@keyframes code-expand-key
|
||||||
0%
|
0%
|
||||||
opacity: .6
|
opacity: .6
|
||||||
@@ -264,8 +290,8 @@ if hexo-config('code_blocks.fullpage')
|
|||||||
& ~ table
|
& ~ table
|
||||||
display: block
|
display: block
|
||||||
overflow: auto
|
overflow: auto
|
||||||
height: calc(100vh - 2.15em)
|
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
height: calc(100vh - 2.15em)
|
||||||
|
|
||||||
@keyframes code-fullpage
|
@keyframes code-fullpage
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
figure.highlight
|
figure.highlight
|
||||||
table
|
table
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
pre .deletion
|
pre .deletion
|
||||||
color: $highlight-deletion
|
color: $highlight-deletion
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ if $highlight_theme != false
|
|||||||
|
|
||||||
.container
|
.container
|
||||||
pre[class*='language-']
|
pre[class*='language-']
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
&:not(.line-numbers)
|
&:not(.line-numbers)
|
||||||
padding: 10px 20px
|
padding: 10px 20px
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
&:not(#card-toc)
|
&:not(#card-toc)
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
&:last-child
|
// &:last-child
|
||||||
margin-bottom: 0
|
// margin-bottom: 0
|
||||||
|
|
||||||
.card-info
|
.card-info
|
||||||
.author-info
|
.author-info
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
text-align: center
|
text-align: center
|
||||||
line-height: 2.4
|
line-height: 2.4
|
||||||
addBorderRadius(7)
|
addBorderRadius(7)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
@@ -180,6 +181,11 @@
|
|||||||
.card-category-list
|
.card-category-list
|
||||||
&.child
|
&.child
|
||||||
padding: 0 0 0 16px
|
padding: 0 0 0 16px
|
||||||
|
overflow: hidden
|
||||||
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
visibility: hidden
|
||||||
|
transition: max-height .3s ease, opacity .3s ease
|
||||||
|
|
||||||
> .parent
|
> .parent
|
||||||
> a
|
> a
|
||||||
@@ -188,7 +194,9 @@
|
|||||||
transform: rotate(-90deg)
|
transform: rotate(-90deg)
|
||||||
|
|
||||||
& + .child
|
& + .child
|
||||||
display: block
|
max-height: 1000px
|
||||||
|
opacity: 1
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
.card-category-list
|
.card-category-list
|
||||||
&-name
|
&-name
|
||||||
@@ -207,7 +215,9 @@
|
|||||||
|
|
||||||
if hexo-config('aside.card_categories.expand') == false
|
if hexo-config('aside.card_categories.expand') == false
|
||||||
> .child
|
> .child
|
||||||
display: none
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
visibility: hidden
|
||||||
|
|
||||||
.card-webinfo
|
.card-webinfo
|
||||||
.webinfo
|
.webinfo
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
font-size: 1.85em
|
font-size: 1.85em
|
||||||
|
@extend .limit-more-line
|
||||||
|
-webkit-line-clamp: 3
|
||||||
|
|
||||||
+minWidth768()
|
+minWidth768()
|
||||||
font-size: 2.85em
|
font-size: 2.85em
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
.pagination
|
.pagination
|
||||||
& > *
|
& > *
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0 6px
|
margin: 6px
|
||||||
width: w = 2.5em
|
width: w = 2.5em
|
||||||
height: w
|
height: w
|
||||||
line-height: w
|
line-height: w
|
||||||
|
|||||||
@@ -15,9 +15,12 @@
|
|||||||
color: var(--btn-color)
|
color: var(--btn-color)
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
addBorderRadius()
|
addBorderRadius()
|
||||||
|
@extend .btn-effects
|
||||||
|
@extend .btn-effects-large
|
||||||
|
|
||||||
i
|
i
|
||||||
margin-right: 5px
|
margin-right: 5px
|
||||||
|
vertical-align: baseline
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
.reward-button
|
.reward-button
|
||||||
@@ -28,19 +31,19 @@
|
|||||||
|
|
||||||
.reward-main
|
.reward-main
|
||||||
position: absolute
|
position: absolute
|
||||||
bottom: 40px
|
bottom: 50px
|
||||||
left: 0
|
left: 0
|
||||||
z-index: 100
|
z-index: 100
|
||||||
display: none
|
display: none
|
||||||
padding: 0 0 15px
|
padding: 0 0 15px
|
||||||
width: 100%
|
width: 100%
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.reward-all
|
.reward-all
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 20px 10px
|
padding: 20px 10px
|
||||||
background: var(--reward-pop)
|
background: var(--reward-pop)
|
||||||
|
addBorderRadius()
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|||||||
@@ -38,9 +38,13 @@
|
|||||||
font-size: 16px
|
font-size: 16px
|
||||||
line-height: w
|
line-height: w
|
||||||
addBorderRadius(5)
|
addBorderRadius(5)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
|
|
||||||
|
i
|
||||||
|
vertical-align: baseline
|
||||||
|
|
||||||
#mobile-toc-button
|
#mobile-toc-button
|
||||||
display: none
|
display: none
|
||||||
@@ -54,19 +58,52 @@
|
|||||||
|
|
||||||
if hexo-config('rightside_scroll_percent')
|
if hexo-config('rightside_scroll_percent')
|
||||||
#go-up
|
#go-up
|
||||||
|
position: relative
|
||||||
|
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
display: none
|
display: none
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
opacity: 0
|
||||||
|
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
|
i
|
||||||
|
position: relative
|
||||||
|
z-index: 1
|
||||||
|
width: 100%
|
||||||
|
opacity: 1
|
||||||
|
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
transform: scale(1)
|
||||||
|
|
||||||
&.show-percent
|
&.show-percent
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
display: block
|
display: block
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1)
|
||||||
|
animation: fadeInScale .4s ease-out
|
||||||
|
|
||||||
& + i
|
i
|
||||||
display: none
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
&:hover
|
&:hover.show-percent
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
display: none
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
& + i
|
i
|
||||||
display: block
|
opacity: 1 !important
|
||||||
|
transform: scale(1) !important
|
||||||
|
|
||||||
|
@keyframes fadeInScale
|
||||||
|
from
|
||||||
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
|
to
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1)
|
||||||
@@ -52,7 +52,10 @@
|
|||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background: var(--text-bg-hover)
|
background: var(--text-bg-hover)
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
|
transition: all .2s ease
|
||||||
|
transform: translateX(3px)
|
||||||
|
|
||||||
i:first-child
|
i:first-child
|
||||||
width: 15%
|
width: 15%
|
||||||
@@ -70,9 +73,25 @@
|
|||||||
transform: rotate(90deg)
|
transform: rotate(90deg)
|
||||||
|
|
||||||
& + .menus_item_child
|
& + .menus_item_child
|
||||||
display: none
|
overflow: hidden
|
||||||
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
transform: scaleY(0)
|
||||||
|
transform-origin: top
|
||||||
|
|
||||||
.menus_item_child
|
.menus_item_child
|
||||||
margin: 0
|
margin: 0
|
||||||
padding-left: 25px
|
padding-left: 25px
|
||||||
list-style: none
|
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)
|
||||||
@@ -28,6 +28,107 @@
|
|||||||
.wl-comment-actions > button:not(last-child)
|
.wl-comment-actions > button:not(last-child)
|
||||||
padding-right: 4px
|
padding-right: 4px
|
||||||
|
|
||||||
|
if hexo-config('artalk.vote')
|
||||||
|
.artalk-vote
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
margin: 4px 0 20px
|
||||||
|
gap: 10px
|
||||||
|
|
||||||
|
.artalk-page-vote-up,
|
||||||
|
.artalk-page-vote-down
|
||||||
|
position: relative
|
||||||
|
display: inline-flex
|
||||||
|
align-items: center
|
||||||
|
overflow: hidden
|
||||||
|
padding: 5px 16px
|
||||||
|
border: 1px solid alpha($theme-color, .15)
|
||||||
|
gap: 6px
|
||||||
|
addBorderRadius(8)
|
||||||
|
background: var(--card-bg)
|
||||||
|
color: var(--font-color)
|
||||||
|
font-weight: 500
|
||||||
|
font-size: .95em
|
||||||
|
cursor: pointer
|
||||||
|
transition: all .2s ease
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
i,
|
||||||
|
.artalk-page-vote-up-count,
|
||||||
|
.artalk-page-vote-down-count
|
||||||
|
position: relative
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: 1em
|
||||||
|
line-height: 1
|
||||||
|
|
||||||
|
.artalk-page-vote-up-count,
|
||||||
|
.artalk-page-vote-down-count
|
||||||
|
min-width: 1em
|
||||||
|
text-align: center
|
||||||
|
font-weight: 600
|
||||||
|
font-size: .9em
|
||||||
|
font-variant-numeric: tabular-nums
|
||||||
|
|
||||||
|
&::after
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
left: 50%
|
||||||
|
z-index: 0
|
||||||
|
width: 8px
|
||||||
|
height: 8px
|
||||||
|
border-radius: 50%
|
||||||
|
content: ''
|
||||||
|
opacity: 0
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color: $theme-color
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transform: scale(.95)
|
||||||
|
|
||||||
|
&::after
|
||||||
|
animation: artalkVoteRipple .6s ease-out forwards
|
||||||
|
|
||||||
|
.artalk-page-vote-up
|
||||||
|
&:hover,
|
||||||
|
&.active
|
||||||
|
border-color: $theme-color
|
||||||
|
background: alpha($theme-color, .1)
|
||||||
|
color: $theme-color
|
||||||
|
|
||||||
|
&::after
|
||||||
|
background: alpha($theme-color, .5)
|
||||||
|
|
||||||
|
.artalk-page-vote-down
|
||||||
|
&:hover,
|
||||||
|
&.active
|
||||||
|
border-color: $light-red
|
||||||
|
background: alpha($light-red, .1)
|
||||||
|
color: $light-red
|
||||||
|
|
||||||
|
&::after
|
||||||
|
background: alpha($light-red, .5)
|
||||||
|
|
||||||
|
+maxWidth600()
|
||||||
|
gap: 8px
|
||||||
|
|
||||||
|
.artalk-page-vote-up,
|
||||||
|
.artalk-page-vote-down
|
||||||
|
padding: 4px 12px
|
||||||
|
|
||||||
|
@keyframes artalkVoteRipple
|
||||||
|
0%
|
||||||
|
opacity: .8
|
||||||
|
transform: translate(-50%, -50%) scale(0)
|
||||||
|
|
||||||
|
100%
|
||||||
|
opacity: 0
|
||||||
|
transform: translate(-50%, -50%) scale(20)
|
||||||
|
|
||||||
if hexo-config('valine.bg')
|
if hexo-config('valine.bg')
|
||||||
#vcomment
|
#vcomment
|
||||||
textarea
|
textarea
|
||||||
@@ -65,12 +166,55 @@ if hexo-config('waline.bg')
|
|||||||
|
|
||||||
if hexo-config('mermaid.enable')
|
if hexo-config('mermaid.enable')
|
||||||
.mermaid-wrap
|
.mermaid-wrap
|
||||||
|
position: relative
|
||||||
margin: 0 0 20px
|
margin: 0 0 20px
|
||||||
|
background: var(--card-bg)
|
||||||
text-align: center
|
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
|
& > svg
|
||||||
|
max-width: 100%
|
||||||
height: 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')
|
if hexo-config('mermaid.code_write')
|
||||||
pre > code.mermaid
|
pre > code.mermaid
|
||||||
display: none
|
display: none
|
||||||
@@ -150,26 +294,6 @@ if hexo-config('math.use')
|
|||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
|
|
||||||
.aplayer
|
|
||||||
color: $font-black
|
|
||||||
|
|
||||||
.container
|
|
||||||
.aplayer
|
|
||||||
margin: 0 0 20px
|
|
||||||
|
|
||||||
if hexo-config('beautify.enable')
|
|
||||||
ol,
|
|
||||||
ul
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
li
|
|
||||||
margin: 0
|
|
||||||
padding: 0 15px
|
|
||||||
|
|
||||||
&:before
|
|
||||||
content: none
|
|
||||||
|
|
||||||
.snackbar-container.snackbar-css
|
.snackbar-container.snackbar-css
|
||||||
addBorderRadius(5)
|
addBorderRadius(5)
|
||||||
opacity: .85 !important
|
opacity: .85 !important
|
||||||
@@ -183,5 +307,6 @@ if hexo-config('math.use')
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
.fancybox__toolbar__column.is-middle
|
.fancybox__toolbar__column.is-middle,
|
||||||
display: none
|
.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)
|
--timeline-bg: lighten(#121212, 5)
|
||||||
--zoom-bg: #121212
|
--zoom-bg: #121212
|
||||||
--mark-bg: alpha($dark-black, .6)
|
--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
|
#web_bg:before
|
||||||
position: absolute
|
position: absolute
|
||||||
@@ -55,9 +105,6 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
|||||||
code
|
code
|
||||||
background: $code-background
|
background: $code-background
|
||||||
|
|
||||||
.aplayer
|
|
||||||
filter: brightness(.8)
|
|
||||||
|
|
||||||
kbd
|
kbd
|
||||||
border-color: #696969
|
border-color: #696969
|
||||||
background-color: #525252
|
background-color: #525252
|
||||||
@@ -78,27 +125,22 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
|||||||
#switch-btn
|
#switch-btn
|
||||||
filter: brightness(.8)
|
filter: brightness(.8)
|
||||||
|
|
||||||
// note
|
|
||||||
if hexo-config('note.style') == 'modern' || hexo-config('note.style') == 'flat'
|
|
||||||
.note
|
|
||||||
filter: brightness(.8)
|
|
||||||
|
|
||||||
// hide-tags
|
// hide-tags
|
||||||
.hide-button,
|
.hide-button,
|
||||||
.btn-beautify,
|
.toggle-button,
|
||||||
.hl-label,
|
|
||||||
#post-outdate-notice,
|
#post-outdate-notice,
|
||||||
.error-img,
|
.error-img,
|
||||||
.container iframe,
|
.container iframe,
|
||||||
.gist,
|
.gist,
|
||||||
.ads-wrap
|
.ads-wrap,
|
||||||
|
.tag-cloud-list > a
|
||||||
filter: brightness(.8)
|
filter: brightness(.8)
|
||||||
|
|
||||||
img
|
img:not(.cover)
|
||||||
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
|
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
|
else
|
||||||
filter: brightness(.8)
|
filter: brightness(.88) contrast(.95)
|
||||||
|
|
||||||
#aside-content .aside-list > .aside-list-item:not(:last-child)
|
#aside-content .aside-list > .aside-list-item:not(:last-child)
|
||||||
border-bottom: 1px dashed alpha(#FFFFFF, .1)
|
border-bottom: 1px dashed alpha(#FFFFFF, .1)
|
||||||
@@ -148,7 +190,7 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
|
|||||||
--at-color-font: alpha(#FFFFFF, .7)
|
--at-color-font: alpha(#FFFFFF, .7)
|
||||||
--at-color-meta: alpha(#FFFFFF, .7)
|
--at-color-meta: alpha(#FFFFFF, .7)
|
||||||
--at-color-grey: alpha(#FFFFFF, .7)
|
--at-color-grey: alpha(#FFFFFF, .7)
|
||||||
|
|
||||||
.atk-send-btn,
|
.atk-send-btn,
|
||||||
.atk-badge
|
.atk-badge
|
||||||
color: alpha(#FFFFFF, .7) !important
|
color: alpha(#FFFFFF, .7) !important
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ if hexo-config('readmode')
|
|||||||
font-size: 16px
|
font-size: 16px
|
||||||
transition: background .3s
|
transition: background .3s
|
||||||
addBorderRadius(8)
|
addBorderRadius(8)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
top: initial
|
top: initial
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
padding: 20px 5px
|
padding: 20px 5px
|
||||||
|
|
||||||
+minWidth2000()
|
+minWidth2000()
|
||||||
max-width: 70%
|
max-width: 60%
|
||||||
|
|
||||||
& > div:first-child:not(.nc)
|
& > div:first-child:not(.nc)
|
||||||
@extend .cardHover
|
@extend .cardHover
|
||||||
|
|||||||
@@ -76,3 +76,110 @@
|
|||||||
|
|
||||||
&.no-comment
|
&.no-comment
|
||||||
display: none
|
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
|
.tag-cloud
|
||||||
&-list
|
&-list
|
||||||
|
animation: tagsFadeIn .6s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
|
||||||
|
&:hover a:not(:hover)
|
||||||
|
opacity: .7
|
||||||
|
transform: scale(.98)
|
||||||
|
|
||||||
a
|
a
|
||||||
|
position: relative
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 2px
|
margin: 5px
|
||||||
padding: 2px 7px
|
padding: 3px 12px
|
||||||
line-height: 1.7
|
line-height: 1.7
|
||||||
transition: all .3s
|
transition: all .3s cubic-bezier(.4, 0, .2, 1)
|
||||||
addBorderRadius(5)
|
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
|
&:hover
|
||||||
background: var(--btn-bg) !important
|
background: var(--btn-hover-color) !important
|
||||||
box-shadow: 2px 2px 6px rgba(0, 0, 0, .2)
|
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
|
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()
|
+maxWidth768()
|
||||||
zoom: .85
|
zoom: .85
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-1px) scale(1.01)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transform: translateY(0) scale(.99)
|
||||||
|
|
||||||
|
&::before
|
||||||
|
display: none
|
||||||
|
|
||||||
&-title
|
&-title
|
||||||
font-size: 2.57em
|
font-size: 2.57em
|
||||||
|
animation: titleSlideIn .8s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
font-size: 2em
|
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
|
.page-title
|
||||||
& + .tag-cloud-list
|
& + .tag-cloud-list
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|||||||
@@ -1,93 +1,13 @@
|
|||||||
#algolia-search
|
#algolia-search
|
||||||
.search-dialog
|
.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
|
.ais-Hits-list
|
||||||
margin: 0
|
+maxWidth768()
|
||||||
padding: 0
|
min-height: calc(var(--search-height) - 245px)
|
||||||
@extend .list-beauty
|
|
||||||
|
|
||||||
a
|
|
||||||
color: var(--search-a-color)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $search-color
|
|
||||||
|
|
||||||
mark
|
|
||||||
background: transparent
|
|
||||||
color: $search-keyword-highlight
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.algolia-hits-item-title
|
|
||||||
font-weight: 600
|
|
||||||
|
|
||||||
.algolia-hit-item-content
|
|
||||||
margin: 0 0 8px
|
|
||||||
word-break: break-word
|
|
||||||
|
|
||||||
.ais-Pagination
|
|
||||||
margin: 15px 0 0
|
|
||||||
padding: 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.ais-Pagination-list
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
.ais-Pagination-item
|
|
||||||
display: inline
|
|
||||||
margin: 0 4px
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
.ais-Pagination-link
|
|
||||||
display: inline-block
|
|
||||||
min-width: 24px
|
|
||||||
height: 24px
|
|
||||||
text-align: center
|
|
||||||
line-height: 24px
|
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.ais-Pagination-item--selected
|
|
||||||
a
|
|
||||||
background: $theme-paginator-color
|
|
||||||
color: #eee
|
|
||||||
cursor: default
|
|
||||||
|
|
||||||
.ais-Pagination-item--disabled
|
|
||||||
visibility: hidden
|
|
||||||
|
|
||||||
#algolia-hits
|
|
||||||
> div
|
|
||||||
overflow-y: overlay
|
|
||||||
margin: 0 -20px
|
|
||||||
padding: 0 22px
|
|
||||||
max-height: calc(80vh - 220px)
|
|
||||||
|
|
||||||
+maxWidth768()
|
|
||||||
max-height: none
|
|
||||||
height: calc(var(--search-height) - 235px)
|
|
||||||
|
|
||||||
#algolia-info
|
#algolia-info
|
||||||
div
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
.algolia-poweredBy
|
.algolia-poweredBy
|
||||||
float: right
|
float: right
|
||||||
vertical-align: text-top
|
padding-top: 2px
|
||||||
|
|
||||||
svg
|
svg
|
||||||
height: 1.1em
|
height: 1.1em
|
||||||
@@ -20,22 +20,174 @@
|
|||||||
border-radius: 0
|
border-radius: 0
|
||||||
|
|
||||||
.search-nav
|
.search-nav
|
||||||
margin: 0 0 14px
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 14px
|
||||||
color: $search-color
|
color: $search-color
|
||||||
font-size: 1.4em
|
font-size: 1.4em
|
||||||
line-height: 1
|
line-height: 1
|
||||||
|
|
||||||
.search-dialog-title
|
.search-dialog-title
|
||||||
margin-right: 10px
|
margin-right: 4px
|
||||||
|
|
||||||
|
#loading-status
|
||||||
|
&[hidden]
|
||||||
|
display: none !important
|
||||||
|
|
||||||
.search-close-button
|
.search-close-button
|
||||||
float: right
|
flex: 1
|
||||||
color: $grey
|
color: $grey
|
||||||
transition: color .2s ease-in-out
|
text-align: right
|
||||||
|
transition: all .2s ease
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: $search-color
|
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
|
hr
|
||||||
margin: 15px auto
|
margin: 15px auto
|
||||||
@extend .custom-hr
|
@extend .custom-hr
|
||||||
@@ -50,6 +202,38 @@
|
|||||||
display: none
|
display: none
|
||||||
background: rgba($dark-black, .6)
|
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'
|
if hexo-config('search.use') == 'algolia_search'
|
||||||
@require 'algolia'
|
@require 'algolia'
|
||||||
else if hexo-config('search.use') == 'local_search'
|
else if hexo-config('search.use') == 'local_search'
|
||||||
|
|||||||
@@ -1,57 +1,18 @@
|
|||||||
#local-search
|
#local-search
|
||||||
.search-dialog
|
.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
|
.search-result-list
|
||||||
overflow-y: overlay
|
|
||||||
margin: 0 -20px
|
|
||||||
padding: 0 22px
|
|
||||||
max-height: calc(80vh - 180px)
|
|
||||||
|
|
||||||
+maxWidth768()
|
+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
|
#local-search-stats
|
||||||
background: transparent
|
.search-result-stats
|
||||||
color: $search-keyword-highlight
|
text-align: left
|
||||||
font-weight: 600
|
|
||||||
|
.search-keyword
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
#loading-database ~ *
|
||||||
|
visibility: hidden
|
||||||
@@ -7,21 +7,20 @@
|
|||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0 4px 6px
|
margin: 0 4px 6px
|
||||||
padding: 0 15px
|
padding: 0 15px
|
||||||
background-color: var(--btn-beautify-color, $btn-default-color)
|
background-color: var(--btn-beautify-color, var(--btn-default-color))
|
||||||
color: $btn-color
|
color: var(--btn-color, $btn-color)
|
||||||
|
vertical-align: top
|
||||||
line-height: 2
|
line-height: 2
|
||||||
addBorderRadius()
|
addBorderRadius()
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
for $type in $color-types
|
for $type in $color-types
|
||||||
&.{$type}
|
&.{$type}
|
||||||
--btn-beautify-color: lookup('$tagsP-' + $type + '-color')
|
--btn-beautify-color: unquote('var(--tags-' + $type + '-color)')
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
|
|
||||||
i + span
|
|
||||||
margin-left: 6px
|
|
||||||
|
|
||||||
&:not(.block) + .btn-beautify:not(.block)
|
&:not(.block) + .btn-beautify:not(.block)
|
||||||
margin: 0 4px 20px
|
margin: 0 4px 20px
|
||||||
|
|
||||||
@@ -39,18 +38,26 @@
|
|||||||
|
|
||||||
&.larger
|
&.larger
|
||||||
padding: 6px 15px
|
padding: 6px 15px
|
||||||
|
@extend .btn-effects-large
|
||||||
&:hover
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
&.outline
|
&.outline
|
||||||
border: 1px solid transparent
|
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
|
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
|
&: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
|
i,
|
||||||
color: white !important
|
span
|
||||||
|
color: var(--btn-color)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
background: $tag-hide-bg
|
background: $tag-hide-bg
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
addBorderRadius()
|
addBorderRadius()
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
@@ -38,11 +39,26 @@
|
|||||||
border: 1px solid $tag-hide-toggle-bg
|
border: 1px solid $tag-hide-toggle-bg
|
||||||
addBorderRadius(5, true)
|
addBorderRadius(5, true)
|
||||||
|
|
||||||
|
& > .toggle-content
|
||||||
|
margin: 30px 24px
|
||||||
|
|
||||||
& > .toggle-button
|
& > .toggle-button
|
||||||
padding: 6px 15px
|
padding: 6px 15px
|
||||||
background: $tag-hide-toggle-bg
|
background: $tag-hide-toggle-bg
|
||||||
color: #1F2D3D
|
color: #1F2D3D
|
||||||
|
list-style: none
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
& > .toggle-content
|
&::-webkit-details-marker
|
||||||
margin: 30px 24px
|
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
|
.hl-label
|
||||||
padding: 2px 4px
|
padding: 2px 4px
|
||||||
color: $btn-color
|
color: var(--btn-color, $btn-color)
|
||||||
addBorderRadius(3)
|
addBorderRadius(3)
|
||||||
|
|
||||||
&.default
|
&.default
|
||||||
background-color: $btn-default-color
|
background-color: var(--btn-default-color)
|
||||||
|
|
||||||
for $type in $color-types
|
for $type in $color-types
|
||||||
&.{$type}
|
&.{$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
|
for $type in $color-types
|
||||||
&.{$type}
|
&.{$type}
|
||||||
&:not(.disabled)
|
&:not(.disabled)
|
||||||
border-left-color: lookup('$tagsP-' + $type + '-color') !important
|
border-left-color: unquote('var(--tags-' + $type + '-color)') !important
|
||||||
|
|
||||||
&.modern
|
&.modern
|
||||||
border-left-color: transparent !important
|
border-left-color: transparent !important
|
||||||
color: lookup('$tagsP-' + $type + '-color')
|
color: unquote('var(--tags-' + $type + '-color)')
|
||||||
|
|
||||||
&:not(.simple)
|
&:not(.simple)
|
||||||
background: lighten(lookup('$tagsP-' + $type + '-color'), 85%) !important
|
background: unquote('var(--tags-' + $type + '-color-lighten)')
|
||||||
|
|
||||||
& > .note-icon
|
& > .note-icon
|
||||||
color: lookup('$tagsP-' + $type + '-color')
|
color: unquote('var(--tags-' + $type + '-color)')
|
||||||
|
|
||||||
&.simple
|
&.simple
|
||||||
border: 1px solid #EEEEEE
|
border: 1px solid var(--note-default-border)
|
||||||
border-left-width: 5px
|
border-left-width: 5px
|
||||||
|
|
||||||
&.modern
|
&.modern
|
||||||
border: 1px solid transparent !important
|
border: 1px solid transparent !important
|
||||||
background-color: #f5f5f5
|
background-color: var(--note-modern-default-bg)
|
||||||
color: $font-black
|
color: var(--note-modern-default-text)
|
||||||
|
|
||||||
&.flat
|
&.flat
|
||||||
border: initial
|
border: initial
|
||||||
border-left: 5px solid #EEEEEE
|
border-left: 5px solid var(--note-default-border)
|
||||||
background-color: lighten(#EEEEEE, 65%)
|
background-color: var(--note-default-bg)
|
||||||
color: $font-black
|
color: var(--note-default-text)
|
||||||
|
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
@@ -90,29 +90,30 @@
|
|||||||
for $type in $note-types
|
for $type in $note-types
|
||||||
&.{$type}
|
&.{$type}
|
||||||
&.flat
|
&.flat
|
||||||
background: lookup('$note-' + $type + '-bg')
|
background: unquote('var(--note-' + $type + '-bg)')
|
||||||
|
color: var(--font-color)
|
||||||
|
|
||||||
&.modern
|
&.modern
|
||||||
border-color: lookup('$note-modern-' + $type + '-border')
|
border-color: unquote('var(--note-modern-' + $type + '-border)') !important
|
||||||
background: lookup('$note-modern-' + $type + '-bg')
|
background: unquote('var(--note-modern-' + $type + '-bg)')
|
||||||
color: lookup('$note-modern-' + $type + '-text')
|
color: unquote('var(--note-modern-' + $type + '-text)')
|
||||||
|
|
||||||
a
|
a
|
||||||
&:not(.btn)
|
&:not(.btn)
|
||||||
color: lookup('$note-modern-' + $type + '-text')
|
color: unquote('var(--note-modern-' + $type + '-text)')
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: lookup('$note-modern-' + $type + '-hover')
|
color: unquote('var(--note-modern-' + $type + '-hover)')
|
||||||
|
|
||||||
&:not(.modern)
|
&:not(.modern)
|
||||||
border-left-color: lookup('$note-' + $type + '-border')
|
border-left-color: unquote('var(--note-' + $type + '-border)')
|
||||||
|
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6
|
h6
|
||||||
color: lookup('$note-' + $type + '-text')
|
color: unquote('var(--note-' + $type + '-text)')
|
||||||
|
|
||||||
if $note-icons
|
if $note-icons
|
||||||
&:not(.no-icon)
|
&:not(.no-icon)
|
||||||
@@ -121,4 +122,4 @@
|
|||||||
|
|
||||||
&:not(.modern)
|
&:not(.modern)
|
||||||
&::before
|
&::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)
|
$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
|
$theme-toc-color = $themeColorEnable && hexo-config('theme_color.toc_color') ? convert(hexo-config('theme_color.toc_color')) : $strong-cyan
|
||||||
// font
|
// font
|
||||||
|
|
||||||
$chineseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
|
$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-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
|
$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
|
$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
|
$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'))
|
$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
|
$tab-to-top-hover-color = $theme-color
|
||||||
// Tag Plugins - timeline
|
// Tag Plugins - timeline
|
||||||
$timeline-default-color = $theme-color
|
$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
|
||||||
+256
-166
@@ -2,18 +2,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let headerContentWidth, $nav
|
let headerContentWidth, $nav
|
||||||
let mobileSidebarOpen = false
|
let mobileSidebarOpen = false
|
||||||
|
|
||||||
|
// rightsideScrollPercent
|
||||||
|
let goUpElement = null
|
||||||
|
let scrollPercentElement = null
|
||||||
|
|
||||||
const adjustMenu = init => {
|
const adjustMenu = init => {
|
||||||
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
let hideMenuIndex = false
|
||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
const blogInfoWidth = Array.from(document.querySelector('#blog-info > a').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||||
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
const menusWidth = Array.from(document.getElementById('menus').children).reduce((w, i) => w + i.offsetWidth, 0)
|
||||||
headerContentWidth = blogInfoWidth + menusWidth
|
headerContentWidth = blogInfoWidth + menusWidth
|
||||||
$nav = document.getElementById('nav')
|
$nav = document.getElementById('nav')
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化header
|
// 初始化header
|
||||||
@@ -54,21 +61,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
* 代碼
|
* 代碼
|
||||||
* 只適用於Hexo默認的代碼渲染
|
* 只適用於Hexo默認的代碼渲染
|
||||||
*/
|
*/
|
||||||
const addHighlightTool = () => {
|
const addHighlightTool = $article => {
|
||||||
const highLight = GLOBAL_CONFIG.highlight
|
const highLight = GLOBAL_CONFIG.highlight
|
||||||
if (!highLight) return
|
if (!highLight) return
|
||||||
|
|
||||||
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||||
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||||
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
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
|
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||||
|
|
||||||
const isPrismjs = plugin === 'prismjs'
|
|
||||||
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||||
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
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 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>' : ''
|
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) {
|
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||||||
btf.snackbarShow(text)
|
btf.snackbarShow(text)
|
||||||
} else {
|
} else {
|
||||||
ele.textContent = text
|
const newEle = document.createElement('div')
|
||||||
ele.style.opacity = 1
|
newEle.className = 'copy-notice'
|
||||||
setTimeout(() => { ele.style.opacity = 0 }, 800)
|
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 highlightCopyFn = (ele, clickEle) => {
|
||||||
const $buttonParent = ele.parentNode
|
const $buttonParent = ele.parentNode
|
||||||
$buttonParent.classList.add('copy-true')
|
$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)
|
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||||
if (!codeElement) return
|
if (!codeElement) return
|
||||||
copy(codeElement.innerText, clickEle.previousElementSibling)
|
copy(codeElement.innerText, clickEle)
|
||||||
$buttonParent.classList.remove('copy-true')
|
$buttonParent.classList.remove('copy-true')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,33 +173,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 獲取隱藏狀態下元素的真實高度
|
// 獲取隱藏狀態下元素的真實高度
|
||||||
const getActualHeight = item => {
|
const getActualHeight = item => {
|
||||||
const hiddenElements = new Map()
|
if (item.offsetHeight > 0) return item.offsetHeight
|
||||||
|
|
||||||
const fix = () => {
|
const clone = item.cloneNode(true)
|
||||||
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 style = 'visibility: hidden !important; display: block !important;'
|
clone.style.cssText = `
|
||||||
hiddenElements.forEach((originalStyle, elem) => {
|
position: absolute !important;
|
||||||
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
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 = () => {
|
item.parentNode.insertBefore(clone, item)
|
||||||
hiddenElements.forEach((originalStyle, elem) => {
|
const height = clone.offsetHeight
|
||||||
if (originalStyle === '') elem.removeAttribute('style')
|
clone.remove()
|
||||||
else elem.setAttribute('style', originalStyle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fix()
|
|
||||||
const height = item.offsetHeight
|
|
||||||
restore()
|
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,20 +213,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fragment.appendChild(ele)
|
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 => {
|
$figureHighlight.forEach(item => {
|
||||||
let langName = ''
|
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) {
|
if (!highlightLang) {
|
||||||
createEle('', item)
|
createEle('', item)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPrismjs) {
|
if (isNotHighlightJs) {
|
||||||
langName = item.getAttribute('data-language') || 'Code'
|
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
|
||||||
} else {
|
} else {
|
||||||
langName = item.getAttribute('class').split(' ')[1]
|
langName = item.getAttribute('class').split(' ')[1]
|
||||||
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||||
@@ -200,9 +241,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* PhotoFigcaption
|
* PhotoFigcaption
|
||||||
*/
|
*/
|
||||||
const addPhotoFigcaption = () => {
|
const addPhotoFigcaption = $article => {
|
||||||
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||||
document.querySelectorAll('#article-container img').forEach(item => {
|
$article.querySelectorAll('img').forEach(item => {
|
||||||
const altValue = item.title || item.alt
|
const altValue = item.title || item.alt
|
||||||
if (!altValue) return
|
if (!altValue) return
|
||||||
const ele = document.createElement('div')
|
const ele = document.createElement('div')
|
||||||
@@ -215,8 +256,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Lightbox
|
* Lightbox
|
||||||
*/
|
*/
|
||||||
const runLightbox = () => {
|
const runLightbox = $article => {
|
||||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
btf.loadLightbox($article.querySelectorAll('img:not(.no-lightbox)'))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const fetchUrl = async url => {
|
const fetchUrl = async url => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch URL:', error)
|
console.error('Failed to fetch URL:', error)
|
||||||
return []
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runJustifiedGallery = (container, data, config) => {
|
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 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
|
// Gallery configuration
|
||||||
const igConfig = {
|
const igConfig = {
|
||||||
@@ -254,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let isLayoutHidden = false
|
let isLayoutHidden = false
|
||||||
|
|
||||||
// Utility functions
|
// 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 createImageItem = item => {
|
||||||
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
||||||
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
||||||
|
const url = item.url ? sanitizeString(item.url) : ''
|
||||||
return `<div class="item">
|
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>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
btf.setLoading.add(container)
|
btf.setLoading.add(container)
|
||||||
ig.on('renderComplete', handleRenderComplete)
|
ig.on('renderComplete', handleRenderComplete)
|
||||||
|
|
||||||
if (isButton) {
|
if (isButton && dataLength) {
|
||||||
appendItems(1, firstLimit, true)
|
appendItems(1, firstLimit, true)
|
||||||
} else {
|
} else if (dataLength) {
|
||||||
ig.on('requestAppend', handleRequestAppend)
|
ig.on('requestAppend', handleRequestAppend)
|
||||||
ig.renderItems()
|
ig.renderItems()
|
||||||
|
} else {
|
||||||
|
btf.setLoading.remove(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
||||||
@@ -356,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const container = element.firstElementChild
|
const container = element.firstElementChild
|
||||||
const content = container.textContent
|
const content = container.textContent
|
||||||
container.textContent = ''
|
container.textContent = ''
|
||||||
element.classList.add('loaded')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
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)
|
runJustifiedGallery(container, data, config)
|
||||||
|
element.classList.add('loaded')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gallery data parsing failed:', error)
|
console.error('Gallery data parsing failed:', error)
|
||||||
}
|
}
|
||||||
@@ -380,11 +434,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
*/
|
*/
|
||||||
const rightsideScrollPercent = currentTop => {
|
const rightsideScrollPercent = currentTop => {
|
||||||
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||||
const goUpElement = document.getElementById('go-up')
|
|
||||||
|
|
||||||
|
if (!goUpElement || !scrollPercentElement) return
|
||||||
if (scrollPercent < 95) {
|
if (scrollPercent < 95) {
|
||||||
goUpElement.classList.add('show-percent')
|
goUpElement.classList.add('show-percent')
|
||||||
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
scrollPercentElement.textContent = scrollPercent
|
||||||
} else {
|
} else {
|
||||||
goUpElement.classList.remove('show-percent')
|
goUpElement.classList.remove('show-percent')
|
||||||
}
|
}
|
||||||
@@ -395,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
*/
|
*/
|
||||||
const scrollFn = () => {
|
const scrollFn = () => {
|
||||||
const $rightside = document.getElementById('rightside')
|
const $rightside = document.getElementById('rightside')
|
||||||
const innerHeight = window.innerHeight + 56
|
|
||||||
let initTop = 0
|
let initTop = 0
|
||||||
const $header = document.getElementById('page-header')
|
const $header = document.getElementById('page-header')
|
||||||
const isChatBtn = typeof chatBtn !== 'undefined'
|
const isChatBtn = typeof window.chatBtn !== 'undefined'
|
||||||
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||||
|
|
||||||
// 檢查文檔高度是否小於視窗高度
|
// 檢查文檔高度是否小於視窗高度
|
||||||
const checkDocumentHeight = () => {
|
const checkDocumentHeight = () => {
|
||||||
if (document.body.scrollHeight <= innerHeight) {
|
if (document.body.scrollHeight <= window.innerHeight + 56) {
|
||||||
$rightside.classList.add('rightside-show')
|
$rightside.classList.add('rightside-show')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果文檔高度小於視窗高度,直接返回
|
|
||||||
if (checkDocumentHeight()) return
|
|
||||||
|
|
||||||
// find the scroll direction
|
// find the scroll direction
|
||||||
const scrollDirection = currentTop => {
|
const scrollDirection = currentTop => {
|
||||||
const result = currentTop > initTop // true is down & false is up
|
const result = currentTop > initTop // true is down & false is up
|
||||||
@@ -421,7 +471,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let flag = ''
|
let flag = ''
|
||||||
const scrollTask = btf.throttle(() => {
|
const scrollTask = btf.rafThrottle(() => {
|
||||||
|
if (checkDocumentHeight()) return
|
||||||
|
|
||||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
const isDown = scrollDirection(currentTop)
|
const isDown = scrollDirection(currentTop)
|
||||||
if (currentTop > 56) {
|
if (currentTop > 56) {
|
||||||
@@ -452,19 +504,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isShowPercent && rightsideScrollPercent(currentTop)
|
isShowPercent && rightsideScrollPercent(currentTop)
|
||||||
checkDocumentHeight()
|
})
|
||||||
}, 300)
|
|
||||||
|
|
||||||
|
checkDocumentHeight()
|
||||||
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* toc,anchor
|
* toc, anchor
|
||||||
*/
|
*/
|
||||||
const scrollFnToDo = () => {
|
const scrollFnToDo = $article => {
|
||||||
const isToc = GLOBAL_CONFIG_SITE.isToc
|
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||||
const isAnchor = GLOBAL_CONFIG.isAnchor
|
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||||
const $article = document.getElementById('article-container')
|
|
||||||
|
|
||||||
if (!($article && (isToc || isAnchor))) return
|
if (!($article && (isToc || isAnchor))) return
|
||||||
|
|
||||||
@@ -477,7 +528,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||||
isExpand = $cardToc.classList.contains('is-expand')
|
isExpand = $cardToc.classList.contains('is-expand')
|
||||||
|
|
||||||
// toc元素點擊
|
|
||||||
const tocItemClickFn = e => {
|
const tocItemClickFn = e => {
|
||||||
const target = e.target.closest('.toc-link')
|
const target = e.target.closest('.toc-link')
|
||||||
if (!target) return
|
if (!target) return
|
||||||
@@ -504,82 +554,105 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 處理 hexo-blog-encrypt 事件
|
|
||||||
$cardToc.style.display = 'block'
|
$cardToc.style.display = 'block'
|
||||||
}
|
}
|
||||||
|
|
||||||
// find head position & add active class
|
|
||||||
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||||
let detectItem = ''
|
if (!$articleList.length) return
|
||||||
|
|
||||||
const findHeadPosition = top => {
|
let activeTocItem = null
|
||||||
if (top === 0) return false
|
let activeParentItems = []
|
||||||
|
|
||||||
let currentId = ''
|
const updateTocUI = currentId => {
|
||||||
let currentIndex = ''
|
const encodedAnchor = currentId ? '#' + encodeURI(decodeURI(currentId)) : ''
|
||||||
|
if (isAnchor) btf.updateAnchor(encodedAnchor)
|
||||||
|
|
||||||
for (let i = 0; i < $articleList.length; i++) {
|
if (!isToc) return
|
||||||
const ele = $articleList[i]
|
|
||||||
if (top > btf.getEleTop(ele) - 80) {
|
if (activeTocItem) activeTocItem.classList.remove('active')
|
||||||
const id = ele.id
|
activeParentItems.forEach(i => i.classList.remove('active'))
|
||||||
currentId = id ? '#' + encodeURI(id) : ''
|
activeParentItems = []
|
||||||
currentIndex = i
|
|
||||||
} else {
|
if (!currentId) {
|
||||||
break
|
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) {
|
if (!isExpand) {
|
||||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
let parent = targetLink.parentNode
|
||||||
|
while (!parent.matches('.toc')) {
|
||||||
if (currentId) {
|
if (parent.matches('li')) {
|
||||||
const currentActive = $tocLink[currentIndex]
|
parent.classList.add('active')
|
||||||
currentActive.classList.add('active')
|
activeParentItems.push(parent)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
parent = parent.parentNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// main of scroll
|
const observerOptions = {
|
||||||
const tocScrollFn = btf.throttle(() => {
|
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
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
|
|
||||||
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||||
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
$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 handleThemeChange = mode => {
|
||||||
const globalFn = window.globalFn || {}
|
const globalFn = window.globalFn || {}
|
||||||
const themeChange = globalFn.themeChange || {}
|
const themeChange = globalFn.themeChange
|
||||||
if (!themeChange) {
|
if (!themeChange) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(themeChange).forEach(key => {
|
Object.keys(themeChange).forEach(key => {
|
||||||
const themeChangeFn = themeChange[key]
|
const fn = themeChange[key]
|
||||||
|
if (typeof fn !== 'function') return
|
||||||
|
|
||||||
if (['disqus', 'disqusjs'].includes(key)) {
|
if (['disqus', 'disqusjs'].includes(key)) {
|
||||||
setTimeout(() => themeChangeFn(mode), 300)
|
setTimeout(() => fn(mode), 300)
|
||||||
} else {
|
} else {
|
||||||
themeChangeFn(mode)
|
fn(mode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -600,7 +673,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
$body.classList.add('read-mode')
|
$body.classList.add('read-mode')
|
||||||
newEle.type = 'button'
|
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)
|
newEle.addEventListener('click', exitReadMode)
|
||||||
$body.appendChild(newEle)
|
$body.appendChild(newEle)
|
||||||
},
|
},
|
||||||
@@ -698,7 +772,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const addCopyright = () => {
|
const addCopyright = () => {
|
||||||
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
||||||
|
|
||||||
const handleCopy = (e) => {
|
const handleCopy = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const copyFont = window.getSelection(0).toString()
|
const copyFont = window.getSelection(0).toString()
|
||||||
let textFont = copyFont
|
let textFont = copyFont
|
||||||
@@ -740,8 +814,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* table overflow
|
* table overflow
|
||||||
*/
|
*/
|
||||||
const addTableWrap = () => {
|
const addTableWrap = $article => {
|
||||||
const $table = document.querySelectorAll('#article-container table')
|
const $table = $article.querySelectorAll('table')
|
||||||
if (!$table.length) return
|
if (!$table.length) return
|
||||||
|
|
||||||
$table.forEach(item => {
|
$table.forEach(item => {
|
||||||
@@ -751,52 +825,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const clickFnOfTagHide = $article => {
|
||||||
* tag-hide
|
const hideButtons = $article.querySelectorAll('.hide-button')
|
||||||
*/
|
|
||||||
const clickFnOfTagHide = () => {
|
|
||||||
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
|
||||||
if (!hideButtons.length) return
|
if (!hideButtons.length) return
|
||||||
hideButtons.forEach(item => item.addEventListener('click', e => {
|
|
||||||
const currentTarget = e.currentTarget
|
const handleClickOfTagHide = e => {
|
||||||
currentTarget.classList.add('open')
|
const button = e.target.closest('.hide-button')
|
||||||
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
if (!button) return
|
||||||
}, { once: true }))
|
button.classList.add('open')
|
||||||
|
addJustifiedGallery(button.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsFn = () => {
|
const tabsFn = $article => {
|
||||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
if (!$article.querySelector('.tabs')) return
|
||||||
if (!navTabsElements.length) return
|
|
||||||
|
|
||||||
const setActiveClass = (elements, activeIndex) => {
|
const setActiveClass = (elements, activeIndex) => {
|
||||||
elements.forEach((el, index) => {
|
elements.forEach((el, index) => el.classList.toggle('active', index === activeIndex))
|
||||||
el.classList.toggle('active', index === activeIndex)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNavClick = e => {
|
const handleClick = e => {
|
||||||
const target = e.target.closest('button')
|
const tabsRoot = e.target.closest('.tabs')
|
||||||
if (!target || target.classList.contains('active')) return
|
if (!tabsRoot) return
|
||||||
|
|
||||||
const navItems = [...e.currentTarget.children]
|
const navContainer = tabsRoot.firstElementChild
|
||||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
const toTopContainer = tabsRoot.lastElementChild
|
||||||
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 handleToTopClick = tabElement => e => {
|
if (navContainer.contains(e.target)) {
|
||||||
if (e.target.closest('button')) {
|
const target = e.target.closest('button')
|
||||||
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
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($article, 'click', handleClick)
|
||||||
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
|
||||||
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCardCategory = () => {
|
const toggleCardCategory = () => {
|
||||||
@@ -862,10 +938,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unRefreshFn = () => {
|
const unRefreshFn = () => {
|
||||||
window.addEventListener('resize', () => {
|
const resizeHandler = btf.rafThrottle(() => {
|
||||||
adjustMenu(false)
|
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')
|
const menuMask = document.getElementById('menu-mask')
|
||||||
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||||
@@ -883,18 +962,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const forPostFn = () => {
|
const forPostFn = () => {
|
||||||
addHighlightTool()
|
const $article = document.getElementById('article-container')
|
||||||
addPhotoFigcaption()
|
if (!$article || $article.querySelector('.hbe-container')) return
|
||||||
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
|
||||||
runLightbox()
|
addHighlightTool($article)
|
||||||
scrollFnToDo()
|
addPhotoFigcaption($article)
|
||||||
addTableWrap()
|
addJustifiedGallery($article.querySelectorAll('.gallery-container'))
|
||||||
clickFnOfTagHide()
|
runLightbox($article)
|
||||||
tabsFn()
|
scrollFnToDo($article)
|
||||||
|
addTableWrap($article)
|
||||||
|
clickFnOfTagHide($article)
|
||||||
|
tabsFn($article)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshFn = () => {
|
const refreshFn = () => {
|
||||||
initAdjust()
|
initAdjust()
|
||||||
|
goUpElement = document.getElementById('go-up')
|
||||||
|
scrollPercentElement = goUpElement?.querySelector('.scroll-percent')
|
||||||
|
|
||||||
justifiedIndexPostUI()
|
justifiedIndexPostUI()
|
||||||
|
|
||||||
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
||||||
@@ -910,8 +995,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
|
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
|
||||||
scrollFn()
|
scrollFn()
|
||||||
|
|
||||||
forPostFn()
|
if (GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo') {
|
||||||
GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo' && btf.switchComments(document)
|
forPostFn()
|
||||||
|
btf.switchComments(document)
|
||||||
|
}
|
||||||
|
|
||||||
openMobileMenu()
|
openMobileMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,4 +1015,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fn()
|
fn()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('shuoshuo:rendered', forPostFn)
|
||||||
})
|
})
|
||||||
|
|||||||
+471
-174
@@ -1,174 +1,471 @@
|
|||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const { algolia } = GLOBAL_CONFIG
|
const { algolia } = GLOBAL_CONFIG
|
||||||
const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia
|
const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia
|
||||||
|
|
||||||
if (!appId || !apiKey || !indexName) {
|
if (!appId || !apiKey || !indexName) {
|
||||||
return console.error('Algolia setting is invalid!')
|
return console.error('Algolia setting is invalid!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const $searchMask = document.getElementById('search-mask')
|
const CONTENT_FIELDS = ['contentStripTruncate', 'contentStrip', 'content']
|
||||||
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
const HIGHLIGHT_PARAMS = {
|
||||||
|
highlightPreTag: '<mark>',
|
||||||
const animateElements = show => {
|
highlightPostTag: '</mark>',
|
||||||
const action = show ? 'animateIn' : 'animateOut'
|
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||||
const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s'
|
}
|
||||||
const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s'
|
|
||||||
btf[action]($searchMask, maskAnimation)
|
// Pre-compiled regex for tag balancing (reused across cutContent calls)
|
||||||
btf[action]($searchDialog, dialogAnimation)
|
const TAG_REGEX = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*\/?>/g
|
||||||
}
|
|
||||||
|
const $searchMask = document.getElementById('search-mask')
|
||||||
const fixSafariHeight = () => {
|
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||||
if (window.innerWidth < 768) {
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
$searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`)
|
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 openSearch = () => {
|
const $pagination = document.getElementById('algolia-pagination')
|
||||||
btf.overflowPaddingR.add()
|
const $paginationList = document.querySelector('#algolia-pagination .ais-Pagination-list')
|
||||||
animateElements(true)
|
const $stats = document.querySelector('#algolia-info .ais-Stats-text')
|
||||||
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
const $searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
||||||
|
const $searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
||||||
const handleEscape = event => {
|
|
||||||
if (event.code === 'Escape') {
|
const animateElements = show => {
|
||||||
closeSearch()
|
const action = show ? 'animateIn' : 'animateOut'
|
||||||
document.removeEventListener('keydown', handleEscape)
|
const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s'
|
||||||
}
|
const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s'
|
||||||
}
|
btf[action]($searchMask, maskAnimation)
|
||||||
|
btf[action]($searchDialog, dialogAnimation)
|
||||||
document.addEventListener('keydown', handleEscape)
|
}
|
||||||
fixSafariHeight()
|
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
const fixSafariHeight = () => {
|
||||||
}
|
if (window.innerWidth < 768) {
|
||||||
|
$searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`)
|
||||||
const closeSearch = () => {
|
}
|
||||||
btf.overflowPaddingR.remove()
|
}
|
||||||
animateElements(false)
|
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
// Debounced resize to avoid layout thrashing
|
||||||
}
|
let resizeTimer
|
||||||
|
const onResize = () => {
|
||||||
const searchClickFn = () => {
|
clearTimeout(resizeTimer)
|
||||||
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchFnOnce = () => {
|
const handleEscape = event => {
|
||||||
$searchMask.addEventListener('click', closeSearch)
|
if (event.code === 'Escape') {
|
||||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
closeSearch()
|
||||||
}
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
}
|
||||||
const cutContent = (content) => {
|
}
|
||||||
if (!content) return ''
|
|
||||||
const firstOccur = content.indexOf('<mark>')
|
const showLoading = show => {
|
||||||
let start = firstOccur - 30
|
if ($loadingStatus) $loadingStatus.hidden = !show
|
||||||
let end = firstOccur + 120
|
}
|
||||||
let pre = ''
|
|
||||||
let post = ''
|
const openSearch = () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
if (start <= 0) {
|
animateElements(true)
|
||||||
start = 0
|
showLoading(false)
|
||||||
end = 140
|
|
||||||
} else {
|
setTimeout(() => {
|
||||||
pre = '...'
|
if ($searchInput) $searchInput.focus()
|
||||||
}
|
}, 100)
|
||||||
|
|
||||||
if (end > content.length) {
|
document.addEventListener('keydown', handleEscape)
|
||||||
end = content.length
|
fixSafariHeight()
|
||||||
} else {
|
window.addEventListener('resize', onResize)
|
||||||
post = '...'
|
}
|
||||||
}
|
|
||||||
|
const closeSearch = () => {
|
||||||
return `${pre}${content.substring(start, end)}${post}`
|
btf.overflowPaddingR.remove()
|
||||||
}
|
animateElements(false)
|
||||||
|
document.removeEventListener('keydown', handleEscape)
|
||||||
const disableDiv = [
|
window.removeEventListener('resize', onResize)
|
||||||
document.getElementById('algolia-hits'),
|
}
|
||||||
document.getElementById('algolia-pagination'),
|
|
||||||
document.querySelector('#algolia-info .algolia-stats')
|
const searchClickFn = () => {
|
||||||
]
|
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||||
|
}
|
||||||
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
|
|
||||||
const search = instantsearch({
|
const searchFnOnce = () => {
|
||||||
indexName,
|
$searchMask.addEventListener('click', closeSearch)
|
||||||
searchClient: searchClient(appId, apiKey),
|
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
searchFunction (helper) {
|
}
|
||||||
disableDiv.forEach(item => {
|
|
||||||
item.style.display = helper.state.query ? '' : 'none'
|
const extractContentStr = content => {
|
||||||
})
|
if (!content) return ''
|
||||||
if (helper.state.query) helper.search()
|
if (typeof content === 'string') return content.trim()
|
||||||
}
|
if (typeof content === 'object') {
|
||||||
})
|
if (content.value !== undefined) {
|
||||||
|
const str = String(content.value).trim()
|
||||||
const widgets = [
|
return str || ''
|
||||||
instantsearch.widgets.configure({ hitsPerPage }),
|
}
|
||||||
instantsearch.widgets.searchBox({
|
if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) return ''
|
||||||
container: '#algolia-search-input',
|
try {
|
||||||
showReset: false,
|
const str = JSON.stringify(content).trim()
|
||||||
showSubmit: false,
|
return (str === '{}' || str === '[]' || str === '""') ? '' : str
|
||||||
placeholder: languages.input_placeholder,
|
} catch (e) { return '' }
|
||||||
showLoadingIndicator: true
|
}
|
||||||
}),
|
if (content.toString && typeof content.toString === 'function') {
|
||||||
instantsearch.widgets.hits({
|
const str = content.toString().trim()
|
||||||
container: '#algolia-hits',
|
return (str === '[object Object]' || str === '[object Array]') ? '' : str
|
||||||
templates: {
|
}
|
||||||
item (data) {
|
return ''
|
||||||
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
}
|
||||||
const result = data._highlightResult
|
|
||||||
const content = result.contentStripTruncate
|
const extractHighlightValue = highlightObj => {
|
||||||
? cutContent(result.contentStripTruncate.value)
|
if (!highlightObj) return ''
|
||||||
: result.contentStrip
|
if (typeof highlightObj === 'string') return highlightObj.trim()
|
||||||
? cutContent(result.contentStrip.value)
|
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
||||||
: result.content
|
return String(highlightObj.value).trim()
|
||||||
? cutContent(result.content.value)
|
}
|
||||||
: ''
|
return ''
|
||||||
return `
|
}
|
||||||
<a href="${link}" class="algolia-hit-item-link">
|
|
||||||
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
const cutContent = content => {
|
||||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
const contentStr = extractContentStr(content)
|
||||||
</a>`
|
if (!contentStr) return ''
|
||||||
},
|
|
||||||
empty (data) {
|
const firstOccur = contentStr.indexOf('<mark>')
|
||||||
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
let start = firstOccur - 30
|
||||||
}
|
let end = firstOccur + 120
|
||||||
}
|
let pre = ''
|
||||||
}),
|
let post = ''
|
||||||
instantsearch.widgets.stats({
|
|
||||||
container: '#algolia-info > .algolia-stats',
|
if (start <= 0) {
|
||||||
templates: {
|
start = 0
|
||||||
text (data) {
|
end = 140
|
||||||
const stats = languages.hits_stats
|
} else {
|
||||||
.replace(/\$\{hits}/, data.nbHits)
|
pre = '...'
|
||||||
.replace(/\$\{time}/, data.processingTimeMS)
|
}
|
||||||
return `<hr>${stats}`
|
|
||||||
}
|
if (end > contentStr.length) {
|
||||||
}
|
end = contentStr.length
|
||||||
}),
|
} else {
|
||||||
instantsearch.widgets.poweredBy({
|
post = '...'
|
||||||
container: '#algolia-info > .algolia-poweredBy'
|
}
|
||||||
}),
|
|
||||||
instantsearch.widgets.pagination({
|
let substr = contentStr.substring(start, end)
|
||||||
container: '#algolia-pagination',
|
|
||||||
totalPages: 5,
|
// Remove incomplete tags at boundaries
|
||||||
templates: {
|
const firstCloseBracket = substr.indexOf('>')
|
||||||
first: '<i class="fas fa-angle-double-left"></i>',
|
const firstOpenBracket = substr.indexOf('<')
|
||||||
last: '<i class="fas fa-angle-double-right"></i>',
|
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
||||||
previous: '<i class="fas fa-angle-left"></i>',
|
substr = substr.substring(firstCloseBracket + 1)
|
||||||
next: '<i class="fas fa-angle-right"></i>'
|
}
|
||||||
}
|
|
||||||
})
|
const lastOpenBracket = substr.lastIndexOf('<')
|
||||||
]
|
const lastCloseBracket = substr.lastIndexOf('>')
|
||||||
|
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
||||||
search.addWidgets(widgets)
|
substr = substr.substring(0, lastOpenBracket)
|
||||||
search.start()
|
}
|
||||||
searchClickFn()
|
|
||||||
searchFnOnce()
|
// Balance tags using regex
|
||||||
|
const tagStack = []
|
||||||
window.addEventListener('pjax:complete', () => {
|
let balancedStr = ''
|
||||||
if (!btf.isHidden($searchMask)) closeSearch()
|
let lastIndex = 0
|
||||||
searchClickFn()
|
let match
|
||||||
})
|
|
||||||
|
TAG_REGEX.lastIndex = 0
|
||||||
if (window.pjax) {
|
while ((match = TAG_REGEX.exec(substr)) !== null) {
|
||||||
search.on('render', () => {
|
const fullTag = match[0]
|
||||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchClient
|
||||||
|
|
||||||
|
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
|
||||||
|
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
|
||||||
|
} else if (typeof window.algoliasearch === 'function') {
|
||||||
|
searchClient = window.algoliasearch(appId, apiKey)
|
||||||
|
} else {
|
||||||
|
return console.error('Algolia search client not found!')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchClient) {
|
||||||
|
return console.error('Failed to initialize Algolia search client')
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSearch()
|
||||||
|
searchClickFn()
|
||||||
|
searchFnOnce()
|
||||||
|
|
||||||
|
window.addEventListener('pjax:complete', () => {
|
||||||
|
if (!btf.isHidden($searchMask)) closeSearch()
|
||||||
|
searchClickFn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+567
-360
@@ -1,360 +1,567 @@
|
|||||||
/**
|
/**
|
||||||
* Refer to hexo-generator-searchdb
|
* Refer to hexo-generator-searchdb
|
||||||
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
|
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
|
||||||
* Modified by hexo-theme-butterfly
|
* Modified by hexo-theme-butterfly
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class LocalSearch {
|
class LocalSearch {
|
||||||
constructor ({
|
constructor ({
|
||||||
path = '',
|
path = '',
|
||||||
unescape = false,
|
unescape = false,
|
||||||
top_n_per_article = 1
|
top_n_per_article = 1
|
||||||
}) {
|
}) {
|
||||||
this.path = path
|
this.path = path
|
||||||
this.unescape = unescape
|
this.unescape = unescape
|
||||||
this.top_n_per_article = top_n_per_article
|
this.top_n_per_article = top_n_per_article
|
||||||
this.isfetched = false
|
this.isfetched = false
|
||||||
this.datas = null
|
this.datas = null
|
||||||
}
|
this._unescapeDiv = unescape ? document.createElement('div') : null
|
||||||
|
this._processedKeywords = null
|
||||||
getIndexByWord (words, text, caseSensitive = false) {
|
}
|
||||||
const index = []
|
|
||||||
const included = new Set()
|
_processKeywords (keywords) {
|
||||||
|
if (this._processedKeywords) return this._processedKeywords
|
||||||
if (!caseSensitive) {
|
this._processedKeywords = keywords.map(word => {
|
||||||
text = text.toLowerCase()
|
if (this.unescape) {
|
||||||
}
|
this._unescapeDiv.innerText = word
|
||||||
words.forEach(word => {
|
return this._unescapeDiv.innerHTML
|
||||||
if (this.unescape) {
|
}
|
||||||
const div = document.createElement('div')
|
return word
|
||||||
div.innerText = word
|
})
|
||||||
word = div.innerHTML
|
return this._processedKeywords
|
||||||
}
|
}
|
||||||
const wordLen = word.length
|
|
||||||
if (wordLen === 0) return
|
getIndexByWord (words, text, caseSensitive = false) {
|
||||||
let startPosition = 0
|
const index = []
|
||||||
let position = -1
|
const included = new Set()
|
||||||
if (!caseSensitive) {
|
const processedWords = this._processKeywords(words)
|
||||||
word = word.toLowerCase()
|
|
||||||
}
|
if (!caseSensitive) {
|
||||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
text = text.toLowerCase()
|
||||||
index.push({ position, word })
|
}
|
||||||
included.add(word)
|
processedWords.forEach((word, i) => {
|
||||||
startPosition = position + wordLen
|
const wordLen = word.length
|
||||||
}
|
if (wordLen === 0) return
|
||||||
})
|
let startPosition = 0
|
||||||
// Sort index by position of keyword
|
let position = -1
|
||||||
index.sort((left, right) => {
|
const searchWord = caseSensitive ? word : word.toLowerCase()
|
||||||
if (left.position !== right.position) {
|
while ((position = text.indexOf(searchWord, startPosition)) > -1) {
|
||||||
return left.position - right.position
|
index.push({ position, word })
|
||||||
}
|
included.add(words[i])
|
||||||
return right.word.length - left.word.length
|
startPosition = position + wordLen
|
||||||
})
|
}
|
||||||
return [index, included]
|
})
|
||||||
}
|
// Sort index by position of keyword
|
||||||
|
index.sort((left, right) => {
|
||||||
// Merge hits into slices
|
if (left.position !== right.position) {
|
||||||
mergeIntoSlice (start, end, index) {
|
return left.position - right.position
|
||||||
let item = index[0]
|
}
|
||||||
let { position, word } = item
|
return right.word.length - left.word.length
|
||||||
const hits = []
|
})
|
||||||
const count = new Set()
|
return [index, included]
|
||||||
while (position + word.length <= end && index.length !== 0) {
|
}
|
||||||
count.add(word)
|
|
||||||
hits.push({
|
// Merge hits into slices
|
||||||
position,
|
mergeIntoSlice (start, end, index) {
|
||||||
length: word.length
|
let item = index[0]
|
||||||
})
|
let { position, word } = item
|
||||||
const wordEnd = position + word.length
|
const hits = []
|
||||||
|
const count = new Set()
|
||||||
// Move to next position of hit
|
while (position + word.length <= end && index.length !== 0) {
|
||||||
index.shift()
|
count.add(word)
|
||||||
while (index.length !== 0) {
|
hits.push({
|
||||||
item = index[0]
|
position,
|
||||||
position = item.position
|
length: word.length
|
||||||
word = item.word
|
})
|
||||||
if (wordEnd > position) {
|
const wordEnd = position + word.length
|
||||||
index.shift()
|
|
||||||
} else {
|
// Move to next position of hit
|
||||||
break
|
index.shift()
|
||||||
}
|
while (index.length !== 0) {
|
||||||
}
|
item = index[0]
|
||||||
}
|
position = item.position
|
||||||
return {
|
word = item.word
|
||||||
hits,
|
if (wordEnd > position) {
|
||||||
start,
|
index.shift()
|
||||||
end,
|
} else {
|
||||||
count: count.size
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Highlight title and content
|
return {
|
||||||
highlightKeyword (val, slice) {
|
hits,
|
||||||
let result = ''
|
start,
|
||||||
let index = slice.start
|
end,
|
||||||
for (const { position, length } of slice.hits) {
|
count: count.size
|
||||||
result += val.substring(index, position)
|
}
|
||||||
index = position + length
|
}
|
||||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
|
||||||
}
|
// Highlight title and content
|
||||||
result += val.substring(index, slice.end)
|
highlightKeyword (val, slice) {
|
||||||
return result
|
const parts = []
|
||||||
}
|
let index = slice.start
|
||||||
|
for (const { position, length } of slice.hits) {
|
||||||
getResultItems (keywords) {
|
parts.push(val.substring(index, position))
|
||||||
const resultItems = []
|
index = position + length
|
||||||
this.datas.forEach(({ title, content, url }) => {
|
parts.push(`<mark class="search-keyword">${val.substring(position, position + length)}</mark>`)
|
||||||
// The number of different keywords included in the article.
|
}
|
||||||
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
parts.push(val.substring(index, slice.end))
|
||||||
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
|
return parts.join('')
|
||||||
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
|
}
|
||||||
|
|
||||||
// Show search results
|
getResultItems (keywords) {
|
||||||
const hitCount = indexOfTitle.length + indexOfContent.length
|
const resultItems = []
|
||||||
if (hitCount === 0) return
|
this._processedKeywords = null
|
||||||
|
// Compute highlight param once instead of per-article
|
||||||
const slicesOfTitle = []
|
const highlightParam = keywords.join(' ')
|
||||||
if (indexOfTitle.length !== 0) {
|
this.datas.forEach(({ title, content, url }) => {
|
||||||
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
|
// The number of different keywords included in the article.
|
||||||
}
|
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||||
|
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
|
||||||
let slicesOfContent = []
|
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
|
||||||
while (indexOfContent.length !== 0) {
|
|
||||||
const item = indexOfContent[0]
|
// Show search results
|
||||||
const { position } = item
|
const hitCount = indexOfTitle.length + indexOfContent.length
|
||||||
// Cut out 120 characters. The maxlength of .search-input is 80.
|
if (hitCount === 0) return
|
||||||
const start = Math.max(0, position - 20)
|
|
||||||
const end = Math.min(content.length, position + 100)
|
const slicesOfTitle = []
|
||||||
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
|
if (indexOfTitle.length !== 0) {
|
||||||
}
|
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
|
||||||
|
}
|
||||||
// Sort slices in content by included keywords' count and hits' count
|
|
||||||
slicesOfContent.sort((left, right) => {
|
let slicesOfContent = []
|
||||||
if (left.count !== right.count) {
|
while (indexOfContent.length !== 0) {
|
||||||
return right.count - left.count
|
const item = indexOfContent[0]
|
||||||
} else if (left.hits.length !== right.hits.length) {
|
const { position } = item
|
||||||
return right.hits.length - left.hits.length
|
// Cut out 120 characters. The maxlength of .search-input is 80.
|
||||||
}
|
const start = Math.max(0, position - 20)
|
||||||
return left.start - right.start
|
const end = Math.min(content.length, position + 100)
|
||||||
})
|
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
|
||||||
|
}
|
||||||
// Select top N slices in content
|
|
||||||
const upperBound = parseInt(this.top_n_per_article, 10)
|
// Sort slices in content by included keywords' count and hits' count
|
||||||
if (upperBound >= 0) {
|
slicesOfContent.sort((left, right) => {
|
||||||
slicesOfContent = slicesOfContent.slice(0, upperBound)
|
if (left.count !== right.count) {
|
||||||
}
|
return right.count - left.count
|
||||||
|
} else if (left.hits.length !== right.hits.length) {
|
||||||
let resultItem = ''
|
return right.hits.length - left.hits.length
|
||||||
|
}
|
||||||
url = new URL(url, location.origin)
|
return left.start - right.start
|
||||||
url.searchParams.append('highlight', keywords.join(' '))
|
})
|
||||||
|
|
||||||
if (slicesOfTitle.length !== 0) {
|
// Select top N slices in content
|
||||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
const upperBound = parseInt(this.top_n_per_article, 10)
|
||||||
} else {
|
if (upperBound >= 0) {
|
||||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
|
slicesOfContent = slicesOfContent.slice(0, upperBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
slicesOfContent.forEach(slice => {
|
let resultItem = ''
|
||||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
|
|
||||||
})
|
url = new URL(url, location.origin)
|
||||||
|
url.searchParams.append('highlight', highlightParam)
|
||||||
resultItem += '</li>'
|
|
||||||
resultItems.push({
|
if (slicesOfTitle.length !== 0) {
|
||||||
item: resultItem,
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||||
id: resultItems.length,
|
} else {
|
||||||
hitCount,
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
|
||||||
includedCount
|
}
|
||||||
})
|
|
||||||
})
|
slicesOfContent.forEach(slice => {
|
||||||
return resultItems
|
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
|
||||||
}
|
})
|
||||||
|
|
||||||
fetchData () {
|
resultItem += '</a></li>'
|
||||||
const isXml = !this.path.endsWith('json')
|
resultItems.push({
|
||||||
fetch(this.path)
|
item: resultItem,
|
||||||
.then(response => response.text())
|
id: resultItems.length,
|
||||||
.then(res => {
|
hitCount,
|
||||||
// Get the contents from search data
|
includedCount
|
||||||
this.isfetched = true
|
})
|
||||||
this.datas = isXml
|
})
|
||||||
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
|
return resultItems
|
||||||
title: element.querySelector('title').textContent,
|
}
|
||||||
content: element.querySelector('content').textContent,
|
|
||||||
url: element.querySelector('url').textContent
|
fetchData () {
|
||||||
}))
|
const isXml = !this.path.endsWith('json')
|
||||||
: JSON.parse(res)
|
fetch(this.path)
|
||||||
// Only match articles with non-empty titles
|
.then(response => {
|
||||||
this.datas = this.datas.filter(data => data.title).map(data => {
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
data.title = data.title.trim()
|
return response.text()
|
||||||
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
|
})
|
||||||
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
|
.then(res => {
|
||||||
return data
|
// Get the contents from search data
|
||||||
})
|
this.isfetched = true
|
||||||
// Remove loading animation
|
this.datas = isXml
|
||||||
window.dispatchEvent(new Event('search:loaded'))
|
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
|
||||||
})
|
title: element.querySelector('title').textContent,
|
||||||
}
|
content: element.querySelector('content').textContent,
|
||||||
|
url: element.querySelector('url').textContent
|
||||||
// Highlight by wrapping node in mark elements with the given class name
|
}))
|
||||||
highlightText (node, slice, className) {
|
: JSON.parse(res)
|
||||||
const val = node.nodeValue
|
// Only match articles with non-empty titles
|
||||||
let index = slice.start
|
this.datas = this.datas.filter(data => data.title).map(data => {
|
||||||
const children = []
|
data.title = data.title.trim()
|
||||||
for (const { position, length } of slice.hits) {
|
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
|
||||||
const text = document.createTextNode(val.substring(index, position))
|
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
|
||||||
index = position + length
|
return data
|
||||||
const mark = document.createElement('mark')
|
})
|
||||||
mark.className = className
|
// Remove loading animation
|
||||||
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
children.push(text, mark)
|
})
|
||||||
}
|
.catch(error => {
|
||||||
node.nodeValue = val.substring(index, slice.end)
|
console.error('Local search data fetch failed:', error)
|
||||||
children.forEach(element => {
|
this.isfetched = true
|
||||||
node.parentNode.insertBefore(element, node)
|
this.datas = []
|
||||||
})
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
}
|
})
|
||||||
|
}
|
||||||
// Highlight the search words provided in the url in the text
|
|
||||||
highlightSearchWords (body) {
|
// Highlight by wrapping node in mark elements with the given class name
|
||||||
const params = new URL(location.href).searchParams.get('highlight')
|
highlightText (node, slice, className) {
|
||||||
const keywords = params ? params.split(' ') : []
|
const val = node.nodeValue
|
||||||
if (!keywords.length || !body) return
|
let index = slice.start
|
||||||
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
|
const children = []
|
||||||
const allNodes = []
|
for (const { position, length } of slice.hits) {
|
||||||
while (walk.nextNode()) {
|
const text = document.createTextNode(val.substring(index, position))
|
||||||
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
|
index = position + length
|
||||||
}
|
const mark = document.createElement('mark')
|
||||||
allNodes.forEach(node => {
|
mark.className = className
|
||||||
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
|
mark.appendChild(document.createTextNode(val.substring(position, position + length)))
|
||||||
if (!indexOfNode.length) return
|
children.push(text, mark)
|
||||||
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
|
}
|
||||||
this.highlightText(node, slice, 'search-keyword')
|
node.nodeValue = val.substring(index, slice.end)
|
||||||
})
|
children.forEach(element => {
|
||||||
}
|
node.parentNode.insertBefore(element, node)
|
||||||
}
|
})
|
||||||
|
}
|
||||||
window.addEventListener('load', () => {
|
|
||||||
// Search
|
// Highlight the search words provided in the url in the text
|
||||||
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
|
highlightSearchWords (body) {
|
||||||
const localSearch = new LocalSearch({
|
const params = new URL(location.href).searchParams.get('highlight')
|
||||||
path,
|
const keywords = params ? params.split(' ') : []
|
||||||
top_n_per_article,
|
if (!keywords.length || !body) return
|
||||||
unescape
|
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
|
||||||
})
|
const allNodes = []
|
||||||
|
while (walk.nextNode()) {
|
||||||
const input = document.querySelector('#local-search-input input')
|
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
|
||||||
const statsItem = document.getElementById('local-search-stats-wrap')
|
}
|
||||||
const $loadingStatus = document.getElementById('loading-status')
|
allNodes.forEach(node => {
|
||||||
const isXml = !path.endsWith('json')
|
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
|
||||||
|
if (!indexOfNode.length) return
|
||||||
const inputEventFunction = () => {
|
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
|
||||||
if (!localSearch.isfetched) return
|
this.highlightText(node, slice, 'search-keyword')
|
||||||
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>'
|
}
|
||||||
const keywords = searchText.split(/[-\s]+/)
|
|
||||||
const container = document.getElementById('local-search-results')
|
window.addEventListener('load', () => {
|
||||||
let resultItems = []
|
// Search
|
||||||
if (searchText.length > 0) {
|
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||||
// Perform local searching
|
const enablePagination = pagination && pagination.enable
|
||||||
resultItems = localSearch.getResultItems(keywords)
|
const localSearch = new LocalSearch({
|
||||||
}
|
path,
|
||||||
if (keywords.length === 1 && keywords[0] === '') {
|
top_n_per_article,
|
||||||
container.textContent = ''
|
unescape
|
||||||
statsItem.textContent = ''
|
})
|
||||||
} else if (resultItems.length === 0) {
|
|
||||||
container.textContent = ''
|
const $input = document.querySelector('.local-search-input input')
|
||||||
const statsDiv = document.createElement('div')
|
const $statsItem = document.getElementById('local-search-stats')
|
||||||
statsDiv.className = 'search-result-stats'
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
const $searchMask = document.getElementById('search-mask')
|
||||||
statsItem.innerHTML = statsDiv.outerHTML
|
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||||
} else {
|
const $results = document.getElementById('local-search-results')
|
||||||
resultItems.sort((left, right) => {
|
const $pagination = document.getElementById('local-search-pagination')
|
||||||
if (left.includedCount !== right.includedCount) {
|
const $paginationList = document.querySelector('#local-search-pagination .ais-Pagination-list')
|
||||||
return right.includedCount - left.includedCount
|
const isXml = !path.endsWith('json')
|
||||||
} else if (left.hitCount !== right.hitCount) {
|
|
||||||
return right.hitCount - left.hitCount
|
// Pagination variables (only initialize if pagination is enabled)
|
||||||
}
|
let currentPage = 0
|
||||||
return right.id - left.id
|
const hitsPerPage = pagination.hitsPerPage || 10
|
||||||
})
|
|
||||||
|
let currentResultItems = []
|
||||||
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
|
||||||
|
if (!enablePagination) {
|
||||||
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
|
currentPage = undefined
|
||||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
currentResultItems = undefined
|
||||||
window.pjax && window.pjax.refresh(container)
|
}
|
||||||
}
|
|
||||||
|
// Show/hide search results area
|
||||||
$loadingStatus.textContent = ''
|
const toggleResultsVisibility = hasResults => {
|
||||||
}
|
$pagination.style.display = (hasResults && enablePagination) ? '' : 'none'
|
||||||
|
}
|
||||||
let loadFlag = false
|
|
||||||
const $searchMask = document.getElementById('search-mask')
|
// Render search results for current page
|
||||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
const renderResults = (searchText, resultItems) => {
|
||||||
|
// Determine items to display based on pagination mode
|
||||||
// fix safari
|
const itemsToDisplay = enablePagination
|
||||||
const fixSafariHeight = () => {
|
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||||
if (window.innerWidth < 768) {
|
: resultItems
|
||||||
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
|
||||||
}
|
// Handle empty page in pagination mode
|
||||||
}
|
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
|
||||||
|
currentPage = 0
|
||||||
const openSearch = () => {
|
renderResults(searchText, resultItems)
|
||||||
btf.overflowPaddingR.add()
|
return
|
||||||
btf.animateIn($searchMask, 'to_show 0.5s')
|
}
|
||||||
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
|
||||||
setTimeout(() => { input.focus() }, 300)
|
// Add numbering to items
|
||||||
if (!loadFlag) {
|
const numberedItems = itemsToDisplay.map((result, index) => {
|
||||||
!localSearch.isfetched && localSearch.fetchData()
|
const itemNumber = enablePagination
|
||||||
input.addEventListener('input', inputEventFunction)
|
? currentPage * hitsPerPage + index + 1
|
||||||
loadFlag = true
|
: index + 1
|
||||||
}
|
return result.item.replace(
|
||||||
// shortcut: ESC
|
'<li class="local-search-hit-item">',
|
||||||
document.addEventListener('keydown', function f (event) {
|
`<li class="local-search-hit-item" value="${itemNumber}">`
|
||||||
if (event.code === 'Escape') {
|
)
|
||||||
closeSearch()
|
})
|
||||||
document.removeEventListener('keydown', f)
|
|
||||||
}
|
$results.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||||
})
|
|
||||||
|
// Update stats
|
||||||
fixSafariHeight()
|
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||||
}
|
$statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
|
||||||
const closeSearch = () => {
|
// Handle pagination
|
||||||
btf.overflowPaddingR.remove()
|
if (enablePagination) {
|
||||||
btf.animateOut($searchDialog, 'search_close .5s')
|
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
|
||||||
btf.animateOut($searchMask, 'to_hide 0.5s')
|
renderPagination(currentPage, nbPages, searchText)
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
}
|
||||||
}
|
|
||||||
|
const hasResults = resultItems.length > 0
|
||||||
const searchClickFn = () => {
|
toggleResultsVisibility(hasResults)
|
||||||
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
|
||||||
}
|
window.pjax && window.pjax.refresh($results)
|
||||||
|
}
|
||||||
const searchFnOnce = () => {
|
|
||||||
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
|
// Render pagination
|
||||||
$searchMask.addEventListener('click', closeSearch)
|
const renderPagination = (page, nbPages) => {
|
||||||
if (GLOBAL_CONFIG.localSearch.preload) {
|
if (nbPages <= 1) {
|
||||||
localSearch.fetchData()
|
$pagination.style.display = 'none'
|
||||||
}
|
$paginationList.innerHTML = ''
|
||||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('search:loaded', () => {
|
const isFirstPage = page === 0
|
||||||
const $loadDataItem = document.getElementById('loading-database')
|
const isLastPage = page === nbPages - 1
|
||||||
$loadDataItem.nextElementSibling.style.display = 'block'
|
|
||||||
$loadDataItem.remove()
|
// Responsive page display
|
||||||
})
|
const isMobile = window.innerWidth < 768
|
||||||
|
const maxVisiblePages = isMobile ? 3 : 5
|
||||||
searchClickFn()
|
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||||
searchFnOnce()
|
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||||
|
|
||||||
// pjax
|
// Adjust starting page to maintain max visible pages
|
||||||
window.addEventListener('pjax:complete', () => {
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
!btf.isHidden($searchMask) && closeSearch()
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
}
|
||||||
searchClickFn()
|
|
||||||
})
|
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()
|
||||||
|
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||||
|
|
||||||
|
if (searchText !== '') $loadingStatus.hidden = false
|
||||||
|
|
||||||
|
const keywords = searchText.split(/[-\s]+/)
|
||||||
|
let resultItems = []
|
||||||
|
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
resultItems = localSearch.getResultItems(keywords)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
|
clearSearchResults()
|
||||||
|
} else if (resultItems.length === 0) {
|
||||||
|
showNoResults(searchText)
|
||||||
|
} else {
|
||||||
|
// Sort results by relevance
|
||||||
|
resultItems.sort((left, right) => {
|
||||||
|
if (left.includedCount !== right.includedCount) {
|
||||||
|
return right.includedCount - left.includedCount
|
||||||
|
} else if (left.hitCount !== right.hitCount) {
|
||||||
|
return right.hitCount - left.hitCount
|
||||||
|
}
|
||||||
|
return right.id - left.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = resultItems
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
renderResults(searchText, resultItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadingStatus.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
if (!loadFlag) {
|
||||||
|
!localSearch.isfetched && localSearch.fetchData()
|
||||||
|
$input.addEventListener('input', debouncedInputEvent)
|
||||||
|
loadFlag = true
|
||||||
|
}
|
||||||
|
// shortcut: ESC
|
||||||
|
document.addEventListener('keydown', handleEscape)
|
||||||
|
|
||||||
|
fixSafariHeight()
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = () => {
|
||||||
|
btf.overflowPaddingR.remove()
|
||||||
|
btf.animateOut($searchDialog, 'search_close .5s')
|
||||||
|
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||||
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchClickFn = () => {
|
||||||
|
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchFnOnce = () => {
|
||||||
|
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
|
$searchMask.addEventListener('click', closeSearch)
|
||||||
|
if (GLOBAL_CONFIG.localSearch.preload) {
|
||||||
|
localSearch.fetchData()
|
||||||
|
}
|
||||||
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
|
||||||
|
// Pagination event delegation - only add if pagination is enabled
|
||||||
|
if (enablePagination) {
|
||||||
|
$pagination.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const link = e.target.closest('a[data-page]')
|
||||||
|
if (link) {
|
||||||
|
const page = parseInt(link.dataset.page, 10)
|
||||||
|
if (!isNaN(page) && currentResultItems.length > 0) {
|
||||||
|
currentPage = page
|
||||||
|
renderResults($input.value.trim().toLowerCase(), currentResultItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('search:loaded', () => {
|
||||||
|
const $loadDataItem = document.getElementById('loading-database')
|
||||||
|
$loadDataItem.nextElementSibling.style.visibility = 'visible'
|
||||||
|
$loadDataItem.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
searchClickFn()
|
||||||
|
searchFnOnce()
|
||||||
|
|
||||||
|
// pjax
|
||||||
|
window.addEventListener('pjax:complete', () => {
|
||||||
|
!btf.isHidden($searchMask) && closeSearch()
|
||||||
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
searchClickFn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+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