diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dc22b16aa5..ba001f7523 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,11 +2,11 @@ # We start with defining ownership globally and later on can get more granular. # General content -* @smahati @danjoa +* @renejeglinsky @danjoa -node.js/ @smahati @danjoa -java/ @smahati @danjoa -tools/ @chgeo @swaldmann @smahati +node.js/ @smahati @renejeglinsky @danjoa +java/ @smahati @renejeglinsky @danjoa +tools/ @chgeo @swaldmann @renejeglinsky # Infra .github/ @chgeo @swaldmann diff --git a/.github/eslint-plugin/js-rule-stub.md b/.github/eslint-plugin/js-rule-stub.md index c592bc247e..b066406da1 100644 --- a/.github/eslint-plugin/js-rule-stub.md +++ b/.github/eslint-plugin/js-rule-stub.md @@ -18,7 +18,7 @@ This rule was introduced in `@sap/eslint-plugin-cds x.y.z`. DESCRIPTION OF CORRECT EXAMPLE ::: code-group -<<< correct/srv/admin-service.js#snippet{js:line-numbers} [srv/admin-service.js] +<<< correct/srv/admin-service.js{js:line-numbers} [srv/admin-service.js] ::: [...playground.plugins()], + }, css: { preprocessorOptions: { scss: { @@ -117,7 +126,7 @@ export default config // ----------------------------------------------------------------------------------------------- // Add rewrites -import rewrites from './rewrites' +import rewrites from './rewrites.js' config.rewrites = rewrites // Read menu from local menu.md, but only if we run standalone, not embeded as @external @@ -132,8 +141,8 @@ const siteURL = new URL(process.env.SITE_HOSTNAME || 'http://localhost:4173/docs if (!siteURL.pathname.endsWith('/')) siteURL.pathname += '/' config.themeConfig.capire = { versions: { - java_services: '5.0.2', - java_cds4j: '5.0.2', + java_services: '5.1.1', + java_cds4j: '5.1.1', cloud_sec_ams: '3.8.1' }, gotoLinks: [], @@ -203,10 +212,10 @@ config.themeConfig.search = { // Add custom markdown renderers... import { dl } from '@mdit/plugin-dl' -import * as MdLiveCode from './lib/cds-playground/md-live-code' -import * as MdAttrsPropagate from './lib/md-attrs-propagate' -import * as MdDiagramSvg from './lib/md-diagram-svg' -import * as MdTypedModels from './lib/md-typed-models' +import * as MdLiveCode from './lib/cds-playground/md-live-code.ts' +import * as MdAttrsPropagate from './lib/md-attrs-propagate.ts' +import * as MdDiagramSvg from './lib/md-diagram-svg.ts' +import * as MdTypedModels from './lib/md-typed-models.ts' config.markdown.config = md => { MdAttrsPropagate.install(md) @@ -227,7 +236,7 @@ if (process.env.VITE_CAPIRE_EXTRA_ASSETS) { // Add custom buildEnd hook import { promises as fs } from 'node:fs' -import * as cdsMavenSite from './lib/cds-maven-site' +import * as cdsMavenSite from './lib/cds-maven-site.ts' config.buildEnd = async ({ outDir, site }) => { const sitemapURL = new URL(config.themeConfig.capire.siteURL.href) sitemapURL.pathname = join(sitemapURL.pathname, 'sitemap.xml') diff --git a/.vitepress/lib/cds-playground/index.js b/.vitepress/lib/cds-playground/index.js index 54ce2ff6d3..74140336cc 100644 --- a/.vitepress/lib/cds-playground/index.js +++ b/.vitepress/lib/cds-playground/index.js @@ -1,6 +1,9 @@ -import templates from './vite-plugin-templates' +import templates from './vite-plugin-templates.ts' import path from 'path' +import { dirname } from 'path' +import { fileURLToPath } from 'node:url' +const __dirname = dirname(fileURLToPath(import.meta.url)) let enabled = false let plugins = () => [] diff --git a/.vitepress/lib/cds-playground/md-live-code.ts b/.vitepress/lib/cds-playground/md-live-code.ts index 69c06fe31e..97689ef564 100644 --- a/.vitepress/lib/cds-playground/md-live-code.ts +++ b/.vitepress/lib/cds-playground/md-live-code.ts @@ -1,6 +1,9 @@ import { MarkdownRenderer, MarkdownEnv } from 'vitepress' import { dirname, join, relative } from 'path' -import { enabled } from '.' +import { fileURLToPath } from 'node:url' +import { enabled } from './index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) /** * Makes code blocks with "live" in the info string interactive by rendering a component. @@ -8,50 +11,142 @@ import { enabled } from '.' * ```cds live * select from Books { title } * ``` - * + * * ```js live * await INSERT.into('Books').entries( * { ID: 2, author_ID: 150, title: 'Eldorado' } * ) * ``` * - * 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(' ')}>` } } function insertScriptSetup(env: MarkdownEnv, imp: string) { - const sfcBlocks = env.sfcBlocks! + const sfcBlocks = env.sfcBlocks! if (!sfcBlocks.scriptSetup) { sfcBlocks.scriptSetup = { content: '', 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/lib/md-diagram-svg.ts b/.vitepress/lib/md-diagram-svg.ts index 583ec39934..b2a2e47dfb 100644 --- a/.vitepress/lib/md-diagram-svg.ts +++ b/.vitepress/lib/md-diagram-svg.ts @@ -20,7 +20,7 @@ export function install(md: MarkdownRenderer) { } const name = 'svg_' + src.replace('?raw', '').replace(/[^a-zA-Z0-9_]/g, '_') // stable variable name for the imported SVG content - const importPath = src.startsWith('/') && src.startsWith('.') ? src : './' + src + const importPath = (src.startsWith('/') || src.startsWith('.')) ? src : './' + src const sfcBlocks = env.sfcBlocks! if (!sfcBlocks.scriptSetup) { diff --git a/.vitepress/menu.js b/.vitepress/menu.js index 276240dc83..db6d86a756 100755 --- a/.vitepress/menu.js +++ b/.vitepress/menu.js @@ -6,6 +6,7 @@ import { dirname, relative, resolve, join, normalize } from 'node:path' import { existsSync, promises as fs } from 'node:fs' import rewrites from './rewrites.js' +import { fileURLToPath } from 'node:url' const DEBUG = process.env.DEBUG?.match(/\b(menu|all)\b/) ? (...args) => console.debug ('[menu.js] -', ...args) : undefined const EXTERNAL = process.env.VITE_CAPIRE_ENV === 'external' @@ -188,4 +189,4 @@ export class Menu extends MenuItem { // Run the CLI method if invoked from command line -if (typeof __filename === 'undefined') Menu.exec (process.argv.slice(2)) +if (process.argv[1] === fileURLToPath(import.meta.url)) Menu.exec (process.argv.slice(2)) diff --git a/.vitepress/theme/components/GA.vue b/.vitepress/theme/components/GA.vue new file mode 100644 index 0000000000..b54d71e186 --- /dev/null +++ b/.vitepress/theme/components/GA.vue @@ -0,0 +1,7 @@ + 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 @@
- +
+ + +
-
+
-