@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
@@ -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 = 22;
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 36;
533
+ return 28;
291
534
  }
292
- // Mirrors Cytoscape mapData(degree, 0, 10, 18, 52).
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(18 + t * (52 - 18));
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: 11,
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: 11,
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 + 4, // below the visible shape
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: 11,
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.show();
928
- for (const edge of allEdges)
929
- edge.show();
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 (keepNodeIds.has(srcId) && keepNodeIds.has(tgtId)) {
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)) {