feat: 实现 VOID 风格文章侧边目录及代码块增强功能

This commit is contained in:
seahi committed 2026-07-16 19:08:48 +08:00
1 parent b0b233f64d
commit 7cd34ec3c0
3 files changed
+565 -81

No files matched your search

+399 -36
View File
@@ -301,74 +301,245 @@ body.single-col .post-list {
} }
.article-content { .article-content {
font-family: var(--font-sans); font: 400 18px/1.75 'Open Sans', var(--font-sans);
font-size: 1.05rem; word-break: break-word;
line-height: 2.2;
} }
.article-content p { .article-content p {
margin-bottom: 1.5em; margin: 0.92em 0;
} }
/* DoIt Style Code Block Wrapper */ .article-content h1,
.code-block { .article-content h2,
.article-content h3,
.article-content h4,
.article-content h5,
.article-content h6 {
font-family: var(--font-sans);
position: relative; position: relative;
margin: 1.5rem 0;
border-radius: 4px;
overflow: hidden;
background-color: #f5f5f5;
} }
.article-content h1 {
font-size: 1.8em;
margin: 1.206em 0 0.36em;
}
.article-content > h1 {
margin-top: 0;
font-size: 2.2em;
}
.article-content h2 {
font-size: 1.5em;
margin: 1.005em 0 0.375em;
border-bottom: none;
padding-bottom: 0;
}
.article-content h3 {
font-size: 1.17em;
margin: 1em 0 0.2925em;
}
.article-content h4,
.article-content h5,
.article-content h6 {
font-size: 1em;
margin: 1em 0 0.35em;
}
.article-content h6 {
font-weight: 500;
}
.article-content a:not([data-fancybox="gallery"]):not(.post-like):not(.edit-button) {
word-wrap: break-word;
word-break: break-all;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
transition: border-color 0.2s;
text-decoration: none;
}
.article-content a:not([data-fancybox="gallery"]):not(.post-like):not(.edit-button):hover {
border-color: #000;
opacity: 1;
}
.article-content img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
/* 适配 VOID 的图文混排结构 (figure + figcaption) */
.article-content figure {
margin: 1.5rem 0;
text-align: center;
}
.article-content figcaption {
font-size: 0.9rem;
color: #888;
margin-top: 0.5rem;
}
/* ========== DoIt 代码块样式 ========== */
.highlight,
.code-block {
font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace;
font-size: 0.875rem;
}
.code-block code,
.code-block pre {
background-color: #f5f5f5 !important;
}
.code-block {
margin: 0.5rem 0;
border-radius: 0;
overflow: hidden;
}
/* 标题栏 — 高度与 admonition-title 精确对齐 */
.code-block-header { .code-block-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%;
background-color: #ededed; background-color: #ededed;
padding: 0.4rem 0.8rem; font-size: 1rem;
font-size: 0.85rem;
color: var(--text-color);
} }
.code-title { .code-block-title-btn {
font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace; margin: 0;
font-weight: 500;
color: var(--text-color);
}
.code-header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; flex: 1;
}
.copy-code-button {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 0.25rem 2rem;
color: inherit; color: inherit;
padding: 0; font: inherit;
position: relative;
}
/* 箭头图标 — 与 admonition 完全一致的绝对定位和间距 */
.code-block-title-btn .chevron-icon {
font-size: 0.9rem;
width: 0.85em;
height: 0.85em;
position: absolute;
top: 0.6rem;
left: 0.75rem;
transition: transform 0.3s ease-in-out;
}
.code-block.is-open .code-block-title-btn .chevron-icon {
transform: rotate(90deg);
}
.code-block-title-btn .code-title {
user-select: none;
display: block;
line-height: 1.6;
}
/* 右侧按钮区 */
.code-header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
transition: color 0.2s;
} }
.copy-code-button:hover { .code-header-actions button,
color: var(--accent-color, #bd6262); .code-header-actions .ellipsis-icon {
background: none;
border: none;
cursor: pointer;
user-select: none;
margin-inline: 0.5rem;
padding: 0;
color: inherit;
display: flex;
align-items: center;
} }
/* 操作按钮:默认隐藏,展开时显示 */
.code-header-actions .action-icon {
display: none;
}
.code-block.is-open .code-header-actions .action-icon {
display: flex;
}
/* 省略号:默认显示,展开时隐藏 */
.code-header-actions .ellipsis-icon {
display: flex;
}
.code-block.is-open .code-header-actions .ellipsis-icon {
display: none;
}
/* 按钮激活态(行号/换行开启时高亮) */
.code-header-actions button.is-active {
color: #2d809a;
}
.code-header-actions button:hover {
color: #2d809a;
}
/* pre */
.code-block pre { .code-block pre {
display: block;
margin: 0; margin: 0;
padding: 1rem; padding: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
line-height: 1.6;
font-size: 0.875rem;
font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace;
background-color: transparent !important;
color: var(--text-color);
border-radius: 0; border-radius: 0;
} }
/* code — max-height + padding 同时过渡,收缩彻底无残留 */
.code-block pre code {
display: block !important;
margin: 0;
max-height: 0;
padding: 0;
transition: max-height 0.3s ease-out, padding 0.3s ease-out;
overflow: hidden;
line-height: 1.6;
}
/* 覆盖 highlight.js github.css 的默认白色背景 */
.code-block pre code.hljs {
padding: 0.8rem 0.75rem;
background: #f5f5f5 !important;
overflow-x: auto;
}
.code-block.is-closed pre code.hljs {
padding-top: 0 !important;
padding-bottom: 0 !important;
overflow: hidden !important; /* 防止横向滚动条撑开高度导致“半行内容”残留 */
}
/* ---- 行号功能 ---- */
.code-block.show-line-numbers pre code .code-line::before {
counter-increment: codeblock;
content: counter(codeblock);
display: inline-block;
min-width: 2.5ch;
text-align: right;
margin-right: 0.75rem;
color: #8c8fa1;
user-select: none;
}
/* ---- 换行功能 ---- */
.code-block.is-wrap pre code {
white-space: pre-wrap;
word-break: break-all;
}
.article-content code:not(pre code) { .article-content code:not(pre code) {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px; border-radius: 4px;
@@ -597,3 +768,195 @@ footer {
background-color: var(--accent-color); background-color: var(--accent-color);
opacity: 1; opacity: 1;
} }
/* ========== VOID 风格 TOC (侧栏目录) ========== */
.toc-mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.85);
z-index: 3;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), visibility 0s 0.3s;
display: block;
}
@media screen and (max-width: 1199.5px) {
.sidebar-show .toc-mask {
opacity: 1;
visibility: visible;
transition: opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
}
.TOC {
position: fixed;
top: 0;
bottom: 0;
z-index: 4;
overflow: auto;
-webkit-overflow-scrolling: touch;
width: 280px;
right: -280px;
background-color: #111; /* VOID style dark background */
box-shadow: -2px 0 8px rgba(0,0,0,0.05);
padding: 1rem;
padding-right: 0;
padding-top: 60px;
transition: right 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.TOC::-webkit-scrollbar {
display: none;
}
.TOC::before {
content: "Contents";
position: absolute;
left: 0;
top: 0;
line-height: 60px;
height: 60px;
width: 100%;
font-size: 18px;
font-weight: 600;
text-transform: uppercase;
text-align: center;
color: #b0b0b0; /* VOID style dark text */
box-sizing: border-box;
}
.sidebar-show .TOC {
right: 0;
}
/* 目录内容列表样式 */
.TOC .toc-list {
position: relative;
list-style: none;
padding-left: 0;
margin: 0;
line-height: 2.1;
}
.TOC .toc-list .toc-list {
position: unset;
padding-left: 1rem;
}
.TOC .is-collapsible {
max-height: 1000px;
overflow: hidden;
transition: all 300ms ease-in-out;
}
.TOC .is-collapsed {
max-height: 0;
}
.TOC li a {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: block;
width: 100%;
color: #b0b0b0; /* VOID style dark text */
transition: color 0.2s;
text-decoration: none;
}
.TOC li a:hover, .TOC li a.is-active-link {
color: #54bc4b;
opacity: 1;
}
.TOC li a.is-active-link {
font-weight: 700;
}
.TOC .is-active-link::before {
content: "";
background-color: rgba(0, 0, 0, 0.3); /* VOID style dark highlight */
display: block;
height: 2.1rem;
left: -1rem;
position: absolute;
width: calc(100% + 1rem);
border-left: 2px solid #54BC4B;
z-index: -1;
box-sizing: border-box;
}
@media screen and (min-width: 1199.5px) {
.TOC .toc-link:hover::before {
content: "";
background-color: rgba(0, 0, 0, 0.3);
display: block;
height: 2.1rem;
left: -1rem;
position: absolute;
width: calc(100% + 1rem);
border-left: 2px solid #54BC4B;
z-index: -1;
box-sizing: border-box;
}
}
/* 主体偏移 */
@media screen and (min-width: 1199.5px) {
div.contents-wrap {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.sidebar-show div.contents-wrap {
transform: translateX(-140px);
}
}
@media screen and (max-width: 1199.5px) {
div.contents-wrap {
transition: opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
}
/* 展开/关闭按钮 */
#toggle-toc {
position: fixed;
right: 20px;
bottom: 70px;
width: 40px;
height: 40px;
background-color: var(--header-bg);
border-radius: 50%;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 101;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
color: #fff;
}
#toggle-toc:hover {
background-color: var(--accent-color);
}
#toggle-toc svg {
width: 20px;
height: 20px;
transition: transform 0.3s;
}
.sidebar-show #toggle-toc svg {
transform: rotate(180deg);
}
@media screen and (min-width: 1199.5px) {
.sidebar-show #toggle-toc {
right: 300px;
}
}
-5
View File
@@ -73,11 +73,6 @@
<!-- Floating Controllers (VOID style) --> <!-- Floating Controllers (VOID style) -->
<aside id="ctrler-panel"> <aside id="ctrler-panel">
<div class="ctrler-item" title="切换排版结构">
<a href="javascript:void(0);" onclick={toggleSingleCol}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="3" x2="12" y2="21"></line></svg>
</a>
</div>
<div class="ctrler-item" title="返回顶部"> <div class="ctrler-item" title="返回顶部">
<a href="javascript:void(0);" onclick={scrollToTop}> <a href="javascript:void(0);" onclick={scrollToTop}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg>
+166 -40
View File
@@ -5,8 +5,13 @@
import 'highlight.js/styles/github.css'; import 'highlight.js/styles/github.css';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Fancybox } from '@fancyapps/ui/dist/index.js';
import '@fancyapps/ui/dist/fancybox/fancybox.css';
import tocbot from 'tocbot';
// Create a new Marked instance to avoid HMR leaks and double-escaping issues let sidebarShow = $state(true);
// 创建独立 Marked 实例,避免 HMR 重复注册导致双重转义
const markedInstance = new Marked(); const markedInstance = new Marked();
markedInstance.use({ markedInstance.use({
renderer: { renderer: {
@@ -15,33 +20,67 @@
const lang = (token.lang || '').split(' ')[0]; const lang = (token.lang || '').split(' ')[0];
const language = hljs.getLanguage(lang) ? lang : 'plaintext'; const language = hljs.getLanguage(lang) ? lang : 'plaintext';
const highlighted = hljs.highlight(code, { language }).value; const highlighted = hljs.highlight(code, { language }).value;
const title = lang || 'text';
const encodedCode = encodeURIComponent(code); const encodedCode = encodeURIComponent(code);
const copyIcon = '<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em" style="font-size: 1.1em"><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v200a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>';
const chevronIcon = '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em" style="margin-right: 0.5rem;"><path d="M256 294.1L383 167c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-144 144c-9.4 9.4-24.6 9.4-33.9 0l-144-144c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0L256 294.1z"></path></svg>'; // 将代码按行分割并包裹 code-line,以支持 CSS 计数器行号
const listOlIcon = '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h34.71L15.32 424.64A29.85 29.85 0 0 0 8 444v16c0 26.51 21.49 48 48 48h24a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H56c-8.84 0-16-7.16-16-16v-4.52a6.01 6.01 0 0 1 1.76-4.24l19.24-19.24a48.28 48.28 0 0 0 7.58 3.06L64 448c0 35.35 28.65 64 64 64s64-28.65 64-64-28.65-64-64-64c-9.67 0-18.77 2.21-27.05 6.04L75.64 361.7A29.8 29.8 0 0 0 56 352h-7.66c10.37-4.14 17.66-14.24 17.66-26.34v-3.31c0-15.46-12.54-28-28-28H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22c2.21 0 4 1.79 4 4v3.31c0 2.21-1.79 4-4 4H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h15.4c17.21 0 33.15 6.94 44.97 19.46l5.4-6.23a43.83 43.83 0 0 1 7.23-7.19h-15zM400 352H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h224a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176 112h224a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zM400 208H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h224a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM88 32H16C7.16 32 0 39.16 0 48v20c0 8.84 7.16 16 16 16h16v64c0 8.84 7.16 16 16 16h16c8.84 0 16-7.16 16-16V48c0-8.84-7.16-16-16-16h-48c0-8.84 7.16-16 16-16zm64 160c0-35.35-28.65-64-64-64s-64 28.65-64 64 28.65 64 64 64 64-28.65 64-64zm-32 0c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32z"></path></svg>'; // highlight.js 生成的 span 在单行内,跨行会被拆开,因此可以直接 split('\n')
const barsIcon = '<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em"><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>'; const wrappedLines = highlighted
.split('\n')
return ` .map((line) => `<span class="code-line">${line}</span>`)
<div class="code-block highlight"> .join('\n');
// chevron-right — 来自 DoIt fontawesome-free/svgs/solid/chevron-right.svg
const chevronRight = '<svg class="chevron-icon" viewBox="0 0 320 512" fill="currentColor" width="0.625em" height="1em"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"></path></svg>';
// list-ol — 来自 DoIt fontawesome-free/svgs/solid/list-ol.svg
const listOlIcon = '<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"></path></svg>';
// bars — 来自 DoIt fontawesome-free/svgs/solid/bars.svg
const barsIcon = '<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>';
const copyIcon = '<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M433.941 65.941l-51.882-51.882A48 48 0 00348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 00-14.059-33.941zM266 464H54a6 6 0 01-6-6V150a6 6 0 016-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 01-6 6zm128-96H182a6 6 0 01-6-6V54a6 6 0 016-6h106v88c0 13.255 10.745 24 24 24h88v200a6 6 0 01-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 011.757 4.243V112z"></path></svg>';
const checkIcon = '<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>';
const ellipsisIcon = '<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"></path></svg>';
return `<div class="code-block highlight is-open show-line-numbers">
<div class="code-block-header"> <div class="code-block-header">
<div style="display: flex; align-items: center;"> <button class="code-block-title-btn" aria-hidden="true">
${chevronIcon} ${chevronRight}
<span class="code-title">${lang || language}</span> <span class="code-title">${title}</span>
</div> </button>
<div class="code-header-actions"> <div class="code-header-actions">
<span style="opacity: 0.5;">${listOlIcon}</span> <button class="action-icon line-number-button is-active" title="Toggle line numbers">${listOlIcon}</button>
<span style="opacity: 0.5;">${barsIcon}</span> <button class="action-icon wrap-code-button" title="Toggle code wrap">${barsIcon}</button>
<button class="copy-code-button" title="Copy code" data-code="${encodedCode}"> <button class="action-icon copy-code-button" title="Copy code" data-code="${encodedCode}">
${copyIcon} <span class="copy-icon" style="display:block">${copyIcon}</span>
<span class="check-icon" style="display:none">${checkIcon}</span>
</button> </button>
<span class="ellipsis-icon" aria-hidden="true">${ellipsisIcon}</span>
</div> </div>
</div> </div>
<pre><code class="hljs language-${language}">${highlighted}</code></pre> <pre style="counter-reset:codeblock;"><code class="hljs language-${language}">${wrappedLines}</code></pre>
</div>`; </div>`;
},
image(token) {
const src = token.href;
const alt = token.text || '';
const title = token.title || alt;
let figcaption = '';
if (title) {
figcaption = `<figcaption>${title}</figcaption>`;
}
return `<figure><a data-fancybox="gallery" data-caption="${title}" href="${src}"><img src="${src}" alt="${alt}" title="${title}"></a>${figcaption}</figure>`;
},
heading(token) {
const text = token.text;
const rawText = text.replace(/<[^>]*>?/gm, ''); // Strip HTML tags
const level = token.depth;
const id = rawText.trim().toLowerCase().replace(/\s+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
} }
} }
}); });
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
// Custom parsing for GitHub-style and Hugo-style Admonitions // Custom parsing for GitHub-style and Hugo-style Admonitions
@@ -97,26 +136,98 @@
const htmlContent = markedInstance.parse(preprocessedContent) as string; const htmlContent = markedInstance.parse(preprocessedContent) as string;
onMount(() => { onMount(() => {
// Initialize copy code buttons tocbot.init({
document.querySelectorAll('.copy-code-button').forEach((btn) => { tocSelector: '.js-toc',
const buttonEl = btn as HTMLElement; contentSelector: '.js-toc-content',
buttonEl.addEventListener('click', (e) => { headingSelector: 'h1, h2, h3, h4, h5',
e.preventDefault(); hasInnerContainers: false,
const code = buttonEl.getAttribute('data-code'); linkClass: 'toc-link',
if (code) { activeLinkClass: 'is-active-link',
navigator.clipboard.writeText(decodeURIComponent(code)).then(() => { listClass: 'toc-list',
// Temporarily change icon to a checkmark or just "Copied!" text if we had a check icon isCollapsedClass: 'is-collapsed',
// For now, let's just make it green temporarily collapsibleClass: 'is-collapsible',
buttonEl.style.color = '#4CAF50'; listItemClass: 'toc-list-item',
setTimeout(() => { collapseDepth: 0,
buttonEl.style.color = ''; scrollSmooth: true,
}, 2000); headingsOffset: 20
}); });
if (window.innerWidth < 1200) {
sidebarShow = false;
}
// 绑定 Fancybox
Fancybox.bind('[data-fancybox]', {});
// 初始化代码块交互(复刻 DoIt initCodeblocks + Admonition 风格动画)
document.querySelectorAll('.code-block').forEach((codeBlock) => {
const titleButton = codeBlock.querySelector('button.code-block-title-btn') as HTMLElement;
const codeEl = codeBlock.querySelector('pre code') as HTMLElement;
const copyCodeButton = codeBlock.querySelector('button.copy-code-button') as HTMLElement;
const lineNumButton = codeBlock.querySelector('button.line-number-button') as HTMLElement;
const wrapButton = codeBlock.querySelector('button.wrap-code-button') as HTMLElement;
const copyIconEl = copyCodeButton?.querySelector('span.copy-icon') as HTMLElement;
const checkIconEl = copyCodeButton?.querySelector('span.check-icon') as HTMLElement;
// 初始展开状态:maxHeight 设为 none,允许自适应高度
if (codeEl && codeBlock.classList.contains('is-open')) {
codeEl.style.maxHeight = 'none';
}
// 展开/收缩切换(与 Admonition 动画逻辑一致)
titleButton?.addEventListener('click', () => {
if (!codeEl) return;
if (codeBlock.classList.contains('is-open')) {
// 收缩:先设定像素高度,再触发过渡到 0
if (codeEl.style.maxHeight === 'none' || codeEl.style.maxHeight === '') {
codeEl.style.maxHeight = codeEl.scrollHeight + 'px';
void codeEl.offsetHeight; // 强制 reflow
}
codeEl.style.maxHeight = '0px';
codeBlock.classList.remove('is-open');
codeBlock.classList.add('is-closed');
} else {
// 展开
codeBlock.classList.remove('is-closed');
codeBlock.classList.add('is-open');
codeEl.style.maxHeight = codeEl.scrollHeight + 'px';
const handler = (e: Event) => {
if (e.target === codeEl && codeBlock.classList.contains('is-open')) {
codeEl.style.maxHeight = 'none';
}
codeEl.removeEventListener('transitionend', handler);
};
codeEl.addEventListener('transitionend', handler);
}
});
// 行号开关
lineNumButton?.addEventListener('click', () => {
codeBlock.classList.toggle('show-line-numbers');
lineNumButton.classList.toggle('is-active');
});
// 自动换行开关
wrapButton?.addEventListener('click', () => {
codeBlock.classList.toggle('is-wrap');
wrapButton.classList.toggle('is-active');
});
// 复制代码
copyCodeButton?.addEventListener('click', () => {
if (codeEl) {
navigator.clipboard.writeText(codeEl.textContent || '');
if (copyIconEl) copyIconEl.style.display = 'none';
if (checkIconEl) checkIconEl.style.display = 'block';
setTimeout(() => {
if (copyIconEl) copyIconEl.style.display = 'block';
if (checkIconEl) checkIconEl.style.display = 'none';
}, 3000);
} }
}); });
}); });
// Initialize details expand/collapse animation logic (DoIt style) // 初始化 Admonition 展开/收缩动画(DoIt style)
document.querySelectorAll('.details').forEach((detailsEl) => { document.querySelectorAll('.details').forEach((detailsEl) => {
const summaryEl = detailsEl.querySelector('.details-summary') as HTMLElement; const summaryEl = detailsEl.querySelector('.details-summary') as HTMLElement;
if (!summaryEl) return; if (!summaryEl) return;
@@ -132,10 +243,8 @@
e.preventDefault(); e.preventDefault();
if (detailsEl.classList.contains('open')) { if (detailsEl.classList.contains('open')) {
// Close it // Close it
// If it's none (fully expanded), set to pixel height first to allow transition
if (contentEl.style.maxHeight === 'none' || contentEl.style.maxHeight === '') { if (contentEl.style.maxHeight === 'none' || contentEl.style.maxHeight === '') {
contentEl.style.maxHeight = contentEl.scrollHeight + 'px'; contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
// Force reflow to ensure the browser applies the pixel value before animating to 0
void contentEl.offsetHeight; void contentEl.offsetHeight;
} }
contentEl.style.maxHeight = '0px'; contentEl.style.maxHeight = '0px';
@@ -145,7 +254,6 @@
detailsEl.classList.add('open'); detailsEl.classList.add('open');
contentEl.style.maxHeight = contentEl.scrollHeight + 'px'; contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
// Once transition finishes, switch to 'none' so it can resize dynamically
const transitionEndHandler = (e: Event) => { const transitionEndHandler = (e: Event) => {
if (e.target === contentEl && detailsEl.classList.contains('open')) { if (e.target === contentEl && detailsEl.classList.contains('open')) {
contentEl.style.maxHeight = 'none'; contentEl.style.maxHeight = 'none';
@@ -156,6 +264,10 @@
} }
}); });
}); });
return () => {
tocbot.destroy();
};
}); });
</script> </script>
@@ -164,8 +276,8 @@
<title>{data.post.title} - BlogFlare</title> <title>{data.post.title} - BlogFlare</title>
</svelte:head> </svelte:head>
<main id="pjax-container"> <main id="pjax-container" class:sidebar-show={sidebarShow}>
<div class="wrapper container narrow"> <div class="wrapper container narrow contents-wrap">
<article class="article-detail yue"> <article class="article-detail yue">
<header class="article-header"> <header class="article-header">
<h1 class="title">{data.post.title}</h1> <h1 class="title">{data.post.title}</h1>
@@ -175,10 +287,24 @@
</div> </div>
</header> </header>
<section class="articleBody article-content"> <section class="articleBody article-content js-toc-content">
{@html htmlContent} {@html htmlContent}
</section> </section>
</article> </article>
</div> </div>
<!-- TOC Overlay and Sidebar -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="toc-mask" onclick={() => sidebarShow = false}></div>
<div class="TOC" aria-label="文章目录">
<div class="js-toc"></div>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div id="toggle-toc" class="ctrler-item" onclick={() => sidebarShow = !sidebarShow}>
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>
</div>
</main> </main>