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: 页 -> 頁
This commit is contained in:
Jerry
2025-12-10 19:16:03 +08:00
parent 4225d23cb6
commit f1397da086
17 changed files with 259 additions and 257 deletions

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

@@ -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,6 +1,6 @@
{ {
"name": "hexo-theme-butterfly", "name": "hexo-theme-butterfly",
"version": "5.5.3-b2", "version": "5.5.3",
"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": {

View File

@@ -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.43.0 version: 5.46.0
aplayer_css: aplayer_css:
name: aplayer name: aplayer
file: dist/APlayer.min.css file: dist/APlayer.min.css
@@ -66,12 +66,12 @@ 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: 4.3.1 version: 4.3.2
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: 4.3.1 version: 4.3.2
egjs_infinitegrid: egjs_infinitegrid:
name: '@egjs/infinitegrid' name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid other_name: egjs-infinitegrid
@@ -80,12 +80,12 @@ egjs_infinitegrid:
fancybox: fancybox:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
version: 6.1.4 version: 6.1.7
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.1.4 version: 6.1.7
other_name: fancyapps-ui other_name: fancyapps-ui
fireworks: fireworks:
name: butterfly-extsrc name: butterfly-extsrc
@@ -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.25 version: 0.16.27
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.25 version: 0.16.27
lazyload: lazyload:
name: vanilla-lazyload name: vanilla-lazyload
file: dist/lazyload.iife.min.js file: dist/lazyload.iife.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.12.1 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
@@ -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.7.1 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.7.1 version: 3.8.0

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

@@ -5,39 +5,46 @@
'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 } = 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}`
} }
@@ -50,10 +57,9 @@ hexo.extend.generator.register('post', locals => {
// 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 +69,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

@@ -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 => {

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

@@ -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 => {
@@ -20,32 +26,27 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
} }
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 generateStyle = (size, unit, page) => {
if (page === 'tags') { const colorStyle = page === 'tags' ? `background-color: ${getRandomColor()};` : `color: ${getRandomColor()};`
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; background-color: ${getRandomColor()};` return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
} else {
return `font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
}
} }
const length = sizes.length - 1 return source.sort(orderby, order).map(tag => {
const result = source.sort(orderby, order).map(tag => { const ratio = length ? sizeMap.get(tag.length) / length : 0
const ratio = length ? sizes.indexOf(tag.length) / length : 0
const size = minfontsize + ((maxfontsize - minfontsize) * ratio) const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
const style = generateStyle(size, unit, page) const style = generateStyle(size, unit, page)
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 +78,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 +90,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 +101,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) => {

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

@@ -141,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

@@ -84,7 +84,11 @@ document.addEventListener('DOMContentLoaded', () => {
const buttonRect = ele.getBoundingClientRect() const buttonRect = ele.getBoundingClientRect()
const scrollTop = window.pageYOffset || document.documentElement.scrollTop const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
const finalLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
// 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 // Show tooltip below button if too close to top
const normalTop = buttonRect.top + scrollTop - 40 const normalTop = buttonRect.top + scrollTop - 40
@@ -159,6 +163,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 = () => {
@@ -545,17 +550,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
@@ -633,7 +650,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

@@ -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 || ''