Browse Source

added mermaid class diagrammer

contributed from https://github.com/h0lg/netAmermaid - find earlier git history there
pull/3324/head
Holger Schmidt 8 months ago
parent
commit
40160f772d
  1. 15
      ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj
  2. 44
      ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs
  3. 114
      ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs
  4. 98
      ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs
  5. 55
      ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs
  6. 61
      ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs
  7. 118
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs
  8. 88
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs
  9. 124
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs
  10. 84
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs
  11. 74
      ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs
  12. 45
      ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs
  13. 147
      ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs
  14. 91
      ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs
  15. 16
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js
  16. 4
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore
  17. 51
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json
  18. 66
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js
  19. 7
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json
  20. 1135
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js
  21. 453
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css
  22. 586
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less
  23. 194
      ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html

15
ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj

@ -65,6 +65,21 @@ @@ -65,6 +65,21 @@
</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="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" />

44
ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
// 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.Diagnostics;
using System.Reflection;
namespace ICSharpCode.ILSpyX.MermaidDiagrammer
{
internal static class AssemblyInfo
{
internal static readonly string Location;
internal static readonly string? Version;
static AssemblyInfo()
{
Assembly assembly = Assembly.GetExecutingAssembly();
Location = assembly.Location;
var version = assembly.GetName().Version?.ToString();
Version = version == null ? null : version.Remove(version.LastIndexOf('.'));
}
internal static string? GetProductVersion()
{
try
{ return FileVersionInfo.GetVersionInfo(Location).ProductVersion ?? Version; }
catch { return Version; }
}
}
}

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; }
}
}
}
}

98
ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
// 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;
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(include, RegexOptions.Compiled),
exclude == null ? null : new(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();
}
}

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/h0lg/netAmermaid";
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; }
}
}

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

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
// 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 = AssemblyInfo.Version,
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)
{
var htmlSourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "html");
string modelJson = SerializeModel(model);
var outputFolder = OutputFolder ??
/* If no out folder is specified and export mode is JsonOnly,
* default to the HTML diagrammer source folder - that's where it's most likely used.
* Otherwise default to a "netAmermaid" folder next to the input assembly. */
(JsonOnly ? htmlSourcePath : Path.Combine(Path.GetDirectoryName(assemblyPath) ?? string.Empty, "netAmermaid"));
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 = File.ReadAllText(Path.Combine(htmlSourcePath, "template.html"));
var html = htmlTemplate
.Replace("{{SourceAssemblyName}}", model.SourceAssemblyName)
.Replace("{{SourceAssemblyVersion}}", model.SourceAssemblyVersion)
.Replace("{{BuilderVersion}}", AssemblyInfo.Version)
.Replace("{{RepoUrl}}", RepoUrl)
.Replace("{{Model}}", modelJson);
File.WriteAllText(Path.Combine(outputFolder, "class-diagrammer.html"), html);
// copy required resources to output folder while flattening paths if required
foreach (var path in new[] { "styles.css", "netAmermaid.ico", "script.js" })
File.Copy(Path.Combine(htmlSourcePath, path), Path.Combine(outputFolder, Path.GetFileName(path)), overwrite: true);
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;
}
}
}

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

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

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
{
// 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": [
"if (Test-Path '../bin/Release/net8.0/netAmermaid.exe') {",
" ../bin/Release/net8.0/netAmermaid.exe -a ../bin/Release/net8.0/netAmermaid.dll -n NetAmermaid System -j -o .",
"} else {",
" Write-Host 'netAmermaid.exe Release 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"
]
}
]
}

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"
}
}

1135
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;
}
#netAmermaid {
position: absolute;
bottom: 2em;
right: 2em;
align-items: end;
}
#netAmermaid #toaster {
margin-right: 2.8em;
}
#netAmermaid #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;
}
#netAmermaid #toaster span.leaving {
animation: 1s ease-in-out fadeOut;
}
#netAmermaid .build-info {
align-items: end;
height: 2.3em;
border-radius: 7px;
background-color: rgba(0, 0, 0, calc(3/16 * 3));
color: whitesmoke;
}
#netAmermaid .build-info > * {
height: 100%;
}
#netAmermaid .build-info #build-info {
text-align: right;
}
#netAmermaid .build-info #build-info > * {
padding: 0 0.5em;
}
#netAmermaid .build-info #build-info a {
color: whitesmoke;
}
#netAmermaid .build-info #build-info a:not(.project) {
text-decoration: none;
}
#netAmermaid .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,
#netAmermaid,
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;
}
}
}
#netAmermaid {
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, #netAmermaid, 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 - netAmermaid</title>
<link rel="icon" type="image/x-icon" href="netAmermaid.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="netAmermaid.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="netAmermaid" 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 with the diagrammer - at the bottom of which you find a script with a reference to the mermaid CDN. Replace its 'src' with the path to your local copy.">📥</a>
</span>
<span>
using <a class="project" target="_blank" href="{{RepoUrl}}#readme" title="🤿 get learned and find out about or 🔱 fork the project">netAmermaid</a> v{{BuilderVersion}}
<a target="_blank" href="{{RepoUrl}}/tree/v{{BuilderVersion}}#readme" title="the manual for v{{BuilderVersion}}">📜</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="netAmermaid.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