Skip to content
Merged
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
26 changes: 17 additions & 9 deletions .vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
const base = process.env.GH_BASE || '/docs/'

// Construct vitepress config object...
import path from 'node:path'
import { dirname, join, resolve } 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 = dirname(fileURLToPath(import.meta.url))
const codeGrSharedScript = readFileSync(resolve(__dirname, './lib/code-groups/shared.js'),'utf-8').replace(/^export\s+/gm, '')
const codeGrRestoreScript = readFileSync(resolve(__dirname, './lib/code-groups/restoreCodeGroupPreferences.js'),'utf-8').replace('__CODE_GROUP_SHARED__', codeGrSharedScript)

const config = defineConfig({

title: 'capire',
Expand Down Expand Up @@ -77,7 +83,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', {}, codeGrRestoreScript]
],

vite: {
Expand Down Expand Up @@ -113,7 +121,7 @@ import rewrites from './rewrites'
config.rewrites = rewrites

// Read menu from local menu.md, but only if we run standalone, not embeded as @external
if (process.cwd() === path.dirname(__dirname)) {
if (process.cwd() === dirname(__dirname)) {
const menu = await Menu.from ('./menu.md', rewrites)
config.themeConfig.sidebar = menu.items
config.themeConfig.nav = menu.navbar
Expand All @@ -140,8 +148,8 @@ if (process.env.VITE_CAPIRE_PREVIEW) {
// Add link to survey
if (process.env.NODE_ENV !== 'production') {
// open in VS Code
const home = path.resolve(__dirname, '..')
let href = 'vscode://' + path.join('file', home, encodeURIComponent('${filePath}')).replaceAll(/\\/g, '/').replace('@external/', '')
const home = resolve(__dirname, '..')
let href = 'vscode://' + join('file', home, encodeURIComponent('${filePath}')).replaceAll(/\\/g, '/').replace('@external/', '')
config.themeConfig.capire.gotoLinks.push({ href, key: 'o', name: 'VS Code' })
}

Expand Down Expand Up @@ -219,13 +227,13 @@ import { promises as fs } from 'node:fs'
import * as cdsMavenSite from './lib/cds-maven-site'
config.buildEnd = async ({ outDir, site }) => {
const sitemapURL = new URL(config.themeConfig.capire.siteURL.href)
sitemapURL.pathname = path.join(sitemapURL.pathname, 'sitemap.xml')
sitemapURL.pathname = join(sitemapURL.pathname, 'sitemap.xml')
console.debug('✓ writing robots.txt with sitemap URL', sitemapURL.href) // eslint-disable-line no-console
const robots = (await fs.readFile(path.resolve(__dirname, 'robots.txt'))).toString().replace('{{SITEMAP}}', sitemapURL.href)
await fs.writeFile(path.join(outDir, 'robots.txt'), robots)
const robots = (await fs.readFile(resolve(__dirname, 'robots.txt'))).toString().replace('{{SITEMAP}}', sitemapURL.href)
await fs.writeFile(join(outDir, 'robots.txt'), robots)

// disabled by default to avoid online fetches during local build
if (process.env.VITE_CAPIRE_EXTRA_ASSETS) {
await cdsMavenSite.copySiteAssets(path.join(outDir, 'java/assets/cds-maven-plugin-site'), site)
await cdsMavenSite.copySiteAssets(join(outDir, 'java/assets/cds-maven-plugin-site'), site)
}
}
145 changes: 145 additions & 0 deletions .vitepress/lib/code-groups/restoreCodeGroupPreferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
;(() => {
// 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

// eslint-disable-next-line no-undef
__CODE_GROUP_SHARED__

// 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
}
}

cleanupOldEntries()

const activeTabs = getActiveTabsByDimension() // eslint-disable-line no-undef
window.__CODE_GROUP_ACTIVE_TABS__ = activeTabs

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

const selectedTab = getBestTab(tabs, activeTabs) // eslint-disable-line no-undef
const selectedIndex = tabs.indexOf(selectedTab)

if (selectedIndex === -1) return

setActiveTab(element, selectedIndex) // eslint-disable-line no-undef
}

const getScrollOffset = () => 134

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)

return codeGroups.length
}

const initialHash = window.location.hash
let hashScrollPending = false

if (initialHash) {
history.replaceState(null, '', window.location.pathname + window.location.search)
hashScrollPending = true
}

const restoreHashScroll = () => {
if (hashScrollPending) {
history.replaceState(null, '', window.location.pathname + window.location.search + initialHash)
requestAnimationFrame(() => {
scrollToHash(initialHash)
hashScrollPending = false
})
}
}

const initialCodeGroupCount = applyToAllCodeGroups()

if (initialCodeGroupCount > 0) restoreHashScroll()

let observer
const stopObserving = () => {
observer?.disconnect()
observer = null
}

if (document.readyState === 'loading' || hashScrollPending) {
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)
restoreHashScroll()
} else if (node.querySelector) {
const codeGroups = node.querySelectorAll('.vp-code-group')
codeGroups.forEach(applyToCodeGroup)

if (codeGroups.length > 0) {
restoreHashScroll()
}
}
}
}
}
})

if (document.documentElement) {
observer.observe(document.documentElement, {
childList: true,
subtree: true
})
}
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
applyToAllCodeGroups()
restoreHashScroll()
stopObserving()
})
} else if (!hashScrollPending) {
stopObserving()
}
})()
156 changes: 156 additions & 0 deletions .vitepress/lib/code-groups/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Shared helpers for code-group preference matching and activation.
*/

export const STORAGE_KEY = 'code-group-active-tabs'

export const TAB_DIMENSIONS = {
runtime: ['Node.js', 'Java'],
os: ['macOS', 'Windows', 'Linux'],
'cloud-runtime': ['Cloud Foundry', 'Kyma']
}

/**
* @param {unknown} value
* @returns {value is Record<string, string>}
*/
export function isTabMap(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

/**
* @param {string} tab1
* @param {string} tab2
*/
export function tabsMatch(tab1, tab2) {
if (tab1 === tab2) return true

const normalized1 = tab1.trim().toLowerCase()
const normalized2 = tab2.trim().toLowerCase()

if (normalized1 && normalized2 && (normalized1.includes(normalized2) || normalized2.includes(normalized1))) {
return true
}

const components1 = tab1.split('/').map(s => s.trim())
const components2 = tab2.split('/').map(s => s.trim())

return components1.some(c1 => components2.includes(c1)) ||
components2.some(c2 => components1.includes(c2))
}

/**
* @param {string} tabLabel
* @returns {string | null}
*/
export function getTabDimension(tabLabel) {
for (const [dimension, tabs] of Object.entries(TAB_DIMENSIONS)) {
for (const dimTab of tabs) {
if (tabsMatch(tabLabel, dimTab)) {
return dimension
}
}
}

return null
}

/**
* @param {Record<string, string> | undefined} [seedTabs]
* @returns {Record<string, string>}
*/
export function getActiveTabsByDimension(seedTabs) {
const activeTabs = {}

if (isTabMap(seedTabs)) {
Object.assign(activeTabs, seedTabs)
}

try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored)
if (Array.isArray(parsed)) {
return activeTabs
}
if (isTabMap(parsed)) {
Object.assign(activeTabs, parsed)
}
}
} catch {
// localStorage might not be available or JSON parsing failed
}

return activeTabs
}

/**
* @param {Record<string, string>} activeTabs
*/
export function saveActiveTabsByDimension(activeTabs) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(activeTabs))
} catch {
// localStorage might not be available
}
}

/**
* @param {string} tabLabel
*/
export function addActiveTab(tabLabel) {
const activeTabs = getActiveTabsByDimension()
const dimension = getTabDimension(tabLabel)

if (dimension) {
activeTabs[dimension] = tabLabel
saveActiveTabsByDimension(activeTabs)
}

return activeTabs
}

/**
* @param {string[]} tabs
* @param {Record<string, string>} [activeTabs]
*/
export function getBestTab(tabs, activeTabs = getActiveTabsByDimension()) {
for (const tab of tabs) {
const dimension = getTabDimension(tab)
if (dimension && activeTabs[dimension]) {
const activeTab = activeTabs[dimension]
if (tab === activeTab || tabsMatch(tab, activeTab)) {
return tab
}
}
}

for (const tab of tabs) {
const dimension = getTabDimension(tab)
if (dimension && TAB_DIMENSIONS[dimension]) {
const defaultTab = TAB_DIMENSIONS[dimension][0]
if (tab === defaultTab || tabsMatch(tab, defaultTab)) {
return tab
}
}
}

return tabs[0]
}

/**
* @param {HTMLElement} element
* @param {number} activeIndex
*/
export function setActiveTab(element, activeIndex) {
const inputs = element.querySelectorAll('.tabs input')
const blocks = element.querySelectorAll('div[class*="language-"], .vp-block')

inputs.forEach((input, index) => {
input.checked = index === activeIndex
})

blocks.forEach((block, index) => {
block.classList.toggle('active', index === activeIndex)
})
}
Loading
Loading