Skip to content

Commit b88427d

Browse files
richlanderCopilot
andauthored
Add --mermaid flag for diagram output in depends command (#304)
* Add --mermaid flag for diagram output in depends command Two modes: - --mermaid: standalone mermaid output via MermaidFormatter (graph TD) - --markdown --mermaid: mermaid embedded as fenced code blocks in markdown Wired through OutputFormat enum, OutputFormatResolver, SharedOptions, DependsOptions, and SearchCommandDefinitions. Supports type hierarchy, library reference, and package dependency modes. Requires Markout with MermaidFormatter (not yet published; validated via ProjectReference). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Markout to 0.12.0 (adds MermaidFormatter) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent db9f04f commit b88427d

6 files changed

Lines changed: 134 additions & 19 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageVersion Include="MarkdownTable.Formatting" Version="0.3.3" />
7-
<PackageVersion Include="Markout" Version="0.11.0" />
7+
<PackageVersion Include="Markout" Version="0.12.0" />
88
<PackageVersion Include="NuGet.Versioning" Version="7.3.0" />
99
<PackageVersion Include="NuGetFetch" Version="0.6.1" />
1010
<PackageVersion Include="ShellComplete" Version="0.1.0" />

src/dotnet-inspect/CommandLine/Commands/SearchCommandDefinitions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ public static Command CreateDependsCommand(SharedOptions opts)
374374
dependsCommand.Options.Add(tfmOption);
375375
dependsCommand.Options.Add(opts.Json);
376376
dependsCommand.Options.Add(compactOption);
377+
dependsCommand.Options.Add(opts.Mermaid);
378+
dependsCommand.Options.Add(opts.Markdown);
377379
opts.AddOutputOptionsTo(dependsCommand);
378380
opts.AddNuGetOptionsTo(dependsCommand);
379381

@@ -391,6 +393,8 @@ public static Command CreateDependsCommand(SharedOptions opts)
391393
Tfm = parseResult.GetValue(tfmOption),
392394
JsonOutput = parseResult.GetValue(opts.Json),
393395
CompactJson = parseResult.GetValue(compactOption),
396+
MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid,
397+
EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult),
394398
Verbose = parseResult.GetValue(opts.Verbose),
395399
SourceOptions = opts.ParseNuGetSourceOptions(parseResult)
396400
};
@@ -424,6 +428,8 @@ public static Command CreateDependsCommand(SharedOptions opts)
424428
Tfm = parseResult.GetValue(tfmOption),
425429
JsonOutput = parseResult.GetValue(opts.Json),
426430
CompactJson = parseResult.GetValue(compactOption),
431+
MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid,
432+
EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult),
427433
Verbose = parseResult.GetValue(opts.Verbose),
428434
SourceOptions = opts.ParseNuGetSourceOptions(parseResult)
429435
};
@@ -439,6 +445,8 @@ public static Command CreateDependsCommand(SharedOptions opts)
439445
Tfm = parseResult.GetValue(tfmOption),
440446
JsonOutput = parseResult.GetValue(opts.Json),
441447
CompactJson = parseResult.GetValue(compactOption),
448+
MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid,
449+
EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult),
442450
Verbose = parseResult.GetValue(opts.Verbose),
443451
SourceOptions = opts.ParseNuGetSourceOptions(parseResult)
444452
};

src/dotnet-inspect/Commands/DependsCommand.cs

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using DotnetInspector.Services;
88
using DotnetInspector.Views;
99
using Markout;
10+
using Markout.Formatting;
1011

1112
namespace DotnetInspector.Commands;
1213

@@ -68,12 +69,25 @@ public static async Task<int> ExecuteTypeDependsAsync(DependsOptions options)
6869
else
6970
{
7071
var rootName = options.TargetType.Contains('<') ? options.TargetType : result.MatchedType!;
71-
var view = new PackageDependenciesView
72+
var treeNodes = ToTreeNodes(result.Tree);
73+
74+
if (options.MermaidOutput)
7275
{
73-
Title = rootName,
74-
Dependencies = ToTreeNodes(result.Tree)
75-
};
76-
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
76+
WriteMermaidTree(rootName, treeNodes);
77+
}
78+
else if (options.EmbeddedMermaid)
79+
{
80+
WriteEmbeddedMermaidTree(rootName, treeNodes);
81+
}
82+
else
83+
{
84+
var view = new PackageDependenciesView
85+
{
86+
Title = rootName,
87+
Dependencies = treeNodes
88+
};
89+
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
90+
}
7791
}
7892

7993
return 0;
@@ -159,12 +173,23 @@ public static async Task<int> ExecuteLibraryDependsAsync(DependsOptions options)
159173

160174
var treeNodes = BuildNestedDependencyTree(refNodes);
161175

162-
var view = new PackageDependenciesView
176+
if (options.MermaidOutput)
177+
{
178+
WriteMermaidTree(assemblyName, treeNodes);
179+
}
180+
else if (options.EmbeddedMermaid)
163181
{
164-
Title = assemblyName,
165-
Dependencies = treeNodes
166-
};
167-
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
182+
WriteEmbeddedMermaidTree(assemblyName, treeNodes);
183+
}
184+
else
185+
{
186+
var view = new PackageDependenciesView
187+
{
188+
Title = assemblyName,
189+
Dependencies = treeNodes
190+
};
191+
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
192+
}
168193
return 0;
169194
}
170195
catch (Exception ex)
@@ -253,12 +278,26 @@ public static async Task<int> ExecutePackageDependsAsync(DependsOptions options)
253278
var depNodes = await DependencyResolutionService.ResolveDependencyTreeAsync(
254279
context.HttpClient, group.Dependencies, tfm, globalSeen, logger.Log);
255280

256-
var view = new PackageDependenciesView
281+
var title = $"{packageName} ({version})";
282+
var treeNodes = ToDependencyTreeNodes(depNodes);
283+
284+
if (options.MermaidOutput)
257285
{
258-
Title = $"{packageName} ({version})",
259-
Dependencies = ToDependencyTreeNodes(depNodes)
260-
};
261-
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
286+
WriteMermaidTree(title, treeNodes);
287+
}
288+
else if (options.EmbeddedMermaid)
289+
{
290+
WriteEmbeddedMermaidTree(title, treeNodes);
291+
}
292+
else
293+
{
294+
var view = new PackageDependenciesView
295+
{
296+
Title = title,
297+
Dependencies = treeNodes
298+
};
299+
MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default);
300+
}
262301
return 0;
263302
}
264303
catch (Exception ex)
@@ -324,4 +363,36 @@ private static void BuildNestedNodes(List<AssemblyReferenceNode> nodes, ref int
324363
target.Add(children.Count > 0 ? new TreeNode(label) { Children = children } : new TreeNode(label));
325364
}
326365
}
366+
367+
/// <summary>
368+
/// Writes standalone mermaid output using the MermaidFormatter.
369+
/// </summary>
370+
private static void WriteMermaidTree(string title, List<TreeNode> treeNodes)
371+
{
372+
var writer = MarkoutWriter.Create(Console.Out, new MermaidFormatter());
373+
writer.WriteHeading(1, title);
374+
writer.WriteTree([.. treeNodes]);
375+
writer.Flush();
376+
}
377+
378+
/// <summary>
379+
/// Writes mermaid embedded in a markdown document (```mermaid code block).
380+
/// </summary>
381+
private static void WriteEmbeddedMermaidTree(string title, List<TreeNode> treeNodes)
382+
{
383+
var mdWriter = MarkoutWriter.Create(Console.Out, new MarkdownFormatter());
384+
mdWriter.WriteHeading(1, title);
385+
386+
// Render the mermaid content to a string
387+
var mermaidWriter = MarkoutWriter.Create(new MermaidFormatter());
388+
mermaidWriter.WriteTree([.. treeNodes]);
389+
var mermaidContent = mermaidWriter.ToString();
390+
391+
mdWriter.WriteCodeStart("mermaid");
392+
Console.Out.Write(mermaidContent);
393+
if (!mermaidContent.EndsWith('\n'))
394+
Console.Out.WriteLine();
395+
mdWriter.WriteCodeEnd();
396+
mdWriter.Flush();
397+
}
327398
}

src/dotnet-inspect/Options/DependsOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ public record DependsOptions : IAssemblySourceOptions
5757
/// </summary>
5858
public bool CompactJson { get; init; }
5959

60+
/// <summary>
61+
/// Output as standalone Mermaid diagram.
62+
/// </summary>
63+
public bool MermaidOutput { get; init; }
64+
65+
/// <summary>
66+
/// Embed mermaid diagrams in markdown output (--markdown --mermaid).
67+
/// </summary>
68+
public bool EmbeddedMermaid { get; init; }
69+
6070
/// <summary>
6171
/// Show progress messages on stderr.
6272
/// </summary>

src/dotnet-inspect/Options/OutputFormat.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ public enum OutputFormat
2424
/// <summary>
2525
/// JSON output.
2626
/// </summary>
27-
Json
27+
Json,
28+
29+
/// <summary>
30+
/// Standalone Mermaid diagram syntax (graph TD, classDiagram, etc.).
31+
/// Only works for commands that produce graph/tree data.
32+
/// </summary>
33+
Mermaid
2834
}
2935

3036
/// <summary>
@@ -40,11 +46,13 @@ public static class OutputFormatResolver
4046
/// Resolves format. Any -v flag implies Markdown. --json implies Json. --markdown implies Markdown.
4147
/// Commands may supply a <paramref name="defaultFormat"/> to override the global default (Markdown).
4248
/// </summary>
43-
public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity? verbosity, bool plainTextFlag = false, OutputFormat defaultFormat = OutputFormat.Markdown)
49+
public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity? verbosity, bool plainTextFlag = false, bool mermaidFlag = false, OutputFormat defaultFormat = OutputFormat.Markdown)
4450
{
4551
// Explicit CLI flags win
4652
if (jsonFlag)
4753
return OutputFormat.Json;
54+
if (mermaidFlag && !markdownFlag)
55+
return OutputFormat.Mermaid;
4856
if (markdownFlag)
4957
return OutputFormat.Markdown;
5058
if (plainTextFlag)
@@ -60,6 +68,13 @@ public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity?
6068
return defaultFormat;
6169
}
6270

71+
/// <summary>
72+
/// Returns true when --mermaid is combined with --markdown (embedded mermaid mode).
73+
/// In this mode, the output is still markdown but tree/graph sections render as mermaid code blocks.
74+
/// </summary>
75+
public static bool IsEmbeddedMermaid(bool markdownFlag, bool mermaidFlag)
76+
=> markdownFlag && mermaidFlag;
77+
6378
/// <summary>
6479
/// Warns when --oneline is combined with a verbosity that produces multiple sections
6580
/// without a section selector. Oneline can only render one table at a time.
@@ -85,6 +100,7 @@ public static bool WarnIfOneLineDetailMismatch(bool oneLine, Verbosity verbosity
85100
"markdown" or "md" => OutputFormat.Markdown,
86101
"plaintext" or "plain-text" or "plain" or "text" => OutputFormat.PlainText,
87102
"json" => OutputFormat.Json,
103+
"mermaid" => OutputFormat.Mermaid,
88104
_ => null
89105
};
90106
_envParsed = true;

src/dotnet-inspect/Services/SharedOptions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class SharedOptions
1515
public Option<bool> Json { get; } = new("--json") { Description = "Output as JSON" };
1616
public Option<bool> Markdown { get; } = new("--markdown") { Description = "Output as markdown" };
1717
public Option<bool> PlainText { get; } = new("--plaintext") { Description = "Output as plain text" };
18+
public Option<bool> Mermaid { get; } = new("--mermaid") { Description = "Output as mermaid diagram (standalone or with --markdown for embedded)" };
1819

1920
// Verbosity options
2021
public Option<bool> Verbose { get; } = new("--verbose") { Description = "Show progress messages on stderr" };
@@ -147,6 +148,7 @@ public void AddAllOptionsTo(Command command)
147148
command.Options.Add(Json);
148149
command.Options.Add(Markdown);
149150
command.Options.Add(PlainText);
151+
command.Options.Add(Mermaid);
150152
AddOutputOptionsTo(command);
151153
AddSectionOptionsTo(command);
152154
AddNuGetOptionsTo(command);
@@ -195,11 +197,18 @@ public OutputFormat ResolveFormat(ParseResult parseResult, OutputFormat defaultF
195197
bool jsonFlag = parseResult.GetValue(Json);
196198
bool markdownFlag = parseResult.GetValue(Markdown);
197199
bool plainTextFlag = parseResult.GetValue(PlainText);
200+
bool mermaidFlag = parseResult.GetValue(Mermaid);
198201
bool hasVerbosity = parseResult.GetResult(Verbosity) is { Implicit: false };
199202
Verbosity? verbosity = hasVerbosity ? ParseVerbosity(parseResult) : null;
200-
return OutputFormatResolver.Resolve(jsonFlag, markdownFlag, verbosity, plainTextFlag, defaultFormat);
203+
return OutputFormatResolver.Resolve(jsonFlag, markdownFlag, verbosity, plainTextFlag, mermaidFlag, defaultFormat);
201204
}
202205

206+
/// <summary>
207+
/// Returns true when --mermaid is combined with --markdown (embedded mermaid in markdown).
208+
/// </summary>
209+
public bool IsEmbeddedMermaid(ParseResult parseResult)
210+
=> OutputFormatResolver.IsEmbeddedMermaid(parseResult.GetValue(Markdown), parseResult.GetValue(Mermaid));
211+
203212
/// <summary>
204213
/// Resolves whether oneline output should be used, considering the --oneline flag and format resolution.
205214
/// Explicit --oneline always wins; otherwise derived from ResolveFormat.
@@ -234,6 +243,7 @@ public bool IsFormatExplicitlySet(ParseResult parseResult, Option<bool>? oneLine
234243
if (parseResult.GetResult(Json) is { Implicit: false }) return true;
235244
if (parseResult.GetResult(Markdown) is { Implicit: false }) return true;
236245
if (parseResult.GetResult(PlainText) is { Implicit: false }) return true;
246+
if (parseResult.GetResult(Mermaid) is { Implicit: false }) return true;
237247
if (parseResult.GetResult(Verbosity) is { Implicit: false }) return true;
238248
return false;
239249
}

0 commit comments

Comments
 (0)