diff --git a/Directory.Packages.props b/Directory.Packages.props index 8805b74..3ef385d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + diff --git a/src/dotnet-inspect/CommandLine/Commands/SearchCommandDefinitions.cs b/src/dotnet-inspect/CommandLine/Commands/SearchCommandDefinitions.cs index afb6295..4b26f93 100644 --- a/src/dotnet-inspect/CommandLine/Commands/SearchCommandDefinitions.cs +++ b/src/dotnet-inspect/CommandLine/Commands/SearchCommandDefinitions.cs @@ -374,6 +374,8 @@ public static Command CreateDependsCommand(SharedOptions opts) dependsCommand.Options.Add(tfmOption); dependsCommand.Options.Add(opts.Json); dependsCommand.Options.Add(compactOption); + dependsCommand.Options.Add(opts.Mermaid); + dependsCommand.Options.Add(opts.Markdown); opts.AddOutputOptionsTo(dependsCommand); opts.AddNuGetOptionsTo(dependsCommand); @@ -391,6 +393,8 @@ public static Command CreateDependsCommand(SharedOptions opts) Tfm = parseResult.GetValue(tfmOption), JsonOutput = parseResult.GetValue(opts.Json), CompactJson = parseResult.GetValue(compactOption), + MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid, + EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult), Verbose = parseResult.GetValue(opts.Verbose), SourceOptions = opts.ParseNuGetSourceOptions(parseResult) }; @@ -424,6 +428,8 @@ public static Command CreateDependsCommand(SharedOptions opts) Tfm = parseResult.GetValue(tfmOption), JsonOutput = parseResult.GetValue(opts.Json), CompactJson = parseResult.GetValue(compactOption), + MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid, + EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult), Verbose = parseResult.GetValue(opts.Verbose), SourceOptions = opts.ParseNuGetSourceOptions(parseResult) }; @@ -439,6 +445,8 @@ public static Command CreateDependsCommand(SharedOptions opts) Tfm = parseResult.GetValue(tfmOption), JsonOutput = parseResult.GetValue(opts.Json), CompactJson = parseResult.GetValue(compactOption), + MermaidOutput = opts.ResolveFormat(parseResult) == OutputFormat.Mermaid, + EmbeddedMermaid = opts.IsEmbeddedMermaid(parseResult), Verbose = parseResult.GetValue(opts.Verbose), SourceOptions = opts.ParseNuGetSourceOptions(parseResult) }; diff --git a/src/dotnet-inspect/Commands/DependsCommand.cs b/src/dotnet-inspect/Commands/DependsCommand.cs index 93a1001..a0b9387 100644 --- a/src/dotnet-inspect/Commands/DependsCommand.cs +++ b/src/dotnet-inspect/Commands/DependsCommand.cs @@ -7,6 +7,7 @@ using DotnetInspector.Services; using DotnetInspector.Views; using Markout; +using Markout.Formatting; namespace DotnetInspector.Commands; @@ -68,12 +69,25 @@ public static async Task ExecuteTypeDependsAsync(DependsOptions options) else { var rootName = options.TargetType.Contains('<') ? options.TargetType : result.MatchedType!; - var view = new PackageDependenciesView + var treeNodes = ToTreeNodes(result.Tree); + + if (options.MermaidOutput) { - Title = rootName, - Dependencies = ToTreeNodes(result.Tree) - }; - MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + WriteMermaidTree(rootName, treeNodes); + } + else if (options.EmbeddedMermaid) + { + WriteEmbeddedMermaidTree(rootName, treeNodes); + } + else + { + var view = new PackageDependenciesView + { + Title = rootName, + Dependencies = treeNodes + }; + MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + } } return 0; @@ -159,12 +173,23 @@ public static async Task ExecuteLibraryDependsAsync(DependsOptions options) var treeNodes = BuildNestedDependencyTree(refNodes); - var view = new PackageDependenciesView + if (options.MermaidOutput) + { + WriteMermaidTree(assemblyName, treeNodes); + } + else if (options.EmbeddedMermaid) { - Title = assemblyName, - Dependencies = treeNodes - }; - MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + WriteEmbeddedMermaidTree(assemblyName, treeNodes); + } + else + { + var view = new PackageDependenciesView + { + Title = assemblyName, + Dependencies = treeNodes + }; + MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + } return 0; } catch (Exception ex) @@ -253,12 +278,26 @@ public static async Task ExecutePackageDependsAsync(DependsOptions options) var depNodes = await DependencyResolutionService.ResolveDependencyTreeAsync( context.HttpClient, group.Dependencies, tfm, globalSeen, logger.Log); - var view = new PackageDependenciesView + var title = $"{packageName} ({version})"; + var treeNodes = ToDependencyTreeNodes(depNodes); + + if (options.MermaidOutput) { - Title = $"{packageName} ({version})", - Dependencies = ToDependencyTreeNodes(depNodes) - }; - MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + WriteMermaidTree(title, treeNodes); + } + else if (options.EmbeddedMermaid) + { + WriteEmbeddedMermaidTree(title, treeNodes); + } + else + { + var view = new PackageDependenciesView + { + Title = title, + Dependencies = treeNodes + }; + MarkoutSerializer.Serialize(view, Console.Out, PackageDependenciesContext.Default); + } return 0; } catch (Exception ex) @@ -324,4 +363,36 @@ private static void BuildNestedNodes(List nodes, ref int target.Add(children.Count > 0 ? new TreeNode(label) { Children = children } : new TreeNode(label)); } } + + /// + /// Writes standalone mermaid output using the MermaidFormatter. + /// + private static void WriteMermaidTree(string title, List treeNodes) + { + var writer = MarkoutWriter.Create(Console.Out, new MermaidFormatter()); + writer.WriteHeading(1, title); + writer.WriteTree([.. treeNodes]); + writer.Flush(); + } + + /// + /// Writes mermaid embedded in a markdown document (```mermaid code block). + /// + private static void WriteEmbeddedMermaidTree(string title, List treeNodes) + { + var mdWriter = MarkoutWriter.Create(Console.Out, new MarkdownFormatter()); + mdWriter.WriteHeading(1, title); + + // Render the mermaid content to a string + var mermaidWriter = MarkoutWriter.Create(new MermaidFormatter()); + mermaidWriter.WriteTree([.. treeNodes]); + var mermaidContent = mermaidWriter.ToString(); + + mdWriter.WriteCodeStart("mermaid"); + Console.Out.Write(mermaidContent); + if (!mermaidContent.EndsWith('\n')) + Console.Out.WriteLine(); + mdWriter.WriteCodeEnd(); + mdWriter.Flush(); + } } diff --git a/src/dotnet-inspect/Options/DependsOptions.cs b/src/dotnet-inspect/Options/DependsOptions.cs index 8dd5ff9..e3cfed1 100644 --- a/src/dotnet-inspect/Options/DependsOptions.cs +++ b/src/dotnet-inspect/Options/DependsOptions.cs @@ -57,6 +57,16 @@ public record DependsOptions : IAssemblySourceOptions /// public bool CompactJson { get; init; } + /// + /// Output as standalone Mermaid diagram. + /// + public bool MermaidOutput { get; init; } + + /// + /// Embed mermaid diagrams in markdown output (--markdown --mermaid). + /// + public bool EmbeddedMermaid { get; init; } + /// /// Show progress messages on stderr. /// diff --git a/src/dotnet-inspect/Options/OutputFormat.cs b/src/dotnet-inspect/Options/OutputFormat.cs index bbd39f5..f8587fb 100644 --- a/src/dotnet-inspect/Options/OutputFormat.cs +++ b/src/dotnet-inspect/Options/OutputFormat.cs @@ -24,7 +24,13 @@ public enum OutputFormat /// /// JSON output. /// - Json + Json, + + /// + /// Standalone Mermaid diagram syntax (graph TD, classDiagram, etc.). + /// Only works for commands that produce graph/tree data. + /// + Mermaid } /// @@ -40,11 +46,13 @@ public static class OutputFormatResolver /// Resolves format. Any -v flag implies Markdown. --json implies Json. --markdown implies Markdown. /// Commands may supply a to override the global default (Markdown). /// - public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity? verbosity, bool plainTextFlag = false, OutputFormat defaultFormat = OutputFormat.Markdown) + public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity? verbosity, bool plainTextFlag = false, bool mermaidFlag = false, OutputFormat defaultFormat = OutputFormat.Markdown) { // Explicit CLI flags win if (jsonFlag) return OutputFormat.Json; + if (mermaidFlag && !markdownFlag) + return OutputFormat.Mermaid; if (markdownFlag) return OutputFormat.Markdown; if (plainTextFlag) @@ -60,6 +68,13 @@ public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity? return defaultFormat; } + /// + /// Returns true when --mermaid is combined with --markdown (embedded mermaid mode). + /// In this mode, the output is still markdown but tree/graph sections render as mermaid code blocks. + /// + public static bool IsEmbeddedMermaid(bool markdownFlag, bool mermaidFlag) + => markdownFlag && mermaidFlag; + /// /// Warns when --oneline is combined with a verbosity that produces multiple sections /// 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 "markdown" or "md" => OutputFormat.Markdown, "plaintext" or "plain-text" or "plain" or "text" => OutputFormat.PlainText, "json" => OutputFormat.Json, + "mermaid" => OutputFormat.Mermaid, _ => null }; _envParsed = true; diff --git a/src/dotnet-inspect/Services/SharedOptions.cs b/src/dotnet-inspect/Services/SharedOptions.cs index b6c1864..aaecd41 100644 --- a/src/dotnet-inspect/Services/SharedOptions.cs +++ b/src/dotnet-inspect/Services/SharedOptions.cs @@ -15,6 +15,7 @@ public class SharedOptions public Option Json { get; } = new("--json") { Description = "Output as JSON" }; public Option Markdown { get; } = new("--markdown") { Description = "Output as markdown" }; public Option PlainText { get; } = new("--plaintext") { Description = "Output as plain text" }; + public Option Mermaid { get; } = new("--mermaid") { Description = "Output as mermaid diagram (standalone or with --markdown for embedded)" }; // Verbosity options public Option Verbose { get; } = new("--verbose") { Description = "Show progress messages on stderr" }; @@ -147,6 +148,7 @@ public void AddAllOptionsTo(Command command) command.Options.Add(Json); command.Options.Add(Markdown); command.Options.Add(PlainText); + command.Options.Add(Mermaid); AddOutputOptionsTo(command); AddSectionOptionsTo(command); AddNuGetOptionsTo(command); @@ -195,11 +197,18 @@ public OutputFormat ResolveFormat(ParseResult parseResult, OutputFormat defaultF bool jsonFlag = parseResult.GetValue(Json); bool markdownFlag = parseResult.GetValue(Markdown); bool plainTextFlag = parseResult.GetValue(PlainText); + bool mermaidFlag = parseResult.GetValue(Mermaid); bool hasVerbosity = parseResult.GetResult(Verbosity) is { Implicit: false }; Verbosity? verbosity = hasVerbosity ? ParseVerbosity(parseResult) : null; - return OutputFormatResolver.Resolve(jsonFlag, markdownFlag, verbosity, plainTextFlag, defaultFormat); + return OutputFormatResolver.Resolve(jsonFlag, markdownFlag, verbosity, plainTextFlag, mermaidFlag, defaultFormat); } + /// + /// Returns true when --mermaid is combined with --markdown (embedded mermaid in markdown). + /// + public bool IsEmbeddedMermaid(ParseResult parseResult) + => OutputFormatResolver.IsEmbeddedMermaid(parseResult.GetValue(Markdown), parseResult.GetValue(Mermaid)); + /// /// Resolves whether oneline output should be used, considering the --oneline flag and format resolution. /// Explicit --oneline always wins; otherwise derived from ResolveFormat. @@ -234,6 +243,7 @@ public bool IsFormatExplicitlySet(ParseResult parseResult, Option? oneLine if (parseResult.GetResult(Json) is { Implicit: false }) return true; if (parseResult.GetResult(Markdown) is { Implicit: false }) return true; if (parseResult.GetResult(PlainText) is { Implicit: false }) return true; + if (parseResult.GetResult(Mermaid) is { Implicit: false }) return true; if (parseResult.GetResult(Verbosity) is { Implicit: false }) return true; return false; }