Files
radzen-blazor/Radzen.Blazor.Api.Generator/Program.cs
Vladimir Enchev 534be03af0 v10 (#2471)
* Chip and ChipList components added

Code improved

demos improved

Update Chip styles

Exctract mixins for button, badge and chip styles

Update Chip demos

Update Chip styles in themes

Fix chip styles in dropdowns

Fix icon position in different chip sizes

Fix chip border-radius

Optimize styles

Update Chip icon

Update premium themes

Chip/ChipList keyboard navigation fixed

Invalid code fixed

Update Chip focus styles

* Gantt added

code improved

demo update with more data

more demo improvements

more improvements

rendering updated

Gantt Splitter ChangeStateOnResize set to false

Gantt connecting lines added

Tooltips added

FIltering demo added

Gantt editing demos added

demo updated

scrollbar improved

Gantt ExpandRows() added

thead height made equal

material fixed

filtering improved

Subtitle2 removed

Gantt drag & resize added

Critical Path, Dependency Types and Baselines added

Today Line, Non-Working Days, TaskRender Callback, Vertical Date Markers and Custom Task Bar Template added

Zoom to fit and multiline headers added

Rendering optimized

Grouped header should be static

Invalidate row count on expand/collapse and filter

progress, critical path, red vertical line and baseline styling improved

frozen columns in demos should have Width set

alignment improved

Auto scroll to events when switching between views

Dependency Data added

* Move scheduler month view "more" button to slot header

The "+X more" button was absolutely positioned inside the events area,
occupying space that could hold an additional appointment. Move it inline
into the slot title header next to the day number so it no longer
consumes an event slot.

- Fix operator precedence bug in MaxAppointmentsInSlot auto-calculation
- Style the button end-aligned in the header using flex + margin-inline-start: auto
- Add demo appointments to trigger the "more" button in the default demo

* Use ResizeObserver to measure month view content height

Replace the hardcoded (Height - 60) / 6 calculation with actual
measurements from a ResizeObserver on the view content element. This
fixes appointment overflow at non-default scheduler heights (e.g. 500px)
where the 60px header estimate was inaccurate (~91px actual).

The MonthView now uses the existing createResizable JS pattern to
observe its own content area and dynamically computes the correct
MaxAppointmentsInSlot from the measured height and actual week count.

* Replace createScheduler with createResizable for ResizeObserver support

The scheduler-specific JS functions only used window resize events.
Switch to the shared createResizable/destroyResizable pattern which
uses ResizeObserver with a window resize fallback, consistent with
charts and gauges.

* Replace DocFX with Blazor class library for API reference generation

Add Radzen.Blazor.Api and Radzen.Blazor.Api.Generator projects that
generate .razor pages from Radzen.Blazor assembly metadata at build time.
Pages are generated conditionally (Release builds or -p:GenerateApiPages=true).
The API assembly is lazy-loaded to avoid initial payload penalty. Old .html
URLs are preserved via 301 redirects in CanonicalRedirectMiddleware.

* Improve API reference navigation and member anchors

- Add anchor links to member headings via RadzenText Anchor parameter
- Fix inherited member links to navigate to the specific member, not just the declaring type
- Add nav panel that expands/selects the current type on navigation
- Fix RadzenBody to scroll to fragment target instead of resetting to top

* Stop RadzenToc from updating URL hash on scroll and fix scroll container

- Remove navigateTo call from scroll handler so URL hash is not overwritten
- Set TOC Selector to .rz-body so it monitors the correct scrollable container

* Align API layout header with MainLayout

* Refactor RadzenToc scroll listener disposal and item link updates.

Return a disposable JS listener handle instead of storing expando handlers on elements, and move TOC item href refresh to NavigationManager location-change handling so links stay in sync with path/query changes.

* Add v10 RadzenToc changelog notes.

Document the RadzenToc breaking changes around link rendering, aria-selected semantics, and address bar behavior during scrolling, plus the high-level scroll tracking update.

* Fix RadzenToc NullReferenceException during concurrent disposal and API reference redirect loop.

Capture scrollListenerRef in a local variable before disposing to prevent a race condition when multiple RadzenTocItem components dispose concurrently. Skip scroll listener re-registration when the parent RadzenToc is already disposed. Remove UseDefaultFiles middleware and fix API Reference link to prevent redirect loops.

* Fix RadzenToc scrollspy to match Bootstrap ScrollSpy behavior.

Remove fallback that always highlighted the first item on load. Require
actual scroll (scrollY > 0) before activating the at-bottom branch so
short pages don't highlight the last item. Add click handler to
RadzenTocItem so clicking an item activates it immediately (matching the
DocFX affix click behavior).

* Fix misaligned code examples in generated API documentation

XML doc comment code blocks retained source-level indentation after
parsing, causing excessive leading whitespace in rendered examples.
Replace simple .Trim() with indentation normalization that strips
the common leading whitespace prefix from all lines.

* Update ApiLayout and MainTopNav

* Fixed .Server project Cannot provide a value for property 'AssemblyLoader' on type 'RadzenBlazorDemos.Routes'.

* Scatter and Bubble series added to Chart

* LinearGauge added

* Popup cut off on rightmost columns and flickers

Fix #2414, #2181

* CheckBoxList filter options update without clicking Apply

Fix #2155

* tests updated

* DataGrid Reload() lags one render behind

Fix #2214

* Gaps in Data for Charts

Fix #765

* DataGrid Fixed Column resizing will show visual white space between fixed columns

Fix #2228

* Setting Parameter on RadzenDataGridColumn<T> does not always trigger render of content

Fix #2215

* DataGrid column resizing does not work on mobile and tablet devices

Fix #2168

* RadzenAccordion invalid ARIA aatributes

Fix #1931

* DropDownDataGrid search text clear button added

Fix #1555

* Caruosel AnimationDuration added

Fix #2438

* Tooltip ARIA attributes improved

Fix #2314

* Long tooltip does not wrap

Fix #2320

* Clicking into Padding of PanelMenuItem does navigate but not expand Item

Fix #2244

* RadzenDatePicker FocusedDate synced with current date

Fix #1991

* Dialog OnSideClose gets called when opening and closing

Fix #1976

* RadzenNumeric RemoveNonNumericCharacters improved

Fix #1937

* Optimize CSS rules and SCSS mixins

* Fix percentage values in SCSS

* Update premium themes

* Line Chart markers cannot be dynamically turned off using @if

Fix #1229

* DropDownDataGrid exception when changing data object

Fix #1941

* RadzenChat new messages button added

Fix #2375

* RadzenHtmlEditor Immediate property added

Fix #2279

* Radzen Popup shows wrongly in a RTL

Fix #2271

* RadzenListBox/RadzenPickList EmptyTemplate and EmptyText added

Fix #2018

* DataGrid footer and header is being incorrectly rendered for frozen columns

Fix #1402

* PropertyAccess.GetValue() fixed to work with sub interfaces

Fix #1249

* RadzenPickList AllowVirtualization added

Fix #2456

* DataGrid add new row issue with LoadData method

Fix #365

* Various components keyboard navigation not working as described in the demos

Fix #2446

* RadzenDataGrid Keyboard Selection jumps when AllowVirtualization is enabled

Fix #2439

* RadzenPanelMenu MenuItemDisplayStyle.IconAndTextStacked added

Fix #2368

* RadzenPickList SelectedSourceChanged/SelectedTargetChanged  added

Fix #2324

* RadzenTabs AllowReorder property added

Fix #2318

* RadzenAccordion ExpandAll()/CollapseAll() added

Fix #2206

* RadzenDropZoneContainer Start/End Drag events added

Fix #2158

* RadzenDataGrid doesn't appear to do colspan/rowspan properly

Fix #1064

* Update Gantt styles

* Update Gantt styles and premium themes

* Update Gantt styles

* Update Gantt demos

* Update Linear Gauge demos

* Update component icons

* tests fixed

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2026-03-11 16:13:33 +02:00

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("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("@", "@@", StringComparison.Ordinal);
}
void WritePage(string path, string content)
{
File.WriteAllText(path, content, Encoding.UTF8);
_pageCount++;
}
}