using System.Diagnostics; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace RadzenBlazorDemos.Tools; class Program { const string BaseUrl = "https://blazor.radzen.com"; static readonly HashSet OptionalCategories = new(StringComparer.OrdinalIgnoreCase) { "UI Fundamentals", "Images", }; static readonly HashSet OrganizationalCategories = new(StringComparer.OrdinalIgnoreCase) { "Data", "Layout", "Navigation", "Forms", "Data Visualization", "Feedback", "Validators", }; static readonly HashSet ExcludedTopLevel = new(StringComparer.OrdinalIgnoreCase) { "Overview", "Get Started", "AI", "Support", "Accessibility", "UI Blocks", "App Templates", "Changelog", }; static readonly HashSet ExcludedPages = new(StringComparer.OrdinalIgnoreCase) { "AccessibilityPage", "AI", "Changelog", "Dashboard", "DashboardPage", "GetStarted", "Index", "NotFound", "Playground", "SupportPage", "ThemeServicePage", "ThemesPage", }; static readonly string[] ExcludedPrefixes = ["Templates", "UIBlocks"]; static readonly HashSet LifecycleMethodNames = new(StringComparer.Ordinal) { "OnInitialized", "OnInitializedAsync", "OnParametersSet", "OnParametersSetAsync", "OnAfterRender", "OnAfterRenderAsync", "SetParametersAsync", "BuildRenderTree", "ShouldRender", "Dispose", "DisposeAsync", }; static bool IsExcluded(string filePath) { var name = Path.GetFileNameWithoutExtension(filePath); if (ExcludedPages.Contains(name)) return true; foreach (var prefix in ExcludedPrefixes) { if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return true; } return false; } static int Main(string[] args) { if (args.Length < 3) { Console.Error.WriteLine("Usage: RadzenBlazorDemos.Tools [xmlDocPath] [sourceDir]"); return 1; } var outputDir = args[0]; var pagesPath = args[1]; var exampleServicePath = args[2]; var xmlDocPath = args.Length > 3 && !string.IsNullOrWhiteSpace(args[3]) ? args[3] : null; var inferredSourceDir = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(exampleServicePath) ?? "", "..", "..", "Radzen.Blazor")); var sourceDir = args.Length > 4 && !string.IsNullOrWhiteSpace(args[4]) ? args[4] : inferredSourceDir; if (!Directory.Exists(pagesPath)) { Console.Error.WriteLine($"Pages path does not exist: {pagesPath}"); return 1; } if (!File.Exists(exampleServicePath)) { Console.Error.WriteLine($"ExampleService.cs not found: {exampleServicePath}"); return 1; } try { var categories = ParseExampleService(exampleServicePath); var xmlDocs = xmlDocPath != null && File.Exists(xmlDocPath) ? ParseXmlDocs(xmlDocPath) : new Dictionary(); var typeMap = Directory.Exists(sourceDir) ? BuildComponentTypeMap(sourceDir) : new Dictionary(StringComparer.Ordinal); Directory.CreateDirectory(outputDir); var mdDir = Path.Combine(outputDir, "md"); Directory.CreateDirectory(mdDir); GenerateComponentPages(categories, pagesPath, xmlDocs, typeMap, mdDir); GenerateIndex(categories, Path.Combine(outputDir, "llms.txt"), xmlDocs); GenerateHomePage(pagesPath, Path.Combine(mdDir, "index.md")); GenerateSitemap(categories, pagesPath, outputDir); var apiCount = Directory.Exists(Path.Combine(mdDir, "api")) ? Directory.GetFiles(Path.Combine(mdDir, "api"), "*.md").Length : 0; var pageCount = Directory.GetFiles(mdDir, "*.md").Length; Console.WriteLine($"Generated llms.txt, {pageCount} component pages, and {apiCount} API reference files in: {outputDir}"); return 0; } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); Console.Error.WriteLine(ex.StackTrace); return 1; } } // ── Data model ────────────────────────────────────────────────────── record ExampleNode(string Name, string Path, string Description, List Children, List Tags); record XmlMemberDoc(string Summary, string TypeName, string Value, string Example, string Remarks); record ParameterDoc(string Name, string Type, string Summary, string ValueDescription, bool IsEvent); record PropertyTypeInfo(string Name, string Type, string XmlPrefix); record MethodTypeInfo(string Name, string ReturnType, List<(string Type, string Name)> Parameters, string XmlPrefix); record ComponentTypeInfo(string ClassName, string BaseClassName, string XmlPrefix, List Properties, List Methods); // ── ExampleService.cs parsing via Roslyn ──────────────────────────── static List ParseExampleService(string filePath) { var source = File.ReadAllText(filePath, Encoding.UTF8); var tree = CSharpSyntaxTree.ParseText(source); var root = tree.GetCompilationUnitRoot(); var fieldDecl = root.DescendantNodes() .OfType() .FirstOrDefault(f => f.Declaration.Variables.Any(v => v.Identifier.Text == "allExamples")); if (fieldDecl == null) throw new InvalidOperationException("Could not find 'allExamples' field in ExampleService.cs"); var initializer = fieldDecl.Declaration.Variables.First().Initializer?.Value; if (initializer is not ImplicitArrayCreationExpressionSyntax arrayExpr) throw new InvalidOperationException("allExamples is not an implicit array creation expression"); return ParseExampleArray(arrayExpr.Initializer); } static List ParseExampleArray(InitializerExpressionSyntax initializer) { var results = new List(); foreach (var expr in initializer.Expressions) { if (expr is ObjectCreationExpressionSyntax objCreate && objCreate.Initializer != null) { results.Add(ParseSingleExample(objCreate.Initializer)); } else if (expr is ImplicitObjectCreationExpressionSyntax implicitCreate && implicitCreate.Initializer != null) { results.Add(ParseSingleExample(implicitCreate.Initializer)); } } return results; } static ExampleNode ParseSingleExample(InitializerExpressionSyntax init) { string name = ""; string path = ""; string description = ""; List children = null; List tags = null; foreach (var assignment in init.Expressions.OfType()) { var propName = assignment.Left.ToString(); var value = assignment.Right; switch (propName) { case "Name": name = ExtractStringLiteral(value); break; case "Path": path = ExtractStringLiteral(value); break; case "Description": description = ExtractStringLiteral(value); break; case "Children": children = ParseChildrenExpression(value); break; case "Tags": tags = ParseTagsExpression(value); break; } } return new ExampleNode(name, path, description, children, tags); } static string ExtractStringLiteral(ExpressionSyntax expr) { if (expr is LiteralExpressionSyntax literal && literal.Token.IsKind(SyntaxKind.StringLiteralToken)) return literal.Token.ValueText; return expr.ToString().Trim('"'); } static List ParseChildrenExpression(ExpressionSyntax expr) { if (expr is ImplicitArrayCreationExpressionSyntax implicitArray) return ParseExampleArray(implicitArray.Initializer); if (expr is ArrayCreationExpressionSyntax arrayCreate && arrayCreate.Initializer != null) return ParseExampleArray(arrayCreate.Initializer); return null; } static List ParseTagsExpression(ExpressionSyntax expr) { IEnumerable elements = expr switch { ImplicitArrayCreationExpressionSyntax implicitArray => implicitArray.Initializer.Expressions, ArrayCreationExpressionSyntax arrayCreate when arrayCreate.Initializer != null => arrayCreate.Initializer.Expressions, CollectionExpressionSyntax collection => collection.Elements .OfType() .Select(e => e.Expression), _ => null }; if (elements == null) return null; return elements.Select(ExtractStringLiteral).Where(s => !string.IsNullOrEmpty(s)).ToList(); } // ── XML documentation parsing ─────────────────────────────────────── static Dictionary ParseXmlDocs(string xmlPath) { var docs = new Dictionary(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")?.Value); var value = CleanXmlDocText(member.Element("value")?.Value); var example = DecodeXmlCodeBlocks(member.Element("example")); var remarks = CleanXmlDocText(member.Element("remarks")?.Value); docs[memberName] = new XmlMemberDoc(summary, memberName, value, example, remarks); } return docs; } static string CleanXmlDocText(string text) { if (string.IsNullOrWhiteSpace(text)) return ""; var result = Regex.Replace(text, @"\s+", " ").Trim(); return result; } static string DecodeXmlCodeBlocks(XElement exampleElement) { if (exampleElement == null) return ""; var sb = new StringBuilder(); foreach (var node in exampleElement.Nodes()) { if (node is XElement element && element.Name.LocalName.Equals("code", StringComparison.OrdinalIgnoreCase)) { var code = WebUtility.HtmlDecode(element.Value).Trim(); if (string.IsNullOrWhiteSpace(code)) continue; if (sb.Length > 0) sb.AppendLine(); sb.AppendLine("```razor"); sb.AppendLine(code); sb.AppendLine("```"); continue; } var text = node is XText textNode ? textNode.Value : Regex.Replace(node.ToString(SaveOptions.DisableFormatting), "<[^>]+>", " "); var prose = CleanXmlDocText(WebUtility.HtmlDecode(text)); if (!string.IsNullOrWhiteSpace(prose)) { if (sb.Length > 0) sb.AppendLine(); sb.AppendLine(prose); } } return sb.ToString().Trim(); } // ── Component source parsing via Roslyn ───────────────────────────── static Dictionary BuildComponentTypeMap(string radzenBlazorDir) { var map = new Dictionary(StringComparer.Ordinal); var files = Directory.GetFiles(radzenBlazorDir, "*.razor.cs", SearchOption.AllDirectories) .Concat(Directory.GetFiles(radzenBlazorDir, "*.cs", SearchOption.AllDirectories)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var file in files) { var source = File.ReadAllText(file, Encoding.UTF8); var tree = CSharpSyntaxTree.ParseText(source); var root = tree.GetCompilationUnitRoot(); foreach (var classDecl in root.DescendantNodes().OfType()) { var className = ToMetadataClassName(classDecl); if (string.IsNullOrEmpty(className)) continue; var ns = GetNamespace(classDecl); var xmlPrefix = string.IsNullOrEmpty(ns) ? className : $"{ns}.{className}"; var baseClassName = ResolveBaseClassName(classDecl); var properties = classDecl.Members .OfType() .Where(HasParameterAttribute) .Select(p => new PropertyTypeInfo( p.Identifier.Text, NormalizeTypeText(p.Type.ToString()), xmlPrefix)) .ToList(); var methods = classDecl.Members .OfType() .Where(IsPublicApiMethod) .Select(m => new MethodTypeInfo( m.Identifier.Text, NormalizeTypeText(m.ReturnType.ToString()), m.ParameterList.Parameters .Select(p => ( NormalizeTypeText(p.Type?.ToString() ?? "object"), p.Identifier.Text)) .ToList(), xmlPrefix)) .ToList(); if (map.TryGetValue(className, out var existing)) { var mergedProps = existing.Properties.Concat(properties).ToList(); var mergedMethods = existing.Methods.Concat(methods).ToList(); map[className] = new ComponentTypeInfo(className, existing.BaseClassName ?? baseClassName, existing.XmlPrefix ?? xmlPrefix, mergedProps, mergedMethods); } else { map[className] = new ComponentTypeInfo(className, baseClassName, xmlPrefix, properties, methods); } } } return map; } static bool HasParameterAttribute(PropertyDeclarationSyntax property) => property.AttributeLists .SelectMany(a => a.Attributes) .Any(a => { var name = a.Name.ToString(); return name == "Parameter" || name.EndsWith(".Parameter", StringComparison.Ordinal); }); static bool IsPublicApiMethod(MethodDeclarationSyntax method) { if (!method.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword))) return false; if (method.Modifiers.Any(m => m.IsKind(SyntaxKind.OverrideKeyword))) return false; if (LifecycleMethodNames.Contains(method.Identifier.Text)) return false; return true; } static string ResolveBaseClassName(ClassDeclarationSyntax classDecl) { if (classDecl.BaseList == null) return null; foreach (var baseType in classDecl.BaseList.Types) { var name = ToMetadataTypeName(baseType.Type); if (string.IsNullOrWhiteSpace(name)) continue; if (name.StartsWith("I", StringComparison.Ordinal) && name.Length > 1 && char.IsUpper(name[1]) && !name.Contains('`')) continue; return name; } return null; } static string ToMetadataClassName(ClassDeclarationSyntax classDecl) { var arity = classDecl.TypeParameterList?.Parameters.Count ?? 0; return arity > 0 ? $"{classDecl.Identifier.Text}`{arity}" : classDecl.Identifier.Text; } static string ToMetadataTypeName(TypeSyntax typeSyntax) { return typeSyntax switch { IdentifierNameSyntax id => id.Identifier.Text, GenericNameSyntax generic => $"{generic.Identifier.Text}`{generic.TypeArgumentList.Arguments.Count}", QualifiedNameSyntax qualified => ToMetadataTypeName(qualified.Right), AliasQualifiedNameSyntax aliasQualified => ToMetadataTypeName(aliasQualified.Name), NullableTypeSyntax nullable => ToMetadataTypeName(nullable.ElementType), _ => NormalizeTypeText(typeSyntax.ToString()) }; } static string NormalizeTypeText(string type) => Regex.Replace(type ?? "", @"\s+", " ").Trim(); static string GetNamespace(SyntaxNode node) { var parent = node.Parent; while (parent != null) { if (parent is FileScopedNamespaceDeclarationSyntax fileNs) return fileNs.Name.ToString(); if (parent is NamespaceDeclarationSyntax ns) return ns.Name.ToString(); parent = parent.Parent; } return ""; } // ── Inheritance resolution ─────────────────────────────────────────── static List ResolveFullPropertyList(string componentClassName, Dictionary typeMap) { var result = new List(); var seen = new HashSet(StringComparer.Ordinal); var current = componentClassName; var visited = new HashSet(StringComparer.Ordinal); while (!string.IsNullOrWhiteSpace(current) && visited.Add(current)) { if (!typeMap.TryGetValue(current, out var info)) break; foreach (var prop in info.Properties) { if (seen.Add(prop.Name)) result.Add(prop); } current = info.BaseClassName; } return result; } static List ResolveComponentMethods(string componentClassName, Dictionary typeMap) { if (!typeMap.TryGetValue(componentClassName, out var info)) return []; return info.Methods; } // ── Parameter/event extraction with types ─────────────────────────── static List GetComponentParameters( string componentClassName, Dictionary xmlDocs, Dictionary typeMap) { var fullProps = ResolveFullPropertyList(componentClassName, typeMap); var seen = new HashSet(StringComparer.Ordinal); var parameters = new List(); foreach (var prop in fullProps) { if (!seen.Add(prop.Name)) continue; var xmlKey = $"P:{prop.XmlPrefix}.{prop.Name}"; xmlDocs.TryGetValue(xmlKey, out var xmlDoc); var summary = xmlDoc?.Summary ?? ""; var valueDesc = xmlDoc?.Value ?? ""; var isEvent = prop.Type.StartsWith("EventCallback", StringComparison.Ordinal); parameters.Add(new ParameterDoc(prop.Name, prop.Type, summary, valueDesc, isEvent)); } return parameters.OrderBy(p => p.IsEvent).ThenBy(p => p.Name, StringComparer.Ordinal).ToList(); } // ── Component name mapping ────────────────────────────────────────── static string MapToComponentClass(string exampleName) { var cleaned = exampleName.Replace(" ", "").Replace("-", ""); return $"Radzen{cleaned}"; } static string ResolveComponentClass(string exampleName, Dictionary xmlDocs) { var className = MapToComponentClass(exampleName); if (HasTypeOrProperties(className, xmlDocs)) return className; for (int arity = 1; arity <= 3; arity++) { var generic = $"{className}`{arity}"; if (HasTypeOrProperties(generic, xmlDocs)) return generic; } return null; } static bool HasTypeOrProperties(string className, Dictionary xmlDocs) { if (xmlDocs.ContainsKey($"T:Radzen.Blazor.{className}")) return true; var propPrefix = $"P:Radzen.Blazor.{className}."; return xmlDocs.Keys.Any(k => k.StartsWith(propPrefix)); } static (string ResolvedClass, string ParentDisplayName) ResolveComponentForNode( ExampleNode node, List ancestors, Dictionary xmlDocs) { var resolved = ResolveComponentClass(node.Name, xmlDocs); if (resolved != null) return (resolved, null); for (int i = ancestors.Count - 1; i >= 0; i--) { if (i == 0 && OrganizationalCategories.Contains(ancestors[i])) continue; resolved = ResolveComponentClass(ancestors[i], xmlDocs); if (resolved != null) return (resolved, ancestors[i]); } return (null, null); } // ── API reference slug helpers ────────────────────────────────────── static string ComponentSlug(string metadataClassName) { var name = metadataClassName; var tick = name.IndexOf('`'); if (tick >= 0) name = name[..tick]; if (name.StartsWith("Radzen", StringComparison.Ordinal)) name = name["Radzen".Length..]; return name.ToLowerInvariant(); } static string ComponentDisplayName(string metadataClassName) { var name = metadataClassName; var tick = name.IndexOf('`'); if (tick >= 0) name = name[..tick]; return name; } // ── API reference generation ──────────────────────────────────────── static string GenerateApiReferencePage( string componentClassName, Dictionary xmlDocs, Dictionary typeMap) { var sb = new StringBuilder(); var displayName = ComponentDisplayName(componentClassName); sb.AppendLine($"# {displayName} API Reference"); sb.AppendLine(); var parameters = GetComponentParameters(componentClassName, xmlDocs, typeMap); var regularParams = parameters.Where(p => !p.IsEvent).ToList(); var eventParams = parameters.Where(p => p.IsEvent).ToList(); if (regularParams.Count > 0) { sb.AppendLine("## Parameters"); sb.AppendLine(); sb.AppendLine("| Parameter | Type | Description |"); sb.AppendLine("|-----------|------|-------------|"); foreach (var p in regularParams) { var type = EscapePipe(p.Type); var desc = EscapePipe(p.Summary); sb.AppendLine($"| {p.Name} | `{type}` | {desc} |"); } sb.AppendLine(); } if (eventParams.Count > 0) { sb.AppendLine("## Events"); sb.AppendLine(); sb.AppendLine("| Event | Type | Description |"); sb.AppendLine("|-------|------|-------------|"); foreach (var p in eventParams) { var type = EscapePipe(p.Type); var desc = EscapePipe(p.Summary); sb.AppendLine($"| {p.Name} | `{type}` | {desc} |"); } sb.AppendLine(); } var methods = ResolveComponentMethods(componentClassName, typeMap); if (methods.Count > 0) { var methodDocs = new List<(string Signature, string Returns, string Description)>(); foreach (var m in methods.OrderBy(m => m.Name, StringComparer.Ordinal)) { var paramList = string.Join(", ", m.Parameters.Select(p => $"{p.Type} {p.Name}")); var signature = $"{m.Name}({paramList})"; var xmlMethodSummary = FindMethodXmlSummary(componentClassName, m, xmlDocs); methodDocs.Add((signature, m.ReturnType, xmlMethodSummary)); } if (methodDocs.Count > 0) { sb.AppendLine("## Methods"); sb.AppendLine(); sb.AppendLine("| Method | Returns | Description |"); sb.AppendLine("|--------|---------|-------------|"); foreach (var (sig, ret, desc) in methodDocs) { sb.AppendLine($"| {EscapePipe(sig)} | `{EscapePipe(ret)}` | {EscapePipe(desc)} |"); } sb.AppendLine(); } } return sb.ToString(); } static string FindMethodXmlSummary(string componentClassName, MethodTypeInfo method, Dictionary xmlDocs) { var prefix = $"M:{method.XmlPrefix}.{method.Name}"; foreach (var (key, doc) in xmlDocs) { if (key.StartsWith(prefix, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(doc.Summary)) return doc.Summary; } return ""; } static string EscapePipe(string text) => (text ?? "").Replace("|", "\\|").Replace("\n", " ").Replace("\r", ""); record LinkInfo(string Name, string Url, string Description, string ParentComponentName); static string FormatLink(LinkInfo link) { var displayName = !string.IsNullOrEmpty(link.ParentComponentName) ? $"{link.ParentComponentName}: {link.Name}" : link.Name; var desc = !string.IsNullOrWhiteSpace(link.Description) ? $": {TrimDescription(link.Description)}" : ""; return $"- [{displayName}]({link.Url}){desc}"; } // ── Index generation (llms.txt) ───────────────────────────────────── static void GenerateIndex(List categories, string outputPath, Dictionary xmlDocs) { var sb = new StringBuilder(); sb.AppendLine("# Radzen Blazor Components"); sb.AppendLine(); sb.AppendLine("> The most comprehensive free UI component library for Blazor — 110+ native components with Material 3, Material 2, Fluent, and Bootstrap design systems. MIT licensed. The only Blazor component library with a companion visual IDE (Radzen Blazor Studio) and an MCP server for AI-assisted development."); sb.AppendLine(); sb.AppendLine("Written entirely in C# with no JavaScript framework dependencies. Supports Blazor Server, Blazor WebAssembly, .NET MAUI Blazor Hybrid, and the Blazor Web App model in .NET 10. Built with accessibility in mind (WCAG 2.2, keyboard navigation). Used at Microsoft, NASA, Porsche, Dell, Siemens, and DHL."); sb.AppendLine(); sb.AppendLine("**Companion tools:** Radzen Blazor Studio — visual IDE with WYSIWYG designer and database scaffolding (https://www.radzen.com/blazor-studio). Radzen Blazor MCP Server — Model Context Protocol server for AI-assisted Blazor development, works with VS Code, Visual Studio, Cursor, and other MCP-capable IDEs (https://blazor.radzen.com/ai). Free Community edition available. Blazor Pro: $799/year (https://www.radzen.com/pricing)."); sb.AppendLine(); sb.AppendLine("## Quick start"); sb.AppendLine(); sb.AppendLine("```bash"); sb.AppendLine("dotnet add package Radzen.Blazor"); sb.AppendLine("```"); sb.AppendLine(); sb.AppendLine("Add `` to `App.razor` ``, `` after the last `