@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
|
@@ -16,7 +16,9 @@ const CSS_EDGE_BG = 'var(--vscode-editor-background, var(--oml-static-backg
|
|
|
16
16
|
const CSS_FONT_FAMILY = 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)';
|
|
17
17
|
|
|
18
18
|
/** Extra height beneath the circle/rect shape to accommodate the below-node label. */
|
|
19
|
-
const NODE_LABEL_HEIGHT =
|
|
19
|
+
const NODE_LABEL_HEIGHT = 18;
|
|
20
|
+
const NODE_FONT_SIZE = 10;
|
|
21
|
+
const EDGE_FONT_SIZE = 10;
|
|
20
22
|
|
|
21
23
|
// --- Module-level library caches ---
|
|
22
24
|
let x6GraphCtor: (new (options: unknown) => any) | undefined;
|
|
@@ -53,6 +55,7 @@ type NodeData = {
|
|
|
53
55
|
degree: number;
|
|
54
56
|
literal: boolean;
|
|
55
57
|
group: string;
|
|
58
|
+
hidden?: boolean;
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
type EdgeData = {
|
|
@@ -61,6 +64,7 @@ type EdgeData = {
|
|
|
61
64
|
target: string;
|
|
62
65
|
value: string;
|
|
63
66
|
label: string;
|
|
67
|
+
hidden?: boolean;
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
type ForceLayoutOptions = {
|
|
@@ -143,6 +147,19 @@ type CsvDataset = {
|
|
|
143
147
|
downloadCsv: (content: string) => void;
|
|
144
148
|
};
|
|
145
149
|
|
|
150
|
+
type GraphExpandPayload = {
|
|
151
|
+
columns: string[];
|
|
152
|
+
rows: string[][];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
type GraphMutableState = {
|
|
156
|
+
nodesById: Map<string, NodeData>;
|
|
157
|
+
edgeKeys: Set<string>;
|
|
158
|
+
edgeSequence: number;
|
|
159
|
+
minDegree: number;
|
|
160
|
+
maxDegree: number;
|
|
161
|
+
};
|
|
162
|
+
|
|
146
163
|
const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
147
164
|
|
|
148
165
|
// --- Renderer ---
|
|
@@ -219,6 +236,7 @@ export class GraphMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
|
219
236
|
const maxDegree = Math.max(...degrees);
|
|
220
237
|
const layout = resolveLayoutOptions(result.options);
|
|
221
238
|
const stylesheet = compileGraphStylesheet(result.options);
|
|
239
|
+
const expandOnClick = resolveExpandOnClickOption(result.options);
|
|
222
240
|
const nodeDataList = [...nodeMap.values()];
|
|
223
241
|
const csvDataset: CsvDataset = {
|
|
224
242
|
columns: payload.columns.slice(),
|
|
@@ -235,6 +253,9 @@ export class GraphMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
|
235
253
|
container,
|
|
236
254
|
layout,
|
|
237
255
|
stylesheet,
|
|
256
|
+
result.blockSource,
|
|
257
|
+
result.blockId,
|
|
258
|
+
expandOnClick,
|
|
238
259
|
csvDataset,
|
|
239
260
|
);
|
|
240
261
|
|
|
@@ -253,6 +274,9 @@ function initializeGraphWhenReady(
|
|
|
253
274
|
messageContainer: HTMLElement,
|
|
254
275
|
layoutOptions: ResolvedLayout,
|
|
255
276
|
stylesheet: ReadonlyArray<GraphStyleRule>,
|
|
277
|
+
blockSource: string | undefined,
|
|
278
|
+
blockId: string,
|
|
279
|
+
expandOnClick: boolean,
|
|
256
280
|
csvDataset: CsvDataset,
|
|
257
281
|
): void {
|
|
258
282
|
const maxAttempts = 20;
|
|
@@ -274,6 +298,9 @@ function initializeGraphWhenReady(
|
|
|
274
298
|
maxDegree,
|
|
275
299
|
layoutOptions,
|
|
276
300
|
stylesheet,
|
|
301
|
+
blockSource,
|
|
302
|
+
blockId,
|
|
303
|
+
expandOnClick,
|
|
277
304
|
csvDataset,
|
|
278
305
|
).catch((error) => {
|
|
279
306
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -295,6 +322,9 @@ async function doGraphInit(
|
|
|
295
322
|
maxDegree: number,
|
|
296
323
|
layoutOptions: ResolvedLayout,
|
|
297
324
|
stylesheet: ReadonlyArray<GraphStyleRule>,
|
|
325
|
+
blockSource: string | undefined,
|
|
326
|
+
blockId: string,
|
|
327
|
+
expandOnClick: boolean,
|
|
298
328
|
csvDataset: CsvDataset,
|
|
299
329
|
): Promise<void> {
|
|
300
330
|
const GraphCtor = await loadX6GraphCtor();
|
|
@@ -308,6 +338,7 @@ async function doGraphInit(
|
|
|
308
338
|
minScale: 0.3,
|
|
309
339
|
maxScale: 3,
|
|
310
340
|
factor: 1.1,
|
|
341
|
+
modifiers: ['meta', 'ctrl'],
|
|
311
342
|
},
|
|
312
343
|
connecting: {
|
|
313
344
|
allowBlank: false,
|
|
@@ -347,13 +378,25 @@ async function doGraphInit(
|
|
|
347
378
|
for (const data of edgeDataList) {
|
|
348
379
|
graphView.addEdge(buildEdgeDef(data));
|
|
349
380
|
}
|
|
381
|
+
const mutableState: GraphMutableState = {
|
|
382
|
+
nodesById: new Map(nodeDataList.map((node) => [node.id, node])),
|
|
383
|
+
edgeKeys: new Set(edgeDataList.map((edge) => `${edge.source}|${toNodeId(edge.value)}|${edge.target}`)),
|
|
384
|
+
edgeSequence: edgeDataList.length,
|
|
385
|
+
minDegree,
|
|
386
|
+
maxDegree,
|
|
387
|
+
};
|
|
350
388
|
|
|
351
389
|
// Resize the X6 canvas whenever the host element resizes.
|
|
390
|
+
const recenterAfterResizeOrMove = (): void => {
|
|
391
|
+
centerGraphContent(graphView, layoutOptions.padding);
|
|
392
|
+
};
|
|
393
|
+
|
|
352
394
|
const resizeObserver = new ResizeObserver(() => {
|
|
353
395
|
if (!graphRoot.isConnected) {
|
|
354
396
|
return;
|
|
355
397
|
}
|
|
356
398
|
graphView.resize(graphRoot.clientWidth, graphRoot.clientHeight);
|
|
399
|
+
recenterAfterResizeOrMove();
|
|
357
400
|
});
|
|
358
401
|
resizeObserver.observe(graphRoot);
|
|
359
402
|
// Also watch the result container so that when the page grows wider the
|
|
@@ -365,6 +408,7 @@ async function doGraphInit(
|
|
|
365
408
|
const nextWidth = Math.max(0, Math.floor(resultContainer.clientWidth));
|
|
366
409
|
graphRoot.style.width = `${nextWidth}px`;
|
|
367
410
|
graphView.resize(graphRoot.clientWidth, graphRoot.clientHeight);
|
|
411
|
+
recenterAfterResizeOrMove();
|
|
368
412
|
});
|
|
369
413
|
resultResizeObserver.observe(resultContainer);
|
|
370
414
|
}
|
|
@@ -384,17 +428,29 @@ async function doGraphInit(
|
|
|
384
428
|
// Reset to CSS-variable base attrs so stale user-rule colors are cleared.
|
|
385
429
|
for (const node of graphView.getNodes() as any[]) {
|
|
386
430
|
const nodeData: NodeData = node.getData() as NodeData;
|
|
431
|
+
node.setData({ hidden: false }, { merge: true });
|
|
387
432
|
node.setAttrs(buildNodeBaseAttrs(nodeData.literal));
|
|
388
433
|
}
|
|
389
434
|
for (const edge of graphView.getEdges() as any[]) {
|
|
435
|
+
edge.setData({ hidden: false }, { merge: true });
|
|
390
436
|
edge.setAttrs(buildEdgeBaseAttrs());
|
|
391
437
|
resetEdgeLabelColors(edge);
|
|
392
438
|
}
|
|
393
439
|
applyGraphStylesheet(graphView, stylesheet, theme);
|
|
440
|
+
applyGraphVisibilityState(graphView);
|
|
394
441
|
};
|
|
395
442
|
|
|
396
443
|
applyCurrentTheme();
|
|
397
444
|
|
|
445
|
+
installGraphNodeInteractions(
|
|
446
|
+
graphView,
|
|
447
|
+
graphRoot,
|
|
448
|
+
applyCurrentTheme,
|
|
449
|
+
blockSource,
|
|
450
|
+
blockId,
|
|
451
|
+
expandOnClick,
|
|
452
|
+
);
|
|
453
|
+
|
|
398
454
|
const themeObserver = new MutationObserver(() => {
|
|
399
455
|
applyCurrentTheme();
|
|
400
456
|
});
|
|
@@ -422,6 +478,27 @@ async function doGraphInit(
|
|
|
422
478
|
startContinuousForceEngine(graphView, graphRoot, layoutOptions.force, grabbedNodes);
|
|
423
479
|
}
|
|
424
480
|
|
|
481
|
+
const onExpandResult = (event: Event): void => {
|
|
482
|
+
if (!(event instanceof CustomEvent)) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const detail = event.detail as { blockId?: unknown; payload?: unknown } | undefined;
|
|
486
|
+
const targetBlockId = typeof detail?.blockId === 'string' ? detail.blockId.trim() : '';
|
|
487
|
+
if (targetBlockId !== blockId) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const payload = normalizeGraphExpandPayload(detail?.payload);
|
|
491
|
+
if (!payload) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
mergeExpandedGraphData(graphView, mutableState, payload);
|
|
495
|
+
applyCurrentTheme();
|
|
496
|
+
void runLayout(graphView, layoutOptions).then(() => {
|
|
497
|
+
centerGraphContent(graphView, layoutOptions.padding);
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
graphRoot.addEventListener('md-graph-expand-result', onExpandResult);
|
|
501
|
+
|
|
425
502
|
installGraphToolbar(graphRoot, graphView, csvDataset);
|
|
426
503
|
|
|
427
504
|
// Manual canvas resize handle.
|
|
@@ -447,11 +524,231 @@ async function doGraphInit(
|
|
|
447
524
|
const onResizePointerEnd = (event: PointerEvent): void => {
|
|
448
525
|
if (!canvasResize || event.pointerId !== canvasResize.pointerId) return;
|
|
449
526
|
canvasResize = undefined;
|
|
527
|
+
recenterAfterResizeOrMove();
|
|
450
528
|
};
|
|
451
529
|
resizeHandle.addEventListener('pointerdown', onResizePointerDown);
|
|
452
530
|
resizeHandle.addEventListener('pointermove', onResizePointerMove);
|
|
453
531
|
resizeHandle.addEventListener('pointerup', onResizePointerEnd);
|
|
454
532
|
resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
|
|
533
|
+
graphView.on('node:mouseup', recenterAfterResizeOrMove);
|
|
534
|
+
graphRoot.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
535
|
+
graphRoot.removeEventListener('md-graph-expand-result', onExpandResult);
|
|
536
|
+
}, { once: true });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function installGraphNodeInteractions(
|
|
540
|
+
graphView: any,
|
|
541
|
+
graphRoot: HTMLElement,
|
|
542
|
+
reapplyBaseTheme: () => void,
|
|
543
|
+
blockSource: string | undefined,
|
|
544
|
+
blockId: string,
|
|
545
|
+
expandOnClick: boolean,
|
|
546
|
+
): void {
|
|
547
|
+
const ensureSvgNativeTitle = (element: SVGElement, iri: string): void => {
|
|
548
|
+
let titleNode: SVGTitleElement | null = null;
|
|
549
|
+
for (const child of Array.from(element.children)) {
|
|
550
|
+
if (child instanceof SVGTitleElement) {
|
|
551
|
+
titleNode = child;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!titleNode) {
|
|
556
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
557
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
558
|
+
}
|
|
559
|
+
titleNode.textContent = iri;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const applyNativeTooltipTitle = (container: Element | undefined, iri: string, eventTarget?: EventTarget | null): void => {
|
|
563
|
+
if (!iri) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (container instanceof HTMLElement || container instanceof SVGElement) {
|
|
567
|
+
container.setAttribute('title', iri);
|
|
568
|
+
}
|
|
569
|
+
if (container instanceof Element) {
|
|
570
|
+
for (const element of Array.from(container.querySelectorAll<HTMLElement | SVGElement>('*'))) {
|
|
571
|
+
element.setAttribute('title', iri);
|
|
572
|
+
if (element instanceof SVGElement) {
|
|
573
|
+
ensureSvgNativeTitle(element, iri);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (container instanceof SVGElement) {
|
|
577
|
+
ensureSvgNativeTitle(container, iri);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (eventTarget instanceof HTMLElement || eventTarget instanceof SVGElement) {
|
|
581
|
+
eventTarget.setAttribute('title', iri);
|
|
582
|
+
if (eventTarget instanceof SVGElement) {
|
|
583
|
+
ensureSvgNativeTitle(eventTarget, iri);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
let hoveredNodeId: string | undefined;
|
|
589
|
+
const isExpandModifierPressed = (event: MouseEvent | undefined): boolean => Boolean(event?.shiftKey);
|
|
590
|
+
|
|
591
|
+
const applyHoverState = (node: any): void => {
|
|
592
|
+
const hoveredId = String(node?.id ?? '');
|
|
593
|
+
if (!hoveredId) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
reapplyBaseTheme();
|
|
597
|
+
hoveredNodeId = hoveredId;
|
|
598
|
+
|
|
599
|
+
const theme = resolveThemePalette();
|
|
600
|
+
const activeNodeIds = new Set<string>([hoveredId]);
|
|
601
|
+
const activeEdgeIds = new Set<string>();
|
|
602
|
+
const connectedEdges = (graphView.getConnectedEdges(node) as any[]) ?? [];
|
|
603
|
+
for (const edge of connectedEdges) {
|
|
604
|
+
activeEdgeIds.add(String(edge.id));
|
|
605
|
+
const sourceId = String(edge.getSourceCellId?.() ?? '');
|
|
606
|
+
const targetId = String(edge.getTargetCellId?.() ?? '');
|
|
607
|
+
if (sourceId) {
|
|
608
|
+
activeNodeIds.add(sourceId);
|
|
609
|
+
}
|
|
610
|
+
if (targetId) {
|
|
611
|
+
activeNodeIds.add(targetId);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for (const candidate of graphView.getNodes() as any[]) {
|
|
616
|
+
const candidateId = String(candidate.id);
|
|
617
|
+
const active = activeNodeIds.has(candidateId);
|
|
618
|
+
if (!active) {
|
|
619
|
+
candidate.setAttrs({
|
|
620
|
+
body: { opacity: 0.2 },
|
|
621
|
+
label: { opacity: 0.28 },
|
|
622
|
+
});
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (candidateId === hoveredId) {
|
|
626
|
+
candidate.setAttrs({
|
|
627
|
+
body: {
|
|
628
|
+
fill: theme.accent,
|
|
629
|
+
stroke: theme.selectedStroke,
|
|
630
|
+
strokeWidth: 2,
|
|
631
|
+
opacity: 1,
|
|
632
|
+
},
|
|
633
|
+
label: {
|
|
634
|
+
fill: theme.accentForeground,
|
|
635
|
+
opacity: 1,
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (const edge of graphView.getEdges() as any[]) {
|
|
642
|
+
const active = activeEdgeIds.has(String(edge.id));
|
|
643
|
+
if (!active) {
|
|
644
|
+
edge.setAttrs({
|
|
645
|
+
line: { opacity: 0.16 },
|
|
646
|
+
});
|
|
647
|
+
try {
|
|
648
|
+
edge.prop('labels/0/attrs/label/opacity', 0.2);
|
|
649
|
+
edge.prop('labels/0/attrs/body/opacity', 0.12);
|
|
650
|
+
} catch {
|
|
651
|
+
// ignore
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
edge.setAttrs({
|
|
656
|
+
line: {
|
|
657
|
+
opacity: 1,
|
|
658
|
+
stroke: theme.accent,
|
|
659
|
+
strokeWidth: Math.max(1.6, Number(edge.attr?.('line/strokeWidth')) || 1.6),
|
|
660
|
+
targetMarker: {
|
|
661
|
+
name: 'classic',
|
|
662
|
+
size: 6,
|
|
663
|
+
fill: theme.accent,
|
|
664
|
+
stroke: theme.accent,
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
try {
|
|
669
|
+
edge.prop('labels/0/attrs/label/fill', theme.accent);
|
|
670
|
+
edge.prop('labels/0/attrs/body/opacity', 1);
|
|
671
|
+
} catch {
|
|
672
|
+
// ignore
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const clearHoverState = (): void => {
|
|
678
|
+
if (!hoveredNodeId) {
|
|
679
|
+
graphRoot.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
hoveredNodeId = undefined;
|
|
683
|
+
reapplyBaseTheme();
|
|
684
|
+
graphRoot.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
graphView.on('node:mouseenter', ({ node, e }: { node: any; e?: MouseEvent }) => {
|
|
688
|
+
const data = node?.getData?.() as NodeData | undefined;
|
|
689
|
+
const iri = String(data?.value ?? '');
|
|
690
|
+
applyHoverState(node);
|
|
691
|
+
if (iri) {
|
|
692
|
+
const view = graphView.findViewByCell?.(node);
|
|
693
|
+
const bbox = view?.container?.getBoundingClientRect?.();
|
|
694
|
+
applyNativeTooltipTitle(view?.container as Element | undefined, iri, e?.target ?? null);
|
|
695
|
+
if (bbox) {
|
|
696
|
+
graphRoot.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
697
|
+
bubbles: true,
|
|
698
|
+
detail: {
|
|
699
|
+
iri,
|
|
700
|
+
previewEnabled: !!e && (/^Mac/i.test(navigator.platform) ? e.metaKey : e.ctrlKey),
|
|
701
|
+
anchorRect: {
|
|
702
|
+
left: bbox.left,
|
|
703
|
+
right: bbox.right,
|
|
704
|
+
top: bbox.top,
|
|
705
|
+
bottom: bbox.bottom,
|
|
706
|
+
width: bbox.width,
|
|
707
|
+
height: bbox.height,
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
graphView.on('node:mouseleave', () => {
|
|
716
|
+
clearHoverState();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
graphView.on('blank:mousemove', () => {
|
|
720
|
+
clearHoverState();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
graphView.on('node:click', ({ node, e }: { node: any; e?: MouseEvent }) => {
|
|
724
|
+
if (e && e.button !== 0) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const data = node?.getData?.() as NodeData | undefined;
|
|
728
|
+
const iri = String(data?.value ?? '');
|
|
729
|
+
if (!isIriValue(iri)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (expandOnClick && isExpandModifierPressed(e)) {
|
|
733
|
+
const source = (blockSource ?? '').trim();
|
|
734
|
+
if (!source) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
graphRoot.dispatchEvent(new CustomEvent('md-graph-expand-request', {
|
|
738
|
+
bubbles: true,
|
|
739
|
+
detail: {
|
|
740
|
+
blockId,
|
|
741
|
+
blockSource: source,
|
|
742
|
+
contextIri: iri.trim(),
|
|
743
|
+
},
|
|
744
|
+
}));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
graphRoot.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
748
|
+
bubbles: true,
|
|
749
|
+
detail: { iri },
|
|
750
|
+
}));
|
|
751
|
+
});
|
|
455
752
|
}
|
|
456
753
|
|
|
457
754
|
// --- X6 / dagre library loaders ---
|
|
@@ -482,11 +779,11 @@ async function loadDagreLib(): Promise<any> {
|
|
|
482
779
|
|
|
483
780
|
function computeNodeSize(degree: number, minDegree: number, maxDegree: number): number {
|
|
484
781
|
if (minDegree === maxDegree) {
|
|
485
|
-
return
|
|
782
|
+
return 28;
|
|
486
783
|
}
|
|
487
|
-
//
|
|
784
|
+
// Keep default nodes compact while still preserving degree-based variation.
|
|
488
785
|
const t = Math.max(0, Math.min(1, degree / 10));
|
|
489
|
-
return Math.round(
|
|
786
|
+
return Math.round(16 + t * (40 - 16));
|
|
490
787
|
}
|
|
491
788
|
|
|
492
789
|
/** Base attrs for a node using CSS variables so color auto-updates with VS Code theme. */
|
|
@@ -496,11 +793,13 @@ function buildNodeBaseAttrs(literal: boolean): Record<string, unknown> {
|
|
|
496
793
|
fill: literal ? CSS_LIT_FILL : CSS_NODE_FILL,
|
|
497
794
|
stroke: literal ? CSS_LIT_STROKE : CSS_NODE_STROKE,
|
|
498
795
|
strokeWidth: 1,
|
|
796
|
+
opacity: 1,
|
|
499
797
|
},
|
|
500
798
|
label: {
|
|
501
799
|
fill: CSS_NODE_TEXT,
|
|
502
|
-
fontSize:
|
|
800
|
+
fontSize: NODE_FONT_SIZE,
|
|
503
801
|
fontFamily: CSS_FONT_FAMILY,
|
|
802
|
+
opacity: 1,
|
|
504
803
|
},
|
|
505
804
|
};
|
|
506
805
|
}
|
|
@@ -511,6 +810,7 @@ function buildEdgeBaseAttrs(): Record<string, unknown> {
|
|
|
511
810
|
line: {
|
|
512
811
|
stroke: CSS_EDGE_STROKE,
|
|
513
812
|
strokeWidth: 1.4,
|
|
813
|
+
opacity: 1,
|
|
514
814
|
targetMarker: {
|
|
515
815
|
name: 'classic',
|
|
516
816
|
size: 6,
|
|
@@ -525,7 +825,9 @@ function buildEdgeBaseAttrs(): Record<string, unknown> {
|
|
|
525
825
|
function resetEdgeLabelColors(edge: any): void {
|
|
526
826
|
try {
|
|
527
827
|
edge.prop('labels/0/attrs/label/fill', CSS_EDGE_TEXT);
|
|
828
|
+
edge.prop('labels/0/attrs/label/opacity', 1);
|
|
528
829
|
edge.prop('labels/0/attrs/body/fill', CSS_EDGE_BG);
|
|
830
|
+
edge.prop('labels/0/attrs/body/opacity', 1);
|
|
529
831
|
} catch {
|
|
530
832
|
// ignore - edge may have no labels
|
|
531
833
|
}
|
|
@@ -555,12 +857,12 @@ function buildNodeDef(data: NodeData, size: number): Record<string, unknown> {
|
|
|
555
857
|
label: {
|
|
556
858
|
text: data.label,
|
|
557
859
|
fill: CSS_NODE_TEXT,
|
|
558
|
-
fontSize:
|
|
860
|
+
fontSize: NODE_FONT_SIZE,
|
|
559
861
|
fontFamily: CSS_FONT_FAMILY,
|
|
560
862
|
textAnchor: 'middle',
|
|
561
863
|
textVerticalAnchor: 'top',
|
|
562
864
|
refX: '50%',
|
|
563
|
-
refY: size +
|
|
865
|
+
refY: size + 3, // below the visible shape
|
|
564
866
|
},
|
|
565
867
|
},
|
|
566
868
|
data: {
|
|
@@ -569,6 +871,7 @@ function buildNodeDef(data: NodeData, size: number): Record<string, unknown> {
|
|
|
569
871
|
degree: data.degree,
|
|
570
872
|
literal: data.literal,
|
|
571
873
|
group: '',
|
|
874
|
+
hidden: Boolean(data.hidden),
|
|
572
875
|
},
|
|
573
876
|
zIndex: 1,
|
|
574
877
|
};
|
|
@@ -617,7 +920,7 @@ function buildEdgeDef(data: EdgeData): Record<string, unknown> {
|
|
|
617
920
|
label: {
|
|
618
921
|
text: data.label,
|
|
619
922
|
fill: CSS_EDGE_TEXT,
|
|
620
|
-
fontSize:
|
|
923
|
+
fontSize: EDGE_FONT_SIZE,
|
|
621
924
|
fontFamily: CSS_FONT_FAMILY,
|
|
622
925
|
textAnchor: 'middle',
|
|
623
926
|
textVerticalAnchor: 'middle',
|
|
@@ -628,6 +931,7 @@ function buildEdgeDef(data: EdgeData): Record<string, unknown> {
|
|
|
628
931
|
data: {
|
|
629
932
|
label: data.label,
|
|
630
933
|
value: data.value,
|
|
934
|
+
hidden: Boolean(data.hidden),
|
|
631
935
|
},
|
|
632
936
|
zIndex: 0,
|
|
633
937
|
};
|
|
@@ -759,6 +1063,18 @@ function fitGraphContent(graphView: any, padding: number): void {
|
|
|
759
1063
|
});
|
|
760
1064
|
}
|
|
761
1065
|
|
|
1066
|
+
function centerGraphContent(graphView: any, padding: number): void {
|
|
1067
|
+
requestAnimationFrame(() => {
|
|
1068
|
+
try {
|
|
1069
|
+
if (typeof graphView.centerContent === 'function') {
|
|
1070
|
+
graphView.centerContent({ padding });
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
// ignore
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
762
1078
|
// --- Continuous force engine ---
|
|
763
1079
|
|
|
764
1080
|
function startContinuousForceEngine(
|
|
@@ -941,6 +1257,29 @@ function applyGraphStylesheet(
|
|
|
941
1257
|
}
|
|
942
1258
|
}
|
|
943
1259
|
|
|
1260
|
+
function applyGraphVisibilityState(graphView: any): void {
|
|
1261
|
+
const visibleNodeIds = new Set<string>();
|
|
1262
|
+
for (const node of graphView.getNodes() as any[]) {
|
|
1263
|
+
const hidden = Boolean((node.getData?.() as { hidden?: boolean } | undefined)?.hidden);
|
|
1264
|
+
if (hidden) {
|
|
1265
|
+
node.hide();
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
node.show();
|
|
1269
|
+
visibleNodeIds.add(String(node.id));
|
|
1270
|
+
}
|
|
1271
|
+
for (const edge of graphView.getEdges() as any[]) {
|
|
1272
|
+
const hidden = Boolean((edge.getData?.() as { hidden?: boolean } | undefined)?.hidden);
|
|
1273
|
+
const sourceId = String(edge.getSourceCellId?.() ?? '');
|
|
1274
|
+
const targetId = String(edge.getTargetCellId?.() ?? '');
|
|
1275
|
+
if (hidden || !visibleNodeIds.has(sourceId) || !visibleNodeIds.has(targetId)) {
|
|
1276
|
+
edge.hide();
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
edge.show();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
944
1283
|
function buildNodeContext(node: any, graphView: any): NodeContext {
|
|
945
1284
|
const nodeId = String(node.id);
|
|
946
1285
|
const nodeContext = buildSelectorNodeContext(node, graphView);
|
|
@@ -1036,11 +1375,18 @@ function applyElementStyle(
|
|
|
1036
1375
|
if (!property) {
|
|
1037
1376
|
continue;
|
|
1038
1377
|
}
|
|
1378
|
+
if ((property === 'display' && value.trim().toLowerCase() === 'none')
|
|
1379
|
+
|| (property === 'visibility' && value.trim().toLowerCase() === 'hidden')
|
|
1380
|
+
|| (property === 'hidden' && value.trim().toLowerCase() === 'true')) {
|
|
1381
|
+
element.setData({ hidden: true }, { merge: true });
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1039
1384
|
const resolved = resolveColorToken(value, theme);
|
|
1040
1385
|
if (property === 'fill') { bodyAttrs.fill = resolved; continue; }
|
|
1041
1386
|
if (property === 'stroke') { bodyAttrs.stroke = resolved; continue; }
|
|
1042
1387
|
if (property === 'stroke-width') { bodyAttrs.strokeWidth = value; continue; }
|
|
1043
1388
|
if (property === 'color') { labelAttrs.fill = resolved; continue; }
|
|
1389
|
+
if (property === 'font-size') { labelAttrs.fontSize = value; continue; }
|
|
1044
1390
|
const camel = property.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
1045
1391
|
bodyAttrs[camel] = resolved;
|
|
1046
1392
|
}
|
|
@@ -1062,6 +1408,12 @@ function applyElementStyle(
|
|
|
1062
1408
|
if (!property) {
|
|
1063
1409
|
continue;
|
|
1064
1410
|
}
|
|
1411
|
+
if ((property === 'display' && value.trim().toLowerCase() === 'none')
|
|
1412
|
+
|| (property === 'visibility' && value.trim().toLowerCase() === 'hidden')
|
|
1413
|
+
|| (property === 'hidden' && value.trim().toLowerCase() === 'true')) {
|
|
1414
|
+
element.setData({ hidden: true }, { merge: true });
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1065
1417
|
const resolved = resolveColorToken(value, theme);
|
|
1066
1418
|
if (property === 'stroke') {
|
|
1067
1419
|
lineAttrs.stroke = resolved;
|
|
@@ -1070,6 +1422,14 @@ function applyElementStyle(
|
|
|
1070
1422
|
}
|
|
1071
1423
|
if (property === 'stroke-width') { lineAttrs.strokeWidth = value; continue; }
|
|
1072
1424
|
if (property === 'color') { edgeLabelFill = resolved; continue; }
|
|
1425
|
+
if (property === 'font-size') {
|
|
1426
|
+
try {
|
|
1427
|
+
element.prop('labels/0/attrs/label/fontSize', value);
|
|
1428
|
+
} catch {
|
|
1429
|
+
// ignore
|
|
1430
|
+
}
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1073
1433
|
const camel = property.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
1074
1434
|
lineAttrs[camel] = resolved;
|
|
1075
1435
|
}
|
|
@@ -1152,15 +1512,40 @@ function applyNeighborhoodFilter(graphView: any, rawQuery: string): void {
|
|
|
1152
1512
|
const query = rawQuery.trim().toLowerCase();
|
|
1153
1513
|
const allNodes = graphView.getNodes() as any[];
|
|
1154
1514
|
const allEdges = graphView.getEdges() as any[];
|
|
1515
|
+
const baselineHiddenNodeIds = new Set(
|
|
1516
|
+
allNodes
|
|
1517
|
+
.filter((node) => Boolean((node.getData?.() as { hidden?: boolean } | undefined)?.hidden))
|
|
1518
|
+
.map((node) => String(node.id))
|
|
1519
|
+
);
|
|
1520
|
+
const baselineHiddenEdgeIds = new Set(
|
|
1521
|
+
allEdges
|
|
1522
|
+
.filter((edge) => Boolean((edge.getData?.() as { hidden?: boolean } | undefined)?.hidden))
|
|
1523
|
+
.map((edge) => String(edge.id))
|
|
1524
|
+
);
|
|
1155
1525
|
|
|
1156
1526
|
if (!query) {
|
|
1157
|
-
for (const node of allNodes)
|
|
1158
|
-
|
|
1527
|
+
for (const node of allNodes) {
|
|
1528
|
+
if (baselineHiddenNodeIds.has(String(node.id))) {
|
|
1529
|
+
node.hide();
|
|
1530
|
+
} else {
|
|
1531
|
+
node.show();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const edge of allEdges) {
|
|
1535
|
+
if (baselineHiddenEdgeIds.has(String(edge.id))) {
|
|
1536
|
+
edge.hide();
|
|
1537
|
+
} else {
|
|
1538
|
+
edge.show();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1159
1541
|
return;
|
|
1160
1542
|
}
|
|
1161
1543
|
|
|
1162
1544
|
const matchedIds = new Set<string>();
|
|
1163
1545
|
for (const node of allNodes) {
|
|
1546
|
+
if (baselineHiddenNodeIds.has(String(node.id))) {
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1164
1549
|
const nodeData = node.getData() as NodeData;
|
|
1165
1550
|
const label = String(nodeData?.label ?? '').toLowerCase();
|
|
1166
1551
|
const value = String(nodeData?.value ?? '').toLowerCase();
|
|
@@ -1184,7 +1569,7 @@ function applyNeighborhoodFilter(graphView: any, rawQuery: string): void {
|
|
|
1184
1569
|
}
|
|
1185
1570
|
|
|
1186
1571
|
for (const node of allNodes) {
|
|
1187
|
-
if (keepNodeIds.has(String(node.id))) {
|
|
1572
|
+
if (keepNodeIds.has(String(node.id)) && !baselineHiddenNodeIds.has(String(node.id))) {
|
|
1188
1573
|
node.show();
|
|
1189
1574
|
} else {
|
|
1190
1575
|
node.hide();
|
|
@@ -1195,7 +1580,11 @@ function applyNeighborhoodFilter(graphView: any, rawQuery: string): void {
|
|
|
1195
1580
|
for (const edge of allEdges) {
|
|
1196
1581
|
const srcId = edge.getSourceCellId();
|
|
1197
1582
|
const tgtId = edge.getTargetCellId();
|
|
1198
|
-
if (
|
|
1583
|
+
if (!baselineHiddenEdgeIds.has(String(edge.id))
|
|
1584
|
+
&& keepNodeIds.has(srcId)
|
|
1585
|
+
&& keepNodeIds.has(tgtId)
|
|
1586
|
+
&& !baselineHiddenNodeIds.has(String(srcId))
|
|
1587
|
+
&& !baselineHiddenNodeIds.has(String(tgtId))) {
|
|
1199
1588
|
edge.show();
|
|
1200
1589
|
} else {
|
|
1201
1590
|
edge.hide();
|
|
@@ -1449,6 +1838,10 @@ function resolveCanvasMinHeight(options: Record<string, unknown> | undefined): s
|
|
|
1449
1838
|
return `${numericCanvasMinHeight(options)}px`;
|
|
1450
1839
|
}
|
|
1451
1840
|
|
|
1841
|
+
function resolveExpandOnClickOption(options: Record<string, unknown> | undefined): boolean {
|
|
1842
|
+
return asBoolean(options?.expandOnClick, false);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1452
1845
|
function numericCanvasMinHeight(options: Record<string, unknown> | undefined): number {
|
|
1453
1846
|
const canvas = isRecord(options?.canvas) ? options.canvas : undefined;
|
|
1454
1847
|
const raw = canvas?.minHeight ?? options?.minHeight;
|
|
@@ -1596,6 +1989,113 @@ function resolveColorToken(value: string, theme: GraphThemePalette): string {
|
|
|
1596
1989
|
return trimmed;
|
|
1597
1990
|
}
|
|
1598
1991
|
|
|
1992
|
+
function normalizeGraphExpandPayload(value: unknown): GraphExpandPayload | undefined {
|
|
1993
|
+
if (!isRecord(value)) {
|
|
1994
|
+
return undefined;
|
|
1995
|
+
}
|
|
1996
|
+
const columns = Array.isArray(value.columns)
|
|
1997
|
+
? value.columns.filter((entry): entry is string => typeof entry === 'string')
|
|
1998
|
+
: [];
|
|
1999
|
+
const rows = Array.isArray(value.rows)
|
|
2000
|
+
? value.rows.map((row) => Array.isArray(row)
|
|
2001
|
+
? row.map((cell) => (typeof cell === 'string' ? cell : String(cell ?? '')))
|
|
2002
|
+
: [])
|
|
2003
|
+
: [];
|
|
2004
|
+
if (columns.length === 0 || rows.length === 0) {
|
|
2005
|
+
return undefined;
|
|
2006
|
+
}
|
|
2007
|
+
return { columns, rows };
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function mergeExpandedGraphData(
|
|
2011
|
+
graphView: any,
|
|
2012
|
+
state: GraphMutableState,
|
|
2013
|
+
payload: GraphExpandPayload,
|
|
2014
|
+
): void {
|
|
2015
|
+
const subjectIndex = payload.columns.indexOf('subject');
|
|
2016
|
+
const predicateIndex = payload.columns.indexOf('predicate');
|
|
2017
|
+
const objectIndex = payload.columns.indexOf('object');
|
|
2018
|
+
if (subjectIndex < 0 || predicateIndex < 0 || objectIndex < 0) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const ensureNodeInState = (rawValue: string): NodeData => {
|
|
2023
|
+
const nodeId = toNodeId(rawValue);
|
|
2024
|
+
const existing = state.nodesById.get(nodeId);
|
|
2025
|
+
if (existing) {
|
|
2026
|
+
return existing;
|
|
2027
|
+
}
|
|
2028
|
+
const node: NodeData = {
|
|
2029
|
+
id: nodeId,
|
|
2030
|
+
label: shortLabel(rawValue),
|
|
2031
|
+
value: rawValue,
|
|
2032
|
+
degree: 0,
|
|
2033
|
+
literal: isLiteralValue(rawValue),
|
|
2034
|
+
group: '',
|
|
2035
|
+
};
|
|
2036
|
+
state.nodesById.set(nodeId, node);
|
|
2037
|
+
graphView.addNode(buildNodeDef(node, computeNodeSize(node.degree, state.minDegree, state.maxDegree)));
|
|
2038
|
+
return node;
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
let hasChanges = false;
|
|
2042
|
+
for (const row of payload.rows) {
|
|
2043
|
+
const subject = row[subjectIndex] ?? '';
|
|
2044
|
+
const predicate = row[predicateIndex] ?? '';
|
|
2045
|
+
const object = row[objectIndex] ?? '';
|
|
2046
|
+
if (!subject || !predicate || !object) {
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
const sourceNode = ensureNodeInState(subject);
|
|
2050
|
+
const targetNode = ensureNodeInState(object);
|
|
2051
|
+
const edgeKey = `${sourceNode.id}|${toNodeId(predicate)}|${targetNode.id}`;
|
|
2052
|
+
if (state.edgeKeys.has(edgeKey)) {
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
state.edgeKeys.add(edgeKey);
|
|
2056
|
+
sourceNode.degree += 1;
|
|
2057
|
+
targetNode.degree += 1;
|
|
2058
|
+
state.minDegree = Math.min(state.minDegree, sourceNode.degree, targetNode.degree);
|
|
2059
|
+
state.maxDegree = Math.max(state.maxDegree, sourceNode.degree, targetNode.degree);
|
|
2060
|
+
graphView.addEdge(buildEdgeDef({
|
|
2061
|
+
id: `${edgeKey}|${state.edgeSequence}`,
|
|
2062
|
+
source: sourceNode.id,
|
|
2063
|
+
target: targetNode.id,
|
|
2064
|
+
value: predicate,
|
|
2065
|
+
label: shortLabel(predicate),
|
|
2066
|
+
}));
|
|
2067
|
+
state.edgeSequence += 1;
|
|
2068
|
+
hasChanges = true;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (!hasChanges) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
for (const node of graphView.getNodes() as any[]) {
|
|
2076
|
+
const data = node.getData?.() as NodeData | undefined;
|
|
2077
|
+
const degree = Number(data?.degree ?? 0);
|
|
2078
|
+
const size = computeNodeSize(degree, state.minDegree, state.maxDegree);
|
|
2079
|
+
node.resize(size, size + NODE_LABEL_HEIGHT);
|
|
2080
|
+
if (node.shape === 'graph-node-literal') {
|
|
2081
|
+
node.attr({
|
|
2082
|
+
body: {
|
|
2083
|
+
width: size,
|
|
2084
|
+
height: size,
|
|
2085
|
+
},
|
|
2086
|
+
});
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
node.attr({
|
|
2090
|
+
body: {
|
|
2091
|
+
r: size / 2,
|
|
2092
|
+
cx: size / 2,
|
|
2093
|
+
cy: size / 2,
|
|
2094
|
+
},
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1599
2099
|
function ensureNode(nodes: Map<string, NodeData>, rawValue: string): void {
|
|
1600
2100
|
const id = toNodeId(rawValue);
|
|
1601
2101
|
if (nodes.has(id)) {
|