Browse Source

Generate Mermaid HTML diagrammer from input assembly via ilspycmd (#3324)

* added mermaid class diagrammer

contributed from https://github.com/h0lg/netAmermaid - find earlier git history there

* reading from embedded resource instead of file

* swapped out icon to brand diagrammers as an ILSpy product

reusing linked ..\ILSpy\Images\ILSpy.ico from UI project

* added required ilspycmd options and routed call

* adjusted VS Code task to generate model.json required by the JS/CSS/HTML dev loop

* added debug launchSettings

* updated help command output

* using ILSpyX build info in generated diagrammers
removing unused code

* using explicit type where it's not obvious

* outputting in to a folder next to and named after the input assembly + " diagrammer" by default

* renamed diagrammer output to index.html

to support default web server configs in the wild

* improved instructions for creating an off-line diagrammer

* added developer-facing doco for how to edit the HTML/JS/CSS parts

* renamed to remove netAmermaid branding

* updated repo URL and doco link to new Wiki page

* copied over doco

* removed obsolete parts

* moved CLI doco into ILSpyCmd README

* removed end-user facing chapters that go into the Wiki from dev-facing doco

* updated to ilspycmd API and rebranded to ILSpy

* removed doco that's now in https://github.com/icsharpcode/ILSpy/wiki/Diagramming

* added tasks
pull/3335/head
Holger Schmidt 6 months ago committed by GitHub
parent
commit
09ed31d391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 68
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  2. 29
      ICSharpCode.ILSpyCmd/Properties/launchSettings.json
  3. 183
      ICSharpCode.ILSpyCmd/README.md
  4. 16
      ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj
  5. 114
      ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs
  6. 99
      ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs
  7. 56
      ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs
  8. 55
      ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs
  9. 61
      ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs
  10. 118
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs
  11. 88
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs
  12. 124
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs
  13. 84
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs
  14. 74
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs
  15. 45
      ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs
  16. 146
      ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs
  17. 13
      ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md
  18. 91
      ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs
  19. 16
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js
  20. 4
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore
  21. 54
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json
  22. 10
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt
  23. 66
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js
  24. 7
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json
  25. 1137
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js
  26. 453
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css
  27. 586
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less
  28. 194
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html

68
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -18,6 +18,7 @@ using ICSharpCode.Decompiler.Disassembler; @@ -18,6 +18,7 @@ using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Solution;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer;
using ICSharpCode.ILSpyX.PdbProvider;
using McMaster.Extensions.CommandLineUtils;
@ -44,6 +45,13 @@ Examples: @@ -44,6 +45,13 @@ Examples:
Decompile assembly to destination directory, create a project file, one source file per type,
into nicely nested directories.
ilspycmd --nested-directories -p -o c:\decompiled sample.dll
Generate a HTML diagrammer containing all type info into a folder next to the input assembly
ilspycmd sample.dll --generate-diagrammer
Generate a HTML diagrammer containing filtered type info into a custom output folder
(including types in the LightJson namespace while excluding types in nested LightJson.Serialization namespace)
ilspycmd sample.dll --generate-diagrammer -o c:\diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+
")]
[HelpOption("-h|--help")]
[ProjectOptionRequiresOutputDirectoryValidation]
@ -114,6 +122,46 @@ Examples: @@ -114,6 +122,46 @@ Examples:
[Option("--disable-updatecheck", "If using ilspycmd in a tight loop or fully automated scenario, you might want to disable the automatic update check.", CommandOptionType.NoValue)]
public bool DisableUpdateCheck { get; }
#region MermaidDiagrammer options
// reused or quoted commands
private const string generateDiagrammerCmd = "--generate-diagrammer",
exclude = generateDiagrammerCmd + "-exclude",
include = generateDiagrammerCmd + "-include";
[Option(generateDiagrammerCmd, "Generates an interactive HTML diagrammer app from selected types in the target assembly" +
" - to the --outputdir or in a 'diagrammer' folder next to to the assembly by default.", CommandOptionType.NoValue)]
public bool GenerateDiagrammer { get; }
[Option(include, "An optional regular expression matching Type.FullName used to whitelist types to include in the generated diagrammer.", CommandOptionType.SingleValue)]
public string Include { get; set; }
[Option(exclude, "An optional regular expression matching Type.FullName used to blacklist types to exclude from the generated diagrammer.", CommandOptionType.SingleValue)]
public string Exclude { get; set; }
[Option(generateDiagrammerCmd + "-report-excluded", "Outputs a report of types excluded from the generated diagrammer" +
$" - whether by default because compiler-generated, explicitly by '{exclude}' or implicitly by '{include}'." +
" You may find this useful to develop and debug your regular expressions.", CommandOptionType.NoValue)]
public bool ReportExludedTypes { get; set; }
[Option(generateDiagrammerCmd + "-docs", "The path or file:// URI of the XML file containing the target assembly's documentation comments." +
" You only need to set this if a) you want your diagrams annotated with them and b) the file name differs from that of the assmbly." +
" To enable XML documentation output for your assmbly, see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output",
CommandOptionType.SingleValue)]
public string XmlDocs { get; set; }
/// <inheritdoc cref="ILSpyX.MermaidDiagrammer.GenerateHtmlDiagrammer.StrippedNamespaces" />
[Option(generateDiagrammerCmd + "-strip-namespaces", "Optional space-separated namespace names that are removed for brevity from XML documentation comments." +
" Note that the order matters: e.g. replace 'System.Collections' before 'System' to remove both of them completely.", CommandOptionType.MultipleValue)]
public string[] StrippedNamespaces { get; set; }
[Option(generateDiagrammerCmd + "-json-only",
"Whether to generate a model.json file instead of baking it into the HTML template." +
" This is useful for the HTML/JS/CSS development loop.", CommandOptionType.NoValue,
ShowInHelpText = false)] // developer option, output is really only useful in combination with the corresponding task in html/gulpfile.js
public bool JsonOnly { get; set; }
#endregion
private readonly IHostEnvironment _env;
public ILSpyCmdProgram(IHostEnvironment env)
{
@ -157,6 +205,26 @@ Examples: @@ -157,6 +205,26 @@ Examples:
SolutionCreator.WriteSolutionFile(Path.Combine(outputDirectory, Path.GetFileNameWithoutExtension(outputDirectory) + ".sln"), projects);
return 0;
}
else if (GenerateDiagrammer)
{
foreach (var file in InputAssemblyNames)
{
var command = new GenerateHtmlDiagrammer {
Assembly = file,
OutputFolder = OutputDirectory,
Include = Include,
Exclude = Exclude,
ReportExludedTypes = ReportExludedTypes,
JsonOnly = JsonOnly,
XmlDocs = XmlDocs,
StrippedNamespaces = StrippedNamespaces
};
command.Run();
}
return 0;
}
else
{
foreach (var file in InputAssemblyNames)

29
ICSharpCode.ILSpyCmd/Properties/launchSettings.json

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
{
"profiles": {
"no args": {
"commandName": "Project",
"commandLineArgs": ""
},
"print help": {
"commandName": "Project",
"commandLineArgs": "--help"
},
"generate diagrammer": {
"commandName": "Project",
// containing all types
// full diagrammer (~6.3 Mb!)
//"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer"
// including types in LightJson namespace while excluding types in nested LightJson.Serialization namespace, matched by what returns System.Type.FullName
//"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+"
// including types in Decompiler.TypeSystem namespace while excluding types in nested Decompiler.TypeSystem.Implementation namespace
"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-include Decompiler\\.TypeSystem\\..+ --generate-diagrammer-exclude Decompiler\\.TypeSystem\\.Implementation\\..+"
},
"generate diagrammer model.json": {
"commandName": "Project",
"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-json-only"
}
}
}

183
ICSharpCode.ILSpyCmd/README.md

@ -9,39 +9,65 @@ dotnet tool install --global ilspycmd @@ -9,39 +9,65 @@ dotnet tool install --global ilspycmd
Help output (`ilspycmd --help`):
```
ilspycmd: 8.2.0.7535
ICSharpCode.Decompiler: 8.2.0.7535
ilspycmd: 9.0.0.7847
ICSharpCode.Decompiler: 9.0.0.7847
dotnet tool for decompiling .NET assemblies and generating portable PDBs
Usage: ilspycmd [options] <Assembly file name(s)>
Arguments:
Assembly file name(s) The list of assemblies that is being decompiled. This argument is mandatory.
Assembly file name(s) The list of assemblies that is being decompiled. This argument is mandatory.
Options:
-v|--version Show version of ICSharpCode.Decompiler used.
-h|--help Show help information.
-o|--outputdir <directory> The output directory, if omitted decompiler output is written to standard out.
-p|--project Decompile assembly as compilable project. This requires the output directory option.
-t|--type <type-name> The fully qualified name of the type to decompile.
-il|--ilcode Show IL code.
--il-sequence-points Show IL with sequence points. Implies -il.
-genpdb|--generate-pdb Generate PDB.
-usepdb|--use-varnames-from-pdb Use variable names from PDB.
-l|--list <entity-type(s)> Lists all entities of the specified type(s). Valid types: c(lass), i(nterface), s(truct), d(elegate), e(num)
-lv|--languageversion <version> C# Language version: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6, CSharp7, CSharp7_1, CSharp7_2,
CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0, Preview or Latest
Allowed values are: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6, CSharp7, CSharp7_1, CSharp7_2,
CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0, CSharp11_0, Preview, Latest.
Default value is: Latest.
-r|--referencepath <path> Path to a directory containing dependencies of the assembly that is being decompiled.
--no-dead-code Remove dead code.
--no-dead-stores Remove dead stores.
-d|--dump-package Dump package assemblies into a folder. This requires the output directory option.
--nested-directories Use nested directories for namespaces.
--disable-updatecheck If using ilspycmd in a tight loop or fully automated scenario, you might want to disable the automatic update
check.
-v|--version Show version of ICSharpCode.Decompiler used.
-h|--help Show help information.
-o|--outputdir <directory> The output directory, if omitted decompiler output is written to standard out.
-p|--project Decompile assembly as compilable project. This requires the output directory
option.
-t|--type <type-name> The fully qualified name of the type to decompile.
-il|--ilcode Show IL code.
--il-sequence-points Show IL with sequence points. Implies -il.
-genpdb|--generate-pdb Generate PDB.
-usepdb|--use-varnames-from-pdb Use variable names from PDB.
-l|--list <entity-type(s)> Lists all entities of the specified type(s). Valid types: c(lass),
i(nterface), s(truct), d(elegate), e(num)
-lv|--languageversion <version> C# Language version: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6,
CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0,
Preview or Latest
Allowed values are: CSharp1, CSharp2, CSharp3, CSharp4, CSharp5, CSharp6,
CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, CSharp10_0,
CSharp11_0, Preview, CSharp12_0, Latest.
Default value is: Latest.
-r|--referencepath <path> Path to a directory containing dependencies of the assembly that is being
decompiled.
--no-dead-code Remove dead code.
--no-dead-stores Remove dead stores.
-d|--dump-package Dump package assemblies into a folder. This requires the output directory
option.
--nested-directories Use nested directories for namespaces.
--disable-updatecheck If using ilspycmd in a tight loop or fully automated scenario, you might want
to disable the automatic update check.
--generate-diagrammer Generates an interactive HTML diagrammer app from selected types in the target
assembly - to the --outputdir or in a 'diagrammer' folder next to to the
assembly by default.
--generate-diagrammer-include An optional regular expression matching Type.FullName used to whitelist types
to include in the generated diagrammer.
--generate-diagrammer-exclude An optional regular expression matching Type.FullName used to blacklist types
to exclude from the generated diagrammer.
--generate-diagrammer-report-excluded Outputs a report of types excluded from the generated diagrammer - whether by
default because compiler-generated, explicitly by
'--generate-diagrammer-exclude' or implicitly by
'--generate-diagrammer-include'. You may find this useful to develop and debug
your regular expressions.
--generate-diagrammer-docs The path or file:// URI of the XML file containing the target assembly's
documentation comments. You only need to set this if a) you want your diagrams
annotated with them and b) the file name differs from that of the assmbly. To
enable XML documentation output for your assmbly, see
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output
--generate-diagrammer-strip-namespaces Optional space-separated namespace names that are removed for brevity from XML
documentation comments. Note that the order matters: e.g. replace
'System.Collections' before 'System' to remove both of them completely.
Remarks:
-o is valid with every option and required when using -p.
@ -59,4 +85,111 @@ Examples: @@ -59,4 +85,111 @@ Examples:
Decompile assembly to destination directory, create a project file, one source file per type,
into nicely nested directories.
ilspycmd --nested-directories -p -o c:\decompiled sample.dll
Generate a HTML diagrammer containing all type info into a folder next to the input assembly
ilspycmd sample.dll --generate-diagrammer
Generate a HTML diagrammer containing filtered type info into a custom output folder
(including types in the LightJson namespace while excluding types in nested LightJson.Serialization namespace)
ilspycmd sample.dll --generate-diagrammer -o c:\diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+
```
## Generate HTML diagrammers
Once you have an output folder in mind, you can adopt either of the following strategies
to generate a HTML diagrammer from a .Net assembly using the console app.
### Manually before use
**Create the output folder** in your location of choice and inside it **a new shell script**.
Using the CMD shell in a Windows environment for example, you'd create a `regenerate.cmd` looking somewhat like this:
<pre>
..\..\path\to\ilspycmd.exe ..\path\to\your\assembly.dll --generate-diagrammer --outputdir .
</pre>
With this script in place, run it to (re-)generate the HTML diagrammer at your leisure. Note that `--outputdir .` directs the output to the current directory.
### Automatically
If you want to deploy an up-to-date HTML diagrammer as part of your live documentation,
you'll want to automate its regeneration to keep it in sync with your code base.
For example, you might like to share the diagrammer on a web server or - in general - with users
who cannot or may not regenerate it; lacking either access to the ilspycmd console app or permission to use it.
In such cases, you can dangle the regeneration off the end of either your build or deployment pipeline.
Note that the macros used here apply to [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild) for [Visual Studio](https://learn.microsoft.com/en-us/visualstudio/ide/reference/pre-build-event-post-build-event-command-line-dialog-box) and your mileage may vary with VS for Mac or VS Code.
#### After building
To regenerate the HTML diagrammer from your output assembly after building,
add something like the following to your project file.
Note that the `Condition` here is optional and configures this step to only run after `Release` builds.
```xml
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release'">
<Exec Command="$(SolutionDir)..\path\to\ilspycmd.exe $(TargetPath) --generate-diagrammer --outputdir $(ProjectDir)diagrammer" />
</Target>
```
#### After publishing
If you'd rather regenerate the diagram after publishing instead of building, all you have to do is change the `AfterTargets` to `Publish`.
Note that the `Target` `Name` doesn't matter here and that the diagrammer is generated into a folder in the `PublishDir` instead of the `ProjectDir`.
```xml
<Target Name="GenerateHtmlDiagrammer" AfterTargets="Publish">
<Exec Command="$(SolutionDir)..\path\to\ilspycmd.exe $(TargetPath) --generate-diagrammer --outputdir $(PublishDir)diagrammer" />
</Target>
```
### Usage tips
**Compiler-generated** types and their nested types are **excluded by default**.
Consider sussing out **big source assemblies** using [ILSpy](https://github.com/icsharpcode/ILSpy) first to get an idea about which subdomains to include in your diagrammers. Otherwise you may experience long build times and large file sizes for the diagrammer as well as a looong type selection opening it. At some point, mermaid may refuse to render all types in your selection because their definitions exceed the maximum input size. If that's where you find yourself, you may want to consider
- using `--generate-diagrammer-include` and `--generate-diagrammer-exclude` to **limit the scope of the individual diagrammer to a certain subdomain**
- generating **multiple diagrammers for different subdomains**.
### Advanced configuration examples
Above examples show how the most important options are used. Let's have a quick look at the remaining ones, which allow for customization in your project setup and diagrams.
#### Filter extracted types
Sometimes the source assembly contains way more types than are sensible to diagram. Types with metadata for validation or mapping for example. Or auto-generated types.
Especially if you want to tailor a diagrammer for a certain target audience and hide away most of the supporting type system to avoid noise and unnecessary questions.
In these scenarios you can supply Regular Expressions for types to `--generate-diagrammer-include` (white-list) and `--generate-diagrammer-exclude` (black-list).
A third option `--generate-diagrammer-report-excluded` will output a `.txt` containing the list of effectively excluded types next to the HTML diagrammer containing the effectively included types.
<pre>
ilspycmd.exe <b>--generate-diagrammer-include Your\.Models\..+ --generate-diagrammer-exclude .+\+Metadata|.+\.Data\..+Map --generate-diagrammer-report-excluded</b> ..\path\to\your\assembly.dll --generate-diagrammer --outputdir .
</pre>
This example
- includes all types in the top-level namespace `Your.Models`
- while excluding
- nested types called `Metadata` and
- types ending in `Map` in descendant `.Data.` namespaces.
#### Strip namespaces from XML comments
You can reduce the noise in the XML documentation comments on classes on your diagrams by supplying a space-separated list of namespaces to omit from the output like so:
<pre>
ilspycmd.exe <b>--generate-diagrammer-strip-namespaces System.Collections.Generic System</b> ..\path\to\your\assembly.dll --generate-diagrammer --output-folder .
</pre>
Note how `System` is replaced **after** other namespaces starting with `System.` to achieve complete removal.
Otherwise `System.Collections.Generic` wouldn't match the `Collections.Generic` left over after removing `System.`, resulting in partial removal only.
#### Adjust for custom XML documentation file names
If - for whatever reason - you have customized your XML documentation file output name, you can specify a custom path to pick it up from.
<pre>
ilspycmd.exe <b>--generate-diagrammer-docs ..\path\to\your\docs.xml</b> ..\path\to\your\assembly.dll --generate-diagrammer --output-folder .
</pre>

16
ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj

@ -65,6 +65,22 @@ @@ -65,6 +65,22 @@
</GetPackageVersionDependsOn>
</PropertyGroup>
<ItemGroup>
<None Remove="MermaidDiagrammer\html\node_modules\**" />
<None Remove="MermaidDiagrammer\html\.eslintrc.js" />
<None Remove="MermaidDiagrammer\html\.gitignore" />
<None Remove="MermaidDiagrammer\html\class-diagrammer.html" />
<None Remove="MermaidDiagrammer\html\gulpfile.js" />
<None Remove="MermaidDiagrammer\html\model.json" />
<None Remove="MermaidDiagrammer\html\package-lock.json" />
<None Remove="MermaidDiagrammer\html\package.json" />
<None Remove="MermaidDiagrammer\html\styles.less" />
<EmbeddedResource Include="..\ILSpy\Images\ILSpy.ico" Link="MermaidDiagrammer\html\ILSpy.ico" />
<EmbeddedResource Include="MermaidDiagrammer\html\script.js" />
<EmbeddedResource Include="MermaidDiagrammer\html\styles.css" />
<EmbeddedResource Include="MermaidDiagrammer\html\template.html" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Composition.AttributedModel" />
<PackageReference Include="System.Reflection.Metadata" />

114
ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
/// <summary>Contains type info and metadata for generating a HTML class diagrammer from a source assembly.
/// Serialized into JSON by <see cref="GenerateHtmlDiagrammer.SerializeModel(ClassDiagrammer)"/>.</summary>
public sealed class ClassDiagrammer
{
internal const string NewLine = "\n";
internal string SourceAssemblyName { get; set; } = null!;
internal string SourceAssemblyVersion { get; set; } = null!;
/// <summary>Types selectable in the diagrammer, grouped by their
/// <see cref="System.Type.Namespace"/> to facilitate a structured type selection.</summary>
internal Dictionary<string, Type[]> TypesByNamespace { get; set; } = null!;
/// <summary>Types not included in the <see cref="ClassDiagrammer"/>,
/// but referenced by <see cref="Type"/>s that are.
/// Contains display names (values; similar to <see cref="Type.Name"/>)
/// by their referenced IDs (keys; similar to <see cref="Type.Id"/>).</summary>
internal Dictionary<string, string> OutsideReferences { get; set; } = null!;
/// <summary>Types excluded from the <see cref="ClassDiagrammer"/>;
/// used to support <see cref="GenerateHtmlDiagrammer.ReportExludedTypes"/>.</summary>
internal string[] Excluded { get; set; } = null!;
/// <summary>A <see cref="Type"/>-like structure with collections
/// of property relations to one or many other <see cref="Type"/>s.</summary>
public abstract class Relationships
{
/// <summary>Relations to zero or one other instances of <see cref="Type"/>s included in the <see cref="ClassDiagrammer"/>,
/// with the display member names as keys and the related <see cref="Type.Id"/> as values.
/// This is because member names must be unique within the owning <see cref="Type"/>,
/// while the related <see cref="Type"/> may be the same for multiple properties.</summary>
public Dictionary<string, string>? HasOne { get; set; }
/// <summary>Relations to zero to infinite other instances of <see cref="Type"/>s included in the <see cref="ClassDiagrammer"/>,
/// with the display member names as keys and the related <see cref="Type.Id"/> as values.
/// This is because member names must be unique within the owning <see cref="Type"/>,
/// while the related <see cref="Type"/> may be the same for multiple properties.</summary>
public Dictionary<string, string>? HasMany { get; set; }
}
/// <summary>The mermaid class diagram definition, inheritance and relationships metadata
/// and XML documentation for a <see cref="System.Type"/> from the source assembly.</summary>
public sealed class Type : Relationships
{
/// <summary>Uniquely identifies the <see cref="System.Type"/> in the scope of the source assembly
/// as well as any HTML diagrammer generated from it.
/// Should match \w+ to be safe to use as select option value and
/// part of the DOM id of the SVG node rendered for this type.
/// May be the type name itself.</summary>
internal string Id { get; set; } = null!;
/// <summary>The human-readable label for the type, if different from <see cref="Id"/>.
/// Not guaranteed to be unique in the scope of the <see cref="ClassDiagrammer"/>.</summary>
public string? Name { get; set; }
/// <summary>Contains the definition of the type and its own (not inherited) flat members
/// in mermaid class diagram syntax, see https://mermaid.js.org/syntax/classDiagram.html .</summary>
public string Body { get; set; } = null!;
/// <summary>The base type directly implemented by this type, with the <see cref="Id"/> as key
/// and the (optional) differing display name as value of the single entry
/// - or null if the base type is <see cref="object"/>.
/// Yes, Christopher Lambert, there can only be one. For now.
/// But using the same interface as for <see cref="Interfaces"/> is convenient
/// and who knows - at some point the .Net bus may roll up with multi-inheritance.
/// Then this'll look visionary!</summary>
public Dictionary<string, string?>? BaseType { get; set; }
/// <summary>Interfaces directly implemented by this type, with their <see cref="Id"/> as keys
/// and their (optional) differing display names as values.</summary>
public Dictionary<string, string?[]>? Interfaces { get; set; }
/// <summary>Contains inherited members by the <see cref="Id"/> of their <see cref="IMember.DeclaringType"/>
/// for the consumer to choose which of them to display in an inheritance scenario.</summary>
public IDictionary<string, InheritedMembers>? Inherited { get; set; }
/// <summary>Contains the XML documentation comments for this type
/// (using a <see cref="string.Empty"/> key) and its members, if available.</summary>
public IDictionary<string, string>? XmlDocs { get; set; }
/// <summary>Members inherited from an ancestor type specified by the Key of <see cref="Inherited"/>.</summary>
public class InheritedMembers : Relationships
{
/// <summary>The simple, non-complex members inherited from another <see cref="Type"/>
/// in mermaid class diagram syntax.</summary>
public string? FlatMembers { get; set; }
}
}
}
}

99
ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
using CD = ClassDiagrammer;
/* See class diagram syntax
* reference (may be outdated!) https://mermaid.js.org/syntax/classDiagram.html
* lexical definition https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/diagrams/class/parser/classDiagram.jison */
/// <summary>Produces mermaid class diagram syntax for a filtered list of types from a specified .Net assembly.</summary>
public partial class ClassDiagrammerFactory
{
private readonly XmlDocumentationFormatter? xmlDocs;
private readonly DecompilerSettings decompilerSettings;
private ITypeDefinition[]? selectedTypes;
private Dictionary<IType, string>? uniqueIds;
private Dictionary<IType, string>? labels;
private Dictionary<string, string>? outsideReferences;
public ClassDiagrammerFactory(XmlDocumentationFormatter? xmlDocs)
{
this.xmlDocs = xmlDocs;
//TODO not sure LanguageVersion.Latest is the wisest choice here; maybe cap this for better mermaid compatibility?
decompilerSettings = new DecompilerSettings(Decompiler.CSharp.LanguageVersion.Latest) {
AutomaticProperties = true // for IsHidden to return true for backing fields
};
}
public CD BuildModel(string assemblyPath, string? include, string? exclude)
{
CSharpDecompiler decompiler = new(assemblyPath, decompilerSettings);
MetadataModule mainModule = decompiler.TypeSystem.MainModule;
IEnumerable<ITypeDefinition> allTypes = mainModule.TypeDefinitions;
selectedTypes = FilterTypes(allTypes,
include == null ? null : new Regex(include, RegexOptions.Compiled),
exclude == null ? null : new Regex(exclude, RegexOptions.Compiled)).ToArray();
// generate dictionary to read names from later
uniqueIds = GenerateUniqueIds(selectedTypes);
labels = [];
outsideReferences = [];
Dictionary<string, CD.Type[]> typesByNamespace = selectedTypes.GroupBy(t => t.Namespace).OrderBy(g => g.Key).ToDictionary(g => g.Key,
ns => ns.OrderBy(t => t.FullName).Select(type => type.Kind == TypeKind.Enum ? BuildEnum(type) : BuildType(type)).ToArray());
string[] excluded = allTypes.Except(selectedTypes).Select(t => t.ReflectionName).ToArray();
return new CD {
SourceAssemblyName = mainModule.AssemblyName,
SourceAssemblyVersion = mainModule.AssemblyVersion.ToString(),
TypesByNamespace = typesByNamespace,
OutsideReferences = outsideReferences,
Excluded = excluded
};
}
/// <summary>The default strategy for pre-filtering the <paramref name="typeDefinitions"/> available in the HTML diagrammer.
/// Applies <see cref="IsIncludedByDefault(ITypeDefinition)"/> as well as
/// matching by <paramref name="include"/> and not by <paramref name="exclude"/>.</summary>
/// <returns>The types to effectively include in the HTML diagrammer.</returns>
protected virtual IEnumerable<ITypeDefinition> FilterTypes(IEnumerable<ITypeDefinition> typeDefinitions, Regex? include, Regex? exclude)
=> typeDefinitions.Where(type => IsIncludedByDefault(type)
&& (include?.IsMatch(type.ReflectionName) != false) // applying optional whitelist filter
&& (exclude?.IsMatch(type.ReflectionName) != true)); // applying optional blacklist filter
/// <summary>The strategy for deciding whether a <paramref name="type"/> should be included
/// in the HTML diagrammer by default. Excludes compiler-generated and their nested types.</summary>
protected virtual bool IsIncludedByDefault(ITypeDefinition type)
=> !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass();
}
}

56
ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.IO;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
public partial class GenerateHtmlDiagrammer
{
/// <summary>A helper for loading resources embedded in the nested html folder.</summary>
private static class EmbeddedResource
{
internal static string ReadText(string resourceName)
{
Stream stream = GetStream(resourceName);
using StreamReader reader = new(stream);
return reader.ReadToEnd();
}
internal static void CopyTo(string outputFolder, string resourceName)
{
Stream resourceStream = GetStream(resourceName);
using FileStream output = new(Path.Combine(outputFolder, resourceName), FileMode.Create, FileAccess.Write);
resourceStream.CopyTo(output);
}
private static Stream GetStream(string resourceName)
{
var type = typeof(EmbeddedResource);
var assembly = type.Assembly;
var fullResourceName = $"{type.Namespace}.html.{resourceName}";
Stream? stream = assembly.GetManifestResourceStream(fullResourceName);
if (stream == null)
throw new FileNotFoundException("Resource not found.", fullResourceName);
return stream;
}
}
}
}

55
ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions
{
internal static class StringExtensions
{
/// <summary>Replaces all consecutive horizontal white space characters in
/// <paramref name="input"/> with <paramref name="normalizeTo"/> while leaving line breaks intact.</summary>
internal static string NormalizeHorizontalWhiteSpace(this string input, string normalizeTo = " ")
=> Regex.Replace(input, @"[ \t]+", normalizeTo);
/// <summary>Replaces all occurrences of <paramref name="oldValues"/> in
/// <paramref name="input"/> with <paramref name="newValue"/>.</summary>
internal static string ReplaceAll(this string input, IEnumerable<string> oldValues, string? newValue)
=> oldValues.Aggregate(input, (aggregate, oldValue) => aggregate.Replace(oldValue, newValue));
/// <summary>Joins the specified <paramref name="strings"/> to a single one
/// using the specified <paramref name="separator"/> as a delimiter.</summary>
/// <param name="pad">Whether to pad the start and end of the string with the <paramref name="separator"/> as well.</param>
internal static string Join(this IEnumerable<string?>? strings, string separator, bool pad = false)
{
if (strings == null)
return string.Empty;
var joined = string.Join(separator, strings);
return pad ? string.Concat(separator, joined, separator) : joined;
}
/// <summary>Formats all items in <paramref name="collection"/> using the supplied <paramref name="format"/> strategy
/// and returns a string collection - even if the incoming <paramref name="collection"/> is null.</summary>
internal static IEnumerable<string> FormatAll<T>(this IEnumerable<T>? collection, Func<T, string> format)
=> collection?.Select(format) ?? [];
}
}

61
ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions
{
internal static class TypeExtensions
{
internal static bool IsObject(this IType t) => t.IsKnownType(KnownTypeCode.Object);
internal static bool IsInterface(this IType t) => t.Kind == TypeKind.Interface;
internal static bool TryGetNullableType(this IType type, [MaybeNullWhen(false)] out IType typeArg)
{
bool isNullable = type.IsKnownType(KnownTypeCode.NullableOfT);
typeArg = isNullable ? type.TypeArguments.Single() : null;
return isNullable;
}
}
internal static class MemberInfoExtensions
{
/// <summary>Groups the <paramref name="members"/> into a dictionary
/// with <see cref="IMember.DeclaringType"/> keys.</summary>
internal static Dictionary<IType, T[]> GroupByDeclaringType<T>(this IEnumerable<T> members) where T : IMember
=> members.GroupByDeclaringType(m => m);
/// <summary>Groups the <paramref name="objectsWithMembers"/> into a dictionary
/// with <see cref="IMember.DeclaringType"/> keys using <paramref name="getMember"/>.</summary>
internal static Dictionary<IType, T[]> GroupByDeclaringType<T>(this IEnumerable<T> objectsWithMembers, Func<T, IMember> getMember)
=> objectsWithMembers.GroupBy(m => getMember(m).DeclaringType).ToDictionary(g => g.Key, g => g.ToArray());
}
internal static class DictionaryExtensions
{
/// <summary>Returns the <paramref name="dictionary"/>s value for the specified <paramref name="key"/>
/// if available and otherwise the default for <typeparamref name="Tout"/>.</summary>
internal static Tout? GetValue<T, Tout>(this IDictionary<T, Tout> dictionary, T key)
=> dictionary.TryGetValue(key, out Tout? value) ? value : default;
}
}

118
ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
using CD = ClassDiagrammer;
partial class ClassDiagrammerFactory
{
private CD.Type BuildEnum(ITypeDefinition type)
{
IField[] fields = type.GetFields(f => f.IsConst && f.IsStatic && f.Accessibility == Accessibility.Public).ToArray();
Dictionary<string, string>? docs = xmlDocs?.GetXmlDocs(type, fields);
string name = GetName(type), typeId = GetId(type);
var body = fields.Select(f => f.Name).Prepend("<<Enumeration>>")
.Join(CD.NewLine + " ", pad: true).TrimEnd(' ');
return new CD.Type {
Id = typeId,
Name = name == typeId ? null : name,
Body = $"class {typeId} {{{body}}}",
XmlDocs = docs
};
}
private CD.Type BuildType(ITypeDefinition type)
{
string typeId = GetId(type);
IMethod[] methods = GetMethods(type).ToArray();
IProperty[] properties = type.GetProperties().ToArray();
IProperty[] hasOneRelations = GetHasOneRelations(properties);
(IProperty property, IType elementType)[] hasManyRelations = GetManyRelations(properties);
var propertyNames = properties.Select(p => p.Name).ToArray();
IField[] fields = GetFields(type, properties);
#region split members up by declaring type
// enables the diagrammer to exclude inherited members from derived types if they are already rendered in a base type
Dictionary<IType, IProperty[]> flatPropertiesByType = properties.Except(hasOneRelations)
.Except(hasManyRelations.Select(r => r.property)).GroupByDeclaringType();
Dictionary<IType, IProperty[]> hasOneRelationsByType = hasOneRelations.GroupByDeclaringType();
Dictionary<IType, (IProperty property, IType elementType)[]> hasManyRelationsByType = hasManyRelations.GroupByDeclaringType(r => r.property);
Dictionary<IType, IField[]> fieldsByType = fields.GroupByDeclaringType();
Dictionary<IType, IMethod[]> methodsByType = methods.GroupByDeclaringType();
#endregion
#region build diagram definitions for the type itself and members declared by it
string members = flatPropertiesByType.GetValue(type).FormatAll(FormatFlatProperty)
.Concat(methodsByType.GetValue(type).FormatAll(FormatMethod))
.Concat(fieldsByType.GetValue(type).FormatAll(FormatField))
.Join(CD.NewLine + " ", pad: true);
// see https://mermaid.js.org/syntax/classDiagram.html#annotations-on-classes
string? annotation = type.IsInterface() ? "Interface" : type.IsAbstract ? type.IsSealed ? "Service" : "Abstract" : null;
string body = annotation == null ? members.TrimEnd(' ') : members + $"<<{annotation}>>" + CD.NewLine;
#endregion
Dictionary<string, string>? docs = xmlDocs?.GetXmlDocs(type, fields, properties, methods);
#region build diagram definitions for inherited members by declaring type
string explicitTypePrefix = typeId + " : ";
// get ancestor types this one is inheriting members from
Dictionary<string, CD.Type.InheritedMembers> inheritedMembersByType = type.GetNonInterfaceBaseTypes().Where(t => t != type && !t.IsObject())
// and group inherited members by declaring type
.ToDictionary(GetId, t => {
IEnumerable<string> flatMembers = flatPropertiesByType.GetValue(t).FormatAll(p => explicitTypePrefix + FormatFlatProperty(p))
.Concat(methodsByType.GetValue(t).FormatAll(m => explicitTypePrefix + FormatMethod(m)))
.Concat(fieldsByType.GetValue(t).FormatAll(f => explicitTypePrefix + FormatField(f)));
return new CD.Type.InheritedMembers {
FlatMembers = flatMembers.Any() ? flatMembers.Join(CD.NewLine) : null,
HasOne = MapHasOneRelations(hasOneRelationsByType, t),
HasMany = MapHasManyRelations(hasManyRelationsByType, t)
};
});
#endregion
string typeName = GetName(type);
return new CD.Type {
Id = typeId,
Name = typeName == typeId ? null : typeName,
Body = $"class {typeId} {{{body}}}",
HasOne = MapHasOneRelations(hasOneRelationsByType, type),
HasMany = MapHasManyRelations(hasManyRelationsByType, type),
BaseType = GetBaseType(type),
Interfaces = GetInterfaces(type),
Inherited = inheritedMembersByType,
XmlDocs = docs
};
}
}
}

88
ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
partial class ClassDiagrammerFactory
{
/// <summary>Wraps a <see cref="CSharpDecompiler"/> method configurable via <see cref="decompilerSettings"/>
/// that can be used to determine whether a member should be hidden.</summary>
private bool IsHidden(IEntity entity) => CSharpDecompiler.MemberIsHidden(entity.ParentModule!.MetadataFile, entity.MetadataToken, decompilerSettings);
private IField[] GetFields(ITypeDefinition type, IProperty[] properties)
// only display fields that are not backing properties of the same name and type
=> type.GetFields(f => !IsHidden(f) // removes compiler-generated backing fields
/* tries to remove remaining manual backing fields by matching type and name */
&& !properties.Any(p => f.ReturnType.Equals(p.ReturnType)
&& Regex.IsMatch(f.Name, "_?" + p.Name, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.NonBacktracking))).ToArray();
private static IEnumerable<IMethod> GetMethods(ITypeDefinition type) => type.GetMethods(m =>
!m.IsOperator && !m.IsCompilerGenerated()
&& (m.DeclaringType == type // include methods if self-declared
/* but exclude methods declared by object and their overrides, if inherited */
|| (!m.DeclaringType.IsObject()
&& (!m.IsOverride || !InheritanceHelper.GetBaseMember(m).DeclaringType.IsObject()))));
private string FormatMethod(IMethod method)
{
string parameters = method.Parameters.Select(p => $"{GetName(p.Type)} {p.Name}").Join(", ");
string? modifier = method.IsAbstract ? "*" : method.IsStatic ? "$" : default;
string name = method.Name;
if (method.IsExplicitInterfaceImplementation)
{
IMember member = method.ExplicitlyImplementedInterfaceMembers.Single();
name = GetName(member.DeclaringType) + '.' + member.Name;
}
string? typeArguments = method.TypeArguments.Count == 0 ? null : $"❰{method.TypeArguments.Select(GetName).Join(", ")}❱";
return $"{GetAccessibility(method.Accessibility)}{name}{typeArguments}({parameters}){modifier} {GetName(method.ReturnType)}";
}
private string FormatFlatProperty(IProperty property)
{
char? visibility = GetAccessibility(property.Accessibility);
string? modifier = property.IsAbstract ? "*" : property.IsStatic ? "$" : default;
return $"{visibility}{GetName(property.ReturnType)} {property.Name}{modifier}";
}
private string FormatField(IField field)
{
string? modifier = field.IsAbstract ? "*" : field.IsStatic ? "$" : default;
return $"{GetAccessibility(field.Accessibility)}{GetName(field.ReturnType)} {field.Name}{modifier}";
}
// see https://stackoverflow.com/a/16024302 for accessibility modifier flags
private static char? GetAccessibility(Accessibility access) => access switch {
Accessibility.Private => '-',
Accessibility.ProtectedAndInternal or Accessibility.Internal => '~',
Accessibility.Protected or Accessibility.ProtectedOrInternal => '#',
Accessibility.Public => '+',
_ => default,
};
}
}

124
ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
using CD = ClassDiagrammer;
partial class ClassDiagrammerFactory
{
private IProperty[] GetHasOneRelations(IProperty[] properties) => properties.Where(property => {
IType type = property.ReturnType;
if (type.TryGetNullableType(out var typeArg))
type = typeArg;
return selectedTypes!.Contains(type);
}).ToArray();
private (IProperty property, IType elementType)[] GetManyRelations(IProperty[] properties)
=> properties.Select(property => {
IType elementType = property.ReturnType.GetElementTypeFromIEnumerable(property.Compilation, true, out bool? isGeneric);
if (isGeneric == false && elementType.IsObject())
{
IProperty[] indexers = property.ReturnType.GetProperties(
p => p.IsIndexer && !p.ReturnType.IsObject(),
GetMemberOptions.IgnoreInheritedMembers).ToArray(); // TODO mayb order by declaring type instead of filtering
if (indexers.Length > 0)
elementType = indexers[0].ReturnType;
}
return isGeneric == true && selectedTypes!.Contains(elementType) ? (property, elementType) : default;
}).Where(pair => pair != default).ToArray();
/// <summary>Returns the relevant direct super type the <paramref name="type"/> inherits from
/// in a format matching <see cref="CD.Type.BaseType"/>.</summary>
private Dictionary<string, string?>? GetBaseType(IType type)
{
IType? relevantBaseType = type.DirectBaseTypes.SingleOrDefault(t => !t.IsInterface() && !t.IsObject());
return relevantBaseType == null ? default : new[] { BuildRelationship(relevantBaseType) }.ToDictionary(r => r.to, r => r.label);
}
/// <summary>Returns the direct interfaces implemented by <paramref name="type"/>
/// in a format matching <see cref="CD.Type.Interfaces"/>.</summary>
private Dictionary<string, string?[]>? GetInterfaces(ITypeDefinition type)
{
var interfaces = type.DirectBaseTypes.Where(t => t.IsInterface()).ToArray();
return interfaces.Length == 0 ? null
: interfaces.Select(i => BuildRelationship(i)).GroupBy(r => r.to)
.ToDictionary(g => g.Key, g => g.Select(r => r.label).ToArray());
}
/// <summary>Returns the one-to-one relations from <paramref name="type"/> to other <see cref="CD.Type"/>s
/// in a format matching <see cref="CD.Relationships.HasOne"/>.</summary>
private Dictionary<string, string>? MapHasOneRelations(Dictionary<IType, IProperty[]> hasOneRelationsByType, IType type)
=> hasOneRelationsByType.GetValue(type)?.Select(p => {
IType type = p.ReturnType;
string label = p.Name;
if (p.IsIndexer)
label += $"[{p.Parameters.Single().Type.Name} {p.Parameters.Single().Name}]";
if (type.TryGetNullableType(out var typeArg))
{
type = typeArg;
label += " ?";
}
return BuildRelationship(type, label);
}).ToDictionary(r => r.label!, r => r.to);
/// <summary>Returns the one-to-many relations from <paramref name="type"/> to other <see cref="CD.Type"/>s
/// in a format matching <see cref="CD.Relationships.HasMany"/>.</summary>
private Dictionary<string, string>? MapHasManyRelations(Dictionary<IType, (IProperty property, IType elementType)[]> hasManyRelationsByType, IType type)
=> hasManyRelationsByType.GetValue(type)?.Select(relation => {
(IProperty property, IType elementType) = relation;
return BuildRelationship(elementType, property.Name);
}).ToDictionary(r => r.label!, r => r.to);
/// <summary>Builds references to super types and (one/many) relations,
/// recording outside references on the way and applying labels if required.</summary>
/// <param name="type">The type to reference.</param>
/// <param name="propertyName">Used only for property one/many relations.</param>
private (string to, string? label) BuildRelationship(IType type, string? propertyName = null)
{
(string id, IType? openGeneric) = GetIdAndOpenGeneric(type);
AddOutsideReference(id, openGeneric ?? type);
// label the relation with the property name if provided or the closed generic type for super types
string? label = propertyName ?? (openGeneric == null ? null : GetName(type));
return (to: id, label);
}
private void AddOutsideReference(string typeId, IType type)
{
if (!selectedTypes!.Contains(type) && outsideReferences?.ContainsKey(typeId) == false)
outsideReferences.Add(typeId, type.Namespace + '.' + GetName(type));
}
}
}

84
ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
using CD = ClassDiagrammer;
public partial class ClassDiagrammerFactory
{
/// <summary>Generates a dictionary of unique and short, but human readable identifiers for
/// <paramref name="types"/> to be able to safely reference them in any combination.</summary>
private static Dictionary<IType, string> GenerateUniqueIds(IEnumerable<ITypeDefinition> types)
{
Dictionary<IType, string> uniqueIds = [];
var groups = types.GroupBy(t => t.Name);
// simplified handling for the majority of unique types
foreach (var group in groups.Where(g => g.Count() == 1))
uniqueIds[group.First()] = SanitizeTypeName(group.Key);
// number non-unique types
foreach (var group in groups.Where(g => g.Count() > 1))
{
var counter = 0;
foreach (var type in group)
uniqueIds[type] = type.Name + ++counter;
}
return uniqueIds;
}
private string GetId(IType type) => GetIdAndOpenGeneric(type).id;
/// <summary>For a non- or open generic <paramref name="type"/>, returns a unique identifier and null.
/// For a closed generic <paramref name="type"/>, returns the open generic type and the unique identifier of it.
/// That helps connecting closed generic references (e.g. Store&lt;int>) to their corresponding
/// open generic <see cref="CD.Type"/> (e.g. Store&lt;T>) like in <see cref="BuildRelationship(IType, string?)"/>.</summary>
private (string id, IType? openGeneric) GetIdAndOpenGeneric(IType type)
{
// get open generic type if type is a closed generic (i.e. has type args none of which are parameters)
var openGeneric = type is ParameterizedType generic && !generic.TypeArguments.Any(a => a is ITypeParameter)
? generic.GenericType : null;
type = openGeneric ?? type; // reference open instead of closed generic type
if (uniqueIds!.TryGetValue(type, out var uniqueId))
return (uniqueId, openGeneric); // types included by FilterTypes
// types excluded by FilterTypes
string? typeParams = type.TypeParameterCount == 0 ? null : ("_" + type.TypeParameters.Select(GetId).Join("_"));
var id = SanitizeTypeName(type.FullName.Replace('.', '_'))
+ typeParams; // to achieve uniqueness for types with same FullName (i.e. generic overloads)
uniqueIds![type] = id; // update dictionary to avoid re-generation
return (id, openGeneric);
}
private static string SanitizeTypeName(string typeName)
=> typeName.Replace('<', '_').Replace('>', '_'); // for module of executable
}
}

74
ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
public partial class ClassDiagrammerFactory
{
/// <summary>Returns a cached display name for <paramref name="type"/>.</summary>
private string GetName(IType type)
{
if (labels!.TryGetValue(type, out string? value))
return value; // return cached value
return labels[type] = GenerateName(type); // generate and cache new value
}
/// <summary>Generates a display name for <paramref name="type"/>.</summary>
private string GenerateName(IType type)
{
// non-generic types
if (type.TypeParameterCount < 1)
{
if (type is ArrayType array)
return GetName(array.ElementType) + "[]";
if (type is ByReferenceType byReference)
return "&" + GetName(byReference.ElementType);
ITypeDefinition? typeDefinition = type.GetDefinition();
if (typeDefinition == null)
return type.Name;
if (typeDefinition.KnownTypeCode == KnownTypeCode.None)
{
if (type.DeclaringType == null)
return type.Name.Replace('<', '❰').Replace('>', '❱'); // for module of executable
else
return type.DeclaringType.Name + '+' + type.Name; // nested types
}
return KnownTypeReference.GetCSharpNameByTypeCode(typeDefinition.KnownTypeCode) ?? type.Name;
}
// nullable types
if (type.TryGetNullableType(out var nullableType))
return GetName(nullableType) + "?";
// other generic types
string typeArguments = type.TypeArguments.Select(GetName).Join(", ");
return type.Name + $"❰{typeArguments}❱";
}
}
}

45
ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
/// <summary>The command for creating an HTML5 diagramming app with an API optimized for binding command line parameters.
/// To use it outside of that context, set its properties and call <see cref="Run"/>.</summary>
public partial class GenerateHtmlDiagrammer
{
internal const string RepoUrl = "https://github.com/icsharpcode/ILSpy";
public required string Assembly { get; set; }
public string? OutputFolder { get; set; }
public string? Include { get; set; }
public string? Exclude { get; set; }
public bool JsonOnly { get; set; }
public bool ReportExludedTypes { get; set; }
public string? XmlDocs { get; set; }
/// <summary>Namespaces to strip from <see cref="XmlDocs"/>.
/// Implemented as a list of exact replacements instead of a single, more powerful RegEx because replacement in
/// <see cref="XmlDocumentationFormatter.GetDoco(Decompiler.TypeSystem.IEntity)"/>
/// happens on the unstructured string where matching and replacing the namespaces of referenced types, members and method parameters
/// using RegExes would add a lot of complicated RegEx-heavy code for a rather unimportant feature.</summary>
public IEnumerable<string>? StrippedNamespaces { get; set; }
}
}

146
ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs

@ -0,0 +1,146 @@ @@ -0,0 +1,146 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
partial class GenerateHtmlDiagrammer
{
public void Run()
{
var assemblyPath = GetPath(Assembly);
XmlDocumentationFormatter? xmlDocs = CreateXmlDocsFormatter(assemblyPath);
ClassDiagrammer model = BuildModel(assemblyPath, xmlDocs);
GenerateOutput(assemblyPath, model);
}
protected virtual XmlDocumentationFormatter? CreateXmlDocsFormatter(string assemblyPath)
{
var xmlDocsPath = XmlDocs == null ? Path.ChangeExtension(assemblyPath, ".xml") : GetPath(XmlDocs);
XmlDocumentationFormatter? xmlDocs = null;
if (File.Exists(xmlDocsPath))
xmlDocs = new XmlDocumentationFormatter(new XmlDocumentationProvider(xmlDocsPath), StrippedNamespaces?.ToArray());
else
Console.WriteLine("No XML documentation file found. Continuing without.");
return xmlDocs;
}
protected virtual ClassDiagrammer BuildModel(string assemblyPath, XmlDocumentationFormatter? xmlDocs)
=> new ClassDiagrammerFactory(xmlDocs).BuildModel(assemblyPath, Include, Exclude);
private string SerializeModel(ClassDiagrammer diagrammer)
{
object jsonModel = new {
diagrammer.OutsideReferences,
/* convert collections to dictionaries for easier access in ES using
* for (let [key, value] of Object.entries(dictionary)) */
TypesByNamespace = diagrammer.TypesByNamespace.ToDictionary(ns => ns.Key,
ns => ns.Value.ToDictionary(t => t.Id, t => t))
};
// wrap model including the data required for doing the template replacement in a JS build task
if (JsonOnly)
{
jsonModel = new {
diagrammer.SourceAssemblyName,
diagrammer.SourceAssemblyVersion,
BuilderVersion = DecompilerVersionInfo.FullVersionWithCommitHash,
RepoUrl,
// pre-serialize to a string so that we don't have to re-serialize it in the JS build task
Model = Serialize(jsonModel)
};
}
return Serialize(jsonModel);
}
private static JsonSerializerOptions serializerOptions = new() {
WriteIndented = true,
// avoid outputting null properties unnecessarily
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static string Serialize(object json) => JsonSerializer.Serialize(json, serializerOptions);
private void GenerateOutput(string assemblyPath, ClassDiagrammer model)
{
string modelJson = SerializeModel(model);
// If no out folder is specified, default to a "<Input.Assembly.Name> diagrammer" folder next to the input assembly.
var outputFolder = OutputFolder
?? Path.Combine(
Path.GetDirectoryName(assemblyPath) ?? string.Empty,
Path.GetFileNameWithoutExtension(assemblyPath) + " diagrammer");
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
if (JsonOnly)
{
File.WriteAllText(Path.Combine(outputFolder, "model.json"), modelJson);
Console.WriteLine("Successfully generated model.json for HTML diagrammer.");
}
else
{
var htmlTemplate = EmbeddedResource.ReadText("template.html");
var html = htmlTemplate
.Replace("{{SourceAssemblyName}}", model.SourceAssemblyName)
.Replace("{{SourceAssemblyVersion}}", model.SourceAssemblyVersion)
.Replace("{{BuilderVersion}}", DecompilerVersionInfo.FullVersionWithCommitHash)
.Replace("{{RepoUrl}}", RepoUrl)
.Replace("{{Model}}", modelJson);
File.WriteAllText(Path.Combine(outputFolder, "index.html"), html);
// copy required resources to output folder while flattening paths if required
foreach (var resource in new[] { "styles.css", "ILSpy.ico", "script.js" })
EmbeddedResource.CopyTo(outputFolder, resource);
Console.WriteLine("Successfully generated HTML diagrammer.");
}
if (ReportExludedTypes)
{
string excludedTypes = model.Excluded.Join(Environment.NewLine);
File.WriteAllText(Path.Combine(outputFolder, "excluded types.txt"), excludedTypes);
}
}
private protected virtual string GetPath(string pathOrUri)
{
// convert file:// style argument, see https://stackoverflow.com/a/38245329
if (!Uri.TryCreate(pathOrUri, UriKind.RelativeOrAbsolute, out Uri? uri))
throw new ArgumentException("'{0}' is not a valid URI", pathOrUri);
// support absolute paths as well as file:// URIs and interpret relative path as relative to the current directory
return uri.IsAbsoluteUri ? uri.AbsolutePath : pathOrUri;
}
}
}

13
ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
# How does it work?
To **extract the type info from the source assembly**, ILSpy side-loads it including all its dependencies.
The extracted type info is **structured into a model optimized for the HTML diagrammer** and serialized to JSON. The model is a mix between drop-in type definitions in mermaid class diagram syntax and destructured metadata about relations, inheritance and documentation comments.
> The JSON type info is injected into the `template.html` alongside other resources like the `script.js` at corresponding `{{placeholders}}`. It comes baked into the HTML diagrammer to enable
> - accessing the data and
> - importing the mermaid module from a CDN
>
> locally without running a web server [while also avoiding CORS restrictions.](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#file_origins)
In the final step, the **HTML diagrammer app re-assembles the type info** based on the in-app type selection and rendering options **to generate [mermaid class diagrams](https://mermaid.js.org/syntax/classDiagram.html)** with the types, their relations and as much inheritance detail as you need.

91
ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
// Copyright (c) 2024 Holger Schmidt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
/// <summary>Wraps the <see cref="IDocumentationProvider"/> to prettify XML documentation comments.
/// Make sure to enable XML documentation output, see
/// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output .</summary>
public class XmlDocumentationFormatter
{
/// <summary>Matches XML indent.</summary>
protected const string linePadding = @"^[ \t]+|[ \t]+$";
/// <summary>Matches reference tags including "see href", "see cref" and "paramref name"
/// with the cref value being prefixed by symbol-specific letter and a colon
/// including the quotes around the attribute value and the closing slash of the tag containing the attribute.</summary>
protected const string referenceAttributes = @"(see\s.ref=""(.:)?)|(paramref\sname="")|(""\s/)";
private readonly IDocumentationProvider docs;
private readonly Regex noiseAndPadding;
public XmlDocumentationFormatter(IDocumentationProvider docs, string[]? strippedNamespaces)
{
this.docs = docs;
List<string> regexes = new() { linePadding, referenceAttributes };
if (strippedNamespaces?.Length > 0)
regexes.AddRange(strippedNamespaces.Select(ns => $"({ns.Replace(".", "\\.")}\\.)"));
noiseAndPadding = new Regex(regexes.Join("|"), RegexOptions.Multiline); // builds an OR | combined regex
}
internal Dictionary<string, string>? GetXmlDocs(ITypeDefinition type, params IMember[][] memberCollections)
{
Dictionary<string, string>? docs = new();
AddXmlDocEntry(docs, type);
foreach (IMember[] members in memberCollections)
{
foreach (IMember member in members)
AddXmlDocEntry(docs, member);
}
return docs?.Keys.Count != 0 ? docs : default;
}
protected virtual string? GetDoco(IEntity entity)
{
string? comment = docs.GetDocumentation(entity)?
.ReplaceAll(["<summary>", "</summary>"], null)
.ReplaceAll(["<para>", "</para>"], ClassDiagrammer.NewLine).Trim() // to format
.Replace('<', '[').Replace('>', ']'); // to prevent ugly escaped output
return comment == null ? null : noiseAndPadding.Replace(comment, string.Empty).NormalizeHorizontalWhiteSpace();
}
private void AddXmlDocEntry(Dictionary<string, string> docs, IEntity entity)
{
string? doc = GetDoco(entity);
if (string.IsNullOrEmpty(doc))
return;
string key = entity is IMember member ? member.Name : string.Empty;
docs[key] = doc;
}
}
}

16
ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
module.exports = {
'env': {
'commonjs': true,
'es6': true,
'browser': true
},
'extends': 'eslint:recommended',
'parserOptions': {
'sourceType': 'module',
'ecmaVersion': 'latest'
},
'rules': {
'indent': ['error', 4, { 'SwitchCase': 1 }],
'semi': ['error', 'always']
}
};

4
ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
/node_modules
/class-diagrammer.html
/model.json
/package-lock.json

54
ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json vendored

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Generate model.json",
"detail": "for editing the template, script or styles in inner dev loop of the HTML diagrammer",
"group": "build",
"type": "shell",
"command": [
"$folder = '../../../ICSharpCode.ILSpyCmd/bin/Debug/net8.0/';", // to avoid repetition
"$exePath = $folder + 'ilspycmd.exe';",
"$assemblyPath = $folder + 'ICSharpCode.Decompiler.dll';", // comes with XML docs for testing the integration
"if (Test-Path $exePath) {",
" & $exePath $assemblyPath --generate-diagrammer --generate-diagrammer-json-only --outputdir .",
"} else {",
" Write-Host 'ilspycmd.exe Debug build not found. Please build it first.';",
" exit 1",
"}"
],
"problemMatcher": []
},
{
"label": "Transpile .less",
"detail": "into .css files",
"group": "build",
"type": "gulp",
"task": "transpileLess",
"problemMatcher": [
"$lessc"
]
},
{
"label": "Generate HTML diagrammer",
"detail": "from the template.html and a model.json",
"group": "build",
"type": "gulp",
"task": "generateHtmlDiagrammer",
"problemMatcher": [
"$gulp-tsc"
]
},
{
"label": "Auto-rebuild on change",
"detail": "run build tasks automatically when source files change",
"type": "gulp",
"task": "autoRebuildOnChange",
"problemMatcher": [
"$gulp-tsc"
]
}
]
}

10
ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
To edit the HTML/JS/CSS for the HTML diagrammer, open this folder in Visual Studio Code.
In that environment you'll find tasks (see https://code.visualstudio.com/Docs/editor/tasks to run and configure)
that you can run to
1. Generate a model.json using the current Debug build of ilspycmd.
This is required to build a diagrammer for testing in development using task 3.
2. Transpile the .less into .css that is tracked by source control and embedded into ILSpyX.
3. Generate a diagrammer for testing in development from template.html and the model.json generated by task 1.
4. Auto-rebuild the development diagrammer by running either task 2 or 3 when the corresponding source files change.

66
ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
const gulp = require('gulp');
const less = require('gulp-less');
const fs = require('fs');
function transpileLess (done) {
gulp
.src('styles.less') // source file(s) to process
.pipe(less()) // pass them through the LESS compiler
.pipe(gulp.dest(f => f.base)); // Use the base directory of the source file for output
done(); // signal task completion
}
function generateHtmlDiagrammer (done) {
// Read and parse model.json
fs.readFile('model.json', 'utf8', function (err, data) {
if (err) {
console.error('Error reading model.json:', err);
done(err);
return;
}
const model = JSON.parse(data); // Parse the JSON data
// Read template.html
fs.readFile('template.html', 'utf8', function (err, templateContent) {
if (err) {
console.error('Error reading template.html:', err);
done(err);
return;
}
// Replace placeholders in template with values from model
let outputContent = templateContent;
for (const [key, value] of Object.entries(model)) {
const placeholder = `{{${key}}}`; // Create the placeholder
outputContent = outputContent.replace(new RegExp(placeholder, 'g'), value); // Replace all occurrences
}
// Save the replaced content
fs.writeFile('class-diagrammer.html', outputContent, 'utf8', function (err) {
if (err) {
console.error('Error writing class-diagrammer.html:', err);
done(err);
return;
}
console.log('class-diagrammer.html generated successfully.');
done(); // Signal completion
});
});
});
}
exports.transpileLess = transpileLess;
exports.generateHtmlDiagrammer = generateHtmlDiagrammer;
/* Run individual build tasks first, then start watching for changes
see https://code.visualstudio.com/Docs/languages/CSS#_automating-sassless-compilation */
exports.autoRebuildOnChange = gulp.series(transpileLess, generateHtmlDiagrammer, function (done) {
// Watch for changes in source files and rerun the corresponding build task
gulp.watch('styles.less', gulp.series(transpileLess));
gulp.watch(['template.html', 'model.json'], gulp.series(generateHtmlDiagrammer));
done(); // signal task completion
});

7
ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
{
"devDependencies": {
"eslint": "^8.57.1",
"gulp": "^4.0.2",
"gulp-less": "^5.0.0"
}
}

1137
ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js

File diff suppressed because it is too large Load Diff

453
ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css

@ -0,0 +1,453 @@ @@ -0,0 +1,453 @@
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
body {
font-family: system-ui, sans-serif;
background: #4e54c8;
background-image: linear-gradient(to left, #8f94fb, #4e54c8);
}
input[type=text] {
border-radius: 3px;
}
button {
border-radius: 3px;
background-color: #aad;
border: none;
color: #117;
cursor: pointer;
}
button.icon {
font-size: 1em;
background-color: transparent;
}
button:disabled {
opacity: 0.5;
}
[type=checkbox],
[type=radio] {
cursor: pointer;
}
[type=checkbox] ~ label,
[type=radio] ~ label {
cursor: pointer;
}
fieldset {
border-radius: 5px;
}
select {
border: none;
border-radius: 3px;
background-color: rgba(0, 0, 0, calc(3/16 * 1));
color: whitesmoke;
}
select option:checked {
background-color: rgba(0, 0, 0, calc(3/16 * 1));
color: darkorange;
}
.flx:not([hidden]) {
display: flex;
}
.flx:not([hidden]).col {
flex-direction: column;
}
.flx:not([hidden]).spaced {
justify-content: space-between;
}
.flx:not([hidden]).gap {
gap: 0.5em;
}
.flx:not([hidden]).aligned {
align-items: center;
}
.flx:not([hidden]) .grow {
flex-grow: 1;
}
.collapse.vertical {
max-height: 0;
overflow: hidden;
transition: max-height ease-in-out 0.5s;
}
.collapse.vertical.open {
max-height: 100vh;
}
.collapse.horizontal {
max-width: 0;
padding: 0;
margin: 0;
transition: all ease-in-out 0.5s;
overflow: hidden;
}
.collapse.horizontal.open {
padding: revert;
max-width: 100vw;
}
.toggle,
[data-toggles] {
cursor: pointer;
}
.container {
position: absolute;
inset: 0;
margin: 0;
}
.scndry {
font-size: smaller;
}
.mano-a-borsa {
transform: rotate(95deg);
cursor: pointer;
}
.mano-a-borsa:after {
content: '🤏';
}
.trawl-net {
transform: rotate(180deg) translateY(-2px);
display: inline-block;
}
.trawl-net:after {
content: '🥅';
}
.torch {
display: inline-block;
}
.torch:after {
content: '🔦';
}
.pulsing {
animation: whiteBoxShadowPulse 2s 3;
}
@keyframes whiteBoxShadowPulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
5% {
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0.5);
}
50% {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
90% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
#content {
height: 100%;
position: relative;
}
#filter {
max-width: 0;
transition: max-width ease-in-out 0.5s;
overflow: hidden;
background-color: rgba(0, 0, 0, calc(3/16 * 1));
color: whitesmoke;
}
#filter.open {
max-width: 15em;
overflow: auto;
}
#filter.resizing {
transition: none;
}
#filter > * {
margin: 0.3em 0.3em 0;
}
#filter > *:last-child {
margin-bottom: 0.3em;
}
#filter #pre-filter-types {
min-width: 3em;
}
#filter [data-toggles="#info"] .torch {
transform: rotate(-90deg);
transition: transform 0.5s;
}
#filter [data-toggles="#info"][aria-expanded=true] .torch {
transform: rotate(-255deg);
}
#filter #info {
overflow: auto;
background-color: rgba(255, 255, 255, calc(1/16 * 2));
}
#filter #info a.toggle {
color: whitesmoke;
}
#filter #info a.toggle img {
height: 1em;
}
#filter #type-select {
overflow: auto;
}
#filter #inheritance {
padding: 0.1em 0.75em 0.2em;
}
#filter #direction [type=radio] {
display: none;
}
#filter #direction [type=radio]:checked + label {
background-color: rgba(255, 255, 255, calc(1/16 * 4));
}
#filter #direction label {
flex-grow: 1;
text-align: center;
margin: -1em 0 -0.7em;
padding-top: 0.2em;
}
#filter #direction label:first-of-type {
margin-left: -0.8em;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
#filter #direction label:last-of-type {
margin-right: -0.8em;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
#filter #actions {
margin-top: 1em;
justify-content: space-between;
}
#filter #actions #render {
font-weight: bold;
}
#filter #exportOptions {
overflow: auto;
background-color: rgba(255, 255, 255, calc(1/16 * 2));
}
#filter #exportOptions #save {
margin-right: 0.5em;
}
#filter #exportOptions #dimensions fieldset {
padding: 0.5em;
}
#filter #exportOptions #dimensions fieldset .scale-size {
margin-left: 0.5em;
}
#filter #exportOptions #dimensions fieldset .scale-size #scale-size {
width: 2.5em;
margin: 0 0.2em;
}
#filter-toggle {
padding: 0;
border-radius: 0;
background-color: #117;
color: whitesmoke;
}
#output {
overflow: auto;
}
#output > svg {
cursor: grab;
}
#output > svg:active {
cursor: grabbing;
}
#output .edgeLabels .edgeTerminals .edgeLabel {
color: whitesmoke;
}
#output .edgeLabels .edgeLabel {
border-radius: 3px;
}
#output .edgeLabels .edgeLabel .edgeLabel[title] {
color: darkgoldenrod;
}
#output path.relation {
stroke: whitesmoke;
}
#output g.nodes > g {
cursor: pointer;
}
#output g.nodes > g > rect {
rx: 5px;
ry: 5px;
}
#output g.nodes g.label .nodeLabel[title] {
color: darkgoldenrod;
}
#about {
position: absolute;
bottom: 2em;
right: 2em;
align-items: end;
}
#about #toaster {
margin-right: 2.8em;
}
#about #toaster span {
animation: 0.5s ease-in fadeIn;
border-radius: 0.5em;
padding: 0.5em;
background-color: rgba(0, 0, 0, calc(3/16 * 2));
color: whitesmoke;
}
#about #toaster span.leaving {
animation: 1s ease-in-out fadeOut;
}
#about .build-info {
align-items: end;
height: 2.3em;
border-radius: 7px;
background-color: rgba(0, 0, 0, calc(3/16 * 3));
color: whitesmoke;
}
#about .build-info > * {
height: 100%;
}
#about .build-info #build-info {
text-align: right;
}
#about .build-info #build-info > * {
padding: 0 0.5em;
}
#about .build-info #build-info a {
color: whitesmoke;
}
#about .build-info #build-info a:not(.project) {
text-decoration: none;
}
#about .build-info #build-info a span {
display: inline-block;
}
#pressed-keys {
position: fixed;
left: 50%;
transform: translateX(-50%);
font-size: 3em;
bottom: 1em;
opacity: 1;
border-radius: 0.5em;
padding: 0.5em;
background-color: rgba(0, 0, 0, calc(3/16 * 2));
color: whitesmoke;
}
#pressed-keys.hidden {
transition: opacity 0.5s ease-in-out;
opacity: 0;
}
#mouse {
position: fixed;
transform: translateX(-50%) translateY(-50%);
height: 2em;
width: 2em;
pointer-events: none;
z-index: 9999;
border-radius: 1em;
border: solid 0.1em yellow;
}
#mouse.down {
background-color: #ff08;
}
/* hide stuff in print view */
@media print {
#filter,
#filter-toggle,
#about,
img,
.bubbles {
display: none;
}
}
/* ANIMATED BACKGROUND, from https://codepen.io/alvarotrigo/pen/GRvYNax
found in https://alvarotrigo.com/blog/animated-backgrounds-css/ */
@keyframes rotateUp {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
border-radius: 100%;
}
100% {
transform: translateY(-150vh) rotate(720deg);
opacity: 0;
border-radius: 0;
}
}
.bubbles {
overflow: hidden;
}
.bubbles li {
position: absolute;
display: block;
list-style: none;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.2);
animation: rotateUp 25s linear infinite;
bottom: -150px;
}
.bubbles li:nth-child(1) {
left: 25%;
width: 80px;
height: 80px;
animation-delay: 0s;
}
.bubbles li:nth-child(2) {
left: 10%;
width: 20px;
height: 20px;
animation-delay: 2s;
animation-duration: 12s;
}
.bubbles li:nth-child(3) {
left: 70%;
width: 20px;
height: 20px;
animation-delay: 4s;
}
.bubbles li:nth-child(4) {
left: 40%;
width: 60px;
height: 60px;
animation-delay: 0s;
animation-duration: 18s;
}
.bubbles li:nth-child(5) {
left: 65%;
width: 20px;
height: 20px;
animation-delay: 0s;
}
.bubbles li:nth-child(6) {
left: 75%;
width: 110px;
height: 110px;
animation-delay: 3s;
}
.bubbles li:nth-child(7) {
left: 35%;
width: 150px;
height: 150px;
animation-delay: 7s;
}
.bubbles li:nth-child(8) {
left: 50%;
width: 25px;
height: 25px;
animation-delay: 15s;
animation-duration: 45s;
}
.bubbles li:nth-child(9) {
left: 20%;
width: 15px;
height: 15px;
animation-delay: 2s;
animation-duration: 35s;
}
.bubbles li:nth-child(10) {
left: 85%;
width: 150px;
height: 150px;
animation-delay: 0s;
animation-duration: 11s;
}

586
ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less

@ -0,0 +1,586 @@ @@ -0,0 +1,586 @@
@darkBlue: #117;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.clickable() {
cursor: pointer;
}
.useBrightText() {
color: whitesmoke;
}
.colorLabelWithDocs() {
color: darkgoldenrod;
}
.darkenBg(@times: 1) {
background-color: rgba(0,0,0, calc(3/16 * @times));
}
.brightenBg(@times: 1) {
background-color: rgba(255,255,255, calc(1/16 * @times));
}
body {
font-family: system-ui, sans-serif;
background: #4e54c8;
background-image: linear-gradient(to left, #8f94fb, #4e54c8);
}
input[type=text] {
border-radius: 3px;
}
button {
border-radius: 3px;
background-color: #aad;
border: none;
color: @darkBlue;
.clickable;
&.icon {
font-size: 1em;
background-color: transparent;
}
&:disabled {
opacity: .5;
}
}
[type=checkbox], [type=radio] {
.clickable;
& ~ label {
.clickable;
}
}
fieldset {
border-radius: 5px;
}
select {
border: none;
border-radius: 3px;
.darkenBg;
.useBrightText;
option:checked {
.darkenBg;
color: darkorange;
}
}
.flx:not([hidden]) {
display: flex;
&.col {
flex-direction: column;
}
&.spaced {
justify-content: space-between;
}
&.gap {
gap: .5em;
}
&.aligned {
align-items: center;
}
.grow {
flex-grow: 1;
}
}
.collapse {
&.vertical {
max-height: 0;
overflow: hidden;
transition: max-height ease-in-out .5s;
&.open {
max-height: 100vh;
}
}
&.horizontal {
max-width: 0;
padding: 0;
margin: 0;
transition: all ease-in-out .5s;
overflow: hidden;
&.open {
padding: revert;
max-width: 100vw;
}
}
}
.toggle, [data-toggles] {
.clickable;
}
.container {
position: absolute;
inset: 0;
margin: 0;
}
.scndry {
font-size: smaller;
}
.mano-a-borsa {
transform: rotate(95deg);
.clickable;
&:after {
content: '🤏';
}
}
.trawl-net {
transform: rotate(180deg) translateY(-2px);
display: inline-block;
&:after {
content: '🥅';
}
}
.torch {
display: inline-block;
&:after {
content: '🔦';
}
}
.pulsing {
animation: whiteBoxShadowPulse 2s 3;
}
@keyframes whiteBoxShadowPulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
5% {
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0.5);
}
50% {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
90% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
#content {
height: 100%;
position: relative;
}
#filter {
max-width: 0;
transition: max-width ease-in-out .5s;
overflow: hidden;
.darkenBg;
.useBrightText;
&.open {
max-width: 15em;
overflow: auto;
}
&.resizing {
transition: none;
}
> * {
margin: .3em .3em 0;
&:last-child {
margin-bottom: .3em;
}
}
#pre-filter-types {
min-width: 3em;
}
[data-toggles="#info"] {
.torch {
transform: rotate(-90deg);
transition: transform .5s;
}
&[aria-expanded=true] {
.torch {
transform: rotate(-255deg);
}
}
}
#info {
overflow: auto;
.brightenBg(2);
a.toggle {
.useBrightText;
img {
height: 1em;
}
}
}
#type-select {
overflow: auto;
}
#inheritance {
padding: .1em .75em .2em;
}
#direction {
[type=radio] {
display: none;
&:checked + label {
.brightenBg(4);
}
}
label {
flex-grow: 1;
text-align: center;
margin: -1em 0 -.7em;
padding-top: .2em;
&:first-of-type {
margin-left: -.8em;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
&:last-of-type {
margin-right: -.8em;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
}
}
#actions {
margin-top: 1em;
justify-content: space-between;
#render {
font-weight: bold;
}
}
#exportOptions {
overflow: auto;
.brightenBg(2);
#save {
margin-right: .5em;
}
#dimensions fieldset {
padding: .5em;
.scale-size {
margin-left: .5em;
#scale-size {
width: 2.5em;
margin: 0 .2em;
}
}
}
}
}
#filter-toggle {
padding: 0;
border-radius: 0;
background-color: @darkBlue;
.useBrightText;
}
#output {
overflow: auto;
> svg {
cursor: grab;
&:active {
cursor: grabbing;
}
}
.edgeLabels {
.edgeTerminals .edgeLabel {
.useBrightText;
}
.edgeLabel {
border-radius: 3px;
.edgeLabel[title] {
.colorLabelWithDocs;
}
}
}
path.relation {
stroke: whitesmoke;
}
g.nodes {
> g {
.clickable;
> rect {
rx: 5px;
ry: 5px;
}
}
g.label .nodeLabel[title] {
.colorLabelWithDocs;
}
}
}
#about {
position: absolute;
bottom: 2em;
right: 2em;
align-items: end;
@logoWidth: 2.3em;
#toaster {
margin-right: @logoWidth + .5em;
span {
animation: .5s ease-in fadeIn;
border-radius: .5em;
padding: .5em;
.darkenBg(2);
.useBrightText;
&.leaving {
animation: 1s ease-in-out fadeOut;
}
}
}
.build-info {
align-items: end;
height: @logoWidth;
border-radius: 7px;
.darkenBg(3);
.useBrightText;
> * {
height: 100%;
}
#build-info {
text-align: right;
> * {
padding: 0 .5em;
}
a {
.useBrightText;
&:not(.project) {
text-decoration: none;
}
span {
display: inline-block;
}
}
}
}
}
#pressed-keys {
position: fixed;
left: 50%;
transform: translateX(-50%);
font-size: 3em;
bottom: 1em;
opacity: 1;
border-radius: .5em;
padding: .5em;
.darkenBg(2);
.useBrightText;
&.hidden {
transition: opacity 0.5s ease-in-out;
opacity: 0;
}
}
#mouse {
position: fixed;
transform: translateX(-50%) translateY(-50%);
height: 2em;
width: 2em;
pointer-events: none;
z-index: 9999;
border-radius: 1em;
border: solid .1em yellow;
&.down {
background-color: #ff08;
}
}
/* hide stuff in print view */
@media print {
#filter, #filter-toggle, #about, img, .bubbles {
display: none;
}
}
/* ANIMATED BACKGROUND, from https://codepen.io/alvarotrigo/pen/GRvYNax
found in https://alvarotrigo.com/blog/animated-backgrounds-css/ */
@keyframes rotateUp {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
border-radius: 100%;
}
100% {
transform: translateY(-150vh) rotate(720deg);
opacity: 0;
border-radius: 0;
}
}
.bubbles {
overflow: hidden;
li {
position: absolute;
display: block;
list-style: none;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, .2);
animation: rotateUp 25s linear infinite;
bottom: -150px;
&:nth-child(1) {
left: 25%;
width: 80px;
height: 80px;
animation-delay: 0s;
}
&:nth-child(2) {
left: 10%;
width: 20px;
height: 20px;
animation-delay: 2s;
animation-duration: 12s;
}
&:nth-child(3) {
left: 70%;
width: 20px;
height: 20px;
animation-delay: 4s;
}
&:nth-child(4) {
left: 40%;
width: 60px;
height: 60px;
animation-delay: 0s;
animation-duration: 18s;
}
&:nth-child(5) {
left: 65%;
width: 20px;
height: 20px;
animation-delay: 0s;
}
&:nth-child(6) {
left: 75%;
width: 110px;
height: 110px;
animation-delay: 3s;
}
&:nth-child(7) {
left: 35%;
width: 150px;
height: 150px;
animation-delay: 7s;
}
&:nth-child(8) {
left: 50%;
width: 25px;
height: 25px;
animation-delay: 15s;
animation-duration: 45s;
}
&:nth-child(9) {
left: 20%;
width: 15px;
height: 15px;
animation-delay: 2s;
animation-duration: 35s;
}
&:nth-child(10) {
left: 85%;
width: 150px;
height: 150px;
animation-delay: 0s;
animation-duration: 11s;
}
}
}

194
ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{SourceAssemblyName}} class diagrammer - ILSpy</title>
<link rel="icon" type="image/x-icon" href="ILSpy.ico" />
<link rel="stylesheet" href="styles.css" type="text/css" />
<style id="filter-width"></style>
</head>
<body class="container">
<!-- for animated background -->
<ul class="bubbles container">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<div id="content" class="flx">
<form id="filter" class="flx col open">
<div class="flx gap">
<input id="pre-filter-types" placeholder="pre-filter" class="grow"
title="🐋 I sift through the types for you.&#10;Feed me vanilla 🦐 plain text or ES/JS flavored 🍤 RegEx.&#10;🔭 Focus me with [Ctrl + K]. " />
<label for="type-select" class="grow">types</label>
<button type="button" class="icon" data-toggles="#info" title="🕯 Shed light on the type selection"><span class="torch"></span></button>
</div>
<div id="info" class="scndry vertical collapse">
<p>
The <big>type picker</big> is ✜ focused when you open the app.
You can just <b> key in the first letter/s</b> of the type
you want to start your diagram with and <b>hit [Enter] to render</b> it.
</p>
<p>
After rendering you can 👆 <b>tap types</b> on the diagram
to update your selection and redraw.
This allows you to <b>explore the domain</b> along relations.
</p>
<p>
Don't forget that you can hold [Shift] to <b>↕ range-select</b>
and [Ctrl] to <b>± add to or subtract from</b> your selection.
</p>
<p>
Note that the diagram has a 🟈 <b>layout direction</b> -
i.e. it depends on how you <b>⇅ sort selected types</b> using [Alt + Arrow Up|Down].
</p>
<p>
Changing the type selection or rendering options
updates the URL in the location bar. That means you can
<ul>
<li><b>🔖 bookmark or 📣 share the URL</b> to your diagram with whoever has access to this diagrammer,</li>
<li><b>access 🕔 earlier diagrams</b> recorded in your 🧾 browser history and</li>
<li><b>⇥ restore your type selection</b> to the picker from the URL using ⟳ Refresh [F5] if you lose it.</li>
</ul>
</p>
<h3>Looking for help with something else?</h3>
<p>
<b>Stop and spot the tooltips.</b> 🌷 They'll give you more info where necessary.
Get a hint for elements with helping tooltips using [Alt + i].
</p>
<p>Alternatively, find helpful links to the docs and discussions in the
<a href="#build-info" class="toggle">build info <img src="ILSpy.ico" /></a></p>
<p>If you find this helpful and want to share your 📺 screen and 🎓 wisdom on how it works
with a 🦗 newcomer, try toggling <b>presentation mode</b> using [Ctrl + i].</p>
</div>
<select multiple id="type-select" class="grow" title="🥢 pick types to include in your diagram"></select>
<fieldset id="inheritance" class="scndry flx" title="You may find these options useful to reason about type inheritance - probably less so when looking at entity relations.">
<legend>show inherited</legend>
<span class="scndry flx" title="Render direct base types.">
<input type="checkbox" id="show-base-types" checked />
<label for="show-base-types">types</label>
</span>
<span class="scndry flx" title="Render direct interfaces.">
<input type="checkbox" id="show-interfaces" checked />
<label for="show-interfaces">interfaces</label>
</span>
<span class="scndry flx" title="Render members inherited from ancestor types - unless those are also selected and rendered in detail.">
<input type="checkbox" id="show-inherited-members" checked />
<label for="show-inherited-members">members</label>
</span>
</fieldset>
<fieldset id="direction" class="scndry flx" title="[Ctrl + arrow keys] You may want to change this depending on your screen or printer and the size of the diagram.">
<legend>layout direction</legend>
<input type="radio" name="direction" value="RL" id="dir-rl" />
<label for="dir-rl"></label>
<input type="radio" name="direction" value="TB" id="dir-tb" />
<label for="dir-tb"></label>
<input type="radio" name="direction" value="BT" id="dir-bt" />
<label for="dir-bt"></label>
<input type="radio" name="direction" value="LR" id="dir-lr" checked />
<label for="dir-lr"></label>
</fieldset>
<div id="actions" class="flx spaced">
<button title="Render the selected types. [Enter] with the side bar in focus will do."
type="submit" id="render" disabled><span class="trawl-net"></span> Cast the diagram</button>
<button type="button" class="icon" data-toggles="#exportOptions" id="exportOptions-toggle" hidden title="toggle 🥡 export options">🎣</button>
</div>
<div id="exportOptions" class="scndry vertical collapse aligned spaced flx gap col">
<div class="flx gap" title="Note that you can also use your browser's Print function [Ctrl + P] to export to PDF or paper or split up the diagram into multiple pages.">
<button type="button" id="save" data-assembly="{{SourceAssemblyName}}" title="[Ctrl + S] Saves the diagram in the selected format using a generated name.">💾 Save</button>
<label>or</label>
<button type="button" id="copy" title="[Ctrl + C] Copies the diagram in the selected format to your clipboard for you to paste directly into a messenger, word- or image processor.">📋 Copy to clipboard</button>
</div>
<div class="flx">
<label>as</label>
<span class="flx" title="Exports the diagram as SVG to render in an HTML document or SVG-enabled word processor.">
<input type="radio" name="saveAs" value="svg" id="saveAs-svg" />
<label for="saveAs-svg">svg</label>
</span>
<span class="flx" title="Exports the diagram as a base-64 encoded PNG.">
<input type="radio" name="saveAs" value="png" id="saveAs-png" checked />
<label for="saveAs-png">png</label>
</span>
<span class="flx" title="Exports the mermaid syntax for the diagram.">
<input type="radio" name="saveAs" value="mmd" id="saveAs-mmd" />
<label for="saveAs-mmd">mmd</label>
</span>
</div>
<div id="dimensions" class="vertical open collapse">
<fieldset title="Applied when saving and in (unscalable) image format. Note these settings indirectly determine the resolution.">
<legend>png dimensions</legend>
<div class="flx">
<input type="radio" name="dimension" value="auto" id="dimension-current" checked />
<label for="dimension-current">current</label>
<input type="radio" name="dimension" value="scale" id="dimension-scale" />
<label for="dimension-scale">scale to fixed</label>
</div>
<div id="scale-controls" class="flx aligned">
<input type="radio" name="scale" value="width" id="scale-width" checked disabled />
<label for="scale-width">width</label>
<input type="radio" name="scale" value="height" id="scale-height" disabled />
<label for="scale-height">height</label>
<div class="scale-size flx aligned">
<label for="scale-size">of</label>
<input type="text" id="scale-size" value="1080" disabled />
<label for="scale-size">px</label>
</div>
</div>
</fieldset>
</div>
</div>
</form>
<button type="button" class="icon" id="filter-toggle" title="🧜 Let me lay it out for you.&#10👆 Tap me to toggle the side bar [Ctrl + B].&#10;👌 Grab and drag me if you need ⇢ more space for the type selection."></button>
<div id="output" class="grow" data-title="🧜 I'm not your basic diagram.&#10;👆 Tap my types to toggle them.&#10;&#10;🔍 Zoom me with [Ctrl + mouse wheel].&#10;👌 Grab and drag me around to pan after.&#10;🧽 Reset zoom and pan with [Ctrl + 0]."></div>
</div>
<div id="about" class="flx col gap" title="🐙 build info and project links">
<div id="toaster" class="flx col gap"></div>
<div class="build-info flx">
<div id="build-info" class="scndry horizontal collapse flx col">
<span>built from {{SourceAssemblyName}} v{{SourceAssemblyVersion}} and mermaid.js from CDN
<a target="_blank" href="https://cdn.jsdelivr.net/npm/mermaid@11.4.0/dist/mermaid.min.js" download="mermaid.min.js"
title="For off-line use, download a copy and save it in the diagrammer folder. At the bottom of the index.html you'll find a script with a reference to the mermaid CDN. Replace its 'src' with the file name of your local copy, e.g. 'mermaid.min.js'.">📥</a>
</span>
<span>
using <a class="project" target="_blank" href="{{RepoUrl}}#readme" title="🤿 get learned and find out about or 🔱 fork the project">ICSharpCode.ILSpyX</a> v{{BuilderVersion}}
<a target="_blank" href="{{RepoUrl}}/wiki/Diagramming" title="the manual">📜</a>
<a target="_blank" href="{{RepoUrl}}/discussions" title="🤔 ask questions, share and discuss 💡 ideas">💬</a>
<a target="_blank" href="{{RepoUrl}}/issues" title="🦟 feed bugs to the fishes and request 🌱 new features"><span class="mano-a-borsa"></span></a>
<a target="_blank" href="{{RepoUrl}}/releases/latest" title="☄ download the latest bits to 🌊 generate better diagrammers">🌩</a>
</span>
</div>
<img data-toggles="#build-info" src="ILSpy.ico" />
</div>
</div>
<div id="pressed-keys" class="hidden"></div>
<div id="mouse" hidden></div>
<script id="model" type="application/json">{{Model}}</script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.0/dist/mermaid.min.js"></script>
<script src="script.js"></script>
</body>
</html>
Loading…
Cancel
Save