From 40160f772d318e6896990b442e4dced94741e38b Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Wed, 6 Nov 2024 19:47:36 +0100 Subject: [PATCH 01/22] added mermaid class diagrammer contributed from https://github.com/h0lg/netAmermaid - find earlier git history there --- ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj | 15 + .../MermaidDiagrammer/AssemblyInfo.cs | 44 + .../MermaidDiagrammer/ClassDiagrammer.cs | 114 ++ .../ClassDiagrammerFactory.cs | 98 ++ .../Extensions/StringExtensions.cs | 55 + .../Extensions/TypeExtensions.cs | 61 + .../MermaidDiagrammer/Factory.BuildTypes.cs | 118 ++ .../MermaidDiagrammer/Factory.FlatMembers.cs | 88 ++ .../Factory.Relationships.cs | 124 ++ .../MermaidDiagrammer/Factory.TypeIds.cs | 84 ++ .../MermaidDiagrammer/Factory.TypeNames.cs | 74 ++ .../GenerateHtmlDiagrammer.cs | 45 + .../MermaidDiagrammer/Generator.Run.cs | 147 +++ .../XmlDocumentationFormatter.cs | 91 ++ .../MermaidDiagrammer/html/.eslintrc.js | 16 + .../MermaidDiagrammer/html/.gitignore | 4 + .../MermaidDiagrammer/html/.vscode/tasks.json | 51 + .../MermaidDiagrammer/html/gulpfile.js | 66 + .../MermaidDiagrammer/html/package.json | 7 + .../MermaidDiagrammer/html/script.js | 1135 +++++++++++++++++ .../MermaidDiagrammer/html/styles.css | 453 +++++++ .../MermaidDiagrammer/html/styles.less | 586 +++++++++ .../MermaidDiagrammer/html/template.html | 194 +++ 23 files changed, 3670 insertions(+) create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html diff --git a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj index 6ee7581b98..3e83e0a0ca 100644 --- a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj +++ b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj @@ -65,6 +65,21 @@ + + + + + + + + + + + + + + + diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs new file mode 100644 index 0000000000..58680250bc --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs @@ -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; } + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs new file mode 100644 index 0000000000..bcf3072d6d --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammer.cs @@ -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 +{ + /// Contains type info and metadata for generating a HTML class diagrammer from a source assembly. + /// Serialized into JSON by . + public sealed class ClassDiagrammer + { + internal const string NewLine = "\n"; + + internal string SourceAssemblyName { get; set; } = null!; + internal string SourceAssemblyVersion { get; set; } = null!; + + /// Types selectable in the diagrammer, grouped by their + /// to facilitate a structured type selection. + internal Dictionary TypesByNamespace { get; set; } = null!; + + /// Types not included in the , + /// but referenced by s that are. + /// Contains display names (values; similar to ) + /// by their referenced IDs (keys; similar to ). + internal Dictionary OutsideReferences { get; set; } = null!; + + /// Types excluded from the ; + /// used to support . + internal string[] Excluded { get; set; } = null!; + + /// A -like structure with collections + /// of property relations to one or many other s. + public abstract class Relationships + { + /// Relations to zero or one other instances of s included in the , + /// with the display member names as keys and the related as values. + /// This is because member names must be unique within the owning , + /// while the related may be the same for multiple properties. + public Dictionary? HasOne { get; set; } + + /// Relations to zero to infinite other instances of s included in the , + /// with the display member names as keys and the related as values. + /// This is because member names must be unique within the owning , + /// while the related may be the same for multiple properties. + public Dictionary? HasMany { get; set; } + } + + /// The mermaid class diagram definition, inheritance and relationships metadata + /// and XML documentation for a from the source assembly. + public sealed class Type : Relationships + { + /// Uniquely identifies the 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. + internal string Id { get; set; } = null!; + + /// The human-readable label for the type, if different from . + /// Not guaranteed to be unique in the scope of the . + public string? Name { get; set; } + + /// 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 . + public string Body { get; set; } = null!; + + /// The base type directly implemented by this type, with the as key + /// and the (optional) differing display name as value of the single entry + /// - or null if the base type is . + /// Yes, Christopher Lambert, there can only be one. For now. + /// But using the same interface as for is convenient + /// and who knows - at some point the .Net bus may roll up with multi-inheritance. + /// Then this'll look visionary! + public Dictionary? BaseType { get; set; } + + /// Interfaces directly implemented by this type, with their as keys + /// and their (optional) differing display names as values. + public Dictionary? Interfaces { get; set; } + + /// Contains inherited members by the of their + /// for the consumer to choose which of them to display in an inheritance scenario. + public IDictionary? Inherited { get; set; } + + /// Contains the XML documentation comments for this type + /// (using a key) and its members, if available. + public IDictionary? XmlDocs { get; set; } + + /// Members inherited from an ancestor type specified by the Key of . + public class InheritedMembers : Relationships + { + /// The simple, non-complex members inherited from another + /// in mermaid class diagram syntax. + public string? FlatMembers { get; set; } + } + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs new file mode 100644 index 0000000000..6834160b78 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs @@ -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 */ + + /// Produces mermaid class diagram syntax for a filtered list of types from a specified .Net assembly. + public partial class ClassDiagrammerFactory + { + private readonly XmlDocumentationFormatter? xmlDocs; + private readonly DecompilerSettings decompilerSettings; + + private ITypeDefinition[]? selectedTypes; + private Dictionary? uniqueIds; + private Dictionary? labels; + private Dictionary? 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 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 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 + }; + } + + /// The default strategy for pre-filtering the available in the HTML diagrammer. + /// Applies as well as + /// matching by and not by . + /// The types to effectively include in the HTML diagrammer. + protected virtual IEnumerable FilterTypes(IEnumerable 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 + + /// The strategy for deciding whether a should be included + /// in the HTML diagrammer by default. Excludes compiler-generated and their nested types. + protected virtual bool IsIncludedByDefault(ITypeDefinition type) + => !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(); + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..49b9302e10 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/StringExtensions.cs @@ -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 + { + /// Replaces all consecutive horizontal white space characters in + /// with while leaving line breaks intact. + internal static string NormalizeHorizontalWhiteSpace(this string input, string normalizeTo = " ") + => Regex.Replace(input, @"[ \t]+", normalizeTo); + + /// Replaces all occurrences of in + /// with . + internal static string ReplaceAll(this string input, IEnumerable oldValues, string? newValue) + => oldValues.Aggregate(input, (aggregate, oldValue) => aggregate.Replace(oldValue, newValue)); + + /// Joins the specified to a single one + /// using the specified as a delimiter. + /// Whether to pad the start and end of the string with the as well. + internal static string Join(this IEnumerable? 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; + } + + /// Formats all items in using the supplied strategy + /// and returns a string collection - even if the incoming is null. + internal static IEnumerable FormatAll(this IEnumerable? collection, Func format) + => collection?.Select(format) ?? []; + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs new file mode 100644 index 0000000000..6432c408af --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Extensions/TypeExtensions.cs @@ -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 + { + /// Groups the into a dictionary + /// with keys. + internal static Dictionary GroupByDeclaringType(this IEnumerable members) where T : IMember + => members.GroupByDeclaringType(m => m); + + /// Groups the into a dictionary + /// with keys using . + internal static Dictionary GroupByDeclaringType(this IEnumerable objectsWithMembers, Func getMember) + => objectsWithMembers.GroupBy(m => getMember(m).DeclaringType).ToDictionary(g => g.Key, g => g.ToArray()); + } + + internal static class DictionaryExtensions + { + /// Returns the s value for the specified + /// if available and otherwise the default for . + internal static Tout? GetValue(this IDictionary dictionary, T key) + => dictionary.TryGetValue(key, out Tout? value) ? value : default; + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs new file mode 100644 index 0000000000..79f4bb86e4 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.BuildTypes.cs @@ -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? docs = xmlDocs?.GetXmlDocs(type, fields); + string name = GetName(type), typeId = GetId(type); + + var body = fields.Select(f => f.Name).Prepend("<>") + .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 flatPropertiesByType = properties.Except(hasOneRelations) + .Except(hasManyRelations.Select(r => r.property)).GroupByDeclaringType(); + + Dictionary hasOneRelationsByType = hasOneRelations.GroupByDeclaringType(); + Dictionary hasManyRelationsByType = hasManyRelations.GroupByDeclaringType(r => r.property); + Dictionary fieldsByType = fields.GroupByDeclaringType(); + Dictionary 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? 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 inheritedMembersByType = type.GetNonInterfaceBaseTypes().Where(t => t != type && !t.IsObject()) + // and group inherited members by declaring type + .ToDictionary(GetId, t => { + IEnumerable 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 + }; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs new file mode 100644 index 0000000000..398b6c96ce --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.FlatMembers.cs @@ -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 + { + /// Wraps a method configurable via + /// that can be used to determine whether a member should be hidden. + 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 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, + }; + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs new file mode 100644 index 0000000000..14d7725983 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.Relationships.cs @@ -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(); + + /// Returns the relevant direct super type the inherits from + /// in a format matching . + private Dictionary? 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); + } + + /// Returns the direct interfaces implemented by + /// in a format matching . + private Dictionary? 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()); + } + + /// Returns the one-to-one relations from to other s + /// in a format matching . + private Dictionary? MapHasOneRelations(Dictionary 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); + + /// Returns the one-to-many relations from to other s + /// in a format matching . + private Dictionary? MapHasManyRelations(Dictionary 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); + + /// Builds references to super types and (one/many) relations, + /// recording outside references on the way and applying labels if required. + /// The type to reference. + /// Used only for property one/many relations. + 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)); + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs new file mode 100644 index 0000000000..cb8226c33a --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeIds.cs @@ -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 + { + /// Generates a dictionary of unique and short, but human readable identifiers for + /// to be able to safely reference them in any combination. + private static Dictionary GenerateUniqueIds(IEnumerable types) + { + Dictionary 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; + + /// For a non- or open generic , returns a unique identifier and null. + /// For a closed generic , returns the open generic type and the unique identifier of it. + /// That helps connecting closed generic references (e.g. Store<int>) to their corresponding + /// open generic (e.g. Store<T>) like in . + 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 + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs new file mode 100644 index 0000000000..a8997df6de --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Factory.TypeNames.cs @@ -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 + { + /// Returns a cached display name for . + 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 + } + + /// Generates a display name for . + 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}❱"; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs new file mode 100644 index 0000000000..c25892ae75 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs @@ -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 +{ + /// 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 . + 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; } + + /// Namespaces to strip from . + /// Implemented as a list of exact replacements instead of a single, more powerful RegEx because replacement in + /// + /// 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. + public IEnumerable? StrippedNamespaces { get; set; } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs new file mode 100644 index 0000000000..c01f07b731 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs new file mode 100644 index 0000000000..6b5aef5686 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/XmlDocumentationFormatter.cs @@ -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 +{ + /// Wraps the 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 . + public class XmlDocumentationFormatter + { + /// Matches XML indent. + protected const string linePadding = @"^[ \t]+|[ \t]+$"; + + /// 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. + 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 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? GetXmlDocs(ITypeDefinition type, params IMember[][] memberCollections) + { + Dictionary? 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(["", ""], null) + .ReplaceAll(["", ""], 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 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; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js new file mode 100644 index 0000000000..3ef10a6461 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.eslintrc.js @@ -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'] + } +}; \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore new file mode 100644 index 0000000000..1922e8652e --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/class-diagrammer.html +/model.json +/package-lock.json diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json new file mode 100644 index 0000000000..361bb32e5e --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json @@ -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" + ] + } + ] +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js new file mode 100644 index 0000000000..0fd2112d4b --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/gulpfile.js @@ -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 +}); diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json new file mode 100644 index 0000000000..b5d722e210 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "eslint": "^8.57.1", + "gulp": "^4.0.2", + "gulp-less": "^5.0.0" + } +} diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js new file mode 100644 index 0000000000..824d5d221c --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js @@ -0,0 +1,1135 @@ +/*globals mermaid:false*/ +(async () => { + const getById = id => document.getElementById(id), + triggerChangeOn = element => { element.dispatchEvent(new Event('change')); }, + hasProperty = (obj, name) => Object.prototype.hasOwnProperty.call(obj, name); + + const checkable = (() => { + const checked = ':checked', + inputsByName = name => `input[name=${name}]`, + getInput = (name, filter, context) => (context || document).querySelector(inputsByName(name) + filter), + getInputs = (name, context) => (context || document).querySelectorAll(inputsByName(name)); + + return { + getValue: (name, context) => getInput(name, checked, context).value, + + onChange: (name, handle, context) => { + for (let input of getInputs(name, context)) input.onchange = handle; + }, + + setChecked: (name, value, triggerChange, context) => { + const input = getInput(name, `[value="${value}"]`, context); + input.checked = true; + if (triggerChange !== false) triggerChangeOn(input); + } + }; + })(); + + const collapse = (() => { + const open = 'open', + isOpen = element => element.classList.contains(open), + + /** Toggles the open class on the collapse. + * @param {HTMLElement} element The collapse to toggle. + * @param {boolean} force The state to force. */ + toggle = (element, force) => element.classList.toggle(open, force); + + return { + toggle, + + open: element => { + if (isOpen(element)) return false; // return whether collapse was opened by this process + return toggle(element, true); + }, + + initToggles: () => { + for (let trigger of [...document.querySelectorAll('.toggle[href],[data-toggles]')]) { + trigger.addEventListener('click', event => { + event.preventDefault(); // to avoid pop-state event + const trigger = event.currentTarget; + trigger.ariaExpanded = !(trigger.ariaExpanded === 'true'); + toggle(document.querySelector(trigger.attributes.href?.value || trigger.dataset.toggles)); + }); + } + } + }; + })(); + + const notify = (() => { + const toaster = getById('toaster'); + + return message => { + const toast = document.createElement('span'); + toast.innerText = message; + toaster.appendChild(toast); // fades in the message + + setTimeout(() => { + toast.classList.add('leaving'); // fades out the message + + // ...and removes it. Note this timeout has to match the animation duration for '.leaving' in the .less file. + setTimeout(() => { toast.remove(); }, 1000); + }, 5000); + }; + })(); + + const output = (function () { + const output = getById('output'), + hasSVG = () => output.childElementCount > 0, + getSVG = () => hasSVG() ? output.children[0] : null, + + updateSvgViewBox = (svg, viewBox) => { + if (svg.originalViewBox === undefined) { + const vb = svg.viewBox.baseVal; + svg.originalViewBox = { x: vb.x, y: vb.y, width: vb.width, height: vb.height, }; + } + + svg.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`); + }; + + // enable zooming SVG using Ctrl + mouse wheel + const zoomFactor = 0.1, panFactor = 2023; // to go with the Zeitgeist + + output.addEventListener('wheel', event => { + if (!event.ctrlKey || !hasSVG()) return; + event.preventDefault(); + + const svg = getSVG(), + delta = event.deltaY < 0 ? 1 : -1, + zoomDelta = 1 + zoomFactor * delta, + viewBox = svg.viewBox.baseVal; + + viewBox.width *= zoomDelta; + viewBox.height *= zoomDelta; + updateSvgViewBox(svg, viewBox); + }); + + // enable panning SVG by grabbing and dragging + let isPanning = false, panStartX = 0, panStartY = 0; + + output.addEventListener('mousedown', event => { + isPanning = true; + panStartX = event.clientX; + panStartY = event.clientY; + }); + + output.addEventListener('mouseup', () => { isPanning = false; }); + + output.addEventListener('mousemove', event => { + if (!isPanning || !hasSVG()) return; + event.preventDefault(); + + const svg = getSVG(), + viewBox = svg.viewBox.baseVal, + dx = event.clientX - panStartX, + dy = event.clientY - panStartY; + + viewBox.x -= dx * panFactor / viewBox.width; + viewBox.y -= dy * panFactor / viewBox.height; + panStartX = event.clientX; + panStartY = event.clientY; + updateSvgViewBox(svg, viewBox); + }); + + return { + getDiagramTitle: () => output.dataset.title, + setSVG: svg => { output.innerHTML = svg; }, + getSVG, + + resetZoomAndPan: () => { + const svg = getSVG(); + if (svg !== null) updateSvgViewBox(svg, svg.originalViewBox); + } + }; + })(); + + const mermaidExtensions = (() => { + + const logLevel = (() => { + /* int indexes as well as string values can identify a valid log level; + see log levels and logger definition at https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/logger.ts . + Note the names correspond to console output methods https://developer.mozilla.org/en-US/docs/Web/API/console .*/ + const names = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], + maxIndex = names.length - 1, + + getIndex = level => { + const index = Number.isInteger(level) ? level : names.indexOf(level); + return index < 0 ? maxIndex : Math.min(index, maxIndex); // normalize, but return maxIndex (i.e. lowest level) by default + }; + + let requested; // the log level index of the in-coming config or the default + + return { + /** Sets the desired log level. + * @param {string|int} level The name or index of the desired log level. */ + setRequested: level => { requested = getIndex(level); }, + + /** Returns all names above (not including) the given level. + * @param {int} level The excluded lower boundary log level index (not name). + * @returns an array. */ + above: level => names.slice(level + 1), + + /** Indicates whether the log level is configured to be enabled. + * @param {string|int} level The log level to test. + * @returns a boolean. */ + isEnabled: level => requested <= getIndex(level) + }; + })(); + + /** Calculates the shortest distance in pixels between a point + * represented by 'top' and 'left' and the closest side of an axis-aligned rectangle. + * Returns 0 if the point is inside or on the edge of the rectangle. + * Inspired by https://gamedev.stackexchange.com/a/50722 . + * @param {int} top The distance of the point from the top of the viewport. + * @param {int} left The distance of the point from the left of the viewport. + * @param {DOMRect} rect The bounding box to get the distance to. + * @returns {int} The distance of the outside point or 0. */ + function getDistanceToRect(top, left, rect) { + const dx = Math.max(rect.left, Math.min(left, rect.right)), + dy = Math.max(rect.top, Math.min(top, rect.bottom)); + + return Math.sqrt((left - dx) * (left - dx) + (top - dy) * (top - dy)); + } + + /** Calculates the distance between two non-overlapping axis-aligned rectangles. + * Returns 0 if the rectangles touch or overlap. + * @param {DOMRect} a The first bounding box. + * @param {DOMRect} b The second bounding box. + * @returns {int} The distance between the two bounding boxes or 0 if they touch or overlap. */ + function getDistance(a, b) { + /** Gets coordinate pairs for the corners of a rectangle r. + * @param {DOMRect} r the rectangle. + * @returns {Array}} */ + const getCorners = r => [[r.top, r.left], [r.top, r.right], [r.bottom, r.left], [r.bottom, r.right]], + /** Gets the distances of the corners of rectA to rectB. */ + getCornerDistances = (rectA, rectB) => getCorners(rectA).map(c => getDistanceToRect(c[0], c[1], rectB)), + aRect = a.getBoundingClientRect(), + bRect = b.getBoundingClientRect(), + cornerDistances = getCornerDistances(aRect, bRect).concat(getCornerDistances(bRect, aRect)); + + return Math.min(...cornerDistances); + } + + function interceptConsole(interceptorsByLevel) { + const originals = {}; + + for (let [level, interceptor] of Object.entries(interceptorsByLevel)) { + if (typeof console[level] !== 'function') continue; + originals[level] = console[level]; + console[level] = function () { interceptor.call(this, originals[level], arguments); }; + } + + return () => { // call to detach interceptors + for (let [level, original] of Object.entries(originals)) + console[level] = original; + }; + } + + let renderedEdges = [], // contains info about the arrows between types on the diagram once rendered + lastRenderedDiagram; + + function getRelationLabels(svg, typeId) { + const edgeLabels = [...svg.querySelectorAll('.edgeLabels span.edgeLabel span')], + extension = 'extension'; + + return renderedEdges.filter(e => e.v === typeId // type name needs to match + && e.value.arrowTypeStart !== extension && e.value.arrowTypeEnd !== extension) // exclude inheritance arrows + .map(edge => { + const labelHtml = edge.value.label, + // filter edge labels with matching HTML + labels = edgeLabels.filter(l => l.outerHTML === labelHtml); + + if (labels.length === 1) return labels[0]; // return the only matching label + else if (labels.length < 1) console.error( + "Tried to find a relation label for the following edge (by its value.label) but couldn't.", edge); + else { // there are multiple edge labels with the same HTML (i.e. matching relation name) + // find the path that is rendered for the edge + const path = svg.querySelector('.edgePaths>path.relation#' + edge.value.id), + labelsByDistance = labels.sort((a, b) => getDistance(path, a) - getDistance(path, b)); + + console.warn('Found multiple relation labels matching the following edge (by its value.label). Returning the closest/first.', + edge, labelsByDistance); + + return labelsByDistance[0]; // and return the matching label closest to it + } + }); + } + + return { + init: config => { + + /* Override console.info to intercept a message posted by mermaid including information about the edges + (represented by arrows between types in the rendered diagram) to access the relationship info + parsed from the diagram descriptions of selected types. + This works around the mermaid API currently not providing access to this information + and it being hard to reconstruct from the rendered SVG alone. + Why do we need that info? Knowing about the relationships between types, we can find the label + corresponding to a relation and attach XML documentation information to it, if available. + See how getRelationLabels is used. */ + const requiredLevel = 2, // to enable intercepting info message + + interceptors = { + info: function (overridden, args) { + // intercept message containing rendered edges + if (args[2] === 'Graph in recursive render: XXX') renderedEdges = args[3].edges; + + // only forward to overridden method if this log level was originally enabled + if (logLevel.isEnabled(requiredLevel)) overridden.call(this, ...args); + } + }; + + logLevel.setRequested(config.logLevel); // remember original log level + + // lower configured log level if required to guarantee above interceptor gets called + if (!logLevel.isEnabled(requiredLevel)) config.logLevel = requiredLevel; + + // suppress console output for higher log levels accidentally activated by lowering to required level + for (let level of logLevel.above(requiredLevel)) + if (!logLevel.isEnabled(level)) interceptors[level] = () => { }; + + const detachInterceptors = interceptConsole(interceptors); // attaches console interceptors + mermaid.initialize(config); // init the mermaid sub-system with interceptors in place + detachInterceptors(); // to avoid intercepting messages outside of that context we're not interested in + }, + + /** Processes the type selection into mermaid diagram syntax (and the corresponding XML documentation data, if available). + * @param {object} typeDetails An object with the IDs of types to display in detail (i.e. with members) for keys + * and objects with the data structure of ClassDiagrammer.Type (excluding the Id) for values. + * @param {function} getTypeLabel A strategy for getting the type label for a type ID. + * @param {string} direction The layout direction of the resulting diagram. + * @param {object} showInherited A regular expression matching things to exclude from the diagram definition. + * @returns {object} An object like { diagram, detailedTypes, xmlDocs } with 'diagram' being the mermaid diagram syntax, + * 'xmlDocs' the corresponding XML documentation to be injected into the rendered diagram in the 'postProcess' step and + * 'detailedTypes' being a flat list of IDs of types that will be rendered in detail (including their members and relations). */ + processTypes: (typeDetails, getTypeLabel, direction, showInherited) => { + const detailedTypes = Object.keys(typeDetails), // types that will be rendered including their members and relations + xmlDocs = {}, // to be appended with docs of selected types below + getAncestorTypes = typeDetails => Object.keys(typeDetails.Inherited), + isRendered = type => detailedTypes.includes(type), + + mayNeedLabelling = new Set(), + + cleanUpDiagramMmd = mmd => mmd.replace(/(\r?\n){3,}/g, '\n\n'), // squash more than two consecutive line breaks down into two + + // renders base type and interfaces depending on settings and selected types + renderSuperType = (supertTypeId, link, typeId, name, displayAll) => { + /* display relation arrow if either the user chose to display this kind of super type + or the super type is selected to be rendered anyway and we might as well for completeness */ + if (displayAll || isRendered(supertTypeId)) { + const label = name ? ' : ' + name : ''; + diagram += `${supertTypeId} <|${link} ${typeId}${label}\n`; + mayNeedLabelling.add(supertTypeId); + } + }, + + // renders HasOne and HasMany relations + renderRelations = (typeId, relations, many) => { + if (relations) // expecting object; only process if not null or undefined + for (let [label, relatedId] of Object.entries(relations)) { + const nullable = label.endsWith(' ?'); + const cardinality = many ? '"*" ' : nullable ? '"?" ' : ''; + if (nullable) label = label.substring(0, label.length - 2); // nullability is expressed via cardinality + diagram += `${typeId} --> ${cardinality}${relatedId} : ${label}\n`; + mayNeedLabelling.add(relatedId); + } + }, + + renderInheritedMembers = (typeId, details) => { + const ancestorTypes = getAncestorTypes(details); + + // only include inherited members in sub classes if they aren't already rendered in a super class + for (let [ancestorType, members] of Object.entries(details.Inherited)) { + if (isRendered(ancestorType)) continue; // inherited members will be rendered in base type + + let ancestorsOfDetailedAncestors = ancestorTypes.filter(t => detailedTypes.includes(t)) // get detailed ancestor types + .map(type => getAncestorTypes(typeDetails[type])) // select their ancestor types + .reduce((union, ancestors) => union.concat(ancestors), []); // squash them into a one-dimensional array (ignoring duplicates) + + // skip displaying inherited members already displayed by detailed ancestor types + if (ancestorsOfDetailedAncestors.includes(ancestorType)) continue; + + diagram += members.FlatMembers + '\n'; + renderRelations(typeId, members.HasOne); + renderRelations(typeId, members.HasMany, true); + } + }; + + // init diagram code with header and layout direction to be appended to below + let diagram = 'classDiagram' + '\n' + + 'direction ' + direction + '\n\n'; + + // process selected types + for (let [typeId, details] of Object.entries(typeDetails)) { + mayNeedLabelling.add(typeId); + diagram += details.Body + '\n\n'; + + if (details.BaseType) // expecting object; only process if not null or undefined + for (let [baseTypeId, label] of Object.entries(details.BaseType)) + renderSuperType(baseTypeId, '--', typeId, label, showInherited.types); + + if (details.Interfaces) // expecting object; only process if not null or undefined + for (let [ifaceId, labels] of Object.entries(details.Interfaces)) + for (let label of labels) + renderSuperType(ifaceId, '..', typeId, label, showInherited.interfaces); + + renderRelations(typeId, details.HasOne); + renderRelations(typeId, details.HasMany, true); + xmlDocs[typeId] = details.XmlDocs; + if (showInherited.members && details.Inherited) renderInheritedMembers(typeId, details); + } + + for (let typeId of mayNeedLabelling) { + const label = getTypeLabel(typeId); + if (label !== typeId) diagram += `class ${typeId} ["${label}"]\n`; + } + + diagram = cleanUpDiagramMmd(diagram); + lastRenderedDiagram = diagram; // store diagram syntax for export + return { diagram, detailedTypes, xmlDocs }; + }, + + getDiagram: () => lastRenderedDiagram, + + /** Enhances the SVG rendered by mermaid by injecting xmlDocs if available + * and attaching type click handlers, if available. + * @param {SVGElement} svg The SVG containing the rendered mermaid diagram. + * @param {object} options An object like { xmlDocs, onTypeClick } + * with 'xmlDocs' being the XML docs by type ID + * and 'onTypeClick' being an event listener for the click event + * that gets the event and the typeId as parameters. */ + postProcess: (svg, options) => { + // matches 'MyClass2' from generated id attributes in the form of 'classId-MyClass2-0' + const typeIdFromDomId = /(?<=classId-)\w+(?=-\d+)/; + + for (let entity of svg.querySelectorAll('g.nodes>g.node').values()) { + const typeId = typeIdFromDomId.exec(entity.id)[0]; + + // clone to have a modifiable collection without affecting the original + const docs = structuredClone((options.xmlDocs || [])[typeId]); + + // splice in XML documentation as label titles if available + if (docs) { + const typeKey = '', nodeLabel = 'span.nodeLabel', + title = entity.querySelector('.label-group'), + relationLabels = getRelationLabels(svg, typeId), + + setDocs = (label, member) => { + label.title = docs[member]; + delete docs[member]; + }, + + documentOwnLabel = (label, member) => { + setDocs(label, member); + ownLabels = ownLabels.filter(l => l !== label); // remove label + }; + + let ownLabels = [...entity.querySelectorAll('g.label ' + nodeLabel)]; + + // document the type label itself + if (hasProperty(docs, typeKey)) documentOwnLabel(title.querySelector(nodeLabel), typeKey); + + // loop through documented members longest name first + for (let member of Object.keys(docs).sort((a, b) => b.length - a.length)) { + // matches only whole words in front of method signatures starting with ( + const memberName = new RegExp(`(? memberName.test(l.textContent)), + related = relationLabels.find(l => l.textContent === member); + + if (related) matchingLabels.push(related); + if (matchingLabels.length === 0) continue; // members may be rendered in an ancestor type + + if (matchingLabels.length > 1) console.warn( + `Expected to find one member or relation label for ${title.textContent}.${member}` + + ' to attach the XML documentation to but found multiple. Applying the first.', matchingLabels); + + documentOwnLabel(matchingLabels[0], member); + } + } + + if (typeof options.onTypeClick === 'function') entity.addEventListener('click', + function (event) { options.onTypeClick.call(this, event, typeId); }); + } + } + }; + })(); + + const state = (() => { + const typeUrlDelimiter = '-', + originalTitle = document.head.getElementsByTagName('title')[0].textContent; + + const restore = async data => { + if (data.d) layoutDirection.set(data.d); + + if (data.t) { + inheritanceFilter.setFlagHash(data.i || ''); // if types are set, enable deselecting all options + typeSelector.setSelected(data.t.split(typeUrlDelimiter)); + await render(true); + } + }; + + function updateQueryString(href, params) { + // see https://developer.mozilla.org/en-US/docs/Web/API/URL + const url = new URL(href), search = url.searchParams; + + for (const [name, value] of Object.entries(params)) { + //see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + if (value === null || value === undefined || value === '') search.delete(name); + else if (Array.isArray(value)) { + search.delete(name); + for (let item of value) search.append(name, item); + } + else search.set(name, value); + } + + url.search = search.toString(); + return url.href; + } + + window.onpopstate = async event => { await restore(event.state); }; + + return { + update: () => { + const types = typeSelector.getSelected(), + t = Object.keys(types).join(typeUrlDelimiter), + d = layoutDirection.get(), + i = inheritanceFilter.getFlagHash(), + data = { t, d, i }, + typeNames = Object.values(types).map(t => t.Name); + + history.pushState(data, '', updateQueryString(location.href, data)); + + // record selected types in title so users see which selection they return to when using a history link + document.title = (typeNames.length ? typeNames.join(', ') + ' - ' : '') + originalTitle; + }, + restore: async () => { + if (!location.search) return; // assume fresh open and don't try to restore state, preventing inheritance options from being unset + const search = new URLSearchParams(location.search); + await restore({ d: search.get('d'), i: search.get('i'), t: search.get('t') }); + } + }; + })(); + + const typeSelector = (() => { + const select = getById('type-select'), + preFilter = getById('pre-filter-types'), + renderBtn = getById('render'), + model = JSON.parse(getById('model').innerHTML), + tags = { optgroup: 'OPTGROUP', option: 'option' }, + getNamespace = option => option.parentElement.nodeName === tags.optgroup ? option.parentElement.label : '', + getOption = typeId => select.querySelector(tags.option + `[value='${typeId}']`); + + // fill select list + for (let [namespace, types] of Object.entries(model.TypesByNamespace)) { + let optionParent; + + if (namespace) { + const group = document.createElement(tags.optgroup); + group.label = namespace; + select.appendChild(group); + optionParent = group; + } else optionParent = select; + + for (let typeId of Object.keys(types)) { + const type = types[typeId], + option = document.createElement(tags.option); + + option.value = typeId; + if (!type.Name) type.Name = typeId; // set omitted label to complete structure + option.innerText = type.Name; + optionParent.appendChild(option); + } + } + + // only enable render button if types are selected + select.onchange = () => { renderBtn.disabled = select.selectedOptions.length < 1; }; + + preFilter.addEventListener('input', () => { + const regex = preFilter.value ? new RegExp(preFilter.value, 'i') : null; + + for (let option of select.options) + option.hidden = regex !== null && !regex.test(option.innerHTML); + + // toggle option groups hidden depending on whether they have visible children + for (let group of select.getElementsByTagName(tags.optgroup)) + group.hidden = regex !== null && [...group.children].filter(o => !o.hidden).length === 0; + }); + + return { + focus: () => select.focus(), + focusFilter: () => preFilter.focus(), + + setSelected: types => { + for (let option of select.options) + option.selected = types.includes(option.value); + + triggerChangeOn(select); + }, + + toggleOption: typeId => { + const option = getOption(typeId); + + if (option !== null) { + option.selected = !option.selected; + triggerChangeOn(select); + } + }, + + /** Returns the types selected by the user in the form of an object with the type IDs for keys + * and objects with the data structure of ClassDiagrammer.Type (excluding the Id) for values. */ + getSelected: () => Object.fromEntries([...select.selectedOptions].map(option => { + const namespace = getNamespace(option), typeId = option.value, + details = model.TypesByNamespace[namespace][typeId]; + + return [typeId, details]; + })), + + moveSelection: up => { + // inspired by https://stackoverflow.com/a/25851154 + for (let option of select.selectedOptions) { + if (up && option.previousElementSibling) { // move up + option.parentElement.insertBefore(option, option.previousElementSibling); + } else if (!up && option.nextElementSibling) { // move down + // see https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore + option.parentElement.insertBefore(option, option.nextElementSibling.nextElementSibling); + } + } + }, + + //TODO add method returning namespace to add to title + getLabel: typeId => { + const option = getOption(typeId); + return option ? option.innerText : model.OutsideReferences[typeId]; + } + }; + })(); + + const inheritanceFilter = (() => { + const baseType = getById('show-base-types'), + interfaces = getById('show-interfaces'), + members = getById('show-inherited-members'), + getFlags = () => { return { types: baseType.checked, interfaces: interfaces.checked, members: members.checked }; }; + + // automatically re-render on change + for (let checkbox of [baseType, interfaces, members]) + checkbox.onchange = async () => { await render(); }; + + return { + getFlags, + + getFlagHash: () => Object.entries(getFlags()) + .filter(([, value]) => value) // only true flags + .map(([key]) => key[0]).join(''), // first character of each flag + + setFlagHash: hash => { + baseType.checked = hash.includes('t'); + interfaces.checked = hash.includes('i'); + members.checked = hash.includes('m'); + } + }; + })(); + + const layoutDirection = (() => { + const inputName = 'direction'; + + // automatically re-render on change + checkable.onChange(inputName, async () => { await render(); }); + + return { + get: () => checkable.getValue(inputName), + set: (value, event) => { + const hasEvent = event !== undefined; + checkable.setChecked(inputName, value, hasEvent); + if (hasEvent) event.preventDefault(); + } + }; + })(); + + const render = async isRestoringState => { + const { diagram, detailedTypes, xmlDocs } = mermaidExtensions.processTypes( + typeSelector.getSelected(), typeSelector.getLabel, layoutDirection.get(), inheritanceFilter.getFlags()); + + console.info(diagram); + const titledDiagram = diagram + '\naccTitle: ' + output.getDiagramTitle().replaceAll('\n', '#10;') + '\n'; + + /* Renders response and deconstructs returned object because we're only interested in the svg. + Note that the ID supplied as the first argument must not match any existing element ID + unless you want its contents to be replaced. See https://mermaid.js.org/config/usage.html#api-usage */ + const { svg } = await mermaid.render('foo', titledDiagram); + output.setSVG(svg); + + mermaidExtensions.postProcess(output.getSVG(), { + xmlDocs, + + onTypeClick: async (event, typeId) => { + // toggle selection and re-render on clicking entity + typeSelector.toggleOption(typeId); + await render(); + } + }); + + exportOptions.enable(detailedTypes.length > 0); + if (!isRestoringState) state.update(); + }; + + const filterSidebar = (() => { + const filterForm = getById('filter'), + resizing = 'resizing', + toggleBtn = getById('filter-toggle'), + toggle = () => collapse.toggle(filterForm); + + // enable rendering by hitting Enter on filter form + filterForm.onsubmit = async (event) => { + event.preventDefault(); + await render(); + }; + + // enable adjusting max sidebar width + (() => { + const filterWidthOverride = getById('filter-width'), // a style tag dedicated to overriding the default filter max-width + minWidth = 210, maxWidth = window.innerWidth / 2; // limit the width of the sidebar + + let isDragging = false; // tracks whether the sidebar is being dragged + let pickedUp = 0; // remembers where the dragging started from + let widthBefore = 0; // remembers the width when dragging starts + let change = 0; // remembers the total distance of the drag + + toggleBtn.addEventListener('mousedown', (event) => { + isDragging = true; + pickedUp = event.clientX; + widthBefore = filterForm.offsetWidth; + }); + + document.addEventListener('mousemove', (event) => { + if (!isDragging) return; + + const delta = event.clientX - pickedUp, + newWidth = Math.max(minWidth, Math.min(maxWidth, widthBefore + delta)); + + change = delta; + filterForm.classList.add(resizing); + filterWidthOverride.innerHTML = `#filter.open { max-width: ${newWidth}px; }`; + }); + + document.addEventListener('mouseup', () => { + if (!isDragging) return; + isDragging = false; + filterForm.classList.remove(resizing); + }); + + // enable toggling filter info on click + toggleBtn.addEventListener('click', () => { + if (Math.abs(change) < 5) toggle(); // prevent toggling for small, accidental drags + change = 0; // reset the remembered distance to enable subsequent clicks + }); + })(); + + return { + toggle, + open: () => collapse.open(filterForm) + }; + })(); + + /* Shamelessly copied from https://github.com/mermaid-js/mermaid-live-editor/blob/develop/src/lib/components/Actions.svelte + with only a few modifications after I failed to get the solutions described here working: + https://stackoverflow.com/questions/28226677/save-inline-svg-as-jpeg-png-svg/28226736#28226736 + The closest I got was with this example https://canvg.js.org/examples/offscreen , but the shapes would remain empty. */ + const exporter = (() => { + const getSVGstring = (svg, width, height) => { + height && svg?.setAttribute('height', `${height}px`); + width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage + if (!svg) svg = getSvgEl(); + + return svg.outerHTML.replaceAll('
', '
') + .replaceAll(/]*)>/g, (m, g) => ``); + }; + + const toBase64 = utf8String => { + const bytes = new TextEncoder().encode(utf8String); + return window.btoa(String.fromCharCode.apply(null, bytes)); + }; + + const getBase64SVG = (svg, width, height) => toBase64(getSVGstring(svg, width, height)); + + const exportImage = (event, exporter, imagemodeselected, userimagesize) => { + const canvas = document.createElement('canvas'); + const svg = document.querySelector('#output svg'); + if (!svg) { + throw new Error('svg not found'); + } + const box = svg.getBoundingClientRect(); + canvas.width = box.width; + canvas.height = box.height; + if (imagemodeselected === 'width') { + const ratio = box.height / box.width; + canvas.width = userimagesize; + canvas.height = userimagesize * ratio; + } else if (imagemodeselected === 'height') { + const ratio = box.width / box.height; + canvas.width = userimagesize * ratio; + canvas.height = userimagesize; + } + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('context not found'); + } + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + const image = new Image(); + image.onload = exporter(context, image); + image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`; + event.stopPropagation(); + event.preventDefault(); + }; + + const getSvgEl = () => { + const svgEl = document.querySelector('#output svg').cloneNode(true); + svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link')) + .map((l) => l.href) + .find((h) => h.includes('font-awesome')); + if (fontAwesomeCdnUrl == null) { + return svgEl; + } + const styleEl = document.createElement('style'); + styleEl.innerText = `@import url("${fontAwesomeCdnUrl}");'`; + svgEl.prepend(styleEl); + return svgEl; + }; + + const simulateDownload = (download, href) => { + const a = document.createElement('a'); + a.download = download; + a.href = href; + a.click(); + a.remove(); + }; + + const downloadImage = (context, image) => { + return () => { + const { canvas } = context; + context.drawImage(image, 0, 0, canvas.width, canvas.height); + simulateDownload( + exportOptions.getFileName('png'), + canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') + ); + }; + }; + + const tryWriteToClipboard = blob => { + try { + if (!blob) throw new Error('blob is empty'); + void navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + const copyPNG = (context, image) => { + return () => { + const { canvas } = context; + context.drawImage(image, 0, 0, canvas.width, canvas.height); + canvas.toBlob(blob => { tryWriteToClipboard(blob); }); + }; + }; + + const tryWriteTextToClipboard = async text => { + try { + if (!text) throw new Error('text is empty'); + await navigator.clipboard.writeText(text); + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + const copyText = async (event, text) => { + if (await tryWriteTextToClipboard(text)) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + return { + isClipboardAvailable: () => hasProperty(window, 'ClipboardItem'), + onCopyPNG: (event, imagemodeselected, userimagesize) => { + exportImage(event, copyPNG, imagemodeselected, userimagesize); + }, + onCopySVG: event => { void copyText(event, getSVGstring()); }, + onCopyMMD: (event, diagram) => { void copyText(event, diagram); }, + onDownloadPNG: (event, imagemodeselected, userimagesize) => { + exportImage(event, downloadImage, imagemodeselected, userimagesize); + }, + onDownloadSVG: () => { + simulateDownload(exportOptions.getFileName('svg'), `data:image/svg+xml;base64,${getBase64SVG()}`); + }, + onDownloadMMD: diagram => { + simulateDownload(exportOptions.getFileName('mmd'), `data:text/vnd.mermaid;base64,${toBase64(diagram)}`); + } + }; + })(); + + const exportOptions = (() => { + let wereOpened = false; // used to track whether user was able to see save options and may quick-save + + const container = getById('exportOptions'), + toggle = getById('exportOptions-toggle'), + saveBtn = getById('save'), + copyBtn = getById('copy'), + saveAs = 'saveAs', + png = 'png', + svg = 'svg', + isDisabled = () => toggle.hidden, // using toggle visibility as indicator + + open = () => { + wereOpened = true; + return collapse.open(container); + }, + + copy = event => { + if (isDisabled()) return; // allow the default for copying text if no types are rendered + + if (!exporter.isClipboardAvailable()) notify('The clipboard seems unavailable in this browser :('); + else { + const type = checkable.getValue(saveAs); + + try { + if (type === png) { + const [dimension, size] = getDimensions(); + exporter.onCopyPNG(event, dimension, size); + } + else if (type === svg) exporter.onCopySVG(event); + else exporter.onCopyMMD(event, mermaidExtensions.getDiagram()); + + notify(`The diagram ${type.toUpperCase()} is in your clipboard.`); + } catch (e) { + notify(e.toString()); + } + } + }, + + save = event => { + const type = checkable.getValue(saveAs); + + if (type === png) { + const [dimension, size] = getDimensions(); + exporter.onDownloadPNG(event, dimension, size); + } + else if (type === svg) exporter.onDownloadSVG(); + else exporter.onDownloadMMD(mermaidExtensions.getDiagram()); + }; + + const getDimensions = (() => { + const inputName = 'dimension', + scale = 'scale', + dimensions = getById('dimensions'), + scaleInputs = container.querySelectorAll('#scale-controls input'); + + // enable toggling dimension controls + checkable.onChange(saveAs, event => { + collapse.toggle(dimensions, event.target.value === png); + }, container); + + // enable toggling scale controls + checkable.onChange(inputName, event => { + const disabled = event.target.value !== scale; + for (let input of scaleInputs) input.disabled = disabled; + }, container); + + return () => { + let dimension = checkable.getValue(inputName); + + // return dimension to scale to desired size if not exporting in current size + if (dimension !== 'auto') dimension = checkable.getValue(scale); + + return [dimension, getById('scale-size').value]; + }; + })(); + + if (exporter.isClipboardAvailable()) copyBtn.onclick = copy; + else copyBtn.hidden = true; + + saveBtn.onclick = save; + + return { + copy, + getFileName: ext => `${saveBtn.dataset.assembly}-diagram-${new Date().toISOString().replace(/[Z:.]/g, '')}.${ext}`, + + enable: enable => { + if (!enable) collapse.toggle(container, false); // make sure the container is closed when disabling + toggle.hidden = !enable; + }, + + quickSave: event => { + if (isDisabled()) return; // allow the default for saving HTML doc if no types are rendered + + if (wereOpened) { + save(event); // allow quick save + return; + } + + const filterOpened = filterSidebar.open(), + optionsOpenend = open(); + + /* Make sure the collapses containing the save options are open and visible when user hits Ctrl + S. + If neither needed opening, trigger saving. I.e. hitting Ctrl + S again should do it. */ + if (!filterOpened && !optionsOpenend) save(event); + else event.preventDefault(); // prevent saving HTML page + } + }; + })(); + + // displays pressed keys and highlights mouse cursor for teaching usage and other presentations + const controlDisplay = (function () { + let used = new Set(), enabled = false, wheelTimeout; + + const alt = 'Alt', + display = getById('pressed-keys'), // a label displaying the keys being pressed and mouse wheel being scrolled + mouse = getById('mouse'), // a circle tracking the mouse to make following it easier + + translateKey = key => key.length === 1 ? key.toUpperCase() : key, + + updateDisplay = () => { + display.textContent = [...used].join(' + '); + display.classList.toggle('hidden', used.size === 0); + }, + + eventHandlers = { + keydown: event => { + if (event.altKey) used.add(alt); // handle separately because Alt key alone doesn't trigger a key event + used.add(translateKey(event.key)); + updateDisplay(); + }, + + keyup: event => { + setTimeout(() => { + if (!event.altKey && used.has(alt)) used.delete(alt); + used.delete(translateKey(event.key)); + updateDisplay(); + }, 500); + }, + + wheel: event => { + const label = 'wheel ' + (event.deltaY < 0 ? 'up' : 'down'), + wasUsed = used.has(label); + + if (wasUsed) { + if (wheelTimeout) clearTimeout(wheelTimeout); + } else { + used.add(label); + updateDisplay(); + } + + // automatically remove + wheelTimeout = setTimeout(() => { + used.delete(label); + updateDisplay(); + wheelTimeout = undefined; + }, 500); + }, + + mousemove: event => { + mouse.style.top = event.clientY + 'px'; + mouse.style.left = event.clientX + 'px'; + }, + + mousedown: () => { mouse.classList.add('down'); }, + mouseup: () => { setTimeout(() => { mouse.classList.remove('down'); }, 300); } + }; + + return { + toggle: () => { + enabled = !enabled; + + if (enabled) { + mouse.hidden = false; + + for (let [event, handler] of Object.entries(eventHandlers)) + document.addEventListener(event, handler); + } else { + mouse.hidden = true; + + for (let [event, handler] of Object.entries(eventHandlers)) + document.removeEventListener(event, handler); + + used.clear(); + updateDisplay(); + } + } + }; + })(); + + // key bindings + document.onkeydown = async (event) => { + const arrowUp = 'ArrowUp', arrowDown = 'ArrowDown'; + + // support Cmd key as alternative on Mac, see https://stackoverflow.com/a/5500536 + if (event.ctrlKey || event.metaKey) { + switch (event.key) { + case 'b': filterSidebar.toggle(); return; + case 'k': + event.preventDefault(); + filterSidebar.open(); + typeSelector.focusFilter(); + return; + case 's': exportOptions.quickSave(event); return; + case 'c': exportOptions.copy(event); return; + case 'i': + event.preventDefault(); + controlDisplay.toggle(); + return; + case 'ArrowLeft': layoutDirection.set('RL', event); return; + case 'ArrowRight': layoutDirection.set('LR', event); return; + case arrowUp: layoutDirection.set('BT', event); return; + case arrowDown: layoutDirection.set('TB', event); return; + case '0': output.resetZoomAndPan(); return; + } + } + + if (event.altKey) { // naturally triggered by Mac's option key as well + // enable moving selected types up and down using arrow keys while holding [Alt] + const upOrDown = event.key === arrowUp ? true : event.key === arrowDown ? false : null; + + if (upOrDown !== null) { + typeSelector.focus(); + typeSelector.moveSelection(upOrDown); + event.preventDefault(); + return; + } + + // pulse-animate elements with helping title attributes to point them out + if (event.key === 'i') { + event.preventDefault(); + const pulsing = 'pulsing'; + + for (let element of document.querySelectorAll('[title],:has(title)')) { + element.addEventListener('animationend', () => { element.classList.remove(pulsing); }, { once: true }); + element.classList.add(pulsing); + } + } + } + }; + + // rewrite help replacing references to 'Ctrl' with 'Cmd' for Mac users + if (/(Mac)/i.test(navigator.userAgent)) { + const ctrl = /Ctrl/mg, + replace = source => source.replaceAll(ctrl, '⌘'); + + for (let titled of document.querySelectorAll('[title]')) + if (ctrl.test(titled.title)) titled.title = replace(titled.title); + + for (let titled of document.querySelectorAll('[data-title]')) + if (ctrl.test(titled.dataset.title)) titled.dataset.title = replace(titled.dataset.title); + + for (let element of getById('info').querySelectorAll('*')) { + const text = element.innerText || element.textContent; // Get the text content of the element + if (ctrl.test(text)) element.innerHTML = replace(text); + } + } + + collapse.initToggles(); + mermaidExtensions.init({ startOnLoad: false }); // initializes mermaid as well + typeSelector.focus(); // focus type filter initially to enable keyboard input + await state.restore(); +})(); diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css new file mode 100644 index 0000000000..b3c9a0595f --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css @@ -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; +} diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less new file mode 100644 index 0000000000..584aa3ec79 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less @@ -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; + } + } +} diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html new file mode 100644 index 0000000000..fbfe799afc --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -0,0 +1,194 @@ + + + + + {{SourceAssemblyName}} class diagrammer - netAmermaid + + + + + + + +
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+ +
+
+
+ + + +
+ +
+

+ The type picker is ✜ focused when you open the app. + You can just ⌨️ key in the first letter/s of the type + you want to start your diagram with and hit [Enter] to render it. +

+

+ After rendering you can 👆 tap types on the diagram + to update your selection and redraw. + This allows you to explore the domain along relations. +

+

+ Don't forget that you can hold [Shift] to ↕ range-select + and [Ctrl] to ± add to or subtract from your selection. +

+

+ Note that the diagram has a 🟈 layout direction - + i.e. it depends on how you ⇅ sort selected types using [Alt + Arrow Up|Down]. +

+

+ Changing the type selection or rendering options + updates the URL in the location bar. That means you can +

    +
  • 🔖 bookmark or 📣 share the URL to your diagram with whoever has access to this diagrammer,
  • +
  • access 🕔 earlier diagrams recorded in your 🧾 browser history and
  • +
  • ⇥ restore your type selection to the picker from the URL using ⟳ Refresh [F5] if you lose it.
  • +
+

+

Looking for help with something else?

+

+ Stop and spot the tooltips. 🌷 They'll give you more info where necessary. + Get a hint for elements with helping tooltips using [Alt + i]. +

+

Alternatively, find helpful links to the docs and discussions in the + build info

+

If you find this helpful and want to share your 📺 screen and 🎓 wisdom on how it works + with a 🦗 newcomer, try toggling presentation mode using [Ctrl + i].

+
+ + + +
+ show inherited + + + + + + + + + + + + + +
+ +
+ layout direction + + + + + + + + +
+ +
+ + +
+ +
+ +
+ + + +
+ +
+ + + + + + + + + + + + + +
+ +
+
+ png dimensions + +
+ + + + +
+ +
+ + + + +
+ + + +
+
+
+
+
+
+ + + +
+
+ +
+
+
+
+ built from {{SourceAssemblyName}} v{{SourceAssemblyVersion}} and mermaid.js from CDN + 📥 + + + using netAmermaid v{{BuilderVersion}} + 📜 + 💬 + + 🌩️ + +
+ +
+
+ + + + + + + + + From 6f91b146019b1d43dcdd391b0c00cc4adc1edd5e Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Wed, 6 Nov 2024 20:27:53 +0100 Subject: [PATCH 02/22] reading from embedded resource instead of file --- .../MermaidDiagrammer/EmbeddedResource.cs | 56 +++++++++++++++++++ .../MermaidDiagrammer/Generator.Run.cs | 14 ++--- 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs new file mode 100644 index 0000000000..332c35b02f --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/EmbeddedResource.cs @@ -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 + { + /// A helper for loading resources embedded in the nested html folder. + 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; + } + } + } +} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs index c01f07b731..9c1fb55406 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -90,14 +90,10 @@ private string SerializeModel(ClassDiagrammer diagrammer) 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 no out folder is specified, default to a "netAmermaid" folder next to the input assembly. + var outputFolder = OutputFolder ?? Path.Combine(Path.GetDirectoryName(assemblyPath) ?? string.Empty, "netAmermaid"); if (!Directory.Exists(outputFolder)) Directory.CreateDirectory(outputFolder); @@ -109,7 +105,7 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) } else { - var htmlTemplate = File.ReadAllText(Path.Combine(htmlSourcePath, "template.html")); + var htmlTemplate = EmbeddedResource.ReadText("template.html"); var html = htmlTemplate .Replace("{{SourceAssemblyName}}", model.SourceAssemblyName) @@ -121,8 +117,8 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) 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); + foreach (var resource in new[] { "styles.css", "netAmermaid.ico", "script.js" }) + EmbeddedResource.CopyTo(outputFolder, resource); Console.WriteLine("Successfully generated HTML diagrammer."); } From 15095361ddc31aa38d8b22075ee45260569c4db6 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Tue, 12 Nov 2024 23:19:38 +0100 Subject: [PATCH 03/22] swapped out icon to brand diagrammers as an ILSpy product reusing linked ..\ILSpy\Images\ILSpy.ico from UI project --- ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj | 1 + ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs | 2 +- ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj index 3e83e0a0ca..76a7b35044 100644 --- a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj +++ b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj @@ -75,6 +75,7 @@ + diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs index 9c1fb55406..a288b1783d 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -117,7 +117,7 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) File.WriteAllText(Path.Combine(outputFolder, "class-diagrammer.html"), html); // copy required resources to output folder while flattening paths if required - foreach (var resource in new[] { "styles.css", "netAmermaid.ico", "script.js" }) + foreach (var resource in new[] { "styles.css", "ILSpy.ico", "script.js" }) EmbeddedResource.CopyTo(outputFolder, resource); Console.WriteLine("Successfully generated HTML diagrammer."); diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html index fbfe799afc..3315cfb853 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -3,7 +3,7 @@ {{SourceAssemblyName}} class diagrammer - netAmermaid - + @@ -66,7 +66,7 @@

Looking for help with something else?

Get a hint for elements with helping tooltips using [Alt + i].

Alternatively, find helpful links to the docs and discussions in the - build info

+ build info

If you find this helpful and want to share your 📺 screen and 🎓 wisdom on how it works with a 🦗 newcomer, try toggling presentation mode using [Ctrl + i].

@@ -180,7 +180,7 @@

Looking for help with something else?

🌩️ - + From 8e91ca32a184a19bf2c09d82e676cce3f4f223eb Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Wed, 6 Nov 2024 21:06:42 +0100 Subject: [PATCH 04/22] added required ilspycmd options and routed call --- ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 57e08d5e38..2879b2d897 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -18,6 +18,7 @@ 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 @@ Decompile assembly to destination directory (single C# file). 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 @@ class ILSpyCmdProgram [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; } + + /// + [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 @@ private async Task OnExecuteAsync(CommandLineApplication app) 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) From 2a4345f14e640480f1bfcb4860929dc211dbbae2 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 01:20:56 +0100 Subject: [PATCH 05/22] adjusted VS Code task to generate model.json required by the JS/CSS/HTML dev loop --- .../MermaidDiagrammer/html/.vscode/tasks.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json index 361bb32e5e..2249378fb8 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/.vscode/tasks.json @@ -9,10 +9,13 @@ "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 .", + "$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 'netAmermaid.exe Release build not found. Please build it first.';", + " Write-Host 'ilspycmd.exe Debug build not found. Please build it first.';", " exit 1", "}" ], From e66cfd74ac62eb0c0a5a966e199f031c7cbed9da Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 00:38:34 +0100 Subject: [PATCH 06/22] added debug launchSettings --- .../Properties/launchSettings.json | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ICSharpCode.ILSpyCmd/Properties/launchSettings.json diff --git a/ICSharpCode.ILSpyCmd/Properties/launchSettings.json b/ICSharpCode.ILSpyCmd/Properties/launchSettings.json new file mode 100644 index 0000000000..5fbb314376 --- /dev/null +++ b/ICSharpCode.ILSpyCmd/Properties/launchSettings.json @@ -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" + } + } +} \ No newline at end of file From 1ff3e951a1a564b9fc3b81ba0b9d5ae8a01a24a3 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 16:49:24 +0100 Subject: [PATCH 07/22] updated help command output --- ICSharpCode.ILSpyCmd/README.md | 85 +++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index f6b11094d6..743e13eeea 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -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] 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 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 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 Lists all entities of the specified type(s). Valid types: c(lass), i(nterface), s(truct), d(elegate), e(num) - -lv|--languageversion 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 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 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 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 Lists all entities of the specified type(s). Valid types: c(lass), + i(nterface), s(truct), d(elegate), e(num) + -lv|--languageversion 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 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. @@ -56,7 +82,14 @@ Examples: Decompile assembly to destination directory, create a project file, one source file per type. ilspycmd -p -o c:\decompiled sample.dll - Decompile assembly to destination directory, create a project file, one source file per type, + 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\\..+ ``` From 42a745679faedacbdf1247e43708f4dfc89fc590 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 13:55:08 +0100 Subject: [PATCH 08/22] using ILSpyX build info in generated diagrammers removing unused code --- .../MermaidDiagrammer/AssemblyInfo.cs | 44 ------------------- .../MermaidDiagrammer/Generator.Run.cs | 4 +- .../MermaidDiagrammer/html/template.html | 4 +- 3 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs deleted file mode 100644 index 58680250bc..0000000000 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/AssemblyInfo.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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; } - } - } -} \ No newline at end of file diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs index a288b1783d..86b8e44b7f 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -70,7 +70,7 @@ private string SerializeModel(ClassDiagrammer diagrammer) jsonModel = new { diagrammer.SourceAssemblyName, diagrammer.SourceAssemblyVersion, - BuilderVersion = AssemblyInfo.Version, + 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) @@ -110,7 +110,7 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) var html = htmlTemplate .Replace("{{SourceAssemblyName}}", model.SourceAssemblyName) .Replace("{{SourceAssemblyVersion}}", model.SourceAssemblyVersion) - .Replace("{{BuilderVersion}}", AssemblyInfo.Version) + .Replace("{{BuilderVersion}}", DecompilerVersionInfo.FullVersionWithCommitHash) .Replace("{{RepoUrl}}", RepoUrl) .Replace("{{Model}}", modelJson); diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html index 3315cfb853..52d42aafad 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -2,7 +2,7 @@ - {{SourceAssemblyName}} class diagrammer - netAmermaid + {{SourceAssemblyName}} class diagrammer - ILSpy @@ -173,7 +173,7 @@

Looking for help with something else?

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.">📥 - using netAmermaid v{{BuilderVersion}} + using ICSharpCode.ILSpyX v{{BuilderVersion}} 📜 💬 From 56643ac0b1b0bd976e626ae93eb024ffa9e86924 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Wed, 13 Nov 2024 20:59:51 +0100 Subject: [PATCH 09/22] using explicit type where it's not obvious --- .../MermaidDiagrammer/ClassDiagrammerFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs index 6834160b78..a726883e29 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs @@ -59,8 +59,8 @@ public CD BuildModel(string assemblyPath, string? include, string? exclude) IEnumerable allTypes = mainModule.TypeDefinitions; selectedTypes = FilterTypes(allTypes, - include == null ? null : new(include, RegexOptions.Compiled), - exclude == null ? null : new(exclude, RegexOptions.Compiled)).ToArray(); + 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); From 5d80c710c455ef5646e2d1464a162afadafb8656 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 17:54:59 +0100 Subject: [PATCH 10/22] outputting in to a folder next to and named after the input assembly + " diagrammer" by default --- ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs index 86b8e44b7f..2c9c8ae3cb 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -92,8 +92,11 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) { string modelJson = SerializeModel(model); - // If no out folder is specified, default to a "netAmermaid" folder next to the input assembly. - var outputFolder = OutputFolder ?? Path.Combine(Path.GetDirectoryName(assemblyPath) ?? string.Empty, "netAmermaid"); + // If no out folder is specified, default to a " 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); From 30e91d94eea589c84ec899e43f00b87f9739703c Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Tue, 12 Nov 2024 20:47:13 +0100 Subject: [PATCH 11/22] renamed diagrammer output to index.html to support default web server configs in the wild --- ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs index 2c9c8ae3cb..6ea6dfd6e0 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Generator.Run.cs @@ -117,7 +117,7 @@ private void GenerateOutput(string assemblyPath, ClassDiagrammer model) .Replace("{{RepoUrl}}", RepoUrl) .Replace("{{Model}}", modelJson); - File.WriteAllText(Path.Combine(outputFolder, "class-diagrammer.html"), html); + 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" }) From beb5e03e44003c4c5f5510b6b11d6bdc36872f51 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Tue, 12 Nov 2024 21:25:42 +0100 Subject: [PATCH 12/22] improved instructions for creating an off-line diagrammer --- ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html index 52d42aafad..c5159c0569 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -170,7 +170,7 @@

Looking for help with something else?

built from {{SourceAssemblyName}} v{{SourceAssemblyVersion}} and mermaid.js from CDN 📥 + 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'.">📥 using ICSharpCode.ILSpyX v{{BuilderVersion}} From c43b458929bd110fd2b16925e8d09b0046076b65 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 21:24:32 +0100 Subject: [PATCH 13/22] added developer-facing doco for how to edit the HTML/JS/CSS parts --- ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt new file mode 100644 index 0000000000..7a668d544f --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/README.txt @@ -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. \ No newline at end of file From 270d086f3d0278a86ed45f9fd9907ba557cc74f5 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 17:54:02 +0100 Subject: [PATCH 14/22] renamed to remove netAmermaid branding --- .../MermaidDiagrammer/html/styles.css | 24 +++++++++---------- .../MermaidDiagrammer/html/styles.less | 4 ++-- .../MermaidDiagrammer/html/template.html | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css index b3c9a0595f..23b929be15 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.css @@ -276,48 +276,48 @@ select option:checked { #output g.nodes g.label .nodeLabel[title] { color: darkgoldenrod; } -#netAmermaid { +#about { position: absolute; bottom: 2em; right: 2em; align-items: end; } -#netAmermaid #toaster { +#about #toaster { margin-right: 2.8em; } -#netAmermaid #toaster span { +#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; } -#netAmermaid #toaster span.leaving { +#about #toaster span.leaving { animation: 1s ease-in-out fadeOut; } -#netAmermaid .build-info { +#about .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 > * { +#about .build-info > * { height: 100%; } -#netAmermaid .build-info #build-info { +#about .build-info #build-info { text-align: right; } -#netAmermaid .build-info #build-info > * { +#about .build-info #build-info > * { padding: 0 0.5em; } -#netAmermaid .build-info #build-info a { +#about .build-info #build-info a { color: whitesmoke; } -#netAmermaid .build-info #build-info a:not(.project) { +#about .build-info #build-info a:not(.project) { text-decoration: none; } -#netAmermaid .build-info #build-info a span { +#about .build-info #build-info a span { display: inline-block; } #pressed-keys { @@ -353,7 +353,7 @@ select option:checked { @media print { #filter, #filter-toggle, - #netAmermaid, + #about, img, .bubbles { display: none; diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less index 584aa3ec79..46e519507a 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/styles.less @@ -382,7 +382,7 @@ select { } } -#netAmermaid { +#about { position: absolute; bottom: 2em; right: 2em; @@ -473,7 +473,7 @@ select { /* hide stuff in print view */ @media print { - #filter, #filter-toggle, #netAmermaid, img, .bubbles { + #filter, #filter-toggle, #about, img, .bubbles { display: none; } } diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html index c5159c0569..aceff72c25 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -164,7 +164,7 @@

Looking for help with something else?

-
+
From 9973c5a12bfd3bac9beb4d79553db0a1d8b7ee43 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 17:57:47 +0100 Subject: [PATCH 15/22] updated repo URL and doco link to new Wiki page --- ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs | 2 +- ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs index c25892ae75..c5abf806d3 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/GenerateHtmlDiagrammer.cs @@ -24,7 +24,7 @@ namespace ICSharpCode.ILSpyX.MermaidDiagrammer /// To use it outside of that context, set its properties and call . public partial class GenerateHtmlDiagrammer { - internal const string RepoUrl = "https://github.com/h0lg/netAmermaid"; + internal const string RepoUrl = "https://github.com/icsharpcode/ILSpy"; public required string Assembly { get; set; } public string? OutputFolder { get; set; } diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html index aceff72c25..b3316dc62a 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/template.html @@ -174,7 +174,7 @@

Looking for help with something else?

using ICSharpCode.ILSpyX v{{BuilderVersion}} - 📜 + 📜 💬 🌩️ From 6d95f302d9f8490fa3e861aa66dab0138a02c10a Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 21:50:40 +0100 Subject: [PATCH 16/22] copied over doco --- .../MermaidDiagrammer/ReadMe.md | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md new file mode 100644 index 0000000000..0e0f8b0a4d --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md @@ -0,0 +1,255 @@ + +# netAmermaid + +An automated documentation tool for visually exploring +[.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) +along type relations using rapid diagramming. + + + + +- [What can it do for you and how?](#what-can-it-do-for-you-and-how) +- [How does it work?](#how-does-it-work) +- [Check out the demo](#check-out-the-demo) +- [Optimized for exploration and sharing](#optimized-for-exploration-and-sharing) +- [Generate a HTML diagrammer using the console app](#generate-a-html-diagrammer-using-the-console-app) + - [Manually before use](#manually-before-use) + - [Automatically](#automatically) + - [After building](#after-building) + - [After publishing](#after-publishing) + - [Options](#options) + - [Tips for using the console app](#tips-for-using-the-console-app) + - [Advanced configuration examples](#advanced-configuration-examples) + - [Filter extracted types](#filter-extracted-types) + - [Strip namespaces from XML comments](#strip-namespaces-from-xml-comments) + - [Adjust for custom XML documentation file names](#adjust-for-custom-xml-documentation-file-names) +- [Tips for using the HTML diagrammer](#tips-for-using-the-html-diagrammer) +- [Thanks to](#thanks-to) +- [Disclaimer](#disclaimer) + + +# What can it do for you and how? + +> **Class diagrams** and Entity/Relationship diagrams **can be really helpful if done right**. +They let us see how types relate - handing us a **ready-made mental map** for a subdomain. +At the same time **they take a load of our minds**, sparing us from having to remember all those relations correctly across frantic code symbol navigations in our IDE. +And occasionally they can serve as [safe and engaging fodder for big-brained busy-bodies](https://grugbrain.dev/#grug-on-factring-your-code) to stay the heck out of our code base. + +> **Drawing them takes way too long though** - even in fancy designers. +And what's the point? Like any other hand-crafted documentation, **they're always outdated** - often from the very beginning. +After a while their usability becomes a function of **how much time you want to spend maintaining** them. +Also, they're next to useless in conversations about the *boundaries* of whatever subdomain or Aggregate you're looking at - because they **lack the interactivity to let you peek beyond the boundary**. + +**netAmermaid** helps you create useful on-the-fly class diagrams within seconds in two simple steps: + +1. Point the **command line tool** at an assembly to extract its type information +and **build a [HTML5](https://en.wikipedia.org/wiki/HTML5#New_APIs) diagramming app** from it. +To get it hot off your latest build, you can script this step and run it just before using the diagrammer - or +hook it into your build pipeline to automate it for Continuous Integration. +1. Open the **HTML diagrammer** to select types and **render class diagrams** from them +within a couple of keystrokes - after which you can interact with the diagram directly +to unfold the domain along type relations. At any point, familiar key commands will copy the diagram to your clipboard +or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mermaid.js.org/syntax/classDiagram.html). You can also just share the URL with anybody with access to the HTML diagrammer or paste it into your code where helpful. + +If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. + +> Dealing with .Net assemblies, you've probably come across [**ILSpy**](https://github.com/icsharpcode/ILSpy) and can appreciate how useful it is to explore and understand even the most poorly documented library. Think of netAmermaid as a **visual version** of that - minus geeky details like code decompilation and symbol usage analysis. Instead, + +What netAmermaid offers is an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use it as +- a **mental mapping** tool to get your bearings in an **unknown domain**. +- a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. + +# How does it work? + +To **extract the type info from the source assembly**, the netAmermaid CLI side-loads it including all its dependencies. +The current implementation actually uses ILSpy under the hood for that because it's really good at figuring out [which runtime, GAC or private bin path to load referenced assemblies from](https://github.com/icsharpcode/ILSpy/blob/master/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs). + +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) +> +> Note that you can +> - **customize** the `template.html`, `script.js` and `styles.css` used in this process, e.g. for rewording or branding the UI. +> - **build a stand-alone offline diagrammer** with *mermaid* baked in instead of loading it from a CDN. All you have to do is replace the `import` statement for `/dist/mermaid.esm.min.mjs` at the top of `script.js` with the *contents* of [`/dist/mermaid.min.js`](https://unpkg.com/mermaid@latest/dist/mermaid.min.js) from the *mermaid* package of your choice. + +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. + +# Check out the demo + +Have a look at the diagrammer generated for [SubTubular](https://github.com/h0lg/SubTubular): +It's got some [type relations](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=Caption-CaptionTrack-PaddedMatch-IncludedMatch-Video-VideoSearchResult-CaptionTrackResult) +and [inheritance](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=RemoteValidated-SearchChannel-SearchCommand-Shows-SearchPlaylist-SearchPlaylistCommand-OrderOptions-SearchVideos) +going on that offer a decent playground. + +> Wouldn't it be great to show off netAmermaid's capabilities applied to itself? +Sure - but with the console app being as simple as it is, its class diagrams +are pretty boring and don't get the benefit across. +As with any documentation, netAmermaid starts to shine at higher complexity. +So you could say it offers little value to itself - +but it rather likes to call that selfless and feel good about it. + +# Optimized for exploration and sharing + +It is not the goal of the HTML diagrammer to create the perfect diagram - +so you'll find few options to customize the layout. +This is - to some degree - due to the nature of generative diagramming itself, +while at other times the [mermaid API](https://mermaid.js.org/syntax/classDiagram.html) poses the limiting factor. +Having said that, you can usually **choose a direction** in which the automated layout works reasonably well. + +Instead, think of the diagrammer as +- a browser for **exploring domains** +- a visual design aid for **reasoning about type relations and inheritance** +- a **communication tool** for contributors and users to share aspects of a model +- a **documentation** you don't have to write. + +You'll find controls and key bindings to help you get those things done as quickly and efficiently as possible. + +# Generate a HTML diagrammer using the console app + +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: + +
+..\..\path\to\netAmermaid.exe --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +With this script in place, run it to (re-)generate the HTML diagrammer at your leisure. Note that `--output-folder .` 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 netAmermaid 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 + + + +``` + +### 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 + + + +``` + +## Options + +The command line app exposes the following parameters. + +| shorthand, name | | +| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-a`, `--assembly` | Required. The path or file:// URI of the .NET assembly to generate a HTML diagrammer for. | +| `-o`, `--output-folder` | The path of the folder to generate the HTML diagrammer into. This defaults to a 'netAmermaid' folder in the directory of the `assembly`, which will be created if required. | +| `-i`, `--include` | A regular expression matching Type.FullName used to whitelist types. | +| `-e`, `--exclude` | A regular expression matching Type.FullName used to blacklist types. | +| `-r`, `--report-excluded` | Outputs a report of types excluded from the HTML 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. | +| `-n`, `--strip-namespaces` | 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. | +| `-d`, `--docs` | The path or file:// URI of the XML file containing the `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 `assembly`. To enable XML documentation output for your `assembly` see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output . | + +## Tips for using the console app + +**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 `--include` and `--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 `--include` (white-list) and `--exclude` (black-list). +A third option `--report-excluded` will output a `.txt` containing the list of effectively excluded types next to the HTML diagrammer containing the effectively included types. + +
+netAmermaid.exe --include Your\.Models\..+ --exclude .+\+Metadata|.+\.Data\..+Map --report-excluded --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +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 member lists of classes on your diagrams by supplying a space-separated list of namespaces to omit from the output like so: + +
+netAmermaid.exe --strip-namespaces System.Collections.Generic System --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +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. + +
+netAmermaid.exe --docs ..\path\to\your\docs.xml --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +# Tips for using the HTML diagrammer + +> **On Mac**, use the Command key ⌘ instead of `Ctrl`. + +- The type selection is focused by default. That means you can **immediately start typing** +to select the type you want to use as a starting point for your diagram and **hit Enter to render** it. +- Don't forget that you can hold [Shift] to **↕ range-select** and [Ctrl] to **± add to or subtract from** your selection. +- With a **big type selection**, you'll want to use the **pre-filter** often. Focus it with [Ctrl + k]. Use plain text or an EcmaScript flavored RegEx to filter the selection. +- After rendering, you can **explore the domain along type relations** by clicking related types on the diagram to toggle them in the filter and trigger re-rendering. +- Changing the type selection or rendering options updates the URL in the location bar. That means you can + - 🔖 **bookmark** or 📣 **share the URL** to your diagram with whoever has access to this diagrammer, + - **access 🕔 earlier diagrams** recorded in your 🧾 browser history and + - **⇥ restore your type selection** to the picker from the URL using ⟳ Refresh [F5] if you lose it. +- The diagram has a **layout direction**, i.e. **rendering depends on the order of your selection**! Use [Alt] + [Arrow Up|Down] to sort selected types. +- You can **zoom the rendered diagram** using [Ctrl + mouse wheel] and **grab and drag to pan** it. Reset zoom and pan with [Ctrl + 0]. +- Need more space? **Adjust the sidebar size** by grabbing and dragging its edge or **hide it completely** with [Ctrl + b] to zen out on the diagram alone. +- You can **copy and save your diagrams** using [Ctrl + c] or [Ctrl + s] respectively. The first time you try to quick-save will open the export options for you to choose the format. +- Showing off the diagrammer remotely? Enter **presentation mode** using [Ctrl + i] to **emphasize your mouse** pointer location, **visualize clicks** and **display pressed keys** for your audience to learn the commands while watching you. +- **Look out for tooltips** to give you **more help** where necessary, like useful **key bindings** to help you get stuff done ASAP. You can also highlight all tool-tipped elements with [Alt + i]. + +# Thanks to + +in the order they're used in netAmermaid's pipeline: + +- [Command Line Parser](https://github.com/commandlineparser/commandline) for parsing shell arguments and making validation and help text display easy. +- [ILSpy](https://github.com/icsharpcode/ILSpy) for reliably retrieving type information from anything that runs with half a broken limb and a dozen broken references. You can have an assembly and a dream and it'll still figure out where to load the dependencies from. In .Net Framework version hell, that is nothing short of amazing. +- [mermaid](https://github.com/mermaid-js/mermaid) for abstracting the capabilities of d3 to a degree that allows the rest of us to build useful stuff without hurting our smol brains more than necessary. + +# Disclaimer + +No mermaids were harmed in the writing of this software and you shouldn't interpret the name as inciting capture of or violence against magical creatures. We would never - [they're doing a great job and we love and respect them for it](https://mermaid.js.org/). \ No newline at end of file From 34e5fe8a26c8f77e909af73b367bab7f7654ccff Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 22:17:20 +0100 Subject: [PATCH 17/22] removed obsolete parts --- .../MermaidDiagrammer/ReadMe.md | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md index 0e0f8b0a4d..4d874a0e15 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md @@ -1,33 +1,9 @@ - -# netAmermaid +# netAmermaid An automated documentation tool for visually exploring [.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) along type relations using rapid diagramming. - - - -- [What can it do for you and how?](#what-can-it-do-for-you-and-how) -- [How does it work?](#how-does-it-work) -- [Check out the demo](#check-out-the-demo) -- [Optimized for exploration and sharing](#optimized-for-exploration-and-sharing) -- [Generate a HTML diagrammer using the console app](#generate-a-html-diagrammer-using-the-console-app) - - [Manually before use](#manually-before-use) - - [Automatically](#automatically) - - [After building](#after-building) - - [After publishing](#after-publishing) - - [Options](#options) - - [Tips for using the console app](#tips-for-using-the-console-app) - - [Advanced configuration examples](#advanced-configuration-examples) - - [Filter extracted types](#filter-extracted-types) - - [Strip namespaces from XML comments](#strip-namespaces-from-xml-comments) - - [Adjust for custom XML documentation file names](#adjust-for-custom-xml-documentation-file-names) -- [Tips for using the HTML diagrammer](#tips-for-using-the-html-diagrammer) -- [Thanks to](#thanks-to) -- [Disclaimer](#disclaimer) - - # What can it do for you and how? > **Class diagrams** and Entity/Relationship diagrams **can be really helpful if done right**. @@ -53,8 +29,6 @@ or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mer If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. -> Dealing with .Net assemblies, you've probably come across [**ILSpy**](https://github.com/icsharpcode/ILSpy) and can appreciate how useful it is to explore and understand even the most poorly documented library. Think of netAmermaid as a **visual version** of that - minus geeky details like code decompilation and symbol usage analysis. Instead, - What netAmermaid offers is an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use it as - a **mental mapping** tool to get your bearings in an **unknown domain**. - a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. @@ -62,7 +36,6 @@ What netAmermaid offers is an **overview** over types, their members and **relat # How does it work? To **extract the type info from the source assembly**, the netAmermaid CLI side-loads it including all its dependencies. -The current implementation actually uses ILSpy under the hood for that because it's really good at figuring out [which runtime, GAC or private bin path to load referenced assemblies from](https://github.com/icsharpcode/ILSpy/blob/master/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs). 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. @@ -71,10 +44,6 @@ The extracted type info is **structured into a model optimized for the HTML diag > - 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) -> -> Note that you can -> - **customize** the `template.html`, `script.js` and `styles.css` used in this process, e.g. for rewording or branding the UI. -> - **build a stand-alone offline diagrammer** with *mermaid* baked in instead of loading it from a CDN. All you have to do is replace the `import` statement for `/dist/mermaid.esm.min.mjs` at the top of `script.js` with the *contents* of [`/dist/mermaid.min.js`](https://unpkg.com/mermaid@latest/dist/mermaid.min.js) from the *mermaid* package of your choice. 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. @@ -85,13 +54,6 @@ It's got some [type relations](https://raw.githack.com/h0lg/SubTubular/netAmerma and [inheritance](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=RemoteValidated-SearchChannel-SearchCommand-Shows-SearchPlaylist-SearchPlaylistCommand-OrderOptions-SearchVideos) going on that offer a decent playground. -> Wouldn't it be great to show off netAmermaid's capabilities applied to itself? -Sure - but with the console app being as simple as it is, its class diagrams -are pretty boring and don't get the benefit across. -As with any documentation, netAmermaid starts to shine at higher complexity. -So you could say it offers little value to itself - -but it rather likes to call that selfless and feel good about it. - # Optimized for exploration and sharing It is not the goal of the HTML diagrammer to create the perfect diagram - @@ -159,20 +121,6 @@ Note that the `Target` `Name` doesn't matter here and that the diagrammer is gen ``` -## Options - -The command line app exposes the following parameters. - -| shorthand, name | | -| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `-a`, `--assembly` | Required. The path or file:// URI of the .NET assembly to generate a HTML diagrammer for. | -| `-o`, `--output-folder` | The path of the folder to generate the HTML diagrammer into. This defaults to a 'netAmermaid' folder in the directory of the `assembly`, which will be created if required. | -| `-i`, `--include` | A regular expression matching Type.FullName used to whitelist types. | -| `-e`, `--exclude` | A regular expression matching Type.FullName used to blacklist types. | -| `-r`, `--report-excluded` | Outputs a report of types excluded from the HTML 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. | -| `-n`, `--strip-namespaces` | 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. | -| `-d`, `--docs` | The path or file:// URI of the XML file containing the `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 `assembly`. To enable XML documentation output for your `assembly` see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output . | - ## Tips for using the console app **Compiler-generated** types and their nested types are **excluded by default**. @@ -241,15 +189,3 @@ to select the type you want to use as a starting point for your diagram and **hi - You can **copy and save your diagrams** using [Ctrl + c] or [Ctrl + s] respectively. The first time you try to quick-save will open the export options for you to choose the format. - Showing off the diagrammer remotely? Enter **presentation mode** using [Ctrl + i] to **emphasize your mouse** pointer location, **visualize clicks** and **display pressed keys** for your audience to learn the commands while watching you. - **Look out for tooltips** to give you **more help** where necessary, like useful **key bindings** to help you get stuff done ASAP. You can also highlight all tool-tipped elements with [Alt + i]. - -# Thanks to - -in the order they're used in netAmermaid's pipeline: - -- [Command Line Parser](https://github.com/commandlineparser/commandline) for parsing shell arguments and making validation and help text display easy. -- [ILSpy](https://github.com/icsharpcode/ILSpy) for reliably retrieving type information from anything that runs with half a broken limb and a dozen broken references. You can have an assembly and a dream and it'll still figure out where to load the dependencies from. In .Net Framework version hell, that is nothing short of amazing. -- [mermaid](https://github.com/mermaid-js/mermaid) for abstracting the capabilities of d3 to a degree that allows the rest of us to build useful stuff without hurting our smol brains more than necessary. - -# Disclaimer - -No mermaids were harmed in the writing of this software and you shouldn't interpret the name as inciting capture of or violence against magical creatures. We would never - [they're doing a great job and we love and respect them for it](https://mermaid.js.org/). \ No newline at end of file From 52728e723f7582c10ed6a5affc59c7fd527a92f9 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Thu, 14 Nov 2024 19:40:58 +0100 Subject: [PATCH 18/22] moved CLI doco into ILSpyCmd README --- ICSharpCode.ILSpyCmd/README.md | 100 ++++++++++++++++++ .../MermaidDiagrammer/ReadMe.md | 100 ------------------ 2 files changed, 100 insertions(+), 100 deletions(-) diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index 743e13eeea..591d2d9ff8 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -93,3 +93,103 @@ Examples: (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 a HTML diagrammer using the console app + +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: + +
+..\..\path\to\netAmermaid.exe --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +With this script in place, run it to (re-)generate the HTML diagrammer at your leisure. Note that `--output-folder .` 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 netAmermaid 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 + + + +``` + +### 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 + + + +``` + +## Tips for using the console app + +**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 `--include` and `--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 `--include` (white-list) and `--exclude` (black-list). +A third option `--report-excluded` will output a `.txt` containing the list of effectively excluded types next to the HTML diagrammer containing the effectively included types. + +
+netAmermaid.exe --include Your\.Models\..+ --exclude .+\+Metadata|.+\.Data\..+Map --report-excluded --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +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 member lists of classes on your diagrams by supplying a space-separated list of namespaces to omit from the output like so: + +
+netAmermaid.exe --strip-namespaces System.Collections.Generic System --assembly ..\path\to\your\assembly.dll --output-folder .
+
+ +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. + +
+netAmermaid.exe --docs ..\path\to\your\docs.xml --assembly ..\path\to\your\assembly.dll --output-folder .
+
diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md index 4d874a0e15..f6489c470c 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md @@ -70,106 +70,6 @@ Instead, think of the diagrammer as You'll find controls and key bindings to help you get those things done as quickly and efficiently as possible. -# Generate a HTML diagrammer using the console app - -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: - -
-..\..\path\to\netAmermaid.exe --assembly ..\path\to\your\assembly.dll --output-folder .
-
- -With this script in place, run it to (re-)generate the HTML diagrammer at your leisure. Note that `--output-folder .` 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 netAmermaid 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 - - - -``` - -### 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 - - - -``` - -## Tips for using the console app - -**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 `--include` and `--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 `--include` (white-list) and `--exclude` (black-list). -A third option `--report-excluded` will output a `.txt` containing the list of effectively excluded types next to the HTML diagrammer containing the effectively included types. - -
-netAmermaid.exe --include Your\.Models\..+ --exclude .+\+Metadata|.+\.Data\..+Map --report-excluded --assembly ..\path\to\your\assembly.dll --output-folder .
-
- -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 member lists of classes on your diagrams by supplying a space-separated list of namespaces to omit from the output like so: - -
-netAmermaid.exe --strip-namespaces System.Collections.Generic System --assembly ..\path\to\your\assembly.dll --output-folder .
-
- -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. - -
-netAmermaid.exe --docs ..\path\to\your\docs.xml --assembly ..\path\to\your\assembly.dll --output-folder .
-
- # Tips for using the HTML diagrammer > **On Mac**, use the Command key ⌘ instead of `Ctrl`. From df49ea0a2cd303116fd55a5dd2a0f58a3ba449d3 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 21:50:40 +0100 Subject: [PATCH 19/22] removed end-user facing chapters that go into the Wiki from dev-facing doco --- .../MermaidDiagrammer/ReadMe.md | 78 ------------------- ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md | 77 ++++++++++++++++++ 2 files changed, 77 insertions(+), 78 deletions(-) create mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md index f6489c470c..85f65059f2 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md @@ -1,38 +1,3 @@ -# netAmermaid - -An automated documentation tool for visually exploring -[.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) -along type relations using rapid diagramming. - -# What can it do for you and how? - -> **Class diagrams** and Entity/Relationship diagrams **can be really helpful if done right**. -They let us see how types relate - handing us a **ready-made mental map** for a subdomain. -At the same time **they take a load of our minds**, sparing us from having to remember all those relations correctly across frantic code symbol navigations in our IDE. -And occasionally they can serve as [safe and engaging fodder for big-brained busy-bodies](https://grugbrain.dev/#grug-on-factring-your-code) to stay the heck out of our code base. - -> **Drawing them takes way too long though** - even in fancy designers. -And what's the point? Like any other hand-crafted documentation, **they're always outdated** - often from the very beginning. -After a while their usability becomes a function of **how much time you want to spend maintaining** them. -Also, they're next to useless in conversations about the *boundaries* of whatever subdomain or Aggregate you're looking at - because they **lack the interactivity to let you peek beyond the boundary**. - -**netAmermaid** helps you create useful on-the-fly class diagrams within seconds in two simple steps: - -1. Point the **command line tool** at an assembly to extract its type information -and **build a [HTML5](https://en.wikipedia.org/wiki/HTML5#New_APIs) diagramming app** from it. -To get it hot off your latest build, you can script this step and run it just before using the diagrammer - or -hook it into your build pipeline to automate it for Continuous Integration. -1. Open the **HTML diagrammer** to select types and **render class diagrams** from them -within a couple of keystrokes - after which you can interact with the diagram directly -to unfold the domain along type relations. At any point, familiar key commands will copy the diagram to your clipboard -or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mermaid.js.org/syntax/classDiagram.html). You can also just share the URL with anybody with access to the HTML diagrammer or paste it into your code where helpful. - -If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. - -What netAmermaid offers is an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use it as -- a **mental mapping** tool to get your bearings in an **unknown domain**. -- a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. - # How does it work? To **extract the type info from the source assembly**, the netAmermaid CLI side-loads it including all its dependencies. @@ -46,46 +11,3 @@ The extracted type info is **structured into a model optimized for the HTML diag > 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. - -# Check out the demo - -Have a look at the diagrammer generated for [SubTubular](https://github.com/h0lg/SubTubular): -It's got some [type relations](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=Caption-CaptionTrack-PaddedMatch-IncludedMatch-Video-VideoSearchResult-CaptionTrackResult) -and [inheritance](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=RemoteValidated-SearchChannel-SearchCommand-Shows-SearchPlaylist-SearchPlaylistCommand-OrderOptions-SearchVideos) -going on that offer a decent playground. - -# Optimized for exploration and sharing - -It is not the goal of the HTML diagrammer to create the perfect diagram - -so you'll find few options to customize the layout. -This is - to some degree - due to the nature of generative diagramming itself, -while at other times the [mermaid API](https://mermaid.js.org/syntax/classDiagram.html) poses the limiting factor. -Having said that, you can usually **choose a direction** in which the automated layout works reasonably well. - -Instead, think of the diagrammer as -- a browser for **exploring domains** -- a visual design aid for **reasoning about type relations and inheritance** -- a **communication tool** for contributors and users to share aspects of a model -- a **documentation** you don't have to write. - -You'll find controls and key bindings to help you get those things done as quickly and efficiently as possible. - -# Tips for using the HTML diagrammer - -> **On Mac**, use the Command key ⌘ instead of `Ctrl`. - -- The type selection is focused by default. That means you can **immediately start typing** -to select the type you want to use as a starting point for your diagram and **hit Enter to render** it. -- Don't forget that you can hold [Shift] to **↕ range-select** and [Ctrl] to **± add to or subtract from** your selection. -- With a **big type selection**, you'll want to use the **pre-filter** often. Focus it with [Ctrl + k]. Use plain text or an EcmaScript flavored RegEx to filter the selection. -- After rendering, you can **explore the domain along type relations** by clicking related types on the diagram to toggle them in the filter and trigger re-rendering. -- Changing the type selection or rendering options updates the URL in the location bar. That means you can - - 🔖 **bookmark** or 📣 **share the URL** to your diagram with whoever has access to this diagrammer, - - **access 🕔 earlier diagrams** recorded in your 🧾 browser history and - - **⇥ restore your type selection** to the picker from the URL using ⟳ Refresh [F5] if you lose it. -- The diagram has a **layout direction**, i.e. **rendering depends on the order of your selection**! Use [Alt] + [Arrow Up|Down] to sort selected types. -- You can **zoom the rendered diagram** using [Ctrl + mouse wheel] and **grab and drag to pan** it. Reset zoom and pan with [Ctrl + 0]. -- Need more space? **Adjust the sidebar size** by grabbing and dragging its edge or **hide it completely** with [Ctrl + b] to zen out on the diagram alone. -- You can **copy and save your diagrams** using [Ctrl + c] or [Ctrl + s] respectively. The first time you try to quick-save will open the export options for you to choose the format. -- Showing off the diagrammer remotely? Enter **presentation mode** using [Ctrl + i] to **emphasize your mouse** pointer location, **visualize clicks** and **display pressed keys** for your audience to learn the commands while watching you. -- **Look out for tooltips** to give you **more help** where necessary, like useful **key bindings** to help you get stuff done ASAP. You can also highlight all tool-tipped elements with [Alt + i]. diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md new file mode 100644 index 0000000000..7783ddbdc7 --- /dev/null +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md @@ -0,0 +1,77 @@ +# netAmermaid + +An automated documentation tool for visually exploring +[.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) +along type relations using rapid diagramming. + +# What can it do for you and how? + +> **Class diagrams** and Entity/Relationship diagrams **can be really helpful if done right**. +They let us see how types relate - handing us a **ready-made mental map** for a subdomain. +At the same time **they take a load of our minds**, sparing us from having to remember all those relations correctly across frantic code symbol navigations in our IDE. +And occasionally they can serve as [safe and engaging fodder for big-brained busy-bodies](https://grugbrain.dev/#grug-on-factring-your-code) to stay the heck out of our code base. + +> **Drawing them takes way too long though** - even in fancy designers. +And what's the point? Like any other hand-crafted documentation, **they're always outdated** - often from the very beginning. +After a while their usability becomes a function of **how much time you want to spend maintaining** them. +Also, they're next to useless in conversations about the *boundaries* of whatever subdomain or Aggregate you're looking at - because they **lack the interactivity to let you peek beyond the boundary**. + +**netAmermaid** helps you create useful on-the-fly class diagrams within seconds in two simple steps: + +1. Point the **command line tool** at an assembly to extract its type information +and **build a [HTML5](https://en.wikipedia.org/wiki/HTML5#New_APIs) diagramming app** from it. +To get it hot off your latest build, you can script this step and run it just before using the diagrammer - or +hook it into your build pipeline to automate it for Continuous Integration. +1. Open the **HTML diagrammer** to select types and **render class diagrams** from them +within a couple of keystrokes - after which you can interact with the diagram directly +to unfold the domain along type relations. At any point, familiar key commands will copy the diagram to your clipboard +or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mermaid.js.org/syntax/classDiagram.html). You can also just share the URL with anybody with access to the HTML diagrammer or paste it into your code where helpful. + +If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. + +What netAmermaid offers is an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use it as +- a **mental mapping** tool to get your bearings in an **unknown domain**. +- a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. + +# Check out the demo + +Have a look at the diagrammer generated for [SubTubular](https://github.com/h0lg/SubTubular): +It's got some [type relations](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=Caption-CaptionTrack-PaddedMatch-IncludedMatch-Video-VideoSearchResult-CaptionTrackResult) +and [inheritance](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=RemoteValidated-SearchChannel-SearchCommand-Shows-SearchPlaylist-SearchPlaylistCommand-OrderOptions-SearchVideos) +going on that offer a decent playground. + +# Optimized for exploration and sharing + +It is not the goal of the HTML diagrammer to create the perfect diagram - +so you'll find few options to customize the layout. +This is - to some degree - due to the nature of generative diagramming itself, +while at other times the [mermaid API](https://mermaid.js.org/syntax/classDiagram.html) poses the limiting factor. +Having said that, you can usually **choose a direction** in which the automated layout works reasonably well. + +Instead, think of the diagrammer as +- a browser for **exploring domains** +- a visual design aid for **reasoning about type relations and inheritance** +- a **communication tool** for contributors and users to share aspects of a model +- a **documentation** you don't have to write. + +You'll find controls and key bindings to help you get those things done as quickly and efficiently as possible. + +# Tips for using the HTML diagrammer + +> **On Mac**, use the Command key ⌘ instead of `Ctrl`. + +- The type selection is focused by default. That means you can **immediately start typing** +to select the type you want to use as a starting point for your diagram and **hit Enter to render** it. +- Don't forget that you can hold [Shift] to **↕ range-select** and [Ctrl] to **± add to or subtract from** your selection. +- With a **big type selection**, you'll want to use the **pre-filter** often. Focus it with [Ctrl + k]. Use plain text or an EcmaScript flavored RegEx to filter the selection. +- After rendering, you can **explore the domain along type relations** by clicking related types on the diagram to toggle them in the filter and trigger re-rendering. +- Changing the type selection or rendering options updates the URL in the location bar. That means you can + - 🔖 **bookmark** or 📣 **share the URL** to your diagram with whoever has access to this diagrammer, + - **access 🕔 earlier diagrams** recorded in your 🧾 browser history and + - **⇥ restore your type selection** to the picker from the URL using ⟳ Refresh [F5] if you lose it. +- The diagram has a **layout direction**, i.e. **rendering depends on the order of your selection**! Use [Alt] + [Arrow Up|Down] to sort selected types. +- You can **zoom the rendered diagram** using [Ctrl + mouse wheel] and **grab and drag to pan** it. Reset zoom and pan with [Ctrl + 0]. +- Need more space? **Adjust the sidebar size** by grabbing and dragging its edge or **hide it completely** with [Ctrl + b] to zen out on the diagram alone. +- You can **copy and save your diagrams** using [Ctrl + c] or [Ctrl + s] respectively. The first time you try to quick-save will open the export options for you to choose the format. +- Showing off the diagrammer remotely? Enter **presentation mode** using [Ctrl + i] to **emphasize your mouse** pointer location, **visualize clicks** and **display pressed keys** for your audience to learn the commands while watching you. +- **Look out for tooltips** to give you **more help** where necessary, like useful **key bindings** to help you get stuff done ASAP. You can also highlight all tool-tipped elements with [Alt + i]. From e43aa0512f01464ca8513c6fb8a9f3e853f59866 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Fri, 8 Nov 2024 22:17:45 +0100 Subject: [PATCH 20/22] updated to ilspycmd API and rebranded to ILSpy --- ICSharpCode.ILSpyCmd/README.md | 44 +++++++++---------- .../MermaidDiagrammer/ReadMe.md | 4 +- ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md | 16 +++---- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/ICSharpCode.ILSpyCmd/README.md b/ICSharpCode.ILSpyCmd/README.md index 591d2d9ff8..f86ef252af 100644 --- a/ICSharpCode.ILSpyCmd/README.md +++ b/ICSharpCode.ILSpyCmd/README.md @@ -94,35 +94,35 @@ Examples: ilspycmd sample.dll --generate-diagrammer -o c:\diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+ ``` -# Generate a HTML diagrammer using the console app +## 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 +### 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:
-..\..\path\to\netAmermaid.exe --assembly ..\path\to\your\assembly.dll --output-folder .
+..\..\path\to\ilspycmd.exe ..\path\to\your\assembly.dll --generate-diagrammer --outputdir .
 
-With this script in place, run it to (re-)generate the HTML diagrammer at your leisure. Note that `--output-folder .` directs the output to the current directory. +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 +### 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 netAmermaid console app or permission to use it. +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 +#### After building To regenerate the HTML diagrammer from your output assembly after building, add something like the following to your project file. @@ -130,43 +130,43 @@ Note that the `Condition` here is optional and configures this step to only run ```xml - + ``` -### After publishing +#### 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 - + ``` -## Tips for using the console app +### 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 `--include` and `--exclude` to **limit the scope of the individual diagrammer to a certain subdomain** +- 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 +### 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 +#### 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 `--include` (white-list) and `--exclude` (black-list). -A third option `--report-excluded` will output a `.txt` containing the list of effectively excluded types next to the HTML diagrammer containing the effectively included types. +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.
-netAmermaid.exe --include Your\.Models\..+ --exclude .+\+Metadata|.+\.Data\..+Map --report-excluded --assembly ..\path\to\your\assembly.dll --output-folder .
+ilspycmd.exe --generate-diagrammer-include Your\.Models\..+ --generate-diagrammer-exclude .+\+Metadata|.+\.Data\..+Map --generate-diagrammer-report-excluded ..\path\to\your\assembly.dll --generate-diagrammer --outputdir .
 
This example @@ -175,21 +175,21 @@ This example - nested types called `Metadata` and - types ending in `Map` in descendant `.Data.` namespaces. -### Strip namespaces from XML comments +#### Strip namespaces from XML comments -You can reduce the noise in the member lists of classes on your diagrams by supplying a space-separated list of namespaces to omit from the output like so: +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:
-netAmermaid.exe --strip-namespaces System.Collections.Generic System --assembly ..\path\to\your\assembly.dll --output-folder .
+ilspycmd.exe --generate-diagrammer-strip-namespaces System.Collections.Generic System ..\path\to\your\assembly.dll --generate-diagrammer --output-folder .
 
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 +#### 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.
-netAmermaid.exe --docs ..\path\to\your\docs.xml --assembly ..\path\to\your\assembly.dll --output-folder .
+ilspycmd.exe --generate-diagrammer-docs ..\path\to\your\docs.xml ..\path\to\your\assembly.dll --generate-diagrammer --output-folder .
 
diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md index 85f65059f2..cef92e08ed 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ReadMe.md @@ -1,6 +1,6 @@ -# How does it work? +# How does it work? -To **extract the type info from the source assembly**, the netAmermaid CLI side-loads it including all its dependencies. +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. diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md index 7783ddbdc7..f45b4d19f5 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md @@ -1,6 +1,6 @@ -# netAmermaid +# Generating diagrammers -An automated documentation tool for visually exploring +ILSpy may be used as an automated documentation tool for visually exploring [.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) along type relations using rapid diagramming. @@ -16,9 +16,9 @@ And what's the point? Like any other hand-crafted documentation, **they're alway After a while their usability becomes a function of **how much time you want to spend maintaining** them. Also, they're next to useless in conversations about the *boundaries* of whatever subdomain or Aggregate you're looking at - because they **lack the interactivity to let you peek beyond the boundary**. -**netAmermaid** helps you create useful on-the-fly class diagrams within seconds in two simple steps: +**ILSpy** helps you create useful on-the-fly class diagrams within seconds in two simple steps: -1. Point the **command line tool** at an assembly to extract its type information +1. Point the [**command line tool**](https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.ILSpyCmd#readme) at an assembly to extract its type information and **build a [HTML5](https://en.wikipedia.org/wiki/HTML5#New_APIs) diagramming app** from it. To get it hot off your latest build, you can script this step and run it just before using the diagrammer - or hook it into your build pipeline to automate it for Continuous Integration. @@ -29,15 +29,15 @@ or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mer If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. -What netAmermaid offers is an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use it as +Generated diagrammers offer an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use them as - a **mental mapping** tool to get your bearings in an **unknown domain**. - a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. # Check out the demo -Have a look at the diagrammer generated for [SubTubular](https://github.com/h0lg/SubTubular): -It's got some [type relations](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=Caption-CaptionTrack-PaddedMatch-IncludedMatch-Video-VideoSearchResult-CaptionTrackResult) -and [inheritance](https://raw.githack.com/h0lg/SubTubular/netAmermaid2/netAmermaid/class-diagrammer.html?d=LR&i=tim&t=RemoteValidated-SearchChannel-SearchCommand-Shows-SearchPlaylist-SearchPlaylistCommand-OrderOptions-SearchVideos) +Have a look at the diagrammer generated for the [ICSharpCode.Decompiler.TypeSystem namespace](https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.Decompiler/TypeSystem): +It's got some [type relations](https://ilspy.net/ICSharpCode.Decompiler/diagrammer/index.html?t=IType-ITypeDefinition&d=LR&i=tm) +and [inheritance](https://ilspy.net/ICSharpCode.Decompiler/diagrammer/index.html?t=IEntity-IEvent-IField-IMember-IMethod-IModule-INamedElement-INamespace-IParameter-IProperty-ISymbol-IVariable&d=LR&i=tim) going on that offer a decent playground. # Optimized for exploration and sharing From 282ed51e87d86638bc315775c609031858b08009 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Thu, 14 Nov 2024 20:41:31 +0100 Subject: [PATCH 21/22] removed doco that's now in https://github.com/icsharpcode/ILSpy/wiki/Diagramming --- ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md | 77 -------------------- 1 file changed, 77 deletions(-) delete mode 100644 ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md b/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md deleted file mode 100644 index f45b4d19f5..0000000000 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/Wiki.md +++ /dev/null @@ -1,77 +0,0 @@ -# Generating diagrammers - -ILSpy may be used as an automated documentation tool for visually exploring -[.NET assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/) (_*.dll_ files) -along type relations using rapid diagramming. - -# What can it do for you and how? - -> **Class diagrams** and Entity/Relationship diagrams **can be really helpful if done right**. -They let us see how types relate - handing us a **ready-made mental map** for a subdomain. -At the same time **they take a load of our minds**, sparing us from having to remember all those relations correctly across frantic code symbol navigations in our IDE. -And occasionally they can serve as [safe and engaging fodder for big-brained busy-bodies](https://grugbrain.dev/#grug-on-factring-your-code) to stay the heck out of our code base. - -> **Drawing them takes way too long though** - even in fancy designers. -And what's the point? Like any other hand-crafted documentation, **they're always outdated** - often from the very beginning. -After a while their usability becomes a function of **how much time you want to spend maintaining** them. -Also, they're next to useless in conversations about the *boundaries* of whatever subdomain or Aggregate you're looking at - because they **lack the interactivity to let you peek beyond the boundary**. - -**ILSpy** helps you create useful on-the-fly class diagrams within seconds in two simple steps: - -1. Point the [**command line tool**](https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.ILSpyCmd#readme) at an assembly to extract its type information -and **build a [HTML5](https://en.wikipedia.org/wiki/HTML5#New_APIs) diagramming app** from it. -To get it hot off your latest build, you can script this step and run it just before using the diagrammer - or -hook it into your build pipeline to automate it for Continuous Integration. -1. Open the **HTML diagrammer** to select types and **render class diagrams** from them -within a couple of keystrokes - after which you can interact with the diagram directly -to unfold the domain along type relations. At any point, familiar key commands will copy the diagram to your clipboard -or export it as either SVG, PNG or in [mermaid class diagram syntax](https://mermaid.js.org/syntax/classDiagram.html). You can also just share the URL with anybody with access to the HTML diagrammer or paste it into your code where helpful. - -If [**XML documentation comments** are available for the source assembly](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output), they're **used to annotate types and members on the generated diagrams**. Commented symbols show up highlighted, making the documentation accessible on hover. - -Generated diagrammers offer an **overview** over types, their members and **relations** and the ability to **unfold the domain** along them until you have enough **context** to make an informed decision. Use them as -- a **mental mapping** tool to get your bearings in an **unknown domain**. -- a **communication** tool for **your own domain** - when talking about the bigger picture with your team mates or even non-technical shareholders like product owners and users. - -# Check out the demo - -Have a look at the diagrammer generated for the [ICSharpCode.Decompiler.TypeSystem namespace](https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.Decompiler/TypeSystem): -It's got some [type relations](https://ilspy.net/ICSharpCode.Decompiler/diagrammer/index.html?t=IType-ITypeDefinition&d=LR&i=tm) -and [inheritance](https://ilspy.net/ICSharpCode.Decompiler/diagrammer/index.html?t=IEntity-IEvent-IField-IMember-IMethod-IModule-INamedElement-INamespace-IParameter-IProperty-ISymbol-IVariable&d=LR&i=tim) -going on that offer a decent playground. - -# Optimized for exploration and sharing - -It is not the goal of the HTML diagrammer to create the perfect diagram - -so you'll find few options to customize the layout. -This is - to some degree - due to the nature of generative diagramming itself, -while at other times the [mermaid API](https://mermaid.js.org/syntax/classDiagram.html) poses the limiting factor. -Having said that, you can usually **choose a direction** in which the automated layout works reasonably well. - -Instead, think of the diagrammer as -- a browser for **exploring domains** -- a visual design aid for **reasoning about type relations and inheritance** -- a **communication tool** for contributors and users to share aspects of a model -- a **documentation** you don't have to write. - -You'll find controls and key bindings to help you get those things done as quickly and efficiently as possible. - -# Tips for using the HTML diagrammer - -> **On Mac**, use the Command key ⌘ instead of `Ctrl`. - -- The type selection is focused by default. That means you can **immediately start typing** -to select the type you want to use as a starting point for your diagram and **hit Enter to render** it. -- Don't forget that you can hold [Shift] to **↕ range-select** and [Ctrl] to **± add to or subtract from** your selection. -- With a **big type selection**, you'll want to use the **pre-filter** often. Focus it with [Ctrl + k]. Use plain text or an EcmaScript flavored RegEx to filter the selection. -- After rendering, you can **explore the domain along type relations** by clicking related types on the diagram to toggle them in the filter and trigger re-rendering. -- Changing the type selection or rendering options updates the URL in the location bar. That means you can - - 🔖 **bookmark** or 📣 **share the URL** to your diagram with whoever has access to this diagrammer, - - **access 🕔 earlier diagrams** recorded in your 🧾 browser history and - - **⇥ restore your type selection** to the picker from the URL using ⟳ Refresh [F5] if you lose it. -- The diagram has a **layout direction**, i.e. **rendering depends on the order of your selection**! Use [Alt] + [Arrow Up|Down] to sort selected types. -- You can **zoom the rendered diagram** using [Ctrl + mouse wheel] and **grab and drag to pan** it. Reset zoom and pan with [Ctrl + 0]. -- Need more space? **Adjust the sidebar size** by grabbing and dragging its edge or **hide it completely** with [Ctrl + b] to zen out on the diagram alone. -- You can **copy and save your diagrams** using [Ctrl + c] or [Ctrl + s] respectively. The first time you try to quick-save will open the export options for you to choose the format. -- Showing off the diagrammer remotely? Enter **presentation mode** using [Ctrl + i] to **emphasize your mouse** pointer location, **visualize clicks** and **display pressed keys** for your audience to learn the commands while watching you. -- **Look out for tooltips** to give you **more help** where necessary, like useful **key bindings** to help you get stuff done ASAP. You can also highlight all tool-tipped elements with [Alt + i]. From f4cb4db8d9b302e4f84935d02824e6692edf832f Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Thu, 7 Nov 2024 23:49:51 +0100 Subject: [PATCH 22/22] added tasks --- ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs | 1 + ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs index a726883e29..7eedf77f6c 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/ClassDiagrammerFactory.cs @@ -47,6 +47,7 @@ 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 }; diff --git a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js index 824d5d221c..f26c851b32 100644 --- a/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js +++ b/ICSharpCode.ILSpyX/MermaidDiagrammer/html/script.js @@ -321,6 +321,8 @@ } }, + /* TODO watch https://github.com/mermaid-js/mermaid/issues/6034 for a solution to render multiple self-references, + which is currently broken. E.g. for LightJson.JsonValue (compare console log) */ // renders HasOne and HasMany relations renderRelations = (typeId, relations, many) => { if (relations) // expecting object; only process if not null or undefined