@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.
- package/README.md +39 -0
- package/out/index.d.ts +2 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -0
- package/out/md/index.d.ts +6 -0
- package/out/md/index.js +8 -0
- package/out/md/index.js.map +1 -0
- package/out/md/md-execution.d.ts +33 -0
- package/out/md/md-execution.js +3 -0
- package/out/md/md-execution.js.map +1 -0
- package/out/md/md-executor.d.ts +21 -0
- package/out/md/md-executor.js +498 -0
- package/out/md/md-executor.js.map +1 -0
- package/out/md/md-frontmatter.d.ts +4 -0
- package/out/md/md-frontmatter.js +48 -0
- package/out/md/md-frontmatter.js.map +1 -0
- package/out/md/md-registry.d.ts +7 -0
- package/out/md/md-registry.js +19 -0
- package/out/md/md-registry.js.map +1 -0
- package/out/md/md-runtime.d.ts +10 -0
- package/out/md/md-runtime.js +166 -0
- package/out/md/md-runtime.js.map +1 -0
- package/out/md/md-types.d.ts +40 -0
- package/out/md/md-types.js +3 -0
- package/out/md/md-types.js.map +1 -0
- package/out/md/md-yaml.d.ts +1 -0
- package/out/md/md-yaml.js +15 -0
- package/out/md/md-yaml.js.map +1 -0
- package/out/renderers/chart-renderer.d.ts +6 -0
- package/out/renderers/chart-renderer.js +392 -0
- package/out/renderers/chart-renderer.js.map +1 -0
- package/out/renderers/diagram-renderer.d.ts +7 -0
- package/out/renderers/diagram-renderer.js +2354 -0
- package/out/renderers/diagram-renderer.js.map +1 -0
- package/out/renderers/graph-renderer.d.ts +6 -0
- package/out/renderers/graph-renderer.js +1384 -0
- package/out/renderers/graph-renderer.js.map +1 -0
- package/out/renderers/index.d.ts +14 -0
- package/out/renderers/index.js +16 -0
- package/out/renderers/index.js.map +1 -0
- package/out/renderers/list-renderer.d.ts +6 -0
- package/out/renderers/list-renderer.js +252 -0
- package/out/renderers/list-renderer.js.map +1 -0
- package/out/renderers/matrix-renderer.d.ts +14 -0
- package/out/renderers/matrix-renderer.js +498 -0
- package/out/renderers/matrix-renderer.js.map +1 -0
- package/out/renderers/message-renderer.d.ts +6 -0
- package/out/renderers/message-renderer.js +14 -0
- package/out/renderers/message-renderer.js.map +1 -0
- package/out/renderers/registry.d.ts +9 -0
- package/out/renderers/registry.js +41 -0
- package/out/renderers/registry.js.map +1 -0
- package/out/renderers/renderer.d.ts +28 -0
- package/out/renderers/renderer.js +61 -0
- package/out/renderers/renderer.js.map +1 -0
- package/out/renderers/table-editor-renderer.d.ts +4 -0
- package/out/renderers/table-editor-renderer.js +9 -0
- package/out/renderers/table-editor-renderer.js.map +1 -0
- package/out/renderers/table-renderer.d.ts +95 -0
- package/out/renderers/table-renderer.js +1571 -0
- package/out/renderers/table-renderer.js.map +1 -0
- package/out/renderers/text-renderer.d.ts +7 -0
- package/out/renderers/text-renderer.js +219 -0
- package/out/renderers/text-renderer.js.map +1 -0
- package/out/renderers/tree-renderer.d.ts +4 -0
- package/out/renderers/tree-renderer.js +9 -0
- package/out/renderers/tree-renderer.js.map +1 -0
- package/out/renderers/types.d.ts +18 -0
- package/out/renderers/types.js +3 -0
- package/out/renderers/types.js.map +1 -0
- package/out/renderers/wikilink-utils.d.ts +6 -0
- package/out/renderers/wikilink-utils.js +100 -0
- package/out/renderers/wikilink-utils.js.map +1 -0
- package/out/static/browser-runtime.bundle.js +74155 -0
- package/out/static/browser-runtime.bundle.js.map +7 -0
- package/out/static/browser-runtime.d.ts +1 -0
- package/out/static/browser-runtime.js +218 -0
- package/out/static/browser-runtime.js.map +1 -0
- package/out/static/index.d.ts +1 -0
- package/out/static/index.js +3 -0
- package/out/static/index.js.map +1 -0
- package/out/static/runtime-assets.d.ts +2 -0
- package/out/static/runtime-assets.js +174 -0
- package/out/static/runtime-assets.js.map +1 -0
- package/package.json +74 -0
- package/src/index.ts +4 -0
- package/src/md/index.ts +8 -0
- package/src/md/md-execution.ts +51 -0
- package/src/md/md-executor.ts +598 -0
- package/src/md/md-frontmatter.ts +53 -0
- package/src/md/md-registry.ts +22 -0
- package/src/md/md-runtime.ts +191 -0
- package/src/md/md-types.ts +48 -0
- package/src/md/md-yaml.ts +17 -0
- package/src/renderers/chart-renderer.ts +473 -0
- package/src/renderers/diagram-renderer.ts +2520 -0
- package/src/renderers/graph-renderer.ts +1653 -0
- package/src/renderers/index.ts +16 -0
- package/src/renderers/list-renderer.ts +289 -0
- package/src/renderers/matrix-renderer.ts +616 -0
- package/src/renderers/message-renderer.ts +18 -0
- package/src/renderers/registry.ts +45 -0
- package/src/renderers/renderer.ts +84 -0
- package/src/renderers/table-editor-renderer.ts +8 -0
- package/src/renderers/table-renderer.ts +1868 -0
- package/src/renderers/text-renderer.ts +252 -0
- package/src/renderers/tree-renderer.ts +7 -0
- package/src/renderers/types.ts +22 -0
- package/src/renderers/wikilink-utils.ts +108 -0
- package/src/static/browser-runtime.ts +249 -0
- package/src/static/index.ts +3 -0
- package/src/static/runtime-assets.ts +175 -0
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
import { QueryMarkdownBlockRenderer } from './renderer.js';
|
|
3
|
+
import { appendTokenizedValueParts, isIriValue, shortLabelFromIri } from './wikilink-utils.js';
|
|
4
|
+
export class TableMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this.tableKinds = ['table'];
|
|
8
|
+
}
|
|
9
|
+
canRender(result) {
|
|
10
|
+
return result.status === 'ok'
|
|
11
|
+
&& result.format === 'table'
|
|
12
|
+
&& !!result.payload
|
|
13
|
+
&& this.tableKinds.includes(result.kind);
|
|
14
|
+
}
|
|
15
|
+
render(result) {
|
|
16
|
+
const container = this.createResultContainer(result.status);
|
|
17
|
+
container.classList.add('oml-md-result-table');
|
|
18
|
+
const stylesheet = compileTableStylesheet(result.options);
|
|
19
|
+
const tableRoot = this.renderInteractiveTable(result.payload?.columns ?? [], result.payload?.rows ?? [], stylesheet, result.options, result.blockSource);
|
|
20
|
+
if (typeof result.blockId === 'string' && result.blockId.length > 0) {
|
|
21
|
+
tableRoot.dataset.tableBlockId = result.blockId;
|
|
22
|
+
}
|
|
23
|
+
container.appendChild(tableRoot);
|
|
24
|
+
return container;
|
|
25
|
+
}
|
|
26
|
+
appendCustomLeftControls(_leftControls, _context) {
|
|
27
|
+
// Default tables do not add extra controls.
|
|
28
|
+
}
|
|
29
|
+
decorateCellValueElement(params) {
|
|
30
|
+
return params.valueElement;
|
|
31
|
+
}
|
|
32
|
+
shouldIgnoreRowDoubleClick(_event) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
onRowDoubleClick(_context) {
|
|
36
|
+
// Default tables do not define row double-click behavior.
|
|
37
|
+
}
|
|
38
|
+
dispatchTableEditorAction(context) {
|
|
39
|
+
const event = new CustomEvent('oml-table-editor-action', { detail: context });
|
|
40
|
+
window.dispatchEvent(event);
|
|
41
|
+
}
|
|
42
|
+
renderInteractiveTable(columns, rows, stylesheet, options, blockSource) {
|
|
43
|
+
const root = document.createElement('div');
|
|
44
|
+
root.className = 'table-root graph-root';
|
|
45
|
+
const isTree = this.tableKinds.includes('tree');
|
|
46
|
+
const allRowsWithIndex = rows.map((cells, index) => ({ index, cells }));
|
|
47
|
+
const containmentColumns = isTree ? resolveContainmentColumns(columns, options) : [];
|
|
48
|
+
const baseVisibleColumnIndexes = isTree
|
|
49
|
+
? columns.map((_, index) => index).filter((index) => !containmentColumns.includes(index))
|
|
50
|
+
: columns.map((_, index) => index);
|
|
51
|
+
const baseVisibleColumns = baseVisibleColumnIndexes.map((index) => columns[index]);
|
|
52
|
+
const baseRowsWithIndex = allRowsWithIndex.map((row) => ({
|
|
53
|
+
...row,
|
|
54
|
+
cells: baseVisibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
|
|
55
|
+
}));
|
|
56
|
+
const selectorRowsByIndex = new Map(baseRowsWithIndex.map((row) => [row.index, row]));
|
|
57
|
+
const baseColumnContexts = createColumnContexts(baseVisibleColumns, baseRowsWithIndex);
|
|
58
|
+
const hiddenColumns = resolveHiddenColumns(baseVisibleColumns, baseColumnContexts, stylesheet);
|
|
59
|
+
const visibleColumnIndexes = baseVisibleColumnIndexes.filter((_, index) => !hiddenColumns.has(baseVisibleColumns[index]));
|
|
60
|
+
const visibleColumns = visibleColumnIndexes.map((index) => columns[index]);
|
|
61
|
+
const rowsWithIndex = allRowsWithIndex.map((row) => ({
|
|
62
|
+
...row,
|
|
63
|
+
cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
|
|
64
|
+
}));
|
|
65
|
+
const columnContexts = createColumnContexts(visibleColumns, rowsWithIndex);
|
|
66
|
+
const treeModel = isTree
|
|
67
|
+
? this.createTreeModel(columns, allRowsWithIndex, visibleColumnIndexes, options)
|
|
68
|
+
: undefined;
|
|
69
|
+
const fullyExpandedTreeRows = isTree && treeModel
|
|
70
|
+
? this.flattenAllTreeRows(treeModel)
|
|
71
|
+
: undefined;
|
|
72
|
+
const state = {
|
|
73
|
+
search: '',
|
|
74
|
+
pageSize: 50,
|
|
75
|
+
page: 0,
|
|
76
|
+
sortKey: '',
|
|
77
|
+
sortDir: 'asc',
|
|
78
|
+
hasUserSort: false,
|
|
79
|
+
columnWidths: new Array(columns.length).fill(undefined),
|
|
80
|
+
initialAutosizeApplied: false,
|
|
81
|
+
expanded: new Set(treeModel?.expandableNodes ?? []),
|
|
82
|
+
};
|
|
83
|
+
const controls = document.createElement('div');
|
|
84
|
+
controls.className = 'table-controls';
|
|
85
|
+
const controlsLeft = document.createElement('div');
|
|
86
|
+
controlsLeft.className = 'table-controls-left';
|
|
87
|
+
const pageSize = document.createElement('select');
|
|
88
|
+
pageSize.className = 'table-pagesize';
|
|
89
|
+
for (const size of [10, 25, 50, 100]) {
|
|
90
|
+
const option = document.createElement('option');
|
|
91
|
+
option.value = String(size);
|
|
92
|
+
option.textContent = `Show ${size}`;
|
|
93
|
+
if (size === state.pageSize) {
|
|
94
|
+
option.selected = true;
|
|
95
|
+
}
|
|
96
|
+
pageSize.appendChild(option);
|
|
97
|
+
}
|
|
98
|
+
pageSize.addEventListener('change', () => {
|
|
99
|
+
state.pageSize = Number.parseInt(pageSize.value, 10);
|
|
100
|
+
state.page = 0;
|
|
101
|
+
renderPage();
|
|
102
|
+
});
|
|
103
|
+
if (!isTree) {
|
|
104
|
+
controlsLeft.appendChild(pageSize);
|
|
105
|
+
}
|
|
106
|
+
else if (treeModel) {
|
|
107
|
+
const expandAllButton = this.createTreeActionButton('Expand all', 'Expand all', 'M7 7h10v2H7zM7 11h10v2H7zM7 15h10v2H7zM3 11h2V9h2V7H5V5H3v2H1v2h2z');
|
|
108
|
+
expandAllButton.addEventListener('click', () => {
|
|
109
|
+
state.expanded = new Set(treeModel.expandableNodes);
|
|
110
|
+
renderPage();
|
|
111
|
+
});
|
|
112
|
+
controlsLeft.appendChild(expandAllButton);
|
|
113
|
+
const collapseAllButton = this.createTreeActionButton('Collapse all', 'Collapse all', 'M7 7h10v2H7zM7 11h10v2H7zM7 15h10v2H7zM1 7h6v2H1z');
|
|
114
|
+
collapseAllButton.addEventListener('click', () => {
|
|
115
|
+
state.expanded = new Set();
|
|
116
|
+
renderPage();
|
|
117
|
+
});
|
|
118
|
+
controlsLeft.appendChild(collapseAllButton);
|
|
119
|
+
}
|
|
120
|
+
const controlsRight = document.createElement('div');
|
|
121
|
+
controlsRight.className = 'table-controls-right';
|
|
122
|
+
const searchInput = document.createElement('input');
|
|
123
|
+
searchInput.type = 'search';
|
|
124
|
+
searchInput.className = 'tree-filter';
|
|
125
|
+
searchInput.placeholder = 'Filter...';
|
|
126
|
+
searchInput.addEventListener('input', () => {
|
|
127
|
+
state.search = searchInput.value.trim().toLowerCase();
|
|
128
|
+
state.page = 0;
|
|
129
|
+
renderPage();
|
|
130
|
+
});
|
|
131
|
+
controlsRight.appendChild(searchInput);
|
|
132
|
+
const downloadButton = document.createElement('button');
|
|
133
|
+
downloadButton.className = 'tree-download-btn';
|
|
134
|
+
downloadButton.title = 'Download CSV';
|
|
135
|
+
downloadButton.setAttribute('aria-label', 'Download CSV');
|
|
136
|
+
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
137
|
+
iconSvg.setAttribute('viewBox', '0 0 24 24');
|
|
138
|
+
iconSvg.setAttribute('width', '20');
|
|
139
|
+
iconSvg.setAttribute('height', '20');
|
|
140
|
+
iconSvg.setAttribute('aria-hidden', 'true');
|
|
141
|
+
iconSvg.setAttribute('focusable', 'false');
|
|
142
|
+
iconSvg.style.fill = 'currentColor';
|
|
143
|
+
const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
144
|
+
iconPath.setAttribute('d', 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z');
|
|
145
|
+
iconSvg.appendChild(iconPath);
|
|
146
|
+
downloadButton.appendChild(iconSvg);
|
|
147
|
+
downloadButton.addEventListener('click', () => {
|
|
148
|
+
const sourceRows = isTree && treeModel
|
|
149
|
+
? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
|
|
150
|
+
: rowsWithIndex;
|
|
151
|
+
if (isTree && treeModel) {
|
|
152
|
+
this.requestTreeJsonDownload(visibleColumns, treeModel, sourceRows);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const filtered = this.applyFiltersAndSorting(columns, sourceRows, state);
|
|
156
|
+
this.requestCsvDownload(visibleColumns, filtered.map((entry) => entry.cells));
|
|
157
|
+
});
|
|
158
|
+
controlsRight.appendChild(downloadButton);
|
|
159
|
+
controls.appendChild(controlsLeft);
|
|
160
|
+
controls.appendChild(controlsRight);
|
|
161
|
+
root.appendChild(controls);
|
|
162
|
+
const wrapper = document.createElement('div');
|
|
163
|
+
wrapper.className = 'table-results-table-container hover-highlight';
|
|
164
|
+
const table = document.createElement('div');
|
|
165
|
+
table.className = 'table-results-table';
|
|
166
|
+
const headerRow = document.createElement('div');
|
|
167
|
+
headerRow.className = 'table-header-row';
|
|
168
|
+
const body = document.createElement('div');
|
|
169
|
+
body.className = 'table-body';
|
|
170
|
+
wrapper.appendChild(table);
|
|
171
|
+
root.appendChild(wrapper);
|
|
172
|
+
const footer = document.createElement('div');
|
|
173
|
+
footer.className = 'table-footer';
|
|
174
|
+
const info = document.createElement('div');
|
|
175
|
+
info.className = 'table-info-bar';
|
|
176
|
+
const footerPagination = document.createElement('div');
|
|
177
|
+
footerPagination.className = 'table-footer-pagination';
|
|
178
|
+
const pager = document.createElement('div');
|
|
179
|
+
pager.className = 'table-pagination';
|
|
180
|
+
const prev = document.createElement('button');
|
|
181
|
+
prev.className = 'table-page-btn';
|
|
182
|
+
prev.textContent = '<';
|
|
183
|
+
const next = document.createElement('button');
|
|
184
|
+
next.className = 'table-page-btn';
|
|
185
|
+
next.textContent = '>';
|
|
186
|
+
const pageLabel = document.createElement('span');
|
|
187
|
+
pageLabel.className = 'table-page-label';
|
|
188
|
+
prev.addEventListener('click', () => {
|
|
189
|
+
if (isTree) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (state.page > 0) {
|
|
193
|
+
state.page -= 1;
|
|
194
|
+
renderPage();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
next.addEventListener('click', () => {
|
|
198
|
+
if (isTree) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const filtered = this.applyFiltersAndSorting(columns, rowsWithIndex, state);
|
|
202
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / state.pageSize));
|
|
203
|
+
if (state.page < totalPages - 1) {
|
|
204
|
+
state.page += 1;
|
|
205
|
+
renderPage();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
pager.appendChild(prev);
|
|
209
|
+
pager.appendChild(pageLabel);
|
|
210
|
+
pager.appendChild(next);
|
|
211
|
+
if (!isTree) {
|
|
212
|
+
footerPagination.appendChild(pager);
|
|
213
|
+
}
|
|
214
|
+
footer.appendChild(info);
|
|
215
|
+
if (!isTree) {
|
|
216
|
+
footer.appendChild(footerPagination);
|
|
217
|
+
}
|
|
218
|
+
root.appendChild(footer);
|
|
219
|
+
const renderPage = () => {
|
|
220
|
+
const sourceRows = isTree && treeModel
|
|
221
|
+
? this.resolveTreeSourceRows(treeModel, visibleColumns, state.expanded, state.search)
|
|
222
|
+
: rowsWithIndex;
|
|
223
|
+
const filtered = isTree ? sourceRows.slice() : this.applyFiltersAndSorting(columns, sourceRows, state);
|
|
224
|
+
const total = filtered.length;
|
|
225
|
+
const totalTreeRows = isTree
|
|
226
|
+
? (fullyExpandedTreeRows?.length ?? total)
|
|
227
|
+
: total;
|
|
228
|
+
const totalPages = isTree ? 1 : Math.max(1, Math.ceil(total / state.pageSize));
|
|
229
|
+
if (!isTree && state.page >= totalPages) {
|
|
230
|
+
state.page = totalPages - 1;
|
|
231
|
+
}
|
|
232
|
+
const gridTemplateColumns = this.buildGridTemplate(visibleColumns.length, state.columnWidths);
|
|
233
|
+
headerRow.innerHTML = '';
|
|
234
|
+
headerRow.style.gridTemplateColumns = gridTemplateColumns;
|
|
235
|
+
for (let columnIndex = 0; columnIndex < visibleColumns.length; columnIndex += 1) {
|
|
236
|
+
const column = visibleColumns[columnIndex];
|
|
237
|
+
const th = document.createElement('div');
|
|
238
|
+
th.className = 'table-header-cell';
|
|
239
|
+
const sorted = !isTree && state.hasUserSort && state.sortKey === column;
|
|
240
|
+
const indicator = sorted ? (state.sortDir === 'asc' ? ' ▲' : ' ▼') : '';
|
|
241
|
+
const label = document.createElement('span');
|
|
242
|
+
label.className = 'table-header-label';
|
|
243
|
+
label.textContent = `${column}${indicator}`;
|
|
244
|
+
th.appendChild(label);
|
|
245
|
+
const headerContext = createHeaderContext(column);
|
|
246
|
+
this.applyStylesheet(th, undefined, stylesheet, [{ kind: 'header', context: headerContext }]);
|
|
247
|
+
if (!isTree) {
|
|
248
|
+
th.addEventListener('click', () => {
|
|
249
|
+
if (state.sortKey === column) {
|
|
250
|
+
state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
state.sortKey = column;
|
|
254
|
+
state.sortDir = 'asc';
|
|
255
|
+
}
|
|
256
|
+
state.hasUserSort = true;
|
|
257
|
+
state.page = 0;
|
|
258
|
+
renderPage();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
this.installColumnResizeHandle({
|
|
262
|
+
handleHost: th,
|
|
263
|
+
columnIndex,
|
|
264
|
+
state,
|
|
265
|
+
root,
|
|
266
|
+
columns: visibleColumns,
|
|
267
|
+
rows: fullyExpandedTreeRows ?? rowsWithIndex,
|
|
268
|
+
referenceCell: label,
|
|
269
|
+
rerender: renderPage,
|
|
270
|
+
});
|
|
271
|
+
headerRow.appendChild(th);
|
|
272
|
+
}
|
|
273
|
+
body.innerHTML = '';
|
|
274
|
+
const start = isTree ? 0 : state.page * state.pageSize;
|
|
275
|
+
const pageRows = isTree ? filtered : filtered.slice(start, start + state.pageSize);
|
|
276
|
+
for (const rowEntry of pageRows) {
|
|
277
|
+
const tr = document.createElement('div');
|
|
278
|
+
tr.className = 'table-row';
|
|
279
|
+
tr.style.gridTemplateColumns = gridTemplateColumns;
|
|
280
|
+
const selectorRow = selectorRowsByIndex.get(rowEntry.index) ?? rowEntry;
|
|
281
|
+
const rowContext = createRowContext(baseVisibleColumns, selectorRow);
|
|
282
|
+
for (let columnIndex = 0; columnIndex < visibleColumns.length; columnIndex += 1) {
|
|
283
|
+
const value = rowEntry.cells[columnIndex] ?? '';
|
|
284
|
+
const td = document.createElement('div');
|
|
285
|
+
td.className = 'table-cell';
|
|
286
|
+
const valueElement = this.renderValue(this.formatCellDisplayValue(value, {
|
|
287
|
+
columnIndex,
|
|
288
|
+
columns: visibleColumns,
|
|
289
|
+
blockSource,
|
|
290
|
+
options,
|
|
291
|
+
}));
|
|
292
|
+
if (isTree && columnIndex === 0) {
|
|
293
|
+
const treeCell = this.renderTreeCell(valueElement, rowEntry, treeModel, state.expanded, () => renderPage());
|
|
294
|
+
td.appendChild(treeCell);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
td.appendChild(this.decorateCellValueElement({
|
|
298
|
+
row: rowEntry,
|
|
299
|
+
columnIndex,
|
|
300
|
+
columns: visibleColumns,
|
|
301
|
+
value,
|
|
302
|
+
valueElement,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
const cellContext = createCellContext(rowContext, visibleColumns[columnIndex], value);
|
|
306
|
+
const columnContext = columnContexts[columnIndex];
|
|
307
|
+
this.applyStylesheet(td, valueElement, stylesheet, [
|
|
308
|
+
{ kind: 'row', context: rowContext },
|
|
309
|
+
{ kind: 'column', context: columnContext },
|
|
310
|
+
{ kind: 'cell', context: cellContext },
|
|
311
|
+
]);
|
|
312
|
+
tr.appendChild(td);
|
|
313
|
+
}
|
|
314
|
+
tr.addEventListener('dblclick', (event) => {
|
|
315
|
+
if (this.shouldIgnoreRowDoubleClick(event)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.onRowDoubleClick({
|
|
319
|
+
root,
|
|
320
|
+
columns: visibleColumns,
|
|
321
|
+
rows: rowsWithIndex.map((row) => ({ index: row.index, cells: row.cells.slice() })),
|
|
322
|
+
row: { index: rowEntry.index, cells: rowEntry.cells.slice() },
|
|
323
|
+
blockSource,
|
|
324
|
+
typeOptions: this.extractTypeOptions(blockSource),
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
body.appendChild(tr);
|
|
328
|
+
}
|
|
329
|
+
table.replaceChildren(headerRow, body);
|
|
330
|
+
info.textContent = `Showing ${pageRows.length} of ${isTree ? totalTreeRows : total} entries`;
|
|
331
|
+
if (!isTree) {
|
|
332
|
+
pageLabel.textContent = `${state.page + 1}/${totalPages}`;
|
|
333
|
+
prev.disabled = state.page === 0;
|
|
334
|
+
next.disabled = state.page >= totalPages - 1;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
this.appendCustomLeftControls(controlsLeft, {
|
|
338
|
+
root,
|
|
339
|
+
columns: visibleColumns,
|
|
340
|
+
rows: rowsWithIndex.map((row) => ({ index: row.index, cells: row.cells.slice() })),
|
|
341
|
+
blockSource,
|
|
342
|
+
options,
|
|
343
|
+
typeOptions: this.extractTypeOptions(blockSource),
|
|
344
|
+
});
|
|
345
|
+
renderPage();
|
|
346
|
+
this.scheduleInitialAutosize({
|
|
347
|
+
root,
|
|
348
|
+
columns: visibleColumns,
|
|
349
|
+
rows: fullyExpandedTreeRows ?? rowsWithIndex,
|
|
350
|
+
state,
|
|
351
|
+
rerender: renderPage,
|
|
352
|
+
});
|
|
353
|
+
return root;
|
|
354
|
+
}
|
|
355
|
+
extractTypeOptions(_blockSource) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
resolveTreeSourceRows(model, columns, expanded, search) {
|
|
359
|
+
if (search) {
|
|
360
|
+
return this.filterTreeRowsBySearch(model, columns, search);
|
|
361
|
+
}
|
|
362
|
+
return this.flattenTreeRows(model, expanded);
|
|
363
|
+
}
|
|
364
|
+
filterTreeRowsBySearch(model, columns, search) {
|
|
365
|
+
const fullyExpanded = this.flattenTreeRows(model, new Set(model.expandableNodes));
|
|
366
|
+
const matchedIds = new Set();
|
|
367
|
+
for (const row of fullyExpanded) {
|
|
368
|
+
const nodeId = row.treeNodeId ?? '';
|
|
369
|
+
if (!nodeId) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (matchesFilterQuery(columns, row.cells, search)) {
|
|
373
|
+
matchedIds.add(nodeId);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (matchedIds.size === 0) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const includeIds = new Set(matchedIds);
|
|
380
|
+
const stack = Array.from(matchedIds);
|
|
381
|
+
while (stack.length > 0) {
|
|
382
|
+
const childId = stack.pop();
|
|
383
|
+
if (!childId) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const parents = model.parentsByChild.get(childId) ?? [];
|
|
387
|
+
for (const parentId of parents) {
|
|
388
|
+
if (includeIds.has(parentId)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
includeIds.add(parentId);
|
|
392
|
+
stack.push(parentId);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return fullyExpanded.filter((row) => {
|
|
396
|
+
const nodeId = row.treeNodeId ?? '';
|
|
397
|
+
return !!nodeId && includeIds.has(nodeId);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
createTreeModel(allColumns, allRows, visibleColumnIndexes, options) {
|
|
401
|
+
const idColumnIndex = 0;
|
|
402
|
+
const containmentColumns = resolveContainmentColumns(allColumns, options);
|
|
403
|
+
const rowsById = new Map();
|
|
404
|
+
const order = [];
|
|
405
|
+
const projectRow = (row) => ({
|
|
406
|
+
...row,
|
|
407
|
+
cells: visibleColumnIndexes.map((columnIndex) => row.cells[columnIndex] ?? ''),
|
|
408
|
+
});
|
|
409
|
+
for (const row of allRows) {
|
|
410
|
+
const id = row.cells[idColumnIndex] ?? '';
|
|
411
|
+
if (!id) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!rowsById.has(id)) {
|
|
415
|
+
rowsById.set(id, projectRow(row));
|
|
416
|
+
order.push(id);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const childrenByParent = new Map();
|
|
420
|
+
const parentsByChild = new Map();
|
|
421
|
+
const parentCountByChild = new Map();
|
|
422
|
+
const pushChild = (parent, child) => {
|
|
423
|
+
if (!parent || !child) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const existing = childrenByParent.get(parent) ?? [];
|
|
427
|
+
if (!existing.includes(child)) {
|
|
428
|
+
existing.push(child);
|
|
429
|
+
childrenByParent.set(parent, existing);
|
|
430
|
+
const parents = parentsByChild.get(child) ?? [];
|
|
431
|
+
if (!parents.includes(parent)) {
|
|
432
|
+
parents.push(parent);
|
|
433
|
+
parentsByChild.set(child, parents);
|
|
434
|
+
}
|
|
435
|
+
parentCountByChild.set(child, (parentCountByChild.get(child) ?? 0) + 1);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
for (const row of allRows) {
|
|
439
|
+
const parent = row.cells[idColumnIndex] ?? '';
|
|
440
|
+
if (!parent) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for (const columnIndex of containmentColumns) {
|
|
444
|
+
const raw = row.cells[columnIndex] ?? '';
|
|
445
|
+
const children = raw.split(/[\s,]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
446
|
+
for (const child of children) {
|
|
447
|
+
pushChild(parent, child);
|
|
448
|
+
if (!rowsById.has(child)) {
|
|
449
|
+
const syntheticAll = new Array(allColumns.length).fill('');
|
|
450
|
+
syntheticAll[idColumnIndex] = child;
|
|
451
|
+
const synthetic = visibleColumnIndexes.map((columnIndex) => syntheticAll[columnIndex] ?? '');
|
|
452
|
+
const syntheticRow = { index: allRows.length + rowsById.size, cells: synthetic };
|
|
453
|
+
rowsById.set(child, syntheticRow);
|
|
454
|
+
order.push(child);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const roots = order.filter((id) => (parentCountByChild.get(id) ?? 0) === 0);
|
|
460
|
+
return {
|
|
461
|
+
rowsById,
|
|
462
|
+
childrenByParent,
|
|
463
|
+
parentsByChild,
|
|
464
|
+
roots: roots.length > 0 ? roots : order.slice(),
|
|
465
|
+
expandableNodes: Array.from(childrenByParent.keys()),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
flattenTreeRows(model, expanded) {
|
|
469
|
+
const flattened = [];
|
|
470
|
+
const visit = (id, depth, path) => {
|
|
471
|
+
const row = model.rowsById.get(id);
|
|
472
|
+
if (!row) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const children = model.childrenByParent.get(id) ?? [];
|
|
476
|
+
flattened.push({
|
|
477
|
+
...row,
|
|
478
|
+
treeNodeId: id,
|
|
479
|
+
treeDepth: depth,
|
|
480
|
+
treeHasChildren: children.length > 0,
|
|
481
|
+
});
|
|
482
|
+
if (!expanded.has(id)) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const nextPath = new Set(path);
|
|
486
|
+
nextPath.add(id);
|
|
487
|
+
for (const child of children) {
|
|
488
|
+
if (nextPath.has(child)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
visit(child, depth + 1, nextPath);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
for (const rootId of model.roots) {
|
|
495
|
+
visit(rootId, 0, new Set());
|
|
496
|
+
}
|
|
497
|
+
return flattened;
|
|
498
|
+
}
|
|
499
|
+
flattenAllTreeRows(model) {
|
|
500
|
+
const flattened = [];
|
|
501
|
+
const visit = (id, depth, path) => {
|
|
502
|
+
const row = model.rowsById.get(id);
|
|
503
|
+
if (!row) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const children = model.childrenByParent.get(id) ?? [];
|
|
507
|
+
flattened.push({
|
|
508
|
+
...row,
|
|
509
|
+
treeNodeId: id,
|
|
510
|
+
treeDepth: depth,
|
|
511
|
+
treeHasChildren: children.length > 0,
|
|
512
|
+
});
|
|
513
|
+
const nextPath = new Set(path);
|
|
514
|
+
nextPath.add(id);
|
|
515
|
+
for (const child of children) {
|
|
516
|
+
if (nextPath.has(child)) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
visit(child, depth + 1, nextPath);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
for (const rootId of model.roots) {
|
|
523
|
+
visit(rootId, 0, new Set());
|
|
524
|
+
}
|
|
525
|
+
return flattened;
|
|
526
|
+
}
|
|
527
|
+
renderTreeCell(valueElement, row, model, expanded, rerender) {
|
|
528
|
+
const wrapper = document.createElement('div');
|
|
529
|
+
wrapper.style.display = 'flex';
|
|
530
|
+
wrapper.style.alignItems = 'center';
|
|
531
|
+
wrapper.style.gap = '0.35rem';
|
|
532
|
+
const depth = row.treeDepth ?? 0;
|
|
533
|
+
wrapper.style.paddingLeft = `${Math.max(0, depth) * 16}px`;
|
|
534
|
+
const nodeId = row.treeNodeId ?? '';
|
|
535
|
+
const hasChildren = !!row.treeHasChildren && !!nodeId && !!model;
|
|
536
|
+
if (hasChildren) {
|
|
537
|
+
const toggle = document.createElement('button');
|
|
538
|
+
toggle.type = 'button';
|
|
539
|
+
toggle.className = 'tree-toggle-button';
|
|
540
|
+
toggle.textContent = expanded.has(nodeId) ? '-' : '+';
|
|
541
|
+
toggle.style.width = '1rem';
|
|
542
|
+
toggle.style.height = '1rem';
|
|
543
|
+
toggle.style.padding = '0';
|
|
544
|
+
toggle.style.lineHeight = '1';
|
|
545
|
+
toggle.style.fontSize = '0.85rem';
|
|
546
|
+
toggle.style.border = '1px solid var(--vscode-editorWidget-border)';
|
|
547
|
+
toggle.style.background = 'var(--vscode-editor-background)';
|
|
548
|
+
toggle.style.color = 'var(--vscode-foreground)';
|
|
549
|
+
toggle.style.cursor = 'pointer';
|
|
550
|
+
toggle.addEventListener('click', (event) => {
|
|
551
|
+
event.preventDefault();
|
|
552
|
+
event.stopPropagation();
|
|
553
|
+
if (expanded.has(nodeId)) {
|
|
554
|
+
expanded.delete(nodeId);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
expanded.add(nodeId);
|
|
558
|
+
}
|
|
559
|
+
rerender();
|
|
560
|
+
});
|
|
561
|
+
wrapper.appendChild(toggle);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const spacer = document.createElement('span');
|
|
565
|
+
spacer.style.display = 'inline-block';
|
|
566
|
+
spacer.style.width = '1rem';
|
|
567
|
+
wrapper.appendChild(spacer);
|
|
568
|
+
}
|
|
569
|
+
wrapper.appendChild(valueElement);
|
|
570
|
+
return wrapper;
|
|
571
|
+
}
|
|
572
|
+
createTreeActionButton(title, ariaLabel, iconPath) {
|
|
573
|
+
const button = document.createElement('button');
|
|
574
|
+
button.type = 'button';
|
|
575
|
+
button.className = 'tree-download-btn';
|
|
576
|
+
button.title = title;
|
|
577
|
+
button.setAttribute('aria-label', ariaLabel);
|
|
578
|
+
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
579
|
+
iconSvg.setAttribute('viewBox', '0 0 24 24');
|
|
580
|
+
iconSvg.setAttribute('width', '20');
|
|
581
|
+
iconSvg.setAttribute('height', '20');
|
|
582
|
+
iconSvg.setAttribute('aria-hidden', 'true');
|
|
583
|
+
iconSvg.setAttribute('focusable', 'false');
|
|
584
|
+
iconSvg.style.fill = 'currentColor';
|
|
585
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
586
|
+
path.setAttribute('d', iconPath);
|
|
587
|
+
iconSvg.appendChild(path);
|
|
588
|
+
button.appendChild(iconSvg);
|
|
589
|
+
return button;
|
|
590
|
+
}
|
|
591
|
+
applyFiltersAndSorting(columns, rows, state) {
|
|
592
|
+
const filtered = state.search
|
|
593
|
+
? rows.filter((row) => matchesFilterQuery(columns, row.cells, state.search))
|
|
594
|
+
: rows.slice();
|
|
595
|
+
if (!state.hasUserSort) {
|
|
596
|
+
return filtered;
|
|
597
|
+
}
|
|
598
|
+
const columnIndex = columns.indexOf(state.sortKey);
|
|
599
|
+
if (columnIndex < 0) {
|
|
600
|
+
return filtered;
|
|
601
|
+
}
|
|
602
|
+
const dir = state.sortDir === 'asc' ? 1 : -1;
|
|
603
|
+
return filtered.sort((left, right) => {
|
|
604
|
+
const a = left.cells[columnIndex] ?? '';
|
|
605
|
+
const b = right.cells[columnIndex] ?? '';
|
|
606
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) * dir;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
applyStylesheet(cellElement, valueElement, rules, contexts) {
|
|
610
|
+
for (const rule of rules) {
|
|
611
|
+
if (rule.target !== 'value') {
|
|
612
|
+
if (!this.matchesRule(rule, contexts)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
for (const [property, cssValue] of Object.entries(rule.style)) {
|
|
616
|
+
cellElement.style.setProperty(property, cssValue);
|
|
617
|
+
}
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (!valueElement) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const valueTargets = this.resolveValueTargets(valueElement);
|
|
624
|
+
for (const target of valueTargets) {
|
|
625
|
+
const valueOverride = target.dataset.value ?? target.textContent ?? '';
|
|
626
|
+
if (!this.matchesRule(rule, contexts, valueOverride)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
for (const [property, cssValue] of Object.entries(rule.style)) {
|
|
630
|
+
target.style.setProperty(property, cssValue);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
matchesRule(rule, contexts, cellValueOverride) {
|
|
636
|
+
return rule.selectors.some((selector) => {
|
|
637
|
+
const contextEntry = contexts.find((entry) => entry.kind === selector.kind);
|
|
638
|
+
if (!contextEntry) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
if (selector.kind !== 'cell' || cellValueOverride === undefined) {
|
|
642
|
+
return evaluateSelectorCondition(selector.condition, contextEntry.context);
|
|
643
|
+
}
|
|
644
|
+
return evaluateSelectorCondition(selector.condition, {
|
|
645
|
+
...contextEntry.context,
|
|
646
|
+
value: cellValueOverride,
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
resolveValueTargets(valueElement) {
|
|
651
|
+
const parts = Array.from(valueElement.querySelectorAll('.table-value-part'));
|
|
652
|
+
if (parts.length > 0) {
|
|
653
|
+
return parts;
|
|
654
|
+
}
|
|
655
|
+
return [valueElement];
|
|
656
|
+
}
|
|
657
|
+
buildGridTemplate(columnCount, columnWidths) {
|
|
658
|
+
if (columnCount <= 0) {
|
|
659
|
+
return 'minmax(0, 1fr)';
|
|
660
|
+
}
|
|
661
|
+
const lastColumnIndex = columnCount - 1;
|
|
662
|
+
return columnWidths
|
|
663
|
+
.slice(0, columnCount)
|
|
664
|
+
.map((width, columnIndex) => {
|
|
665
|
+
if (columnIndex === lastColumnIndex) {
|
|
666
|
+
if (width && width > 0) {
|
|
667
|
+
return `minmax(${Math.round(width)}px, 1fr)`;
|
|
668
|
+
}
|
|
669
|
+
return 'minmax(0, 1fr)';
|
|
670
|
+
}
|
|
671
|
+
return width && width > 0 ? `${Math.round(width)}px` : 'minmax(0, 1fr)';
|
|
672
|
+
})
|
|
673
|
+
.join(' ');
|
|
674
|
+
}
|
|
675
|
+
createInitialColumnWidths(columns, rows, root) {
|
|
676
|
+
if (columns.length === 0) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
const minWidth = 72;
|
|
680
|
+
const valueReference = root.querySelector('.table-cell') ?? root;
|
|
681
|
+
const headerReference = root.querySelector('.table-header-label') ?? root;
|
|
682
|
+
const valueFont = this.resolveFont(valueReference);
|
|
683
|
+
const headerFont = this.resolveFont(headerReference);
|
|
684
|
+
return columns.map((_, columnIndex) => Math.max(minWidth, this.autosizeColumnWidth(columnIndex, columns, rows, valueFont, headerFont)));
|
|
685
|
+
}
|
|
686
|
+
scheduleInitialAutosize(params) {
|
|
687
|
+
const tryAutosize = () => {
|
|
688
|
+
if (params.state.initialAutosizeApplied) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (!params.root.isConnected) {
|
|
692
|
+
requestAnimationFrame(tryAutosize);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
params.state.columnWidths = this.createInitialColumnWidths(params.columns, params.rows, params.root);
|
|
696
|
+
params.state.initialAutosizeApplied = true;
|
|
697
|
+
params.rerender();
|
|
698
|
+
};
|
|
699
|
+
requestAnimationFrame(tryAutosize);
|
|
700
|
+
}
|
|
701
|
+
installColumnResizeHandle(params) {
|
|
702
|
+
const handle = document.createElement('span');
|
|
703
|
+
handle.className = 'table-column-resize-handle';
|
|
704
|
+
handle.title = 'Drag to resize. Double-click to autosize.';
|
|
705
|
+
handle.setAttribute('aria-hidden', 'true');
|
|
706
|
+
params.handleHost.appendChild(handle);
|
|
707
|
+
handle.addEventListener('click', (event) => {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
event.stopPropagation();
|
|
710
|
+
});
|
|
711
|
+
const minWidth = 72;
|
|
712
|
+
handle.addEventListener('mousedown', (event) => {
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
event.stopPropagation();
|
|
715
|
+
const currentWidth = params.handleHost.getBoundingClientRect().width;
|
|
716
|
+
const startWidth = params.state.columnWidths[params.columnIndex] ?? currentWidth;
|
|
717
|
+
const startX = event.clientX;
|
|
718
|
+
params.state.columnWidths[params.columnIndex] = Math.max(minWidth, Math.round(startWidth));
|
|
719
|
+
params.root.classList.add('table-resizing');
|
|
720
|
+
const onMouseMove = (moveEvent) => {
|
|
721
|
+
const delta = moveEvent.clientX - startX;
|
|
722
|
+
params.state.columnWidths[params.columnIndex] = Math.max(minWidth, Math.round(startWidth + delta));
|
|
723
|
+
params.rerender();
|
|
724
|
+
};
|
|
725
|
+
const onMouseUp = () => {
|
|
726
|
+
params.root.classList.remove('table-resizing');
|
|
727
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
728
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
729
|
+
};
|
|
730
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
731
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
732
|
+
});
|
|
733
|
+
handle.addEventListener('dblclick', (event) => {
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
event.stopPropagation();
|
|
736
|
+
const width = this.autosizeColumnWidth(params.columnIndex, params.columns, params.rows, params.referenceCell);
|
|
737
|
+
params.state.columnWidths[params.columnIndex] = Math.max(minWidth, width);
|
|
738
|
+
params.rerender();
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
autosizeColumnWidth(columnIndex, columns, rows, referenceCellOrFont, headerFontOverride) {
|
|
742
|
+
const headerText = `${columns[columnIndex] ?? ''} ▲`;
|
|
743
|
+
const values = rows.map((row) => formatCellDisplayText(row.cells[columnIndex] ?? ''));
|
|
744
|
+
const valueFont = typeof referenceCellOrFont === 'string'
|
|
745
|
+
? referenceCellOrFont
|
|
746
|
+
: this.resolveFont(referenceCellOrFont);
|
|
747
|
+
const headerFont = headerFontOverride ?? valueFont;
|
|
748
|
+
const widestValue = values.reduce((max, text) => {
|
|
749
|
+
const width = this.measureTextWidth(text, valueFont);
|
|
750
|
+
return Math.max(max, width);
|
|
751
|
+
}, 0);
|
|
752
|
+
const headerWidth = this.measureTextWidth(headerText, headerFont);
|
|
753
|
+
const widestText = Math.max(headerWidth, widestValue);
|
|
754
|
+
const horizontalPadding = 20;
|
|
755
|
+
const handleSpace = 12;
|
|
756
|
+
const treePrefixSpace = this.measureTreePrefixWidth(columnIndex, rows);
|
|
757
|
+
return Math.ceil(widestText + horizontalPadding + handleSpace + treePrefixSpace);
|
|
758
|
+
}
|
|
759
|
+
measureTreePrefixWidth(columnIndex, rows) {
|
|
760
|
+
if (columnIndex !== 0) {
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
const treeRows = rows.filter((row) => row.treeDepth !== undefined || row.treeNodeId !== undefined);
|
|
764
|
+
if (treeRows.length === 0) {
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
const treeIndent = 16;
|
|
768
|
+
const toggleWidth = 16;
|
|
769
|
+
const toggleGap = 6;
|
|
770
|
+
const toggleBorderAllowance = 2;
|
|
771
|
+
return treeRows.reduce((max, row) => {
|
|
772
|
+
const depth = Math.max(0, row.treeDepth ?? 0);
|
|
773
|
+
const prefix = (depth * treeIndent) + toggleWidth + toggleGap + toggleBorderAllowance;
|
|
774
|
+
return Math.max(max, prefix);
|
|
775
|
+
}, 0);
|
|
776
|
+
}
|
|
777
|
+
resolveFont(referenceCell) {
|
|
778
|
+
const computed = window.getComputedStyle(referenceCell);
|
|
779
|
+
const lineHeight = computed.lineHeight && computed.lineHeight !== 'normal' ? computed.lineHeight : computed.fontSize;
|
|
780
|
+
return `${computed.fontStyle} ${computed.fontVariant} ${computed.fontWeight} ${computed.fontSize} / ${lineHeight} ${computed.fontFamily}`;
|
|
781
|
+
}
|
|
782
|
+
measureTextWidth(text, font) {
|
|
783
|
+
const canvas = document.createElement('canvas');
|
|
784
|
+
const context = canvas.getContext('2d');
|
|
785
|
+
if (!context) {
|
|
786
|
+
return text.length * 8;
|
|
787
|
+
}
|
|
788
|
+
context.font = font;
|
|
789
|
+
return context.measureText(text).width;
|
|
790
|
+
}
|
|
791
|
+
formatCellDisplayValue(raw, _context) {
|
|
792
|
+
return raw;
|
|
793
|
+
}
|
|
794
|
+
renderValue(raw) {
|
|
795
|
+
const value = document.createElement('span');
|
|
796
|
+
value.className = 'table-value';
|
|
797
|
+
appendTokenizedValueParts(value, raw, 'table-value-part');
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
requestCsvDownload(columns, rows) {
|
|
801
|
+
if (columns.length === 0) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const escapeCell = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
805
|
+
const lines = [
|
|
806
|
+
columns.map(escapeCell).join(','),
|
|
807
|
+
...rows.map((row) => row.map(escapeCell).join(',')),
|
|
808
|
+
];
|
|
809
|
+
this.requestTextFileDownload(lines.join('\n'), 'table', 'csv');
|
|
810
|
+
}
|
|
811
|
+
requestTreeJsonDownload(columns, model, rows) {
|
|
812
|
+
if (columns.length === 0) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const keyMap = this.createTreeJsonKeys(columns);
|
|
816
|
+
const idColumn = columns[0] ?? '';
|
|
817
|
+
const idKey = keyMap.get(idColumn) ?? idColumn;
|
|
818
|
+
const includeIds = new Set(rows
|
|
819
|
+
.map((row) => row.treeNodeId ?? '')
|
|
820
|
+
.filter((id) => id.length > 0));
|
|
821
|
+
const serializeNode = (nodeId, path) => {
|
|
822
|
+
if (!includeIds.has(nodeId) || path.has(nodeId)) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
const row = model.rowsById.get(nodeId);
|
|
826
|
+
if (!row) {
|
|
827
|
+
return undefined;
|
|
828
|
+
}
|
|
829
|
+
const node = {};
|
|
830
|
+
columns.forEach((column, index) => {
|
|
831
|
+
const key = keyMap.get(column) ?? column;
|
|
832
|
+
const value = row.cells[index] ?? '';
|
|
833
|
+
if (index === 0) {
|
|
834
|
+
node['@id'] = value;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (value.length > 0) {
|
|
838
|
+
node[key] = value;
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
const nextPath = new Set(path);
|
|
842
|
+
nextPath.add(nodeId);
|
|
843
|
+
const children = (model.childrenByParent.get(nodeId) ?? [])
|
|
844
|
+
.map((childId) => serializeNode(childId, nextPath))
|
|
845
|
+
.filter((entry) => !!entry);
|
|
846
|
+
if (children.length > 0) {
|
|
847
|
+
node.children = children;
|
|
848
|
+
}
|
|
849
|
+
return node;
|
|
850
|
+
};
|
|
851
|
+
const graph = model.roots
|
|
852
|
+
.map((rootId) => serializeNode(rootId, new Set()))
|
|
853
|
+
.filter((entry) => !!entry);
|
|
854
|
+
const context = {};
|
|
855
|
+
context[idKey] = idColumn;
|
|
856
|
+
for (let columnIndex = 1; columnIndex < columns.length; columnIndex += 1) {
|
|
857
|
+
const column = columns[columnIndex];
|
|
858
|
+
const key = keyMap.get(column) ?? column;
|
|
859
|
+
context[key] = column;
|
|
860
|
+
}
|
|
861
|
+
context.children = 'children';
|
|
862
|
+
const payload = {
|
|
863
|
+
'@context': context,
|
|
864
|
+
'@graph': graph,
|
|
865
|
+
};
|
|
866
|
+
this.requestTextFileDownload(JSON.stringify(payload, null, 2), 'tree', 'json');
|
|
867
|
+
}
|
|
868
|
+
createTreeJsonKeys(columns) {
|
|
869
|
+
const keys = new Map();
|
|
870
|
+
const counts = new Map();
|
|
871
|
+
for (const column of columns) {
|
|
872
|
+
const base = shortLabelFromIri(column.trim()) || column;
|
|
873
|
+
const currentCount = counts.get(base) ?? 0;
|
|
874
|
+
counts.set(base, currentCount + 1);
|
|
875
|
+
const key = currentCount === 0 ? base : `${base}_${currentCount + 1}`;
|
|
876
|
+
keys.set(column, key);
|
|
877
|
+
}
|
|
878
|
+
return keys;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function compileTableStylesheet(options) {
|
|
882
|
+
const rawStylesheet = options?.stylesheet;
|
|
883
|
+
if (!Array.isArray(rawStylesheet)) {
|
|
884
|
+
return [];
|
|
885
|
+
}
|
|
886
|
+
const compiled = [];
|
|
887
|
+
for (const rawRule of rawStylesheet) {
|
|
888
|
+
if (!isRecord(rawRule)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const selectors = parseSelectors(rawRule.selector);
|
|
892
|
+
if (selectors.length === 0) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const style = normalizeStyle(rawRule.style);
|
|
896
|
+
if (Object.keys(style).length === 0) {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const target = rawRule.target === 'value' ? 'value' : 'cell';
|
|
900
|
+
compiled.push({ selectors, target, style });
|
|
901
|
+
}
|
|
902
|
+
return compiled;
|
|
903
|
+
}
|
|
904
|
+
function parseSelectors(rawSelector) {
|
|
905
|
+
if (typeof rawSelector !== 'string') {
|
|
906
|
+
return [];
|
|
907
|
+
}
|
|
908
|
+
const parts = splitSelectors(rawSelector);
|
|
909
|
+
const selectors = [];
|
|
910
|
+
for (const part of parts) {
|
|
911
|
+
const parsed = parseSelector(part);
|
|
912
|
+
if (parsed) {
|
|
913
|
+
selectors.push(parsed);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return selectors;
|
|
917
|
+
}
|
|
918
|
+
function splitSelectors(rawSelector) {
|
|
919
|
+
const parts = [];
|
|
920
|
+
let current = '';
|
|
921
|
+
let bracketDepth = 0;
|
|
922
|
+
let quote;
|
|
923
|
+
for (let i = 0; i < rawSelector.length; i += 1) {
|
|
924
|
+
const char = rawSelector[i];
|
|
925
|
+
if (quote) {
|
|
926
|
+
current += char;
|
|
927
|
+
if (char === quote && rawSelector[i - 1] !== '\\') {
|
|
928
|
+
quote = undefined;
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (char === '"' || char === '\'') {
|
|
933
|
+
quote = char;
|
|
934
|
+
current += char;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (char === '[') {
|
|
938
|
+
bracketDepth += 1;
|
|
939
|
+
current += char;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (char === ']' && bracketDepth > 0) {
|
|
943
|
+
bracketDepth -= 1;
|
|
944
|
+
current += char;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (char === ',' && bracketDepth === 0) {
|
|
948
|
+
parts.push(current.trim());
|
|
949
|
+
current = '';
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
current += char;
|
|
953
|
+
}
|
|
954
|
+
if (current.trim()) {
|
|
955
|
+
parts.push(current.trim());
|
|
956
|
+
}
|
|
957
|
+
return parts;
|
|
958
|
+
}
|
|
959
|
+
function parseSelector(selector) {
|
|
960
|
+
const trimmed = selector.trim();
|
|
961
|
+
if (!trimmed) {
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
const bracketStart = trimmed.indexOf('[');
|
|
965
|
+
if (bracketStart === -1) {
|
|
966
|
+
return toSelector(trimmed, undefined);
|
|
967
|
+
}
|
|
968
|
+
if (!trimmed.endsWith(']')) {
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
const kind = trimmed.slice(0, bracketStart).trim();
|
|
972
|
+
const condition = trimmed.slice(bracketStart + 1, -1).trim();
|
|
973
|
+
return toSelector(kind, condition || undefined);
|
|
974
|
+
}
|
|
975
|
+
function toSelector(kind, condition) {
|
|
976
|
+
const normalized = kind.trim().toLowerCase();
|
|
977
|
+
if (normalized !== 'row' && normalized !== 'column' && normalized !== 'cell' && normalized !== 'header') {
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
return { kind: normalized, condition };
|
|
981
|
+
}
|
|
982
|
+
function normalizeStyle(rawStyle) {
|
|
983
|
+
if (!isRecord(rawStyle)) {
|
|
984
|
+
return {};
|
|
985
|
+
}
|
|
986
|
+
const style = {};
|
|
987
|
+
for (const [property, rawValue] of Object.entries(rawStyle)) {
|
|
988
|
+
if (!property.trim()) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (rawValue === undefined || rawValue === null) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
style[property] = String(rawValue);
|
|
995
|
+
}
|
|
996
|
+
return style;
|
|
997
|
+
}
|
|
998
|
+
function createRowContext(columns, row) {
|
|
999
|
+
const values = row.cells;
|
|
1000
|
+
const getByKey = (key) => {
|
|
1001
|
+
if (typeof key === 'number' && Number.isInteger(key)) {
|
|
1002
|
+
return values[key];
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof key === 'string') {
|
|
1005
|
+
const index = columns.indexOf(key);
|
|
1006
|
+
return index >= 0 ? values[index] : undefined;
|
|
1007
|
+
}
|
|
1008
|
+
return undefined;
|
|
1009
|
+
};
|
|
1010
|
+
const matches = (cellValue, expected) => {
|
|
1011
|
+
if (expected === undefined) {
|
|
1012
|
+
return cellValue.length > 0;
|
|
1013
|
+
}
|
|
1014
|
+
return cellValue === String(expected);
|
|
1015
|
+
};
|
|
1016
|
+
return {
|
|
1017
|
+
index: row.index,
|
|
1018
|
+
get: (key) => getByKey(key),
|
|
1019
|
+
any: (expected) => values.some((value) => matches(value, expected)),
|
|
1020
|
+
all: (expected) => values.every((value) => matches(value, expected)),
|
|
1021
|
+
count: (expected) => expected === undefined
|
|
1022
|
+
? values.length
|
|
1023
|
+
: values.filter((value) => matches(value, expected)).length,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function createColumnContexts(columns, rows) {
|
|
1027
|
+
return columns.map((name, columnIndex) => {
|
|
1028
|
+
const values = rows.map((row) => row.cells[columnIndex] ?? '');
|
|
1029
|
+
const matches = (cellValue, expected) => {
|
|
1030
|
+
if (expected === undefined) {
|
|
1031
|
+
return cellValue.length > 0;
|
|
1032
|
+
}
|
|
1033
|
+
return cellValue === String(expected);
|
|
1034
|
+
};
|
|
1035
|
+
return {
|
|
1036
|
+
name,
|
|
1037
|
+
get: (index) => {
|
|
1038
|
+
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
return values[index];
|
|
1042
|
+
},
|
|
1043
|
+
any: (expected) => values.some((value) => matches(value, expected)),
|
|
1044
|
+
all: (expected) => values.every((value) => matches(value, expected)),
|
|
1045
|
+
count: (expected) => expected === undefined
|
|
1046
|
+
? values.length
|
|
1047
|
+
: values.filter((value) => matches(value, expected)).length,
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function createCellContext(row, columnName, value) {
|
|
1052
|
+
const rowContext = toCellRowSelectorContext(row);
|
|
1053
|
+
return {
|
|
1054
|
+
row: rowContext,
|
|
1055
|
+
rowIndex: rowContext.index,
|
|
1056
|
+
col: columnName,
|
|
1057
|
+
value,
|
|
1058
|
+
datatype: undefined,
|
|
1059
|
+
lang: undefined,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function toCellRowSelectorContext(row) {
|
|
1063
|
+
if (typeof row === 'number') {
|
|
1064
|
+
const index = row;
|
|
1065
|
+
return {
|
|
1066
|
+
index,
|
|
1067
|
+
get: () => undefined,
|
|
1068
|
+
any: () => false,
|
|
1069
|
+
all: () => false,
|
|
1070
|
+
count: () => 0,
|
|
1071
|
+
valueOf: () => index,
|
|
1072
|
+
toString: () => String(index),
|
|
1073
|
+
[Symbol.toPrimitive]: (hint) => hint === 'string' ? String(index) : index,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const base = row;
|
|
1077
|
+
const index = base.index;
|
|
1078
|
+
return {
|
|
1079
|
+
index,
|
|
1080
|
+
get: (key) => base.get(key),
|
|
1081
|
+
any: (expected) => base.any(expected),
|
|
1082
|
+
all: (expected) => base.all(expected),
|
|
1083
|
+
count: (expected) => base.count(expected),
|
|
1084
|
+
valueOf: () => index,
|
|
1085
|
+
toString: () => String(index),
|
|
1086
|
+
[Symbol.toPrimitive]: (hint) => hint === 'string' ? String(index) : index,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function createHeaderContext(name) {
|
|
1090
|
+
return { name };
|
|
1091
|
+
}
|
|
1092
|
+
function resolveHiddenColumns(columns, columnContexts, rules) {
|
|
1093
|
+
const hidden = new Set();
|
|
1094
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
1095
|
+
const columnName = columns[index];
|
|
1096
|
+
const columnContext = columnContexts[index];
|
|
1097
|
+
const headerContext = createHeaderContext(columnName);
|
|
1098
|
+
const cellContext = createCellContext(0, columnName, '');
|
|
1099
|
+
for (const rule of rules) {
|
|
1100
|
+
if (rule.target === 'value') {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const display = rule.style.display?.trim().toLowerCase();
|
|
1104
|
+
if (display !== 'none') {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const matches = rule.selectors.some((selector) => {
|
|
1108
|
+
if (selector.kind === 'header') {
|
|
1109
|
+
return evaluateSelectorCondition(selector.condition, headerContext);
|
|
1110
|
+
}
|
|
1111
|
+
if (selector.kind === 'column') {
|
|
1112
|
+
return evaluateSelectorCondition(selector.condition, columnContext);
|
|
1113
|
+
}
|
|
1114
|
+
if (selector.kind === 'cell') {
|
|
1115
|
+
return evaluateSelectorCondition(selector.condition, cellContext);
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
});
|
|
1119
|
+
if (matches) {
|
|
1120
|
+
hidden.add(columnName);
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return hidden;
|
|
1126
|
+
}
|
|
1127
|
+
function formatCellDisplayText(raw) {
|
|
1128
|
+
const parts = raw.split(/[\s,]+/).filter((entry) => entry.length > 0);
|
|
1129
|
+
if (parts.length === 0) {
|
|
1130
|
+
return raw;
|
|
1131
|
+
}
|
|
1132
|
+
return parts.map((part) => isIriValue(part) ? shortLabelFromIri(part) : part).join(' ');
|
|
1133
|
+
}
|
|
1134
|
+
function matchesFilterQuery(columns, cells, rawQuery) {
|
|
1135
|
+
const terms = parseFilterTerms(rawQuery);
|
|
1136
|
+
if (terms.length === 0) {
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
const lowerColumns = columns.map((column) => column.toLowerCase());
|
|
1140
|
+
const lowerCells = cells.map((cell) => cell.toLowerCase());
|
|
1141
|
+
return terms.every((term) => {
|
|
1142
|
+
if (term.kind === 'global') {
|
|
1143
|
+
return lowerCells.some((cell) => cell.includes(term.value));
|
|
1144
|
+
}
|
|
1145
|
+
const indexes = term.columns
|
|
1146
|
+
.map((columnName) => lowerColumns.indexOf(columnName))
|
|
1147
|
+
.filter((index) => index >= 0);
|
|
1148
|
+
if (indexes.length === 0) {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
return indexes.some((index) => (lowerCells[index] ?? '').includes(term.value));
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function parseFilterTerms(rawQuery) {
|
|
1155
|
+
const tokens = tokenizeFilterQuery(rawQuery);
|
|
1156
|
+
const terms = [];
|
|
1157
|
+
for (const token of tokens) {
|
|
1158
|
+
const scoped = parseScopedFilterToken(token);
|
|
1159
|
+
if (scoped) {
|
|
1160
|
+
terms.push(scoped);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const value = normalizeFilterValue(token);
|
|
1164
|
+
if (!value) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
terms.push({ kind: 'global', value });
|
|
1168
|
+
}
|
|
1169
|
+
return terms;
|
|
1170
|
+
}
|
|
1171
|
+
function tokenizeFilterQuery(rawQuery) {
|
|
1172
|
+
const tokens = [];
|
|
1173
|
+
let current = '';
|
|
1174
|
+
let quote;
|
|
1175
|
+
for (let index = 0; index < rawQuery.length; index += 1) {
|
|
1176
|
+
const char = rawQuery[index];
|
|
1177
|
+
if (quote) {
|
|
1178
|
+
if (char === quote && rawQuery[index - 1] !== '\\') {
|
|
1179
|
+
quote = undefined;
|
|
1180
|
+
}
|
|
1181
|
+
current += char;
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (char === '"' || char === '\'') {
|
|
1185
|
+
quote = char;
|
|
1186
|
+
current += char;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (/\s/.test(char)) {
|
|
1190
|
+
if (current.trim()) {
|
|
1191
|
+
tokens.push(current.trim());
|
|
1192
|
+
}
|
|
1193
|
+
current = '';
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
current += char;
|
|
1197
|
+
}
|
|
1198
|
+
if (current.trim()) {
|
|
1199
|
+
tokens.push(current.trim());
|
|
1200
|
+
}
|
|
1201
|
+
return tokens;
|
|
1202
|
+
}
|
|
1203
|
+
function parseScopedFilterToken(token) {
|
|
1204
|
+
const separator = token.indexOf(':');
|
|
1205
|
+
if (separator <= 0) {
|
|
1206
|
+
return undefined;
|
|
1207
|
+
}
|
|
1208
|
+
const rawScope = token.slice(0, separator).trim();
|
|
1209
|
+
const rawValue = token.slice(separator + 1).trim();
|
|
1210
|
+
const value = normalizeFilterValue(rawValue);
|
|
1211
|
+
if (!value) {
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
const multiScope = /^\((.+)\)$/.exec(rawScope);
|
|
1215
|
+
const scopeColumns = multiScope
|
|
1216
|
+
? multiScope[1].split('|').map((entry) => normalizeFilterValue(entry)).filter((entry) => !!entry)
|
|
1217
|
+
: [normalizeFilterValue(rawScope)].filter((entry) => !!entry);
|
|
1218
|
+
if (scopeColumns.length === 0) {
|
|
1219
|
+
return undefined;
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
kind: 'scoped',
|
|
1223
|
+
columns: scopeColumns,
|
|
1224
|
+
value
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeFilterValue(raw) {
|
|
1228
|
+
const trimmed = raw.trim();
|
|
1229
|
+
if (!trimmed) {
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
1232
|
+
const unquoted = unquoteFilterValue(trimmed);
|
|
1233
|
+
const normalized = unquoted.trim().toLowerCase();
|
|
1234
|
+
return normalized || undefined;
|
|
1235
|
+
}
|
|
1236
|
+
function unquoteFilterValue(value) {
|
|
1237
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
1238
|
+
return value.slice(1, -1);
|
|
1239
|
+
}
|
|
1240
|
+
return value;
|
|
1241
|
+
}
|
|
1242
|
+
function resolveContainmentColumns(columns, options) {
|
|
1243
|
+
const containment = options?.containment;
|
|
1244
|
+
if (!Array.isArray(containment) || containment.length === 0) {
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
const names = new Set();
|
|
1248
|
+
for (const entry of containment) {
|
|
1249
|
+
if (typeof entry !== 'string') {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const normalized = normalizeContainmentName(entry);
|
|
1253
|
+
if (normalized) {
|
|
1254
|
+
names.add(normalized);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (names.size === 0) {
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
const indexes = [];
|
|
1261
|
+
columns.forEach((column, index) => {
|
|
1262
|
+
if (names.has(normalizeContainmentName(column))) {
|
|
1263
|
+
indexes.push(index);
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
return indexes;
|
|
1267
|
+
}
|
|
1268
|
+
function normalizeContainmentName(raw) {
|
|
1269
|
+
const trimmed = raw.trim().toLowerCase();
|
|
1270
|
+
if (!trimmed) {
|
|
1271
|
+
return '';
|
|
1272
|
+
}
|
|
1273
|
+
const colon = trimmed.lastIndexOf(':');
|
|
1274
|
+
if (colon >= 0 && colon < trimmed.length - 1) {
|
|
1275
|
+
return trimmed.slice(colon + 1);
|
|
1276
|
+
}
|
|
1277
|
+
const hash = trimmed.lastIndexOf('#');
|
|
1278
|
+
if (hash >= 0 && hash < trimmed.length - 1) {
|
|
1279
|
+
return trimmed.slice(hash + 1);
|
|
1280
|
+
}
|
|
1281
|
+
const slash = trimmed.lastIndexOf('/');
|
|
1282
|
+
if (slash >= 0 && slash < trimmed.length - 1) {
|
|
1283
|
+
return trimmed.slice(slash + 1);
|
|
1284
|
+
}
|
|
1285
|
+
return trimmed;
|
|
1286
|
+
}
|
|
1287
|
+
function evaluateSelectorCondition(condition, context) {
|
|
1288
|
+
if (!condition) {
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
return Boolean(evaluateExpression(condition, context));
|
|
1293
|
+
}
|
|
1294
|
+
catch {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
class ExpressionParser {
|
|
1299
|
+
constructor(tokens, scope) {
|
|
1300
|
+
this.tokens = tokens;
|
|
1301
|
+
this.scope = scope;
|
|
1302
|
+
this.index = 0;
|
|
1303
|
+
}
|
|
1304
|
+
parse() {
|
|
1305
|
+
const value = this.parseOr();
|
|
1306
|
+
this.expectType('eof');
|
|
1307
|
+
return value;
|
|
1308
|
+
}
|
|
1309
|
+
parseOr() {
|
|
1310
|
+
let left = this.parseAnd();
|
|
1311
|
+
while (this.matchSymbol('||')) {
|
|
1312
|
+
const right = this.parseAnd();
|
|
1313
|
+
left = Boolean(left) || Boolean(right);
|
|
1314
|
+
}
|
|
1315
|
+
return left;
|
|
1316
|
+
}
|
|
1317
|
+
parseAnd() {
|
|
1318
|
+
let left = this.parseEquality();
|
|
1319
|
+
while (this.matchSymbol('&&')) {
|
|
1320
|
+
const right = this.parseEquality();
|
|
1321
|
+
left = Boolean(left) && Boolean(right);
|
|
1322
|
+
}
|
|
1323
|
+
return left;
|
|
1324
|
+
}
|
|
1325
|
+
parseEquality() {
|
|
1326
|
+
let left = this.parseComparison();
|
|
1327
|
+
while (true) {
|
|
1328
|
+
if (this.matchSymbol('===')) {
|
|
1329
|
+
left = left === this.parseComparison();
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (this.matchSymbol('!==')) {
|
|
1333
|
+
left = left !== this.parseComparison();
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (this.matchSymbol('==')) {
|
|
1337
|
+
left = left == this.parseComparison();
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (this.matchSymbol('!=')) {
|
|
1341
|
+
left = left != this.parseComparison();
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
return left;
|
|
1347
|
+
}
|
|
1348
|
+
parseComparison() {
|
|
1349
|
+
let left = this.parseAdditive();
|
|
1350
|
+
while (true) {
|
|
1351
|
+
if (this.matchSymbol('>=')) {
|
|
1352
|
+
left = Number(left) >= Number(this.parseAdditive());
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
if (this.matchSymbol('<=')) {
|
|
1356
|
+
left = Number(left) <= Number(this.parseAdditive());
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (this.matchSymbol('>')) {
|
|
1360
|
+
left = Number(left) > Number(this.parseAdditive());
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (this.matchSymbol('<')) {
|
|
1364
|
+
left = Number(left) < Number(this.parseAdditive());
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
return left;
|
|
1370
|
+
}
|
|
1371
|
+
parseAdditive() {
|
|
1372
|
+
let left = this.parseMultiplicative();
|
|
1373
|
+
while (true) {
|
|
1374
|
+
if (this.matchSymbol('+')) {
|
|
1375
|
+
const right = this.parseMultiplicative();
|
|
1376
|
+
left = typeof left === 'string' || typeof right === 'string'
|
|
1377
|
+
? `${left ?? ''}${right ?? ''}`
|
|
1378
|
+
: Number(left) + Number(right);
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (this.matchSymbol('-')) {
|
|
1382
|
+
left = Number(left) - Number(this.parseMultiplicative());
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
return left;
|
|
1388
|
+
}
|
|
1389
|
+
parseMultiplicative() {
|
|
1390
|
+
let left = this.parseUnary();
|
|
1391
|
+
while (true) {
|
|
1392
|
+
if (this.matchSymbol('*')) {
|
|
1393
|
+
left = Number(left) * Number(this.parseUnary());
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (this.matchSymbol('/')) {
|
|
1397
|
+
left = Number(left) / Number(this.parseUnary());
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (this.matchSymbol('%')) {
|
|
1401
|
+
left = Number(left) % Number(this.parseUnary());
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
return left;
|
|
1407
|
+
}
|
|
1408
|
+
parseUnary() {
|
|
1409
|
+
if (this.matchSymbol('!')) {
|
|
1410
|
+
return !Boolean(this.parseUnary());
|
|
1411
|
+
}
|
|
1412
|
+
if (this.matchSymbol('-')) {
|
|
1413
|
+
return -Number(this.parseUnary());
|
|
1414
|
+
}
|
|
1415
|
+
return this.parseCallChain();
|
|
1416
|
+
}
|
|
1417
|
+
parseCallChain() {
|
|
1418
|
+
let current = this.parsePrimary();
|
|
1419
|
+
let receiver = undefined;
|
|
1420
|
+
while (true) {
|
|
1421
|
+
if (this.matchSymbol('.')) {
|
|
1422
|
+
const propertyToken = this.expectType('identifier');
|
|
1423
|
+
if (current === null || current === undefined) {
|
|
1424
|
+
current = undefined;
|
|
1425
|
+
receiver = undefined;
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
const obj = Object(current);
|
|
1429
|
+
receiver = current;
|
|
1430
|
+
current = obj[propertyToken.value];
|
|
1431
|
+
}
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (this.matchSymbol('(')) {
|
|
1435
|
+
const args = [];
|
|
1436
|
+
if (!this.matchSymbol(')')) {
|
|
1437
|
+
do {
|
|
1438
|
+
args.push(this.parseOr());
|
|
1439
|
+
} while (this.matchSymbol(','));
|
|
1440
|
+
this.expectSymbol(')');
|
|
1441
|
+
}
|
|
1442
|
+
if (typeof current !== 'function') {
|
|
1443
|
+
current = undefined;
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
const fn = current;
|
|
1447
|
+
current = receiver !== undefined ? fn.apply(receiver, args) : fn(...args);
|
|
1448
|
+
}
|
|
1449
|
+
receiver = undefined;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
return current;
|
|
1455
|
+
}
|
|
1456
|
+
parsePrimary() {
|
|
1457
|
+
const token = this.current();
|
|
1458
|
+
if (token.type === 'number') {
|
|
1459
|
+
this.index += 1;
|
|
1460
|
+
return Number(token.value);
|
|
1461
|
+
}
|
|
1462
|
+
if (token.type === 'string') {
|
|
1463
|
+
this.index += 1;
|
|
1464
|
+
return token.value;
|
|
1465
|
+
}
|
|
1466
|
+
if (token.type === 'identifier') {
|
|
1467
|
+
this.index += 1;
|
|
1468
|
+
if (token.value === 'true')
|
|
1469
|
+
return true;
|
|
1470
|
+
if (token.value === 'false')
|
|
1471
|
+
return false;
|
|
1472
|
+
if (token.value === 'null')
|
|
1473
|
+
return null;
|
|
1474
|
+
if (token.value === 'undefined')
|
|
1475
|
+
return undefined;
|
|
1476
|
+
return this.scope[token.value];
|
|
1477
|
+
}
|
|
1478
|
+
if (this.matchSymbol('(')) {
|
|
1479
|
+
const value = this.parseOr();
|
|
1480
|
+
this.expectSymbol(')');
|
|
1481
|
+
return value;
|
|
1482
|
+
}
|
|
1483
|
+
throw new Error('Expected expression');
|
|
1484
|
+
}
|
|
1485
|
+
current() {
|
|
1486
|
+
return this.tokens[this.index];
|
|
1487
|
+
}
|
|
1488
|
+
matchSymbol(symbol) {
|
|
1489
|
+
const token = this.current();
|
|
1490
|
+
if (token.type === 'symbol' && token.value === symbol) {
|
|
1491
|
+
this.index += 1;
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
expectType(type) {
|
|
1497
|
+
const token = this.current();
|
|
1498
|
+
if (token.type !== type) {
|
|
1499
|
+
throw new Error(`Expected ${type}`);
|
|
1500
|
+
}
|
|
1501
|
+
this.index += 1;
|
|
1502
|
+
return token;
|
|
1503
|
+
}
|
|
1504
|
+
expectSymbol(symbol) {
|
|
1505
|
+
if (!this.matchSymbol(symbol)) {
|
|
1506
|
+
throw new Error(`Expected ${symbol}`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function evaluateExpression(expression, scope) {
|
|
1511
|
+
const parser = new ExpressionParser(tokenizeExpression(expression), scope);
|
|
1512
|
+
return parser.parse();
|
|
1513
|
+
}
|
|
1514
|
+
function tokenizeExpression(expression) {
|
|
1515
|
+
const tokens = [];
|
|
1516
|
+
let index = 0;
|
|
1517
|
+
const operators = ['===', '!==', '>=', '<=', '&&', '||', '==', '!=', '(', ')', ',', '.', '+', '-', '*', '/', '%', '!', '>', '<'];
|
|
1518
|
+
while (index < expression.length) {
|
|
1519
|
+
const char = expression[index];
|
|
1520
|
+
if (/\s/.test(char)) {
|
|
1521
|
+
index += 1;
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const operator = operators.find((op) => expression.startsWith(op, index));
|
|
1525
|
+
if (operator) {
|
|
1526
|
+
tokens.push({ type: 'symbol', value: operator });
|
|
1527
|
+
index += operator.length;
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (char === '"' || char === '\'') {
|
|
1531
|
+
const quote = char;
|
|
1532
|
+
let value = '';
|
|
1533
|
+
index += 1;
|
|
1534
|
+
while (index < expression.length) {
|
|
1535
|
+
const current = expression[index];
|
|
1536
|
+
if (current === quote) {
|
|
1537
|
+
index += 1;
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
if (current === '\\' && index + 1 < expression.length) {
|
|
1541
|
+
value += expression[index + 1];
|
|
1542
|
+
index += 2;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
value += current;
|
|
1546
|
+
index += 1;
|
|
1547
|
+
}
|
|
1548
|
+
tokens.push({ type: 'string', value });
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const numberMatch = /^[0-9]+(?:\.[0-9]+)?/.exec(expression.slice(index));
|
|
1552
|
+
if (numberMatch) {
|
|
1553
|
+
tokens.push({ type: 'number', value: numberMatch[0] });
|
|
1554
|
+
index += numberMatch[0].length;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
const identifierMatch = /^[A-Za-z_][A-Za-z0-9_]*/.exec(expression.slice(index));
|
|
1558
|
+
if (identifierMatch) {
|
|
1559
|
+
tokens.push({ type: 'identifier', value: identifierMatch[0] });
|
|
1560
|
+
index += identifierMatch[0].length;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
throw new Error(`Unexpected token '${char}'`);
|
|
1564
|
+
}
|
|
1565
|
+
tokens.push({ type: 'eof', value: '' });
|
|
1566
|
+
return tokens;
|
|
1567
|
+
}
|
|
1568
|
+
function isRecord(value) {
|
|
1569
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1570
|
+
}
|
|
1571
|
+
//# sourceMappingURL=table-renderer.js.map
|