Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="MarkdownTable.Formatting" Version="0.3.3" />
<PackageVersion Include="Markout" Version="0.11.0" />
<PackageVersion Include="Markout" Version="0.12.0" />
<PackageVersion Include="NuGet.Versioning" Version="7.3.0" />
<PackageVersion Include="NuGetFetch" Version="0.6.1" />
<PackageVersion Include="ShellComplete" Version="0.1.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)
};
Expand Down Expand Up @@ -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)
};
Expand All @@ -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)
};
Expand Down
101 changes: 86 additions & 15 deletions src/dotnet-inspect/Commands/DependsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using DotnetInspector.Services;
using DotnetInspector.Views;
using Markout;
using Markout.Formatting;

namespace DotnetInspector.Commands;

Expand Down Expand Up @@ -68,12 +69,25 @@ public static async Task<int> 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;
Expand Down Expand Up @@ -159,12 +173,23 @@ public static async Task<int> 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)
Expand Down Expand Up @@ -253,12 +278,26 @@ public static async Task<int> 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)
Expand Down Expand Up @@ -324,4 +363,36 @@ private static void BuildNestedNodes(List<AssemblyReferenceNode> nodes, ref int
target.Add(children.Count > 0 ? new TreeNode(label) { Children = children } : new TreeNode(label));
}
}

/// <summary>
/// Writes standalone mermaid output using the MermaidFormatter.
/// </summary>
private static void WriteMermaidTree(string title, List<TreeNode> treeNodes)
{
var writer = MarkoutWriter.Create(Console.Out, new MermaidFormatter());
writer.WriteHeading(1, title);
writer.WriteTree([.. treeNodes]);
writer.Flush();
}

/// <summary>
/// Writes mermaid embedded in a markdown document (```mermaid code block).
/// </summary>
private static void WriteEmbeddedMermaidTree(string title, List<TreeNode> 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();
}
}
10 changes: 10 additions & 0 deletions src/dotnet-inspect/Options/DependsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public record DependsOptions : IAssemblySourceOptions
/// </summary>
public bool CompactJson { get; init; }

/// <summary>
/// Output as standalone Mermaid diagram.
/// </summary>
public bool MermaidOutput { get; init; }

/// <summary>
/// Embed mermaid diagrams in markdown output (--markdown --mermaid).
/// </summary>
public bool EmbeddedMermaid { get; init; }

/// <summary>
/// Show progress messages on stderr.
/// </summary>
Expand Down
20 changes: 18 additions & 2 deletions src/dotnet-inspect/Options/OutputFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ public enum OutputFormat
/// <summary>
/// JSON output.
/// </summary>
Json
Json,

/// <summary>
/// Standalone Mermaid diagram syntax (graph TD, classDiagram, etc.).
/// Only works for commands that produce graph/tree data.
/// </summary>
Mermaid
}

/// <summary>
Expand All @@ -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 <paramref name="defaultFormat"/> to override the global default (Markdown).
/// </summary>
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)
Expand All @@ -60,6 +68,13 @@ public static OutputFormat Resolve(bool jsonFlag, bool markdownFlag, Verbosity?
return defaultFormat;
}

/// <summary>
/// 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.
/// </summary>
public static bool IsEmbeddedMermaid(bool markdownFlag, bool mermaidFlag)
=> markdownFlag && mermaidFlag;

/// <summary>
/// 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.
Expand All @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion src/dotnet-inspect/Services/SharedOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class SharedOptions
public Option<bool> Json { get; } = new("--json") { Description = "Output as JSON" };
public Option<bool> Markdown { get; } = new("--markdown") { Description = "Output as markdown" };
public Option<bool> PlainText { get; } = new("--plaintext") { Description = "Output as plain text" };
public Option<bool> Mermaid { get; } = new("--mermaid") { Description = "Output as mermaid diagram (standalone or with --markdown for embedded)" };

// Verbosity options
public Option<bool> Verbose { get; } = new("--verbose") { Description = "Show progress messages on stderr" };
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

/// <summary>
/// Returns true when --mermaid is combined with --markdown (embedded mermaid in markdown).
/// </summary>
public bool IsEmbeddedMermaid(ParseResult parseResult)
=> OutputFormatResolver.IsEmbeddedMermaid(parseResult.GetValue(Markdown), parseResult.GetValue(Mermaid));

/// <summary>
/// Resolves whether oneline output should be used, considering the --oneline flag and format resolution.
/// Explicit --oneline always wins; otherwise derived from ResolveFormat.
Expand Down Expand Up @@ -234,6 +243,7 @@ public bool IsFormatExplicitlySet(ParseResult parseResult, Option<bool>? 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;
}
Expand Down