diff --git a/.vitepress/config.js b/.vitepress/config.js index 4ea7eac3ab..0267cb554e 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -83,7 +83,7 @@ const config = defineConfig({ head: [ ['meta', { name: 'theme-color', content: '#db8b0b' }], - ['meta', { 'http-equiv': 'Content-Security-Policy', content: "script-src 'self' https://www.capire-matomo.cloud.sap 'unsafe-inline' 'unsafe-eval'" }], + ['meta', { 'http-equiv': 'Content-Security-Policy', content: "script-src 'self' https://www.capire-matomo.cloud.sap 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:" }], ['link', { rel: 'icon', href: base+'favicon.ico' }], ['link', { rel: 'shortcut icon', href: base+'favicon.ico' }], ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: base+'logos/cap.png' }], @@ -98,6 +98,15 @@ const config = defineConfig({ build: { chunkSizeWarningLimit: 6000, // chunk for local search index dominates }, + // cds-worker.js is constructed with `type: 'module'`; match that at build time so its + // dynamic import('@sap/cds') is emitted as native ESM instead of an iife require() shim + worker: { + format: 'es', + rolldownOptions: { output: { keepNames: true, } }, + // Vite doesn't reuse the main `plugins` array for worker bundles; without vite-plugin-cds's + // node()/cap() here, the worker build misses their Node built-in shims (e.g. lazify's module.require) + plugins: () => [...playground.plugins()], + }, css: { preprocessorOptions: { scss: { diff --git a/.vitepress/lib/cds-playground/md-live-code.ts b/.vitepress/lib/cds-playground/md-live-code.ts index 022bb006f2..97689ef564 100644 --- a/.vitepress/lib/cds-playground/md-live-code.ts +++ b/.vitepress/lib/cds-playground/md-live-code.ts @@ -18,38 +18,130 @@ const __dirname = dirname(fileURLToPath(import.meta.url)) * ) * ``` * - * Additional options: - * - as : specify the language to execute the code block as (defaults to the language specified before "live") - * example: ```cds live as cql + * Options use key=value pairs; boolean flags are standalone words: + * - model=: run query against a named model defined elsewhere on the page + * example: ```cds live model=FooBar + * - result=: format the result as the given language (e.g. sql) instead of JSON + * example: ```js live result=sql + * - as=: execute the code block as a different language + * example: ```cds live as=cql * - readonly: make the code block readonly * example: ```cds live readonly + * + * Named model definitions (static, non-live): + * - ```cds model=FooBar — defines a named model; rendered as a plain code block + * - ```cds model=FooBarBoo:FooBar — extends FooBar; combined source is resolved at render time + * - ```cds model=FooBar data=FooData — attaches a named CSV data set to the model + * + * Named CSV data sets (static, non-live): + * - ```csv data=FooData:db/Foo.csv — defines a named data set; rendered as a plain code block + * - ```csv hidden data=FooData:db/Foo.csv — same, but suppressed from output (not rendered) + * + * CSV and model blocks may appear anywhere on the page — they are collected in a full token pass + * before any fence is rendered, so forward references work. */ + +interface ModelDef { source: string; csvs?: Record } + +function parseInfoKV(parts: string[]): { flags: Set; kv: Record } { + const flags = new Set() + const kv: Record = {} + for (const part of parts) { + const eq = part.indexOf('=') + if (eq === -1) flags.add(part) + else kv[part.slice(0, eq)] = part.slice(eq + 1) + } + return { flags, kv } +} + +function buildDataMap(tokens: any[]): Record> { + const result: Record> = {} + for (const token of tokens) { + if (token.type !== 'fence') continue + const parts = token.info.trim().split(/\s+/) + if (parts[0] !== 'csv') continue + const { kv } = parseInfoKV(parts.slice(1)) + if (!kv.data) continue + const colonIdx = kv.data.indexOf(':') + if (colonIdx === -1) continue + const name = kv.data.slice(0, colonIdx) + const path = kv.data.slice(colonIdx + 1) + result[name] = { [path]: token.content.trim() } + } + return result +} + +function buildModelMap(tokens: any[], dataMap: Record>): Record { + const raw: Record }> = {} + for (const token of tokens) { + if (token.type !== 'fence') continue + const parts = token.info.trim().split(/\s+/) + if (parts[0] !== 'cds') continue + const { flags, kv } = parseInfoKV(parts.slice(1)) + if (flags.has('live') || !kv.model) continue + const colonIdx = kv.model.indexOf(':') + const name = colonIdx === -1 ? kv.model : kv.model.slice(0, colonIdx) + const base = colonIdx === -1 ? undefined : kv.model.slice(colonIdx + 1) + raw[name] = { source: token.content.trim(), base, csvs: kv.data ? dataMap[kv.data] : undefined } + } + const resolved: Record = {} + function resolve(name: string): ModelDef { + if (name in resolved) return resolved[name] + const def = raw[name] + if (!def) return { source: '' } + const baseDef = def.base ? resolve(def.base) : null + const source = baseDef ? `${baseDef.source}\n${def.source}` : def.source + const csvs = def.csvs ?? baseDef?.csvs + return (resolved[name] = { source, csvs }) + } + Object.keys(raw).forEach(resolve) + return resolved +} + export function install(md: MarkdownRenderer) { if (!enabled) return const fence = md.renderer.rules.fence md.renderer.rules.fence = (tokens, idx, options, env: MarkdownEnv, ...args) => { + if (!(env as any)._modelMap) { + const dataMap = buildDataMap(tokens) + ;(env as any)._modelMap = buildModelMap(tokens, dataMap) + } const { info } = tokens[idx] - const [language, live, ...rest] = info.split(' ') - if (live === 'live') { - const mdDir = dirname(env.realPath ?? env.path) - const filePath = './' + relative(mdDir, join(__dirname, '../../theme/components/cds-playground/LiveCode.vue')) - const imp = `import LiveCode from "${filePath}";` - insertScriptSetup(env, imp) - - const opts = Object.fromEntries(['as'].map(key => { - const idx = rest.findIndex(k => k === key) - return idx > -1 ? [key, rest.splice(idx+1, 1)[0]] : []; - })) - const props = { - language: opts.as ?? language, - } - const flags = ['readonly'].filter(k => rest.includes(k)) - - const content = tokens[idx].content.trim() - return ` `${k}="${v}"`)} ${flags.join(' ')}>` + const hlMatch = info.match(/\{[\d,\-]+\}/) + const highlightSpec = hlMatch?.[0] ?? '' + const infoNormalized = info.replace(/\s*\{[\d,\-]+\}/, '').trim() + const parts = infoNormalized.split(/\s+/).filter(Boolean) + const [language = ''] = parts + const { flags, kv } = parseInfoKV(parts.slice(1)) + + // Suppress hidden CSV data blocks — content is captured in the pre-pass + if (language === 'csv' && flags.has('hidden') && kv.data) return '' + + if (!flags.has('live')) { + return fence!(tokens, idx, options, env, ...args) } - return fence!(tokens, idx, options, env, ...args) + + const mdDir = dirname(env.realPath ?? env.path) + const filePath = './' + relative(mdDir, join(__dirname, '../../theme/components/cds-playground/LiveCode.vue')) + const imp = `import LiveCode from "${filePath}";` + insertScriptSetup(env, imp) + + const modelName = kv.model ?? null + const modelDef: ModelDef | undefined = modelName ? (env as any)._modelMap[modelName] : undefined + + const props: Record = { + language: kv.as ?? language, + } + if (modelDef?.source) props.modelSource = md.utils.escapeHtml(modelDef.source) + if (modelDef?.csvs) props.modelData = md.utils.escapeHtml(JSON.stringify(modelDef.csvs)) + if (highlightSpec) props.highlightLines = highlightSpec + if (kv.result) props.resultKind = kv.result + + const liveFlags = ['readonly'].filter(k => flags.has(k)) + + const content = tokens[idx].content.trim() + return ` `${k}="${v}"`).join(' ')} ${liveFlags.join(' ')}>` } } diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv new file mode 100644 index 0000000000..bac27500cf --- /dev/null +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Addresses.csv @@ -0,0 +1,5 @@ +ID,street,town_ID +1,6 Place des Vosges,1 +2,Church Street,2 +3,North Street,3 +4,King Street,4 \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv index 9b418c17f2..d0f9f0c48c 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Authors.csv @@ -1,5 +1,6 @@ -ID,name,dateOfBirth,placeOfBirth,dateOfDeath,placeOfDeath -101,Emily Brontë,1818-07-30,"Thornton, Yorkshire",1848-12-19,"Haworth, Yorkshire" -107,Charlotte Brontë,1818-04-21,"Thornton, Yorkshire",1855-03-31,"Haworth, Yorkshire" -150,Edgar Allen Poe,1809-01-19,"Boston, Massachusetts",1849-10-07,"Baltimore, Maryland" -170,Richard Carpenter,1929-08-14,"King’s Lynn, Norfolk",2012-02-26,"Hertfordshire, England" +ID,name,dateOfBirth,placeOfBirth,dateOfDeath,placeOfDeath,address_ID +10,Victor Hugo,1802-02-26,"Besançon, Franche-Comté",1885-05-22,"Paris, Île-de-France",1 +101,Emily Brontë,1818-07-30,"Thornton, Yorkshire",1848-12-19,"Haworth, Yorkshire",2 +107,Charlotte Brontë,1818-04-21,"Thornton, Yorkshire",1855-03-31,"Haworth, Yorkshire",2 +150,Edgar Allen Poe,1809-01-19,"Boston, Massachusetts",1849-10-07,"Baltimore, Maryland",3 +170,Richard Carpenter,1929-08-14,"King’s Lynn, Norfolk",2012-02-26,"Hertfordshire, England",4 diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv index d9cc9ee2ee..87ff63081e 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Books.csv @@ -4,3 +4,4 @@ ID,title,descr,author_ID,stock,price,currency_code,genre_ID 251,The Raven,"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.",150,333,13.13,USD,16aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa 252,Eleonora,"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.",150,555,14,USD,15aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa 271,Catweazle,"Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.",170,22,150,JPY,13aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +281,Les Misérables,"Les Misérables (French pronunciation: ​[le mizeʁabl]) is a French historical novel by Victor Hugo, first published in 1862, that is considered one of the greatest novels of the 19th century. In the English-speaking world, the novel is usually referred to by its original French title, although it is sometimes translated as The Miserable Ones, The Wretched, or The Poor Ones. The story examines the nature of law and grace, and expounds upon the history of France, the architecture and urban design of Paris, politics, moral philosophy, antimonarchism, justice, religion, and the types and nature of romantic and familial love.",10,33,20.20,EUR,12aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv new file mode 100644 index 0000000000..882b2840a3 --- /dev/null +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/data/sap.capire.bookshop-Towns.csv @@ -0,0 +1,5 @@ +ID,name,zip,country +1,Paris,75000,France +2,Thornton,NN14,UK +3,Boston,02108,USA +4,King’s Lynn,PE30,UK \ No newline at end of file diff --git a/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds b/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds index 8c510a3599..763744a288 100644 --- a/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds +++ b/.vitepress/lib/cds-playground/templates/bookshop/db/schema.cds @@ -27,6 +27,23 @@ entity Authors { age = years_between(dateOfBirth, coalesce(dateOfDeath, date( $now ))); } +extend Authors with { + address : Association to Addresses; +} + +entity Addresses { + key ID : Integer; + street : String; + town : Association to Towns; +} + +entity Towns { + key ID : Integer; + name : String; + zip : String; + country : String; +} + /** Hierarchically organized Code List for Genres */ entity Genres : cuid, sap.common.CodeList { parent : Association to Genres; diff --git a/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js b/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js index 1268e3823b..f7cd50bad1 100644 --- a/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js +++ b/.vitepress/lib/code-groups/restoreCodeGroupPreferences.js @@ -44,6 +44,10 @@ if (tabs.length === 0) return + // Skip code groups unrelated to the OS/runtime/cloud-runtime dimensions (e.g. file-path + // tabs), otherwise they'd be forced back to their first tab on every re-init. + if (!tabs.some((tab) => getTabDimension(tab))) return // eslint-disable-line no-undef + const selectedTab = getBestTab(tabs, activeTabs) // eslint-disable-line no-undef const selectedIndex = tabs.indexOf(selectedTab) diff --git a/.vitepress/lib/code-groups/useCodeGroupSync.ts b/.vitepress/lib/code-groups/useCodeGroupSync.ts index 8efdbbd3c7..f2a9544e1a 100644 --- a/.vitepress/lib/code-groups/useCodeGroupSync.ts +++ b/.vitepress/lib/code-groups/useCodeGroupSync.ts @@ -12,6 +12,7 @@ import { addActiveTab, getActiveTabsByDimension, getBestTab, + getTabDimension, setActiveTab, tabsMatch } from './shared.js' @@ -47,6 +48,11 @@ function findCodeGroups(): CodeGroupInfo[] { function applyPreference(codeGroup: CodeGroupInfo): void { const { element, tabs } = codeGroup + + // Skip code groups unrelated to the OS/runtime/cloud-runtime dimensions (e.g. file-path + // tabs), otherwise they'd be forced back to their first tab on every re-init. + if (!tabs.some((tab) => getTabDimension(tab))) return + const selectedTab = getBestTab( tabs, getActiveTabsByDimension((window as any).__CODE_GROUP_ACTIVE_TABS__) @@ -88,6 +94,12 @@ function handleDocumentClick(event: Event): void { const tabLabel = (label.textContent || '').trim() if (!tabLabel) return + // Only tabs that belong to a recognized dimension (OS/runtime/cloud-runtime) should be + // synced across the page. Otherwise unrelated code groups sharing a "/" path segment + // (e.g. "srv/admin-service.cds" vs. "srv/cat-service.cds") get fuzzy-matched and forced + // into the wrong active tab. + if (!getTabDimension(tabLabel)) return + const clickedRect = label.getBoundingClientRect() syncTabs(tabLabel) diff --git a/.vitepress/theme/components/cds-playground/LiveCode.vue b/.vitepress/theme/components/cds-playground/LiveCode.vue index d10b51179f..582a4b9d61 100644 --- a/.vitepress/theme/components/cds-playground/LiveCode.vue +++ b/.vitepress/theme/components/cds-playground/LiveCode.vue @@ -5,7 +5,7 @@
{{ props.language === 'cds'? 'cql' : props.language }} - +
@@ -14,25 +14,36 @@
- +
+ + +
-
+
-