@oml/cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +28 -21
  2. package/out/{auth.d.ts → auth/auth.d.ts} +7 -0
  3. package/out/{auth.js → auth/auth.js} +42 -3
  4. package/out/auth/auth.js.map +1 -0
  5. package/out/{platform-constants.js → auth/constants.js} +1 -1
  6. package/out/auth/constants.js.map +1 -0
  7. package/out/{platform.d.ts → auth/platform.d.ts} +5 -7
  8. package/out/{platform.js → auth/platform.js} +13 -19
  9. package/out/auth/platform.js.map +1 -0
  10. package/out/cli.d.ts +6 -0
  11. package/out/cli.js +175 -62
  12. package/out/cli.js.map +1 -1
  13. package/out/commands/export.d.ts +8 -0
  14. package/out/commands/export.js +27 -0
  15. package/out/commands/export.js.map +1 -0
  16. package/out/commands/lint.d.ts +22 -2
  17. package/out/commands/lint.js +64 -18
  18. package/out/commands/lint.js.map +1 -1
  19. package/out/commands/reason.d.ts +2 -9
  20. package/out/commands/reason.js +52 -48
  21. package/out/commands/reason.js.map +1 -1
  22. package/out/commands/render.d.ts +1 -9
  23. package/out/commands/render.js +15 -726
  24. package/out/commands/render.js.map +1 -1
  25. package/out/commands/server/actions.d.ts +22 -0
  26. package/out/commands/server/actions.js +394 -0
  27. package/out/commands/server/actions.js.map +1 -0
  28. package/out/commands/server/require.d.ts +1 -0
  29. package/out/commands/server/require.js +89 -0
  30. package/out/commands/server/require.js.map +1 -0
  31. package/out/commands/server/rest.d.ts +2 -0
  32. package/out/commands/server/rest.js +117 -0
  33. package/out/commands/server/rest.js.map +1 -0
  34. package/out/commands/validate.d.ts +3 -3
  35. package/out/commands/validate.js +35 -171
  36. package/out/commands/validate.js.map +1 -1
  37. package/package.json +5 -7
  38. package/src/{auth.ts → auth/auth.ts} +54 -3
  39. package/src/{platform.ts → auth/platform.ts} +13 -19
  40. package/src/cli.ts +207 -63
  41. package/src/commands/export.ts +54 -0
  42. package/src/commands/lint.ts +88 -18
  43. package/src/commands/reason.ts +69 -56
  44. package/src/commands/render.ts +23 -995
  45. package/src/commands/server/actions.ts +480 -0
  46. package/src/commands/server/require.ts +99 -0
  47. package/src/commands/server/rest.ts +135 -0
  48. package/src/commands/validate.ts +46 -207
  49. package/out/auth.js.map +0 -1
  50. package/out/backend/backend-types.d.ts +0 -21
  51. package/out/backend/backend-types.js +0 -3
  52. package/out/backend/backend-types.js.map +0 -1
  53. package/out/backend/create-backend.d.ts +0 -2
  54. package/out/backend/create-backend.js +0 -6
  55. package/out/backend/create-backend.js.map +0 -1
  56. package/out/backend/direct-backend.d.ts +0 -20
  57. package/out/backend/direct-backend.js +0 -150
  58. package/out/backend/direct-backend.js.map +0 -1
  59. package/out/backend/reasoned-output.d.ts +0 -38
  60. package/out/backend/reasoned-output.js +0 -568
  61. package/out/backend/reasoned-output.js.map +0 -1
  62. package/out/commands/closure.d.ts +0 -33
  63. package/out/commands/closure.js +0 -537
  64. package/out/commands/closure.js.map +0 -1
  65. package/out/commands/compile.d.ts +0 -11
  66. package/out/commands/compile.js +0 -63
  67. package/out/commands/compile.js.map +0 -1
  68. package/out/platform-constants.js.map +0 -1
  69. package/out/platform.js.map +0 -1
  70. package/src/backend/backend-types.ts +0 -27
  71. package/src/backend/create-backend.ts +0 -8
  72. package/src/backend/direct-backend.ts +0 -169
  73. package/src/backend/reasoned-output.ts +0 -697
  74. package/src/commands/closure.ts +0 -624
  75. package/src/commands/compile.ts +0 -88
  76. /package/out/{platform-constants.d.ts → auth/constants.d.ts} +0 -0
  77. /package/src/{platform-constants.ts → auth/constants.ts} +0 -0
@@ -1,1010 +1,38 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
3
  import chalk from 'chalk';
4
- import * as url from 'node:url';
5
- import * as fs from 'node:fs/promises';
6
4
  import * as path from 'node:path';
7
- import { createRequire } from 'node:module';
8
- import { createHash } from 'node:crypto';
9
- import {
10
- MarkdownHandlerRegistry,
11
- MarkdownPreviewRuntime,
12
- extractLeadingFrontMatter,
13
- stripLeadingFrontMatter,
14
- type MdBlockExecutionResult,
15
- type MdBlockKind,
16
- type MdExecutableBlock
17
- } from '@oml/markdown';
18
- import { runWithIriLabelSnapshot } from '@oml/markdown/renderers';
19
- import { STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE, STATIC_MARKDOWN_RUNTIME_CSS } from '@oml/markdown/static';
20
- import { normalizeFormatExtension } from '../backend/reasoned-output.js';
21
- import { createBackend } from '../backend/create-backend.js';
22
- import type { CliBackend } from '../backend/backend-types.js';
23
5
  import { failCli } from '../cli-error.js';
24
6
  import { formatDuration } from '../util.js';
25
- import type { ReasonOptions } from './reason.js';
26
- import { reasonAction } from './reason.js';
27
- import { resolveCompileOutputRoot, resolveCompileWorkspaceRoot } from './compile.js';
28
- const markdownRuntime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
29
- const SUPPORTED_MD_BLOCK_KINDS = new Set<MdBlockKind>([
30
- 'table',
31
- 'tree',
32
- 'graph',
33
- 'chart',
34
- 'diagram',
35
- 'list',
36
- 'text',
37
- 'matrix',
38
- 'table-editor'
39
- ]);
40
- const LINK_ATTRIBUTE_KEYS = new Set(['href', 'src', 'xlinkHref', 'xlink:href']);
7
+ import { restPost } from './server/rest.js';
8
+ import { lintAction } from './lint.js';
41
9
 
42
10
  export type RenderOptions = {
43
- workspace?: string,
44
11
  md: string,
45
12
  web: string,
46
- owl?: string,
47
- format?: string,
48
13
  context?: string,
49
- clean?: boolean,
50
- only?: boolean,
51
- pretty?: boolean,
52
- uniqueNamesAssumption?: boolean,
53
- explanations?: boolean,
54
- profile?: boolean
14
+ authToken?: string
55
15
  };
56
16
 
57
- export const renderAction = async (
58
- opts: RenderOptions
59
- ): Promise<void> => {
60
- const workspaceRoot = resolveCompileWorkspaceRoot(opts.workspace);
61
- const owlOutputRoot = resolveCompileOutputRoot(workspaceRoot, opts.owl);
62
- const rdfFormat = normalizeFormatExtension(opts.format);
63
- if (!opts.only) {
64
- const reasonOpts: ReasonOptions = {
65
- workspace: workspaceRoot,
66
- owl: owlOutputRoot,
67
- format: rdfFormat,
68
- clean: opts.clean,
69
- pretty: opts.pretty,
70
- only: false,
71
- checkOnly: false,
72
- uniqueNamesAssumption: opts.uniqueNamesAssumption,
73
- explanations: opts.explanations,
74
- profile: opts.profile,
75
- };
76
- await reasonAction(reasonOpts);
77
- }
78
- const backend = createBackend({
79
- owlOutputRoot,
80
- rdfFormat,
81
- });
82
- try {
83
- const renderStartedAt = Date.now();
84
- const markdownRoot = path.resolve(workspaceRoot, opts.md);
85
- const output = path.resolve(opts.web);
86
- const workspaceStat = await fs.stat(workspaceRoot).catch(() => undefined);
87
- if (!workspaceStat || !workspaceStat.isDirectory()) {
88
- failCli(chalk.red(`Workspace folder does not exist: ${workspaceRoot}`));
89
- }
90
- const markdownStat = await fs.stat(markdownRoot).catch(() => undefined);
91
- if (!markdownStat || !markdownStat.isDirectory()) {
92
- failCli(chalk.red(`Markdown folder does not exist: ${markdownRoot}`));
93
- }
94
-
95
- if (isSameOrDescendant(markdownRoot, output)) {
96
- failCli(chalk.red('Web output folder cannot be inside the markdown folder.'));
97
- }
98
-
99
- if (opts.clean) {
100
- await fs.rm(output, { recursive: true, force: true });
101
- }
102
-
103
- const renderableMarkdownFiles = await discoverRenderableMarkdownFiles(markdownRoot, output);
104
- const discoveredWorkspaceMarkdownFiles = await discoverMarkdownFiles(workspaceRoot, output);
105
- const templateFiles = await discoverTemplateMarkdownFiles(discoveredWorkspaceMarkdownFiles);
106
- const templateCatalog = buildTemplateCatalog(templateFiles);
107
- const explicitContextModelUri = resolveModelUriOption(opts.context, workspaceRoot);
108
- const templateGenerationEnabled = explicitContextModelUri !== undefined;
109
- const memberLabelSnapshot = await backend.getWorkspaceMemberLabelSnapshot(workspaceRoot);
110
- const wikiPageIndex = buildWikiPageIndex(renderableMarkdownFiles, markdownRoot, output);
111
- const generatedInstancePages = new Set<string>();
112
- const instancePageByIri = new Map<string, string>();
113
- const attemptedInstanceIris = new Set<string>();
114
- const workspaceAssetFiles = new Set<string>();
115
- const staticAssets = await writeStaticAssets(output);
116
- let blockArtifactFiles = 0;
117
- for (const file of renderableMarkdownFiles) {
118
- const relative = path.relative(markdownRoot, file);
119
- const outputFile = path.join(output, relative.replace(/\.md$/i, '.html'));
120
- const rendered = await renderMarkdownFile(
121
- backend,
122
- workspaceRoot,
123
- markdownRoot,
124
- output,
125
- file,
126
- outputFile,
127
- staticAssets.runtimeScriptFile,
128
- staticAssets.stylesheetFile,
129
- templateGenerationEnabled,
130
- explicitContextModelUri,
131
- memberLabelSnapshot,
132
- wikiPageIndex,
133
- templateCatalog,
134
- generatedInstancePages,
135
- instancePageByIri,
136
- attemptedInstanceIris
137
- );
138
- const referencedWorkspaceAssets = rendered.workspaceAssets;
139
- for (const asset of referencedWorkspaceAssets) {
140
- workspaceAssetFiles.add(asset);
141
- }
142
- blockArtifactFiles += rendered.blockArtifactFiles;
143
- }
144
- for (const file of workspaceAssetFiles) {
145
- const workspaceRelative = path.relative(workspaceRoot, file);
146
- if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)) {
147
- continue;
148
- }
149
- if (workspaceRelative.toLowerCase().endsWith('.md')) {
150
- continue;
151
- }
152
- const markdownRelative = path.relative(markdownRoot, file);
153
- const isInsideMarkdownRoot = !markdownRelative.startsWith('..') && !path.isAbsolute(markdownRelative);
154
- const outputRelative = isInsideMarkdownRoot ? markdownRelative : workspaceRelative;
155
- const target = path.join(output, outputRelative);
156
- await fs.mkdir(path.dirname(target), { recursive: true });
157
- await fs.copyFile(file, target);
158
- }
159
-
160
- if (renderableMarkdownFiles.length === 0) {
161
- console.log(chalk.yellow(`No markdown files found under ${markdownRoot}.`));
162
- }
163
- console.log(chalk.green(`render: ${renderableMarkdownFiles.length} markdown file(s) rendered in ${path.relative(process.cwd(), output) || output} [${formatDuration(Date.now() - renderStartedAt)}]`));
164
- } finally {
165
- await backend.dispose();
166
- }
167
- };
168
-
169
- function isSameOrDescendant(parent: string, candidate: string): boolean {
170
- const relative = path.relative(parent, candidate);
171
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
172
- }
173
-
174
- type TemplateFile = {
175
- filePath: string;
176
- typeIri: string;
177
- priority: number;
178
- };
179
-
180
- type TemplateCatalog = {
181
- byType: Map<string, TemplateFile[]>;
182
- };
183
-
184
- async function discoverTemplateMarkdownFiles(markdownFiles: ReadonlyArray<string>): Promise<TemplateFile[]> {
185
- const templateFiles: TemplateFile[] = [];
186
- for (const filePath of markdownFiles) {
187
- const content = await fs.readFile(filePath, 'utf-8');
188
- const frontMatter = extractLeadingFrontMatter(content);
189
- const rawType = frontMatter?.data?.type;
190
- if (typeof rawType === 'string' && rawType.trim().length > 0) {
191
- const priorityValue = frontMatter?.data?.priority;
192
- const priority = typeof priorityValue === 'number'
193
- ? priorityValue
194
- : Number.parseInt(String(priorityValue ?? '0'), 10);
195
- templateFiles.push({
196
- filePath,
197
- typeIri: normalizeTemplateTypeIri(rawType),
198
- priority: Number.isFinite(priority) ? priority : 0
199
- });
200
- }
201
- }
202
- return templateFiles;
203
- }
204
-
205
- function normalizeTemplateTypeIri(value: string): string {
206
- return value.trim().replace(/^<|>$/g, '');
207
- }
208
-
209
- function buildTemplateCatalog(templateFiles: ReadonlyArray<TemplateFile>): TemplateCatalog {
210
- const byType = new Map<string, TemplateFile[]>();
211
- for (const template of templateFiles) {
212
- const list = byType.get(template.typeIri) ?? [];
213
- list.push(template);
214
- byType.set(template.typeIri, list);
215
- }
216
- for (const list of byType.values()) {
217
- list.sort((left, right) => (left.priority - right.priority) || left.filePath.localeCompare(right.filePath));
218
- }
219
- return { byType };
220
- }
221
-
222
- function resolveModelUriOption(value: string | undefined, workspaceRoot: string): string | undefined {
223
- if (!value) {
224
- return undefined;
225
- }
226
- const trimmed = value.trim();
227
- if (!trimmed) {
228
- return undefined;
229
- }
230
- if (trimmed.startsWith('workspace:/')) {
231
- const resolved = resolveWorkspacePath(workspaceRoot, trimmed);
232
- return resolved ? url.pathToFileURL(resolved).toString() : undefined;
233
- }
234
- if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
235
- return trimmed;
236
- }
237
- const absolute = path.isAbsolute(trimmed)
238
- ? trimmed
239
- : path.resolve(workspaceRoot, trimmed);
240
- return url.pathToFileURL(absolute).toString();
241
- }
242
-
243
- async function discoverRenderableMarkdownFiles(root: string, outputRoot: string): Promise<string[]> {
244
- const markdownFiles = await discoverMarkdownFiles(root, outputRoot);
245
- const renderableFiles: string[] = [];
246
- for (const filePath of markdownFiles) {
247
- const content = await fs.readFile(filePath, 'utf-8');
248
- const frontMatter = extractLeadingFrontMatter(content);
249
- const rawType = frontMatter?.data?.type;
250
- if (typeof rawType === 'string' && rawType.trim().length > 0) {
251
- continue;
252
- }
253
- renderableFiles.push(filePath);
254
- }
255
- return renderableFiles;
256
- }
257
-
258
- async function discoverMarkdownFiles(root: string, outputRoot: string): Promise<string[]> {
259
- const entries = await fs.readdir(root, { withFileTypes: true });
260
- const markdownFiles: string[] = [];
261
- for (const entry of entries) {
262
- const fullPath = path.join(root, entry.name);
263
- if (entry.isDirectory()) {
264
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
265
- continue;
266
- }
267
- if (isSameOrDescendant(outputRoot, fullPath)) {
268
- continue;
269
- }
270
- markdownFiles.push(...await discoverMarkdownFiles(fullPath, outputRoot));
271
- continue;
272
- }
273
- if (!entry.isFile()) {
274
- continue;
275
- }
276
- if (entry.name.toLowerCase().endsWith('.md')) {
277
- markdownFiles.push(fullPath);
278
- }
279
- }
280
- return markdownFiles;
281
- }
282
-
283
- async function renderMarkdownFile(
284
- backend: CliBackend,
285
- workspaceRoot: string,
286
- inputRoot: string,
287
- outputRoot: string,
288
- inputFile: string,
289
- outputFile: string,
290
- runtimeScriptFile: string,
291
- stylesheetFile: string,
292
- templateGenerationEnabled: boolean,
293
- defaultContextModelUri: string | undefined,
294
- memberLabelSnapshot: Record<string, string>,
295
- wikiPageIndex: ReadonlyMap<string, string>,
296
- templateCatalog: TemplateCatalog,
297
- generatedInstancePages: Set<string>,
298
- instancePageByIri: Map<string, string>,
299
- attemptedInstanceIris: Set<string>,
300
- currentPageStack: Set<string> = new Set<string>(),
301
- explicitMarkdown?: string,
302
- forcedModelUri?: string
303
- ): Promise<{ workspaceAssets: Set<string>; blockArtifactFiles: number }> {
304
- const markdown = explicitMarkdown ?? await fs.readFile(inputFile, 'utf-8');
305
- const inlineContextUri = extractLeadingFrontMatter(markdown)?.contextUri;
306
- const modelUri = forcedModelUri ?? resolveContextModelUri(inputFile, inlineContextUri, workspaceRoot) ?? defaultContextModelUri;
307
- const prepared = runWithIriLabelSnapshot(memberLabelSnapshot, () => markdownRuntime.prepare(markdown));
308
- const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
309
- workspaceRoot,
310
- inputRoot,
311
- inputFile,
312
- outputRoot,
313
- outputFile,
314
- });
315
- const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
316
- const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options] as const));
317
- const blockResults = executableBlocks.length === 0
318
- ? []
319
- : (await backend.executeMarkdownBlocks({
320
- documentUri: url.pathToFileURL(path.resolve(inputFile)).toString(),
321
- modelUri,
322
- blocks: executableBlocks
323
- }, workspaceRoot)).results;
324
- const renderedBlockResults: RenderedBlockResult[] = blockResults.map((result) => ({
325
- ...result,
326
- options: optionsByBlockId.get(result.blockId)
327
- }));
328
- const blockRewriteResult = rewriteBlockResultLinks(renderedBlockResults, {
329
- workspaceRoot,
330
- inputRoot,
331
- inputFile,
332
- outputRoot,
333
- outputFile,
334
- });
335
- const blockArtifacts = await writeBlockArtifacts(outputFile, blockRewriteResult.results);
336
- const pageWikilinkIris = collectWikilinkIrisFromHtml(rewriteResult.html);
337
- const iriAliasHrefByIri = templateGenerationEnabled && modelUri
338
- ? await generateInstanceTemplatePages(
339
- backend,
340
- workspaceRoot,
341
- inputRoot,
342
- outputRoot,
343
- outputFile,
344
- runtimeScriptFile,
345
- stylesheetFile,
346
- templateGenerationEnabled,
347
- modelUri,
348
- memberLabelSnapshot,
349
- pageWikilinkIris,
350
- renderedBlockResults,
351
- wikiPageIndex,
352
- templateCatalog,
353
- generatedInstancePages,
354
- instancePageByIri,
355
- attemptedInstanceIris,
356
- currentPageStack
357
- )
358
- : {};
359
- const runtimeScriptRelative = toRelativeWebPath(path.dirname(outputFile), runtimeScriptFile);
360
- const stylesheetRelative = toRelativeWebPath(path.dirname(outputFile), stylesheetFile);
361
- const wikiLinkHrefByKey = buildWikiLinkHrefMapForPage(wikiPageIndex, outputFile);
362
- const resolvedInstanceLinks = Object.fromEntries(
363
- [...instancePageByIri.entries()].map(([iri, absolute]) => [iri, toRelativeWebPath(path.dirname(outputFile), absolute)])
364
- );
365
- const html = wrapHtml(
366
- rewriteResult.html,
367
- runtimeScriptRelative,
368
- stylesheetRelative,
369
- blockArtifacts.manifest,
370
- blockRewriteResult.results,
371
- wikiLinkHrefByKey,
372
- { ...resolvedInstanceLinks, ...iriAliasHrefByIri },
373
- templateGenerationEnabled && Boolean(modelUri),
374
- memberLabelSnapshot
375
- );
376
- await fs.mkdir(path.dirname(outputFile), { recursive: true });
377
- await fs.writeFile(outputFile, html, 'utf-8');
378
- const workspaceAssets = new Set<string>(rewriteResult.workspaceAssets);
379
- for (const asset of blockRewriteResult.workspaceAssets) {
380
- workspaceAssets.add(asset);
381
- }
382
- return { workspaceAssets, blockArtifactFiles: blockArtifacts.count };
383
- }
384
-
385
- type RenderedBlockResult = MdBlockExecutionResult & {
386
- options?: Record<string, unknown>;
387
- };
388
-
389
- function toExecutableBlocks(codeBlocks: ReadonlyArray<{
390
- id: string;
391
- language: string;
392
- content: string;
393
- meta?: string;
394
- options?: Record<string, unknown>;
395
- lineStart: number;
396
- lineEnd: number;
397
- }>): MdExecutableBlock[] {
398
- const executable: MdExecutableBlock[] = [];
399
- for (const block of codeBlocks) {
400
- const kind = toMdBlockKind(block.language);
401
- if (!kind) {
402
- continue;
403
- }
404
- executable.push({
405
- id: block.id,
406
- kind,
407
- source: block.content,
408
- meta: block.meta,
409
- options: block.options,
410
- lineStart: block.lineStart,
411
- lineEnd: block.lineEnd
412
- });
413
- }
414
- return executable;
415
- }
416
-
417
- function toMdBlockKind(language: string): MdBlockKind | undefined {
418
- return SUPPORTED_MD_BLOCK_KINDS.has(language as MdBlockKind) ? (language as MdBlockKind) : undefined;
419
- }
420
-
421
- function resolveContextModelUri(documentFile: string, contextUri: string | undefined, workspaceRoot: string): string | undefined {
422
- if (!contextUri) {
423
- return undefined;
424
- }
425
- const trimmed = contextUri.trim();
426
- if (!trimmed) {
427
- return undefined;
428
- }
429
- if (trimmed.startsWith('workspace:/')) {
430
- const resolved = resolveWorkspacePath(workspaceRoot, trimmed);
431
- return resolved ? url.pathToFileURL(resolved).toString() : undefined;
432
- }
433
- if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
434
- return trimmed;
435
- }
436
- if (trimmed.startsWith('/')) {
437
- return url.pathToFileURL(path.resolve(workspaceRoot, trimmed.replace(/^\/+/, ''))).toString();
438
- }
439
- return url.pathToFileURL(path.resolve(path.dirname(documentFile), trimmed)).toString();
440
- }
441
-
442
- async function writeStaticAssets(outputRoot: string): Promise<{
443
- runtimeScriptFile: string;
444
- stylesheetFile: string;
445
- }> {
446
- const runtimeBundle = await loadStaticRuntimeBundle();
447
- const sourceStylesheet = await loadCodeBlockStylesheet();
448
- const mergedStylesheet = `${sourceStylesheet}\n\n${STATIC_MARKDOWN_RUNTIME_CSS}\n`;
449
- const runtimeVersion = createHash('sha1').update(runtimeBundle).digest('hex').slice(0, 12);
450
- const stylesheetVersion = createHash('sha1').update(mergedStylesheet).digest('hex').slice(0, 12);
451
- const runtimeFile = path.join(outputRoot, 'assets', `markdown-static-${runtimeVersion}.js`);
452
- const stylesheetFile = path.join(outputRoot, 'assets', `markdown-webview-${stylesheetVersion}.css`);
453
- await fs.mkdir(path.dirname(runtimeFile), { recursive: true });
454
- await fs.writeFile(runtimeFile, runtimeBundle, 'utf-8');
455
- await fs.writeFile(stylesheetFile, mergedStylesheet, 'utf-8');
456
- return { runtimeScriptFile: runtimeFile, stylesheetFile };
457
- }
458
-
459
- async function loadStaticRuntimeBundle(): Promise<string> {
460
- const require = createRequire(import.meta.url);
461
- const staticEntry = require.resolve('@oml/markdown/static');
462
- const bundlePath = path.join(path.dirname(staticEntry), STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE);
463
- try {
464
- return await fs.readFile(bundlePath, 'utf-8');
465
- } catch {
466
- throw new Error(
467
- `Unable to load markdown static runtime bundle at '${bundlePath}'. `
468
- + 'Run the markdown package build to generate it.'
469
- );
470
- }
471
- }
472
-
473
- async function loadCodeBlockStylesheet(): Promise<string> {
474
- const require = createRequire(import.meta.url);
475
- const staticEntry = require.resolve('@oml/markdown/static');
476
- const stylesheetPath = path.resolve(path.dirname(staticEntry), '..', '..', 'src', 'static', 'markdown-webview.css');
477
- try {
478
- return await fs.readFile(stylesheetPath, 'utf-8');
479
- } catch {
480
- throw new Error(
481
- `Unable to load markdown webview stylesheet at '${stylesheetPath}'. `
482
- + 'The installed @oml/markdown package is missing a required static asset.'
483
- );
484
- }
485
- }
486
-
487
- async function writeBlockArtifacts(outputFile: string, results: ReadonlyArray<RenderedBlockResult>): Promise<{
488
- count: number;
489
- manifest: Array<{ blockId: string; path: string }>;
490
- }> {
491
- if (results.length === 0) {
492
- return { count: 0, manifest: [] };
493
- }
494
- const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
495
- const blockDir = path.join(path.dirname(outputFile), blockDirName);
496
- await fs.mkdir(blockDir, { recursive: true });
497
- const manifest: Array<{ blockId: string; path: string }> = [];
498
- for (const result of results) {
499
- const safeId = sanitizeBlockId(result.blockId);
500
- const fileName = `${safeId}.json`;
501
- const filePath = path.join(blockDir, fileName);
502
- await fs.writeFile(filePath, JSON.stringify(result, null, 2), 'utf-8');
503
- manifest.push({
504
- blockId: result.blockId,
505
- path: `./${blockDirName}/${fileName}`
506
- });
507
- }
508
- return { count: results.length, manifest };
509
- }
510
-
511
- function rewriteBlockResultLinks(
512
- results: ReadonlyArray<RenderedBlockResult>,
513
- context: {
514
- workspaceRoot: string;
515
- inputRoot: string;
516
- inputFile: string;
517
- outputRoot: string;
518
- outputFile: string;
519
- }
520
- ): {
521
- results: RenderedBlockResult[];
522
- workspaceAssets: Set<string>;
523
- } {
524
- const workspaceAssets = new Set<string>();
525
- const rewrittenResults = results.map((result) => rewriteLinkValue(result, context, workspaceAssets));
526
- return { results: rewrittenResults, workspaceAssets };
527
- }
528
-
529
- function rewriteLinkValue<T>(
530
- value: T,
531
- context: {
532
- workspaceRoot: string;
533
- inputRoot: string;
534
- inputFile: string;
535
- outputRoot: string;
536
- outputFile: string;
537
- },
538
- workspaceAssets: Set<string>,
539
- parentKey?: string
540
- ): T {
541
- if (typeof value === 'string') {
542
- if (parentKey && LINK_ATTRIBUTE_KEYS.has(parentKey)) {
543
- return rewriteHref(value, context, workspaceAssets) as T;
544
- }
545
- return value;
546
- }
547
- if (Array.isArray(value)) {
548
- return value.map((entry) => rewriteLinkValue(entry, context, workspaceAssets)) as T;
549
- }
550
- if (!value || typeof value !== 'object') {
551
- return value;
552
- }
553
- const source = value as Record<string, unknown>;
554
- const rewritten: Record<string, unknown> = {};
555
- for (const [key, entry] of Object.entries(source)) {
556
- rewritten[key] = rewriteLinkValue(entry, context, workspaceAssets, key);
557
- }
558
- return rewritten as T;
559
- }
560
-
561
- function sanitizeBlockId(value: string): string {
562
- const trimmed = value.trim();
563
- if (!trimmed) {
564
- return 'block';
565
- }
566
- return trimmed.replace(/[^a-zA-Z0-9._-]/g, '_');
567
- }
568
-
569
- function rewriteRenderedLinks(
570
- content: string,
571
- context: {
572
- workspaceRoot: string;
573
- inputRoot: string;
574
- inputFile: string;
575
- outputRoot: string;
576
- outputFile: string;
577
- }
578
- ): {
579
- html: string;
580
- workspaceAssets: Set<string>;
581
- } {
582
- const workspaceAssets = new Set<string>();
583
- const html = content.replace(/(\b(?:href|src)\s*=\s*)(["'])([^"']+)\2/gi, (_match, prefix: string, quote: string, rawHref: string) => {
584
- const rewritten = rewriteHref(rawHref, context, workspaceAssets);
585
- return `${prefix}${quote}${rewritten}${quote}`;
586
- });
587
- return { html, workspaceAssets };
588
- }
589
-
590
- function rewriteHref(
591
- href: string,
592
- context: {
593
- workspaceRoot: string;
594
- inputRoot: string;
595
- inputFile: string;
596
- outputRoot: string;
597
- outputFile: string;
598
- },
599
- workspaceAssets: Set<string>
600
- ): string {
601
- const parts = splitHref(href);
602
- if (!parts.path) {
603
- return href;
604
- }
605
-
606
- const trimmedPath = parts.path.trim();
607
- if (!trimmedPath || trimmedPath.startsWith('#') || trimmedPath.startsWith('//')) {
608
- return href;
609
- }
610
-
611
- if (trimmedPath.startsWith('workspace:/')) {
612
- const resolved = resolveWorkspacePath(context.workspaceRoot, trimmedPath);
613
- if (!resolved) {
614
- return href;
615
- }
616
- if (!resolved.toLowerCase().endsWith('.md')) {
617
- workspaceAssets.add(resolved);
618
- }
619
- const workspaceRelative = path.relative(context.workspaceRoot, resolved);
620
- const inputRelative = path.relative(context.inputRoot, resolved);
621
- const isInsideInput = !inputRelative.startsWith('..') && !path.isAbsolute(inputRelative);
622
- const outputRelative = isInsideInput ? inputRelative : workspaceRelative;
623
- const targetInOutput = path.join(
624
- context.outputRoot,
625
- resolved.toLowerCase().endsWith('.md')
626
- ? outputRelative.replace(/\.md$/i, '.html')
627
- : outputRelative
628
- );
629
- const rewrittenPath = toRelativeWebPath(path.dirname(context.outputFile), targetInOutput);
630
- return `${rewrittenPath}${parts.query}${parts.fragment}`;
631
- }
632
-
633
- if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedPath)) {
634
- return href;
635
- }
636
-
637
- if (!trimmedPath.startsWith('/')) {
638
- const resolved = path.resolve(path.dirname(context.inputFile), trimmedPath);
639
- const workspaceRelative = path.relative(context.workspaceRoot, resolved);
640
- if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative) && !resolved.toLowerCase().endsWith('.md')) {
641
- workspaceAssets.add(resolved);
642
- }
643
- }
644
-
645
- const rewrittenPath = trimmedPath.toLowerCase().endsWith('.md')
646
- ? trimmedPath.slice(0, -3) + '.html'
647
- : trimmedPath;
648
- return `${rewrittenPath}${parts.query}${parts.fragment}`;
649
- }
650
-
651
- function resolveWorkspacePath(workspaceRoot: string, hrefPath: string): string | undefined {
652
- const relative = hrefPath.slice('workspace:/'.length).replace(/^\/+/, '');
653
- const resolved = path.resolve(workspaceRoot, relative);
654
- const relativeToWorkspace = path.relative(workspaceRoot, resolved);
655
- if (relativeToWorkspace.startsWith('..') || path.isAbsolute(relativeToWorkspace)) {
656
- return undefined;
657
- }
658
- return resolved;
659
- }
660
-
661
- function splitHref(href: string): { path: string; query: string; fragment: string } {
662
- const hashIndex = href.indexOf('#');
663
- const pathAndQuery = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
664
- const fragment = hashIndex >= 0 ? href.slice(hashIndex) : '';
665
- const queryIndex = pathAndQuery.indexOf('?');
666
- return {
667
- path: queryIndex >= 0 ? pathAndQuery.slice(0, queryIndex) : pathAndQuery,
668
- query: queryIndex >= 0 ? pathAndQuery.slice(queryIndex) : '',
669
- fragment
670
- };
671
- }
672
-
673
- function toRelativeWebPath(fromDir: string, toFile: string): string {
674
- const relative = path.relative(fromDir, toFile).split(path.sep).join('/');
675
- if (!relative || relative === '.') {
676
- return '.';
677
- }
678
- return relative.startsWith('.') ? relative : `./${relative}`;
679
- }
680
-
681
- function normalizeWikiPathKey(raw: string): string {
682
- const normalized = raw.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
683
- if (!normalized) {
684
- return '';
685
- }
686
- const withoutHtml = normalized.replace(/\.html$/i, '');
687
- return withoutHtml.replace(/^\.\//, '').toLowerCase();
688
- }
689
-
690
- function buildWikiPageIndex(markdownFiles: ReadonlyArray<string>, inputRoot: string, outputRoot: string): Map<string, string> {
691
- const index = new Map<string, string>();
692
- for (const markdownFile of markdownFiles) {
693
- const relativeInput = path.relative(inputRoot, markdownFile).split(path.sep).join('/');
694
- const normalizedRelative = normalizeWikiPathKey(relativeInput.replace(/\.md$/i, ''));
695
- if (!normalizedRelative) {
696
- continue;
697
- }
698
- const outputFile = path.join(outputRoot, relativeInput.replace(/\.md$/i, '.html'));
699
- index.set(normalizedRelative, outputFile);
700
- }
701
- return index;
702
- }
703
-
704
- function buildWikiLinkHrefMapForPage(
705
- wikiPageIndex: ReadonlyMap<string, string>,
706
- outputFile: string
707
- ): Record<string, string> {
708
- const map: Record<string, string> = {};
709
- for (const [key, targetFile] of wikiPageIndex.entries()) {
710
- map[key] = toRelativeWebPath(path.dirname(outputFile), targetFile);
711
- }
712
- return map;
713
- }
714
-
715
- function collectFragmentIrisFromValue(value: unknown, collected: Set<string>): void {
716
- if (typeof value === 'string') {
717
- const trimmed = value.trim();
718
- if (trimmed.length > 0) {
719
- const iriPattern = /[a-z][a-z0-9+.-]*:[^\s<>"')\]]+/gi;
720
- for (const match of trimmed.matchAll(iriPattern)) {
721
- let iri = (match[0] ?? '').trim();
722
- iri = iri.replace(/[.,;:!?]+$/, '');
723
- if (/^[a-z][a-z0-9+.-]*:/i.test(iri)) {
724
- collected.add(iri);
725
- }
726
- }
727
- }
17
+ export const renderAction = async (opts: RenderOptions): Promise<void> => {
18
+ await lintAction({ authToken: opts.authToken });
19
+ const renderStartedAt = Date.now();
20
+ const result = await restPost<{
21
+ success: boolean;
22
+ filesRendered: number;
23
+ outputDir: string;
24
+ error?: string;
25
+ }>('/v0/render', {
26
+ ...opts,
27
+ only: true,
28
+ } as unknown as Record<string, unknown>, opts.authToken);
29
+
30
+ if (!result.success) {
31
+ failCli(chalk.red(result.error?.trim() || 'render failed.'));
32
+ }
33
+ if (result.filesRendered === 0) {
34
+ console.log(chalk.yellow(`No markdown files found.`));
728
35
  return;
729
36
  }
730
- if (Array.isArray(value)) {
731
- for (const entry of value) {
732
- collectFragmentIrisFromValue(entry, collected);
733
- }
734
- return;
735
- }
736
- if (value && typeof value === 'object') {
737
- for (const entry of Object.values(value)) {
738
- collectFragmentIrisFromValue(entry, collected);
739
- }
740
- }
741
- }
742
-
743
- function collectWikilinkIrisFromResults(results: ReadonlyArray<RenderedBlockResult>): Set<string> {
744
- const collected = new Set<string>();
745
- for (const result of results) {
746
- collectFragmentIrisFromValue(result, collected);
747
- }
748
- return collected;
749
- }
750
-
751
- function collectWikilinkIrisFromHtml(renderedHtml: string): Set<string> {
752
- const collected = new Set<string>();
753
- const pattern = /<a\b[^>]*\bclass=(["'])[^"']*\bwikilink\b[^"']*\1[^>]*\biri=(["'])([^"']+)\2/gi;
754
- for (const match of renderedHtml.matchAll(pattern)) {
755
- const iri = (match[3] ?? '').trim();
756
- if (iri) {
757
- collected.add(iri);
758
- }
759
- }
760
- return collected;
761
- }
762
-
763
- function sanitizePathSegment(value: string): string {
764
- const decoded = decodeURIComponent(value);
765
- const trimmed = decoded.trim();
766
- if (!trimmed) {
767
- return 'instance';
768
- }
769
- return encodeURIComponent(trimmed);
770
- }
771
-
772
- function resolveInstanceOutputFile(outputRoot: string, iri: string): string | undefined {
773
- let parsed: URL;
774
- try {
775
- parsed = new URL(iri);
776
- } catch {
777
- return undefined;
778
- }
779
- if (!parsed.hostname) {
780
- return undefined;
781
- }
782
- const host = parsed.hostname.toLowerCase();
783
- const pathSegments = parsed.pathname
784
- .split('/')
785
- .map((segment) => segment.trim())
786
- .filter((segment) => segment.length > 0)
787
- .map((segment) => sanitizePathSegment(segment));
788
- const fragment = parsed.hash.replace(/^#/, '').trim();
789
- const fileSegments = [host, ...pathSegments];
790
- if (fragment.length > 0) {
791
- fileSegments.push(sanitizePathSegment(fragment));
792
- } else if (fileSegments.length === 1) {
793
- fileSegments.push('index');
794
- }
795
- if (fileSegments.length === 0) {
796
- return undefined;
797
- }
798
- return path.join(outputRoot, ...fileSegments) + '.html';
799
- }
800
-
801
- function selectTemplateForTypes(catalog: TemplateCatalog, typeIris: ReadonlySet<string>): TemplateFile | undefined {
802
- const candidates: TemplateFile[] = [];
803
- for (const typeIri of typeIris) {
804
- const templates = catalog.byType.get(typeIri);
805
- if (!templates || templates.length === 0) {
806
- continue;
807
- }
808
- candidates.push(...templates);
809
- }
810
- if (candidates.length === 0) {
811
- return undefined;
812
- }
813
- candidates.sort((left, right) => (left.priority - right.priority) || left.filePath.localeCompare(right.filePath));
814
- return candidates[0];
815
- }
816
-
817
- async function queryRdfTypesForIris(
818
- backend: CliBackend,
819
- workspaceRoot: string,
820
- modelUri: string,
821
- iris: ReadonlyArray<string>
822
- ): Promise<Map<string, Set<string>>> {
823
- const byIri = new Map<string, Set<string>>();
824
- if (iris.length === 0) {
825
- return byIri;
826
- }
827
- const valuesClause = iris.map((iri) => `<${iri}>`).join(' ');
828
- const query = `PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
829
- SELECT ?s ?type
830
- WHERE {
831
- VALUES ?s { ${valuesClause} }
832
- GRAPH ?g { ?s rdf:type ?type . }
833
- }`;
834
- const result = await backend.executeMarkdownBlocks({
835
- documentUri: 'file:///__oml_cli_wikilink_types__.md',
836
- modelUri,
837
- blocks: [{
838
- id: '__oml_cli_wikilink_types__',
839
- kind: 'table',
840
- source: query,
841
- lineStart: 0,
842
- lineEnd: 0,
843
- }]
844
- }, workspaceRoot);
845
- const block = result.results[0];
846
- if (!block || block.status !== 'ok' || block.format !== 'table' || !block.payload) {
847
- return byIri;
848
- }
849
- const subjectIndex = block.payload.columns.findIndex((column) => column === 's');
850
- const typeIndex = block.payload.columns.findIndex((column) => column === 'type');
851
- const left = subjectIndex >= 0 ? subjectIndex : 0;
852
- const right = typeIndex >= 0 ? typeIndex : 1;
853
- for (const row of block.payload.rows) {
854
- const subject = (row[left] ?? '').trim();
855
- const typeIri = (row[right] ?? '').trim().replace(/^<|>$/g, '');
856
- if (!subject || !typeIri) {
857
- continue;
858
- }
859
- const types = byIri.get(subject) ?? new Set<string>();
860
- types.add(typeIri);
861
- byIri.set(subject, types);
862
- }
863
- return byIri;
864
- }
865
-
866
- function buildTemplatePageMarkdown(templateMarkdown: string, focusIri: string, queryContextOption: string): string {
867
- const body = stripLeadingFrontMatter(templateMarkdown).split('${contextIri}').join(focusIri);
868
- const escapedContext = JSON.stringify(queryContextOption);
869
- return `---
870
- contextUri: ${escapedContext}
871
- ---
872
- ${body}`;
873
- }
874
-
875
- async function generateInstanceTemplatePages(
876
- backend: CliBackend,
877
- workspaceRoot: string,
878
- inputRoot: string,
879
- outputRoot: string,
880
- pageOutputFile: string,
881
- runtimeScriptFile: string,
882
- stylesheetFile: string,
883
- templateGenerationEnabled: boolean,
884
- queryContextModelUri: string,
885
- memberLabelSnapshot: Record<string, string>,
886
- pageWikilinkIris: ReadonlySet<string>,
887
- results: ReadonlyArray<RenderedBlockResult>,
888
- wikiPageIndex: ReadonlyMap<string, string>,
889
- templateCatalog: TemplateCatalog,
890
- generatedInstancePages: Set<string>,
891
- instancePageByIri: Map<string, string>,
892
- attemptedInstanceIris: Set<string>,
893
- currentPageStack: Set<string>
894
- ): Promise<Record<string, string>> {
895
- const iriToHref: Record<string, string> = {};
896
- if (!templateGenerationEnabled) {
897
- return iriToHref;
898
- }
899
- const wikilinkIris = [...new Set<string>([
900
- ...pageWikilinkIris,
901
- ...collectWikilinkIrisFromResults(results),
902
- ])];
903
- const unresolved = wikilinkIris.filter((iri) => !attemptedInstanceIris.has(iri));
904
- for (const iri of unresolved) {
905
- attemptedInstanceIris.add(iri);
906
- }
907
- const typesByIri = await queryRdfTypesForIris(backend, workspaceRoot, queryContextModelUri, unresolved);
908
- for (const iri of unresolved) {
909
- const types = typesByIri.get(iri) ?? new Set<string>();
910
- const template = selectTemplateForTypes(templateCatalog, types);
911
- if (!template) {
912
- continue;
913
- }
914
- const instanceOutputFile = resolveInstanceOutputFile(outputRoot, iri);
915
- if (!instanceOutputFile) {
916
- continue;
917
- }
918
- instancePageByIri.set(iri, instanceOutputFile);
919
- if (generatedInstancePages.has(instanceOutputFile) || currentPageStack.has(instanceOutputFile)) {
920
- continue;
921
- }
922
- generatedInstancePages.add(instanceOutputFile);
923
- const templateMarkdown = await fs.readFile(template.filePath, 'utf-8');
924
- const instantiatedMarkdown = buildTemplatePageMarkdown(templateMarkdown, iri, queryContextModelUri);
925
- const nextStack = new Set(currentPageStack);
926
- nextStack.add(instanceOutputFile);
927
- await renderMarkdownFile(
928
- backend,
929
- workspaceRoot,
930
- inputRoot,
931
- outputRoot,
932
- template.filePath,
933
- instanceOutputFile,
934
- runtimeScriptFile,
935
- stylesheetFile,
936
- templateGenerationEnabled,
937
- queryContextModelUri,
938
- memberLabelSnapshot,
939
- wikiPageIndex,
940
- templateCatalog,
941
- generatedInstancePages,
942
- instancePageByIri,
943
- attemptedInstanceIris,
944
- nextStack,
945
- instantiatedMarkdown,
946
- queryContextModelUri
947
- );
948
- }
949
- for (const iri of wikilinkIris) {
950
- const absolute = instancePageByIri.get(iri);
951
- if (!absolute) {
952
- continue;
953
- }
954
- iriToHref[iri] = toRelativeWebPath(path.dirname(pageOutputFile), absolute);
955
- }
956
- return iriToHref;
957
- }
958
-
959
- function wrapHtml(
960
- content: string,
961
- runtimeScriptPath: string,
962
- stylesheetPath: string,
963
- blockManifest: Array<{ blockId: string; path: string }>,
964
- blockResults: ReadonlyArray<RenderedBlockResult>,
965
- wikiLinkHrefByKey: Record<string, string>,
966
- iriAliasHrefByIri: Record<string, string>,
967
- linkingEnabled: boolean,
968
- memberLabelSnapshot?: Record<string, string>
969
- ): string {
970
- const escapedManifest = escapeJsonForScript(JSON.stringify(blockManifest));
971
- const inlineResults = Object.fromEntries(blockResults.map((result) => [result.blockId, result]));
972
- const escapedInlineResults = escapeJsonForScript(JSON.stringify(inlineResults));
973
- const escapedWikiIndex = escapeJsonForScript(JSON.stringify(wikiLinkHrefByKey));
974
- const escapedIriAliasIndex = escapeJsonForScript(JSON.stringify(iriAliasHrefByIri));
975
- const escapedLinkingConfig = escapeJsonForScript(JSON.stringify({ linkingEnabled }));
976
- const escapedMemberLabels = escapeJsonForScript(JSON.stringify(memberLabelSnapshot ?? {}));
977
- return `<!doctype html>
978
- <html lang="en">
979
- <head>
980
- <meta charset="UTF-8">
981
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
982
- <title>OML Markdown</title>
983
- <link rel="stylesheet" href="${escapeAttribute(stylesheetPath)}">
984
- </head>
985
- <body>
986
- ${content}
987
- <script id="oml-md-block-manifest" type="application/json">${escapedManifest}</script>
988
- <script id="oml-md-block-inline-results" type="application/json">${escapedInlineResults}</script>
989
- <script id="oml-md-wikilink-index" type="application/json">${escapedWikiIndex}</script>
990
- <script id="oml-md-wikilink-iri-aliases" type="application/json">${escapedIriAliasIndex}</script>
991
- <script id="oml-md-wikilink-config" type="application/json">${escapedLinkingConfig}</script>
992
- <script id="oml-md-member-labels" type="application/json">${escapedMemberLabels}</script>
993
- <script src="${escapeAttribute(runtimeScriptPath)}"></script>
994
- </body>
995
- </html>
996
- `;
997
- }
998
-
999
- function escapeAttribute(value: string): string {
1000
- return value
1001
- .replace(/&/g, '&amp;')
1002
- .replace(/</g, '&lt;')
1003
- .replace(/>/g, '&gt;')
1004
- .replace(/"/g, '&quot;')
1005
- .replace(/'/g, '&#39;');
1006
- }
1007
-
1008
- function escapeJsonForScript(value: string): string {
1009
- return value.replace(/</g, '\\u003c');
1010
- }
37
+ console.log(chalk.green(`render: ${result.filesRendered} markdown file(s) rendered in ${path.relative(process.cwd(), result.outputDir) || result.outputDir} [${formatDuration(Date.now() - renderStartedAt)}]`));
38
+ };