From b6384b11f496a34d6d02dc3ed16894dbc4b03c39 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Oct 2025 14:14:03 +0800 Subject: [PATCH] feat: support code block title --- .../styles/components/vp-doc.css | 34 ++++++++++++++ src/client/theme-default/styles/vars.css | 2 + src/node/markdown/plugins/containers.ts | 2 +- src/node/markdown/plugins/preWrapper.ts | 44 ++++++++++++++++--- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index abc58243..a5be6368 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -426,6 +426,32 @@ border-color 0.5s, color 0.5s; } +.vp-doc [class*='language-'] > .title-bar { + position: relative; + display: flex; + margin-right: -24px; + margin-left: -24px; + padding: 0 12px; + background-color: var(--vp-code-block-title-bg); + box-shadow: inset 0 -1px var(--vp-code-block-divider-color); +} + +@media (min-width: 640px) { + .vp-doc [class*='language-'] > .title-bar { + margin-right: 0; + margin-left: 0; + border-radius: 8px 8px 0 0; + } +} + +.vp-doc [class*='language-'] > .title-bar > .title-text { + padding: 0 12px; + line-height: 48px; + font-size: 14px; + font-weight: 500; + color: var(--vp-code-block-title-color); + white-space: nowrap; +} .vp-doc [class*='language-'] > button.copy { /*rtl:ignore*/ @@ -452,6 +478,10 @@ opacity 0.25s; } +.vp-doc [class*='language-'] > .title-bar ~ button.copy { + top: 60px; +} + .vp-doc [class*='language-']:hover > button.copy, .vp-doc [class*='language-'] > button.copy:focus { opacity: 1; @@ -512,6 +542,10 @@ opacity 0.4s; } +.vp-doc [class*='language-'] > .title-bar ~ span.lang { + top: 50px; +} + .vp-doc [class*='language-']:hover > button.copy + span.lang, .vp-doc [class*='language-'] > button.copy:focus + span.lang { opacity: 0; diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css index 2a974f25..48502a40 100644 --- a/src/client/theme-default/styles/vars.css +++ b/src/client/theme-default/styles/vars.css @@ -339,6 +339,8 @@ --vp-code-block-color: var(--vp-c-text-2); --vp-code-block-bg: var(--vp-c-bg-alt); --vp-code-block-divider-color: var(--vp-c-gutter); + --vp-code-block-title-color: var(--vp-c-text-2); + --vp-code-block-title-bg: var(--vp-code-block-bg); --vp-code-lang-color: var(--vp-c-text-2); diff --git a/src/node/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts index 39efce17..831b1db6 100644 --- a/src/node/markdown/plugins/containers.ts +++ b/src/node/markdown/plugins/containers.ts @@ -83,7 +83,7 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs { ) { const title = extractTitle( isHtml ? tokens[i].content : tokens[i].info, - isHtml + { html: isHtml, fallbackToLang: true } ) if (title) { diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts index b75fa618..2e62b421 100644 --- a/src/node/markdown/plugins/preWrapper.ts +++ b/src/node/markdown/plugins/preWrapper.ts @@ -1,4 +1,5 @@ import type { MarkdownItAsync } from 'markdown-it-async' +import type Token from 'markdown-it/lib/token.mjs' export interface Options { codeCopyButtonTitle: string @@ -16,7 +17,12 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) { const [tokens, idx] = args const token = tokens[idx] - // remove title from info + // @ts-ignore + const isFromSnippet = !!token.src + const title = + isFromSnippet || isInCodeGroup(tokens, idx) + ? '' + : extractTitle(token.info) token.info = token.info.replace(/\[.*\]/, '') const active = / active( |$)/.test(token.info) ? ' active' : '' @@ -27,21 +33,34 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) { return ( `
` + + (title + ? `
` + + `${title}` + + `
` + : '') + `` + `${label}` + fence(...args) + - '
' + `` ) } } -export function extractTitle(info: string, html = false) { - if (html) { +export interface ExtractTitleOptions { + html?: boolean + fallbackToLang?: boolean +} + +export function extractTitle(info: string, options?: ExtractTitleOptions) { + if (options?.html) { return ( info.replace(//g, '').match(/data-title="(.*?)"/)?.[1] || '' ) } - return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt' + return ( + info.match(/\[(.*)\]/)?.[1] || + (options?.fallbackToLang ? extractLang(info) || 'txt' : '') + ) } function extractLang(info: string) { @@ -53,3 +72,18 @@ function extractLang(info: string) { .replace(/^vue-html$/, 'template') .replace(/^ansi$/, '') } + +/** + * Whether the `idx` within `tokens` is inside a code-group container + */ +function isInCodeGroup(tokens: Token[], idx: number): boolean { + for (let i = idx - 1; i >= 0; --i) { + if (tokens[i].type === 'container_code-group_open') { + return true + } + if (tokens[i].type === 'container_code-group_close') { + return false + } + } + return false +}