feat: 实现 VOID 风格文章侧边目录及代码块增强功能
This commit is contained in:
1 parent
b0b233f64d
commit
7cd34ec3c0
3 files changed
+563
-79
No files matched your search
+399
-36
@@ -301,74 +301,245 @@ body.single-col .post-list {
|
||||
}
|
||||
|
||||
.article-content {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1.05rem;
|
||||
line-height: 2.2;
|
||||
font: 400 18px/1.75 'Open Sans', var(--font-sans);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.article-content p {
|
||||
margin-bottom: 1.5em;
|
||||
margin: 0.92em 0;
|
||||
}
|
||||
|
||||
/* DoIt Style Code Block Wrapper */
|
||||
.code-block {
|
||||
.article-content h1,
|
||||
.article-content h2,
|
||||
.article-content h3,
|
||||
.article-content h4,
|
||||
.article-content h5,
|
||||
.article-content h6 {
|
||||
font-family: var(--font-sans);
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #ededed;
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.code-title {
|
||||
font-family: "Source Code Pro", Menlo, Consolas, Monaco, monospace;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.code-header-actions {
|
||||
.code-block-title-btn {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.copy-code-button {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 2rem;
|
||||
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;
|
||||
align-items: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.copy-code-button:hover {
|
||||
color: var(--accent-color, #bd6262);
|
||||
.code-header-actions button,
|
||||
.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 {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
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);
|
||||
padding: 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) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
@@ -597,3 +768,195 @@ footer {
|
||||
background-color: var(--accent-color);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -73,11 +73,6 @@
|
||||
|
||||
<!-- Floating Controllers (VOID style) -->
|
||||
<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="返回顶部">
|
||||
<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>
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
import 'highlight.js/styles/github.css';
|
||||
import DOMPurify from 'dompurify';
|
||||
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();
|
||||
markedInstance.use({
|
||||
renderer: {
|
||||
@@ -15,29 +20,63 @@
|
||||
const lang = (token.lang || '').split(' ')[0];
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
const highlighted = hljs.highlight(code, { language }).value;
|
||||
const title = lang || 'text';
|
||||
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>';
|
||||
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>';
|
||||
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>';
|
||||
|
||||
return `
|
||||
<div class="code-block highlight">
|
||||
// 将代码按行分割并包裹 code-line,以支持 CSS 计数器行号
|
||||
// highlight.js 生成的 span 在单行内,跨行会被拆开,因此可以直接 split('\n')
|
||||
const wrappedLines = highlighted
|
||||
.split('\n')
|
||||
.map((line) => `<span class="code-line">${line}</span>`)
|
||||
.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 style="display: flex; align-items: center;">
|
||||
${chevronIcon}
|
||||
<span class="code-title">${lang || language}</span>
|
||||
</div>
|
||||
<div class="code-header-actions">
|
||||
<span style="opacity: 0.5;">${listOlIcon}</span>
|
||||
<span style="opacity: 0.5;">${barsIcon}</span>
|
||||
<button class="copy-code-button" title="Copy code" data-code="${encodedCode}">
|
||||
${copyIcon}
|
||||
<button class="code-block-title-btn" aria-hidden="true">
|
||||
${chevronRight}
|
||||
<span class="code-title">${title}</span>
|
||||
</button>
|
||||
<div class="code-header-actions">
|
||||
<button class="action-icon line-number-button is-active" title="Toggle line numbers">${listOlIcon}</button>
|
||||
<button class="action-icon wrap-code-button" title="Toggle code wrap">${barsIcon}</button>
|
||||
<button class="action-icon copy-code-button" title="Copy code" data-code="${encodedCode}">
|
||||
<span class="copy-icon" style="display:block">${copyIcon}</span>
|
||||
<span class="check-icon" style="display:none">${checkIcon}</span>
|
||||
</button>
|
||||
<span class="ellipsis-icon" aria-hidden="true">${ellipsisIcon}</span>
|
||||
</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>`;
|
||||
},
|
||||
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}>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -97,26 +136,98 @@
|
||||
const htmlContent = markedInstance.parse(preprocessedContent) as string;
|
||||
|
||||
onMount(() => {
|
||||
// Initialize copy code buttons
|
||||
document.querySelectorAll('.copy-code-button').forEach((btn) => {
|
||||
const buttonEl = btn as HTMLElement;
|
||||
buttonEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const code = buttonEl.getAttribute('data-code');
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(decodeURIComponent(code)).then(() => {
|
||||
// Temporarily change icon to a checkmark or just "Copied!" text if we had a check icon
|
||||
// For now, let's just make it green temporarily
|
||||
buttonEl.style.color = '#4CAF50';
|
||||
setTimeout(() => {
|
||||
buttonEl.style.color = '';
|
||||
}, 2000);
|
||||
tocbot.init({
|
||||
tocSelector: '.js-toc',
|
||||
contentSelector: '.js-toc-content',
|
||||
headingSelector: 'h1, h2, h3, h4, h5',
|
||||
hasInnerContainers: false,
|
||||
linkClass: 'toc-link',
|
||||
activeLinkClass: 'is-active-link',
|
||||
listClass: 'toc-list',
|
||||
isCollapsedClass: 'is-collapsed',
|
||||
collapsibleClass: 'is-collapsible',
|
||||
listItemClass: 'toc-list-item',
|
||||
collapseDepth: 0,
|
||||
scrollSmooth: true,
|
||||
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) => {
|
||||
const summaryEl = detailsEl.querySelector('.details-summary') as HTMLElement;
|
||||
if (!summaryEl) return;
|
||||
@@ -132,10 +243,8 @@
|
||||
e.preventDefault();
|
||||
if (detailsEl.classList.contains('open')) {
|
||||
// Close it
|
||||
// If it's none (fully expanded), set to pixel height first to allow transition
|
||||
if (contentEl.style.maxHeight === 'none' || contentEl.style.maxHeight === '') {
|
||||
contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
|
||||
// Force reflow to ensure the browser applies the pixel value before animating to 0
|
||||
void contentEl.offsetHeight;
|
||||
}
|
||||
contentEl.style.maxHeight = '0px';
|
||||
@@ -145,7 +254,6 @@
|
||||
detailsEl.classList.add('open');
|
||||
contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
|
||||
|
||||
// Once transition finishes, switch to 'none' so it can resize dynamically
|
||||
const transitionEndHandler = (e: Event) => {
|
||||
if (e.target === contentEl && detailsEl.classList.contains('open')) {
|
||||
contentEl.style.maxHeight = 'none';
|
||||
@@ -156,6 +264,10 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
tocbot.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -164,8 +276,8 @@
|
||||
<title>{data.post.title} - BlogFlare</title>
|
||||
</svelte:head>
|
||||
|
||||
<main id="pjax-container">
|
||||
<div class="wrapper container narrow">
|
||||
<main id="pjax-container" class:sidebar-show={sidebarShow}>
|
||||
<div class="wrapper container narrow contents-wrap">
|
||||
<article class="article-detail yue">
|
||||
<header class="article-header">
|
||||
<h1 class="title">{data.post.title}</h1>
|
||||
@@ -175,10 +287,24 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="articleBody article-content">
|
||||
<section class="articleBody article-content js-toc-content">
|
||||
{@html htmlContent}
|
||||
</section>
|
||||
</article>
|
||||
</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>
|
||||
|
||||
Reference in new issue
Block a user