From 5dd431023dd457cb4fa5303d9e4af2314c7c7382 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Mon, 27 Apr 2026 14:17:23 +0200 Subject: [PATCH 1/4] Auto-toggled code groups Synchronize tab selections of certain code groups across pages and persist them locally. Known values: - Node.js / Java - macOS / Linux / Windows --- .vitepress/config.js | 8 +- .vitepress/lib/restoreCodeGroupPreferences.js | 193 ++++++++++ .vitepress/lib/useCodeGroupSync.ts | 345 ++++++++++++++++++ .vitepress/theme/index.ts | 7 + 4 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 .vitepress/lib/restoreCodeGroupPreferences.js create mode 100644 .vitepress/lib/useCodeGroupSync.ts diff --git a/.vitepress/config.js b/.vitepress/config.js index 4b9b2d721d..83ff1d816e 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -3,11 +3,15 @@ const base = process.env.GH_BASE || '/docs/' // Construct vitepress config object... import path from 'node:path' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitepress' import playground from './lib/cds-playground/index.js' import languages from './languages' import { Menu } from './menu.js' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + const config = defineConfig({ title: 'capire', @@ -77,7 +81,9 @@ const config = defineConfig({ ['link', { rel: 'shortcut icon', href: base+'favicon.ico' }], ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: base+'logos/cap.png' }], // Inline script to restore impl-variant selection immediately (before first paint) - ['script', { id: 'check-impl-variant' }, `{const p=new URLSearchParams(location.search),v=p.get('impl-variant')||localStorage.getItem('impl-variant');if(v)document.documentElement.classList.add(v)}`] + ['script', { id: 'check-impl-variant' }, `{const p=new URLSearchParams(location.search),v=p.get('impl-variant')||localStorage.getItem('impl-variant');if(v)document.documentElement.classList.add(v)}`], + // Inline script to restore code group tab preferences (before Vue hydration) + ['script', {}, readFileSync(path.resolve(__dirname, './lib/restoreCodeGroupPreferences.js'), 'utf-8')] ], vite: { diff --git a/.vitepress/lib/restoreCodeGroupPreferences.js b/.vitepress/lib/restoreCodeGroupPreferences.js new file mode 100644 index 0000000000..f3202783e8 --- /dev/null +++ b/.vitepress/lib/restoreCodeGroupPreferences.js @@ -0,0 +1,193 @@ +;(() => { + // Code Group Tab Synchronization - Early Execution Script + // This script loads preferences and applies them before Vue hydration to prevent flicker + // + // Features: + // - Syncs tabs with exact or fuzzy matching ("/" delimiter) + // - "macOS/Linux" matches "macOS/Linux", "macOS", and "Linux" + // - "macOS" matches "macOS" and "macOS/Linux" + // - Stores preferences by independent dimensions (runtime vs OS) + // - runtime: Node.js ↔ Java + // - os: macOS ↔ Windows ↔ Linux (+ combinations) + // - Storage format: { "runtime": "Java", "os": "macOS" } + // - First entry in each dimension array is the default + + // Define independent dimensions of tabs + // Tabs within a dimension are mutually exclusive + // Note: First entry in each dimension is the default (used when no preference is saved) + // Note: Combinations like "macOS/Linux" are handled automatically by fuzzy matching + const TAB_DIMENSIONS = { + 'runtime': ['Node.js', 'Java'], + 'os': ['macOS', 'Windows', 'Linux'] + } + + // Determine which dimension a tab belongs to (including fuzzy matches) + const getTabDimension = (tabLabel) => { + for (const [dimension, tabs] of Object.entries(TAB_DIMENSIONS)) { + for (const dimTab of tabs) { + if (tabsMatch(tabLabel, dimTab)) { + return dimension + } + } + } + return null // Unknown dimension + } + + // Check if two tab labels match (exact or fuzzy match) + // Treats "/" as a delimiter for combined tabs + const tabsMatch = (tab1, tab2) => { + if (tab1 === tab2) return true + + // Split by "/" to get components + const components1 = tab1.split('/').map(s => s.trim()) + const components2 = tab2.split('/').map(s => s.trim()) + + // Check if any component from tab1 exists in components2 or vice versa + return components1.some(c1 => components2.includes(c1)) || + components2.some(c2 => components1.includes(c2)) + } + + // Get active tabs from localStorage (dimension-based storage) + const getActiveTabsByDimension = () => { + try { + const stored = localStorage.getItem('code-group-active-tabs') + if (stored) { + const parsed = JSON.parse(stored) + // Handle both old format (array) and new format (object) + if (Array.isArray(parsed)) { + // Migrate from old single-value format + return {} + } + return typeof parsed === 'object' ? parsed : {} + } + } catch (e) { + // localStorage might not be available or JSON parse failed + } + return {} + } + + // Clean up old localStorage entries from previous implementation + const cleanupOldEntries = () => { + try { + const keysToRemove = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && (key.startsWith('code-group-preference:') || key.startsWith('code-group-tab:'))) { + keysToRemove.push(key) + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)) + } catch (e) { + // localStorage might not be available + } + } + + // Clean up old entries on first run + cleanupOldEntries() + + // Determine the best tab from a set based on preferences and defaults + const getBestTab = (tabs, activeTabs) => { + // Check if any tab matches an active preference (exact or fuzzy match) + for (const tab of tabs) { + // Find which dimension this tab belongs to + const dimension = getTabDimension(tab) + if (dimension && activeTabs[dimension]) { + const activeTab = activeTabs[dimension] + // Check if this tab matches the active preference + if (tab === activeTab || tabsMatch(tab, activeTab)) { + return tab + } + } + } + + // Apply dimension defaults (first entry in each dimension) + for (const tab of tabs) { + const dimension = getTabDimension(tab) + if (dimension && TAB_DIMENSIONS[dimension]) { + const defaultTab = TAB_DIMENSIONS[dimension][0] + // Check if this tab matches the dimension default (exact or fuzzy) + if (tab === defaultTab || tabsMatch(tab, defaultTab)) { + return tab + } + } + } + + // Fallback to first tab alphabetically if no match + return tabs.sort()[0] + } + + // Load active tabs from storage + const activeTabs = getActiveTabsByDimension() + + // Store in global variable for later use by Vue components + window.__CODE_GROUP_ACTIVE_TABS__ = activeTabs + + // Apply preferences to a code group element + const applyToCodeGroup = (element) => { + const tabElements = element.querySelectorAll('.tabs label') + const tabs = Array.from(tabElements).map((label) => + (label.textContent || '').trim() + ).filter(Boolean) + + if (tabs.length === 0) return + + // Determine which tab should be selected + const selectedTab = getBestTab(tabs, activeTabs) + const selectedIndex = tabs.indexOf(selectedTab) + + if (selectedIndex === -1) return + + // Apply the selection immediately to prevent flicker + const inputs = element.querySelectorAll('.tabs input') + const blocks = element.querySelectorAll('div[class*="language-"], .vp-block') + + inputs.forEach((input, index) => { + input.checked = (index === selectedIndex) + }) + + blocks.forEach((block, index) => { + if (index === selectedIndex) { + block.classList.add('active') + } else { + block.classList.remove('active') + } + }) + } + + const applyToAllCodeGroups = () => { + const codeGroups = document.querySelectorAll('.vp-code-group') + codeGroups.forEach(applyToCodeGroup) + } + + // Apply immediately to any existing code groups (runs synchronously) + applyToAllCodeGroups() + + // Watch for code groups being added dynamically (SPA navigation, HMR in dev mode) + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement) { + if (node.classList?.contains('vp-code-group')) { + applyToCodeGroup(node) + } else if (node.querySelector) { + const codeGroups = node.querySelectorAll('.vp-code-group') + codeGroups.forEach(applyToCodeGroup) + } + } + } + } + }) + + // Start observing as soon as script runs + if (document.documentElement) { + observer.observe(document.documentElement, { + childList: true, + subtree: true + }) + } + + // Apply again on DOMContentLoaded as safety net + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', applyToAllCodeGroups) + } +})() diff --git a/.vitepress/lib/useCodeGroupSync.ts b/.vitepress/lib/useCodeGroupSync.ts new file mode 100644 index 0000000000..7192172e3b --- /dev/null +++ b/.vitepress/lib/useCodeGroupSync.ts @@ -0,0 +1,345 @@ +/** + * Code Group Tab Synchronization Composable + * + * Manages tab preferences for VitePress code groups: + * - Synchronizes tab selection across all code groups with exact or fuzzy matching + * - Fuzzy matching treats "/" as delimiter: "macOS/Linux" matches both "macOS" and "Linux" + * - Stores preferences by independent dimensions (runtime vs OS): + * - runtime: Node.js ↔ Java + * - os: Windows ↔ macOS ↔ Linux (+ combinations) + * - Selecting "Java" won't overwrite "macOS" (different dimensions) + * - Storage format: { "runtime": "Java", "os": "macOS" } + * - First entry in each dimension array is the default + */ + +// Define independent dimensions of tabs (must match restoreCodeGroupPreferences.js) +// Tabs within a dimension are mutually exclusive +// Note: First entry in each dimension is the default (used when no preference is saved) +// Note: Combinations like "macOS/Linux" are handled automatically by fuzzy matching +const TAB_DIMENSIONS: Record = { + 'runtime': ['Node.js', 'Java'], + 'os': ['macOS', 'Windows', 'Linux'] +} + +interface CodeGroupInfo { + element: HTMLElement + tabs: string[] +} + +/** + * Determine which dimension a tab belongs to (including fuzzy matches) + */ +function getTabDimension(tabLabel: string): string | null { + for (const [dimension, tabs] of Object.entries(TAB_DIMENSIONS)) { + for (const dimTab of tabs) { + if (tabsMatch(tabLabel, dimTab)) { + return dimension + } + } + } + return null // Unknown dimension +} + +/** + * Check if two tab labels match (exact or fuzzy match) + * Treats "/" as a delimiter for combined tabs + * Examples: + * - "macOS" matches "macOS" (exact) + * - "macOS" matches "macOS/Linux" (fuzzy - macOS is part of the combined tab) + * - "macOS/Linux" matches "macOS" (fuzzy - macOS is part of the combined tab) + * - "Windows" does NOT match "macOS/Linux" (no overlap) + */ +function tabsMatch(tab1: string, tab2: string): boolean { + if (tab1 === tab2) return true + + // Split by "/" to get components + const components1 = tab1.split('/').map(s => s.trim()) + const components2 = tab2.split('/').map(s => s.trim()) + + // Check if any component from tab1 exists in components2 or vice versa + return components1.some(c1 => components2.includes(c1)) || + components2.some(c2 => components1.includes(c2)) +} + +/** + * Get the best tab to select based on preferences and defaults + */ +function getBestTab(tabs: string[]): string { + // Get active tabs from localStorage or early-loaded window variable + let activeTabs: Record = getActiveTabsByDimension() + + // Fallback to early-loaded active tabs if available + const earlyActiveTabs = (window as any).__CODE_GROUP_ACTIVE_TABS__ + if (earlyActiveTabs && Object.keys(earlyActiveTabs).length > 0) { + activeTabs = earlyActiveTabs + } + + // Check if any tab matches an active preference (exact or fuzzy match) + for (const tab of tabs) { + // Find which dimension this tab belongs to + const dimension = getTabDimension(tab) + if (dimension && activeTabs[dimension]) { + const activeTab = activeTabs[dimension] + // Check if this tab matches the active preference + if (tab === activeTab || tabsMatch(tab, activeTab)) { + return tab + } + } + } + + // Apply dimension defaults (first entry in each dimension) + for (const tab of tabs) { + const dimension = getTabDimension(tab) + if (dimension && TAB_DIMENSIONS[dimension]) { + const defaultTab = TAB_DIMENSIONS[dimension][0] + // Check if this tab matches the dimension default (exact or fuzzy) + if (tab === defaultTab || tabsMatch(tab, defaultTab)) { + return tab + } + } + } + + // Fallback to first tab alphabetically + return [...tabs].sort()[0] +} + +/** + * Get active tabs from localStorage (dimension-based storage) + */ +function getActiveTabsByDimension(): Record { + try { + const stored = localStorage.getItem('code-group-active-tabs') + if (stored) { + const parsed = JSON.parse(stored) + // Handle both old format (array) and new format (object) + if (Array.isArray(parsed)) { + // Migrate from old single-value format + return {} + } + return typeof parsed === 'object' ? parsed : {} + } + } catch (e) { + // localStorage might not be available or JSON parse failed + } + return {} +} + +/** + * Save active tabs to localStorage (dimension-based storage) + */ +function saveActiveTabsByDimension(activeTabs: Record): void { + try { + localStorage.setItem('code-group-active-tabs', JSON.stringify(activeTabs)) + } catch (e) { + // localStorage might not be available + } +} + +/** + * Add a tab to the active tabs (updates only the relevant dimension) + */ +function addActiveTab(tabLabel: string): void { + const activeTabs = getActiveTabsByDimension() + const dimension = getTabDimension(tabLabel) + + if (dimension) { + // Update only this dimension + activeTabs[dimension] = tabLabel + saveActiveTabsByDimension(activeTabs) + } +} + +/** + * Find all code groups in the document + */ +function findCodeGroups(): CodeGroupInfo[] { + const codeGroups: CodeGroupInfo[] = [] + const elements = document.querySelectorAll('.vp-code-group') + + elements.forEach((element) => { + const tabElements = element.querySelectorAll('.tabs label') + const tabs = Array.from(tabElements).map((label) => + (label.textContent || '').trim() + ).filter(Boolean) + + if (tabs.length > 0) { + codeGroups.push({ + element: element as HTMLElement, + tabs + }) + } + }) + + return codeGroups +} + +/** + * Apply saved preference to a code group + */ +function applyPreference(codeGroup: CodeGroupInfo): void { + const { element, tabs } = codeGroup + const selectedTab = getBestTab(tabs) + + // Find and check the corresponding radio button and activate content + const labels = element.querySelectorAll('.tabs label') + const blocks = element.querySelectorAll('div[class*="language-"], .vp-block') + + labels.forEach((label, index) => { + const tabLabel = (label.textContent || '').trim() + const input = element.querySelector(`.tabs input:nth-of-type(${index + 1})`) as HTMLInputElement + const block = blocks[index] as HTMLElement + + if (tabLabel === selectedTab) { + // Activate this tab + if (input && !input.checked) { + input.checked = true + } + if (block && !block.classList.contains('active')) { + block.classList.add('active') + } + } else { + // Deactivate other tabs + if (input && input.checked) { + input.checked = false + } + if (block && block.classList.contains('active')) { + block.classList.remove('active') + } + } + }) +} + +/** + * Synchronize tab selection across all code groups + * Syncs both exact tab set matches and fuzzy matches (tab label matching) + */ +function syncTabs(selectedTab: string): void { + const codeGroups = findCodeGroups() + + codeGroups.forEach((codeGroup) => { + // Find a matching tab in this code group (exact or fuzzy match) + const matchingTab = codeGroup.tabs.find(tab => tabsMatch(tab, selectedTab)) + + if (matchingTab) { + const { element, tabs } = codeGroup + const tabIndex = tabs.indexOf(matchingTab) + const blocks = element.querySelectorAll('div[class*="language-"], .vp-block') + + if (tabIndex !== -1) { + // Update all tabs and blocks in this code group + tabs.forEach((_, index) => { + const input = element.querySelector(`.tabs input:nth-of-type(${index + 1})`) as HTMLInputElement + const block = blocks[index] as HTMLElement + + if (index === tabIndex) { + // Activate selected tab + if (input) input.checked = true + if (block) block.classList.add('active') + } else { + // Deactivate other tabs + if (input) input.checked = false + if (block) block.classList.remove('active') + } + }) + } + } + }) + + // Save the selected tab to active tabs + addActiveTab(selectedTab) +} + +/** + * Setup event listeners for tab clicks + */ +function setupEventListeners(): void { + // Use event delegation for better performance + document.addEventListener('click', (event) => { + const target = event.target as HTMLElement + + // Check if clicked on a code group tab label or input + const label = target.closest('.vp-code-group .tabs label') as HTMLLabelElement + if (!label) return + + const codeGroup = target.closest('.vp-code-group') as HTMLElement + if (!codeGroup) return + + const tabLabel = (label.textContent || '').trim() + if (!tabLabel) return + + // Sync all code groups with fuzzy matching + syncTabs(tabLabel) + }) +} + +/** + * Initialize code group synchronization + */ +function initCodeGroupSync(): void { + // Apply saved preferences to all code groups + const codeGroups = findCodeGroups() + codeGroups.forEach(applyPreference) + + // Setup event listeners for future interactions + setupEventListeners() +} + +/** + * Reinitialize for SPA navigation + */ +function reinitCodeGroupSync(): void { + // Apply preferences to newly rendered code groups + const codeGroups = findCodeGroups() + codeGroups.forEach(applyPreference) +} + +/** + * Setup code group synchronization + * Call this function when the app is mounted and after route changes + */ +export function setupCodeGroupSync(): void { + // Initialize on first load + if (typeof window !== 'undefined') { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initCodeGroupSync()) + } else { + // DOM is already ready + initCodeGroupSync() + } + + // Handle dynamic content changes (e.g., hot module replacement in dev mode) + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + // Check if any added nodes contain code groups + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement) { + if (node.classList?.contains('vp-code-group') || + node.querySelector?.('.vp-code-group')) { + reinitCodeGroupSync() + break + } + } + } + } + } + }) + + // Start observing the document body for added code groups + observer.observe(document.body, { + childList: true, + subtree: true + }) + } +} + +/** + * Reinitialize after route change (for SPA navigation) + */ +export function onRouteChange(): void { + if (typeof window !== 'undefined') { + // Use setTimeout to ensure DOM has updated + setTimeout(() => { reinitCodeGroupSync() }, 0) + } +} diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 18433e6ad3..1bc3806ba1 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -12,6 +12,7 @@ import Since from './components/Since.vue'; import UnderConstruction from './components/UnderConstruction.vue'; import CfgInspect from './components/ConfigInspect.vue'; import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' +import { setupCodeGroupSync, onRouteChange } from '../lib/useCodeGroupSync' import '@shikijs/vitepress-twoslash/style.css' import './styles.scss' @@ -36,5 +37,11 @@ export default { ctx.app.component('Since', Since) ctx.app.component('UnderConstruction', UnderConstruction) ctx.app.use(TwoslashFloatingVue) + + // Setup code group tab synchronization + setupCodeGroupSync() + + // Reinitialize on route changes (SPA navigation) + ctx.router.onAfterRouteChange = () => onRouteChange() } } \ No newline at end of file From df9d67512ad79a5c51dca7ca100bcf6664b74295 Mon Sep 17 00:00:00 2001 From: Johannes Vogt Date: Tue, 5 May 2026 15:43:45 +0200 Subject: [PATCH 2/4] Auto toggle scroll (#2545) Co-authored-by: Christian Georgi --- .vitepress/lib/restoreCodeGroupPreferences.js | 65 ++++++++++++++++++- .vitepress/lib/useCodeGroupSync.ts | 16 +++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/.vitepress/lib/restoreCodeGroupPreferences.js b/.vitepress/lib/restoreCodeGroupPreferences.js index f3202783e8..d189c47610 100644 --- a/.vitepress/lib/restoreCodeGroupPreferences.js +++ b/.vitepress/lib/restoreCodeGroupPreferences.js @@ -60,7 +60,7 @@ } return typeof parsed === 'object' ? parsed : {} } - } catch (e) { + } catch { // localStorage might not be available or JSON parse failed } return {} @@ -77,7 +77,7 @@ } } keysToRemove.forEach(key => localStorage.removeItem(key)) - } catch (e) { + } catch { // localStorage might not be available } } @@ -154,14 +154,60 @@ }) } + // VitePress's default scrollOffset (134) accounts for the fixed header + // and padding. This must match VitePress's getScrollOffset() to ensure + // consistent scroll positions between hash-link clicks and page reloads. + const getScrollOffset = () => 134 + + // Function to scroll to hash (matches VitePress's scrollTo logic) + const scrollToHash = (hash) => { + try { + const target = document.getElementById(decodeURIComponent(hash).slice(1)) + if (target) { + const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10) + const targetTop = window.scrollY + + target.getBoundingClientRect().top - + getScrollOffset() + + targetPadding + + window.scrollTo(0, targetTop) + } + } catch { /* ignore invalid hash */ } + } + const applyToAllCodeGroups = () => { const codeGroups = document.querySelectorAll('.vp-code-group') codeGroups.forEach(applyToCodeGroup) } + // Track if we need to restore hash scroll + const initialHash = window.location.hash + let hashScrollPending = false + + if (initialHash) { + // Clear hash to prevent browser's auto-scroll + history.replaceState(null, '', window.location.pathname + window.location.search) + hashScrollPending = true + } + + const restoreHashScroll = () => { + if (hashScrollPending) { + // Restore hash and scroll immediately + history.replaceState(null, '', window.location.pathname + window.location.search + initialHash) + // Scroll on next frame to let layout settle + requestAnimationFrame(() => { + scrollToHash(initialHash) + hashScrollPending = false + }) + } + } + // Apply immediately to any existing code groups (runs synchronously) applyToAllCodeGroups() + // If we have code groups and a hash, restore scroll now + if (document.querySelectorAll('.vp-code-group').length > 0) restoreHashScroll() + // Watch for code groups being added dynamically (SPA navigation, HMR in dev mode) const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { @@ -169,9 +215,17 @@ if (node instanceof HTMLElement) { if (node.classList?.contains('vp-code-group')) { applyToCodeGroup(node) + + // This might be the last code group, try to scroll + restoreHashScroll() } else if (node.querySelector) { const codeGroups = node.querySelectorAll('.vp-code-group') codeGroups.forEach(applyToCodeGroup) + + // Try to scroll after processing all code groups + if (codeGroups.length > 0) { + restoreHashScroll() + } } } } @@ -188,6 +242,11 @@ // Apply again on DOMContentLoaded as safety net if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', applyToAllCodeGroups) + document.addEventListener('DOMContentLoaded', () => { + applyToAllCodeGroups() + + // Final attempt to restore hash scroll if still pending + restoreHashScroll() + }) } })() diff --git a/.vitepress/lib/useCodeGroupSync.ts b/.vitepress/lib/useCodeGroupSync.ts index 7192172e3b..5f2154db9e 100644 --- a/.vitepress/lib/useCodeGroupSync.ts +++ b/.vitepress/lib/useCodeGroupSync.ts @@ -267,8 +267,24 @@ function setupEventListeners(): void { const tabLabel = (label.textContent || '').trim() if (!tabLabel) return + // Capture the viewport position of the clicked tab before syncing + const clickedRect = label.getBoundingClientRect() + // Sync all code groups with fuzzy matching syncTabs(tabLabel) + + // Restore scroll position to keep the clicked tab in view + requestAnimationFrame(() => { + const newRect = label.getBoundingClientRect() + const scrollDelta = newRect.top - clickedRect.top + + if (scrollDelta !== 0) { + window.scrollTo({ + top: (window.pageYOffset || document.documentElement.scrollTop) + scrollDelta, + behavior: 'instant' + }) + } + }) }) } From 61aa78239c37c13efc6e0aa307f21a3a5e126b7e Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Tue, 5 May 2026 17:04:31 +0200 Subject: [PATCH 3/4] Add CF/Kyma --- .vitepress/lib/restoreCodeGroupPreferences.js | 3 ++- .vitepress/lib/useCodeGroupSync.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.vitepress/lib/restoreCodeGroupPreferences.js b/.vitepress/lib/restoreCodeGroupPreferences.js index d189c47610..e6920bb585 100644 --- a/.vitepress/lib/restoreCodeGroupPreferences.js +++ b/.vitepress/lib/restoreCodeGroupPreferences.js @@ -18,7 +18,8 @@ // Note: Combinations like "macOS/Linux" are handled automatically by fuzzy matching const TAB_DIMENSIONS = { 'runtime': ['Node.js', 'Java'], - 'os': ['macOS', 'Windows', 'Linux'] + 'os': ['macOS', 'Windows', 'Linux'], + 'cloud-runtime': ['Cloud Foundry', 'Kyma'] } // Determine which dimension a tab belongs to (including fuzzy matches) diff --git a/.vitepress/lib/useCodeGroupSync.ts b/.vitepress/lib/useCodeGroupSync.ts index 5f2154db9e..df8077868f 100644 --- a/.vitepress/lib/useCodeGroupSync.ts +++ b/.vitepress/lib/useCodeGroupSync.ts @@ -18,7 +18,8 @@ // Note: Combinations like "macOS/Linux" are handled automatically by fuzzy matching const TAB_DIMENSIONS: Record = { 'runtime': ['Node.js', 'Java'], - 'os': ['macOS', 'Windows', 'Linux'] + 'os': ['macOS', 'Windows', 'Linux'], + 'cloud-runtime': ['Cloud Foundry', 'Kyma'] } interface CodeGroupInfo { From 0b6852c57f59946b4590cdfabaa577e3abd95c72 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 8 May 2026 11:39:14 +0200 Subject: [PATCH 4/4] Relax matching of tab titles --- .vitepress/lib/restoreCodeGroupPreferences.js | 9 +++++++++ .vitepress/lib/useCodeGroupSync.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.vitepress/lib/restoreCodeGroupPreferences.js b/.vitepress/lib/restoreCodeGroupPreferences.js index e6920bb585..ca4159664a 100644 --- a/.vitepress/lib/restoreCodeGroupPreferences.js +++ b/.vitepress/lib/restoreCodeGroupPreferences.js @@ -39,6 +39,15 @@ const tabsMatch = (tab1, tab2) => { if (tab1 === tab2) return true + // Relaxed matching for labels with additional context, + // for example: "mta.yaml (Cloud Foundry)" <-> "Cloud Foundry" + const normalized1 = tab1.trim().toLowerCase() + const normalized2 = tab2.trim().toLowerCase() + + if (normalized1 && normalized2 && (normalized1.includes(normalized2) || normalized2.includes(normalized1))) { + return true + } + // Split by "/" to get components const components1 = tab1.split('/').map(s => s.trim()) const components2 = tab2.split('/').map(s => s.trim()) diff --git a/.vitepress/lib/useCodeGroupSync.ts b/.vitepress/lib/useCodeGroupSync.ts index df8077868f..8c60b363ff 100644 --- a/.vitepress/lib/useCodeGroupSync.ts +++ b/.vitepress/lib/useCodeGroupSync.ts @@ -53,6 +53,15 @@ function getTabDimension(tabLabel: string): string | null { function tabsMatch(tab1: string, tab2: string): boolean { if (tab1 === tab2) return true + // Relaxed matching for labels with additional context, + // for example: "mta.yaml (Cloud Foundry)" <-> "Cloud Foundry" + const normalized1 = tab1.trim().toLowerCase() + const normalized2 = tab2.trim().toLowerCase() + + if (normalized1 && normalized2 && (normalized1.includes(normalized2) || normalized2.includes(normalized1))) { + return true + } + // Split by "/" to get components const components1 = tab1.split('/').map(s => s.trim()) const components2 = tab2.split('/').map(s => s.trim())