mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-08-09 03:58:51 +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
-1
@@ -1,2 +1,2 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.DS_Store
|
node_modules/
|
||||||
|
|||||||
@@ -1,117 +1,193 @@
|
|||||||
<div align="right">
|
<div align="right">
|
||||||
<a title="Chinese" href="/README_CN.md">中文</a>
|
<a title="中文" href="/README_CN.md">中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
|
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
|
||||||
|
|
||||||
# hexo-theme-butterfly
|
# hexo-theme-butterfly
|
||||||
|
|
||||||
|
A modern, elegant and feature-rich theme for Hexo
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

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

|

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

|
### 💬 Social Interaction
|
||||||

|
- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
|
||||||

|
- [x] **Dual Comments Support** - Enable two comment systems simultaneously
|
||||||

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

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## ⭐ Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
|
||||||
|
|
||||||
|
## 🤝 一起構建更美好的主題
|
||||||
|
|
||||||
|
我們相信,**開源的力量來自於每一個人的參與**!無論您是開發者、設計師還是用戶,都可以為 Butterfly 主題的發展貢獻力量。
|
||||||
|
|
||||||
|
### 💬 獲取幫助與支援
|
||||||
|
|
||||||
|
- 🐛 **發現問題?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - 讓我們一起解決!
|
||||||
|
- 💡 **有好想法?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - 分享您的創意想法!
|
||||||
|
- 📚 **學習使用?** → [官方文檔](https://butterfly.js.org/) - 詳細的使用指南
|
||||||
|
- 💬 **即時討論?** → [Telegram 群組](https://t.me/bu2fly) - 與社群成員實時交流
|
||||||
|
|
||||||
|
### 🎯 參與貢獻
|
||||||
|
|
||||||
|
想要讓 Butterfly 變得更好嗎?我們歡迎您的任何形式的貢獻:
|
||||||
|
|
||||||
|
- **🔧 代碼貢獻** - 修復 Bug、添加新功能、優化性能
|
||||||
|
- **📝 文檔完善** - 改進文檔、翻譯內容、撰寫教程
|
||||||
|
- **🎨 設計建議** - UI/UX 改進、主題配色、圖示設計
|
||||||
|
- **🧪 測試反饋** - 測試新功能、回報問題、提供使用體驗
|
||||||
|
- **💰 資金支援** - [贊助項目](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - 支持長期發展
|
||||||
|
|
||||||
|
## 📄 授權條款
|
||||||
|
|
||||||
|
本專案採用 [Apache 2.0](LICENSE) 授權條款。
|
||||||
|
|
||||||
|
## 🙏 致敬與感謝
|
||||||
|
|
||||||
|
本主題基於 [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody) 進行開發,感謝原作者的精彩創作為我們提供了靈感與基礎!
|
||||||
|
|
||||||
|
感謝所有為 Butterfly 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star!✨**
|
||||||
|
</div>
|
||||||
|
|||||||
+44
-10
@@ -13,6 +13,7 @@ nav:
|
|||||||
# Navigation bar logo image
|
# Navigation bar logo image
|
||||||
logo:
|
logo:
|
||||||
display_title: true
|
display_title: true
|
||||||
|
display_post_title: true
|
||||||
# Whether to fix navigation bar
|
# Whether to fix navigation bar
|
||||||
fixed: false
|
fixed: false
|
||||||
|
|
||||||
@@ -90,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:
|
||||||
@@ -254,12 +256,15 @@ noticeOutdate:
|
|||||||
# Footer Settings
|
# Footer Settings
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
footer:
|
footer:
|
||||||
|
nav:
|
||||||
owner:
|
owner:
|
||||||
enable: true
|
enable: true
|
||||||
since: 2019
|
since: 2025
|
||||||
custom_text:
|
|
||||||
# Copyright of theme and framework
|
# Copyright of theme and framework
|
||||||
copyright: true
|
copyright:
|
||||||
|
enable: true
|
||||||
|
version: true
|
||||||
|
custom_text:
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Aside Settings
|
# Aside Settings
|
||||||
@@ -314,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
|
||||||
@@ -399,6 +405,9 @@ rightside_item_order:
|
|||||||
# Default: toc,chat,comment
|
# Default: toc,chat,comment
|
||||||
show:
|
show:
|
||||||
|
|
||||||
|
# Animation for the bottom right config button
|
||||||
|
rightside_config_animation: true
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Global Settings
|
# Global Settings
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
@@ -481,6 +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
|
||||||
@@ -503,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
|
||||||
@@ -645,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]
|
||||||
@@ -666,6 +680,10 @@ tidio:
|
|||||||
crisp:
|
crisp:
|
||||||
website_id:
|
website_id:
|
||||||
|
|
||||||
|
# https://trtc.io/solutions/knocket
|
||||||
|
knocket:
|
||||||
|
identifier:
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Analysis
|
# Analysis
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
@@ -687,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:
|
||||||
@@ -696,6 +715,12 @@ umami_analytics:
|
|||||||
# Umami Cloud (API key) / self-hosted Umami (token)
|
# Umami Cloud (API key) / self-hosted Umami (token)
|
||||||
token:
|
token:
|
||||||
|
|
||||||
|
# https://www.googletagmanager.com/
|
||||||
|
google_tag_manager:
|
||||||
|
tag_id:
|
||||||
|
# optional
|
||||||
|
domain:
|
||||||
|
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
# Advertisement
|
# Advertisement
|
||||||
# --------------------------------------
|
# --------------------------------------
|
||||||
@@ -919,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/
|
||||||
@@ -987,6 +1016,8 @@ instantpage: false
|
|||||||
# https://github.com/verlok/vanilla-lazyload
|
# https://github.com/verlok/vanilla-lazyload
|
||||||
lazyload:
|
lazyload:
|
||||||
enable: false
|
enable: false
|
||||||
|
# Use browser's native lazyload instead of vanilla-lazyload
|
||||||
|
native: false
|
||||||
# Specify the field to use lazyload (site or post)
|
# Specify the field to use lazyload (site or post)
|
||||||
field: site
|
field: site
|
||||||
placeholder:
|
placeholder:
|
||||||
@@ -1018,7 +1049,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
|
||||||
@@ -1042,7 +1077,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}
|
||||||
@@ -1078,7 +1113,6 @@ CDN:
|
|||||||
# gitalk_css:
|
# gitalk_css:
|
||||||
# giscus:
|
# giscus:
|
||||||
# instantpage:
|
# instantpage:
|
||||||
# instantsearch:
|
|
||||||
# katex:
|
# katex:
|
||||||
# katex_copytex:
|
# katex_copytex:
|
||||||
# lazyload:
|
# lazyload:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
||||||
recommend: Related Articles
|
recommend: Related Articles
|
||||||
edit: Edit
|
edit: Edit
|
||||||
|
back_to_home: Back to Home
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: Search
|
title: Search
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
copyright_content: 'All articles on this blog are licensed under <a href="%s">%s</a> unless otherwise stated.'
|
||||||
recommend: Related Articles
|
recommend: Related Articles
|
||||||
edit: Edit
|
edit: Edit
|
||||||
|
back_to_home: Back to Home
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: Search
|
title: Search
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: Previous
|
prev: Previous
|
||||||
next: Next
|
next: Next
|
||||||
|
page_info: 'Page ${current} of ${total}'
|
||||||
|
|
||||||
comment: Comments
|
comment: Comments
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: 'このブログのすべての記事は、<a href="%s">%s</a> ライセンスの下で提供されており、特に明記されていない限り、すべての権利を留保します。転載時には出典を明記してください: <a href="%s">%s</a>。'
|
copyright_content: 'このブログのすべての記事は、<a href="%s">%s</a> ライセンスの下で提供されており、特に明記されていない限り、すべての権利を留保します。転載時には出典を明記してください: <a href="%s">%s</a>。'
|
||||||
recommend: 関連記事
|
recommend: 関連記事
|
||||||
edit: 編集
|
edit: 編集
|
||||||
|
back_to_home: ホームに戻る
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: 検索
|
title: 検索
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 前へ
|
prev: 前へ
|
||||||
next: 次へ
|
next: 次へ
|
||||||
|
page_info: '${current} ページ / 合計 ${total} ページ'
|
||||||
|
|
||||||
comment: コメント
|
comment: コメント
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: '이 블로그의 모든 글은 <a href="%s">%s</a> 라이선스를 따르며, 별도로 명시되지 않는 한 모든 권리를 보유합니다. 재배포 시 출처를 명시해 주세요: <a href="%s">%s</a>.'
|
copyright_content: '이 블로그의 모든 글은 <a href="%s">%s</a> 라이선스를 따르며, 별도로 명시되지 않는 한 모든 권리를 보유합니다. 재배포 시 출처를 명시해 주세요: <a href="%s">%s</a>.'
|
||||||
recommend: 관련 글
|
recommend: 관련 글
|
||||||
edit: 편집
|
edit: 편집
|
||||||
|
back_to_home: 홈으로 돌아가기
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: 검색
|
title: 검색
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 이전
|
prev: 이전
|
||||||
next: 다음
|
next: 다음
|
||||||
|
page_info: '${current} 페이지 / 총 ${total} 페이지'
|
||||||
|
|
||||||
comment: 댓글
|
comment: 댓글
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ post:
|
|||||||
<a href="%s" target="_blank">%s</a> 许可协议。转载请注明来源 <a href="%s" target="_blank">%s</a>!'
|
<a href="%s" target="_blank">%s</a> 许可协议。转载请注明来源 <a href="%s" target="_blank">%s</a>!'
|
||||||
recommend: 相关推荐
|
recommend: 相关推荐
|
||||||
edit: 编辑
|
edit: 编辑
|
||||||
|
back_to_home: 返回首页
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: 搜索
|
title: 搜索
|
||||||
@@ -48,6 +49,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 页 / 共 ${total} 页'
|
||||||
|
|
||||||
comment: 评论
|
comment: 评论
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: '除特別聲明外,本博客所有文章均採用<a href="%s">%s</a> 授權協議。轉載請註明出處:<a href="%s">%s</a>。'
|
copyright_content: '除特別聲明外,本博客所有文章均採用<a href="%s">%s</a> 授權協議。轉載請註明出處:<a href="%s">%s</a>。'
|
||||||
recommend: 相關文章
|
recommend: 相關文章
|
||||||
edit: 編輯
|
edit: 編輯
|
||||||
|
back_to_home: 返回首頁
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: 搜尋
|
title: 搜尋
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一頁
|
prev: 上一頁
|
||||||
next: 下一頁
|
next: 下一頁
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ post:
|
|||||||
copyright_content: '本部落格所有文章除特別聲明外,均採用<a href="%s" target="_blank">%s</a> 授權協議。轉載請註明來源 <a href="%s" target="_blank">%s</a>!'
|
copyright_content: '本部落格所有文章除特別聲明外,均採用<a href="%s" target="_blank">%s</a> 授權協議。轉載請註明來源 <a href="%s" target="_blank">%s</a>!'
|
||||||
recommend: 相關推薦
|
recommend: 相關推薦
|
||||||
edit: 編輯
|
edit: 編輯
|
||||||
|
back_to_home: 返回首頁
|
||||||
|
|
||||||
search:
|
search:
|
||||||
title: 搜尋
|
title: 搜尋
|
||||||
@@ -47,6 +48,7 @@ search:
|
|||||||
pagination:
|
pagination:
|
||||||
prev: 上一篇
|
prev: 上一篇
|
||||||
next: 下一篇
|
next: 下一篇
|
||||||
|
page_info: '第 ${current} 頁 / 共 ${total} 頁'
|
||||||
|
|
||||||
comment: 評論
|
comment: 評論
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ div
|
|||||||
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
|
||||||
|
|
||||||
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
|
||||||
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
|
script(async data-pjax src=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 })
|
||||||
|
|
||||||
|
if theme.google_tag_manager && theme.google_tag_manager.tag_id
|
||||||
|
noscript
|
||||||
|
iframe(src=`${theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/ns.html?id=${theme.google_tag_manager.tag_id}` height="0" width="0" style="display:none;visibility:hidden")
|
||||||
+31
-11
@@ -1,19 +1,39 @@
|
|||||||
#footer-wrap
|
- const { nav, owner, copyright, custom_text } = theme.footer
|
||||||
if theme.footer.owner.enable
|
|
||||||
|
if nav
|
||||||
|
.footer-flex
|
||||||
|
for block in nav
|
||||||
|
.footer-flex-items(style=`${ block.width ? 'flex-grow:' + block.width : '' }`)
|
||||||
|
for blockItem in block.content
|
||||||
|
.footer-flex-item
|
||||||
|
.footer-flex-title= blockItem.title
|
||||||
|
.footer-flex-content
|
||||||
|
for subitem in blockItem.item
|
||||||
|
if subitem.html
|
||||||
|
div!= subitem.html
|
||||||
|
else if subitem.url
|
||||||
|
a(href=url_for(subitem.url), target='_blank' title=subitem.title)= subitem.title
|
||||||
|
else if subitem.title
|
||||||
|
div!= subitem.title
|
||||||
|
.footer-other
|
||||||
|
.footer-copyright
|
||||||
|
if owner.enable
|
||||||
- const currentYear = new Date().getFullYear()
|
- const currentYear = new Date().getFullYear()
|
||||||
- const sinceYear = theme.footer.owner.since
|
- const sinceYear = owner.since
|
||||||
.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 theme.footer.copyright
|
if copyright.enable
|
||||||
- const v = getVersion()
|
- const v = copyright.version ? getVersion() : false
|
||||||
.framework-info
|
span.framework-info
|
||||||
|
if owner.enable && nav
|
||||||
|
span.footer-separator |
|
||||||
span= _p('footer.framework') + ' '
|
span= _p('footer.framework') + ' '
|
||||||
a(href='https://hexo.io')= `Hexo ${v.hexo}`
|
a(href='https://hexo.io')= `Hexo${ v ? ' ' + v.hexo : '' }`
|
||||||
span.footer-separator |
|
span.footer-separator |
|
||||||
span= _p('footer.theme') + ' '
|
span= _p('footer.theme') + ' '
|
||||||
a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly ${v.theme}`
|
a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly${ v ? ' ' + v.theme : '' }`
|
||||||
if theme.footer.custom_text
|
if theme.footer.custom_text
|
||||||
.footer_custom_text!= theme.footer.custom_text
|
.footer_custom_text!= theme.footer.custom_text
|
||||||
@@ -32,3 +32,14 @@ if theme.microsoft_clarity
|
|||||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||||
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}");
|
})(window, document, "clarity", "script", "!{theme.microsoft_clarity}");
|
||||||
|
|
||||||
|
if (theme.google_tag_manager && theme.google_tag_manager.tag_id)
|
||||||
|
script.
|
||||||
|
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
|
"!{theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','!{theme.google_tag_manager.tag_id}');
|
||||||
|
btf.addGlobalFn('pjaxComplete', () => {
|
||||||
|
dataLayer.push({'event': 'pjaxComplete', 'page_title': document.title, 'page_location': location.href, 'page_path': window.location.pathname})
|
||||||
|
}, 'google_tag_manager')
|
||||||
@@ -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
|
|
||||||
highlight = JSON.stringify({
|
|
||||||
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
|
|
||||||
highlightCopy: copy,
|
highlightCopy: copy,
|
||||||
highlightLang: language,
|
highlightLang: language,
|
||||||
highlightHeightLimit: height_limit,
|
highlightHeightLimit: height_limit,
|
||||||
highlightFullpage: fullpage,
|
highlightFullpage: fullpage,
|
||||||
highlightMacStyle: macStyle
|
highlightMacStyle: macStyle
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
script.
|
script.
|
||||||
const GLOBAL_CONFIG = {
|
const GLOBAL_CONFIG = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
if theme.structured_data && page.layout === 'post'
|
if theme.structured_data
|
||||||
|
if page.layout === 'post'
|
||||||
-
|
-
|
||||||
// use json-ld to add structured data
|
// https://developers.google.com/search/docs/appearance/structured-data/article
|
||||||
|
|
||||||
const title = page.title
|
const title = page.title
|
||||||
const url = page.permalink
|
const url = page.permalink
|
||||||
@@ -9,8 +10,8 @@ if theme.structured_data && page.layout === 'post'
|
|||||||
const datePublished = page.date.toISOString()
|
const datePublished = page.date.toISOString()
|
||||||
const dateModified = (page.updated || page.date).toISOString()
|
const dateModified = (page.updated || page.date).toISOString()
|
||||||
const author = page.copyright_author || config.author
|
const author = page.copyright_author || config.author
|
||||||
const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || site.url;
|
const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || config.url
|
||||||
const authorHref = full_url_for(authorHrefVal);
|
const authorHref = full_url_for(authorHrefVal)
|
||||||
|
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -25,9 +26,41 @@ if theme.structured_data && page.layout === 'post'
|
|||||||
"name": author,
|
"name": author,
|
||||||
"url": authorHref
|
"url": authorHref
|
||||||
}]
|
}]
|
||||||
};
|
}
|
||||||
|
|
||||||
jsonLdScript = JSON.stringify(jsonLd, null, 2);
|
jsonLdScript = JSON.stringify(jsonLd, null, 2)
|
||||||
|
-
|
||||||
|
|
||||||
|
else if is_home() && (!page.current || page.current === 1)
|
||||||
|
-
|
||||||
|
// https://developers.google.com/search/docs/appearance/site-names#website
|
||||||
|
|
||||||
|
const baseUrl = config.url;
|
||||||
|
const currentPath = url_for('/');
|
||||||
|
const isRootOrSubdomain = currentPath.split('/').filter(Boolean).length === 0;
|
||||||
|
|
||||||
|
if (isRootOrSubdomain) {
|
||||||
|
const domain = new URL(config.url).hostname;
|
||||||
|
const alternateNames = theme.structured_data.alternate_name || [];
|
||||||
|
|
||||||
|
if (config.subtitle) {
|
||||||
|
alternateNames.push(config.subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
alternateNames.push(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": config.title,
|
||||||
|
"alternateName": alternateNames,
|
||||||
|
"url": full_url_for('/'),
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonLdScript = JSON.stringify(jsonLd, null, 2)
|
||||||
|
}
|
||||||
-
|
-
|
||||||
|
|
||||||
script(type="application/ld+json").
|
script(type="application/ld+json").
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ nav#nav
|
|||||||
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
|
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
|
||||||
if theme.nav.display_title
|
if theme.nav.display_title
|
||||||
span.site-name=config.title
|
span.site-name=config.title
|
||||||
if globalPageType === 'post'
|
if globalPageType === 'post' && theme.nav.display_post_title
|
||||||
a.nav-page-title(href=url_for('/'))
|
a.nav-page-title(href=url_for('/'))
|
||||||
span.site-name=(page.title || config.title)
|
span.site-name=(page.title || config.title)
|
||||||
|
span.site-name
|
||||||
|
i.fa-solid.fa-circle-arrow-left
|
||||||
|
span= ' ' + _p('post.back_to_home')
|
||||||
|
|
||||||
#menus
|
#menus
|
||||||
if theme.search.use
|
if theme.search.use
|
||||||
#search-button
|
#search-button
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
preloader.endLoading()
|
||||||
|
} else {
|
||||||
window.addEventListener('load', preloader.endLoading)
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- page.toc = false
|
- page.toc = false
|
||||||
|
|
||||||
#article-container
|
#article-container
|
||||||
|
if page.shuoshuo_url || (site.data.shuoshuo && site.data.shuoshuo.length)
|
||||||
if page.comments !== false && theme.comments.use
|
if page.comments !== false && theme.comments.use
|
||||||
- commentsJsLoad = true
|
- commentsJsLoad = true
|
||||||
|
|
||||||
@@ -42,11 +43,19 @@
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
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,13 +66,10 @@
|
|||||||
return data.filter(item => new Date(item.date) >= limitDate)
|
return data.filter(item => new Date(item.date) >= limitDate)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatToTimeZone = (date) => {
|
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||||
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
timeZone: !{JSON.stringify(config.timezone || '')} || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
||||||
const options = {
|
|
||||||
timeZone: visitorTimeZone,
|
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -71,34 +77,60 @@
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}
|
})
|
||||||
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
|
|
||||||
.format(new Date(fullDate))
|
const formatToTimeZone = (date) => {
|
||||||
.match(/\d+/g)
|
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
|
||||||
|
const [day, month, year, hour, minute, second] = dateFormatter.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)
|
||||||
|
|
||||||
|
if (lazyConfig.placeholder) {
|
||||||
|
img.setAttribute('src', lazyConfig.placeholder)
|
||||||
|
} else {
|
||||||
|
img.removeAttribute('src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPage = 1
|
||||||
|
const itemsPerPage = 8
|
||||||
|
let totalPages = 0
|
||||||
|
let data = []
|
||||||
|
|
||||||
const renderData = (dataSlice) => {
|
const renderData = (dataSlice) => {
|
||||||
const content = dataSlice.map(item => {
|
const content = dataSlice.map(item => {
|
||||||
const formattedDate = formatToTimeZone(item.date)
|
const formattedDate = formatToTimeZone(item.date)
|
||||||
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
|
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${escapeHtml(tag)}</span>`).join('') || ''
|
||||||
const commentButton = item.key && !{commentsJsLoad}
|
const commentButton = item.key && !{commentsJsLoad || false}
|
||||||
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
|
||||||
<i class="fa-solid fa-comments"></i>
|
<i class="fa-solid fa-comments"></i>
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
const commentContainer = item.key
|
const commentContainer = item.key
|
||||||
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>`
|
? `<div class="shuoshuo-comment no-comment" data-key="${escapeHtml(item.key)}"></div>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -106,16 +138,16 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="shuoshuo-item-header">
|
<div class="shuoshuo-item-header">
|
||||||
<div class="shuoshuo-avatar">
|
<div class="shuoshuo-avatar">
|
||||||
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
|
<img class="no-lightbox" src="${item.avatar ? escapeHtml(item.avatar) : !{JSON.stringify(url_for(theme.avatar.img))}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="shuoshuo-info">
|
<div class="shuoshuo-info">
|
||||||
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
|
<div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(config.author || '')}}</div>
|
||||||
<time class="shuoshuo-date" title="${formattedDate}">
|
<time class="shuoshuo-date" title="${formattedDate}">
|
||||||
${btf.diffDate(formattedDate, true)}
|
${btf.diffDate(formattedDate, true)}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shuoshuo-content">${item.content}</div>
|
<div class="shuoshuo-content">${addLazyload(item.content)}</div>
|
||||||
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
|
||||||
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
|
||||||
${commentButton}
|
${commentButton}
|
||||||
@@ -125,64 +157,153 @@
|
|||||||
</div>`
|
</div>`
|
||||||
}).join('')
|
}).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)'))
|
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIntersection = (entries) => {
|
const setupNavEvents = (nav) => {
|
||||||
if (!entries[0].isIntersecting) return
|
nav.querySelector('.shuoshuo-prev-btn').addEventListener('click', () => {
|
||||||
observer.unobserve(entries[0].target)
|
if (currentPage > 1) { currentPage--; renderPage(currentPage) }
|
||||||
|
})
|
||||||
const slice = data.slice(start, start + 10)
|
nav.querySelector('.shuoshuo-next-btn').addEventListener('click', () => {
|
||||||
renderData(slice)
|
if (currentPage < totalPages) { currentPage++; renderPage(currentPage) }
|
||||||
start += 10
|
|
||||||
|
|
||||||
if (start < data.length) {
|
|
||||||
setTimeout(() => observer.observe(container.lastElementChild), 100)
|
|
||||||
} else {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(handleIntersection, {
|
|
||||||
root: null,
|
|
||||||
rootMargin: '0px',
|
|
||||||
threshold: 1.0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
renderData(data.slice(start, 10))
|
const input = nav.querySelector('.shuoshuo-page-input')
|
||||||
start += 10
|
|
||||||
|
|
||||||
if (container.lastElementChild) observer.observe(container.lastElementChild)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d$/.test(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = parseInt(value) || 0
|
||||||
|
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.target.classList.add('invalid')
|
||||||
|
setTimeout(() => e.target.classList.remove('invalid'), 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNavigation = () => {
|
||||||
|
const container = document.getElementById('article-container')
|
||||||
|
let nav = container.nextElementSibling
|
||||||
|
const pageInfoTemplate = '#{__('pagination.page_info')}'
|
||||||
|
const pageInfoText = pageInfoTemplate
|
||||||
|
.replace(/\$\{current}/g, currentPage)
|
||||||
|
.replace(/\$\{total}/g, totalPages)
|
||||||
|
|
||||||
|
if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
|
||||||
|
nav = document.createElement('div')
|
||||||
|
nav.className = 'shuoshuo-navigation'
|
||||||
|
nav.innerHTML = `
|
||||||
|
<button class="shuoshuo-prev-btn"><i class="fa-solid fa-chevron-left"></i></button>
|
||||||
|
<span class="shuoshuo-page-info"></span>
|
||||||
|
<input type="number" class="shuoshuo-page-input" min="1">
|
||||||
|
<button class="shuoshuo-next-btn"><i class="fa-solid fa-chevron-right"></i></button>
|
||||||
|
`
|
||||||
|
container.insertAdjacentElement('afterend', nav)
|
||||||
|
setupNavEvents(nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
|
||||||
|
nav.querySelector('.shuoshuo-prev-btn').disabled = currentPage === 1
|
||||||
|
nav.querySelector('.shuoshuo-next-btn').disabled = currentPage === totalPages
|
||||||
|
const input = nav.querySelector('.shuoshuo-page-input')
|
||||||
|
input.max = totalPages
|
||||||
|
input.placeholder = currentPage
|
||||||
|
}
|
||||||
|
|
||||||
|
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})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
if page.total !== 1
|
||||||
-
|
-
|
||||||
var options = {
|
var options = {
|
||||||
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
|
prev_text: '<i class="fas fa-chevron-left fa-fw"></i>',
|
||||||
@@ -7,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if globalPageType === 'post'
|
if globalPageType === 'post'
|
||||||
- let paginationOrder = theme.post_pagination === 1 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
|
- let paginationOrder = theme.post_pagination === 2 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev }
|
||||||
|
|
||||||
nav#pagination.pagination-post
|
nav#pagination.pagination-post
|
||||||
each direction, key in paginationOrder
|
each direction, key in paginationOrder
|
||||||
@@ -18,7 +19,7 @@ if globalPageType === 'post'
|
|||||||
|
|
||||||
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)'}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
- const { readmode, translate, darkmode, aside, chat } = theme
|
- const { readmode, translate, darkmode, aside, chat } = theme
|
||||||
|
|
||||||
mixin rightsideItem(array)
|
mixin rightsideItem(array)
|
||||||
each item in array
|
each item in array
|
||||||
case item
|
case item
|
||||||
@@ -30,30 +31,22 @@ mixin rightsideItem(array)
|
|||||||
a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))
|
a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))
|
||||||
i.fas.fa-comments
|
i.fas.fa-comments
|
||||||
|
|
||||||
#rightside
|
|
||||||
- const { enable, hide, show } = theme.rightside_item_order
|
- const { enable, hide, show } = theme.rightside_item_order
|
||||||
- const hideArray = enable ? hide && hide.split(',') : ['readmode','translate','darkmode','hideAside']
|
- const hideArray = enable && hide ? hide.split(',') : ['readmode','translate','darkmode','hideAside']
|
||||||
- const showArray = enable ? show && show.split(',') : ['toc','chat','comment']
|
- const showArray = enable && show ? show.split(',') : ['toc','chat','comment']
|
||||||
|
- const needCogBtn = (enable && hide) || (!enable && ((globalPageType === 'post' && (readmode || translate.enable || (darkmode.enable && darkmode.button))) || (translate.enable || (darkmode.enable && darkmode.button))))
|
||||||
|
|
||||||
|
#rightside
|
||||||
#rightside-config-hide
|
#rightside-config-hide
|
||||||
if hideArray
|
if hideArray.length
|
||||||
+rightsideItem(hideArray)
|
+rightsideItem(hideArray)
|
||||||
#rightside-config-show
|
|
||||||
if enable
|
|
||||||
if hide
|
|
||||||
button#rightside-config(type="button" title=_p("rightside.setting"))
|
|
||||||
i.fas.fa-cog.fa-spin
|
|
||||||
else
|
|
||||||
if globalPageType === 'post'
|
|
||||||
if (readmode || translate.enable || (darkmode.enable && darkmode.button))
|
|
||||||
button#rightside-config(type="button" title=_p("rightside.setting"))
|
|
||||||
i.fas.fa-cog.fa-spin
|
|
||||||
else if translate.enable || (darkmode.enable && darkmode.button)
|
|
||||||
button#rightside-config(type="button" title=_p("rightside.setting"))
|
|
||||||
i.fas.fa-cog.fa-spin
|
|
||||||
|
|
||||||
if showArray
|
#rightside-config-show
|
||||||
|
if needCogBtn
|
||||||
|
button#rightside-config(type="button" title=_p("rightside.setting"))
|
||||||
|
i.fas.fa-cog(class=theme.rightside_config_animation ? 'fa-spin' : '')
|
||||||
|
|
||||||
|
if showArray.length
|
||||||
+rightsideItem(showArray)
|
+rightsideItem(showArray)
|
||||||
|
|
||||||
button#go-up(type="button" title=_p("rightside.back_to_top"))
|
button#go-up(type="button" title=_p("rightside.back_to_top"))
|
||||||
|
|||||||
+10
-7
@@ -1,9 +1,12 @@
|
|||||||
script.
|
script.
|
||||||
(function() {
|
(() => {
|
||||||
const abcjsInit = function() {
|
const abcjsInit = () => {
|
||||||
const abcjsFn = function() {
|
const abcjsFn = () => {
|
||||||
setTimeout(function() {
|
const $article = document.getElementById("article-container")
|
||||||
const sheets = document.querySelectorAll(".abc-music-sheet")
|
if (!$article || $article.querySelector(".hbe-container")) return
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
if (res[index]) {
|
||||||
item.textContent = res[index].count
|
item.textContent = res[index].count
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
|||||||
+11
-5
@@ -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')
|
||||||
|
return el && el.classList.contains('chatra--expanded') ? close() : open()
|
||||||
|
}
|
||||||
|
|
||||||
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open()
|
const chatBtn = document.getElementById('chat-btn')
|
||||||
|
if (chatBtn) chatBtn.style.display = 'block'
|
||||||
document.getElementById('chat-btn').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"]),
|
||||||
|
|||||||
+2
@@ -5,3 +5,5 @@ case theme.chat.use
|
|||||||
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)
|
||||||
|
})
|
||||||
|
})()
|
||||||
+2
-2
@@ -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) {
|
||||||
|
|||||||
+3
-3
@@ -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) {
|
||||||
|
|||||||
+50
-16
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+321
-8
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleError = err => {
|
||||||
|
console.error(`[mermaid] render failed for block #${index}:`, err)
|
||||||
|
const errorEl = document.createElement('div')
|
||||||
|
errorEl.className = 'mermaid-error'
|
||||||
|
errorEl.textContent = `Mermaid render error: ${err.message || err}`
|
||||||
|
mermaidSrc.insertAdjacentElement('afterend', errorEl)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||||
// mermaid v9 and v10 compatibility
|
// mermaid v9 and v10 compatibility
|
||||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
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)
|
||||||
|
|||||||
Vendored
+18
-9
@@ -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)
|
||||||
+17
-5
@@ -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)
|
||||||
|
|||||||
+69
-18
@@ -1,4 +1,4 @@
|
|||||||
- let { serverURL, website_id, option, UV_PV } = theme.umami_analytics
|
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
|
||||||
- const isServerURL = !!serverURL
|
- const isServerURL = !!serverURL
|
||||||
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
|
||||||
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
|
||||||
@@ -9,51 +9,97 @@ script.
|
|||||||
const config = !{JSON.stringify(UV_PV)}
|
const config = !{JSON.stringify(UV_PV)}
|
||||||
|
|
||||||
const runTrack = () => {
|
const runTrack = () => {
|
||||||
|
if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
|
||||||
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
|
||||||
|
} else {
|
||||||
|
console.warn('Umami Analytics: umami.track is not available')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadUmamiJS = () => {
|
const loadUmamiJS = () => {
|
||||||
btf.getScript('!{baseURL}/script.js', {
|
btf.getScript('!{baseURL}/!{script_name}', {
|
||||||
'data-website-id': '!{website_id}',
|
'data-website-id': '!{website_id}',
|
||||||
'data-auto-track': 'false',
|
'data-auto-track': 'false',
|
||||||
...option
|
...option
|
||||||
}).then(runTrack)
|
}).then(() => {
|
||||||
|
runTrack()
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Umami Analytics: Error loading script', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
|
||||||
|
|
||||||
const getData = async (isPost) => {
|
const getData = async (isPost) => {
|
||||||
|
try {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
|
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
|
||||||
const headerList = { 'Accept': 'application/json' }
|
const headers = {
|
||||||
if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
|
'Accept': 'application/json',
|
||||||
else headerList['x-umami-api-key'] = config.token
|
[!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
|
||||||
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
|
}
|
||||||
method: "GET",
|
|
||||||
headers: headerList
|
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
return await res.json()
|
return await res.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Umami Analytics: Failed to fetch data', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertData = async () => {
|
const insertData = async () => {
|
||||||
try {
|
try {
|
||||||
|
const tasks = []
|
||||||
|
|
||||||
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
|
||||||
|
tasks.push((async () => {
|
||||||
const pagePV = document.getElementById('umamiPV')
|
const pagePV = document.getElementById('umamiPV')
|
||||||
if (pagePV) {
|
if (pagePV) {
|
||||||
const data = await getData(true)
|
const data = await getData(true)
|
||||||
pagePV.textContent = data.pageviews.value
|
if (data && data.pageviews) {
|
||||||
}
|
pagePV.textContent = extractValue(data.pageviews)
|
||||||
} else {
|
} else {
|
||||||
const data = (config.site_uv || config.site_pv) && await getData()
|
console.warn('Umami Analytics: Invalid page view data received')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.site_uv || config.site_pv) {
|
||||||
|
tasks.push((async () => {
|
||||||
|
const data = await getData(false)
|
||||||
|
|
||||||
if (config.site_uv) {
|
if (config.site_uv) {
|
||||||
const siteUV = document.getElementById('umami-site-uv')
|
const siteUV = document.getElementById('umami-site-uv')
|
||||||
if (siteUV) siteUV.textContent = data.visitors.value
|
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) {
|
if (config.site_pv) {
|
||||||
const sitePV = document.getElementById('umami-site-pv')
|
const sitePV = document.getElementById('umami-site-pv')
|
||||||
if (sitePV) sitePV.textContent = data.pageviews.value
|
if (sitePV && data && data.pageviews) {
|
||||||
|
sitePV.textContent = extractValue(data.pageviews)
|
||||||
|
} else if (sitePV) {
|
||||||
|
console.warn('Umami Analytics: Invalid site PV data received')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})())
|
||||||
console.error('Failed to load Umami Analytics:', e)
|
}
|
||||||
|
|
||||||
|
await Promise.all(tasks)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Umami Analytics: Failed to insert data', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,5 +107,10 @@ script.
|
|||||||
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
|
||||||
|
|
||||||
loadUmamiJS()
|
loadUmamiJS()
|
||||||
insertData()
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', insertData)
|
||||||
|
} else {
|
||||||
|
setTimeout(insertData, 100)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
|
|||||||
i.fas.fa-tags
|
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'})
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hexo-theme-butterfly",
|
"name": "hexo-theme-butterfly",
|
||||||
"version": "5.3.5",
|
"version": "5.6.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": {
|
||||||
@@ -23,8 +23,10 @@
|
|||||||
"email": "my@crazywong.com"
|
"email": "my@crazywong.com"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"hexo-renderer-pug": "^3.0.0",
|
||||||
"hexo-renderer-stylus": "^3.0.1",
|
"hexo-renderer-stylus": "^3.0.1",
|
||||||
"hexo-renderer-pug": "^3.0.0"
|
"hexo-util": "^4.0.0",
|
||||||
|
"moment-timezone": "^0.6.2"
|
||||||
},
|
},
|
||||||
"homepage": "https://butterfly.js.org/",
|
"homepage": "https://butterfly.js.org/",
|
||||||
"author": "Jerry <my@crazywong.com>",
|
"author": "Jerry <my@crazywong.com>",
|
||||||
|
|||||||
+33
-37
@@ -1,15 +1,15 @@
|
|||||||
abcjs_basic_js:
|
abcjs_basic_js:
|
||||||
name: abcjs
|
name: abcjs
|
||||||
file: dist/abcjs-basic-min.js
|
file: dist/abcjs-basic-min.js
|
||||||
version: 6.4.4
|
version: 6.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.20.3
|
version: 5.56.0
|
||||||
aplayer_css:
|
aplayer_css:
|
||||||
name: aplayer
|
name: aplayer
|
||||||
file: dist/APlayer.min.css
|
file: dist/APlayer.min.css
|
||||||
@@ -33,69 +33,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.4.8
|
version: 4.5.1
|
||||||
clickShowText:
|
clickShowText:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-show-text.min.js
|
file: dist/click-show-text.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
click_heart:
|
click_heart:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/click-heart.min.js
|
file: dist/click-heart.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
disqusjs:
|
disqusjs:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/disqusjs.es2015.umd.min.js
|
file: dist/browser/disqusjs.es2015.umd.min.js
|
||||||
version: 3.0.2
|
version: 3.2.1
|
||||||
disqusjs_css:
|
disqusjs_css:
|
||||||
name: disqusjs
|
name: disqusjs
|
||||||
file: dist/browser/styles/disqusjs.css
|
file: dist/browser/styles/disqusjs.css
|
||||||
version: 3.0.2
|
version: 3.2.1
|
||||||
docsearch_css:
|
docsearch_css:
|
||||||
name: '@docsearch/css'
|
name: '@docsearch/css'
|
||||||
other_name: docsearch-css
|
other_name: docsearch-css
|
||||||
file: dist/style.css
|
file: dist/style.css
|
||||||
version: 3.9.0
|
version: 4.6.3
|
||||||
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.6.3
|
||||||
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: 5.0.36
|
version: 6.1.14
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fancybox_css:
|
fancybox_css:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.css
|
file: dist/fancybox/fancybox.css
|
||||||
version: 5.0.36
|
version: 6.1.14
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fireworks:
|
fireworks:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/fireworks.min.js
|
file: dist/fireworks.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
fontawesome:
|
fontawesome:
|
||||||
name: '@fortawesome/fontawesome-free'
|
name: '@fortawesome/fontawesome-free'
|
||||||
file: css/all.min.css
|
file: css/all.min.css
|
||||||
other_name: font-awesome
|
other_name: font-awesome
|
||||||
version: 6.7.2
|
version: 7.3.1
|
||||||
gitalk:
|
gitalk:
|
||||||
name: gitalk
|
name: gitalk
|
||||||
file: dist/gitalk.min.js
|
file: dist/gitalk.min.js
|
||||||
@@ -108,28 +108,24 @@ instantpage:
|
|||||||
name: instant.page
|
name: instant.page
|
||||||
file: instantpage.js
|
file: instantpage.js
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
instantsearch:
|
|
||||||
name: instantsearch.js
|
|
||||||
file: dist/instantsearch.production.min.js
|
|
||||||
version: 4.77.3
|
|
||||||
katex:
|
katex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/katex.min.css
|
file: dist/katex.min.css
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.21
|
version: 0.17.0
|
||||||
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.21
|
version: 0.17.0
|
||||||
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 +133,11 @@ medium_zoom:
|
|||||||
mermaid:
|
mermaid:
|
||||||
name: mermaid
|
name: mermaid
|
||||||
file: dist/mermaid.min.js
|
file: dist/mermaid.min.js
|
||||||
version: 11.4.1
|
version: 11.16.0
|
||||||
meting_js:
|
meting_js:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: metingjs/dist/Meting.min.js
|
file: metingjs/dist/Meting.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
pace_default_css:
|
pace_default_css:
|
||||||
name: pace-js
|
name: pace-js
|
||||||
other_name: pace
|
other_name: pace
|
||||||
@@ -160,25 +156,25 @@ prismjs_autoloader:
|
|||||||
name: prismjs
|
name: prismjs
|
||||||
file: plugins/autoloader/prism-autoloader.min.js
|
file: plugins/autoloader/prism-autoloader.min.js
|
||||||
other_name: prism
|
other_name: prism
|
||||||
version: 1.29.0
|
version: 1.30.0
|
||||||
prismjs_js:
|
prismjs_js:
|
||||||
name: prismjs
|
name: prismjs
|
||||||
file: prism.js
|
file: prism.js
|
||||||
other_name: prism
|
other_name: prism
|
||||||
version: 1.29.0
|
version: 1.30.0
|
||||||
prismjs_lineNumber_js:
|
prismjs_lineNumber_js:
|
||||||
name: prismjs
|
name: prismjs
|
||||||
file: plugins/line-numbers/prism-line-numbers.min.js
|
file: plugins/line-numbers/prism-line-numbers.min.js
|
||||||
other_name: prism
|
other_name: prism
|
||||||
version: 1.29.0
|
version: 1.30.0
|
||||||
sharejs:
|
sharejs:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/js/social-share.min.js
|
file: sharejs/dist/js/social-share.min.js
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
sharejs_css:
|
sharejs_css:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: sharejs/dist/css/share.min.css
|
file: sharejs/dist/css/share.min.css
|
||||||
version: 1.1.4
|
version: 1.1.6
|
||||||
snackbar:
|
snackbar:
|
||||||
name: node-snackbar
|
name: node-snackbar
|
||||||
file: dist/snackbar.min.js
|
file: dist/snackbar.min.js
|
||||||
@@ -190,11 +186,11 @@ snackbar_css:
|
|||||||
twikoo:
|
twikoo:
|
||||||
name: twikoo
|
name: twikoo
|
||||||
file: dist/twikoo.all.min.js
|
file: dist/twikoo.all.min.js
|
||||||
version: 1.6.41
|
version: 1.7.14
|
||||||
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 +199,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.5
|
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.5
|
version: 3.15.2
|
||||||
|
|||||||
@@ -0,0 +1,604 @@
|
|||||||
|
// Butterfly 主題默認配置
|
||||||
|
// Default configuration for Butterfly theme
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
nav: {
|
||||||
|
logo: null,
|
||||||
|
display_title: true,
|
||||||
|
display_post_title: true,
|
||||||
|
fixed: false
|
||||||
|
},
|
||||||
|
menu: null,
|
||||||
|
code_blocks: {
|
||||||
|
theme: 'light',
|
||||||
|
macStyle: false,
|
||||||
|
height_limit: false,
|
||||||
|
word_wrap: false,
|
||||||
|
copy: true,
|
||||||
|
language: true,
|
||||||
|
shrink: false,
|
||||||
|
fullpage: false
|
||||||
|
},
|
||||||
|
social: null,
|
||||||
|
favicon: '/img/favicon.png',
|
||||||
|
avatar: {
|
||||||
|
img: '/img/butterfly-icon.png',
|
||||||
|
effect: false
|
||||||
|
},
|
||||||
|
disable_top_img: false,
|
||||||
|
default_top_img: null,
|
||||||
|
index_img: null,
|
||||||
|
archive_img: null,
|
||||||
|
tag_img: null,
|
||||||
|
tag_per_img: null,
|
||||||
|
category_img: null,
|
||||||
|
category_per_img: null,
|
||||||
|
footer_img: false,
|
||||||
|
background: null,
|
||||||
|
cover: {
|
||||||
|
index_enable: true,
|
||||||
|
aside_enable: true,
|
||||||
|
archives_enable: true,
|
||||||
|
default_cover: null
|
||||||
|
},
|
||||||
|
error_img: {
|
||||||
|
flink: '/img/friend_404.gif',
|
||||||
|
post_page: '/img/404.jpg'
|
||||||
|
},
|
||||||
|
error_404: {
|
||||||
|
enable: false,
|
||||||
|
subtitle: 'Page Not Found',
|
||||||
|
background: '/img/error-page.png'
|
||||||
|
},
|
||||||
|
post_meta: {
|
||||||
|
page: {
|
||||||
|
date_type: 'created',
|
||||||
|
date_format: 'date',
|
||||||
|
categories: true,
|
||||||
|
tags: false,
|
||||||
|
label: true
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
position: 'left',
|
||||||
|
date_type: 'both',
|
||||||
|
date_format: 'date',
|
||||||
|
categories: true,
|
||||||
|
tags: true,
|
||||||
|
label: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
index_site_info_top: null,
|
||||||
|
index_top_img_height: null,
|
||||||
|
subtitle: {
|
||||||
|
enable: false,
|
||||||
|
effect: true,
|
||||||
|
typed_option: null,
|
||||||
|
source: false,
|
||||||
|
sub: null
|
||||||
|
},
|
||||||
|
index_layout: 3,
|
||||||
|
index_post_content: {
|
||||||
|
method: 3,
|
||||||
|
length: 500
|
||||||
|
},
|
||||||
|
toc: {
|
||||||
|
post: true,
|
||||||
|
page: false,
|
||||||
|
number: true,
|
||||||
|
expand: false,
|
||||||
|
style_simple: false,
|
||||||
|
scroll_percent: true
|
||||||
|
},
|
||||||
|
post_copyright: {
|
||||||
|
enable: true,
|
||||||
|
decode: false,
|
||||||
|
author_href: null,
|
||||||
|
license: 'CC BY-NC-SA 4.0',
|
||||||
|
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
|
||||||
|
},
|
||||||
|
reward: {
|
||||||
|
enable: false,
|
||||||
|
text: null,
|
||||||
|
QR_code: null
|
||||||
|
},
|
||||||
|
post_edit: {
|
||||||
|
enable: false,
|
||||||
|
url: null
|
||||||
|
},
|
||||||
|
related_post: {
|
||||||
|
enable: true,
|
||||||
|
limit: 6,
|
||||||
|
date_type: 'created'
|
||||||
|
},
|
||||||
|
post_pagination: 1,
|
||||||
|
noticeOutdate: {
|
||||||
|
enable: false,
|
||||||
|
style: 'flat',
|
||||||
|
limit_day: 365,
|
||||||
|
position: 'top',
|
||||||
|
message_prev: 'It has been',
|
||||||
|
message_next: 'days since the last update, the content of the article may be outdated.'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
nav: null,
|
||||||
|
owner: {
|
||||||
|
enable: true,
|
||||||
|
since: 2025
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
enable: true,
|
||||||
|
version: true
|
||||||
|
},
|
||||||
|
custom_text: null
|
||||||
|
},
|
||||||
|
aside: {
|
||||||
|
enable: true,
|
||||||
|
hide: false,
|
||||||
|
button: true,
|
||||||
|
mobile: true,
|
||||||
|
position: 'right',
|
||||||
|
display: {
|
||||||
|
archive: true,
|
||||||
|
tag: true,
|
||||||
|
category: true
|
||||||
|
},
|
||||||
|
card_author: {
|
||||||
|
enable: true,
|
||||||
|
description: null,
|
||||||
|
button: {
|
||||||
|
enable: true,
|
||||||
|
icon: 'fab fa-github',
|
||||||
|
text: 'Follow Me',
|
||||||
|
link: 'https://github.com/xxxxxx'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
card_announcement: {
|
||||||
|
enable: true,
|
||||||
|
content: 'This is my Blog'
|
||||||
|
},
|
||||||
|
card_recent_post: {
|
||||||
|
enable: true,
|
||||||
|
limit: 5,
|
||||||
|
sort: 'date',
|
||||||
|
sort_order: null
|
||||||
|
},
|
||||||
|
card_newest_comments: {
|
||||||
|
enable: false,
|
||||||
|
sort_order: null,
|
||||||
|
limit: 6,
|
||||||
|
storage: 10,
|
||||||
|
avatar: true
|
||||||
|
},
|
||||||
|
card_categories: {
|
||||||
|
enable: true,
|
||||||
|
limit: 8,
|
||||||
|
expand: 'none',
|
||||||
|
sort_order: null
|
||||||
|
},
|
||||||
|
card_tags: {
|
||||||
|
enable: true,
|
||||||
|
limit: 40,
|
||||||
|
color: false,
|
||||||
|
custom_colors: null,
|
||||||
|
orderby: 'random',
|
||||||
|
order: 1,
|
||||||
|
sort_order: null
|
||||||
|
},
|
||||||
|
card_archives: {
|
||||||
|
enable: true,
|
||||||
|
type: 'monthly',
|
||||||
|
format: 'MMMM YYYY',
|
||||||
|
order: -1,
|
||||||
|
limit: 8,
|
||||||
|
sort_order: null
|
||||||
|
},
|
||||||
|
card_post_series: {
|
||||||
|
enable: true,
|
||||||
|
series_title: false,
|
||||||
|
orderBy: 'date',
|
||||||
|
order: -1
|
||||||
|
},
|
||||||
|
card_webinfo: {
|
||||||
|
enable: true,
|
||||||
|
post_count: true,
|
||||||
|
last_push_date: true,
|
||||||
|
sort_order: null,
|
||||||
|
runtime_date: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rightside_bottom: null,
|
||||||
|
translate: {
|
||||||
|
enable: false,
|
||||||
|
default: '繁',
|
||||||
|
defaultEncoding: 2,
|
||||||
|
translateDelay: 0,
|
||||||
|
msgToTraditionalChinese: '繁',
|
||||||
|
msgToSimplifiedChinese: '簡'
|
||||||
|
},
|
||||||
|
readmode: true,
|
||||||
|
darkmode: {
|
||||||
|
enable: true,
|
||||||
|
button: true,
|
||||||
|
autoChangeMode: false,
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
},
|
||||||
|
rightside_scroll_percent: false,
|
||||||
|
rightside_item_order: {
|
||||||
|
enable: false,
|
||||||
|
hide: null,
|
||||||
|
show: null
|
||||||
|
},
|
||||||
|
rightside_config_animation: true,
|
||||||
|
anchor: {
|
||||||
|
auto_update: false,
|
||||||
|
click_to_scroll: false
|
||||||
|
},
|
||||||
|
photofigcaption: false,
|
||||||
|
copy: {
|
||||||
|
enable: true,
|
||||||
|
copyright: {
|
||||||
|
enable: false,
|
||||||
|
limit_count: 150
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wordcount: {
|
||||||
|
enable: false,
|
||||||
|
post_wordcount: true,
|
||||||
|
min2read: true,
|
||||||
|
total_wordcount: true
|
||||||
|
},
|
||||||
|
busuanzi: {
|
||||||
|
site_uv: true,
|
||||||
|
site_pv: true,
|
||||||
|
page_pv: true
|
||||||
|
},
|
||||||
|
math: {
|
||||||
|
use: null,
|
||||||
|
per_page: true,
|
||||||
|
hide_scrollbar: false,
|
||||||
|
mathjax: {
|
||||||
|
enableMenu: true,
|
||||||
|
tags: 'none'
|
||||||
|
},
|
||||||
|
katex: {
|
||||||
|
copy_tex: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
use: null,
|
||||||
|
placeholder: null,
|
||||||
|
algolia_search: {
|
||||||
|
hitsPerPage: 6
|
||||||
|
},
|
||||||
|
local_search: {
|
||||||
|
preload: false,
|
||||||
|
top_n_per_article: 1,
|
||||||
|
unescape: false,
|
||||||
|
pagination: {
|
||||||
|
enable: false,
|
||||||
|
hitsPerPage: 8
|
||||||
|
},
|
||||||
|
CDN: null
|
||||||
|
},
|
||||||
|
docsearch: {
|
||||||
|
appId: null,
|
||||||
|
apiKey: null,
|
||||||
|
indexName: null,
|
||||||
|
option: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
use: 'sharejs',
|
||||||
|
sharejs: {
|
||||||
|
sites: 'facebook,x,wechat,weibo,qq'
|
||||||
|
},
|
||||||
|
addtoany: {
|
||||||
|
item: 'facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
use: null,
|
||||||
|
text: true,
|
||||||
|
lazyload: false,
|
||||||
|
count: false,
|
||||||
|
card_post_count: false
|
||||||
|
},
|
||||||
|
disqus: {
|
||||||
|
shortname: null,
|
||||||
|
apikey: null
|
||||||
|
},
|
||||||
|
disqusjs: {
|
||||||
|
shortname: null,
|
||||||
|
apikey: null,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
livere: {
|
||||||
|
uid: null
|
||||||
|
},
|
||||||
|
gitalk: {
|
||||||
|
client_id: null,
|
||||||
|
client_secret: null,
|
||||||
|
repo: null,
|
||||||
|
owner: null,
|
||||||
|
admin: null,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
valine: {
|
||||||
|
appId: null,
|
||||||
|
appKey: null,
|
||||||
|
avatar: 'monsterid',
|
||||||
|
serverURLs: null,
|
||||||
|
bg: null,
|
||||||
|
visitor: false,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
waline: {
|
||||||
|
serverURL: null,
|
||||||
|
bg: null,
|
||||||
|
pageview: false,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
utterances: {
|
||||||
|
repo: null,
|
||||||
|
issue_term: 'pathname',
|
||||||
|
light_theme: 'github-light',
|
||||||
|
dark_theme: 'photon-dark',
|
||||||
|
js: null,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
facebook_comments: {
|
||||||
|
app_id: null,
|
||||||
|
user_id: null,
|
||||||
|
pageSize: 10,
|
||||||
|
order_by: 'social',
|
||||||
|
lang: 'en_US'
|
||||||
|
},
|
||||||
|
twikoo: {
|
||||||
|
envId: null,
|
||||||
|
region: null,
|
||||||
|
visitor: false,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
giscus: {
|
||||||
|
repo: null,
|
||||||
|
repo_id: null,
|
||||||
|
category_id: null,
|
||||||
|
light_theme: 'light',
|
||||||
|
dark_theme: 'dark',
|
||||||
|
js: null,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
remark42: {
|
||||||
|
host: null,
|
||||||
|
siteId: null,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
artalk: {
|
||||||
|
server: null,
|
||||||
|
site: null,
|
||||||
|
visitor: false,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
use: null,
|
||||||
|
rightside_button: false,
|
||||||
|
button_hide_show: false
|
||||||
|
},
|
||||||
|
chatra: {
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
tidio: {
|
||||||
|
public_key: null
|
||||||
|
},
|
||||||
|
crisp: {
|
||||||
|
website_id: null
|
||||||
|
},
|
||||||
|
knocket: {
|
||||||
|
identifier: null
|
||||||
|
},
|
||||||
|
google_tag_manager: {
|
||||||
|
tag_id: null,
|
||||||
|
domain: 'https://www.googletagmanager.com'
|
||||||
|
},
|
||||||
|
baidu_analytics: null,
|
||||||
|
google_analytics: null,
|
||||||
|
cloudflare_analytics: null,
|
||||||
|
microsoft_clarity: null,
|
||||||
|
umami_analytics: {
|
||||||
|
enable: false,
|
||||||
|
serverURL: null,
|
||||||
|
script_name: 'script.js',
|
||||||
|
website_id: null,
|
||||||
|
option: null,
|
||||||
|
UV_PV: {
|
||||||
|
site_uv: false,
|
||||||
|
site_pv: false,
|
||||||
|
page_pv: false,
|
||||||
|
token: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
google_adsense: {
|
||||||
|
enable: false,
|
||||||
|
auto_ads: true,
|
||||||
|
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
|
||||||
|
client: null,
|
||||||
|
enable_page_level_ads: true
|
||||||
|
},
|
||||||
|
ad: {
|
||||||
|
index: null,
|
||||||
|
aside: null,
|
||||||
|
post: null
|
||||||
|
},
|
||||||
|
site_verification: null,
|
||||||
|
category_ui: null,
|
||||||
|
tag_ui: null,
|
||||||
|
rounded_corners_ui: true,
|
||||||
|
text_align_justify: false,
|
||||||
|
mask: {
|
||||||
|
header: true,
|
||||||
|
footer: true
|
||||||
|
},
|
||||||
|
preloader: {
|
||||||
|
enable: false,
|
||||||
|
source: 1,
|
||||||
|
pace_css_url: null
|
||||||
|
},
|
||||||
|
enter_transitions: true,
|
||||||
|
display_mode: 'light',
|
||||||
|
beautify: {
|
||||||
|
enable: false,
|
||||||
|
field: 'post',
|
||||||
|
title_prefix_icon: null,
|
||||||
|
title_prefix_icon_color: null
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
global_font_size: null,
|
||||||
|
code_font_size: null,
|
||||||
|
font_family: null,
|
||||||
|
code_font_family: null
|
||||||
|
},
|
||||||
|
blog_title_font: {
|
||||||
|
font_link: null,
|
||||||
|
font_family: null
|
||||||
|
},
|
||||||
|
hr_icon: {
|
||||||
|
enable: true,
|
||||||
|
icon: null,
|
||||||
|
icon_top: null
|
||||||
|
},
|
||||||
|
activate_power_mode: {
|
||||||
|
enable: false,
|
||||||
|
colorful: true,
|
||||||
|
shake: true,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
canvas_ribbon: {
|
||||||
|
enable: false,
|
||||||
|
size: 150,
|
||||||
|
alpha: 0.6,
|
||||||
|
zIndex: -1,
|
||||||
|
click_to_change: false,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
canvas_fluttering_ribbon: {
|
||||||
|
enable: false,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
canvas_nest: {
|
||||||
|
enable: false,
|
||||||
|
color: '0,0,255',
|
||||||
|
opacity: 0.7,
|
||||||
|
zIndex: -1,
|
||||||
|
count: 99,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
fireworks: {
|
||||||
|
enable: false,
|
||||||
|
zIndex: 9999,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
click_heart: {
|
||||||
|
enable: false,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
clickShowText: {
|
||||||
|
enable: false,
|
||||||
|
text: null,
|
||||||
|
fontSize: '15px',
|
||||||
|
random: false,
|
||||||
|
mobile: false
|
||||||
|
},
|
||||||
|
lightbox: null,
|
||||||
|
series: {
|
||||||
|
enable: false,
|
||||||
|
orderBy: 'title',
|
||||||
|
order: 1,
|
||||||
|
number: true
|
||||||
|
},
|
||||||
|
abcjs: {
|
||||||
|
enable: false,
|
||||||
|
per_page: true
|
||||||
|
},
|
||||||
|
mermaid: {
|
||||||
|
enable: false,
|
||||||
|
code_write: false,
|
||||||
|
theme: {
|
||||||
|
light: 'default',
|
||||||
|
dark: 'dark'
|
||||||
|
},
|
||||||
|
open_in_new_tab: true,
|
||||||
|
zoom_pan: true
|
||||||
|
},
|
||||||
|
chartjs: {
|
||||||
|
enable: false,
|
||||||
|
fontColor: {
|
||||||
|
light: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
dark: 'rgba(255, 255, 255, 0.8)'
|
||||||
|
},
|
||||||
|
borderColor: {
|
||||||
|
light: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
dark: 'rgba(255, 255, 255, 0.2)'
|
||||||
|
},
|
||||||
|
scale_ticks_backdropColor: {
|
||||||
|
light: 'transparent',
|
||||||
|
dark: 'transparent'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
style: 'flat',
|
||||||
|
icons: true,
|
||||||
|
border_radius: 3,
|
||||||
|
light_bg_offset: 0
|
||||||
|
},
|
||||||
|
pjax: {
|
||||||
|
enable: false,
|
||||||
|
exclude: null
|
||||||
|
},
|
||||||
|
aplayerInject: {
|
||||||
|
enable: false,
|
||||||
|
per_page: true
|
||||||
|
},
|
||||||
|
snackbar: {
|
||||||
|
enable: false,
|
||||||
|
position: 'bottom-left',
|
||||||
|
bg_light: '#49b1f5',
|
||||||
|
bg_dark: '#1f1f1f'
|
||||||
|
},
|
||||||
|
instantpage: false,
|
||||||
|
lazyload: {
|
||||||
|
enable: false,
|
||||||
|
native: false,
|
||||||
|
field: 'site',
|
||||||
|
placeholder: null,
|
||||||
|
blur: false
|
||||||
|
},
|
||||||
|
pwa: {
|
||||||
|
enable: false,
|
||||||
|
manifest: null,
|
||||||
|
apple_touch_icon: null,
|
||||||
|
favicon_32_32: null,
|
||||||
|
favicon_16_16: null,
|
||||||
|
mask_icon: null
|
||||||
|
},
|
||||||
|
Open_Graph_meta: {
|
||||||
|
enable: true,
|
||||||
|
option: null
|
||||||
|
},
|
||||||
|
structured_data: {
|
||||||
|
enable: false,
|
||||||
|
alternate_name: null
|
||||||
|
},
|
||||||
|
css_prefix: true,
|
||||||
|
inject: {
|
||||||
|
head: null,
|
||||||
|
bottom: null
|
||||||
|
},
|
||||||
|
CDN: {
|
||||||
|
internal_provider: 'local',
|
||||||
|
third_party_provider: 'jsdelivr',
|
||||||
|
version: true,
|
||||||
|
custom_format: null,
|
||||||
|
option: null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,14 @@
|
|||||||
const { stripHTML, truncate } = require('hexo-util')
|
const { stripHTML, truncate } = require('hexo-util')
|
||||||
|
|
||||||
// Truncates the given content to a specified length, removing HTML tags and replacing newlines with spaces.
|
// Truncates the given content to a specified length, removing HTML tags and replacing newlines with spaces.
|
||||||
const truncateContent = (content, length) => {
|
const truncateContent = (content, length, encrypt = false) => {
|
||||||
return truncate(stripHTML(content), { length, separator: ' ' }).replace(/\n/g, ' ')
|
if (!content || encrypt) return ''
|
||||||
|
return truncate(stripHTML(content).replace(/\n/g, ' '), { length })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a post description based on the provided data and theme configuration.
|
// Generates a post description based on the provided data and theme configuration.
|
||||||
const postDesc = (data, hexo) => {
|
const postDesc = (data, hexo) => {
|
||||||
const { description, content, postDesc } = data
|
const { description, content, postDesc, encrypt } = data
|
||||||
|
|
||||||
if (postDesc) return postDesc
|
if (postDesc) return postDesc
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@ const postDesc = (data, hexo) => {
|
|||||||
result = description
|
result = description
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
result = description || truncateContent(content, length)
|
result = description || truncateContent(content, length, encrypt)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
result = truncateContent(content, length)
|
result = truncateContent(content, length, encrypt)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.postDesc = result
|
data.postDesc = result
|
||||||
|
|||||||
+18
-20
@@ -46,45 +46,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
|
|
||||||
})
|
|
||||||
+73
-7
@@ -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')
|
|
||||||
|
|
||||||
if (hexoVer < 5.3) {
|
// Cache default config to avoid repeated file reads
|
||||||
|
let cachedDefaultConfig = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Hexo version and configuration
|
||||||
|
*/
|
||||||
|
function checkHexoEnvironment (hexo) {
|
||||||
|
const { version, log, locals } = hexo
|
||||||
|
|
||||||
|
const [major, minor] = version.split('.').map(Number)
|
||||||
|
const requiredMajor = 5
|
||||||
|
const requiredMinor = 3
|
||||||
|
|
||||||
|
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
|
||||||
log.error('Please update Hexo to V5.3.0 or higher!')
|
log.error('Please update Hexo to V5.3.0 or higher!')
|
||||||
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
|
||||||
process.exit(-1)
|
throw new Error('Hexo version too old')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for deprecated configuration file
|
||||||
if (locals.get) {
|
if (locals.get) {
|
||||||
const data = locals.get('data')
|
const data = locals.get('data')
|
||||||
if (data && data.butterfly) {
|
if (data && data.butterfly) {
|
||||||
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
|
||||||
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
|
||||||
process.exit(-1)
|
throw new Error('Deprecated configuration file')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load default configuration
|
||||||
|
*/
|
||||||
|
function loadDefaultConfig () {
|
||||||
|
if (cachedDefaultConfig) {
|
||||||
|
return cachedDefaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(__dirname, '../common/default_config.js')
|
||||||
|
cachedDefaultConfig = require(configPath)
|
||||||
|
return cachedDefaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process comment system configuration
|
||||||
|
*/
|
||||||
|
function processCommentConfig (themeConfig) {
|
||||||
|
const { comments } = themeConfig
|
||||||
|
if (!comments || !comments.use) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { use } = comments
|
||||||
|
|
||||||
|
if (!Array.isArray(use)) {
|
||||||
|
use = typeof use === 'string' ? use.split(',') : [use]
|
||||||
|
}
|
||||||
|
|
||||||
|
use = use
|
||||||
|
.map(item => {
|
||||||
|
if (typeof item !== 'string') return item
|
||||||
|
return item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
// Handle Disqus and Disqusjs conflict
|
||||||
|
if (use.includes('Disqus') && use.includes('Disqusjs')) {
|
||||||
|
hexo.log.warn('Disqus and Disqusjs conflict detected, keeping only the first one')
|
||||||
|
hexo.log.warn('檢測到 Disqus 和 Disqusjs 衝突,只保留第一個')
|
||||||
|
use = [use[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
themeConfig.comments.use = use
|
||||||
|
}
|
||||||
|
|
||||||
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
|
checkHexoEnvironment(hexo)
|
||||||
|
|
||||||
|
const defaultConfig = loadDefaultConfig()
|
||||||
|
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
|
||||||
|
|
||||||
|
processCommentConfig(hexo.theme.config)
|
||||||
|
}, 1)
|
||||||
|
|||||||
@@ -1,583 +0,0 @@
|
|||||||
const { deepMerge } = require('hexo-util')
|
|
||||||
|
|
||||||
hexo.extend.filter.register('before_generate', () => {
|
|
||||||
const defaultConfig = {
|
|
||||||
nav: {
|
|
||||||
logo: null,
|
|
||||||
display_title: true,
|
|
||||||
fixed: false
|
|
||||||
},
|
|
||||||
menu: null,
|
|
||||||
code_blocks: {
|
|
||||||
theme: 'light',
|
|
||||||
macStyle: false,
|
|
||||||
height_limit: false,
|
|
||||||
word_wrap: false,
|
|
||||||
copy: true,
|
|
||||||
language: true,
|
|
||||||
shrink: false,
|
|
||||||
fullpage: false
|
|
||||||
},
|
|
||||||
social: null,
|
|
||||||
favicon: '/img/favicon.png',
|
|
||||||
avatar: {
|
|
||||||
img: '/img/butterfly-icon.png',
|
|
||||||
effect: false
|
|
||||||
},
|
|
||||||
disable_top_img: false,
|
|
||||||
default_top_img: null,
|
|
||||||
index_img: null,
|
|
||||||
archive_img: null,
|
|
||||||
tag_img: null,
|
|
||||||
tag_per_img: null,
|
|
||||||
category_img: null,
|
|
||||||
category_per_img: null,
|
|
||||||
footer_img: false,
|
|
||||||
background: null,
|
|
||||||
cover: {
|
|
||||||
index_enable: true,
|
|
||||||
aside_enable: true,
|
|
||||||
archives_enable: true,
|
|
||||||
default_cover: null
|
|
||||||
},
|
|
||||||
error_img: {
|
|
||||||
flink: '/img/friend_404.gif',
|
|
||||||
post_page: '/img/404.jpg'
|
|
||||||
},
|
|
||||||
error_404: {
|
|
||||||
enable: false,
|
|
||||||
subtitle: 'Page Not Found',
|
|
||||||
background: '/img/error-page.png'
|
|
||||||
},
|
|
||||||
post_meta: {
|
|
||||||
page: {
|
|
||||||
date_type: 'created',
|
|
||||||
date_format: 'date',
|
|
||||||
categories: true,
|
|
||||||
tags: false,
|
|
||||||
label: true
|
|
||||||
},
|
|
||||||
post: {
|
|
||||||
position: 'left',
|
|
||||||
date_type: 'both',
|
|
||||||
date_format: 'date',
|
|
||||||
categories: true,
|
|
||||||
tags: true,
|
|
||||||
label: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
index_site_info_top: null,
|
|
||||||
index_top_img_height: null,
|
|
||||||
subtitle: {
|
|
||||||
enable: false,
|
|
||||||
effect: true,
|
|
||||||
typed_option: null,
|
|
||||||
source: false,
|
|
||||||
sub: null
|
|
||||||
},
|
|
||||||
index_layout: 3,
|
|
||||||
index_post_content: {
|
|
||||||
method: 3,
|
|
||||||
length: 500
|
|
||||||
},
|
|
||||||
toc: {
|
|
||||||
post: true,
|
|
||||||
page: false,
|
|
||||||
number: true,
|
|
||||||
expand: false,
|
|
||||||
style_simple: false,
|
|
||||||
scroll_percent: true
|
|
||||||
},
|
|
||||||
post_copyright: {
|
|
||||||
enable: true,
|
|
||||||
decode: false,
|
|
||||||
author_href: null,
|
|
||||||
license: 'CC BY-NC-SA 4.0',
|
|
||||||
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
|
|
||||||
},
|
|
||||||
reward: {
|
|
||||||
enable: false,
|
|
||||||
text: null,
|
|
||||||
QR_code: null
|
|
||||||
},
|
|
||||||
post_edit: {
|
|
||||||
enable: false,
|
|
||||||
url: null
|
|
||||||
},
|
|
||||||
related_post: {
|
|
||||||
enable: true,
|
|
||||||
limit: 6,
|
|
||||||
date_type: 'created'
|
|
||||||
},
|
|
||||||
post_pagination: 1,
|
|
||||||
noticeOutdate: {
|
|
||||||
enable: false,
|
|
||||||
style: 'flat',
|
|
||||||
limit_day: 365,
|
|
||||||
position: 'top',
|
|
||||||
message_prev: 'It has been',
|
|
||||||
message_next: 'days since the last update, the content of the article may be outdated.'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
owner: {
|
|
||||||
enable: true,
|
|
||||||
since: 2019
|
|
||||||
},
|
|
||||||
custom_text: null,
|
|
||||||
copyright: true
|
|
||||||
},
|
|
||||||
aside: {
|
|
||||||
enable: true,
|
|
||||||
hide: false,
|
|
||||||
button: true,
|
|
||||||
mobile: true,
|
|
||||||
position: 'right',
|
|
||||||
display: {
|
|
||||||
archive: true,
|
|
||||||
tag: true,
|
|
||||||
category: true
|
|
||||||
},
|
|
||||||
card_author: {
|
|
||||||
enable: true,
|
|
||||||
description: null,
|
|
||||||
button: {
|
|
||||||
enable: true,
|
|
||||||
icon: 'fab fa-github',
|
|
||||||
text: 'Follow Me',
|
|
||||||
link: 'https://github.com/xxxxxx'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
card_announcement: {
|
|
||||||
enable: true,
|
|
||||||
content: 'This is my Blog'
|
|
||||||
},
|
|
||||||
card_recent_post: {
|
|
||||||
enable: true,
|
|
||||||
limit: 5,
|
|
||||||
sort: 'date',
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_newest_comments: {
|
|
||||||
enable: false,
|
|
||||||
sort_order: null,
|
|
||||||
limit: 6,
|
|
||||||
storage: 10,
|
|
||||||
avatar: true
|
|
||||||
},
|
|
||||||
card_categories: {
|
|
||||||
enable: true,
|
|
||||||
limit: 8,
|
|
||||||
expand: 'none',
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_tags: {
|
|
||||||
enable: true,
|
|
||||||
limit: 40,
|
|
||||||
color: false,
|
|
||||||
orderby: 'random',
|
|
||||||
order: 1,
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_archives: {
|
|
||||||
enable: true,
|
|
||||||
type: 'monthly',
|
|
||||||
format: 'MMMM YYYY',
|
|
||||||
order: -1,
|
|
||||||
limit: 8,
|
|
||||||
sort_order: null
|
|
||||||
},
|
|
||||||
card_post_series: {
|
|
||||||
enable: true,
|
|
||||||
series_title: false,
|
|
||||||
orderBy: 'date',
|
|
||||||
order: -1
|
|
||||||
},
|
|
||||||
card_webinfo: {
|
|
||||||
enable: true,
|
|
||||||
post_count: true,
|
|
||||||
last_push_date: true,
|
|
||||||
sort_order: null,
|
|
||||||
runtime_date: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rightside_bottom: null,
|
|
||||||
translate: {
|
|
||||||
enable: false,
|
|
||||||
default: '繁',
|
|
||||||
defaultEncoding: 2,
|
|
||||||
translateDelay: 0,
|
|
||||||
msgToTraditionalChinese: '繁',
|
|
||||||
msgToSimplifiedChinese: '簡'
|
|
||||||
},
|
|
||||||
readmode: true,
|
|
||||||
darkmode: {
|
|
||||||
enable: true,
|
|
||||||
button: true,
|
|
||||||
autoChangeMode: false,
|
|
||||||
start: null,
|
|
||||||
end: null
|
|
||||||
},
|
|
||||||
rightside_scroll_percent: false,
|
|
||||||
rightside_item_order: {
|
|
||||||
enable: false,
|
|
||||||
hide: null,
|
|
||||||
show: null
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
auto_update: false,
|
|
||||||
click_to_scroll: false
|
|
||||||
},
|
|
||||||
photofigcaption: false,
|
|
||||||
copy: {
|
|
||||||
enable: true,
|
|
||||||
copyright: {
|
|
||||||
enable: false,
|
|
||||||
limit_count: 150
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wordcount: {
|
|
||||||
enable: false,
|
|
||||||
post_wordcount: true,
|
|
||||||
min2read: true,
|
|
||||||
total_wordcount: true
|
|
||||||
},
|
|
||||||
busuanzi: {
|
|
||||||
site_uv: true,
|
|
||||||
site_pv: true,
|
|
||||||
page_pv: true
|
|
||||||
},
|
|
||||||
math: {
|
|
||||||
use: null,
|
|
||||||
per_page: true,
|
|
||||||
hide_scrollbar: false,
|
|
||||||
mathjax: {
|
|
||||||
enableMenu: true,
|
|
||||||
tags: 'none'
|
|
||||||
},
|
|
||||||
katex: {
|
|
||||||
copy_tex: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
use: null,
|
|
||||||
placeholder: null,
|
|
||||||
algolia_search: {
|
|
||||||
hitsPerPage: 6
|
|
||||||
},
|
|
||||||
local_search: {
|
|
||||||
preload: false,
|
|
||||||
top_n_per_article: 1,
|
|
||||||
unescape: false,
|
|
||||||
CDN: null
|
|
||||||
},
|
|
||||||
docsearch: {
|
|
||||||
appId: null,
|
|
||||||
apiKey: null,
|
|
||||||
indexName: null,
|
|
||||||
option: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
use: 'sharejs',
|
|
||||||
sharejs: {
|
|
||||||
sites: 'facebook,twitter,wechat,weibo,qq'
|
|
||||||
},
|
|
||||||
addtoany: {
|
|
||||||
item: 'facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
comments: {
|
|
||||||
use: null,
|
|
||||||
text: true,
|
|
||||||
lazyload: false,
|
|
||||||
count: false,
|
|
||||||
card_post_count: false
|
|
||||||
},
|
|
||||||
disqus: {
|
|
||||||
shortname: null,
|
|
||||||
apikey: null
|
|
||||||
},
|
|
||||||
disqusjs: {
|
|
||||||
shortname: null,
|
|
||||||
apikey: null,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
livere: {
|
|
||||||
uid: null
|
|
||||||
},
|
|
||||||
gitalk: {
|
|
||||||
client_id: null,
|
|
||||||
client_secret: null,
|
|
||||||
repo: null,
|
|
||||||
owner: null,
|
|
||||||
admin: null,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
valine: {
|
|
||||||
appId: null,
|
|
||||||
appKey: null,
|
|
||||||
avatar: 'monsterid',
|
|
||||||
serverURLs: null,
|
|
||||||
bg: null,
|
|
||||||
visitor: false,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
waline: {
|
|
||||||
serverURL: null,
|
|
||||||
bg: null,
|
|
||||||
pageview: false,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
utterances: {
|
|
||||||
repo: null,
|
|
||||||
issue_term: 'pathname',
|
|
||||||
light_theme: 'github-light',
|
|
||||||
dark_theme: 'photon-dark',
|
|
||||||
js: null,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
facebook_comments: {
|
|
||||||
app_id: null,
|
|
||||||
user_id: null,
|
|
||||||
pageSize: 10,
|
|
||||||
order_by: 'social',
|
|
||||||
lang: 'en_US'
|
|
||||||
},
|
|
||||||
twikoo: {
|
|
||||||
envId: null,
|
|
||||||
region: null,
|
|
||||||
visitor: false,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
giscus: {
|
|
||||||
repo: null,
|
|
||||||
repo_id: null,
|
|
||||||
category_id: null,
|
|
||||||
light_theme: 'light',
|
|
||||||
dark_theme: 'dark',
|
|
||||||
js: null,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
remark42: {
|
|
||||||
host: null,
|
|
||||||
siteId: null,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
artalk: {
|
|
||||||
server: null,
|
|
||||||
site: null,
|
|
||||||
visitor: false,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
use: null,
|
|
||||||
rightside_button: false,
|
|
||||||
button_hide_show: false
|
|
||||||
},
|
|
||||||
chatra: {
|
|
||||||
id: null
|
|
||||||
},
|
|
||||||
tidio: {
|
|
||||||
public_key: null
|
|
||||||
},
|
|
||||||
crisp: {
|
|
||||||
website_id: null
|
|
||||||
},
|
|
||||||
baidu_analytics: null,
|
|
||||||
google_analytics: null,
|
|
||||||
cloudflare_analytics: null,
|
|
||||||
microsoft_clarity: null,
|
|
||||||
umami_analytics: {
|
|
||||||
enable: false,
|
|
||||||
serverURL: null,
|
|
||||||
website_id: null,
|
|
||||||
option: null,
|
|
||||||
UV_PV: {
|
|
||||||
site_uv: false,
|
|
||||||
site_pv: false,
|
|
||||||
page_pv: false,
|
|
||||||
token: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
google_adsense: {
|
|
||||||
enable: false,
|
|
||||||
auto_ads: true,
|
|
||||||
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
|
|
||||||
client: null,
|
|
||||||
enable_page_level_ads: true
|
|
||||||
},
|
|
||||||
ad: {
|
|
||||||
index: null,
|
|
||||||
aside: null,
|
|
||||||
post: null
|
|
||||||
},
|
|
||||||
site_verification: null,
|
|
||||||
category_ui: null,
|
|
||||||
tag_ui: null,
|
|
||||||
rounded_corners_ui: true,
|
|
||||||
text_align_justify: false,
|
|
||||||
mask: {
|
|
||||||
header: true,
|
|
||||||
footer: true
|
|
||||||
},
|
|
||||||
preloader: {
|
|
||||||
enable: false,
|
|
||||||
source: 1,
|
|
||||||
pace_css_url: null
|
|
||||||
},
|
|
||||||
enter_transitions: true,
|
|
||||||
display_mode: 'light',
|
|
||||||
beautify: {
|
|
||||||
enable: false,
|
|
||||||
field: 'post',
|
|
||||||
title_prefix_icon: null,
|
|
||||||
title_prefix_icon_color: null
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
global_font_size: null,
|
|
||||||
code_font_size: null,
|
|
||||||
font_family: null,
|
|
||||||
code_font_family: null
|
|
||||||
},
|
|
||||||
blog_title_font: {
|
|
||||||
font_link: null,
|
|
||||||
font_family: null
|
|
||||||
},
|
|
||||||
hr_icon: {
|
|
||||||
enable: true,
|
|
||||||
icon: null,
|
|
||||||
icon_top: null
|
|
||||||
},
|
|
||||||
activate_power_mode: {
|
|
||||||
enable: false,
|
|
||||||
colorful: true,
|
|
||||||
shake: true,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
canvas_ribbon: {
|
|
||||||
enable: false,
|
|
||||||
size: 150,
|
|
||||||
alpha: 0.6,
|
|
||||||
zIndex: -1,
|
|
||||||
click_to_change: false,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
canvas_fluttering_ribbon: {
|
|
||||||
enable: false,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
canvas_nest: {
|
|
||||||
enable: false,
|
|
||||||
color: '0,0,255',
|
|
||||||
opacity: 0.7,
|
|
||||||
zIndex: -1,
|
|
||||||
count: 99,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
fireworks: {
|
|
||||||
enable: false,
|
|
||||||
zIndex: 9999,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
click_heart: {
|
|
||||||
enable: false,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
clickShowText: {
|
|
||||||
enable: false,
|
|
||||||
text: null,
|
|
||||||
fontSize: '15px',
|
|
||||||
random: false,
|
|
||||||
mobile: false
|
|
||||||
},
|
|
||||||
lightbox: null,
|
|
||||||
series: {
|
|
||||||
enable: false,
|
|
||||||
orderBy: 'title',
|
|
||||||
order: 1,
|
|
||||||
number: true
|
|
||||||
},
|
|
||||||
abcjs: {
|
|
||||||
enable: false,
|
|
||||||
per_page: true
|
|
||||||
},
|
|
||||||
mermaid: {
|
|
||||||
enable: false,
|
|
||||||
code_write: false,
|
|
||||||
theme: {
|
|
||||||
light: 'default',
|
|
||||||
dark: 'dark'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
chartjs: {
|
|
||||||
enable: false,
|
|
||||||
fontColor: {
|
|
||||||
light: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
dark: 'rgba(255, 255, 255, 0.8)'
|
|
||||||
},
|
|
||||||
borderColor: {
|
|
||||||
light: 'rgba(0, 0, 0, 0.1)',
|
|
||||||
dark: 'rgba(255, 255, 255, 0.2)'
|
|
||||||
},
|
|
||||||
scale_ticks_backdropColor: {
|
|
||||||
light: 'transparent',
|
|
||||||
dark: 'transparent'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
style: 'flat',
|
|
||||||
icons: true,
|
|
||||||
border_radius: 3,
|
|
||||||
light_bg_offset: 0
|
|
||||||
},
|
|
||||||
pjax: {
|
|
||||||
enable: false,
|
|
||||||
exclude: null
|
|
||||||
},
|
|
||||||
aplayerInject: {
|
|
||||||
enable: false,
|
|
||||||
per_page: true
|
|
||||||
},
|
|
||||||
snackbar: {
|
|
||||||
enable: false,
|
|
||||||
position: 'bottom-left',
|
|
||||||
bg_light: '#49b1f5',
|
|
||||||
bg_dark: '#1f1f1f'
|
|
||||||
},
|
|
||||||
instantpage: false,
|
|
||||||
lazyload: {
|
|
||||||
enable: false,
|
|
||||||
native: false,
|
|
||||||
field: 'site',
|
|
||||||
placeholder: null,
|
|
||||||
blur: false
|
|
||||||
},
|
|
||||||
pwa: {
|
|
||||||
enable: false,
|
|
||||||
manifest: null,
|
|
||||||
apple_touch_icon: null,
|
|
||||||
favicon_32_32: null,
|
|
||||||
favicon_16_16: null,
|
|
||||||
mask_icon: null
|
|
||||||
},
|
|
||||||
Open_Graph_meta: {
|
|
||||||
enable: true,
|
|
||||||
option: null
|
|
||||||
},
|
|
||||||
structured_data: true,
|
|
||||||
css_prefix: true,
|
|
||||||
inject: {
|
|
||||||
head: null,
|
|
||||||
bottom: null
|
|
||||||
},
|
|
||||||
CDN: {
|
|
||||||
internal_provider: 'local',
|
|
||||||
third_party_provider: 'jsdelivr',
|
|
||||||
version: false,
|
|
||||||
custom_format: null,
|
|
||||||
option: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hexo.theme.config = deepMerge(defaultConfig, hexo.theme.config)
|
|
||||||
}, 1)
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Butterfly
|
* 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
|
|
||||||
|
|
||||||
let index
|
let index
|
||||||
|
|
||||||
do {
|
do {
|
||||||
index = Math.floor(Math.random() * coverCount)
|
index = Math.floor(Math.random() * coverCount)
|
||||||
} while (previousIndexes.includes(index) && previousIndexes.length < coverCount)
|
} while (history.includes(index))
|
||||||
|
|
||||||
previousIndexes.push(index)
|
history.push(index)
|
||||||
if (previousIndexes.length > maxPreviousIndexes) {
|
|
||||||
previousIndexes.shift()
|
if (history.length > maxHistory) {
|
||||||
|
history.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultCover[index]
|
yield defaultCover[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverGenerator = createCoverGenerator()
|
||||||
|
|
||||||
|
const resolvePostAsset = (value, postPath) => {
|
||||||
|
if (
|
||||||
|
!postAssetFolder ||
|
||||||
|
typeof value !== 'string' ||
|
||||||
|
value.includes('/') ||
|
||||||
|
!imgTestReg.test(value)
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${postPath}${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
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,12 +1,8 @@
|
|||||||
'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 {
|
const { archive_dir: archiveDir, timezone, language } = config
|
||||||
archive_dir: archiveDir,
|
|
||||||
timezone,
|
|
||||||
language
|
|
||||||
} = config
|
|
||||||
|
|
||||||
// Destructure and set default options with object destructuring
|
// Destructure and set default options with object destructuring
|
||||||
const {
|
const {
|
||||||
@@ -21,46 +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
|
site.posts.forEach(post => {
|
||||||
.sort('date', order)
|
const date = post.date
|
||||||
.reduce((acc, post) => {
|
|
||||||
let date = post.date.clone()
|
|
||||||
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
|
} else {
|
||||||
const lastEntry = acc[acc.length - 1]
|
archives.set(key, {
|
||||||
if (!lastEntry || !compareFunc(
|
|
||||||
lastEntry.year,
|
|
||||||
lastEntry.month,
|
|
||||||
year,
|
|
||||||
month
|
|
||||||
)) {
|
|
||||||
acc.push({
|
|
||||||
name: date.format(format),
|
|
||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
count: 1
|
count: 1,
|
||||||
|
date // Store date object for later formatting
|
||||||
})
|
})
|
||||||
} 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 => {
|
||||||
@@ -68,43 +61,45 @@ 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
|
||||||
const limitedData = limit > 0
|
const limitedData = limit > 0 ? data.slice(0, Math.min(data.length, limit)) : data
|
||||||
? data.slice(0, Math.min(data.length, limit))
|
|
||||||
: data
|
|
||||||
|
|
||||||
// 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>
|
<i class="fas fa-archive"></i>
|
||||||
<span>${_p('aside.card_archives')}</span>
|
<span>${_p('aside.card_archives')}</span>
|
||||||
${data.length > limitedData.length
|
${
|
||||||
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
|
data.length > limitedData.length
|
||||||
|
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
|
||||||
title="${_p('aside.more_button')}">
|
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
|
||||||
const archiveList = `
|
const archiveList = `
|
||||||
<ul class="card-archive-list">
|
<ul class="card-archive-list">
|
||||||
${limitedData.map(item => `
|
${limitedData
|
||||||
|
.map(
|
||||||
|
item => `
|
||||||
<li class="card-archive-list-item">
|
<li class="card-archive-list-item">
|
||||||
<a class="card-archive-list-link" href="${createArchiveLink(item)}">
|
<a class="card-archive-list-link" href="${createArchiveLink(item)}">
|
||||||
<span class="card-archive-list-date">
|
<span class="card-archive-list-date">
|
||||||
${transform ? transform(item.name) : item.name}
|
${transform ? transform(item.name) : item.name}
|
||||||
</span>
|
</span>
|
||||||
${showCount
|
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''}
|
||||||
? `<span class="card-archive-list-count">${item.count}</span>`
|
|
||||||
: ''}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
`).join('')}
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</ul>
|
</ul>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -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>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {}) => {
|
||||||
|
if (scriptCache.has(url)) {
|
||||||
|
return scriptCache.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
|
|
||||||
script.src = url
|
script.src = url
|
||||||
script.async = true
|
script.async = true
|
||||||
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
|
|
||||||
script.onload = script.onreadystatechange = () => {
|
for (const key in attr) {
|
||||||
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
|
script.setAttribute(key, attr[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
script.onload = resolve
|
||||||
script.onerror = reject
|
script.onerror = reject
|
||||||
|
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
}),
|
})
|
||||||
getCSS: (url, id) => new Promise((resolve, reject) => {
|
|
||||||
|
scriptCache.set(url, promise)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
},
|
||||||
|
getCSS: (url, id) => {
|
||||||
|
if (cssCache.has(url)) {
|
||||||
|
return cssCache.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
const link = document.createElement('link')
|
const link = document.createElement('link')
|
||||||
|
|
||||||
link.rel = 'stylesheet'
|
link.rel = 'stylesheet'
|
||||||
link.href = url
|
link.href = url
|
||||||
if (id) link.id = id
|
|
||||||
link.onload = link.onreadystatechange = () => {
|
if (id) {
|
||||||
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
link.id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link.onload = resolve
|
||||||
link.onerror = reject
|
link.onerror = reject
|
||||||
|
|
||||||
document.head.appendChild(link)
|
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()
|
||||||
`
|
`
|
||||||
|
|||||||
+84
-44
@@ -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,17 +119,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
|
|||||||
return loop(menu) || defaultTitle
|
return loop(menu) || defaultTitle
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getBgPath', path => {
|
hexo.extend.helper.register('getBgPath', function (path) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
|
|
||||||
const 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(${path});`
|
return `background-image: url(${this.url_for(path)});`
|
||||||
} else {
|
} else {
|
||||||
return `background: ${path};`
|
return `background: ${path};`
|
||||||
}
|
}
|
||||||
@@ -99,37 +133,35 @@ hexo.extend.helper.register('getBgPath', 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) {
|
||||||
return data.slice(0, limit.value)
|
processedData = processedData.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)
|
||||||
return data.filter(item => Date.parse(item.date) >= limitDate)
|
processedData = processedData.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')
|
||||||
|
// markdown
|
||||||
|
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
|
||||||
})
|
})
|
||||||
|
|
||||||
return finalResult
|
return processedData
|
||||||
})
|
})
|
||||||
|
|
||||||
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
hexo.extend.helper.register('getPageType', (page, isHome) => {
|
||||||
@@ -139,7 +171,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 +179,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,19 +49,26 @@ 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) {
|
||||||
|
return b.weight - a.weight
|
||||||
|
}
|
||||||
|
return b.random - a.random
|
||||||
|
})
|
||||||
|
|
||||||
if (relatedPosts.length > 0) {
|
let result = '<div class="relatedPosts">'
|
||||||
result += '<div class="relatedPosts">'
|
|
||||||
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
|
||||||
result += '<div class="relatedPosts-list">'
|
result += '<div class="relatedPosts-list">'
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
|
const max = Math.min(relatedPostsList.length, limitNum)
|
||||||
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i]
|
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
|
const { escape_html, url_for, date } = this
|
||||||
cover = cover || 'var(--default-bg-color)'
|
cover = cover || 'var(--default-bg-color)'
|
||||||
title = escape_html(title)
|
title = escape_html(title)
|
||||||
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
|
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}">`
|
result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
|
||||||
if (cover_type === 'img') {
|
if (cover_type === 'img') {
|
||||||
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
|
||||||
@@ -73,25 +82,12 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
|
|||||||
}
|
}
|
||||||
result += `<div class="info-item-2">${title}</div></div>`
|
result += `<div class="info-item-2">${title}</div></div>`
|
||||||
|
|
||||||
if (postDesc) {
|
if (desc) {
|
||||||
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
|
result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
|
||||||
}
|
}
|
||||||
result += '</div></a>'
|
result += '</div></a>'
|
||||||
}
|
}
|
||||||
|
|
||||||
result += '</div></div>'
|
result += '</div></div>'
|
||||||
return result
|
return result
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function isTagRelated (tagName, tags) {
|
|
||||||
return tags.some(tag => tag.name === tagName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findItem (arrayToSearch, attr, val) {
|
|
||||||
return arrayToSearch.findIndex(item => item[attr] === val)
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare (attr) {
|
|
||||||
return (a, b) => b[attr] - a[attr]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,15 +7,16 @@
|
|||||||
|
|
||||||
'use strict'
|
'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({
|
||||||
|
|||||||
+1
-1
@@ -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="'
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||||
const groups = {}
|
const groups = {}
|
||||||
|
|
||||||
|
hexo.extend.filter.register('before_generate', () => {
|
||||||
|
Object.keys(groups).forEach(k => delete groups[k])
|
||||||
|
})
|
||||||
|
|
||||||
hexo.extend.filter.register('before_post_render', data => {
|
hexo.extend.filter.register('before_post_render', data => {
|
||||||
if (!hexo.theme.config.series.enable) return data
|
if (!hexo.theme.config.series.enable) return data
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -222,3 +281,7 @@ blockquote
|
|||||||
|
|
||||||
& > :last-child
|
& > :last-child
|
||||||
margin-bottom: 0 !important
|
margin-bottom: 0 !important
|
||||||
|
|
||||||
|
.fa-fw
|
||||||
|
width: 1.25em
|
||||||
|
text-align: center
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -13,17 +13,16 @@
|
|||||||
background-color: var(--mark-bg)
|
background-color: var(--mark-bg)
|
||||||
content: ''
|
content: ''
|
||||||
|
|
||||||
#footer-wrap
|
& > *
|
||||||
position: relative
|
position: relative
|
||||||
padding: 40px 20px
|
|
||||||
color: var(--light-grey)
|
color: var(--light-grey)
|
||||||
text-align: center
|
|
||||||
|
|
||||||
a
|
a
|
||||||
color: var(--light-grey)
|
color: var(--light-grey)
|
||||||
|
transition: all .3s ease-in-out
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
text-decoration: underline
|
color: $light-blue
|
||||||
|
|
||||||
.footer-separator
|
.footer-separator
|
||||||
margin: 0 4px
|
margin: 0 4px
|
||||||
@@ -33,3 +32,56 @@
|
|||||||
max-height: 1.4em
|
max-height: 1.4em
|
||||||
width: auto
|
width: auto
|
||||||
vertical-align: text-bottom
|
vertical-align: text-bottom
|
||||||
|
|
||||||
|
.footer-flex
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: space-between
|
||||||
|
margin: 0 auto
|
||||||
|
padding: 40px 60px
|
||||||
|
max-width: 1200px
|
||||||
|
width: 100%
|
||||||
|
text-align: left
|
||||||
|
gap: 13px
|
||||||
|
|
||||||
|
+maxWidth768()
|
||||||
|
padding: 30px
|
||||||
|
gap: 10px
|
||||||
|
|
||||||
|
.footer-flex-items
|
||||||
|
flex-shrink: 0
|
||||||
|
min-width: 100px
|
||||||
|
text-align: left
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.footer-flex-title
|
||||||
|
margin-bottom: 5px
|
||||||
|
white-space: nowrap
|
||||||
|
font-weight: 600
|
||||||
|
font-size: 1.4em
|
||||||
|
|
||||||
|
.footer-flex-item
|
||||||
|
margin: 10px 0
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
a
|
||||||
|
display: block
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.footer-other
|
||||||
|
padding: 40px 20px
|
||||||
|
width: 100%
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
if hexo-config('footer.nav')
|
||||||
|
padding: 10px 8px
|
||||||
|
background-color: rgba(0, 0, 0, .1)
|
||||||
|
|
||||||
|
.copyright,
|
||||||
|
.framework-info,
|
||||||
|
.footer_custom_text
|
||||||
|
font-size: .9em
|
||||||
|
else
|
||||||
|
.framework-info
|
||||||
|
display: block
|
||||||
@@ -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
|
||||||
@@ -436,3 +438,28 @@
|
|||||||
&:hover
|
&:hover
|
||||||
&:after
|
&:after
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
.nav-page-title
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
& > :first-child,
|
||||||
|
& > :last-child
|
||||||
|
display: inline-block
|
||||||
|
transition: all .3s ease-in-out
|
||||||
|
|
||||||
|
& > :last-child
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
left: 0
|
||||||
|
opacity: 0
|
||||||
|
transform: translateY(-50%) translateY(-10px)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
& > :last-child
|
||||||
|
opacity: 1
|
||||||
|
transform: translateY(-50%) translateY(0)
|
||||||
|
|
||||||
|
& > :first-child
|
||||||
|
opacity: 0
|
||||||
|
transform: translateY(10px)
|
||||||
@@ -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,10 +38,14 @@
|
|||||||
font-size: 16px
|
font-size: 16px
|
||||||
line-height: w
|
line-height: w
|
||||||
addBorderRadius(5)
|
addBorderRadius(5)
|
||||||
|
@extend .btn-effects
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--btn-hover-color)
|
background-color: var(--btn-hover-color)
|
||||||
|
|
||||||
|
i
|
||||||
|
vertical-align: baseline
|
||||||
|
|
||||||
#mobile-toc-button
|
#mobile-toc-button
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
@@ -54,19 +58,52 @@
|
|||||||
|
|
||||||
if hexo-config('rightside_scroll_percent')
|
if hexo-config('rightside_scroll_percent')
|
||||||
#go-up
|
#go-up
|
||||||
|
position: relative
|
||||||
|
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
display: none
|
display: none
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
opacity: 0
|
||||||
|
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
|
i
|
||||||
|
position: relative
|
||||||
|
z-index: 1
|
||||||
|
width: 100%
|
||||||
|
opacity: 1
|
||||||
|
transition: all .4s cubic-bezier(.4, 0, .2, 1)
|
||||||
|
transform: scale(1)
|
||||||
|
|
||||||
&.show-percent
|
&.show-percent
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
display: block
|
display: block
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1)
|
||||||
|
animation: fadeInScale .4s ease-out
|
||||||
|
|
||||||
& + i
|
i
|
||||||
display: none
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
&:hover
|
&:hover.show-percent
|
||||||
.scroll-percent
|
.scroll-percent
|
||||||
display: none
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
& + i
|
i
|
||||||
display: block
|
opacity: 1 !important
|
||||||
|
transform: scale(1) !important
|
||||||
|
|
||||||
|
@keyframes fadeInScale
|
||||||
|
from
|
||||||
|
opacity: 0
|
||||||
|
transform: scale(.8)
|
||||||
|
|
||||||
|
to
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1)
|
||||||
@@ -52,7 +52,10 @@
|
|||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background: var(--text-bg-hover)
|
background: var(--text-bg-hover)
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .1)
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
|
transition: all .2s ease
|
||||||
|
transform: translateX(3px)
|
||||||
|
|
||||||
i:first-child
|
i:first-child
|
||||||
width: 15%
|
width: 15%
|
||||||
@@ -70,9 +73,25 @@
|
|||||||
transform: rotate(90deg)
|
transform: rotate(90deg)
|
||||||
|
|
||||||
& + .menus_item_child
|
& + .menus_item_child
|
||||||
display: none
|
overflow: hidden
|
||||||
|
max-height: 0
|
||||||
|
opacity: 0
|
||||||
|
transform: scaleY(0)
|
||||||
|
transform-origin: top
|
||||||
|
|
||||||
.menus_item_child
|
.menus_item_child
|
||||||
margin: 0
|
margin: 0
|
||||||
padding-left: 25px
|
padding-left: 25px
|
||||||
|
max-height: 0
|
||||||
list-style: none
|
list-style: none
|
||||||
|
opacity: 0
|
||||||
|
transition: transform .3s ease, opacity .3s ease, max-height .3s ease
|
||||||
|
transform: scaleY(0)
|
||||||
|
transform-origin: top
|
||||||
|
will-change: transform, opacity, max-height
|
||||||
|
|
||||||
|
// 當父元素沒有 .hide 類時,顯示子目錄
|
||||||
|
.site-page.group:not(.hide) + .menus_item_child
|
||||||
|
max-height: 1000px
|
||||||
|
opacity: 1
|
||||||
|
transform: scaleY(1)
|
||||||
@@ -65,12 +65,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
|
||||||
@@ -183,5 +226,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
|
||||||
@@ -78,27 +128,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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -172,3 +172,4 @@ $indexEnable = hexo-config('cover.index_enable')
|
|||||||
& > .content
|
& > .content
|
||||||
@extend .limit-more-line
|
@extend .limit-more-line
|
||||||
-webkit-line-clamp: 2
|
-webkit-line-clamp: 2
|
||||||
|
word-break: break-word
|
||||||
@@ -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
|
|
||||||
padding: 0
|
|
||||||
@extend .list-beauty
|
|
||||||
|
|
||||||
a
|
|
||||||
color: var(--search-a-color)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $search-color
|
|
||||||
|
|
||||||
mark
|
|
||||||
background: transparent
|
|
||||||
color: $search-keyword-highlight
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.algolia-hits-item-title
|
|
||||||
font-weight: 600
|
|
||||||
|
|
||||||
.algolia-hit-item-content
|
|
||||||
margin: 0 0 8px
|
|
||||||
word-break: break-word
|
|
||||||
|
|
||||||
.ais-Pagination
|
|
||||||
margin: 15px 0 0
|
|
||||||
padding: 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.ais-Pagination-list
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
.ais-Pagination-item
|
|
||||||
display: inline
|
|
||||||
margin: 0 4px
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
.ais-Pagination-link
|
|
||||||
display: inline-block
|
|
||||||
min-width: 24px
|
|
||||||
height: 24px
|
|
||||||
text-align: center
|
|
||||||
line-height: 24px
|
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.ais-Pagination-item--selected
|
|
||||||
a
|
|
||||||
background: $theme-paginator-color
|
|
||||||
color: #eee
|
|
||||||
cursor: default
|
|
||||||
|
|
||||||
.ais-Pagination-item--disabled
|
|
||||||
visibility: hidden
|
|
||||||
|
|
||||||
#algolia-hits
|
|
||||||
> div
|
|
||||||
overflow-y: overlay
|
|
||||||
margin: 0 -20px
|
|
||||||
padding: 0 22px
|
|
||||||
max-height: calc(80vh - 220px)
|
|
||||||
|
|
||||||
+maxWidth768()
|
+maxWidth768()
|
||||||
max-height: none
|
min-height: calc(var(--search-height) - 245px)
|
||||||
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
|
||||||
|
|
||||||
|
#local-search-stats
|
||||||
|
.search-result-stats
|
||||||
|
text-align: left
|
||||||
|
|
||||||
.search-keyword
|
.search-keyword
|
||||||
background: transparent
|
|
||||||
color: $search-keyword-highlight
|
|
||||||
font-weight: 600
|
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
|
||||||
+237
-152
@@ -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
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
$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
|
||||||
if (isToc) {
|
setTimeout(() => autoScrollToc(targetLink), 0)
|
||||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
|
||||||
|
|
||||||
if (currentId) {
|
|
||||||
const currentActive = $tocLink[currentIndex]
|
|
||||||
currentActive.classList.add('active')
|
|
||||||
|
|
||||||
setTimeout(() => autoScrollToc(currentActive), 0)
|
|
||||||
|
|
||||||
if (!isExpand) {
|
if (!isExpand) {
|
||||||
let parent = currentActive.parentNode
|
let parent = targetLink.parentNode
|
||||||
while (!parent.matches('.toc')) {
|
while (!parent.matches('.toc')) {
|
||||||
if (parent.matches('li')) parent.classList.add('active')
|
if (parent.matches('li')) {
|
||||||
|
parent.classList.add('active')
|
||||||
|
activeParentItems.push(parent)
|
||||||
|
}
|
||||||
parent = parent.parentNode
|
parent = parent.parentNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const observerOptions = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '-60px 0px -80% 0px',
|
||||||
|
threshold: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// main of scroll
|
const observer = new IntersectionObserver(entries => {
|
||||||
const tocScrollFn = btf.throttle(() => {
|
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'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsFn = () => {
|
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
|
||||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
}
|
||||||
if (!navTabsElements.length) return
|
|
||||||
|
const tabsFn = $article => {
|
||||||
|
if (!$article.querySelector('.tabs')) 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 tabsRoot = e.target.closest('.tabs')
|
||||||
|
if (!tabsRoot) return
|
||||||
|
|
||||||
|
const navContainer = tabsRoot.firstElementChild
|
||||||
|
const toTopContainer = tabsRoot.lastElementChild
|
||||||
|
|
||||||
|
if (navContainer.contains(e.target)) {
|
||||||
const target = e.target.closest('button')
|
const target = e.target.closest('button')
|
||||||
if (!target || target.classList.contains('active')) return
|
if (!target || target.classList.contains('active')) return
|
||||||
|
|
||||||
const navItems = [...e.currentTarget.children]
|
const navItems = [...navContainer.children]
|
||||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
const tabContents = [...navContainer.nextElementSibling.children]
|
||||||
const indexOfButton = navItems.indexOf(target)
|
const indexOfButton = navItems.indexOf(target)
|
||||||
setActiveClass(navItems, indexOfButton)
|
setActiveClass(navItems, indexOfButton)
|
||||||
e.currentTarget.classList.remove('no-default')
|
navContainer.classList.remove('no-default')
|
||||||
setActiveClass(tabContents, indexOfButton)
|
setActiveClass(tabContents, indexOfButton)
|
||||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToTopClick = tabElement => e => {
|
if (toTopContainer.contains(e.target) && e.target.closest('button')) {
|
||||||
if (e.target.closest('button')) {
|
btf.scrollToDest(btf.getEleTop(tabsRoot), 300)
|
||||||
btf.scrollToDest(btf.getEleTop(tabElement), 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') {
|
||||||
|
|||||||
+381
-84
@@ -6,8 +6,28 @@ window.addEventListener('load', () => {
|
|||||||
return console.error('Algolia setting is invalid!')
|
return console.error('Algolia setting is invalid!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTENT_FIELDS = ['contentStripTruncate', 'contentStrip', 'content']
|
||||||
|
const HIGHLIGHT_PARAMS = {
|
||||||
|
highlightPreTag: '<mark>',
|
||||||
|
highlightPostTag: '</mark>',
|
||||||
|
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compiled regex for tag balancing (reused across cutContent calls)
|
||||||
|
const TAG_REGEX = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*\/?>/g
|
||||||
|
|
||||||
const $searchMask = document.getElementById('search-mask')
|
const $searchMask = document.getElementById('search-mask')
|
||||||
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||||
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const $hits = document.getElementById('algolia-hits')
|
||||||
|
const $hitsEmpty = document.getElementById('algolia-hits-empty')
|
||||||
|
const $hitsList = document.querySelector('#algolia-hits .ais-Hits-list')
|
||||||
|
const $hitsWrapper = document.querySelector('#algolia-hits .ais-Hits')
|
||||||
|
const $pagination = document.getElementById('algolia-pagination')
|
||||||
|
const $paginationList = document.querySelector('#algolia-pagination .ais-Pagination-list')
|
||||||
|
const $stats = document.querySelector('#algolia-info .ais-Stats-text')
|
||||||
|
const $searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
|
||||||
|
const $searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
|
||||||
|
|
||||||
const animateElements = show => {
|
const animateElements = show => {
|
||||||
const action = show ? 'animateIn' : 'animateOut'
|
const action = show ? 'animateIn' : 'animateOut'
|
||||||
@@ -23,10 +43,12 @@ window.addEventListener('load', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSearch = () => {
|
// Debounced resize to avoid layout thrashing
|
||||||
btf.overflowPaddingR.add()
|
let resizeTimer
|
||||||
animateElements(true)
|
const onResize = () => {
|
||||||
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
clearTimeout(resizeTimer)
|
||||||
|
resizeTimer = setTimeout(fixSafariHeight, 150)
|
||||||
|
}
|
||||||
|
|
||||||
const handleEscape = event => {
|
const handleEscape = event => {
|
||||||
if (event.code === 'Escape') {
|
if (event.code === 'Escape') {
|
||||||
@@ -35,15 +57,29 @@ window.addEventListener('load', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showLoading = show => {
|
||||||
|
if ($loadingStatus) $loadingStatus.hidden = !show
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
|
animateElements(true)
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if ($searchInput) $searchInput.focus()
|
||||||
|
}, 100)
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
document.addEventListener('keydown', handleEscape)
|
||||||
fixSafariHeight()
|
fixSafariHeight()
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
window.addEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
btf.overflowPaddingR.remove()
|
btf.overflowPaddingR.remove()
|
||||||
animateElements(false)
|
animateElements(false)
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchClickFn = () => {
|
const searchClickFn = () => {
|
||||||
@@ -55,9 +91,41 @@ window.addEventListener('load', () => {
|
|||||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutContent = (content) => {
|
const extractContentStr = content => {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
const firstOccur = content.indexOf('<mark>')
|
if (typeof content === 'string') return content.trim()
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
if (content.value !== undefined) {
|
||||||
|
const str = String(content.value).trim()
|
||||||
|
return str || ''
|
||||||
|
}
|
||||||
|
if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) return ''
|
||||||
|
try {
|
||||||
|
const str = JSON.stringify(content).trim()
|
||||||
|
return (str === '{}' || str === '[]' || str === '""') ? '' : str
|
||||||
|
} catch (e) { return '' }
|
||||||
|
}
|
||||||
|
if (content.toString && typeof content.toString === 'function') {
|
||||||
|
const str = content.toString().trim()
|
||||||
|
return (str === '[object Object]' || str === '[object Array]') ? '' : str
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractHighlightValue = highlightObj => {
|
||||||
|
if (!highlightObj) return ''
|
||||||
|
if (typeof highlightObj === 'string') return highlightObj.trim()
|
||||||
|
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
|
||||||
|
return String(highlightObj.value).trim()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutContent = content => {
|
||||||
|
const contentStr = extractContentStr(content)
|
||||||
|
if (!contentStr) return ''
|
||||||
|
|
||||||
|
const firstOccur = contentStr.indexOf('<mark>')
|
||||||
let start = firstOccur - 30
|
let start = firstOccur - 30
|
||||||
let end = firstOccur + 120
|
let end = firstOccur + 120
|
||||||
let pre = ''
|
let pre = ''
|
||||||
@@ -70,94 +138,329 @@ window.addEventListener('load', () => {
|
|||||||
pre = '...'
|
pre = '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end > content.length) {
|
if (end > contentStr.length) {
|
||||||
end = content.length
|
end = contentStr.length
|
||||||
} else {
|
} else {
|
||||||
post = '...'
|
post = '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${pre}${content.substring(start, end)}${post}`
|
let substr = contentStr.substring(start, end)
|
||||||
|
|
||||||
|
// Remove incomplete tags at boundaries
|
||||||
|
const firstCloseBracket = substr.indexOf('>')
|
||||||
|
const firstOpenBracket = substr.indexOf('<')
|
||||||
|
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
|
||||||
|
substr = substr.substring(firstCloseBracket + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableDiv = [
|
const lastOpenBracket = substr.lastIndexOf('<')
|
||||||
document.getElementById('algolia-hits'),
|
const lastCloseBracket = substr.lastIndexOf('>')
|
||||||
document.getElementById('algolia-pagination'),
|
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
|
||||||
document.querySelector('#algolia-info .algolia-stats')
|
substr = substr.substring(0, lastOpenBracket)
|
||||||
]
|
|
||||||
|
|
||||||
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
|
|
||||||
const search = instantsearch({
|
|
||||||
indexName,
|
|
||||||
searchClient: searchClient(appId, apiKey),
|
|
||||||
searchFunction (helper) {
|
|
||||||
disableDiv.forEach(item => {
|
|
||||||
item.style.display = helper.state.query ? '' : 'none'
|
|
||||||
})
|
|
||||||
if (helper.state.query) helper.search()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const widgets = [
|
// Balance tags using regex
|
||||||
instantsearch.widgets.configure({ hitsPerPage }),
|
const tagStack = []
|
||||||
instantsearch.widgets.searchBox({
|
let balancedStr = ''
|
||||||
container: '#algolia-search-input',
|
let lastIndex = 0
|
||||||
showReset: false,
|
let match
|
||||||
showSubmit: false,
|
|
||||||
placeholder: languages.input_placeholder,
|
TAG_REGEX.lastIndex = 0
|
||||||
showLoadingIndicator: true
|
while ((match = TAG_REGEX.exec(substr)) !== null) {
|
||||||
}),
|
const fullTag = match[0]
|
||||||
instantsearch.widgets.hits({
|
const tagName = match[1]
|
||||||
container: '#algolia-hits',
|
const tagStart = match.index
|
||||||
templates: {
|
|
||||||
item (data) {
|
// Append text before this tag
|
||||||
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
balancedStr += substr.substring(lastIndex, tagStart)
|
||||||
const result = data._highlightResult
|
|
||||||
const content = result.contentStripTruncate
|
if (fullTag.startsWith('</')) {
|
||||||
? cutContent(result.contentStripTruncate.value)
|
// Closing tag - remove matching opening tag from stack
|
||||||
: result.contentStrip
|
const idx = tagStack.lastIndexOf(tagName)
|
||||||
? cutContent(result.contentStrip.value)
|
if (idx !== -1) tagStack.splice(idx, 1)
|
||||||
: result.content
|
} else if (!fullTag.endsWith('/>') && !fullTag.startsWith('<!')) {
|
||||||
? cutContent(result.content.value)
|
// Opening tag (not self-closing or comment)
|
||||||
: ''
|
tagStack.push(tagName)
|
||||||
return `
|
}
|
||||||
|
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">
|
<a href="${link}" class="algolia-hit-item-link">
|
||||||
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
<span class="algolia-hits-item-title">${title}</span>
|
||||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||||
</a>`
|
</a>
|
||||||
},
|
</li>`
|
||||||
empty (data) {
|
}).join('')
|
||||||
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
|
||||||
|
$hitsList.innerHTML = hitsHTML
|
||||||
|
$hitsWrapper.style.display = query ? '' : 'none'
|
||||||
|
|
||||||
|
if (hits.length > 0) {
|
||||||
|
$stats.style.display = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
instantsearch.widgets.stats({
|
const renderPagination = (page, nbPages) => {
|
||||||
container: '#algolia-info > .algolia-stats',
|
if (nbPages <= 1) {
|
||||||
templates: {
|
$pagination.style.display = 'none'
|
||||||
text (data) {
|
$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
|
const stats = languages.hits_stats
|
||||||
.replace(/\$\{hits}/, data.nbHits)
|
.replace(/\$\{hits}/, nbHits)
|
||||||
.replace(/\$\{time}/, data.processingTimeMS)
|
.replace(/\$\{time}/, processingTimeMS)
|
||||||
return `<hr>${stats}`
|
$stats.innerHTML = `<hr>${stats}`
|
||||||
|
$stats.style.display = ''
|
||||||
|
} else {
|
||||||
|
$stats.style.display = 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
instantsearch.widgets.poweredBy({
|
const performSearch = async (query, page = 0) => {
|
||||||
container: '#algolia-info > .algolia-poweredBy'
|
const trimmedQuery = query.trim()
|
||||||
}),
|
|
||||||
instantsearch.widgets.pagination({
|
if (!trimmedQuery) {
|
||||||
container: '#algolia-pagination',
|
currentQuery = ''
|
||||||
totalPages: 5,
|
searchRequestId++
|
||||||
templates: {
|
renderHits([], '', 0)
|
||||||
first: '<i class="fas fa-angle-double-left"></i>',
|
renderPagination(0, 0)
|
||||||
last: '<i class="fas fa-angle-double-right"></i>',
|
renderStats(0, 0, '')
|
||||||
previous: '<i class="fas fa-angle-left"></i>',
|
toggleResultsVisibility(false)
|
||||||
next: '<i class="fas fa-angle-right"></i>'
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(true)
|
||||||
|
currentQuery = trimmedQuery
|
||||||
|
const requestId = ++searchRequestId
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
|
||||||
|
if (searchClient && typeof searchClient.search === 'function') {
|
||||||
|
// v5 multi-index search
|
||||||
|
const searchResult = await searchClient.search([{
|
||||||
|
indexName,
|
||||||
|
query: trimmedQuery,
|
||||||
|
params: { page, hitsPerPage, ...HIGHLIGHT_PARAMS }
|
||||||
|
}])
|
||||||
|
result = searchResult.results[0]
|
||||||
|
} else if (searchClient && typeof searchClient.initIndex === 'function') {
|
||||||
|
// v4 single-index search
|
||||||
|
const index = searchClient.initIndex(indexName)
|
||||||
|
result = await index.search(trimmedQuery, { page, hitsPerPage, ...HIGHLIGHT_PARAMS })
|
||||||
|
} else {
|
||||||
|
throw new Error('Algolia: No compatible search method available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard stale results from superseded searches
|
||||||
|
if (requestId !== searchRequestId) return
|
||||||
|
|
||||||
|
renderHits(result.hits || [], trimmedQuery, page)
|
||||||
|
|
||||||
|
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
|
||||||
|
renderPagination(page, actualNbPages)
|
||||||
|
renderStats(result.nbHits || 0, result.processingTimeMS || 0, trimmedQuery)
|
||||||
|
|
||||||
|
const hasResults = result.hits && result.hits.length > 0
|
||||||
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
|
// Refresh Pjax links
|
||||||
|
if (window.pjax) {
|
||||||
|
window.pjax.refresh($hits)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== searchRequestId) return
|
||||||
|
console.error('Algolia search error:', error)
|
||||||
|
renderHits([], trimmedQuery, page)
|
||||||
|
renderPagination(0, 0)
|
||||||
|
renderStats(0, 0, trimmedQuery)
|
||||||
|
} finally {
|
||||||
|
if (requestId === searchRequestId) {
|
||||||
|
showLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchTimeout
|
||||||
|
const debouncedSearch = (query, delay = 300) => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
// Empty query: clear results immediately without debounce delay
|
||||||
|
if (!query.trim()) {
|
||||||
|
performSearch(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => performSearch(query), delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeSearch = () => {
|
||||||
|
showLoading(false)
|
||||||
|
|
||||||
|
if ($searchInput) {
|
||||||
|
$searchInput.addEventListener('input', e => {
|
||||||
|
debouncedSearch(e.target.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchForm) {
|
||||||
|
$searchForm.addEventListener('submit', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
performSearch($searchInput ? $searchInput.value : '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination event delegation
|
||||||
|
$pagination.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const link = e.target.closest('a[data-page]')
|
||||||
|
if (link) {
|
||||||
|
const page = parseInt(link.dataset.page, 10)
|
||||||
|
if (!isNaN(page) && currentQuery) {
|
||||||
|
performSearch(currentQuery, page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
|
||||||
|
|
||||||
search.addWidgets(widgets)
|
// Initial state
|
||||||
search.start()
|
toggleResultsVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSearch()
|
||||||
searchClickFn()
|
searchClickFn()
|
||||||
searchFnOnce()
|
searchFnOnce()
|
||||||
|
|
||||||
@@ -165,10 +468,4 @@ window.addEventListener('load', () => {
|
|||||||
if (!btf.isHidden($searchMask)) closeSearch()
|
if (!btf.isHidden($searchMask)) closeSearch()
|
||||||
searchClickFn()
|
searchClickFn()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (window.pjax) {
|
|
||||||
search.on('render', () => {
|
|
||||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,31 +15,39 @@ class LocalSearch {
|
|||||||
this.top_n_per_article = top_n_per_article
|
this.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
|
||||||
|
}
|
||||||
|
|
||||||
|
_processKeywords (keywords) {
|
||||||
|
if (this._processedKeywords) return this._processedKeywords
|
||||||
|
this._processedKeywords = keywords.map(word => {
|
||||||
|
if (this.unescape) {
|
||||||
|
this._unescapeDiv.innerText = word
|
||||||
|
return this._unescapeDiv.innerHTML
|
||||||
|
}
|
||||||
|
return word
|
||||||
|
})
|
||||||
|
return this._processedKeywords
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexByWord (words, text, caseSensitive = false) {
|
getIndexByWord (words, text, caseSensitive = false) {
|
||||||
const index = []
|
const index = []
|
||||||
const included = new Set()
|
const included = new Set()
|
||||||
|
const processedWords = this._processKeywords(words)
|
||||||
|
|
||||||
if (!caseSensitive) {
|
if (!caseSensitive) {
|
||||||
text = text.toLowerCase()
|
text = text.toLowerCase()
|
||||||
}
|
}
|
||||||
words.forEach(word => {
|
processedWords.forEach((word, i) => {
|
||||||
if (this.unescape) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerText = word
|
|
||||||
word = div.innerHTML
|
|
||||||
}
|
|
||||||
const wordLen = word.length
|
const wordLen = word.length
|
||||||
if (wordLen === 0) return
|
if (wordLen === 0) return
|
||||||
let startPosition = 0
|
let startPosition = 0
|
||||||
let position = -1
|
let position = -1
|
||||||
if (!caseSensitive) {
|
const searchWord = caseSensitive ? word : word.toLowerCase()
|
||||||
word = word.toLowerCase()
|
while ((position = text.indexOf(searchWord, startPosition)) > -1) {
|
||||||
}
|
|
||||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
|
||||||
index.push({ position, word })
|
index.push({ position, word })
|
||||||
included.add(word)
|
included.add(words[i])
|
||||||
startPosition = position + wordLen
|
startPosition = position + wordLen
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -90,19 +98,22 @@ class LocalSearch {
|
|||||||
|
|
||||||
// Highlight title and content
|
// Highlight title and content
|
||||||
highlightKeyword (val, slice) {
|
highlightKeyword (val, slice) {
|
||||||
let result = ''
|
const parts = []
|
||||||
let index = slice.start
|
let index = slice.start
|
||||||
for (const { position, length } of slice.hits) {
|
for (const { position, length } of slice.hits) {
|
||||||
result += val.substring(index, position)
|
parts.push(val.substring(index, position))
|
||||||
index = position + length
|
index = position + length
|
||||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
parts.push(`<mark class="search-keyword">${val.substring(position, position + length)}</mark>`)
|
||||||
}
|
}
|
||||||
result += val.substring(index, slice.end)
|
parts.push(val.substring(index, slice.end))
|
||||||
return result
|
return parts.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
getResultItems (keywords) {
|
getResultItems (keywords) {
|
||||||
const resultItems = []
|
const resultItems = []
|
||||||
|
this._processedKeywords = null
|
||||||
|
// Compute highlight param once instead of per-article
|
||||||
|
const highlightParam = keywords.join(' ')
|
||||||
this.datas.forEach(({ title, content, url }) => {
|
this.datas.forEach(({ title, content, url }) => {
|
||||||
// The number of different keywords included in the article.
|
// The number of different keywords included in the article.
|
||||||
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||||
@@ -147,7 +158,7 @@ class LocalSearch {
|
|||||||
let resultItem = ''
|
let resultItem = ''
|
||||||
|
|
||||||
url = new URL(url, location.origin)
|
url = new URL(url, location.origin)
|
||||||
url.searchParams.append('highlight', keywords.join(' '))
|
url.searchParams.append('highlight', highlightParam)
|
||||||
|
|
||||||
if (slicesOfTitle.length !== 0) {
|
if (slicesOfTitle.length !== 0) {
|
||||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||||
@@ -156,10 +167,10 @@ class LocalSearch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slicesOfContent.forEach(slice => {
|
slicesOfContent.forEach(slice => {
|
||||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
|
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
|
||||||
})
|
})
|
||||||
|
|
||||||
resultItem += '</li>'
|
resultItem += '</a></li>'
|
||||||
resultItems.push({
|
resultItems.push({
|
||||||
item: resultItem,
|
item: resultItem,
|
||||||
id: resultItems.length,
|
id: resultItems.length,
|
||||||
@@ -173,7 +184,10 @@ class LocalSearch {
|
|||||||
fetchData () {
|
fetchData () {
|
||||||
const isXml = !this.path.endsWith('json')
|
const isXml = !this.path.endsWith('json')
|
||||||
fetch(this.path)
|
fetch(this.path)
|
||||||
.then(response => response.text())
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
// Get the contents from search data
|
// Get the contents from search data
|
||||||
this.isfetched = true
|
this.isfetched = true
|
||||||
@@ -194,6 +208,12 @@ class LocalSearch {
|
|||||||
// Remove loading animation
|
// Remove loading animation
|
||||||
window.dispatchEvent(new Event('search:loaded'))
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
})
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Local search data fetch failed:', error)
|
||||||
|
this.isfetched = true
|
||||||
|
this.datas = []
|
||||||
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight by wrapping node in mark elements with the given class name
|
// Highlight by wrapping node in mark elements with the given class name
|
||||||
@@ -206,7 +226,7 @@ class LocalSearch {
|
|||||||
index = position + length
|
index = position + length
|
||||||
const mark = document.createElement('mark')
|
const mark = document.createElement('mark')
|
||||||
mark.className = className
|
mark.className = className
|
||||||
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
mark.appendChild(document.createTextNode(val.substring(position, position + length)))
|
||||||
children.push(text, mark)
|
children.push(text, mark)
|
||||||
}
|
}
|
||||||
node.nodeValue = val.substring(index, slice.end)
|
node.nodeValue = val.substring(index, slice.end)
|
||||||
@@ -236,40 +256,190 @@ class LocalSearch {
|
|||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Search
|
// Search
|
||||||
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
|
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||||
|
const enablePagination = pagination && pagination.enable
|
||||||
const localSearch = new LocalSearch({
|
const localSearch = new LocalSearch({
|
||||||
path,
|
path,
|
||||||
top_n_per_article,
|
top_n_per_article,
|
||||||
unescape
|
unescape
|
||||||
})
|
})
|
||||||
|
|
||||||
const input = document.querySelector('#local-search-input input')
|
const $input = document.querySelector('.local-search-input input')
|
||||||
const statsItem = document.getElementById('local-search-stats-wrap')
|
const $statsItem = document.getElementById('local-search-stats')
|
||||||
const $loadingStatus = document.getElementById('loading-status')
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const $searchMask = document.getElementById('search-mask')
|
||||||
|
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||||
|
const $results = document.getElementById('local-search-results')
|
||||||
|
const $pagination = document.getElementById('local-search-pagination')
|
||||||
|
const $paginationList = document.querySelector('#local-search-pagination .ais-Pagination-list')
|
||||||
const isXml = !path.endsWith('json')
|
const isXml = !path.endsWith('json')
|
||||||
|
|
||||||
const inputEventFunction = () => {
|
// Pagination variables (only initialize if pagination is enabled)
|
||||||
if (!localSearch.isfetched) return
|
let currentPage = 0
|
||||||
let searchText = input.value.trim().toLowerCase()
|
const hitsPerPage = pagination.hitsPerPage || 10
|
||||||
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
|
||||||
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
|
let currentResultItems = []
|
||||||
const keywords = searchText.split(/[-\s]+/)
|
|
||||||
const container = document.getElementById('local-search-results')
|
if (!enablePagination) {
|
||||||
let resultItems = []
|
currentPage = undefined
|
||||||
if (searchText.length > 0) {
|
currentResultItems = undefined
|
||||||
// Perform local searching
|
|
||||||
resultItems = localSearch.getResultItems(keywords)
|
|
||||||
}
|
}
|
||||||
if (keywords.length === 1 && keywords[0] === '') {
|
|
||||||
container.textContent = ''
|
// Show/hide search results area
|
||||||
statsItem.textContent = ''
|
const toggleResultsVisibility = hasResults => {
|
||||||
} else if (resultItems.length === 0) {
|
$pagination.style.display = (hasResults && enablePagination) ? '' : 'none'
|
||||||
container.textContent = ''
|
}
|
||||||
|
|
||||||
|
// Render search results for current page
|
||||||
|
const renderResults = (searchText, resultItems) => {
|
||||||
|
// Determine items to display based on pagination mode
|
||||||
|
const itemsToDisplay = enablePagination
|
||||||
|
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||||
|
: resultItems
|
||||||
|
|
||||||
|
// Handle empty page in pagination mode
|
||||||
|
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
|
||||||
|
currentPage = 0
|
||||||
|
renderResults(searchText, resultItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numbering to items
|
||||||
|
const numberedItems = itemsToDisplay.map((result, index) => {
|
||||||
|
const itemNumber = enablePagination
|
||||||
|
? currentPage * hitsPerPage + index + 1
|
||||||
|
: index + 1
|
||||||
|
return result.item.replace(
|
||||||
|
'<li class="local-search-hit-item">',
|
||||||
|
`<li class="local-search-hit-item" value="${itemNumber}">`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
$results.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||||
|
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||||
|
$statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
|
||||||
|
// Handle pagination
|
||||||
|
if (enablePagination) {
|
||||||
|
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
|
||||||
|
renderPagination(currentPage, nbPages, searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasResults = resultItems.length > 0
|
||||||
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
|
window.pjax && window.pjax.refresh($results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render pagination
|
||||||
|
const renderPagination = (page, nbPages) => {
|
||||||
|
if (nbPages <= 1) {
|
||||||
|
$pagination.style.display = 'none'
|
||||||
|
$paginationList.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstPage = page === 0
|
||||||
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
|
// Responsive page display
|
||||||
|
const isMobile = window.innerWidth < 768
|
||||||
|
const maxVisiblePages = isMobile ? 3 : 5
|
||||||
|
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||||
|
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||||
|
|
||||||
|
// Adjust starting page to maintain max visible pages
|
||||||
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
|
||||||
|
// Only add ellipsis and first page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a></li>')
|
||||||
|
if (startPage > 1) {
|
||||||
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add middle page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
if (i === page) {
|
||||||
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected"><span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span></li>`)
|
||||||
|
} else {
|
||||||
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a></li>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add ellipsis and last page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
|
if (endPage < nbPages - 2) {
|
||||||
|
parts.push('<li class="ais-Pagination-item ais-Pagination-item--ellipsis"><span class="ais-Pagination-link">...</span></li>')
|
||||||
|
}
|
||||||
|
parts.push(`<li class="ais-Pagination-item ais-Pagination-item--page"><a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a></li>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build prev/next links
|
||||||
|
const prevLink = isFirstPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
|
const nextLink = isLastPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
|
|
||||||
|
$paginationList.innerHTML = `<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">${prevLink}</li>${parts.join('')}<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">${nextLink}</li>`
|
||||||
|
$pagination.style.display = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search results and stats
|
||||||
|
const clearSearchResults = () => {
|
||||||
|
$results.textContent = ''
|
||||||
|
$statsItem.textContent = ''
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = []
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show no results message
|
||||||
|
const showNoResults = searchText => {
|
||||||
|
$results.textContent = ''
|
||||||
const statsDiv = document.createElement('div')
|
const statsDiv = document.createElement('div')
|
||||||
statsDiv.className = 'search-result-stats'
|
statsDiv.className = 'search-result-stats'
|
||||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||||
statsItem.innerHTML = statsDiv.outerHTML
|
$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 {
|
} else {
|
||||||
|
// Sort results by relevance
|
||||||
resultItems.sort((left, right) => {
|
resultItems.sort((left, right) => {
|
||||||
if (left.includedCount !== right.includedCount) {
|
if (left.includedCount !== right.includedCount) {
|
||||||
return right.includedCount - left.includedCount
|
return right.includedCount - left.includedCount
|
||||||
@@ -279,54 +449,73 @@ window.addEventListener('load', () => {
|
|||||||
return right.id - left.id
|
return right.id - left.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
if (enablePagination) {
|
||||||
|
currentResultItems = resultItems
|
||||||
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
|
currentPage = 0
|
||||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
}
|
||||||
window.pjax && window.pjax.refresh(container)
|
renderResults(searchText, resultItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
$loadingStatus.textContent = ''
|
$loadingStatus.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced input handler
|
||||||
|
let searchTimeout
|
||||||
|
const debouncedInputEvent = () => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
// Empty input: clear results immediately without debounce delay
|
||||||
|
if (!$input.value.trim()) {
|
||||||
|
inputEventFunction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(inputEventFunction, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadFlag = false
|
let loadFlag = false
|
||||||
const $searchMask = document.getElementById('search-mask')
|
|
||||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
|
||||||
|
|
||||||
// fix safari
|
|
||||||
const fixSafariHeight = () => {
|
const fixSafariHeight = () => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
$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 = () => {
|
const openSearch = () => {
|
||||||
btf.overflowPaddingR.add()
|
btf.overflowPaddingR.add()
|
||||||
btf.animateIn($searchMask, 'to_show 0.5s')
|
btf.animateIn($searchMask, 'to_show 0.5s')
|
||||||
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
||||||
setTimeout(() => { input.focus() }, 300)
|
setTimeout(() => { $input.focus() }, 300)
|
||||||
if (!loadFlag) {
|
if (!loadFlag) {
|
||||||
!localSearch.isfetched && localSearch.fetchData()
|
!localSearch.isfetched && localSearch.fetchData()
|
||||||
input.addEventListener('input', inputEventFunction)
|
$input.addEventListener('input', debouncedInputEvent)
|
||||||
loadFlag = true
|
loadFlag = true
|
||||||
}
|
}
|
||||||
// shortcut: ESC
|
// shortcut: ESC
|
||||||
document.addEventListener('keydown', function f (event) {
|
document.addEventListener('keydown', handleEscape)
|
||||||
if (event.code === 'Escape') {
|
|
||||||
closeSearch()
|
|
||||||
document.removeEventListener('keydown', f)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fixSafariHeight()
|
fixSafariHeight()
|
||||||
window.addEventListener('resize', fixSafariHeight)
|
window.addEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
btf.overflowPaddingR.remove()
|
btf.overflowPaddingR.remove()
|
||||||
btf.animateOut($searchDialog, 'search_close .5s')
|
btf.animateOut($searchDialog, 'search_close .5s')
|
||||||
btf.animateOut($searchMask, 'to_hide 0.5s')
|
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||||
window.removeEventListener('resize', fixSafariHeight)
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchClickFn = () => {
|
const searchClickFn = () => {
|
||||||
@@ -340,11 +529,29 @@ window.addEventListener('load', () => {
|
|||||||
localSearch.fetchData()
|
localSearch.fetchData()
|
||||||
}
|
}
|
||||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
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', () => {
|
window.addEventListener('search:loaded', () => {
|
||||||
const $loadDataItem = document.getElementById('loading-database')
|
const $loadDataItem = document.getElementById('loading-database')
|
||||||
$loadDataItem.nextElementSibling.style.display = 'block'
|
$loadDataItem.nextElementSibling.style.visibility = 'visible'
|
||||||
$loadDataItem.remove()
|
$loadDataItem.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+27
-14
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