diff --git a/.vitepress/config.js b/.vitepress/config.js index 298b5b49e..168e1d124 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,14 @@ 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', + // 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 69c06fe31..9725bf8f7 100644 --- a/.vitepress/lib/cds-playground/md-live-code.ts +++ b/.vitepress/lib/cds-playground/md-live-code.ts @@ -8,7 +8,7 @@ import { enabled } from '.' * ```cds live * select from Books { title } * ``` - * + * * ```js live * await INSERT.into('Books').entries( * { ID: 2, author_ID: 150, title: 'Eldorado' } @@ -20,14 +20,106 @@ import { enabled } from '.' * example: ```cds live as cql * - readonly: make the code block readonly * example: ```cds live readonly + * - [ModelName]: run query against a named model defined elsewhere on the page + * example: ```cds live [FooBar] + * + * Named model definitions (static, non-live): + * - ```cds [FooBar] — defines a named model; rendered as a plain code block + * - ```cds [FooBarBoo: FooBar] — extends FooBar; combined source is resolved at render time + * - ```cds [FooBar, data: FooData] — attaches a named CSV data set to the model + * + * Named CSV data sets (static, non-live): + * - ```csv [FooData: data/Foo.csv] — defines a named data set; rendered as a plain code block + * - ```csv hidden [FooData: data/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. */ + +const MODEL_ARG_RE = /^\[.+\]$/ + +interface ModelDef { source: string; csvs?: Record } + +function parseBracketKV(inner: string): { name: string; base?: string; data?: string } { + const commaIdx = inner.indexOf(',') + const namePart = commaIdx === -1 ? inner.trim() : inner.slice(0, commaIdx).trim() + const colonIdx = namePart.indexOf(':') + const name = colonIdx === -1 ? namePart : namePart.slice(0, colonIdx).trim() + const base = colonIdx === -1 ? undefined : namePart.slice(colonIdx + 1).trim() + let data: string | undefined + if (commaIdx !== -1) { + const dataMatch = inner.slice(commaIdx + 1).match(/\bdata\s*:\s*(\S+)/) + if (dataMatch) data = dataMatch[1] + } + return { name, base, data } +} + +function buildDataMap(tokens: any[]): Record> { + const result: Record> = {} + for (const token of tokens) { + if (token.type !== 'fence') continue + const bracketMatch = token.info.match(/\[([^\]]+)\]/) + if (!bracketMatch) continue + const [lang] = token.info.slice(0, bracketMatch.index).trim().split(/\s+/) + if (lang !== 'csv') continue + const inner = bracketMatch[1] + const colonIdx = inner.indexOf(':') + if (colonIdx === -1) continue + const name = inner.slice(0, colonIdx).trim() + const path = inner.slice(colonIdx + 1).trim() + 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 + // Match the bracket first since its content may contain spaces (e.g. "[Foo: Bar]"), + // which would otherwise be broken apart by a naive split(' '). + const bracketMatch = token.info.match(/\[([^\]]+)\]/) + if (!bracketMatch) continue + const before = token.info.slice(0, bracketMatch.index).trim().split(/\s+/) + const [lang] = before + if (lang !== 'cds') continue + // Only pick up non-live model definition blocks + if (before.includes('live')) continue + const { name, base, data } = parseBracketKV(bracketMatch[1]) + raw[name] = { source: token.content.trim(), base, csvs: data ? dataMap[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) => { + // Build the model map before any fence is rendered: VitePress's preWrapperPlugin + // strips "[...]" from token.info as a side effect of rendering (for code-group tab + // titles), so scanning tokens lazily would miss brackets on already-rendered fences. + 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(' ') + + // Suppress named CSV data blocks marked hidden — content is captured in the pre-pass and shown as a model tab. + if (language === 'csv' && live === 'hidden' && /\[[^\]]+:[^\]]+\]/.test(info)) return '' + if (live === 'live') { const mdDir = dirname(env.realPath ?? env.path) const filePath = './' + relative(mdDir, join(__dirname, '../../theme/components/cds-playground/LiveCode.vue')) @@ -38,20 +130,28 @@ export function install(md: MarkdownRenderer) { const idx = rest.findIndex(k => k === key) return idx > -1 ? [key, rest.splice(idx+1, 1)[0]] : []; })) - const props = { + + const modelArg = rest.find((p: string) => MODEL_ARG_RE.test(p)) + const modelName = modelArg ? modelArg.slice(1, -1) : null + const modelDef: ModelDef | undefined = modelName ? (env as any)._modelMap[modelName] : undefined + + const props: Record = { language: opts.as ?? language, } + if (modelDef?.source) props.modelSource = md.utils.escapeHtml(modelDef.source) + if (modelDef?.csvs) props.modelData = md.utils.escapeHtml(JSON.stringify(modelDef.csvs)) + const flags = ['readonly'].filter(k => rest.includes(k)) const content = tokens[idx].content.trim() - return ` `${k}="${v}"`)} ${flags.join(' ')}>` + return ` `${k}="${v}"`).join(' ')} ${flags.join(' ')}>` } return fence!(tokens, idx, options, env, ...args) } } function insertScriptSetup(env: MarkdownEnv, imp: string) { - const sfcBlocks = env.sfcBlocks! + const sfcBlocks = env.sfcBlocks! if (!sfcBlocks.scriptSetup) { sfcBlocks.scriptSetup = { content: '', diff --git a/.vitepress/theme/components/cds-playground/LiveCode.vue b/.vitepress/theme/components/cds-playground/LiveCode.vue index 87935b2aa..479897be8 100644 --- a/.vitepress/theme/components/cds-playground/LiveCode.vue +++ b/.vitepress/theme/components/cds-playground/LiveCode.vue @@ -30,7 +30,7 @@