Compare commits

...

65 Commits
5.4.1 ... dev

Author SHA1 Message Date
Jerry
e3e1d9e6ce feat: 新增 Mermaid 圖表功能的選項,支持在新標籤頁中打開和縮放平移互動
fix: 更新插件版本以修正相容性問題
refactor: 優化代碼結構,提升可讀性
2026-01-26 22:48:18 +08:00
Jerry
954598b45c Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2026-01-16 01:16:49 +08:00
Jerry
b6afb6edad fix: 修正 ABCJS 渲染使用 textContent 以提高音樂譜顯示準確性
style: 移除多餘的換行符號以保持代碼整潔
2026-01-16 01:16:47 +08:00
Jerry Wong
b06e704281 Merge pull request #1777 from bugwz/mermaid_svg_action
Add pan and zoom support for mermaid diagrams
2026-01-15 23:30:45 +08:00
Jerry
e4597deded update 2026-01-15 23:28:14 +08:00
Jerry Wong
b034637bc1 Merge pull request #1778 from Void4m0n/feature/cloudtags-custom-colors
feat(cloudTags): add custom colors support for cloudTags on card_tags widget & tags page Pr_Satus(Finished)
2026-01-15 23:08:29 +08:00
Void4m0n
bdc97cc22b fix(tagcloud): independent tags page and card_tags, drop colormode, use inline css, keep tag-color classes 2026-01-12 13:18:09 +01:00
Void4m0n
5878e3b7ee refactor(tagcloud): switch custom_colors from inline styles to CSS classes 2026-01-12 11:42:43 +01:00
Void4m0n
d15620198c feat(card_tags): add colormode/custom_colors to cloudTags call 2026-01-12 11:42:43 +01:00
Void4m0n
3b4b143c27 feat(cloudTags): add custom colors support for cloudTags on tags page 2026-01-12 11:42:43 +01:00
Jerry
2bab33d312 Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2026-01-10 13:19:26 +08:00
Jerry
b0951197a9 feat: 優化代碼高亮系統並修復樣式問題 (v5.5.4-b1)
- 簡化 config.pug 中的高亮設置檢測邏輯
- 重構滾動條樣式,提取為可重用的 $scrollbar-style
- 優化 JavaScript 代碼高亮邏輯,增加對默認高亮的支持
- 修復打賞彈窗的邊框圓角應用位置
- 改善代碼高亮工具的檢測和處理流程
2026-01-10 13:19:23 +08:00
Jerry Wong
572321a00f Merge pull request #1782 from akFace/dev
优化页面标题过长显示的问题
2026-01-06 17:21:24 +08:00
墨迹
e7f796e110 Update head.styl 2026-01-04 00:36:44 +08:00
Jerry Wong
02c8e8e3b5 Merge pull request #1784 from jerryc127/revert-1776-css/text-autospace
Revert "chore: use `text-autospace` by default"
2026-01-04 00:03:07 +08:00
Jerry Wong
edbd4d10c3 Revert "chore: use text-autospace by default" 2026-01-04 00:02:54 +08:00
墨迹
2cdf93c58c Restore page header styles and layout
Restored the page header styles with background and layout properties.
2026-01-02 15:16:27 +08:00
bugwz
2c5e8c7cbb Optimize mermaid view 2025-12-21 19:19:42 +08:00
Jerry Wong
80da110698 Merge pull request #1776 from DeepChirp/css/text-autospace
chore: use `text-autospace` by default
2025-12-21 12:37:37 +08:00
bugwz
0cc950a111 Add view button 2025-12-20 22:24:10 +08:00
bugwz
dd2d051b3d Add pan and zoom support for mermaid diagrams 2025-12-20 20:57:32 +08:00
DeepChirp
3f3102d10b chore: use text-autospace by default 2025-12-16 23:34:24 +08:00
Jerry
c771efa9bc Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2025-12-10 19:23:00 +08:00
Jerry Wong
1ebad6b1b7 Merge pull request #1769 from Void4m0n/add-post-pagination-cover-dev
Allow to set specific post pagination cover
2025-12-10 19:19:34 +08:00
Jerry
10c967957b 恢复修改 2025-12-10 19:18:12 +08:00
Jerry
f1397da086 chore: release v5.5.3
- Bump version from 5.5.3-b2 to 5.5.3 in package.json
- Update third-party dependencies:
  * algolia_search: 5.43.0 -> 5.46.0
  * docsearch: 4.3.1 -> 4.3.2
  * fancybox: 6.1.4 -> 6.1.7
  * katex: 0.16.25 -> 0.16.27
  * mermaid: 11.12.1 -> 11.12.2
  * waline: 3.7.1 -> 3.8.0

perf: optimize JavaScript performance
- Add defer attribute to script tags in pjax and prismjs
- Improve DOM content loading timing in pjax
- Optimize utilities with better throttle implementation
- Cache header positions for TOC performance
- Optimize related posts generation with Maps
- Improve archive helpers performance

fix: improve error handling and UI fixes
- Replace process.exit with proper error throwing
- Fix tooltip positioning with boundary checks
- Add btn-effects to readmode exit button
- Fix element height calculation for hidden elements
- Improve image filters in dark mode (brightness .88, contrast .95)

style: code improvements and consistency
- Refactor random cover generation with generator pattern
- Optimize data processing in helpers
- Clean up unused functions and improve code structure
- Fix Chinese translation: 页 -> 頁
2025-12-10 19:16:03 +08:00
Void4m0n
bb7b206369 support asset_folder for pagination_cover yaml 2025-11-25 21:37:48 +01:00
Void4m0n
586805ceb2 Search post pagination_cover yaml for prev/next card img 2025-11-25 21:37:21 +01:00
Jerry Wong
7dc7942230 Merge pull request #1764 from zhdbk3/dev
feat: 随机背景
2025-11-25 14:54:04 +08:00
Jerry Wong
8ae57ac9bf Update layout.pug 2025-11-25 14:52:42 +08:00
着火的冰块nya
57a7db7a52 fix: 随机背景适配 Pjax 2025-11-25 12:29:48 +08:00
Jerry
4225d23cb6 feat: 升級到 v5.5.3-b2 並優化 hide 標籤組件
- 升級版本號到 5.5.3-b2
- 優化 hide 標籤的 toggle 按鈕樣式,添加旋轉圖標動畫
- 增強 Umami Analytics,在 URL 參數中添加 path 欄位
- 為 toggle-button 添加暗色模式支援
- 修復代碼格式問題
2025-11-21 16:33:27 +08:00
着火的冰块nya
c882e84cd2 feat: 随机背景(修正上次的错误) 2025-11-18 10:27:29 +08:00
着火的冰块nya
29a6fa455a Merge branch 'jerryc127:dev' into dev 2025-11-17 18:21:26 +08:00
Jerry
7985bdda9e - fix: 修正 Umami Analytics API 參數與資料處理邏輯
- style: 最佳化程式碼高亮工具列顯示與間距設定
- improve: 改善複製工具提示的定位邏輯
- bump: 版本號更新至 5.5.3
2025-11-17 15:42:00 +08:00
Jerry Wong
bdf77e328d Merge pull request #1765 from zhdbk3/fix-1762
fix: 修复 busuanzi data-pjax src
2025-11-17 14:46:39 +08:00
Jerry Wong
65a077a00d Refactor additional-js.pug structure and scripts 2025-11-17 14:45:38 +08:00
Jerry Wong
17fc32b59d Merge pull request #1759 from DeepChirp/fix/umami-v3
fix(umami): adapt umami v3
2025-11-17 14:39:56 +08:00
着火的冰块nya
83574c75ad fix: 修复 busuanzi data-pjax src 2025-11-17 11:21:22 +08:00
着火的冰块nya
eb263085cf feat: 随机背景 2025-11-16 10:34:08 +08:00
DeepChirp
15b7a018c6 fix(umami): adapt umami v3 2025-11-08 11:54:34 +08:00
Jerry
439014bbb6 chore: 升級版本至 5.5.2 並更新相依套件
- perf(highlight): 改善複製提示 UI,新增浮動動畫效果
- fix(shuoshuo): 為說說內容中的圖片新增延遲載入支援
- fix(mermaid): 支援在標籤助手中使用自訂配置選項
- fix: 使用 url_for() 處理主題資源路徑 (busuanzi, artalk)
- refactor(aside_archives): 重新命名 url_for 變數以保持一致性
- refactor(inject_head_js): 使用解構賦值命名方式
- chore(deps): 更新 hexo-util 至 4.0.0
- chore(plugins): 更新多個 CDN 套件版本
- style(highlight): 優化程式碼區塊工具列佈局與溢位處理
- fix(zh-CN): 修正分頁文字使用正確的簡體字「页」
2025-11-07 22:19:16 +08:00
Jerry
2d4765202d Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2025-10-02 14:32:19 +08:00
Jerry
5e766ac40a chore: bump version to 5.5.1 and update dependencies
- Upgrade version from 5.5.0 to 5.5.1
- Update third-party dependencies: algolia, disqusjs, docsearch, fancybox, fontawesome, mermaid
- Replace Twitter with X in share configurations
- Enable CDN version numbers by default
- Fix shuoshuo page JSON security with safeJSON helper
- Improve image lazy loading regex to handle minified HTML
- Fix search result HTML structure and styling
- Add margin-top to search result numbering for better alignment
2025-10-02 14:32:08 +08:00
Jerry Wong
4c3a782610 Merge pull request #1736 from DeepChirp/fix-zh-cn
fix(languages): 将大陆地区用字改为简体(`頁`→`页`)
2025-09-09 18:31:48 +08:00
DeepChirp
8645a4355d fix(languages): 将大陆地区用字改为简体( 2025-09-09 16:36:25 +08:00
Jerry
4226c95818 Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2025-09-09 15:40:14 +08:00
Jerry
67c95cee0c 🔍 搜索功能改進:
- 新增本地搜索分頁配置 (enablePagination, hitsPerPage)
- 重構 Algolia 搜索邏輯,支援多索引和更好的錯誤處理
- 優化搜索 UI 樣式,包括分頁按鈕和響應式設計
- 改進搜索結果顯示,新增編號和更好的高亮處理

📦 依賴項更新:
- 更新 plugins.yml 中的多個插件版本 (abcjs, algolia, aplayer 等)
- 更新 package.json 版本號為 5.5.0

🎨 UI/UX 優化:
- 改進側邊欄和目錄的動畫效果
- 優化樣式佈局,調整寬度百分比
- 新增說說頁面的分頁導航組件
- 改進右側邊欄按鈕樣式

🐛 錯誤處理和代碼優化:
- 修復 Umami Analytics 的錯誤處理和數據驗證
- 改進懶加載圖片的正則表達式,避免匹配腳本標籤
- 移除未使用的變數和改進代碼結構
- 新增說說內容的 Markdown 渲染支援

🔧 其他改進:
- 更新翻譯功能,移除箭頭函數語法以提升相容性
2025-09-09 15:40:08 +08:00
Jerry Wong
0a72f43fae Merge pull request #1732 from DeepChirp/umami-script-name
feat(umami): 支持自定义脚本名称
2025-09-08 20:04:49 +08:00
Jerry Wong
006414da98 Merge pull request #1734 from DeepChirp/fix-pjax-failure
fix(Pjax): 增加错误处理以防止Pjax失效
2025-09-08 20:04:20 +08:00
DeepChirp
c61f55a773 fix(Pjax): 增加错误处理以防止Pjax失效 2025-09-05 23:43:14 +08:00
DeepChirp
6feac51de7 feat(umami): 支持自定义脚本名称 2025-09-03 12:56:07 +08:00
Jerry Wong
fcd760011a Update FUNDING.yml 2025-08-27 11:35:54 +08:00
Jerry
ec1a226774 Fix merge conflicts 2025-08-19 14:55:15 +08:00
Jerry
5ee24defc3 feat: 預設關閉 structured_data
feat: 升級一些項目依賴
feat: 重寫 README.md 和 README_CN.md,改進文檔結構和內容
feat: tags 標籤插件夜間模式調整
feat: 加按鈕懸停效果和動畫
fix: 修復右下角箭頭圖標位置沒有居中的 bug
feat: 增加右下角箭頭和滾動百分比的切換效果
improvement: 優化 tags 頁標籤雲顯示效果
improvement: 整合部分js到 init.js
improvement: 統一 CSS 變數使用,改進主題一致性
2025-08-19 14:49:39 +08:00
Jerry Wong
58818a0630 Merge pull request #1718 from Windsland52/dev
fix(gitalk): 修复 MD5 函数导致的评论聚合问题
2025-08-14 13:51:09 +08:00
Jerry Wong
a551b9277f Merge pull request #1720 from DeepChirp/structured_data
feat(structured_data): add `alternateNames` for backup
2025-08-14 13:50:33 +08:00
DeepChirp
1b5bc97431 feat(structured_data): add alternateNames for backup 2025-07-31 19:37:25 +08:00
Windsland52
0da72787fa fix(gitalk): 修复 MD5 函数导致的评论聚合问题
- 修改 url_for 调用添加 {relative: false} 参数
- 解决空字符串 MD5 导致多页面评论混合的问题
2025-07-30 08:27:46 +08:00
Jerry
0e9b8f5b69 Merge branch 'dev' of https://github.com/jerryc127/hexo-theme-butterfly into dev 2025-07-27 15:26:37 +08:00
Jerry
89b3626c84 fix: 修復 aside 最後一個 沒有 margin-bottom 的 bug
feat: 適配 fortawesome v7
improvement: 優化 loading 加載
2025-07-27 15:26:28 +08:00
Jerry Wong
c5417d4532 Merge pull request #1699 from DeepChirp/copyright-space
在`©`后添加空格
2025-07-23 15:21:23 +08:00
DeepChirp
dd8a18e8f4 chore: clean up unnecessary comments 2025-07-15 13:41:26 +08:00
Jerry
a61e216452 fix: 修正 abcjs 報錯的 bug 2025-07-09 13:24:42 +08:00
DeepChirp
b89f165be3 chore: add a space after © 2025-07-07 13:14:38 +08:00
82 changed files with 3815 additions and 1725 deletions

2
.github/FUNDING.yml vendored
View File

@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk','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']

226
README.md
View File

@@ -1,117 +1,193 @@
<div align="right">
<a title="Chinese" href="/README_CN.md">中文</a>
<a title="中文" href="/README_CN.md">中文</a>
</div>
<div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
# hexo-theme-butterfly
A modern, elegant and feature-rich theme for Hexo
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/master?color=%231ab1ad&label=master)
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83c)
![dev version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![npm version](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83cd)
![license](https://img.shields.io/github/license/jerryc127/hexo-theme-butterfly?color=FF5531)
![GitHub stars](https://img.shields.io/github/stars/jerryc127/hexo-theme-butterfly?style=social)
📢 Demo: [Butterfly](https://butterfly.js.org/) / [CrazyWong](https://blog.crazywong.com/)
📢 **Demo**: [Butterfly Official](https://butterfly.js.org/) | [CrazyWong's Blog](https://blog.crazywong.com/)
📖 Docs: [English](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) / [Chinese](https://butterfly.js.org/posts/21cfbf15/)
📖 **Documentation**: [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) | [中文文档](https://butterfly.js.org/posts/21cfbf15/)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
![Butterfly Theme Preview](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div>
---
## 💻 Installation
## 🚀 Quick Start
### GIT
### 💾 Installation
> If you are in Mainland China, you can download in [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git)
#### Method 1: Git Installation (Recommended)
Stable branch [recommend]:
> 💡 **Tip**: If GitHub access is slow in mainland China, you can use the [Gitee Mirror](https://gitee.com/immyw/hexo-theme-butterfly.git)
```
Execute in your Hexo blog root directory:
```bash
# Install stable version (recommended)
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
Dev branch:
```
```bash
# Install development version (early access to new features)
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
### NPM
#### Method 2: NPM Installation
> It supports Hexo 5.0.0 or later
> ⚠️ **Note**: NPM installation only supports Hexo 5.0.0 and above
In Hexo site root directory
```powershell
npm i hexo-theme-butterfly
```bash
npm install hexo-theme-butterfly
```
## ⚙ Configuration
### Theme Configuration
Set theme in the hexo work folder's root config file `_config.yml`:
1. **Enable Theme**: Modify your Hexo configuration file `_config.yml`:
> theme: butterfly
```yaml
theme: butterfly
```
If you don't have pug & stylus renderer, try this:
2. **Install Dependencies**: If you haven't installed pug and stylus renderers, please run:
> npm install hexo-renderer-pug hexo-renderer-stylus
```bash
npm install hexo-renderer-pug hexo-renderer-stylus --save
```
## 🎉 Features
## ✨ Theme Features
- [x] Card UI Design
- [x] Rounded Design/Squared Design
- [X] Support sub-menu
- [x] Two-column layout
- [x] Responsive Web Design
- [x] Dark Mode
- [x] Pjax
- [x] Read Mode
- [x] Conversion between Traditional and Simplified Chinese
- [X] TOC catalog is available for both computers and mobile phones
- [X] Built-in Syntax Highlighting Themes (darker/pale night/light/ocean), also support customization
- [X] Code Blocks (Display code language/close or expand Code Blocks/Copy Button/word wrap)
- [X] Disable copy/Add a Copyright Notice to the Copied Text
- [X] Search (Algolia Search/Local Search)
- [x] Mathjax and Katex
- [x] Built-in 404 page
- [x] WordCount
- [x] Related articles
- [x] Displays outdated notice for a post
- [x] Share (Sharejs/Addtoany)
- [X] Comment (Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk)
- [x] Multiple Comment System Support
- [x] Online Chats (Chatra/Tidio/Crisp)
- [x] Web analytics
- [x] Google AdSense
- [x] Webmaster Verification
- [x] Change website colour scheme
- [x] Typewriter Effect: activate_power_mode
- [x] Background effects (Canvas ribbon/canvas_ribbon_piao/canvas_nest)
- [x] Mouse click effects (Fireworks/Heart/Text)
- [x] Preloader/Loading Animation/pace.js
- [x] Busuanzi visitor counter
- [x] Medium Zoom/Fancybox
- [x] Mermaid
- [x] Chart.js
- [x] Justified Gallery
- [x] Lazyload images
- [x] Instantpage/Snackbar notification toast/PWA......
### 🎨 Design Style
- [x] **Card-based Design** - Modern card-style layout
- [x] **Rounded/Square Design** - Customizable border styles
- [x] **Responsive Design** - Perfect adaptation to all screen sizes
- [x] **Two-column Layout** - Optimized reading experience
- [x] **Dark Mode** - Eye-friendly night mode
## Contributors
### 📝 Content Features
- [x] **Multi-level Menu** - Support for secondary navigation menus
- [x] **Reading Mode** - Focused article reading experience
- [x] **TOC Navigation** - Desktop and mobile TOC support
- [x] **Word Count** - Display article word count and reading time
- [x] **Related Articles** - Smart recommendation of related content
- [x] **Outdated Reminder** - Automatic article update status alerts
- [x] **Traditional/Simplified Chinese** - Support for Traditional and Simplified Chinese switching
- [x] **Tag Plugins** - Rich tag plugin support
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
</a>
### 🔍 Search & Navigation
- [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch
- [x] **Built-in 404** - Beautiful 404 error page
- [x] **Pjax Support** - Smooth page transition experience
## 📷 Screenshots
### 🎨 Code Display
- [x] **Syntax Highlighting** - Built-in multiple themes (darker/pale night/light/ocean)
- [x] **Code Features** - Language display/fold expand/copy button/auto-wrap
- [x] **Math Formulas** - Support for Mathjax and Katex
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg)
### 💬 Social Interaction
- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
- [x] **Dual Comments Support** - Enable two comment systems simultaneously
- [x] **Share Features** - Sharejs/Addtoany sharing components
- [x] **Live Chat** - Chatra/Tidio/Crisp instant messaging
### 📊 Analytics & Statistics
- [x] **Visit Statistics** - Busuanzi counter
- [x] **Site Analytics** - Google Analytics/Baidu Analytics/Cloudflare Analytics/Microsoft Clarity/Umami
- [x] **Webmaster Verification** - Major search engine verification
- [x] **Ad Support** - Google AdSense/custom ad slots
### 🎪 Visual Effects
- [x] **Typing Effects** - activate_power_mode animations
- [x] **Background Effects** - Static ribbons/dynamic ribbons/floating ribbons/Canvas Nest
- [x] **Mouse Effects** - Fireworks/hearts/text click effects
- [x] **Loading Animations** - Preloader and pace.js progress bars
- [x] **Image Effects** - Medium Zoom/Fancybox image lightbox
- [x] **Lazy Loading** - Image lazy loading optimization
### 🛠️ Advanced Features
- [x] **PWA Support** - Progressive Web App
- [x] **Copy Protection** - Disable text copying/copyright info append
- [x] **Theme Customization** - Custom site color schemes
- [x] **Chart Support** - Mermaid flowcharts/Chart.js data charts
- [x] **Music Notation** - ABCJS music notation support
- [x] **Music Player** - APlayer/Meting music playback
- [x] **Article Series** - Series article organization
- [x] **Instantpage** - Page preloading acceleration
- [x] **Snackbar** - Elegant notification messages
## 🤝 Contributors
Thanks to all the developers who have contributed to the Butterfly theme!
[![Contributors](https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly)](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
## 📸 Screenshots
<div align="center">
![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg)
![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg)
![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg)
![Theme Demo](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg)
</div>
## ⭐ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=jerryc127/hexo-theme-butterfly&type=Date)](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
## 🤝 Building a Better Theme Together
We believe **the power of open source comes from everyone's participation**! Whether you're a developer, designer, or user, you can contribute to the development of the Butterfly theme.
### 💬 Get Help & Support
- 🐛 **Found a bug?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - Let's solve it together!
- 💡 **Have ideas?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - Share your creative ideas!
- 📚 **Learning to use?** → [Official Documentation](https://butterfly.js.org/) - Detailed usage guide
- 💬 **Real-time discussion?** → [Telegram Group](https://t.me/bu2fly) - Chat with community members
### 🎯 Contributing
Want to make Butterfly better? We welcome any form of contribution:
- **🔧 Code Contributions** - Fix bugs, add new features, optimize performance
- **📝 Documentation** - Improve docs, translate content, write tutorials
- **🎨 Design Suggestions** - UI/UX improvements, theme colors, icon design
- **🧪 Testing & Feedback** - Test new features, report issues, provide user experience
- **💰 Financial Support** - [Sponsor the Project](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - Support long-term development
## 📄 License
This project is licensed under the [Apache 2.0](LICENSE) License.
## 🙏 Acknowledgments
This theme is developed based on [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody). Thanks to the original author for their excellent work that provided inspiration and foundation!
Thanks to all friends who have contributed to the development of the Butterfly theme. Your support has made this theme continuously improve and progress.
---
<div align="center">
**✨ If this theme helps you, please give us a ⭐ Star! ✨**
</div>

View File

@@ -4,114 +4,190 @@
<div align="center">
<img src="./source/img/butterfly-icon.png" width="150" height="150" />
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
# hexo-theme-butterfly
一個適用於 Hexo 的現代化、美觀且功能豐富的主題
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/master?color=%231ab1ad&label=master)
![master version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83c)
![dev version](https://img.shields.io/github/package-json/v/jerryc127/hexo-theme-butterfly/dev?label=dev)
![npm version](https://img.shields.io/npm/v/hexo-theme-butterfly?color=%09%23bf00ff)
![hexo version](https://img.shields.io/badge/hexo-5.3.0+-0e83cd)
![license](https://img.shields.io/github/license/jerryc127/hexo-theme-butterfly?color=FF5531)
![GitHub stars](https://img.shields.io/github/stars/jerryc127/hexo-theme-butterfly?style=social)
📢 預覽: [Butterfly](https://butterfly.js.org/) / [CrazyWong](https://blog.crazywong.com/)
📢 **在線預覽**: [Butterfly 官方](https://butterfly.js.org/) | [CrazyWong 博客](https://blog.crazywong.com/)
📖 文檔: [中文](https://butterfly.js.org/posts/21cfbf15/) / [English](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/)
📖 **完整文檔**: [中文文檔](https://butterfly.js.org/posts/21cfbf15/) | [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
![Butterfly 主題預覽](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/theme-butterfly-readme.png)
</div>
---
## 💻 安裝
## 🚀 快速開始
### Git 安裝
### 💾 安裝方式
> 本倉庫同時上傳到 [Gitee](https://gitee.com/immyw/hexo-theme-butterfly.git),如果你訪問 Github 緩慢,可從 Gitee 中下載。
#### 方式一Git 安裝(推薦)
在博客根目錄裡安裝穩定版【推薦】
> 💡 **提示**: 如果您在中國大陸訪問 GitHub 速度較慢,可以使用 [Gitee 鏡像](https://gitee.com/immyw/hexo-theme-butterfly.git)
```powershell
在您的 Hexo 博客根目錄下執行:
```bash
# 安裝穩定版本(推薦)
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
如果想要安裝比較新的dev分支可以
```powershell
```bash
# 安裝開發版本(搶先體驗新功能)
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
```
### npm 安裝
#### 方式二NPM 安裝
> 此方法只支持Hexo 5.0.0以上版本
> ⚠️ **注意**: NPM 安裝方式僅支援 Hexo 5.0.0以上版本
在博客根目錄裡
```powershell
npm i hexo-theme-butterfly
```bash
npm install hexo-theme-butterfly
```
## ⚙ 應用主題
### 主題配置
修改hexo配置文件`_config.yml`,把主題改為`Butterfly`
1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`
```
```yaml
theme: butterfly
```
>如果你沒有pug以及stylus渲染器,請下載安裝: npm install hexo-renderer-pug hexo-renderer-stylus --save
2. **安裝依賴**: 如果您尚未安裝 pug 和 stylus 渲染器,請執行:
## 🎉 特色
```bash
npm install hexo-renderer-pug hexo-renderer-stylus --save
```
- [x] 卡片化設計
- [x] 圓角化設計/直角化設計
- [X] 支持二級目錄
- [x] 雙欄設計
- [x] 響應式主題
- [x] 夜間模式
- [x] Pjax
- [x] 文章閲讀模式
- [x] 簡體和繁體轉換
- [X] 電腦和手機都可查看TOC目錄
- [X] 內置多種代碼配色darker/pale night/light/ocean可自定義代碼配色
- [X] 代碼塊顯示代碼語言/關閉或展開代碼塊/代碼複製/代碼自動換行
- [X] 可關閉文字複製/可開啟內容複製增加版權信息)
- [X] 兩種搜索( Algolia 搜索和本地搜索)
- [x] Mathjax 和 Katex
- [x] 內置404頁面
- [x] 顯示字數統計
- [x] 顯示相關文章
- [x] 過期文章提醒
- [x] 多種分享系統Sharejs/Addtoany
- [X] 多種評論系統Disqus/Disqusjs/Livere/Gitalk/Valine/Waline/Utterances/Facebook Comments/Twikoo/Giscus/Remark42/artalk
- [x] 支持雙評論部署
- [x] 多種在線聊天Chatra/Tidio/Crisp
- [x] 多種分析系統
- [x] 谷歌廣告/手動廣告位置
- [x] 各種站長驗證
- [x] 修改網站配色
- [x] 打字特效 activate_power_mode
- [x] 多種背景特效(靜止彩帶/動態彩帶/Canvas Nest
- [x] 多種鼠標點擊特效(煙花/文字/愛心)
- [x] 內置一種 Preloader 加載動畫和 pace.js 加載動畫條
- [x] 不蒜子訪問統計
- [x] 兩種大圖模式Medium Zoom/Fancybox
- [x] Mermaid 圖表顯示
- [x] Chart.js 圖表顯示
- [x] 照片牆
- [x] 圖片懶加載
- [x] Instantpage/Snackbar彈窗/PWA......
## ✨ 主題特色
## ✨ 貢獻者
### 🎨 設計風格
- [x] **卡片化設計** - 現代化的卡片式佈局
- [x] **圓角/直角設計** - 支援自訂邊框樣式
- [x] **響應式設計** - 完美適配各種螢幕尺寸
- [x] **雙欄佈局** - 優化的閱讀體驗
- [x] **深色模式** - 護眼的夜間模式
<a href="https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly" />
</a>
### 📝 內容功能
- [x] **多級選單** - 支援二級導航選單
- [x] **閱讀模式** - 專注的文章閱讀體驗
- [x] **目錄導航** - 電腦和手機雙端支援 TOC
- [x] **字數統計** - 顯示文章字數和閱讀時間
- [x] **相關文章** - 智能推薦相關內容
- [x] **過期提醒** - 自動提示文章更新狀態
- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換
- [x] **標籤外掛** - 豐富的標籤外掛支持
## 📷 截圖
### 🔍 搜尋與導航
- [x] **多種搜尋** - Algolia 搜尋 / 本地搜尋 / Docsearch
- [x] **內建 404** - 美觀的 404 錯誤頁面
- [x] **Pjax 支援** - 流暢的頁面切換體驗
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg)
![](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg)
### 🎨 程式碼展示
- [x] **語法高亮** - 內建多種主題darker/pale night/light/ocean
- [x] **程式碼功能** - 語言顯示/摺疊展開/複製按鈕/自動換行
- [x] **數學公式** - 支援 Mathjax 和 Katex
### 💬 社交互動
- [x] **多元評論系統** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk 等
- [x] **雙評論支援** - 可同時啟用兩套評論系統
- [x] **分享功能** - Sharejs/Addtoany 分享套件
- [x] **線上客服** - Chatra/Tidio/Crisp 即時聊天
### 📊 數據分析
- [x] **訪問統計** - 不蒜子計數器
- [x] **網站分析** - Google Analytics/百度統計/Cloudflare Analytics/Microsoft Clarity/Umami
- [x] **站長驗證** - 各大搜尋引擎驗證
- [x] **廣告支援** - Google AdSense/自訂廣告位
### 🎪 視覺效果
- [x] **打字特效** - activate_power_mode 動畫
- [x] **背景特效** - 靜態彩帶/動態彩帶/飄帶效果/Canvas Nest
- [x] **滑鼠特效** - 煙花/愛心/文字點擊效果
- [x] **載入動畫** - Preloader 和 pace.js 進度條
- [x] **圖片效果** - Medium Zoom/Fancybox 圖片燈箱
- [x] **懶載入** - 圖片延遲載入優化
### 🛠️ 進階功能
- [x] **PWA 支援** - 漸進式網頁應用
- [x] **複製保護** - 可關閉文字複製/版權資訊追加
- [x] **主題定製** - 自訂網站配色方案
- [x] **圖表支援** - Mermaid 流程圖/Chart.js 數據圖表
- [x] **音樂符號** - ABCJS 音樂記譜法支援
- [x] **音樂播放器** - APlayer/Meting 音樂播放功能
- [x] **系列文章** - 系列文章組織功能
- [x] **Instantpage** - 頁面預載入加速
- [x] **Snackbar** - 優雅的提示訊息
## 🤝 貢獻者
感謝所有為 Butterfly 主題做出貢獻的開發者們!
[![Contributors](https://contrib.rocks/image?repo=jerryc127/hexo-theme-butterfly)](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
## 📸 主題截圖
<div align="center">
![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-1.jpg)
![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-2.jpg)
![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-3.jpg)
![主題展示](https://cdn.jsdelivr.net/gh/jerryc127/CDN@m2/img/butterfly-readme-screenshots-4.jpg)
</div>
## ⭐ Star 趨勢
[![Star History Chart](https://api.star-history.com/svg?repos=jerryc127/hexo-theme-butterfly&type=Date)](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
## 🤝 一起構建更美好的主題
我們相信,**開源的力量來自於每一個人的參與**!無論您是開發者、設計師還是用戶,都可以為 Butterfly 主題的發展貢獻力量。
### 💬 獲取幫助與支援
- 🐛 **發現問題?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - 讓我們一起解決!
- 💡 **有好想法?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - 分享您的創意想法!
- 📚 **學習使用?** → [官方文檔](https://butterfly.js.org/) - 詳細的使用指南
- 💬 **即時討論?** → [Telegram 群組](https://t.me/bu2fly) - 與社群成員實時交流
### 🎯 參與貢獻
想要讓 Butterfly 變得更好嗎?我們歡迎您的任何形式的貢獻:
- **🔧 代碼貢獻** - 修復 Bug、添加新功能、優化性能
- **📝 文檔完善** - 改進文檔、翻譯內容、撰寫教程
- **🎨 設計建議** - UI/UX 改進、主題配色、圖示設計
- **🧪 測試反饋** - 測試新功能、回報問題、提供使用體驗
- **💰 資金支援** - [贊助項目](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - 支持長期發展
## 📄 授權條款
本專案採用 [Apache 2.0](LICENSE) 授權條款。
## 🙏 致敬與感謝
本主題基於 [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody) 進行開發,感謝原作者的精彩創作為我們提供了靈感與基礎!
感謝所有為 Butterfly 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。
---
<div align="center">
**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star✨**
</div>

View File

@@ -91,7 +91,8 @@ category_per_img:
footer_img: false
# Website Background
# Can set it to color or image url
# Can set it to color, image URL or an array containing colors and/or image URLs
# If an array is provided, a random background will be selected from the array on each load
background:
cover:
@@ -318,6 +319,7 @@ aside:
# If set 0 will show all
limit: 40
color: false
custom_colors:
# Order of tags, random/name/length
orderby: random
# Sort of order. 1, asc for ascending; -1, desc for descending
@@ -488,6 +490,11 @@ search:
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Enable pagination for search results
pagination:
enable: false
# Number of search results per page
hitsPerPage: 8
CDN:
# Docsearch
@@ -510,12 +517,12 @@ share:
# Share.js
# https://github.com/overtrue/share.js
sharejs:
sites: facebook,twitter,wechat,weibo,qq
sites: facebook,x,wechat,weibo,qq
# AddToAny
# https://www.addtoany.com/
addtoany:
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link
item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
# --------------------------------------
# Comments System
@@ -694,6 +701,7 @@ umami_analytics:
enable: false
# For self-hosted setups, configure the hostname of the Umami instance
serverURL:
script_name: script.js
website_id:
option:
UV_PV:
@@ -932,6 +940,10 @@ mermaid:
theme:
light: default
dark: dark
# Enable "Open in New Tab" button to view diagram in a separate window
open_in_new_tab: true
# Enable zoom and pan interactions on diagrams
zoom_pan: true
# chartjs
# see https://www.chartjs.org/docs/latest/
@@ -1033,7 +1045,11 @@ Open_Graph_meta:
# Structured Data
# https://developers.google.com/search/docs/guides/intro-structured-data
structured_data: true
structured_data:
enable: false
# Alternate name for the site, used in structured data
# Format: ['name1', 'name2']
alternate_name:
# Add the vendor prefixes to ensure compatibility
css_prefix: true
@@ -1057,7 +1073,7 @@ CDN:
third_party_provider: jsdelivr
# Add version number to url, true or false
version: false
version: true
# Custom format
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}
@@ -1093,7 +1109,6 @@ CDN:
# gitalk_css:
# giscus:
# instantpage:
# instantsearch:
# katex:
# katex_copytex:
# lazyload:

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: Previous
next: Next
page_info: 'Page ${current} of ${total}'
comment: Comments

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: Previous
next: Next
page_info: 'Page ${current} of ${total}'
comment: Comments

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: 前へ
next: 次へ
page_info: '${current} ページ / 合計 ${total} ページ'
comment: コメント

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: 이전
next: 다음
page_info: '${current} 페이지 / 총 ${total} 페이지'
comment: 댓글

View File

@@ -49,6 +49,7 @@ search:
pagination:
prev: 上一篇
next: 下一篇
page_info: '第 ${current} 页 / 共 ${total} 页'
comment: 评论

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: 上一頁
next: 下一頁
page_info: '第 ${current} 頁 / 共 ${total} 頁'
comment: 評論

View File

@@ -48,6 +48,7 @@ search:
pagination:
prev: 上一篇
next: 下一篇
page_info: '第 ${current} 頁 / 共 ${total} 頁'
comment: 評論

View File

@@ -52,7 +52,7 @@ div
!= partial("includes/third-party/umami_analytics", {}, { cache: true })
if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv
script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
script(async data-pjax src=theme.asset.busuanzi ? url_for(theme.asset.busuanzi) : '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js')
!= partial('includes/third-party/search/index', {}, { cache: true })

View File

@@ -22,9 +22,9 @@ if nav
- const sinceYear = owner.since
span.copyright
if sinceYear && sinceYear != currentYear
!= `&copy;${sinceYear} - ${currentYear} By ${config.author}`
!= `&copy;&nbsp;${sinceYear} - ${currentYear} By ${config.author}`
else
!= `&copy;${currentYear} By ${config.author}`
!= `&copy;&nbsp;${currentYear} By ${config.author}`
if copyright.enable
- const v = copyright.version ? getVersion() : false
span.framework-info

View File

@@ -10,7 +10,6 @@
hitsPerPage: theme.search.algolia_search.hitsPerPage,
// search languages
languages: {
input_placeholder: theme.search.placeholder || _p("search.input_placeholder"),
hits_empty: _p("search.algolia_search.hits_empty"),
hits_stats: _p("search.algolia_search.hits_stats"),
}
@@ -19,12 +18,16 @@
let localSearch = 'undefined'
if (theme.search.use === 'local_search') {
const { CDN, preload, top_n_per_article, unescape } = theme.search.local_search
const { CDN, preload, top_n_per_article, pagination, unescape } = theme.search.local_search
localSearch = JSON.stringify({
path: CDN || config.root + config.search.path,
preload,
top_n_per_article,
unescape,
pagination: {
enable: pagination.enable,
hitsPerPage: pagination.hitsPerPage
},
languages: {
// search languages
hits_empty: _p("search.local_search.hits_empty"),
@@ -69,20 +72,16 @@
})
}
let highlight = 'undefined'
let syntaxHighlighter = config.syntax_highlighter
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
if (highlightEnable) {
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
highlight = JSON.stringify({
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
let highlight = JSON.stringify({
plugin: highlightProvider,
highlightCopy: copy,
highlightLang: language,
highlightHeightLimit: height_limit,
highlightFullpage: fullpage,
highlightMacStyle: macStyle
})
}
script.
const GLOBAL_CONFIG = {

View File

@@ -40,10 +40,22 @@ if theme.structured_data
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('/'),
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
mixin indexPostUI()
- const indexLayout = theme.index_layout
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : ''
- const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
.recent-post-items
each article, index in page.posts.data
@@ -8,17 +8,17 @@ mixin indexPostUI()
- const link = article.link || article.path
- const title = article.title || _p('no_title')
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
- const post_cover = article.cover
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
- const postCover = article.cover
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
if post_cover && theme.cover.index_enable
if postCover && theme.cover.index_enable
.post_cover(class=leftOrRight)
a(href=url_for(link) title=title)
if article.cover_type === 'img'
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
else
div.post-bg(style=`background: ${post_cover}`)
.recent-post-info(class=no_cover)
div.post-bg(style=`background: ${postCover}`)
.recent-post-info(class=noCover)
a.article-title(href=url_for(link) title=title)
if globalPageType === 'home' && (article.top || article.sticky > 0)
i.fas.fa-thumbtack.sticky
@@ -35,13 +35,13 @@ mixin indexPostUI()
span.article-meta-label=_p('post.updated')
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
else
- const data_type_updated = theme.post_meta.page.date_type === 'updated'
- const date_type = data_type_updated ? 'updated' : 'date'
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
i(class=date_icon)
span.article-meta-label= date_title
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format)
- const isUpdatedType = theme.post_meta.page.date_type === 'updated'
- const dateType = isUpdatedType ? 'updated' : 'date'
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
i(class=dateIcon)
span.article-meta-label= dateTitle
time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format)
if theme.post_meta.page.categories && article.categories.data.length > 0
span.article-meta
span.article-meta-separator |
@@ -69,7 +69,10 @@ mixin indexPostUI()
span.article-meta-label= ' ' + _p('card_post_count')
if theme.comments.card_post_count && theme.comments.use
case theme.comments.use[0]
- const commentSystem = theme.comments.use[0]
- const commentLink = url_for(link) + '#post-comment'
case commentSystem
when 'Disqus'
when 'Disqusjs'
+countBlockInIndex
@@ -77,30 +80,30 @@ mixin indexPostUI()
i.fa-solid.fa-spinner.fa-spin
when 'Valine'
+countBlockInIndex
a(href=url_for(link) + '#post-comment')
a(href=commentLink)
span.valine-comment-count(data-xid=url_for(link))
i.fa-solid.fa-spinner.fa-spin
when 'Waline'
+countBlockInIndex
a(href=url_for(link) + '#post-comment')
a(href=commentLink)
span.waline-comment-count(data-path=url_for(link))
i.fa-solid.fa-spinner.fa-spin
when 'Twikoo'
+countBlockInIndex
a.twikoo-count(href=url_for(link) + '#post-comment')
a.twikoo-count(href=commentLink)
i.fa-solid.fa-spinner.fa-spin
when 'Facebook Comments'
+countBlockInIndex
a(href=url_for(link) + '#post-comment')
a(href=commentLink)
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
when 'Remark42'
+countBlockInIndex
a(href=url_for(link) + '#post-comment')
a(href=commentLink)
span.remark42__counter(data-url=urlNoIndex(article.permalink))
i.fa-solid.fa-spinner.fa-spin
when 'Artalk'
+countBlockInIndex
a(href=url_for(link) + '#post-comment')
a(href=commentLink)
span.artalk-count(data-page-key=url_for(link))
i.fa-solid.fa-spinner.fa-spin

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ if page.total !== 1
a.pagination-related(class=className href=url_for(direction.path) title=direction.title)
if direction.cover_type === 'img'
img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
img.cover(src=url_for(direction.pagination_cover || direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`)
else
.cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`)

View File

@@ -1,5 +1,5 @@
script.
(() = {
(() => {
const abcjsInit = () => {
const abcjsFn = () => {
setTimeout(() => {
@@ -24,7 +24,7 @@ script.
const options = { ...params, responsive: "resize" }
// Render the music score using ABCJS.renderAbc
ABCJS.renderAbc(ele, ele.innerHTML, options)
ABCJS.renderAbc(ele, ele.textContent, options)
}
}, 100)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,292 @@
script.
(() => {
const parseViewBox = viewBox => {
if (!viewBox) return null
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
return parts
}
const getSvgViewBox = svg => {
const attr = parseViewBox(svg.getAttribute('viewBox'))
if (attr) return attr
// Fallback: use bbox to build a viewBox
try {
const bbox = svg.getBBox()
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
} catch (e) {
// getBBox may fail on some edge cases; ignore
}
const w = Number(svg.getAttribute('width')) || 0
const h = Number(svg.getAttribute('height')) || 0
if (w > 0 && h > 0) return [0, 0, w, h]
return [0, 0, 100, 100]
}
const setSvgViewBox = (svg, vb) => {
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
}
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
const openSvgInNewTab = ({ source, initViewBox }) => {
const getClonedSvg = () => {
if (typeof source === 'string') {
const template = document.createElement('template')
template.innerHTML = source.trim()
const svg = template.content.querySelector('svg')
return svg ? svg.cloneNode(true) : null
}
if (source && typeof source.cloneNode === 'function') {
return source.cloneNode(true)
}
return null
}
const clone = getClonedSvg()
if (!clone) return
if (initViewBox && initViewBox.length === 4) {
clone.setAttribute('viewBox', initViewBox.join(' '))
}
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
if (!clone.getAttribute('xmlns:xlink') && clone.outerHTML.includes('xlink:')) {
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
}
// inject background to match current theme
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff')
if (!clone.style.background) clone.style.background = bg
const serializer = new XMLSerializer()
const svgSource = serializer.serializeToString(clone)
const htmlSource = `<!doctype html><html><head><meta charset="utf-8" />
<style>
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
</style>
</head><body>${svgSource}</body></html>`
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener')
setTimeout(() => URL.revokeObjectURL(url), 30000)
}
const attachMermaidViewerButton = wrap => {
let btn = wrap.querySelector('.mermaid-open-btn')
if (!btn) {
btn = document.createElement('button')
btn.type = 'button'
btn.className = 'mermaid-open-btn'
wrap.appendChild(btn)
}
btn.innerHTML = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
if (!btn.__mermaidViewerBound) {
btn.addEventListener('click', e => {
e.preventDefault()
e.stopPropagation()
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
if (!svg) return
const initViewBox = wrap.__mermaidInitViewBox
if (typeof svg === 'string') {
openSvgInNewTab({ source: svg, initViewBox })
return
}
openSvgInNewTab({ source: svg, initViewBox })
})
btn.__mermaidViewerBound = true
}
}
// Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates)
const zoomAtPoint = (vb, factor, px, py) => {
const w = vb[2] * factor
const h = vb[3] * factor
const nx = px - (px - vb[0]) * factor
const ny = py - (py - vb[1]) * factor
return [nx, ny, w, h]
}
const initMermaidGestures = wrap => {
const svg = wrap.querySelector('svg')
if (!svg) return
// Ensure viewBox exists so gestures always work
const initVb = getSvgViewBox(svg)
wrap.__mermaidInitViewBox = initVb
wrap.__mermaidCurViewBox = initVb.slice()
setSvgViewBox(svg, initVb)
// Avoid binding multiple times on themeChange/pjax
if (wrap.__mermaidGestureBound) return
wrap.__mermaidGestureBound = true
// Helper: map client (viewport) coordinate -> viewBox coordinate
const clientToViewBox = (clientX, clientY) => {
const rect = svg.getBoundingClientRect()
const vb = wrap.__mermaidCurViewBox || getSvgViewBox(svg)
const x = vb[0] + (clientX - rect.left) * (vb[2] / rect.width)
const y = vb[1] + (clientY - rect.top) * (vb[3] / rect.height)
return { x, y, rect, vb }
}
const state = {
pointers: new Map(),
startVb: null,
startDist: 0,
startCenter: null
}
const clampVb = vb => {
const init = wrap.__mermaidInitViewBox || vb
const minW = init[2] * 0.1
const maxW = init[2] * 10
const minH = init[3] * 0.1
const maxH = init[3] * 10
vb[2] = clamp(vb[2], minW, maxW)
vb[3] = clamp(vb[3], minH, maxH)
return vb
}
const setCurVb = vb => {
vb = clampVb(vb)
wrap.__mermaidCurViewBox = vb
setSvgViewBox(svg, vb)
}
const onPointerDown = e => {
// Allow only primary button for mouse
if (e.pointerType === 'mouse' && e.button !== 0) return
svg.setPointerCapture(e.pointerId)
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
if (state.pointers.size === 1) {
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
} else if (state.pointers.size === 2) {
const pts = [...state.pointers.values()]
const dx = pts[0].x - pts[1].x
const dy = pts[0].y - pts[1].y
state.startDist = Math.hypot(dx, dy)
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
state.startCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 }
}
}
const onPointerMove = e => {
if (!state.pointers.has(e.pointerId)) return
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
// Pan with 1 pointer
if (state.pointers.size === 1 && state.startVb) {
const p = [...state.pointers.values()][0]
const prev = { x: e.clientX - e.movementX, y: e.clientY - e.movementY }
// movementX/Y unreliable on touch, compute from stored last position
const last = wrap.__mermaidLastSinglePointer || p
const dxClient = p.x - last.x
const dyClient = p.y - last.y
wrap.__mermaidLastSinglePointer = p
const { rect } = clientToViewBox(p.x, p.y)
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
const dx = dxClient * (vb[2] / rect.width)
const dy = dyClient * (vb[3] / rect.height)
setCurVb([vb[0] - dx, vb[1] - dy, vb[2], vb[3]])
return
}
// Pinch zoom with 2 pointers
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
const pts = [...state.pointers.values()]
const dx = pts[0].x - pts[1].x
const dy = pts[0].y - pts[1].y
const dist = Math.hypot(dx, dy)
if (!dist) return
const factor = state.startDist / dist // dist bigger => zoom in (viewBox smaller)
const cx = (pts[0].x + pts[1].x) / 2
const cy = (pts[0].y + pts[1].y) / 2
const centerClient = { x: cx, y: cy }
const pxy = clientToViewBox(centerClient.x, centerClient.y)
const cpx = pxy.x
const cpy = pxy.y
const vb = zoomAtPoint(state.startVb, factor, cpx, cpy)
setCurVb(vb)
}
}
const onPointerUpOrCancel = e => {
state.pointers.delete(e.pointerId)
if (state.pointers.size === 0) {
state.startVb = null
state.startDist = 0
state.startCenter = null
wrap.__mermaidLastSinglePointer = null
} else if (state.pointers.size === 1) {
// reset single pointer baseline to avoid jump
wrap.__mermaidLastSinglePointer = [...state.pointers.values()][0]
}
}
// Wheel zoom (mouse/trackpad)
const onWheel = e => {
// ctrlKey on mac trackpad pinch; we treat both as zoom
e.preventDefault()
const delta = e.deltaY
const zoomFactor = delta > 0 ? 1.1 : 0.9
const { x, y } = clientToViewBox(e.clientX, e.clientY)
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
setCurVb(zoomAtPoint(vb, zoomFactor, x, y))
}
const onDblClick = () => {
const init = wrap.__mermaidInitViewBox
if (!init) return
wrap.__mermaidCurViewBox = init.slice()
setSvgViewBox(svg, init)
}
svg.addEventListener('pointerdown', onPointerDown)
svg.addEventListener('pointermove', onPointerMove)
svg.addEventListener('pointerup', onPointerUpOrCancel)
svg.addEventListener('pointercancel', onPointerUpOrCancel)
svg.addEventListener('wheel', onWheel, { passive: false })
svg.addEventListener('dblclick', onDblClick)
}
const runMermaid = ele => {
window.loadMermaid = true
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
ele.forEach((item, index) => {
const mermaidSrc = item.firstElementChild
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
// Clear old render (themeChange/pjax will rerun)
const oldSvg = item.querySelector('svg')
if (oldSvg) oldSvg.remove()
item.__mermaidGestureBound = false
const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
if (!config.theme) {
config.theme = theme
}
const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`
const mermaidID = `mermaid-${index}`
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
const renderMermaid = svg => {
mermaidSrc.insertAdjacentHTML('afterend', svg)
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
item.__mermaidOriginalSvg = svg
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
}
// mermaid v9 and v10 compatibility
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
i.fas.fa-tags
span= _p('aside.card_tags')
- let { limit, orderby, order } = theme.aside.card_tags
- let { limit, orderby, order, custom_colors } = theme.aside.card_tags
- limit = limit === 0 ? 0 : limit || 40
if theme.aside.card_tags.color
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em'})
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
else
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})

View File

@@ -1,6 +1,6 @@
{
"name": "hexo-theme-butterfly",
"version": "5.4.1",
"version": "5.5.4",
"description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json",
"scripts": {
@@ -25,8 +25,8 @@
"dependencies": {
"hexo-renderer-pug": "^3.0.0",
"hexo-renderer-stylus": "^3.0.1",
"hexo-util": "^3.3.0",
"moment-timezone": "^0.5.48"
"hexo-util": "^4.0.0",
"moment-timezone": "^0.6.0"
},
"homepage": "https://butterfly.js.org/",
"author": "Jerry <my@crazywong.com>",

View File

@@ -1,15 +1,15 @@
abcjs_basic_js:
name: abcjs
file: dist/abcjs-basic-min.js
version: 6.5.1
version: 6.6.0
activate_power_mode:
name: butterfly-extsrc
file: dist/activate-power-mode.min.js
version: 1.1.4
version: 1.1.6
algolia_search:
name: algoliasearch
file: dist/lite/builds/browser.umd.js
version: 5.30.0
version: 5.47.0
aplayer_css:
name: aplayer
file: dist/APlayer.min.css
@@ -33,69 +33,69 @@ blueimp_md5:
canvas_fluttering_ribbon:
name: butterfly-extsrc
file: dist/canvas-fluttering-ribbon.min.js
version: 1.1.4
version: 1.1.6
canvas_nest:
name: butterfly-extsrc
file: dist/canvas-nest.min.js
version: 1.1.4
version: 1.1.6
canvas_ribbon:
name: butterfly-extsrc
file: dist/canvas-ribbon.min.js
version: 1.1.4
version: 1.1.6
chartjs:
name: chart.js
file: dist/chart.umd.js
version: 4.5.0
version: 4.5.1
clickShowText:
name: butterfly-extsrc
file: dist/click-show-text.min.js
version: 1.1.4
version: 1.1.6
click_heart:
name: butterfly-extsrc
file: dist/click-heart.min.js
version: 1.1.4
version: 1.1.6
disqusjs:
name: disqusjs
file: dist/browser/disqusjs.es2015.umd.min.js
version: 3.1.0
version: 3.2.1
disqusjs_css:
name: disqusjs
file: dist/browser/styles/disqusjs.css
version: 3.1.0
version: 3.2.1
docsearch_css:
name: '@docsearch/css'
other_name: docsearch-css
file: dist/style.css
version: 3.9.0
version: 4.5.3
docsearch_js:
name: '@docsearch/js'
other_name: docsearch-js
file: dist/umd/index.js
version: 3.9.0
version: 4.5.3
egjs_infinitegrid:
name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid
file: dist/infinitegrid.min.js
version: 4.12.0
version: 4.13.0
fancybox:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js
version: 6.0.7
version: 6.1.9
other_name: fancyapps-ui
fancybox_css:
name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css
version: 6.0.7
version: 6.1.9
other_name: fancyapps-ui
fireworks:
name: butterfly-extsrc
file: dist/fireworks.min.js
version: 1.1.4
version: 1.1.6
fontawesome:
name: '@fortawesome/fontawesome-free'
file: css/all.min.css
other_name: font-awesome
version: 6.7.2
version: 7.1.0
gitalk:
name: gitalk
file: dist/gitalk.min.js
@@ -108,28 +108,24 @@ instantpage:
name: instant.page
file: instantpage.js
version: 5.2.0
instantsearch:
name: instantsearch.js
file: dist/instantsearch.production.min.js
version: 4.79.0
katex:
name: katex
file: dist/katex.min.css
other_name: KaTeX
version: 0.16.22
version: 0.16.28
katex_copytex:
name: katex
file: dist/contrib/copy-tex.min.js
other_name: KaTeX
version: 0.16.22
version: 0.16.28
lazyload:
name: vanilla-lazyload
file: dist/lazyload.iife.min.js
version: 19.1.3
mathjax:
name: mathjax
file: es5/tex-mml-chtml.js
version: 3.2.2
file: tex-mml-chtml.js
version: 4.1.0
medium_zoom:
name: medium-zoom
file: dist/medium-zoom.min.js
@@ -137,11 +133,11 @@ medium_zoom:
mermaid:
name: mermaid
file: dist/mermaid.min.js
version: 11.8.0
version: 11.12.2
meting_js:
name: butterfly-extsrc
file: metingjs/dist/Meting.min.js
version: 1.1.4
version: 1.1.6
pace_default_css:
name: pace-js
other_name: pace
@@ -174,11 +170,11 @@ prismjs_lineNumber_js:
sharejs:
name: butterfly-extsrc
file: sharejs/dist/js/social-share.min.js
version: 1.1.4
version: 1.1.6
sharejs_css:
name: butterfly-extsrc
file: sharejs/dist/css/share.min.css
version: 1.1.4
version: 1.1.6
snackbar:
name: node-snackbar
file: dist/snackbar.min.js
@@ -194,7 +190,7 @@ twikoo:
typed:
name: typed.js
file: dist/typed.umd.js
version: 2.1.0
version: 3.0.0
valine:
name: valine
file: dist/Valine.min.js
@@ -203,9 +199,9 @@ waline_css:
name: '@waline/client'
file: dist/waline.css
other_name: waline
version: 3.5.7
version: 3.8.0
waline_js:
name: '@waline/client'
file: dist/waline.js
other_name: waline
version: 3.5.7
version: 3.8.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use strict'
hexo.extend.helper.register('aside_archives', function (options = {}) {
const { config, page, site, url_for, _p } = this
const { config, page, site, url_for: urlFor, _p } = this
const { archive_dir: archiveDir, timezone, language } = config
// Destructure and set default options with object destructuring
@@ -17,56 +17,43 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
// Optimize locale handling
const lang = toMomentLocale(page.lang || page.language || language)
// Memoize comparison function to improve performance
const compareFunc =
type === 'monthly'
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
: (yearA, yearB) => yearA === yearB
// Early return if no posts
if (!site.posts.length) return ''
// Use reduce for more efficient data processing
const data = site.posts.sort('date', order).reduce((acc, post) => {
let date = post.date.clone()
if (timezone) date = date.tz(timezone)
const archives = new Map()
site.posts.forEach(post => {
const date = post.date
const year = date.year()
const month = date.month() + 1
const key = type === 'yearly' ? year : `${year}-${month}`
if (archives.has(key)) {
archives.get(key).count++
} else {
archives.set(key, {
year,
month,
count: 1,
date // Store date object for later formatting
})
}
})
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)
// Find or create archive entry
const lastEntry = acc[acc.length - 1]
if (type === 'yearly') {
const existingYearIndex = acc.findIndex(entry => entry.year === year)
if (existingYearIndex !== -1) {
acc[existingYearIndex].count++
} else {
// 否則創建新條目
acc.push({
name: date.format(format),
year,
month,
count: 1
item.name = date.format(format)
delete item.date // Clean up
})
}
} else {
if (!lastEntry || !compareFunc(lastEntry.year, lastEntry.month, year, month)) {
acc.push({
name: date.format(format),
year,
month,
count: 1
})
} else {
lastEntry.count++
}
}
return acc
}, [])
// Create link generator function
const createArchiveLink = item => {
@@ -74,7 +61,7 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
if (type === 'monthly') {
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
}
return url_for(url)
return urlFor(url)
}
// Limit results efficiently
@@ -87,7 +74,7 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
<span>${_p('aside.card_archives')}</span>
${
data.length > limitedData.length
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
? `<a class="card-more-btn" href="${urlFor(archiveDir)}/"
title="${_p('aside.more_button')}">
<i class="fas fa-angle-right"></i>
</a>`

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,14 +9,22 @@
const { postDesc } = require('../common/postDesc')
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
let relatedPosts = []
hexo.extend.helper.register('related_posts', function (currentPost) {
const relatedPosts = new Map()
const tagsData = currentPost.tags
tagsData.length && tagsData.forEach(function (tag) {
allPosts.forEach(function (post) {
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
if (!tagsData || !tagsData.length) return ''
tagsData.forEach(tag => {
const posts = tag.posts
posts.forEach(post => {
if (currentPost.path === post.path) return
if (relatedPosts.has(post.path)) {
relatedPosts.get(post.path).weight += 1
} else {
const getPostDesc = post.postDesc || postDesc(post, hexo)
const relatedPost = {
relatedPosts.set(post.path, {
title: post.title,
path: post.path,
cover: post.cover,
@@ -24,22 +32,17 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
weight: 1,
updated: post.updated,
created: post.date,
postDesc: getPostDesc
}
const index = findItem(relatedPosts, 'path', post.path)
if (index !== -1) {
relatedPosts[index].weight += 1
} else {
relatedPosts.push(relatedPost)
}
postDesc: getPostDesc,
random: Math.random()
})
}
})
})
if (relatedPosts.length === 0) {
if (relatedPosts.size === 0) {
return ''
}
let result = ''
const hexoConfig = hexo.config
const config = hexo.theme.config
@@ -47,15 +50,19 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
const dateType = config.related_post.date_type || 'created'
const headlineLang = this._p('post.recommend')
relatedPosts = relatedPosts.sort(compare('weight'))
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) {
result += '<div class="relatedPosts">'
let result = '<div class="relatedPosts">'
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i]
for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i]
const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)'
title = escape_html(title)
@@ -81,17 +88,4 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
result += '</div></div>'
return result
}
})
function isTagRelated (tagName, tags) {
return tags.some(tag => tag.name === tagName)
}
function findItem (arrayToSearch, attr, val) {
return arrayToSearch.findIndex(item => item[attr] === val)
}
function compare (attr) {
return (a, b) => b[attr] - a[attr]
}

View File

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

View File

@@ -37,7 +37,7 @@ const score = (args, content) => {
try {
paramsObj = JSON.parse(paramPart)
} catch (e) {
console.error("Failed to parse JSON in score tag:", e)
console.error('Failed to parse JSON in score tag:', e)
}
// Use double quotes for data-params attribute value,
@@ -47,4 +47,4 @@ const score = (args, content) => {
</div>`
}
hexo.extend.tag.register("score", score, { ends: true })
hexo.extend.tag.register('score', score, { ends: true })

View File

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

View File

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

View File

@@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo
--hlscrollbar-bg: lighten(#121212, 5)
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
$scrollbar-style
// scrollbar - firefox
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
if $highlight_enable
@require 'highlight/index'
@@ -88,6 +96,11 @@ $code-block
&:hover
border-bottom-color: var(--hl-color)
&.default
pre
padding: 10px 20px
@extend $scrollbar-style
&.copy-true
user-select: all
-webkit-user-select: all
@@ -100,18 +113,23 @@ $code-block
.highlight-tools
display: flex
align-items: center
overflow: hidden
padding: 0 8px
min-height: 24px
height: 2.15em
background: var(--hltools-bg)
color: var(--hltools-color)
font-size: $code-font-size
overflow: hidden
& > *
padding: 5px
flex: 0 0 auto
margin: 2px
i
display: inline-flex
justify-content: center
align-items: center
padding: 5px
cursor: pointer
transition: all .3s
@@ -127,31 +145,28 @@ $code-block
if !$highlight_macstyle
& > .macStyle
padding: 0
margin: 0
.code-lang
flex: 1
flex: 1 1 auto
overflow: hidden
padding-right: 10px
text-transform: uppercase
text-overflow: ellipsis
white-space: nowrap
font-weight: bold
font-size: 1.15em
user-select: none
-webkit-user-select: none
padding 2px
.copy-notice
padding-right: 2px
opacity: 0
transition: opacity .4s
if hexo-config('code_blocks.language')
.code-lang
flex: 1
else if (!$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none')
& > div:nth-child(2)
flex: 1
margin-right: auto
else if !$highlight_macstyle && hexo-config('code_blocks.shrink') != 'none'
& > :nth-child(2)
margin-right: auto
else
.macStyle
flex: 1
& > :nth-child(1)
margin-right: auto
.gutter
user-select: none
@@ -163,17 +178,28 @@ $code-block
td
border: none
.copy-notice
position: absolute
z-index: 99999
padding: 2px 6px
border-radius: 3px
background: var(--hltools-bg)
white-space: nowrap
font-size: 12px
pointer-events: none
if $highlight_macstyle
.container
figure.highlight
margin: 0 0 24px
border-radius: 7px
border-radius: 8px
box-shadow: 0 5px 10px 0 $highlight-mac-border
-webkit-transform: translateZ(0)
.highlight-tools
.macStyle
display: flex
padding: 3px
& > *
margin-right: 8px
@@ -264,8 +290,8 @@ if hexo-config('code_blocks.fullpage')
& ~ table
display: block
overflow: auto
height: calc(100vh - 2.15em)
margin-bottom: 0
height: calc(100vh - 2.15em)
@keyframes code-fullpage
0%,

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,8 @@
margin: 0
color: var(--white)
font-size: 1.85em
@extend .limit-more-line
-webkit-line-clamp: 3
+minWidth768()
font-size: 2.85em

View File

@@ -15,9 +15,12 @@
color: var(--btn-color)
cursor: pointer
addBorderRadius()
@extend .btn-effects
@extend .btn-effects-large
i
margin-right: 5px
vertical-align: baseline
&:hover
.reward-button
@@ -28,19 +31,19 @@
.reward-main
position: absolute
bottom: 40px
bottom: 50px
left: 0
z-index: 100
display: none
padding: 0 0 15px
width: 100%
addBorderRadius()
.reward-all
display: inline-block
margin: 0
padding: 20px 10px
background: var(--reward-pop)
addBorderRadius()
&:before
position: absolute

View File

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

View File

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

View File

@@ -65,12 +65,55 @@ if hexo-config('waline.bg')
if hexo-config('mermaid.enable')
.mermaid-wrap
position: relative
margin: 0 0 20px
background: var(--card-bg)
text-align: center
if hexo-config('mermaid.open_in_new_tab')
.mermaid-open-btn
position: absolute
top: 8px
right: 8px
z-index: 2
display: flex
justify-content: center
align-items: center
padding: 0
width: 34px
height: 25px
border: none
border-radius: 20%
background: #D3D3D3
box-shadow: 0 4px 10px rgba(0, 0, 0, .15)
color: #fff
font-size: 0
line-height: 1
cursor: pointer
transition: background .2s ease, transform .2s ease
i
font-size: 16px
line-height: 1
&:hover,
&:focus-visible
outline: none
background: #C0C0C0
transform: translateY(-1px)
& > svg
max-width: 100%
height: 100%
if hexo-config('mermaid.zoom_pan')
cursor: grab
user-select: none
touch-action: none
&:active
cursor: grabbing
if hexo-config('mermaid.code_write')
pre > code.mermaid
display: none

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,9 @@ $code-foreground = $themeColorEnable && hexo-config('theme_color.code_foreground
$code-background = $themeColorEnable && hexo-config('theme_color.code_background') ? convert(hexo-config('theme_color.code_background')) : rgba(27, 31, 35, .05)
$theme-toc-color = $themeColorEnable && hexo-config('theme_color.toc_color') ? convert(hexo-config('theme_color.toc_color')) : $strong-cyan
// font
$chineseFont = $language == 'zh-CN' ? 'Microsoft YaHei' : 'Microsoft JhengHei'
$default-font-family = -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', $chineseFont, sans-serif
$default-code-font = consolas, Menlo, monospace, 'PingFang SC', $chineseFont, sans-serif
$font-family = hexo-config('font.font_family') ? unquote(hexo-config('font.font_family')) : $default-font-family
$code-font-family = hexo-config('font.code_font_family') ? unquote(hexo-config('font.code_font_family')) : $default-code-font
$site-name-font = hexo-config('blog_title_font.font_family') && unquote(hexo-config('blog_title_font.font_family'))
@@ -184,3 +182,52 @@ $tab-to-top-color = #99a9bf
$tab-to-top-hover-color = $theme-color
// Tag Plugins - timeline
$timeline-default-color = $theme-color
// note - darkmode
// Default
$note-dark-default-border = #5a5a5a
$note-dark-default-bg = #2b2b2b
$note-dark-default-text = #b3b3b3
$note-dark-modern-default-border = #9a9a9a
$note-dark-modern-default-bg = #353535
$note-dark-modern-default-text = #c4c4c4
$note-dark-modern-default-hover = #d0d0d0
// Primary
$note-dark-primary-border = #5935a1
$note-dark-primary-bg = #2e1c3e
$note-dark-primary-text = #a47dd4
$note-dark-modern-primary-border = #9985cc
$note-dark-modern-primary-bg = #3c2d4c
$note-dark-modern-primary-text = #b693e6
$note-dark-modern-primary-hover = #c9a8f0
// Info
$note-dark-info-border = #346fa2
$note-dark-info-bg = #1f2e3b
$note-dark-info-text = #7bb3db
$note-dark-modern-info-border = #7ca8b5
$note-dark-modern-info-bg = #2b3c44
$note-dark-modern-info-text = #8fc6e0
$note-dark-modern-info-hover = #a3d4ea
// Success
$note-dark-success-border = #4a944a
$note-dark-success-bg = #202e20
$note-dark-success-text = #82c682
$note-dark-modern-success-border = #8bb087
$note-dark-modern-success-bg = #2c3d2c
$note-dark-modern-success-text = #96d196
$note-dark-modern-success-hover = #a8dca8
// Warning
$note-dark-warning-border = #c08a3e
$note-dark-warning-bg = #3e301f
$note-dark-warning-text = #e6ba6b
$note-dark-modern-warning-border = #b8a285
$note-dark-modern-warning-bg = #4b3c2b
$note-dark-modern-warning-text = #d4b373
$note-dark-modern-warning-hover = #e0c080
// Danger
$note-dark-danger-border = #b34440
$note-dark-danger-bg = #3b201f
$note-dark-danger-text = #e67572
$note-dark-modern-danger-border = #c7898c
$note-dark-modern-danger-bg = #4d2b2e
$note-dark-modern-danger-text = #d98b8e
$note-dark-modern-danger-hover = #e59fa2

View File

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

View File

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

View File

@@ -156,10 +156,10 @@ class LocalSearch {
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
})
resultItem += '</li>'
resultItem += '</a></li>'
resultItems.push({
item: resultItem,
id: resultItems.length,
@@ -236,40 +236,229 @@ class LocalSearch {
window.addEventListener('load', () => {
// 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({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
const input = document.querySelector('.local-search-input input')
const statsItem = document.getElementById('local-search-stats')
const $loadingStatus = document.getElementById('loading-status')
const isXml = !path.endsWith('json')
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
// Pagination variables (only initialize if pagination is enabled)
let currentPage = 0
const hitsPerPage = pagination.hitsPerPage || 10
let currentResultItems = []
if (!enablePagination) {
// If pagination is disabled, we don't need these variables
currentPage = undefined
currentResultItems = undefined
}
if (keywords.length === 1 && keywords[0] === '') {
// Cache frequently used elements
const elements = {
get pagination () { return document.getElementById('local-search-pagination') },
get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') }
}
// Show/hide search results area
const toggleResultsVisibility = hasResults => {
if (enablePagination) {
elements.pagination.style.display = hasResults ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render search results for current page
const renderResults = (searchText, resultItems) => {
const container = document.getElementById('local-search-results')
// Determine items to display based on pagination mode
const itemsToDisplay = enablePagination
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
: resultItems
// Handle empty page in pagination mode
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
currentPage = 0
renderResults(searchText, resultItems)
return
}
// Add numbering to items
const numberedItems = itemsToDisplay.map((result, index) => {
const itemNumber = enablePagination
? currentPage * hitsPerPage + index + 1
: index + 1
return result.item.replace(
'<li class="local-search-hit-item">',
`<li class="local-search-hit-item" value="${itemNumber}">`
)
})
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
// Update stats
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
// Handle pagination
if (enablePagination) {
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
renderPagination(currentPage, nbPages, searchText)
}
const hasResults = resultItems.length > 0
toggleResultsVisibility(hasResults)
window.pjax && window.pjax.refresh(container)
}
// Render pagination
const renderPagination = (page, nbPages, query) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
</li>`
if (startPage > 1) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
</li>`
} else {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
</li>`
}
}
// Only add ellipsis and last page when there are many pages
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
</li>`
}
if (nbPages > 1) {
elements.paginationList.innerHTML = `
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
${isFirstPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</li>
${pagesHTML}
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
${isLastPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
} else {
elements.pagination.style.display = 'none'
}
}
// Clear search results and stats
const clearSearchResults = () => {
const container = document.getElementById('local-search-results')
container.textContent = ''
statsItem.textContent = ''
} else if (resultItems.length === 0) {
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
// Show no results message
const showNoResults = searchText => {
const container = document.getElementById('local-search-results')
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.hidden = false
const keywords = searchText.split(/[-\s]+/)
let resultItems = []
if (searchText.length > 0) {
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
clearSearchResults()
} else if (resultItems.length === 0) {
showNoResults(searchText)
} else {
// Sort results by relevance
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
@@ -279,14 +468,14 @@ window.addEventListener('load', () => {
return right.id - left.id
})
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
if (enablePagination) {
currentResultItems = resultItems
currentPage = 0
}
renderResults(searchText, resultItems)
}
$loadingStatus.textContent = ''
$loadingStatus.hidden = true
}
let loadFlag = false
@@ -340,11 +529,29 @@ window.addEventListener('load', () => {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
// Pagination event delegation - only add if pagination is enabled
if (enablePagination) {
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentResultItems.length > 0) {
currentPage = page
renderResults(input.value.trim().toLowerCase(), currentResultItems)
}
}
})
}
// Initial state
toggleResultsVisibility(false)
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.nextElementSibling.style.visibility = 'visible'
$loadDataItem.remove()
})

File diff suppressed because one or more lines are too long

View File

@@ -14,37 +14,35 @@
}
},
throttle: function (func, wait, options = {}) {
let timeout, context, args
throttle: (func, wait, options = {}) => {
let timeout, args
let previous = 0
const later = () => {
previous = options.leading === false ? 0 : new Date().getTime()
timeout = null
func.apply(context, args)
if (!timeout) context = args = null
func(...args)
if (!timeout) args = null
}
const throttled = (...params) => {
return (...params) => {
const now = new Date().getTime()
if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous)
context = this
args = params
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(context, args)
if (!timeout) context = args = null
func(...args)
if (!timeout) args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}
return throttled
},
overflowPaddingR: {
@@ -106,7 +104,7 @@
loadComment: (dom, callback) => {
if ('IntersectionObserver' in window) {
const observerItem = new IntersectionObserver((entries) => {
const observerItem = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
callback()
observerItem.disconnect()
@@ -169,17 +167,7 @@
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => {
let actualTop = ele.offsetTop
let current = ele.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
},
getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY,
loadLightbox: ele => {
const service = GLOBAL_CONFIG.lightbox
@@ -190,7 +178,7 @@
}
if (service === 'fancybox') {
Array.from(ele).forEach(i => {
ele.forEach(i => {
if (i.parentNode.tagName !== 'A') {
const dataSrc = i.dataset.lazySrc || i.src
const dataCaption = i.title || i.alt || ''
@@ -249,7 +237,7 @@
'rotateCW',
'flipX',
'flipY',
"reset"
'reset'
],
right: ['autoplay', 'thumbs', 'close']
}