Compare commits

...

47 Commits
5.5.0 ... dev

Author SHA1 Message Date
Jerry
13315f6710 improvement: fancybox 手機頁面不顯示 toolbar 2026-04-08 15:53:06 +08:00
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
45 changed files with 900 additions and 405 deletions

View File

@@ -91,7 +91,8 @@ category_per_img:
footer_img: false footer_img: false
# Website Background # Website Background
# Can set it to color or image url # Can set it to color, image URL or an array containing colors and/or image URLs
# If an array is provided, a random background will be selected from the array on each load
background: background:
cover: cover:
@@ -318,6 +319,7 @@ aside:
# If set 0 will show all # If set 0 will show all
limit: 40 limit: 40
color: false color: false
custom_colors:
# Order of tags, random/name/length # Order of tags, random/name/length
orderby: random orderby: random
# Sort of order. 1, asc for ascending; -1, desc for descending # Sort of order. 1, asc for ascending; -1, desc for descending
@@ -515,12 +517,12 @@ share:
# Share.js # Share.js
# https://github.com/overtrue/share.js # https://github.com/overtrue/share.js
sharejs: sharejs:
sites: facebook,twitter,wechat,weibo,qq sites: facebook,x,wechat,weibo,qq
# AddToAny # AddToAny
# https://www.addtoany.com/ # https://www.addtoany.com/
addtoany: addtoany:
item: facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link item: facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link
# -------------------------------------- # --------------------------------------
# Comments System # Comments System
@@ -938,6 +940,10 @@ mermaid:
theme: theme:
light: default light: default
dark: dark dark: dark
# Enable "Open in New Tab" button to view diagram in a separate window
open_in_new_tab: true
# Enable zoom and pan interactions on diagrams
zoom_pan: true
# chartjs # chartjs
# see https://www.chartjs.org/docs/latest/ # see https://www.chartjs.org/docs/latest/
@@ -1067,7 +1073,7 @@ CDN:
third_party_provider: jsdelivr third_party_provider: jsdelivr
# Add version number to url, true or false # Add version number to url, true or false
version: false version: true
# Custom format # Custom format
# For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file} # For example: https://cdn.staticfile.org/${cdnjs_name}/${version}/${min_cdnjs_file}

View File

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

View File

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

View File

@@ -72,20 +72,16 @@
}) })
} }
let highlight = 'undefined' let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
let syntaxHighlighter = config.syntax_highlighter const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable) let highlight = JSON.stringify({
if (highlightEnable) { plugin: highlightProvider,
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
highlight = JSON.stringify({
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
highlightCopy: copy, highlightCopy: copy,
highlightLang: language, highlightLang: language,
highlightHeightLimit: height_limit, highlightHeightLimit: height_limit,
highlightFullpage: fullpage, highlightFullpage: fullpage,
highlightMacStyle: macStyle highlightMacStyle: macStyle
}) })
}
script. script.
const GLOBAL_CONFIG = { const GLOBAL_CONFIG = {

View File

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

View File

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

View File

@@ -46,6 +46,10 @@
- const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page) - const localDate = page.shuoshuo_url ? [] : shuoshuoFN(site.data.shuoshuo, page)
if !page.shuoshuo_url
script(type='application/json' id='shuoshuo-data')!= safeJSON(localDate)
- const { enable, native, placeholder, field } = theme.lazyload
script. script.
(() => { (() => {
const limitConfig = !{ JSON.stringify(page.limit || {}) } const limitConfig = !{ JSON.stringify(page.limit || {}) }
@@ -81,6 +85,36 @@
return `${year}-${month}-${day} ${hour}:${minute}:${second}` return `${year}-${month}-${day} ${hour}:${minute}:${second}`
} }
const addLazyload = str => {
const config = {
enable: !{Boolean(enable)},
native: !{Boolean(native)},
field: '!{field}',
placeholder: '!{url_for(placeholder)}',
}
if (!config.enable || config.field !== 'site') return str
const parser = new DOMParser()
const doc = parser.parseFromString(str, 'text/html')
const images = doc.querySelectorAll('img')
images.forEach(img => {
if (config.native) {
img.setAttribute('loading', 'lazy')
} else {
const src = img.getAttribute('src')
img.setAttribute('data-lazy-src', src)
if (config.placeholder) {
img.setAttribute('src', config.placeholder)
} else {
img.removeAttribute('src')
}
}
})
return doc.body.innerHTML
}
let currentPage = 1 let currentPage = 1
const itemsPerPage = 8 const itemsPerPage = 8
let totalPages = 0 let totalPages = 0
@@ -114,7 +148,7 @@
</time> </time>
</div> </div>
</div> </div>
<div class="shuoshuo-content">${item.content}</div> <div class="shuoshuo-content">${addLazyload(item.content)}</div>
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}"> <div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''} ${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
${commentButton} ${commentButton}
@@ -280,7 +314,8 @@
const response = await fetch('!{url_for(page.shuoshuo_url)}') const response = await fetch('!{url_for(page.shuoshuo_url)}')
originData = await response.json() originData = await response.json()
} else { } else {
originData = !{JSON.stringify(localDate)} const dataElement = document.getElementById('shuoshuo-data')
originData = dataElement ? JSON.parse(dataElement.textContent) : []
} }
data = filterDataByLimit(sortDataByDate(originData), limitConfig) data = filterDataByLimit(sortDataByDate(originData), limitConfig)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,292 @@
script. script.
(() => { (() => {
const parseViewBox = viewBox => {
if (!viewBox) return null
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
return parts
}
const getSvgViewBox = svg => {
const attr = parseViewBox(svg.getAttribute('viewBox'))
if (attr) return attr
// Fallback: use bbox to build a viewBox
try {
const bbox = svg.getBBox()
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
} catch (e) {
// getBBox may fail on some edge cases; ignore
}
const w = Number(svg.getAttribute('width')) || 0
const h = Number(svg.getAttribute('height')) || 0
if (w > 0 && h > 0) return [0, 0, w, h]
return [0, 0, 100, 100]
}
const setSvgViewBox = (svg, vb) => {
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
}
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
const openSvgInNewTab = ({ source, initViewBox }) => {
const getClonedSvg = () => {
if (typeof source === 'string') {
const template = document.createElement('template')
template.innerHTML = source.trim()
const svg = template.content.querySelector('svg')
return svg ? svg.cloneNode(true) : null
}
if (source && typeof source.cloneNode === 'function') {
return source.cloneNode(true)
}
return null
}
const clone = getClonedSvg()
if (!clone) return
if (initViewBox && initViewBox.length === 4) {
clone.setAttribute('viewBox', initViewBox.join(' '))
}
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
if (!clone.getAttribute('xmlns:xlink') && clone.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 => { const runMermaid = ele => {
window.loadMermaid = true window.loadMermaid = true
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
ele.forEach((item, index) => { ele.forEach((item, index) => {
const mermaidSrc = item.firstElementChild const mermaidSrc = item.firstElementChild
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
// 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 mermaidID = `mermaid-${index}`
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
const renderFn = mermaid.render(mermaidID, mermaidDefinition) const renderFn = mermaid.render(mermaidID, mermaidDefinition)
const renderMermaid = svg => { const renderMermaid = svg => {
mermaidSrc.insertAdjacentHTML('afterend', svg) mermaidSrc.insertAdjacentHTML('afterend', svg)
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
item.__mermaidOriginalSvg = svg
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
} }
// mermaid v9 and v10 compatibility // mermaid v9 and v10 compatibility
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg)) typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
}) })

View File

@@ -14,9 +14,9 @@ if choose
else else
- pjaxSelectors.unshift('meta[name="description"]') - pjaxSelectors.unshift('meta[name="description"]')
script(src=url_for(theme.asset.pjax)) script(src=url_for(theme.asset.pjax) defer)
script. script.
(() => { document.addEventListener('DOMContentLoaded', () => {
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)} const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
window.pjax = new Pjax({ window.pjax = new Pjax({
@@ -65,10 +65,9 @@ script.
document.addEventListener('pjax:error', e => { document.addEventListener('pjax:error', e => {
if (e.request.status === 404) { if (e.request.status === 404) {
const usePjax = !{theme.pjax && theme.pjax.enable}
!{theme.error_404 && theme.error_404.enable} !{theme.error_404 && theme.error_404.enable}
? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{url_for("/404.html")}') ? pjax.loadUrl('!{url_for("/404.html")}')
: window.location.href = e.request.responseURL : window.location.href = e.request.responseURL
} }
}) })
})() })

View File

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

View File

@@ -1,5 +1,5 @@
- const { effect, source, sub, typed_option } = theme.subtitle - const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = sub || new Array() - let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
script. script.
window.typedJSFn = { window.typedJSFn = {

View File

@@ -31,7 +31,7 @@ script.
const getData = async (isPost) => { const getData = async (isPost) => {
try { try {
const now = Date.now() 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' } const headerList = { 'Accept': 'application/json' }
if (!{isServerURL}) { if (!{isServerURL}) {
@@ -62,8 +62,8 @@ script.
const pagePV = document.getElementById('umamiPV') const pagePV = document.getElementById('umamiPV')
if (pagePV) { if (pagePV) {
const data = await getData(true) const data = await getData(true)
if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') { if (data && data.pageviews) {
pagePV.textContent = data.pageviews.value pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else { } else {
console.warn('Umami Analytics: Invalid page view data received') console.warn('Umami Analytics: Invalid page view data received')
} }
@@ -75,8 +75,8 @@ script.
if (config.site_uv) { if (config.site_uv) {
const siteUV = document.getElementById('umami-site-uv') const siteUV = document.getElementById('umami-site-uv')
if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') { if (siteUV && data && data.visitors) {
siteUV.textContent = data.visitors.value siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors
} else if (siteUV) { } else if (siteUV) {
console.warn('Umami Analytics: Invalid site UV data received') console.warn('Umami Analytics: Invalid site UV data received')
} }
@@ -84,8 +84,8 @@ script.
if (config.site_pv) { if (config.site_pv) {
const sitePV = document.getElementById('umami-site-pv') const sitePV = document.getElementById('umami-site-pv')
if (sitePV && data && data.pageviews && typeof data.pageviews.value !== 'undefined') { if (sitePV && data && data.pageviews) {
sitePV.textContent = data.pageviews.value sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else if (sitePV) { } else if (sitePV) {
console.warn('Umami Analytics: Invalid site PV data received') console.warn('Umami Analytics: Invalid site PV data received')
} }

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "hexo-theme-butterfly", "name": "hexo-theme-butterfly",
"version": "5.5.0", "version": "5.5.5-b1",
"description": "A Simple and Card UI Design theme for Hexo", "description": "A Simple and Card UI Design theme for Hexo",
"main": "package.json", "main": "package.json",
"scripts": { "scripts": {
@@ -25,7 +25,7 @@
"dependencies": { "dependencies": {
"hexo-renderer-pug": "^3.0.0", "hexo-renderer-pug": "^3.0.0",
"hexo-renderer-stylus": "^3.0.1", "hexo-renderer-stylus": "^3.0.1",
"hexo-util": "^3.3.0", "hexo-util": "^4.0.0",
"moment-timezone": "^0.6.0" "moment-timezone": "^0.6.0"
}, },
"homepage": "https://butterfly.js.org/", "homepage": "https://butterfly.js.org/",

View File

@@ -1,7 +1,7 @@
abcjs_basic_js: abcjs_basic_js:
name: abcjs name: abcjs
file: dist/abcjs-basic-min.js file: dist/abcjs-basic-min.js
version: 6.5.2 version: 6.6.0
activate_power_mode: activate_power_mode:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/activate-power-mode.min.js file: dist/activate-power-mode.min.js
@@ -9,7 +9,7 @@ activate_power_mode:
algolia_search: algolia_search:
name: algoliasearch name: algoliasearch
file: dist/lite/builds/browser.umd.js file: dist/lite/builds/browser.umd.js
version: 5.37.0 version: 5.47.0
aplayer_css: aplayer_css:
name: aplayer name: aplayer
file: dist/APlayer.min.css file: dist/APlayer.min.css
@@ -45,7 +45,7 @@ canvas_ribbon:
chartjs: chartjs:
name: chart.js name: chart.js
file: dist/chart.umd.js file: dist/chart.umd.js
version: 4.5.0 version: 4.5.1
clickShowText: clickShowText:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/click-show-text.min.js file: dist/click-show-text.min.js
@@ -57,35 +57,35 @@ click_heart:
disqusjs: disqusjs:
name: disqusjs name: disqusjs
file: dist/browser/disqusjs.es2015.umd.min.js file: dist/browser/disqusjs.es2015.umd.min.js
version: 3.1.0 version: 3.2.1
disqusjs_css: disqusjs_css:
name: disqusjs name: disqusjs
file: dist/browser/styles/disqusjs.css file: dist/browser/styles/disqusjs.css
version: 3.1.0 version: 3.2.1
docsearch_css: docsearch_css:
name: '@docsearch/css' name: '@docsearch/css'
other_name: docsearch-css other_name: docsearch-css
file: dist/style.css file: dist/style.css
version: 3.9.0 version: 4.5.3
docsearch_js: docsearch_js:
name: '@docsearch/js' name: '@docsearch/js'
other_name: docsearch-js other_name: docsearch-js
file: dist/umd/index.js file: dist/umd/index.js
version: 3.9.0 version: 4.5.3
egjs_infinitegrid: egjs_infinitegrid:
name: '@egjs/infinitegrid' name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid other_name: egjs-infinitegrid
file: dist/infinitegrid.min.js file: dist/infinitegrid.min.js
version: 4.12.0 version: 4.13.0
fancybox: fancybox:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
version: 6.0.29 version: 6.1.9
other_name: fancyapps-ui other_name: fancyapps-ui
fancybox_css: fancybox_css:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css file: dist/fancybox/fancybox.css
version: 6.0.29 version: 6.1.9
other_name: fancyapps-ui other_name: fancyapps-ui
fireworks: fireworks:
name: butterfly-extsrc name: butterfly-extsrc
@@ -95,7 +95,7 @@ fontawesome:
name: '@fortawesome/fontawesome-free' name: '@fortawesome/fontawesome-free'
file: css/all.min.css file: css/all.min.css
other_name: font-awesome other_name: font-awesome
version: 7.0.1 version: 7.1.0
gitalk: gitalk:
name: gitalk name: gitalk
file: dist/gitalk.min.js file: dist/gitalk.min.js
@@ -112,12 +112,12 @@ katex:
name: katex name: katex
file: dist/katex.min.css file: dist/katex.min.css
other_name: KaTeX other_name: KaTeX
version: 0.16.22 version: 0.16.28
katex_copytex: katex_copytex:
name: katex name: katex
file: dist/contrib/copy-tex.min.js file: dist/contrib/copy-tex.min.js
other_name: KaTeX other_name: KaTeX
version: 0.16.22 version: 0.16.28
lazyload: lazyload:
name: vanilla-lazyload name: vanilla-lazyload
file: dist/lazyload.iife.min.js file: dist/lazyload.iife.min.js
@@ -125,7 +125,7 @@ lazyload:
mathjax: mathjax:
name: mathjax name: mathjax
file: tex-mml-chtml.js file: tex-mml-chtml.js
version: 4.0.0 version: 4.1.0
medium_zoom: medium_zoom:
name: medium-zoom name: medium-zoom
file: dist/medium-zoom.min.js file: dist/medium-zoom.min.js
@@ -133,7 +133,7 @@ medium_zoom:
mermaid: mermaid:
name: mermaid name: mermaid
file: dist/mermaid.min.js file: dist/mermaid.min.js
version: 11.11.0 version: 11.12.2
meting_js: meting_js:
name: butterfly-extsrc name: butterfly-extsrc
file: metingjs/dist/Meting.min.js file: metingjs/dist/Meting.min.js
@@ -190,7 +190,7 @@ twikoo:
typed: typed:
name: typed.js name: typed.js
file: dist/typed.umd.js file: dist/typed.umd.js
version: 2.1.0 version: 3.0.0
valine: valine:
name: valine name: valine
file: dist/Valine.min.js file: dist/Valine.min.js
@@ -199,9 +199,9 @@ waline_css:
name: '@waline/client' name: '@waline/client'
file: dist/waline.css file: dist/waline.css
other_name: waline other_name: waline
version: 3.6.0 version: 3.8.0
waline_js: waline_js:
name: '@waline/client' name: '@waline/client'
file: dist/waline.js file: dist/waline.js
other_name: waline other_name: waline
version: 3.6.0 version: 3.8.0

View File

@@ -179,6 +179,7 @@ module.exports = {
enable: true, enable: true,
limit: 40, limit: 40,
color: false, color: false,
custom_colors: null,
orderby: 'random', orderby: 'random',
order: 1, order: 1,
sort_order: null sort_order: null
@@ -290,10 +291,10 @@ module.exports = {
share: { share: {
use: 'sharejs', use: 'sharejs',
sharejs: { sharejs: {
sites: 'facebook,twitter,wechat,weibo,qq' sites: 'facebook,x,wechat,weibo,qq'
}, },
addtoany: { addtoany: {
item: 'facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link' item: 'facebook,x,wechat,sina_weibo,facebook_messenger,email,copy_link'
} }
}, },
comments: { comments: {
@@ -522,7 +523,9 @@ module.exports = {
theme: { theme: {
light: 'default', light: 'default',
dark: 'dark' dark: 'dark'
} },
open_in_new_tab: true,
zoom_pan: true
}, },
chartjs: { chartjs: {
enable: false, enable: false,
@@ -591,7 +594,7 @@ module.exports = {
CDN: { CDN: {
internal_provider: 'local', internal_provider: 'local',
third_party_provider: 'jsdelivr', third_party_provider: 'jsdelivr',
version: false, version: true,
custom_format: null, custom_format: null,
option: null option: null
} }

View File

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

View File

@@ -17,7 +17,7 @@ function checkHexoEnvironment (hexo) {
if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) { if (major < requiredMajor || (major === requiredMajor && minor < requiredMinor)) {
log.error('Please update Hexo to V5.3.0 or higher!') log.error('Please update Hexo to V5.3.0 or higher!')
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!') log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
process.exit(-1) throw new Error('Hexo version too old')
} }
// Check for deprecated configuration file // Check for deprecated configuration file
@@ -26,7 +26,7 @@ function checkHexoEnvironment (hexo) {
if (data && data.butterfly) { if (data && data.butterfly) {
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'") log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'") log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
process.exit(-1) throw new Error('Deprecated configuration file')
} }
} }
} }

View File

@@ -18,20 +18,12 @@ const lazyload = htmlContent => {
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
// Use more precise replacement: handle src attributes with double and single quotes, but avoid replacing content inside script tags // Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
let result = htmlContent // 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) => {
// Handle src attributes with double quotes const src = srcDoubleQuote || srcSingleQuote || srcNoQuote
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc="([^"]+)")(?![^<]*<\/script>)/gi, (match, tag, src) => { return `${prefix}"${bg}" data-lazy-src="${src}"`
return tag.replace(`src="${src}"`, `src="${bg}" data-lazy-src="${src}"`)
}) })
// Handle src attributes with single quotes
result = result.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc='([^']+)')(?![^<]*<\/script>)/gi, (match, tag, src) => {
return tag.replace(`src='${src}'`, `src='${bg}' data-lazy-src='${src}'`)
})
return result
} }
hexo.extend.filter.register('after_render:html', data => { hexo.extend.filter.register('after_render:html', data => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,12 @@ const { prettyUrls } = require('hexo-util')
const crypto = require('crypto') const crypto = require('crypto')
const moment = require('moment-timezone') const moment = require('moment-timezone')
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
const archiveRegex = /\/archives\//
hexo.extend.helper.register('truncate', truncateContent) hexo.extend.helper.register('truncate', truncateContent)
hexo.extend.helper.register('postDesc', data => { hexo.extend.helper.register('postDesc', data => {
@@ -13,39 +19,67 @@ hexo.extend.helper.register('postDesc', data => {
hexo.extend.helper.register('cloudTags', function (options = {}) { hexo.extend.helper.register('cloudTags', function (options = {}) {
const env = this const env = this
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
if (limit > 0) { if (limit > 0) {
source = source.limit(limit) source = source.limit(limit)
} }
const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))] const sizes = [...new Set(source.map(tag => tag.length).sort((a, b) => a - b))]
const sizeMap = new Map(sizes.map((size, index) => [size, index]))
const length = sizes.length - 1
const getRandomColor = () => { const getRandomColor = () => {
const randomColor = () => Math.floor(Math.random() * 201) const r = Math.floor(Math.random() * 201)
const r = randomColor() const g = Math.floor(Math.random() * 201)
const g = randomColor() const b = Math.floor(Math.random() * 201)
const b = randomColor()
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})` return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
} }
const generateStyle = (size, unit, page) => { const normalizeColors = input => {
if (page === 'tags') { if (!input) return null
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; background-color: ${getRandomColor()};` if (typeof input === 'string') {
} else { const color = input.trim()
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};` return color ? [color] : null
} }
if (Array.isArray(input)) {
const result = []
for (let i = 0; i < input.length; i++) {
const value = input[i]
if (value === null || value === undefined) continue
const color = String(value).trim()
if (!color) continue
result.push(color)
}
return result.length ? result : null
}
return null
} }
const length = sizes.length - 1 const userColors = normalizeColors(custom_colors)
const result = source.sort(orderby, order).map(tag => {
const ratio = length ? sizes.indexOf(tag.length) / length : 0 const 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 size = minfontsize + ((maxfontsize - minfontsize) * ratio)
const style = generateStyle(size, unit, page)
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>` return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
}).join('') }).join('')
return result
}) })
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) { hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
@@ -77,7 +111,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
if (result) return result if (result) return result
} }
if (/\/archives\//.test(m[key])) { if (archiveRegex.test(m[key])) {
return key return key
} }
} }
@@ -89,13 +123,9 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
hexo.extend.helper.register('getBgPath', function (path) { hexo.extend.helper.register('getBgPath', function (path) {
if (!path) return '' if (!path) return ''
const absoluteUrlPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i
const relativeUrlPattern = /^(\.\/|\.\.\/|\/|[^/]+\/).*$/
const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
if (colorPattern.test(path)) { if (colorPattern.test(path)) {
return `background-color: ${path};` return `background-color: ${path};`
} else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path)) { } else if (absoluteUrlPattern.test(path) || relativeUrlPattern.test(path) || simpleFilePattern.test(path)) {
return `background-image: url(${this.url_for(path)});` return `background-image: url(${this.url_for(path)});`
} else { } else {
return `background: ${path};` return `background: ${path};`
@@ -104,39 +134,34 @@ hexo.extend.helper.register('getBgPath', function (path) {
hexo.extend.helper.register('shuoshuoFN', (data, page) => { hexo.extend.helper.register('shuoshuoFN', (data, page) => {
const { limit } = page const { limit } = page
let finalResult = ''
// Shallow copy to avoid mutating original data
let processedData = data.map(item => ({ ...item }))
// Check if limit.value is a valid date // Check if limit.value is a valid date
const isValidDate = date => !isNaN(Date.parse(date)) const isValidDate = date => !isNaN(Date.parse(date))
// order by date // order by date
const orderByDate = data => data.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) processedData.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
// Apply number limit or time limit conditionally // Apply number limit or time limit conditionally
const limitData = data => {
if (limit && limit.type === 'num' && limit.value > 0) { if (limit && limit.type === 'num' && limit.value > 0) {
return data.slice(0, limit.value) processedData = processedData.slice(0, limit.value)
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) { } else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
const limitDate = Date.parse(limit.value) const limitDate = Date.parse(limit.value)
return data.filter(item => Date.parse(item.date) >= limitDate) processedData = processedData.filter(item => Date.parse(item.date) >= limitDate)
} }
return data
}
orderByDate(data)
finalResult = limitData(data)
// This is a hack method, because hexo treats time as UTC time // This is a hack method, because hexo treats time as UTC time
// so you need to manually convert the time zone // so you need to manually convert the time zone
finalResult.forEach(item => { processedData.forEach(item => {
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss')
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss') item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss')
// markdown // markdown
item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' }) item.content = hexo.render.renderSync({ text: item.content, engine: 'markdown' })
}) })
return finalResult return processedData
}) })
hexo.extend.helper.register('getPageType', (page, isHome) => { hexo.extend.helper.register('getPageType', (page, isHome) => {
@@ -157,3 +182,12 @@ hexo.extend.helper.register('getVersion', () => {
const { version } = require('../../package.json') const { version } = require('../../package.json')
return { hexo: hexo.version, theme: version } 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') const { postDesc } = require('../common/postDesc')
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) { hexo.extend.helper.register('related_posts', function (currentPost) {
let relatedPosts = [] const relatedPosts = new Map()
const tagsData = currentPost.tags const tagsData = currentPost.tags
tagsData.length && tagsData.forEach(function (tag) {
allPosts.forEach(function (post) { if (!tagsData || !tagsData.length) return ''
if (currentPost.path !== post.path && isTagRelated(tag.name, post.tags)) {
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 getPostDesc = post.postDesc || postDesc(post, hexo)
const relatedPost = { relatedPosts.set(post.path, {
title: post.title, title: post.title,
path: post.path, path: post.path,
cover: post.cover, cover: post.cover,
@@ -24,22 +32,17 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
weight: 1, weight: 1,
updated: post.updated, updated: post.updated,
created: post.date, created: post.date,
postDesc: getPostDesc postDesc: getPostDesc,
} random: Math.random()
const index = findItem(relatedPosts, 'path', post.path) })
if (index !== -1) {
relatedPosts[index].weight += 1
} else {
relatedPosts.push(relatedPost)
}
} }
}) })
}) })
if (relatedPosts.length === 0) { if (relatedPosts.size === 0) {
return '' return ''
} }
let result = ''
const hexoConfig = hexo.config const hexoConfig = hexo.config
const config = hexo.theme.config const config = hexo.theme.config
@@ -47,15 +50,19 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
const dateType = config.related_post.date_type || 'created' const dateType = config.related_post.date_type || 'created'
const headlineLang = this._p('post.recommend') const headlineLang = this._p('post.recommend')
relatedPosts = relatedPosts.sort(compare('weight')) const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => {
if (b.weight !== a.weight) {
return b.weight - a.weight
}
return b.random - a.random
})
if (relatedPosts.length > 0) { let result = '<div class="relatedPosts">'
result += '<div class="relatedPosts">'
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>` result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">' result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) { for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[i] let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i]
const { escape_html, url_for, date } = this const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)' cover = cover || 'var(--default-bg-color)'
title = escape_html(title) title = escape_html(title)
@@ -81,17 +88,4 @@ hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
result += '</div></div>' result += '</div></div>'
return result return result
}
}) })
function isTagRelated (tagName, tags) {
return tags.some(tag => tag.name === tagName)
}
function findItem (arrayToSearch, attr, val) {
return arrayToSearch.findIndex(item => item[attr] === val)
}
function compare (attr) {
return (a, b) => b[attr] - a[attr]
}

View File

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

View File

@@ -159,7 +159,7 @@ if hexo-config('enter_transitions')
animation: titleScale 1s animation: titleScale 1s
canvas:not(#ribbon-canvas), canvas:not(#ribbon-canvas),
#web_bg #web_bg.bg-animation
animation: to_show 4s animation: to_show 4s
#ribbon-canvas #ribbon-canvas

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,13 +37,13 @@
display: none display: none
padding: 0 0 15px padding: 0 0 15px
width: 100% width: 100%
addBorderRadius()
.reward-all .reward-all
display: inline-block display: inline-block
margin: 0 margin: 0
padding: 20px 10px padding: 20px 10px
background: var(--reward-pop) background: var(--reward-pop)
addBorderRadius()
&:before &:before
position: absolute position: absolute

View File

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

View File

@@ -130,6 +130,7 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
// hide-tags // hide-tags
.hide-button, .hide-button,
.toggle-button,
#post-outdate-notice, #post-outdate-notice,
.error-img, .error-img,
.container iframe, .container iframe,
@@ -140,9 +141,9 @@ if hexo-config('darkmode.enable') || hexo-config('display_mode') == 'dark'
img:not(.cover) img:not(.cover)
if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder') if hexo-config('lazyload.enable') && hexo-config('lazyload.blur') && !hexo-config('lazyload.placeholder')
filter: blur(0) brightness(.8) filter: blur(0) brightness(.88) contrast(.95)
else else
filter: brightness(.8) filter: brightness(.88) contrast(.95)
#aside-content .aside-list > .aside-list-item:not(:last-child) #aside-content .aside-list > .aside-list-item:not(:last-child)
border-bottom: 1px dashed alpha(#FFFFFF, .1) border-bottom: 1px dashed alpha(#FFFFFF, .1)

View File

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

View File

@@ -91,6 +91,7 @@
justify-content: center justify-content: center
align-items: center align-items: center
margin-right: 6px margin-right: 6px
margin-top: 3px
min-width: 24px min-width: 24px
color: $search-color color: $search-color
content: attr(value) '.' content: attr(value) '.'

View File

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

View File

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

View File

@@ -156,10 +156,10 @@ class LocalSearch {
} }
slicesOfContent.forEach(slice => { slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>` resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
}) })
resultItem += '</li>' resultItem += '</a></li>'
resultItems.push({ resultItems.push({
item: resultItem, item: resultItem,
id: resultItems.length, id: resultItems.length,

View File

@@ -14,37 +14,35 @@
} }
}, },
throttle: function (func, wait, options = {}) { throttle: (func, wait, options = {}) => {
let timeout, context, args let timeout, args
let previous = 0 let previous = 0
const later = () => { const later = () => {
previous = options.leading === false ? 0 : new Date().getTime() previous = options.leading === false ? 0 : new Date().getTime()
timeout = null timeout = null
func.apply(context, args) func(...args)
if (!timeout) context = args = null if (!timeout) args = null
} }
const throttled = (...params) => { return (...params) => {
const now = new Date().getTime() const now = new Date().getTime()
if (!previous && options.leading === false) previous = now if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous) const remaining = wait - (now - previous)
context = this
args = params args = params
if (remaining <= 0 || remaining > wait) { if (remaining <= 0 || remaining > wait) {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
timeout = null timeout = null
} }
previous = now previous = now
func.apply(context, args) func(...args)
if (!timeout) context = args = null if (!timeout) args = null
} else if (!timeout && options.trailing !== false) { } else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining) timeout = setTimeout(later, remaining)
} }
} }
return throttled
}, },
overflowPaddingR: { overflowPaddingR: {
@@ -169,17 +167,7 @@
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0, isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => { getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY,
let actualTop = ele.offsetTop
let current = ele.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
},
loadLightbox: ele => { loadLightbox: ele => {
const service = GLOBAL_CONFIG.lightbox const service = GLOBAL_CONFIG.lightbox
@@ -190,7 +178,7 @@
} }
if (service === 'fancybox') { if (service === 'fancybox') {
Array.from(ele).forEach(i => { ele.forEach(i => {
if (i.parentNode.tagName !== 'A') { if (i.parentNode.tagName !== 'A') {
const dataSrc = i.dataset.lazySrc || i.src const dataSrc = i.dataset.lazySrc || i.src
const dataCaption = i.title || i.alt || '' const dataCaption = i.title || i.alt || ''