Forge
csharp4405de34
1using System.Text;
2using System.Text.Json;
3
4namespace CascadeIDE.Tests;
5
6internal static class ControlFlowSubgraphTextPresenter
7{
8 public static string Render(string subgraphJson)
9 {
10 using var doc = JsonDocument.Parse(subgraphJson);
11 var root = doc.RootElement;
12 var nodeElements = root.GetProperty("nodes").EnumerateArray().ToList();
13 var nodes = nodeElements
14 .ToDictionary(
15 n => n.GetProperty("id").GetString() ?? string.Empty,
16 ToCaption,
17 StringComparer.Ordinal);
18 var edges = root.GetProperty("edges").EnumerateArray()
19 .Select((e, index) => new EdgeView(
20 e.GetProperty("from_id").GetString() ?? string.Empty,
21 e.GetProperty("to_id").GetString() ?? string.Empty,
22 e.GetProperty("kind").GetString() ?? "Call",
23 index))
24 .ToList();
25 if (edges.Count == 0)
26 return string.Join(", ", nodes.Values);
27
28 var anchorId = nodeElements
29 .Where(n => string.Equals(n.GetProperty("kind").GetString(), "anchor", StringComparison.Ordinal))
30 .Select(n => n.GetProperty("id").GetString())
31 .FirstOrDefault()
32 ?? "n0";
33
34 var outgoing = edges
35 .GroupBy(e => e.FromId, StringComparer.Ordinal)
36 .ToDictionary(
37 g => g.Key,
38 g => g.OrderBy(x => x.Order).ToList(),
39 StringComparer.Ordinal);
40
41 var hasBranch = outgoing.Values.Any(v => v.Count > 1);
42 return hasBranch
43 ? RenderBranched(anchorId, nodes, outgoing)
44 : RenderLinear(anchorId, nodes, outgoing, edges.Count);
45 }
46
47 private static string RenderLinear(
48 string anchorId,
49 IReadOnlyDictionary<string, string> nodes,
50 IReadOnlyDictionary<string, List<EdgeView>> outgoing,
51 int edgeCount)
52 {
53 var sb = new StringBuilder();
54 var currentId = anchorId;
55 sb.Append(nodes.GetValueOrDefault(currentId, currentId));
56
57 // edgeCount bound protects from accidental cycles.
58 for (var i = 0; i < edgeCount; i++)
59 {
60 if (!outgoing.TryGetValue(currentId, out var nextEdges) || nextEdges.Count == 0)
61 break;
62
63 var edge = nextEdges[0];
64 sb.Append(" -(").Append(edge.Kind).Append(")-> ")
65 .Append(nodes.GetValueOrDefault(edge.ToId, edge.ToId));
66 currentId = edge.ToId;
67 }
68
69 return sb.ToString();
70 }
71
72 private static string RenderBranched(
73 string anchorId,
74 IReadOnlyDictionary<string, string> nodes,
75 IReadOnlyDictionary<string, List<EdgeView>> outgoing)
76 {
77 var sb = new StringBuilder();
78 sb.Append(nodes.GetValueOrDefault(anchorId, anchorId));
79 AppendBranches(sb, anchorId, "", nodes, outgoing, []);
80 return sb.ToString();
81 }
82
83 private static void AppendBranches(
84 StringBuilder sb,
85 string fromId,
86 string prefix,
87 IReadOnlyDictionary<string, string> nodes,
88 IReadOnlyDictionary<string, List<EdgeView>> outgoing,
89 HashSet<string> path)
90 {
91 if (!outgoing.TryGetValue(fromId, out var edges) || edges.Count == 0)
92 return;
93
94 var localPath = new HashSet<string>(path, StringComparer.Ordinal) { fromId };
95 for (var i = 0; i < edges.Count; i++)
96 {
97 var edge = edges[i];
98 var isLast = i == edges.Count - 1;
99 var branch = isLast ? "`-- " : "|-- ";
100 var nextPrefix = prefix + (isLast ? " " : "| ");
101 var toCaption = nodes.GetValueOrDefault(edge.ToId, edge.ToId);
102 var isCycle = localPath.Contains(edge.ToId);
103
104 sb.AppendLine();
105 sb.Append(prefix)
106 .Append(branch)
107 .Append('(').Append(edge.Kind).Append(") ")
108 .Append(toCaption);
109 if (isCycle)
110 {
111 sb.Append(" [cycle]");
112 continue;
113 }
114
115 AppendBranches(sb, edge.ToId, nextPrefix, nodes, outgoing, localPath);
116 }
117 }
118
119 private static string ToCaption(JsonElement node)
120 {
121 var kind = node.GetProperty("kind").GetString() ?? string.Empty;
122 return kind switch
123 {
124 "anchor" => "A",
125 "condition_step" => "?",
126 "exit_step" => "R",
127 "protected_step" => "T",
128 "handler_step" => "!",
129 _ => node.GetProperty("label").GetString() ?? "?"
130 };
131 }
132
133 private readonly record struct EdgeView(string FromId, string ToId, string Kind, int Order);
134}
135
View only · write via MCP/CIDE