@oml/markdown 0.16.0 → 0.16.2
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/static/browser-runtime.bundle.js +75 -9
- package/out/static/browser-runtime.bundle.js.map +3 -3
- package/out/static/browser-runtime.js +81 -10
- 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/static/browser-runtime.ts +84 -11
- package/src/template/binder.ts +37 -8
- package/src/template/engine.ts +3 -4
- package/src/template/types.ts +0 -1
|
@@ -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
|
|
|
@@ -626,9 +645,9 @@ function getScriptSparqlCache(): Record<string, Record<string, QueryResult>> {
|
|
|
626
645
|
return parseJsonNode<Record<string, Record<string, QueryResult>>>('oml-md-script-sparql-cache', {});
|
|
627
646
|
}
|
|
628
647
|
|
|
629
|
-
const PYTHON_PANEL_CACHE_STORAGE_KEY = 'oml-md-python-panel-cache-
|
|
630
|
-
const R_PANEL_CACHE_STORAGE_KEY = 'oml-md-r-panel-cache-
|
|
631
|
-
const JS_PANEL_CACHE_STORAGE_KEY = 'oml-md-js-panel-cache-
|
|
648
|
+
const PYTHON_PANEL_CACHE_STORAGE_KEY = 'oml-md-python-panel-cache-v2';
|
|
649
|
+
const R_PANEL_CACHE_STORAGE_KEY = 'oml-md-r-panel-cache-v2';
|
|
650
|
+
const JS_PANEL_CACHE_STORAGE_KEY = 'oml-md-js-panel-cache-v2';
|
|
632
651
|
const MAX_PYTHON_PANEL_CACHE_ENTRIES = 64;
|
|
633
652
|
const MAX_R_PANEL_CACHE_ENTRIES = 64;
|
|
634
653
|
const MAX_JS_PANEL_CACHE_ENTRIES = 64;
|
|
@@ -737,7 +756,7 @@ function rememberJsPanelCache(key: string, html: string): void {
|
|
|
737
756
|
|
|
738
757
|
function buildPythonPanelCacheKey(blockId: string, source: string, blockCache: Record<string, QueryResult>): string {
|
|
739
758
|
const page = `${window.location.origin}${window.location.pathname}${window.location.search}`;
|
|
740
|
-
return `python|${page}|${blockId}|${hash32(source)}|${hash32(JSON.stringify(blockCache))}`;
|
|
759
|
+
return `python-v2|${page}|${blockId}|${hash32(source)}|${hash32(JSON.stringify(blockCache))}`;
|
|
741
760
|
}
|
|
742
761
|
|
|
743
762
|
function buildRPanelCacheKey(blockId: string, source: string, blockCache: Record<string, QueryResult>): string {
|
|
@@ -841,7 +860,7 @@ async function executeJsCode(
|
|
|
841
860
|
const el = document.createElement('div');
|
|
842
861
|
el.className = 'oml-md-js-html-output';
|
|
843
862
|
el.style.color = window.getComputedStyle(document.body).color;
|
|
844
|
-
el.innerHTML = html;
|
|
863
|
+
el.innerHTML = expandWikilinksInHtml(html);
|
|
845
864
|
container.appendChild(el);
|
|
846
865
|
};
|
|
847
866
|
const appendText = (text: string): void => {
|
|
@@ -852,13 +871,26 @@ async function executeJsCode(
|
|
|
852
871
|
container.appendChild(pre);
|
|
853
872
|
};
|
|
854
873
|
const display = (content: unknown): void => {
|
|
855
|
-
if (typeof content === 'string') {
|
|
874
|
+
if (typeof content === 'string') {
|
|
875
|
+
appendHtml(content);
|
|
876
|
+
} else if (content instanceof Node) {
|
|
877
|
+
clearStatus();
|
|
878
|
+
const el = document.createElement('div');
|
|
879
|
+
el.className = 'oml-md-js-html-output';
|
|
880
|
+
el.style.color = window.getComputedStyle(document.body).color;
|
|
881
|
+
el.appendChild(content);
|
|
882
|
+
container.appendChild(el);
|
|
883
|
+
} else {
|
|
884
|
+
appendText(String(content));
|
|
885
|
+
}
|
|
856
886
|
};
|
|
857
887
|
const query = async (sparql: string): Promise<QueryResult> => {
|
|
858
888
|
return blockCache[sparql] ?? { success: false, columns: [], rows: [], error: 'SPARQL not pre-fetched for this query' };
|
|
859
889
|
};
|
|
860
890
|
const table = (result: QueryResult): string => sparqlResultToHtmlTable(result);
|
|
861
891
|
const load = async (url: string): Promise<void> => loadScript(url);
|
|
892
|
+
const update = async (_ops: unknown): Promise<{ errors?: unknown[] }> => ({});
|
|
893
|
+
const interactive = (html: string, _ops: unknown): string => html;
|
|
862
894
|
|
|
863
895
|
const savedLog = console.log;
|
|
864
896
|
const savedWarn = console.warn;
|
|
@@ -874,8 +906,8 @@ async function executeJsCode(
|
|
|
874
906
|
};
|
|
875
907
|
try {
|
|
876
908
|
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);
|
|
909
|
+
const fn = new AsyncFunction('display', 'query', 'table', 'load', 'update', 'interactive', code);
|
|
910
|
+
await fn(display, query, table, load, update, interactive);
|
|
879
911
|
clearStatus();
|
|
880
912
|
return {};
|
|
881
913
|
} catch (error) {
|
|
@@ -914,6 +946,9 @@ async function applyJsBlocks(): Promise<void> {
|
|
|
914
946
|
const panel = makeScriptPanel();
|
|
915
947
|
pre.replaceWith(panel);
|
|
916
948
|
const result = await executeJsCode(source, blockCache, panel);
|
|
949
|
+
if (!panel.isConnected) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
917
952
|
appendResult(panel, result);
|
|
918
953
|
if (!result.error) {
|
|
919
954
|
rememberJsPanelCache(cacheKey, panel.innerHTML);
|
|
@@ -1065,6 +1100,20 @@ def table(data):
|
|
|
1065
1100
|
for r in rows
|
|
1066
1101
|
)
|
|
1067
1102
|
display(f'<div class="oml-md-table-wrapper"><table class="oml-md-table"><thead><tr>{hdr}</tr></thead><tbody>{bdy}</tbody></table></div>')
|
|
1103
|
+
|
|
1104
|
+
async def update(_operations):
|
|
1105
|
+
return {}
|
|
1106
|
+
|
|
1107
|
+
def interactive(html, _operations):
|
|
1108
|
+
return str(html)
|
|
1109
|
+
|
|
1110
|
+
async def js(code, **kwargs):
|
|
1111
|
+
code = code.replace('{{', '\x01').replace('}}', '\x02')
|
|
1112
|
+
for k, v in kwargs.items():
|
|
1113
|
+
code = code.replace(f'{{{k}}}', str(v))
|
|
1114
|
+
code = code.replace('\x01', '{').replace('\x02', '}')
|
|
1115
|
+
result = await _js_eval(code)
|
|
1116
|
+
return result.to_py() if hasattr(result, 'to_py') else result
|
|
1068
1117
|
`;
|
|
1069
1118
|
|
|
1070
1119
|
let pyodideExecQueue: Promise<unknown> = Promise.resolve();
|
|
@@ -1101,7 +1150,7 @@ async function _executePyCode(
|
|
|
1101
1150
|
const el = document.createElement('div');
|
|
1102
1151
|
el.className = 'oml-md-js-html-output';
|
|
1103
1152
|
el.style.color = window.getComputedStyle(document.body).color;
|
|
1104
|
-
el.innerHTML = html;
|
|
1153
|
+
el.innerHTML = expandWikilinksInHtml(html);
|
|
1105
1154
|
container.appendChild(el);
|
|
1106
1155
|
};
|
|
1107
1156
|
const appendText = (text: string): void => {
|
|
@@ -1128,6 +1177,12 @@ async function _executePyCode(
|
|
|
1128
1177
|
pyodide.globals.set('_js_display', appendHtml);
|
|
1129
1178
|
pyodide.globals.set('_js_query', (sparql: string) =>
|
|
1130
1179
|
blockCache[sparql] ?? { success: false, columns: [], rows: [], error: 'SPARQL not pre-fetched' });
|
|
1180
|
+
pyodide.globals.set('_js_update', async (_opsJson: string) => ({}));
|
|
1181
|
+
pyodide.globals.set('_js_eval', async (code: string) => {
|
|
1182
|
+
// eslint-disable-next-line no-new-func
|
|
1183
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { /* */ }).constructor as new (...a: string[]) => () => Promise<unknown>;
|
|
1184
|
+
return new AsyncFunction(code)();
|
|
1185
|
+
});
|
|
1131
1186
|
pyodide.setStdout({ batched: appendText });
|
|
1132
1187
|
pyodide.setStderr({ batched: () => { /* suppress library noise */ } });
|
|
1133
1188
|
try {
|
|
@@ -1158,6 +1213,9 @@ async function applyPyBlocks(): Promise<void> {
|
|
|
1158
1213
|
const panel = makeScriptPanel();
|
|
1159
1214
|
pre.replaceWith(panel);
|
|
1160
1215
|
const result = await executePyCode(source, blockCache, panel);
|
|
1216
|
+
if (!panel.isConnected) {
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1161
1219
|
appendResult(panel, result);
|
|
1162
1220
|
if (!result.error) {
|
|
1163
1221
|
rememberPythonPanelCache(cacheKey, panel.innerHTML);
|
|
@@ -1221,6 +1279,18 @@ load <- function(pkg) {
|
|
|
1221
1279
|
if (!requireNamespace(pkg, quietly=TRUE)) { webr::install(pkg) }
|
|
1222
1280
|
library(pkg, character.only=TRUE)
|
|
1223
1281
|
}
|
|
1282
|
+
interactive <- function(html, operations) as.character(html)
|
|
1283
|
+
js <- function(code, ...) {
|
|
1284
|
+
args <- list(...)
|
|
1285
|
+
code <- gsub('{{', '\x01', code, fixed=TRUE)
|
|
1286
|
+
code <- gsub('}}', '\x02', code, fixed=TRUE)
|
|
1287
|
+
for (nm in names(args)) {
|
|
1288
|
+
code <- gsub(paste0('{', nm, '}'), as.character(args[[nm]]), code, fixed=TRUE)
|
|
1289
|
+
}
|
|
1290
|
+
code <- gsub('\x01', '{', code, fixed=TRUE)
|
|
1291
|
+
code <- gsub('\x02', '}', code, fixed=TRUE)
|
|
1292
|
+
webr::eval_js(paste0('(async()=>{\n', code, '\n})()'))
|
|
1293
|
+
}
|
|
1224
1294
|
`;
|
|
1225
1295
|
|
|
1226
1296
|
function buildRDataFrameCode(result: QueryResult, varName: string): string {
|
|
@@ -1298,7 +1368,7 @@ async function executeRCode(
|
|
|
1298
1368
|
const el = document.createElement('div');
|
|
1299
1369
|
el.className = 'oml-md-js-html-output';
|
|
1300
1370
|
el.style.color = window.getComputedStyle(document.body).color;
|
|
1301
|
-
el.innerHTML = html;
|
|
1371
|
+
el.innerHTML = expandWikilinksInHtml(html);
|
|
1302
1372
|
container.appendChild(el);
|
|
1303
1373
|
};
|
|
1304
1374
|
const appendText = (text: string): void => {
|
|
@@ -1367,6 +1437,9 @@ async function applyRBlocks(): Promise<void> {
|
|
|
1367
1437
|
const panel = makeScriptPanel();
|
|
1368
1438
|
pre.replaceWith(panel);
|
|
1369
1439
|
const result = await executeRCode(source, blockCache, panel);
|
|
1440
|
+
if (!panel.isConnected) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1370
1443
|
appendResult(panel, result);
|
|
1371
1444
|
if (!result.error) {
|
|
1372
1445
|
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 '';
|