mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-04-05 22:01:04 +00:00
1705 lines
71 KiB
C#
1705 lines
71 KiB
C#
|
|
using System.Globalization;
|
||
|
|
using System.Net;
|
||
|
|
using System.Reflection;
|
||
|
|
using System.Text;
|
||
|
|
using System.Text.RegularExpressions;
|
||
|
|
using System.Xml.Linq;
|
||
|
|
|
||
|
|
namespace Radzen.Blazor.Api.Generator;
|
||
|
|
|
||
|
|
sealed class Program
|
||
|
|
{
|
||
|
|
static int Main(string[] args)
|
||
|
|
{
|
||
|
|
if (args.Length < 3)
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine("Usage: Radzen.Blazor.Api.Generator <assemblyPath> <xmlDocPath> <outputDir>");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
var assemblyPath = args[0];
|
||
|
|
var xmlDocPath = args[1];
|
||
|
|
var outputDir = args[2];
|
||
|
|
|
||
|
|
if (!File.Exists(assemblyPath))
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine($"Assembly not found: {assemblyPath}");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!File.Exists(xmlDocPath))
|
||
|
|
{
|
||
|
|
Console.Error.WriteLine($"XML doc not found: {xmlDocPath}");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
Directory.CreateDirectory(outputDir);
|
||
|
|
|
||
|
|
var xmlDocs = XmlDocParser.Parse(xmlDocPath);
|
||
|
|
var types = AssemblyInspector.GetPublicTypes(assemblyPath);
|
||
|
|
|
||
|
|
var generator = new RazorPageGenerator(xmlDocs, types, outputDir);
|
||
|
|
generator.Generate();
|
||
|
|
|
||
|
|
Console.WriteLine($"Generated {generator.PageCount} API reference pages in {outputDir}");
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sealed record XmlMemberDoc(
|
||
|
|
string Summary,
|
||
|
|
string Value,
|
||
|
|
List<ExampleSegment> Examples,
|
||
|
|
string Remarks,
|
||
|
|
Dictionary<string, string> Params,
|
||
|
|
string Returns,
|
||
|
|
Dictionary<string, string> TypeParams);
|
||
|
|
|
||
|
|
sealed record ExampleSegment(bool IsCode, string Content);
|
||
|
|
|
||
|
|
static class XmlDocParser
|
||
|
|
{
|
||
|
|
public static Dictionary<string, XmlMemberDoc> Parse(string xmlPath)
|
||
|
|
{
|
||
|
|
var docs = new Dictionary<string, XmlMemberDoc>(StringComparer.Ordinal);
|
||
|
|
var xdoc = XDocument.Load(xmlPath);
|
||
|
|
var members = xdoc.Root?.Element("members");
|
||
|
|
|
||
|
|
if (members == null)
|
||
|
|
return docs;
|
||
|
|
|
||
|
|
foreach (var member in members.Elements("member"))
|
||
|
|
{
|
||
|
|
var memberName = member.Attribute("name")?.Value;
|
||
|
|
if (memberName == null) continue;
|
||
|
|
|
||
|
|
var summary = CleanXmlDocText(member.Element("summary"));
|
||
|
|
var value = CleanText(member.Element("value")?.Value);
|
||
|
|
var examples = ParseExample(member.Element("example"));
|
||
|
|
var remarks = CleanText(member.Element("remarks")?.Value);
|
||
|
|
|
||
|
|
var paramDict = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
|
|
foreach (var paramEl in member.Elements("param"))
|
||
|
|
{
|
||
|
|
var pName = paramEl.Attribute("name")?.Value;
|
||
|
|
if (pName != null)
|
||
|
|
{
|
||
|
|
paramDict[pName] = CleanXmlDocText(paramEl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var returnsText = CleanXmlDocText(member.Element("returns"));
|
||
|
|
|
||
|
|
var typeParamDict = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
|
|
foreach (var tpEl in member.Elements("typeparam"))
|
||
|
|
{
|
||
|
|
var tpName = tpEl.Attribute("name")?.Value;
|
||
|
|
if (tpName != null)
|
||
|
|
{
|
||
|
|
typeParamDict[tpName] = CleanXmlDocText(tpEl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
docs[memberName] = new XmlMemberDoc(summary, value, examples, remarks, paramDict, returnsText, typeParamDict);
|
||
|
|
}
|
||
|
|
|
||
|
|
return docs;
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<ExampleSegment> ParseExample(XElement? element)
|
||
|
|
{
|
||
|
|
var segments = new List<ExampleSegment>();
|
||
|
|
if (element == null) return segments;
|
||
|
|
|
||
|
|
foreach (var node in element.Nodes())
|
||
|
|
{
|
||
|
|
if (node is XText textNode)
|
||
|
|
{
|
||
|
|
var text = textNode.Value.Trim();
|
||
|
|
if (!string.IsNullOrEmpty(text))
|
||
|
|
{
|
||
|
|
segments.Add(new ExampleSegment(false, text));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (node is XElement el && el.Name.LocalName == "code")
|
||
|
|
{
|
||
|
|
var code = TrimCodeIndentation(el.Value);
|
||
|
|
if (!string.IsNullOrEmpty(code))
|
||
|
|
{
|
||
|
|
segments.Add(new ExampleSegment(true, code));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return segments;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string TrimCodeIndentation(string code)
|
||
|
|
{
|
||
|
|
var lines = code.Split('\n');
|
||
|
|
|
||
|
|
// Drop leading and trailing empty lines
|
||
|
|
int start = 0;
|
||
|
|
while (start < lines.Length && string.IsNullOrWhiteSpace(lines[start]))
|
||
|
|
start++;
|
||
|
|
int end = lines.Length - 1;
|
||
|
|
while (end >= start && string.IsNullOrWhiteSpace(lines[end]))
|
||
|
|
end--;
|
||
|
|
if (start > end)
|
||
|
|
return "";
|
||
|
|
|
||
|
|
// Find the minimum leading whitespace among non-empty lines
|
||
|
|
int minIndent = int.MaxValue;
|
||
|
|
for (int i = start; i <= end; i++)
|
||
|
|
{
|
||
|
|
var line = lines[i].TrimEnd('\r');
|
||
|
|
if (string.IsNullOrWhiteSpace(line))
|
||
|
|
continue;
|
||
|
|
int spaces = 0;
|
||
|
|
while (spaces < line.Length && line[spaces] == ' ')
|
||
|
|
spaces++;
|
||
|
|
if (spaces < minIndent)
|
||
|
|
minIndent = spaces;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (minIndent == int.MaxValue)
|
||
|
|
minIndent = 0;
|
||
|
|
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
for (int i = start; i <= end; i++)
|
||
|
|
{
|
||
|
|
var line = lines[i].TrimEnd('\r');
|
||
|
|
if (string.IsNullOrWhiteSpace(line))
|
||
|
|
sb.AppendLine();
|
||
|
|
else
|
||
|
|
sb.AppendLine(line.Length > minIndent ? line[minIndent..] : line.TrimStart());
|
||
|
|
}
|
||
|
|
|
||
|
|
return sb.ToString().TrimEnd();
|
||
|
|
}
|
||
|
|
|
||
|
|
static string CleanXmlDocText(XElement? element)
|
||
|
|
{
|
||
|
|
if (element == null) return "";
|
||
|
|
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
foreach (var node in element.Nodes())
|
||
|
|
{
|
||
|
|
if (node is XText textNode)
|
||
|
|
{
|
||
|
|
sb.Append(textNode.Value);
|
||
|
|
}
|
||
|
|
else if (node is XElement el)
|
||
|
|
{
|
||
|
|
switch (el.Name.LocalName)
|
||
|
|
{
|
||
|
|
case "see":
|
||
|
|
case "seealso":
|
||
|
|
var cref = el.Attribute("cref")?.Value ?? "";
|
||
|
|
if (cref.Length > 2 && cref[1] == ':')
|
||
|
|
cref = cref[2..];
|
||
|
|
var display = FormatCref(cref);
|
||
|
|
sb.Append(CultureInfo.InvariantCulture, $"\x01TYPEREF:{cref}\x02{display}\x01/TYPEREF\x02");
|
||
|
|
break;
|
||
|
|
case "paramref":
|
||
|
|
case "typeparamref":
|
||
|
|
sb.Append(el.Attribute("name")?.Value ?? "");
|
||
|
|
break;
|
||
|
|
case "c":
|
||
|
|
sb.Append(el.Value);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
sb.Append(el.Value);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return CleanText(sb.ToString());
|
||
|
|
}
|
||
|
|
|
||
|
|
static string FormatCref(string cref)
|
||
|
|
{
|
||
|
|
var tickIdx = cref.IndexOf('`', StringComparison.Ordinal);
|
||
|
|
if (tickIdx >= 0)
|
||
|
|
{
|
||
|
|
var basePart = cref[..tickIdx];
|
||
|
|
var afterTick = cref[(tickIdx + 1)..];
|
||
|
|
var parenIdx = afterTick.IndexOf('(', StringComparison.Ordinal);
|
||
|
|
string countStr;
|
||
|
|
string suffix;
|
||
|
|
if (parenIdx >= 0)
|
||
|
|
{
|
||
|
|
countStr = afterTick[..parenIdx];
|
||
|
|
suffix = afterTick[parenIdx..];
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
countStr = afterTick;
|
||
|
|
suffix = "";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (int.TryParse(countStr, out var count))
|
||
|
|
{
|
||
|
|
var args = string.Join(", ", Enumerable.Range(0, count).Select(i => count == 1 ? "T" : $"T{i + 1}"));
|
||
|
|
var lastDot = basePart.LastIndexOf('.');
|
||
|
|
var shortName = lastDot >= 0 ? basePart[(lastDot + 1)..] : basePart;
|
||
|
|
return $"{shortName}<{args}>{suffix}";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var dot = cref.LastIndexOf('.');
|
||
|
|
return dot >= 0 ? cref[(dot + 1)..] : cref;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string CleanText(string? text)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(text)) return "";
|
||
|
|
return Regex.Replace(text, @"\s+", " ").Trim();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sealed record InheritedMemberInfo(string Name, string DeclaringTypeName, MemberKind Kind);
|
||
|
|
|
||
|
|
sealed record TypeMemberInfo(string Name, string DeclaringTypeName, string TypeName, string Summary, MemberKind Kind, string Signature, IReadOnlyList<ParameterInfo> Parameters, string? ReturnType, string? ReturnSummary, List<ExampleSegment> Examples);
|
||
|
|
sealed record ParameterInfo(string Name, string TypeName, string Summary);
|
||
|
|
sealed record ApiTypeInfo(
|
||
|
|
string FullName,
|
||
|
|
string Name,
|
||
|
|
string Namespace,
|
||
|
|
TypeKind Kind,
|
||
|
|
string? BaseTypeName,
|
||
|
|
string Summary,
|
||
|
|
string Remarks,
|
||
|
|
string Syntax,
|
||
|
|
IReadOnlyList<string> Inheritance,
|
||
|
|
IReadOnlyList<string> Interfaces,
|
||
|
|
IReadOnlyList<string> DerivedTypes,
|
||
|
|
IReadOnlyList<string> TypeParameters,
|
||
|
|
Dictionary<string, string> TypeParameterDescriptions,
|
||
|
|
IReadOnlyList<TypeMemberInfo> Members,
|
||
|
|
IReadOnlyList<EnumFieldInfo> EnumFields,
|
||
|
|
List<ExampleSegment> Examples,
|
||
|
|
IReadOnlyList<InheritedMemberInfo> InheritedMembers);
|
||
|
|
sealed record EnumFieldInfo(string Name, string Summary, string? Value);
|
||
|
|
|
||
|
|
enum TypeKind { Class, Interface, Enum, Struct, Delegate }
|
||
|
|
enum MemberKind { Constructor, Property, Method, Event, Field, Operator }
|
||
|
|
|
||
|
|
static class AssemblyInspector
|
||
|
|
{
|
||
|
|
static readonly HashSet<string> ExcludedNamespacePrefixes = new()
|
||
|
|
{
|
||
|
|
"Radzen.Blazor.Rendering"
|
||
|
|
};
|
||
|
|
|
||
|
|
static readonly Regex ExcludedTypePattern = new(@"^Radzen\.Blazor\.RadzenHtmlEditor.*Base$", RegexOptions.Compiled);
|
||
|
|
|
||
|
|
static bool IsExcluded(Type type)
|
||
|
|
{
|
||
|
|
var fullName = type.FullName ?? type.Name;
|
||
|
|
var ns = type.Namespace ?? "";
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(ns))
|
||
|
|
return true;
|
||
|
|
|
||
|
|
foreach (var prefix in ExcludedNamespacePrefixes)
|
||
|
|
{
|
||
|
|
if (ns.StartsWith(prefix, StringComparison.Ordinal))
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ExcludedTypePattern.IsMatch(fullName))
|
||
|
|
return true;
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static List<ApiTypeInfo> GetPublicTypes(string assemblyPath)
|
||
|
|
{
|
||
|
|
var allDlls = CollectAssemblyPaths(assemblyPath);
|
||
|
|
var resolver = new PathAssemblyResolver(allDlls);
|
||
|
|
|
||
|
|
using var mlc = new MetadataLoadContext(resolver);
|
||
|
|
var assembly = mlc.LoadFromAssemblyPath(assemblyPath);
|
||
|
|
|
||
|
|
var allPublicTypes = assembly.GetExportedTypes()
|
||
|
|
.Where(t => !IsExcluded(t) && !IsCompilerGenerated(t))
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
var derivedMap = BuildDerivedTypeMap(allPublicTypes);
|
||
|
|
|
||
|
|
return allPublicTypes.Select(t => InspectType(t, allPublicTypes, derivedMap)).ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<string> CollectAssemblyPaths(string assemblyPath)
|
||
|
|
{
|
||
|
|
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
|
||
|
|
var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
|
||
|
|
foreach (var dll in Directory.GetFiles(runtimeDir, "*.dll"))
|
||
|
|
paths.Add(dll);
|
||
|
|
|
||
|
|
var dotnetRoot = Path.GetDirectoryName(Path.GetDirectoryName(runtimeDir.TrimEnd(Path.DirectorySeparatorChar)));
|
||
|
|
if (dotnetRoot != null)
|
||
|
|
{
|
||
|
|
var aspNetDir = Path.Combine(dotnetRoot, "Microsoft.AspNetCore.App");
|
||
|
|
if (Directory.Exists(aspNetDir))
|
||
|
|
{
|
||
|
|
var latest = Directory.GetDirectories(aspNetDir).OrderDescending().FirstOrDefault();
|
||
|
|
if (latest != null)
|
||
|
|
{
|
||
|
|
foreach (var dll in Directory.GetFiles(latest, "*.dll"))
|
||
|
|
paths.Add(dll);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var assemblyDir = Path.GetDirectoryName(assemblyPath)!;
|
||
|
|
foreach (var dll in Directory.GetFiles(assemblyDir, "*.dll"))
|
||
|
|
paths.Add(dll);
|
||
|
|
|
||
|
|
paths.Add(assemblyPath);
|
||
|
|
return paths.ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool IsCompilerGenerated(Type t)
|
||
|
|
{
|
||
|
|
return t.Name.Contains('<', StringComparison.Ordinal) || t.Name.Contains('>', StringComparison.Ordinal) || t.Name.StartsWith("__", StringComparison.Ordinal);
|
||
|
|
}
|
||
|
|
|
||
|
|
static Dictionary<string, List<string>> BuildDerivedTypeMap(List<Type> types)
|
||
|
|
{
|
||
|
|
var map = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||
|
|
foreach (var t in types)
|
||
|
|
{
|
||
|
|
var baseName = t.BaseType?.FullName;
|
||
|
|
if (baseName != null && !baseName.StartsWith("System.", StringComparison.Ordinal))
|
||
|
|
{
|
||
|
|
if (!map.ContainsKey(baseName))
|
||
|
|
map[baseName] = new List<string>();
|
||
|
|
map[baseName].Add(FormatTypeName(t));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return map;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ApiTypeInfo InspectType(Type type, List<Type> allTypes, Dictionary<string, List<string>> derivedMap)
|
||
|
|
{
|
||
|
|
var fullName = FormatTypeName(type);
|
||
|
|
var name = FormatShortName(type);
|
||
|
|
var ns = type.Namespace ?? "";
|
||
|
|
var kind = GetTypeKind(type);
|
||
|
|
var baseTypeName = type.BaseType != null && type.BaseType.FullName != "System.Object" && type.BaseType.FullName != "System.ValueType" && type.BaseType.FullName != "System.Enum"
|
||
|
|
? FormatTypeName(type.BaseType) : null;
|
||
|
|
|
||
|
|
var inheritance = BuildInheritanceChain(type);
|
||
|
|
var interfaces = type.GetInterfaces()
|
||
|
|
.Where(i => i.IsPublic && !IsSystemInterface(i))
|
||
|
|
.Select(FormatTypeName)
|
||
|
|
.Distinct()
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
var derived = derivedMap.TryGetValue(type.FullName ?? "", out var d) ? d : new List<string>();
|
||
|
|
|
||
|
|
var typeParams = type.IsGenericType
|
||
|
|
? type.GetGenericArguments().Select(a => a.Name).ToList()
|
||
|
|
: new List<string>();
|
||
|
|
|
||
|
|
var members = kind != TypeKind.Enum ? GetMembers(type) : new List<TypeMemberInfo>();
|
||
|
|
var enumFields = kind == TypeKind.Enum ? GetEnumFields(type) : new List<EnumFieldInfo>();
|
||
|
|
var inheritedMembers = kind != TypeKind.Enum ? GetInheritedMembers(type) : new List<InheritedMemberInfo>();
|
||
|
|
|
||
|
|
var syntax = BuildSyntaxDeclaration(type);
|
||
|
|
|
||
|
|
return new ApiTypeInfo(fullName, name, ns, kind, baseTypeName, "", "", syntax,
|
||
|
|
inheritance, interfaces, derived, typeParams, new Dictionary<string, string>(), members, enumFields, new List<ExampleSegment>(), inheritedMembers);
|
||
|
|
}
|
||
|
|
|
||
|
|
static TypeKind GetTypeKind(Type type)
|
||
|
|
{
|
||
|
|
if (type.IsEnum) return TypeKind.Enum;
|
||
|
|
if (type.IsInterface) return TypeKind.Interface;
|
||
|
|
if (type.IsValueType) return TypeKind.Struct;
|
||
|
|
if (typeof(Delegate).IsAssignableFrom(type) || (type.BaseType?.FullName?.Contains("Delegate", StringComparison.Ordinal) ?? false))
|
||
|
|
return TypeKind.Delegate;
|
||
|
|
return TypeKind.Class;
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool IsSystemInterface(Type iface)
|
||
|
|
{
|
||
|
|
var ns = iface.Namespace ?? "";
|
||
|
|
return ns.StartsWith("System.", StringComparison.Ordinal) || ns == "System";
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<string> BuildInheritanceChain(Type type)
|
||
|
|
{
|
||
|
|
var chain = new List<string>();
|
||
|
|
var current = type.BaseType;
|
||
|
|
while (current != null && current.FullName != "System.Object")
|
||
|
|
{
|
||
|
|
chain.Insert(0, FormatTypeName(current));
|
||
|
|
current = current.BaseType;
|
||
|
|
}
|
||
|
|
chain.Insert(0, "Object");
|
||
|
|
return chain;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string GetAccessModifier(MethodBase? method)
|
||
|
|
{
|
||
|
|
if (method == null) return "";
|
||
|
|
if (method.IsPublic) return "public";
|
||
|
|
if (method.IsFamily || method.IsFamilyOrAssembly) return "protected";
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
static string GetMethodAccessModifier(MethodInfo method)
|
||
|
|
{
|
||
|
|
if (method.IsPublic) return "public";
|
||
|
|
if (method.IsFamily || method.IsFamilyOrAssembly) return "protected";
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool IsPublicOrProtected(MethodBase? method) =>
|
||
|
|
method != null && (method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly);
|
||
|
|
|
||
|
|
static string BuildMethodModifiers(MethodInfo method)
|
||
|
|
{
|
||
|
|
var parts = new List<string>();
|
||
|
|
parts.Add(GetMethodAccessModifier(method));
|
||
|
|
if (method.IsStatic) parts.Add("static");
|
||
|
|
if (method.IsAbstract) parts.Add("abstract");
|
||
|
|
else if (method.IsVirtual && !method.IsFinal)
|
||
|
|
{
|
||
|
|
var isOverride = method.Attributes.HasFlag(System.Reflection.MethodAttributes.ReuseSlot) &&
|
||
|
|
method.DeclaringType?.BaseType?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
|
||
|
|
.Any(m => m.Name == method.Name && m.IsVirtual) == true;
|
||
|
|
parts.Add(isOverride ? "override" : "virtual");
|
||
|
|
}
|
||
|
|
return string.Join(" ", parts.Where(p => p.Length > 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<TypeMemberInfo> GetMembers(Type type)
|
||
|
|
{
|
||
|
|
var members = new List<TypeMemberInfo>();
|
||
|
|
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||
|
|
|
||
|
|
foreach (var ctor in type.GetConstructors(flags).Where(c => c.IsPublic || c.IsFamily || c.IsFamilyOrAssembly))
|
||
|
|
{
|
||
|
|
var access = GetAccessModifier(ctor);
|
||
|
|
var parameters = ctor.GetParameters().Select(p => new ParameterInfo(p.Name ?? "", FormatTypeName(p.ParameterType), "")).ToList();
|
||
|
|
var sig = $"{access} {FormatShortName(type)}({string.Join(", ", parameters.Select(p => $"{p.TypeName} {p.Name}"))})";
|
||
|
|
members.Add(new TypeMemberInfo(FormatShortName(type), FormatTypeName(type), "", "", MemberKind.Constructor, sig, parameters, null, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var prop in type.GetProperties(flags))
|
||
|
|
{
|
||
|
|
if (!IsPublicOrProtected(prop.GetMethod) && !IsPublicOrProtected(prop.SetMethod)) continue;
|
||
|
|
if (IsCompilerGeneratedMember(prop.Name)) continue;
|
||
|
|
var propTypeName = FormatTypeName(prop.PropertyType);
|
||
|
|
var accessors = new List<string>();
|
||
|
|
if (IsPublicOrProtected(prop.GetMethod)) accessors.Add("get");
|
||
|
|
if (IsPublicOrProtected(prop.SetMethod)) accessors.Add("set");
|
||
|
|
var access = IsPublicOrProtected(prop.GetMethod) ? GetAccessModifier(prop.GetMethod) : GetAccessModifier(prop.SetMethod);
|
||
|
|
var sig = $"{access} {propTypeName} {prop.Name} {{ {string.Join("; ", accessors)}; }}";
|
||
|
|
members.Add(new TypeMemberInfo(prop.Name, FormatTypeName(type), propTypeName, "", MemberKind.Property, sig, Array.Empty<ParameterInfo>(), propTypeName, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var method in type.GetMethods(flags).Where(m => (m.IsPublic || m.IsFamily || m.IsFamilyOrAssembly) && !m.IsSpecialName))
|
||
|
|
{
|
||
|
|
if (IsCompilerGeneratedMember(method.Name)) continue;
|
||
|
|
if (IsObjectMethod(method.Name)) continue;
|
||
|
|
var parameters = method.GetParameters().Select(p => new ParameterInfo(p.Name ?? "", FormatTypeName(p.ParameterType), "")).ToList();
|
||
|
|
var returnType = FormatTypeName(method.ReturnType);
|
||
|
|
var mods = BuildMethodModifiers(method);
|
||
|
|
var sig = $"{mods} {returnType} {method.Name}({string.Join(", ", parameters.Select(p => $"{p.TypeName} {p.Name}"))})";
|
||
|
|
members.Add(new TypeMemberInfo(method.Name, FormatTypeName(type), "", "", MemberKind.Method, sig, parameters, returnType, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var method in type.GetMethods(flags).Where(m => m.IsPublic && m.IsSpecialName && m.IsStatic && m.Name.StartsWith("op_", StringComparison.Ordinal)))
|
||
|
|
{
|
||
|
|
var opName = FormatOperatorName(method.Name);
|
||
|
|
if (opName == null) continue;
|
||
|
|
var parameters = method.GetParameters().Select(p => new ParameterInfo(p.Name ?? "", FormatTypeName(p.ParameterType), "")).ToList();
|
||
|
|
var returnType = FormatTypeName(method.ReturnType);
|
||
|
|
var sig = $"public static {returnType} {opName}({string.Join(", ", parameters.Select(p => $"{p.TypeName} {p.Name}"))})";
|
||
|
|
members.Add(new TypeMemberInfo(opName, FormatTypeName(type), "", "", MemberKind.Operator, sig, parameters, returnType, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var evt in type.GetEvents(flags))
|
||
|
|
{
|
||
|
|
if (!IsPublicOrProtected(evt.AddMethod)) continue;
|
||
|
|
var evtTypeName = evt.EventHandlerType != null ? FormatTypeName(evt.EventHandlerType) : "EventHandler";
|
||
|
|
var access = GetAccessModifier(evt.AddMethod);
|
||
|
|
var sig = $"{access} event {evtTypeName} {evt.Name}";
|
||
|
|
members.Add(new TypeMemberInfo(evt.Name, FormatTypeName(type), evtTypeName, "", MemberKind.Event, sig, Array.Empty<ParameterInfo>(), null, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var field in type.GetFields(flags).Where(f => (f.IsPublic || f.IsFamily || f.IsFamilyOrAssembly) && !f.IsSpecialName))
|
||
|
|
{
|
||
|
|
if (IsCompilerGeneratedMember(field.Name)) continue;
|
||
|
|
var fieldTypeName = FormatTypeName(field.FieldType);
|
||
|
|
var access = field.IsPublic ? "public" : "protected";
|
||
|
|
var modifier = field.IsStatic ? "static " : "";
|
||
|
|
var readonlyMod = field.IsInitOnly ? "readonly " : "";
|
||
|
|
var sig = $"{access} {modifier}{readonlyMod}{fieldTypeName} {field.Name}";
|
||
|
|
members.Add(new TypeMemberInfo(field.Name, FormatTypeName(type), fieldTypeName, "", MemberKind.Field, sig, Array.Empty<ParameterInfo>(), null, null, new List<ExampleSegment>()));
|
||
|
|
}
|
||
|
|
|
||
|
|
return members;
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<InheritedMemberInfo> GetInheritedMembers(Type type)
|
||
|
|
{
|
||
|
|
var inherited = new List<InheritedMemberInfo>();
|
||
|
|
var declaredNames = new HashSet<string>(StringComparer.Ordinal);
|
||
|
|
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||
|
|
|
||
|
|
foreach (var m in type.GetMembers(flags))
|
||
|
|
{
|
||
|
|
if (m is MethodInfo mi && mi.IsSpecialName) continue;
|
||
|
|
declaredNames.Add(m.Name);
|
||
|
|
}
|
||
|
|
|
||
|
|
var current = type.BaseType;
|
||
|
|
while (current != null && current.FullName != "System.Object")
|
||
|
|
{
|
||
|
|
var declaringName = FormatTypeName(current);
|
||
|
|
foreach (var m in current.GetMembers(flags))
|
||
|
|
{
|
||
|
|
if (m is ConstructorInfo) continue;
|
||
|
|
if (m is MethodInfo mi && mi.IsSpecialName) continue;
|
||
|
|
if (IsCompilerGeneratedMember(m.Name)) continue;
|
||
|
|
if (IsObjectMethod(m.Name)) continue;
|
||
|
|
|
||
|
|
bool isAccessible = m switch
|
||
|
|
{
|
||
|
|
PropertyInfo pi => IsPublicOrProtected(pi.GetMethod) || IsPublicOrProtected(pi.SetMethod),
|
||
|
|
MethodInfo method => method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly,
|
||
|
|
EventInfo ei => IsPublicOrProtected(ei.AddMethod),
|
||
|
|
FieldInfo fi => fi.IsPublic || fi.IsFamily || fi.IsFamilyOrAssembly,
|
||
|
|
_ => false
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!isAccessible) continue;
|
||
|
|
if (declaredNames.Contains(m.Name)) continue;
|
||
|
|
|
||
|
|
var kind = m switch
|
||
|
|
{
|
||
|
|
PropertyInfo => MemberKind.Property,
|
||
|
|
EventInfo => MemberKind.Event,
|
||
|
|
FieldInfo => MemberKind.Field,
|
||
|
|
_ => MemberKind.Method
|
||
|
|
};
|
||
|
|
|
||
|
|
inherited.Add(new InheritedMemberInfo(m.Name, declaringName, kind));
|
||
|
|
declaredNames.Add(m.Name);
|
||
|
|
}
|
||
|
|
|
||
|
|
current = current.BaseType;
|
||
|
|
}
|
||
|
|
|
||
|
|
return inherited;
|
||
|
|
}
|
||
|
|
|
||
|
|
static readonly HashSet<string> ObjectOnlyMethods = new()
|
||
|
|
{
|
||
|
|
"GetType", "ReferenceEquals", "MemberwiseClone"
|
||
|
|
};
|
||
|
|
static bool IsObjectMethod(string name) => ObjectOnlyMethods.Contains(name);
|
||
|
|
|
||
|
|
static readonly Dictionary<string, string> OperatorNames = new()
|
||
|
|
{
|
||
|
|
["op_Equality"] = "operator ==",
|
||
|
|
["op_Inequality"] = "operator !=",
|
||
|
|
["op_Addition"] = "operator +",
|
||
|
|
["op_Subtraction"] = "operator -",
|
||
|
|
["op_Multiply"] = "operator *",
|
||
|
|
["op_Division"] = "operator /",
|
||
|
|
["op_Modulus"] = "operator %",
|
||
|
|
["op_GreaterThan"] = "operator >",
|
||
|
|
["op_LessThan"] = "operator <",
|
||
|
|
["op_GreaterThanOrEqual"] = "operator >=",
|
||
|
|
["op_LessThanOrEqual"] = "operator <=",
|
||
|
|
["op_BitwiseAnd"] = "operator &",
|
||
|
|
["op_BitwiseOr"] = "operator |",
|
||
|
|
["op_ExclusiveOr"] = "operator ^",
|
||
|
|
["op_LeftShift"] = "operator <<",
|
||
|
|
["op_RightShift"] = "operator >>",
|
||
|
|
["op_UnaryPlus"] = "operator +",
|
||
|
|
["op_UnaryNegation"] = "operator -",
|
||
|
|
["op_LogicalNot"] = "operator !",
|
||
|
|
["op_OnesComplement"] = "operator ~",
|
||
|
|
["op_True"] = "operator true",
|
||
|
|
["op_False"] = "operator false",
|
||
|
|
["op_Implicit"] = "implicit operator",
|
||
|
|
["op_Explicit"] = "explicit operator",
|
||
|
|
["op_Increment"] = "operator ++",
|
||
|
|
["op_Decrement"] = "operator --",
|
||
|
|
};
|
||
|
|
|
||
|
|
static string? FormatOperatorName(string methodName) =>
|
||
|
|
OperatorNames.TryGetValue(methodName, out var name) ? name : null;
|
||
|
|
|
||
|
|
static bool IsCompilerGeneratedMember(string name) =>
|
||
|
|
name.Contains('<', StringComparison.Ordinal) || name.Contains('>', StringComparison.Ordinal) || name.StartsWith("__", StringComparison.Ordinal);
|
||
|
|
|
||
|
|
static List<EnumFieldInfo> GetEnumFields(Type type)
|
||
|
|
{
|
||
|
|
var fields = new List<EnumFieldInfo>();
|
||
|
|
foreach (var name in Enum.GetNames(type))
|
||
|
|
{
|
||
|
|
var field = type.GetField(name);
|
||
|
|
var value = field?.GetRawConstantValue()?.ToString();
|
||
|
|
fields.Add(new EnumFieldInfo(name, "", value));
|
||
|
|
}
|
||
|
|
return fields;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string BuildSyntaxDeclaration(Type type)
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder("public ");
|
||
|
|
|
||
|
|
if (type.IsEnum)
|
||
|
|
{
|
||
|
|
sb.Append("enum ");
|
||
|
|
}
|
||
|
|
else if (type.IsInterface)
|
||
|
|
{
|
||
|
|
sb.Append("interface ");
|
||
|
|
}
|
||
|
|
else if (type.IsValueType)
|
||
|
|
{
|
||
|
|
sb.Append("struct ");
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (type.IsAbstract && type.IsSealed) sb.Append("static ");
|
||
|
|
else if (type.IsAbstract) sb.Append("abstract ");
|
||
|
|
else if (type.IsSealed) sb.Append("sealed ");
|
||
|
|
sb.Append("class ");
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.Append(FormatShortName(type));
|
||
|
|
|
||
|
|
var baseAndInterfaces = new List<string>();
|
||
|
|
if (type.BaseType != null && type.BaseType.FullName != "System.Object" &&
|
||
|
|
type.BaseType.FullName != "System.ValueType" && type.BaseType.FullName != "System.Enum" &&
|
||
|
|
!type.IsEnum)
|
||
|
|
{
|
||
|
|
baseAndInterfaces.Add(FormatTypeName(type.BaseType));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var iface in type.GetInterfaces().Where(i => i.IsPublic && !IsSystemInterface(i)))
|
||
|
|
{
|
||
|
|
baseAndInterfaces.Add(FormatTypeName(iface));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (baseAndInterfaces.Count > 0)
|
||
|
|
{
|
||
|
|
sb.Append(" : ");
|
||
|
|
sb.Append(string.Join(", ", baseAndInterfaces.Distinct()));
|
||
|
|
}
|
||
|
|
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
static string FormatTypeName(Type type)
|
||
|
|
{
|
||
|
|
if (type.IsGenericType)
|
||
|
|
{
|
||
|
|
var name = type.FullName ?? (type.Namespace != null ? $"{type.Namespace}.{type.Name}" : type.Name);
|
||
|
|
var tick = name.IndexOf('`', StringComparison.Ordinal);
|
||
|
|
if (tick >= 0)
|
||
|
|
{
|
||
|
|
var baseName = name[..tick];
|
||
|
|
var args = type.GetGenericArguments();
|
||
|
|
return $"{baseName}<{string.Join(", ", args.Select(FormatTypeName))}>";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return type.FullName ?? (type.Namespace != null ? $"{type.Namespace}.{type.Name}" : type.Name);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string FormatShortName(Type type)
|
||
|
|
{
|
||
|
|
if (type.IsGenericType)
|
||
|
|
{
|
||
|
|
var name = type.Name;
|
||
|
|
var tick = name.IndexOf('`', StringComparison.Ordinal);
|
||
|
|
if (tick >= 0)
|
||
|
|
{
|
||
|
|
var baseName = name[..tick];
|
||
|
|
var args = type.GetGenericArguments();
|
||
|
|
return $"{baseName}<{string.Join(", ", args.Select(a => a.Name))}>";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return type.Name;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sealed class RazorPageGenerator
|
||
|
|
{
|
||
|
|
readonly Dictionary<string, XmlMemberDoc> _xmlDocs;
|
||
|
|
readonly List<ApiTypeInfo> _types;
|
||
|
|
readonly string _outputDir;
|
||
|
|
int _pageCount;
|
||
|
|
Dictionary<string, string> _typeUrlMap = null!;
|
||
|
|
|
||
|
|
public int PageCount => _pageCount;
|
||
|
|
|
||
|
|
public RazorPageGenerator(Dictionary<string, XmlMemberDoc> xmlDocs, List<ApiTypeInfo> types, string outputDir)
|
||
|
|
{
|
||
|
|
_xmlDocs = xmlDocs;
|
||
|
|
_types = types;
|
||
|
|
_outputDir = outputDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Generate()
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(_outputDir);
|
||
|
|
|
||
|
|
BuildTypeUrlMap();
|
||
|
|
EnrichTypesWithXmlDocs();
|
||
|
|
|
||
|
|
GenerateIndexPage();
|
||
|
|
GenerateNamespacePages();
|
||
|
|
GenerateTypePages();
|
||
|
|
GenerateApiNavComponent();
|
||
|
|
GenerateImportsFile();
|
||
|
|
}
|
||
|
|
|
||
|
|
void BuildTypeUrlMap()
|
||
|
|
{
|
||
|
|
_typeUrlMap = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
|
|
|
||
|
|
foreach (var type in _types)
|
||
|
|
{
|
||
|
|
var url = TypePageUrl(type);
|
||
|
|
_typeUrlMap[type.FullName] = url;
|
||
|
|
_typeUrlMap[type.Name] = url;
|
||
|
|
|
||
|
|
var normalizedFull = NormalizeGenericName(type.FullName);
|
||
|
|
if (normalizedFull != type.FullName)
|
||
|
|
_typeUrlMap[normalizedFull] = url;
|
||
|
|
|
||
|
|
var normalizedShort = NormalizeGenericName(type.Name);
|
||
|
|
if (normalizedShort != type.Name)
|
||
|
|
_typeUrlMap[normalizedShort] = url;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
string? TryResolveTypeUrl(string typeName)
|
||
|
|
{
|
||
|
|
if (_typeUrlMap.TryGetValue(typeName, out var url))
|
||
|
|
return url;
|
||
|
|
|
||
|
|
var normalized = NormalizeGenericName(typeName);
|
||
|
|
if (_typeUrlMap.TryGetValue(normalized, out url))
|
||
|
|
return url;
|
||
|
|
|
||
|
|
var simplified = SimplifyTypeName(typeName);
|
||
|
|
if (_typeUrlMap.TryGetValue(simplified, out url))
|
||
|
|
return url;
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EnrichTypesWithXmlDocs()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < _types.Count; i++)
|
||
|
|
{
|
||
|
|
var t = _types[i];
|
||
|
|
var typeKey = $"T:{t.FullName}";
|
||
|
|
var cleanKey = $"T:{NormalizeGenericName(t.FullName)}";
|
||
|
|
|
||
|
|
string summary = "", remarks = "";
|
||
|
|
var examples = new List<ExampleSegment>();
|
||
|
|
var typeParamDescs = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
|
|
XmlMemberDoc? typeDoc = null;
|
||
|
|
if (_xmlDocs.TryGetValue(cleanKey, out typeDoc) || _xmlDocs.TryGetValue(typeKey, out typeDoc))
|
||
|
|
{
|
||
|
|
summary = typeDoc.Summary;
|
||
|
|
remarks = typeDoc.Remarks;
|
||
|
|
examples = typeDoc.Examples;
|
||
|
|
typeParamDescs = typeDoc.TypeParams;
|
||
|
|
}
|
||
|
|
|
||
|
|
var enrichedMembers = new List<TypeMemberInfo>();
|
||
|
|
foreach (var member in t.Members)
|
||
|
|
{
|
||
|
|
var memberDoc = FindMemberDoc(t, member);
|
||
|
|
var memberSummary = memberDoc?.Summary ?? "";
|
||
|
|
if (string.IsNullOrEmpty(memberSummary) && member.Kind == MemberKind.Constructor)
|
||
|
|
{
|
||
|
|
memberSummary = summary;
|
||
|
|
}
|
||
|
|
var enriched = member with
|
||
|
|
{
|
||
|
|
Summary = memberSummary,
|
||
|
|
TypeName = member.TypeName,
|
||
|
|
Examples = memberDoc?.Examples ?? new List<ExampleSegment>()
|
||
|
|
};
|
||
|
|
|
||
|
|
if (member.Kind is MemberKind.Method or MemberKind.Constructor or MemberKind.Operator)
|
||
|
|
{
|
||
|
|
var enrichedParams = new List<ParameterInfo>();
|
||
|
|
foreach (var p in member.Parameters)
|
||
|
|
{
|
||
|
|
var paramSummary = memberDoc?.Params.TryGetValue(p.Name, out var ps) == true ? ps : "";
|
||
|
|
enrichedParams.Add(p with { Summary = paramSummary });
|
||
|
|
}
|
||
|
|
enriched = enriched with { Parameters = enrichedParams };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (member.ReturnType != null)
|
||
|
|
{
|
||
|
|
enriched = enriched with { ReturnSummary = memberDoc?.Returns ?? "" };
|
||
|
|
}
|
||
|
|
|
||
|
|
enrichedMembers.Add(enriched);
|
||
|
|
}
|
||
|
|
|
||
|
|
var enrichedEnumFields = new List<EnumFieldInfo>();
|
||
|
|
foreach (var field in t.EnumFields)
|
||
|
|
{
|
||
|
|
var fieldKey = $"F:{t.FullName}.{field.Name}";
|
||
|
|
var fieldDoc = _xmlDocs.TryGetValue(fieldKey, out var fd) ? fd : null;
|
||
|
|
enrichedEnumFields.Add(field with { Summary = fieldDoc?.Summary ?? "" });
|
||
|
|
}
|
||
|
|
|
||
|
|
_types[i] = t with { Summary = summary, Remarks = remarks, Members = enrichedMembers, EnumFields = enrichedEnumFields, Examples = examples, TypeParameterDescriptions = typeParamDescs };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
XmlMemberDoc? FindMemberDoc(ApiTypeInfo type, TypeMemberInfo member)
|
||
|
|
{
|
||
|
|
var prefix = member.Kind switch
|
||
|
|
{
|
||
|
|
MemberKind.Property => "P",
|
||
|
|
MemberKind.Method => "M",
|
||
|
|
MemberKind.Event => "E",
|
||
|
|
MemberKind.Field => "F",
|
||
|
|
MemberKind.Constructor => "M",
|
||
|
|
MemberKind.Operator => "M",
|
||
|
|
_ => "P"
|
||
|
|
};
|
||
|
|
|
||
|
|
var typeName = NormalizeGenericName(type.FullName);
|
||
|
|
|
||
|
|
if (member.Kind == MemberKind.Constructor)
|
||
|
|
{
|
||
|
|
var ctorKey = $"M:{typeName}.#ctor";
|
||
|
|
foreach (var (key, doc) in _xmlDocs)
|
||
|
|
{
|
||
|
|
if (key.StartsWith(ctorKey, StringComparison.Ordinal))
|
||
|
|
return doc;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
var memberKey = $"{prefix}:{typeName}.{member.Name}";
|
||
|
|
if (_xmlDocs.TryGetValue(memberKey, out var result))
|
||
|
|
return result;
|
||
|
|
|
||
|
|
foreach (var (key, doc) in _xmlDocs)
|
||
|
|
{
|
||
|
|
if (key.StartsWith(memberKey, StringComparison.Ordinal))
|
||
|
|
return doc;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string NormalizeGenericName(string fullName)
|
||
|
|
{
|
||
|
|
var ltIdx = fullName.IndexOf('<', StringComparison.Ordinal);
|
||
|
|
if (ltIdx < 0) return fullName;
|
||
|
|
|
||
|
|
var genericCount = fullName[ltIdx..].Count(c => c == ',') + 1;
|
||
|
|
return $"{fullName[..ltIdx]}`{genericCount}";
|
||
|
|
}
|
||
|
|
|
||
|
|
string RenderTypeLink(string typeName)
|
||
|
|
{
|
||
|
|
var display = SimplifyTypeName(typeName);
|
||
|
|
var url = TryResolveTypeUrl(typeName);
|
||
|
|
if (url != null)
|
||
|
|
{
|
||
|
|
return $"<RadzenLink Path=\"{url}\" Text=\"{EscapeHtml(display)}\" />";
|
||
|
|
}
|
||
|
|
return EscapeHtml(display);
|
||
|
|
}
|
||
|
|
|
||
|
|
string RenderSummary(string text)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(text)) return "";
|
||
|
|
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
int i = 0;
|
||
|
|
while (i < text.Length)
|
||
|
|
{
|
||
|
|
var markerStart = text.IndexOf("\x01TYPEREF:", i, StringComparison.Ordinal);
|
||
|
|
if (markerStart < 0)
|
||
|
|
{
|
||
|
|
sb.Append(EscapeHtml(text[i..]));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (markerStart > i)
|
||
|
|
sb.Append(EscapeHtml(text[i..markerStart]));
|
||
|
|
|
||
|
|
var crefEnd = text.IndexOf('\x02', markerStart);
|
||
|
|
if (crefEnd < 0) { sb.Append(EscapeHtml(text[markerStart..])); break; }
|
||
|
|
|
||
|
|
var cref = text[(markerStart + "\x01TYPEREF:".Length)..crefEnd];
|
||
|
|
|
||
|
|
var displayStart = crefEnd + 1;
|
||
|
|
var displayEnd = text.IndexOf("\x01/TYPEREF\x02", displayStart, StringComparison.Ordinal);
|
||
|
|
if (displayEnd < 0) { sb.Append(EscapeHtml(text[markerStart..])); break; }
|
||
|
|
|
||
|
|
var display = text[displayStart..displayEnd];
|
||
|
|
|
||
|
|
var url = TryResolveTypeUrl(cref);
|
||
|
|
if (url != null)
|
||
|
|
{
|
||
|
|
sb.Append(CultureInfo.InvariantCulture, $"<RadzenLink Path=\"{url}\" Text=\"{EscapeHtml(display)}\" />");
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
sb.Append(EscapeHtml(display));
|
||
|
|
}
|
||
|
|
|
||
|
|
i = displayEnd + "\x01/TYPEREF\x02".Length;
|
||
|
|
}
|
||
|
|
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateIndexPage()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("@page \"/docs/api\"");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine("<PageTitle>API Reference - Radzen Blazor Components</PageTitle>");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine("<RadzenStack Gap=\"1rem\" class=\"rz-pt-4\">");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H3\" TagName=\"TagName.H1\">API Reference</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Body1\">Search for a component by its name or part of the name.</RadzenText>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
var namespaces = _types.Select(t => t.Namespace).Where(n => !string.IsNullOrEmpty(n)).Distinct().OrderBy(n => n).ToList();
|
||
|
|
|
||
|
|
sb.AppendLine(" <RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H5\" TagName=\"TagName.H2\" class=\"rz-mb-3\">Namespaces</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Namespace</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
foreach (var ns in namespaces)
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenTableRow>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell><RadzenLink Path=\"/docs/api/{ns}\" Text=\"{EscapeHtml(ns)}\" /></RadzenTableCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableRow>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
sb.AppendLine(" </RadzenCard>");
|
||
|
|
sb.AppendLine("</RadzenStack>");
|
||
|
|
|
||
|
|
WritePage(Path.Combine(_outputDir, "ApiIndex.razor"), sb.ToString());
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateNamespacePages()
|
||
|
|
{
|
||
|
|
var namespaces = _types.GroupBy(t => t.Namespace).Where(g => !string.IsNullOrEmpty(g.Key)).OrderBy(g => g.Key);
|
||
|
|
|
||
|
|
foreach (var nsGroup in namespaces)
|
||
|
|
{
|
||
|
|
var ns = nsGroup.Key;
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"@page \"/docs/api/{ns}\"");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"<PageTitle>{ns} Namespace - Radzen Blazor Components</PageTitle>");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine("<RadzenStack Gap=\"1rem\" class=\"rz-pt-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.H3\" TagName=\"TagName.H1\" class=\"text-break\">{EscapeHtml(ns)} Namespace</RadzenText>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
var classes = nsGroup.Where(t => t.Kind == TypeKind.Class).OrderBy(t => t.Name).ToList();
|
||
|
|
var structs = nsGroup.Where(t => t.Kind == TypeKind.Struct).OrderBy(t => t.Name).ToList();
|
||
|
|
var interfaces = nsGroup.Where(t => t.Kind == TypeKind.Interface).OrderBy(t => t.Name).ToList();
|
||
|
|
var enums = nsGroup.Where(t => t.Kind == TypeKind.Enum).OrderBy(t => t.Name).ToList();
|
||
|
|
var delegates = nsGroup.Where(t => t.Kind == TypeKind.Delegate).OrderBy(t => t.Name).ToList();
|
||
|
|
|
||
|
|
WriteTypeSection(sb, "Classes", classes);
|
||
|
|
WriteTypeSection(sb, "Structs", structs);
|
||
|
|
WriteTypeSection(sb, "Interfaces", interfaces);
|
||
|
|
WriteTypeSection(sb, "Enums", enums);
|
||
|
|
WriteTypeSection(sb, "Delegates", delegates);
|
||
|
|
sb.AppendLine("</RadzenStack>");
|
||
|
|
|
||
|
|
var fileName = $"NS_{ns.Replace('.', '_')}.razor";
|
||
|
|
WritePage(Path.Combine(_outputDir, fileName), sb.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void WriteTypeSection(StringBuilder sb, string heading, List<ApiTypeInfo> types)
|
||
|
|
{
|
||
|
|
if (types.Count == 0) return;
|
||
|
|
|
||
|
|
sb.AppendLine(" <RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.H5\" TagName=\"TagName.H2\" class=\"rz-mb-3\">{EscapeHtml(heading)}</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Name</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
foreach (var t in types)
|
||
|
|
{
|
||
|
|
var link = TypePageUrl(t);
|
||
|
|
sb.AppendLine(" <RadzenTableRow>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell><RadzenLink Path=\"{link}\" Text=\"{EscapeHtml(t.Name)}\" /></RadzenTableCell>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell><RadzenText TextStyle=\"TextStyle.Body2\">{RenderSummary(t.Summary)}</RadzenText></RadzenTableCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableRow>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
sb.AppendLine(" </RadzenCard>");
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateTypePages()
|
||
|
|
{
|
||
|
|
foreach (var type in _types)
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
var routeName = GetRouteTypeName(type);
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"@page \"/docs/api/{routeName}\"");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"<PageTitle>{EscapeHtml(type.Name)} {type.Kind} - Radzen Blazor Components</PageTitle>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
var tocItems = new List<(string Id, string Text, int Level)>();
|
||
|
|
|
||
|
|
sb.AppendLine("<RadzenRow Gap=\"2rem\">");
|
||
|
|
sb.AppendLine("<RadzenColumn Size=\"12\" SizeMD=\"10\">");
|
||
|
|
sb.AppendLine("<RadzenStack Gap=\"1rem\" class=\"rz-pt-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.H3\" TagName=\"TagName.H1\" class=\"text-break\">{EscapeHtml(type.Name)} {type.Kind}</RadzenText>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(type.Summary))
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body1\">{RenderSummary(type.Summary)}</RadzenText>");
|
||
|
|
sb.AppendLine(" </RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type.Kind == TypeKind.Class || type.Kind == TypeKind.Struct || type.Kind == TypeKind.Interface)
|
||
|
|
{
|
||
|
|
GenerateClassContent(sb, type, tocItems);
|
||
|
|
}
|
||
|
|
else if (type.Kind == TypeKind.Enum)
|
||
|
|
{
|
||
|
|
GenerateEnumContent(sb, type, tocItems);
|
||
|
|
}
|
||
|
|
else if (type.Kind == TypeKind.Delegate)
|
||
|
|
{
|
||
|
|
GenerateDelegateContent(sb, type);
|
||
|
|
}
|
||
|
|
sb.AppendLine("</RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenColumn>");
|
||
|
|
|
||
|
|
if (tocItems.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenColumn Size=\"2\" class=\"rz-display-none rz-display-md-block\">");
|
||
|
|
sb.AppendLine(" <RadzenStack class=\"rz-pt-4\" Style=\"position: sticky; top: 1rem;\">");
|
||
|
|
sb.AppendLine(" <RadzenText Text=\"In This Article\" TextStyle=\"TextStyle.H6\" class=\"rz-mb-4\" />");
|
||
|
|
sb.AppendLine(" <RadzenToc Selector=\".rz-body\">");
|
||
|
|
foreach (var (id, text, _) in tocItems)
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTocItem Text=\"{EscapeHtml(text)}\" Selector=\"#{id}\" />");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenToc>");
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenColumn>");
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.AppendLine("</RadzenRow>");
|
||
|
|
|
||
|
|
var fileName = $"T_{GetSafeFileName(type)}.razor";
|
||
|
|
WritePage(Path.Combine(_outputDir, fileName), sb.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateClassContent(StringBuilder sb, ApiTypeInfo type, List<(string Id, string Text, int Level)> tocItems)
|
||
|
|
{
|
||
|
|
if (type.Kind == TypeKind.Class && type.Inheritance.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"inheritance\">");
|
||
|
|
tocItems.Add(("inheritance", "Inheritance", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Inheritance</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenStack Gap=\"0.25rem\">");
|
||
|
|
for (int i = 0; i < type.Inheritance.Count; i++)
|
||
|
|
{
|
||
|
|
var indent = new string(' ', i * 2);
|
||
|
|
var inherited = type.Inheritance[i];
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">{indent}{RenderTypeLink(inherited)}</RadzenText>");
|
||
|
|
}
|
||
|
|
var selfIndent = new string(' ', type.Inheritance.Count * 2);
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Subtitle1\">{selfIndent}{EscapeHtml(SimplifyTypeName(type.FullName))}</RadzenText>");
|
||
|
|
if (type.DerivedTypes.Count > 0)
|
||
|
|
{
|
||
|
|
var derivedIndent = new string(' ', (type.Inheritance.Count + 1) * 2);
|
||
|
|
foreach (var derived in type.DerivedTypes)
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">{derivedIndent}{RenderTypeLink(derived)}</RadzenText>");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type.Interfaces.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"implements\">");
|
||
|
|
tocItems.Add(("implements", "Implements", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Implements</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenStack Gap=\"0.25rem\">");
|
||
|
|
foreach (var iface in type.Interfaces)
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">{RenderTypeLink(iface)}</RadzenText>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type.InheritedMembers.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"inherited-members\">");
|
||
|
|
tocItems.Add(("inherited-members", "Inherited Members", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Inherited Members</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenStack Gap=\"0.25rem\">");
|
||
|
|
foreach (var m in type.InheritedMembers)
|
||
|
|
{
|
||
|
|
var declaringDisplay = SimplifyTypeName(m.DeclaringTypeName);
|
||
|
|
var declaringUrl = TryResolveTypeUrl(m.DeclaringTypeName);
|
||
|
|
if (declaringUrl != null)
|
||
|
|
{
|
||
|
|
var fragment = $"#{MemberKindToSectionId(m.Kind)}-{SanitizeId(m.Name)}";
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\"><RadzenLink Path=\"{declaringUrl}{fragment}\" Text=\"{EscapeHtml(declaringDisplay)}.{EscapeHtml(m.Name)}\" /></RadzenText>");
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">{EscapeHtml(declaringDisplay)}.{EscapeHtml(m.Name)}</RadzenText>");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(" <RadzenStack Gap=\"0.5rem\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">Namespace: <RadzenLink Path=\"/docs/api/{type.Namespace}\" Text=\"{EscapeHtml(type.Namespace)}\" /></RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Body2\">Assembly: Radzen.Blazor.dll</RadzenText>");
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"syntax\">");
|
||
|
|
tocItems.Add(("syntax", "Syntax", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Syntax</RadzenText>");
|
||
|
|
WriteCodeBlock(sb, SimplifySignature(type.Syntax), 2);
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
if (type.TypeParameters.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"type-parameters\">");
|
||
|
|
tocItems.Add(("type-parameters", "Type Parameters", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Type Parameters</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Name</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
foreach (var tp in type.TypeParameters)
|
||
|
|
{
|
||
|
|
var tpDesc = type.TypeParameterDescriptions.TryGetValue(tp, out var desc) ? desc : "";
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableRow><RadzenTableCell>{EscapeHtml(tp)}</RadzenTableCell><RadzenTableCell>{EscapeHtml(tpDesc)}</RadzenTableCell></RadzenTableRow>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type.Examples.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"examples\">");
|
||
|
|
tocItems.Add(("examples", "Examples", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Examples</RadzenText>");
|
||
|
|
WriteExampleSegments(sb, type.Examples, 2);
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(type.Remarks))
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"remarks\">");
|
||
|
|
tocItems.Add(("remarks", "Remarks", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Remarks</RadzenText>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">{EscapeHtml(type.Remarks)}</RadzenText>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
var constructors = type.Members.Where(m => m.Kind == MemberKind.Constructor).ToList();
|
||
|
|
var properties = type.Members.Where(m => m.Kind == MemberKind.Property).ToList();
|
||
|
|
var methods = type.Members.Where(m => m.Kind == MemberKind.Method).ToList();
|
||
|
|
var events = type.Members.Where(m => m.Kind == MemberKind.Event).ToList();
|
||
|
|
var fields = type.Members.Where(m => m.Kind == MemberKind.Field).ToList();
|
||
|
|
var operators = type.Members.Where(m => m.Kind == MemberKind.Operator).ToList();
|
||
|
|
|
||
|
|
var routeName = GetRouteTypeName(type);
|
||
|
|
WriteMembersSection(sb, "Constructors", "constructors", constructors, tocItems, routeName);
|
||
|
|
WriteMembersSection(sb, "Fields", "fields", fields, tocItems, routeName);
|
||
|
|
WriteMembersSection(sb, "Properties", "properties", properties, tocItems, routeName);
|
||
|
|
WriteMembersSection(sb, "Methods", "methods", methods, tocItems, routeName);
|
||
|
|
WriteMembersSection(sb, "Events", "events", events, tocItems, routeName);
|
||
|
|
WriteMembersSection(sb, "Operators", "operators", operators, tocItems, routeName);
|
||
|
|
}
|
||
|
|
|
||
|
|
void WriteMembersSection(StringBuilder sb, string heading, string sectionId, List<TypeMemberInfo> members, List<(string Id, string Text, int Level)> tocItems, string routeName)
|
||
|
|
{
|
||
|
|
if (members.Count == 0) return;
|
||
|
|
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"{sectionId}\">");
|
||
|
|
tocItems.Add((sectionId, heading, 0));
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.H5\" TagName=\"TagName.H2\" class=\"rz-mb-3\">{EscapeHtml(heading)}</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenStack Gap=\"1rem\">");
|
||
|
|
|
||
|
|
foreach (var member in members.OrderBy(m => m.Name, StringComparer.Ordinal))
|
||
|
|
{
|
||
|
|
var memberId = $"{sectionId}-{SanitizeId(member.Name)}";
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenCard Variant=\"Variant.Text\" class=\"rz-p-3\" id=\"{memberId}\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText Anchor=\"{routeName}#{memberId}\" TextStyle=\"TextStyle.H6\" TagName=\"TagName.H3\" class=\"rz-mb-2\">{EscapeHtml(member.Name)}</RadzenText>");
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(member.Summary))
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\" class=\"rz-mb-2\">{RenderSummary(member.Summary)}</RadzenText>");
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Subtitle2\" class=\"rz-mb-1\">Declaration</RadzenText>");
|
||
|
|
WriteCodeBlock(sb, SimplifySignature(member.Signature), 6);
|
||
|
|
|
||
|
|
if (member.Parameters.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Subtitle2\" class=\"rz-mb-1 rz-mt-2\">Parameters</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Type</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Name</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
foreach (var p in member.Parameters)
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenTableRow>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell>{RenderTypeLink(p.TypeName)}</RadzenTableCell>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell>{EscapeHtml(p.Name)}</RadzenTableCell>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell>{RenderSummary(p.Summary)}</RadzenTableCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableRow>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (member.Kind != MemberKind.Property && member.ReturnType != null && member.ReturnType != "System.Void" && member.ReturnType != "Void")
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Subtitle2\" class=\"rz-mb-1 rz-mt-2\">Returns</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Type</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableRow><RadzenTableCell>{RenderTypeLink(member.ReturnType)}</RadzenTableCell><RadzenTableCell>{RenderSummary(member.ReturnSummary ?? "")}</RadzenTableCell></RadzenTableRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (member.Kind == MemberKind.Property && !string.IsNullOrEmpty(member.TypeName))
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Subtitle2\" class=\"rz-mb-1 rz-mt-2\">Property Value</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Type</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableRow><RadzenTableCell>{RenderTypeLink(member.TypeName)}</RadzenTableCell><RadzenTableCell>{RenderSummary(member.Summary)}</RadzenTableCell></RadzenTableRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (member.Examples.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Subtitle2\" class=\"rz-mb-1 rz-mt-2\">Examples</RadzenText>");
|
||
|
|
WriteExampleSegments(sb, member.Examples, 6);
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.AppendLine(" </RadzenCard>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenStack>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateEnumContent(StringBuilder sb, ApiTypeInfo type, List<(string Id, string Text, int Level)> tocItems)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">Namespace: <RadzenLink Path=\"/docs/api/{type.Namespace}\" Text=\"{EscapeHtml(type.Namespace)}\" /></RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Body2\">Assembly: Radzen.Blazor.dll</RadzenText>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"syntax\">");
|
||
|
|
tocItems.Add(("syntax", "Syntax", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Syntax</RadzenText>");
|
||
|
|
WriteCodeBlock(sb, SimplifySignature(type.Syntax), 2);
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
if (type.EnumFields.Count > 0)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\" id=\"fields\">");
|
||
|
|
tocItems.Add(("fields", "Fields", 0));
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H5\" TagName=\"TagName.H2\" class=\"rz-mb-3\">Fields</RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenTable>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Name</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" <RadzenTableHeaderCell>Description</RadzenTableHeaderCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeaderRow>");
|
||
|
|
sb.AppendLine(" </RadzenTableHeader>");
|
||
|
|
sb.AppendLine(" <RadzenTableBody>");
|
||
|
|
foreach (var field in type.EnumFields)
|
||
|
|
{
|
||
|
|
sb.AppendLine(" <RadzenTableRow>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell>{EscapeHtml(field.Name)}</RadzenTableCell>");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenTableCell>{RenderSummary(field.Summary)}</RadzenTableCell>");
|
||
|
|
sb.AppendLine(" </RadzenTableRow>");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" </RadzenTableBody>");
|
||
|
|
sb.AppendLine(" </RadzenTable>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void GenerateDelegateContent(StringBuilder sb, ApiTypeInfo type)
|
||
|
|
{
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" <RadzenText TextStyle=\"TextStyle.Body2\">Namespace: <RadzenLink Path=\"/docs/api/{type.Namespace}\" Text=\"{EscapeHtml(type.Namespace)}\" /></RadzenText>");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.Body2\">Assembly: Radzen.Blazor.dll</RadzenText>");
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
sb.AppendLine();
|
||
|
|
|
||
|
|
sb.AppendLine("<RadzenCard Variant=\"Variant.Outlined\" class=\"rz-p-4\">");
|
||
|
|
sb.AppendLine(" <RadzenText TextStyle=\"TextStyle.H6\" TagName=\"TagName.H2\" class=\"rz-mb-2\">Syntax</RadzenText>");
|
||
|
|
WriteCodeBlock(sb, SimplifySignature(type.Syntax), 2);
|
||
|
|
sb.AppendLine("</RadzenCard>");
|
||
|
|
}
|
||
|
|
|
||
|
|
static void WriteCodeBlock(StringBuilder sb, string code, int indentSpaces)
|
||
|
|
{
|
||
|
|
var indent = new string(' ', indentSpaces);
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}<pre style=\"background: var(--rz-base-200); padding: 1rem; border-radius: var(--rz-border-radius); overflow-x: auto; font-family: monospace; font-size: 0.875rem;\"><code>{EscapeHtml(code)}</code></pre>");
|
||
|
|
}
|
||
|
|
|
||
|
|
static void WriteExampleSegments(StringBuilder sb, List<ExampleSegment> segments, int indentSpaces)
|
||
|
|
{
|
||
|
|
var indent = new string(' ', indentSpaces);
|
||
|
|
foreach (var segment in segments)
|
||
|
|
{
|
||
|
|
if (segment.IsCode)
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}<pre style=\"background: var(--rz-base-200); padding: 1rem; border-radius: var(--rz-border-radius); overflow-x: auto; font-family: monospace; font-size: 0.875rem;\"><code>{EscapeHtml(segment.Content)}</code></pre>");
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $"{indent}<RadzenText TextStyle=\"TextStyle.Body2\" class=\"rz-mb-2\">{EscapeHtml(segment.Content)}</RadzenText>");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateApiNavComponent()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("namespace Radzen.Blazor.Api.Generated.Pages;");
|
||
|
|
sb.AppendLine();
|
||
|
|
sb.AppendLine("public static class ApiNavData");
|
||
|
|
sb.AppendLine("{");
|
||
|
|
sb.AppendLine(" public static readonly (string Name, (string Name, string Path)[] Types)[] Namespaces =");
|
||
|
|
sb.AppendLine(" [");
|
||
|
|
|
||
|
|
var namespaces = _types.GroupBy(t => t.Namespace)
|
||
|
|
.Where(g => !string.IsNullOrEmpty(g.Key))
|
||
|
|
.OrderBy(g => g.Key);
|
||
|
|
|
||
|
|
foreach (var nsGroup in namespaces)
|
||
|
|
{
|
||
|
|
var ns = nsGroup.Key;
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" (\"{ns}\", new (string, string)[]");
|
||
|
|
sb.AppendLine(" {");
|
||
|
|
foreach (var type in nsGroup.OrderBy(t => t.Name, StringComparer.Ordinal))
|
||
|
|
{
|
||
|
|
var url = TypePageUrl(type);
|
||
|
|
sb.AppendLine(CultureInfo.InvariantCulture, $" (\"{EscapeCSharpString(type.Name)}\", \"{url}\"),");
|
||
|
|
}
|
||
|
|
sb.AppendLine(" }),");
|
||
|
|
}
|
||
|
|
|
||
|
|
sb.AppendLine(" ];");
|
||
|
|
sb.AppendLine("}");
|
||
|
|
|
||
|
|
File.WriteAllText(Path.Combine(_outputDir, "ApiNavData.cs"), sb.ToString(), Encoding.UTF8);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string EscapeCSharpString(string text)
|
||
|
|
{
|
||
|
|
return text.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||
|
|
.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||
|
|
}
|
||
|
|
|
||
|
|
void GenerateImportsFile()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("@using Microsoft.AspNetCore.Components");
|
||
|
|
sb.AppendLine("@using Microsoft.AspNetCore.Components.Routing");
|
||
|
|
sb.AppendLine("@using Microsoft.AspNetCore.Components.Web");
|
||
|
|
sb.AppendLine("@using Radzen");
|
||
|
|
sb.AppendLine("@using Radzen.Blazor");
|
||
|
|
|
||
|
|
File.WriteAllText(Path.Combine(_outputDir, "_Imports.razor"), sb.ToString(), Encoding.UTF8);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string TypePageUrl(ApiTypeInfo type) => $"/docs/api/{GetRouteTypeName(type)}";
|
||
|
|
|
||
|
|
static string GetRouteTypeName(ApiTypeInfo type)
|
||
|
|
{
|
||
|
|
var name = type.FullName;
|
||
|
|
var ltIdx = name.IndexOf('<', StringComparison.Ordinal);
|
||
|
|
if (ltIdx >= 0)
|
||
|
|
{
|
||
|
|
var genericCount = name[ltIdx..].Count(c => c == ',') + 1;
|
||
|
|
name = $"{name[..ltIdx]}-{genericCount}";
|
||
|
|
}
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string GetSafeFileName(ApiTypeInfo type)
|
||
|
|
{
|
||
|
|
var name = GetRouteTypeName(type);
|
||
|
|
return name.Replace('.', '_').Replace('<', '_').Replace('>', '_').Replace(',', '_').Replace(' ', '_');
|
||
|
|
}
|
||
|
|
|
||
|
|
static string SanitizeId(string name)
|
||
|
|
{
|
||
|
|
return name.Replace(' ', '-').Replace('<', '_').Replace('>', '_').Replace(',', '_').Replace('(', '_').Replace(')', '_');
|
||
|
|
}
|
||
|
|
|
||
|
|
static string MemberKindToSectionId(MemberKind kind) => kind switch
|
||
|
|
{
|
||
|
|
MemberKind.Constructor => "constructors",
|
||
|
|
MemberKind.Field => "fields",
|
||
|
|
MemberKind.Property => "properties",
|
||
|
|
MemberKind.Method => "methods",
|
||
|
|
MemberKind.Event => "events",
|
||
|
|
MemberKind.Operator => "operators",
|
||
|
|
_ => "properties"
|
||
|
|
};
|
||
|
|
|
||
|
|
static readonly Dictionary<string, string> BuiltInTypeMap = new(StringComparer.Ordinal)
|
||
|
|
{
|
||
|
|
["System.String"] = "string",
|
||
|
|
["System.Boolean"] = "bool",
|
||
|
|
["System.Int32"] = "int",
|
||
|
|
["System.Int64"] = "long",
|
||
|
|
["System.Int16"] = "short",
|
||
|
|
["System.Byte"] = "byte",
|
||
|
|
["System.SByte"] = "sbyte",
|
||
|
|
["System.UInt32"] = "uint",
|
||
|
|
["System.UInt64"] = "ulong",
|
||
|
|
["System.UInt16"] = "ushort",
|
||
|
|
["System.Single"] = "float",
|
||
|
|
["System.Double"] = "double",
|
||
|
|
["System.Decimal"] = "decimal",
|
||
|
|
["System.Char"] = "char",
|
||
|
|
["System.Object"] = "object",
|
||
|
|
["System.Void"] = "void",
|
||
|
|
["Void"] = "void",
|
||
|
|
};
|
||
|
|
|
||
|
|
static readonly string[] StripPrefixes = ["System.Collections.Generic.", "System.Collections.ObjectModel.", "System.Threading.Tasks.", "System.Linq.", "System.Linq.Expressions.", "Microsoft.AspNetCore.Components.Web.", "Microsoft.AspNetCore.Components.", "Radzen.Blazor.", "Radzen.", "System."];
|
||
|
|
|
||
|
|
static string SimplifyTypeName(string typeName)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(typeName)) return typeName;
|
||
|
|
|
||
|
|
if (BuiltInTypeMap.TryGetValue(typeName, out var builtIn))
|
||
|
|
return builtIn;
|
||
|
|
|
||
|
|
var result = HandleNullable(typeName);
|
||
|
|
result = StripNamespacePrefixes(result);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string HandleNullable(string typeName)
|
||
|
|
{
|
||
|
|
const string nullablePrefix = "System.Nullable<";
|
||
|
|
if (typeName.StartsWith(nullablePrefix, StringComparison.Ordinal) && typeName.EndsWith('>'))
|
||
|
|
{
|
||
|
|
var inner = typeName[nullablePrefix.Length..^1];
|
||
|
|
return SimplifyTypeName(inner) + "?";
|
||
|
|
}
|
||
|
|
return typeName;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string StripNamespacePrefixes(string typeName)
|
||
|
|
{
|
||
|
|
var result = new StringBuilder();
|
||
|
|
int i = 0;
|
||
|
|
while (i < typeName.Length)
|
||
|
|
{
|
||
|
|
if (typeName[i] == '<')
|
||
|
|
{
|
||
|
|
result.Append('<');
|
||
|
|
i++;
|
||
|
|
int depth = 1;
|
||
|
|
int segStart = i;
|
||
|
|
var segments = new List<string>();
|
||
|
|
while (i < typeName.Length && depth > 0)
|
||
|
|
{
|
||
|
|
if (typeName[i] == '<') depth++;
|
||
|
|
else if (typeName[i] == '>') { depth--; if (depth == 0) break; }
|
||
|
|
else if (typeName[i] == ',' && depth == 1)
|
||
|
|
{
|
||
|
|
segments.Add(typeName[segStart..i].Trim());
|
||
|
|
segStart = i + 1;
|
||
|
|
}
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
segments.Add(typeName[segStart..i].Trim());
|
||
|
|
result.Append(string.Join(", ", segments.Select(SimplifyTypeName)));
|
||
|
|
if (i < typeName.Length && typeName[i] == '>')
|
||
|
|
{
|
||
|
|
result.Append('>');
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
result.Append(typeName[i]);
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
|
||
|
|
var simplified = result.ToString();
|
||
|
|
foreach (var prefix in StripPrefixes)
|
||
|
|
{
|
||
|
|
if (simplified.StartsWith(prefix, StringComparison.Ordinal))
|
||
|
|
{
|
||
|
|
simplified = simplified[prefix.Length..];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return simplified;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string SimplifySignature(string signature)
|
||
|
|
{
|
||
|
|
var result = new StringBuilder();
|
||
|
|
var tokens = TokenizeSignature(signature);
|
||
|
|
foreach (var token in tokens)
|
||
|
|
{
|
||
|
|
if (token.IsType)
|
||
|
|
result.Append(SimplifyTypeName(token.Text));
|
||
|
|
else
|
||
|
|
result.Append(token.Text);
|
||
|
|
}
|
||
|
|
return result.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
static List<(string Text, bool IsType)> TokenizeSignature(string sig)
|
||
|
|
{
|
||
|
|
var tokens = new List<(string Text, bool IsType)>();
|
||
|
|
var separators = new HashSet<char> { ' ', '(', ')', '{', '}', ';', ',' };
|
||
|
|
var keywords = new HashSet<string>(StringComparer.Ordinal)
|
||
|
|
{
|
||
|
|
"public", "protected", "private", "internal", "static", "virtual", "override",
|
||
|
|
"abstract", "sealed", "readonly", "new", "event", "class", "struct", "interface",
|
||
|
|
"enum", "get", "set", "operator", "implicit", "explicit"
|
||
|
|
};
|
||
|
|
|
||
|
|
int i = 0;
|
||
|
|
while (i < sig.Length)
|
||
|
|
{
|
||
|
|
if (separators.Contains(sig[i]))
|
||
|
|
{
|
||
|
|
tokens.Add((sig[i].ToString(), false));
|
||
|
|
i++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
int start = i;
|
||
|
|
int depth = 0;
|
||
|
|
while (i < sig.Length && (!separators.Contains(sig[i]) || depth > 0))
|
||
|
|
{
|
||
|
|
if (sig[i] == '<') depth++;
|
||
|
|
else if (sig[i] == '>') depth--;
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
|
||
|
|
var word = sig[start..i];
|
||
|
|
bool isType = !keywords.Contains(word) && (word.Contains('.', StringComparison.Ordinal) || word.Contains('<', StringComparison.Ordinal) ||
|
||
|
|
char.IsUpper(word[0]) && !keywords.Contains(word));
|
||
|
|
tokens.Add((word, isType));
|
||
|
|
}
|
||
|
|
|
||
|
|
return tokens;
|
||
|
|
}
|
||
|
|
|
||
|
|
static string EscapeHtml(string text)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(text)) return "";
|
||
|
|
return text
|
||
|
|
.Replace("&", "&", StringComparison.Ordinal)
|
||
|
|
.Replace("<", "<", StringComparison.Ordinal)
|
||
|
|
.Replace(">", ">", StringComparison.Ordinal)
|
||
|
|
.Replace("\"", """, StringComparison.Ordinal)
|
||
|
|
.Replace("@", "@@", StringComparison.Ordinal);
|
||
|
|
}
|
||
|
|
|
||
|
|
void WritePage(string path, string content)
|
||
|
|
{
|
||
|
|
File.WriteAllText(path, content, Encoding.UTF8);
|
||
|
|
_pageCount++;
|
||
|
|
}
|
||
|
|
}
|