@oml/markdown 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.
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/md/md-execution.d.ts +3 -3
- package/out/md/md-execution.js +1 -1
- package/out/md/md-execution.js.map +1 -1
- package/out/md/md-executor.js +6 -8
- package/out/md/md-executor.js.map +1 -1
- package/out/md/md-frontmatter.d.ts +1 -1
- package/out/md/md-frontmatter.js +15 -10
- package/out/md/md-frontmatter.js.map +1 -1
- package/out/md/md-runtime.js +1 -1
- package/out/md/md-runtime.js.map +1 -1
- package/out/md/md-types.d.ts +2 -2
- package/out/renderers/diagram-renderer.js +231 -12
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/graph-renderer.js +2 -2
- package/out/renderers/graph-renderer.js.map +1 -1
- package/out/renderers/table-renderer.js +25 -10
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +489 -25
- package/out/static/browser-runtime.bundle.js.map +3 -3
- package/out/static/browser-runtime.js +258 -0
- package/out/static/browser-runtime.js.map +1 -1
- package/out/static/runtime-assets.d.ts +1 -1
- package/out/static/runtime-assets.js +1 -0
- package/out/static/runtime-assets.js.map +1 -1
- package/out/template/binder.d.ts +2 -0
- package/out/template/binder.js +56 -0
- package/out/template/binder.js.map +1 -0
- package/out/template/catalog.d.ts +8 -0
- package/out/template/catalog.js +26 -0
- package/out/template/catalog.js.map +1 -0
- package/out/template/compose.d.ts +7 -0
- package/out/template/compose.js +93 -0
- package/out/template/compose.js.map +1 -0
- package/out/template/definition.d.ts +3 -0
- package/out/template/definition.js +204 -0
- package/out/template/definition.js.map +1 -0
- package/out/template/engine.d.ts +8 -0
- package/out/template/engine.js +30 -0
- package/out/template/engine.js.map +1 -0
- package/out/template/index.d.ts +7 -0
- package/out/template/index.js +9 -0
- package/out/template/index.js.map +1 -0
- package/out/template/resolver.d.ts +4 -0
- package/out/template/resolver.js +58 -0
- package/out/template/resolver.js.map +1 -0
- package/out/template/types.d.ts +82 -0
- package/out/template/types.js +3 -0
- package/out/template/types.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/md/md-execution.ts +3 -3
- package/src/md/md-executor.ts +6 -9
- package/src/md/md-frontmatter.ts +15 -10
- package/src/md/md-runtime.ts +1 -1
- package/src/md/md-types.ts +2 -2
- package/src/renderers/diagram-renderer.ts +229 -12
- package/src/renderers/graph-renderer.ts +2 -2
- package/src/renderers/table-renderer.ts +26 -10
- package/src/static/browser-runtime.ts +305 -0
- package/src/static/markdown-webview.css +13 -0
- package/src/static/runtime-assets.ts +1 -0
- package/src/template/binder.ts +70 -0
- package/src/template/catalog.ts +35 -0
- package/src/template/compose.ts +107 -0
- package/src/template/definition.ts +222 -0
- package/src/template/engine.ts +45 -0
- package/src/template/index.ts +9 -0
- package/src/template/resolver.ts +75 -0
- package/src/template/types.ts +111 -0
|
@@ -7,6 +7,20 @@ const SUPPORTED = new Set(['table', 'tree', 'graph', 'chart', 'diagram', 'list',
|
|
|
7
7
|
|
|
8
8
|
type ManifestEntry = { blockId: string; path: string };
|
|
9
9
|
type WikilinkConfig = { linkingEnabled?: boolean };
|
|
10
|
+
type HoverAnchorRect = {
|
|
11
|
+
left: number;
|
|
12
|
+
right: number;
|
|
13
|
+
top: number;
|
|
14
|
+
bottom: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let wikilinkPreviewCard: HTMLDivElement | undefined;
|
|
20
|
+
let wikilinkPreviewHideTimer: number | undefined;
|
|
21
|
+
let wikilinkPreviewRequestToken = 0;
|
|
22
|
+
let tabPreviewModifierActive = false;
|
|
23
|
+
let activeWikilinkHover: { link: HTMLAnchorElement; iri: string } | undefined;
|
|
10
24
|
|
|
11
25
|
function parseJsonNode<T>(id: string, fallback: T): T {
|
|
12
26
|
const node = document.getElementById(id);
|
|
@@ -189,6 +203,296 @@ function setupIriNavigationHandler(
|
|
|
189
203
|
});
|
|
190
204
|
}
|
|
191
205
|
|
|
206
|
+
function isHoverPreviewEnabled(event: MouseEvent | undefined): boolean {
|
|
207
|
+
if (!event) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return event.altKey || tabPreviewModifierActive;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeHoverAnchorRect(value: unknown): HoverAnchorRect | undefined {
|
|
214
|
+
if (!value || typeof value !== 'object') {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const obj = value as Record<string, unknown>;
|
|
218
|
+
if (typeof obj.left !== 'number' || typeof obj.right !== 'number' ||
|
|
219
|
+
typeof obj.top !== 'number' || typeof obj.bottom !== 'number' ||
|
|
220
|
+
typeof obj.width !== 'number' || typeof obj.height !== 'number') {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
left: obj.left,
|
|
225
|
+
right: obj.right,
|
|
226
|
+
top: obj.top,
|
|
227
|
+
bottom: obj.bottom,
|
|
228
|
+
width: obj.width,
|
|
229
|
+
height: obj.height,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ensureWikilinkPreviewCard(): HTMLDivElement {
|
|
234
|
+
if (wikilinkPreviewCard) {
|
|
235
|
+
return wikilinkPreviewCard;
|
|
236
|
+
}
|
|
237
|
+
const card = document.createElement('div');
|
|
238
|
+
card.className = 'oml-wikilink-preview-card';
|
|
239
|
+
card.style.position = 'fixed';
|
|
240
|
+
card.style.zIndex = '1200';
|
|
241
|
+
card.style.display = 'none';
|
|
242
|
+
card.style.width = 'min(640px, calc(100vw - 48px))';
|
|
243
|
+
card.style.height = 'min(480px, calc(100vh - 48px))';
|
|
244
|
+
card.style.overflow = 'hidden';
|
|
245
|
+
card.style.padding = '0';
|
|
246
|
+
card.style.border = '1px solid var(--vscode-editorWidget-border, #c8ccd1)';
|
|
247
|
+
card.style.borderRadius = '10px';
|
|
248
|
+
card.style.background = 'var(--vscode-editor-background, #ffffff)';
|
|
249
|
+
card.style.color = 'var(--vscode-editor-foreground, #1f2328)';
|
|
250
|
+
card.style.boxShadow = '0 8px 24px rgb(0 0 0 / 24%)';
|
|
251
|
+
card.style.fontFamily = 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif';
|
|
252
|
+
card.addEventListener('mouseenter', () => {
|
|
253
|
+
if (wikilinkPreviewHideTimer !== undefined) {
|
|
254
|
+
window.clearTimeout(wikilinkPreviewHideTimer);
|
|
255
|
+
wikilinkPreviewHideTimer = undefined;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// Don't hide on mouse leave - only ESC closes the preview now
|
|
259
|
+
document.body.appendChild(card);
|
|
260
|
+
wikilinkPreviewCard = card;
|
|
261
|
+
return card;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function scheduleHideWikilinkPreview(): void {
|
|
265
|
+
wikilinkPreviewRequestToken += 1;
|
|
266
|
+
if (wikilinkPreviewHideTimer !== undefined) {
|
|
267
|
+
window.clearTimeout(wikilinkPreviewHideTimer);
|
|
268
|
+
wikilinkPreviewHideTimer = undefined;
|
|
269
|
+
}
|
|
270
|
+
// Immediately hide the preview (no delay)
|
|
271
|
+
if (wikilinkPreviewCard) {
|
|
272
|
+
wikilinkPreviewCard.style.display = 'none';
|
|
273
|
+
wikilinkPreviewCard.replaceChildren();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function scheduleIriPreview(
|
|
278
|
+
iri: string,
|
|
279
|
+
href: string,
|
|
280
|
+
anchorRect: HoverAnchorRect
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
if (wikilinkPreviewHideTimer !== undefined) {
|
|
283
|
+
window.clearTimeout(wikilinkPreviewHideTimer);
|
|
284
|
+
wikilinkPreviewHideTimer = undefined;
|
|
285
|
+
}
|
|
286
|
+
const token = ++wikilinkPreviewRequestToken;
|
|
287
|
+
await new Promise<void>((resolve) => window.setTimeout(resolve, 220));
|
|
288
|
+
if (token !== wikilinkPreviewRequestToken) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const card = ensureWikilinkPreviewCard();
|
|
293
|
+
|
|
294
|
+
// Create iframe to show the linked page
|
|
295
|
+
const iframe = document.createElement('iframe');
|
|
296
|
+
// Add preview mode parameter to prevent nested previews
|
|
297
|
+
const iframeSrc = new URL(href, window.location.href);
|
|
298
|
+
iframeSrc.searchParams.set('_oml_preview', '1');
|
|
299
|
+
iframe.src = iframeSrc.toString();
|
|
300
|
+
iframe.style.width = '100%';
|
|
301
|
+
iframe.style.height = '100%';
|
|
302
|
+
iframe.style.border = 'none';
|
|
303
|
+
iframe.style.borderRadius = '4px';
|
|
304
|
+
iframe.style.background = 'var(--vscode-editor-background, #ffffff)';
|
|
305
|
+
iframe.style.display = 'block';
|
|
306
|
+
|
|
307
|
+
// Intercept clicks in the iframe to navigate the parent page
|
|
308
|
+
iframe.addEventListener('load', () => {
|
|
309
|
+
try {
|
|
310
|
+
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
311
|
+
if (!iframeDoc) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Remove download attributes from all links to prevent file downloads
|
|
316
|
+
const allLinks = iframeDoc.querySelectorAll('a[download]');
|
|
317
|
+
allLinks.forEach(link => link.removeAttribute('download'));
|
|
318
|
+
|
|
319
|
+
// Add click listener to all links and interactive elements in the iframe
|
|
320
|
+
iframeDoc.addEventListener('click', (event) => {
|
|
321
|
+
const target = event.target;
|
|
322
|
+
if (!(target instanceof Element)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const link = target.closest('a[href]');
|
|
327
|
+
if (!link || !(link instanceof HTMLAnchorElement)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const linkHref = link.getAttribute('href');
|
|
332
|
+
if (!linkHref) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Allow internal anchor links within the iframe
|
|
337
|
+
if (linkHref.startsWith('#')) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Prevent any navigation in the iframe
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
event.stopPropagation();
|
|
344
|
+
event.stopImmediatePropagation();
|
|
345
|
+
|
|
346
|
+
// Get the absolute URL, accounting for any base URL in the iframe
|
|
347
|
+
const iframeLocation = iframe.contentWindow?.location.href;
|
|
348
|
+
const absoluteUrl = new URL(linkHref, iframeLocation || iframe.src);
|
|
349
|
+
|
|
350
|
+
// Remove the preview parameter for the main navigation
|
|
351
|
+
absoluteUrl.searchParams.delete('_oml_preview');
|
|
352
|
+
|
|
353
|
+
// Close the preview and navigate the parent page
|
|
354
|
+
scheduleHideWikilinkPreview();
|
|
355
|
+
|
|
356
|
+
// Navigate the parent window (not the iframe)
|
|
357
|
+
const targetUrl = absoluteUrl.toString();
|
|
358
|
+
window.location.assign(targetUrl);
|
|
359
|
+
|
|
360
|
+
return false;
|
|
361
|
+
}, true); // Use capture phase to catch events early
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// If we can't access iframe content (CORS), silently fail
|
|
364
|
+
console.warn('Could not access iframe content for click interception:', error);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
card.replaceChildren(iframe);
|
|
369
|
+
card.style.display = 'block';
|
|
370
|
+
const cardRect = card.getBoundingClientRect();
|
|
371
|
+
const margin = 12;
|
|
372
|
+
const left = Math.min(
|
|
373
|
+
Math.max(16, anchorRect.left),
|
|
374
|
+
Math.max(16, window.innerWidth - cardRect.width - 16)
|
|
375
|
+
);
|
|
376
|
+
let top = anchorRect.bottom + margin;
|
|
377
|
+
if (top + cardRect.height > window.innerHeight - 16) {
|
|
378
|
+
top = Math.max(16, window.innerHeight - cardRect.height - 16);
|
|
379
|
+
}
|
|
380
|
+
card.style.left = `${left}px`;
|
|
381
|
+
card.style.top = `${top}px`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function setupWikilinkHoverPreview(
|
|
385
|
+
wikilinkIndex: Record<string, string>,
|
|
386
|
+
iriAliasIndex: Record<string, string>,
|
|
387
|
+
linkingEnabled: boolean
|
|
388
|
+
): void {
|
|
389
|
+
// Don't enable previews if we're already in preview mode (prevents nested previews)
|
|
390
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
391
|
+
if (urlParams.has('_oml_preview')) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Close preview when clicking anywhere outside the preview card
|
|
396
|
+
document.addEventListener('click', (event) => {
|
|
397
|
+
if (!wikilinkPreviewCard || wikilinkPreviewCard.style.display === 'none') {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const target = event.target;
|
|
401
|
+
if (!(target instanceof Element)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Check if click is outside the preview card
|
|
405
|
+
if (!target.closest('.oml-wikilink-preview-card')) {
|
|
406
|
+
scheduleHideWikilinkPreview();
|
|
407
|
+
activeWikilinkHover = undefined;
|
|
408
|
+
}
|
|
409
|
+
}, true); // Use capture to catch clicks before they propagate
|
|
410
|
+
|
|
411
|
+
// Handle hover events for wikilinks in text content
|
|
412
|
+
document.body.addEventListener('mouseover', (event) => {
|
|
413
|
+
const target = event.target instanceof Element
|
|
414
|
+
? event.target
|
|
415
|
+
: (event.target instanceof Node ? event.target.parentElement : null);
|
|
416
|
+
if (!target || target.closest('.oml-wikilink-preview-card')) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const link = target.closest<HTMLAnchorElement>('a.wikilink[iri]');
|
|
420
|
+
if (!link) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const iri = link.getAttribute('iri')?.trim();
|
|
424
|
+
const href = link.getAttribute('href')?.trim();
|
|
425
|
+
if (!iri || !href || href === '#') {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
activeWikilinkHover = { link, iri };
|
|
429
|
+
if (isHoverPreviewEnabled(event)) {
|
|
430
|
+
void scheduleIriPreview(iri, href, link.getBoundingClientRect());
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
window.addEventListener('keydown', (event) => {
|
|
435
|
+
// ESC key closes the preview
|
|
436
|
+
if (event.key === 'Escape') {
|
|
437
|
+
scheduleHideWikilinkPreview();
|
|
438
|
+
activeWikilinkHover = undefined;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (event.key === 'Tab') {
|
|
443
|
+
tabPreviewModifierActive = true;
|
|
444
|
+
}
|
|
445
|
+
if (!activeWikilinkHover) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!(event.altKey || event.key === 'Alt' || (event.key === 'Tab' && tabPreviewModifierActive))) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const link = activeWikilinkHover.link;
|
|
452
|
+
if (!link.isConnected || !link.matches(':hover')) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const href = link.getAttribute('href')?.trim();
|
|
456
|
+
if (!href || href === '#') {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
void scheduleIriPreview(activeWikilinkHover.iri, href, link.getBoundingClientRect());
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
window.addEventListener('keyup', (event) => {
|
|
463
|
+
if (event.key === 'Tab') {
|
|
464
|
+
tabPreviewModifierActive = false;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
window.addEventListener('blur', () => {
|
|
469
|
+
tabPreviewModifierActive = false;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Handle hover events dispatched by renderers (diagram, graph, table, etc.)
|
|
473
|
+
window.addEventListener('md-show-iri-hover', (event: Event) => {
|
|
474
|
+
if (!(event instanceof CustomEvent)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const eventTarget = event.target;
|
|
478
|
+
if (eventTarget instanceof Element && eventTarget.closest('.oml-wikilink-preview-card')) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const detail = event.detail as { iri?: unknown; anchorRect?: unknown; previewEnabled?: unknown } | undefined;
|
|
482
|
+
const iri = typeof detail?.iri === 'string' ? detail.iri.trim() : '';
|
|
483
|
+
const anchorRect = normalizeHoverAnchorRect(detail?.anchorRect);
|
|
484
|
+
const previewEnabled = detail?.previewEnabled === true;
|
|
485
|
+
if (!iri || !anchorRect || !previewEnabled || !linkingEnabled) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const href = resolveWikiHref(iri, wikilinkIndex, iriAliasIndex);
|
|
489
|
+
if (!href || href === '#') {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
void scheduleIriPreview(iri, href, anchorRect);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
192
496
|
function getMdKindFromCodeElement(code: Element): string | undefined {
|
|
193
497
|
for (const className of Array.from(code.classList)) {
|
|
194
498
|
if (!className.startsWith('language-')) {
|
|
@@ -229,6 +533,7 @@ async function applyExecutionResults(): Promise<void> {
|
|
|
229
533
|
// Re-apply wikilinks for dynamic renderer re-renders (tables, filters, paging, etc.).
|
|
230
534
|
installWikilinkObserver(wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
231
535
|
setupIriNavigationHandler(wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
536
|
+
setupWikilinkHoverPreview(wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
232
537
|
|
|
233
538
|
const manifest = parseJsonNode<ManifestEntry[]>('oml-md-block-manifest', []);
|
|
234
539
|
if (!Array.isArray(manifest) || manifest.length === 0) {
|
|
@@ -34,6 +34,19 @@ body {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/* Hide OML code blocks until they're replaced by rendered panels to prevent flash of code */
|
|
38
|
+
pre > code.language-table,
|
|
39
|
+
pre > code.language-tree,
|
|
40
|
+
pre > code.language-graph,
|
|
41
|
+
pre > code.language-chart,
|
|
42
|
+
pre > code.language-diagram,
|
|
43
|
+
pre > code.language-list,
|
|
44
|
+
pre > code.language-text,
|
|
45
|
+
pre > code.language-matrix,
|
|
46
|
+
pre > code.language-table-editor {
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
h1,
|
|
38
51
|
h2,
|
|
39
52
|
h3,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { TemplateBindResult, TemplateDefinition, TemplateInvocationContext, TemplateParameterDefinition, TemplateValue } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function bindTemplateParameters(
|
|
6
|
+
template: TemplateDefinition,
|
|
7
|
+
context: TemplateInvocationContext,
|
|
8
|
+
explicitArgs: Readonly<Record<string, TemplateValue>> = {}
|
|
9
|
+
): TemplateBindResult {
|
|
10
|
+
const values: Record<string, TemplateValue> = {};
|
|
11
|
+
const missingRequired: string[] = [];
|
|
12
|
+
const parameters = template.parameters ?? [];
|
|
13
|
+
for (const parameter of parameters) {
|
|
14
|
+
const explicit = explicitArgs[parameter.id];
|
|
15
|
+
if (explicit !== undefined) {
|
|
16
|
+
values[parameter.id] = explicit;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const bound = resolveValueFromContext(parameter, context);
|
|
20
|
+
if (bound !== undefined) {
|
|
21
|
+
values[parameter.id] = bound;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (parameter.defaultValue !== undefined) {
|
|
25
|
+
values[parameter.id] = parameter.defaultValue;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (parameter.required) {
|
|
29
|
+
missingRequired.push(parameter.id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { values, missingRequired };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveValueFromContext(
|
|
36
|
+
parameter: TemplateParameterDefinition,
|
|
37
|
+
context: TemplateInvocationContext
|
|
38
|
+
): TemplateValue | undefined {
|
|
39
|
+
switch (parameter.from) {
|
|
40
|
+
case 'context.member':
|
|
41
|
+
return context.focus?.memberIri;
|
|
42
|
+
case 'context.ontology':
|
|
43
|
+
return context.model?.ontologyIri;
|
|
44
|
+
case 'context.modelUri':
|
|
45
|
+
return context.model?.modelUri;
|
|
46
|
+
case 'context.selection[*]':
|
|
47
|
+
return context.selection?.iris ?? [];
|
|
48
|
+
default:
|
|
49
|
+
return resolveSelectionIndexedBinding(parameter.from, context);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveSelectionIndexedBinding(
|
|
54
|
+
source: TemplateParameterDefinition['from'],
|
|
55
|
+
context: TemplateInvocationContext
|
|
56
|
+
): TemplateValue | undefined {
|
|
57
|
+
if (!source || !source.startsWith('context.selection[')) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const match = /^context\.selection\[(\d+)]$/.exec(source);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const index = Number.parseInt(match[1] ?? '', 10);
|
|
65
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const iris = context.selection?.iris ?? [];
|
|
69
|
+
return iris[index];
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { TemplateDefinition } from './types.js';
|
|
4
|
+
|
|
5
|
+
export interface TemplateCatalog {
|
|
6
|
+
readonly byId: ReadonlyMap<string, TemplateDefinition>;
|
|
7
|
+
readonly all: ReadonlyArray<TemplateDefinition>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildTemplateCatalog(templates: ReadonlyArray<TemplateDefinition>): TemplateCatalog {
|
|
11
|
+
const byId = new Map<string, TemplateDefinition>();
|
|
12
|
+
for (const template of templates) {
|
|
13
|
+
const id = normalizeTemplateId(template.id);
|
|
14
|
+
if (!id) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
byId.set(id, { ...template, id });
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
byId,
|
|
21
|
+
all: [...byId.values()]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeTemplateId(value: string): string {
|
|
26
|
+
return value.trim().replace(/^<|>$/g, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getTemplateById(catalog: TemplateCatalog, templateId: string): TemplateDefinition | undefined {
|
|
30
|
+
const normalized = normalizeTemplateId(templateId);
|
|
31
|
+
if (!normalized) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return catalog.byId.get(normalized);
|
|
35
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import type { TemplateBindingSource, TemplateValue } from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface TemplateComposeDirective {
|
|
7
|
+
id: string;
|
|
8
|
+
args: Record<string, TemplateValue>;
|
|
9
|
+
bind: Record<string, TemplateBindingSource>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseTemplateComposeDirective(source: string): TemplateComposeDirective | undefined {
|
|
13
|
+
const normalized = source.replace(/^\uFEFF/, '');
|
|
14
|
+
const frontMatterMatch = /^(---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*)([\s\S]*)$/.exec(normalized);
|
|
15
|
+
if (!frontMatterMatch) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const trailing = frontMatterMatch[2] ?? '';
|
|
19
|
+
if (trailing.trim().length > 0) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const yamlBody = frontMatterMatch[1]
|
|
23
|
+
.replace(/^---[ \t]*\r?\n/, '')
|
|
24
|
+
.replace(/\r?\n---[ \t]*$/, '');
|
|
25
|
+
const parsed = parse(yamlBody);
|
|
26
|
+
if (!isRecord(parsed)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const id = normalizeString(parsed.id);
|
|
30
|
+
if (!id) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const args = parseArgs(parsed.args);
|
|
34
|
+
const bind = parseBind(parsed.bind);
|
|
35
|
+
return { id, args, bind };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(value: unknown): Record<string, TemplateValue> {
|
|
39
|
+
if (!isRecord(value)) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
const args: Record<string, TemplateValue> = {};
|
|
43
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
44
|
+
const normalizedKey = normalizeString(key);
|
|
45
|
+
if (!normalizedKey) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const converted = toTemplateValue(raw);
|
|
49
|
+
if (converted !== undefined) {
|
|
50
|
+
args[normalizedKey] = converted;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseBind(value: unknown): Record<string, TemplateBindingSource> {
|
|
57
|
+
if (!isRecord(value)) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
const bind: Record<string, TemplateBindingSource> = {};
|
|
61
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
62
|
+
const normalizedKey = normalizeString(key);
|
|
63
|
+
const source = normalizeString(raw);
|
|
64
|
+
if (!normalizedKey || !source || !isTemplateBindingSource(source)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
bind[normalizedKey] = source;
|
|
68
|
+
}
|
|
69
|
+
return bind;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toTemplateValue(value: unknown): TemplateValue | undefined {
|
|
73
|
+
if (value === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
return value.every((entry) => typeof entry === 'string') ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
if (isRecord(value)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isTemplateBindingSource(value: string): value is TemplateBindingSource {
|
|
89
|
+
return value === 'context.member'
|
|
90
|
+
|| value === 'context.ontology'
|
|
91
|
+
|| value === 'context.modelUri'
|
|
92
|
+
|| value === 'context.selection[*]'
|
|
93
|
+
|| value === 'user'
|
|
94
|
+
|| /^context\.selection\[\d+]$/.test(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeString(value: unknown): string | undefined {
|
|
98
|
+
if (typeof value !== 'string') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const normalized = value.trim();
|
|
102
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
106
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|