@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.
- package/out/renderers/diagram-renderer.js +12 -1
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +125 -14
- package/out/static/browser-runtime.bundle.js.map +3 -3
- package/out/static/browser-runtime.js +121 -14
- package/out/static/browser-runtime.js.map +1 -1
- package/out/template/binder.js +37 -8
- package/out/template/binder.js.map +1 -1
- package/out/template/engine.d.ts +0 -1
- package/out/template/engine.js +3 -3
- package/out/template/engine.js.map +1 -1
- package/out/template/types.d.ts +0 -1
- package/package.json +2 -2
- package/src/renderers/diagram-renderer.ts +11 -1
- package/src/static/browser-runtime.ts +124 -15
- package/src/template/binder.ts +37 -8
- package/src/template/engine.ts +3 -4
- package/src/template/types.ts +0 -1
package/out/template/engine.js
CHANGED
|
@@ -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_]*)
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/out/template/types.d.ts
CHANGED
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.
|
|
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.
|
|
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
|
-
|
|
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, '<').replace(/>/g, '>');
|
|
15
|
+
}
|
|
16
|
+
const trimmed = (iri ?? '').trim();
|
|
17
|
+
if (!trimmed) return match;
|
|
18
|
+
const label = displayLabelFromIri(trimmed);
|
|
19
|
+
const safeIri = trimmed.replace(/&/g, '&').replace(/"/g, '"');
|
|
20
|
+
const safeLabel = label.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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') {
|
|
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
|
|
878
|
-
|
|
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);
|
package/src/template/binder.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
package/src/template/engine.ts
CHANGED
|
@@ -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_]*)
|
|
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
|
-
|
|
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 '';
|