mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
* added mermaid class diagrammer contributed from https://github.com/h0lg/netAmermaid - find earlier git history there * reading from embedded resource instead of file * swapped out icon to brand diagrammers as an ILSpy product reusing linked ..\ILSpy\Images\ILSpy.ico from UI project * added required ilspycmd options and routed call * adjusted VS Code task to generate model.json required by the JS/CSS/HTML dev loop * added debug launchSettings * updated help command output * using ILSpyX build info in generated diagrammers removing unused code * using explicit type where it's not obvious * outputting in to a folder next to and named after the input assembly + " diagrammer" by default * renamed diagrammer output to index.html to support default web server configs in the wild * improved instructions for creating an off-line diagrammer * added developer-facing doco for how to edit the HTML/JS/CSS parts * renamed to remove netAmermaid branding * updated repo URL and doco link to new Wiki page * copied over doco * removed obsolete parts * moved CLI doco into ILSpyCmd README * removed end-user facing chapters that go into the Wiki from dev-facing doco * updated to ilspycmd API and rebranded to ILSpy * removed doco that's now in https://github.com/icsharpcode/ILSpy/wiki/Diagramming * added taskspull/3335/head
28 changed files with 3967 additions and 26 deletions
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
{ |
||||
"profiles": { |
||||
"no args": { |
||||
"commandName": "Project", |
||||
"commandLineArgs": "" |
||||
}, |
||||
"print help": { |
||||
"commandName": "Project", |
||||
"commandLineArgs": "--help" |
||||
}, |
||||
"generate diagrammer": { |
||||
"commandName": "Project", |
||||
// containing all types |
||||
|
||||
// full diagrammer (~6.3 Mb!) |
||||
//"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer" |
||||
|
||||
// including types in LightJson namespace while excluding types in nested LightJson.Serialization namespace, matched by what returns System.Type.FullName |
||||
//"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+" |
||||
|
||||
// including types in Decompiler.TypeSystem namespace while excluding types in nested Decompiler.TypeSystem.Implementation namespace |
||||
"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-include Decompiler\\.TypeSystem\\..+ --generate-diagrammer-exclude Decompiler\\.TypeSystem\\.Implementation\\..+" |
||||
}, |
||||
"generate diagrammer model.json": { |
||||
"commandName": "Project", |
||||
"commandLineArgs": "ICSharpCode.Decompiler.dll --generate-diagrammer --generate-diagrammer-json-only" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
/// <summary>Contains type info and metadata for generating a HTML class diagrammer from a source assembly.
|
||||
/// Serialized into JSON by <see cref="GenerateHtmlDiagrammer.SerializeModel(ClassDiagrammer)"/>.</summary>
|
||||
public sealed class ClassDiagrammer |
||||
{ |
||||
internal const string NewLine = "\n"; |
||||
|
||||
internal string SourceAssemblyName { get; set; } = null!; |
||||
internal string SourceAssemblyVersion { get; set; } = null!; |
||||
|
||||
/// <summary>Types selectable in the diagrammer, grouped by their
|
||||
/// <see cref="System.Type.Namespace"/> to facilitate a structured type selection.</summary>
|
||||
internal Dictionary<string, Type[]> TypesByNamespace { get; set; } = null!; |
||||
|
||||
/// <summary>Types not included in the <see cref="ClassDiagrammer"/>,
|
||||
/// but referenced by <see cref="Type"/>s that are.
|
||||
/// Contains display names (values; similar to <see cref="Type.Name"/>)
|
||||
/// by their referenced IDs (keys; similar to <see cref="Type.Id"/>).</summary>
|
||||
internal Dictionary<string, string> OutsideReferences { get; set; } = null!; |
||||
|
||||
/// <summary>Types excluded from the <see cref="ClassDiagrammer"/>;
|
||||
/// used to support <see cref="GenerateHtmlDiagrammer.ReportExludedTypes"/>.</summary>
|
||||
internal string[] Excluded { get; set; } = null!; |
||||
|
||||
/// <summary>A <see cref="Type"/>-like structure with collections
|
||||
/// of property relations to one or many other <see cref="Type"/>s.</summary>
|
||||
public abstract class Relationships |
||||
{ |
||||
/// <summary>Relations to zero or one other instances of <see cref="Type"/>s included in the <see cref="ClassDiagrammer"/>,
|
||||
/// with the display member names as keys and the related <see cref="Type.Id"/> as values.
|
||||
/// This is because member names must be unique within the owning <see cref="Type"/>,
|
||||
/// while the related <see cref="Type"/> may be the same for multiple properties.</summary>
|
||||
public Dictionary<string, string>? HasOne { get; set; } |
||||
|
||||
/// <summary>Relations to zero to infinite other instances of <see cref="Type"/>s included in the <see cref="ClassDiagrammer"/>,
|
||||
/// with the display member names as keys and the related <see cref="Type.Id"/> as values.
|
||||
/// This is because member names must be unique within the owning <see cref="Type"/>,
|
||||
/// while the related <see cref="Type"/> may be the same for multiple properties.</summary>
|
||||
public Dictionary<string, string>? HasMany { get; set; } |
||||
} |
||||
|
||||
/// <summary>The mermaid class diagram definition, inheritance and relationships metadata
|
||||
/// and XML documentation for a <see cref="System.Type"/> from the source assembly.</summary>
|
||||
public sealed class Type : Relationships |
||||
{ |
||||
/// <summary>Uniquely identifies the <see cref="System.Type"/> in the scope of the source assembly
|
||||
/// as well as any HTML diagrammer generated from it.
|
||||
/// Should match \w+ to be safe to use as select option value and
|
||||
/// part of the DOM id of the SVG node rendered for this type.
|
||||
/// May be the type name itself.</summary>
|
||||
internal string Id { get; set; } = null!; |
||||
|
||||
/// <summary>The human-readable label for the type, if different from <see cref="Id"/>.
|
||||
/// Not guaranteed to be unique in the scope of the <see cref="ClassDiagrammer"/>.</summary>
|
||||
public string? Name { get; set; } |
||||
|
||||
/// <summary>Contains the definition of the type and its own (not inherited) flat members
|
||||
/// in mermaid class diagram syntax, see https://mermaid.js.org/syntax/classDiagram.html .</summary>
|
||||
public string Body { get; set; } = null!; |
||||
|
||||
/// <summary>The base type directly implemented by this type, with the <see cref="Id"/> as key
|
||||
/// and the (optional) differing display name as value of the single entry
|
||||
/// - or null if the base type is <see cref="object"/>.
|
||||
/// Yes, Christopher Lambert, there can only be one. For now.
|
||||
/// But using the same interface as for <see cref="Interfaces"/> is convenient
|
||||
/// and who knows - at some point the .Net bus may roll up with multi-inheritance.
|
||||
/// Then this'll look visionary!</summary>
|
||||
public Dictionary<string, string?>? BaseType { get; set; } |
||||
|
||||
/// <summary>Interfaces directly implemented by this type, with their <see cref="Id"/> as keys
|
||||
/// and their (optional) differing display names as values.</summary>
|
||||
public Dictionary<string, string?[]>? Interfaces { get; set; } |
||||
|
||||
/// <summary>Contains inherited members by the <see cref="Id"/> of their <see cref="IMember.DeclaringType"/>
|
||||
/// for the consumer to choose which of them to display in an inheritance scenario.</summary>
|
||||
public IDictionary<string, InheritedMembers>? Inherited { get; set; } |
||||
|
||||
/// <summary>Contains the XML documentation comments for this type
|
||||
/// (using a <see cref="string.Empty"/> key) and its members, if available.</summary>
|
||||
public IDictionary<string, string>? XmlDocs { get; set; } |
||||
|
||||
/// <summary>Members inherited from an ancestor type specified by the Key of <see cref="Inherited"/>.</summary>
|
||||
public class InheritedMembers : Relationships |
||||
{ |
||||
/// <summary>The simple, non-complex members inherited from another <see cref="Type"/>
|
||||
/// in mermaid class diagram syntax.</summary>
|
||||
public string? FlatMembers { get; set; } |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.RegularExpressions; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.CSharp; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
using CD = ClassDiagrammer; |
||||
|
||||
/* See class diagram syntax |
||||
* reference (may be outdated!) https://mermaid.js.org/syntax/classDiagram.html
|
||||
* lexical definition https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/diagrams/class/parser/classDiagram.jison */
|
||||
|
||||
/// <summary>Produces mermaid class diagram syntax for a filtered list of types from a specified .Net assembly.</summary>
|
||||
public partial class ClassDiagrammerFactory |
||||
{ |
||||
private readonly XmlDocumentationFormatter? xmlDocs; |
||||
private readonly DecompilerSettings decompilerSettings; |
||||
|
||||
private ITypeDefinition[]? selectedTypes; |
||||
private Dictionary<IType, string>? uniqueIds; |
||||
private Dictionary<IType, string>? labels; |
||||
private Dictionary<string, string>? outsideReferences; |
||||
|
||||
public ClassDiagrammerFactory(XmlDocumentationFormatter? xmlDocs) |
||||
{ |
||||
this.xmlDocs = xmlDocs; |
||||
|
||||
//TODO not sure LanguageVersion.Latest is the wisest choice here; maybe cap this for better mermaid compatibility?
|
||||
decompilerSettings = new DecompilerSettings(Decompiler.CSharp.LanguageVersion.Latest) { |
||||
AutomaticProperties = true // for IsHidden to return true for backing fields
|
||||
}; |
||||
} |
||||
|
||||
public CD BuildModel(string assemblyPath, string? include, string? exclude) |
||||
{ |
||||
CSharpDecompiler decompiler = new(assemblyPath, decompilerSettings); |
||||
MetadataModule mainModule = decompiler.TypeSystem.MainModule; |
||||
IEnumerable<ITypeDefinition> allTypes = mainModule.TypeDefinitions; |
||||
|
||||
selectedTypes = FilterTypes(allTypes, |
||||
include == null ? null : new Regex(include, RegexOptions.Compiled), |
||||
exclude == null ? null : new Regex(exclude, RegexOptions.Compiled)).ToArray(); |
||||
|
||||
// generate dictionary to read names from later
|
||||
uniqueIds = GenerateUniqueIds(selectedTypes); |
||||
labels = []; |
||||
outsideReferences = []; |
||||
|
||||
Dictionary<string, CD.Type[]> typesByNamespace = selectedTypes.GroupBy(t => t.Namespace).OrderBy(g => g.Key).ToDictionary(g => g.Key, |
||||
ns => ns.OrderBy(t => t.FullName).Select(type => type.Kind == TypeKind.Enum ? BuildEnum(type) : BuildType(type)).ToArray()); |
||||
|
||||
string[] excluded = allTypes.Except(selectedTypes).Select(t => t.ReflectionName).ToArray(); |
||||
|
||||
return new CD { |
||||
SourceAssemblyName = mainModule.AssemblyName, |
||||
SourceAssemblyVersion = mainModule.AssemblyVersion.ToString(), |
||||
TypesByNamespace = typesByNamespace, |
||||
OutsideReferences = outsideReferences, |
||||
Excluded = excluded |
||||
}; |
||||
} |
||||
|
||||
/// <summary>The default strategy for pre-filtering the <paramref name="typeDefinitions"/> available in the HTML diagrammer.
|
||||
/// Applies <see cref="IsIncludedByDefault(ITypeDefinition)"/> as well as
|
||||
/// matching by <paramref name="include"/> and not by <paramref name="exclude"/>.</summary>
|
||||
/// <returns>The types to effectively include in the HTML diagrammer.</returns>
|
||||
protected virtual IEnumerable<ITypeDefinition> FilterTypes(IEnumerable<ITypeDefinition> typeDefinitions, Regex? include, Regex? exclude) |
||||
=> typeDefinitions.Where(type => IsIncludedByDefault(type) |
||||
&& (include?.IsMatch(type.ReflectionName) != false) // applying optional whitelist filter
|
||||
&& (exclude?.IsMatch(type.ReflectionName) != true)); // applying optional blacklist filter
|
||||
|
||||
/// <summary>The strategy for deciding whether a <paramref name="type"/> should be included
|
||||
/// in the HTML diagrammer by default. Excludes compiler-generated and their nested types.</summary>
|
||||
protected virtual bool IsIncludedByDefault(ITypeDefinition type) |
||||
=> !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(); |
||||
} |
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.IO; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
public partial class GenerateHtmlDiagrammer |
||||
{ |
||||
/// <summary>A helper for loading resources embedded in the nested html folder.</summary>
|
||||
private static class EmbeddedResource |
||||
{ |
||||
internal static string ReadText(string resourceName) |
||||
{ |
||||
Stream stream = GetStream(resourceName); |
||||
using StreamReader reader = new(stream); |
||||
return reader.ReadToEnd(); |
||||
} |
||||
|
||||
internal static void CopyTo(string outputFolder, string resourceName) |
||||
{ |
||||
Stream resourceStream = GetStream(resourceName); |
||||
using FileStream output = new(Path.Combine(outputFolder, resourceName), FileMode.Create, FileAccess.Write); |
||||
resourceStream.CopyTo(output); |
||||
} |
||||
|
||||
private static Stream GetStream(string resourceName) |
||||
{ |
||||
var type = typeof(EmbeddedResource); |
||||
var assembly = type.Assembly; |
||||
var fullResourceName = $"{type.Namespace}.html.{resourceName}"; |
||||
Stream? stream = assembly.GetManifestResourceStream(fullResourceName); |
||||
|
||||
if (stream == null) |
||||
throw new FileNotFoundException("Resource not found.", fullResourceName); |
||||
|
||||
return stream; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.RegularExpressions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions |
||||
{ |
||||
internal static class StringExtensions |
||||
{ |
||||
/// <summary>Replaces all consecutive horizontal white space characters in
|
||||
/// <paramref name="input"/> with <paramref name="normalizeTo"/> while leaving line breaks intact.</summary>
|
||||
internal static string NormalizeHorizontalWhiteSpace(this string input, string normalizeTo = " ") |
||||
=> Regex.Replace(input, @"[ \t]+", normalizeTo); |
||||
|
||||
/// <summary>Replaces all occurrences of <paramref name="oldValues"/> in
|
||||
/// <paramref name="input"/> with <paramref name="newValue"/>.</summary>
|
||||
internal static string ReplaceAll(this string input, IEnumerable<string> oldValues, string? newValue) |
||||
=> oldValues.Aggregate(input, (aggregate, oldValue) => aggregate.Replace(oldValue, newValue)); |
||||
|
||||
/// <summary>Joins the specified <paramref name="strings"/> to a single one
|
||||
/// using the specified <paramref name="separator"/> as a delimiter.</summary>
|
||||
/// <param name="pad">Whether to pad the start and end of the string with the <paramref name="separator"/> as well.</param>
|
||||
internal static string Join(this IEnumerable<string?>? strings, string separator, bool pad = false) |
||||
{ |
||||
if (strings == null) |
||||
return string.Empty; |
||||
|
||||
var joined = string.Join(separator, strings); |
||||
return pad ? string.Concat(separator, joined, separator) : joined; |
||||
} |
||||
|
||||
/// <summary>Formats all items in <paramref name="collection"/> using the supplied <paramref name="format"/> strategy
|
||||
/// and returns a string collection - even if the incoming <paramref name="collection"/> is null.</summary>
|
||||
internal static IEnumerable<string> FormatAll<T>(this IEnumerable<T>? collection, Func<T, string> format) |
||||
=> collection?.Select(format) ?? []; |
||||
} |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions |
||||
{ |
||||
internal static class TypeExtensions |
||||
{ |
||||
internal static bool IsObject(this IType t) => t.IsKnownType(KnownTypeCode.Object); |
||||
internal static bool IsInterface(this IType t) => t.Kind == TypeKind.Interface; |
||||
|
||||
internal static bool TryGetNullableType(this IType type, [MaybeNullWhen(false)] out IType typeArg) |
||||
{ |
||||
bool isNullable = type.IsKnownType(KnownTypeCode.NullableOfT); |
||||
typeArg = isNullable ? type.TypeArguments.Single() : null; |
||||
return isNullable; |
||||
} |
||||
} |
||||
|
||||
internal static class MemberInfoExtensions |
||||
{ |
||||
/// <summary>Groups the <paramref name="members"/> into a dictionary
|
||||
/// with <see cref="IMember.DeclaringType"/> keys.</summary>
|
||||
internal static Dictionary<IType, T[]> GroupByDeclaringType<T>(this IEnumerable<T> members) where T : IMember |
||||
=> members.GroupByDeclaringType(m => m); |
||||
|
||||
/// <summary>Groups the <paramref name="objectsWithMembers"/> into a dictionary
|
||||
/// with <see cref="IMember.DeclaringType"/> keys using <paramref name="getMember"/>.</summary>
|
||||
internal static Dictionary<IType, T[]> GroupByDeclaringType<T>(this IEnumerable<T> objectsWithMembers, Func<T, IMember> getMember) |
||||
=> objectsWithMembers.GroupBy(m => getMember(m).DeclaringType).ToDictionary(g => g.Key, g => g.ToArray()); |
||||
} |
||||
|
||||
internal static class DictionaryExtensions |
||||
{ |
||||
/// <summary>Returns the <paramref name="dictionary"/>s value for the specified <paramref name="key"/>
|
||||
/// if available and otherwise the default for <typeparamref name="Tout"/>.</summary>
|
||||
internal static Tout? GetValue<T, Tout>(this IDictionary<T, Tout> dictionary, T key) |
||||
=> dictionary.TryGetValue(key, out Tout? value) ? value : default; |
||||
} |
||||
} |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
using CD = ClassDiagrammer; |
||||
|
||||
partial class ClassDiagrammerFactory |
||||
{ |
||||
private CD.Type BuildEnum(ITypeDefinition type) |
||||
{ |
||||
IField[] fields = type.GetFields(f => f.IsConst && f.IsStatic && f.Accessibility == Accessibility.Public).ToArray(); |
||||
Dictionary<string, string>? docs = xmlDocs?.GetXmlDocs(type, fields); |
||||
string name = GetName(type), typeId = GetId(type); |
||||
|
||||
var body = fields.Select(f => f.Name).Prepend("<<Enumeration>>") |
||||
.Join(CD.NewLine + " ", pad: true).TrimEnd(' '); |
||||
|
||||
return new CD.Type { |
||||
Id = typeId, |
||||
Name = name == typeId ? null : name, |
||||
Body = $"class {typeId} {{{body}}}", |
||||
XmlDocs = docs |
||||
}; |
||||
} |
||||
|
||||
private CD.Type BuildType(ITypeDefinition type) |
||||
{ |
||||
string typeId = GetId(type); |
||||
IMethod[] methods = GetMethods(type).ToArray(); |
||||
IProperty[] properties = type.GetProperties().ToArray(); |
||||
IProperty[] hasOneRelations = GetHasOneRelations(properties); |
||||
(IProperty property, IType elementType)[] hasManyRelations = GetManyRelations(properties); |
||||
|
||||
var propertyNames = properties.Select(p => p.Name).ToArray(); |
||||
IField[] fields = GetFields(type, properties); |
||||
|
||||
#region split members up by declaring type
|
||||
// enables the diagrammer to exclude inherited members from derived types if they are already rendered in a base type
|
||||
Dictionary<IType, IProperty[]> flatPropertiesByType = properties.Except(hasOneRelations) |
||||
.Except(hasManyRelations.Select(r => r.property)).GroupByDeclaringType(); |
||||
|
||||
Dictionary<IType, IProperty[]> hasOneRelationsByType = hasOneRelations.GroupByDeclaringType(); |
||||
Dictionary<IType, (IProperty property, IType elementType)[]> hasManyRelationsByType = hasManyRelations.GroupByDeclaringType(r => r.property); |
||||
Dictionary<IType, IField[]> fieldsByType = fields.GroupByDeclaringType(); |
||||
Dictionary<IType, IMethod[]> methodsByType = methods.GroupByDeclaringType(); |
||||
#endregion
|
||||
|
||||
#region build diagram definitions for the type itself and members declared by it
|
||||
string members = flatPropertiesByType.GetValue(type).FormatAll(FormatFlatProperty) |
||||
.Concat(methodsByType.GetValue(type).FormatAll(FormatMethod)) |
||||
.Concat(fieldsByType.GetValue(type).FormatAll(FormatField)) |
||||
.Join(CD.NewLine + " ", pad: true); |
||||
|
||||
// see https://mermaid.js.org/syntax/classDiagram.html#annotations-on-classes
|
||||
string? annotation = type.IsInterface() ? "Interface" : type.IsAbstract ? type.IsSealed ? "Service" : "Abstract" : null; |
||||
|
||||
string body = annotation == null ? members.TrimEnd(' ') : members + $"<<{annotation}>>" + CD.NewLine; |
||||
#endregion
|
||||
|
||||
Dictionary<string, string>? docs = xmlDocs?.GetXmlDocs(type, fields, properties, methods); |
||||
|
||||
#region build diagram definitions for inherited members by declaring type
|
||||
string explicitTypePrefix = typeId + " : "; |
||||
|
||||
// get ancestor types this one is inheriting members from
|
||||
Dictionary<string, CD.Type.InheritedMembers> inheritedMembersByType = type.GetNonInterfaceBaseTypes().Where(t => t != type && !t.IsObject()) |
||||
// and group inherited members by declaring type
|
||||
.ToDictionary(GetId, t => { |
||||
IEnumerable<string> flatMembers = flatPropertiesByType.GetValue(t).FormatAll(p => explicitTypePrefix + FormatFlatProperty(p)) |
||||
.Concat(methodsByType.GetValue(t).FormatAll(m => explicitTypePrefix + FormatMethod(m))) |
||||
.Concat(fieldsByType.GetValue(t).FormatAll(f => explicitTypePrefix + FormatField(f))); |
||||
|
||||
return new CD.Type.InheritedMembers { |
||||
FlatMembers = flatMembers.Any() ? flatMembers.Join(CD.NewLine) : null, |
||||
HasOne = MapHasOneRelations(hasOneRelationsByType, t), |
||||
HasMany = MapHasManyRelations(hasManyRelationsByType, t) |
||||
}; |
||||
}); |
||||
#endregion
|
||||
|
||||
string typeName = GetName(type); |
||||
|
||||
return new CD.Type { |
||||
Id = typeId, |
||||
Name = typeName == typeId ? null : typeName, |
||||
Body = $"class {typeId} {{{body}}}", |
||||
HasOne = MapHasOneRelations(hasOneRelationsByType, type), |
||||
HasMany = MapHasManyRelations(hasManyRelationsByType, type), |
||||
BaseType = GetBaseType(type), |
||||
Interfaces = GetInterfaces(type), |
||||
Inherited = inheritedMembersByType, |
||||
XmlDocs = docs |
||||
}; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.RegularExpressions; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.CSharp; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
partial class ClassDiagrammerFactory |
||||
{ |
||||
/// <summary>Wraps a <see cref="CSharpDecompiler"/> method configurable via <see cref="decompilerSettings"/>
|
||||
/// that can be used to determine whether a member should be hidden.</summary>
|
||||
private bool IsHidden(IEntity entity) => CSharpDecompiler.MemberIsHidden(entity.ParentModule!.MetadataFile, entity.MetadataToken, decompilerSettings); |
||||
|
||||
private IField[] GetFields(ITypeDefinition type, IProperty[] properties) |
||||
// only display fields that are not backing properties of the same name and type
|
||||
=> type.GetFields(f => !IsHidden(f) // removes compiler-generated backing fields
|
||||
/* tries to remove remaining manual backing fields by matching type and name */ |
||||
&& !properties.Any(p => f.ReturnType.Equals(p.ReturnType) |
||||
&& Regex.IsMatch(f.Name, "_?" + p.Name, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.NonBacktracking))).ToArray(); |
||||
|
||||
private static IEnumerable<IMethod> GetMethods(ITypeDefinition type) => type.GetMethods(m => |
||||
!m.IsOperator && !m.IsCompilerGenerated() |
||||
&& (m.DeclaringType == type // include methods if self-declared
|
||||
/* but exclude methods declared by object and their overrides, if inherited */ |
||||
|| (!m.DeclaringType.IsObject() |
||||
&& (!m.IsOverride || !InheritanceHelper.GetBaseMember(m).DeclaringType.IsObject())))); |
||||
|
||||
private string FormatMethod(IMethod method) |
||||
{ |
||||
string parameters = method.Parameters.Select(p => $"{GetName(p.Type)} {p.Name}").Join(", "); |
||||
string? modifier = method.IsAbstract ? "*" : method.IsStatic ? "$" : default; |
||||
string name = method.Name; |
||||
|
||||
if (method.IsExplicitInterfaceImplementation) |
||||
{ |
||||
IMember member = method.ExplicitlyImplementedInterfaceMembers.Single(); |
||||
name = GetName(member.DeclaringType) + '.' + member.Name; |
||||
} |
||||
|
||||
string? typeArguments = method.TypeArguments.Count == 0 ? null : $"❰{method.TypeArguments.Select(GetName).Join(", ")}❱"; |
||||
return $"{GetAccessibility(method.Accessibility)}{name}{typeArguments}({parameters}){modifier} {GetName(method.ReturnType)}"; |
||||
} |
||||
|
||||
private string FormatFlatProperty(IProperty property) |
||||
{ |
||||
char? visibility = GetAccessibility(property.Accessibility); |
||||
string? modifier = property.IsAbstract ? "*" : property.IsStatic ? "$" : default; |
||||
return $"{visibility}{GetName(property.ReturnType)} {property.Name}{modifier}"; |
||||
} |
||||
|
||||
private string FormatField(IField field) |
||||
{ |
||||
string? modifier = field.IsAbstract ? "*" : field.IsStatic ? "$" : default; |
||||
return $"{GetAccessibility(field.Accessibility)}{GetName(field.ReturnType)} {field.Name}{modifier}"; |
||||
} |
||||
|
||||
// see https://stackoverflow.com/a/16024302 for accessibility modifier flags
|
||||
private static char? GetAccessibility(Accessibility access) => access switch { |
||||
Accessibility.Private => '-', |
||||
Accessibility.ProtectedAndInternal or Accessibility.Internal => '~', |
||||
Accessibility.Protected or Accessibility.ProtectedOrInternal => '#', |
||||
Accessibility.Public => '+', |
||||
_ => default, |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
using CD = ClassDiagrammer; |
||||
|
||||
partial class ClassDiagrammerFactory |
||||
{ |
||||
private IProperty[] GetHasOneRelations(IProperty[] properties) => properties.Where(property => { |
||||
IType type = property.ReturnType; |
||||
|
||||
if (type.TryGetNullableType(out var typeArg)) |
||||
type = typeArg; |
||||
|
||||
return selectedTypes!.Contains(type); |
||||
}).ToArray(); |
||||
|
||||
private (IProperty property, IType elementType)[] GetManyRelations(IProperty[] properties) |
||||
=> properties.Select(property => { |
||||
IType elementType = property.ReturnType.GetElementTypeFromIEnumerable(property.Compilation, true, out bool? isGeneric); |
||||
|
||||
if (isGeneric == false && elementType.IsObject()) |
||||
{ |
||||
IProperty[] indexers = property.ReturnType.GetProperties( |
||||
p => p.IsIndexer && !p.ReturnType.IsObject(), |
||||
GetMemberOptions.IgnoreInheritedMembers).ToArray(); // TODO mayb order by declaring type instead of filtering
|
||||
|
||||
if (indexers.Length > 0) |
||||
elementType = indexers[0].ReturnType; |
||||
} |
||||
|
||||
return isGeneric == true && selectedTypes!.Contains(elementType) ? (property, elementType) : default; |
||||
}).Where(pair => pair != default).ToArray(); |
||||
|
||||
/// <summary>Returns the relevant direct super type the <paramref name="type"/> inherits from
|
||||
/// in a format matching <see cref="CD.Type.BaseType"/>.</summary>
|
||||
private Dictionary<string, string?>? GetBaseType(IType type) |
||||
{ |
||||
IType? relevantBaseType = type.DirectBaseTypes.SingleOrDefault(t => !t.IsInterface() && !t.IsObject()); |
||||
return relevantBaseType == null ? default : new[] { BuildRelationship(relevantBaseType) }.ToDictionary(r => r.to, r => r.label); |
||||
} |
||||
|
||||
/// <summary>Returns the direct interfaces implemented by <paramref name="type"/>
|
||||
/// in a format matching <see cref="CD.Type.Interfaces"/>.</summary>
|
||||
private Dictionary<string, string?[]>? GetInterfaces(ITypeDefinition type) |
||||
{ |
||||
var interfaces = type.DirectBaseTypes.Where(t => t.IsInterface()).ToArray(); |
||||
|
||||
return interfaces.Length == 0 ? null |
||||
: interfaces.Select(i => BuildRelationship(i)).GroupBy(r => r.to) |
||||
.ToDictionary(g => g.Key, g => g.Select(r => r.label).ToArray()); |
||||
} |
||||
|
||||
/// <summary>Returns the one-to-one relations from <paramref name="type"/> to other <see cref="CD.Type"/>s
|
||||
/// in a format matching <see cref="CD.Relationships.HasOne"/>.</summary>
|
||||
private Dictionary<string, string>? MapHasOneRelations(Dictionary<IType, IProperty[]> hasOneRelationsByType, IType type) |
||||
=> hasOneRelationsByType.GetValue(type)?.Select(p => { |
||||
IType type = p.ReturnType; |
||||
string label = p.Name; |
||||
|
||||
if (p.IsIndexer) |
||||
label += $"[{p.Parameters.Single().Type.Name} {p.Parameters.Single().Name}]"; |
||||
|
||||
if (type.TryGetNullableType(out var typeArg)) |
||||
{ |
||||
type = typeArg; |
||||
label += " ?"; |
||||
} |
||||
|
||||
return BuildRelationship(type, label); |
||||
}).ToDictionary(r => r.label!, r => r.to); |
||||
|
||||
/// <summary>Returns the one-to-many relations from <paramref name="type"/> to other <see cref="CD.Type"/>s
|
||||
/// in a format matching <see cref="CD.Relationships.HasMany"/>.</summary>
|
||||
private Dictionary<string, string>? MapHasManyRelations(Dictionary<IType, (IProperty property, IType elementType)[]> hasManyRelationsByType, IType type) |
||||
=> hasManyRelationsByType.GetValue(type)?.Select(relation => { |
||||
(IProperty property, IType elementType) = relation; |
||||
return BuildRelationship(elementType, property.Name); |
||||
}).ToDictionary(r => r.label!, r => r.to); |
||||
|
||||
/// <summary>Builds references to super types and (one/many) relations,
|
||||
/// recording outside references on the way and applying labels if required.</summary>
|
||||
/// <param name="type">The type to reference.</param>
|
||||
/// <param name="propertyName">Used only for property one/many relations.</param>
|
||||
private (string to, string? label) BuildRelationship(IType type, string? propertyName = null) |
||||
{ |
||||
(string id, IType? openGeneric) = GetIdAndOpenGeneric(type); |
||||
AddOutsideReference(id, openGeneric ?? type); |
||||
|
||||
// label the relation with the property name if provided or the closed generic type for super types
|
||||
string? label = propertyName ?? (openGeneric == null ? null : GetName(type)); |
||||
|
||||
return (to: id, label); |
||||
} |
||||
|
||||
private void AddOutsideReference(string typeId, IType type) |
||||
{ |
||||
if (!selectedTypes!.Contains(type) && outsideReferences?.ContainsKey(typeId) == false) |
||||
outsideReferences.Add(typeId, type.Namespace + '.' + GetName(type)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
using CD = ClassDiagrammer; |
||||
|
||||
public partial class ClassDiagrammerFactory |
||||
{ |
||||
/// <summary>Generates a dictionary of unique and short, but human readable identifiers for
|
||||
/// <paramref name="types"/> to be able to safely reference them in any combination.</summary>
|
||||
private static Dictionary<IType, string> GenerateUniqueIds(IEnumerable<ITypeDefinition> types) |
||||
{ |
||||
Dictionary<IType, string> uniqueIds = []; |
||||
var groups = types.GroupBy(t => t.Name); |
||||
|
||||
// simplified handling for the majority of unique types
|
||||
foreach (var group in groups.Where(g => g.Count() == 1)) |
||||
uniqueIds[group.First()] = SanitizeTypeName(group.Key); |
||||
|
||||
// number non-unique types
|
||||
foreach (var group in groups.Where(g => g.Count() > 1)) |
||||
{ |
||||
var counter = 0; |
||||
|
||||
foreach (var type in group) |
||||
uniqueIds[type] = type.Name + ++counter; |
||||
} |
||||
|
||||
return uniqueIds; |
||||
} |
||||
|
||||
private string GetId(IType type) => GetIdAndOpenGeneric(type).id; |
||||
|
||||
/// <summary>For a non- or open generic <paramref name="type"/>, returns a unique identifier and null.
|
||||
/// For a closed generic <paramref name="type"/>, returns the open generic type and the unique identifier of it.
|
||||
/// That helps connecting closed generic references (e.g. Store<int>) to their corresponding
|
||||
/// open generic <see cref="CD.Type"/> (e.g. Store<T>) like in <see cref="BuildRelationship(IType, string?)"/>.</summary>
|
||||
private (string id, IType? openGeneric) GetIdAndOpenGeneric(IType type) |
||||
{ |
||||
// get open generic type if type is a closed generic (i.e. has type args none of which are parameters)
|
||||
var openGeneric = type is ParameterizedType generic && !generic.TypeArguments.Any(a => a is ITypeParameter) |
||||
? generic.GenericType : null; |
||||
|
||||
type = openGeneric ?? type; // reference open instead of closed generic type
|
||||
|
||||
if (uniqueIds!.TryGetValue(type, out var uniqueId)) |
||||
return (uniqueId, openGeneric); // types included by FilterTypes
|
||||
|
||||
// types excluded by FilterTypes
|
||||
string? typeParams = type.TypeParameterCount == 0 ? null : ("_" + type.TypeParameters.Select(GetId).Join("_")); |
||||
|
||||
var id = SanitizeTypeName(type.FullName.Replace('.', '_')) |
||||
+ typeParams; // to achieve uniqueness for types with same FullName (i.e. generic overloads)
|
||||
|
||||
uniqueIds![type] = id; // update dictionary to avoid re-generation
|
||||
return (id, openGeneric); |
||||
} |
||||
|
||||
private static string SanitizeTypeName(string typeName) |
||||
=> typeName.Replace('<', '_').Replace('>', '_'); // for module of executable
|
||||
} |
||||
} |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
public partial class ClassDiagrammerFactory |
||||
{ |
||||
/// <summary>Returns a cached display name for <paramref name="type"/>.</summary>
|
||||
private string GetName(IType type) |
||||
{ |
||||
if (labels!.TryGetValue(type, out string? value)) |
||||
return value; // return cached value
|
||||
|
||||
return labels[type] = GenerateName(type); // generate and cache new value
|
||||
} |
||||
|
||||
/// <summary>Generates a display name for <paramref name="type"/>.</summary>
|
||||
private string GenerateName(IType type) |
||||
{ |
||||
// non-generic types
|
||||
if (type.TypeParameterCount < 1) |
||||
{ |
||||
if (type is ArrayType array) |
||||
return GetName(array.ElementType) + "[]"; |
||||
|
||||
if (type is ByReferenceType byReference) |
||||
return "&" + GetName(byReference.ElementType); |
||||
|
||||
ITypeDefinition? typeDefinition = type.GetDefinition(); |
||||
|
||||
if (typeDefinition == null) |
||||
return type.Name; |
||||
|
||||
if (typeDefinition.KnownTypeCode == KnownTypeCode.None) |
||||
{ |
||||
if (type.DeclaringType == null) |
||||
return type.Name.Replace('<', '❰').Replace('>', '❱'); // for module of executable
|
||||
else |
||||
return type.DeclaringType.Name + '+' + type.Name; // nested types
|
||||
} |
||||
|
||||
return KnownTypeReference.GetCSharpNameByTypeCode(typeDefinition.KnownTypeCode) ?? type.Name; |
||||
} |
||||
|
||||
// nullable types
|
||||
if (type.TryGetNullableType(out var nullableType)) |
||||
return GetName(nullableType) + "?"; |
||||
|
||||
// other generic types
|
||||
string typeArguments = type.TypeArguments.Select(GetName).Join(", "); |
||||
return type.Name + $"❰{typeArguments}❱"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
/// <summary>The command for creating an HTML5 diagramming app with an API optimized for binding command line parameters.
|
||||
/// To use it outside of that context, set its properties and call <see cref="Run"/>.</summary>
|
||||
public partial class GenerateHtmlDiagrammer |
||||
{ |
||||
internal const string RepoUrl = "https://github.com/icsharpcode/ILSpy"; |
||||
|
||||
public required string Assembly { get; set; } |
||||
public string? OutputFolder { get; set; } |
||||
|
||||
public string? Include { get; set; } |
||||
public string? Exclude { get; set; } |
||||
public bool JsonOnly { get; set; } |
||||
public bool ReportExludedTypes { get; set; } |
||||
public string? XmlDocs { get; set; } |
||||
|
||||
/// <summary>Namespaces to strip from <see cref="XmlDocs"/>.
|
||||
/// Implemented as a list of exact replacements instead of a single, more powerful RegEx because replacement in
|
||||
/// <see cref="XmlDocumentationFormatter.GetDoco(Decompiler.TypeSystem.IEntity)"/>
|
||||
/// happens on the unstructured string where matching and replacing the namespaces of referenced types, members and method parameters
|
||||
/// using RegExes would add a lot of complicated RegEx-heavy code for a rather unimportant feature.</summary>
|
||||
public IEnumerable<string>? StrippedNamespaces { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Text.Json; |
||||
using System.Text.Json.Serialization; |
||||
|
||||
using ICSharpCode.Decompiler.Documentation; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
partial class GenerateHtmlDiagrammer |
||||
{ |
||||
public void Run() |
||||
{ |
||||
var assemblyPath = GetPath(Assembly); |
||||
XmlDocumentationFormatter? xmlDocs = CreateXmlDocsFormatter(assemblyPath); |
||||
ClassDiagrammer model = BuildModel(assemblyPath, xmlDocs); |
||||
GenerateOutput(assemblyPath, model); |
||||
} |
||||
|
||||
protected virtual XmlDocumentationFormatter? CreateXmlDocsFormatter(string assemblyPath) |
||||
{ |
||||
var xmlDocsPath = XmlDocs == null ? Path.ChangeExtension(assemblyPath, ".xml") : GetPath(XmlDocs); |
||||
XmlDocumentationFormatter? xmlDocs = null; |
||||
|
||||
if (File.Exists(xmlDocsPath)) |
||||
xmlDocs = new XmlDocumentationFormatter(new XmlDocumentationProvider(xmlDocsPath), StrippedNamespaces?.ToArray()); |
||||
else |
||||
Console.WriteLine("No XML documentation file found. Continuing without."); |
||||
|
||||
return xmlDocs; |
||||
} |
||||
|
||||
protected virtual ClassDiagrammer BuildModel(string assemblyPath, XmlDocumentationFormatter? xmlDocs) |
||||
=> new ClassDiagrammerFactory(xmlDocs).BuildModel(assemblyPath, Include, Exclude); |
||||
|
||||
private string SerializeModel(ClassDiagrammer diagrammer) |
||||
{ |
||||
object jsonModel = new { |
||||
diagrammer.OutsideReferences, |
||||
|
||||
/* convert collections to dictionaries for easier access in ES using |
||||
* for (let [key, value] of Object.entries(dictionary)) */ |
||||
TypesByNamespace = diagrammer.TypesByNamespace.ToDictionary(ns => ns.Key, |
||||
ns => ns.Value.ToDictionary(t => t.Id, t => t)) |
||||
}; |
||||
|
||||
// wrap model including the data required for doing the template replacement in a JS build task
|
||||
if (JsonOnly) |
||||
{ |
||||
jsonModel = new { |
||||
diagrammer.SourceAssemblyName, |
||||
diagrammer.SourceAssemblyVersion, |
||||
BuilderVersion = DecompilerVersionInfo.FullVersionWithCommitHash, |
||||
RepoUrl, |
||||
// pre-serialize to a string so that we don't have to re-serialize it in the JS build task
|
||||
Model = Serialize(jsonModel) |
||||
}; |
||||
} |
||||
|
||||
return Serialize(jsonModel); |
||||
} |
||||
|
||||
private static JsonSerializerOptions serializerOptions = new() { |
||||
WriteIndented = true, |
||||
// avoid outputting null properties unnecessarily
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
||||
}; |
||||
|
||||
private static string Serialize(object json) => JsonSerializer.Serialize(json, serializerOptions); |
||||
|
||||
private void GenerateOutput(string assemblyPath, ClassDiagrammer model) |
||||
{ |
||||
string modelJson = SerializeModel(model); |
||||
|
||||
// If no out folder is specified, default to a "<Input.Assembly.Name> diagrammer" folder next to the input assembly.
|
||||
var outputFolder = OutputFolder |
||||
?? Path.Combine( |
||||
Path.GetDirectoryName(assemblyPath) ?? string.Empty, |
||||
Path.GetFileNameWithoutExtension(assemblyPath) + " diagrammer"); |
||||
|
||||
if (!Directory.Exists(outputFolder)) |
||||
Directory.CreateDirectory(outputFolder); |
||||
|
||||
if (JsonOnly) |
||||
{ |
||||
File.WriteAllText(Path.Combine(outputFolder, "model.json"), modelJson); |
||||
Console.WriteLine("Successfully generated model.json for HTML diagrammer."); |
||||
} |
||||
else |
||||
{ |
||||
var htmlTemplate = EmbeddedResource.ReadText("template.html"); |
||||
|
||||
var html = htmlTemplate |
||||
.Replace("{{SourceAssemblyName}}", model.SourceAssemblyName) |
||||
.Replace("{{SourceAssemblyVersion}}", model.SourceAssemblyVersion) |
||||
.Replace("{{BuilderVersion}}", DecompilerVersionInfo.FullVersionWithCommitHash) |
||||
.Replace("{{RepoUrl}}", RepoUrl) |
||||
.Replace("{{Model}}", modelJson); |
||||
|
||||
File.WriteAllText(Path.Combine(outputFolder, "index.html"), html); |
||||
|
||||
// copy required resources to output folder while flattening paths if required
|
||||
foreach (var resource in new[] { "styles.css", "ILSpy.ico", "script.js" }) |
||||
EmbeddedResource.CopyTo(outputFolder, resource); |
||||
|
||||
Console.WriteLine("Successfully generated HTML diagrammer."); |
||||
} |
||||
|
||||
if (ReportExludedTypes) |
||||
{ |
||||
string excludedTypes = model.Excluded.Join(Environment.NewLine); |
||||
File.WriteAllText(Path.Combine(outputFolder, "excluded types.txt"), excludedTypes); |
||||
} |
||||
} |
||||
|
||||
private protected virtual string GetPath(string pathOrUri) |
||||
{ |
||||
// convert file:// style argument, see https://stackoverflow.com/a/38245329
|
||||
if (!Uri.TryCreate(pathOrUri, UriKind.RelativeOrAbsolute, out Uri? uri)) |
||||
throw new ArgumentException("'{0}' is not a valid URI", pathOrUri); |
||||
|
||||
// support absolute paths as well as file:// URIs and interpret relative path as relative to the current directory
|
||||
return uri.IsAbsoluteUri ? uri.AbsolutePath : pathOrUri; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
# How does it work? |
||||
|
||||
To **extract the type info from the source assembly**, ILSpy side-loads it including all its dependencies. |
||||
|
||||
The extracted type info is **structured into a model optimized for the HTML diagrammer** and serialized to JSON. The model is a mix between drop-in type definitions in mermaid class diagram syntax and destructured metadata about relations, inheritance and documentation comments. |
||||
|
||||
> The JSON type info is injected into the `template.html` alongside other resources like the `script.js` at corresponding `{{placeholders}}`. It comes baked into the HTML diagrammer to enable |
||||
> - accessing the data and |
||||
> - importing the mermaid module from a CDN |
||||
> |
||||
> locally without running a web server [while also avoiding CORS restrictions.](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#file_origins) |
||||
|
||||
In the final step, the **HTML diagrammer app re-assembles the type info** based on the in-app type selection and rendering options **to generate [mermaid class diagrams](https://mermaid.js.org/syntax/classDiagram.html)** with the types, their relations and as much inheritance detail as you need. |
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2024 Holger Schmidt
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.RegularExpressions; |
||||
|
||||
using ICSharpCode.Decompiler.Documentation; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX.MermaidDiagrammer.Extensions; |
||||
|
||||
namespace ICSharpCode.ILSpyX.MermaidDiagrammer |
||||
{ |
||||
/// <summary>Wraps the <see cref="IDocumentationProvider"/> to prettify XML documentation comments.
|
||||
/// Make sure to enable XML documentation output, see
|
||||
/// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/#create-xml-documentation-output .</summary>
|
||||
public class XmlDocumentationFormatter |
||||
{ |
||||
/// <summary>Matches XML indent.</summary>
|
||||
protected const string linePadding = @"^[ \t]+|[ \t]+$"; |
||||
|
||||
/// <summary>Matches reference tags including "see href", "see cref" and "paramref name"
|
||||
/// with the cref value being prefixed by symbol-specific letter and a colon
|
||||
/// including the quotes around the attribute value and the closing slash of the tag containing the attribute.</summary>
|
||||
protected const string referenceAttributes = @"(see\s.ref=""(.:)?)|(paramref\sname="")|(""\s/)"; |
||||
|
||||
private readonly IDocumentationProvider docs; |
||||
private readonly Regex noiseAndPadding; |
||||
|
||||
public XmlDocumentationFormatter(IDocumentationProvider docs, string[]? strippedNamespaces) |
||||
{ |
||||
this.docs = docs; |
||||
List<string> regexes = new() { linePadding, referenceAttributes }; |
||||
|
||||
if (strippedNamespaces?.Length > 0) |
||||
regexes.AddRange(strippedNamespaces.Select(ns => $"({ns.Replace(".", "\\.")}\\.)")); |
||||
|
||||
noiseAndPadding = new Regex(regexes.Join("|"), RegexOptions.Multiline); // builds an OR | combined regex
|
||||
} |
||||
|
||||
internal Dictionary<string, string>? GetXmlDocs(ITypeDefinition type, params IMember[][] memberCollections) |
||||
{ |
||||
Dictionary<string, string>? docs = new(); |
||||
AddXmlDocEntry(docs, type); |
||||
|
||||
foreach (IMember[] members in memberCollections) |
||||
{ |
||||
foreach (IMember member in members) |
||||
AddXmlDocEntry(docs, member); |
||||
} |
||||
|
||||
return docs?.Keys.Count != 0 ? docs : default; |
||||
} |
||||
|
||||
protected virtual string? GetDoco(IEntity entity) |
||||
{ |
||||
string? comment = docs.GetDocumentation(entity)? |
||||
.ReplaceAll(["<summary>", "</summary>"], null) |
||||
.ReplaceAll(["<para>", "</para>"], ClassDiagrammer.NewLine).Trim() // to format
|
||||
.Replace('<', '[').Replace('>', ']'); // to prevent ugly escaped output
|
||||
|
||||
return comment == null ? null : noiseAndPadding.Replace(comment, string.Empty).NormalizeHorizontalWhiteSpace(); |
||||
} |
||||
|
||||
private void AddXmlDocEntry(Dictionary<string, string> docs, IEntity entity) |
||||
{ |
||||
string? doc = GetDoco(entity); |
||||
|
||||
if (string.IsNullOrEmpty(doc)) |
||||
return; |
||||
|
||||
string key = entity is IMember member ? member.Name : string.Empty; |
||||
docs[key] = doc; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
module.exports = { |
||||
'env': { |
||||
'commonjs': true, |
||||
'es6': true, |
||||
'browser': true |
||||
}, |
||||
'extends': 'eslint:recommended', |
||||
'parserOptions': { |
||||
'sourceType': 'module', |
||||
'ecmaVersion': 'latest' |
||||
}, |
||||
'rules': { |
||||
'indent': ['error', 4, { 'SwitchCase': 1 }], |
||||
'semi': ['error', 'always'] |
||||
} |
||||
}; |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
/node_modules |
||||
/class-diagrammer.html |
||||
/model.json |
||||
/package-lock.json |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
{ |
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558 |
||||
// for the documentation about the tasks.json format |
||||
"version": "2.0.0", |
||||
"tasks": [ |
||||
{ |
||||
"label": "Generate model.json", |
||||
"detail": "for editing the template, script or styles in inner dev loop of the HTML diagrammer", |
||||
"group": "build", |
||||
"type": "shell", |
||||
"command": [ |
||||
"$folder = '../../../ICSharpCode.ILSpyCmd/bin/Debug/net8.0/';", // to avoid repetition |
||||
"$exePath = $folder + 'ilspycmd.exe';", |
||||
"$assemblyPath = $folder + 'ICSharpCode.Decompiler.dll';", // comes with XML docs for testing the integration |
||||
"if (Test-Path $exePath) {", |
||||
" & $exePath $assemblyPath --generate-diagrammer --generate-diagrammer-json-only --outputdir .", |
||||
"} else {", |
||||
" Write-Host 'ilspycmd.exe Debug build not found. Please build it first.';", |
||||
" exit 1", |
||||
"}" |
||||
], |
||||
"problemMatcher": [] |
||||
}, |
||||
{ |
||||
"label": "Transpile .less", |
||||
"detail": "into .css files", |
||||
"group": "build", |
||||
"type": "gulp", |
||||
"task": "transpileLess", |
||||
"problemMatcher": [ |
||||
"$lessc" |
||||
] |
||||
}, |
||||
{ |
||||
"label": "Generate HTML diagrammer", |
||||
"detail": "from the template.html and a model.json", |
||||
"group": "build", |
||||
"type": "gulp", |
||||
"task": "generateHtmlDiagrammer", |
||||
"problemMatcher": [ |
||||
"$gulp-tsc" |
||||
] |
||||
}, |
||||
{ |
||||
"label": "Auto-rebuild on change", |
||||
"detail": "run build tasks automatically when source files change", |
||||
"type": "gulp", |
||||
"task": "autoRebuildOnChange", |
||||
"problemMatcher": [ |
||||
"$gulp-tsc" |
||||
] |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
To edit the HTML/JS/CSS for the HTML diagrammer, open this folder in Visual Studio Code. |
||||
|
||||
In that environment you'll find tasks (see https://code.visualstudio.com/Docs/editor/tasks to run and configure) |
||||
that you can run to |
||||
|
||||
1. Generate a model.json using the current Debug build of ilspycmd. |
||||
This is required to build a diagrammer for testing in development using task 3. |
||||
2. Transpile the .less into .css that is tracked by source control and embedded into ILSpyX. |
||||
3. Generate a diagrammer for testing in development from template.html and the model.json generated by task 1. |
||||
4. Auto-rebuild the development diagrammer by running either task 2 or 3 when the corresponding source files change. |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
const gulp = require('gulp'); |
||||
const less = require('gulp-less'); |
||||
const fs = require('fs'); |
||||
|
||||
function transpileLess (done) { |
||||
gulp |
||||
.src('styles.less') // source file(s) to process
|
||||
.pipe(less()) // pass them through the LESS compiler
|
||||
.pipe(gulp.dest(f => f.base)); // Use the base directory of the source file for output
|
||||
|
||||
done(); // signal task completion
|
||||
} |
||||
|
||||
function generateHtmlDiagrammer (done) { |
||||
// Read and parse model.json
|
||||
fs.readFile('model.json', 'utf8', function (err, data) { |
||||
if (err) { |
||||
console.error('Error reading model.json:', err); |
||||
done(err); |
||||
return; |
||||
} |
||||
|
||||
const model = JSON.parse(data); // Parse the JSON data
|
||||
|
||||
// Read template.html
|
||||
fs.readFile('template.html', 'utf8', function (err, templateContent) { |
||||
if (err) { |
||||
console.error('Error reading template.html:', err); |
||||
done(err); |
||||
return; |
||||
} |
||||
|
||||
// Replace placeholders in template with values from model
|
||||
let outputContent = templateContent; |
||||
|
||||
for (const [key, value] of Object.entries(model)) { |
||||
const placeholder = `{{${key}}}`; // Create the placeholder
|
||||
outputContent = outputContent.replace(new RegExp(placeholder, 'g'), value); // Replace all occurrences
|
||||
} |
||||
|
||||
// Save the replaced content
|
||||
fs.writeFile('class-diagrammer.html', outputContent, 'utf8', function (err) { |
||||
if (err) { |
||||
console.error('Error writing class-diagrammer.html:', err); |
||||
done(err); |
||||
return; |
||||
} |
||||
|
||||
console.log('class-diagrammer.html generated successfully.'); |
||||
done(); // Signal completion
|
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
exports.transpileLess = transpileLess; |
||||
exports.generateHtmlDiagrammer = generateHtmlDiagrammer; |
||||
|
||||
/* Run individual build tasks first, then start watching for changes |
||||
see https://code.visualstudio.com/Docs/languages/CSS#_automating-sassless-compilation */
|
||||
exports.autoRebuildOnChange = gulp.series(transpileLess, generateHtmlDiagrammer, function (done) { |
||||
// Watch for changes in source files and rerun the corresponding build task
|
||||
gulp.watch('styles.less', gulp.series(transpileLess)); |
||||
gulp.watch(['template.html', 'model.json'], gulp.series(generateHtmlDiagrammer)); |
||||
done(); // signal task completion
|
||||
}); |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
{ |
||||
"devDependencies": { |
||||
"eslint": "^8.57.1", |
||||
"gulp": "^4.0.2", |
||||
"gulp-less": "^5.0.0" |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,453 @@
@@ -0,0 +1,453 @@
|
||||
@keyframes fadeIn { |
||||
from { |
||||
opacity: 0; |
||||
} |
||||
to { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
@keyframes fadeOut { |
||||
from { |
||||
opacity: 1; |
||||
} |
||||
to { |
||||
opacity: 0; |
||||
} |
||||
} |
||||
body { |
||||
font-family: system-ui, sans-serif; |
||||
background: #4e54c8; |
||||
background-image: linear-gradient(to left, #8f94fb, #4e54c8); |
||||
} |
||||
input[type=text] { |
||||
border-radius: 3px; |
||||
} |
||||
button { |
||||
border-radius: 3px; |
||||
background-color: #aad; |
||||
border: none; |
||||
color: #117; |
||||
cursor: pointer; |
||||
} |
||||
button.icon { |
||||
font-size: 1em; |
||||
background-color: transparent; |
||||
} |
||||
button:disabled { |
||||
opacity: 0.5; |
||||
} |
||||
[type=checkbox], |
||||
[type=radio] { |
||||
cursor: pointer; |
||||
} |
||||
[type=checkbox] ~ label, |
||||
[type=radio] ~ label { |
||||
cursor: pointer; |
||||
} |
||||
fieldset { |
||||
border-radius: 5px; |
||||
} |
||||
select { |
||||
border: none; |
||||
border-radius: 3px; |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 1)); |
||||
color: whitesmoke; |
||||
} |
||||
select option:checked { |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 1)); |
||||
color: darkorange; |
||||
} |
||||
.flx:not([hidden]) { |
||||
display: flex; |
||||
} |
||||
.flx:not([hidden]).col { |
||||
flex-direction: column; |
||||
} |
||||
.flx:not([hidden]).spaced { |
||||
justify-content: space-between; |
||||
} |
||||
.flx:not([hidden]).gap { |
||||
gap: 0.5em; |
||||
} |
||||
.flx:not([hidden]).aligned { |
||||
align-items: center; |
||||
} |
||||
.flx:not([hidden]) .grow { |
||||
flex-grow: 1; |
||||
} |
||||
.collapse.vertical { |
||||
max-height: 0; |
||||
overflow: hidden; |
||||
transition: max-height ease-in-out 0.5s; |
||||
} |
||||
.collapse.vertical.open { |
||||
max-height: 100vh; |
||||
} |
||||
.collapse.horizontal { |
||||
max-width: 0; |
||||
padding: 0; |
||||
margin: 0; |
||||
transition: all ease-in-out 0.5s; |
||||
overflow: hidden; |
||||
} |
||||
.collapse.horizontal.open { |
||||
padding: revert; |
||||
max-width: 100vw; |
||||
} |
||||
.toggle, |
||||
[data-toggles] { |
||||
cursor: pointer; |
||||
} |
||||
.container { |
||||
position: absolute; |
||||
inset: 0; |
||||
margin: 0; |
||||
} |
||||
.scndry { |
||||
font-size: smaller; |
||||
} |
||||
.mano-a-borsa { |
||||
transform: rotate(95deg); |
||||
cursor: pointer; |
||||
} |
||||
.mano-a-borsa:after { |
||||
content: '🤏'; |
||||
} |
||||
.trawl-net { |
||||
transform: rotate(180deg) translateY(-2px); |
||||
display: inline-block; |
||||
} |
||||
.trawl-net:after { |
||||
content: '🥅'; |
||||
} |
||||
.torch { |
||||
display: inline-block; |
||||
} |
||||
.torch:after { |
||||
content: '🔦'; |
||||
} |
||||
.pulsing { |
||||
animation: whiteBoxShadowPulse 2s 3; |
||||
} |
||||
@keyframes whiteBoxShadowPulse { |
||||
0% { |
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); |
||||
} |
||||
5% { |
||||
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0.5); |
||||
} |
||||
50% { |
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); |
||||
} |
||||
90% { |
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); |
||||
} |
||||
} |
||||
#content { |
||||
height: 100%; |
||||
position: relative; |
||||
} |
||||
#filter { |
||||
max-width: 0; |
||||
transition: max-width ease-in-out 0.5s; |
||||
overflow: hidden; |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 1)); |
||||
color: whitesmoke; |
||||
} |
||||
#filter.open { |
||||
max-width: 15em; |
||||
overflow: auto; |
||||
} |
||||
#filter.resizing { |
||||
transition: none; |
||||
} |
||||
#filter > * { |
||||
margin: 0.3em 0.3em 0; |
||||
} |
||||
#filter > *:last-child { |
||||
margin-bottom: 0.3em; |
||||
} |
||||
#filter #pre-filter-types { |
||||
min-width: 3em; |
||||
} |
||||
#filter [data-toggles="#info"] .torch { |
||||
transform: rotate(-90deg); |
||||
transition: transform 0.5s; |
||||
} |
||||
#filter [data-toggles="#info"][aria-expanded=true] .torch { |
||||
transform: rotate(-255deg); |
||||
} |
||||
#filter #info { |
||||
overflow: auto; |
||||
background-color: rgba(255, 255, 255, calc(1/16 * 2)); |
||||
} |
||||
#filter #info a.toggle { |
||||
color: whitesmoke; |
||||
} |
||||
#filter #info a.toggle img { |
||||
height: 1em; |
||||
} |
||||
#filter #type-select { |
||||
overflow: auto; |
||||
} |
||||
#filter #inheritance { |
||||
padding: 0.1em 0.75em 0.2em; |
||||
} |
||||
#filter #direction [type=radio] { |
||||
display: none; |
||||
} |
||||
#filter #direction [type=radio]:checked + label { |
||||
background-color: rgba(255, 255, 255, calc(1/16 * 4)); |
||||
} |
||||
#filter #direction label { |
||||
flex-grow: 1; |
||||
text-align: center; |
||||
margin: -1em 0 -0.7em; |
||||
padding-top: 0.2em; |
||||
} |
||||
#filter #direction label:first-of-type { |
||||
margin-left: -0.8em; |
||||
border-top-left-radius: 5px; |
||||
border-bottom-left-radius: 5px; |
||||
} |
||||
#filter #direction label:last-of-type { |
||||
margin-right: -0.8em; |
||||
border-top-right-radius: 5px; |
||||
border-bottom-right-radius: 5px; |
||||
} |
||||
#filter #actions { |
||||
margin-top: 1em; |
||||
justify-content: space-between; |
||||
} |
||||
#filter #actions #render { |
||||
font-weight: bold; |
||||
} |
||||
#filter #exportOptions { |
||||
overflow: auto; |
||||
background-color: rgba(255, 255, 255, calc(1/16 * 2)); |
||||
} |
||||
#filter #exportOptions #save { |
||||
margin-right: 0.5em; |
||||
} |
||||
#filter #exportOptions #dimensions fieldset { |
||||
padding: 0.5em; |
||||
} |
||||
#filter #exportOptions #dimensions fieldset .scale-size { |
||||
margin-left: 0.5em; |
||||
} |
||||
#filter #exportOptions #dimensions fieldset .scale-size #scale-size { |
||||
width: 2.5em; |
||||
margin: 0 0.2em; |
||||
} |
||||
#filter-toggle { |
||||
padding: 0; |
||||
border-radius: 0; |
||||
background-color: #117; |
||||
color: whitesmoke; |
||||
} |
||||
#output { |
||||
overflow: auto; |
||||
} |
||||
#output > svg { |
||||
cursor: grab; |
||||
} |
||||
#output > svg:active { |
||||
cursor: grabbing; |
||||
} |
||||
#output .edgeLabels .edgeTerminals .edgeLabel { |
||||
color: whitesmoke; |
||||
} |
||||
#output .edgeLabels .edgeLabel { |
||||
border-radius: 3px; |
||||
} |
||||
#output .edgeLabels .edgeLabel .edgeLabel[title] { |
||||
color: darkgoldenrod; |
||||
} |
||||
#output path.relation { |
||||
stroke: whitesmoke; |
||||
} |
||||
#output g.nodes > g { |
||||
cursor: pointer; |
||||
} |
||||
#output g.nodes > g > rect { |
||||
rx: 5px; |
||||
ry: 5px; |
||||
} |
||||
#output g.nodes g.label .nodeLabel[title] { |
||||
color: darkgoldenrod; |
||||
} |
||||
#about { |
||||
position: absolute; |
||||
bottom: 2em; |
||||
right: 2em; |
||||
align-items: end; |
||||
} |
||||
#about #toaster { |
||||
margin-right: 2.8em; |
||||
} |
||||
#about #toaster span { |
||||
animation: 0.5s ease-in fadeIn; |
||||
border-radius: 0.5em; |
||||
padding: 0.5em; |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 2)); |
||||
color: whitesmoke; |
||||
} |
||||
#about #toaster span.leaving { |
||||
animation: 1s ease-in-out fadeOut; |
||||
} |
||||
#about .build-info { |
||||
align-items: end; |
||||
height: 2.3em; |
||||
border-radius: 7px; |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 3)); |
||||
color: whitesmoke; |
||||
} |
||||
#about .build-info > * { |
||||
height: 100%; |
||||
} |
||||
#about .build-info #build-info { |
||||
text-align: right; |
||||
} |
||||
#about .build-info #build-info > * { |
||||
padding: 0 0.5em; |
||||
} |
||||
#about .build-info #build-info a { |
||||
color: whitesmoke; |
||||
} |
||||
#about .build-info #build-info a:not(.project) { |
||||
text-decoration: none; |
||||
} |
||||
#about .build-info #build-info a span { |
||||
display: inline-block; |
||||
} |
||||
#pressed-keys { |
||||
position: fixed; |
||||
left: 50%; |
||||
transform: translateX(-50%); |
||||
font-size: 3em; |
||||
bottom: 1em; |
||||
opacity: 1; |
||||
border-radius: 0.5em; |
||||
padding: 0.5em; |
||||
background-color: rgba(0, 0, 0, calc(3/16 * 2)); |
||||
color: whitesmoke; |
||||
} |
||||
#pressed-keys.hidden { |
||||
transition: opacity 0.5s ease-in-out; |
||||
opacity: 0; |
||||
} |
||||
#mouse { |
||||
position: fixed; |
||||
transform: translateX(-50%) translateY(-50%); |
||||
height: 2em; |
||||
width: 2em; |
||||
pointer-events: none; |
||||
z-index: 9999; |
||||
border-radius: 1em; |
||||
border: solid 0.1em yellow; |
||||
} |
||||
#mouse.down { |
||||
background-color: #ff08; |
||||
} |
||||
/* hide stuff in print view */ |
||||
@media print { |
||||
#filter, |
||||
#filter-toggle, |
||||
#about, |
||||
img, |
||||
.bubbles { |
||||
display: none; |
||||
} |
||||
} |
||||
/* ANIMATED BACKGROUND, from https://codepen.io/alvarotrigo/pen/GRvYNax |
||||
found in https://alvarotrigo.com/blog/animated-backgrounds-css/ */ |
||||
@keyframes rotateUp { |
||||
0% { |
||||
transform: translateY(0) rotate(0deg); |
||||
opacity: 1; |
||||
border-radius: 100%; |
||||
} |
||||
100% { |
||||
transform: translateY(-150vh) rotate(720deg); |
||||
opacity: 0; |
||||
border-radius: 0; |
||||
} |
||||
} |
||||
.bubbles { |
||||
overflow: hidden; |
||||
} |
||||
.bubbles li { |
||||
position: absolute; |
||||
display: block; |
||||
list-style: none; |
||||
width: 20px; |
||||
height: 20px; |
||||
background: rgba(255, 255, 255, 0.2); |
||||
animation: rotateUp 25s linear infinite; |
||||
bottom: -150px; |
||||
} |
||||
.bubbles li:nth-child(1) { |
||||
left: 25%; |
||||
width: 80px; |
||||
height: 80px; |
||||
animation-delay: 0s; |
||||
} |
||||
.bubbles li:nth-child(2) { |
||||
left: 10%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 2s; |
||||
animation-duration: 12s; |
||||
} |
||||
.bubbles li:nth-child(3) { |
||||
left: 70%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 4s; |
||||
} |
||||
.bubbles li:nth-child(4) { |
||||
left: 40%; |
||||
width: 60px; |
||||
height: 60px; |
||||
animation-delay: 0s; |
||||
animation-duration: 18s; |
||||
} |
||||
.bubbles li:nth-child(5) { |
||||
left: 65%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 0s; |
||||
} |
||||
.bubbles li:nth-child(6) { |
||||
left: 75%; |
||||
width: 110px; |
||||
height: 110px; |
||||
animation-delay: 3s; |
||||
} |
||||
.bubbles li:nth-child(7) { |
||||
left: 35%; |
||||
width: 150px; |
||||
height: 150px; |
||||
animation-delay: 7s; |
||||
} |
||||
.bubbles li:nth-child(8) { |
||||
left: 50%; |
||||
width: 25px; |
||||
height: 25px; |
||||
animation-delay: 15s; |
||||
animation-duration: 45s; |
||||
} |
||||
.bubbles li:nth-child(9) { |
||||
left: 20%; |
||||
width: 15px; |
||||
height: 15px; |
||||
animation-delay: 2s; |
||||
animation-duration: 35s; |
||||
} |
||||
.bubbles li:nth-child(10) { |
||||
left: 85%; |
||||
width: 150px; |
||||
height: 150px; |
||||
animation-delay: 0s; |
||||
animation-duration: 11s; |
||||
} |
@ -0,0 +1,586 @@
@@ -0,0 +1,586 @@
|
||||
@darkBlue: #117; |
||||
|
||||
@keyframes fadeIn { |
||||
from { |
||||
opacity: 0; |
||||
} |
||||
|
||||
to { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
@keyframes fadeOut { |
||||
from { |
||||
opacity: 1; |
||||
} |
||||
|
||||
to { |
||||
opacity: 0; |
||||
} |
||||
} |
||||
|
||||
.clickable() { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.useBrightText() { |
||||
color: whitesmoke; |
||||
} |
||||
|
||||
.colorLabelWithDocs() { |
||||
color: darkgoldenrod; |
||||
} |
||||
|
||||
.darkenBg(@times: 1) { |
||||
background-color: rgba(0,0,0, calc(3/16 * @times)); |
||||
} |
||||
|
||||
.brightenBg(@times: 1) { |
||||
background-color: rgba(255,255,255, calc(1/16 * @times)); |
||||
} |
||||
|
||||
body { |
||||
font-family: system-ui, sans-serif; |
||||
background: #4e54c8; |
||||
background-image: linear-gradient(to left, #8f94fb, #4e54c8); |
||||
} |
||||
|
||||
input[type=text] { |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
button { |
||||
border-radius: 3px; |
||||
background-color: #aad; |
||||
border: none; |
||||
color: @darkBlue; |
||||
.clickable; |
||||
|
||||
&.icon { |
||||
font-size: 1em; |
||||
background-color: transparent; |
||||
} |
||||
|
||||
&:disabled { |
||||
opacity: .5; |
||||
} |
||||
} |
||||
|
||||
[type=checkbox], [type=radio] { |
||||
.clickable; |
||||
|
||||
& ~ label { |
||||
.clickable; |
||||
} |
||||
} |
||||
|
||||
fieldset { |
||||
border-radius: 5px; |
||||
} |
||||
|
||||
select { |
||||
border: none; |
||||
border-radius: 3px; |
||||
.darkenBg; |
||||
.useBrightText; |
||||
|
||||
option:checked { |
||||
.darkenBg; |
||||
color: darkorange; |
||||
} |
||||
} |
||||
|
||||
.flx:not([hidden]) { |
||||
display: flex; |
||||
|
||||
&.col { |
||||
flex-direction: column; |
||||
} |
||||
|
||||
&.spaced { |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
&.gap { |
||||
gap: .5em; |
||||
} |
||||
|
||||
&.aligned { |
||||
align-items: center; |
||||
} |
||||
|
||||
.grow { |
||||
flex-grow: 1; |
||||
} |
||||
} |
||||
|
||||
.collapse { |
||||
&.vertical { |
||||
max-height: 0; |
||||
overflow: hidden; |
||||
transition: max-height ease-in-out .5s; |
||||
|
||||
&.open { |
||||
max-height: 100vh; |
||||
} |
||||
} |
||||
|
||||
&.horizontal { |
||||
max-width: 0; |
||||
padding: 0; |
||||
margin: 0; |
||||
transition: all ease-in-out .5s; |
||||
overflow: hidden; |
||||
|
||||
&.open { |
||||
padding: revert; |
||||
max-width: 100vw; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.toggle, [data-toggles] { |
||||
.clickable; |
||||
} |
||||
|
||||
.container { |
||||
position: absolute; |
||||
inset: 0; |
||||
margin: 0; |
||||
} |
||||
|
||||
.scndry { |
||||
font-size: smaller; |
||||
} |
||||
|
||||
.mano-a-borsa { |
||||
transform: rotate(95deg); |
||||
.clickable; |
||||
|
||||
&:after { |
||||
content: '🤏'; |
||||
} |
||||
} |
||||
|
||||
.trawl-net { |
||||
transform: rotate(180deg) translateY(-2px); |
||||
display: inline-block; |
||||
|
||||
&:after { |
||||
content: '🥅'; |
||||
} |
||||
} |
||||
|
||||
.torch { |
||||
display: inline-block; |
||||
|
||||
&:after { |
||||
content: '🔦'; |
||||
} |
||||
} |
||||
|
||||
.pulsing { |
||||
animation: whiteBoxShadowPulse 2s 3; |
||||
} |
||||
|
||||
@keyframes whiteBoxShadowPulse { |
||||
0% { |
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); |
||||
} |
||||
|
||||
5% { |
||||
box-shadow: 0 0 0 15px rgba(255, 255, 255, 0.5); |
||||
} |
||||
|
||||
50% { |
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); |
||||
} |
||||
|
||||
90% { |
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); |
||||
} |
||||
} |
||||
|
||||
#content { |
||||
height: 100%; |
||||
position: relative; |
||||
} |
||||
|
||||
#filter { |
||||
max-width: 0; |
||||
transition: max-width ease-in-out .5s; |
||||
overflow: hidden; |
||||
.darkenBg; |
||||
.useBrightText; |
||||
|
||||
&.open { |
||||
max-width: 15em; |
||||
overflow: auto; |
||||
} |
||||
|
||||
&.resizing { |
||||
transition: none; |
||||
} |
||||
|
||||
> * { |
||||
margin: .3em .3em 0; |
||||
|
||||
&:last-child { |
||||
margin-bottom: .3em; |
||||
} |
||||
} |
||||
|
||||
#pre-filter-types { |
||||
min-width: 3em; |
||||
} |
||||
|
||||
[data-toggles="#info"] { |
||||
.torch { |
||||
transform: rotate(-90deg); |
||||
transition: transform .5s; |
||||
} |
||||
|
||||
&[aria-expanded=true] { |
||||
.torch { |
||||
transform: rotate(-255deg); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#info { |
||||
overflow: auto; |
||||
.brightenBg(2); |
||||
|
||||
a.toggle { |
||||
.useBrightText; |
||||
|
||||
img { |
||||
height: 1em; |
||||
} |
||||
} |
||||
} |
||||
|
||||
#type-select { |
||||
overflow: auto; |
||||
} |
||||
|
||||
#inheritance { |
||||
padding: .1em .75em .2em; |
||||
} |
||||
|
||||
#direction { |
||||
[type=radio] { |
||||
display: none; |
||||
|
||||
&:checked + label { |
||||
.brightenBg(4); |
||||
} |
||||
} |
||||
|
||||
label { |
||||
flex-grow: 1; |
||||
text-align: center; |
||||
margin: -1em 0 -.7em; |
||||
padding-top: .2em; |
||||
|
||||
&:first-of-type { |
||||
margin-left: -.8em; |
||||
border-top-left-radius: 5px; |
||||
border-bottom-left-radius: 5px; |
||||
} |
||||
|
||||
&:last-of-type { |
||||
margin-right: -.8em; |
||||
border-top-right-radius: 5px; |
||||
border-bottom-right-radius: 5px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
#actions { |
||||
margin-top: 1em; |
||||
justify-content: space-between; |
||||
|
||||
#render { |
||||
font-weight: bold; |
||||
} |
||||
} |
||||
|
||||
#exportOptions { |
||||
overflow: auto; |
||||
.brightenBg(2); |
||||
|
||||
#save { |
||||
margin-right: .5em; |
||||
} |
||||
|
||||
#dimensions fieldset { |
||||
padding: .5em; |
||||
|
||||
.scale-size { |
||||
margin-left: .5em; |
||||
|
||||
#scale-size { |
||||
width: 2.5em; |
||||
margin: 0 .2em; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#filter-toggle { |
||||
padding: 0; |
||||
border-radius: 0; |
||||
background-color: @darkBlue; |
||||
.useBrightText; |
||||
} |
||||
|
||||
#output { |
||||
overflow: auto; |
||||
|
||||
> svg { |
||||
cursor: grab; |
||||
|
||||
&:active { |
||||
cursor: grabbing; |
||||
} |
||||
} |
||||
|
||||
.edgeLabels { |
||||
.edgeTerminals .edgeLabel { |
||||
.useBrightText; |
||||
} |
||||
|
||||
.edgeLabel { |
||||
border-radius: 3px; |
||||
|
||||
.edgeLabel[title] { |
||||
.colorLabelWithDocs; |
||||
} |
||||
} |
||||
} |
||||
|
||||
path.relation { |
||||
stroke: whitesmoke; |
||||
} |
||||
|
||||
g.nodes { |
||||
> g { |
||||
.clickable; |
||||
|
||||
> rect { |
||||
rx: 5px; |
||||
ry: 5px; |
||||
} |
||||
} |
||||
|
||||
g.label .nodeLabel[title] { |
||||
.colorLabelWithDocs; |
||||
} |
||||
} |
||||
} |
||||
|
||||
#about { |
||||
position: absolute; |
||||
bottom: 2em; |
||||
right: 2em; |
||||
align-items: end; |
||||
@logoWidth: 2.3em; |
||||
|
||||
#toaster { |
||||
margin-right: @logoWidth + .5em; |
||||
|
||||
span { |
||||
animation: .5s ease-in fadeIn; |
||||
border-radius: .5em; |
||||
padding: .5em; |
||||
.darkenBg(2); |
||||
.useBrightText; |
||||
|
||||
&.leaving { |
||||
animation: 1s ease-in-out fadeOut; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.build-info { |
||||
align-items: end; |
||||
height: @logoWidth; |
||||
border-radius: 7px; |
||||
.darkenBg(3); |
||||
.useBrightText; |
||||
|
||||
> * { |
||||
height: 100%; |
||||
} |
||||
|
||||
#build-info { |
||||
text-align: right; |
||||
|
||||
> * { |
||||
padding: 0 .5em; |
||||
} |
||||
|
||||
a { |
||||
.useBrightText; |
||||
|
||||
&:not(.project) { |
||||
text-decoration: none; |
||||
} |
||||
|
||||
span { |
||||
display: inline-block; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#pressed-keys { |
||||
position: fixed; |
||||
left: 50%; |
||||
transform: translateX(-50%); |
||||
font-size: 3em; |
||||
bottom: 1em; |
||||
opacity: 1; |
||||
border-radius: .5em; |
||||
padding: .5em; |
||||
.darkenBg(2); |
||||
.useBrightText; |
||||
|
||||
&.hidden { |
||||
transition: opacity 0.5s ease-in-out; |
||||
opacity: 0; |
||||
} |
||||
} |
||||
|
||||
#mouse { |
||||
position: fixed; |
||||
transform: translateX(-50%) translateY(-50%); |
||||
height: 2em; |
||||
width: 2em; |
||||
pointer-events: none; |
||||
z-index: 9999; |
||||
border-radius: 1em; |
||||
border: solid .1em yellow; |
||||
|
||||
&.down { |
||||
background-color: #ff08; |
||||
} |
||||
} |
||||
|
||||
/* hide stuff in print view */ |
||||
@media print { |
||||
#filter, #filter-toggle, #about, img, .bubbles { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
/* ANIMATED BACKGROUND, from https://codepen.io/alvarotrigo/pen/GRvYNax |
||||
found in https://alvarotrigo.com/blog/animated-backgrounds-css/ */ |
||||
|
||||
@keyframes rotateUp { |
||||
0% { |
||||
transform: translateY(0) rotate(0deg); |
||||
opacity: 1; |
||||
border-radius: 100%; |
||||
} |
||||
|
||||
100% { |
||||
transform: translateY(-150vh) rotate(720deg); |
||||
opacity: 0; |
||||
border-radius: 0; |
||||
} |
||||
} |
||||
|
||||
.bubbles { |
||||
overflow: hidden; |
||||
|
||||
li { |
||||
position: absolute; |
||||
display: block; |
||||
list-style: none; |
||||
width: 20px; |
||||
height: 20px; |
||||
background: rgba(255, 255, 255, .2); |
||||
animation: rotateUp 25s linear infinite; |
||||
bottom: -150px; |
||||
|
||||
&:nth-child(1) { |
||||
left: 25%; |
||||
width: 80px; |
||||
height: 80px; |
||||
animation-delay: 0s; |
||||
} |
||||
|
||||
&:nth-child(2) { |
||||
left: 10%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 2s; |
||||
animation-duration: 12s; |
||||
} |
||||
|
||||
&:nth-child(3) { |
||||
left: 70%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 4s; |
||||
} |
||||
|
||||
&:nth-child(4) { |
||||
left: 40%; |
||||
width: 60px; |
||||
height: 60px; |
||||
animation-delay: 0s; |
||||
animation-duration: 18s; |
||||
} |
||||
|
||||
&:nth-child(5) { |
||||
left: 65%; |
||||
width: 20px; |
||||
height: 20px; |
||||
animation-delay: 0s; |
||||
} |
||||
|
||||
&:nth-child(6) { |
||||
left: 75%; |
||||
width: 110px; |
||||
height: 110px; |
||||
animation-delay: 3s; |
||||
} |
||||
|
||||
&:nth-child(7) { |
||||
left: 35%; |
||||
width: 150px; |
||||
height: 150px; |
||||
animation-delay: 7s; |
||||
} |
||||
|
||||
&:nth-child(8) { |
||||
left: 50%; |
||||
width: 25px; |
||||
height: 25px; |
||||
animation-delay: 15s; |
||||
animation-duration: 45s; |
||||
} |
||||
|
||||
&:nth-child(9) { |
||||
left: 20%; |
||||
width: 15px; |
||||
height: 15px; |
||||
animation-delay: 2s; |
||||
animation-duration: 35s; |
||||
} |
||||
|
||||
&:nth-child(10) { |
||||
left: 85%; |
||||
width: 150px; |
||||
height: 150px; |
||||
animation-delay: 0s; |
||||
animation-duration: 11s; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue