diff --git a/_config.yml b/_config.yml index f8eeacb..da4ee3d 100644 --- a/_config.yml +++ b/_config.yml @@ -319,6 +319,7 @@ aside: # If set 0 will show all limit: 40 color: false + custom_colors: # Order of tags, random/name/length orderby: random # Sort of order. 1, asc for ascending; -1, desc for descending @@ -939,6 +940,10 @@ mermaid: theme: light: default 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 # see https://www.chartjs.org/docs/latest/ diff --git a/layout/includes/head/config.pug b/layout/includes/head/config.pug index 99e1305..05f11a8 100644 --- a/layout/includes/head/config.pug +++ b/layout/includes/head/config.pug @@ -72,20 +72,16 @@ }) } - let highlight = 'undefined' - let syntaxHighlighter = config.syntax_highlighter - let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable) - if (highlightEnable) { - const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks - highlight = JSON.stringify({ - plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs', - highlightCopy: copy, - highlightLang: language, - highlightHeightLimit: height_limit, - highlightFullpage: fullpage, - highlightMacStyle: macStyle - }) - } + let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null) + const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks + let highlight = JSON.stringify({ + plugin: highlightProvider, + highlightCopy: copy, + highlightLang: language, + highlightHeightLimit: height_limit, + highlightFullpage: fullpage, + highlightMacStyle: macStyle + }) script. const GLOBAL_CONFIG = { diff --git a/layout/includes/mixins/indexPostUI.pug b/layout/includes/mixins/indexPostUI.pug index 874ad5e..16f5e70 100644 --- a/layout/includes/mixins/indexPostUI.pug +++ b/layout/includes/mixins/indexPostUI.pug @@ -1,6 +1,6 @@ mixin indexPostUI() - 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-post-items each article, index in page.posts.data @@ -8,17 +8,17 @@ mixin indexPostUI() - const link = article.link || article.path - const title = article.title || _p('no_title') - const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '') - - const post_cover = article.cover - - const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : '' + - const postCover = article.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) a(href=url_for(link) title=title) 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 - div.post-bg(style=`background: ${post_cover}`) - .recent-post-info(class=no_cover) + div.post-bg(style=`background: ${postCover}`) + .recent-post-info(class=noCover) a.article-title(href=url_for(link) title=title) if globalPageType === 'home' && (article.top || article.sticky > 0) i.fas.fa-thumbtack.sticky @@ -35,13 +35,13 @@ mixin indexPostUI() 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) else - - const data_type_updated = theme.post_meta.page.date_type === 'updated' - - const date_type = data_type_updated ? 'updated' : 'date' - - const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt' - - const date_title = data_type_updated ? _p('post.updated') : _p('post.created') - i(class=date_icon) - span.article-meta-label= date_title - time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format) + - const isUpdatedType = theme.post_meta.page.date_type === 'updated' + - const dateType = isUpdatedType ? 'updated' : 'date' + - const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt' + - const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created') + i(class=dateIcon) + span.article-meta-label= dateTitle + 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 span.article-meta span.article-meta-separator | @@ -69,7 +69,10 @@ mixin indexPostUI() span.article-meta-label= ' ' + _p('card_post_count') 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 'Disqusjs' +countBlockInIndex @@ -77,30 +80,30 @@ mixin indexPostUI() i.fa-solid.fa-spinner.fa-spin when 'Valine' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.valine-comment-count(data-xid=url_for(link)) i.fa-solid.fa-spinner.fa-spin when 'Waline' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.waline-comment-count(data-path=url_for(link)) i.fa-solid.fa-spinner.fa-spin when 'Twikoo' +countBlockInIndex - a.twikoo-count(href=url_for(link) + '#post-comment') + a.twikoo-count(href=commentLink) i.fa-solid.fa-spinner.fa-spin when 'Facebook Comments' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.fb-comments-count(data-href=urlNoIndex(article.permalink)) when 'Remark42' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.remark42__counter(data-url=urlNoIndex(article.permalink)) i.fa-solid.fa-spinner.fa-spin when 'Artalk' +countBlockInIndex - a(href=url_for(link) + '#post-comment') + a(href=commentLink) span.artalk-count(data-page-key=url_for(link)) i.fa-solid.fa-spinner.fa-spin diff --git a/layout/includes/page/tags.pug b/layout/includes/page/tags.pug index 9c48197..8216efc 100644 --- a/layout/includes/page/tags.pug +++ b/layout/includes/page/tags.pug @@ -1,2 +1,2 @@ .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'}) \ No newline at end of file + !=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}) diff --git a/layout/includes/third-party/abcjs/abcjs.pug b/layout/includes/third-party/abcjs/abcjs.pug index d1a32bf..22a98b8 100644 --- a/layout/includes/third-party/abcjs/abcjs.pug +++ b/layout/includes/third-party/abcjs/abcjs.pug @@ -24,7 +24,7 @@ script. const options = { ...params, responsive: "resize" } // Render the music score using ABCJS.renderAbc - ABCJS.renderAbc(ele, ele.innerHTML, options) + ABCJS.renderAbc(ele, ele.textContent, options) } }, 100) } diff --git a/layout/includes/third-party/math/mermaid.pug b/layout/includes/third-party/math/mermaid.pug index 0be9dec..f1d26b0 100644 --- a/layout/includes/third-party/math/mermaid.pug +++ b/layout/includes/third-party/math/mermaid.pug @@ -1,11 +1,275 @@ script. (() => { + const parseViewBox = viewBox => { + if (!viewBox) return null + const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n)) + if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null + return parts + } + + const getSvgViewBox = svg => { + const attr = parseViewBox(svg.getAttribute('viewBox')) + if (attr) return attr + + // Fallback: use bbox to build a viewBox + try { + const bbox = svg.getBBox() + if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height] + } catch (e) { + // getBBox may fail on some edge cases; ignore + } + + const w = Number(svg.getAttribute('width')) || 0 + const h = Number(svg.getAttribute('height')) || 0 + if (w > 0 && h > 0) return [0, 0, w, h] + return [0, 0, 100, 100] + } + + const setSvgViewBox = (svg, vb) => { + svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`) + } + + const clamp = (v, min, max) => Math.max(min, Math.min(max, v)) + + const openSvgInNewTab = ({ source, initViewBox }) => { + const getClonedSvg = () => { + if (typeof source === 'string') { + const template = document.createElement('template') + template.innerHTML = source.trim() + const svg = template.content.querySelector('svg') + return svg ? svg.cloneNode(true) : null + } + if (source && typeof source.cloneNode === 'function') { + return source.cloneNode(true) + } + return null + } + + const clone = getClonedSvg() + if (!clone) return + if (initViewBox && initViewBox.length === 4) { + clone.setAttribute('viewBox', initViewBox.join(' ')) + } + if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + if (!clone.getAttribute('xmlns:xlink') && clone.outerHTML.includes('xlink:')) { + clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') + } + // inject background to match current theme + const isDark = document.documentElement.getAttribute('data-theme') === 'dark' + const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff') + if (!clone.style.background) clone.style.background = bg + + const serializer = new XMLSerializer() + const svgSource = serializer.serializeToString(clone) + const htmlSource = `
+ + ${svgSource}` + const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' }) + const url = URL.createObjectURL(blob) + window.open(url, '_blank', 'noopener') + setTimeout(() => URL.revokeObjectURL(url), 30000) + } + + const attachMermaidViewerButton = wrap => { + let btn = wrap.querySelector('.mermaid-open-btn') + if (!btn) { + btn = document.createElement('button') + btn.type = 'button' + btn.className = 'mermaid-open-btn' + wrap.appendChild(btn) + } + + btn.innerHTML = '' + + if (!btn.__mermaidViewerBound) { + btn.addEventListener('click', e => { + e.preventDefault() + e.stopPropagation() + const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg') + if (!svg) return + const initViewBox = wrap.__mermaidInitViewBox + if (typeof svg === 'string') { + openSvgInNewTab({ source: svg, initViewBox }) + return + } + openSvgInNewTab({ source: svg, initViewBox }) + }) + btn.__mermaidViewerBound = true + } + } + + // Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates) + const zoomAtPoint = (vb, factor, px, py) => { + const w = vb[2] * factor + const h = vb[3] * factor + const nx = px - (px - vb[0]) * factor + const ny = py - (py - vb[1]) * factor + return [nx, ny, w, h] + } + + const initMermaidGestures = wrap => { + const svg = wrap.querySelector('svg') + if (!svg) return + + // Ensure viewBox exists so gestures always work + const initVb = getSvgViewBox(svg) + wrap.__mermaidInitViewBox = initVb + wrap.__mermaidCurViewBox = initVb.slice() + setSvgViewBox(svg, initVb) + + // Avoid binding multiple times on themeChange/pjax + if (wrap.__mermaidGestureBound) return + wrap.__mermaidGestureBound = true + + // Helper: map client (viewport) coordinate -> viewBox coordinate + const clientToViewBox = (clientX, clientY) => { + const rect = svg.getBoundingClientRect() + const vb = wrap.__mermaidCurViewBox || getSvgViewBox(svg) + const x = vb[0] + (clientX - rect.left) * (vb[2] / rect.width) + const y = vb[1] + (clientY - rect.top) * (vb[3] / rect.height) + return { x, y, rect, vb } + } + + const state = { + pointers: new Map(), + startVb: null, + startDist: 0, + startCenter: null + } + + const clampVb = vb => { + const init = wrap.__mermaidInitViewBox || vb + const minW = init[2] * 0.1 + const maxW = init[2] * 10 + const minH = init[3] * 0.1 + const maxH = init[3] * 10 + vb[2] = clamp(vb[2], minW, maxW) + vb[3] = clamp(vb[3], minH, maxH) + return vb + } + + const setCurVb = vb => { + vb = clampVb(vb) + wrap.__mermaidCurViewBox = vb + setSvgViewBox(svg, vb) + } + + const onPointerDown = e => { + // Allow only primary button for mouse + if (e.pointerType === 'mouse' && e.button !== 0) return + svg.setPointerCapture(e.pointerId) + state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) + + if (state.pointers.size === 1) { + state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + } else if (state.pointers.size === 2) { + const pts = [...state.pointers.values()] + const dx = pts[0].x - pts[1].x + const dy = pts[0].y - pts[1].y + state.startDist = Math.hypot(dx, dy) + state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + state.startCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 } + } + } + + const onPointerMove = e => { + if (!state.pointers.has(e.pointerId)) return + state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) + + // Pan with 1 pointer + if (state.pointers.size === 1 && state.startVb) { + const p = [...state.pointers.values()][0] + const prev = { x: e.clientX - e.movementX, y: e.clientY - e.movementY } + // movementX/Y unreliable on touch, compute from stored last position + const last = wrap.__mermaidLastSinglePointer || p + const dxClient = p.x - last.x + const dyClient = p.y - last.y + wrap.__mermaidLastSinglePointer = p + + const { rect } = clientToViewBox(p.x, p.y) + const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + const dx = dxClient * (vb[2] / rect.width) + const dy = dyClient * (vb[3] / rect.height) + setCurVb([vb[0] - dx, vb[1] - dy, vb[2], vb[3]]) + return + } + + // Pinch zoom with 2 pointers + if (state.pointers.size === 2 && state.startVb && state.startDist > 0) { + const pts = [...state.pointers.values()] + const dx = pts[0].x - pts[1].x + const dy = pts[0].y - pts[1].y + const dist = Math.hypot(dx, dy) + if (!dist) return + const factor = state.startDist / dist // dist bigger => zoom in (viewBox smaller) + + const cx = (pts[0].x + pts[1].x) / 2 + const cy = (pts[0].y + pts[1].y) / 2 + const centerClient = { x: cx, y: cy } + + const pxy = clientToViewBox(centerClient.x, centerClient.y) + const cpx = pxy.x + const cpy = pxy.y + + const vb = zoomAtPoint(state.startVb, factor, cpx, cpy) + setCurVb(vb) + } + } + + const onPointerUpOrCancel = e => { + state.pointers.delete(e.pointerId) + if (state.pointers.size === 0) { + state.startVb = null + state.startDist = 0 + state.startCenter = null + wrap.__mermaidLastSinglePointer = null + } else if (state.pointers.size === 1) { + // reset single pointer baseline to avoid jump + wrap.__mermaidLastSinglePointer = [...state.pointers.values()][0] + } + } + + // Wheel zoom (mouse/trackpad) + const onWheel = e => { + // ctrlKey on mac trackpad pinch; we treat both as zoom + e.preventDefault() + const delta = e.deltaY + const zoomFactor = delta > 0 ? 1.1 : 0.9 + const { x, y } = clientToViewBox(e.clientX, e.clientY) + const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice() + setCurVb(zoomAtPoint(vb, zoomFactor, x, y)) + } + + const onDblClick = () => { + const init = wrap.__mermaidInitViewBox + if (!init) return + wrap.__mermaidCurViewBox = init.slice() + setSvgViewBox(svg, init) + } + + svg.addEventListener('pointerdown', onPointerDown) + svg.addEventListener('pointermove', onPointerMove) + svg.addEventListener('pointerup', onPointerUpOrCancel) + svg.addEventListener('pointercancel', onPointerUpOrCancel) + svg.addEventListener('wheel', onWheel, { passive: false }) + svg.addEventListener('dblclick', onDblClick) + } + const runMermaid = ele => { window.loadMermaid = true const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' ele.forEach((item, index) => { const mermaidSrc = item.firstElementChild + + // Clear old render (themeChange/pjax will rerun) + const oldSvg = item.querySelector('svg') + if (oldSvg) oldSvg.remove() + item.__mermaidGestureBound = false + const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {} if (!config.theme) { config.theme = theme @@ -17,8 +281,12 @@ script. const renderFn = mermaid.render(mermaidID, mermaidDefinition) const renderMermaid = 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 typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg)) }) @@ -52,4 +320,4 @@ script. btf.addGlobalFn('encrypt', loadMermaid, 'mermaid') window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid) - })() \ No newline at end of file + })() diff --git a/layout/includes/widget/card_tags.pug b/layout/includes/widget/card_tags.pug index 0bc51b5..788ab7f 100644 --- a/layout/includes/widget/card_tags.pug +++ b/layout/includes/widget/card_tags.pug @@ -5,10 +5,10 @@ if theme.aside.card_tags.enable i.fas.fa-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 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 - .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'}) diff --git a/package.json b/package.json index 702094f..ad45920 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hexo-theme-butterfly", - "version": "5.5.3", + "version": "5.5.4", "description": "A Simple and Card UI Design theme for Hexo", "main": "package.json", "scripts": { diff --git a/plugins.yml b/plugins.yml index 3ae26b0..4eb9381 100644 --- a/plugins.yml +++ b/plugins.yml @@ -1,7 +1,7 @@ abcjs_basic_js: name: abcjs file: dist/abcjs-basic-min.js - version: 6.5.2 + version: 6.6.0 activate_power_mode: name: butterfly-extsrc file: dist/activate-power-mode.min.js @@ -9,7 +9,7 @@ activate_power_mode: algolia_search: name: algoliasearch file: dist/lite/builds/browser.umd.js - version: 5.46.0 + version: 5.47.0 aplayer_css: name: aplayer file: dist/APlayer.min.css @@ -66,26 +66,26 @@ docsearch_css: name: '@docsearch/css' other_name: docsearch-css file: dist/style.css - version: 4.3.2 + version: 4.5.3 docsearch_js: name: '@docsearch/js' other_name: docsearch-js file: dist/umd/index.js - version: 4.3.2 + version: 4.5.3 egjs_infinitegrid: name: '@egjs/infinitegrid' other_name: egjs-infinitegrid file: dist/infinitegrid.min.js - version: 4.12.0 + version: 4.13.0 fancybox: name: '@fancyapps/ui' file: dist/fancybox/fancybox.umd.js - version: 6.1.7 + version: 6.1.9 other_name: fancyapps-ui fancybox_css: name: '@fancyapps/ui' file: dist/fancybox/fancybox.css - version: 6.1.7 + version: 6.1.9 other_name: fancyapps-ui fireworks: name: butterfly-extsrc @@ -112,12 +112,12 @@ katex: name: katex file: dist/katex.min.css other_name: KaTeX - version: 0.16.27 + version: 0.16.28 katex_copytex: name: katex file: dist/contrib/copy-tex.min.js other_name: KaTeX - version: 0.16.27 + version: 0.16.28 lazyload: name: vanilla-lazyload file: dist/lazyload.iife.min.js @@ -125,7 +125,7 @@ lazyload: mathjax: name: mathjax file: tex-mml-chtml.js - version: 4.0.0 + version: 4.1.0 medium_zoom: name: medium-zoom file: dist/medium-zoom.min.js @@ -190,7 +190,7 @@ twikoo: typed: name: typed.js file: dist/typed.umd.js - version: 2.1.0 + version: 3.0.0 valine: name: valine file: dist/Valine.min.js diff --git a/scripts/common/default_config.js b/scripts/common/default_config.js index d8ae8c2..ab3f01f 100644 --- a/scripts/common/default_config.js +++ b/scripts/common/default_config.js @@ -179,6 +179,7 @@ module.exports = { enable: true, limit: 40, color: false, + custom_colors: null, orderby: 'random', order: 1, sort_order: null @@ -522,7 +523,9 @@ module.exports = { theme: { light: 'default', dark: 'dark' - } + }, + open_in_new_tab: true, + zoom_pan: true }, chartjs: { enable: false, diff --git a/scripts/helpers/page.js b/scripts/helpers/page.js index 0144ef2..01fbcc0 100644 --- a/scripts/helpers/page.js +++ b/scripts/helpers/page.js @@ -19,7 +19,7 @@ hexo.extend.helper.register('postDesc', data => { hexo.extend.helper.register('cloudTags', function (options = {}) { 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) { source = source.limit(limit) @@ -36,15 +36,48 @@ hexo.extend.helper.register('cloudTags', function (options = {}) { return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})` } - const generateStyle = (size, unit, page) => { - const colorStyle = page === 'tags' ? `background-color: ${getRandomColor()};` : `color: ${getRandomColor()};` + const normalizeColors = input => { + 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 resolveColorClass = (idx) => `tag-color-${idx % userColors.length}` + + const generateStyle = (size, unit, page, color) => { + const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};` return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}` } - return source.sort(orderby, order).map(tag => { + return source.sort(orderby, order).map((tag, idx) => { const ratio = length ? sizeMap.get(tag.length) / length : 0 const size = minfontsize + ((maxfontsize - minfontsize) * ratio) - const style = generateStyle(size, unit, page) + + if (userColors && userColors.length) { + const colorClass = resolveColorClass(idx) + const color = userColors[idx % userColors.length] + const style = generateStyle(size, unit, page, color) + return `${tag.name}` + } + + const color = getRandomColor() + const style = generateStyle(size, unit, page, color) return `${tag.name}` }).join('') }) diff --git a/source/css/_highlight/highlight.styl b/source/css/_highlight/highlight.styl index 735dc07..bc12363 100644 --- a/source/css/_highlight/highlight.styl +++ b/source/css/_highlight/highlight.styl @@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo --hlscrollbar-bg: lighten(#121212, 5) --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 @require 'highlight/index' @@ -88,6 +96,11 @@ $code-block &:hover border-bottom-color: var(--hl-color) + &.default + pre + padding: 10px 20px + @extend $scrollbar-style + &.copy-true user-select: all -webkit-user-select: all diff --git a/source/css/_highlight/highlight/diff.styl b/source/css/_highlight/highlight/diff.styl index 728bc55..43f9652 100644 --- a/source/css/_highlight/highlight/diff.styl +++ b/source/css/_highlight/highlight/diff.styl @@ -1,11 +1,6 @@ figure.highlight table - // scrollbar - firefox - @-moz-document url-prefix() - scrollbar-color: var(--hlscrollbar-bg) transparent - - &::-webkit-scrollbar-thumb - background: var(--hlscrollbar-bg) + @extend $scrollbar-style pre .deletion color: $highlight-deletion diff --git a/source/css/_highlight/prismjs/index.styl b/source/css/_highlight/prismjs/index.styl index 30637d7..e4ed7c4 100644 --- a/source/css/_highlight/prismjs/index.styl +++ b/source/css/_highlight/prismjs/index.styl @@ -6,12 +6,7 @@ if $highlight_theme != false .container pre[class*='language-'] - // scrollbar - firefox - @-moz-document url-prefix() - scrollbar-color: var(--hlscrollbar-bg) transparent - - &::-webkit-scrollbar-thumb - background: var(--hlscrollbar-bg) + @extend $scrollbar-style &:not(.line-numbers) padding: 10px 20px diff --git a/source/css/_layout/head.styl b/source/css/_layout/head.styl index 72d3a3b..128a5ab 100644 --- a/source/css/_layout/head.styl +++ b/source/css/_layout/head.styl @@ -37,6 +37,8 @@ margin: 0 color: var(--white) font-size: 1.85em + @extend .limit-more-line + -webkit-line-clamp: 3 +minWidth768() font-size: 2.85em diff --git a/source/css/_layout/reward.styl b/source/css/_layout/reward.styl index b6cce62..ebefab9 100644 --- a/source/css/_layout/reward.styl +++ b/source/css/_layout/reward.styl @@ -37,13 +37,13 @@ display: none padding: 0 0 15px width: 100% - addBorderRadius() .reward-all display: inline-block margin: 0 padding: 20px 10px background: var(--reward-pop) + addBorderRadius() &:before position: absolute diff --git a/source/css/_layout/third-party.styl b/source/css/_layout/third-party.styl index 8d7ac55..8e85e6c 100644 --- a/source/css/_layout/third-party.styl +++ b/source/css/_layout/third-party.styl @@ -65,12 +65,55 @@ if hexo-config('waline.bg') if hexo-config('mermaid.enable') .mermaid-wrap + position: relative margin: 0 0 20px + background: var(--card-bg) 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 + max-width: 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') pre > code.mermaid display: none @@ -184,4 +227,4 @@ if hexo-config('math.use') +maxWidth768() .fancybox__toolbar__column.is-middle - display: none \ No newline at end of file + display: none diff --git a/source/js/main.js b/source/js/main.js index caabcec..0aab1d5 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -61,11 +61,14 @@ document.addEventListener('DOMContentLoaded', () => { const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink 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(document.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement) + : document.querySelectorAll('figure.highlight') if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return - const isPrismjs = plugin === 'prismjs' const highlightShrinkClass = isHighlightShrink === true ? 'closed' : '' const highlightShrinkEle = isHighlightShrink !== undefined ? '' : '' const highlightCopyEle = highlightCopy ? '' : '' @@ -133,7 +136,7 @@ document.addEventListener('DOMContentLoaded', () => { const highlightCopyFn = (ele, clickEle) => { const $buttonParent = ele.parentNode $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) if (!codeElement) return copy(codeElement.innerText, clickEle) @@ -213,20 +216,23 @@ document.addEventListener('DOMContentLoaded', () => { 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 => { 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) { createEle('', item) return } - if (isPrismjs) { - langName = item.getAttribute('data-language') || 'Code' + if (isNotHighlightJs) { + langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '') } else { langName = item.getAttribute('class').split(' ')[1] if (langName === 'plain' || langName === undefined) langName = 'Code'