@oml/markdown 0.11.0 → 0.13.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 (37) hide show
  1. package/out/md/md-execution.d.ts +16 -0
  2. package/out/md/md-executor.d.ts +1 -0
  3. package/out/md/md-executor.js +219 -35
  4. package/out/md/md-executor.js.map +1 -1
  5. package/out/renderers/diagram-renderer.js +160 -1
  6. package/out/renderers/diagram-renderer.js.map +1 -1
  7. package/out/renderers/graph-renderer.js +452 -18
  8. package/out/renderers/graph-renderer.js.map +1 -1
  9. package/out/renderers/matrix-renderer.d.ts +0 -2
  10. package/out/renderers/matrix-renderer.js +45 -40
  11. package/out/renderers/matrix-renderer.js.map +1 -1
  12. package/out/renderers/renderer.d.ts +4 -1
  13. package/out/renderers/renderer.js +98 -0
  14. package/out/renderers/renderer.js.map +1 -1
  15. package/out/renderers/table-renderer.d.ts +4 -2
  16. package/out/renderers/table-renderer.js +104 -38
  17. package/out/renderers/table-renderer.js.map +1 -1
  18. package/out/renderers/types.d.ts +16 -0
  19. package/out/renderers/wikilink-utils.d.ts +1 -0
  20. package/out/renderers/wikilink-utils.js +60 -32
  21. package/out/renderers/wikilink-utils.js.map +1 -1
  22. package/out/static/browser-runtime.bundle.js +7452 -1297
  23. package/out/static/browser-runtime.bundle.js.map +4 -4
  24. package/out/static/browser-runtime.js +15 -2
  25. package/out/static/browser-runtime.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/md/md-execution.ts +20 -0
  28. package/src/md/md-executor.ts +268 -40
  29. package/src/renderers/diagram-renderer.ts +167 -1
  30. package/src/renderers/graph-renderer.ts +512 -12
  31. package/src/renderers/matrix-renderer.ts +57 -44
  32. package/src/renderers/renderer.ts +105 -1
  33. package/src/renderers/table-renderer.ts +151 -39
  34. package/src/renderers/types.ts +20 -0
  35. package/src/renderers/wikilink-utils.ts +66 -31
  36. package/src/static/browser-runtime.ts +20 -2
  37. package/src/static/markdown-webview.css +44 -15
@@ -1,8 +1,8 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
3
  import { QueryMarkdownBlockRenderer, type TableBlockKind } from './renderer.js';
4
- import type { MdBlockExecutionResult } from './types.js';
5
- import { appendTokenizedValueParts, isIriValue, shortLabelFromIri } from './wikilink-utils.js';
4
+ import type { MdBlockExecutionResult, MdTableCell } from './types.js';
5
+ import { shortLabelFromIri } from './wikilink-utils.js';
6
6
 
7
7
  type TableEditorActionKind = 'add' | 'remove' | 'validate' | 'link' | 'undo' | 'redo' | 'ai' | 'properties';
8
8
  type TableEditorActionContext = {
@@ -35,6 +35,20 @@ type TableRootWithAiContext = HTMLDivElement & {
35
35
  };
36
36
  };
37
37
 
38
+ type PersistedTableUiState = {
39
+ search: string;
40
+ pageSize: number;
41
+ page: number;
42
+ sortKey: string;
43
+ sortDir: 'asc' | 'desc';
44
+ hasUserSort: boolean;
45
+ };
46
+
47
+ type TableRendererWindow = Window & typeof globalThis & {
48
+ __omlGetPersistedTableUiState?: (blockId: string) => Partial<PersistedTableUiState> | undefined;
49
+ __omlSetPersistedTableUiState?: (blockId: string, state: PersistedTableUiState) => void;
50
+ };
51
+
38
52
  export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
39
53
  protected readonly tableKinds: ReadonlyArray<TableBlockKind> = ['table'];
40
54
 
@@ -52,9 +66,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
52
66
  const tableRoot = this.renderInteractiveTable(
53
67
  result.payload?.columns ?? [],
54
68
  result.payload?.rows ?? [],
69
+ result.payload?.rowsTyped,
55
70
  stylesheet,
56
71
  result.options,
57
- result.blockSource
72
+ result.blockSource,
73
+ result.blockId
58
74
  );
59
75
  if (typeof result.blockId === 'string' && result.blockId.length > 0) {
60
76
  tableRoot.dataset.tableBlockId = result.blockId;
@@ -96,14 +112,20 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
96
112
  protected renderInteractiveTable(
97
113
  columns: string[],
98
114
  rows: string[][],
115
+ rowsTyped: MdTableCell[][] | undefined,
99
116
  stylesheet: CompiledTableStyleRule[],
100
117
  options: Record<string, unknown> | undefined,
101
- blockSource?: string
118
+ blockSource?: string,
119
+ blockId?: string
102
120
  ): HTMLElement {
103
121
  const root = document.createElement('div') as TableRootWithAiContext;
104
122
  root.className = 'table-root graph-root';
105
123
  const isTree = this.tableKinds.includes('tree');
106
- const allRowsWithIndex = rows.map((cells, index) => ({ index, cells }));
124
+ const allRowsWithIndex = rows.map((cells, index) => ({
125
+ index,
126
+ cells,
127
+ typedCells: rowsTyped?.[index],
128
+ }));
107
129
  const containmentColumns = isTree ? resolveContainmentColumns(columns, options) : [];
108
130
  const baseVisibleColumnIndexes = isTree
109
131
  ? columns.map((_, index) => index).filter((index) => !containmentColumns.includes(index))
@@ -112,6 +134,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
112
134
  const baseRowsWithIndex = allRowsWithIndex.map((row) => ({
113
135
  ...row,
114
136
  cells: baseVisibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
137
+ typedCells: baseVisibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
115
138
  }));
116
139
  const selectorRowsByIndex = new Map(baseRowsWithIndex.map((row) => [row.index, row] as const));
117
140
  const baseColumnContexts = createColumnContexts(baseVisibleColumns, baseRowsWithIndex);
@@ -121,6 +144,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
121
144
  const rowsWithIndex = allRowsWithIndex.map((row) => ({
122
145
  ...row,
123
146
  cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
147
+ typedCells: visibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
124
148
  }));
125
149
  root.__omlAiContext = {
126
150
  columns: visibleColumns.slice(),
@@ -139,19 +163,36 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
139
163
  const fullyExpandedTreeRows = isTree && treeModel
140
164
  ? this.flattenAllTreeRows(treeModel)
141
165
  : undefined;
166
+ const persistedState = blockId
167
+ ? (window as TableRendererWindow).__omlGetPersistedTableUiState?.(blockId)
168
+ : undefined;
142
169
 
143
170
  const state = {
144
- search: '',
145
- pageSize: 50,
146
- page: 0,
147
- sortKey: '' as string,
148
- sortDir: 'asc' as 'asc' | 'desc',
149
- hasUserSort: false,
171
+ search: persistedState?.search ?? '',
172
+ pageSize: persistedState?.pageSize ?? 50,
173
+ page: persistedState?.page ?? 0,
174
+ sortKey: persistedState?.sortKey ?? '' as string,
175
+ sortDir: persistedState?.sortDir ?? 'asc' as 'asc' | 'desc',
176
+ hasUserSort: persistedState?.hasUserSort ?? false,
150
177
  columnWidths: new Array<number | undefined>(columns.length).fill(undefined),
151
178
  initialAutosizeApplied: false,
152
179
  expanded: new Set<string>(treeModel?.expandableNodes ?? []),
153
180
  };
154
181
 
182
+ const persistUiState = () => {
183
+ if (!blockId) {
184
+ return;
185
+ }
186
+ (window as TableRendererWindow).__omlSetPersistedTableUiState?.(blockId, {
187
+ search: state.search,
188
+ pageSize: state.pageSize,
189
+ page: state.page,
190
+ sortKey: state.sortKey,
191
+ sortDir: state.sortDir,
192
+ hasUserSort: state.hasUserSort,
193
+ });
194
+ };
195
+
155
196
  const controls = document.createElement('div');
156
197
  controls.className = 'table-controls';
157
198
 
@@ -168,9 +209,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
168
209
  }
169
210
  pageSize.appendChild(option);
170
211
  }
212
+ pageSize.value = String(state.pageSize);
171
213
  pageSize.addEventListener('change', () => {
172
214
  state.pageSize = Number.parseInt(pageSize.value, 10);
173
215
  state.page = 0;
216
+ persistUiState();
174
217
  renderPage();
175
218
  });
176
219
  if (!isTree) {
@@ -205,9 +248,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
205
248
  searchInput.type = 'search';
206
249
  searchInput.className = 'tree-filter';
207
250
  searchInput.placeholder = 'Filter...';
251
+ searchInput.value = state.search;
208
252
  searchInput.addEventListener('input', () => {
209
253
  state.search = searchInput.value.trim().toLowerCase();
210
254
  state.page = 0;
255
+ persistUiState();
211
256
  renderPage();
212
257
  });
213
258
  controlsRight.appendChild(searchInput);
@@ -229,13 +274,13 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
229
274
  downloadButton.appendChild(iconSvg);
230
275
  downloadButton.addEventListener('click', () => {
231
276
  const sourceRows = isTree && treeModel
232
- ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
277
+ ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search, blockSource, options)
233
278
  : rowsWithIndex;
234
279
  if (isTree && treeModel) {
235
280
  this.requestTreeJsonDownload(visibleColumns, treeModel, sourceRows);
236
281
  return;
237
282
  }
238
- const filtered = this.applyFiltersAndSorting(columns, sourceRows, state);
283
+ const filtered = this.applyFiltersAndSorting(visibleColumns, sourceRows, state, blockSource, options);
239
284
  const csvExport = this.transformCsvDownloadData(visibleColumns, filtered.map((entry) => entry.cells));
240
285
  this.requestCsvDownload(csvExport.columns, csvExport.rows);
241
286
  });
@@ -283,6 +328,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
283
328
  }
284
329
  if (state.page > 0) {
285
330
  state.page -= 1;
331
+ persistUiState();
286
332
  renderPage();
287
333
  }
288
334
  });
@@ -290,10 +336,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
290
336
  if (isTree) {
291
337
  return;
292
338
  }
293
- const filtered = this.applyFiltersAndSorting(columns, rowsWithIndex, state);
339
+ const filtered = this.applyFiltersAndSorting(visibleColumns, rowsWithIndex, state, blockSource, options);
294
340
  const totalPages = Math.max(1, Math.ceil(filtered.length / state.pageSize));
295
341
  if (state.page < totalPages - 1) {
296
342
  state.page += 1;
343
+ persistUiState();
297
344
  renderPage();
298
345
  }
299
346
  });
@@ -311,9 +358,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
311
358
 
312
359
  const renderPage = () => {
313
360
  const sourceRows = isTree && treeModel
314
- ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
361
+ ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search, blockSource, options)
315
362
  : rowsWithIndex;
316
- const filtered = isTree ? sourceRows.slice() : this.applyFiltersAndSorting(columns, sourceRows, state);
363
+ const filtered = isTree
364
+ ? sourceRows.slice()
365
+ : this.applyFiltersAndSorting(visibleColumns, sourceRows, state, blockSource, options);
317
366
  const total = filtered.length;
318
367
  const totalTreeRows = isTree
319
368
  ? (fullyExpandedTreeRows?.length ?? total)
@@ -321,6 +370,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
321
370
  const totalPages = isTree ? 1 : Math.max(1, Math.ceil(total / state.pageSize));
322
371
  if (!isTree && state.page >= totalPages) {
323
372
  state.page = totalPages - 1;
373
+ persistUiState();
324
374
  }
325
375
  const gridTemplateColumns = this.buildGridTemplate(visibleColumns.length, state.columnWidths);
326
376
 
@@ -348,6 +398,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
348
398
  }
349
399
  state.hasUserSort = true;
350
400
  state.page = 0;
401
+ persistUiState();
351
402
  renderPage();
352
403
  });
353
404
  }
@@ -375,6 +426,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
375
426
  const rowContext = createRowContext(baseVisibleColumns, selectorRow);
376
427
  for (let columnIndex = 0; columnIndex < visibleColumns.length; columnIndex += 1) {
377
428
  const value = rowEntry.cells[columnIndex] ?? '';
429
+ const typedValue = rowEntry.typedCells?.[columnIndex];
378
430
  const td = document.createElement('div');
379
431
  td.className = 'table-cell';
380
432
  const valueElement = this.renderValue(this.formatCellDisplayValue(value, {
@@ -382,7 +434,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
382
434
  columns: visibleColumns,
383
435
  blockSource,
384
436
  options,
385
- }));
437
+ }), typedValue);
386
438
  if (isTree && columnIndex === 0) {
387
439
  const treeCell = this.renderTreeCell(valueElement, rowEntry, treeModel, state.expanded, () => renderPage());
388
440
  td.appendChild(treeCell);
@@ -457,15 +509,23 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
457
509
  model: TreeModel,
458
510
  columns: string[],
459
511
  expanded: ReadonlySet<string>,
460
- search: string
512
+ search: string,
513
+ blockSource?: string,
514
+ options?: Record<string, unknown>
461
515
  ): TableRowData[] {
462
516
  if (search) {
463
- return this.filterTreeRowsBySearch(model, columns, search);
517
+ return this.filterTreeRowsBySearch(model, columns, search, blockSource, options);
464
518
  }
465
519
  return this.flattenTreeRows(model, expanded);
466
520
  }
467
521
 
468
- private filterTreeRowsBySearch(model: TreeModel, columns: string[], search: string): TableRowData[] {
522
+ private filterTreeRowsBySearch(
523
+ model: TreeModel,
524
+ columns: string[],
525
+ search: string,
526
+ blockSource?: string,
527
+ options?: Record<string, unknown>
528
+ ): TableRowData[] {
469
529
  const fullyExpanded = this.flattenTreeRows(model, new Set<string>(model.expandableNodes));
470
530
  const matchedIds = new Set<string>();
471
531
  for (const row of fullyExpanded) {
@@ -473,7 +533,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
473
533
  if (!nodeId) {
474
534
  continue;
475
535
  }
476
- if (matchesFilterQuery(columns, row.cells, search)) {
536
+ if (matchesFilterQuery(columns, this.getDisplayFilterCells(row, columns, blockSource, options), search)) {
477
537
  matchedIds.add(nodeId);
478
538
  }
479
539
  }
@@ -517,6 +577,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
517
577
  const projectRow = (row: TableRowData): TableRowData => ({
518
578
  ...row,
519
579
  cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
580
+ typedCells: visibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
520
581
  });
521
582
  for (const row of allRows) {
522
583
  const id = row.cells[idColumnIndex] ?? '';
@@ -563,7 +624,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
563
624
  const syntheticAll = new Array<string>(allColumns.length).fill('');
564
625
  syntheticAll[idColumnIndex] = child;
565
626
  const synthetic = visibleColumnIndexes.map((columnIndex) => syntheticAll[columnIndex] ?? '');
566
- const syntheticRow: TableRowData = { index: allRows.length + rowsById.size, cells: synthetic };
627
+ const syntheticRow: TableRowData = { index: allRows.length + rowsById.size, cells: synthetic, typedCells: new Array(visibleColumnIndexes.length).fill(undefined) };
567
628
  rowsById.set(child, syntheticRow);
568
629
  order.push(child);
569
630
  }
@@ -716,9 +777,15 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
716
777
  columns: string[],
717
778
  rows: TableRowData[],
718
779
  state: { search: string; sortKey: string; sortDir: 'asc' | 'desc'; hasUserSort: boolean },
780
+ blockSource?: string,
781
+ options?: Record<string, unknown>
719
782
  ): TableRowData[] {
720
783
  const filtered = state.search
721
- ? rows.filter((row) => matchesFilterQuery(columns, row.cells, state.search))
784
+ ? rows.filter((row) => matchesFilterQuery(
785
+ columns,
786
+ this.getDisplayFilterCells(row, columns, blockSource, options),
787
+ state.search
788
+ ))
722
789
  : rows.slice();
723
790
  if (!state.hasUserSort) {
724
791
  return filtered;
@@ -735,6 +802,27 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
735
802
  });
736
803
  }
737
804
 
805
+ private getDisplayFilterCells(
806
+ row: TableRowData,
807
+ columns: string[],
808
+ blockSource?: string,
809
+ options?: Record<string, unknown>
810
+ ): string[] {
811
+ return columns.map((_, columnIndex) => {
812
+ const raw = row.cells[columnIndex] ?? '';
813
+ const typedValue = row.typedCells?.[columnIndex];
814
+ if (typedValue?.values?.length) {
815
+ return this.formatCellDisplayText(raw, typedValue);
816
+ }
817
+ return this.formatCellDisplayValue(raw, {
818
+ columnIndex,
819
+ columns,
820
+ blockSource,
821
+ options,
822
+ });
823
+ });
824
+ }
825
+
738
826
  private applyStylesheet(
739
827
  cellElement: HTMLElement,
740
828
  valueElement: HTMLElement | undefined,
@@ -910,13 +998,12 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
910
998
  headerFontOverride?: string
911
999
  ): number {
912
1000
  const headerText = `${columns[columnIndex] ?? ''} ▲`;
913
- const values = rows.map((row) => formatCellDisplayText(row.cells[columnIndex] ?? ''));
914
1001
  const valueFont = typeof referenceCellOrFont === 'string'
915
1002
  ? referenceCellOrFont
916
1003
  : this.resolveFont(referenceCellOrFont);
917
1004
  const headerFont = headerFontOverride ?? valueFont;
918
- const widestValue = values.reduce((max, text) => {
919
- const width = this.measureTextWidth(text, valueFont);
1005
+ const widestValue = rows.reduce((max, row) => {
1006
+ const width = this.measureRenderedCellContentWidth(row, columnIndex, columns, valueFont);
920
1007
  return Math.max(max, width);
921
1008
  }, 0);
922
1009
  const headerWidth = this.measureTextWidth(headerText, headerFont);
@@ -946,6 +1033,41 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
946
1033
  }, 0);
947
1034
  }
948
1035
 
1036
+ private measureRenderedCellContentWidth(
1037
+ row: TableRowData,
1038
+ columnIndex: number,
1039
+ columns: string[],
1040
+ font: string
1041
+ ): number {
1042
+ const rawValue = row.cells[columnIndex] ?? '';
1043
+ const typedValue = row.typedCells?.[columnIndex];
1044
+ const formattedValue = this.formatCellDisplayValue(rawValue, {
1045
+ columnIndex,
1046
+ columns,
1047
+ });
1048
+ const valueElement = this.renderValue(formattedValue, typedValue);
1049
+ const decorated = this.decorateCellValueElement({
1050
+ row: { index: row.index, cells: row.cells },
1051
+ columnIndex,
1052
+ columns,
1053
+ value: rawValue,
1054
+ valueElement,
1055
+ });
1056
+
1057
+ const probe = document.createElement('div');
1058
+ probe.style.position = 'fixed';
1059
+ probe.style.left = '-10000px';
1060
+ probe.style.top = '-10000px';
1061
+ probe.style.visibility = 'hidden';
1062
+ probe.style.whiteSpace = 'nowrap';
1063
+ probe.style.font = font;
1064
+ probe.appendChild(decorated);
1065
+ document.body.appendChild(probe);
1066
+ const width = probe.getBoundingClientRect().width;
1067
+ probe.remove();
1068
+ return width;
1069
+ }
1070
+
949
1071
  private resolveFont(referenceCell: HTMLElement): string {
950
1072
  const computed = window.getComputedStyle(referenceCell);
951
1073
  const lineHeight = computed.lineHeight && computed.lineHeight !== 'normal' ? computed.lineHeight : computed.fontSize;
@@ -974,11 +1096,8 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
974
1096
  return raw;
975
1097
  }
976
1098
 
977
- private renderValue(raw: string): HTMLElement {
978
- const value = document.createElement('span');
979
- value.className = 'table-value';
980
- appendTokenizedValueParts(value, raw, 'table-value-part');
981
- return value;
1099
+ private renderValue(raw: string, typedValue?: MdTableCell): HTMLElement {
1100
+ return this.renderCellValue(raw, typedValue);
982
1101
  }
983
1102
 
984
1103
  protected createUploadCsvButton(
@@ -1084,6 +1203,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
1084
1203
  type TableRowData = {
1085
1204
  index: number;
1086
1205
  cells: string[];
1206
+ typedCells?: Array<MdTableCell | undefined>;
1087
1207
  treeNodeId?: string;
1088
1208
  treeDepth?: number;
1089
1209
  treeHasChildren?: boolean;
@@ -1418,14 +1538,6 @@ function resolveHiddenColumns(
1418
1538
  return hidden;
1419
1539
  }
1420
1540
 
1421
- function formatCellDisplayText(raw: string): string {
1422
- const parts = raw.split(/[\s,]+/).filter((entry) => entry.length > 0);
1423
- if (parts.length === 0) {
1424
- return raw;
1425
- }
1426
- return parts.map((part) => isIriValue(part) ? shortLabelFromIri(part) : part).join(' ');
1427
- }
1428
-
1429
1541
  type ParsedFilterTerm =
1430
1542
  | { kind: 'global'; value: string }
1431
1543
  | { kind: 'scoped'; columns: string[]; value: string };
@@ -2,9 +2,29 @@
2
2
 
3
3
  export type MdExecutionStatus = 'ok' | 'error' | 'unimplemented';
4
4
 
5
+ export type MdTableCellValueKind = 'iri' | 'literal' | 'bnode' | 'unknown';
6
+
7
+ export interface MdTableCellValue {
8
+ kind: MdTableCellValueKind;
9
+ value: string;
10
+ datatype?: string;
11
+ language?: string;
12
+ }
13
+
14
+ export type MdTableCellKind = 'iri' | 'literal' | 'bnode' | 'unknown';
15
+
16
+ export interface MdTableCell {
17
+ kind: MdTableCellKind;
18
+ value: string;
19
+ values: MdTableCellValue[];
20
+ datatype?: string;
21
+ language?: string;
22
+ }
23
+
5
24
  export interface MdTablePayload {
6
25
  columns: string[];
7
26
  rows: string[][];
27
+ rowsTyped?: MdTableCell[][];
8
28
  }
9
29
 
10
30
  export interface MdBlockExecutionResult {
@@ -1,26 +1,23 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
+ import MarkdownIt from 'markdown-it';
4
+ import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs';
3
5
  import { displayLabelFromIri } from './renderer.js';
4
6
 
7
+ const INLINE_MARKDOWN_RENDERER = new MarkdownIt({
8
+ html: true,
9
+ linkify: true,
10
+ typographer: false,
11
+ });
12
+
5
13
  export function appendInlineValue(container: HTMLElement, value: string): void {
6
- const wikilinkPattern = /\[\[([^\]]+)\]\]/g;
7
- let cursor = 0;
8
- for (const match of value.matchAll(wikilinkPattern)) {
9
- const index = match.index ?? 0;
10
- if (index > cursor) {
11
- appendPlainSegment(container, value.slice(cursor, index));
12
- }
13
- const iri = (match[1] ?? '').trim();
14
- if (iri.length > 0) {
15
- container.appendChild(createWikiLinkElement(iri, shortLabelFromIri(iri)));
16
- } else {
17
- container.appendChild(document.createTextNode(match[0] ?? ''));
18
- }
19
- cursor = index + (match[0]?.length ?? 0);
20
- }
21
- if (cursor < value.length) {
22
- appendPlainSegment(container, value.slice(cursor));
14
+ const rendered = INLINE_MARKDOWN_RENDERER.renderInline(value);
15
+ if (!rendered) {
16
+ return;
23
17
  }
18
+ const template = document.createElement('template');
19
+ template.innerHTML = rendered;
20
+ container.append(...Array.from(template.content.childNodes));
24
21
  }
25
22
 
26
23
  export function appendTokenizedValueParts(container: HTMLElement, raw: string, partClass: string): void {
@@ -43,8 +40,8 @@ export function appendTokenizedValueParts(container: HTMLElement, raw: string, p
43
40
  }
44
41
 
45
42
  fragment.dataset.value = token;
46
- if (isIriValue(token)) {
47
- fragment.appendChild(createWikiLinkElement(token, shortLabelFromIri(token)));
43
+ if (isUrlValue(token)) {
44
+ fragment.appendChild(createExternalLinkElement(token));
48
45
  } else {
49
46
  appendInlineValue(fragment, token);
50
47
  }
@@ -77,21 +74,26 @@ export function createWikiLinkElement(iri: string, label: string): HTMLAnchorEle
77
74
  return link;
78
75
  }
79
76
 
80
- function appendPlainSegment(container: HTMLElement, value: string): void {
81
- const parts = value.split(/([\s,]+)/).filter((part) => part.length > 0);
82
- for (const part of parts) {
83
- if (/^[\s,]+$/.test(part)) {
84
- container.appendChild(document.createTextNode(part));
85
- continue;
86
- }
87
- if (!isIriValue(part)) {
88
- container.appendChild(document.createTextNode(part));
89
- continue;
90
- }
91
- container.appendChild(createWikiLinkElement(part, shortLabelFromIri(part)));
77
+ export function isUrlValue(value: string): boolean {
78
+ const trimmed = value.trim();
79
+ if (!trimmed) {
80
+ return false;
81
+ }
82
+ try {
83
+ const parsed = new URL(trimmed);
84
+ return parsed.protocol.length > 1;
85
+ } catch {
86
+ return false;
92
87
  }
93
88
  }
94
89
 
90
+ function createExternalLinkElement(href: string, label?: string): HTMLAnchorElement {
91
+ const link = document.createElement('a');
92
+ link.href = href;
93
+ link.textContent = label ?? href;
94
+ return link;
95
+ }
96
+
95
97
  function parseWikilinkToken(value: string): string | undefined {
96
98
  const match = /^\[\[([^\]]+)\]\]$/.exec(value);
97
99
  if (!match) {
@@ -100,3 +102,36 @@ function parseWikilinkToken(value: string): string | undefined {
100
102
  const iri = (match[1] ?? '').trim();
101
103
  return iri.length > 0 ? iri : undefined;
102
104
  }
105
+
106
+ const wikilinkRule: RuleInline = (state, silent): boolean => {
107
+ const { src } = state;
108
+ const start = state.pos;
109
+ if (src.charCodeAt(start) !== 0x5b || src.charCodeAt(start + 1) !== 0x5b) {
110
+ return false;
111
+ }
112
+ const close = src.indexOf(']]', start + 2);
113
+ if (close < 0) {
114
+ return false;
115
+ }
116
+ const iri = src.slice(start + 2, close).trim();
117
+ if (!iri) {
118
+ return false;
119
+ }
120
+ if (!silent) {
121
+ const label = shortLabelFromIri(iri);
122
+ const open = state.push('link_open', 'a', 1);
123
+ open.attrSet('class', 'wikilink');
124
+ open.attrSet('iri', iri);
125
+ open.attrSet('href', '#');
126
+ open.attrSet('title', iri);
127
+
128
+ const text = state.push('text', '', 0);
129
+ text.content = label;
130
+
131
+ state.push('link_close', 'a', -1);
132
+ }
133
+ state.pos = close + 2;
134
+ return true;
135
+ };
136
+
137
+ INLINE_MARKDOWN_RENDERER.inline.ruler.before('link', 'wiki-link', wikilinkRule);
@@ -110,7 +110,6 @@ function applyWikilinks(
110
110
  const span = document.createElement('span');
111
111
  span.className = 'wikilink';
112
112
  span.setAttribute('iri', iri);
113
- span.title = iri;
114
113
  span.textContent = label;
115
114
  link.replaceWith(span);
116
115
  continue;
@@ -123,7 +122,6 @@ function applyWikilinks(
123
122
  const span = document.createElement('span');
124
123
  span.className = 'wikilink';
125
124
  span.setAttribute('iri', iri);
126
- span.title = iri;
127
125
  span.textContent = label;
128
126
  link.replaceWith(span);
129
127
  }
@@ -172,6 +170,25 @@ function setupDownloadHandler(): void {
172
170
  });
173
171
  }
174
172
 
173
+ function setupIriNavigationHandler(
174
+ wikilinkIndex: Record<string, string>,
175
+ iriAliasIndex: Record<string, string>,
176
+ linkingEnabled: boolean
177
+ ): void {
178
+ window.addEventListener('md-navigate-iri', (event: Event) => {
179
+ const detail = (event as CustomEvent<{ iri?: string }>).detail;
180
+ const iri = typeof detail?.iri === 'string' ? detail.iri.trim() : '';
181
+ if (!iri || !linkingEnabled) {
182
+ return;
183
+ }
184
+ const href = resolveWikiHref(iri, wikilinkIndex, iriAliasIndex);
185
+ if (!href) {
186
+ return;
187
+ }
188
+ window.location.assign(href);
189
+ });
190
+ }
191
+
175
192
  function getMdKindFromCodeElement(code: Element): string | undefined {
176
193
  for (const className of Array.from(code.classList)) {
177
194
  if (!className.startsWith('language-')) {
@@ -211,6 +228,7 @@ async function applyExecutionResults(): Promise<void> {
211
228
  applyWikilinks(document, wikilinkIndex, iriAliasIndex, linkingEnabled);
212
229
  // Re-apply wikilinks for dynamic renderer re-renders (tables, filters, paging, etc.).
213
230
  installWikilinkObserver(wikilinkIndex, iriAliasIndex, linkingEnabled);
231
+ setupIriNavigationHandler(wikilinkIndex, iriAliasIndex, linkingEnabled);
214
232
 
215
233
  const manifest = parseJsonNode<ManifestEntry[]>('oml-md-block-manifest', []);
216
234
  if (!Array.isArray(manifest) || manifest.length === 0) {