This commit is contained in:
2024-07-30 21:41:51 +08:00
commit 192ef21b12
574 changed files with 70686 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/**
* Butterfly
* 404 error page
*/
'use strict'
hexo.extend.generator.register('404', function (locals) {
if (!hexo.theme.config.error_404.enable) return
return {
path: '404.html',
layout: ['page'],
data: {
type: '404',
top_img: false
}
}
})

View File

@@ -0,0 +1,97 @@
/**
* Butterfly
* Merge CDN
*/
'use strict'
const { version } = require('../../package.json')
const path = require('path')
hexo.extend.filter.register('before_generate', () => {
const themeConfig = hexo.theme.config
const { CDN } = themeConfig
const thirdPartySrc = hexo.render.renderSync({ path: path.join(hexo.theme_dir, '/plugins.yml'), engine: 'yaml' })
const internalSrc = {
main: {
name: 'hexo-theme-butterfly',
file: 'js/main.js',
version
},
utils: {
name: 'hexo-theme-butterfly',
file: 'js/utils.js',
version
},
translate: {
name: 'hexo-theme-butterfly',
file: 'js/tw_cn.js',
version
},
local_search: {
name: 'hexo-theme-butterfly',
file: 'js/search/local-search.js',
version
},
algolia_js: {
name: 'hexo-theme-butterfly',
file: 'js/search/algolia.js',
version
}
}
const minFile = file => {
return file.replace(/(?<!\.min)\.(js|css)$/g, ext => '.min' + ext)
}
const createCDNLink = (data, type, cond = '') => {
Object.keys(data).forEach(key => {
let { name, version, file, other_name } = data[key]
const cdnjs_name = other_name || name
const cdnjs_file = file.replace(/^[lib|dist]*\/|browser\//g, '')
const min_cdnjs_file = minFile(cdnjs_file)
if (cond === 'internal') file = `source/${file}`
const min_file = minFile(file)
const verType = CDN.version ? (type === 'local' ? `?v=${version}` : `@${version}`) : ''
const value = {
version,
name,
file,
cdnjs_file,
min_file,
min_cdnjs_file,
cdnjs_name
}
const cdnSource = {
local: cond === 'internal' ? `${cdnjs_file + verType}` : `/pluginsSrc/${name}/${file + verType}`,
jsdelivr: `https://cdn.jsdelivr.net/npm/${name}${verType}/${min_file}`,
unpkg: `https://unpkg.com/${name}${verType}/${file}`,
cdnjs: `https://cdnjs.cloudflare.com/ajax/libs/${cdnjs_name}/${version}/${min_cdnjs_file}`,
custom: (CDN.custom_format || '').replace(/\$\{(.+?)\}/g, (match, $1) => value[$1])
}
data[key] = cdnSource[type]
})
if (cond === 'internal') data.main_css = 'css/index.css' + (CDN.version ? `?v=${version}` : '')
return data
}
// delete null value
const deleteNullValue = obj => {
if (!obj) return
for (const i in obj) {
obj[i] === null && delete obj[i]
}
return obj
}
themeConfig.asset = Object.assign(
createCDNLink(internalSrc, CDN.internal_provider, 'internal'),
createCDNLink(thirdPartySrc, CDN.third_party_provider),
deleteNullValue(CDN.option)
)
})

View File

@@ -0,0 +1,14 @@
/**
* Capitalize the first letter of comment name
*/
hexo.extend.filter.register('before_generate', () => {
const themeConfig = hexo.theme.config
let { use } = themeConfig.comments
if (!use) return
if (typeof use === 'string') {
use = use.split(',')
}
const newArray = use.map(item => item.toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase()))
themeConfig.comments.use = newArray
})

View File

@@ -0,0 +1,20 @@
hexo.extend.filter.register('before_generate', () => {
// Get first two digits of the Hexo version number
const { version, log, locals } = hexo
const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
if (hexoVer < 5.3) {
log.error('Please update Hexo to V5.3.0 or higher!')
log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
process.exit(-1)
}
if (locals.get) {
const data = locals.get('data')
if (data && data.butterfly) {
log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
process.exit(-1)
}
}
})

View File

@@ -0,0 +1,545 @@
hexo.extend.filter.register('before_generate', () => {
const defaultConfig = {
nav: {
logo: null,
display_title: true,
fixed: false
},
menu: null,
highlight_theme: 'light',
highlight_copy: true,
highlight_lang: true,
highlight_shrink: false,
highlight_height_limit: false,
code_word_wrap: false,
social: null,
favicon: '/img/favicon.png',
avatar: {
img: 'https://i.loli.net/2021/02/24/5O1day2nriDzjSu.png',
effect: false
},
disable_top_img: false,
index_img: null,
default_top_img: null,
archive_img: null,
tag_img: null,
tag_per_img: null,
category_img: null,
category_per_img: null,
cover: {
index_enable: true,
aside_enable: true,
archives_enable: true,
position: 'both',
default_cover: null
},
error_img: {
flink: '/img/friend_404.gif',
post_page: '/img/404.jpg'
},
error_404: {
enable: false,
subtitle: 'Page Not Found',
background: 'https://i.loli.net/2020/05/19/aKOcLiyPl2JQdFD.png'
},
post_meta: {
page: {
date_type: 'created',
date_format: 'date',
categories: true,
tags: false,
label: true
},
post: {
date_type: 'both',
date_format: 'date',
categories: true,
tags: true,
label: true
}
},
index_post_content: {
method: 3,
length: 500
},
anchor: {
auto_update: false,
click_to_scroll: false
},
photofigcaption: false,
copy: {
enable: true,
copyright: {
enable: false,
limit_count: 50
}
},
toc: {
post: true,
page: false,
number: true,
expand: false,
style_simple: false,
scroll_percent: true
},
post_copyright: {
enable: true,
decode: false,
author_href: null,
license: 'CC BY-NC-SA 4.0',
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
},
reward: {
enable: false,
text: null,
QR_code: null
},
post_edit: {
enable: false,
url: null
},
related_post: {
enable: true,
limit: 6,
date_type: 'created'
},
post_pagination: 1,
noticeOutdate: {
enable: false,
style: 'flat',
limit_day: 500,
position: 'top',
message_prev: 'It has been',
message_next: 'days since the last update, the content of the article may be outdated.'
},
footer: {
owner: {
enable: true,
since: 2020
},
custom_text: null,
copyright: true
},
aside: {
enable: true,
hide: false,
button: true,
mobile: true,
position: 'right',
display: {
archive: true,
tag: true,
category: true
},
card_author: {
enable: true,
description: null,
button: {
enable: true,
icon: 'fab fa-github',
text: 'Follow Me',
link: 'https://github.com/xxxxxx'
}
},
card_announcement: {
enable: true,
content: 'This is my Blog'
},
card_recent_post: {
enable: true,
limit: 5,
sort: 'date',
sort_order: null
},
card_categories: {
enable: true,
limit: 8,
expand: 'none',
sort_order: null
},
card_tags: {
enable: true,
limit: 40,
color: false,
orderby: 'random',
order: 1,
sort_order: null
},
card_archives: {
enable: true,
type: 'monthly',
format: 'MMMM YYYY',
order: -1,
limit: 8,
sort_order: null
},
card_webinfo: {
enable: true,
post_count: true,
last_push_date: true,
sort_order: null
},
card_post_series: {
enable: true,
series_title: false,
orderBy: 'date',
order: -1
}
},
busuanzi: {
site_uv: true,
site_pv: true,
page_pv: true
},
runtimeshow: {
enable: false,
publish_date: null
},
newest_comments: {
enable: false,
sort_order: null,
limit: 6,
storage: 10,
avatar: true
},
translate: {
enable: false,
default: '繁',
defaultEncoding: 2,
translateDelay: 0,
msgToTraditionalChinese: '繁',
msgToSimplifiedChinese: '簡'
},
readmode: true,
darkmode: {
enable: true,
button: true,
autoChangeMode: false,
start: null,
end: null
},
rightside_scroll_percent: false,
rightside_item_order: {
enable: false,
hide: null,
show: null
},
mathjax: {
enable: false,
per_page: false
},
katex: {
enable: false,
per_page: false,
hide_scrollbar: true
},
algolia_search: {
enable: false,
hits: {
per_page: 6
}
},
local_search: {
enable: false,
preload: false,
top_n_per_article: 1,
unescape: false,
CDN: null
},
docsearch: {
enable: false,
appId: null,
apiKey: null,
indexName: null,
option: null
},
sharejs: {
enable: true,
sites: 'facebook,twitter,wechat,weibo,qq'
},
addtoany: {
enable: false,
item: 'facebook,twitter,wechat,sina_weibo,facebook_messenger,email,copy_link'
},
comments: {
use: null,
text: true,
lazyload: false,
count: false,
card_post_count: false
},
disqus: {
shortname: null,
apikey: null
},
disqusjs: {
shortname: null,
apikey: null,
option: null
},
livere: {
uid: null
},
gitalk: {
client_id: null,
client_secret: null,
repo: null,
owner: null,
admin: null,
option: null
},
valine: {
appId: null,
appKey: null,
avatar: 'monsterid',
serverURLs: null,
bg: null,
visitor: false,
option: null
},
waline: {
serverURL: null,
bg: null,
pageview: false,
option: null
},
utterances: {
repo: null,
issue_term: 'pathname',
light_theme: 'github-light',
dark_theme: 'photon-dark'
},
facebook_comments: {
app_id: null,
user_id: null,
pageSize: 10,
order_by: 'social',
lang: 'zh_TW'
},
twikoo: {
envId: null,
region: null,
visitor: false,
option: null
},
giscus: {
repo: null,
repo_id: null,
category_id: null,
theme: {
light: 'light',
dark: 'dark'
},
option: null
},
remark42: {
host: null,
siteId: null,
option: null
},
artalk: {
server: null,
site: null,
visitor: false,
option: null
},
chat_btn: false,
chat_hide_show: false,
chatra: {
enable: false,
id: null
},
tidio: {
enable: false,
public_key: null
},
daovoice: {
enable: false,
app_id: null
},
crisp: {
enable: false,
website_id: null
},
messenger: {
enable: false,
pageID: null,
lang: 'zh_TW'
},
baidu_analytics: null,
google_analytics: null,
cloudflare_analytics: null,
microsoft_clarity: null,
google_adsense: {
enable: false,
auto_ads: true,
js: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
client: null,
enable_page_level_ads: true
},
site_verification: null,
index_site_info_top: null,
index_top_img_height: null,
category_ui: null,
tag_ui: null,
text_align_justify: false,
background: null,
footer_bg: false,
mask: {
header: true,
footer: true
},
rightside_bottom: null,
enter_transitions: true,
activate_power_mode: {
enable: false,
colorful: true,
shake: true,
mobile: false
},
canvas_ribbon: {
enable: false,
size: 150,
alpha: 0.6,
zIndex: -1,
click_to_change: false,
mobile: false
},
canvas_fluttering_ribbon: {
enable: false,
mobile: false
},
canvas_nest: {
enable: false,
color: '0,0,255',
opacity: 0.7,
zIndex: -1,
count: 99,
mobile: false
},
fireworks: {
enable: false,
zIndex: 9999,
mobile: false
},
click_heart: {
enable: false,
mobile: false
},
clickShowText: {
enable: false,
text: null,
fontSize: '15px',
random: false,
mobile: false
},
display_mode: 'light',
beautify: {
enable: false,
field: 'post',
'title-prefix-icon': null,
'title-prefix-icon-color': null
},
font: {
'global-font-size': null,
'code-font-size': null,
'font-family': null,
'code-font-family': null
},
blog_title_font: {
font_link: null,
'font-family': null
},
hr_icon: {
enable: true,
icon: null,
'icon-top': null
},
subtitle: {
enable: false,
effect: true,
typed_option: null,
source: false,
sub: null
},
preloader: {
enable: false,
source: 1,
pace_css_url: null
},
wordcount: {
enable: false,
post_wordcount: true,
min2read: true,
total_wordcount: true
},
medium_zoom: false,
fancybox: true,
series: {
enable: true,
orderBy: 'title',
order: 1,
number: true
},
abcjs: {
enable: false,
per_page: true
},
mermaid: {
enable: false,
theme: {
light: 'default',
dark: 'dark'
}
},
note: {
style: 'flat',
icons: true,
border_radius: 3,
light_bg_offset: 0
},
pjax: {
enable: false,
exclude: null
},
aplayerInject: {
enable: false,
per_page: true
},
snackbar: {
enable: false,
position: 'bottom-left',
bg_light: '#49b1f5',
bg_dark: '#1f1f1f'
},
instantpage: false,
pangu: {
enable: false,
field: 'site'
},
lazyload: {
enable: false,
field: 'site',
placeholder: null,
blur: false
},
Open_Graph_meta: {
enable: true,
option: null
},
css_prefix: true,
inject: {
head: null,
bottom: null
},
CDN: {
internal_provider: 'local',
third_party_provider: 'jsdelivr',
version: true,
custom_format: null,
option: null
}
}
hexo.theme.config = Object.assign(defaultConfig, hexo.theme.config)
}, 1)

View File

@@ -0,0 +1,23 @@
/**
* Stylus renderer
*/
'use strict'
hexo.extend.filter.register('stylus:renderer', style => {
const { syntax_highlighter: syntaxHighlighter, highlight, prismjs } = hexo.config
let { enable: highlightEnable, line_number: highlightLineNumber } = highlight
let { enable: prismjsEnable, line_number: prismjsLineNumber } = prismjs
// for hexo > 7.0
if (syntaxHighlighter) {
highlightEnable = syntaxHighlighter === 'highlight.js'
prismjsEnable = syntaxHighlighter === 'prismjs'
}
style.define('$highlight_enable', highlightEnable)
.define('$highlight_line_number', highlightLineNumber)
.define('$prismjs_enable', prismjsEnable)
.define('$prismjs_line_number', prismjsLineNumber)
// .import(`${this.source_dir.replace(/\\/g, '/')}_data/css/*`)
})

View File

@@ -0,0 +1,13 @@
hexo.on('ready', () => {
const { version } = require('../../package.json')
hexo.log.info(`
===================================================================
##### # # ##### ##### ###### ##### ###### # # #
# # # # # # # # # # # # #
##### # # # # ##### # # ##### # #
# # # # # # # ##### # # #
# # # # # # # # # # # #
##### #### # # ###### # # # ###### #
${version}
===================================================================`)
})

View File

@@ -0,0 +1,27 @@
/**
* Butterfly
* lazyload
* replace src to data-lazy-src
*/
'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo)
const lazyload = htmlContent => {
const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`)
}
hexo.extend.filter.register('after_render:html', data => {
const { enable, field } = hexo.theme.config.lazyload
if (!enable || field !== 'site') return
return lazyload(data)
})
hexo.extend.filter.register('after_post_render', data => {
const { enable, field } = hexo.theme.config.lazyload
if (!enable || field !== 'post') return
data.content = lazyload(data.content)
return data
})

View File

@@ -0,0 +1,40 @@
/**
* Butterfly
* ramdom cover
*/
'use strict'
hexo.extend.filter.register('before_post_render', data => {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/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}`
}
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)
return defaultCover[num]
}
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
})

View File

@@ -0,0 +1,113 @@
/**
* Butterfly
* for aside archives
*/
'use strict'
hexo.extend.helper.register('aside_archives', function (options = {}) {
const { config } = this
const archiveDir = config.archive_dir
const { timezone } = config
const lang = toMomentLocale(this.page.lang || this.page.language || config.language)
let { format } = options
const type = options.type || 'monthly'
const { transform } = options
const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true
const order = options.order || -1
const compareFunc = type === 'monthly'
? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB
: (yearA, monthA, yearB, monthB) => yearA === yearB
const limit = options.limit
let result = ''
if (!format) {
format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY'
}
const posts = this.site.posts.sort('date', order)
if (!posts.length) return result
const data = []
let length = 0
posts.forEach(post => {
// Clone the date object to avoid pollution
let date = post.date.clone()
if (timezone) date = date.tz(timezone)
const year = date.year()
const month = date.month() + 1
const lastData = data[length - 1]
if (!lastData || !compareFunc(lastData.year, lastData.month, year, month)) {
if (lang) date = date.locale(lang)
const name = date.format(format)
length = data.push({
name,
year,
month,
count: 1
})
} else {
lastData.count++
}
})
const link = item => {
let url = `${archiveDir}/${item.year}/`
if (type === 'monthly') {
if (item.month < 10) url += '0'
url += `${item.month}/`
}
return this.url_for(url)
}
const len = data.length
const Judge = limit === 0 ? len : Math.min(len, limit)
result += `<div class="item-headline"><i class="fas fa-archive"></i><span>${this._p('aside.card_archives')}</span>`
if (len > Judge) {
result += `<a class="card-more-btn" href="${this.url_for(archiveDir)}/" title="${this._p('aside.more_button')}">
<i class="fas fa-angle-right"></i></a>`
}
result += '</div><ul class="card-archive-list">'
for (let i = 0; i < Judge; i++) {
const item = data[i]
result += '<li class="card-archive-list-item">'
result += `<a class="card-archive-list-link" href="${link(item)}">`
result += '<span class="card-archive-list-date">'
result += transform ? transform(item.name) : item.name
result += '</span>'
if (showCount) {
result += `<span class="card-archive-list-count">${item.count}</span>`
}
result += '</a>'
result += '</li>'
}
result += '</ul>'
return result
})
const toMomentLocale = function (lang) {
if (lang === undefined) {
return undefined
}
// moment.locale('') equals moment.locale('en')
// moment.locale(null) equals moment.locale('en')
if (!lang || lang === 'en' || lang === 'default') {
return 'en'
}
return lang.toLowerCase().replace('_', '-')
}

View File

@@ -0,0 +1,99 @@
/**
* Butterfly
* for aside categories
*/
'use strict'
hexo.extend.helper.register('aside_categories', function (categories, options) {
if (!options && (!categories || !Object.prototype.hasOwnProperty.call(categories, 'length'))
) {
options = categories
categories = this.site.categories
}
if (!categories || !categories.length) return ''
options = options || {}
const { config } = this
const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count')
? options.show_count
: true
const depth = options.depth ? parseInt(options.depth, 10) : 0
const orderby = options.orderby || 'name'
const order = options.order || 1
const categoryDir = this.url_for(config.category_dir)
const limit = options.limit === 0 ? categories.length : options.limit
const isExpand = options.expand !== 'none'
const expandClass = isExpand && options.expand === true ? 'expand' : ''
const buttonLabel = this._p('aside.more_button')
const prepareQuery = (parent) => {
const query = {}
if (parent) { query.parent = parent } else { query.parent = { $exists: false } }
return categories.find(query).sort(orderby, order).filter((cat) => cat.length)
}
let expandBtn = ''
const hierarchicalList = (t, level, parent, topparent = true) => {
let result = ''
const isTopParent = topparent
if (t > 0) {
prepareQuery(parent).forEach((cat, i) => {
if (t > 0) {
t = t - 1
let child
if (!depth || level + 1 < depth) {
const childList = hierarchicalList(t, level + 1, cat._id, false)
child = childList[0]
t = childList[1]
}
const parentClass = isExpand && isTopParent && child ? 'parent' : ''
result += `<li class="card-category-list-item ${parentClass}">`
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
result += `<span class="card-category-list-name">${cat.name}</span>`
if (showCount) {
result += `<span class="card-category-list-count">${cat.length}</span>`
}
if (isExpand && isTopParent && child) {
expandBtn = ' expandBtn'
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
}
result += '</a>'
if (child) {
result += `<ul class="card-category-list child">${child}</ul>`
}
result += '</li>'
}
})
}
return [result, t]
}
const list = hierarchicalList(limit, 0)
const moreButton = function () {
if (categories.length <= limit) return ''
const moreHtml = `<a class="card-more-btn" href="${categoryDir}/" title="${buttonLabel}">
<i class="fas fa-angle-right"></i></a>`
return moreHtml
}
return `<div class="item-headline">
<i class="fas fa-folder-open"></i>
<span>${this._p('aside.card_categories')}</span>
${moreButton()}
</div>
<ul class="card-category-list${expandBtn}" id="aside-cat-list">
${list[0]}
</ul>`
})

View File

@@ -0,0 +1,58 @@
hexo.extend.helper.register('getArchiveLength', function () {
const { archive_generator: archiveGenerator } = hexo.config
if (archiveGenerator && archiveGenerator.enable === false) return this.site.posts.length
const { yearly, monthly, daily } = archiveGenerator
const { year, month, day } = this.page
if (yearly === false || !year) return this.site.posts.length
const posts = this.site.posts.sort('date')
const compareFunc = (type, y1, m1, d1, y2, m2, d2) => {
switch (type) {
case 'year':
return y1 === y2
case 'month':
return y1 === y2 && m1 === m2
case 'day':
return y1 === y2 && m1 === m2 && d1 === d2
default:
return false
}
}
const generateDateObj = (type) => {
return posts.reduce((dateObj, post) => {
const date = post.date.clone()
const year = date.year()
const month = date.month() + 1
const day = date.date()
const lastData = dateObj[dateObj.length - 1]
if (!lastData || !compareFunc(type, lastData.year, lastData.month, lastData.day, year, month, day)) {
const name = type === 'year' ? year : type === 'month' ? `${year}-${month}` : `${year}-${month}-${day}`
dateObj.push({
name,
year,
month,
day,
count: 1
})
} else {
lastData.count++
}
return dateObj
}, [])
}
const data = this.fragment_cache('createArchiveObj', () => {
const dateObjs = []
if (yearly) dateObjs.push(...generateDateObj('year'))
if (monthly) dateObjs.push(...generateDateObj('month'))
if (daily) dateObjs.push(...generateDateObj('day'))
return dateObjs
})
const name = month ? (day ? `${year}-${month}-${day}` : `${year}-${month}`) : year
return data.find(item => item.name === name).count
})

View File

@@ -0,0 +1,183 @@
/**
* Butterfly
* inject js to head
*/
'use strict'
hexo.extend.helper.register('inject_head_js', function () {
const { darkmode, aside } = this.theme
const start = darkmode.start || 6
const end = darkmode.end || 18
const { theme_color } = hexo.theme.config
const themeColorLight = (theme_color && theme_color.enable && theme_color.meta_theme_color_light) || '#ffffff'
const themeColorDark = (theme_color && theme_color.enable && theme_color.meta_theme_color_dark) || '#0d0d0d'
const createLocalStore = () => {
return `
win.saveToLocal = {
set: (key, value, ttl) => {
if (ttl === 0) return
const now = Date.now()
const expiry = now + ttl * 86400000
const item = {
value,
expiry
}
localStorage.setItem(key, JSON.stringify(item))
},
get: key => {
const itemStr = localStorage.getItem(key)
if (!itemStr) {
return undefined
}
const item = JSON.parse(itemStr)
const now = Date.now()
if (now > item.expiry) {
localStorage.removeItem(key)
return undefined
}
return item.value
}
}
`
}
// https://stackoverflow.com/questions/16839698/jquery-getscript-alternative-in-native-javascript
const createGetScript = () => {
return `
win.getScript = (url, attr = {}) => new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.async = true
script.onerror = reject
script.onload = script.onreadystatechange = function() {
const loadState = this.readyState
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
script.onload = script.onreadystatechange = null
resolve()
}
Object.keys(attr).forEach(key => {
script.setAttribute(key, attr[key])
})
document.head.appendChild(script)
})
`
}
const createGetCSS = () => {
return `
win.getCSS = (url, id = false) => new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
if (id) link.id = id
link.onerror = reject
link.onload = link.onreadystatechange = function() {
const loadState = this.readyState
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
link.onload = link.onreadystatechange = null
resolve()
}
document.head.appendChild(link)
})
`
}
const createDarkmodeJs = () => {
if (!darkmode.enable) return ''
let darkmodeJs = `
win.activateDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'dark')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorDark}')
}
}
win.activateLightMode = () => {
document.documentElement.setAttribute('data-theme', 'light')
if (document.querySelector('meta[name="theme-color"]') !== null) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${themeColorLight}')
}
}
const t = saveToLocal.get('theme')
`
const autoChangeMode = darkmode.autoChangeMode
if (autoChangeMode === 1) {
darkmodeJs += `
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
const isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches
const isNotSpecified = window.matchMedia('(prefers-color-scheme: no-preference)').matches
const hasNoSupport = !isDarkMode && !isLightMode && !isNotSpecified
if (t === undefined) {
if (isLightMode) activateLightMode()
else if (isDarkMode) activateDarkMode()
else if (isNotSpecified || hasNoSupport) {
const now = new Date()
const hour = now.getHours()
const isNight = hour <= ${start} || hour >= ${end}
isNight ? activateDarkMode() : activateLightMode()
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (saveToLocal.get('theme') === undefined) {
e.matches ? activateDarkMode() : activateLightMode()
}
})
} else if (t === 'light') activateLightMode()
else activateDarkMode()
`
} else if (autoChangeMode === 2) {
darkmodeJs += `
const now = new Date()
const hour = now.getHours()
const isNight = hour <= ${start} || hour >= ${end}
if (t === undefined) isNight ? activateDarkMode() : activateLightMode()
else if (t === 'light') activateLightMode()
else activateDarkMode()
`
} else {
darkmodeJs += `
if (t === 'dark') activateDarkMode()
else if (t === 'light') activateLightMode()
`
}
return darkmodeJs
}
const createAsideStatus = () => {
if (!aside.enable || !aside.button) return ''
return `
const asideStatus = saveToLocal.get('aside-status')
if (asideStatus !== undefined) {
if (asideStatus === 'hide') {
document.documentElement.classList.add('hide-aside')
} else {
document.documentElement.classList.remove('hide-aside')
}
}
`
}
const createDetectApple = () => {
return `
const detectApple = () => {
if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
document.documentElement.classList.add('apple')
}
}
detectApple()
`
}
return `<script>(win=>{${createLocalStore() + createGetScript() + createGetCSS() + createDarkmodeJs() + createAsideStatus() + createDetectApple()}})(window)</script>`
})

View File

@@ -0,0 +1,88 @@
'use strict'
const { stripHTML, escapeHTML, prettyUrls } = require('hexo-util')
const crypto = require('crypto')
hexo.extend.helper.register('page_description', function () {
const { config, page } = this
let description = page.description || page.content || page.title || config.description
if (description) {
description = escapeHTML(stripHTML(description).substring(0, 150)
.trim()
).replace(/\n/g, ' ')
return description
}
})
hexo.extend.helper.register('cloudTags', function (options = {}) {
const env = this
let { source, minfontsize, maxfontsize, limit, unit, orderby, order } = options
unit = unit || 'px'
let result = ''
if (limit > 0) {
source = source.limit(limit)
}
const sizes = []
source.sort('length').forEach(tag => {
const { length } = tag
if (sizes.includes(length)) return
sizes.push(length)
})
const length = sizes.length - 1
source.sort(orderby, order).forEach(tag => {
const ratio = length ? sizes.indexOf(tag.length) / length : 0
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};`
const color = 'rgb(' + Math.floor(Math.random() * 201) + ', ' + Math.floor(Math.random() * 201) + ', ' + Math.floor(Math.random() * 201) + ')' // 0,0,0 -> 200,200,200
style += ` color: ${color}`
result += `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
})
return result
})
hexo.extend.helper.register('urlNoIndex', function (url = null, trailingIndex = false, trailingHtml = false) {
return prettyUrls(url || this.url, { trailing_index: trailingIndex, trailing_html: trailingHtml })
})
hexo.extend.helper.register('md5', function (path) {
return crypto.createHash('md5').update(decodeURI(this.url_for(path))).digest('hex')
})
hexo.extend.helper.register('injectHtml', function (data) {
if (!data) return ''
return data.join('')
})
hexo.extend.helper.register('findArchivesTitle', function (page, menu, date) {
if (page.year) {
const dateStr = page.month ? `${page.year}-${page.month}` : `${page.year}`
const dateFormat = page.month ? hexo.theme.config.aside.card_archives.format : 'YYYY'
return date(dateStr, dateFormat)
}
const defaultTitle = this._p('page.archives')
if (!menu) return defaultTitle
const loop = (m) => {
for (const key in m) {
if (typeof m[key] === 'object') {
loop(m[key])
}
if (/\/archives\//.test(m[key])) {
return key
}
}
}
return loop(menu) || defaultTitle
})
hexo.extend.helper.register('isImgOrUrl', function (path) {
const imgTestReg = /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/i
return path.indexOf('//') !== -1 || imgTestReg.test(path)
})

View File

@@ -0,0 +1,100 @@
/**
* Butterfly
* Related Posts
* According the tag
*/
'use strict'
hexo.extend.helper.register('related_posts', function (currentPost, allPosts) {
let relatedPosts = []
currentPost.tags.forEach(function (tag) {
allPosts.forEach(function (post) {
if (isTagRelated(tag.name, post.tags)) {
const relatedPost = {
title: post.title,
path: post.path,
cover: post.cover,
cover_type: post.cover_type,
weight: 1,
updated: post.updated,
created: post.date
}
const index = findItem(relatedPosts, 'path', post.path)
if (index !== -1) {
relatedPosts[index].weight += 1
} else {
if (currentPost.path !== post.path) {
relatedPosts.push(relatedPost)
}
}
}
})
})
if (relatedPosts.length === 0) {
return ''
}
let result = ''
const hexoConfig = hexo.config
const config = hexo.theme.config
const limitNum = config.related_post.limit || 6
const dateType = config.related_post.date_type || 'created'
const headlineLang = this._p('post.recommend')
relatedPosts = relatedPosts.sort(compare('weight'))
if (relatedPosts.length > 0) {
result += '<div class="relatedPosts">'
result += `<div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>${headlineLang}</span></div>`
result += '<div class="relatedPosts-list">'
for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) {
const cover = relatedPosts[i].cover || 'var(--default-bg-color)'
const title = this.escape_html(relatedPosts[i].title)
result += `<div><a href="${this.url_for(relatedPosts[i].path)}" title="${title}">`
if (relatedPosts[i].cover_type === 'img') {
result += `<img class="cover" src="${this.url_for(cover)}" alt="cover">`
} else {
result += `<div class="cover" style="background: ${cover}"></div>`
}
if (dateType === 'created') {
result += `<div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> ${this.date(relatedPosts[i].created, hexoConfig.date_format)}</div>`
} else {
result += `<div class="content is-center"><div class="date"><i class="fas fa-history fa-fw"></i> ${this.date(relatedPosts[i].updated, hexoConfig.date_format)}</div>`
}
result += `<div class="title">${title}</div>`
result += '</div></a></div>'
}
result += '</div></div>'
return result
}
})
function isTagRelated (tagName, TBDtags) {
let result = false
TBDtags.forEach(function (tag) {
if (tagName === tag.name) {
result = true
}
})
return result
}
function findItem (arrayToSearch, attr, val) {
for (let i = 0; i < arrayToSearch.length; i++) {
if (arrayToSearch[i][attr] === val) {
return i
}
}
return -1
}
function compare (attr) {
return function (a, b) {
const val1 = a[attr]
const val2 = b[attr]
return val2 - val1
}
}

View File

@@ -0,0 +1,22 @@
'use strict'
hexo.extend.helper.register('groupPosts', function () {
const getGroupArray = array => {
const groups = {}
array.forEach(item => {
const Key = item.series
if (!Key) return
groups[Key] = groups[Key] || []
groups[Key].push(item)
})
return groups
}
const sortPosts = posts => {
const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
if (orderBy === 'title') return posts.sort('title', order)
return posts.sort('date', order)
}
return getGroupArray(sortPosts(this.site.posts))
})

View File

@@ -0,0 +1,20 @@
/**
* Button
* {% btn url text icon option %}
* option: color outline center block larger
* color : default/blue/pink/red/purple/orange/green
*/
'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo)
const btn = args => {
args = args.join(' ').split(',')
const [url = '', text = '', icon = '', option = ''] = args.map(arg => arg.trim())
return `<a class="btn-beautify ${option}" href="${urlFor(url)}"
title="${text}">${icon.length ? `<i class="${icon}"></i>` : ''}${text.length ? `<span>${text}</span>` : ''}</a>`
}
hexo.extend.tag.register('btn', btn, { ends: false })

View File

@@ -0,0 +1,39 @@
/**
* flink
*/
'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo)
const flinkFn = (args, content) => {
content = hexo.render.renderSync({ text: content, engine: 'yaml' })
let result = ''
content.forEach(i => {
const className = i.class_name ? `<div class="flink-name">${i.class_name}</div>` : ''
const classDesc = i.class_desc ? `<div class="flink-desc">${i.class_desc}</div>` : ''
let listResult = ''
i.link_list.forEach(j => {
listResult += `
<div class="flink-list-item">
<a href="${j.link}" title="${j.name}" target="_blank">
<div class="flink-item-icon">
<img class="no-lightbox" src="${j.avatar}" onerror='this.onerror=null;this.src="${urlFor(hexo.theme.config.error_img.flink)}"' alt="${j.name}" />
</div>
<div class="flink-item-name">${j.name}</div>
<div class="flink-item-desc" title="${j.descr}">${j.descr}</div>
</a>
</div>`
})
result += `${className}${classDesc} <div class="flink-list">${listResult}</div>`
})
return `<div class="flink">${result}</div>`
}
hexo.extend.tag.register('flink', flinkFn, { ends: true })

View File

@@ -0,0 +1,65 @@
/**
* Butterfly
* galleryGroup and gallery
* {% galleryGroup [name] [descr] [url] [img] %}
*
* {% gallery [button],%}
* {% gallery url,[url],[button]%}
*/
'use strict'
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 = ''
if (args[0] === 'url') {
[type, dataStr, button] = args // url,[link],[lazyload]
} 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]
})
}
dataStr = JSON.stringify(arr)
}
return `<div class="gallery-container" data-type="${type}" data-button="${button}">
<div class="gallery-data">${dataStr}</div>
<div class="gallery-items">
</div>
</div>`
}
const galleryGroup = args => {
const [name, descr, url, img] = args
const imgUrl = urlFor(img)
const urlLink = urlFor(url)
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>
`
}
hexo.extend.tag.register('gallery', gallery, { ends: true })
hexo.extend.tag.register('galleryGroup', galleryGroup)

View File

@@ -0,0 +1,65 @@
/**
* Butterfly
* @example
* hideInline
* {% hideInline content,display,bg,color %}
* content不能包含當引號可用 &apos;
* hideBlock
* {% hideBlock display,bg,color %}
* content
* {% endhideBlock %}
* hideToggle
* {% hideToggle display,bg,color %}
* content
* {% endhideToggle %}
*/
'use strict'
const parseArgs = args => {
return args.join(' ').split(',')
}
const generateStyle = (bg, color) => {
let style = 'style="'
if (bg) {
style += `background-color: ${bg};`
}
if (color) {
style += `color: ${color}`
}
style += '"'
return style
}
const hideInline = args => {
const [content, display = 'Click', bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color)
return `<span class="hide-inline"><button type="button" class="hide-button" ${group}>${display}
</button><span class="hide-content">${content}</span></span>`
}
const hideBlock = (args, content) => {
const [display = 'Click', bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color)
return `<div class="hide-block"><button type="button" class="hide-button" ${group}>${display}
</button><div class="hide-content">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></div>`
}
const hideToggle = (args, content) => {
const [display, bg = false, color = false] = parseArgs(args)
const group = generateStyle(bg, color)
let border = ''
if (bg) {
border = `style="border: 1px solid ${bg}"`
}
return `<details class="toggle" ${border}><summary class="toggle-button" ${group}>${display}</summary><div class="toggle-content">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></details>`
}
hexo.extend.tag.register('hideInline', hideInline)
hexo.extend.tag.register('hideBlock', hideBlock, { ends: true })
hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })

View File

@@ -0,0 +1,19 @@
/**
* inlineImg 圖片
* @param {Array} args 圖片名稱和高度
* @param {string} args[0] 圖片名稱
* @param {number} args[1] 圖片高度
* @returns {string} 圖片標籤
*/
'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo)
const inlineImg = ([img, height = '']) => {
const heightStyle = height ? `style="height:${height}"` : ''
const src = urlFor(img)
return `<img class="inline-img" src="${src}" ${heightStyle} />`
}
hexo.extend.tag.register('inlineImg', inlineImg, { ends: false })

View File

@@ -0,0 +1,14 @@
/**
* Butterfly
* label
* {% label text color %}
*/
'use strict'
const addLabel = args => {
const [text, className = 'default'] = args
return `<mark class="hl-label ${className}">${text}</mark> `
}
hexo.extend.tag.register('label', addLabel, { ends: false })

View File

@@ -0,0 +1,17 @@
/**
* Butterfly
* mermaid
* https://github.com/mermaid-js/mermaid
*/
'use strict'
const { escapeHTML } = require('hexo-util')
const mermaid = (args, content) => {
return `<div class="mermaid-wrap"><pre class="mermaid-src" hidden>
${escapeHTML(content)}
</pre></div>`
}
hexo.extend.tag.register('mermaid', mermaid, { ends: true })

View File

@@ -0,0 +1,27 @@
/**
* note.js
* transplant from hexo-theme-next
* Modify by Jerry
*/
'use strict'
const postNote = (args, content) => {
const styleConfig = hexo.theme.config.note.style
const noteTag = ['flat', 'modern', 'simple', 'disabled']
if (!noteTag.includes(args[args.length - 1])) {
args.push(styleConfig)
}
let icon = ''
const iconArray = args[args.length - 2]
if (iconArray && iconArray.startsWith('fa')) {
icon = `<i class="note-icon ${iconArray}"></i>`
args[args.length - 2] = 'icon-padding'
}
return `<div class="note ${args.join(' ')}">${icon + hexo.render.renderSync({ text: content, engine: 'markdown' })}</div>`
}
hexo.extend.tag.register('note', postNote, { ends: true })
hexo.extend.tag.register('subnote', postNote, { ends: true })

View File

@@ -0,0 +1,22 @@
/**
* Music Score
* {% score %}
*/
'use strict'
const score = (args, content) => {
const escapeHtmlTags = s => {
const lookup = {
'&': '&amp;',
'"': '&quot;',
'\'': '&apos;',
'<': '&lt;',
'>': '&gt;'
}
return s.replace(/[&"'<>]/g, c => lookup[c])
}
return `<div class="abc-music-sheet">${escapeHtmlTags(content)}</div>`
}
hexo.extend.tag.register('score', score, { ends: true })

View File

@@ -0,0 +1,69 @@
/**
* series plugin
* Syntax:
* {% series [series name] %}
* Usage:
* {% series %}
* {% series series1 %}
*/
'use strict'
const urlFor = require('hexo-util').url_for.bind(hexo)
const groups = {}
hexo.extend.filter.register('before_post_render', data => {
if (!hexo.theme.config.series.enable) return data
const { layout, series } = data
if (layout === 'post' && series) {
groups[series] = groups[series] || []
groups[series].push({
title: data.title,
path: data.path,
date: data.date.unix()
})
}
return data
})
function series (args) {
const { series } = hexo.theme.config
if (!series.enable) {
hexo.log.warn('Series plugin is disabled in the theme config')
return ''
}
const seriesArr = args.length ? groups[args[0]] : groups[this.series]
if (!seriesArr) {
hexo.log.warn(`There is no series named "${args[0]}"`)
return ''
}
const isAsc = (series.order || 1) === 1 // 1: asc, -1: desc
const isSortByTitle = series.orderBy === 'title'
const compareFn = (a, b) => {
const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
if (itemA < itemB) {
return isAsc ? -1 : 1
}
if (itemA > itemB) {
return isAsc ? 1 : -1
}
return 0
}
seriesArr.sort(compareFn)
let result = ''
seriesArr.forEach(ele => {
result += `<li><a href="${urlFor(ele.path)}" title="${ele.title}">${ele.title}</a></li>`
})
return series.number ? `<ol>${result}</ol>` : `<ul>${result}</ul>`
}
hexo.extend.tag.register('series', series, { ends: false })

View File

@@ -0,0 +1,64 @@
/**
* Tabs
* transplant from hexo-theme-next
* modify by Jerry
*/
'use strict'
const postTabs = (args, content) => {
const tabBlock = /<!--\s*tab (.*?)\s*-->\n([\w\W\s\S]*?)<!--\s*endtab\s*-->/g
args = args.join(' ').split(',')
const tabName = args[0]
const tabActive = Number(args[1]) || 0
const matches = []
let match
let tabId = 0
let tabNav = ''
let tabContent = ''
let noDefault = true
!tabName && hexo.log.warn('Tabs block must have unique name!')
while ((match = tabBlock.exec(content)) !== null) {
matches.push(match[1], match[2])
}
for (let i = 0; i < matches.length; i += 2) {
const tabParameters = matches[i].split('@')
let postContent = matches[i + 1]
let tabCaption = tabParameters[0] || ''
let tabIcon = tabParameters[1] || ''
let tabHref = ''
postContent = hexo.render.renderSync({ text: postContent, engine: 'markdown' }).trim()
tabId += 1
tabHref = (tabName + ' ' + tabId).toLowerCase().split(' ').join('-');
((tabCaption.length === 0) && (tabIcon.length === 0)) && (tabCaption = tabName + ' ' + tabId)
const isOnlyicon = tabIcon.length > 0 && tabCaption.length === 0 ? ' style="text-align: center;"' : ''
const icon = tabIcon.trim()
tabIcon.length > 0 && (tabIcon = `<i class="${icon}"${isOnlyicon}></i>`)
let isActive = ''
if ((tabActive > 0 && tabActive === tabId) || (tabActive === 0 && tabId === 1)) {
isActive = ' active'
noDefault = false
}
tabNav += `<button type="button" class="tab ${isActive}" data-href="${tabHref}">${tabIcon + tabCaption.trim()}</button>`
tabContent += `<div class="tab-item-content${isActive}" id="${tabHref}">${postContent}</div>`
}
const toTop = '<div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div>'
tabNav = `<ul class="nav-tabs${noDefault ? ' no-default' : ''}">${tabNav}</ul>`
tabContent = `<div class="tab-contents">${tabContent}</div>`
return `<div class="tabs" id="${tabName.toLowerCase().split(' ').join('-')}">${tabNav + tabContent + toTop}</div>`
}
hexo.extend.tag.register('tabs', postTabs, { ends: true })
hexo.extend.tag.register('subtabs', postTabs, { ends: true })
hexo.extend.tag.register('subsubtabs', postTabs, { ends: true })

View File

@@ -0,0 +1,41 @@
/**
* timeline
* by Jerry
*/
'use strict'
const timeLineFn = (args, content) => {
const tlBlock = /<!--\s*timeline (.*?)\s*-->\n([\w\W\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>`
}
const matches = []
let match
while ((match = tlBlock.exec(content)) !== null) {
matches.push(match[1])
matches.push(match[2])
}
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' })
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>`
}
hexo.extend.tag.register('timeline', timeLineFn, { ends: true })