Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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: {
Expand Down
253 changes: 253 additions & 0 deletions .vitepress/lib/restoreCodeGroupPreferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
;(() => {
// 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'],
'cloud-runtime': ['Cloud Foundry', 'Kyma']
}

// 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 {
// 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 {
// 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')
}
})
}

// 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) {
for (const node of mutation.addedNodes) {
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()
}
}
}
}
}
})

// 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()

// Final attempt to restore hash scroll if still pending
restoreHashScroll()
})
}
})()
Loading
Loading