From ee91a05436fea3103365074375c81263f9c03b57 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 9 Feb 2025 13:48:24 +0100 Subject: [PATCH] [dev-v5] Refactoring the generator of API documentation (#3322) --- .gitignore | 1 + Directory.Packages.props | 2 + Microsoft.FluentUI-v5.sln | 14 +- .../FluentUI.Demo.Client.csproj | 1 - .../ServiceCollectionExtensions.cs | 47 +- .../ApiClassGenerator.cs | 242 +++++++++ .../FluentUI.Demo.DocApiGen/Constants.cs | 42 ++ .../Extensions/DocXmlReaderExtensions.cs | 69 +++ .../Extensions/ReflectionExtensions.cs | 468 ++++++++++++++++++ .../FluentUI.Demo.DocApiGen.csproj | 29 ++ .../Models/ApiClass.cs | 269 ++++++++++ .../Models/ApiClassOptions.cs | 39 ++ .../Models/ApiMember.cs | 77 +++ .../Tools/FluentUI.Demo.DocApiGen/Program.cs | 69 +++ .../Properties/launchSettings.json | 8 + .../Components/ConsoleLogProvider.razor.cs | 4 +- .../Components/MarkdownViewer.razor.cs | 15 + .../Extensions/ServicesExtensions.cs | 30 ++ .../FluentUI.Demo.DocViewer.csproj | 2 +- .../Models/ApiClass.cs | 2 +- .../Models/ApiDocSummary.cs | 21 + .../Services/DocViewerOptions.cs | 2 +- .../Services/DocViewerService.cs | 2 +- .../CodeCommentsGenerator.cs | 179 ------- .../FluentUI.Demo.Generators.csproj | 42 -- .../Properties/launchSettings.json | 8 - .../Tools/FluentUI.Demo.Generators/Readme.md | 22 - .../Components/Base/FluentComponentBase.cs | 14 +- src/Core/Components/Base/FluentInputBase.cs | 36 +- .../Base/FluentInputImmediateBase.cs | 2 +- .../Components/Field/FluentField.razor.cs | 24 +- src/Core/Components/Grid/FluentGrid.razor.cs | 2 +- .../TextInput/FluentTextInput.razor.cs | 4 +- ...soft.FluentUI.AspNetCore.Components.csproj | 2 +- 34 files changed, 1481 insertions(+), 309 deletions(-) create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Constants.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Extensions/DocXmlReaderExtensions.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/FluentUI.Demo.DocApiGen.csproj create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Program.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json create mode 100644 examples/Tools/FluentUI.Demo.DocViewer/Models/ApiDocSummary.cs delete mode 100644 examples/Tools/FluentUI.Demo.Generators/CodeCommentsGenerator.cs delete mode 100644 examples/Tools/FluentUI.Demo.Generators/FluentUI.Demo.Generators.csproj delete mode 100644 examples/Tools/FluentUI.Demo.Generators/Properties/launchSettings.json delete mode 100644 examples/Tools/FluentUI.Demo.Generators/Readme.md diff --git a/.gitignore b/.gitignore index 4ecb37e843..fa7ab1e08b 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,4 @@ FodyWeavers.xsd Microsoft.FluentUI.AspNetCore.Components.xml /examples/Demo/FluentUI.Demo.Client/wwwroot/sources/ /examples/Demo/FluentUI.Demo.Client/wwwroot/documentation/ +api-comments.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 57d2e7ea1f..0b902a0e92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,7 @@ + @@ -53,5 +54,6 @@ + diff --git a/Microsoft.FluentUI-v5.sln b/Microsoft.FluentUI-v5.sln index 9a4f22fc08..c92a378d52 100644 --- a/Microsoft.FluentUI-v5.sln +++ b/Microsoft.FluentUI-v5.sln @@ -31,14 +31,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{B98A7516 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentUI.Demo.DocViewer.Tests", "examples\Tools\FluentUI.Demo.DocViewer.Tests\FluentUI.Demo.DocViewer.Tests.csproj", "{2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentUI.Demo.Generators", "examples\Tools\FluentUI.Demo.Generators\FluentUI.Demo.Generators.csproj", "{C78BB9AD-B8CF-417B-990E-02410447649F}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.SampleData", "examples\Tools\FluentUI.Demo.SampleData\FluentUI.Demo.SampleData.csproj", "{32466925-47C6-420F-B869-5F922162C3A7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.SampleApi", "examples\Tools\FluentUI.Demo.SampleApi\FluentUI.Demo.SampleApi.csproj", "{E67B08B6-AEE4-4281-8700-1C87A5A3C11E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.IntegrationTests", "tests\Integration\Components.IntegrationTests.csproj", "{F380FA22-53D8-4381-B89B-4047AF544D53}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentUI.Demo.DocApiGen", "examples\Tools\FluentUI.Demo.DocApiGen\FluentUI.Demo.DocApiGen.csproj", "{D52F6265-A983-46E0-8831-67FA80D95FBE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,10 +75,6 @@ Global {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF}.Release|Any CPU.Build.0 = Release|Any CPU - {C78BB9AD-B8CF-417B-990E-02410447649F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C78BB9AD-B8CF-417B-990E-02410447649F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C78BB9AD-B8CF-417B-990E-02410447649F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C78BB9AD-B8CF-417B-990E-02410447649F}.Release|Any CPU.Build.0 = Release|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {32466925-47C6-420F-B869-5F922162C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -90,6 +86,10 @@ Global {F380FA22-53D8-4381-B89B-4047AF544D53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.ActiveCfg = Release|AnyCPU {F380FA22-53D8-4381-B89B-4047AF544D53}.Release|Any CPU.Build.0 = Release|AnyCPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D52F6265-A983-46E0-8831-67FA80D95FBE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,10 +105,10 @@ Global {958BF092-4CF2-470C-B058-9244496B234F} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} = {F273876F-7528-42B3-BFE8-7CFF8ED1E2A2} {2462C5CC-81BC-47AF-85B8-5FADD9E47ADF} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} - {C78BB9AD-B8CF-417B-990E-02410447649F} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} {32466925-47C6-420F-B869-5F922162C3A7} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} {E67B08B6-AEE4-4281-8700-1C87A5A3C11E} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} {F380FA22-53D8-4381-B89B-4047AF544D53} = {A7EC98D2-21E3-4967-8C5A-D62E640305EB} + {D52F6265-A983-46E0-8831-67FA80D95FBE} = {B98A7516-E9B2-4301-B6A3-33656BF4F4D9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {44D95FF7-AEBE-41FB-9D40-CF1E09ADC6BC} diff --git a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj index 146e6fb5f1..aaacc6153a 100644 --- a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj +++ b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj @@ -30,7 +30,6 @@ - diff --git a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs index dcc7525b42..ac1c70e03d 100644 --- a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs +++ b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using System.Reflection; using FluentUI.Demo.DocViewer; namespace FluentUI.Demo.Client; @@ -64,10 +65,54 @@ public static DemoServices AddFluentUIDemoServices(this IServiceCollection servi options.ComponentsAssembly = typeof(Client._Imports).Assembly; options.ResourcesAssembly = typeof(Client._Imports).Assembly; options.ApiAssembly = typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly; - options.ApiCommentSummary = (component, member) => member is null ? CodeComments.GetSummary(component.Name) : CodeComments.GetSummary(member); + options.ApiCommentSummary = (data, component, member) => + { + if (member is null && (data is null || data?.Items?.Count <= 1)) + { + return "⚠️ The file `api-comments.json` was not found. " + + "Run the project `FluentUI.Demo.DocApiGen` to generate the file. "; + } + + // Component summary + if (member is null) + { + return GetMemberSummary(component.Name, "__summary__"); + } + + // Member summary + else + { + return GetMemberSummary(component.Name, member.GetSignature()); + } + + string GetMemberSummary(string sectionName, string signature) + { + if (data?.Items?.TryGetValue(sectionName, out var section) == true) + { + if (section.TryGetValue(signature, out var summary)) + { + return summary; + } + } + + return ""; + } + }; options.SourceCodeUrl = "/sources/{0}.txt"; }); return new DemoServices(services); } + + /// + /// Get the signature of the method. + /// + /// + /// ⚠️ Must be identical to FluentUI.Demo.DocApiGen.Extensions.ReflectionExtensions.GetSignature + public static string GetSignature(this MemberInfo member) + { + return member.MemberType == MemberTypes.Method + ? $"{member.Name}({string.Join(", ", ((MethodInfo)member).GetParameters().Select(p => p.ParameterType.Name))})" + : member.Name; + } } diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs new file mode 100644 index 0000000000..e2a21e3e8d --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/ApiClassGenerator.cs @@ -0,0 +1,242 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models; +using System.Globalization; +using System.Reflection; +using System.Text; + +namespace FluentUI.Demo.DocApiGen; + +/// +/// Engine to generate the documentation classes. +/// +public class ApiClassGenerator +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ApiClassGenerator(Assembly assembly, FileInfo xmlDocumentation) + { + Assembly = assembly; + DocXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + } + + /// + /// Gets the assembly to generate the documentation. + /// + public Assembly Assembly { get; } + + /// + /// Gets the summary reader. + /// + public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } + + /// + /// Gets the for the specified component. + /// + /// + /// + public ApiClass FromTypeName(Type type) + { + var options = new ApiClassOptions(Assembly, DocXmlReader) + { + PropertyParameterOnly = false, + }; + + return new ApiClass(type, options); + } + + /// + /// Generates the C# code for the documentation. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Not necessary")] + public string GenerateCSharp() + { + var code = new StringBuilder(); + var assemblyInfo = GetAssemblyInfo(Assembly); + + code.AppendLine("// ------------------------------------------------------------------------"); + code.AppendLine("// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. "); + code.AppendLine("// ------------------------------------------------------------------------"); + code.AppendLine(); + code.AppendLine("//------------------------------------------------------------------------------"); + code.AppendLine("// "); + code.AppendLine("// This code was generated by a tool."); + code.AppendLine("//"); + code.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if"); + code.AppendLine("// the code is regenerated."); + code.AppendLine("//"); + code.AppendLine("// Version: " + assemblyInfo.Version + " - " + assemblyInfo.Date); + code.AppendLine("// "); + code.AppendLine("//------------------------------------------------------------------------------"); + code.AppendLine(); + code.AppendLine("using System.Reflection;"); + code.AppendLine(); + code.AppendLine("/// "); + code.AppendLine("public class CodeComments"); + code.AppendLine("{"); + + code.AppendLine(" /// "); + code.AppendLine(" public static readonly IDictionary> SummaryData = new Dictionary>"); + code.AppendLine(" {"); + + foreach (var type in Assembly.GetTypes().Where(i => i.IsValidType())) + { + var apiClass = FromTypeName(type); + var apiClassMembers = apiClass.ToDictionary(); + + if (apiClassMembers.Any()) + { + code.AppendLine($" {{ \"{apiClass.Name}\", new Dictionary"); + code.AppendLine($" {{"); + code.AppendLine($" {{ \"__summary__\", \"{FormatDescription(apiClass.Summary)}\" }},"); + + foreach (var member in apiClass.ToDictionary()) + { + code.AppendLine($" {{ \"{member.Key}\", \"{FormatDescription(member.Value)}\" }},"); + } + + code.AppendLine($" }}"); + code.AppendLine($" }},"); + } + } + + code.AppendLine(" };"); + code.AppendLine(); + code.AppendLine(" /// "); + code.AppendLine(" public static string GetSignature(MemberInfo member)"); + code.AppendLine(" {"); + code.AppendLine(" return member.MemberType == MemberTypes.Method"); + code.AppendLine(" ? $\"{member.Name}({string.Join(\", \", ((MethodInfo)member).GetParameters().Select(p => p.ParameterType.Name))})\""); + code.AppendLine(" : member.Name;"); + code.AppendLine(" }"); + code.AppendLine(); + code.AppendLine(" /// "); + code.AppendLine(" public static string GetSummary(MemberInfo member)"); + code.AppendLine(" {"); + code.AppendLine(" var name = member.Name;"); + code.AppendLine(" var signature = GetSignature(member);"); + code.AppendLine(); + code.AppendLine(" return SummaryData.TryGetValue(name, out var comments) && comments.TryGetValue(signature, out var comment)"); + code.AppendLine(" ? comment"); + code.AppendLine(" : string.Empty;"); + code.AppendLine(" }"); + code.AppendLine("}"); + code.AppendLine(); + + return code.ToString(); + } + + /// + /// Generates the JSON for the documentation. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Not necessary")] + public string GenerateJson() + { + var code = new StringBuilder(); + var assemblyInfo = GetAssemblyInfo(Assembly); + + code.AppendLine("{"); + code.AppendLine($" \"__Generated__\": {{"); + code.AppendLine($" \"AssemblyVersion\": \"{assemblyInfo.Version}\","); + code.AppendLine($" \"DateUtc\": \"{assemblyInfo.Date}\""); + code.AppendLine($" }},"); + + foreach (var type in Assembly.GetTypes().Where(i => i.IsValidType())) + { + var apiClass = FromTypeName(type); + var apiClassMembers = apiClass.ToDictionary(); + + if (apiClassMembers.Any()) + { + code.AppendLine($" \"{apiClass.Name}\": {{"); + code.AppendLine($" \"__summary__\": \"{FormatDescription(apiClass.Summary)}\","); + + foreach (var member in apiClass.ToDictionary()) + { + code.AppendLine($" \"{member.Key}\": \"{FormatDescription(member.Value)}\","); + } + + RemoveLastComma(code); // Remove the last comma + code.AppendLine($" }},"); + } + } + + RemoveLastComma(code); // Remove the last comma + code.AppendLine("}"); + code.AppendLine(); + + return code.ToString(); + } + + /// + /// Saves the documentation to a file. + /// + /// + /// + public void SaveToFile(string fileName, string format) + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + if (format == "json") + { + File.WriteAllText(fileName, GenerateJson()); + } + else + { + File.WriteAllText(fileName, GenerateCSharp()); + } + } + + /// + private static string FormatDescription(string description) + { + return description.Replace("\r\n", " ").Replace("\n", " ").Replace("\"", "\\\""); + } + + /// + private static void RemoveLastComma(StringBuilder sb) + { + if (sb == null || sb.Length == 0) + { + return; + } + + var lastIndex = sb.ToString().LastIndexOf(','); + sb.Remove(lastIndex, sb.Length - lastIndex); + sb.AppendLine(); + } + + internal static (string Version, string Date) GetAssemblyInfo(Assembly assembly) + { + // Assembly version + string strVersion = default!; + var versionAttribute = assembly.GetCustomAttribute(); + if (versionAttribute != null) + { + var version = versionAttribute.InformationalVersion; + var plusIndex = version.IndexOf('+'); + if (plusIndex >= 0 && plusIndex + 9 < version.Length) + { + strVersion = version[..(plusIndex + 9)]; + } + else + { + strVersion = version; + } + } + + // Date + return (strVersion, DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Constants.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Constants.cs new file mode 100644 index 0000000000..88d2a3724d --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Constants.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocApiGen; + +/// +/// List of constants used in the application. +/// +public static class Constants +{ + /// + /// List of types to exclude from the documentation. + /// + public static readonly string[] EXCLUDE_TYPES = + [ + "TypeInference", + "InternalListContext`1", + "SpacingGenerator", + "FluentLocalizerInternal", + "FluentJSModule", + "FluentServiceProviderException`1", + ]; + + /// + /// List of members to exclude from the documentation. + /// + public static readonly string[] MEMBERS_TO_EXCLUDE = + [ + "AdditionalAttributes", + "ParentReference", + "Element", + "Equals", + "GetHashCode", + "GetType", + "SetParametersAsync", + "ToString", + "Dispose", + "DisposeAsync", + "ValueExpression", + ]; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/DocXmlReaderExtensions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/DocXmlReaderExtensions.cs new file mode 100644 index 0000000000..091abd9984 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/DocXmlReaderExtensions.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.RegularExpressions; + +namespace FluentUI.Demo.DocApiGen.Extensions; + +/// +/// Reads the summary comments from the XML documentation file. +/// +public static class DocXmlReaderExtensions +{ + /// + /// Gets the summary comments for the component. + /// + /// + /// + /// + public static string GetComponentSummary(this LoxSmoke.DocXml.DocXmlReader docReader, Type type) + { + var comments = docReader.GetTypeComments(type); + + var summary = $"{comments.Summary}" + + $"{(string.IsNullOrEmpty(comments.Remarks) ? "" : $" _{comments.Remarks}_")}" + + $"{(string.IsNullOrEmpty(comments.Example) ? "" : $" Example: `{comments.Example}`")}"; + + return RemoveCrLf(ConvertSeeHref(ConvertSeeCref(summary))); + } + + /// + /// Gets the summary comments for the member. + /// + /// + /// + /// + public static string GetMemberSummary(this LoxSmoke.DocXml.DocXmlReader docReader, MemberInfo member) + { + var comments = docReader.GetMemberComments(member); + + var summary = $"{comments.Summary}" + + $"{(string.IsNullOrEmpty(comments.Remarks) ? "" : $" _{comments.Remarks}_")}" + + $"{(string.IsNullOrEmpty(comments.Example) ? "" : $" Example: `{comments.Example}`")}"; + + return RemoveCrLf(ConvertSeeHref(ConvertSeeCref(summary))); + } + + private static string ConvertSeeCref(string input) + { + const string pattern = @""; + const string replacement = "`$2`"; + + return Regex.Replace(input, pattern, replacement); + } + + private static string ConvertSeeHref(string input) + { + const string pattern = @"([^<]+)"; + const string replacement = "[$2]($1)"; + + return Regex.Replace(input, pattern, replacement); + } + + private static string RemoveCrLf(string input) + { + return input.Replace("\r", "").Replace("\n", ""); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000000..0c0934c9e4 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Extensions/ReflectionExtensions.cs @@ -0,0 +1,468 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace FluentUI.Demo.DocApiGen.Extensions; + +/// +/// Reflection extension methods with supporting properties. +/// +public static class ReflectionExtensions +{ + private static Dictionary? _knownTypeNames; + private static readonly string[] EXCLUDE_TYPES = Constants.EXCLUDE_TYPES; + + /// + /// Get the signature of the method. + /// + /// + /// ⚠️ Must be identical to FluentUI.Demo.Client.ServiceCollectionExtensions.GetSignature + public static string GetSignature(this MemberInfo member) + { + return member.MemberType == MemberTypes.Method + ? $"{member.Name}({string.Join(", ", ((MethodInfo)member).GetParameters().Select(p => p.ParameterType.Name))})" + : member.Name; + } + + /// + /// Check if the specified type is a valid type that can be used in the documentation. + /// + /// + /// + public static bool IsValidType(this Type type) + { + return type != null && + type != typeof(void) && + EXCLUDE_TYPES.Contains(type.Name) == false && + type.Name.Contains('>', StringComparison.InvariantCulture) == false && + type.Name.Contains('<', StringComparison.InvariantCulture) == false && + type.BaseType != typeof(Regex) && + type.BaseType?.Name != "Icon" && + type.IsAbstract == false && + type.Name.EndsWith("_g") == false; + } + + /// + /// A dictionary containing a mapping of type to type names. + /// + public static Dictionary KnownTypeNames => _knownTypeNames ??= CreateKnownTypeNamesDictionary(); + + /// + /// Create a dictionary of standard value types and a string type. + /// + /// Dictionary mapping types to type names + public static Dictionary CreateKnownTypeNamesDictionary() + { + return new Dictionary() + { + {typeof(DateTime), "DateTime"}, + {typeof(double), "double"}, + {typeof(float), "float"}, + {typeof(decimal), "decimal"}, + {typeof(sbyte), "sbyte"}, + {typeof(byte), "byte"}, + {typeof(char), "char"}, + {typeof(short), "short"}, + {typeof(ushort), "ushort"}, + {typeof(int), "int"}, + {typeof(uint), "uint"}, + {typeof(long), "long"}, + {typeof(ulong), "ulong"}, + {typeof(bool), "bool"}, + {typeof(void), "void"}, + {typeof(string), "string" } + }; + } + + /// + /// Checks if the specified type is a nullable value type. + /// Returns false for object references. + /// + /// Type to check. + /// True if the type is nullable like int? or Nullable<Something> + public static bool IsNullable(this Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + /// + /// Checks if the specified member is or return a nullable value type. + /// + /// Member to check. + /// True if the type is nullable like int? + public static bool IsNullable(this MemberInfo member) + { + return member switch + { + MethodInfo method => !method.ReturnType.IsValueType && (new NullabilityInfoContext().Create(method.ReturnParameter).ReadState is NullabilityState.Nullable), + PropertyInfo property => !property.PropertyType.IsValueType && (new NullabilityInfoContext().Create(property).WriteState is NullabilityState.Nullable), + FieldInfo field => !field.FieldType.IsValueType && (new NullabilityInfoContext().Create(field).WriteState is NullabilityState.Nullable), + _ => false + }; + } + + /// + /// Convert type to the proper type _name. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types without field names. + /// + /// Type information. + /// The optional function that converts type _name to string. + /// Full type _name + public static string ToNameString(this Type type, Func? typeNameConverter = null) + { + return type.ToNameString(null, typeNameConverter == null ? null : (t, _) => typeNameConverter(t)); + } + + /// + /// Convert type to the proper type _name. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types without field names. + /// + /// Type information. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name + public static string ToNameString( + this Type type, + Func?, string>? typeNameConverter, + bool invokeTypeNameConverterForGenericType = false) + { + return type.ToNameString(null, typeNameConverter, invokeTypeNameConverterForGenericType); + } + + /// + /// Convert method parameters to the string. If method has no parameters then returned string is () + /// If parameters are present then returned string contains parameter names with their type names. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// Method information + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full list of parameter types and their names + public static string ToParametersString( + this MethodBase methodInfo, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 0) + { + return "()"; + } + + return "(" + string.Join(", ", parameters.Select(p => p.ToTypeNameString(typeNameConverter, invokeTypeNameConverterForGenericType) + " " + p.Name)) + ")"; + } + + /// + /// Convert method parameter type to the string. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// Parameter information. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name of the parameter + public static string ToTypeNameString( + this ParameterInfo parameterInfo, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + if (parameterInfo.ParameterType.IsByRef) + { + var transformNames = parameterInfo.GetCustomAttribute()?.TransformNames; + var nameStringWithValueTupleNames = parameterInfo.ParameterType.GetElementType()?.ToNameStringWithValueTupleNames(transformNames, typeNameConverter, invokeTypeNameConverterForGenericType); + + return (parameterInfo.IsIn ? "in " : (parameterInfo.IsOut ? "out " : "ref ")) + nameStringWithValueTupleNames; + } + + return + parameterInfo.ParameterType.ToNameStringWithValueTupleNames( + parameterInfo.GetCustomAttribute()?.TransformNames, typeNameConverter, + invokeTypeNameConverterForGenericType); + } + + /// + /// Convert method return value type to the string. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// Method information. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name of the return value + public static string ToTypeNameString( + this MethodInfo methodInfo, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + return methodInfo.ReturnType.ToNameStringWithValueTupleNames( + methodInfo.ReturnParameter?.GetCustomAttribute()?.TransformNames, typeNameConverter, + invokeTypeNameConverterForGenericType) + (IsNullable(methodInfo) ? "?" : string.Empty); + } + + /// + /// Convert property type to the string. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// Property information. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name of the property + public static string ToTypeNameString( + this PropertyInfo propertyInfo, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + return propertyInfo.PropertyType.ToNameStringWithValueTupleNames( + propertyInfo.GetCustomAttribute()?.TransformNames, typeNameConverter, + invokeTypeNameConverterForGenericType) + (IsNullable(propertyInfo) ? "?" : string.Empty); + } + + /// + /// Convert field type to the string. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// Field information. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name of the field + public static string ToTypeNameString( + this FieldInfo fieldInfo, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + return fieldInfo.FieldType.ToNameStringWithValueTupleNames( + fieldInfo.GetCustomAttribute()?.TransformNames, typeNameConverter, + invokeTypeNameConverterForGenericType) + (IsNullable(fieldInfo) ? "?" : string.Empty); + } + + /// + /// Convert type to the string. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns ValueTuple types with field names like this (Type1 name1, Type2 name2). + /// + /// + /// The names of the tuple fields from compiler-generated TupleElementNames attribute + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full _name of the specified type + public static string ToNameStringWithValueTupleNames( + this Type type, + IList? tupleNames, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + var tq = tupleNames == null ? null : new Queue(tupleNames); + return ToNameString(type, tq, typeNameConverter, invokeTypeNameConverterForGenericType); + } + + /// + /// Convert type to the proper type _name. + /// Optional function can convert type names to strings + /// if type names should be decorated in some way either by converting text to markdown or + /// HTML links or adding some formatting. + /// + /// This method returns named tuples with field names like this (Type1 field1, Type2 field2). parameter + /// must be specified with all tuple field names stored in the same order as they are in compiler-generated TupleElementNames attribute. + /// If you do not know what it is then the better and easier way is to use ToTypeNameString() methods that retrieve field names from attributes. + /// + /// + /// The names of value tuple fields as stored in TupleElementNames attribute. This queue is modified during call. + /// The optional function that converts type _name to string. + /// + /// True if typeNameConverter lambda function should be invoked for generic type _name such as for the List _name in case of List<SomeType> + /// If the parameter value is false then typeNameConverter is not invoked for the generic type _name and only the plain type _name is returned. + /// If the parameter value is true then typeNameConverter must handle generic type definitions carefully and avoid calling + /// ToNameString() to avoid infinite recursion. + /// This is an optional parameter with default value of false. + /// Full type _name + public static string ToNameString( + this Type type, + Queue? tupleFieldNames, + Func?, string>? typeNameConverter = null, + bool invokeTypeNameConverterForGenericType = false) + { + if (type.IsByRef) + { + return "ref " + type.GetElementType()?.ToNameString(tupleFieldNames, typeNameConverter, invokeTypeNameConverterForGenericType); + } + + var decoratedTypeName = type.IsGenericType || tupleFieldNames is null ? null : typeNameConverter?.Invoke(type, tupleFieldNames); + + if (decoratedTypeName != null && (tupleFieldNames == null || tupleFieldNames.Count == 0)) + { + // If there are no tuple field names then return the _name from converter + // Otherwise do full type _name conversion to remove the proper number of tuple field names from the queue and then discard that _name + return decoratedTypeName; + } + + string? newTypeName = null; + + if (KnownTypeNames.TryGetValue(type, out var value)) + { + newTypeName = value; + } + + else if (IsNullable(type)) + { + newTypeName = type.GenericTypeArguments[0].ToNameString(tupleFieldNames, typeNameConverter, invokeTypeNameConverterForGenericType) + "?"; + } + + else if (type.IsGenericType) + { + var genericTypeDefinition = type.GetGenericTypeDefinition(); + if (GenericTuples.Contains(genericTypeDefinition)) + { + // Tuple fields must not go breadth first as that is the order of names in the tupleFieldNamesQueue + var tupleFields = type.GetGenericArguments().Select((arg) => (argumentType: arg, argumentName: tupleFieldNames?.Dequeue())).ToList(); + newTypeName = "(" + + string.Join(", ", tupleFields + .Select(arg => arg.argumentType.ToNameString(tupleFieldNames, typeNameConverter, invokeTypeNameConverterForGenericType) + + (arg.argumentName == null ? string.Empty : (" " + arg.argumentName)))) + ")"; + } + else if (type.Name.Contains('`')) + { + var genericTypeName = invokeTypeNameConverterForGenericType ? + typeNameConverter?.Invoke(genericTypeDefinition, tupleFieldNames) : null; + newTypeName = + (genericTypeName ?? type.Name.CleanGenericTypeName()) + "<" + + string.Join(", ", type.GetGenericArguments() + .Select(argType => argType.ToNameString(tupleFieldNames, typeNameConverter, invokeTypeNameConverterForGenericType))) + ">"; + } + else + { + newTypeName = type.Name; + } + } + else if (type.IsArray) + { + newTypeName = type.GetElementType()?.ToNameString(tupleFieldNames, typeNameConverter, invokeTypeNameConverterForGenericType) + + $"[{(type.GetArrayRank() > 1 ? new string(',', type.GetArrayRank() - 1) : string.Empty)}]"; + } + else + { + newTypeName = type.Name; + } + + // If decoratedTypeName is not null then all formatting above was just for tuple _name removal from the queue + return decoratedTypeName ?? newTypeName; + } + + /// + /// Hash of all possible ValueTuple type definitions for quick check if type is value tuple. + /// + private static readonly HashSet GenericTuples = [.. new Type[] { + typeof(ValueTuple<>), + typeof(ValueTuple<,>), + typeof(ValueTuple<,,>), + typeof(ValueTuple<,,,>), + typeof(ValueTuple<,,,,>), + typeof(ValueTuple<,,,,,>), + typeof(ValueTuple<,,,,,,>), + typeof(ValueTuple<,,,,,,,>) }]; + + /// + /// Remove the parameter count part of the generic type name. + /// For example the generic list type name is List`1. + /// This method leaves only the _name part of the type such as List. + /// If specified string does not contain the number of parameters + /// part then the same string is returned. + /// + /// Type name + /// Type name without the number of parameters. + public static string CleanGenericTypeName(this string genericTypeName) + { + var index = genericTypeName.IndexOf('`'); + return (index < 0) ? genericTypeName : genericTypeName[..index]; + } + + /// + /// Returns the enum values for the specified property. + /// + /// + /// + public static string[] GetEnumValues(this PropertyInfo? propertyInfo) + { + if (propertyInfo != null) + { + if (propertyInfo.PropertyType.IsEnum) + { + return propertyInfo.PropertyType.GetEnumNames(); + } + else if (propertyInfo.PropertyType.GenericTypeArguments?.Length > 0 && + propertyInfo.PropertyType.GenericTypeArguments[0].IsEnum) + { + return propertyInfo.PropertyType.GenericTypeArguments[0].GetEnumNames(); + } + } + + return []; + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/FluentUI.Demo.DocApiGen.csproj b/examples/Tools/FluentUI.Demo.DocApiGen/FluentUI.Demo.DocApiGen.csproj new file mode 100644 index 0000000000..d78a217065 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/FluentUI.Demo.DocApiGen.csproj @@ -0,0 +1,29 @@ + + + + Exe + net9.0 + enable + enable + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs new file mode 100644 index 0000000000..f218086c68 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClass.cs @@ -0,0 +1,269 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using FluentUI.Demo.DocApiGen.Extensions; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace FluentUI.Demo.DocApiGen.Models; + +/// +/// Represents a class with properties, methods, and events. +/// +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +public class ApiClass +{ + private static readonly string[] MEMBERS_TO_EXCLUDE = Constants.MEMBERS_TO_EXCLUDE; + private readonly Type _component; + private IEnumerable? _allMembers; + private readonly ApiClassOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ApiClass(Type component, ApiClassOptions options) + { + _component = component; + _options = options; + } + + /// + /// It Component is a generic type, a generic type argument needs to be provided + /// so an instance of the type can be created. + /// This is needed to get and display any default values + /// Default for this parameter is 'typeof(string)' + /// + public Type[] InstanceTypes + { + get + { + return _component.GetType() switch + { + //Type t when t == typeof(int) => new[] { typeof(int) }, + //Type t when t == typeof(bool) => new[] { typeof(bool) }, + _ => [typeof(string)] + }; + } + } + + /// + /// Gets the name of the specified component. + /// + public string Name => _component.Name; + + /// + /// Gets the class summary for the specified component. + /// + public string Summary => GetSummary(_component, null); + + /// + /// Gets the list of properties for the specified component. + /// + public IEnumerable Properties => GetMembers(MemberTypes.Property).Where(i => _options.PropertyParameterOnly == false ? true : i.IsParameter); + + /// + /// Gets the list of Events for the specified component. + /// + + public IEnumerable Events => GetMembers(MemberTypes.Event); + + /// + /// Gets the list of Methods for the specified component. + /// + + public IEnumerable Methods => GetMembers(MemberTypes.Method); + + /// + /// Returns a dictionary of the properties, methods, and events. + /// + /// + public IDictionary ToDictionary() + { + var result = new Dictionary(); + var members = Properties.Union(Methods).Union(Events).OrderBy(i => i.Name); + + foreach (var member in members) + { + result.Add(member.GetSignature(), member.Description); + } + + return result; + } + + /// + private IEnumerable GetMembers(MemberTypes type) + { + if (_allMembers == null) + { + List? members = []; + object? obj = null; + var created = false; + + // Create an instance of the component to get the default values + object? GetObjectValue(string propertyName) + { + try + { + + if (!created) + { + if (_component.IsGenericType) + { + if (InstanceTypes is null) + { + throw new InvalidCastException("InstanceTypes must be specified when Component is a generic type"); + } + + // Supply the type to create the generic instance with (needs to be an array) + obj = Activator.CreateInstance(_component.MakeGenericType(InstanceTypes)); + } + else + { + obj = Activator.CreateInstance(_component); + } + + created = true; + } + + return obj?.GetType().GetProperty(propertyName)?.GetValue(obj); + + } + catch (Exception) + { + return null; + } + } + + var allEnums = _component.IsEnum ? _component.GetFields(BindingFlags.Public | BindingFlags.Static).Select(i => (MemberInfo)i) : []; + var allProperties = _component.GetProperties().Select(i => (MemberInfo)i); + var allMethods = _component.GetMethods().Where(i => !i.IsSpecialName).Select(i => (MemberInfo)i); + + foreach (var memberInfo in allProperties.Union(allMethods).Union(allEnums).OrderBy(m => m.Name)) + { + try + { + if (!MEMBERS_TO_EXCLUDE.Contains(memberInfo.Name) || _component.Name == "FluentComponentBase") + { + var enumInfo = memberInfo as FieldInfo; + var propertyInfo = memberInfo as PropertyInfo; + var methodInfo = memberInfo as MethodInfo; + + var isObsolete = memberInfo.GetCustomAttribute() != null; + if (isObsolete) + { + continue; + } + + if (enumInfo != null && enumInfo.FieldType.IsEnum) + { + members.Add(new ApiMember() + { + MemberInfo = memberInfo, + MemberType = MemberTypes.Property, + Name = enumInfo.Name, + Type = "", + EnumValues = [], + Default = "", + Description = GetSummary(_component, enumInfo), + IsParameter = false, + }); + } + + if (!_component.IsEnum && propertyInfo != null) + { + var isParameter = memberInfo.GetCustomAttribute() != null; + + var t = propertyInfo.PropertyType; + var isEvent = t == typeof(EventCallback) || t.IsGenericType && t.GetGenericTypeDefinition() == typeof(EventCallback<>); + + // Parameters/properties + if (!isEvent) + { + // Icon? icon = null; + var defaultValue = ""; + if (propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string)) + { + defaultValue = GetObjectValue(propertyInfo.Name)?.ToString(); + } + + members.Add(new ApiMember() + { + MemberInfo = memberInfo, + MemberType = MemberTypes.Property, + Name = propertyInfo.Name, + Type = propertyInfo.ToTypeNameString(), + EnumValues = propertyInfo.GetEnumValues(), + Default = defaultValue, + Description = GetSummary(_component, propertyInfo), + IsParameter = isParameter, + }); + } + + // Events + if (isEvent) + { + var eventTypes = string.Join(", ", propertyInfo.PropertyType.GenericTypeArguments.Select(i => i.Name)); + members.Add(new ApiMember() + { + MemberInfo = memberInfo, + MemberType = MemberTypes.Event, + Name = propertyInfo.Name, + Type = propertyInfo.ToTypeNameString(), + Description = GetSummary(_component, propertyInfo), + }); + } + } + + // Methods + if (!_component.IsEnum && methodInfo != null) + { + var isJSInvokable = memberInfo.GetCustomAttribute() != null; + if (isJSInvokable) + { + continue; + } + + var genericArguments = ""; + if (methodInfo.IsGenericMethod) + { + genericArguments = "<" + string.Join(", ", methodInfo.GetGenericArguments().Select(i => i.Name)) + ">"; + } + + members.Add(new ApiMember() + { + MemberInfo = memberInfo, + MemberType = MemberTypes.Method, + Name = methodInfo.Name + genericArguments, + Parameters = methodInfo.GetParameters().Select(i => $"{i.ToTypeNameString()} {i.Name}").ToArray(), + Type = methodInfo.ToTypeNameString(), + Description = GetSummary(_component, methodInfo), + }); + } + } + } + catch (Exception) + { + Console.WriteLine($"[ApiDocumentation] ERROR: Cannot found {_component.FullName} -> {memberInfo.Name}"); + throw; + } + } + + _allMembers = [.. members.OrderBy(i => i.Name)]; + } + + return _allMembers.Where(i => i.MemberType == type); + } + + /// + private string GetSummary(Type component, MemberInfo? member) + { + return member == null + ? _options.DocXmlReader.GetComponentSummary(component) + : _options.DocXmlReader.GetMemberSummary(member); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs new file mode 100644 index 0000000000..758cb29571 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiClassOptions.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Reflection; + +namespace FluentUI.Demo.DocApiGen.Models; + +/// +/// Represents the options for the class generation. +/// +public class ApiClassOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ApiClassOptions(Assembly assembly, LoxSmoke.DocXml.DocXmlReader docReader) + { + Assembly = assembly; + DocXmlReader = docReader; + } + + /// + /// Gets the assembly to generate the documentation. + /// + public Assembly Assembly { get; } + + /// + /// Gets the summary reader. + /// + public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } + + /// + /// Gets or sets whether to include all properties (false) or only those with [Parameter] attribute (true). + /// + public bool PropertyParameterOnly { get; set; } = true; +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs new file mode 100644 index 0000000000..0f938ef974 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/ApiMember.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Reflection; +using FluentUI.Demo.DocApiGen.Extensions; + +namespace FluentUI.Demo.DocApiGen.Models; + +/// +/// Represents a member of a class (Property, Method, Event). +/// +public record ApiMember +{ + /// + /// Gets the MemberInfo for the member. + /// + public required MemberInfo MemberInfo { get; init; } + + /// + /// Gets the type of the MemberInfo: Property, Method, Event. + /// + public MemberTypes MemberType { get; init; } = MemberTypes.Property; + + /// + /// Gets the name of the MemberInfo. + /// + public string Name { get; init; } = ""; + + /// + /// Gets the return type of the MemberInfo. + /// + public string Type { get; init; } = ""; + + /// + /// Gets the list of enum values for the method / property (empty for Event). + /// + public string[] EnumValues { get; init; } = []; + + /// + /// Gets the list of parameters for the method (empty for Property or Event). + /// + public string[] Parameters { get; init; } = []; + + /// + /// Gets the default value for the MemberInfo. + /// + public string? Default { get; init; } + + /// + /// Gets the description comment for the MemberInfo. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets true if the property is flagged with [Parameter] attribute + /// + public bool IsParameter { get; init; } + + /// + /// Returns the identifier of the member. + /// + /// + public string GetSignature() + { + return MemberInfo.GetSignature(); + } + + /// + /// Returns the signature of the method. + /// + /// + public string GetMethodSignature() + { + return $"{Type} {Name}({string.Join(", ", Parameters)})"; + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs new file mode 100644 index 0000000000..8b5669ee7b --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.Configuration; +using System.Reflection; + +namespace FluentUI.Demo.DocApiGen; + +/// +public class Program +{ + private static readonly System.Diagnostics.Stopwatch _watcher = new (); + + /// + public static void Main(string[] args) + { + _watcher.Start(); + + Console.WriteLine($"-------------------------------------------------------------------"); + Console.WriteLine($" DocApiGen v{Assembly.GetEntryAssembly()?.GetName().Version} "); + Console.WriteLine($" A simple command line tool to generate the documentation classes. "); + Console.WriteLine($"-------------------------------------------------------------------"); + Console.WriteLine(); + + // Build a configuration object from command line + var config = new ConfigurationBuilder().AddCommandLine(args).Build(); + var xmlFile = config["xml"]; + var dllFile = config["dll"]; + var outputFile = config["output"]; + var format = config["format"] ?? "json"; + + // Help + if (string.IsNullOrEmpty(xmlFile) || string.IsNullOrEmpty(dllFile)) + { + Console.WriteLine("Usage: DocApiGen --xml " + + " --dll " + + " --output " + + " --format "); + return; + } + + // Assembly and documentation file + var assembly = Assembly.LoadFrom(dllFile); + var docXml = new FileInfo(xmlFile); + var apiGenerator = new ApiClassGenerator(assembly, docXml); + + Console.WriteLine("Generating documentation..."); + if (!string.IsNullOrEmpty(outputFile)) + { + apiGenerator.SaveToFile(outputFile, format); + Console.WriteLine($"Documentation saved to {outputFile}"); + } + else + { + Console.WriteLine(); + if (format == "json") + { + Console.WriteLine(apiGenerator.GenerateJson()); + } + else + { + Console.WriteLine(apiGenerator.GenerateCSharp()); + } + } + + Console.WriteLine($"Completed in {_watcher.ElapsedMilliseconds} ms"); + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json new file mode 100644 index 0000000000..1619966f4a --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FluentUI.Demo.DocApiGen": { + "commandName": "Project", + "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.xml\" --dll \"$(SolutionDir)src/Core/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments.json\" --format json" + } + } +} diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/ConsoleLogProvider.razor.cs b/examples/Tools/FluentUI.Demo.DocViewer/Components/ConsoleLogProvider.razor.cs index 69c5b6f184..5bd1fb2b94 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/ConsoleLogProvider.razor.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/ConsoleLogProvider.razor.cs @@ -70,9 +70,7 @@ private async Task CloseConsoleAsync() } } - /// - /// - /// + /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "Allowing Blazor async infrastructure to handle the state updates without forcing synchronous execution")] protected override void OnInitialized() { diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs index a6d86e5722..c4623f89df 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs @@ -22,6 +22,14 @@ public partial class MarkdownViewer [Inject] internal DocViewerService DocViewerService { get; set; } = default!; + /// + [Inject] + internal HttpClient HttpClient { get; set; } = default!; + + /// + [Inject] + internal NavigationManager NavigationManager { get; set; } = default!; + /// [Inject] internal IJSRuntime JSRuntime { get; set; } = default!; @@ -56,6 +64,13 @@ protected override async Task OnInitializedAsync() PageTitle = page.Title; Sections = await page.ExtractSectionsAsync(); + + // Read api-comments.json + if (ApiDocSummary.Cached is null) + { + HttpClient.BaseAddress ??= new Uri(NavigationManager.BaseUri); + ApiDocSummary.Cached = await HttpClient.LoadSummariesAsync("/api-comments.json"); + } } /// diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Extensions/ServicesExtensions.cs b/examples/Tools/FluentUI.Demo.DocViewer/Extensions/ServicesExtensions.cs index 28bdc64606..56ece2ce9f 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Extensions/ServicesExtensions.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Extensions/ServicesExtensions.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using FluentUI.Demo.DocViewer.Components.ConsoleLog; +using FluentUI.Demo.DocViewer.Models; using FluentUI.Demo.DocViewer.Services; using Microsoft.Extensions.DependencyInjection; @@ -40,4 +41,33 @@ public static IServiceCollection AddDocViewer(this IServiceCollection services, return services; } + + /// + /// Load the summaries from the api-comments.json file. + /// + /// + /// + /// + public static async Task LoadSummariesAsync(this HttpClient httpClient, string jsonFile) + { + // Read api-comments.json + try + { + var json = await httpClient.GetStringAsync(jsonFile); + return new ApiDocSummary() + { + Items = System.Text.Json.JsonSerializer.Deserialize>>(json) + }; + } + catch (Exception ex) + { + return new ApiDocSummary() + { + Items = new Dictionary> + { + ["ERROR"] = new Dictionary { [$"{jsonFile} cannot be loaded"] = ex.Message }, + } + }; + } + } } diff --git a/examples/Tools/FluentUI.Demo.DocViewer/FluentUI.Demo.DocViewer.csproj b/examples/Tools/FluentUI.Demo.DocViewer/FluentUI.Demo.DocViewer.csproj index 555a8cd61d..09a51270b8 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/FluentUI.Demo.DocViewer.csproj +++ b/examples/Tools/FluentUI.Demo.DocViewer/FluentUI.Demo.DocViewer.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiClass.cs b/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiClass.cs index 5f3674572f..aa8277f2c7 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiClass.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiClass.cs @@ -225,7 +225,7 @@ private IEnumerable GetMembers(MemberTypes type) /// private string GetSummary(Type component, MemberInfo? member) { - var summary = _docViewerService.ApiCommentSummary(component, member); + var summary = _docViewerService.ApiCommentSummary(ApiDocSummary.Cached, component, member); if (string.IsNullOrWhiteSpace(summary)) { diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiDocSummary.cs b/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiDocSummary.cs new file mode 100644 index 0000000000..21030998c4 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocViewer/Models/ApiDocSummary.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace FluentUI.Demo.DocViewer.Models; + +/// +/// Represents the summary of the API documentation. +/// +public class ApiDocSummary +{ + /// + /// Gets or sets the content of the API documentation, read from the api-comments.json file. + /// + public Dictionary>? Items { get; set; } + + /// + /// Gets or sets the cached API documentation. + /// + public static ApiDocSummary? Cached { get; set; } +} diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs index d51ca4f968..e314ffce5f 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs @@ -34,7 +34,7 @@ public class DocViewerOptions /// /// Function to get the summary of an API comment. /// - public Func ApiCommentSummary { get; set; } = (type, member) => member?.Name ?? type.Name; + public Func ApiCommentSummary { get; set; } = (data, type, member) => member?.Name ?? type.Name; /// /// Path to the external source code files, where {0} will be replaced by the razor component name diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs index 0f2633fa2f..0de4c6906b 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs @@ -57,7 +57,7 @@ public DocViewerService(DocViewerOptions options) /// /// Function to get the summary of an API comment. /// - public Func ApiCommentSummary { get; } + public Func ApiCommentSummary { get; } /// /// Gets the list of all markdown pages found in the resources diff --git a/examples/Tools/FluentUI.Demo.Generators/CodeCommentsGenerator.cs b/examples/Tools/FluentUI.Demo.Generators/CodeCommentsGenerator.cs deleted file mode 100644 index 874b43ada4..0000000000 --- a/examples/Tools/FluentUI.Demo.Generators/CodeCommentsGenerator.cs +++ /dev/null @@ -1,179 +0,0 @@ -// ------------------------------------------------------------------------ -// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------------------ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace FluentUI.Demo.Generators; - -[Generator] -public class CodeCommentsGenerator : IIncrementalGenerator -{ - private static readonly string[] REGEX_CLEANUP = - [ - @"Microsoft\.FluentUI\.AspNetCore\.Components\.", - @"FluentUI\.Demo\.Client\.", - @"\[\[.*?\]\]", - @"\[.*?\]" - ]; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var files = context.AdditionalTextsProvider.Where(at => at.Path.EndsWith(".xml")).Collect(); - context.RegisterSourceOutput(files, GenerateSource); - } - - public void GenerateSource(SourceProductionContext context, ImmutableArray files) - { - List members = []; - - foreach (var file in files) - { - if (context.CancellationToken.IsCancellationRequested) - { - return; - } - - var f = file.GetText(context.CancellationToken); - var xml = XDocument.Parse(f.ToString(), LoadOptions.None); - - members.AddRange(xml.Descendants("member")); - } - - StringBuilder sb = new(); - - sb.AppendLine("#pragma warning disable CS1591"); - sb.AppendLine(""); - sb.AppendLine("using System;"); - sb.AppendLine("using System.Collections.Generic;"); - sb.AppendLine("using System.Linq;"); - sb.AppendLine(""); - sb.AppendLine("namespace FluentUI.Demo.Client;"); - sb.AppendLine(""); - sb.AppendLine("public static partial class CodeComments"); - sb.AppendLine("{"); - sb.AppendLine(); - sb.AppendLine(" private static readonly string[] REGEX_CLEANUP = [\"" + string.Join("\", \"", REGEX_CLEANUP.Select(i => i.Replace("\\", "\\\\"))) + "\"];"); - sb.AppendLine(); - sb.AppendLine(" public static string GetSummary(string name)"); - sb.AppendLine(" {"); - sb.AppendLine(" Dictionary summaryData = new Dictionary()"); - sb.AppendLine(" {"); - - if (members.Count > 0) - { - foreach (var m in members) - { - var paramName = CleanupParamName(m.Attribute("name").Value.ToString()); - var summary = CleanupSummary(m.Descendants().First().ToString()); - - if (summary != "") - { - sb.AppendLine(" [\"" + paramName + "\"] = \"" + summary + "\", "); - } - } - - var lastComma = sb.ToString().LastIndexOf(','); - - sb.Remove(lastComma, 1); - } - - sb.AppendLine(" };"); - sb.AppendLine(); - sb.AppendLine(" KeyValuePair foundPair = summaryData.FirstOrDefault(x => x.Key.Equals(name));"); - sb.AppendLine(); - sb.AppendLine(" return foundPair.Value;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" public static string GetSummary(System.Reflection.MemberInfo memberInfo) => GetSummary(GetApiCommentName(memberInfo));"); - sb.AppendLine(); - sb.AppendLine(" public static string GetApiCommentName(System.Reflection.MemberInfo memberInfo)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (memberInfo is System.Reflection.MethodInfo methodInfo)"); - sb.AppendLine(" {"); - sb.AppendLine(" var parameters = string.Join(\", \", methodInfo.GetParameters().Select(p => $\"{p.ParameterType.FullName}\"));"); - sb.AppendLine(" return CleanupName($\"{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}({parameters})\");"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" if (memberInfo is System.Reflection.PropertyInfo propertyInfo)"); - sb.AppendLine(" {"); - sb.AppendLine(" return CleanupName($\"{propertyInfo.DeclaringType?.FullName}.{propertyInfo.Name}\");"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" return string.Empty;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" private static string CleanupName(string value)"); - sb.AppendLine(" {"); - sb.AppendLine(" foreach (var cleanup in REGEX_CLEANUP)"); - sb.AppendLine(" {"); - sb.AppendLine(" System.Text.RegularExpressions.Regex r = new(cleanup);"); - sb.AppendLine(" value = r.Replace(value, string.Empty);"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" return value;"); - sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine("}"); - - context.AddSource($"CodeComments.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); - } - - private static string CleanupParamName(string value) - { - foreach (var cleanup in REGEX_CLEANUP) - { - Regex r = new($"[P,T,M,F]:{cleanup}"); - value = r.Replace(value, string.Empty); - } - - return value; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1035:Do not use APIs banned for analyzers", Justification = "Need to use Environment to ensure the right newline is generated for platform it is being built and run on")] - private static string CleanupSummary(string value) - { - Regex regex = new(@"[ ]{2,}"); - value = regex.Replace(value, ""); - - regex = new("`\\d)?[\"|']\\s*/>"); - value = regex.Replace(value, m => m.Groups["generic"].Success ? $"{m.Groups[1].Value}<T>" : $"{m.Groups[1].Value}"); - - regex = new("`\\d)?[\"|']\\s*/>"); - value = regex.Replace(value, m => m.Groups["generic"].Success ? $"{m.Groups[1].Value}<T>" : $"{m.Groups[1].Value}"); - - regex = new(""); - value = regex.Replace(value, m => $"{m.Groups[1].Value}"); - - regex = new(""); - value = regex.Replace(value, m => $"{m.Groups[1].Value}"); - - regex = new("(.*?)"); - value = regex.Replace(value, "$2"); - - regex = new(""); - value = regex.Replace(value, m => $"{m.Groups[0].Value}{m.Groups[1].Value}"); - - return value.Trim() - .Replace("" + Environment.NewLine, "") - .Replace(Environment.NewLine + "", "") - .Replace(Environment.NewLine, "
") - .Replace("\"", "'") - .Replace("Microsoft.FluentUI.AspNetCore.Components.", "") - .Replace("FluentDataGrid`1.", "") - .Replace("System.Linq.", "") - .Replace("System.Linq.Queryable.", "") - .Replace("System.Collections.", "") - .Replace("`1", "") - .Replace("`0", ""); - - } -} diff --git a/examples/Tools/FluentUI.Demo.Generators/FluentUI.Demo.Generators.csproj b/examples/Tools/FluentUI.Demo.Generators/FluentUI.Demo.Generators.csproj deleted file mode 100644 index df3cc43d40..0000000000 --- a/examples/Tools/FluentUI.Demo.Generators/FluentUI.Demo.Generators.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - netstandard2.0 - false - disable - latest - false - Microsoft - Copyright © Microsoft - $(PackageVersion) - portable - 2.0.0 - MIT - true - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - $(GetTargetPathDependsOn);GetDependencyTargetPaths - - - - - - - - - - - \ No newline at end of file diff --git a/examples/Tools/FluentUI.Demo.Generators/Properties/launchSettings.json b/examples/Tools/FluentUI.Demo.Generators/Properties/launchSettings.json deleted file mode 100644 index 86ef1827fc..0000000000 --- a/examples/Tools/FluentUI.Demo.Generators/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "FluentUI.Demo.Generators": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Shared\\FluentUI.Demo.Shared.csproj" - } - } -} \ No newline at end of file diff --git a/examples/Tools/FluentUI.Demo.Generators/Readme.md b/examples/Tools/FluentUI.Demo.Generators/Readme.md deleted file mode 100644 index 34ee79dc59..0000000000 --- a/examples/Tools/FluentUI.Demo.Generators/Readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# Code Comments Generators - -This project contains a set of code generators that can be used to generate code comments for C# code files. - -## Usage - -To use the code generators, you need to add the following package reference to your project file: - -```xml - - - -``` - -The first time, you need to restart Visual Studio to make the code generators available. - -## Engine - -The engine is responsible for generating the code comments based on the -`Microsoft.FluentUI.AspNetCore.Components.xml` code file (in FluentUI.Demo.Client project). - -The file generated is under the `Dependencies / Analyzers / FluentUI.Demo.Generators` folder in the project. diff --git a/src/Core/Components/Base/FluentComponentBase.cs b/src/Core/Components/Base/FluentComponentBase.cs index d18fd383ba..3e5cbf70f2 100644 --- a/src/Core/Components/Base/FluentComponentBase.cs +++ b/src/Core/Components/Base/FluentComponentBase.cs @@ -44,31 +44,31 @@ public abstract class FluentComponentBase : ComponentBase, IAsyncDisposable, IFl .AddStyle("margin", Margin.ConvertSpacing().Style) .AddStyle("padding", Padding.ConvertSpacing().Style); - /// + /// [Parameter] public virtual string? Id { get; set; } - /// + /// [Parameter] public virtual string? Class { get; set; } - /// + /// [Parameter] public virtual string? Style { get; set; } - /// + /// [Parameter] public virtual string? Margin { get; set; } - /// + /// [Parameter] public virtual string? Padding { get; set; } - /// + /// [Parameter] public virtual object? Data { get; set; } - /// + /// [Parameter(CaptureUnmatchedValues = true)] public virtual IReadOnlyDictionary? AdditionalAttributes { get; set; } diff --git a/src/Core/Components/Base/FluentInputBase.cs b/src/Core/Components/Base/FluentInputBase.cs index 0c21702262..c84964cae0 100644 --- a/src/Core/Components/Base/FluentInputBase.cs +++ b/src/Core/Components/Base/FluentInputBase.cs @@ -50,27 +50,27 @@ protected FluentInputBase() #region IFluentComponentBase - /// + /// [Parameter] public virtual string? Id { get; set; } = Identifier.NewId(); - /// + /// [Parameter] public virtual string? Class { get; set; } - /// + /// [Parameter] public virtual string? Style { get; set; } - /// + /// [Parameter] public virtual string? Margin { get; set; } - /// + /// [Parameter] public virtual string? Padding { get; set; } - /// + /// [Parameter] public virtual object? Data { get; set; } @@ -78,50 +78,50 @@ protected FluentInputBase() #region IFluentField - /// + /// public virtual bool FocusLost { get; protected set; } - /// + /// [Parameter] public virtual bool? Disabled { get; set; } - /// + /// [Parameter] public virtual string? Label { get; set; } - /// + /// [Parameter] public virtual RenderFragment? LabelTemplate { get; set; } - /// + /// [Parameter] public virtual LabelPosition? LabelPosition { get; set; } - /// + /// [Parameter] public virtual string? LabelWidth { get; set; } - /// + /// [Parameter] public virtual bool? Required { get; set; } - /// + /// [Parameter] public virtual string? Message { get; set; } - /// + /// [Parameter] public virtual Icon? MessageIcon { get; set; } - /// + /// [Parameter] public virtual RenderFragment? MessageTemplate { get; set; } - /// + /// [Parameter] public virtual Func? MessageCondition { get; set; } - /// + /// [Parameter] public virtual MessageState? MessageState { get; set; } diff --git a/src/Core/Components/Base/FluentInputImmediateBase.cs b/src/Core/Components/Base/FluentInputImmediateBase.cs index 02213de2ad..2968351181 100644 --- a/src/Core/Components/Base/FluentInputImmediateBase.cs +++ b/src/Core/Components/Base/FluentInputImmediateBase.cs @@ -53,7 +53,7 @@ protected virtual async Task InputHandlerAsync(ChangeEventArgs e) } } - /// + /// [ExcludeFromCodeCoverage()] protected override void Dispose(bool disposing) { diff --git a/src/Core/Components/Field/FluentField.razor.cs b/src/Core/Components/Field/FluentField.razor.cs index 3a71b98882..4671b7aa7b 100644 --- a/src/Core/Components/Field/FluentField.razor.cs +++ b/src/Core/Components/Field/FluentField.razor.cs @@ -46,30 +46,30 @@ public partial class FluentField : FluentComponentBase, IFluentField [Parameter] public string? ForId { get; set; } - /// " + /// public bool FocusLost { get; internal set; } - /// " + /// [Parameter] public string? Label { get; set; } - /// " + /// [Parameter] public RenderFragment? LabelTemplate { get; set; } - /// " + /// [Parameter] public LabelPosition? LabelPosition { get; set; } - /// " + /// [Parameter] public string? LabelWidth { get; set; } - /// " + /// [Parameter] public bool? Required { get; set; } - /// " + /// [Parameter] public bool? Disabled { get; set; } @@ -79,23 +79,23 @@ public partial class FluentField : FluentComponentBase, IFluentField [Parameter] public RenderFragment? ChildContent { get; set; } - /// " + /// [Parameter] public string? Message { get; set; } - /// " + /// [Parameter] public Icon? MessageIcon { get; set; } - /// " + /// [Parameter] public RenderFragment? MessageTemplate { get; set; } - /// + /// [Parameter] public Func? MessageCondition { get; set; } - /// " + /// [Parameter] public MessageState? MessageState { get; set; } diff --git a/src/Core/Components/Grid/FluentGrid.razor.cs b/src/Core/Components/Grid/FluentGrid.razor.cs index 10f86c6afb..2b1de96bb9 100644 --- a/src/Core/Components/Grid/FluentGrid.razor.cs +++ b/src/Core/Components/Grid/FluentGrid.razor.cs @@ -100,7 +100,7 @@ public async Task FluentGrid_MediaChangedAsync(string size) } /// - /// + /// /// /// [ExcludeFromCodeCoverage(Justification = "Tested via integration tests.")] diff --git a/src/Core/Components/TextInput/FluentTextInput.razor.cs b/src/Core/Components/TextInput/FluentTextInput.razor.cs index 8d55eb8e92..5c8ab26d30 100644 --- a/src/Core/Components/TextInput/FluentTextInput.razor.cs +++ b/src/Core/Components/TextInput/FluentTextInput.razor.cs @@ -34,7 +34,7 @@ public FluentTextInput() } - /// + /// [Parameter] public ElementReference Element { get; set; } @@ -119,7 +119,7 @@ public FluentTextInput() [Parameter] public TextInputMode? InputMode { get; set; } // TODO: To verify if this is supported by the component - /// + /// protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj index 3bf4c01949..db2483ad72 100644 --- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj +++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj @@ -95,7 +95,7 @@ - +