feat(markdown)!: group code copy button strings into one option

`markdown.codeCopyButton` now holds both the button tooltip
(`tooltipText`) and the text shown after copying (`copiedText`),
per-locale overridable via `locales.<index>.markdown.codeCopyButton`.
The copied text is emitted as a `data-copied` attribute on the button
and rendered by the theme with `content: attr(data-copied)`.

BREAKING CHANGE: `markdown.codeCopyButtonTitle` is now
`markdown.codeCopyButton.tooltipText`, and its default changed from
"Copy Code" to "Copy code". The `--vp-code-copy-copied-text-content`
CSS variable and its built-in per-language `:lang()` defaults are
removed - set `codeCopyButton.copiedText` (per locale) instead.

close #4431

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5335/head
Divyansh Singh 2 months ago
parent 4e263772b4
commit 2fa0dedbb2

@ -91,7 +91,7 @@ describe('node/markdown/plugins/containers', () => {
<p>content</p> <p>content</p>
</div> </div>
<details class="details custom-block"><summary>Click me to toggle the code</summary> <details class="details custom-block"><summary>Click me to toggle the code</summary>
<div class="language-js"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">console.log('hi') <div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">console.log('hi')
</code></pre> </code></pre>
</div></details> </div></details>
" "
@ -273,9 +273,9 @@ describe('node/markdown/plugins/containers', () => {
].join('\n') ].join('\n')
expect(await render(src)).toMatchInlineSnapshot(` expect(await render(src)).toMatchInlineSnapshot(`
"<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-0" id="tab-1" checked><label data-title="config.js" for="tab-1">config.js</label><input type="radio" name="group-0" id="tab-2" ><label data-title="config.ts" for="tab-2">config.ts</label></div><div class="blocks"> "<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-0" id="tab-1" checked><label data-title="config.js" for="tab-1">config.js</label><input type="radio" name="group-0" id="tab-2" ><label data-title="config.ts" for="tab-2">config.ts</label></div><div class="blocks">
<div class="language-js active"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1 <div class="language-js active"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
</code></pre> </code></pre>
</div><div class="language-ts"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre><code class="language-ts">const a: number = 1 </div><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre><code class="language-ts">const a: number = 1
</code></pre> </code></pre>
</div></div></div> </div></div></div>
" "
@ -435,7 +435,7 @@ describe('node/markdown/plugins/containers (github alerts)', () => {
<ul> <ul>
<li>list item</li> <li>list item</li>
</ul> </ul>
<div class="language-js"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1 <div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
</code></pre> </code></pre>
</div></div> </div></div>
" "
@ -569,18 +569,24 @@ describe('node/markdown/plugins/containers (locales)', () => {
).rejects.toThrow('is not registered in the root markdown config') ).rejects.toThrow('is not registered in the root markdown config')
}) })
test('resolves the code copy button title for the active locale', async () => { test('resolves the code copy button strings for the active locale', async () => {
const src = '```js\nconst a = 1\n```' const src = '```js\nconst a = 1\n```'
const opts: MarkdownOptions = { const opts: MarkdownOptions = {
codeCopyButtonTitle: 'Copy Code', codeCopyButton: { copiedText: 'Copied!' },
locales: { zh: { codeCopyButtonTitle: '复制代码' } } locales: {
zh: {
codeCopyButton: { tooltipText: '复制代码', copiedText: '已复制' }
}
}
} }
expect(await render(src, opts, { localeIndex: 'zh' })).toContain( const zh = await render(src, opts, { localeIndex: 'zh' })
'title="复制代码"' expect(zh).toContain('title="复制代码"')
) expect(zh).toContain('data-copied="已复制"')
expect(await render(src, opts, { localeIndex: 'es' })).toContain( const es = await render(src, opts, { localeIndex: 'es' })
'title="Copy Code"' expect(es).toContain('title="Copy code"')
) expect(es).toContain('data-copied="Copied!"')
expect(await render(src, opts)).toContain('title="Copy Code"') const root = await render(src, opts)
expect(root).toContain('title="Copy code"')
expect(root).toContain('data-copied="Copied!"')
}) })
}) })

@ -45,7 +45,6 @@ export default defineConfig({
markdown: { markdown: {
math: true, math: true,
codeCopyButtonTitle: 'Copy code',
codeTransformers: [ codeTransformers: [
// We use `[!!code` and `@@include` in demo to prevent transformation, // We use `[!!code` and `@@include` in demo to prevent transformation,
// here we revert it back. // here we revert it back.

@ -59,7 +59,7 @@ Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types
## Per-locale Markdown Strings ## Per-locale Markdown Strings
Strings baked into pages by the markdown renderer - the default titles of [custom containers](./markdown#custom-containers) and [GitHub-flavored alerts](./markdown#github-flavored-alerts), and the tooltip of the code copy button - can be overridden per locale with the `markdown` key of a locale entry: Strings baked into pages by the markdown renderer - the default titles of [custom containers](./markdown#custom-containers) and [GitHub-flavored alerts](./markdown#github-flavored-alerts), and the strings of the code copy button - can be overridden per locale with the `markdown` key of a locale entry:
```ts [docs/.vitepress/config.ts] ```ts [docs/.vitepress/config.ts]
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
@ -76,7 +76,7 @@ export default defineConfig({
warningLabel: '警告' warningLabel: '警告'
// ...the other labels, and titles of `customContainers` // ...the other labels, and titles of `customContainers`
}, },
codeCopyButtonTitle: '复制代码' codeCopyButton: { tooltipText: '复制代码', copiedText: '已复制' }
} }
} }
} }

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: 'IMPORTANTE', importantLabel: 'IMPORTANTE',
cautionLabel: 'PRECAUCIÓN' cautionLabel: 'PRECAUCIÓN'
}, },
codeCopyButtonTitle: 'Copiar código' codeCopyButton: { tooltipText: 'Copiar código', copiedText: 'Copiado' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: 'مهم', importantLabel: 'مهم',
cautionLabel: 'احتیاط' cautionLabel: 'احتیاط'
}, },
codeCopyButtonTitle: 'کپی کد' codeCopyButton: { tooltipText: 'کپی کد', copiedText: 'کپی شد' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: '重要', importantLabel: '重要',
cautionLabel: '注意' cautionLabel: '注意'
}, },
codeCopyButtonTitle: 'コードをコピー' codeCopyButton: { tooltipText: 'コードをコピー', copiedText: 'コピー完了' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: '중요', importantLabel: '중요',
cautionLabel: '주의' cautionLabel: '주의'
}, },
codeCopyButtonTitle: '코드 복사' codeCopyButton: { tooltipText: '코드 복사', copiedText: '복사됨' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: 'IMPORTANTE', importantLabel: 'IMPORTANTE',
cautionLabel: 'CUIDADO' cautionLabel: 'CUIDADO'
}, },
codeCopyButtonTitle: 'Copiar código' codeCopyButton: { tooltipText: 'Copiar código', copiedText: 'Copiado' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: 'ВАЖНО', importantLabel: 'ВАЖНО',
cautionLabel: 'ВНИМАНИЕ' cautionLabel: 'ВНИМАНИЕ'
}, },
codeCopyButtonTitle: 'Скопировать код' codeCopyButton: { tooltipText: 'Скопировать код', copiedText: 'Скопировано' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -19,7 +19,7 @@ export const markdown: MarkdownLocaleOptions = {
importantLabel: '重要', importantLabel: '重要',
cautionLabel: '小心' cautionLabel: '小心'
}, },
codeCopyButtonTitle: '复制代码' codeCopyButton: { tooltipText: '复制代码', copiedText: '已复制' }
} }
export default defineAdditionalConfig({ export default defineAdditionalConfig({

@ -518,7 +518,7 @@
color: var(--vp-code-copy-code-active-text); color: var(--vp-code-copy-code-active-text);
background-color: var(--vp-code-copy-code-hover-bg); background-color: var(--vp-code-copy-code-hover-bg);
white-space: nowrap; white-space: nowrap;
content: var(--vp-code-copy-copied-text-content); content: attr(data-copied);
} }
.vp-doc [class*='language-'] > span.lang { .vp-doc [class*='language-'] > span.lang {

@ -365,7 +365,6 @@
--vp-code-copy-code-hover-border-color: var(--vp-c-divider); --vp-code-copy-code-hover-border-color: var(--vp-c-divider);
--vp-code-copy-code-hover-bg: var(--vp-c-bg); --vp-code-copy-code-hover-bg: var(--vp-c-bg);
--vp-code-copy-code-active-text: var(--vp-c-text-2); --vp-code-copy-code-active-text: var(--vp-c-text-2);
--vp-code-copy-copied-text-content: 'Copied';
--vp-code-tab-divider: var(--vp-code-block-divider-color); --vp-code-tab-divider: var(--vp-code-block-divider-color);
--vp-code-tab-text-color: var(--vp-c-text-2); --vp-code-tab-text-color: var(--vp-c-text-2);
@ -375,26 +374,6 @@
--vp-code-tab-active-bar-color: var(--vp-c-brand-1); --vp-code-tab-active-bar-color: var(--vp-c-brand-1);
} }
:lang(es),
:lang(pt) {
--vp-code-copy-copied-text-content: 'Copiado';
}
:lang(fa) {
--vp-code-copy-copied-text-content: 'کپی شد';
}
:lang(ja) {
--vp-code-copy-copied-text-content: 'コピー完了';
}
:lang(ko) {
--vp-code-copy-copied-text-content: '복사됨';
}
:lang(ru) {
--vp-code-copy-copied-text-content: 'Скопировано';
}
:lang(zh) {
--vp-code-copy-copied-text-content: '已复制';
}
/** /**
* Component: Button * Component: Button
* -------------------------------------------------------------------------- */ * -------------------------------------------------------------------------- */

@ -35,7 +35,12 @@ import mditCjkFriendly from 'markdown-it-cjk-friendly'
import path from 'node:path' import path from 'node:path'
import type { BuiltinLanguage, BuiltinTheme, Highlighter } from 'shiki' import type { BuiltinLanguage, BuiltinTheme, Highlighter } from 'shiki'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import type { Awaitable, LocaleConfig, MarkdownLocaleOptions } from '../shared' import type {
Awaitable,
CodeCopyButtonOptions,
LocaleConfig,
MarkdownLocaleOptions
} from '../shared'
import { import {
containerPlugin, containerPlugin,
gitHubAlertsPlugin, gitHubAlertsPlugin,
@ -176,10 +181,11 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
preWrapper?: boolean preWrapper?: boolean
/** /**
* The tooltip text for the copy button in code blocks. * Strings for the copy button in code blocks: `tooltipText` is the
* @default 'Copy Code' * button's tooltip and `copiedText` is shown next to it after copying.
* @default { tooltipText: 'Copy code', copiedText: 'Copied' }
*/ */
codeCopyButtonTitle?: string codeCopyButton?: CodeCopyButtonOptions
/** /**
* Custom language labels for display. * Custom language labels for display.
* Overrides the default language label shown in code blocks. * Overrides the default language label shown in code blocks.
@ -349,7 +355,10 @@ export async function createMarkdownRenderer(
publicDir ??= path.resolve(srcDir, 'public') publicDir ??= path.resolve(srcDir, 'public')
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' const codeCopyButton = {
tooltipText: options.codeCopyButton?.tooltipText || 'Copy code',
copiedText: options.codeCopyButton?.copiedText || 'Copied'
}
const [highlight, dispose] = options.highlight const [highlight, dispose] = options.highlight
? [options.highlight, () => {}] ? [options.highlight, () => {}]
@ -372,7 +381,7 @@ export async function createMarkdownRenderer(
// VitePress customizations // VitePress customizations
if (options.preWrapper !== false) { if (options.preWrapper !== false) {
preWrapperPlugin(md, { preWrapperPlugin(md, {
codeCopyButtonTitle, codeCopyButton,
languageLabel: options.languageLabel, languageLabel: options.languageLabel,
locales: options.locales locales: options.locales
}) })

@ -2,10 +2,10 @@ import type { MarkdownItAsync } from 'markdown-it-async'
import type { MarkdownEnv, MarkdownLocaleOptions } from '../../shared' import type { MarkdownEnv, MarkdownLocaleOptions } from '../../shared'
export interface Options { export interface Options {
codeCopyButtonTitle: string codeCopyButton: { tooltipText: string; copiedText: string }
languageLabel?: Record<string, string> languageLabel?: Record<string, string>
/** /**
* Per-locale overrides for the copy button title, keyed by locale index. * Per-locale overrides for the copy button strings, keyed by locale index.
*/ */
locales?: Record<string, MarkdownLocaleOptions | undefined> locales?: Record<string, MarkdownLocaleOptions | undefined>
} }
@ -31,13 +31,18 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) {
const label = langLabel[lang.toLowerCase()] || lang.replace(/_/g, ' ') const label = langLabel[lang.toLowerCase()] || lang.replace(/_/g, ' ')
const { localeIndex } = (env ?? {}) as MarkdownEnv const { localeIndex } = (env ?? {}) as MarkdownEnv
const codeCopyButtonTitle = const localeButton = localeIndex
(localeIndex && options.locales?.[localeIndex]?.codeCopyButtonTitle) || ? options.locales?.[localeIndex]?.codeCopyButton
options.codeCopyButtonTitle : undefined
const tooltipText =
localeButton?.tooltipText || options.codeCopyButton.tooltipText
// rendered by the theme via `content: attr(data-copied)`
const copiedText =
localeButton?.copiedText || options.codeCopyButton.copiedText
return ( return (
`<div class="language-${lang}${active}">` + `<div class="language-${lang}${active}">` +
`<button title="${codeCopyButtonTitle}" class="copy"></button>` + `<button title="${tooltipText}" data-copied="${copiedText}" class="copy"></button>` +
`<span class="lang">${label}</span>` + `<span class="lang">${label}</span>` +
fence(...args) + fence(...args) +
'</div>' '</div>'

@ -10,6 +10,7 @@ export type {
AdditionalConfigDict, AdditionalConfigDict,
AdditionalConfigLoader, AdditionalConfigLoader,
Awaitable, Awaitable,
CodeCopyButtonOptions,
ContainerOptions, ContainerOptions,
DefaultTheme, DefaultTheme,
HeadConfig, HeadConfig,

17
types/shared.d.ts vendored

@ -230,6 +230,19 @@ export interface ContainerOptions {
customContainers?: Record<string, string> customContainers?: Record<string, string>
} }
export interface CodeCopyButtonOptions {
/**
* The tooltip (`title` attribute) of the copy button in code blocks.
* @default 'Copy code'
*/
tooltipText?: string
/**
* The text shown next to the copy button after copying.
* @default 'Copied'
*/
copiedText?: string
}
/** /**
* Build-time markdown strings that can be overridden per locale. Set them * Build-time markdown strings that can be overridden per locale. Set them
* under `locales.<index>.markdown` in the site config; values fall back to * under `locales.<index>.markdown` in the site config; values fall back to
@ -242,9 +255,9 @@ export interface MarkdownLocaleOptions {
*/ */
container?: ContainerOptions container?: ContainerOptions
/** /**
* Locale-specific tooltip text for the copy button in code blocks. * Locale-specific strings for the copy button in code blocks.
*/ */
codeCopyButtonTitle?: string codeCopyButton?: CodeCopyButtonOptions
} }
export type LocaleConfig<ThemeConfig = any> = Record< export type LocaleConfig<ThemeConfig = any> = Record<

Loading…
Cancel
Save