Compare commits

..
52 Commits
50 changed files with 2482 additions and 1947 deletions
+12 -4
View File
@@ -3,17 +3,25 @@ name: npm publish
on: on:
release: release:
types: [created] types: [created]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC 必須
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v1 - uses: actions/setup-node@v4
with: with:
node-version: '12.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm install - run: npm install
- run: npm publish
- run: npm publish --provenance --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+10 -1
View File
@@ -319,6 +319,7 @@ aside:
# If set 0 will show all # If set 0 will show all
limit: 40 limit: 40
color: false color: false
custom_colors:
# Order of tags, random/name/length # Order of tags, random/name/length
orderby: random orderby: random
# Sort of order. 1, asc for ascending; -1, desc for descending # Sort of order. 1, asc for ascending; -1, desc for descending
@@ -658,7 +659,7 @@ artalk:
# -------------------------------------- # --------------------------------------
chat: chat:
# Choose: chatra/tidio/crisp # Choose: chatra/tidio/crisp/knocket
# Leave it empty if you don't need chat # Leave it empty if you don't need chat
use: use:
# Chat Button [recommend] # Chat Button [recommend]
@@ -679,6 +680,10 @@ tidio:
crisp: crisp:
website_id: website_id:
# https://trtc.io/solutions/knocket
knocket:
identifier:
# -------------------------------------- # --------------------------------------
# Analysis # Analysis
# -------------------------------------- # --------------------------------------
@@ -939,6 +944,10 @@ mermaid:
theme: theme:
light: default light: default
dark: dark dark: dark
# Enable "Open in New Tab" button to view diagram in a separate window
open_in_new_tab: true
# Enable zoom and pan interactions on diagrams
zoom_pan: true
# chartjs # chartjs
# see https://www.chartjs.org/docs/latest/ # see https://www.chartjs.org/docs/latest/
+10 -14
View File
@@ -72,20 +72,16 @@
}) })
} }
let highlight = 'undefined' let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
let syntaxHighlighter = config.syntax_highlighter const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable) let highlight = JSON.stringify({
if (highlightEnable) { plugin: highlightProvider,
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks highlightCopy: copy,
highlight = JSON.stringify({ highlightLang: language,
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs', highlightHeightLimit: height_limit,
highlightCopy: copy, highlightFullpage: fullpage,
highlightLang: language, highlightMacStyle: macStyle
highlightHeightLimit: height_limit, })
highlightFullpage: fullpage,
highlightMacStyle: macStyle
})
}
script. script.
const GLOBAL_CONFIG = { const GLOBAL_CONFIG = {
+24 -21
View File
@@ -1,6 +1,6 @@
mixin indexPostUI() mixin indexPostUI()
- const indexLayout = theme.index_layout - const indexLayout = theme.index_layout
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : '' - const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
#recent-posts.recent-posts.nc(class=masonryLayoutClass) #recent-posts.recent-posts.nc(class=masonryLayoutClass)
.recent-post-items .recent-post-items
each article, index in page.posts.data each article, index in page.posts.data
@@ -8,17 +8,17 @@ mixin indexPostUI()
- const link = article.link || article.path - const link = article.link || article.path
- const title = article.title || _p('no_title') - const title = article.title || _p('no_title')
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '') - const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
- const post_cover = article.cover - const postCover = article.cover
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : '' - const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
if post_cover && theme.cover.index_enable if postCover && theme.cover.index_enable
.post_cover(class=leftOrRight) .post_cover(class=leftOrRight)
a(href=url_for(link) title=title) a(href=url_for(link) title=title)
if article.cover_type === 'img' if article.cover_type === 'img'
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title) img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
else else
div.post-bg(style=`background: ${post_cover}`) div.post-bg(style=`background: ${postCover}`)
.recent-post-info(class=no_cover) .recent-post-info(class=noCover)
a.article-title(href=url_for(link) title=title) a.article-title(href=url_for(link) title=title)
if globalPageType === 'home' && (article.top || article.sticky > 0) if globalPageType === 'home' && (article.top || article.sticky > 0)
i.fas.fa-thumbtack.sticky i.fas.fa-thumbtack.sticky
@@ -35,13 +35,13 @@ mixin indexPostUI()
span.article-meta-label=_p('post.updated') span.article-meta-label=_p('post.updated')
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format) time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
else else
- const data_type_updated = theme.post_meta.page.date_type === 'updated' - const isUpdatedType = theme.post_meta.page.date_type === 'updated'
- const date_type = data_type_updated ? 'updated' : 'date' - const dateType = isUpdatedType ? 'updated' : 'date'
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt' - const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created') - const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
i(class=date_icon) i(class=dateIcon)
span.article-meta-label= date_title span.article-meta-label= dateTitle
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format) time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format)
if theme.post_meta.page.categories && article.categories.data.length > 0 if theme.post_meta.page.categories && article.categories.data.length > 0
span.article-meta span.article-meta
span.article-meta-separator | span.article-meta-separator |
@@ -69,7 +69,10 @@ mixin indexPostUI()
span.article-meta-label= ' ' + _p('card_post_count') span.article-meta-label= ' ' + _p('card_post_count')
if theme.comments.card_post_count && theme.comments.use if theme.comments.card_post_count && theme.comments.use
case theme.comments.use[0] - const commentSystem = theme.comments.use[0]
- const commentLink = url_for(link) + '#post-comment'
case commentSystem
when 'Disqus' when 'Disqus'
when 'Disqusjs' when 'Disqusjs'
+countBlockInIndex +countBlockInIndex
@@ -77,30 +80,30 @@ mixin indexPostUI()
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Valine' when 'Valine'
+countBlockInIndex +countBlockInIndex
a(href=url_for(link) + '#post-comment') a(href=commentLink)
span.valine-comment-count(data-xid=url_for(link)) span.valine-comment-count(data-xid=url_for(link))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Waline' when 'Waline'
+countBlockInIndex +countBlockInIndex
a(href=url_for(link) + '#post-comment') a(href=commentLink)
span.waline-comment-count(data-path=url_for(link)) span.waline-comment-count(data-path=url_for(link))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Twikoo' when 'Twikoo'
+countBlockInIndex +countBlockInIndex
a.twikoo-count(href=url_for(link) + '#post-comment') a.twikoo-count(href=commentLink)
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Facebook Comments' when 'Facebook Comments'
+countBlockInIndex +countBlockInIndex
a(href=url_for(link) + '#post-comment') a(href=commentLink)
span.fb-comments-count(data-href=urlNoIndex(article.permalink)) span.fb-comments-count(data-href=urlNoIndex(article.permalink))
when 'Remark42' when 'Remark42'
+countBlockInIndex +countBlockInIndex
a(href=url_for(link) + '#post-comment') a(href=commentLink)
span.remark42__counter(data-url=urlNoIndex(article.permalink)) span.remark42__counter(data-url=urlNoIndex(article.permalink))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
when 'Artalk' when 'Artalk'
+countBlockInIndex +countBlockInIndex
a(href=url_for(link) + '#post-comment') a(href=commentLink)
span.artalk-count(data-page-key=url_for(link)) span.artalk-count(data-page-key=url_for(link))
i.fa-solid.fa-spinner.fa-spin i.fa-solid.fa-spinner.fa-spin
+133 -156
View File
@@ -54,6 +54,8 @@
(() => { (() => {
const limitConfig = !{ JSON.stringify(page.limit || {}) } const limitConfig = !{ JSON.stringify(page.limit || {}) }
const escapeHtml = str => String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date)) const sortDataByDate = data => data.sort((a, b) => new Date(b.date) - new Date(a.date))
const filterDataByLimit = (data, limit) => { const filterDataByLimit = (data, limit) => {
@@ -64,49 +66,47 @@
return data.filter(item => new Date(item.date) >= limitDate) return data.filter(item => new Date(item.date) >= limitDate)
} }
return data return data
}; }
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: !{JSON.stringify(config.timezone || '')} || Intl.DateTimeFormat().resolvedOptions().timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
const formatToTimeZone = (date) => { const formatToTimeZone = (date) => {
const fullDate = date.length === 10 ? `${date} 00:00:00` : date const fullDate = date.length === 10 ? `${date} 00:00:00` : date
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone const [day, month, year, hour, minute, second] = dateFormatter.format(new Date(fullDate)).match(/\d+/g)
const options = {
timeZone: visitorTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
.format(new Date(fullDate))
.match(/\d+/g)
return `${year}-${month}-${day} ${hour}:${minute}:${second}` return `${year}-${month}-${day} ${hour}:${minute}:${second}`
} }
const addLazyload = str => { const addLazyload = str => {
const config = { const lazyConfig = {
enable: !{Boolean(enable)}, enable: !{Boolean(enable)},
native: !{Boolean(native)}, native: !{Boolean(native)},
field: '!{field}', field: !{JSON.stringify(field || '')},
placeholder: '!{url_for(placeholder)}', placeholder: !{JSON.stringify(url_for(placeholder))},
} }
if (!config.enable || config.field !== 'site') return str if (!lazyConfig.enable || lazyConfig.field !== 'site' || str.indexOf('<img') === -1) return str
const parser = new DOMParser() const parser = new DOMParser()
const doc = parser.parseFromString(str, 'text/html') const doc = parser.parseFromString(str, 'text/html')
const images = doc.querySelectorAll('img') const images = doc.querySelectorAll('img')
images.forEach(img => { images.forEach(img => {
if (config.native) { if (lazyConfig.native) {
img.setAttribute('loading', 'lazy') img.setAttribute('loading', 'lazy')
} else { } else {
const src = img.getAttribute('src') const src = img.getAttribute('src')
img.setAttribute('data-lazy-src', src) img.setAttribute('data-lazy-src', src)
if (config.placeholder) { if (lazyConfig.placeholder) {
img.setAttribute('src', config.placeholder) img.setAttribute('src', lazyConfig.placeholder)
} else { } else {
img.removeAttribute('src') img.removeAttribute('src')
} }
@@ -119,19 +119,18 @@
const itemsPerPage = 8 const itemsPerPage = 8
let totalPages = 0 let totalPages = 0
let data = [] let data = []
let inputEventsAttached = false // Flag to mark if input event listeners have been added
const renderData = (dataSlice) => { const renderData = (dataSlice) => {
const content = dataSlice.map(item => { const content = dataSlice.map(item => {
const formattedDate = formatToTimeZone(item.date) const formattedDate = formatToTimeZone(item.date)
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || '' const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${escapeHtml(tag)}</span>`).join('') || ''
const commentButton = item.key && !{commentsJsLoad} const commentButton = item.key && !{commentsJsLoad || false}
? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)"> ? `<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
<i class="fa-solid fa-comments"></i> <i class="fa-solid fa-comments"></i>
</div>` </div>`
: '' : ''
const commentContainer = item.key const commentContainer = item.key
? `<div class="shuoshuo-comment no-comment" data-key="${item.key}"></div>` ? `<div class="shuoshuo-comment no-comment" data-key="${escapeHtml(item.key)}"></div>`
: '' : ''
return ` return `
@@ -139,10 +138,10 @@
<div class="container"> <div class="container">
<div class="shuoshuo-item-header"> <div class="shuoshuo-item-header">
<div class="shuoshuo-avatar"> <div class="shuoshuo-avatar">
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}"> <img class="no-lightbox" src="${item.avatar ? escapeHtml(item.avatar) : !{JSON.stringify(url_for(theme.avatar.img))}}">
</div> </div>
<div class="shuoshuo-info"> <div class="shuoshuo-info">
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div> <div class="shuoshuo-author">${item.author ? escapeHtml(item.author) : !{JSON.stringify(config.author || '')}}</div>
<time class="shuoshuo-date" title="${formattedDate}"> <time class="shuoshuo-date" title="${formattedDate}">
${btf.diffDate(formattedDate, true)} ${btf.diffDate(formattedDate, true)}
</time> </time>
@@ -165,70 +164,94 @@
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
} }
const setupNavEvents = (nav) => {
nav.querySelector('.shuoshuo-prev-btn').addEventListener('click', () => {
if (currentPage > 1) { currentPage--; renderPage(currentPage) }
})
nav.querySelector('.shuoshuo-next-btn').addEventListener('click', () => {
if (currentPage < totalPages) { currentPage++; renderPage(currentPage) }
})
const input = nav.querySelector('.shuoshuo-page-input')
input.addEventListener('focus', e => { e.target.placeholder = '' })
input.addEventListener('blur', e => {
if (!e.target.value.trim()) e.target.placeholder = currentPage
})
input.addEventListener('input', e => {
const value = parseInt(e.target.value) || 0
let wasInvalid = false
if (value > totalPages) { e.target.value = totalPages; wasInvalid = true }
else if (value < 1 && e.target.value !== '') { e.target.value = 1; wasInvalid = true }
if (wasInvalid) {
e.target.classList.add('invalid')
setTimeout(() => e.target.classList.remove('invalid'), 500)
}
})
input.addEventListener('keydown', e => {
const value = e.target.value + e.key
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete' ||
e.key === 'ArrowLeft' || e.key === 'ArrowRight' ||
e.key === 'Tab' || e.ctrlKey || e.metaKey) {
if (e.key === 'Enter') {
const inputValue = e.target.value.trim()
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
currentPage = inputPage
renderPage(currentPage)
} else if (inputValue === '') {
renderPage(currentPage)
}
}
return
}
if (!/^\d$/.test(e.key)) {
e.preventDefault()
return
}
const newValue = parseInt(value) || 0
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
e.preventDefault()
e.target.classList.add('invalid')
setTimeout(() => e.target.classList.remove('invalid'), 500)
}
})
}
const renderNavigation = () => { const renderNavigation = () => {
const container = document.getElementById('article-container') const container = document.getElementById('article-container')
const existingNav = container.nextElementSibling let nav = container.nextElementSibling
if (existingNav && existingNav.classList.contains('shuoshuo-navigation')) {
existingNav.remove()
}
const pageInfoTemplate = '#{__('pagination.page_info')}' const pageInfoTemplate = '#{__('pagination.page_info')}'
const pageInfoText = pageInfoTemplate const pageInfoText = pageInfoTemplate
.replace(/\$\{current}/g, currentPage) .replace(/\$\{current}/g, currentPage)
.replace(/\$\{total}/g, totalPages) .replace(/\$\{total}/g, totalPages)
const navHtml = ` if (!nav || !nav.classList.contains('shuoshuo-navigation')) {
<div class="shuoshuo-navigation"> nav = document.createElement('div')
<button onclick="window.shuoshuoPrevPage()" ${currentPage === 1 ? 'disabled' : ''}><i class="fa-solid fa-chevron-left"></i></button> nav.className = 'shuoshuo-navigation'
<span class="shuoshuo-page-info">${pageInfoText}</span> nav.innerHTML = `
<input type="number" class="shuoshuo-page-input" min="1" max="${totalPages}" placeholder="${currentPage}" onkeydown="window.shuoshuoHandleKeyDown(event)"> <button class="shuoshuo-prev-btn"><i class="fa-solid fa-chevron-left"></i></button>
<button onclick="window.shuoshuoNextPage()" ${currentPage === totalPages ? 'disabled' : ''}><i class="fa-solid fa-chevron-right"></i></button> <span class="shuoshuo-page-info"></span>
</div> <input type="number" class="shuoshuo-page-input" min="1">
` <button class="shuoshuo-next-btn"><i class="fa-solid fa-chevron-right"></i></button>
container.insertAdjacentHTML('afterend', navHtml) `
container.insertAdjacentElement('afterend', nav)
// Add input validation event listeners (only once) setupNavEvents(nav)
if (!inputEventsAttached) {
setTimeout(() => {
const input = document.querySelector('.shuoshuo-page-input')
if (input) {
// Clear placeholder when clicking the input box
input.addEventListener('focus', (event) => {
event.target.placeholder = ''
})
// Restore placeholder if no content when losing focus
input.addEventListener('blur', (event) => {
if (!event.target.value.trim()) {
event.target.placeholder = currentPage
}
})
input.addEventListener('input', (event) => {
const value = parseInt(event.target.value) || 0
let wasInvalid = false
if (value > totalPages) {
event.target.value = totalPages
wasInvalid = true
} else if (value < 1 && event.target.value !== '') {
event.target.value = 1
wasInvalid = true
}
// If value is corrected, show red and shake effect
if (wasInvalid) {
event.target.classList.add('invalid')
setTimeout(() => {
event.target.classList.remove('invalid')
}, 500)
}
})
inputEventsAttached = true // Mark that event listeners have been added
}
}, 0)
} }
nav.querySelector('.shuoshuo-page-info').textContent = pageInfoText
nav.querySelector('.shuoshuo-prev-btn').disabled = currentPage === 1
nav.querySelector('.shuoshuo-next-btn').disabled = currentPage === totalPages
const input = nav.querySelector('.shuoshuo-page-input')
input.max = totalPages
input.placeholder = currentPage
} }
const renderPage = (page) => { const renderPage = (page) => {
@@ -239,79 +262,14 @@
renderNavigation() renderNavigation()
} }
window.shuoshuoPrevPage = () => {
if (currentPage > 1) {
currentPage--
renderPage(currentPage)
}
}
window.shuoshuoNextPage = () => {
if (currentPage < totalPages) {
currentPage++
renderPage(currentPage)
}
}
window.shuoshuoGoToPage = (page) => {
if (typeof page === 'number') {
// Directly jump to the specified page
if (page >= 1 && page <= totalPages && page !== currentPage) {
currentPage = page
renderPage(currentPage)
}
} else {
// Get page from input box
const input = document.querySelector('.shuoshuo-page-input')
const inputValue = input.value.trim()
const inputPage = inputValue === '' ? currentPage : parseInt(inputValue)
if (inputPage >= 1 && inputPage <= totalPages && inputPage !== currentPage) {
currentPage = inputPage
renderPage(currentPage)
} else if (inputValue === '') {
// If input box is empty, re-render current page (update placeholder)
renderPage(currentPage)
}
}
}
window.shuoshuoHandleKeyDown = (event) => {
const input = event.target
const value = input.value + event.key
// Allow delete, arrow keys, backspace, etc.
if (event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Delete' ||
event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
event.key === 'Tab' || event.ctrlKey || event.metaKey) {
if (event.key === 'Enter') {
window.shuoshuoGoToPage()
}
return
}
// Only allow numbers
if (!/^\d$/.test(event.key)) {
event.preventDefault()
return
}
// Check if the value after input exceeds the range
const newValue = parseInt(value) || 0
if (newValue > totalPages || (value.length > 1 && newValue === 0)) {
event.preventDefault()
// Add red and shake effect
input.classList.add('invalid')
setTimeout(() => {
input.classList.remove('invalid')
}, 500)
}
}
const loadShuoshuo = async () => { const loadShuoshuo = async () => {
const container = document.getElementById('article-container')
try { try {
let originData = [] let originData = []
if (!{Boolean(page.shuoshuo_url)}) { if (!{Boolean(page.shuoshuo_url)}) {
container.innerHTML = '<div class="shuoshuo-loading"><i class="fa-solid fa-circle-notch fa-spin"></i></div>'
const response = await fetch('!{url_for(page.shuoshuo_url)}') const response = await fetch('!{url_for(page.shuoshuo_url)}')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
originData = await response.json() originData = await response.json()
} else { } else {
const dataElement = document.getElementById('shuoshuo-data') const dataElement = document.getElementById('shuoshuo-data')
@@ -319,14 +277,33 @@
} }
data = filterDataByLimit(sortDataByDate(originData), limitConfig) data = filterDataByLimit(sortDataByDate(originData), limitConfig)
totalPages = Math.ceil(data.length / itemsPerPage) totalPages = Math.ceil(data.length / itemsPerPage)
if (data.length === 0) {
container.innerHTML = '<div class="shuoshuo-empty"></div>'
return
}
renderPage(currentPage) renderPage(currentPage)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
container.innerHTML = '<div class="shuoshuo-error"><i class="fa-solid fa-circle-exclamation"></i></div>'
} }
}; }
window.pjax ? loadShuoshuo() : window.addEventListener('load', loadShuoshuo) const pjaxCleanup = () => {
})() const container = document.getElementById('article-container')
if (container) {
const nav = container.nextElementSibling
if (nav && nav.classList.contains('shuoshuo-navigation')) nav.remove()
}
document.removeEventListener('pjax:send', pjaxCleanup)
}
if (window.pjax) {
document.addEventListener('pjax:send', pjaxCleanup)
loadShuoshuo()
} else {
window.addEventListener('load', loadShuoshuo)
}
})()
+1 -1
View File
@@ -1,2 +1,2 @@
.tag-cloud-list.text-center .tag-cloud-list.text-center
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'}) !=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors})
+6 -3
View File
@@ -2,8 +2,11 @@ script.
(() => { (() => {
const abcjsInit = () => { const abcjsInit = () => {
const abcjsFn = () => { const abcjsFn = () => {
const $article = document.getElementById("article-container")
if (!$article || $article.querySelector(".hbe-container")) return
setTimeout(() => { setTimeout(() => {
const sheets = document.querySelectorAll(".abc-music-sheet") const sheets = $article.querySelectorAll(".abc-music-sheet")
for (let i = 0; i < sheets.length; i++) { for (let i = 0; i < sheets.length; i++) {
const ele = sheets[i] const ele = sheets[i]
if (ele.children.length > 0) continue if (ele.children.length > 0) continue
@@ -24,12 +27,12 @@ script.
const options = { ...params, responsive: "resize" } const options = { ...params, responsive: "resize" }
// Render the music score using ABCJS.renderAbc // Render the music score using ABCJS.renderAbc
ABCJS.renderAbc(ele, ele.innerHTML, options) ABCJS.renderAbc(ele, ele.textContent, options)
} }
}, 100) }, 100)
} }
if (typeof ABCJS === "object") { if (typeof ABCJS === "object" && ABCJS !== null) {
abcjsFn() abcjsFn()
} else { } else {
btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn) btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn)
+1 -1
View File
@@ -8,7 +8,7 @@ script.
const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' }) const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' })
const result = await res.json() const result = await res.json()
result.data.forEach((count, index) => { result.data.forEach((count, index) => {
eleGroup[index].textContent = count eleGroup[index].textContent = count
}) })
+13 -7
View File
@@ -6,10 +6,14 @@ script.
(window.Chatra.q = window.Chatra.q || []).push(arguments) (window.Chatra.q = window.Chatra.q || []).push(arguments)
} }
btf.getScript('https://call.chatra.io/chatra.js').then(() => { const isChatBtn = !{theme.chat.rightside_button}
const isChatBtn = !{theme.chat.rightside_button} const isChatHideShow = !{theme.chat.button_hide_show}
const isChatHideShow = !{theme.chat.button_hide_show}
if (isChatBtn) {
window.ChatraSetup = { startHidden: true }
}
btf.getScript('https://call.chatra.io/chatra.js').then(() => {
if (isChatBtn) { if (isChatBtn) {
const close = () => { const close = () => {
Chatra('minimizeWidget') Chatra('minimizeWidget')
@@ -21,11 +25,13 @@ script.
Chatra('show') Chatra('show')
} }
window.ChatraSetup = { startHidden: true } window.chatBtnFn = () => {
const el = document.getElementById('chatra')
window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open() return el && el.classList.contains('chatra--expanded') ? close() : open()
}
document.getElementById('chat-btn').style.display = 'block' const chatBtn = document.getElementById('chat-btn')
if (chatBtn) chatBtn.style.display = 'block'
} else if (isChatHideShow) { } else if (isChatHideShow) {
window.chatBtn = { window.chatBtn = {
hide: () => Chatra('hide'), hide: () => Chatra('hide'),
+2 -1
View File
@@ -21,7 +21,8 @@ script.
window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open() window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open()
document.getElementById('chat-btn').style.display = 'block' const chatBtn = document.getElementById('chat-btn')
if (chatBtn) chatBtn.style.display = 'block'
} else if (isChatHideShow) { } else if (isChatHideShow) {
window.chatBtn = { window.chatBtn = {
hide: () => $crisp.push(["do", "chat:hide"]), hide: () => $crisp.push(["do", "chat:hide"]),
+4 -2
View File
@@ -3,5 +3,7 @@ case theme.chat.use
include ./chatra.pug include ./chatra.pug
when 'tidio' when 'tidio'
include ./tidio.pug include ./tidio.pug
when 'crisp' when 'crisp'
include ./crisp.pug include ./crisp.pug
when 'knocket'
include ./knocket.pug
+42
View File
@@ -0,0 +1,42 @@
//- Knocket live chat — https://trtc.io/solutions/knocket
script.
(() => {
const identifier = !{JSON.stringify(theme.knocket.identifier || '').replace(/</g, '\\u003c')}
if (!identifier) return
btf.getScript(`https://trtc.io/knocket-sdk/sdk.js?identifier=${encodeURIComponent(identifier)}`).then(() => {
const isChatBtn = !{theme.chat.rightside_button}
const isChatHideShow = !{theme.chat.button_hide_show}
let widget = null
const getWidget = () => widget || (widget = document.getElementById('contact-widget-auto'))
if (isChatBtn) {
const hide = () => { const w = getWidget(); if (w) w.style.display = 'none' }
const show = () => { const w = getWidget(); if (w) w.style.display = '' }
// Hide the native Knocket floating button
const observer = new MutationObserver(() => {
if (getWidget()) { hide(); observer.disconnect() }
})
observer.observe(document.body, { childList: true, subtree: true })
hide()
window.chatBtnFn = () => {
const w = getWidget()
if (!w) return
w.style.display = w.style.display === 'none' ? '' : 'none'
}
const chatBtn = document.getElementById('chat-btn')
if (chatBtn) chatBtn.style.display = 'block'
} else if (isChatHideShow) {
window.chatBtn = {
hide: () => { const w = getWidget(); if (w) w.style.display = 'none' },
show: () => { const w = getWidget(); if (w) w.style.display = '' }
}
}
}).catch(error => {
console.warn('[Knocket] Failed to load the chat SDK.', error)
})
})()
+3 -3
View File
@@ -10,7 +10,7 @@ script.
window.tidioChatApi.hide() window.tidioChatApi.hide()
isShow = false isShow = false
} }
const open = () => { const open = () => {
window.tidioChatApi.open() window.tidioChatApi.open()
window.tidioChatApi.show() window.tidioChatApi.show()
@@ -32,8 +32,8 @@ script.
isShow ? close() : open() isShow ? close() : open()
} }
document.getElementById('chat-btn').style.display = 'block' const chatBtn = document.getElementById('chat-btn')
if (chatBtn) chatBtn.style.display = 'block'
} else if (isChatHideShow) { } else if (isChatHideShow) {
window.chatBtn = { window.chatBtn = {
hide: () => window.tidioChatApi && window.tidioChatApi.hide(), hide: () => window.tidioChatApi && window.tidioChatApi.hide(),
+1 -1
View File
@@ -3,7 +3,7 @@
script. script.
(() => { (() => {
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo' const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const dqOption = !{JSON.stringify(dqOption)} const dqOption = !{JSON.stringify(dqOption)}
const destroyDisqusjs = () => { const destroyDisqusjs = () => {
+1 -1
View File
@@ -3,7 +3,7 @@
script. script.
(()=>{ (()=>{
const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo' const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'
const loadFBComment = (el = document, path) => { const loadFBComment = (el = document, path) => {
if (isShuoshuo) { if (isShuoshuo) {
+20 -17
View File
@@ -2,7 +2,22 @@
- const { tags, enableMenu } = theme.math.mathjax - const { tags, enableMenu } = theme.math.mathjax
script. script.
(() => { (() => {
const changeScriptToMath = article => {
article.querySelectorAll('script[type^="math/tex"]').forEach(el => {
const display = /mode=display/.test(el.type)
const node = document.createElement(display ? 'div' : 'span')
node.textContent = display
? `$$${el.textContent}$$`
: `$${el.textContent}$`
el.parentNode.replaceChild(node, el)
})
}
const loadMathjax = () => { const loadMathjax = () => {
const article = document.getElementById('article-container')
if (!article || article.querySelector('.hbe-container')) return
changeScriptToMath(article)
if (!window.MathJax) { if (!window.MathJax) {
window.MathJax = { window.MathJax = {
loader: { loader: {
@@ -11,7 +26,8 @@ script.
//- '[tex]/bbm', //- '[tex]/bbm',
//- '[tex]/bboldx', //- '[tex]/bboldx',
//- '[tex]/dsfont', //- '[tex]/dsfont',
'[tex]/mhchem' '[tex]/mhchem',
'ui/lazy'
], ],
paths: { paths: {
'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font', 'mathjax-newcm': '[mathjax]/../@mathjax/mathjax-newcm-font',
@@ -39,25 +55,13 @@ script.
scale: 1.1 scale: 1.1
}, },
options: { options: {
lazyMargin: '200px',
enableMenu: !{enableMenu}, enableMenu: !{enableMenu},
menuOptions: { menuOptions: {
settings: { settings: {
enrich: false // Turn off Braille and voice narration text automatic generation enrich: false // Turn off Braille and voice narration text automatic generation
} }
}, },
renderActions: {
findScript: [10, doc => {
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
const display = !!node.type.match(/; *mode=display/)
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display)
const text = document.createTextNode('')
node.parentNode.replaceChild(text, node)
math.start = {node: text, delim: '', n: 0}
math.end = {node: text, delim: '', n: 0}
doc.math.push(math)
}
}, '']
}
} }
} }
@@ -67,9 +71,8 @@ script.
script.async = true script.async = true
document.head.appendChild(script) document.head.appendChild(script)
} else { } else {
MathJax.startup.document.state(0) MathJax.typesetClear()
MathJax.texReset() MathJax.typesetPromise([ article ])
MathJax.typesetPromise()
} }
} }
+319 -10
View File
@@ -1,12 +1,299 @@
script. script.
(() => { (() => {
const parseViewBox = viewBox => {
if (!viewBox) return null
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
return parts
}
const getSvgViewBox = svg => {
const attr = parseViewBox(svg.getAttribute('viewBox'))
if (attr) return attr
// Fallback: use bbox to build a viewBox
try {
const bbox = svg.getBBox()
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
} catch (e) {
// getBBox may fail on some edge cases; ignore
}
const w = Number(svg.getAttribute('width')) || 0
const h = Number(svg.getAttribute('height')) || 0
if (w > 0 && h > 0) return [0, 0, w, h]
return [0, 0, 100, 100]
}
const setSvgViewBox = (svg, vb) => {
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
}
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
const openSvgInNewTab = ({ source, initViewBox }) => {
const getClonedSvg = () => {
if (typeof source === 'string') {
const template = document.createElement('template')
template.innerHTML = source.trim()
const svg = template.content.querySelector('svg')
return svg ? svg.cloneNode(true) : null
}
if (source && typeof source.cloneNode === 'function') {
return source.cloneNode(true)
}
return null
}
const clone = getClonedSvg()
if (!clone) return
if (initViewBox && initViewBox.length === 4) {
clone.setAttribute('viewBox', initViewBox.join(' '))
}
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
if (!clone.getAttribute('xmlns:xlink') && clone.innerHTML.includes('xlink:')) {
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
}
// inject background to match current theme
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff')
if (!clone.style.background) clone.style.background = bg
const serializer = new XMLSerializer()
const svgSource = serializer.serializeToString(clone)
const htmlSource = `<!doctype html><html><head><meta charset="utf-8" />
<style>
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
</style>
</head><body>${svgSource}</body></html>`
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener')
setTimeout(() => URL.revokeObjectURL(url), 5000)
}
const attachMermaidViewerButton = wrap => {
let btn = wrap.querySelector('.mermaid-open-btn')
if (!btn) {
btn = document.createElement('button')
btn.type = 'button'
btn.className = 'mermaid-open-btn'
wrap.appendChild(btn)
}
btn.innerHTML = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
if (!btn.__mermaidViewerBound) {
btn.addEventListener('click', e => {
e.preventDefault()
e.stopPropagation()
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
if (!svg) return
const initViewBox = wrap.__mermaidInitViewBox
openSvgInNewTab({ source: svg, initViewBox })
})
btn.__mermaidViewerBound = true
}
}
// Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates)
const zoomAtPoint = (vb, factor, px, py) => {
const w = vb[2] * factor
const h = vb[3] * factor
const nx = px - (px - vb[0]) * factor
const ny = py - (py - vb[1]) * factor
return [nx, ny, w, h]
}
const initMermaidGestures = wrap => {
// Clean up previous event listeners and pending frames
if (wrap.__mermaidAbortController) {
wrap.__mermaidAbortController.abort()
if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId)
}
const ac = new AbortController()
wrap.__mermaidAbortController = ac
const svg = wrap.querySelector('svg')
if (!svg) return
// Ensure viewBox exists so gestures always work
const initVb = getSvgViewBox(svg)
wrap.__mermaidInitViewBox = initVb
wrap.__mermaidCurViewBox = initVb.slice()
setSvgViewBox(svg, initVb)
// Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chrome
svg.style.touchAction = 'none'
// Cache BoundingClientRect, throttled on scroll to reduce reflow
let cachedRect = svg.getBoundingClientRect()
let rectDirty = false
const markRectDirty = () => { rectDirty = true }
window.addEventListener('resize', markRectDirty, { signal: ac.signal })
window.addEventListener('scroll', markRectDirty, { signal: ac.signal, capture: true })
const getRect = () => {
if (rectDirty) {
cachedRect = svg.getBoundingClientRect()
rectDirty = false
}
return cachedRect
}
// Precompute clamp bounds from initial viewBox
const minW = initVb[2] * 0.1
const maxW = initVb[2] * 10
const minH = initVb[3] * 0.1
const maxH = initVb[3] * 10
const clampVb = vb => {
const out = vb.slice()
out[2] = clamp(out[2], minW, maxW)
out[3] = clamp(out[3], minH, maxH)
return out
}
// Throttle DOM updates using requestAnimationFrame
let pendingVb = null
let rafId = null
const applyVb = () => {
if (pendingVb) {
wrap.__mermaidCurViewBox = pendingVb
setSvgViewBox(svg, pendingVb)
pendingVb = null
}
rafId = null
wrap.__mermaidRafId = null
}
const setCurVb = vb => {
pendingVb = clampVb(vb)
if (!rafId) {
rafId = requestAnimationFrame(applyVb)
wrap.__mermaidRafId = rafId
}
}
const state = {
pointers: new Map(),
startVb: null,
startDist: 0,
lastPointerX: 0,
lastPointerY: 0
}
const onPointerDown = e => {
if (e.pointerType === 'mouse' && e.button !== 0) return
svg.setPointerCapture(e.pointerId)
const curVb = wrap.__mermaidCurViewBox
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
if (state.pointers.size === 1) {
state.startVb = curVb.slice()
state.lastPointerX = e.clientX
state.lastPointerY = e.clientY
} else if (state.pointers.size === 2) {
const pts = [...state.pointers.values()]
const dx = pts[0].x - pts[1].x
const dy = pts[0].y - pts[1].y
state.startDist = Math.hypot(dx, dy)
state.startVb = curVb.slice()
}
}
const onPointerMove = e => {
if (!state.pointers.has(e.pointerId)) return
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
const curVb = wrap.__mermaidCurViewBox
const rect = getRect()
if (state.pointers.size === 1 && state.startVb) {
const p = state.pointers.values().next().value
const dxClient = p.x - state.lastPointerX
const dyClient = p.y - state.lastPointerY
state.lastPointerX = p.x
state.lastPointerY = p.y
const dx = dxClient * (curVb[2] / rect.width)
const dy = dyClient * (curVb[3] / rect.height)
setCurVb([curVb[0] - dx, curVb[1] - dy, curVb[2], curVb[3]])
return
}
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
const pts = [...state.pointers.values()]
const dx = pts[0].x - pts[1].x
const dy = pts[0].y - pts[1].y
const dist = Math.hypot(dx, dy)
if (!dist) return
const factor = state.startDist / dist
const cx = (pts[0].x + pts[1].x) / 2
const cy = (pts[0].y + pts[1].y) / 2
const px = curVb[0] + (cx - rect.left) * (curVb[2] / rect.width)
const py = curVb[1] + (cy - rect.top) * (curVb[3] / rect.height)
setCurVb(zoomAtPoint(state.startVb, factor, px, py))
}
}
const onPointerUpOrCancel = e => {
// Release PointerCapture to avoid event capture anomalies
if (svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) {
svg.releasePointerCapture(e.pointerId)
}
state.pointers.delete(e.pointerId)
if (state.pointers.size === 0) {
state.startVb = null
state.startDist = 0
} else if (state.pointers.size === 1) {
const p = state.pointers.values().next().value
state.lastPointerX = p.x
state.lastPointerY = p.y
}
}
const onWheel = e => {
// Prevent event bubbling from triggering external scroll
e.preventDefault()
e.stopPropagation()
// Normalize deltaY across deltaMode (Chrome uses pixels, Safari uses lines)
let delta = e.deltaY
if (e.deltaMode === 1) delta *= 16
else if (e.deltaMode === 2) delta *= 400
// Continuous zoom factor: smoother than fixed 1.1/0.9 steps
const zoomFactor = Math.pow(1.001, delta)
const curVb = wrap.__mermaidCurViewBox
const rect = getRect()
const px = curVb[0] + (e.clientX - rect.left) * (curVb[2] / rect.width)
const py = curVb[1] + (e.clientY - rect.top) * (curVb[3] / rect.height)
setCurVb(zoomAtPoint(curVb, zoomFactor, px, py))
}
const onDblClick = () => {
const init = wrap.__mermaidInitViewBox
if (init) setCurVb(init)
}
svg.addEventListener('pointerdown', onPointerDown, { signal: ac.signal })
svg.addEventListener('pointermove', onPointerMove, { signal: ac.signal })
svg.addEventListener('pointerup', onPointerUpOrCancel, { signal: ac.signal })
svg.addEventListener('pointercancel', onPointerUpOrCancel, { signal: ac.signal })
svg.addEventListener('wheel', onWheel, { passive: false, signal: ac.signal })
svg.addEventListener('dblclick', onDblClick, { signal: ac.signal })
}
const runMermaid = ele => { const runMermaid = ele => {
window.loadMermaid = true window.loadMermaid = true
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
ele.forEach((item, index) => { ele.forEach((item, index) => {
const mermaidSrc = item.firstElementChild const mermaidSrc = item.firstElementChild
const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {} // Clean up event listeners before removing old SVG
if (item.__mermaidAbortController) {
item.__mermaidAbortController.abort()
}
const oldSvg = item.querySelector('svg')
if (oldSvg) oldSvg.remove()
let config = {}
try {
config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
} catch (e) {
console.warn('[mermaid] failed to parse dataset.config:', e)
}
if (!config.theme) { if (!config.theme) {
config.theme = theme config.theme = theme
} }
@@ -14,18 +301,37 @@ script.
const mermaidID = `mermaid-${index}` const mermaidID = `mermaid-${index}`
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
const renderMermaid = svg => { const renderMermaid = svg => {
mermaidSrc.insertAdjacentHTML('afterend', svg) mermaidSrc.insertAdjacentHTML('afterend', svg)
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
item.__mermaidOriginalSvg = svg
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
} }
// mermaid v9 and v10 compatibility const handleError = err => {
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg)) console.error(`[mermaid] render failed for block #${index}:`, err)
const errorEl = document.createElement('div')
errorEl.className = 'mermaid-error'
errorEl.textContent = `Mermaid render error: ${err.message || err}`
mermaidSrc.insertAdjacentElement('afterend', errorEl)
}
try {
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
// mermaid v9 and v10 compatibility
if (typeof renderFn === 'string') {
renderMermaid(renderFn)
} else {
renderFn.then(({ svg }) => renderMermaid(svg)).catch(handleError)
}
} catch (err) {
handleError(err)
}
}) })
} }
const codeToMermaid = () => { const codeToMermaid = $article => {
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid') const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')
if (codeMermaidEle.length === 0) return if (codeMermaidEle.length === 0) return
codeMermaidEle.forEach(ele => { codeMermaidEle.forEach(ele => {
@@ -41,8 +347,11 @@ script.
} }
const loadMermaid = () => { const loadMermaid = () => {
if (!{theme.mermaid.code_write}) codeToMermaid() const $article = document.getElementById('article-container')
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap') if (!$article) return
if (!{theme.mermaid.code_write}) codeToMermaid($article)
const $mermaid = $article.querySelectorAll('.mermaid-wrap')
if ($mermaid.length === 0) return if ($mermaid.length === 0) return
const runMermaidFn = () => runMermaid($mermaid) const runMermaidFn = () => runMermaid($mermaid)
@@ -52,4 +361,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)
})() })()
+8 -4
View File
@@ -1,4 +1,4 @@
- var pjaxExclude = 'a:not([target="_blank"])' - var pjaxExclude = 'a:not([target="_blank"]):not([data-fancybox])'
if theme.pjax.exclude if theme.pjax.exclude
each val in theme.pjax.exclude each val in theme.pjax.exclude
- pjaxExclude += `:not([href="${val}"])` - pjaxExclude += `:not([href="${val}"])`
@@ -53,8 +53,9 @@ script.
document.addEventListener('pjax:complete', () => { document.addEventListener('pjax:complete', () => {
btf.removeGlobalFnEvent('pjaxCompleteOnce') btf.removeGlobalFnEvent('pjaxCompleteOnce')
document.querySelectorAll('script[data-pjax]').forEach(item => { document.querySelectorAll('script[data-pjax]').forEach(item => {
if (!item.parentNode) return
const newScript = document.createElement('script') const newScript = document.createElement('script')
const content = item.text || item.textContent || item.innerHTML || "" const content = item.text || item.textContent || ''
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)) Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
newScript.appendChild(document.createTextNode(content)) newScript.appendChild(document.createTextNode(content))
item.parentNode.replaceChild(newScript, item) item.parentNode.replaceChild(newScript, item)
@@ -64,10 +65,13 @@ script.
}) })
document.addEventListener('pjax:error', e => { document.addEventListener('pjax:error', e => {
if (e.request.status === 404) { const responseURL = e.request && e.request.responseURL
if (e.request && e.request.status === 404) {
!{theme.error_404 && theme.error_404.enable} !{theme.error_404 && theme.error_404.enable}
? pjax.loadUrl('!{url_for("/404.html")}') ? pjax.loadUrl('!{url_for("/404.html")}')
: window.location.href = e.request.responseURL : responseURL && (window.location.href = responseURL)
} else if (responseURL) {
window.location.href = responseURL
} }
}) })
}) })
+5 -1
View File
@@ -5,11 +5,15 @@
if (syntax_highlighter === 'prismjs' || enable) && !preprocess if (syntax_highlighter === 'prismjs' || enable) && !preprocess
script. script.
(() => { (() => {
const $article = document.getElementById('article-container')
if (!$article || $article.querySelector('.hbe-container')) return
window.Prism = window.Prism || {} window.Prism = window.Prism || {}
window.Prism.manual = true window.Prism.manual = true
const highlightAll = () => { const highlightAll = () => {
window.Prism.highlightAll() // window.Prism.highlightAll()
window.Prism.highlightAllUnder($article, true)
} }
window.addEventListener('load', highlightAll) window.addEventListener('load', highlightAll)
+6 -6
View File
@@ -1,5 +1,5 @@
- const { effect, source, sub, typed_option } = theme.subtitle - const { effect, source, sub, typed_option } = theme.subtitle
- let subContent = sub || new Array() - let subContent = typeof sub === 'string' ? [sub] : (sub || new Array())
script. script.
window.typedJSFn = { window.typedJSFn = {
@@ -44,7 +44,7 @@ script.
} }
} }
} }
btf.addGlobalFn('pjaxSendOnce', () => { typed.destroy() }, 'typedDestroy') btf.addGlobalFn('pjaxSendOnce', () => { typed && typed.destroy() }, 'typedDestroy')
case source case source
when 1 when 1
@@ -53,8 +53,8 @@ case source
fetch('https://v1.hitokoto.cn') fetch('https://v1.hitokoto.cn')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const from = '出自 ' + data.from const from = data.from ? '出自 ' + data.from : ''
typedJSFn.processSubtitle(data.hitokoto, [from]) typedJSFn.processSubtitle(data.hitokoto, from ? [from] : [])
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Hitokoto API:', err) console.error('Failed to get the Hitokoto API:', err)
@@ -79,7 +79,7 @@ case source
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Yiyan API:', err) console.error('Failed to get the Yiyan API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}) })
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
@@ -99,7 +99,7 @@ case source
}) })
.catch(err => { .catch(err => {
console.error('Failed to get the Jinrishici API:', err) console.error('Failed to get the Jinrishici API:', err)
typedJSFn.processSubtitle(!{JSON.stringify(subContent.length)}) typedJSFn.processSubtitle(!{JSON.stringify(subContent)})
}) })
} }
typedJSFn.run(subtitleType) typedJSFn.run(subtitleType)
+116 -110
View File
@@ -1,110 +1,116 @@
- let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics - let { serverURL, script_name, website_id, option, UV_PV } = theme.umami_analytics
- const isServerURL = !!serverURL - const isServerURL = !!serverURL
- const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is' - const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
- const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1' - const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
script. script.
(() => { (() => {
const option = !{JSON.stringify(option)} const option = !{JSON.stringify(option)}
const config = !{JSON.stringify(UV_PV)} const config = !{JSON.stringify(UV_PV)}
const runTrack = () => { const runTrack = () => {
if (typeof umami !== 'undefined' && typeof umami.track === 'function') { if (typeof umami !== 'undefined' && typeof umami.track === 'function') {
umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title })) umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
} else { } else {
console.warn('Umami Analytics: umami.track is not available') console.warn('Umami Analytics: umami.track is not available')
} }
} }
const loadUmamiJS = () => { const loadUmamiJS = () => {
btf.getScript('!{baseURL}/!{script_name}', { btf.getScript('!{baseURL}/!{script_name}', {
'data-website-id': '!{website_id}', 'data-website-id': '!{website_id}',
'data-auto-track': 'false', 'data-auto-track': 'false',
...option ...option
}).then(() => { }).then(() => {
runTrack() runTrack()
}).catch(error => { }).catch(error => {
console.error('Umami Analytics: Error loading script', error) console.error('Umami Analytics: Error loading script', error)
}) })
} }
const getData = async (isPost) => { const extractValue = stat => (stat && typeof stat.value !== 'undefined') ? stat.value : stat
try {
const now = Date.now() const getData = async (isPost) => {
const keyUrl = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : '' try {
const headerList = { 'Accept': 'application/json' } const now = Date.now()
const pathQuery = isPost ? `&url=${window.location.pathname}&path=${window.location.pathname}` : ''
if (!{isServerURL}) { const headers = {
headerList['Authorization'] = `Bearer ${config.token}` 'Accept': 'application/json',
} else { [!{isServerURL} ? 'Authorization' : 'x-umami-api-key']: !{isServerURL} ? `Bearer ${config.token}` : config.token
headerList['x-umami-api-key'] = config.token }
}
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${pathQuery}`, {
const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, { method: 'GET',
method: "GET", headers
headers: headerList })
})
if (!res.ok) {
if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`)
throw new Error(`HTTP error! status: ${res.status}`) }
}
return await res.json()
return await res.json() } catch (error) {
} catch (error) { console.error('Umami Analytics: Failed to fetch data', error)
console.error('Umami Analytics: Failed to fetch data', error) throw error
throw error }
} }
}
const insertData = async () => {
const insertData = async () => { try {
try { const tasks = []
if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
const pagePV = document.getElementById('umamiPV') if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
if (pagePV) { tasks.push((async () => {
const data = await getData(true) const pagePV = document.getElementById('umamiPV')
if (data && data.pageviews) { if (pagePV) {
pagePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews const data = await getData(true)
} else { if (data && data.pageviews) {
console.warn('Umami Analytics: Invalid page view data received') pagePV.textContent = extractValue(data.pageviews)
} } else {
} console.warn('Umami Analytics: Invalid page view data received')
} }
}
if (config.site_uv || config.site_pv) { })())
const data = await getData(false) }
if (config.site_uv) { if (config.site_uv || config.site_pv) {
const siteUV = document.getElementById('umami-site-uv') tasks.push((async () => {
if (siteUV && data && data.visitors) { const data = await getData(false)
siteUV.textContent = typeof data.visitors.value !== 'undefined' ? data.visitors.value : data.visitors
} else if (siteUV) { if (config.site_uv) {
console.warn('Umami Analytics: Invalid site UV data received') const siteUV = document.getElementById('umami-site-uv')
} if (siteUV && data && data.visitors) {
} siteUV.textContent = extractValue(data.visitors)
} else if (siteUV) {
if (config.site_pv) { console.warn('Umami Analytics: Invalid site UV data received')
const sitePV = document.getElementById('umami-site-pv') }
if (sitePV && data && data.pageviews) { }
sitePV.textContent = typeof data.pageviews.value !== 'undefined' ? data.pageviews.value : data.pageviews
} else if (sitePV) { if (config.site_pv) {
console.warn('Umami Analytics: Invalid site PV data received') const sitePV = document.getElementById('umami-site-pv')
} if (sitePV && data && data.pageviews) {
} sitePV.textContent = extractValue(data.pageviews)
} } else if (sitePV) {
} catch (error) { console.warn('Umami Analytics: Invalid site PV data received')
console.error('Umami Analytics: Failed to insert data', error) }
} }
} })())
}
btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert') await Promise.all(tasks)
} catch (error) {
console.error('Umami Analytics: Failed to insert data', error)
loadUmamiJS() }
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData) btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
} else { btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
setTimeout(insertData, 100)
} loadUmamiJS()
})()
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertData)
} else {
setTimeout(insertData, 100)
}
})()
+3 -3
View File
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
i.fas.fa-tags i.fas.fa-tags
span= _p('aside.card_tags') span= _p('aside.card_tags')
- let { limit, orderby, order } = theme.aside.card_tags - let { limit, orderby, order, custom_colors } = theme.aside.card_tags
- limit = limit === 0 ? 0 : limit || 40 - limit = limit === 0 ? 0 : limit || 40
if theme.aside.card_tags.color if theme.aside.card_tags.color
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'}) .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
else else
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'}) .card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "hexo-theme-butterfly", "name": "hexo-theme-butterfly",
"version": "5.5.3", "version": "5.6.1",
"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": {
@@ -26,7 +26,7 @@
"hexo-renderer-pug": "^3.0.0", "hexo-renderer-pug": "^3.0.0",
"hexo-renderer-stylus": "^3.0.1", "hexo-renderer-stylus": "^3.0.1",
"hexo-util": "^4.0.0", "hexo-util": "^4.0.0",
"moment-timezone": "^0.6.0" "moment-timezone": "^0.6.2"
}, },
"homepage": "https://butterfly.js.org/", "homepage": "https://butterfly.js.org/",
"author": "Jerry <my@crazywong.com>", "author": "Jerry <my@crazywong.com>",
+16 -16
View File
@@ -1,7 +1,7 @@
abcjs_basic_js: abcjs_basic_js:
name: abcjs name: abcjs
file: dist/abcjs-basic-min.js file: dist/abcjs-basic-min.js
version: 6.5.2 version: 6.6.4
activate_power_mode: activate_power_mode:
name: butterfly-extsrc name: butterfly-extsrc
file: dist/activate-power-mode.min.js file: dist/activate-power-mode.min.js
@@ -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.46.0 version: 5.56.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.3.2 version: 4.6.3
docsearch_js: docsearch_js:
name: '@docsearch/js' name: '@docsearch/js'
other_name: docsearch-js other_name: docsearch-js
file: dist/umd/index.js file: dist/umd/index.js
version: 4.3.2 version: 4.6.3
egjs_infinitegrid: egjs_infinitegrid:
name: '@egjs/infinitegrid' name: '@egjs/infinitegrid'
other_name: egjs-infinitegrid other_name: egjs-infinitegrid
file: dist/infinitegrid.min.js file: dist/infinitegrid.min.js
version: 4.12.0 version: 4.13.0
fancybox: fancybox:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
version: 6.1.7 version: 6.1.14
other_name: fancyapps-ui other_name: fancyapps-ui
fancybox_css: fancybox_css:
name: '@fancyapps/ui' name: '@fancyapps/ui'
file: dist/fancybox/fancybox.css file: dist/fancybox/fancybox.css
version: 6.1.7 version: 6.1.14
other_name: fancyapps-ui other_name: fancyapps-ui
fireworks: fireworks:
name: butterfly-extsrc name: butterfly-extsrc
@@ -95,7 +95,7 @@ fontawesome:
name: '@fortawesome/fontawesome-free' name: '@fortawesome/fontawesome-free'
file: css/all.min.css file: css/all.min.css
other_name: font-awesome other_name: font-awesome
version: 7.1.0 version: 7.3.1
gitalk: gitalk:
name: gitalk name: gitalk
file: dist/gitalk.min.js file: dist/gitalk.min.js
@@ -112,12 +112,12 @@ katex:
name: katex name: katex
file: dist/katex.min.css file: dist/katex.min.css
other_name: KaTeX other_name: KaTeX
version: 0.16.27 version: 0.17.0
katex_copytex: katex_copytex:
name: katex name: katex
file: dist/contrib/copy-tex.min.js file: dist/contrib/copy-tex.min.js
other_name: KaTeX other_name: KaTeX
version: 0.16.27 version: 0.17.0
lazyload: lazyload:
name: vanilla-lazyload name: vanilla-lazyload
file: dist/lazyload.iife.min.js file: dist/lazyload.iife.min.js
@@ -125,7 +125,7 @@ lazyload:
mathjax: mathjax:
name: mathjax name: mathjax
file: tex-mml-chtml.js file: tex-mml-chtml.js
version: 4.0.0 version: 4.1.3
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.16.0
meting_js: meting_js:
name: butterfly-extsrc name: butterfly-extsrc
file: metingjs/dist/Meting.min.js file: metingjs/dist/Meting.min.js
@@ -186,11 +186,11 @@ snackbar_css:
twikoo: twikoo:
name: twikoo name: twikoo
file: dist/twikoo.all.min.js file: dist/twikoo.all.min.js
version: 1.6.44 version: 1.7.14
typed: typed:
name: typed.js name: typed.js
file: dist/typed.umd.js file: dist/typed.umd.js
version: 2.1.0 version: 3.0.0
valine: valine:
name: valine name: valine
file: dist/Valine.min.js file: dist/Valine.min.js
@@ -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.15.2
waline_js: waline_js:
name: '@waline/client' name: '@waline/client'
file: dist/waline.js file: dist/waline.js
other_name: waline other_name: waline
version: 3.8.0 version: 3.15.2
+7 -1
View File
@@ -179,6 +179,7 @@ module.exports = {
enable: true, enable: true,
limit: 40, limit: 40,
color: false, color: false,
custom_colors: null,
orderby: 'random', orderby: 'random',
order: 1, order: 1,
sort_order: null sort_order: null
@@ -393,6 +394,9 @@ module.exports = {
crisp: { crisp: {
website_id: null website_id: null
}, },
knocket: {
identifier: null
},
google_tag_manager: { google_tag_manager: {
tag_id: null, tag_id: null,
domain: 'https://www.googletagmanager.com' domain: 'https://www.googletagmanager.com'
@@ -522,7 +526,9 @@ module.exports = {
theme: { theme: {
light: 'default', light: 'default',
dark: 'dark' dark: 'dark'
} },
open_in_new_tab: true,
zoom_pan: true
}, },
chartjs: { chartjs: {
enable: false, enable: false,
+1 -1
View File
@@ -21,7 +21,7 @@ const lazyload = htmlContent => {
// Handle src attributes with double quotes, single quotes, or no quotes (unified approach) // Handle src attributes with double quotes, single quotes, or no quotes (unified approach)
// Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify) // Matches: src="..." or src='...' or src=... (e.g., after minification by hexo-minify)
return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => { return htmlContent.replace(/(<img(?![^>]*?\bdata-lazy-src=)(?:\s[^>]*?)?\ssrc=)(?:"([^"]*)"|'([^']*)'|([^\s>]+))(?![^<]*<\/script>)/gi, (match, prefix, srcDoubleQuote, srcSingleQuote, srcNoQuote) => {
const src = srcDoubleQuote || srcSingleQuote || srcNoQuote const src = srcDoubleQuote ?? srcSingleQuote ?? srcNoQuote
return `${prefix}"${bg}" data-lazy-src="${src}"` return `${prefix}"${bg}" data-lazy-src="${src}"`
}) })
} }
+51 -32
View File
@@ -6,33 +6,46 @@
hexo.extend.generator.register('post', locals => { hexo.extend.generator.register('post', locals => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
const remoteImgReg = /^(?:https?:)?\/\//i
const dataImgReg = /^data:image\//i
const { post_asset_folder: postAssetFolder } = hexo.config const { post_asset_folder: postAssetFolder } = hexo.config
const { cover: { default_cover: defaultCover } } = hexo.theme.config const { cover: { default_cover: defaultCover } } = hexo.theme.config
const isImage = value => {
return typeof value === 'string' &&
(remoteImgReg.test(value) || dataImgReg.test(value) || imgTestReg.test(value))
}
function * createCoverGenerator () { function * createCoverGenerator () {
if (!defaultCover) { if (!defaultCover || (Array.isArray(defaultCover) && defaultCover.length === 0)) {
while (true) yield false while (true) yield false
} }
if (!Array.isArray(defaultCover)) { if (!Array.isArray(defaultCover)) {
while (true) yield defaultCover while (true) yield defaultCover
} }
const coverCount = defaultCover.length if (defaultCover.length === 1) {
if (coverCount === 1) {
while (true) yield defaultCover[0] while (true) yield defaultCover[0]
} }
const coverCount = defaultCover.length
const maxHistory = Math.min(3, coverCount - 1) const maxHistory = Math.min(3, coverCount - 1)
const history = [] const history = []
while (true) { while (true) {
let index let index
do { do {
index = Math.floor(Math.random() * coverCount) index = Math.floor(Math.random() * coverCount)
} while (history.includes(index)) } while (history.includes(index))
history.push(index) history.push(index)
if (history.length > maxHistory) history.shift()
if (history.length > maxHistory) {
history.shift()
}
yield defaultCover[index] yield defaultCover[index]
} }
@@ -40,32 +53,31 @@ hexo.extend.generator.register('post', locals => {
const coverGenerator = createCoverGenerator() const coverGenerator = createCoverGenerator()
const resolvePostAsset = (value, postPath) => {
if (
!postAssetFolder ||
typeof value !== 'string' ||
value.includes('/') ||
!imgTestReg.test(value)
) {
return value
}
return `${postPath}${value}`
}
const handleImg = data => { const handleImg = data => {
let { cover: coverVal, top_img: topImg, pagination_cover: paginationCover } = data data.top_img = resolvePostAsset(data.top_img, data.path)
data.cover = resolvePostAsset(data.cover, data.path)
data.pagination_cover = resolvePostAsset(data.pagination_cover, data.path)
// Add path to top_img and cover if post_asset_folder is enabled if (data.cover === false) return data
if (postAssetFolder) {
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) { if (!data.cover) {
data.top_img = `${data.path}${topImg}` data.cover = coverGenerator.next().value
}
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
data.cover = `${data.path}${coverVal}`
}
if (paginationCover && paginationCover.indexOf('/') === -1 && imgTestReg.test(paginationCover)) {
data.pagination_cover = `${data.path}${paginationCover}`
}
} }
if (coverVal === false) return data if (isImage(data.cover)) {
// If cover is not set, use random cover
if (!coverVal) {
const randomCover = coverGenerator.next().value
data.cover = randomCover
coverVal = randomCover
}
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
data.cover_type = 'img' data.cover_type = 'img'
} }
@@ -75,16 +87,23 @@ hexo.extend.generator.register('post', locals => {
const posts = locals.posts.sort('date').toArray() const posts = locals.posts.sort('date').toArray()
const { length } = posts const { length } = posts
return posts.map((post, i) => { return posts.map((post, index) => {
if (i) post.prev = posts[i - 1] const data = post
if (i < length - 1) post.next = posts[i + 1]
post.__post = true if (index > 0) {
data.prev = posts[index - 1]
}
if (index < length - 1) {
data.next = posts[index + 1]
}
data.__post = true
return { return {
data: handleImg(post), data: handleImg(data),
layout: 'post', layout: 'post',
path: post.path path: data.path
} }
}) })
}) })
+6 -7
View File
@@ -17,12 +17,11 @@ hexo.extend.helper.register('getArchiveLength', function () {
const m = date.month() + 1 const m = date.month() + 1
const d = date.date() const d = date.date()
if (yearly) { // Always track year so year archive pages can be counted
const keyYear = `${y}` const keyYear = `${y}`
map.set(keyYear, (map.get(keyYear) || 0) + 1) map.set(keyYear, (map.get(keyYear) || 0) + 1)
}
if (monthly) { if (monthly || daily) {
const keyMonth = `${y}-${m}` const keyMonth = `${y}-${m}`
map.set(keyMonth, (map.get(keyMonth) || 0) + 1) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
} }
@@ -37,8 +36,8 @@ hexo.extend.helper.register('getArchiveLength', function () {
// Determine the appropriate key to fetch based on current page context // Determine the appropriate key to fetch based on current page context
let key let key
if (yearly && year) key = `${year}` if (yearly || monthly || daily) key = `${year}`
if (monthly && month) key = `${year}-${month}` if ((monthly || daily) && month) key = `${year}-${month}`
if (daily && day) key = `${year}-${month}-${day}` if (daily && day) key = `${year}-${month}-${day}`
// Return the count for the current period or default to the total posts // Return the count for the current period or default to the total posts
+89 -37
View File
@@ -11,46 +11,87 @@ hexo.extend.helper.register('inject_head_js', function () {
const createCustomJs = () => ` const createCustomJs = () => `
const saveToLocal = { const saveToLocal = {
set: (key, value, ttl) => { set: (key, value, ttl) => {
if (!ttl) return const data = { value }
const expiry = Date.now() + ttl * 86400000
localStorage.setItem(key, JSON.stringify({ value, expiry })) if (ttl != null) {
data.expiry = Date.now() + ttl * 86400000
}
localStorage.setItem(key, JSON.stringify(data))
}, },
get: key => { get: key => {
const itemStr = localStorage.getItem(key) const itemStr = localStorage.getItem(key)
if (!itemStr) return undefined if (!itemStr) return
const { value, expiry } = JSON.parse(itemStr)
if (Date.now() > expiry) { try {
const data = JSON.parse(itemStr)
if (data.expiry && Date.now() > data.expiry) {
localStorage.removeItem(key)
return
}
return data.value
} catch {
localStorage.removeItem(key) localStorage.removeItem(key)
return undefined
} }
return value
} }
} }
const scriptCache = new Map()
const cssCache = new Map()
window.btf = { window.btf = {
saveToLocal, saveToLocal,
getScript: (url, attr = {}) => new Promise((resolve, reject) => { getScript: (url, attr = {}) => {
const script = document.createElement('script') if (scriptCache.has(url)) {
script.src = url return scriptCache.get(url)
script.async = true
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
script.onload = script.onreadystatechange = () => {
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
} }
script.onerror = reject
document.head.appendChild(script) const promise = new Promise((resolve, reject) => {
}), const script = document.createElement('script')
getCSS: (url, id) => new Promise((resolve, reject) => {
const link = document.createElement('link') script.src = url
link.rel = 'stylesheet' script.async = true
link.href = url
if (id) link.id = id for (const key in attr) {
link.onload = link.onreadystatechange = () => { script.setAttribute(key, attr[key])
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve() }
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
scriptCache.set(url, promise)
return promise
},
getCSS: (url, id) => {
if (cssCache.has(url)) {
return cssCache.get(url)
} }
link.onerror = reject
document.head.appendChild(link) const promise = new Promise((resolve, reject) => {
}), const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
if (id) {
link.id = id
}
link.onload = resolve
link.onerror = reject
document.head.appendChild(link)
})
cssCache.set(url, promise)
return promise
},
addGlobalFn: (key, fn, name = false, parent = window) => { addGlobalFn: (key, fn, name = false, parent = window) => {
if (!${pjax.enable} && key.startsWith('pjax')) return if (!${pjax.enable} && key.startsWith('pjax')) return
const globalFn = parent.globalFn || {} const globalFn = parent.globalFn || {}
@@ -65,16 +106,17 @@ hexo.extend.helper.register('inject_head_js', function () {
if (!darkmode.enable) return '' if (!darkmode.enable) return ''
let darkmodeJs = ` let darkmodeJs = `
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
const activateDarkMode = () => { const activateDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'dark') document.documentElement.dataset.theme = 'dark'
if (document.querySelector('meta[name="theme-color"]') !== null) { if (metaThemeColor !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}') metaThemeColor.setAttribute('content', '${themeColorDark}')
} }
} }
const activateLightMode = () => { const activateLightMode = () => {
document.documentElement.setAttribute('data-theme', 'light') document.documentElement.dataset.theme = 'light'
if (document.querySelector('meta[name="theme-color"]') !== null) { if (metaThemeColor !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}') metaThemeColor.setAttribute('content', '${themeColorLight}')
} }
} }
@@ -95,10 +137,15 @@ hexo.extend.helper.register('inject_head_js', function () {
else if (mediaQueryDark.matches) activateDarkMode() else if (mediaQueryDark.matches) activateDarkMode()
else { else {
const hour = new Date().getHours() const hour = new Date().getHours()
const isNight = hour <= ${start} || hour >= ${end} const start = ${start}
const end = ${end}
const isNight =
start < end
? hour < start || hour >= end
: hour >= start || hour < end
isNight ? activateDarkMode() : activateLightMode() isNight ? activateDarkMode() : activateLightMode()
} }
mediaQueryDark.addEventListener('change', () => { mediaQueryDark.addEventListener('change', e => {
if (saveToLocal.get('theme') === undefined) { if (saveToLocal.get('theme') === undefined) {
e.matches ? activateDarkMode() : activateLightMode() e.matches ? activateDarkMode() : activateLightMode()
} }
@@ -111,7 +158,12 @@ hexo.extend.helper.register('inject_head_js', function () {
case 2: case 2:
darkmodeJs += ` darkmodeJs += `
const hour = new Date().getHours() const hour = new Date().getHours()
const isNight = hour <= ${start} || hour >= ${end} const start = ${start}
const end = ${end}
const isNight =
start < end
? hour < start || hour >= end
: hour >= start || hour < end
if (theme === undefined) isNight ? activateDarkMode() : activateLightMode() if (theme === undefined) isNight ? activateDarkMode() : activateLightMode()
else theme === 'light' ? activateLightMode() : activateDarkMode() else theme === 'light' ? activateLightMode() : activateDarkMode()
` `
+46 -14
View File
@@ -11,6 +11,8 @@ const colorPattern = /^(#|rgb|rgba|hsl|hsla)/i
const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i const simpleFilePattern = /\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff)$/i
const archiveRegex = /\/archives\// const archiveRegex = /\/archives\//
const { version: themeVersion } = require('../../package.json')
hexo.extend.helper.register('truncate', truncateContent) hexo.extend.helper.register('truncate', truncateContent)
hexo.extend.helper.register('postDesc', data => { hexo.extend.helper.register('postDesc', data => {
@@ -19,7 +21,7 @@ hexo.extend.helper.register('postDesc', data => {
hexo.extend.helper.register('cloudTags', function (options = {}) { hexo.extend.helper.register('cloudTags', function (options = {}) {
const env = this const env = this
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
if (limit > 0) { if (limit > 0) {
source = source.limit(limit) source = source.limit(limit)
@@ -36,15 +38,45 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})` return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
} }
const generateStyle = (size, unit, page) => { const normalizeColors = input => {
const colorStyle = page === 'tags' ? `background-color: ${getRandomColor()};` : `color: ${getRandomColor()};` if (!input) return null
if (typeof input === 'string') {
const color = input.trim()
return color ? [color] : null
}
if (Array.isArray(input)) {
const result = []
for (let i = 0; i < input.length; i++) {
const value = input[i]
if (value === null || value === undefined) continue
const color = String(value).trim()
if (!color) continue
result.push(color)
}
return result.length ? result : null
}
return null
}
const userColors = normalizeColors(custom_colors)
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 `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
} }
return source.sort(orderby, order).map(tag => { return source.sort(orderby, order).map((tag, idx) => {
const ratio = length ? sizeMap.get(tag.length) / length : 0 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 color = userColors[idx % userColors.length]
const style = generateStyle(size, unit, page, color)
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item" style="${style}">${tag.name}</a>`
}
const color = getRandomColor()
const style = generateStyle(size, unit, page, color)
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>` return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
}).join('') }).join('')
}) })
@@ -72,13 +104,13 @@ hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
if (!menu) return defaultTitle if (!menu) return defaultTitle
const loop = m => { const loop = m => {
for (const key in m) { for (const [key, value] of Object.entries(m)) {
if (typeof m[key] === 'object') { if (value && typeof value === 'object') {
const result = loop(m[key]) const result = loop(value)
if (result) return result if (result) return result
} }
if (archiveRegex.test(m[key])) { if (typeof value === 'string' && archiveRegex.test(value)) {
return key return key
} }
} }
@@ -121,9 +153,10 @@ hexo.extend.helper.register('shuoshuoFN', (data, page) => {
// 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
const timezone = hexo.config.timezone
processedData.forEach(item => { processedData.forEach(item => {
const utcDate = moment.utc(item.date).format('YYYY-MM-DD HH:mm:ss') const parsed = moment.utc(item.date)
item.date = moment.tz(utcDate, hexo.config.timezone).format('YYYY-MM-DD HH:mm:ss') item.date = moment.tz(parsed.format('YYYY-MM-DD HH:mm:ss'), 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' })
}) })
@@ -138,7 +171,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
if (category) return 'category' if (category) return 'category'
if (archive) return 'archive' if (archive) return 'archive'
if (type) { if (type) {
if (type === 'tags' || type === 'categories') return type if (type === 'tags' || type === 'categories' || type === '404') return type
else return 'page' else return 'page'
} }
if (isHome) return 'home' if (isHome) return 'home'
@@ -146,8 +179,7 @@ hexo.extend.helper.register('getPageType', (page, isHome) => {
}) })
hexo.extend.helper.register('getVersion', () => { hexo.extend.helper.register('getVersion', () => {
const { version } = require('../../package.json') return { hexo: hexo.version, theme: themeVersion }
return { hexo: hexo.version, theme: version }
}) })
hexo.extend.helper.register('safeJSON', data => { hexo.extend.helper.register('safeJSON', data => {
+9 -7
View File
@@ -23,7 +23,6 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
if (relatedPosts.has(post.path)) { if (relatedPosts.has(post.path)) {
relatedPosts.get(post.path).weight += 1 relatedPosts.get(post.path).weight += 1
} else { } else {
const getPostDesc = post.postDesc || postDesc(post, hexo)
relatedPosts.set(post.path, { relatedPosts.set(post.path, {
title: post.title, title: post.title,
path: post.path, path: post.path,
@@ -32,7 +31,7 @@ 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, post,
random: Math.random() random: Math.random()
}) })
} }
@@ -61,12 +60,15 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
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(relatedPostsList.length, limitNum); i++) { const max = Math.min(relatedPostsList.length, limitNum)
let { cover, title, path, cover_type, created, updated, postDesc } = relatedPostsList[i] for (let i = 0; i < max; i++) {
const item = relatedPostsList[i]
let { cover, title, path, cover_type, created, updated, post } = item
const { escape_html, url_for, date } = this const { escape_html, url_for, date } = this
cover = cover || 'var(--default-bg-color)' cover = cover || 'var(--default-bg-color)'
title = escape_html(title) title = escape_html(title)
const className = postDesc ? 'pagination-related' : 'pagination-related no-desc' const desc = post.postDesc || postDesc(post, hexo)
const className = desc ? 'pagination-related' : 'pagination-related no-desc'
result += `<a class="${className}" href="${url_for(path)}" title="${title}">` result += `<a class="${className}" href="${url_for(path)}" title="${title}">`
if (cover_type === 'img') { if (cover_type === 'img') {
result += `<img class="cover" src="${url_for(cover)}" alt="cover">` result += `<img class="cover" src="${url_for(cover)}" alt="cover">`
@@ -80,8 +82,8 @@ hexo.extend.helper.register('related_posts', function (currentPost) {
} }
result += `<div class="info-item-2">${title}</div></div>` result += `<div class="info-item-2">${title}</div></div>`
if (postDesc) { if (desc) {
result += `<div class="info-2"><div class="info-item-1">${postDesc}</div></div>` result += `<div class="info-2"><div class="info-item-1">${desc}</div></div>`
} }
result += '</div></a>' result += '</div></a>'
} }
+24 -22
View File
@@ -1,22 +1,24 @@
'use strict' 'use strict'
hexo.extend.helper.register('groupPosts', function () { // Read pre-built groups from tag/series.js (hexo._seriesGroups)
const getGroupArray = array => { // and sort each group according to the sidebar card config.
return array.reduce((groups, item) => { hexo.extend.helper.register('groupPosts', function () {
const key = item.series const groups = hexo._seriesGroups || {}
if (key) { const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
groups[key] = groups[key] || []
groups[key].push(item) const result = {}
} Object.keys(groups).forEach(key => {
return groups const arr = groups[key].slice() // copy to avoid mutating the shared data
}, {}) if (orderBy === 'title') {
} arr.sort((a, b) => {
const cmp = a.title.toUpperCase().localeCompare(b.title.toUpperCase())
const sortPosts = posts => { return order === 1 ? cmp : -cmp
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series })
if (orderBy === 'title') return posts.sort('title', order) } else {
return posts.sort('date', order) arr.sort((a, b) => order === 1 ? a.date - b.date : b.date - a.date)
} }
result[key] = arr
return getGroupArray(sortPosts(this.site.posts)) })
})
return result
})
+5 -4
View File
@@ -7,15 +7,16 @@
'use strict' 'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo) const { url_for, escapeHTML } = require('hexo-util')
const urlFor = url_for.bind(hexo)
const btn = args => { const btn = args => {
const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim()) const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
const iconHTML = icon ? `<i class="${icon}"></i>` : '' const iconHTML = icon ? `<i class="${escapeHTML(icon)}"></i>` : ''
const textHTML = text ? `<span>${text}</span>` : '' const textHTML = text ? `<span>${escapeHTML(text)}</span>` : ''
return `<a class="btn-beautify ${option}" href="${urlFor(url)}" title="${text}">${iconHTML}${textHTML}</a>` return `<a class="btn-beautify ${escapeHTML(option)}" href="${urlFor(url)}" title="${escapeHTML(text)}">${iconHTML}${textHTML}</a>`
} }
hexo.extend.tag.register('btn', btn, { ends: false }) hexo.extend.tag.register('btn', btn, { ends: false })
+8 -7
View File
@@ -4,24 +4,25 @@
'use strict' 'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo) const { url_for, escapeHTML } = require('hexo-util')
const urlFor = url_for.bind(hexo)
const flinkFn = (args, content) => { const flinkFn = (args, content) => {
const data = hexo.render.renderSync({ text: content, engine: 'yaml' }) const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
let result = '' let result = ''
data.forEach(item => { data.forEach(item => {
const className = item.class_name ? `<div class="flink-name">${item.class_name}</div>` : '' const className = item.class_name ? `<div class="flink-name">${escapeHTML(item.class_name)}</div>` : ''
const classDesc = item.class_desc ? `<div class="flink-desc">${item.class_desc}</div>` : '' const classDesc = item.class_desc ? `<div class="flink-desc">${escapeHTML(item.class_desc)}</div>` : ''
const listResult = item.link_list.map(link => ` const listResult = item.link_list.map(link => `
<div class="flink-list-item"> <div class="flink-list-item">
<a href="${link.link}" title="${link.name}" target="_blank"> <a href="${escapeHTML(link.link)}" title="${escapeHTML(link.name)}" target="_blank">
<div class="flink-item-icon"> <div class="flink-item-icon">
<img class="no-lightbox" src="${link.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${link.name}" /> <img class="no-lightbox" src="${escapeHTML(link.avatar)}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${escapeHTML(link.name)}" />
</div> </div>
<div class="flink-item-name">${link.name}</div> <div class="flink-item-name">${escapeHTML(link.name)}</div>
<div class="flink-item-desc" title="${link.descr}">${link.descr}</div> <div class="flink-item-desc" title="${escapeHTML(link.descr)}">${escapeHTML(link.descr)}</div>
</a> </a>
</div>`).join('') </div>`).join('')
+1
View File
@@ -27,6 +27,7 @@ const parseGalleryArgs = args => {
const parseImageContent = content => { const parseImageContent = content => {
const images = [] const images = []
let match let match
IMAGE_REGEX.lastIndex = 0
while ((match = IMAGE_REGEX.exec(content)) !== null) { while ((match = IMAGE_REGEX.exec(content)) !== null) {
images.push({ images.push({
+1 -1
View File
@@ -16,7 +16,7 @@
'use strict' 'use strict'
const parseArgs = args => args.join(' ').split(',') const parseArgs = args => args.join(' ').split(',').map(s => s.trim())
const generateStyle = (bg, color) => { const generateStyle = (bg, color) => {
let style = 'style="' let style = 'style="'
+3 -13
View File
@@ -5,20 +5,10 @@
'use strict' 'use strict'
const { escapeHTML } = require('hexo-util')
const score = (args, content) => { const score = (args, content) => {
// Escape HTML tags and some special characters, including curly braces const escapeHtmlTags = s => escapeHTML(s).replace(/[{}]/g, c => c === '{' ? '&#123;' : '&#125;')
const escapeHtmlTags = s => {
const lookup = {
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'<': '&lt;',
'>': '&gt;',
'{': '&#123;',
'}': '&#125;'
}
return s.replace(/[&"'<>{}]/g, c => lookup[c])
}
const trimmed = content.trim() const trimmed = content.trim()
// Split content using six dashes as a delimiter // Split content using six dashes as a delimiter
+88 -63
View File
@@ -1,63 +1,88 @@
/** /**
* series plugin * series plugin
* Syntax: * Syntax:
* {% series [series name] %} * {% series [series name] %}
* Usage: * Usage:
* {% series %} * {% series %}
* {% series series1 %} * {% series series1 %}
*/ */
'use strict' 'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo) const urlFor = require('hexo-util').url_for.bind(hexo)
const groups = {}
// Iterate all posts once and group by series name.
hexo.extend.filter.register('before_post_render', data => { // Result is stored on hexo._seriesGroups, shared by both tag and helper.
if (!hexo.theme.config.series.enable) return data function buildSeriesGroups () {
const groups = {}
const { layout, series } = data const posts = hexo.model('Post').toArray()
if (layout === 'post' && series) { for (let i = 0, len = posts.length; i < len; i++) {
if (!groups[series]) groups[series] = [] const p = posts[i]
groups[series].push({ if (p.series) {
title: data.title, if (!groups[p.series]) groups[p.series] = []
path: data.path, groups[p.series].push({
date: data.date.unix() title: p.title,
}) path: p.path,
} date: p.date.unix()
return data })
}) }
}
function series (args) { return groups
const { series } = hexo.theme.config }
if (!series.enable) {
hexo.log.warn('Series plugin is disabled in the theme config') // Build once before the first render (hexo generate / hexo server initial load).
return '' // before_generate runs after tag rendering, so we need this as a fallback.
} hexo.extend.filter.register('before_post_render', data => {
if (!hexo.theme.config.series.enable) return data
const seriesArr = args.length ? groups[args[0]] : groups[this.series] if (!hexo._seriesGroups) {
hexo._seriesGroups = buildSeriesGroups()
if (!seriesArr) { }
hexo.log.warn(`There is no series named "${args[0]}"`) return data
return '' })
}
// Rebuild after each generate cycle in hexo server to reflect latest post data.
const isAsc = (series.order || 1) === 1 // 1: asc, -1: desc hexo.extend.filter.register('before_generate', () => {
const isSortByTitle = series.orderBy === 'title' if (!hexo.theme.config.series.enable) return
hexo._seriesGroups = buildSeriesGroups()
const compareFn = (a, b) => { })
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date function series (args) {
const { series } = hexo.theme.config
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0 if (!series.enable) {
} hexo.log.warn('Series plugin is disabled in the theme config')
return ''
seriesArr.sort(compareFn) }
const listItems = seriesArr.map(ele => const groupName = args.length ? args[0] : this.series
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
).join('') if (!groupName) {
hexo.log.warn('No series specified')
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>` return ''
} }
hexo.extend.tag.register('series', series, { ends: false }) const groups = hexo._seriesGroups || {}
const source = groups[groupName]
if (!source || !source.length) {
hexo.log.warn(`There is no series named "${groupName}"`)
return ''
}
const isAsc = (series.order || 1) === 1
const isSortByTitle = series.orderBy === 'title'
// .slice() to copy before sorting, so we don't mutate the shared data
const seriesArr = source.slice().sort((a, b) => {
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
})
const listItems = seriesArr.map(ele =>
`<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
).join('')
return series.number ? `<ol class="series-items">${listItems}</ol>` : `<ul class="series-items">${listItems}</ul>`
}
hexo.extend.tag.register('series', series, { ends: false })
+13
View File
@@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo
--hlscrollbar-bg: lighten(#121212, 5) --hlscrollbar-bg: lighten(#121212, 5)
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9)) --hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
$scrollbar-style
// scrollbar - firefox
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
if $highlight_enable if $highlight_enable
@require 'highlight/index' @require 'highlight/index'
@@ -88,6 +96,11 @@ $code-block
&:hover &:hover
border-bottom-color: var(--hl-color) border-bottom-color: var(--hl-color)
&.default
pre
padding: 10px 20px
@extend $scrollbar-style
&.copy-true &.copy-true
user-select: all user-select: all
-webkit-user-select: all -webkit-user-select: all
+1 -6
View File
@@ -1,11 +1,6 @@
figure.highlight figure.highlight
table table
// scrollbar - firefox @extend $scrollbar-style
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
pre .deletion pre .deletion
color: $highlight-deletion color: $highlight-deletion
+1 -6
View File
@@ -6,12 +6,7 @@ if $highlight_theme != false
.container .container
pre[class*='language-'] pre[class*='language-']
// scrollbar - firefox @extend $scrollbar-style
@-moz-document url-prefix()
scrollbar-color: var(--hlscrollbar-bg) transparent
&::-webkit-scrollbar-thumb
background: var(--hlscrollbar-bg)
&:not(.line-numbers) &:not(.line-numbers)
padding: 10px 20px padding: 10px 20px
+2
View File
@@ -37,6 +37,8 @@
margin: 0 margin: 0
color: var(--white) color: var(--white)
font-size: 1.85em font-size: 1.85em
@extend .limit-more-line
-webkit-line-clamp: 3
+minWidth768() +minWidth768()
font-size: 2.85em font-size: 2.85em
+1 -1
View File
@@ -39,7 +39,7 @@
.pagination .pagination
& > * & > *
display: inline-block display: inline-block
margin: 0 6px margin: 6px
width: w = 2.5em width: w = 2.5em
height: w height: w
line-height: w line-height: w
+1 -1
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
+46 -2
View File
@@ -65,12 +65,55 @@ if hexo-config('waline.bg')
if hexo-config('mermaid.enable') if hexo-config('mermaid.enable')
.mermaid-wrap .mermaid-wrap
position: relative
margin: 0 0 20px margin: 0 0 20px
background: var(--card-bg)
text-align: center text-align: center
if hexo-config('mermaid.open_in_new_tab')
.mermaid-open-btn
position: absolute
top: 8px
right: 8px
z-index: 2
display: flex
justify-content: center
align-items: center
padding: 0
width: 34px
height: 25px
border: none
border-radius: 20%
background: #D3D3D3
box-shadow: 0 4px 10px rgba(0, 0, 0, .15)
color: #fff
font-size: 0
line-height: 1
cursor: pointer
transition: background .2s ease, transform .2s ease
i
font-size: 16px
line-height: 1
&:hover,
&:focus-visible
outline: none
background: #C0C0C0
transform: translateY(-1px)
& > svg & > svg
max-width: 100%
height: 100% height: 100%
if hexo-config('mermaid.zoom_pan')
cursor: grab
user-select: none
touch-action: none
&:active
cursor: grabbing
if hexo-config('mermaid.code_write') if hexo-config('mermaid.code_write')
pre > code.mermaid pre > code.mermaid
display: none display: none
@@ -183,5 +226,6 @@ if hexo-config('math.use')
opacity: 1 opacity: 1
+maxWidth768() +maxWidth768()
.fancybox__toolbar__column.is-middle .fancybox__toolbar__column.is-middle,
display: none .f-carousel__toolbar__column.is-middle
visibility: hidden
+210 -176
View File
@@ -2,18 +2,25 @@ document.addEventListener('DOMContentLoaded', () => {
let headerContentWidth, $nav let headerContentWidth, $nav
let mobileSidebarOpen = false let mobileSidebarOpen = false
// rightsideScrollPercent
let goUpElement = null
let scrollPercentElement = null
const adjustMenu = init => { const adjustMenu = init => {
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0) let hideMenuIndex = false
if (init) { if (init) {
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children) const blogInfoWidth = Array.from(document.querySelector('#blog-info > a').children).reduce((w, i) => w + i.offsetWidth, 0)
const menusWidth = getAllWidth(document.getElementById('menus').children) const menusWidth = Array.from(document.getElementById('menus').children).reduce((w, i) => w + i.offsetWidth, 0)
headerContentWidth = blogInfoWidth + menusWidth headerContentWidth = blogInfoWidth + menusWidth
$nav = document.getElementById('nav') $nav = document.getElementById('nav')
} }
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120 hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
$nav.classList.toggle('hide-menu', hideMenuIndex)
requestAnimationFrame(() => {
$nav.classList.toggle('hide-menu', hideMenuIndex)
})
} }
// 初始化header // 初始化header
@@ -54,18 +61,21 @@ document.addEventListener('DOMContentLoaded', () => {
* 代碼 * 代碼
* 只適用於Hexo默認的代碼渲染 * 只適用於Hexo默認的代碼渲染
*/ */
const addHighlightTool = () => { const addHighlightTool = $article => {
const highLight = GLOBAL_CONFIG.highlight const highLight = GLOBAL_CONFIG.highlight
if (!highLight) return if (!highLight) return
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]') const isNotHighlightJs = plugin !== 'highlight.js'
const isPrismjs = plugin === 'prismjs'
const $figureHighlight = isNotHighlightJs
? Array.from($article.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
: $article.querySelectorAll('figure.highlight')
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
const isPrismjs = plugin === 'prismjs'
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : '' const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : '' const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : '' const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
@@ -133,7 +143,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 = isPrismjs ? 'pre code' : 'table .code pre' const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
const codeElement = $buttonParent.querySelector(preCodeSelector) const codeElement = $buttonParent.querySelector(preCodeSelector)
if (!codeElement) return if (!codeElement) return
copy(codeElement.innerText, clickEle) copy(codeElement.innerText, clickEle)
@@ -164,33 +174,23 @@ document.addEventListener('DOMContentLoaded', () => {
// 獲取隱藏狀態下元素的真實高度 // 獲取隱藏狀態下元素的真實高度
const getActualHeight = item => { const getActualHeight = item => {
if (item.offsetHeight > 0) return item.offsetHeight if (item.offsetHeight > 0) return item.offsetHeight
const hiddenElements = new Map()
const fix = () => { const clone = item.cloneNode(true)
let current = item
while (current !== document.body && current != null) {
if (window.getComputedStyle(current).display === 'none') {
hiddenElements.set(current, current.getAttribute('style') || '')
}
current = current.parentNode
}
const style = 'visibility: hidden !important; display: block !important;' clone.style.cssText = `
hiddenElements.forEach((originalStyle, elem) => { position: absolute !important;
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style) visibility: hidden !important;
}) display: block !important;
} left: 0 !important;
top: 0 !important;
pointer-events: none !important;
z-index: -1 !important;
margin: 0 !important;
`
const restore = () => { item.parentNode.insertBefore(clone, item)
hiddenElements.forEach((originalStyle, elem) => { const height = clone.offsetHeight
if (originalStyle === '') elem.removeAttribute('style') clone.remove()
else elem.setAttribute('style', originalStyle)
})
}
fix()
const height = item.offsetHeight
restore()
return height return height
} }
@@ -213,20 +213,23 @@ document.addEventListener('DOMContentLoaded', () => {
fragment.appendChild(ele) fragment.appendChild(ele)
} }
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild) isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
} }
$figureHighlight.forEach(item => { $figureHighlight.forEach(item => {
let langName = '' let langName = ''
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' }) if (isNotHighlightJs) {
const newClassName = isPrismjs ? 'prismjs' : 'default'
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
}
if (!highlightLang) { if (!highlightLang) {
createEle('', item) createEle('', item)
return return
} }
if (isPrismjs) { if (isNotHighlightJs) {
langName = item.getAttribute('data-language') || 'Code' langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
} else { } else {
langName = item.getAttribute('class').split(' ')[1] langName = item.getAttribute('class').split(' ')[1]
if (langName === 'plain' || langName === undefined) langName = 'Code' if (langName === 'plain' || langName === undefined) langName = 'Code'
@@ -238,9 +241,9 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* PhotoFigcaption * PhotoFigcaption
*/ */
const addPhotoFigcaption = () => { const addPhotoFigcaption = $article => {
if (!GLOBAL_CONFIG.isPhotoFigcaption) return if (!GLOBAL_CONFIG.isPhotoFigcaption) return
document.querySelectorAll('#article-container img').forEach(item => { $article.querySelectorAll('img').forEach(item => {
const altValue = item.title || item.alt const altValue = item.title || item.alt
if (!altValue) return if (!altValue) return
const ele = document.createElement('div') const ele = document.createElement('div')
@@ -253,8 +256,8 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Lightbox * Lightbox
*/ */
const runLightbox = () => { const runLightbox = $article => {
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) btf.loadLightbox($article.querySelectorAll('img:not(.no-lightbox)'))
} }
/** /**
@@ -264,18 +267,23 @@ document.addEventListener('DOMContentLoaded', () => {
const fetchUrl = async url => { const fetchUrl = async url => {
try { try {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return await response.json() return await response.json()
} catch (error) { } catch (error) {
console.error('Failed to fetch URL:', error) console.error('Failed to fetch URL:', error)
return [] throw error
} }
} }
const runJustifiedGallery = (container, data, config) => { const runJustifiedGallery = (container, data, config) => {
const { isButton, limit, firstLimit, tabs } = config const { isButton, tabs } = config
const limit = Math.max(1, Number(config.limit) || 20)
const firstLimit = Math.max(1, Number(config.firstLimit) || limit)
const dataLength = data.length const dataLength = data.length
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1) const maxGroupKey = dataLength
? Math.ceil(Math.max(0, dataLength - firstLimit) / limit) + 1
: 0
// Gallery configuration // Gallery configuration
const igConfig = { const igConfig = {
@@ -292,13 +300,19 @@ document.addEventListener('DOMContentLoaded', () => {
let isLayoutHidden = false let isLayoutHidden = false
// Utility functions // Utility functions
const sanitizeString = str => (str && str.replace(/"/g, '&quot;')) || '' const sanitizeString = str => String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
const createImageItem = item => { const createImageItem = item => {
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : '' const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
const title = item.title ? `title="${sanitizeString(item.title)}"` : '' const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
const url = item.url ? sanitizeString(item.url) : ''
return `<div class="item"> return `<div class="item">
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} /> <img src="${url}" data-grid-maintained-target="true" ${alt} ${title} />
</div>` </div>`
} }
@@ -367,11 +381,13 @@ document.addEventListener('DOMContentLoaded', () => {
btf.setLoading.add(container) btf.setLoading.add(container)
ig.on('renderComplete', handleRenderComplete) ig.on('renderComplete', handleRenderComplete)
if (isButton) { if (isButton && dataLength) {
appendItems(1, firstLimit, true) appendItems(1, firstLimit, true)
} else { } else if (dataLength) {
ig.on('requestAppend', handleRequestAppend) ig.on('requestAppend', handleRequestAppend)
ig.renderItems() ig.renderItems()
} else {
btf.setLoading.remove(container)
} }
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy()) btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
@@ -394,11 +410,11 @@ document.addEventListener('DOMContentLoaded', () => {
const container = element.firstElementChild const container = element.firstElementChild
const content = container.textContent const content = container.textContent
container.textContent = '' container.textContent = ''
element.classList.add('loaded')
try { try {
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content) const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
if (!Array.isArray(data)) throw new TypeError('Gallery data must be an array')
runJustifiedGallery(container, data, config) runJustifiedGallery(container, data, config)
element.classList.add('loaded')
} catch (error) { } catch (error) {
console.error('Gallery data parsing failed:', error) console.error('Gallery data parsing failed:', error)
} }
@@ -418,11 +434,11 @@ document.addEventListener('DOMContentLoaded', () => {
*/ */
const rightsideScrollPercent = currentTop => { const rightsideScrollPercent = currentTop => {
const scrollPercent = btf.getScrollPercent(currentTop, document.body) const scrollPercent = btf.getScrollPercent(currentTop, document.body)
const goUpElement = document.getElementById('go-up')
if (!goUpElement || !scrollPercentElement) return
if (scrollPercent < 95) { if (scrollPercent < 95) {
goUpElement.classList.add('show-percent') goUpElement.classList.add('show-percent')
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent scrollPercentElement.textContent = scrollPercent
} else { } else {
goUpElement.classList.remove('show-percent') goUpElement.classList.remove('show-percent')
} }
@@ -433,24 +449,20 @@ document.addEventListener('DOMContentLoaded', () => {
*/ */
const scrollFn = () => { const scrollFn = () => {
const $rightside = document.getElementById('rightside') const $rightside = document.getElementById('rightside')
const innerHeight = window.innerHeight + 56
let initTop = 0 let initTop = 0
const $header = document.getElementById('page-header') const $header = document.getElementById('page-header')
const isChatBtn = typeof chatBtn !== 'undefined' const isChatBtn = typeof window.chatBtn !== 'undefined'
const isShowPercent = GLOBAL_CONFIG.percent.rightside const isShowPercent = GLOBAL_CONFIG.percent.rightside
// 檢查文檔高度是否小於視窗高度 // 檢查文檔高度是否小於視窗高度
const checkDocumentHeight = () => { const checkDocumentHeight = () => {
if (document.body.scrollHeight <= innerHeight) { if (document.body.scrollHeight <= window.innerHeight + 56) {
$rightside.classList.add('rightside-show') $rightside.classList.add('rightside-show')
return true return true
} }
return false return false
} }
// 如果文檔高度小於視窗高度,直接返回
if (checkDocumentHeight()) return
// find the scroll direction // find the scroll direction
const scrollDirection = currentTop => { const scrollDirection = currentTop => {
const result = currentTop > initTop // true is down & false is up const result = currentTop > initTop // true is down & false is up
@@ -459,7 +471,9 @@ document.addEventListener('DOMContentLoaded', () => {
} }
let flag = '' let flag = ''
const scrollTask = btf.throttle(() => { const scrollTask = btf.rafThrottle(() => {
if (checkDocumentHeight()) return
const currentTop = window.scrollY || document.documentElement.scrollTop const currentTop = window.scrollY || document.documentElement.scrollTop
const isDown = scrollDirection(currentTop) const isDown = scrollDirection(currentTop)
if (currentTop > 56) { if (currentTop > 56) {
@@ -487,22 +501,21 @@ document.addEventListener('DOMContentLoaded', () => {
$header.classList.remove('nav-fixed', 'nav-visible') $header.classList.remove('nav-fixed', 'nav-visible')
} }
$rightside.classList.remove('rightside-show') $rightside.classList.remove('rightside-show')
} }
isShowPercent && rightsideScrollPercent(currentTop) isShowPercent && rightsideScrollPercent(currentTop)
checkDocumentHeight() })
}, 300)
checkDocumentHeight()
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true }) btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
} }
/** /**
* toc,anchor * toc, anchor
*/ */
const scrollFnToDo = () => { const scrollFnToDo = $article => {
const isToc = GLOBAL_CONFIG_SITE.isToc const isToc = GLOBAL_CONFIG_SITE.isToc
const isAnchor = GLOBAL_CONFIG.isAnchor const isAnchor = GLOBAL_CONFIG.isAnchor
const $article = document.getElementById('article-container')
if (!($article && (isToc || isAnchor))) return if (!($article && (isToc || isAnchor))) return
@@ -515,7 +528,6 @@ document.addEventListener('DOMContentLoaded', () => {
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage') $tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
isExpand = $cardToc.classList.contains('is-expand') isExpand = $cardToc.classList.contains('is-expand')
// toc元素點擊
const tocItemClickFn = e => { const tocItemClickFn = e => {
const target = e.target.closest('.toc-link') const target = e.target.closest('.toc-link')
if (!target) return if (!target) return
@@ -542,94 +554,105 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// 處理 hexo-blog-encrypt 事件
$cardToc.style.display = 'block' $cardToc.style.display = 'block'
} }
// find head position & add active class
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6') const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
let detectItem = '' if (!$articleList.length) return
// Optimization: Cache header positions let activeTocItem = null
let headerList = [] let activeParentItems = []
const updateHeaderPositions = () => {
headerList = Array.from($articleList).map(ele => ({
ele,
top: btf.getEleTop(ele),
id: ele.id
}))
}
updateHeaderPositions() const updateTocUI = currentId => {
btf.addEventListenerPjax(window, 'resize', btf.throttle(updateHeaderPositions, 200)) const encodedAnchor = currentId ? '#' + encodeURI(decodeURI(currentId)) : ''
if (isAnchor) btf.updateAnchor(encodedAnchor)
const findHeadPosition = top => { if (!isToc) return
if (top === 0) return false
let currentId = '' if (activeTocItem) activeTocItem.classList.remove('active')
let currentIndex = '' activeParentItems.forEach(i => i.classList.remove('active'))
activeParentItems = []
for (let i = 0; i < headerList.length; i++) { if (!currentId) {
const item = headerList[i] activeTocItem = null
if (top > item.top - 80) { return
currentId = item.id ? '#' + encodeURI(item.id) : ''
currentIndex = i
} else {
break
}
} }
if (detectItem === currentIndex) return const targetLink = Array.from($tocLink).find(link => {
const href = link.getAttribute('href')
if (!href) return false
return decodeURI(href).replace('#', '') === decodeURI(currentId)
})
if (isAnchor) btf.updateAnchor(currentId) if (!targetLink) return
detectItem = currentIndex targetLink.classList.add('active')
activeTocItem = targetLink
setTimeout(() => autoScrollToc(targetLink), 0)
if (isToc) { if (!isExpand) {
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active')) let parent = targetLink.parentNode
while (!parent.matches('.toc')) {
if (currentId) { if (parent.matches('li')) {
const currentActive = $tocLink[currentIndex] parent.classList.add('active')
currentActive.classList.add('active') activeParentItems.push(parent)
setTimeout(() => autoScrollToc(currentActive), 0)
if (!isExpand) {
let parent = currentActive.parentNode
while (!parent.matches('.toc')) {
if (parent.matches('li')) parent.classList.add('active')
parent = parent.parentNode
}
} }
parent = parent.parentNode
} }
} }
} }
// main of scroll const observerOptions = {
const tocScrollFn = btf.throttle(() => { root: null,
rootMargin: '-60px 0px -80% 0px',
threshold: 0
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
updateTocUI(entry.target.id)
}
})
}, observerOptions)
$articleList.forEach(ele => observer.observe(ele))
const scrollHandler = btf.rafThrottle(() => {
const currentTop = window.scrollY || document.documentElement.scrollTop const currentTop = window.scrollY || document.documentElement.scrollTop
if (isToc && GLOBAL_CONFIG.percent.toc) { if (isToc && GLOBAL_CONFIG.percent.toc) {
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article) $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
} }
findHeadPosition(currentTop)
}, 100)
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true }) if (currentTop === 0) {
updateTocUI('')
} else if (currentTop + window.innerHeight >= document.documentElement.scrollHeight - 10) {
const lastHeader = $articleList[$articleList.length - 1]
updateTocUI(lastHeader.id)
}
})
btf.addEventListenerPjax(window, 'scroll', scrollHandler, { passive: true })
btf.addGlobalFn('pjaxSendOnce', () => {
observer.disconnect()
})
} }
const handleThemeChange = mode => { const handleThemeChange = mode => {
const globalFn = window.globalFn || {} const globalFn = window.globalFn || {}
const themeChange = globalFn.themeChange || {} const themeChange = globalFn.themeChange
if (!themeChange) { if (!themeChange) return
return
}
Object.keys(themeChange).forEach(key => { Object.keys(themeChange).forEach(key => {
const themeChangeFn = themeChange[key] const fn = themeChange[key]
if (typeof fn !== 'function') return
if (['disqus', 'disqusjs'].includes(key)) { if (['disqus', 'disqusjs'].includes(key)) {
setTimeout(() => themeChangeFn(mode), 300) setTimeout(() => fn(mode), 300)
} else { } else {
themeChangeFn(mode) fn(mode)
} }
}) })
} }
@@ -791,8 +814,8 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* table overflow * table overflow
*/ */
const addTableWrap = () => { const addTableWrap = $article => {
const $table = document.querySelectorAll('#article-container table') const $table = $article.querySelectorAll('table')
if (!$table.length) return if (!$table.length) return
$table.forEach(item => { $table.forEach(item => {
@@ -802,52 +825,54 @@ document.addEventListener('DOMContentLoaded', () => {
}) })
} }
/** const clickFnOfTagHide = $article => {
* tag-hide const hideButtons = $article.querySelectorAll('.hide-button')
*/
const clickFnOfTagHide = () => {
const hideButtons = document.querySelectorAll('#article-container .hide-button')
if (!hideButtons.length) return if (!hideButtons.length) return
hideButtons.forEach(item => item.addEventListener('click', e => {
const currentTarget = e.currentTarget const handleClickOfTagHide = e => {
currentTarget.classList.add('open') const button = e.target.closest('.hide-button')
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container')) if (!button) return
}, { once: true })) button.classList.add('open')
addJustifiedGallery(button.nextElementSibling.querySelectorAll('.gallery-container'))
}
btf.addEventListenerPjax($article, 'click', handleClickOfTagHide)
} }
const tabsFn = () => { const tabsFn = $article => {
const navTabsElements = document.querySelectorAll('#article-container .tabs') if (!$article.querySelector('.tabs')) return
if (!navTabsElements.length) return
const setActiveClass = (elements, activeIndex) => { const setActiveClass = (elements, activeIndex) => {
elements.forEach((el, index) => { elements.forEach((el, index) => el.classList.toggle('active', index === activeIndex))
el.classList.toggle('active', index === activeIndex)
})
} }
const handleNavClick = e => { const handleClick = e => {
const target = e.target.closest('button') const tabsRoot = e.target.closest('.tabs')
if (!target || target.classList.contains('active')) return if (!tabsRoot) return
const navItems = [...e.currentTarget.children] const navContainer = tabsRoot.firstElementChild
const tabContents = [...e.currentTarget.nextElementSibling.children] const toTopContainer = tabsRoot.lastElementChild
const indexOfButton = navItems.indexOf(target)
setActiveClass(navItems, indexOfButton)
e.currentTarget.classList.remove('no-default')
setActiveClass(tabContents, indexOfButton)
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
}
const handleToTopClick = tabElement => e => { if (navContainer.contains(e.target)) {
if (e.target.closest('button')) { const target = e.target.closest('button')
btf.scrollToDest(btf.getEleTop(tabElement), 300) if (!target || target.classList.contains('active')) return
const navItems = [...navContainer.children]
const tabContents = [...navContainer.nextElementSibling.children]
const indexOfButton = navItems.indexOf(target)
setActiveClass(navItems, indexOfButton)
navContainer.classList.remove('no-default')
setActiveClass(tabContents, indexOfButton)
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
return
}
if (toTopContainer.contains(e.target) && e.target.closest('button')) {
btf.scrollToDest(btf.getEleTop(tabsRoot), 300)
} }
} }
navTabsElements.forEach(tabElement => { btf.addEventListenerPjax($article, 'click', handleClick)
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
})
} }
const toggleCardCategory = () => { const toggleCardCategory = () => {
@@ -913,10 +938,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
const unRefreshFn = () => { const unRefreshFn = () => {
window.addEventListener('resize', () => { const resizeHandler = btf.rafThrottle(() => {
adjustMenu(false) adjustMenu(false)
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close() if (mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu'))) {
sidebarFn.close()
}
}) })
window.addEventListener('resize', resizeHandler, { passive: true })
const menuMask = document.getElementById('menu-mask') const menuMask = document.getElementById('menu-mask')
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() }) menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
@@ -934,18 +962,24 @@ document.addEventListener('DOMContentLoaded', () => {
} }
const forPostFn = () => { const forPostFn = () => {
addHighlightTool() const $article = document.getElementById('article-container')
addPhotoFigcaption() if (!$article || $article.querySelector('.hbe-container')) return
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
runLightbox() addHighlightTool($article)
scrollFnToDo() addPhotoFigcaption($article)
addTableWrap() addJustifiedGallery($article.querySelectorAll('.gallery-container'))
clickFnOfTagHide() runLightbox($article)
tabsFn() scrollFnToDo($article)
addTableWrap($article)
clickFnOfTagHide($article)
tabsFn($article)
} }
const refreshFn = () => { const refreshFn = () => {
initAdjust() initAdjust()
goUpElement = document.getElementById('go-up')
scrollPercentElement = goUpElement?.querySelector('.scroll-percent')
justifiedIndexPostUI() justifiedIndexPostUI()
if (GLOBAL_CONFIG_SITE.pageType === 'post') { if (GLOBAL_CONFIG_SITE.pageType === 'post') {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -11
View File
File diff suppressed because one or more lines are too long
+48 -21
View File
@@ -45,29 +45,55 @@
} }
}, },
overflowPaddingR: { rafThrottle: fn => {
add: () => { let rafId = null
const paddingRight = window.innerWidth - document.body.clientWidth return (...args) => {
if (rafId) return
if (paddingRight > 0) { rafId = requestAnimationFrame(() => {
document.body.style.paddingRight = `${paddingRight}px` fn(...args)
document.body.style.overflow = 'hidden' rafId = null
const menuElement = document.querySelector('#page-header.nav-fixed #menus') })
if (menuElement) {
menuElement.style.paddingRight = `${paddingRight}px`
}
}
},
remove: () => {
document.body.style.paddingRight = ''
document.body.style.overflow = ''
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
if (menuElement) {
menuElement.style.paddingRight = ''
}
} }
}, },
overflowPaddingR: (() => {
let headerElement = null
let menuElement = null
const getElements = () => {
if (!headerElement) {
headerElement = document.getElementById('page-header')
}
if (!menuElement) {
menuElement = document.getElementById('menus')
}
return { headerElement, menuElement }
}
return {
add: () => {
const paddingRight = window.innerWidth - document.body.clientWidth
if (paddingRight > 0) {
document.body.style.paddingRight = `${paddingRight}px`
document.body.style.overflow = 'hidden'
const { headerElement: header, menuElement: menu } = getElements()
if (header && menu && header.classList.contains('nav-fixed')) {
menu.style.paddingRight = `${paddingRight}px`
}
}
},
remove: () => {
document.body.style.paddingRight = ''
document.body.style.overflow = ''
const { headerElement: header, menuElement: menu } = getElements()
if (header && menu && header.classList.contains('nav-fixed')) {
menu.style.paddingRight = ''
}
}
}
})(),
snackbarShow: (text, showAction = false, duration = 2000) => { snackbarShow: (text, showAction = false, duration = 2000) => {
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
@@ -133,7 +159,8 @@
const animate = currentTime => { const animate = currentTime => {
const timeElapsed = currentTime - startTime const timeElapsed = currentTime - startTime
const progress = Math.min(timeElapsed / time, 1) const progress = Math.min(timeElapsed / time, 1)
window.scrollTo(0, currentPos + (pos - currentPos) * progress) const easedProgress = 1 - Math.pow(1 - progress, 4) // easeOutQuart
window.scrollTo(0, currentPos + (pos - currentPos) * easedProgress)
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(animate) requestAnimationFrame(animate)
} }