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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
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 @@ |
|||||||
|
/node_modules |
||||||
|
/class-diagrammer.html |
||||||
|
/model.json |
||||||
|
/package-lock.json |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
@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 @@ |
|||||||
|
@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