pull/4898/merge
Valentin Maerten 1 month ago committed by GitHub
commit c23167a70a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,7 +15,7 @@ You can try VitePress directly in your browser on [StackBlitz](https://vitepress
VitePress can be used on its own, or be installed into an existing project. In both cases, you can install it with: VitePress can be used on its own, or be installed into an existing project. In both cases, you can install it with:
::: code-group ::: code-group :package-manager
```sh [npm] ```sh [npm]
$ npm add -D vitepress $ npm add -D vitepress
@ -49,7 +49,7 @@ VitePress is an ESM-only package. Don't use `require()` to import it, and make s
VitePress ships with a command line setup wizard that will help you scaffold a basic project. After installation, start the wizard by running: VitePress ships with a command line setup wizard that will help you scaffold a basic project. After installation, start the wizard by running:
::: code-group ::: code-group :package-manager
```sh [npm] ```sh [npm]
$ npx vitepress init $ npx vitepress init
@ -144,7 +144,7 @@ The tool should have also injected the following npm scripts to your `package.js
The `docs:dev` script will start a local dev server with instant hot updates. Run it with the following command: The `docs:dev` script will start a local dev server with instant hot updates. Run it with the following command:
::: code-group ::: code-group :package-manager
```sh [npm] ```sh [npm]
$ npm run docs:dev $ npm run docs:dev
@ -166,7 +166,7 @@ $ bun run docs:dev
Instead of npm scripts, you can also invoke VitePress directly with: Instead of npm scripts, you can also invoke VitePress directly with:
::: code-group ::: code-group :package-manager
```sh [npm] ```sh [npm]
$ npx vitepress dev docs $ npx vitepress dev docs

@ -776,6 +776,82 @@ You can also [import snippets](#import-code-snippets) in code groups:
::: :::
### Code Group Synchronization
Code groups can be synchronized across the page using a key. When you click on a tab in one code group, all other code groups with the same key will automatically switch to the corresponding tab. The selected tab is also reflected in the URL as a query parameter.
**Input**
````md
::: code-group :package-manager
```bash [npm]
npm install vitepress
```
```bash [yarn]
yarn add vitepress
```
```bash [pnpm]
pnpm add vitepress
```
:::
::: code-group :package-manager
```bash [npm]
npm run dev
```
```bash [yarn]
yarn dev
```
```bash [pnpm]
pnpm dev
```
:::
````
**Output**
::: code-group :package-manager
```bash [npm]
npm install vitepress
```
```bash [yarn]
yarn add vitepress
```
```bash [pnpm]
pnpm add vitepress
```
:::
::: code-group :package-manager
```bash [npm]
npm run dev
```
```bash [yarn]
yarn dev
```
```bash [pnpm]
pnpm dev
```
:::
When you select a tab (e.g., "pnpm"), both code groups will switch to show the pnpm version, and the URL will update to include `?package-manager=pnpm`. This makes it easy to share links that preserve the user's tab selection.
## Markdown File Inclusion ## Markdown File Inclusion
You can include a markdown file in another markdown file, even nested. You can include a markdown file in another markdown file, even nested.

@ -1,21 +1,9 @@
lockfileVersion: '9.0' lockfileVersion: '9.0'
settings: settings:
autoInstallPeers: false autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides:
ora>string-width: ^5
vite: npm:rolldown-vite@latest
patchedDependencies:
'@types/mdurl@2.0.0':
hash: 3460e7d18ce390685cf4b8d8237fb20df9ad952c1336f479995a508a6395bfa4
path: patches/@types__mdurl@2.0.0.patch
markdown-it-anchor@9.2.0:
hash: cdc28e7c329be30688ad192126ba505446611fbe526ad51483e4b1287aa35cf9
path: patches/markdown-it-anchor@9.2.0.patch
importers: importers:
.: .:
@ -40,7 +28,7 @@ importers:
version: 3.8.1 version: 3.8.1
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(rolldown-vite@7.0.10(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(yaml@2.8.0))(vue@3.5.18(typescript@5.8.3)) version: 6.0.0(vite@7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0))(vue@3.5.18(typescript@5.8.3))
'@vue/devtools-api': '@vue/devtools-api':
specifier: ^7.7.7 specifier: ^7.7.7
version: 7.7.7 version: 7.7.7
@ -66,8 +54,8 @@ importers:
specifier: ^3.8.1 specifier: ^3.8.1
version: 3.8.1 version: 3.8.1
vite: vite:
specifier: npm:rolldown-vite@latest specifier: ^7.0.6
version: rolldown-vite@7.0.10(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(yaml@2.8.0) version: 7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0)
vue: vue:
specifier: ^3.5.18 specifier: ^3.5.18
version: 3.5.18(typescript@5.8.3) version: 3.5.18(typescript@5.8.3)
@ -200,7 +188,7 @@ importers:
version: 14.1.0 version: 14.1.0
markdown-it-anchor: markdown-it-anchor:
specifier: ^9.2.0 specifier: ^9.2.0
version: 9.2.0(patch_hash=cdc28e7c329be30688ad192126ba505446611fbe526ad51483e4b1287aa35cf9)(@types/markdown-it@14.1.2)(markdown-it@14.1.0) version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0)
markdown-it-async: markdown-it-async:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
@ -335,7 +323,7 @@ importers:
version: link:.. version: link:..
vitepress-plugin-group-icons: vitepress-plugin-group-icons:
specifier: ^1.6.1 specifier: ^1.6.1
version: 1.6.1(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(markdown-it@14.1.0)(yaml@2.8.0) version: 1.6.1(markdown-it@14.1.0)(vite@7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0))
vitepress-plugin-llms: vitepress-plugin-llms:
specifier: ^1.7.1 specifier: ^1.7.1
version: 1.7.1 version: 1.7.1
@ -3089,10 +3077,51 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
vite@7.1.1:
resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitepress-plugin-group-icons@1.6.1: vitepress-plugin-group-icons@1.6.1:
resolution: {integrity: sha512-eoFlFAhAy/yTZDbaIgA/nMbjVYXkf8pz8rr75MN2VCw7yH60I3cw6bW5EuwddAeafZtBqbo8OsEGU7TIWFiAjg==} resolution: {integrity: sha512-eoFlFAhAy/yTZDbaIgA/nMbjVYXkf8pz8rr75MN2VCw7yH60I3cw6bW5EuwddAeafZtBqbo8OsEGU7TIWFiAjg==}
peerDependencies: peerDependencies:
markdown-it: '>=14' markdown-it: '>=14'
vite: '>=3'
vitepress-plugin-llms@1.7.1: vitepress-plugin-llms@1.7.1:
resolution: {integrity: sha512-RF5hl2vGxKhbcGirLLUhIlnWNSaoscPKBVnKaGxrKzj76i+mI+HBvfi/DF7a1u2L05LAnf7KSBkEVsMexczsAg==} resolution: {integrity: sha512-RF5hl2vGxKhbcGirLLUhIlnWNSaoscPKBVnKaGxrKzj76i+mI+HBvfi/DF7a1u2L05LAnf7KSBkEVsMexczsAg==}
@ -3834,13 +3863,13 @@ snapshots:
'@types/markdown-it@14.1.2': '@types/markdown-it@14.1.2':
dependencies: dependencies:
'@types/linkify-it': 5.0.0 '@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0(patch_hash=3460e7d18ce390685cf4b8d8237fb20df9ad952c1336f479995a508a6395bfa4) '@types/mdurl': 2.0.0
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/mdurl@2.0.0(patch_hash=3460e7d18ce390685cf4b8d8237fb20df9ad952c1336f479995a508a6395bfa4)': {} '@types/mdurl@2.0.0': {}
'@types/minimist@1.2.5': {} '@types/minimist@1.2.5': {}
@ -3881,10 +3910,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-vue@6.0.0(rolldown-vite@7.0.10(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(yaml@2.8.0))(vue@3.5.18(typescript@5.8.3))': '@vitejs/plugin-vue@6.0.0(vite@7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.19 '@rolldown/pluginutils': 1.0.0-beta.19
vite: rolldown-vite@7.0.10(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(yaml@2.8.0) vite: 7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0)
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
'@vitest/expect@4.0.0-beta.4': '@vitest/expect@4.0.0-beta.4':
@ -4938,7 +4967,7 @@ snapshots:
mark.js@8.11.1: {} mark.js@8.11.1: {}
markdown-it-anchor@9.2.0(patch_hash=cdc28e7c329be30688ad192126ba505446611fbe526ad51483e4b1287aa35cf9)(@types/markdown-it@14.1.2)(markdown-it@14.1.0): markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0):
dependencies: dependencies:
'@types/markdown-it': 14.1.2 '@types/markdown-it': 14.1.2
markdown-it: 14.1.0 markdown-it: 14.1.0
@ -5305,7 +5334,7 @@ snapshots:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
log-symbols: 6.0.0 log-symbols: 6.0.0
stdin-discarder: 0.2.2 stdin-discarder: 0.2.2
string-width: 5.1.2 string-width: 7.2.0
strip-ansi: 7.1.0 strip-ansi: 7.1.0
oxc-minify@0.78.0: oxc-minify@0.78.0:
@ -5973,26 +6002,30 @@ snapshots:
- tsx - tsx
- yaml - yaml
vitepress-plugin-group-icons@1.6.1(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(markdown-it@14.1.0)(yaml@2.8.0): vite@7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0):
dependencies:
esbuild: 0.25.8
fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.45.1
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 24.1.0
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.30.1
yaml: 2.8.0
vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0)):
dependencies: dependencies:
'@iconify-json/logos': 1.2.5 '@iconify-json/logos': 1.2.5
'@iconify-json/vscode-icons': 1.2.23 '@iconify-json/vscode-icons': 1.2.23
'@iconify/utils': 2.3.0 '@iconify/utils': 2.3.0
markdown-it: 14.1.0 markdown-it: 14.1.0
vite: rolldown-vite@7.0.10(@types/node@24.1.0)(esbuild@0.25.8)(jiti@1.21.7)(yaml@2.8.0) vite: 7.1.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.30.1)(yaml@2.8.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node'
- esbuild
- jiti
- less
- sass
- sass-embedded
- stylus
- sugarss
- supports-color - supports-color
- terser
- tsx
- yaml
vitepress-plugin-llms@1.7.1: vitepress-plugin-llms@1.7.1:
dependencies: dependencies:

@ -1,46 +1,155 @@
import { inBrowser, onContentUpdated } from 'vitepress' import { inBrowser, onContentUpdated } from 'vitepress'
const codeGroupCache = new Map<string, HTMLElement[]>()
export function useCodeGroups() { export function useCodeGroups() {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
onContentUpdated(() => { onContentUpdated(() => {
clearCache()
document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => { document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => {
Array.from(el.children).forEach((child) => { Array.from(el.children).forEach((child) => {
child.classList.remove('active') child.classList.remove('active')
}) })
el.children[0].classList.add('active') el.children[0].classList.add('active')
}) })
handleQueryParamNavigation()
}) })
} }
if (inBrowser) { if (inBrowser) {
const handleUrlChange = () => {
handleQueryParamNavigation()
}
handleQueryParamNavigation()
window.addEventListener('popstate', handleUrlChange)
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
const el = e.target as HTMLInputElement const el = e.target as HTMLInputElement
if (el.matches('.vp-code-group input')) { if (el.matches('.vp-code-group input')) {
// input <- .tabs <- .vp-code-group
const group = el.parentElement?.parentElement const group = el.parentElement?.parentElement
if (!group) return if (!group) return
const i = Array.from(group.querySelectorAll('input')).indexOf(el) const label = group?.querySelector(`label[for="${el.id}"]`)
if (i < 0) return if (!label) return
if (!activateTab(group, el)) return
label.scrollIntoView({ block: 'nearest' })
// Get the group key and tab title for URL update and sync
const groupKey = group.getAttribute('data-group-key')
const tabTitle = label.getAttribute('data-title')?.toLowerCase()
if (groupKey && tabTitle) {
syncCodeGroupsByKeyAndValue(groupKey, tabTitle, group)
updateUrl(groupKey, tabTitle)
}
}
})
}
}
function getCodeGroupsByKey(groupKey: string): HTMLElement[] {
if (!codeGroupCache.has(groupKey)) {
codeGroupCache.set(
groupKey,
Array.from(
document.querySelectorAll(
`.vp-code-group[data-group-key="${groupKey}"]`
)
)
)
}
return codeGroupCache.get(groupKey) || []
}
function clearCache() {
codeGroupCache.clear()
}
function activateTab(group: HTMLElement, input: HTMLInputElement): boolean {
const inputs = Array.from(
group.querySelectorAll('input')
) as HTMLInputElement[]
const index = inputs.indexOf(input)
if (index < 0) return false
const blocks = group.querySelector('.blocks') const blocks = group.querySelector('.blocks')
if (!blocks) return if (!blocks) return false
const current = Array.from(blocks.children).find((child) => // Update radio input checked state
child.classList.contains('active') inputs.forEach((radioInput, i) => {
radioInput.checked = i === index
})
// Remove active class from all blocks and add to the target block
Array.from(blocks.children).forEach((child, i) => {
child.classList.toggle('active', i === index)
})
return true
}
function findTabByTitle(
group: HTMLElement,
tabTitle: string
): HTMLInputElement | null {
if (!tabTitle) return null
const labels = Array.from(group.querySelectorAll('label[data-title]'))
const targetLabel = labels.find(
(label) =>
label.getAttribute('data-title')?.toLowerCase() === tabTitle.toLowerCase()
) )
if (!current) return
const next = blocks.children[i] if (!targetLabel) return null
if (!next || current === next) return
current.classList.remove('active') const inputId = targetLabel.getAttribute('for')
next.classList.add('active') if (!inputId) return null
const label = group?.querySelector(`label[for="${el.id}"]`) return group.querySelector(`#${inputId}`)
label?.scrollIntoView({ block: 'nearest' }) }
function syncCodeGroupsByKeyAndValue(
groupKey: string,
tabValue: string,
excludeGroup?: HTMLElement
) {
const groups = getCodeGroupsByKey(groupKey)
groups.forEach((group) => {
// Skip the group that was just clicked
if (excludeGroup && group === excludeGroup) return
const input = findTabByTitle(group, tabValue)
if (input) {
activateTab(group, input)
} }
}) })
} }
function updateUrl(groupKey: string, tabValue: string) {
const url = new URL(window.location.href)
url.searchParams.set(groupKey, tabValue)
window.history.replaceState(null, '', url.toString())
}
function handleQueryParamNavigation() {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.size === 0) return
for (const [groupKey, tabValue] of urlParams.entries()) {
const groups = getCodeGroupsByKey(groupKey)
for (const group of groups) {
const input = findTabByTitle(group, tabValue)
if (input) {
activateTab(group, input)
}
}
}
} }

@ -3,7 +3,7 @@ import container from 'markdown-it-container'
import type { RenderRule } from 'markdown-it/lib/renderer.mjs' import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
import type Token from 'markdown-it/lib/token.mjs' import type Token from 'markdown-it/lib/token.mjs'
import type { MarkdownEnv } from '../../shared' import type { MarkdownEnv } from '../../shared'
import { extractTitle } from './preWrapper' import { extractTitle, extractCodeGroupKey } from './preWrapper'
export const containerPlugin = ( export const containerPlugin = (
md: MarkdownItAsync, md: MarkdownItAsync,
@ -64,6 +64,12 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs {
{ {
render(tokens, idx) { render(tokens, idx) {
if (tokens[idx].nesting === 1) { if (tokens[idx].nesting === 1) {
// Extract the key from the code-group container info
const groupKey = extractCodeGroupKey(tokens[idx].info)
const groupKeyAttr = groupKey
? ` data-group-key="${md.utils.escapeHtml(groupKey)}"`
: ''
let tabs = '' let tabs = ''
let checked = 'checked' let checked = 'checked'
@ -87,7 +93,12 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs {
) )
if (title) { if (title) {
tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}><label data-title="${md.utils.escapeHtml(title)}" for="tab-${i}">${title}</label>` // Use the group key for all tabs in this group
const keyAttr = groupKey
? ` data-key="${md.utils.escapeHtml(groupKey)}"`
: ''
tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}><label data-title="${md.utils.escapeHtml(title)}"${keyAttr} for="tab-${i}">${title}</label>`
if (checked && !isHtml) tokens[i].info += ' active' if (checked && !isHtml) tokens[i].info += ' active'
checked = '' checked = ''
@ -95,7 +106,7 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs {
} }
} }
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n` return `<div class="vp-code-group"${groupKeyAttr}><div class="tabs">${tabs}</div><div class="blocks">\n`
} }
return `</div></div>\n` return `</div></div>\n`
} }

@ -37,6 +37,13 @@ export function extractTitle(info: string, html = false) {
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt' return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
} }
export function extractCodeGroupKey(info: string) {
// Extract key from code-group container info like ":package-manager", ":framework" etc.
const trimmedInfo = info.trim().replace(/^code-group\s*/, '')
const match = trimmedInfo.match(/^:([a-zA-Z0-9_-]+)/)
return match ? match[1] : null
}
function extractLang(info: string) { function extractLang(info: string) {
return info return info
.trim() .trim()

Loading…
Cancel
Save