@oml/markdown 0.10.0 → 0.12.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 (40) 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/chart-renderer.js +72 -4
  6. package/out/renderers/chart-renderer.js.map +1 -1
  7. package/out/renderers/diagram-renderer.js +896 -245
  8. package/out/renderers/diagram-renderer.js.map +1 -1
  9. package/out/renderers/graph-renderer.js +452 -18
  10. package/out/renderers/graph-renderer.js.map +1 -1
  11. package/out/renderers/matrix-renderer.d.ts +0 -2
  12. package/out/renderers/matrix-renderer.js +45 -40
  13. package/out/renderers/matrix-renderer.js.map +1 -1
  14. package/out/renderers/renderer.d.ts +4 -1
  15. package/out/renderers/renderer.js +98 -0
  16. package/out/renderers/renderer.js.map +1 -1
  17. package/out/renderers/table-renderer.d.ts +12 -2
  18. package/out/renderers/table-renderer.js +126 -39
  19. package/out/renderers/table-renderer.js.map +1 -1
  20. package/out/renderers/types.d.ts +16 -0
  21. package/out/renderers/wikilink-utils.d.ts +1 -0
  22. package/out/renderers/wikilink-utils.js +60 -32
  23. package/out/renderers/wikilink-utils.js.map +1 -1
  24. package/out/static/browser-runtime.bundle.js +8011 -1292
  25. package/out/static/browser-runtime.bundle.js.map +4 -4
  26. package/out/static/browser-runtime.js +15 -2
  27. package/out/static/browser-runtime.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/md/md-execution.ts +20 -0
  30. package/src/md/md-executor.ts +268 -40
  31. package/src/renderers/chart-renderer.ts +93 -2
  32. package/src/renderers/diagram-renderer.ts +964 -253
  33. package/src/renderers/graph-renderer.ts +512 -12
  34. package/src/renderers/matrix-renderer.ts +57 -44
  35. package/src/renderers/renderer.ts +105 -1
  36. package/src/renderers/table-renderer.ts +190 -41
  37. package/src/renderers/types.ts +20 -0
  38. package/src/renderers/wikilink-utils.ts +66 -31
  39. package/src/static/browser-runtime.ts +20 -2
  40. 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 = {
@@ -27,6 +27,28 @@ type TableEditorActionContext = {
27
27
  pointerY?: number;
28
28
  };
29
29
 
30
+ type TableRootWithAiContext = HTMLDivElement & {
31
+ __omlAiContext?: {
32
+ columns: string[];
33
+ rows: Array<{ iri: string; cells: string[] }>;
34
+ blockSource?: string;
35
+ };
36
+ };
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
+
30
52
  export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
31
53
  protected readonly tableKinds: ReadonlyArray<TableBlockKind> = ['table'];
32
54
 
@@ -44,9 +66,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
44
66
  const tableRoot = this.renderInteractiveTable(
45
67
  result.payload?.columns ?? [],
46
68
  result.payload?.rows ?? [],
69
+ result.payload?.rowsTyped,
47
70
  stylesheet,
48
71
  result.options,
49
- result.blockSource
72
+ result.blockSource,
73
+ result.blockId
50
74
  );
51
75
  if (typeof result.blockId === 'string' && result.blockId.length > 0) {
52
76
  tableRoot.dataset.tableBlockId = result.blockId;
@@ -88,14 +112,20 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
88
112
  protected renderInteractiveTable(
89
113
  columns: string[],
90
114
  rows: string[][],
115
+ rowsTyped: MdTableCell[][] | undefined,
91
116
  stylesheet: CompiledTableStyleRule[],
92
117
  options: Record<string, unknown> | undefined,
93
- blockSource?: string
118
+ blockSource?: string,
119
+ blockId?: string
94
120
  ): HTMLElement {
95
- const root = document.createElement('div');
121
+ const root = document.createElement('div') as TableRootWithAiContext;
96
122
  root.className = 'table-root graph-root';
97
123
  const isTree = this.tableKinds.includes('tree');
98
- const allRowsWithIndex = rows.map((cells, index) => ({ index, cells }));
124
+ const allRowsWithIndex = rows.map((cells, index) => ({
125
+ index,
126
+ cells,
127
+ typedCells: rowsTyped?.[index],
128
+ }));
99
129
  const containmentColumns = isTree ? resolveContainmentColumns(columns, options) : [];
100
130
  const baseVisibleColumnIndexes = isTree
101
131
  ? columns.map((_, index) => index).filter((index) => !containmentColumns.includes(index))
@@ -104,6 +134,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
104
134
  const baseRowsWithIndex = allRowsWithIndex.map((row) => ({
105
135
  ...row,
106
136
  cells: baseVisibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
137
+ typedCells: baseVisibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
107
138
  }));
108
139
  const selectorRowsByIndex = new Map(baseRowsWithIndex.map((row) => [row.index, row] as const));
109
140
  const baseColumnContexts = createColumnContexts(baseVisibleColumns, baseRowsWithIndex);
@@ -113,7 +144,18 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
113
144
  const rowsWithIndex = allRowsWithIndex.map((row) => ({
114
145
  ...row,
115
146
  cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
147
+ typedCells: visibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
116
148
  }));
149
+ root.__omlAiContext = {
150
+ columns: visibleColumns.slice(),
151
+ rows: rowsWithIndex
152
+ .map((row) => ({
153
+ iri: (row.cells[0] ?? '').trim(),
154
+ cells: row.cells.slice(),
155
+ }))
156
+ .filter((row) => row.iri.length > 0),
157
+ blockSource,
158
+ };
117
159
  const columnContexts = createColumnContexts(visibleColumns, rowsWithIndex);
118
160
  const treeModel = isTree
119
161
  ? this.createTreeModel(columns, allRowsWithIndex, visibleColumnIndexes, options)
@@ -121,19 +163,36 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
121
163
  const fullyExpandedTreeRows = isTree && treeModel
122
164
  ? this.flattenAllTreeRows(treeModel)
123
165
  : undefined;
166
+ const persistedState = blockId
167
+ ? (window as TableRendererWindow).__omlGetPersistedTableUiState?.(blockId)
168
+ : undefined;
124
169
 
125
170
  const state = {
126
- search: '',
127
- pageSize: 50,
128
- page: 0,
129
- sortKey: '' as string,
130
- sortDir: 'asc' as 'asc' | 'desc',
131
- 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,
132
177
  columnWidths: new Array<number | undefined>(columns.length).fill(undefined),
133
178
  initialAutosizeApplied: false,
134
179
  expanded: new Set<string>(treeModel?.expandableNodes ?? []),
135
180
  };
136
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
+
137
196
  const controls = document.createElement('div');
138
197
  controls.className = 'table-controls';
139
198
 
@@ -150,9 +209,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
150
209
  }
151
210
  pageSize.appendChild(option);
152
211
  }
212
+ pageSize.value = String(state.pageSize);
153
213
  pageSize.addEventListener('change', () => {
154
214
  state.pageSize = Number.parseInt(pageSize.value, 10);
155
215
  state.page = 0;
216
+ persistUiState();
156
217
  renderPage();
157
218
  });
158
219
  if (!isTree) {
@@ -187,9 +248,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
187
248
  searchInput.type = 'search';
188
249
  searchInput.className = 'tree-filter';
189
250
  searchInput.placeholder = 'Filter...';
251
+ searchInput.value = state.search;
190
252
  searchInput.addEventListener('input', () => {
191
253
  state.search = searchInput.value.trim().toLowerCase();
192
254
  state.page = 0;
255
+ persistUiState();
193
256
  renderPage();
194
257
  });
195
258
  controlsRight.appendChild(searchInput);
@@ -211,17 +274,23 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
211
274
  downloadButton.appendChild(iconSvg);
212
275
  downloadButton.addEventListener('click', () => {
213
276
  const sourceRows = isTree && treeModel
214
- ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
277
+ ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search, blockSource, options)
215
278
  : rowsWithIndex;
216
279
  if (isTree && treeModel) {
217
280
  this.requestTreeJsonDownload(visibleColumns, treeModel, sourceRows);
218
281
  return;
219
282
  }
220
- const filtered = this.applyFiltersAndSorting(columns, sourceRows, state);
221
- this.requestCsvDownload(visibleColumns, filtered.map((entry) => entry.cells));
283
+ const filtered = this.applyFiltersAndSorting(visibleColumns, sourceRows, state, blockSource, options);
284
+ const csvExport = this.transformCsvDownloadData(visibleColumns, filtered.map((entry) => entry.cells));
285
+ this.requestCsvDownload(csvExport.columns, csvExport.rows);
222
286
  });
223
287
  controlsRight.appendChild(downloadButton);
224
288
 
289
+ const uploadButton = this.createUploadCsvButton(visibleColumns, rowsWithIndex, isTree, blockSource);
290
+ if (uploadButton) {
291
+ controlsRight.appendChild(uploadButton);
292
+ }
293
+
225
294
  controls.appendChild(controlsLeft);
226
295
  controls.appendChild(controlsRight);
227
296
  root.appendChild(controls);
@@ -259,6 +328,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
259
328
  }
260
329
  if (state.page > 0) {
261
330
  state.page -= 1;
331
+ persistUiState();
262
332
  renderPage();
263
333
  }
264
334
  });
@@ -266,10 +336,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
266
336
  if (isTree) {
267
337
  return;
268
338
  }
269
- const filtered = this.applyFiltersAndSorting(columns, rowsWithIndex, state);
339
+ const filtered = this.applyFiltersAndSorting(visibleColumns, rowsWithIndex, state, blockSource, options);
270
340
  const totalPages = Math.max(1, Math.ceil(filtered.length / state.pageSize));
271
341
  if (state.page < totalPages - 1) {
272
342
  state.page += 1;
343
+ persistUiState();
273
344
  renderPage();
274
345
  }
275
346
  });
@@ -287,9 +358,11 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
287
358
 
288
359
  const renderPage = () => {
289
360
  const sourceRows = isTree && treeModel
290
- ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
361
+ ? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search, blockSource, options)
291
362
  : rowsWithIndex;
292
- 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);
293
366
  const total = filtered.length;
294
367
  const totalTreeRows = isTree
295
368
  ? (fullyExpandedTreeRows?.length ?? total)
@@ -297,6 +370,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
297
370
  const totalPages = isTree ? 1 : Math.max(1, Math.ceil(total / state.pageSize));
298
371
  if (!isTree && state.page >= totalPages) {
299
372
  state.page = totalPages - 1;
373
+ persistUiState();
300
374
  }
301
375
  const gridTemplateColumns = this.buildGridTemplate(visibleColumns.length, state.columnWidths);
302
376
 
@@ -324,6 +398,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
324
398
  }
325
399
  state.hasUserSort = true;
326
400
  state.page = 0;
401
+ persistUiState();
327
402
  renderPage();
328
403
  });
329
404
  }
@@ -351,6 +426,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
351
426
  const rowContext = createRowContext(baseVisibleColumns, selectorRow);
352
427
  for (let columnIndex = 0; columnIndex < visibleColumns.length; columnIndex += 1) {
353
428
  const value = rowEntry.cells[columnIndex] ?? '';
429
+ const typedValue = rowEntry.typedCells?.[columnIndex];
354
430
  const td = document.createElement('div');
355
431
  td.className = 'table-cell';
356
432
  const valueElement = this.renderValue(this.formatCellDisplayValue(value, {
@@ -358,7 +434,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
358
434
  columns: visibleColumns,
359
435
  blockSource,
360
436
  options,
361
- }));
437
+ }), typedValue);
362
438
  if (isTree && columnIndex === 0) {
363
439
  const treeCell = this.renderTreeCell(valueElement, rowEntry, treeModel, state.expanded, () => renderPage());
364
440
  td.appendChild(treeCell);
@@ -433,15 +509,23 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
433
509
  model: TreeModel,
434
510
  columns: string[],
435
511
  expanded: ReadonlySet<string>,
436
- search: string
512
+ search: string,
513
+ blockSource?: string,
514
+ options?: Record<string, unknown>
437
515
  ): TableRowData[] {
438
516
  if (search) {
439
- return this.filterTreeRowsBySearch(model, columns, search);
517
+ return this.filterTreeRowsBySearch(model, columns, search, blockSource, options);
440
518
  }
441
519
  return this.flattenTreeRows(model, expanded);
442
520
  }
443
521
 
444
- 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[] {
445
529
  const fullyExpanded = this.flattenTreeRows(model, new Set<string>(model.expandableNodes));
446
530
  const matchedIds = new Set<string>();
447
531
  for (const row of fullyExpanded) {
@@ -449,7 +533,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
449
533
  if (!nodeId) {
450
534
  continue;
451
535
  }
452
- if (matchesFilterQuery(columns, row.cells, search)) {
536
+ if (matchesFilterQuery(columns, this.getDisplayFilterCells(row, columns, blockSource, options), search)) {
453
537
  matchedIds.add(nodeId);
454
538
  }
455
539
  }
@@ -493,6 +577,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
493
577
  const projectRow = (row: TableRowData): TableRowData => ({
494
578
  ...row,
495
579
  cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
580
+ typedCells: visibleColumnIndexes.map((columnIndex) => row.typedCells?.[columnIndex]),
496
581
  });
497
582
  for (const row of allRows) {
498
583
  const id = row.cells[idColumnIndex] ?? '';
@@ -539,7 +624,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
539
624
  const syntheticAll = new Array<string>(allColumns.length).fill('');
540
625
  syntheticAll[idColumnIndex] = child;
541
626
  const synthetic = visibleColumnIndexes.map((columnIndex) => syntheticAll[columnIndex] ?? '');
542
- 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) };
543
628
  rowsById.set(child, syntheticRow);
544
629
  order.push(child);
545
630
  }
@@ -692,9 +777,15 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
692
777
  columns: string[],
693
778
  rows: TableRowData[],
694
779
  state: { search: string; sortKey: string; sortDir: 'asc' | 'desc'; hasUserSort: boolean },
780
+ blockSource?: string,
781
+ options?: Record<string, unknown>
695
782
  ): TableRowData[] {
696
783
  const filtered = state.search
697
- ? 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
+ ))
698
789
  : rows.slice();
699
790
  if (!state.hasUserSort) {
700
791
  return filtered;
@@ -711,6 +802,27 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
711
802
  });
712
803
  }
713
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
+
714
826
  private applyStylesheet(
715
827
  cellElement: HTMLElement,
716
828
  valueElement: HTMLElement | undefined,
@@ -886,13 +998,12 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
886
998
  headerFontOverride?: string
887
999
  ): number {
888
1000
  const headerText = `${columns[columnIndex] ?? ''} ▲`;
889
- const values = rows.map((row) => formatCellDisplayText(row.cells[columnIndex] ?? ''));
890
1001
  const valueFont = typeof referenceCellOrFont === 'string'
891
1002
  ? referenceCellOrFont
892
1003
  : this.resolveFont(referenceCellOrFont);
893
1004
  const headerFont = headerFontOverride ?? valueFont;
894
- const widestValue = values.reduce((max, text) => {
895
- const width = this.measureTextWidth(text, valueFont);
1005
+ const widestValue = rows.reduce((max, row) => {
1006
+ const width = this.measureRenderedCellContentWidth(row, columnIndex, columns, valueFont);
896
1007
  return Math.max(max, width);
897
1008
  }, 0);
898
1009
  const headerWidth = this.measureTextWidth(headerText, headerFont);
@@ -922,6 +1033,41 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
922
1033
  }, 0);
923
1034
  }
924
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
+
925
1071
  private resolveFont(referenceCell: HTMLElement): string {
926
1072
  const computed = window.getComputedStyle(referenceCell);
927
1073
  const lineHeight = computed.lineHeight && computed.lineHeight !== 'normal' ? computed.lineHeight : computed.fontSize;
@@ -950,11 +1096,21 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
950
1096
  return raw;
951
1097
  }
952
1098
 
953
- private renderValue(raw: string): HTMLElement {
954
- const value = document.createElement('span');
955
- value.className = 'table-value';
956
- appendTokenizedValueParts(value, raw, 'table-value-part');
957
- return value;
1099
+ private renderValue(raw: string, typedValue?: MdTableCell): HTMLElement {
1100
+ return this.renderCellValue(raw, typedValue);
1101
+ }
1102
+
1103
+ protected createUploadCsvButton(
1104
+ _columns: string[],
1105
+ _rows: Array<{ index: number; cells: string[] }>,
1106
+ _isTree: boolean,
1107
+ _blockSource: string | undefined
1108
+ ): HTMLElement | undefined {
1109
+ return undefined;
1110
+ }
1111
+
1112
+ protected transformCsvDownloadData(columns: string[], rows: string[][]): { columns: string[]; rows: string[][] } {
1113
+ return { columns, rows };
958
1114
  }
959
1115
 
960
1116
  private requestCsvDownload(columns: string[], rows: string[][]): void {
@@ -1047,6 +1203,7 @@ export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
1047
1203
  type TableRowData = {
1048
1204
  index: number;
1049
1205
  cells: string[];
1206
+ typedCells?: Array<MdTableCell | undefined>;
1050
1207
  treeNodeId?: string;
1051
1208
  treeDepth?: number;
1052
1209
  treeHasChildren?: boolean;
@@ -1381,14 +1538,6 @@ function resolveHiddenColumns(
1381
1538
  return hidden;
1382
1539
  }
1383
1540
 
1384
- function formatCellDisplayText(raw: string): string {
1385
- const parts = raw.split(/[\s,]+/).filter((entry) => entry.length > 0);
1386
- if (parts.length === 0) {
1387
- return raw;
1388
- }
1389
- return parts.map((part) => isIriValue(part) ? shortLabelFromIri(part) : part).join(' ');
1390
- }
1391
-
1392
1541
  type ParsedFilterTerm =
1393
1542
  | { kind: 'global'; value: string }
1394
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) {