@oml/markdown 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/md/md-execution.d.ts +16 -0
- package/out/md/md-executor.d.ts +1 -0
- package/out/md/md-executor.js +219 -35
- package/out/md/md-executor.js.map +1 -1
- package/out/renderers/diagram-renderer.js +160 -1
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/graph-renderer.js +452 -18
- package/out/renderers/graph-renderer.js.map +1 -1
- package/out/renderers/matrix-renderer.d.ts +0 -2
- package/out/renderers/matrix-renderer.js +45 -40
- package/out/renderers/matrix-renderer.js.map +1 -1
- package/out/renderers/renderer.d.ts +4 -1
- package/out/renderers/renderer.js +98 -0
- package/out/renderers/renderer.js.map +1 -1
- package/out/renderers/table-renderer.d.ts +4 -2
- package/out/renderers/table-renderer.js +104 -38
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/renderers/types.d.ts +16 -0
- package/out/renderers/wikilink-utils.d.ts +1 -0
- package/out/renderers/wikilink-utils.js +60 -32
- package/out/renderers/wikilink-utils.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +7452 -1297
- package/out/static/browser-runtime.bundle.js.map +4 -4
- package/out/static/browser-runtime.js +15 -2
- package/out/static/browser-runtime.js.map +1 -1
- package/package.json +2 -2
- package/src/md/md-execution.ts +20 -0
- package/src/md/md-executor.ts +268 -40
- package/src/renderers/diagram-renderer.ts +167 -1
- package/src/renderers/graph-renderer.ts +512 -12
- package/src/renderers/matrix-renderer.ts +57 -44
- package/src/renderers/renderer.ts +105 -1
- package/src/renderers/table-renderer.ts +151 -39
- package/src/renderers/types.ts +20 -0
- package/src/renderers/wikilink-utils.ts +66 -31
- package/src/static/browser-runtime.ts +20 -2
- package/src/static/markdown-webview.css +44 -15
|
@@ -12,7 +12,9 @@ const CSS_EDGE_TEXT = CSS_EDGE_STROKE;
|
|
|
12
12
|
const CSS_EDGE_BG = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
13
13
|
const CSS_FONT_FAMILY = 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)';
|
|
14
14
|
/** Extra height beneath the circle/rect shape to accommodate the below-node label. */
|
|
15
|
-
const NODE_LABEL_HEIGHT =
|
|
15
|
+
const NODE_LABEL_HEIGHT = 18;
|
|
16
|
+
const NODE_FONT_SIZE = 10;
|
|
17
|
+
const EDGE_FONT_SIZE = 10;
|
|
16
18
|
// --- Module-level library caches ---
|
|
17
19
|
let x6GraphCtor;
|
|
18
20
|
let dagreLib;
|
|
@@ -80,18 +82,19 @@ export class GraphMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
|
80
82
|
const maxDegree = Math.max(...degrees);
|
|
81
83
|
const layout = resolveLayoutOptions(result.options);
|
|
82
84
|
const stylesheet = compileGraphStylesheet(result.options);
|
|
85
|
+
const expandOnClick = resolveExpandOnClickOption(result.options);
|
|
83
86
|
const nodeDataList = [...nodeMap.values()];
|
|
84
87
|
const csvDataset = {
|
|
85
88
|
columns: payload.columns.slice(),
|
|
86
89
|
rows: payload.rows.map((row) => row.slice()),
|
|
87
90
|
downloadCsv: (content) => this.requestTextFileDownload(content, 'graph', 'csv'),
|
|
88
91
|
};
|
|
89
|
-
initializeGraphWhenReady(graphRoot, nodeDataList, edgeList, minDegree, maxDegree, container, layout, stylesheet, csvDataset);
|
|
92
|
+
initializeGraphWhenReady(graphRoot, nodeDataList, edgeList, minDegree, maxDegree, container, layout, stylesheet, result.blockSource, result.blockId, expandOnClick, csvDataset);
|
|
90
93
|
return container;
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
// --- Graph initialization ---
|
|
94
|
-
function initializeGraphWhenReady(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, messageContainer, layoutOptions, stylesheet, csvDataset) {
|
|
97
|
+
function initializeGraphWhenReady(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, messageContainer, layoutOptions, stylesheet, blockSource, blockId, expandOnClick, csvDataset) {
|
|
95
98
|
const maxAttempts = 20;
|
|
96
99
|
let attempts = 0;
|
|
97
100
|
const tryInit = () => {
|
|
@@ -102,7 +105,7 @@ function initializeGraphWhenReady(graphRoot, nodeDataList, edgeDataList, minDegr
|
|
|
102
105
|
}
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
105
|
-
void doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, layoutOptions, stylesheet, csvDataset).catch((error) => {
|
|
108
|
+
void doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, layoutOptions, stylesheet, blockSource, blockId, expandOnClick, csvDataset).catch((error) => {
|
|
106
109
|
const detail = error instanceof Error ? error.message : String(error);
|
|
107
110
|
const message = document.createElement('div');
|
|
108
111
|
message.className = 'oml-md-result-message';
|
|
@@ -112,7 +115,7 @@ function initializeGraphWhenReady(graphRoot, nodeDataList, edgeDataList, minDegr
|
|
|
112
115
|
};
|
|
113
116
|
requestAnimationFrame(tryInit);
|
|
114
117
|
}
|
|
115
|
-
async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, layoutOptions, stylesheet, csvDataset) {
|
|
118
|
+
async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, maxDegree, layoutOptions, stylesheet, blockSource, blockId, expandOnClick, csvDataset) {
|
|
116
119
|
const GraphCtor = await loadX6GraphCtor();
|
|
117
120
|
const graphView = new GraphCtor({
|
|
118
121
|
container: graphRoot,
|
|
@@ -124,6 +127,7 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
124
127
|
minScale: 0.3,
|
|
125
128
|
maxScale: 3,
|
|
126
129
|
factor: 1.1,
|
|
130
|
+
modifiers: ['meta', 'ctrl'],
|
|
127
131
|
},
|
|
128
132
|
connecting: {
|
|
129
133
|
allowBlank: false,
|
|
@@ -161,12 +165,23 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
161
165
|
for (const data of edgeDataList) {
|
|
162
166
|
graphView.addEdge(buildEdgeDef(data));
|
|
163
167
|
}
|
|
168
|
+
const mutableState = {
|
|
169
|
+
nodesById: new Map(nodeDataList.map((node) => [node.id, node])),
|
|
170
|
+
edgeKeys: new Set(edgeDataList.map((edge) => `${edge.source}|${toNodeId(edge.value)}|${edge.target}`)),
|
|
171
|
+
edgeSequence: edgeDataList.length,
|
|
172
|
+
minDegree,
|
|
173
|
+
maxDegree,
|
|
174
|
+
};
|
|
164
175
|
// Resize the X6 canvas whenever the host element resizes.
|
|
176
|
+
const recenterAfterResizeOrMove = () => {
|
|
177
|
+
centerGraphContent(graphView, layoutOptions.padding);
|
|
178
|
+
};
|
|
165
179
|
const resizeObserver = new ResizeObserver(() => {
|
|
166
180
|
if (!graphRoot.isConnected) {
|
|
167
181
|
return;
|
|
168
182
|
}
|
|
169
183
|
graphView.resize(graphRoot.clientWidth, graphRoot.clientHeight);
|
|
184
|
+
recenterAfterResizeOrMove();
|
|
170
185
|
});
|
|
171
186
|
resizeObserver.observe(graphRoot);
|
|
172
187
|
// Also watch the result container so that when the page grows wider the
|
|
@@ -178,6 +193,7 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
178
193
|
const nextWidth = Math.max(0, Math.floor(resultContainer.clientWidth));
|
|
179
194
|
graphRoot.style.width = `${nextWidth}px`;
|
|
180
195
|
graphView.resize(graphRoot.clientWidth, graphRoot.clientHeight);
|
|
196
|
+
recenterAfterResizeOrMove();
|
|
181
197
|
});
|
|
182
198
|
resultResizeObserver.observe(resultContainer);
|
|
183
199
|
}
|
|
@@ -196,15 +212,19 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
196
212
|
// Reset to CSS-variable base attrs so stale user-rule colors are cleared.
|
|
197
213
|
for (const node of graphView.getNodes()) {
|
|
198
214
|
const nodeData = node.getData();
|
|
215
|
+
node.setData({ hidden: false }, { merge: true });
|
|
199
216
|
node.setAttrs(buildNodeBaseAttrs(nodeData.literal));
|
|
200
217
|
}
|
|
201
218
|
for (const edge of graphView.getEdges()) {
|
|
219
|
+
edge.setData({ hidden: false }, { merge: true });
|
|
202
220
|
edge.setAttrs(buildEdgeBaseAttrs());
|
|
203
221
|
resetEdgeLabelColors(edge);
|
|
204
222
|
}
|
|
205
223
|
applyGraphStylesheet(graphView, stylesheet, theme);
|
|
224
|
+
applyGraphVisibilityState(graphView);
|
|
206
225
|
};
|
|
207
226
|
applyCurrentTheme();
|
|
227
|
+
installGraphNodeInteractions(graphView, graphRoot, applyCurrentTheme, blockSource, blockId, expandOnClick);
|
|
208
228
|
const themeObserver = new MutationObserver(() => {
|
|
209
229
|
applyCurrentTheme();
|
|
210
230
|
});
|
|
@@ -230,6 +250,26 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
230
250
|
if (layoutOptions.mode === 'force' && layoutOptions.running) {
|
|
231
251
|
startContinuousForceEngine(graphView, graphRoot, layoutOptions.force, grabbedNodes);
|
|
232
252
|
}
|
|
253
|
+
const onExpandResult = (event) => {
|
|
254
|
+
if (!(event instanceof CustomEvent)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const detail = event.detail;
|
|
258
|
+
const targetBlockId = typeof detail?.blockId === 'string' ? detail.blockId.trim() : '';
|
|
259
|
+
if (targetBlockId !== blockId) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const payload = normalizeGraphExpandPayload(detail?.payload);
|
|
263
|
+
if (!payload) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
mergeExpandedGraphData(graphView, mutableState, payload);
|
|
267
|
+
applyCurrentTheme();
|
|
268
|
+
void runLayout(graphView, layoutOptions).then(() => {
|
|
269
|
+
centerGraphContent(graphView, layoutOptions.padding);
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
graphRoot.addEventListener('md-graph-expand-result', onExpandResult);
|
|
233
273
|
installGraphToolbar(graphRoot, graphView, csvDataset);
|
|
234
274
|
// Manual canvas resize handle.
|
|
235
275
|
const parsedMinHeight = parseInt(graphRoot.style.minHeight, 10);
|
|
@@ -257,11 +297,214 @@ async function doGraphInit(graphRoot, nodeDataList, edgeDataList, minDegree, max
|
|
|
257
297
|
if (!canvasResize || event.pointerId !== canvasResize.pointerId)
|
|
258
298
|
return;
|
|
259
299
|
canvasResize = undefined;
|
|
300
|
+
recenterAfterResizeOrMove();
|
|
260
301
|
};
|
|
261
302
|
resizeHandle.addEventListener('pointerdown', onResizePointerDown);
|
|
262
303
|
resizeHandle.addEventListener('pointermove', onResizePointerMove);
|
|
263
304
|
resizeHandle.addEventListener('pointerup', onResizePointerEnd);
|
|
264
305
|
resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
|
|
306
|
+
graphView.on('node:mouseup', recenterAfterResizeOrMove);
|
|
307
|
+
graphRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
308
|
+
graphRoot.removeEventListener('md-graph-expand-result', onExpandResult);
|
|
309
|
+
}, { once: true });
|
|
310
|
+
}
|
|
311
|
+
function installGraphNodeInteractions(graphView, graphRoot, reapplyBaseTheme, blockSource, blockId, expandOnClick) {
|
|
312
|
+
const ensureSvgNativeTitle = (element, iri) => {
|
|
313
|
+
let titleNode = null;
|
|
314
|
+
for (const child of Array.from(element.children)) {
|
|
315
|
+
if (child instanceof SVGTitleElement) {
|
|
316
|
+
titleNode = child;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!titleNode) {
|
|
321
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
322
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
323
|
+
}
|
|
324
|
+
titleNode.textContent = iri;
|
|
325
|
+
};
|
|
326
|
+
const applyNativeTooltipTitle = (container, iri, eventTarget) => {
|
|
327
|
+
if (!iri) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (container instanceof HTMLElement || container instanceof SVGElement) {
|
|
331
|
+
container.setAttribute('title', iri);
|
|
332
|
+
}
|
|
333
|
+
if (container instanceof Element) {
|
|
334
|
+
for (const element of Array.from(container.querySelectorAll('*'))) {
|
|
335
|
+
element.setAttribute('title', iri);
|
|
336
|
+
if (element instanceof SVGElement) {
|
|
337
|
+
ensureSvgNativeTitle(element, iri);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (container instanceof SVGElement) {
|
|
341
|
+
ensureSvgNativeTitle(container, iri);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (eventTarget instanceof HTMLElement || eventTarget instanceof SVGElement) {
|
|
345
|
+
eventTarget.setAttribute('title', iri);
|
|
346
|
+
if (eventTarget instanceof SVGElement) {
|
|
347
|
+
ensureSvgNativeTitle(eventTarget, iri);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
let hoveredNodeId;
|
|
352
|
+
const isExpandModifierPressed = (event) => Boolean(event?.shiftKey);
|
|
353
|
+
const applyHoverState = (node) => {
|
|
354
|
+
const hoveredId = String(node?.id ?? '');
|
|
355
|
+
if (!hoveredId) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
reapplyBaseTheme();
|
|
359
|
+
hoveredNodeId = hoveredId;
|
|
360
|
+
const theme = resolveThemePalette();
|
|
361
|
+
const activeNodeIds = new Set([hoveredId]);
|
|
362
|
+
const activeEdgeIds = new Set();
|
|
363
|
+
const connectedEdges = graphView.getConnectedEdges(node) ?? [];
|
|
364
|
+
for (const edge of connectedEdges) {
|
|
365
|
+
activeEdgeIds.add(String(edge.id));
|
|
366
|
+
const sourceId = String(edge.getSourceCellId?.() ?? '');
|
|
367
|
+
const targetId = String(edge.getTargetCellId?.() ?? '');
|
|
368
|
+
if (sourceId) {
|
|
369
|
+
activeNodeIds.add(sourceId);
|
|
370
|
+
}
|
|
371
|
+
if (targetId) {
|
|
372
|
+
activeNodeIds.add(targetId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const candidate of graphView.getNodes()) {
|
|
376
|
+
const candidateId = String(candidate.id);
|
|
377
|
+
const active = activeNodeIds.has(candidateId);
|
|
378
|
+
if (!active) {
|
|
379
|
+
candidate.setAttrs({
|
|
380
|
+
body: { opacity: 0.2 },
|
|
381
|
+
label: { opacity: 0.28 },
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (candidateId === hoveredId) {
|
|
386
|
+
candidate.setAttrs({
|
|
387
|
+
body: {
|
|
388
|
+
fill: theme.accent,
|
|
389
|
+
stroke: theme.selectedStroke,
|
|
390
|
+
strokeWidth: 2,
|
|
391
|
+
opacity: 1,
|
|
392
|
+
},
|
|
393
|
+
label: {
|
|
394
|
+
fill: theme.accentForeground,
|
|
395
|
+
opacity: 1,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const edge of graphView.getEdges()) {
|
|
401
|
+
const active = activeEdgeIds.has(String(edge.id));
|
|
402
|
+
if (!active) {
|
|
403
|
+
edge.setAttrs({
|
|
404
|
+
line: { opacity: 0.16 },
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
edge.prop('labels/0/attrs/label/opacity', 0.2);
|
|
408
|
+
edge.prop('labels/0/attrs/body/opacity', 0.12);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// ignore
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
edge.setAttrs({
|
|
416
|
+
line: {
|
|
417
|
+
opacity: 1,
|
|
418
|
+
stroke: theme.accent,
|
|
419
|
+
strokeWidth: Math.max(1.6, Number(edge.attr?.('line/strokeWidth')) || 1.6),
|
|
420
|
+
targetMarker: {
|
|
421
|
+
name: 'classic',
|
|
422
|
+
size: 6,
|
|
423
|
+
fill: theme.accent,
|
|
424
|
+
stroke: theme.accent,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
edge.prop('labels/0/attrs/label/fill', theme.accent);
|
|
430
|
+
edge.prop('labels/0/attrs/body/opacity', 1);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// ignore
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const clearHoverState = () => {
|
|
438
|
+
if (!hoveredNodeId) {
|
|
439
|
+
graphRoot.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
hoveredNodeId = undefined;
|
|
443
|
+
reapplyBaseTheme();
|
|
444
|
+
graphRoot.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
445
|
+
};
|
|
446
|
+
graphView.on('node:mouseenter', ({ node, e }) => {
|
|
447
|
+
const data = node?.getData?.();
|
|
448
|
+
const iri = String(data?.value ?? '');
|
|
449
|
+
applyHoverState(node);
|
|
450
|
+
if (iri) {
|
|
451
|
+
const view = graphView.findViewByCell?.(node);
|
|
452
|
+
const bbox = view?.container?.getBoundingClientRect?.();
|
|
453
|
+
applyNativeTooltipTitle(view?.container, iri, e?.target ?? null);
|
|
454
|
+
if (bbox) {
|
|
455
|
+
graphRoot.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
456
|
+
bubbles: true,
|
|
457
|
+
detail: {
|
|
458
|
+
iri,
|
|
459
|
+
previewEnabled: !!e && (/^Mac/i.test(navigator.platform) ? e.metaKey : e.ctrlKey),
|
|
460
|
+
anchorRect: {
|
|
461
|
+
left: bbox.left,
|
|
462
|
+
right: bbox.right,
|
|
463
|
+
top: bbox.top,
|
|
464
|
+
bottom: bbox.bottom,
|
|
465
|
+
width: bbox.width,
|
|
466
|
+
height: bbox.height,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
graphView.on('node:mouseleave', () => {
|
|
474
|
+
clearHoverState();
|
|
475
|
+
});
|
|
476
|
+
graphView.on('blank:mousemove', () => {
|
|
477
|
+
clearHoverState();
|
|
478
|
+
});
|
|
479
|
+
graphView.on('node:click', ({ node, e }) => {
|
|
480
|
+
if (e && e.button !== 0) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const data = node?.getData?.();
|
|
484
|
+
const iri = String(data?.value ?? '');
|
|
485
|
+
if (!isIriValue(iri)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (expandOnClick && isExpandModifierPressed(e)) {
|
|
489
|
+
const source = (blockSource ?? '').trim();
|
|
490
|
+
if (!source) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
graphRoot.dispatchEvent(new CustomEvent('md-graph-expand-request', {
|
|
494
|
+
bubbles: true,
|
|
495
|
+
detail: {
|
|
496
|
+
blockId,
|
|
497
|
+
blockSource: source,
|
|
498
|
+
contextIri: iri.trim(),
|
|
499
|
+
},
|
|
500
|
+
}));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
graphRoot.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
504
|
+
bubbles: true,
|
|
505
|
+
detail: { iri },
|
|
506
|
+
}));
|
|
507
|
+
});
|
|
265
508
|
}
|
|
266
509
|
// --- X6 / dagre library loaders ---
|
|
267
510
|
async function loadX6GraphCtor() {
|
|
@@ -287,11 +530,11 @@ async function loadDagreLib() {
|
|
|
287
530
|
// --- Node / edge definition builders ---
|
|
288
531
|
function computeNodeSize(degree, minDegree, maxDegree) {
|
|
289
532
|
if (minDegree === maxDegree) {
|
|
290
|
-
return
|
|
533
|
+
return 28;
|
|
291
534
|
}
|
|
292
|
-
//
|
|
535
|
+
// Keep default nodes compact while still preserving degree-based variation.
|
|
293
536
|
const t = Math.max(0, Math.min(1, degree / 10));
|
|
294
|
-
return Math.round(
|
|
537
|
+
return Math.round(16 + t * (40 - 16));
|
|
295
538
|
}
|
|
296
539
|
/** Base attrs for a node using CSS variables so color auto-updates with VS Code theme. */
|
|
297
540
|
function buildNodeBaseAttrs(literal) {
|
|
@@ -300,11 +543,13 @@ function buildNodeBaseAttrs(literal) {
|
|
|
300
543
|
fill: literal ? CSS_LIT_FILL : CSS_NODE_FILL,
|
|
301
544
|
stroke: literal ? CSS_LIT_STROKE : CSS_NODE_STROKE,
|
|
302
545
|
strokeWidth: 1,
|
|
546
|
+
opacity: 1,
|
|
303
547
|
},
|
|
304
548
|
label: {
|
|
305
549
|
fill: CSS_NODE_TEXT,
|
|
306
|
-
fontSize:
|
|
550
|
+
fontSize: NODE_FONT_SIZE,
|
|
307
551
|
fontFamily: CSS_FONT_FAMILY,
|
|
552
|
+
opacity: 1,
|
|
308
553
|
},
|
|
309
554
|
};
|
|
310
555
|
}
|
|
@@ -314,6 +559,7 @@ function buildEdgeBaseAttrs() {
|
|
|
314
559
|
line: {
|
|
315
560
|
stroke: CSS_EDGE_STROKE,
|
|
316
561
|
strokeWidth: 1.4,
|
|
562
|
+
opacity: 1,
|
|
317
563
|
targetMarker: {
|
|
318
564
|
name: 'classic',
|
|
319
565
|
size: 6,
|
|
@@ -327,7 +573,9 @@ function buildEdgeBaseAttrs() {
|
|
|
327
573
|
function resetEdgeLabelColors(edge) {
|
|
328
574
|
try {
|
|
329
575
|
edge.prop('labels/0/attrs/label/fill', CSS_EDGE_TEXT);
|
|
576
|
+
edge.prop('labels/0/attrs/label/opacity', 1);
|
|
330
577
|
edge.prop('labels/0/attrs/body/fill', CSS_EDGE_BG);
|
|
578
|
+
edge.prop('labels/0/attrs/body/opacity', 1);
|
|
331
579
|
}
|
|
332
580
|
catch {
|
|
333
581
|
// ignore - edge may have no labels
|
|
@@ -356,12 +604,12 @@ function buildNodeDef(data, size) {
|
|
|
356
604
|
label: {
|
|
357
605
|
text: data.label,
|
|
358
606
|
fill: CSS_NODE_TEXT,
|
|
359
|
-
fontSize:
|
|
607
|
+
fontSize: NODE_FONT_SIZE,
|
|
360
608
|
fontFamily: CSS_FONT_FAMILY,
|
|
361
609
|
textAnchor: 'middle',
|
|
362
610
|
textVerticalAnchor: 'top',
|
|
363
611
|
refX: '50%',
|
|
364
|
-
refY: size +
|
|
612
|
+
refY: size + 3, // below the visible shape
|
|
365
613
|
},
|
|
366
614
|
},
|
|
367
615
|
data: {
|
|
@@ -370,6 +618,7 @@ function buildNodeDef(data, size) {
|
|
|
370
618
|
degree: data.degree,
|
|
371
619
|
literal: data.literal,
|
|
372
620
|
group: '',
|
|
621
|
+
hidden: Boolean(data.hidden),
|
|
373
622
|
},
|
|
374
623
|
zIndex: 1,
|
|
375
624
|
};
|
|
@@ -417,7 +666,7 @@ function buildEdgeDef(data) {
|
|
|
417
666
|
label: {
|
|
418
667
|
text: data.label,
|
|
419
668
|
fill: CSS_EDGE_TEXT,
|
|
420
|
-
fontSize:
|
|
669
|
+
fontSize: EDGE_FONT_SIZE,
|
|
421
670
|
fontFamily: CSS_FONT_FAMILY,
|
|
422
671
|
textAnchor: 'middle',
|
|
423
672
|
textVerticalAnchor: 'middle',
|
|
@@ -428,6 +677,7 @@ function buildEdgeDef(data) {
|
|
|
428
677
|
data: {
|
|
429
678
|
label: data.label,
|
|
430
679
|
value: data.value,
|
|
680
|
+
hidden: Boolean(data.hidden),
|
|
431
681
|
},
|
|
432
682
|
zIndex: 0,
|
|
433
683
|
};
|
|
@@ -553,6 +803,18 @@ function fitGraphContent(graphView, padding) {
|
|
|
553
803
|
}
|
|
554
804
|
});
|
|
555
805
|
}
|
|
806
|
+
function centerGraphContent(graphView, padding) {
|
|
807
|
+
requestAnimationFrame(() => {
|
|
808
|
+
try {
|
|
809
|
+
if (typeof graphView.centerContent === 'function') {
|
|
810
|
+
graphView.centerContent({ padding });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
// ignore
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
556
818
|
// --- Continuous force engine ---
|
|
557
819
|
function startContinuousForceEngine(graphView, graphRoot, options, grabbedNodes) {
|
|
558
820
|
const velocity = new Map();
|
|
@@ -712,6 +974,28 @@ function applyGraphStylesheet(graphView, rules, theme) {
|
|
|
712
974
|
}
|
|
713
975
|
}
|
|
714
976
|
}
|
|
977
|
+
function applyGraphVisibilityState(graphView) {
|
|
978
|
+
const visibleNodeIds = new Set();
|
|
979
|
+
for (const node of graphView.getNodes()) {
|
|
980
|
+
const hidden = Boolean(node.getData?.()?.hidden);
|
|
981
|
+
if (hidden) {
|
|
982
|
+
node.hide();
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
node.show();
|
|
986
|
+
visibleNodeIds.add(String(node.id));
|
|
987
|
+
}
|
|
988
|
+
for (const edge of graphView.getEdges()) {
|
|
989
|
+
const hidden = Boolean(edge.getData?.()?.hidden);
|
|
990
|
+
const sourceId = String(edge.getSourceCellId?.() ?? '');
|
|
991
|
+
const targetId = String(edge.getTargetCellId?.() ?? '');
|
|
992
|
+
if (hidden || !visibleNodeIds.has(sourceId) || !visibleNodeIds.has(targetId)) {
|
|
993
|
+
edge.hide();
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
edge.show();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
715
999
|
function buildNodeContext(node, graphView) {
|
|
716
1000
|
const nodeId = String(node.id);
|
|
717
1001
|
const nodeContext = buildSelectorNodeContext(node, graphView);
|
|
@@ -796,6 +1080,12 @@ function applyElementStyle(element, style, kind, theme) {
|
|
|
796
1080
|
if (!property) {
|
|
797
1081
|
continue;
|
|
798
1082
|
}
|
|
1083
|
+
if ((property === 'display' && value.trim().toLowerCase() === 'none')
|
|
1084
|
+
|| (property === 'visibility' && value.trim().toLowerCase() === 'hidden')
|
|
1085
|
+
|| (property === 'hidden' && value.trim().toLowerCase() === 'true')) {
|
|
1086
|
+
element.setData({ hidden: true }, { merge: true });
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
799
1089
|
const resolved = resolveColorToken(value, theme);
|
|
800
1090
|
if (property === 'fill') {
|
|
801
1091
|
bodyAttrs.fill = resolved;
|
|
@@ -813,6 +1103,10 @@ function applyElementStyle(element, style, kind, theme) {
|
|
|
813
1103
|
labelAttrs.fill = resolved;
|
|
814
1104
|
continue;
|
|
815
1105
|
}
|
|
1106
|
+
if (property === 'font-size') {
|
|
1107
|
+
labelAttrs.fontSize = value;
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
816
1110
|
const camel = property.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
817
1111
|
bodyAttrs[camel] = resolved;
|
|
818
1112
|
}
|
|
@@ -835,6 +1129,12 @@ function applyElementStyle(element, style, kind, theme) {
|
|
|
835
1129
|
if (!property) {
|
|
836
1130
|
continue;
|
|
837
1131
|
}
|
|
1132
|
+
if ((property === 'display' && value.trim().toLowerCase() === 'none')
|
|
1133
|
+
|| (property === 'visibility' && value.trim().toLowerCase() === 'hidden')
|
|
1134
|
+
|| (property === 'hidden' && value.trim().toLowerCase() === 'true')) {
|
|
1135
|
+
element.setData({ hidden: true }, { merge: true });
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
838
1138
|
const resolved = resolveColorToken(value, theme);
|
|
839
1139
|
if (property === 'stroke') {
|
|
840
1140
|
lineAttrs.stroke = resolved;
|
|
@@ -849,6 +1149,15 @@ function applyElementStyle(element, style, kind, theme) {
|
|
|
849
1149
|
edgeLabelFill = resolved;
|
|
850
1150
|
continue;
|
|
851
1151
|
}
|
|
1152
|
+
if (property === 'font-size') {
|
|
1153
|
+
try {
|
|
1154
|
+
element.prop('labels/0/attrs/label/fontSize', value);
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
// ignore
|
|
1158
|
+
}
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
852
1161
|
const camel = property.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
853
1162
|
lineAttrs[camel] = resolved;
|
|
854
1163
|
}
|
|
@@ -922,15 +1231,36 @@ function applyNeighborhoodFilter(graphView, rawQuery) {
|
|
|
922
1231
|
const query = rawQuery.trim().toLowerCase();
|
|
923
1232
|
const allNodes = graphView.getNodes();
|
|
924
1233
|
const allEdges = graphView.getEdges();
|
|
1234
|
+
const baselineHiddenNodeIds = new Set(allNodes
|
|
1235
|
+
.filter((node) => Boolean(node.getData?.()?.hidden))
|
|
1236
|
+
.map((node) => String(node.id)));
|
|
1237
|
+
const baselineHiddenEdgeIds = new Set(allEdges
|
|
1238
|
+
.filter((edge) => Boolean(edge.getData?.()?.hidden))
|
|
1239
|
+
.map((edge) => String(edge.id)));
|
|
925
1240
|
if (!query) {
|
|
926
|
-
for (const node of allNodes)
|
|
927
|
-
node.
|
|
928
|
-
|
|
929
|
-
|
|
1241
|
+
for (const node of allNodes) {
|
|
1242
|
+
if (baselineHiddenNodeIds.has(String(node.id))) {
|
|
1243
|
+
node.hide();
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
node.show();
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
for (const edge of allEdges) {
|
|
1250
|
+
if (baselineHiddenEdgeIds.has(String(edge.id))) {
|
|
1251
|
+
edge.hide();
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
edge.show();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
930
1257
|
return;
|
|
931
1258
|
}
|
|
932
1259
|
const matchedIds = new Set();
|
|
933
1260
|
for (const node of allNodes) {
|
|
1261
|
+
if (baselineHiddenNodeIds.has(String(node.id))) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
934
1264
|
const nodeData = node.getData();
|
|
935
1265
|
const label = String(nodeData?.label ?? '').toLowerCase();
|
|
936
1266
|
const value = String(nodeData?.value ?? '').toLowerCase();
|
|
@@ -952,7 +1282,7 @@ function applyNeighborhoodFilter(graphView, rawQuery) {
|
|
|
952
1282
|
}
|
|
953
1283
|
}
|
|
954
1284
|
for (const node of allNodes) {
|
|
955
|
-
if (keepNodeIds.has(String(node.id))) {
|
|
1285
|
+
if (keepNodeIds.has(String(node.id)) && !baselineHiddenNodeIds.has(String(node.id))) {
|
|
956
1286
|
node.show();
|
|
957
1287
|
}
|
|
958
1288
|
else {
|
|
@@ -963,7 +1293,11 @@ function applyNeighborhoodFilter(graphView, rawQuery) {
|
|
|
963
1293
|
for (const edge of allEdges) {
|
|
964
1294
|
const srcId = edge.getSourceCellId();
|
|
965
1295
|
const tgtId = edge.getTargetCellId();
|
|
966
|
-
if (
|
|
1296
|
+
if (!baselineHiddenEdgeIds.has(String(edge.id))
|
|
1297
|
+
&& keepNodeIds.has(srcId)
|
|
1298
|
+
&& keepNodeIds.has(tgtId)
|
|
1299
|
+
&& !baselineHiddenNodeIds.has(String(srcId))
|
|
1300
|
+
&& !baselineHiddenNodeIds.has(String(tgtId))) {
|
|
967
1301
|
edge.show();
|
|
968
1302
|
}
|
|
969
1303
|
else {
|
|
@@ -1180,6 +1514,9 @@ function resolveCanvasHeight(options) {
|
|
|
1180
1514
|
function resolveCanvasMinHeight(options) {
|
|
1181
1515
|
return `${numericCanvasMinHeight(options)}px`;
|
|
1182
1516
|
}
|
|
1517
|
+
function resolveExpandOnClickOption(options) {
|
|
1518
|
+
return asBoolean(options?.expandOnClick, false);
|
|
1519
|
+
}
|
|
1183
1520
|
function numericCanvasMinHeight(options) {
|
|
1184
1521
|
const canvas = isRecord(options?.canvas) ? options.canvas : undefined;
|
|
1185
1522
|
const raw = canvas?.minHeight ?? options?.minHeight;
|
|
@@ -1331,6 +1668,103 @@ function resolveColorToken(value, theme) {
|
|
|
1331
1668
|
return theme.border;
|
|
1332
1669
|
return trimmed;
|
|
1333
1670
|
}
|
|
1671
|
+
function normalizeGraphExpandPayload(value) {
|
|
1672
|
+
if (!isRecord(value)) {
|
|
1673
|
+
return undefined;
|
|
1674
|
+
}
|
|
1675
|
+
const columns = Array.isArray(value.columns)
|
|
1676
|
+
? value.columns.filter((entry) => typeof entry === 'string')
|
|
1677
|
+
: [];
|
|
1678
|
+
const rows = Array.isArray(value.rows)
|
|
1679
|
+
? value.rows.map((row) => Array.isArray(row)
|
|
1680
|
+
? row.map((cell) => (typeof cell === 'string' ? cell : String(cell ?? '')))
|
|
1681
|
+
: [])
|
|
1682
|
+
: [];
|
|
1683
|
+
if (columns.length === 0 || rows.length === 0) {
|
|
1684
|
+
return undefined;
|
|
1685
|
+
}
|
|
1686
|
+
return { columns, rows };
|
|
1687
|
+
}
|
|
1688
|
+
function mergeExpandedGraphData(graphView, state, payload) {
|
|
1689
|
+
const subjectIndex = payload.columns.indexOf('subject');
|
|
1690
|
+
const predicateIndex = payload.columns.indexOf('predicate');
|
|
1691
|
+
const objectIndex = payload.columns.indexOf('object');
|
|
1692
|
+
if (subjectIndex < 0 || predicateIndex < 0 || objectIndex < 0) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const ensureNodeInState = (rawValue) => {
|
|
1696
|
+
const nodeId = toNodeId(rawValue);
|
|
1697
|
+
const existing = state.nodesById.get(nodeId);
|
|
1698
|
+
if (existing) {
|
|
1699
|
+
return existing;
|
|
1700
|
+
}
|
|
1701
|
+
const node = {
|
|
1702
|
+
id: nodeId,
|
|
1703
|
+
label: shortLabel(rawValue),
|
|
1704
|
+
value: rawValue,
|
|
1705
|
+
degree: 0,
|
|
1706
|
+
literal: isLiteralValue(rawValue),
|
|
1707
|
+
group: '',
|
|
1708
|
+
};
|
|
1709
|
+
state.nodesById.set(nodeId, node);
|
|
1710
|
+
graphView.addNode(buildNodeDef(node, computeNodeSize(node.degree, state.minDegree, state.maxDegree)));
|
|
1711
|
+
return node;
|
|
1712
|
+
};
|
|
1713
|
+
let hasChanges = false;
|
|
1714
|
+
for (const row of payload.rows) {
|
|
1715
|
+
const subject = row[subjectIndex] ?? '';
|
|
1716
|
+
const predicate = row[predicateIndex] ?? '';
|
|
1717
|
+
const object = row[objectIndex] ?? '';
|
|
1718
|
+
if (!subject || !predicate || !object) {
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
const sourceNode = ensureNodeInState(subject);
|
|
1722
|
+
const targetNode = ensureNodeInState(object);
|
|
1723
|
+
const edgeKey = `${sourceNode.id}|${toNodeId(predicate)}|${targetNode.id}`;
|
|
1724
|
+
if (state.edgeKeys.has(edgeKey)) {
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
state.edgeKeys.add(edgeKey);
|
|
1728
|
+
sourceNode.degree += 1;
|
|
1729
|
+
targetNode.degree += 1;
|
|
1730
|
+
state.minDegree = Math.min(state.minDegree, sourceNode.degree, targetNode.degree);
|
|
1731
|
+
state.maxDegree = Math.max(state.maxDegree, sourceNode.degree, targetNode.degree);
|
|
1732
|
+
graphView.addEdge(buildEdgeDef({
|
|
1733
|
+
id: `${edgeKey}|${state.edgeSequence}`,
|
|
1734
|
+
source: sourceNode.id,
|
|
1735
|
+
target: targetNode.id,
|
|
1736
|
+
value: predicate,
|
|
1737
|
+
label: shortLabel(predicate),
|
|
1738
|
+
}));
|
|
1739
|
+
state.edgeSequence += 1;
|
|
1740
|
+
hasChanges = true;
|
|
1741
|
+
}
|
|
1742
|
+
if (!hasChanges) {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
for (const node of graphView.getNodes()) {
|
|
1746
|
+
const data = node.getData?.();
|
|
1747
|
+
const degree = Number(data?.degree ?? 0);
|
|
1748
|
+
const size = computeNodeSize(degree, state.minDegree, state.maxDegree);
|
|
1749
|
+
node.resize(size, size + NODE_LABEL_HEIGHT);
|
|
1750
|
+
if (node.shape === 'graph-node-literal') {
|
|
1751
|
+
node.attr({
|
|
1752
|
+
body: {
|
|
1753
|
+
width: size,
|
|
1754
|
+
height: size,
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
node.attr({
|
|
1760
|
+
body: {
|
|
1761
|
+
r: size / 2,
|
|
1762
|
+
cx: size / 2,
|
|
1763
|
+
cy: size / 2,
|
|
1764
|
+
},
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1334
1768
|
function ensureNode(nodes, rawValue) {
|
|
1335
1769
|
const id = toNodeId(rawValue);
|
|
1336
1770
|
if (nodes.has(id)) {
|