@oml/markdown 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/out/md/md-execution.d.ts +16 -0
  2. package/out/md/md-executor.d.ts +1 -0
  3. package/out/md/md-executor.js +219 -35
  4. package/out/md/md-executor.js.map +1 -1
  5. package/out/renderers/chart-renderer.js +72 -4
  6. package/out/renderers/chart-renderer.js.map +1 -1
  7. package/out/renderers/diagram-renderer.js +896 -245
  8. package/out/renderers/diagram-renderer.js.map +1 -1
  9. package/out/renderers/graph-renderer.js +452 -18
  10. package/out/renderers/graph-renderer.js.map +1 -1
  11. package/out/renderers/matrix-renderer.d.ts +0 -2
  12. package/out/renderers/matrix-renderer.js +45 -40
  13. package/out/renderers/matrix-renderer.js.map +1 -1
  14. package/out/renderers/renderer.d.ts +4 -1
  15. package/out/renderers/renderer.js +98 -0
  16. package/out/renderers/renderer.js.map +1 -1
  17. package/out/renderers/table-renderer.d.ts +12 -2
  18. package/out/renderers/table-renderer.js +126 -39
  19. package/out/renderers/table-renderer.js.map +1 -1
  20. package/out/renderers/types.d.ts +16 -0
  21. package/out/renderers/wikilink-utils.d.ts +1 -0
  22. package/out/renderers/wikilink-utils.js +60 -32
  23. package/out/renderers/wikilink-utils.js.map +1 -1
  24. package/out/static/browser-runtime.bundle.js +8011 -1292
  25. package/out/static/browser-runtime.bundle.js.map +4 -4
  26. package/out/static/browser-runtime.js +15 -2
  27. package/out/static/browser-runtime.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/md/md-execution.ts +20 -0
  30. package/src/md/md-executor.ts +268 -40
  31. package/src/renderers/chart-renderer.ts +93 -2
  32. package/src/renderers/diagram-renderer.ts +964 -253
  33. package/src/renderers/graph-renderer.ts +512 -12
  34. package/src/renderers/matrix-renderer.ts +57 -44
  35. package/src/renderers/renderer.ts +105 -1
  36. package/src/renderers/table-renderer.ts +190 -41
  37. package/src/renderers/types.ts +20 -0
  38. package/src/renderers/wikilink-utils.ts +66 -31
  39. package/src/static/browser-runtime.ts +20 -2
  40. package/src/static/markdown-webview.css +44 -15
@@ -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 = 22;
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 36;
782
+ return 28;
486
783
  }
487
- // Mirrors Cytoscape mapData(degree, 0, 10, 18, 52).
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(18 + t * (52 - 18));
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: 11,
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: 11,
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 + 4, // below the visible shape
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: 11,
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) node.show();
1158
- for (const edge of allEdges) edge.show();
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 (keepNodeIds.has(srcId) && keepNodeIds.has(tgtId)) {
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)) {