sysprom 1.27.2 → 1.28.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/dist/src/cli/commands/graph.d.ts +1 -0
- package/dist/src/cli/commands/graph.js +11 -1
- package/dist/src/cli/commands/json2md.d.ts +1 -0
- package/dist/src/cli/commands/json2md.js +5 -0
- package/dist/src/cli/commands/update.js +0 -5
- package/dist/src/json-to-md.d.ts +2 -0
- package/dist/src/json-to-md.js +31 -0
- package/dist/src/operations/graph-decision.d.ts +1 -0
- package/dist/src/operations/graph-decision.js +9 -4
- package/dist/src/operations/graph-dependency.d.ts +1 -0
- package/dist/src/operations/graph-dependency.js +9 -4
- package/dist/src/operations/graph-refinement.d.ts +1 -0
- package/dist/src/operations/graph-refinement.js +9 -4
- package/dist/src/operations/graph-shared.d.ts +15 -0
- package/dist/src/operations/graph-shared.js +33 -0
- package/dist/src/operations/graph.d.ts +1 -0
- package/dist/src/operations/graph.js +11 -7
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ declare const optsSchema: z.ZodObject<{
|
|
|
24
24
|
}>>;
|
|
25
25
|
cluster: z.ZodOptional<z.ZodBoolean>;
|
|
26
26
|
connectedOnly: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
links: z.ZodOptional<z.ZodBoolean>;
|
|
27
28
|
}, z.core.$strip>;
|
|
28
29
|
export declare const graphCommand: CommandDef<typeof noArgs, typeof optsSchema>;
|
|
29
30
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { graphOp } from "../../operations/index.js";
|
|
4
|
+
import { buildExternalRefClickMap } from "../../operations/graph-shared.js";
|
|
4
5
|
import { readOpts, loadDoc } from "../shared.js";
|
|
5
6
|
const optsSchema = readOpts.extend({
|
|
6
7
|
format: z.enum(["mermaid", "dot"]).optional().describe("Output format"),
|
|
@@ -36,6 +37,10 @@ const optsSchema = readOpts.extend({
|
|
|
36
37
|
.boolean()
|
|
37
38
|
.optional()
|
|
38
39
|
.describe("Only show nodes that have relationships"),
|
|
40
|
+
links: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Add click hyperlinks from external references to Mermaid nodes"),
|
|
39
44
|
});
|
|
40
45
|
export const graphCommand = {
|
|
41
46
|
name: "graph",
|
|
@@ -54,9 +59,13 @@ export const graphCommand = {
|
|
|
54
59
|
const relTypes = opts.relTypes
|
|
55
60
|
? opts.relTypes.split(",").map((s) => s.trim())
|
|
56
61
|
: undefined;
|
|
62
|
+
const format = opts.format ?? "mermaid";
|
|
63
|
+
const clickTargets = opts.links && format === "mermaid"
|
|
64
|
+
? Object.fromEntries(buildExternalRefClickMap(doc.nodes))
|
|
65
|
+
: undefined;
|
|
57
66
|
const output = graphOp({
|
|
58
67
|
doc,
|
|
59
|
-
format
|
|
68
|
+
format,
|
|
60
69
|
typeFilter: opts.type,
|
|
61
70
|
nodeTypes,
|
|
62
71
|
nodeIds,
|
|
@@ -65,6 +74,7 @@ export const graphCommand = {
|
|
|
65
74
|
cluster: opts.cluster ?? true,
|
|
66
75
|
labelMode: opts.labelMode ?? "friendly",
|
|
67
76
|
connectedOnly: opts.connectedOnly ?? false,
|
|
77
|
+
clickTargets,
|
|
68
78
|
});
|
|
69
79
|
console.log(output);
|
|
70
80
|
}
|
|
@@ -37,6 +37,10 @@ const optsSchema = z
|
|
|
37
37
|
.enum(["LR", "TD", "RL", "BT"])
|
|
38
38
|
.optional()
|
|
39
39
|
.describe("Override layout for dependency diagrams"),
|
|
40
|
+
diagramLinks: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Add click hyperlinks to Mermaid diagram nodes"),
|
|
40
44
|
})
|
|
41
45
|
.strict();
|
|
42
46
|
export const json2mdCommand = {
|
|
@@ -71,6 +75,7 @@ export const json2mdCommand = {
|
|
|
71
75
|
jsonToMarkdown(raw, outputPath, {
|
|
72
76
|
form,
|
|
73
77
|
embedDiagrams: opts.embedDiagrams,
|
|
78
|
+
diagramLinks: opts.diagramLinks,
|
|
74
79
|
// forward labelMode for embedded diagrams (default friendly)
|
|
75
80
|
labelMode: opts.labelMode ?? "friendly",
|
|
76
81
|
relationshipLayout: opts.relationshipLayout,
|
|
@@ -277,11 +277,6 @@ const removeRefSubcommand = {
|
|
|
277
277
|
const opts = removeRefOpts.parse(rawOpts);
|
|
278
278
|
const loaded = loadDoc(opts.path);
|
|
279
279
|
const { doc } = loaded;
|
|
280
|
-
removeExternalReferenceOp({
|
|
281
|
-
doc,
|
|
282
|
-
nodeId: args.id,
|
|
283
|
-
identifier: opts.identifier,
|
|
284
|
-
});
|
|
285
280
|
const newDoc = removeExternalReferenceOp({
|
|
286
281
|
doc,
|
|
287
282
|
nodeId: args.id,
|
package/dist/src/json-to-md.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ type DiagramLayout = "LR" | "TD" | "RL" | "BT";
|
|
|
4
4
|
export interface ConvertOptions {
|
|
5
5
|
form: "single-file" | "multi-doc";
|
|
6
6
|
embedDiagrams?: boolean;
|
|
7
|
+
diagramLinks?: boolean;
|
|
7
8
|
labelMode?: "friendly" | "compact";
|
|
8
9
|
relationshipLayout?: DiagramLayout;
|
|
9
10
|
refinementLayout?: DiagramLayout;
|
|
@@ -12,6 +13,7 @@ export interface ConvertOptions {
|
|
|
12
13
|
}
|
|
13
14
|
interface MarkdownRenderOptions {
|
|
14
15
|
embedDiagrams?: boolean;
|
|
16
|
+
diagramLinks?: boolean;
|
|
15
17
|
labelMode?: "friendly" | "compact";
|
|
16
18
|
relationshipLayout?: DiagramLayout;
|
|
17
19
|
refinementLayout?: DiagramLayout;
|
package/dist/src/json-to-md.js
CHANGED
|
@@ -424,6 +424,7 @@ function generateDiagramsFile(doc, opts) {
|
|
|
424
424
|
labelMode: opts?.labelMode ?? "friendly",
|
|
425
425
|
cluster: true,
|
|
426
426
|
connectedOnly: false,
|
|
427
|
+
clickTargets: opts?.clickTargets,
|
|
427
428
|
}));
|
|
428
429
|
lines.push("```");
|
|
429
430
|
lines.push("");
|
|
@@ -432,6 +433,7 @@ function generateDiagramsFile(doc, opts) {
|
|
|
432
433
|
format: "mermaid",
|
|
433
434
|
layout: opts?.refinementLayout ?? "TD",
|
|
434
435
|
labelMode: opts?.labelMode ?? "friendly",
|
|
436
|
+
clickTargets: opts?.clickTargets,
|
|
435
437
|
});
|
|
436
438
|
if (refinement.includes("-->")) {
|
|
437
439
|
lines.push("## Refinement Chain");
|
|
@@ -446,6 +448,7 @@ function generateDiagramsFile(doc, opts) {
|
|
|
446
448
|
format: "mermaid",
|
|
447
449
|
layout: opts?.decisionLayout ?? "TD",
|
|
448
450
|
labelMode: opts?.labelMode ?? "friendly",
|
|
451
|
+
clickTargets: opts?.clickTargets,
|
|
449
452
|
});
|
|
450
453
|
if (decisions.includes("-->")) {
|
|
451
454
|
lines.push("## Decision Map");
|
|
@@ -460,6 +463,7 @@ function generateDiagramsFile(doc, opts) {
|
|
|
460
463
|
format: "mermaid",
|
|
461
464
|
layout: opts?.dependencyLayout ?? "LR",
|
|
462
465
|
labelMode: opts?.labelMode ?? "friendly",
|
|
466
|
+
clickTargets: opts?.clickTargets,
|
|
463
467
|
});
|
|
464
468
|
if (dependencies.includes("-->") || dependencies.includes("-.->")) {
|
|
465
469
|
lines.push("## Dependency Graph");
|
|
@@ -471,6 +475,23 @@ function generateDiagramsFile(doc, opts) {
|
|
|
471
475
|
}
|
|
472
476
|
return lines.join("\n") + "\n";
|
|
473
477
|
}
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Public API
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
/** Build a click target map from node anchors for use in embedded diagrams. */
|
|
482
|
+
function buildAnchorClickMap(nodes, nodeMap, currentFile) {
|
|
483
|
+
const targets = {};
|
|
484
|
+
for (const node of nodes) {
|
|
485
|
+
const loc = nodeMap.get(node.id);
|
|
486
|
+
if (!loc)
|
|
487
|
+
continue;
|
|
488
|
+
targets[node.id] =
|
|
489
|
+
loc.file === "" || loc.file === currentFile
|
|
490
|
+
? `#${loc.anchor}`
|
|
491
|
+
: `./${loc.file}#${loc.anchor}`;
|
|
492
|
+
}
|
|
493
|
+
return targets;
|
|
494
|
+
}
|
|
474
495
|
/**
|
|
475
496
|
* Convert a SysProM document to a single Markdown string.
|
|
476
497
|
* @param doc - The SysProM document to convert.
|
|
@@ -518,6 +539,9 @@ export function jsonToMarkdownSingle(doc, options) {
|
|
|
518
539
|
if (options?.embedDiagrams &&
|
|
519
540
|
doc.relationships &&
|
|
520
541
|
doc.relationships.length > 0) {
|
|
542
|
+
const clickTargets = options?.diagramLinks
|
|
543
|
+
? buildAnchorClickMap(doc.nodes, nodeMap, "")
|
|
544
|
+
: undefined;
|
|
521
545
|
lines.push("## Diagrams");
|
|
522
546
|
lines.push("");
|
|
523
547
|
lines.push("### Relationship Graph");
|
|
@@ -530,6 +554,7 @@ export function jsonToMarkdownSingle(doc, options) {
|
|
|
530
554
|
labelMode: options?.labelMode ?? "friendly",
|
|
531
555
|
cluster: true,
|
|
532
556
|
connectedOnly: false,
|
|
557
|
+
clickTargets,
|
|
533
558
|
}));
|
|
534
559
|
lines.push("```");
|
|
535
560
|
lines.push("");
|
|
@@ -591,12 +616,16 @@ export function jsonToMarkdownMultiDoc(doc, outDir, options) {
|
|
|
591
616
|
if (options?.embedDiagrams &&
|
|
592
617
|
doc.relationships &&
|
|
593
618
|
doc.relationships.length > 0) {
|
|
619
|
+
const clickTargets = options?.diagramLinks
|
|
620
|
+
? buildAnchorClickMap(doc.nodes, nodeMap, "DIAGRAMS.md")
|
|
621
|
+
: undefined;
|
|
594
622
|
writeFileSync(join(outDir, "DIAGRAMS.md"), generateDiagramsFile(doc, {
|
|
595
623
|
labelMode: options?.labelMode ?? "friendly",
|
|
596
624
|
relationshipLayout: options?.relationshipLayout,
|
|
597
625
|
refinementLayout: options?.refinementLayout,
|
|
598
626
|
decisionLayout: options?.decisionLayout,
|
|
599
627
|
dependencyLayout: options?.dependencyLayout,
|
|
628
|
+
clickTargets,
|
|
600
629
|
}));
|
|
601
630
|
}
|
|
602
631
|
// Subsystem folders or single files
|
|
@@ -662,6 +691,7 @@ export function jsonToMarkdown(doc, output, options) {
|
|
|
662
691
|
if (options.form === "single-file") {
|
|
663
692
|
writeFileSync(output, jsonToMarkdownSingle(doc, {
|
|
664
693
|
embedDiagrams: options.embedDiagrams,
|
|
694
|
+
diagramLinks: options.diagramLinks,
|
|
665
695
|
labelMode: options.labelMode,
|
|
666
696
|
relationshipLayout: options.relationshipLayout,
|
|
667
697
|
refinementLayout: options.refinementLayout,
|
|
@@ -672,6 +702,7 @@ export function jsonToMarkdown(doc, output, options) {
|
|
|
672
702
|
else {
|
|
673
703
|
jsonToMarkdownMultiDoc(doc, output, {
|
|
674
704
|
embedDiagrams: options.embedDiagrams,
|
|
705
|
+
diagramLinks: options.diagramLinks,
|
|
675
706
|
labelMode: options.labelMode,
|
|
676
707
|
relationshipLayout: options.relationshipLayout,
|
|
677
708
|
refinementLayout: options.refinementLayout,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { defineOperation } from "./define-operation.js";
|
|
3
3
|
import { SysProMDocument } from "../schema.js";
|
|
4
|
-
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
|
|
4
|
+
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
|
|
5
5
|
const DECISION_REL_TYPES = new Set([
|
|
6
6
|
"must_preserve",
|
|
7
7
|
"affects",
|
|
@@ -23,7 +23,7 @@ function collectDecisionMap(doc, seedIds) {
|
|
|
23
23
|
const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
|
|
24
24
|
return { nodes, rels };
|
|
25
25
|
}
|
|
26
|
-
function generateDecisionMermaid(nodes, rels, labelMode) {
|
|
26
|
+
function generateDecisionMermaid(nodes, rels, labelMode, clickMap) {
|
|
27
27
|
const lines = [];
|
|
28
28
|
lines.push("graph TD");
|
|
29
29
|
for (const def of renderMermaidClassDefs()) {
|
|
@@ -43,6 +43,7 @@ function generateDecisionMermaid(nodes, rels, labelMode) {
|
|
|
43
43
|
const label = renderRelationshipLabel(rel);
|
|
44
44
|
lines.push(` ${fromId} ${style}|${label}| ${toId}`);
|
|
45
45
|
}
|
|
46
|
+
lines.push(...renderMermaidClickDirectives(nodes, clickMap));
|
|
46
47
|
return lines.join("\n");
|
|
47
48
|
}
|
|
48
49
|
function generateDecisionDot(nodes, rels, layout, labelMode) {
|
|
@@ -80,14 +81,18 @@ export const graphDecisionOp = defineOperation({
|
|
|
80
81
|
seedIds: z.array(z.string()).optional(),
|
|
81
82
|
layout: z.enum(["LR", "TD", "RL", "BT"]).default("TD"),
|
|
82
83
|
labelMode: z.enum(["friendly", "compact"]).default("friendly"),
|
|
84
|
+
clickTargets: z.record(z.string(), z.string()).optional(),
|
|
83
85
|
}),
|
|
84
86
|
output: z.string(),
|
|
85
|
-
fn({ doc, format, seedIds, layout, labelMode }) {
|
|
87
|
+
fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
|
|
86
88
|
const { nodes, rels } = collectDecisionMap(doc, seedIds);
|
|
87
89
|
if (format === "dot") {
|
|
88
90
|
return generateDecisionDot(nodes, rels, layout, labelMode);
|
|
89
91
|
}
|
|
90
|
-
const
|
|
92
|
+
const clickMap = clickTargets
|
|
93
|
+
? new Map(Object.entries(clickTargets))
|
|
94
|
+
: undefined;
|
|
95
|
+
const mermaid = generateDecisionMermaid(nodes, rels, labelMode, clickMap);
|
|
91
96
|
const mermaidLines = mermaid.split("\n");
|
|
92
97
|
mermaidLines[0] = `graph ${layout}`;
|
|
93
98
|
return mermaidLines.join("\n");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { defineOperation } from "./define-operation.js";
|
|
3
3
|
import { SysProMDocument } from "../schema.js";
|
|
4
|
-
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
|
|
4
|
+
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
|
|
5
5
|
const DEPENDENCY_REL_TYPES = new Set([
|
|
6
6
|
"depends_on",
|
|
7
7
|
"constrained_by",
|
|
@@ -33,7 +33,7 @@ function collectDependencyGraph(doc, seedIds) {
|
|
|
33
33
|
const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
|
|
34
34
|
return { nodes, rels };
|
|
35
35
|
}
|
|
36
|
-
function generateDependencyMermaid(nodes, rels, labelMode) {
|
|
36
|
+
function generateDependencyMermaid(nodes, rels, labelMode, clickMap) {
|
|
37
37
|
const lines = [];
|
|
38
38
|
lines.push("graph LR");
|
|
39
39
|
for (const def of renderMermaidClassDefs()) {
|
|
@@ -52,6 +52,7 @@ function generateDependencyMermaid(nodes, rels, labelMode) {
|
|
|
52
52
|
const label = renderRelationshipLabel(rel);
|
|
53
53
|
lines.push(` ${fromId} -->|${label}| ${toId}`);
|
|
54
54
|
}
|
|
55
|
+
lines.push(...renderMermaidClickDirectives(nodes, clickMap));
|
|
55
56
|
return lines.join("\n");
|
|
56
57
|
}
|
|
57
58
|
function generateDependencyDot(nodes, rels, layout, labelMode) {
|
|
@@ -86,14 +87,18 @@ export const graphDependencyOp = defineOperation({
|
|
|
86
87
|
seedIds: z.array(z.string()).optional(),
|
|
87
88
|
layout: z.enum(["LR", "TD", "RL", "BT"]).default("LR"),
|
|
88
89
|
labelMode: z.enum(["friendly", "compact"]).default("friendly"),
|
|
90
|
+
clickTargets: z.record(z.string(), z.string()).optional(),
|
|
89
91
|
}),
|
|
90
92
|
output: z.string(),
|
|
91
|
-
fn({ doc, format, seedIds, layout, labelMode }) {
|
|
93
|
+
fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
|
|
92
94
|
const { nodes, rels } = collectDependencyGraph(doc, seedIds);
|
|
93
95
|
if (format === "dot") {
|
|
94
96
|
return generateDependencyDot(nodes, rels, layout, labelMode);
|
|
95
97
|
}
|
|
96
|
-
const
|
|
98
|
+
const clickMap = clickTargets
|
|
99
|
+
? new Map(Object.entries(clickTargets))
|
|
100
|
+
: undefined;
|
|
101
|
+
const mermaid = generateDependencyMermaid(nodes, rels, labelMode, clickMap);
|
|
97
102
|
const mermaidLines = mermaid.split("\n");
|
|
98
103
|
mermaidLines[0] = `graph ${layout}`;
|
|
99
104
|
return mermaidLines.join("\n");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { defineOperation } from "./define-operation.js";
|
|
3
3
|
import { SysProMDocument } from "../schema.js";
|
|
4
|
-
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
|
|
4
|
+
import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
|
|
5
5
|
const REFINEMENT_REL_TYPES = new Set(["refines", "realises", "implements"]);
|
|
6
6
|
function collectRefinementChain(doc, seedIds) {
|
|
7
7
|
const rels = (doc.relationships ?? []).filter((r) => REFINEMENT_REL_TYPES.has(r.type));
|
|
@@ -29,7 +29,7 @@ function collectRefinementChain(doc, seedIds) {
|
|
|
29
29
|
const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
|
|
30
30
|
return { nodes, rels };
|
|
31
31
|
}
|
|
32
|
-
function generateRefinementMermaid(nodes, rels, labelMode) {
|
|
32
|
+
function generateRefinementMermaid(nodes, rels, labelMode, clickMap) {
|
|
33
33
|
const lines = [];
|
|
34
34
|
lines.push("graph TD");
|
|
35
35
|
for (const def of renderMermaidClassDefs()) {
|
|
@@ -48,6 +48,7 @@ function generateRefinementMermaid(nodes, rels, labelMode) {
|
|
|
48
48
|
const label = renderRelationshipLabel(rel);
|
|
49
49
|
lines.push(` ${fromId} -->|${label}| ${toId}`);
|
|
50
50
|
}
|
|
51
|
+
lines.push(...renderMermaidClickDirectives(nodes, clickMap));
|
|
51
52
|
return lines.join("\n");
|
|
52
53
|
}
|
|
53
54
|
function generateRefinementDot(nodes, rels, layout, labelMode) {
|
|
@@ -82,14 +83,18 @@ export const graphRefinementOp = defineOperation({
|
|
|
82
83
|
seedIds: z.array(z.string()).optional(),
|
|
83
84
|
layout: z.enum(["LR", "TD", "RL", "BT"]).default("TD"),
|
|
84
85
|
labelMode: z.enum(["friendly", "compact"]).default("friendly"),
|
|
86
|
+
clickTargets: z.record(z.string(), z.string()).optional(),
|
|
85
87
|
}),
|
|
86
88
|
output: z.string(),
|
|
87
|
-
fn({ doc, format, seedIds, layout, labelMode }) {
|
|
89
|
+
fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
|
|
88
90
|
const { nodes, rels } = collectRefinementChain(doc, seedIds);
|
|
89
91
|
if (format === "dot") {
|
|
90
92
|
return generateRefinementDot(nodes, rels, layout, labelMode);
|
|
91
93
|
}
|
|
92
|
-
const
|
|
94
|
+
const clickMap = clickTargets
|
|
95
|
+
? new Map(Object.entries(clickTargets))
|
|
96
|
+
: undefined;
|
|
97
|
+
const mermaid = generateRefinementMermaid(nodes, rels, labelMode, clickMap);
|
|
93
98
|
const mermaidLines = mermaid.split("\n");
|
|
94
99
|
mermaidLines[0] = `graph ${layout}`;
|
|
95
100
|
return mermaidLines.join("\n");
|
|
@@ -114,3 +114,18 @@ export declare function filterRelationships(rels: Relationship[], opts: GraphFil
|
|
|
114
114
|
* @example
|
|
115
115
|
*/
|
|
116
116
|
export declare function applyConnectedOnly(nodes: Node[], rels: Relationship[]): Node[];
|
|
117
|
+
/** Map from raw node ID to click target URL. */
|
|
118
|
+
export type MermaidClickMap = Map<string, string>;
|
|
119
|
+
/**
|
|
120
|
+
* Render Mermaid click directives for nodes that have a click target.
|
|
121
|
+
* @param nodes - Nodes to render click directives for
|
|
122
|
+
* @param clickMap - Map from node ID to target URL
|
|
123
|
+
* @returns Lines like `click INT1 "url" "tooltip"`
|
|
124
|
+
*/
|
|
125
|
+
export declare function renderMermaidClickDirectives(nodes: Node[], clickMap: MermaidClickMap | undefined): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Build a MermaidClickMap from nodes' external_references.
|
|
128
|
+
* Uses the first external reference's identifier as the URL.
|
|
129
|
+
* @param nodes - Nodes to extract external references from
|
|
130
|
+
*/
|
|
131
|
+
export declare function buildExternalRefClickMap(nodes: Node[]): MermaidClickMap;
|
|
@@ -272,3 +272,36 @@ export function applyConnectedOnly(nodes, rels) {
|
|
|
272
272
|
}
|
|
273
273
|
return nodes.filter((n) => connectedIds.has(n.id));
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Render Mermaid click directives for nodes that have a click target.
|
|
277
|
+
* @param nodes - Nodes to render click directives for
|
|
278
|
+
* @param clickMap - Map from node ID to target URL
|
|
279
|
+
* @returns Lines like `click INT1 "url" "tooltip"`
|
|
280
|
+
*/
|
|
281
|
+
export function renderMermaidClickDirectives(nodes, clickMap) {
|
|
282
|
+
if (!clickMap || clickMap.size === 0)
|
|
283
|
+
return [];
|
|
284
|
+
const lines = [""];
|
|
285
|
+
for (const node of nodes) {
|
|
286
|
+
const url = clickMap.get(node.id);
|
|
287
|
+
if (!url)
|
|
288
|
+
continue;
|
|
289
|
+
const safeId = sanitiseMermaidId(node.id);
|
|
290
|
+
lines.push(` click ${safeId} "${url}" "${node.name}"`);
|
|
291
|
+
}
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Build a MermaidClickMap from nodes' external_references.
|
|
296
|
+
* Uses the first external reference's identifier as the URL.
|
|
297
|
+
* @param nodes - Nodes to extract external references from
|
|
298
|
+
*/
|
|
299
|
+
export function buildExternalRefClickMap(nodes) {
|
|
300
|
+
const map = new Map();
|
|
301
|
+
for (const node of nodes) {
|
|
302
|
+
const ref = node.external_references?.[0];
|
|
303
|
+
if (ref)
|
|
304
|
+
map.set(node.id, ref.identifier);
|
|
305
|
+
}
|
|
306
|
+
return map;
|
|
307
|
+
}
|
|
@@ -277,4 +277,5 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
|
|
|
277
277
|
compact: "compact";
|
|
278
278
|
}>>;
|
|
279
279
|
connectedOnly: z.ZodDefault<z.ZodBoolean>;
|
|
280
|
+
clickTargets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
280
281
|
}, z.core.$strip>, z.ZodString>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { defineOperation } from "./define-operation.js";
|
|
3
3
|
import { SysProMDocument } from "../schema.js";
|
|
4
|
-
import { sanitiseMermaidId, NODE_CATEGORIES, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, filterNodes, filterRelationships, applyConnectedOnly, } from "./graph-shared.js";
|
|
5
|
-
import { renderRelationshipLabel } from "./graph-shared.js";
|
|
4
|
+
import { sanitiseMermaidId, NODE_CATEGORIES, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, filterNodes, filterRelationships, applyConnectedOnly, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
|
|
6
5
|
// ---------------------------------------------------------------------------
|
|
7
6
|
// DOT generation
|
|
8
7
|
// ---------------------------------------------------------------------------
|
|
@@ -61,7 +60,7 @@ function generateDot(nodes, rels, cluster, layout, labelMode) {
|
|
|
61
60
|
// ---------------------------------------------------------------------------
|
|
62
61
|
// Mermaid generation
|
|
63
62
|
// ---------------------------------------------------------------------------
|
|
64
|
-
function generateMermaid(nodes, rels, cluster, layout, labelMode) {
|
|
63
|
+
function generateMermaid(nodes, rels, cluster, layout, labelMode, clickMap) {
|
|
65
64
|
const lines = [];
|
|
66
65
|
lines.push(`graph ${layout}`);
|
|
67
66
|
for (const def of renderMermaidClassDefs()) {
|
|
@@ -112,12 +111,13 @@ function generateMermaid(nodes, rels, cluster, layout, labelMode) {
|
|
|
112
111
|
}
|
|
113
112
|
lines.push(` ${edge}`);
|
|
114
113
|
}
|
|
114
|
+
lines.push(...renderMermaidClickDirectives(nodes, clickMap));
|
|
115
115
|
return lines.join("\n");
|
|
116
116
|
}
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
118
118
|
// Graph generation
|
|
119
119
|
// ---------------------------------------------------------------------------
|
|
120
|
-
function generateGraph(doc, format, filterOpts, layout, cluster, labelMode) {
|
|
120
|
+
function generateGraph(doc, format, filterOpts, layout, cluster, labelMode, clickMap) {
|
|
121
121
|
let nodes = filterNodes(doc.nodes, filterOpts);
|
|
122
122
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
123
123
|
let rels = filterRelationships(doc.relationships ?? [], filterOpts, nodeIds);
|
|
@@ -134,7 +134,7 @@ function generateGraph(doc, format, filterOpts, layout, cluster, labelMode) {
|
|
|
134
134
|
const dotLayout = layout === "TD" ? "LR" : layout;
|
|
135
135
|
return generateDot(nodes, rels, cluster, dotLayout, labelMode);
|
|
136
136
|
}
|
|
137
|
-
return generateMermaid(nodes, rels, cluster, layout, labelMode);
|
|
137
|
+
return generateMermaid(nodes, rels, cluster, layout, labelMode, clickMap);
|
|
138
138
|
}
|
|
139
139
|
/** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering. */
|
|
140
140
|
export const graphOp = defineOperation({
|
|
@@ -151,15 +151,19 @@ export const graphOp = defineOperation({
|
|
|
151
151
|
cluster: z.boolean().default(true),
|
|
152
152
|
labelMode: z.enum(["friendly", "compact"]).default("friendly"),
|
|
153
153
|
connectedOnly: z.boolean().default(false),
|
|
154
|
+
clickTargets: z.record(z.string(), z.string()).optional(),
|
|
154
155
|
}),
|
|
155
156
|
output: z.string(),
|
|
156
|
-
fn({ doc, format, typeFilter, nodeTypes, nodeIds, relTypes, layout, cluster, labelMode, connectedOnly, }) {
|
|
157
|
+
fn({ doc, format, typeFilter, nodeTypes, nodeIds, relTypes, layout, cluster, labelMode, connectedOnly, clickTargets, }) {
|
|
157
158
|
const filterOpts = {
|
|
158
159
|
nodeTypes,
|
|
159
160
|
nodeIds,
|
|
160
161
|
relTypes: relTypes ?? (typeFilter ? [typeFilter] : undefined),
|
|
161
162
|
connectedOnly,
|
|
162
163
|
};
|
|
163
|
-
|
|
164
|
+
const clickMap = clickTargets
|
|
165
|
+
? new Map(Object.entries(clickTargets))
|
|
166
|
+
: undefined;
|
|
167
|
+
return generateGraph(doc, format, filterOpts, layout, cluster, labelMode, clickMap);
|
|
164
168
|
},
|
|
165
169
|
});
|