Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6113ed2
CQL w/ live snippets
chgeo Jul 31, 2026
07e19b5
Multimodel support in live queries
chgeo Aug 4, 2026
f9d673b
Show model next to query
chgeo Aug 4, 2026
4124ca8
Cosmetics
chgeo Aug 4, 2026
dad2bf1
Button styles
chgeo Aug 5, 2026
5261cd9
Use cql instead of cds lang type
chgeo Aug 5, 2026
50801df
Merge the 2 tab groups
chgeo Aug 6, 2026
e16b6e8
Name models
chgeo Aug 7, 2026
d3dbc9b
Worker isolation
chgeo Aug 10, 2026
df3b980
Show sample data
chgeo Aug 11, 2026
3b2ea8d
Fix loading on Safari
chgeo Aug 12, 2026
3b78ef1
CQL JS live
chgeo Aug 4, 2026
07e7789
fix lint
chgeo Aug 12, 2026
16fbf59
More JS examples
chgeo Aug 13, 2026
51435fa
Merge branch 'main' into cql-live-js
chgeo Aug 17, 2026
20161f4
Simplify md syntax to just key=value pairs
chgeo Aug 17, 2026
2acabb2
Merge remote-tracking branch 'origin/main'
chgeo Aug 19, 2026
bb85177
Merge remote-tracking branch 'origin/main'
chgeo Aug 19, 2026
e38bf9e
Run bookshop model in worker too
chgeo Aug 19, 2026
14bd588
always async + linked csn
Akatuoro Aug 19, 2026
17ca463
lint
Akatuoro Aug 19, 2026
60c0dc1
fix async for single declared variables
Akatuoro Aug 19, 2026
b3aa55a
Allow tabbing outside the editor
chgeo Aug 20, 2026
f5ca673
Enable snippets for `SQL Injection` section
chgeo Aug 20, 2026
2fcd198
Merge remote-tracking branch 'origin/main'
chgeo Aug 20, 2026
508b64c
replace patch with rolldown option: keepNames
Akatuoro Aug 20, 2026
cc61d5d
Merge branch 'cql-live-js' of https://github.com/cap-js/docs into cql…
Akatuoro Aug 20, 2026
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
11 changes: 10 additions & 1 deletion .vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
Expand All @@ -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: {
Expand Down
136 changes: 114 additions & 22 deletions .vitepress/lib/cds-playground/md-live-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,130 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
* )
* ```
*
* Additional options:
* - as <lang>: 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=<name>: run query against a named model defined elsewhere on the page
* example: ```cds live model=FooBar
* - result=<lang>: format the result as the given language (e.g. sql) instead of JSON
* example: ```js live result=sql
* - as=<lang>: 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<string, string> }

function parseInfoKV(parts: string[]): { flags: Set<string>; kv: Record<string, string> } {
const flags = new Set<string>()
const kv: Record<string, string> = {}
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<string, Record<string, string>> {
const result: Record<string, Record<string, string>> = {}
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<string, Record<string, string>>): Record<string, ModelDef> {
const raw: Record<string, { source: string; base?: string; csvs?: Record<string, string> }> = {}
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<string, ModelDef> = {}
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 `<LiveCode initialQuery="${md.utils.escapeHtml(content)}" ${Object.entries(props).map(([k, v]) => `${k}="${v}"`)} ${flags.join(' ')}></LiveCode>`
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<string, string> = {
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 `<LiveCode initialQuery="${md.utils.escapeHtml(content)}" ${Object.entries(props).map(([k, v]) => `${k}="${v}"`).join(' ')} ${liveFlags.join(' ')}></LiveCode>`
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions .vitepress/lib/cds-playground/templates/bookshop/db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions .vitepress/lib/code-groups/restoreCodeGroupPreferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 12 additions & 0 deletions .vitepress/lib/code-groups/useCodeGroupSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
addActiveTab,
getActiveTabsByDimension,
getBestTab,
getTabDimension,
setActiveTab,
tabsMatch
} from './shared.js'
Expand Down Expand Up @@ -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__)
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading