Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions site/.vitepress/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { navZh, navEn } from './nav'
import { kbdPlugin } from '../plugins/kbd-plugin'
import { cppTemplateEscapePlugin } from '../plugins/escape-cpp-templates'
import { mermaidPlugin } from '../plugins/mermaid-plugin'
import { codeFoldPlugin } from '../plugins/code-fold-plugin'
import { getBuildInfo } from './build-info'

// 模块加载时算一次,两个 themeConfig 函数共用;同一构建进程内一致。
Expand All @@ -28,6 +29,7 @@ export const sharedMarkdown = {
cppTemplateEscapePlugin(md)
md.use(kbdPlugin)
md.use(mermaidPlugin)
md.use(codeFoldPlugin)
},
}

Expand Down
85 changes: 85 additions & 0 deletions site/.vitepress/plugins/code-fold-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { PluginSimple } from 'markdown-it'
import type MarkdownIt from 'markdown-it'

/**
* 长代码折叠:把超过阈值行数的代码块包成
* <div class="vp-code-fold"><details><summary/></details><代码块/></div>
* (代码在 <details> 外,见下方说明),用原生 <details> 做开关、纯 CSS :has() 控展开。
*
* 为什么是构建期 markdown-it 插件、而不是客户端 JS 增强?
* - FOUC:站点是 SSG,首屏静态 HTML 里代码块就是全展开的;客户端 bundle 异步加载,
* 冷缓存首访「先展开再闪收」必然。构建期产出即折叠态,零闪烁。
* - 无障碍 / 无 JS:<details> 原生 disclosure,无 JS 也能展开,键盘/AT 语义白送。
*
* 实现要点(已核实 VitePress 1.6.4 编译产物 node/chunk-D3CUZ4fa.js):
* - sharedMarkdown.config(md) 在 VitePress 把 Shiki/复制按钮/行号/preWrapper 全部接好
* 【之后】才执行(options.config(md) 在所有 md.use() 链之后),故此处捕获的
* md.renderer.rules.fence 已是完整链 —— 调用它即得含 .copy/.lang/pre.shiki/
* .line-numbers-wrapper 的整段 HTML。直接整段包 <details>,内部兄弟链不动。
* - 必须整段包(而非只包 <pre>):VitePress 复制按钮靠 button.copy.nextElementSibling
* 定位 <pre>,兄弟链一断复制即失效。
* - 代码放在 <details>【外面】(.vp-code-fold 里与 <details> 同级),而非放进 <details> 内:
* 原生 <details> 关闭时不渲染非 summary 内容;放外面则代码始终在 DOM 中,纯 CSS
* :has(details[open]) 控制 display(收起完全隐藏、不做预览;展开全显示) —— 零 JS、
* 无 JS 也能点 summary 展开、@media print 强制显示即完整打印。
* - code-group 内的 fence 不折叠(tab 本身已是折叠语义):core ruler 用独立 depth 计数
* 打 token.meta.inCodeGroup 标记 —— 不依赖 render 期才追加的 " active" info。
*
* 阈值 30 依据全站实测(7732 个代码块):>30 行占 ~12%,即 Issue #71 所指「一大坨」;
* 20-30 行可正常阅读的中等块不误伤。改这一处常量即可全局调档。
*/
const FOLD_THRESHOLD = 20

export const codeFoldPlugin: PluginSimple = (md: MarkdownIt) => {
// ① core ruler:标记 code-group 内的 fence,折叠时跳过。
// 容器 token 与子 fence 都在 state.tokens 平铺流里(core 阶段已全部就绪),
// 用 depth 计数即可判定祖先,render 时仍读同一 token 对象的 meta。
md.core.ruler.push('code_fold_mark_codegroup', (state) => {
let depth = 0
for (const token of state.tokens) {
if (token.type === 'container_code-group_open') {
depth++
} else if (token.type === 'container_code-group_close') {
if (depth > 0) depth--
} else if (depth > 0 && token.type === 'fence') {
if (!token.meta) token.meta = {}
token.meta.inCodeGroup = true
}
}
return true
})

// ② 覆写 fence:整段包 <details>。
const originalFence = md.renderer.rules.fence
if (!originalFence) return

md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const html = originalFence(tokens, idx, options, env, self)
const token = tokens[idx]

// code-group 内不折;mermaid 已被 mermaidPlugin 改型为 mermaid_diagram,不进 fence rule。
if (token.meta && token.meta.inCodeGroup) return html

// 数行数:token.content 末尾通常带单个 \n,去掉再 split。
const body = token.content.replace(/\n$/, '')
const lineCount = body === '' ? 0 : body.split('\n').length
if (lineCount <= FOLD_THRESHOLD) return html

// 双语 summary:EN 站构建时源码拷到 <srcDir>/en/<vol>/,env.relativePath 以 "en/" 开头
// (见 scripts/build.ts 的 EN 分支)。CN 卷不带该前缀。
const relativePath = env?.relativePath
const isEn = typeof relativePath === 'string' && relativePath.startsWith('en/')
const closedLabel = isEn
? `Expand <em>(${lineCount} lines)</em>`
: `展开代码 <em>(共 ${lineCount} 行)</em>`
const openLabel = isEn ? 'Collapse' : '收起代码'

return (
`<div class="vp-code-fold" data-lines="${lineCount}">` +
`<details><summary><span class="vp-cf-closed">${closedLabel}</span>` +
`<span class="vp-cf-open">${openLabel}</span></summary></details>` +
html +
`</div>`
)
}
}
99 changes: 99 additions & 0 deletions site/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,105 @@ html[data-font-size='xxlarge'] {
border-radius: 4px;
}

/* ================================================================
长代码折叠(.vp-code-fold):构建期 code-fold-plugin 把 >30 行的代码块
包成 <div class="vp-code-fold"><details><summary/></details><代码/></div>。
- 行为:收起 = 只显示 summary 条(代码完全隐藏);展开 = 代码全部显示。
不做"预览"(收起态露几行)—— 要么全展开要么全不展开,二态干净。
- 代码在 <details>【外】,靠纯 CSS :has(details[open]) 控制 display;
零 JS、无 JS 也能点 summary 展开;@media print 强制显示,打印完整(纯 CSS)。
- 内部 div.language-* 兄弟链(copy/lang/pre/line-numbers)不动,复制按钮不受影响。
================================================================ */
.vp-doc .vp-code-fold {
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
background: var(--vp-code-block-bg);
}

.vp-doc .vp-code-fold > details > summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
font-size: 0.9rem;
font-weight: 500;
color: var(--vp-c-text-2);
user-select: none;
background: var(--vp-code-block-bg);
transition: color 0.2s ease;
}

.vp-doc .vp-code-fold > details > summary:hover {
color: var(--vp-c-text-1);
}

.vp-doc .vp-code-fold > details > summary::-webkit-details-marker {
display: none;
}

/* 展开三角:放大到 1rem,醒目;hover 变品牌色,展开时旋转 */
.vp-doc .vp-code-fold > details > summary::before {
content: '▶';
font-size: 1rem;
line-height: 1;
color: var(--vp-c-text-3);
transition: transform 0.2s ease, color 0.2s ease;
}

.vp-doc .vp-code-fold > details > summary:hover::before {
color: var(--vp-c-brand-1);
}

.vp-doc .vp-code-fold:has(details[open]) > details > summary::before {
transform: rotate(90deg);
}

/* 展开/收起文案随 details[open] 切换(:has 全浏览器 2023+ 支持) */
.vp-doc .vp-code-fold:not(:has(details[open])) .vp-cf-open {
display: none;
}

.vp-doc .vp-code-fold:has(details[open]) .vp-cf-closed {
display: none;
}

.vp-doc .vp-code-fold:has(details[open]) .vp-cf-open {
display: inline;
}

.vp-doc .vp-code-fold > details > summary em {
font-style: normal;
color: var(--vp-c-text-3);
margin-left: 4px;
}

/* 代码块:收起态完全隐藏(不预览);展开态默认显示。margin/圆角由外层 .vp-code-fold 统一裁 */
.vp-doc .vp-code-fold > div[class*='language-'] {
margin: 0;
border-radius: 0;
}

.vp-doc .vp-code-fold:not(:has(details[open])) > div[class*='language-'] {
display: none;
}

/* 打印:强制显示所有折叠代码,PDF 完整(代码始终在 DOM 中,纯 CSS,无需 JS) */
@media print {
.vp-doc .vp-code-fold > div[class*='language-'] {
display: block !important;
}
}

@media (prefers-reduced-motion: reduce) {
.vp-doc .vp-code-fold > details > summary,
.vp-doc .vp-code-fold > details > summary::before {
transition: none;
}
}

/* Tables */
.vp-doc table {
font-size: 0.88rem;
Expand Down
Loading