Skip to content
Open
Show file tree
Hide file tree
Changes from all 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