@oml/markdown 0.16.1 → 0.16.3

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.
@@ -6,17 +6,17 @@ export function renderTemplate(template, invocation) {
6
6
  return {
7
7
  output,
8
8
  values: bound.values,
9
- missingRequired: bound.missingRequired
10
9
  };
11
10
  }
12
11
  export function interpolateTemplateBody(source, values) {
13
- return source.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, key) => {
12
+ return source.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?:\|([^|}]*)(?:\|([^}]*))?)?\}/g, (_match, key, delimiter, entryTemplate) => {
14
13
  if (!(key in values)) {
15
14
  return _match;
16
15
  }
17
16
  const value = values[key];
18
17
  if (Array.isArray(value)) {
19
- return value.map((entry) => String(entry)).join(', ');
18
+ const format = (entry) => entryTemplate ? entryTemplate.replace('%s', entry) : entry;
19
+ return value.map((entry) => format(String(entry))).join(delimiter ?? ', ');
20
20
  }
21
21
  if (value === null || value === undefined) {
22
22
  return '';
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/template/engine.ts"],"names":[],"mappings":"AAAA,qDAAqD;AAErD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AASrD,MAAM,UAAU,cAAc,CAC1B,QAA4B,EAC5B,UAA8B;IAE9B,MAAM,KAAK,GAAG,sBAAsB,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,uBAAuB,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpE,OAAO;QACH,MAAM;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,eAAe,EAAE,KAAK,CAAC,eAAe;KACzC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,uBAAuB,CACnC,MAAc,EACd,MAA+C;IAE/C,OAAO,MAAM,CAAC,OAAO,CAAC,iCAAiC,EAAE,CAAC,MAAc,EAAE,GAAW,EAAU,EAAE;QAC7F,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/template/engine.ts"],"names":[],"mappings":"AAAA,qDAAqD;AAErD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAQrD,MAAM,UAAU,cAAc,CAC1B,QAA4B,EAC5B,UAA8B;IAE9B,MAAM,KAAK,GAAG,sBAAsB,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,uBAAuB,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpE,OAAO;QACH,MAAM;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;KACvB,CAAC;AACN,CAAC;AAED,MAAM,UAAU,uBAAuB,CACnC,MAAc,EACd,MAA+C;IAE/C,OAAO,MAAM,CAAC,OAAO,CAAC,8DAA8D,EAAE,CAAC,MAAc,EAAE,GAAW,EAAE,SAA6B,EAAE,aAAiC,EAAU,EAAE;QAC5L,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAC7F,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -75,5 +75,4 @@ export interface TemplateResolutionResult {
75
75
  }
76
76
  export interface TemplateBindResult {
77
77
  values: Record<string, TemplateValue>;
78
- missingRequired: string[];
79
78
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oml/markdown",
3
3
  "description": "Markdown runtime and execution contracts for OML",
4
- "version": "0.16.1",
4
+ "version": "0.16.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20.10.0",
@@ -56,7 +56,7 @@
56
56
  "dependencies": {
57
57
  "@antv/x6": "^3.1.6",
58
58
  "@dagrejs/dagre": "^2.0.4",
59
- "@oml/owl": "0.16.1",
59
+ "@oml/owl": "0.16.3",
60
60
  "chart.js": "^4.4.0",
61
61
  "markdown-it": "^14.1.1",
62
62
  "reflect-metadata": "^0.2.2",
@@ -2967,7 +2967,17 @@ function normalizeImageHref(href: string): string {
2967
2967
  return href;
2968
2968
  }
2969
2969
  try {
2970
- return new URL(href, window.location.href).toString();
2970
+ const params = new URLSearchParams(window.location.search);
2971
+ const isPreviewFrame = params.get('_oml_preview') === '1';
2972
+ let baseHref = document.baseURI || window.location.href;
2973
+ if (isPreviewFrame && window.parent && window.parent !== window) {
2974
+ try {
2975
+ baseHref = window.parent.document.baseURI || window.parent.location.href || baseHref;
2976
+ } catch {
2977
+ // Ignore cross-origin access errors and keep current document base.
2978
+ }
2979
+ }
2980
+ return new URL(href, baseHref).toString();
2971
2981
  } catch {
2972
2982
  return href;
2973
2983
  }
@@ -1,7 +1,26 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
3
  import { createMarkdownRendererRegistry } from '../renderers/registry.js';
4
- import { setIriLabelSnapshot } from '../renderers/renderer.js';
4
+ import { setIriLabelSnapshot, displayLabelFromIri } from '../renderers/renderer.js';
5
+
6
+ // Replace [[iri]] tokens in an HTML string with wikilink anchors, skipping content inside tags.
7
+ function expandWikilinksInHtml(html: string): string {
8
+ return html.replace(/(<[^>]*>)|\[\[([^\]]+)\]\]/g, (match, tag: string | undefined, iri: string | undefined) => {
9
+ if (tag !== undefined) {
10
+ // Only pass through real HTML tags: <tagname, </tagname, <!-- , <!doctype, <?.
11
+ // Tag names are letters/digits/hyphens only — no colons or slashes.
12
+ // <http://...> fails because ':' immediately follows the tag name letters.
13
+ if (/^<[!?]/.test(tag) || /^<\/?[a-zA-Z][a-zA-Z0-9-]*(?:[\s>\/]|$)/.test(tag)) return tag;
14
+ return tag.replace(/</g, '&lt;').replace(/>/g, '&gt;');
15
+ }
16
+ const trimmed = (iri ?? '').trim();
17
+ if (!trimmed) return match;
18
+ const label = displayLabelFromIri(trimmed);
19
+ const safeIri = trimmed.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
20
+ const safeLabel = label.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
21
+ return `<a class="wikilink" iri="${safeIri}" href="#" title="${safeIri}">${safeLabel}</a>`;
22
+ });
23
+ }
5
24
 
6
25
  const SUPPORTED = new Set(['table', 'tree', 'graph', 'chart', 'diagram', 'list', 'text', 'matrix', 'table-editor']);
7
26
 
@@ -199,7 +218,12 @@ function setupIriNavigationHandler(
199
218
  if (!href) {
200
219
  return;
201
220
  }
202
- window.location.assign(href);
221
+ const targetHref = new URL(href, document.baseURI).toString();
222
+ if (window.parent && window.parent !== window) {
223
+ window.parent.postMessage({ type: 'oml-md:navigate', href: targetHref }, '*');
224
+ } else {
225
+ window.location.assign(targetHref);
226
+ }
203
227
  });
204
228
  }
205
229
 
@@ -294,7 +318,7 @@ async function scheduleIriPreview(
294
318
  // Create iframe to show the linked page
295
319
  const iframe = document.createElement('iframe');
296
320
  // Add preview mode parameter to prevent nested previews
297
- const iframeSrc = new URL(href, window.location.href);
321
+ const iframeSrc = new URL(href, document.baseURI);
298
322
  iframeSrc.searchParams.set('_oml_preview', '1');
299
323
  iframe.src = iframeSrc.toString();
300
324
  iframe.style.width = '100%';
@@ -626,9 +650,29 @@ function getScriptSparqlCache(): Record<string, Record<string, QueryResult>> {
626
650
  return parseJsonNode<Record<string, Record<string, QueryResult>>>('oml-md-script-sparql-cache', {});
627
651
  }
628
652
 
629
- const PYTHON_PANEL_CACHE_STORAGE_KEY = 'oml-md-python-panel-cache-v1';
630
- const R_PANEL_CACHE_STORAGE_KEY = 'oml-md-r-panel-cache-v1';
631
- const JS_PANEL_CACHE_STORAGE_KEY = 'oml-md-js-panel-cache-v1';
653
+ function getScriptIncludeCache(): Record<string, string> {
654
+ return parseJsonNode<Record<string, string>>('oml-md-script-include-cache', {});
655
+ }
656
+
657
+ function extractIncludePaths(code: string): string[] {
658
+ const re = /\binclude\s*\(\s*(?:"((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)'|`((?:[^`\\]|\\[\s\S])*)`)\s*\)/g;
659
+ const seen = new Set<string>();
660
+ let m: RegExpExecArray | null;
661
+ while ((m = re.exec(code)) !== null) {
662
+ seen.add(m[1] ?? m[2] ?? m[3]);
663
+ }
664
+ return [...seen];
665
+ }
666
+
667
+ function buildIncludePreamble(code: string, cache: Record<string, string>): string {
668
+ const paths = extractIncludePaths(code);
669
+ const parts = paths.map((p) => cache[p] ?? '').filter(Boolean);
670
+ return parts.length > 0 ? parts.join('\n') + '\n' : '';
671
+ }
672
+
673
+ const PYTHON_PANEL_CACHE_STORAGE_KEY = 'oml-md-python-panel-cache-v2';
674
+ const R_PANEL_CACHE_STORAGE_KEY = 'oml-md-r-panel-cache-v2';
675
+ const JS_PANEL_CACHE_STORAGE_KEY = 'oml-md-js-panel-cache-v2';
632
676
  const MAX_PYTHON_PANEL_CACHE_ENTRIES = 64;
633
677
  const MAX_R_PANEL_CACHE_ENTRIES = 64;
634
678
  const MAX_JS_PANEL_CACHE_ENTRIES = 64;
@@ -737,7 +781,7 @@ function rememberJsPanelCache(key: string, html: string): void {
737
781
 
738
782
  function buildPythonPanelCacheKey(blockId: string, source: string, blockCache: Record<string, QueryResult>): string {
739
783
  const page = `${window.location.origin}${window.location.pathname}${window.location.search}`;
740
- return `python|${page}|${blockId}|${hash32(source)}|${hash32(JSON.stringify(blockCache))}`;
784
+ return `python-v2|${page}|${blockId}|${hash32(source)}|${hash32(JSON.stringify(blockCache))}`;
741
785
  }
742
786
 
743
787
  function buildRPanelCacheKey(blockId: string, source: string, blockCache: Record<string, QueryResult>): string {
@@ -841,7 +885,7 @@ async function executeJsCode(
841
885
  const el = document.createElement('div');
842
886
  el.className = 'oml-md-js-html-output';
843
887
  el.style.color = window.getComputedStyle(document.body).color;
844
- el.innerHTML = html;
888
+ el.innerHTML = expandWikilinksInHtml(html);
845
889
  container.appendChild(el);
846
890
  };
847
891
  const appendText = (text: string): void => {
@@ -852,13 +896,28 @@ async function executeJsCode(
852
896
  container.appendChild(pre);
853
897
  };
854
898
  const display = (content: unknown): void => {
855
- if (typeof content === 'string') { appendHtml(content); } else { appendText(String(content)); }
899
+ if (typeof content === 'string') {
900
+ appendHtml(content);
901
+ } else if (content instanceof Node) {
902
+ clearStatus();
903
+ const el = document.createElement('div');
904
+ el.className = 'oml-md-js-html-output';
905
+ el.style.color = window.getComputedStyle(document.body).color;
906
+ el.appendChild(content);
907
+ container.appendChild(el);
908
+ } else {
909
+ appendText(String(content));
910
+ }
856
911
  };
857
912
  const query = async (sparql: string): Promise<QueryResult> => {
858
913
  return blockCache[sparql] ?? { success: false, columns: [], rows: [], error: 'SPARQL not pre-fetched for this query' };
859
914
  };
860
915
  const table = (result: QueryResult): string => sparqlResultToHtmlTable(result);
861
916
  const load = async (url: string): Promise<void> => loadScript(url);
917
+ const update = async (_ops: unknown): Promise<{ errors?: unknown[] }> => ({});
918
+ const interactive = (_html: string, _ops: unknown): string => '';
919
+ const includeCache = getScriptIncludeCache();
920
+ const includePreamble = buildIncludePreamble(code, includeCache);
862
921
 
863
922
  const savedLog = console.log;
864
923
  const savedWarn = console.warn;
@@ -874,8 +933,10 @@ async function executeJsCode(
874
933
  };
875
934
  try {
876
935
  const AsyncFunction = Object.getPrototypeOf(async function () { /* */ }).constructor as new (...a: string[]) => (...a: unknown[]) => Promise<void>;
877
- const fn = new AsyncFunction('display', 'query', 'table', 'load', code);
878
- await fn(display, query, table, load);
936
+ const include = async (_path: string): Promise<void> => { /* content already prepended */ };
937
+ const finalCode = includePreamble ? includePreamble + code : code;
938
+ const fn = new AsyncFunction('display', 'query', 'table', 'load', 'update', 'interactive', 'include', finalCode);
939
+ await fn(display, query, table, load, update, interactive, include);
879
940
  clearStatus();
880
941
  return {};
881
942
  } catch (error) {
@@ -914,8 +975,11 @@ async function applyJsBlocks(): Promise<void> {
914
975
  const panel = makeScriptPanel();
915
976
  pre.replaceWith(panel);
916
977
  const result = await executeJsCode(source, blockCache, panel);
978
+ if (!panel.isConnected) {
979
+ continue;
980
+ }
917
981
  appendResult(panel, result);
918
- if (!result.error) {
982
+ if (!result.error && !panel.querySelector('.oml-md-js-empty')) {
919
983
  rememberJsPanelCache(cacheKey, panel.innerHTML);
920
984
  }
921
985
  }
@@ -1065,6 +1129,23 @@ def table(data):
1065
1129
  for r in rows
1066
1130
  )
1067
1131
  display(f'<div class="oml-md-table-wrapper"><table class="oml-md-table"><thead><tr>{hdr}</tr></thead><tbody>{bdy}</tbody></table></div>')
1132
+
1133
+ async def update(_operations):
1134
+ return {}
1135
+
1136
+ def interactive(_html, _operations):
1137
+ return ''
1138
+
1139
+ def include(_path):
1140
+ pass
1141
+
1142
+ async def js(code, **kwargs):
1143
+ code = code.replace('{{', '\x01').replace('}}', '\x02')
1144
+ for k, v in kwargs.items():
1145
+ code = code.replace(f'{{{k}}}', str(v))
1146
+ code = code.replace('\x01', '{').replace('\x02', '}')
1147
+ result = await _js_eval(code)
1148
+ return result.to_py() if hasattr(result, 'to_py') else result
1068
1149
  `;
1069
1150
 
1070
1151
  let pyodideExecQueue: Promise<unknown> = Promise.resolve();
@@ -1101,7 +1182,7 @@ async function _executePyCode(
1101
1182
  const el = document.createElement('div');
1102
1183
  el.className = 'oml-md-js-html-output';
1103
1184
  el.style.color = window.getComputedStyle(document.body).color;
1104
- el.innerHTML = html;
1185
+ el.innerHTML = expandWikilinksInHtml(html);
1105
1186
  container.appendChild(el);
1106
1187
  };
1107
1188
  const appendText = (text: string): void => {
@@ -1128,10 +1209,17 @@ async function _executePyCode(
1128
1209
  pyodide.globals.set('_js_display', appendHtml);
1129
1210
  pyodide.globals.set('_js_query', (sparql: string) =>
1130
1211
  blockCache[sparql] ?? { success: false, columns: [], rows: [], error: 'SPARQL not pre-fetched' });
1212
+ pyodide.globals.set('_js_update', async (_opsJson: string) => ({}));
1213
+ pyodide.globals.set('_js_eval', async (code: string) => {
1214
+ // eslint-disable-next-line no-new-func
1215
+ const AsyncFunction = Object.getPrototypeOf(async function () { /* */ }).constructor as new (...a: string[]) => () => Promise<unknown>;
1216
+ return new AsyncFunction(code)();
1217
+ });
1131
1218
  pyodide.setStdout({ batched: appendText });
1132
1219
  pyodide.setStderr({ batched: () => { /* suppress library noise */ } });
1220
+ const preamble = buildIncludePreamble(code, getScriptIncludeCache());
1133
1221
  try {
1134
- await pyodide.runPythonAsync(code);
1222
+ await pyodide.runPythonAsync(preamble ? preamble + code : code);
1135
1223
  clearStatus();
1136
1224
  return {};
1137
1225
  } catch (error) {
@@ -1158,6 +1246,9 @@ async function applyPyBlocks(): Promise<void> {
1158
1246
  const panel = makeScriptPanel();
1159
1247
  pre.replaceWith(panel);
1160
1248
  const result = await executePyCode(source, blockCache, panel);
1249
+ if (!panel.isConnected) {
1250
+ continue;
1251
+ }
1161
1252
  appendResult(panel, result);
1162
1253
  if (!result.error) {
1163
1254
  rememberPythonPanelCache(cacheKey, panel.innerHTML);
@@ -1221,6 +1312,19 @@ load <- function(pkg) {
1221
1312
  if (!requireNamespace(pkg, quietly=TRUE)) { webr::install(pkg) }
1222
1313
  library(pkg, character.only=TRUE)
1223
1314
  }
1315
+ interactive <- function(html, operations) ''
1316
+ include <- function(path) invisible(NULL)
1317
+ js <- function(code, ...) {
1318
+ args <- list(...)
1319
+ code <- gsub('{{', '\x01', code, fixed=TRUE)
1320
+ code <- gsub('}}', '\x02', code, fixed=TRUE)
1321
+ for (nm in names(args)) {
1322
+ code <- gsub(paste0('{', nm, '}'), as.character(args[[nm]]), code, fixed=TRUE)
1323
+ }
1324
+ code <- gsub('\x01', '{', code, fixed=TRUE)
1325
+ code <- gsub('\x02', '}', code, fixed=TRUE)
1326
+ webr::eval_js(paste0('(async()=>{\n', code, '\n})()'))
1327
+ }
1224
1328
  `;
1225
1329
 
1226
1330
  function buildRDataFrameCode(result: QueryResult, varName: string): string {
@@ -1298,7 +1402,7 @@ async function executeRCode(
1298
1402
  const el = document.createElement('div');
1299
1403
  el.className = 'oml-md-js-html-output';
1300
1404
  el.style.color = window.getComputedStyle(document.body).color;
1301
- el.innerHTML = html;
1405
+ el.innerHTML = expandWikilinksInHtml(html);
1302
1406
  container.appendChild(el);
1303
1407
  };
1304
1408
  const appendText = (text: string): void => {
@@ -1327,11 +1431,13 @@ async function executeRCode(
1327
1431
  clearStatus();
1328
1432
  return { error: `R bootstrap failed: ${e instanceof Error ? e.message : String(e)}` };
1329
1433
  }
1434
+ const includePreamble = buildIncludePreamble(code, getScriptIncludeCache());
1330
1435
  let finalCode = code;
1331
1436
  if (prefetchResults.length > 0) {
1332
1437
  const queryMap = new Map(prefetchResults.map((r) => [r.sparql, r.varName] as [string, string]));
1333
1438
  finalCode = prefetchResults.map((r) => r.preamble).join('\n') + '\n' + rewriteRQueryCalls(code, queryMap);
1334
1439
  }
1440
+ if (includePreamble) { finalCode = includePreamble + finalCode; }
1335
1441
  try {
1336
1442
  await webR.evalRVoid(
1337
1443
  '.oml_buf_ <- character(0)\n' +
@@ -1367,6 +1473,9 @@ async function applyRBlocks(): Promise<void> {
1367
1473
  const panel = makeScriptPanel();
1368
1474
  pre.replaceWith(panel);
1369
1475
  const result = await executeRCode(source, blockCache, panel);
1476
+ if (!panel.isConnected) {
1477
+ continue;
1478
+ }
1370
1479
  appendResult(panel, result);
1371
1480
  if (!result.error) {
1372
1481
  rememberRPanelCache(cacheKey, panel.innerHTML);
@@ -8,8 +8,13 @@ export function bindTemplateParameters(
8
8
  explicitArgs: Readonly<Record<string, TemplateValue>> = {}
9
9
  ): TemplateBindResult {
10
10
  const values: Record<string, TemplateValue> = {};
11
- const missingRequired: string[] = [];
12
11
  const parameters = template.parameters ?? [];
12
+ const declaredIds = new Set(parameters.map((p) => p.id));
13
+ for (const argId of Object.keys(explicitArgs)) {
14
+ if (!declaredIds.has(argId)) {
15
+ throw new Error(`Unknown template parameter: ${argId}`);
16
+ }
17
+ }
13
18
  for (const parameter of parameters) {
14
19
  const explicit = explicitArgs[parameter.id];
15
20
  if (explicit !== undefined) {
@@ -22,12 +27,11 @@ export function bindTemplateParameters(
22
27
  continue;
23
28
  }
24
29
  if (parameter.required) {
25
- missingRequired.push(parameter.id);
26
- continue;
30
+ throw new Error(`Missing required template parameter: ${parameter.id}`);
27
31
  }
28
32
  values[parameter.id] = naturalDefaultValue(parameter.type);
29
33
  }
30
- return { values, missingRequired };
34
+ return { values };
31
35
  }
32
36
 
33
37
  function resolveTemplateDefaultValue(
@@ -92,8 +96,9 @@ function resolveContextToken(token: string, context: TemplateInvocationContext):
92
96
  function coerceParameterValue(parameter: TemplateParameterDefinition, value: TemplateValue): TemplateValue {
93
97
  switch (parameter.type) {
94
98
  case 'string':
95
- case 'iri':
96
99
  return coerceString(parameter, value);
100
+ case 'iri':
101
+ return coerceIri(parameter, value);
97
102
  case 'number':
98
103
  return coerceNumber(parameter, value);
99
104
  case 'boolean':
@@ -115,6 +120,18 @@ function coerceString(parameter: TemplateParameterDefinition, value: TemplateVal
115
120
  throw new Error(`Template parameter '${parameter.id}' must be a string.`);
116
121
  }
117
122
 
123
+ function coerceIri(parameter: TemplateParameterDefinition, value: TemplateValue): string {
124
+ if (typeof value !== 'string') {
125
+ throw new Error(`Template parameter '${parameter.id}' must be a valid IRI.`);
126
+ }
127
+ try {
128
+ new URL(value);
129
+ } catch {
130
+ throw new Error(`Template parameter '${parameter.id}' must be a valid IRI, got: ${value}`);
131
+ }
132
+ return value;
133
+ }
134
+
118
135
  function coerceNumber(parameter: TemplateParameterDefinition, value: TemplateValue): number {
119
136
  if (typeof value === 'number' && Number.isFinite(value)) {
120
137
  return value;
@@ -144,18 +161,30 @@ function coerceBoolean(parameter: TemplateParameterDefinition, value: TemplateVa
144
161
  throw new Error(`Template parameter '${parameter.id}' must be a boolean.`);
145
162
  }
146
163
 
164
+ function validateIriArrayEntries(parameter: TemplateParameterDefinition, entries: string[]): string[] {
165
+ for (const entry of entries) {
166
+ try {
167
+ new URL(entry);
168
+ } catch {
169
+ throw new Error(`Template parameter '${parameter.id}' contains an invalid IRI: ${entry}`);
170
+ }
171
+ }
172
+ return entries;
173
+ }
174
+
147
175
  function coerceIriArray(parameter: TemplateParameterDefinition, value: TemplateValue): string[] {
148
176
  if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
149
- return value;
177
+ return validateIriArrayEntries(parameter, value as string[]);
150
178
  }
151
179
  if (typeof value === 'string') {
152
180
  const normalized = value.trim();
153
181
  if (!normalized) {
154
182
  return [];
155
183
  }
156
- return normalized.split(',').map((entry) => entry.trim()).filter((entry) => entry.length > 0);
184
+ const entries = normalized.split(',').map((entry) => entry.trim()).filter((entry) => entry.length > 0);
185
+ return validateIriArrayEntries(parameter, entries);
157
186
  }
158
- throw new Error(`Template parameter '${parameter.id}' must be an array of strings.`);
187
+ throw new Error(`Template parameter '${parameter.id}' must be an array of IRIs.`);
159
188
  }
160
189
 
161
190
  function coerceJson(parameter: TemplateParameterDefinition, value: TemplateValue): TemplateValue {
@@ -6,7 +6,6 @@ import type { TemplateDefinition, TemplateInvocation, TemplateValue } from './ty
6
6
  export interface TemplateRenderResult {
7
7
  output: string;
8
8
  values: Record<string, TemplateValue>;
9
- missingRequired: string[];
10
9
  }
11
10
 
12
11
  export function renderTemplate(
@@ -18,7 +17,6 @@ export function renderTemplate(
18
17
  return {
19
18
  output,
20
19
  values: bound.values,
21
- missingRequired: bound.missingRequired
22
20
  };
23
21
  }
24
22
 
@@ -26,13 +24,14 @@ export function interpolateTemplateBody(
26
24
  source: string,
27
25
  values: Readonly<Record<string, TemplateValue>>
28
26
  ): string {
29
- return source.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match: string, key: string): string => {
27
+ return source.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?:\|([^|}]*)(?:\|([^}]*))?)?\}/g, (_match: string, key: string, delimiter: string | undefined, entryTemplate: string | undefined): string => {
30
28
  if (!(key in values)) {
31
29
  return _match;
32
30
  }
33
31
  const value = values[key];
34
32
  if (Array.isArray(value)) {
35
- return value.map((entry) => String(entry)).join(', ');
33
+ const format = (entry: string) => entryTemplate ? entryTemplate.replace('%s', entry) : entry;
34
+ return value.map((entry) => format(String(entry))).join(delimiter ?? ', ');
36
35
  }
37
36
  if (value === null || value === undefined) {
38
37
  return '';
@@ -97,5 +97,4 @@ export interface TemplateResolutionResult {
97
97
 
98
98
  export interface TemplateBindResult {
99
99
  values: Record<string, TemplateValue>;
100
- missingRequired: string[];
101
100
  }