@oml/markdown 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +39 -0
  2. package/out/index.d.ts +2 -0
  3. package/out/index.js +4 -0
  4. package/out/index.js.map +1 -0
  5. package/out/md/index.d.ts +6 -0
  6. package/out/md/index.js +8 -0
  7. package/out/md/index.js.map +1 -0
  8. package/out/md/md-execution.d.ts +33 -0
  9. package/out/md/md-execution.js +3 -0
  10. package/out/md/md-execution.js.map +1 -0
  11. package/out/md/md-executor.d.ts +21 -0
  12. package/out/md/md-executor.js +498 -0
  13. package/out/md/md-executor.js.map +1 -0
  14. package/out/md/md-frontmatter.d.ts +4 -0
  15. package/out/md/md-frontmatter.js +48 -0
  16. package/out/md/md-frontmatter.js.map +1 -0
  17. package/out/md/md-registry.d.ts +7 -0
  18. package/out/md/md-registry.js +19 -0
  19. package/out/md/md-registry.js.map +1 -0
  20. package/out/md/md-runtime.d.ts +10 -0
  21. package/out/md/md-runtime.js +166 -0
  22. package/out/md/md-runtime.js.map +1 -0
  23. package/out/md/md-types.d.ts +40 -0
  24. package/out/md/md-types.js +3 -0
  25. package/out/md/md-types.js.map +1 -0
  26. package/out/md/md-yaml.d.ts +1 -0
  27. package/out/md/md-yaml.js +15 -0
  28. package/out/md/md-yaml.js.map +1 -0
  29. package/out/renderers/chart-renderer.d.ts +6 -0
  30. package/out/renderers/chart-renderer.js +392 -0
  31. package/out/renderers/chart-renderer.js.map +1 -0
  32. package/out/renderers/diagram-renderer.d.ts +7 -0
  33. package/out/renderers/diagram-renderer.js +2354 -0
  34. package/out/renderers/diagram-renderer.js.map +1 -0
  35. package/out/renderers/graph-renderer.d.ts +6 -0
  36. package/out/renderers/graph-renderer.js +1384 -0
  37. package/out/renderers/graph-renderer.js.map +1 -0
  38. package/out/renderers/index.d.ts +14 -0
  39. package/out/renderers/index.js +16 -0
  40. package/out/renderers/index.js.map +1 -0
  41. package/out/renderers/list-renderer.d.ts +6 -0
  42. package/out/renderers/list-renderer.js +252 -0
  43. package/out/renderers/list-renderer.js.map +1 -0
  44. package/out/renderers/matrix-renderer.d.ts +14 -0
  45. package/out/renderers/matrix-renderer.js +498 -0
  46. package/out/renderers/matrix-renderer.js.map +1 -0
  47. package/out/renderers/message-renderer.d.ts +6 -0
  48. package/out/renderers/message-renderer.js +14 -0
  49. package/out/renderers/message-renderer.js.map +1 -0
  50. package/out/renderers/registry.d.ts +9 -0
  51. package/out/renderers/registry.js +41 -0
  52. package/out/renderers/registry.js.map +1 -0
  53. package/out/renderers/renderer.d.ts +28 -0
  54. package/out/renderers/renderer.js +61 -0
  55. package/out/renderers/renderer.js.map +1 -0
  56. package/out/renderers/table-editor-renderer.d.ts +4 -0
  57. package/out/renderers/table-editor-renderer.js +9 -0
  58. package/out/renderers/table-editor-renderer.js.map +1 -0
  59. package/out/renderers/table-renderer.d.ts +95 -0
  60. package/out/renderers/table-renderer.js +1571 -0
  61. package/out/renderers/table-renderer.js.map +1 -0
  62. package/out/renderers/text-renderer.d.ts +7 -0
  63. package/out/renderers/text-renderer.js +219 -0
  64. package/out/renderers/text-renderer.js.map +1 -0
  65. package/out/renderers/tree-renderer.d.ts +4 -0
  66. package/out/renderers/tree-renderer.js +9 -0
  67. package/out/renderers/tree-renderer.js.map +1 -0
  68. package/out/renderers/types.d.ts +18 -0
  69. package/out/renderers/types.js +3 -0
  70. package/out/renderers/types.js.map +1 -0
  71. package/out/renderers/wikilink-utils.d.ts +6 -0
  72. package/out/renderers/wikilink-utils.js +100 -0
  73. package/out/renderers/wikilink-utils.js.map +1 -0
  74. package/out/static/browser-runtime.bundle.js +74155 -0
  75. package/out/static/browser-runtime.bundle.js.map +7 -0
  76. package/out/static/browser-runtime.d.ts +1 -0
  77. package/out/static/browser-runtime.js +218 -0
  78. package/out/static/browser-runtime.js.map +1 -0
  79. package/out/static/index.d.ts +1 -0
  80. package/out/static/index.js +3 -0
  81. package/out/static/index.js.map +1 -0
  82. package/out/static/runtime-assets.d.ts +2 -0
  83. package/out/static/runtime-assets.js +174 -0
  84. package/out/static/runtime-assets.js.map +1 -0
  85. package/package.json +74 -0
  86. package/src/index.ts +4 -0
  87. package/src/md/index.ts +8 -0
  88. package/src/md/md-execution.ts +51 -0
  89. package/src/md/md-executor.ts +598 -0
  90. package/src/md/md-frontmatter.ts +53 -0
  91. package/src/md/md-registry.ts +22 -0
  92. package/src/md/md-runtime.ts +191 -0
  93. package/src/md/md-types.ts +48 -0
  94. package/src/md/md-yaml.ts +17 -0
  95. package/src/renderers/chart-renderer.ts +473 -0
  96. package/src/renderers/diagram-renderer.ts +2520 -0
  97. package/src/renderers/graph-renderer.ts +1653 -0
  98. package/src/renderers/index.ts +16 -0
  99. package/src/renderers/list-renderer.ts +289 -0
  100. package/src/renderers/matrix-renderer.ts +616 -0
  101. package/src/renderers/message-renderer.ts +18 -0
  102. package/src/renderers/registry.ts +45 -0
  103. package/src/renderers/renderer.ts +84 -0
  104. package/src/renderers/table-editor-renderer.ts +8 -0
  105. package/src/renderers/table-renderer.ts +1868 -0
  106. package/src/renderers/text-renderer.ts +252 -0
  107. package/src/renderers/tree-renderer.ts +7 -0
  108. package/src/renderers/types.ts +22 -0
  109. package/src/renderers/wikilink-utils.ts +108 -0
  110. package/src/static/browser-runtime.ts +249 -0
  111. package/src/static/index.ts +3 -0
  112. package/src/static/runtime-assets.ts +175 -0
@@ -0,0 +1,598 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import { deriveSelectQueryFromShape, type OwlConstructResult, type OwlQueryResult } from '@oml/owl';
4
+ import type { Term } from '@rdfjs/types';
5
+ import type { MdBlockExecutionResult, MdExecuteBlocksParams, MdExecuteBlocksResult } from './md-execution.js';
6
+
7
+ export interface MarkdownExecutionBackend {
8
+ ensureContext(modelUri: string): Promise<void>;
9
+ resolveContextIri(modelUri: string): string;
10
+ countContextQuads?: (modelUri: string) => Promise<number>;
11
+ query(modelUri: string, sparql: string): Promise<OwlQueryResult>;
12
+ construct(modelUri: string, sparql: string): Promise<OwlConstructResult>;
13
+ }
14
+
15
+ export class MarkdownExecutor {
16
+ constructor(private readonly backend: MarkdownExecutionBackend) {}
17
+
18
+ async executeBlocks(params: MdExecuteBlocksParams): Promise<MdExecuteBlocksResult> {
19
+ const resolvedModelUri = params.modelUri?.trim() || undefined;
20
+ const contextQuadCount = resolvedModelUri && this.backend.countContextQuads
21
+ ? await this.backend.countContextQuads(resolvedModelUri)
22
+ : undefined;
23
+
24
+ if (resolvedModelUri) {
25
+ await this.backend.ensureContext(resolvedModelUri);
26
+ }
27
+
28
+ const results: MdBlockExecutionResult[] = [];
29
+ for (const block of params.blocks) {
30
+ try {
31
+ if (block.kind === 'table' || block.kind === 'table-editor' || block.kind === 'chart') {
32
+ results.push(await this.executeTableBlock(block.kind, resolvedModelUri, block.id, block.source, block.options));
33
+ continue;
34
+ }
35
+ if (block.kind === 'matrix') {
36
+ results.push(await this.executeMatrixBlock(resolvedModelUri, block.id, block.source));
37
+ continue;
38
+ }
39
+ if (block.kind === 'tree') {
40
+ results.push(await this.executeTreeBlock(resolvedModelUri, block.id, block.source, block.options));
41
+ continue;
42
+ }
43
+ if (block.kind === 'graph') {
44
+ results.push(await this.executeGraphBlock(resolvedModelUri, block.id, block.source));
45
+ continue;
46
+ }
47
+ if (block.kind === 'diagram') {
48
+ results.push(await this.executeDiagramBlock(resolvedModelUri, block.id, block.source));
49
+ continue;
50
+ }
51
+ if (block.kind === 'text') {
52
+ results.push(await this.executeTextBlock(resolvedModelUri, block.id, block.source));
53
+ continue;
54
+ }
55
+ if (block.kind === 'list') {
56
+ results.push(await this.executeListBlock(resolvedModelUri, block.id, block.source));
57
+ continue;
58
+ }
59
+ results.push({
60
+ blockId: block.id,
61
+ kind: block.kind,
62
+ status: 'unimplemented',
63
+ format: 'message',
64
+ message: `${block.kind} blocks are not implemented yet.`
65
+ });
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ results.push({
69
+ blockId: block.id,
70
+ kind: block.kind,
71
+ status: 'error',
72
+ format: 'message',
73
+ message: message || `${block.kind} block execution failed.`
74
+ });
75
+ }
76
+ }
77
+ return { results, contextQuadCount };
78
+ }
79
+
80
+ private async executeMatrixBlock(
81
+ modelUri: string | undefined,
82
+ blockId: string,
83
+ source: string
84
+ ): Promise<MdBlockExecutionResult> {
85
+ if (!modelUri) {
86
+ return missingContext(blockId, 'matrix');
87
+ }
88
+
89
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
90
+ const queryResult = await this.backend.query(modelUri, sparql);
91
+ if (!queryResult.success) {
92
+ return queryFailed(blockId, 'matrix', queryResult.error ?? 'SPARQL SELECT failed.');
93
+ }
94
+
95
+ const columns = extractSelectVariables(sparql);
96
+ const columnSet = new Set<string>(columns);
97
+ for (const row of queryResult.rows) {
98
+ for (const key of row.keys()) {
99
+ if (columnSet.has(key)) {
100
+ continue;
101
+ }
102
+ columnSet.add(key);
103
+ columns.push(key);
104
+ }
105
+ }
106
+
107
+ const rows = queryResult.rows.map((row) => columns.map((column) => row.get(column)?.value ?? ''));
108
+ return okTable(blockId, 'matrix', columns, rows);
109
+ }
110
+
111
+ private async executeTableBlock(
112
+ kind: 'table' | 'table-editor' | 'chart',
113
+ modelUri: string | undefined,
114
+ blockId: string,
115
+ source: string,
116
+ options?: Record<string, unknown>
117
+ ): Promise<MdBlockExecutionResult> {
118
+ if (!modelUri) {
119
+ return missingContext(blockId, kind);
120
+ }
121
+
122
+ const preparedSource = preprocessQuerySource(this.backend, modelUri, source);
123
+ const ownGraphIri = this.backend.resolveContextIri(modelUri);
124
+ const derivedQuery = kind === 'table-editor'
125
+ ? deriveSelectQueryFromShape(preparedSource, ownGraphIri)
126
+ : undefined;
127
+ const sparql = kind === 'table-editor'
128
+ ? ensureOrderedByFirstVariable(derivedQuery?.sparql ?? preparedSource, derivedQuery?.columnVariables?.[0])
129
+ : (derivedQuery?.sparql ?? preparedSource);
130
+ if (kind === 'table-editor' && !derivedQuery) {
131
+ return queryFailed(blockId, kind, 'Unable to derive SPARQL SELECT query from table-editor block shape.');
132
+ }
133
+
134
+ const queryResult = await this.backend.query(modelUri, sparql);
135
+ if (!queryResult.success) {
136
+ return queryFailed(blockId, kind, queryResult.error ?? 'SPARQL SELECT failed.');
137
+ }
138
+
139
+ const variableColumns = kind === 'table-editor'
140
+ ? derivedQuery?.columnVariables.slice() ?? []
141
+ : extractSelectVariables(sparql);
142
+ const labelByVariable = kind === 'table-editor'
143
+ ? (derivedQuery?.columnLabels ?? new Map<string, string>())
144
+ : new Map<string, string>();
145
+ const optionLabelByVariable = extractColumnLabelOverrides(options);
146
+ for (const [variable, label] of optionLabelByVariable.entries()) {
147
+ labelByVariable.set(variable, label);
148
+ }
149
+ const columns = variableColumns;
150
+ const columnSet = new Set<string>(columns);
151
+ for (const row of queryResult.rows) {
152
+ for (const key of row.keys()) {
153
+ if (columnSet.has(key)) {
154
+ continue;
155
+ }
156
+ columnSet.add(key);
157
+ columns.push(key);
158
+ }
159
+ }
160
+
161
+ const rows = queryResult.rows.map((row) => columns.map((column) => row.get(column)?.value ?? ''));
162
+ const displayColumns = columns.map((column) => labelByVariable.get(column) ?? column);
163
+ return okTable(blockId, kind, displayColumns, rows);
164
+ }
165
+
166
+ private async executeGraphBlock(
167
+ modelUri: string | undefined,
168
+ blockId: string,
169
+ source: string
170
+ ): Promise<MdBlockExecutionResult> {
171
+ if (!modelUri) {
172
+ return missingContext(blockId, 'graph');
173
+ }
174
+
175
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
176
+ const constructResult = await this.backend.construct(modelUri, sparql);
177
+ if (!constructResult.success) {
178
+ return queryFailed(blockId, 'graph', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
179
+ }
180
+
181
+ return constructRowsResult(blockId, 'graph', constructResult);
182
+ }
183
+
184
+ private async executeDiagramBlock(
185
+ modelUri: string | undefined,
186
+ blockId: string,
187
+ source: string
188
+ ): Promise<MdBlockExecutionResult> {
189
+ if (!modelUri) {
190
+ return missingContext(blockId, 'diagram');
191
+ }
192
+
193
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
194
+ const constructResult = await this.backend.construct(modelUri, sparql);
195
+ if (!constructResult.success) {
196
+ return queryFailed(blockId, 'diagram', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
197
+ }
198
+
199
+ return constructRowsResult(blockId, 'diagram', constructResult);
200
+ }
201
+
202
+ private async executeTreeBlock(
203
+ modelUri: string | undefined,
204
+ blockId: string,
205
+ source: string,
206
+ options?: Record<string, unknown>
207
+ ): Promise<MdBlockExecutionResult> {
208
+ if (!modelUri) {
209
+ return missingContext(blockId, 'tree');
210
+ }
211
+
212
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
213
+ const constructResult = await this.backend.construct(modelUri, sparql);
214
+ if (!constructResult.success) {
215
+ return queryFailed(blockId, 'tree', constructResult.error ?? 'SPARQL CONSTRUCT failed.');
216
+ }
217
+
218
+ const containmentNames = extractContainmentPropertyNames(options);
219
+ const focusOrder: string[] = [];
220
+ const focusSet = new Set<string>();
221
+ const valuesByFocus = new Map<string, Map<string, string[]>>();
222
+ const predicateToColumn = new Map<string, string>();
223
+ const usedColumns = new Set<string>(['focus']);
224
+ const columnOrder: string[] = [];
225
+ const columnLabelByVariable = new Map<string, string>([['focus', 'focus']]);
226
+
227
+ const addFocus = (focus: string): void => {
228
+ if (focusSet.has(focus)) {
229
+ return;
230
+ }
231
+ focusSet.add(focus);
232
+ focusOrder.push(focus);
233
+ valuesByFocus.set(focus, new Map<string, string[]>());
234
+ };
235
+ const addCellValue = (focus: string, column: string, value: string): void => {
236
+ const byColumn = valuesByFocus.get(focus);
237
+ if (!byColumn) {
238
+ return;
239
+ }
240
+ const values = byColumn.get(column) ?? [];
241
+ if (!values.includes(value)) {
242
+ values.push(value);
243
+ byColumn.set(column, values);
244
+ }
245
+ };
246
+
247
+ for (const quad of constructResult.quads) {
248
+ const focus = termToCellValue(quad.subject as Term);
249
+ addFocus(focus);
250
+
251
+ const predicateIri = quad.predicate.value;
252
+ let column = predicateToColumn.get(predicateIri);
253
+ if (!column) {
254
+ const base = normalizeSparqlVariableName(localName(predicateIri) || 'value');
255
+ column = uniqueVariableName(base, usedColumns);
256
+ usedColumns.add(column);
257
+ predicateToColumn.set(predicateIri, column);
258
+ columnOrder.push(column);
259
+ columnLabelByVariable.set(column, localName(predicateIri) || column);
260
+ }
261
+
262
+ const objectValue = termToCellValue(quad.object as Term);
263
+ addCellValue(focus, column, objectValue);
264
+
265
+ if (containmentNames.size > 0
266
+ && (quad.object.termType === 'NamedNode' || quad.object.termType === 'BlankNode')
267
+ && containmentNames.has(normalizePropertyIdentifier(predicateIri))) {
268
+ addFocus(objectValue);
269
+ }
270
+ }
271
+
272
+ const optionLabelByVariable = extractColumnLabelOverrides(options);
273
+ for (const [variable, label] of optionLabelByVariable.entries()) {
274
+ columnLabelByVariable.set(variable, label);
275
+ }
276
+
277
+ const columns = ['focus', ...columnOrder];
278
+ const displayColumns = columns.map((column) => columnLabelByVariable.get(column) ?? column);
279
+ const rows = focusOrder.map((focus) => {
280
+ const byColumn = valuesByFocus.get(focus) ?? new Map<string, string[]>();
281
+ return columns.map((column) => {
282
+ if (column === 'focus') {
283
+ return focus;
284
+ }
285
+ return (byColumn.get(column) ?? []).join(', ');
286
+ });
287
+ });
288
+ return okTable(blockId, 'tree', displayColumns, rows);
289
+ }
290
+
291
+ private async executeTextBlock(
292
+ modelUri: string | undefined,
293
+ blockId: string,
294
+ source: string
295
+ ): Promise<MdBlockExecutionResult> {
296
+ if (!modelUri) {
297
+ return missingContext(blockId, 'text');
298
+ }
299
+
300
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
301
+ const queryResult = await this.backend.query(modelUri, sparql);
302
+ if (!queryResult.success) {
303
+ return queryFailed(blockId, 'text', queryResult.error ?? 'SPARQL SELECT failed.');
304
+ }
305
+
306
+ const selected = extractSelectVariables(sparql);
307
+ const firstVariable = selected[0] ?? queryResult.rows[0]?.keys().next().value;
308
+ if (!firstVariable) {
309
+ return queryFailed(blockId, 'text', 'Text block requires at least one SELECT variable.');
310
+ }
311
+
312
+ const columns = [firstVariable];
313
+ const columnSet = new Set<string>(columns);
314
+ for (const row of queryResult.rows) {
315
+ for (const key of row.keys()) {
316
+ if (columnSet.has(key)) {
317
+ continue;
318
+ }
319
+ columnSet.add(key);
320
+ columns.push(key);
321
+ }
322
+ }
323
+ const rows = queryResult.rows.map((row) => columns.map((column) => row.get(column)?.value ?? ''));
324
+ return okTable(blockId, 'text', columns, rows);
325
+ }
326
+
327
+ private async executeListBlock(
328
+ modelUri: string | undefined,
329
+ blockId: string,
330
+ source: string
331
+ ): Promise<MdBlockExecutionResult> {
332
+ if (!modelUri) {
333
+ return missingContext(blockId, 'list');
334
+ }
335
+
336
+ const sparql = preprocessQuerySource(this.backend, modelUri, source);
337
+ const queryResult = await this.backend.query(modelUri, sparql);
338
+ if (!queryResult.success) {
339
+ return queryFailed(blockId, 'list', queryResult.error ?? 'SPARQL SELECT failed.');
340
+ }
341
+
342
+ const selected = extractSelectVariables(sparql);
343
+ const firstVariable = selected[0] ?? queryResult.rows[0]?.keys().next().value;
344
+ if (!firstVariable) {
345
+ return queryFailed(blockId, 'list', 'List block requires at least one SELECT variable.');
346
+ }
347
+
348
+ const columns = [firstVariable];
349
+ const columnSet = new Set<string>(columns);
350
+ for (const row of queryResult.rows) {
351
+ for (const key of row.keys()) {
352
+ if (columnSet.has(key)) {
353
+ continue;
354
+ }
355
+ columnSet.add(key);
356
+ columns.push(key);
357
+ }
358
+ }
359
+ const rows = queryResult.rows.map((row) => columns.map((column) => row.get(column)?.value ?? ''));
360
+ return okTable(blockId, 'list', columns, rows);
361
+ }
362
+ }
363
+
364
+ function preprocessQuerySource(backend: MarkdownExecutionBackend, modelUri: string, source: string): string {
365
+ if (!source.includes('${contextIri}')) {
366
+ return source;
367
+ }
368
+ return source.split('${contextIri}').join(backend.resolveContextIri(modelUri));
369
+ }
370
+
371
+ function missingContext(blockId: string, kind: MdBlockExecutionResult['kind']): MdBlockExecutionResult {
372
+ return queryFailed(blockId, kind, 'No contextUri model is available for this markdown document.');
373
+ }
374
+
375
+ function queryFailed(blockId: string, kind: MdBlockExecutionResult['kind'], message: string): MdBlockExecutionResult {
376
+ return {
377
+ blockId,
378
+ kind,
379
+ status: 'error',
380
+ format: 'message',
381
+ message,
382
+ };
383
+ }
384
+
385
+ function okTable(blockId: string, kind: MdBlockExecutionResult['kind'], columns: string[], rows: string[][]): MdBlockExecutionResult {
386
+ return {
387
+ blockId,
388
+ kind,
389
+ status: 'ok',
390
+ format: 'table',
391
+ payload: { columns, rows },
392
+ };
393
+ }
394
+
395
+ function constructRowsResult(
396
+ blockId: string,
397
+ kind: 'graph' | 'diagram',
398
+ constructResult: OwlConstructResult
399
+ ): MdBlockExecutionResult {
400
+ return okTable(
401
+ blockId,
402
+ kind,
403
+ ['subject', 'predicate', 'object'],
404
+ constructResult.quads.map((quad) => [
405
+ termToCellValue(quad.subject as Term),
406
+ termToCellValue(quad.predicate as Term),
407
+ termToCellValue(quad.object as Term),
408
+ ]),
409
+ );
410
+ }
411
+
412
+ function extractSelectVariables(sparql: string): string[] {
413
+ const match = /select\s+(?:distinct\s+|reduced\s+)?([\s\S]*?)\bwhere\b/i.exec(sparql);
414
+ if (!match) {
415
+ return [];
416
+ }
417
+ const variables: string[] = [];
418
+ for (const term of splitTopLevelProjectionTerms(match[1])) {
419
+ const trimmed = term.trim();
420
+ if (!trimmed) {
421
+ continue;
422
+ }
423
+ const directVariable = /^\?([A-Za-z_][A-Za-z0-9_-]*)$/i.exec(trimmed);
424
+ if (directVariable) {
425
+ variables.push(`?${directVariable[1]}`);
426
+ continue;
427
+ }
428
+ const alias = /\bas\s+\?([A-Za-z_][A-Za-z0-9_-]*)\b/i.exec(trimmed);
429
+ if (alias) {
430
+ variables.push(`?${alias[1]}`);
431
+ }
432
+ }
433
+ const seen = new Set<string>();
434
+ const result: string[] = [];
435
+ for (const variable of variables) {
436
+ const name = variable.slice(1);
437
+ if (seen.has(name)) {
438
+ continue;
439
+ }
440
+ seen.add(name);
441
+ result.push(name);
442
+ }
443
+ return result;
444
+ }
445
+
446
+ function splitTopLevelProjectionTerms(rawProjection: string): string[] {
447
+ const terms: string[] = [];
448
+ let current = '';
449
+ let depth = 0;
450
+ let quote: '"' | '\'' | undefined;
451
+ for (let index = 0; index < rawProjection.length; index += 1) {
452
+ const char = rawProjection[index];
453
+ if (quote) {
454
+ current += char;
455
+ if (char === quote && rawProjection[index - 1] !== '\\') {
456
+ quote = undefined;
457
+ }
458
+ continue;
459
+ }
460
+ if (char === '"' || char === '\'') {
461
+ quote = char;
462
+ current += char;
463
+ continue;
464
+ }
465
+ if (char === '(') {
466
+ depth += 1;
467
+ current += char;
468
+ continue;
469
+ }
470
+ if (char === ')') {
471
+ depth = Math.max(0, depth - 1);
472
+ current += char;
473
+ continue;
474
+ }
475
+ if (/\s/.test(char) && depth === 0) {
476
+ if (current.trim()) {
477
+ terms.push(current.trim());
478
+ current = '';
479
+ }
480
+ continue;
481
+ }
482
+ current += char;
483
+ }
484
+ if (current.trim()) {
485
+ terms.push(current.trim());
486
+ }
487
+ return terms;
488
+ }
489
+
490
+ function ensureOrderedByFirstVariable(sparql: string, firstVariable: string | undefined): string {
491
+ const variable = (firstVariable ?? '').trim().replace(/^\?/, '');
492
+ if (!variable || /\border\s+by\b/i.test(sparql)) {
493
+ return sparql;
494
+ }
495
+ return `${sparql.trimEnd()}\nORDER BY ?${variable}`;
496
+ }
497
+
498
+ function extractColumnLabelOverrides(options: Record<string, unknown> | undefined): Map<string, string> {
499
+ const labels = new Map<string, string>();
500
+ const columns = options?.columns;
501
+ if (!isPlainObject(columns)) {
502
+ return labels;
503
+ }
504
+ for (const [variable, rawConfig] of Object.entries(columns)) {
505
+ if (typeof rawConfig === 'string') {
506
+ labels.set(variable, rawConfig);
507
+ continue;
508
+ }
509
+ if (!isPlainObject(rawConfig)) {
510
+ continue;
511
+ }
512
+ const label = rawConfig.label;
513
+ if (typeof label === 'string' && label.trim().length > 0) {
514
+ labels.set(variable, label);
515
+ }
516
+ }
517
+ return labels;
518
+ }
519
+
520
+ function extractContainmentPropertyNames(options: Record<string, unknown> | undefined): Set<string> {
521
+ const names = new Set<string>();
522
+ const containment = options?.containment;
523
+ if (!Array.isArray(containment)) {
524
+ return names;
525
+ }
526
+ for (const entry of containment) {
527
+ if (typeof entry !== 'string') {
528
+ continue;
529
+ }
530
+ const normalized = normalizePropertyIdentifier(entry);
531
+ if (normalized) {
532
+ names.add(normalized);
533
+ }
534
+ }
535
+ return names;
536
+ }
537
+
538
+ function normalizePropertyIdentifier(raw: string): string {
539
+ const trimmed = raw.trim();
540
+ if (!trimmed) {
541
+ return '';
542
+ }
543
+ if (trimmed.includes(':')) {
544
+ return trimmed.slice(trimmed.lastIndexOf(':') + 1).trim().toLowerCase();
545
+ }
546
+ return localName(trimmed).trim().toLowerCase();
547
+ }
548
+
549
+ function termToCellValue(term: Term): string {
550
+ if (term.termType === 'NamedNode') {
551
+ return term.value;
552
+ }
553
+ if (term.termType === 'BlankNode') {
554
+ return `_:${term.value}`;
555
+ }
556
+ if (term.termType === 'Literal') {
557
+ return term.value;
558
+ }
559
+ return String((term as { value?: unknown }).value ?? '');
560
+ }
561
+
562
+ function normalizeSparqlVariableName(raw: string): string {
563
+ const normalized = raw.replace(/[^A-Za-z0-9_]/g, '_');
564
+ if (!normalized) {
565
+ return 'value';
566
+ }
567
+ if (!/^[A-Za-z_]/.test(normalized)) {
568
+ return `v_${normalized}`;
569
+ }
570
+ return normalized;
571
+ }
572
+
573
+ function uniqueVariableName(base: string, used: ReadonlySet<string>): string {
574
+ if (!used.has(base)) {
575
+ return base;
576
+ }
577
+ let index = 2;
578
+ while (used.has(`${base}_${index}`)) {
579
+ index += 1;
580
+ }
581
+ return `${base}_${index}`;
582
+ }
583
+
584
+ function localName(iri: string): string {
585
+ const hash = iri.lastIndexOf('#');
586
+ if (hash >= 0 && hash < iri.length - 1) {
587
+ return iri.slice(hash + 1);
588
+ }
589
+ const slash = iri.lastIndexOf('/');
590
+ if (slash >= 0 && slash < iri.length - 1) {
591
+ return iri.slice(slash + 1);
592
+ }
593
+ return iri;
594
+ }
595
+
596
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
597
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
598
+ }
@@ -0,0 +1,53 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import type { MarkdownFrontMatter } from './md-types.js';
4
+ import { parseYamlMap } from './md-yaml.js';
5
+
6
+ export function stripLeadingFrontMatter(markdown: string): string {
7
+ const normalized = markdown.replace(/^\uFEFF/, '');
8
+ const frontMatter = extractLeadingFrontMatter(normalized);
9
+ if (!frontMatter) {
10
+ return normalized;
11
+ }
12
+ return normalized.slice(frontMatter.raw.length);
13
+ }
14
+
15
+ export function extractLeadingFrontMatter(markdown: string): MarkdownFrontMatter | undefined {
16
+ const normalized = markdown.replace(/^\uFEFF/, '');
17
+ const yamlMatch = /^(---\s*\r?\n[\s\S]*?\r?\n---(?:\s*\r?\n|$))/.exec(normalized);
18
+ if (yamlMatch) {
19
+ const yamlBody = yamlMatch[1]
20
+ .replace(/^---\s*\r?\n/, '')
21
+ .replace(/\r?\n---(?:\s*\r?\n|$)$/, '');
22
+ const yamlData = parseYamlMap(yamlBody);
23
+ return {
24
+ raw: yamlMatch[1],
25
+ contextUri: extractContextUriFromData(yamlData) ?? extractContextUri(yamlMatch[1]),
26
+ data: yamlData,
27
+ };
28
+ }
29
+ const tomlMatch = /^(\+\+\+\s*\r?\n[\s\S]*?\r?\n\+\+\+(?:\s*\r?\n|$))/.exec(normalized);
30
+ if (tomlMatch) {
31
+ return {
32
+ raw: tomlMatch[1],
33
+ contextUri: extractContextUri(tomlMatch[1])
34
+ };
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function extractContextUri(frontMatterRaw: string): string | undefined {
40
+ const match = /^contextUri\s*:\s*(.+)$/m.exec(frontMatterRaw);
41
+ if (!match) {
42
+ return undefined;
43
+ }
44
+ return match[1].trim().replace(/^['"]|['"]$/g, '');
45
+ }
46
+
47
+ function extractContextUriFromData(data: Record<string, unknown> | undefined): string | undefined {
48
+ if (!data) {
49
+ return undefined;
50
+ }
51
+ const value = data.contextUri;
52
+ return typeof value === 'string' ? value : undefined;
53
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import type { CodeBlockHandler, ParsedCodeBlock } from './md-types.js';
4
+
5
+ export class MarkdownHandlerRegistry {
6
+ private readonly handlers: CodeBlockHandler[] = [];
7
+
8
+ register(handler: CodeBlockHandler): void {
9
+ if (this.handlers.some((h) => h.id === handler.id)) {
10
+ throw new Error(`Duplicate markdown handler id: ${handler.id}`);
11
+ }
12
+ this.handlers.push(handler);
13
+ }
14
+
15
+ match(block: ParsedCodeBlock): CodeBlockHandler | undefined {
16
+ return this.handlers.find((handler) => handler.supports(block));
17
+ }
18
+
19
+ list(): ReadonlyArray<CodeBlockHandler> {
20
+ return this.handlers;
21
+ }
22
+ }