Compare commits

..

104 Commits

37 changed files with 345 additions and 761 deletions

View File

@@ -91,8 +91,7 @@ category_per_img:
footer_img: false footer_img: false
# Website Background # Website Background
# Can set it to color, image URL or an array containing colors and/or image URLs # Can set it to color or image url
# If an array is provided, a random background will be selected from the array on each load
background: background:
cover: cover:
@@ -319,7 +318,6 @@ 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
@@ -940,10 +938,6 @@ 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/

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 ? url_for(theme.asset.busuanzi) : '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js') script(async data-pjax src=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,16 +72,20 @@
}) })
} }
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null) let highlight = 'undefined'
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks let syntaxHighlighter = config.syntax_highlighter
let highlight = JSON.stringify({ let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
plugin: highlightProvider, if (highlightEnable) {
highlightCopy: copy, const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
highlightLang: language, highlight = JSON.stringify({
highlightHeightLimit: height_limit, plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
highlightFullpage: fullpage, highlightCopy: copy,
highlightMacStyle: macStyle highlightLang: language,
}) highlightHeightLimit: height_limit,
highlightFullpage: fullpage,
highlightMacStyle: macStyle
})
}
script. script.
const GLOBAL_CONFIG = { const GLOBAL_CONFIG = {

View File

@@ -13,29 +13,7 @@ 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
if !Array.isArray(theme.background) #web_bg(style=getBgPath(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 = [6, 7].includes(indexLayout) ? 'masonry' : '' - const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? '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 postCover = article.cover - const post_cover = article.cover
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : '' - const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
if postCover && theme.cover.index_enable if post_cover && 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(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title) img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
else else
div.post-bg(style=`background: ${postCover}`) div.post-bg(style=`background: ${post_cover}`)
.recent-post-info(class=noCover) .recent-post-info(class=no_cover)
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 isUpdatedType = theme.post_meta.page.date_type === 'updated' - const data_type_updated = theme.post_meta.page.date_type === 'updated'
- const dateType = isUpdatedType ? 'updated' : 'date' - const date_type = data_type_updated ? 'updated' : 'date'
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt' - const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created') - const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
i(class=dateIcon) i(class=date_icon)
span.article-meta-label= dateTitle span.article-meta-label= date_title
time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format) time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], 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,10 +69,7 @@ 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
- const commentSystem = theme.comments.use[0] case theme.comments.use[0]
- const commentLink = url_for(link) + '#post-comment'
case commentSystem
when 'Disqus' when 'Disqus'
when 'Disqusjs' when 'Disqusjs'
+countBlockInIndex +countBlockInIndex
@@ -80,30 +77,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=commentLink) a(href=url_for(link) + '#post-comment')
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=commentLink) a(href=url_for(link) + '#post-comment')
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=commentLink) a.twikoo-count(href=url_for(link) + '#post-comment')
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Facebook Comments' when 'Facebook Comments'
+countBlockInIndex +countBlockInIndex
a(href=commentLink) a(href=url_for(link) + '#post-comment')
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=commentLink) a(href=url_for(link) + '#post-comment')
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=commentLink) a(href=url_for(link) + '#post-comment')
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

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

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.pagination_cover || 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.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.textContent, options) ABCJS.renderAbc(ele, ele.innerHTML, options)
} }
}, 100) }, 100)
} }

View File

@@ -1,275 +1,11 @@
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
// 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) : {} const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
if (!config.theme) { if (!config.theme) {
config.theme = theme config.theme = theme
@@ -281,12 +17,8 @@ script.
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))
}) })
@@ -320,4 +52,4 @@ script.
btf.addGlobalFn('encrypt', loadMermaid, 'mermaid') btf.addGlobalFn('encrypt', loadMermaid, 'mermaid')
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid) window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)
})() })()

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) defer) script(src=url_for(theme.asset.pjax))
script. script.
document.addEventListener('DOMContentLoaded', () => { (() => {
const pjaxSelectors = !{JSON.stringify(pjaxSelectors)} const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
window.pjax = new Pjax({ window.pjax = new Pjax({
@@ -65,9 +65,10 @@ 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}
? pjax.loadUrl('!{url_for("/404.html")}') ? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{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) defer) script(src=url_for(prismjs_js))
script(src=url_for(prismjs_autoloader) defer) script(src=url_for(prismjs_autoloader))
if (line_number) if (line_number)
script(src=url_for(prismjs_lineNumber_js) defer) script(src=url_for(prismjs_lineNumber_js))

View File

@@ -1,5 +1,5 @@
- const { effect, source, sub, typed_option } = theme.subtitle - const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = typeof sub === 'string' ? [sub] : (sub || new Array()) - let subContent = 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}&path=${window.location.pathname}` : '' const keyUrl = isPost ? `&url=${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) { if (data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews pagePV.textContent = data.pageviews.value
} 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) { if (siteUV && data && data.visitors && typeof data.visitors.value !== 'undefined') {
siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors siteUV.textContent = data.visitors.value
} 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) { if (sitePV && data && data.pageviews && typeof data.pageviews.value !== 'undefined') {
sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews sitePV.textContent = data.pageviews.value
} 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, custom_colors } = theme.aside.card_tags - let { limit, orderby, order } = 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', custom_colors: custom_colors}) .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'})
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.5-b1", "version": "5.5.2",
"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

@@ -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.6.0 version: 6.5.2
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.47.0 version: 5.43.0
aplayer_css: aplayer_css:
name: aplayer name: aplayer
file: dist/APlayer.min.css file: dist/APlayer.min.css
@@ -66,26 +66,26 @@ 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.5.3 version: 4.3.1
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.5.3 version: 4.3.1
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.13.0 version: 4.12.0
fancybox: fancybox:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
version: 6.1.9 version: 6.1.4
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.9 version: 6.1.4
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.28 version: 0.16.25
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.28 version: 0.16.25
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.1.0 version: 4.0.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.12.2 version: 11.12.1
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: 3.0.0 version: 2.1.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.8.0 version: 3.7.1
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.8.0 version: 3.7.1

View File

@@ -179,7 +179,6 @@ 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
@@ -523,9 +522,7 @@ 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,

View File

@@ -46,43 +46,45 @@ hexo.extend.filter.register('before_generate', () => {
} }
const createCDNLink = (data, type, cond = '') => { const createCDNLink = (data, type, cond = '') => {
return Object.keys(data).reduce((result, key) => { Object.keys(data).forEach(key => {
let { name, version, file, other_name: otherName } = data[key] let { name, version, file, other_name } = data[key]
const cdnjsName = otherName || name const cdnjs_name = other_name || name
const cdnjsFile = file.replace(/^[lib|dist]*\/|browser\//g, '') const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
const minCdnjsFile = minFile(cdnjsFile) const min_cdnjs_file = minFile(cdnjs_file)
if (cond === 'internal') file = `source/${file}` if (cond === 'internal') file = `source/${file}`
const minFilePath = minFile(file) const min_file = 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: cdnjsFile, cdnjs_file,
min_file: minFilePath, min_file,
min_cdnjs_file: minCdnjsFile, min_cdnjs_file,
cdnjs_name: cdnjsName cdnjs_name
} }
const cdnSource = { const cdnSource = {
local: cond === 'internal' ? `${cdnjsFile + verType}` : `/pluginsSrc/${name}/${file + verType}`, local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${minFilePath}`, jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
unpkg: `https://unpkg.com/${name}${verType}/${file}`, unpkg: `https://unpkg.com/${name}${verType}/${file}`,
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjsName}/${version}/${minCdnjsFile}`, cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjs_name}/${version}/${min_cdnjs_file}`,
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1]) custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
} }
result[key] = cdnSource[type] data[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) {
if (obj[i] === null) delete obj[i] 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 或更高的版本!')
throw new Error('Hexo version too old') process.exit(-1)
} }
// 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'")
throw new Error('Deprecated configuration file') process.exit(-1)
} }
} }
} }

View File

@@ -5,64 +5,55 @@
'use strict' 'use strict'
hexo.extend.generator.register('post', locals => { hexo.extend.generator.register('post', locals => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i const previousIndexes = []
const { post_asset_folder: postAssetFolder } = hexo.config
const { cover: { default_cover: defaultCover } } = hexo.theme.config
function * createCoverGenerator () { const getRandomCover = defaultCover => {
if (!defaultCover) { if (!defaultCover) return false
while (true) yield false if (!Array.isArray(defaultCover)) return defaultCover
}
if (!Array.isArray(defaultCover)) {
while (true) yield defaultCover
}
const coverCount = defaultCover.length const coverCount = defaultCover.length
if (coverCount === 1) { if (coverCount === 1) {
while (true) yield defaultCover[0] return defaultCover[0]
} }
const maxHistory = Math.min(3, coverCount - 1) const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
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))
history.push(index) previousIndexes.push(index)
if (history.length > maxHistory) history.shift() if (previousIndexes.length > maxPreviousIndexes) {
previousIndexes.shift()
yield defaultCover[index]
} }
return defaultCover[index]
} }
const coverGenerator = createCoverGenerator()
const handleImg = data => { const handleImg = data => {
let { cover: coverVal, top_img: topImg, pagination_cover: paginationCover } = data const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
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 (postAssetFolder) { if (hexo.config.post_asset_folder) {
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 randomCover = coverGenerator.next().value const { cover: { default_cover: defaultCover } } = hexo.theme.config
const randomCover = getRandomCover(defaultCover)
data.cover = randomCover data.cover = randomCover
coverVal = randomCover coverVal = randomCover // update coverVal
} }
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) { if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
@@ -72,6 +63,7 @@ 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,43 +17,56 @@ 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 ''
const archives = new Map() // Use reduce for more efficient data processing
site.posts.forEach(post => { const data = site.posts.sort('date', order).reduce((acc, post) => {
const date = post.date let date = post.date.clone()
if (timezone) date = date.tz(timezone)
const year = date.year() const year = date.year()
const month = date.month() + 1 const month = date.month() + 1
const key = type === 'yearly' ? year : `${year}-${month}`
if (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)
delete item.date // Clean up // Find or create archive entry
}) const lastEntry = acc[acc.length - 1]
if (type === 'yearly') {
const existingYearIndex = acc.findIndex(entry => entry.year === year)
if (existingYearIndex !== -1) {
acc[existingYearIndex].count++
} else {
// 否則創建新條目
acc.push({
name: date.format(format),
year,
month,
count: 1
})
}
} else {
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,33 +19,15 @@ 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 categoryMap = new Map() const prepareQuery = parent => {
categories.forEach(cat => { const query = parent ? { parent } : { parent: { $exists: false } }
if (cat.length) { return categories.find(query).sort(orderby, order).filter(cat => 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
} }
for (const list of categoryMap.values()) { const hierarchicalList = (remaining, level = 0, parent) => {
list.sort(sortFn)
}
const hierarchicalList = (remaining, level = 0, parentId = 'root') => {
let result = '' let result = ''
if (remaining > 0 && categoryMap.has(parentId)) { if (remaining > 0) {
categoryMap.get(parentId).forEach(cat => { prepareQuery(parent).forEach(cat => {
if (remaining > 0) { if (remaining > 0) {
remaining -= 1 remaining -= 1
let child = '' let child = ''
@@ -55,8 +37,7 @@ hexo.extend.helper.register('aside_categories', function (categories, options =
remaining = childList.remaining remaining = childList.remaining
} }
const isTopLevel = parentId === 'root' const parentClass = isExpand && !parent && child ? 'parent' : ''
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>`
@@ -65,7 +46,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 && isTopLevel && child) { if (isExpand && !parent && child) {
result += `<i class="fas fa-caret-left ${expandClass}"></i>` result += `<i class="fas fa-caret-left ${expandClass}"></i>`
} }

View File

@@ -8,29 +8,28 @@ 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 date = post.date const keyYear = getKey(post, 'year')
const y = date.year() const keyMonth = getKey(post, 'month')
const m = date.month() + 1 const keyDay = getKey(post, 'day')
const d = date.date()
if (yearly) { if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
const keyYear = `${y}` if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
map.set(keyYear, (map.get(keyYear) || 0) + 1) if (daily) map.set(keyDay, (map.get(keyDay) || 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,12 +5,6 @@ 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 => {
@@ -19,67 +13,39 @@ 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', custom_colors } = options let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = 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 r = Math.floor(Math.random() * 201) const randomColor = () => Math.floor(Math.random() * 201)
const g = Math.floor(Math.random() * 201) const r = randomColor()
const b = Math.floor(Math.random() * 201) const g = randomColor()
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 normalizeColors = input => { const generateStyle = (size, unit, page) => {
if (!input) return null if (page === 'tags') {
if (typeof input === 'string') { return `font-size: ${parseFloat(size.toFixed(2)) + unit}; background-color: ${getRandomColor()};`
const color = input.trim() } else {
return color ? [color] : null return `font-size: ${parseFloat(size.toFixed(2)) + unit}; color: ${getRandomColor()};`
} }
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 userColors = normalizeColors(custom_colors) const length = sizes.length - 1
const result = source.sort(orderby, order).map(tag => {
const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}` const ratio = length ? sizes.indexOf(tag.length) / length : 0
const generateStyle = (size, unit, page, color) => {
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
}
return source.sort(orderby, order).map((tag, idx) => {
const ratio = length ? sizeMap.get(tag.length) / length : 0
const size = minfontsize + ((maxfontsize - minfontsize) * ratio) const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
const style = generateStyle(size, unit, 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) {
@@ -111,7 +77,7 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
if (result) return result if (result) return result
} }
if (archiveRegex.test(m[key])) { if (/\/archives\//.test(m[key])) {
return key return key
} }
} }
@@ -123,9 +89,13 @@ 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) || simpleFilePattern.test(path)) { } else if (absoluteUrlPattern.test(path) || relativeUrlPattern.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};`
@@ -134,34 +104,39 @@ 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
processedData.sort((a, b) => Date.parse(b.date) - Date.parse(a.date)) const orderByDate = data => data.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
if (limit && limit.type === 'num' && limit.value > 0) { const limitData = data => {
processedData = processedData.slice(0, limit.value) if (limit && limit.type === 'num' && limit.value > 0) {
} else if (limit && limit.type === 'date' && isValidDate(limit.value)) { return data.slice(0, limit.value)
const limitDate = Date.parse(limit.value) } else if (limit && limit.type === 'date' && isValidDate(limit.value)) {
processedData = processedData.filter(item => Date.parse(item.date) >= limitDate) const limitDate = Date.parse(limit.value)
return data.filter(item => Date.parse(item.date) >= limitDate)
}
return data
} }
orderByDate(data)
finalResult = limitData(data)
// This is a hack method, because hexo treats time as UTC time // This is a hack method, because hexo treats time as UTC time
// so you need to manually convert the time zone // so you need to manually convert the time zone
processedData.forEach(item => { finalResult.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 processedData return finalResult
}) })
hexo.extend.helper.register('getPageType', (page, isHome) => { hexo.extend.helper.register('getPageType', (page, isHome) => {

View File

@@ -9,22 +9,14 @@
const { postDesc } = require('../common/postDesc') const { postDesc } = require('../common/postDesc')
hexo.extend.helper.register('related_posts', function (currentPost) { hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
const relatedPosts = new Map() let relatedPosts = []
const tagsData = currentPost.tags const tagsData = currentPost.tags
tagsData.length && tagsData.forEach(function (tag) {
if (!tagsData || !tagsData.length) return '' allPosts.forEach(function (post) {
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)
relatedPosts.set(post.path, { const relatedPost = {
title: post.title, title: post.title,
path: post.path, path: post.path,
cover: post.cover, cover: post.cover,
@@ -32,17 +24,22 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
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.size === 0) { if (relatedPosts.length === 0) {
return '' return ''
} }
let result = ''
const hexoConfig = hexo.config const hexoConfig = hexo.config
const config = hexo.theme.config const config = hexo.theme.config
@@ -50,42 +47,51 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
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')
const relatedPostsList = Array.from(relatedPosts.values()).sort((a, b) => { relatedPosts = relatedPosts.sort(compare('weight'))
if (b.weight !== a.weight) {
return b.weight - a.weight
}
return b.random - a.random
})
let result = '<div class="relatedPosts">' if (relatedPosts.length > 0) {
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>` result += '<div class="relatedPosts">'
result += '<div class="relatedPosts-list">' result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPostsList.length, limitNum); i++) { for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i] let { cover, title, path, cover_type, created, updated, postDesc } = relatedPosts[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)
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc' const className = postDesc ? 'pagination-related' : 'pagination-related no-desc'
result += `<a class="${className}" href="${url_for(path)}" title="${title}">` result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
if (cover_type === 'img') { if (cover_type === 'img') {
result += `<img class="cover" src="${url_for(cover)}" alt="cover">` result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
} else { } else {
result += `<div class="cover" style="background: ${cover}"></div>` result += `<div class="cover" style="background: ${cover}"></div>`
} }
if (dateType === 'created') { if (dateType === 'created') {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>` result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="far fa-calendar-alt fa-fw"></i> ${date(created, hexoConfig.date_format)}</div>`
} else { } else {
result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>` result += `<div class="info text-center"><div class="info-1"><div class="info-item-1"><i class="fas fa-history fa-fw"></i> ${date(updated, hexoConfig.date_format)}</div>`
} }
result += `<div class="info-item-2">${title}</div></div>` result += `<div class="info-item-2">${title}</div></div>`
if (postDesc) { if (postDesc) {
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>` result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>`
}
result += '</div></a>'
} }
result += '</div></a>'
result += '</div></div>'
return result
} }
result += '</div></div>'
return result
}) })
function isTagRelated (tagName, tags) {
return tags.some(tag => tag.name === tagName)
}
function findItem (arrayToSearch, attr, val) {
return arrayToSearch.findIndex(item => item[attr] === val)
}
function compare (attr) {
return (a, b) => b[attr] - a[attr]
}

View File

@@ -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.bg-animation #web_bg
animation: to_show 4s animation: to_show 4s
#ribbon-canvas #ribbon-canvas

View File

@@ -24,14 +24,6 @@ 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'
@@ -96,11 +88,6 @@ $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
@@ -126,10 +113,6 @@ $code-block
margin: 2px 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
@@ -145,7 +128,7 @@ $code-block
if !$highlight_macstyle if !$highlight_macstyle
& > .macStyle & > .macStyle
margin: 0 padding: 0
.code-lang .code-lang
flex: 1 1 auto flex: 1 1 auto
@@ -199,7 +182,6 @@ if $highlight_macstyle
.highlight-tools .highlight-tools
.macStyle .macStyle
display: flex display: flex
padding: 3px
& > * & > *
margin-right: 8px margin-right: 8px

View File

@@ -1,6 +1,11 @@
figure.highlight figure.highlight
table table
@extend $scrollbar-style // scrollbar - firefox
@-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,7 +6,12 @@ if $highlight_theme != false
.container .container
pre[class*='language-'] pre[class*='language-']
@extend $scrollbar-style // scrollbar - firefox
@-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,8 +37,6 @@
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,55 +65,12 @@ 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
@@ -226,6 +183,5 @@ if hexo-config('math.use')
opacity: 1 opacity: 1
+maxWidth768() +maxWidth768()
.fancybox__toolbar__column.is-middle, .fancybox__toolbar__column.is-middle
.f-carousel__toolbar__column.is-middle display: none
visibility: hidden

View File

@@ -130,7 +130,6 @@ 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,
@@ -141,9 +140,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(.88) contrast(.95) filter: blur(0) brightness(.8)
else else
filter: brightness(.88) contrast(.95) filter: brightness(.8)
#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,7 +42,6 @@ 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

@@ -39,26 +39,11 @@
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
&::-webkit-details-marker & > .toggle-content
display: none margin: 30px 24px
&::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,11 @@ 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 isNotHighlightJs = plugin !== 'highlight.js' const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
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 ? '<i class="fas fa-paste copy-button"></i>' : '' const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
@@ -87,17 +84,10 @@ 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 finalTop = buttonRect.top + scrollTop - 40
const finalLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
// X-axis boundary check const topValue = ele.closest('figure.highlight').classList.contains('code-fullpage') ? finalTop + 60 : finalTop
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 = ` newEle.style.cssText = `
top: ${topValue + 10}px; top: ${topValue + 10}px;
@@ -121,7 +111,6 @@ document.addEventListener('DOMContentLoaded', () => {
}, 800) }, 800)
} }
} }
const copy = async (text, ctx) => { const copy = async (text, ctx) => {
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
@@ -136,7 +125,7 @@ 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 = isNotHighlightJs ? 'pre code' : 'table .code pre' const preCodeSelector = isPrismjs ? '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) copy(codeElement.innerText, clickEle)
@@ -166,7 +155,6 @@ 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 = () => {
@@ -216,23 +204,20 @@ document.addEventListener('DOMContentLoaded', () => {
fragment.appendChild(ele) fragment.appendChild(ele)
} }
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild) isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
} }
$figureHighlight.forEach(item => { $figureHighlight.forEach(item => {
let langName = '' let langName = ''
if (isNotHighlightJs) { if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
const newClassName = isPrismjs ? 'prismjs' : 'default'
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
}
if (!highlightLang) { if (!highlightLang) {
createEle('', item) createEle('', item)
return return
} }
if (isNotHighlightJs) { if (isPrismjs) {
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '') langName = item.getAttribute('data-language') || 'Code'
} 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'
@@ -556,29 +541,17 @@ 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 < headerList.length; i++) { for (let i = 0; i < $articleList.length; i++) {
const item = headerList[i] const ele = $articleList[i]
if (top > item.top - 80) { if (top > btf.getEleTop(ele) - 80) {
currentId = item.id ? '#' + encodeURI(item.id) : '' const id = ele.id
currentId = id ? '#' + encodeURI(id) : ''
currentIndex = i currentIndex = i
} else { } else {
break break
@@ -656,8 +629,7 @@ document.addEventListener('DOMContentLoaded', () => {
$body.classList.add('read-mode') $body.classList.add('read-mode')
newEle.type = 'button' newEle.type = 'button'
newEle.className = 'exit-readmode' newEle.className = 'fas fa-sign-out-alt 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,35 +14,37 @@
} }
}, },
throttle: (func, wait, options = {}) => { throttle: function (func, wait, options = {}) {
let timeout, args let timeout, context, 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(...args) func.apply(context, args)
if (!timeout) args = null if (!timeout) context = args = null
} }
return (...params) => { const throttled = (...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(...args) func.apply(context, args)
if (!timeout) args = null if (!timeout) context = 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: {
@@ -167,7 +169,17 @@
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0, isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY, getEleTop: ele => {
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
@@ -178,7 +190,7 @@
} }
if (service === 'fancybox') { if (service === 'fancybox') {
ele.forEach(i => { Array.from(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 || ''