diff --git a/site/.vitepress/config/shared.ts b/site/.vitepress/config/shared.ts index 14984fe22..53ba0495a 100644 --- a/site/.vitepress/config/shared.ts +++ b/site/.vitepress/config/shared.ts @@ -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 函数共用;同一构建进程内一致。 @@ -28,6 +29,7 @@ export const sharedMarkdown = { cppTemplateEscapePlugin(md) md.use(kbdPlugin) md.use(mermaidPlugin) + md.use(codeFoldPlugin) }, } diff --git a/site/.vitepress/plugins/code-fold-plugin.ts b/site/.vitepress/plugins/code-fold-plugin.ts new file mode 100644 index 000000000..a2ffc1f70 --- /dev/null +++ b/site/.vitepress/plugins/code-fold-plugin.ts @@ -0,0 +1,85 @@ +import type { PluginSimple } from 'markdown-it' +import type MarkdownIt from 'markdown-it' + +/** + * 长代码折叠:把超过阈值行数的代码块包成 + *
<代码块/>
+ * (代码在
外,见下方说明),用原生
做开关、纯 CSS :has() 控展开。 + * + * 为什么是构建期 markdown-it 插件、而不是客户端 JS 增强? + * - FOUC:站点是 SSG,首屏静态 HTML 里代码块就是全展开的;客户端 bundle 异步加载, + * 冷缓存首访「先展开再闪收」必然。构建期产出即折叠态,零闪烁。 + * - 无障碍 / 无 JS:
原生 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。直接整段包
,内部兄弟链不动。 + * - 必须整段包(而非只包
):VitePress 复制按钮靠 button.copy.nextElementSibling
+ *     定位 
,兄弟链一断复制即失效。
+ *   - 代码放在 
【外面】(.vp-code-fold 里与
同级),而非放进
内: + * 原生
关闭时不渲染非 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:整段包
。 + 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 站构建时源码拷到 /en//,env.relativePath 以 "en/" 开头 + // (见 scripts/build.ts 的 EN 分支)。CN 卷不带该前缀。 + const relativePath = env?.relativePath + const isEn = typeof relativePath === 'string' && relativePath.startsWith('en/') + const closedLabel = isEn + ? `Expand (${lineCount} lines)` + : `展开代码 (共 ${lineCount} 行)` + const openLabel = isEn ? 'Collapse' : '收起代码' + + return ( + `
` + + `
${closedLabel}` + + `${openLabel}
` + + html + + `
` + ) + } +} diff --git a/site/.vitepress/theme/custom.css b/site/.vitepress/theme/custom.css index 90f4f5faa..1ebd59dfc 100644 --- a/site/.vitepress/theme/custom.css +++ b/site/.vitepress/theme/custom.css @@ -128,6 +128,105 @@ html[data-font-size='xxlarge'] { border-radius: 4px; } +/* ================================================================ + 长代码折叠(.vp-code-fold):构建期 code-fold-plugin 把 >30 行的代码块 + 包成
<代码/>
。 + - 行为:收起 = 只显示 summary 条(代码完全隐藏);展开 = 代码全部显示。 + 不做"预览"(收起态露几行)—— 要么全展开要么全不展开,二态干净。 + - 代码在
【外】,靠纯 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;