diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 3446b4efdb1e..f57f20e1ac1f 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -173,6 +173,76 @@ export default config ::: +### Group Name Basic + +::: code-group name=installs + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +::: + +### Group Name Second Instance (Same Name for Sync Test) + +::: code-group name=installs + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +::: + +### Group Name with Hyphens and Underscores + +::: code-group name=install_methods-v2 + +```bash [npm] +npm install vitepress@next +``` + +```bash [pnpm] +pnpm add vitepress@next +``` + +::: + +### Group Name with Spaces (Should be Rejected) + +::: code-group name="install methods" + +```bash [npm] +npm install vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +### Group Name with Invalid Characters (Should be Rejected) + +::: code-group name=install@methods! + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +::: + ## Markdown File Inclusion diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 839f953cba9e..656b669e0686 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -91,6 +91,11 @@ describe('Table of Contents', () => { "Code Groups", "Basic Code Group", "With Other Features", + "Group Name Basic", + "Group Name Second Instance (Same Name for Sync Test)", + "Group Name with Hyphens and Underscores", + "Group Name with Spaces (Should be Rejected)", + "Group Name with Invalid Characters (Should be Rejected)", "Markdown File Inclusion", "Region", "Markdown At File Inclusion", @@ -277,6 +282,91 @@ describe('Code Groups', () => { await getClassList(blocks.nth(1).locator('code > span').nth(0)) ).toContain('highlighted') }) + + test('group-name basic', async () => { + const div = page.locator('#group-name-basic + div') + + // Verify data attribute exists + const groupName = await div.getAttribute('data-group-name') + expect(groupName).toBe('installs') + + // Verify tabs still work + const labels = div.locator('.tabs > label') + expect(await labels.count()).toBe(2) + + // Verify clicking still switches tabs + await labels.nth(1).click() + const blocks = div.locator('.blocks > div') + expect(await getClassList(blocks.nth(1))).toContain('active') + }) + + test('group-name synchronization across groups', async () => { + // Clear localStorage to ensure clean test state + await page.evaluate(() => localStorage.clear()) + await page.reload() + await page.waitForSelector('#group-name-basic + div') + + const div1 = page.locator('#group-name-basic + div') + const div2 = page.locator( + '#group-name-second-instance-same-name-for-sync-test + div' + ) + + // Both groups should have the same group-name + expect(await div1.getAttribute('data-group-name')).toBe('installs') + expect(await div2.getAttribute('data-group-name')).toBe('installs') + + // Initially, both should have first tab active + expect(await getClassList(div1.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + + // Click second tab in first group + await div1.locator('.tabs > label').nth(1).click() + + // Both groups should now have second tab active (synced) + expect(await getClassList(div1.locator('.blocks > div').nth(1))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(1))).toContain( + 'active' + ) + + // Click first tab in second group + await div2.locator('.tabs > label').nth(0).click() + + // Both groups should now have first tab active again (synced back) + expect(await getClassList(div1.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + expect(await getClassList(div2.locator('.blocks > div').nth(0))).toContain( + 'active' + ) + }) + + test('group-name with hyphens and underscores', async () => { + const div = page.locator('#group-name-with-hyphens-and-underscores + div') + const groupName = await div.getAttribute('data-group-name') + expect(groupName).toBe('install_methods-v2') + }) + + test('group-name with spaces should be rejected', async () => { + const div = page.locator('#group-name-with-spaces-should-be-rejected + div') + const groupName = await div.getAttribute('data-group-name') + // Quoted names with spaces should be rejected + expect(groupName).toBeNull() + }) + + test('group-name with invalid characters should be rejected', async () => { + const div = page.locator( + '#group-name-with-invalid-characters-should-be-rejected + div' + ) + const groupName = await div.getAttribute('data-group-name') + // Should be rejected due to invalid characters + expect(groupName).toBeNull() + }) }) describe('Markdown File Inclusion', () => { diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index e1be3e6b366f..fcba9834449a 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -776,6 +776,102 @@ You can also [import snippets](#import-code-snippets) in code groups: ::: +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + ## Markdown File Inclusion You can include a markdown file in another markdown file, even nested. diff --git a/docs/es/guide/markdown.md b/docs/es/guide/markdown.md index ebbd0d5e6b77..b436354a67aa 100644 --- a/docs/es/guide/markdown.md +++ b/docs/es/guide/markdown.md @@ -756,6 +756,104 @@ También puede [importar _snippets_ de código](#import-code-snippets) en grupos ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## Inclusión de Archivo Markdown {#markdown-file-inclusion} Puede incluir un archivo markdown en otro archvo markdown, incluso anidado. diff --git a/docs/fa/guide/markdown.md b/docs/fa/guide/markdown.md index 4e17bd312e5e..616670d177cd 100644 --- a/docs/fa/guide/markdown.md +++ b/docs/fa/guide/markdown.md @@ -707,6 +707,104 @@ export default config <<< @/snippets/snippet-with-region.js#snippet{1,2 ts:line-numbers} [قطعه با منطقه] ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## ادغام فایل‌های Markdown {#markdown-file-inclusion} می‌توانید یک فایل Markdown را در یک فایل Markdown دیگر، حتی در صورت وجود تو در تو، وارد کنید. diff --git a/docs/ja/guide/markdown.md b/docs/ja/guide/markdown.md index 966ce72c8d2c..cb6a91d04724 100644 --- a/docs/ja/guide/markdown.md +++ b/docs/ja/guide/markdown.md @@ -781,6 +781,104 @@ export default { ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## Markdown ファイルのインクルード {#markdown-file-inclusion} ある Markdown ファイルの中に、別の Markdown ファイルを取り込めます(入れ子も可能)。 diff --git a/docs/ko/guide/markdown.md b/docs/ko/guide/markdown.md index f2003ca61b97..9e786dcfd5ac 100644 --- a/docs/ko/guide/markdown.md +++ b/docs/ko/guide/markdown.md @@ -754,6 +754,104 @@ export default config ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## 마크다운 파일 포함 {#markdown-file-inclusion} 마크다운 파일을 다른 마크다운 파일에 포함시킬 수 있으며, 중첩도 가능합니다. diff --git a/docs/pt/guide/markdown.md b/docs/pt/guide/markdown.md index c41862706c29..7e7abad2ea3e 100644 --- a/docs/pt/guide/markdown.md +++ b/docs/pt/guide/markdown.md @@ -754,6 +754,104 @@ Você também pode [importar _snippets_ de código](#import-code-snippets) em gr ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## Inclusão de Arquivo Markdown {#markdown-file-inclusion} Você pode incluir um arquivo markdown em outro arquivo markdown, mesmo aninhado. diff --git a/docs/ru/guide/markdown.md b/docs/ru/guide/markdown.md index 2bd1a5c1649d..1595b568b170 100644 --- a/docs/ru/guide/markdown.md +++ b/docs/ru/guide/markdown.md @@ -778,6 +778,104 @@ export default config ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## Включение файла Markdown {#markdown-file-inclusion} Вы можете включить файл Markdown в другой файл Markdown, даже вложенный. diff --git a/docs/zh/guide/markdown.md b/docs/zh/guide/markdown.md index a401774ccfd4..93fc190f64b8 100644 --- a/docs/zh/guide/markdown.md +++ b/docs/zh/guide/markdown.md @@ -754,6 +754,104 @@ export default config ::: + +### Named Code Groups + +You can name code groups to synchronize tab selections across multiple groups. When you have multiple code groups with the same name, selecting a tab in one will automatically select the corresponding tab in all other groups with the same name. + +**Input** + +````md +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + + + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: +```` + +When you click on a tab (e.g., "pnpm") in one group, all other groups with `name=package-managers` will automatically switch to the same tab. + +**Output** + +::: code-group name=package-managers + +```bash [npm] +npm install vitepress +``` + +```bash [pnpm] +pnpm add vitepress +``` + +```bash [yarn] +yarn add vitepress +``` + +::: + +::: code-group name=package-managers + +```bash [npm] +npm run docs +``` + +```bash [pnpm] +pnpm run docs +``` + +```bash [yarn] +yarn docs +``` + +::: + +Try clicking different tabs above! Notice how both code groups switch together because they share the same `name`. + +::: info +Your tab selection is automatically saved to localStorage. When you return to the page, your preferred tab for each name will be automatically selected. +::: + +The `name` parameter accepts only alphanumeric characters, hyphens, and underscores. No whitespace is allowed. + +Valid examples: +- `name=installs` +- `name=install-methods` +- `name=install_methods` +- `name=installMethods` + +::: tip +This feature is especially useful in documentation where you show the same tool (like package managers or programming languages) in multiple places, providing a consistent experience for users. +::: + + ## 包含 markdown 文件 {#markdown-file-inclusion} 可以像这样在一个 markdown 文件中包含另一个 markdown 文件,甚至是内嵌的。 diff --git a/src/client/app/composables/codeGroups.ts b/src/client/app/composables/codeGroups.ts index ba56bc05f84f..d946f589c927 100644 --- a/src/client/app/composables/codeGroups.ts +++ b/src/client/app/composables/codeGroups.ts @@ -1,9 +1,41 @@ import { inBrowser, onContentUpdated } from 'vitepress' +const STORAGE_KEY = 'vitepress:tabsCache' + +function getStoredTabIndex(groupName: string): number | null { + if (!inBrowser) return null + try { + const cache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + return cache[groupName] ?? null + } catch { + return null + } +} + +function setStoredTabIndex(groupName: string, index: number) { + if (!inBrowser) return + try { + const cache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + cache[groupName] = index + localStorage.setItem(STORAGE_KEY, JSON.stringify(cache)) + } catch { + // Silently ignore localStorage errors + } +} + export function useCodeGroups() { if (import.meta.env.DEV) { onContentUpdated(() => { document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => { + const group = el.closest('.vp-code-group') + const groupName = group?.getAttribute('data-group-name') + + // Don't reset if user has a saved preference + if (groupName && getStoredTabIndex(groupName) !== null) { + return // Keep user's preference + } + + // Only reset groups without saved preferences Array.from(el.children).forEach((child) => { child.classList.remove('active') }) @@ -13,6 +45,38 @@ export function useCodeGroups() { } if (inBrowser) { + // Restore tabs from localStorage on page load, but only on first content load + let hasRestoredTabs = false + onContentUpdated(() => { + if (hasRestoredTabs) return + hasRestoredTabs = true + + document + .querySelectorAll('.vp-code-group[data-group-name]') + .forEach((group) => { + const groupName = group.getAttribute('data-group-name') + if (!groupName) return + + const storedIndex = getStoredTabIndex(groupName) + if (storedIndex === null) return + + const inputs = group.querySelectorAll('input') + const blocks = group.querySelector('.blocks') + if (!blocks || !inputs[storedIndex]) return + + // Update radio input + inputs[storedIndex].checked = true + + // Update active block + const currentActive = blocks.querySelector('.active') + const newActive = blocks.children[storedIndex] + if (currentActive && newActive && currentActive !== newActive) { + currentActive.classList.remove('active') + newActive.classList.add('active') + } + }) + }) + window.addEventListener('click', (e) => { const el = e.target as HTMLInputElement @@ -24,6 +88,7 @@ export function useCodeGroups() { const i = Array.from(group.querySelectorAll('input')).indexOf(el) if (i < 0) return + // Update current group const blocks = group.querySelector('.blocks') if (!blocks) return @@ -40,11 +105,49 @@ export function useCodeGroups() { const label = group?.querySelector(`label[for="${el.id}"]`) label?.scrollIntoView({ block: 'nearest' }) + + // Sync other groups with same group-name and save to localStorage + const groupName = group.getAttribute('data-group-name') + if (groupName) { + setStoredTabIndex(groupName, i) + syncTabsInOtherGroups(groupName, i, group as HTMLElement) + } } }) } } +function syncTabsInOtherGroups( + groupName: string, + tabIndex: number, + currentGroup: HTMLElement +) { + // Find all code groups with the same group-name + const groups = document.querySelectorAll( + `.vp-code-group[data-group-name="${groupName}"]` + ) + + groups.forEach((g) => { + // Skip the current group that was clicked + if (g === currentGroup) return + + const inputs = g.querySelectorAll('input') + const blocks = g.querySelector('.blocks') + if (!blocks || !inputs[tabIndex]) return + + // Update radio input + inputs[tabIndex].checked = true + + // Update active block + const currentActive = blocks.querySelector('.active') + const newActive = blocks.children[tabIndex] + if (currentActive && newActive && currentActive !== newActive) { + currentActive.classList.remove('active') + newActive.classList.add('active') + } + }) +} + function activate(el: Element): void { el.classList.add('active') window.dispatchEvent( diff --git a/src/node/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts index 39efce17fd2f..c79924a9b315 100644 --- a/src/node/markdown/plugins/containers.ts +++ b/src/node/markdown/plugins/containers.ts @@ -64,6 +64,23 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs { { render(tokens, idx) { if (tokens[idx].nesting === 1) { + const token = tokens[idx] + const info = token.info.trim() + + // Extract name parameter + const nameMatch = info.match(/name=(\S+)/) + let name = nameMatch ? nameMatch[1] : null + + // Validate: only allow alphanumeric, hyphens, and underscores + if (name && !/^[a-zA-Z0-9_-]+$/.test(name)) { + name = null + } + + // Build data attribute + const nameAttr = name + ? ` data-group-name="${md.utils.escapeHtml(name)}"` + : '' + let tabs = '' let checked = 'checked' @@ -95,7 +112,7 @@ function createCodeGroup(md: MarkdownItAsync): ContainerArgs { } } - return `
${tabs}
\n` + return `
${tabs}
\n` } return `
\n` }