mirror of
https://github.com/jerryc127/hexo-theme-butterfly.git
synced 2026-04-08 12:07:06 +08:00
feat: 更新 plugins.yml 中的依賴版本至最新
feat: 優化 aside_archives ,改進性能和可讀性 feat: 改善 inlineImg 和 timeline 標籤的文檔,優化時間線邏輯 feat: 更新 gallery 標籤以支持額外參數,優化圖片顯示邏輯 improvement: 優化隨機封面過濾器邏輯, 避免連續重複 feat: 最新評論限制顯示 1-10 條之間 fix: artalk 的最新評論顯示待定或者封禁的評論的 bug
This commit is contained in:
@@ -1,40 +1,60 @@
|
||||
/**
|
||||
* Butterfly
|
||||
* ramdom cover
|
||||
* Random cover for posts
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
hexo.extend.filter.register('before_post_render', data => {
|
||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||
let { cover: coverVal, top_img: topImg } = data
|
||||
|
||||
// Add path to top_img and cover if post_asset_folder is enabled
|
||||
if (hexo.config.post_asset_folder) {
|
||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) data.top_img = `${data.path}${topImg}`
|
||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) data.cover = `${data.path}${coverVal}`
|
||||
}
|
||||
|
||||
hexo.extend.generator.register('post', locals => {
|
||||
const recentCovers = []
|
||||
const randomCoverFn = () => {
|
||||
const { cover: { default_cover: defaultCover } } = hexo.theme.config
|
||||
if (!defaultCover) return false
|
||||
if (!Array.isArray(defaultCover)) return defaultCover
|
||||
const num = Math.floor(Math.random() * defaultCover.length)
|
||||
const defaultCoverLen = defaultCover.length
|
||||
const limit = 3
|
||||
|
||||
let num
|
||||
do {
|
||||
num = Math.floor(Math.random() * defaultCoverLen)
|
||||
} while (recentCovers.includes(num))
|
||||
|
||||
recentCovers.push(num)
|
||||
if (recentCovers.length > limit) recentCovers.shift()
|
||||
|
||||
return defaultCover[num]
|
||||
}
|
||||
|
||||
if (coverVal === false) return data
|
||||
const handleImg = data => {
|
||||
const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
|
||||
let { cover: coverVal, top_img: topImg } = data
|
||||
|
||||
// If cover is not set, use random cover
|
||||
if (!coverVal) {
|
||||
const randomCover = randomCoverFn()
|
||||
data.cover = randomCover
|
||||
coverVal = randomCover // update coverVal
|
||||
// Add path to top_img and cover if post_asset_folder is enabled
|
||||
if (hexo.config.post_asset_folder) {
|
||||
if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) data.top_img = `${data.path}${topImg}`
|
||||
if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) data.cover = `${data.path}${coverVal}`
|
||||
}
|
||||
|
||||
if (coverVal === false) return data
|
||||
|
||||
// If cover is not set, use random cover
|
||||
if (!coverVal) {
|
||||
const randomCover = randomCoverFn()
|
||||
data.cover = randomCover
|
||||
coverVal = randomCover // update coverVal
|
||||
}
|
||||
|
||||
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
|
||||
data.cover_type = 'img'
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
|
||||
data.cover_type = 'img'
|
||||
}
|
||||
|
||||
return data
|
||||
return locals.posts.sort('date').map(post => {
|
||||
return {
|
||||
data: handleImg(post),
|
||||
layout: 'post',
|
||||
path: post.path
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,38 +2,68 @@
|
||||
|
||||
hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
const { config, page, site, url_for, _p } = this
|
||||
const archiveDir = config.archive_dir
|
||||
const { timezone } = config
|
||||
const lang = toMomentLocale(page.lang || page.language || config.language)
|
||||
const type = options.type || 'monthly'
|
||||
const format = options.format || (type === 'monthly' ? 'MMMM YYYY' : 'YYYY')
|
||||
const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true
|
||||
const order = options.order || -1
|
||||
const limit = options.limit
|
||||
const {
|
||||
archive_dir: archiveDir,
|
||||
timezone,
|
||||
language
|
||||
} = config
|
||||
|
||||
// Destructure and set default options with object destructuring
|
||||
const {
|
||||
type = 'monthly',
|
||||
format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY',
|
||||
show_count: showCount = true,
|
||||
order = -1,
|
||||
limit,
|
||||
transform
|
||||
} = options
|
||||
|
||||
// Optimize locale handling
|
||||
const lang = toMomentLocale(page.lang || page.language || language)
|
||||
|
||||
// Memoize comparison function to improve performance
|
||||
const compareFunc = type === 'monthly'
|
||||
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
|
||||
: (yearA, monthA, yearB, monthB) => yearA === yearB
|
||||
: (yearA, yearB) => yearA === yearB
|
||||
|
||||
const posts = site.posts.sort('date', order)
|
||||
if (!posts.length) return ''
|
||||
// Early return if no posts
|
||||
if (!site.posts.length) return ''
|
||||
|
||||
const data = []
|
||||
posts.forEach(post => {
|
||||
let date = post.date.clone()
|
||||
if (timezone) date = date.tz(timezone)
|
||||
// Use reduce for more efficient data processing
|
||||
const data = site.posts
|
||||
.sort('date', order)
|
||||
.reduce((acc, post) => {
|
||||
let date = post.date.clone()
|
||||
if (timezone) date = date.tz(timezone)
|
||||
|
||||
const year = date.year()
|
||||
const month = date.month() + 1
|
||||
const year = date.year()
|
||||
const month = date.month() + 1
|
||||
|
||||
if (!data.length || !compareFunc(data[data.length - 1].year, data[data.length - 1].month, year, month)) {
|
||||
if (lang) date = date.locale(lang)
|
||||
data.push({ name: date.format(format), year, month, count: 1 })
|
||||
} else {
|
||||
data[data.length - 1].count++
|
||||
}
|
||||
})
|
||||
|
||||
const link = item => {
|
||||
// Find or create archive entry
|
||||
const lastEntry = acc[acc.length - 1]
|
||||
if (!lastEntry || !compareFunc(
|
||||
lastEntry.year,
|
||||
lastEntry.month,
|
||||
year,
|
||||
month
|
||||
)) {
|
||||
acc.push({
|
||||
name: date.format(format),
|
||||
year,
|
||||
month,
|
||||
count: 1
|
||||
})
|
||||
} else {
|
||||
lastEntry.count++
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Create link generator function
|
||||
const createArchiveLink = item => {
|
||||
let url = `${archiveDir}/${item.year}/`
|
||||
if (type === 'monthly') {
|
||||
url += item.month < 10 ? `0${item.month}/` : `${item.month}/`
|
||||
@@ -41,37 +71,48 @@ hexo.extend.helper.register('aside_archives', function (options = {}) {
|
||||
return url_for(url)
|
||||
}
|
||||
|
||||
const len = data.length
|
||||
const limitLength = limit === 0 ? len : Math.min(len, limit)
|
||||
// Limit results efficiently
|
||||
const limitedData = limit > 0
|
||||
? data.slice(0, Math.min(data.length, limit))
|
||||
: data
|
||||
|
||||
let result = `
|
||||
// Use template literal for better readability
|
||||
const archiveHeader = `
|
||||
<div class="item-headline">
|
||||
<i class="fas fa-archive"></i>
|
||||
<span>${_p('aside.card_archives')}</span>
|
||||
${len > limitLength ? `<a class="card-more-btn" href="${url_for(archiveDir)}/" title="${_p('aside.more_button')}"><i class="fas fa-angle-right"></i></a>` : ''}
|
||||
${data.length > limitedData.length
|
||||
? `<a class="card-more-btn" href="${url_for(archiveDir)}/"
|
||||
title="${_p('aside.more_button')}">
|
||||
<i class="fas fa-angle-right"></i>
|
||||
</a>`
|
||||
: ''}
|
||||
</div>
|
||||
<ul class="card-archive-list">
|
||||
`
|
||||
|
||||
for (let i = 0; i < limitLength; i++) {
|
||||
const item = data[i]
|
||||
result += `
|
||||
<li class="card-archive-list-item">
|
||||
<a class="card-archive-list-link" href="${link(item)}">
|
||||
<span class="card-archive-list-date">${options.transform ? options.transform(item.name) : item.name}</span>
|
||||
${showCount ? `<span class="card-archive-list-count">${item.count}</span>` : ''}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
// Use map for generating list items, join for performance
|
||||
const archiveList = `
|
||||
<ul class="card-archive-list">
|
||||
${limitedData.map(item => `
|
||||
<li class="card-archive-list-item">
|
||||
<a class="card-archive-list-link" href="${createArchiveLink(item)}">
|
||||
<span class="card-archive-list-date">
|
||||
${transform ? transform(item.name) : item.name}
|
||||
</span>
|
||||
${showCount
|
||||
? `<span class="card-archive-list-count">${item.count}</span>`
|
||||
: ''}
|
||||
</a>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
`
|
||||
|
||||
result += '</ul>'
|
||||
return result
|
||||
return archiveHeader + archiveList
|
||||
})
|
||||
|
||||
const toMomentLocale = function (lang) {
|
||||
if (!lang || lang === 'en' || lang === 'default') {
|
||||
return 'en'
|
||||
}
|
||||
// Improved locale conversion function
|
||||
const toMomentLocale = lang => {
|
||||
if (!lang || ['en', 'default'].includes(lang)) return 'en'
|
||||
return lang.toLowerCase().replace('_', '-')
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* galleryGroup and gallery
|
||||
* {% galleryGroup [name] [descr] [url] [img] %}
|
||||
*
|
||||
* {% gallery [button] %}
|
||||
* {% gallery [button],[limit],[firstLimit] %}
|
||||
* {% gallery url,[url],[button] %}
|
||||
*/
|
||||
|
||||
@@ -11,54 +11,66 @@
|
||||
|
||||
const urlFor = require('hexo-util').url_for.bind(hexo)
|
||||
|
||||
const gallery = (args, content) => {
|
||||
args = args.join(' ').split(',')
|
||||
let button = false
|
||||
let type = 'data'
|
||||
let dataStr = ''
|
||||
const DEFAULT_LIMIT = 10
|
||||
const DEFAULT_FIRST_LIMIT = 10
|
||||
const IMAGE_REGEX = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g
|
||||
|
||||
if (args[0] === 'url') {
|
||||
[type, dataStr, button] = args // url,[link],[lazyload]
|
||||
dataStr = urlFor(dataStr)
|
||||
} else {
|
||||
[button] = args // [lazyload]
|
||||
const regex = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g
|
||||
let m
|
||||
const arr = []
|
||||
while ((m = regex.exec(content)) !== null) {
|
||||
if (m.index === regex.lastIndex) {
|
||||
regex.lastIndex++
|
||||
}
|
||||
arr.push({
|
||||
url: m[2],
|
||||
alt: m[1],
|
||||
title: m[3]
|
||||
})
|
||||
}
|
||||
// Helper functions
|
||||
const parseGalleryArgs = args => {
|
||||
const [type, ...rest] = args.join(' ').split(',').map(arg => arg.trim())
|
||||
return {
|
||||
isUrl: type === 'url',
|
||||
params: type === 'url' ? rest : [type, ...rest]
|
||||
}
|
||||
}
|
||||
|
||||
dataStr = JSON.stringify(arr)
|
||||
const parseImageContent = content => {
|
||||
const images = []
|
||||
let match
|
||||
|
||||
while ((match = IMAGE_REGEX.exec(content)) !== null) {
|
||||
images.push({
|
||||
url: match[2],
|
||||
alt: match[1] || '',
|
||||
title: match[3] || ''
|
||||
})
|
||||
}
|
||||
|
||||
return `<div class="gallery-container" data-type="${type}" data-button="${button}">
|
||||
<div class="gallery-items">${dataStr}</div>
|
||||
</div>`
|
||||
return images
|
||||
}
|
||||
|
||||
const createGalleryHTML = (type, dataStr, button, limit, firstLimit) => {
|
||||
return `<div class="gallery-container" data-type="${type}" data-button="${button}" data-limit="${limit}" data-first="${firstLimit}">
|
||||
<div class="gallery-items">${dataStr}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
const gallery = (args, content) => {
|
||||
const { isUrl, params } = parseGalleryArgs(args)
|
||||
|
||||
if (isUrl) {
|
||||
const [dataStr, button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
|
||||
return createGalleryHTML('url', urlFor(dataStr), button, limit, firstLimit)
|
||||
}
|
||||
|
||||
const [button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
|
||||
const images = parseImageContent(content)
|
||||
return createGalleryHTML('data', JSON.stringify(images), button, limit, firstLimit)
|
||||
}
|
||||
|
||||
const galleryGroup = args => {
|
||||
const [name, descr, url, img] = args
|
||||
const imgUrl = urlFor(img)
|
||||
const urlLink = urlFor(url)
|
||||
const [name = '', descr = '', url = '', img = ''] = args.map(arg => arg.trim())
|
||||
|
||||
return `<figure class="gallery-group">
|
||||
<img class="gallery-group-img no-lightbox" src='${imgUrl}' alt="Group Image Gallery">
|
||||
<figcaption>
|
||||
<div class="gallery-group-name">${name}</div>
|
||||
<p>${descr}</p>
|
||||
<a href='${urlLink}'></a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`
|
||||
<img class="gallery-group-img no-lightbox" src='${urlFor(img)}' alt="Group Image Gallery">
|
||||
<figcaption>
|
||||
<div class="gallery-group-name">${name}</div>
|
||||
<p>${descr}</p>
|
||||
<a href='${urlFor(url)}'></a>
|
||||
</figcaption>
|
||||
</figure>`
|
||||
}
|
||||
|
||||
// Register tags
|
||||
hexo.extend.tag.register('gallery', gallery, { ends: true })
|
||||
hexo.extend.tag.register('galleryGroup', galleryGroup)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* inlineImg 圖片
|
||||
* @param {Array} args 圖片名稱和高度
|
||||
* @param {string} args[0] 圖片名稱
|
||||
* @param {number} args[1] 圖片高度
|
||||
* @returns {string} 圖片標籤
|
||||
* inlineImg
|
||||
* @param {Array} args - Image name and height
|
||||
* @param {string} args[0] - Image name
|
||||
* @param {number} args[1] - Image height
|
||||
* @returns {string} - Image tag
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
@@ -10,7 +10,7 @@ const { escapeHTML } = require('hexo-util')
|
||||
|
||||
const mermaid = (args, content) => {
|
||||
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
|
||||
${escapeHTML(content)}
|
||||
${escapeHTML(content)}
|
||||
</pre></div>`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
/**
|
||||
* timeline
|
||||
* by Jerry
|
||||
* Timeline tag for Hexo
|
||||
* Syntax:
|
||||
* {% timeline [headline],[color] %}
|
||||
* <!-- timeline [title] -->
|
||||
* [content]
|
||||
* <!-- endtimeline -->
|
||||
* <!-- timeline [title] -->
|
||||
* [content]
|
||||
* <!-- endtimeline -->
|
||||
* {% endtimeline %}
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const timeLineFn = (args, content) => {
|
||||
const tlBlock = /<!--\s*timeline (.*?)\s*-->\n([\w\W\s\S]*?)<!--\s*endtimeline\s*-->/g
|
||||
// Use named capture groups for better readability
|
||||
const tlBlock = /<!--\s*timeline\s*(?<title>.*?)\s*-->\n(?<content>[\s\S]*?)<!--\s*endtimeline\s*-->/g
|
||||
|
||||
let result = ''
|
||||
let color = ''
|
||||
let text = ''
|
||||
if (args.length) {
|
||||
[text, color] = args.join(' ').split(',')
|
||||
const mdContent = hexo.render.renderSync({ text, engine: 'markdown' })
|
||||
result += `<div class='timeline-item headline'><div class='timeline-item-title'><div class='item-circle'>${mdContent}</div></div></div>`
|
||||
}
|
||||
// Pre-compile markdown render function
|
||||
const renderMd = text => hexo.render.renderSync({ text, engine: 'markdown' })
|
||||
|
||||
const matches = []
|
||||
let match
|
||||
// Parse arguments more efficiently
|
||||
const [text, color = ''] = args.length ? args.join(' ').split(',') : []
|
||||
|
||||
while ((match = tlBlock.exec(content)) !== null) {
|
||||
matches.push(match[1])
|
||||
matches.push(match[2])
|
||||
}
|
||||
// Build initial headline if text exists
|
||||
const headline = text
|
||||
? `<div class='timeline-item headline'>
|
||||
<div class='timeline-item-title'>
|
||||
<div class='item-circle'>${renderMd(text)}</div>
|
||||
</div>
|
||||
</div>`
|
||||
: ''
|
||||
|
||||
for (let i = 0; i < matches.length; i += 2) {
|
||||
const tlChildTitle = hexo.render.renderSync({ text: matches[i], engine: 'markdown' })
|
||||
const tlChildContent = hexo.render.renderSync({ text: matches[i + 1], engine: 'markdown' })
|
||||
// Match all timeline blocks in one pass and transform
|
||||
const items = Array.from(content.matchAll(tlBlock))
|
||||
.map(({ groups: { title, content } }) =>
|
||||
`<div class='timeline-item'>
|
||||
<div class='timeline-item-title'>
|
||||
<div class='item-circle'>${renderMd(title)}</div>
|
||||
</div>
|
||||
<div class='timeline-item-content'>${renderMd(content)}</div>
|
||||
</div>`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const tlTitleHtml = `<div class='timeline-item-title'><div class='item-circle'>${tlChildTitle}</div></div>`
|
||||
const tlContentHtml = `<div class='timeline-item-content'>${tlChildContent}</div>`
|
||||
|
||||
result += `<div class='timeline-item'>${tlTitleHtml + tlContentHtml}</div>`
|
||||
}
|
||||
|
||||
return `<div class="timeline ${color || ''}">${result}</div>`
|
||||
return `<div class="timeline ${color}">${headline}${items}</div>`
|
||||
}
|
||||
|
||||
hexo.extend.tag.register('timeline', timeLineFn, { ends: true })
|
||||
|
||||
Reference in New Issue
Block a user