diff --git a/BootstrapBlazor.Extensions.slnx b/BootstrapBlazor.Extensions.slnx index e51d5906..21620bd9 100644 --- a/BootstrapBlazor.Extensions.slnx +++ b/BootstrapBlazor.Extensions.slnx @@ -124,5 +124,6 @@ + diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/BootstrapBlazor.LLMsDocsGenerator.csproj b/tools/BootstrapBlazor.LLMsDocsGenerator/BootstrapBlazor.LLMsDocsGenerator.csproj new file mode 100644 index 00000000..61893ece --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/BootstrapBlazor.LLMsDocsGenerator.csproj @@ -0,0 +1,30 @@ + + + + 10.0.0 + Exe + net10.0 + enable + enable + + + + BootstrapBlazor LLMs docs Generator + BootstrapBlazor.LLMsDocsGenerator + true + llms-docs + ice6 (ice6@live.cn) + Copyright 2026 + BootstrapBlazor + + + + + + + + + + + + diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs b/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs new file mode 100644 index 00000000..a157d6a1 --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Text.RegularExpressions; + +namespace LlmsDocsGenerator; + +/// +/// Analyzes Blazor component source files using Roslyn +/// +public partial class ComponentAnalyzer +{ + private readonly string _sourcePath; + private readonly string _componentsPath; + private readonly string _samplesPath; + + public ComponentAnalyzer(string sourcePath) + { + _sourcePath = sourcePath; + _componentsPath = Path.Combine(sourcePath, "Components"); + _samplesPath = Path.Combine(Path.GetDirectoryName(sourcePath)!, "BootstrapBlazor.Server", "Components", "Samples"); + } + + /// + /// Analyze all components in the source directory + /// + public async Task> AnalyzeAllComponentsAsync() + { + var components = new List(); + + if (!Directory.Exists(_componentsPath)) + { + Console.WriteLine($"Components directory not found: {_componentsPath}"); + return components; + } + + // Find all .razor.cs files + var files = Directory.GetFiles(_componentsPath, "*.razor.cs", SearchOption.AllDirectories); + + foreach (var file in files) + { + var component = await AnalyzeFileAsync(file); + if (component != null && component.Parameters.Count > 0) + { + components.Add(component); + } + } + + // Also analyze .cs files that might be component base classes + var csFiles = Directory.GetFiles(_componentsPath, "*Base.cs", SearchOption.AllDirectories); + foreach (var file in csFiles) + { + var component = await AnalyzeFileAsync(file); + if (component != null && component.Parameters.Count > 0) + { + components.Add(component); + } + } + + return components.OrderBy(c => c.Name).ToList(); + } + + /// + /// Analyze a specific component by name + /// + public async Task AnalyzeComponentAsync(string componentName) + { + var pattern = $"{componentName}.razor.cs"; + var files = Directory.GetFiles(_componentsPath, pattern, SearchOption.AllDirectories); + + if (files.Length == 0) + { + // Try without .razor extension + pattern = $"{componentName}.cs"; + files = Directory.GetFiles(_componentsPath, pattern, SearchOption.AllDirectories); + } + + if (files.Length == 0) + { + return null; + } + + return await AnalyzeFileAsync(files[0]); + } + + private async Task AnalyzeFileAsync(string filePath) + { + try + { + var code = await File.ReadAllTextAsync(filePath); + var tree = CSharpSyntaxTree.ParseText(code); + var root = await tree.GetRootAsync(); + + // Find the class declaration + var classDeclaration = root.DescendantNodes() + .OfType() + .FirstOrDefault(); + + if (classDeclaration == null) + { + return null; + } + + var component = new ComponentInfo + { + Name = GetClassName(classDeclaration), + FullName = GetFullClassName(classDeclaration, root), + Summary = ExtractXmlSummary(classDeclaration), + TypeParameters = GetTypeParameters(classDeclaration), + BaseClass = GetBaseClass(classDeclaration), + SourcePath = GetRelativePath(filePath), + LastModified = File.GetLastWriteTimeUtc(filePath), + SamplePath = FindSamplePath(GetClassName(classDeclaration)) + }; + + // Extract parameters + component.Parameters = ExtractParameters(classDeclaration); + + // Extract public methods + component.PublicMethods = ExtractPublicMethods(classDeclaration); + + return component; + } + catch (Exception ex) + { + Console.WriteLine($"Error analyzing {filePath}: {ex.Message}"); + return null; + } + } + + private string GetClassName(ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Identifier.Text; + } + + private string GetFullClassName(ClassDeclarationSyntax classDeclaration, SyntaxNode root) + { + var namespaceName = root.DescendantNodes() + .OfType() + .FirstOrDefault()?.Name.ToString() ?? ""; + + var className = classDeclaration.Identifier.Text; + + if (classDeclaration.TypeParameterList != null) + { + className += classDeclaration.TypeParameterList.ToString(); + } + + return string.IsNullOrEmpty(namespaceName) ? className : $"{namespaceName}.{className}"; + } + + private List GetTypeParameters(ClassDeclarationSyntax classDeclaration) + { + if (classDeclaration.TypeParameterList == null) + { + return new List(); + } + + return classDeclaration.TypeParameterList.Parameters + .Select(p => p.Identifier.Text) + .ToList(); + } + + private string? GetBaseClass(ClassDeclarationSyntax classDeclaration) + { + var baseList = classDeclaration.BaseList; + if (baseList == null) return null; + + var baseType = baseList.Types.FirstOrDefault(); + return baseType?.Type.ToString(); + } + + private List ExtractParameters(ClassDeclarationSyntax classDeclaration) + { + var parameters = new List(); + + var properties = classDeclaration.DescendantNodes() + .OfType(); + + foreach (var property in properties) + { + // Check if property has [Parameter] attribute + var hasParameterAttr = property.AttributeLists + .SelectMany(a => a.Attributes) + .Any(a => a.Name.ToString() is "Parameter" or "ParameterAttribute"); + + if (!hasParameterAttr) continue; + + var paramInfo = new ParameterInfo + { + Name = property.Identifier.Text, + Type = SimplifyTypeName(property.Type?.ToString() ?? "unknown"), + DefaultValue = GetDefaultValue(property), + Description = ExtractXmlSummary(property), + IsRequired = HasAttribute(property, "EditorRequired"), + IsObsolete = HasAttribute(property, "Obsolete"), + ObsoleteMessage = GetObsoleteMessage(property), + IsEventCallback = property.Type?.ToString().Contains("EventCallback") ?? false + }; + + // Skip obsolete parameters + if (!paramInfo.IsObsolete) + { + parameters.Add(paramInfo); + } + } + + return parameters.OrderBy(p => p.Name).ToList(); + } + + private List ExtractPublicMethods(ClassDeclarationSyntax classDeclaration) + { + var methods = new List(); + + var methodDeclarations = classDeclaration.DescendantNodes() + .OfType() + .Where(m => m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword))); + + foreach (var method in methodDeclarations) + { + // Skip property accessors, overrides of base class methods + if (method.Modifiers.Any(m => m.IsKind(SyntaxKind.OverrideKeyword))) + continue; + + var methodInfo = new MethodInfo + { + Name = method.Identifier.Text, + ReturnType = SimplifyTypeName(method.ReturnType.ToString()), + Description = ExtractXmlSummary(method), + IsJSInvokable = HasAttribute(method, "JSInvokable"), + Parameters = method.ParameterList.Parameters + .Select(p => (SimplifyTypeName(p.Type?.ToString() ?? ""), p.Identifier.Text)) + .ToList() + }; + + methods.Add(methodInfo); + } + + return methods; + } + + private string? ExtractXmlSummary(SyntaxNode node) + { + var trivia = node.GetLeadingTrivia(); + var xmlTrivia = trivia.FirstOrDefault(t => + t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) || + t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia)); + + if (xmlTrivia == default) + return null; + + var xmlText = xmlTrivia.ToString(); + + // Extract content from tags + var match = SummaryRegex().Match(xmlText); + if (match.Success) + { + var summary = match.Groups[1].Value; + // Clean up the summary + summary = CleanXmlComment(summary); + return string.IsNullOrWhiteSpace(summary) ? null : summary; + } + + return null; + } + + private string CleanXmlComment(string comment) + { + // Remove /// prefixes and extra whitespace + var lines = comment.Split('\n') + .Select(l => l.Trim().TrimStart('/').Trim()) + .Where(l => !string.IsNullOrWhiteSpace(l)); + + return string.Join(" ", lines); + } + + private string? GetDefaultValue(PropertyDeclarationSyntax property) + { + var initializer = property.Initializer; + if (initializer != null) + { + return SimplifyDefaultValue(initializer.Value.ToString()); + } + + // Check for default in constructor or OnParametersSet + return null; + } + + private string SimplifyDefaultValue(string value) + { + // Simplify common patterns + if (value == "false") return "false"; + if (value == "true") return "true"; + if (value == "null") return "null"; + if (value == "0") return "0"; + if (value == "string.Empty" || value == "\"\"") return "\"\""; + if (value.StartsWith("new ")) return "new()"; + + return value; + } + + private string SimplifyTypeName(string typeName) + { + // Remove common namespace prefixes + var result = typeName + .Replace("System.", "") + .Replace("Collections.Generic.", "") + .Replace("Threading.Tasks.", ""); + + // Handle Nullable -> T? (must be done carefully to not break other generics) + result = NullableRegex().Replace(result, "$1?"); + + // Simplify primitive type names + result = result + .Replace("Int32", "int") + .Replace("Int64", "long") + .Replace("Boolean", "bool") + .Replace("String", "string") + .Replace("Object", "object"); + + return result; + } + + [GeneratedRegex(@"Nullable<([^>]+)>")] + private static partial Regex NullableRegex(); + + private bool HasAttribute(MemberDeclarationSyntax member, string attributeName) + { + return member.AttributeLists + .SelectMany(a => a.Attributes) + .Any(a => a.Name.ToString() == attributeName || + a.Name.ToString() == attributeName + "Attribute"); + } + + private string? GetObsoleteMessage(PropertyDeclarationSyntax property) + { + var obsoleteAttr = property.AttributeLists + .SelectMany(a => a.Attributes) + .FirstOrDefault(a => a.Name.ToString() is "Obsolete" or "ObsoleteAttribute"); + + if (obsoleteAttr?.ArgumentList?.Arguments.Count > 0) + { + return obsoleteAttr.ArgumentList.Arguments[0].ToString().Trim('"'); + } + + return null; + } + + private string GetRelativePath(string fullPath) + { + var basePath = Path.GetDirectoryName(Path.GetDirectoryName(_sourcePath))!; + return Path.GetRelativePath(basePath, fullPath).Replace('\\', '/'); + } + + private string? FindSamplePath(string componentName) + { + if (!Directory.Exists(_samplesPath)) + return null; + + // Try common sample file naming patterns + var patterns = new[] + { + $"{componentName}s.razor", + $"{componentName}.razor", + $"{componentName}Demo.razor", + $"{componentName}Sample.razor" + }; + + foreach (var pattern in patterns) + { + var files = Directory.GetFiles(_samplesPath, pattern, SearchOption.AllDirectories); + if (files.Length > 0) + { + return GetRelativePath(files[0]); + } + } + + return null; + } + + [GeneratedRegex(@"\s*(.*?)\s*", RegexOptions.Singleline)] + private static partial Regex SummaryRegex(); +} diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentInfo.cs b/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentInfo.cs new file mode 100644 index 00000000..4c7ac9cd --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/ComponentInfo.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace LlmsDocsGenerator; + +/// +/// Represents information about a Blazor component +/// +public class ComponentInfo +{ + /// + /// Component name (e.g., "Table", "Button") + /// + public string Name { get; set; } = string.Empty; + + /// + /// Full type name including namespace + /// + public string FullName { get; set; } = string.Empty; + + /// + /// XML documentation summary + /// + public string? Summary { get; set; } + + /// + /// Generic type parameters (e.g., "TItem", "TValue") + /// + public List TypeParameters { get; set; } = new(); + + /// + /// Component parameters ([Parameter] properties) + /// + public List Parameters { get; set; } = new(); + + /// + /// Public methods + /// + public List PublicMethods { get; set; } = new(); + + /// + /// Base class name + /// + public string? BaseClass { get; set; } + + /// + /// Source file path + /// + public string SourcePath { get; set; } = string.Empty; + + /// + /// Last modification time of the source file + /// + public DateTime LastModified { get; set; } + + /// + /// Related sample file path (if exists) + /// + public string? SamplePath { get; set; } +} + +/// +/// Represents a component parameter +/// +public class ParameterInfo +{ + /// + /// Parameter name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Parameter type as string + /// + public string Type { get; set; } = string.Empty; + + /// + /// Default value (if any) + /// + public string? DefaultValue { get; set; } + + /// + /// XML documentation summary + /// + public string? Description { get; set; } + + /// + /// Whether this is an EditorRequired parameter + /// + public bool IsRequired { get; set; } + + /// + /// Whether this parameter is obsolete + /// + public bool IsObsolete { get; set; } + + /// + /// Obsolete message (if obsolete) + /// + public string? ObsoleteMessage { get; set; } + + /// + /// Whether this is an EventCallback + /// + public bool IsEventCallback { get; set; } +} + +/// +/// Represents a public method +/// +public class MethodInfo +{ + /// + /// Method name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Return type + /// + public string ReturnType { get; set; } = string.Empty; + + /// + /// Method parameters + /// + public List<(string Type, string Name)> Parameters { get; set; } = new(); + + /// + /// XML documentation summary + /// + public string? Description { get; set; } + + /// + /// Whether this is a JSInvokable method + /// + public bool IsJSInvokable { get; set; } +} diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs b/tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs new file mode 100644 index 00000000..f5141654 --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs @@ -0,0 +1,243 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace LlmsDocsGenerator; + +/// +/// Main documentation generator class +/// +public class DocsGenerator +{ + private readonly string _outputPath; + private readonly string _componentsOutputPath; + private readonly string _sourcePath; + private readonly ComponentAnalyzer _analyzer; + private readonly MarkdownBuilder _markdownBuilder; + private readonly bool _debug; + + public DocsGenerator(string? rootFolder, bool debug) + { + _debug = debug; + + // Find the source directory (relative to tool location or current directory) + var root = FindSourcePath(rootFolder); + + _sourcePath = Path.Combine(root, "src", "BootstrapBlazor"); + _outputPath = Path.Combine(root, "src", "BootstrapBlazor.Server", "wwwroot", "llms"); + _componentsOutputPath = Path.Combine(_outputPath, "components"); + _analyzer = new ComponentAnalyzer(_sourcePath); + _markdownBuilder = new MarkdownBuilder(); + } + + private string FindSourcePath(string? rootFolder) + { + // Try to find src/BootstrapBlazor from current directory or parent directories + var current = rootFolder ?? AppContext.BaseDirectory; + Logger($"Root path: {current}"); + + while (!string.IsNullOrEmpty(current)) + { + var parent = Directory.GetParent(current); + if (parent == null) + { + break; + } + if (parent.Name.Equals("BootstrapBlazor", StringComparison.OrdinalIgnoreCase)) + { + return parent.FullName; + } + current = parent.FullName; + } + + throw new DirectoryNotFoundException("Could not find src directory. Please run this tool from the BootstrapBlazor repository root."); + } + + /// + /// Generate all documentation files + /// + public async Task GenerateAllAsync() + { + Logger($"Source path: {_sourcePath}"); + Logger($"Output path: {_outputPath}"); + Logger($"Components path: {_componentsOutputPath}"); + + // Ensure output directories exist + Directory.CreateDirectory(_outputPath); + Directory.CreateDirectory(_componentsOutputPath); + + // Analyze all components + Logger("Analyzing components..."); + var components = await _analyzer.AnalyzeAllComponentsAsync(); + Logger($"Found {components.Count} components"); + + // Generate index file + await GenerateIndexAsync(components); + + // Generate individual component documentation files + Logger("Generating individual component documentation..."); + foreach (var component in components) + { + await GenerateComponentDocAsync(component); + } + + Logger("Documentation generation complete!"); + } + + /// + /// Generate only the index file + /// + public async Task GenerateIndexAsync() + { + var components = await _analyzer.AnalyzeAllComponentsAsync(); + await GenerateIndexAsync(components); + } + + private async Task GenerateIndexAsync(List components) + { + // Ensure output directory exists + Directory.CreateDirectory(_outputPath); + + var indexPath = Path.Combine(_outputPath, "llms.txt"); + var content = _markdownBuilder.BuildIndex(components); + await File.WriteAllTextAsync(indexPath, content); + Logger($"Generated: {indexPath}"); + } + + /// + /// Generate documentation for a specific component + /// + public async Task GenerateComponentAsync(string componentName) + { + var component = await _analyzer.AnalyzeComponentAsync(componentName); + if (component == null) + { + Logger($"Component not found: {componentName}"); + return; + } + + // Ensure output directory exists + Directory.CreateDirectory(_componentsOutputPath); + + await GenerateComponentDocAsync(component); + } + + private async Task GenerateComponentDocAsync(ComponentInfo component) + { + var content = _markdownBuilder.BuildComponentDoc(component); + var fileName = $"{component.Name}.txt"; + var filePath = Path.Combine(_componentsOutputPath, fileName); + await File.WriteAllTextAsync(filePath, content); + Logger($"Generated: {filePath}"); + } + + /// + /// Check if documentation is up-to-date + /// + public async Task CheckAsync() + { + Logger("Checking documentation freshness..."); + + var components = await _analyzer.AnalyzeAllComponentsAsync(); + + // Check index file + var indexPath = Path.Combine(_outputPath, "llms.txt"); + if (!File.Exists(indexPath)) + { + Logger("OUTDATED: llms.txt does not exist"); + return false; + } + + var indexLastWrite = File.GetLastWriteTimeUtc(indexPath); + + // compute the most recent component source timestamp: + var newestComponentWrite = components + .Select(c => c.LastModified) + .DefaultIfEmpty(indexLastWrite) + .Max(); + + if (indexLastWrite < newestComponentWrite) + { + Logger("Index file is stale relative to component sources. Please regenerate docs."); + return false; + } + + // Check each component file + foreach (var component in components) + { + var fileName = $"{component.Name}.txt"; + var filePath = Path.Combine(_componentsOutputPath, fileName); + + if (!File.Exists(filePath)) + { + Logger($"OUTDATED: {fileName} does not exist"); + return false; + } + + // Check if source file is newer than the doc file + var docLastWrite = File.GetLastWriteTimeUtc(filePath); + if (component.LastModified > docLastWrite) + { + Logger($"OUTDATED: {component.Name} was modified after {fileName}"); + return false; + } + } + + Logger("Documentation is up-to-date"); + return true; + } + + private static string GetComponentCategory(string componentName) + { + return componentName.ToLowerInvariant() switch + { + // Table family + var n when n.Contains("table") => "table", + + // Input family + var n when n.Contains("input") || n.Contains("textarea") || + n.Contains("password") || n == "otpinput" => "input", + + // Select family + var n when n.Contains("select") || n.Contains("dropdown") || + n.Contains("autocomplete") || n.Contains("cascader") || + n.Contains("transfer") || n.Contains("multiselect") => "select", + + // Button family + var n when n.Contains("button") || n == "gotop" || + n.Contains("popconfirm") => "button", + + // Dialog family + var n when n.Contains("dialog") || n.Contains("modal") || + n.Contains("drawer") || n.Contains("swal") || + n.Contains("toast") || n.Contains("message") => "dialog", + + // Navigation family + var n when n.Contains("menu") || n.Contains("tab") || + n.Contains("breadcrumb") || n.Contains("step") || + n.Contains("anchor") || n.Contains("nav") => "nav", + + // Card/Container family + var n when n.Contains("card") || n.Contains("collapse") || + n.Contains("groupbox") || n.Contains("panel") => "card", + + // TreeView + var n when n.Contains("tree") => "treeview", + + // Form + var n when n.Contains("validateform") || n.Contains("editorform") || + n.Contains("validator") => "form", + + _ => "other" + }; + } + + private void Logger(string message) + { + if (_debug) + { + Console.WriteLine(message); + } + } +} diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/MarkdownBuilder.cs b/tools/BootstrapBlazor.LLMsDocsGenerator/MarkdownBuilder.cs new file mode 100644 index 00000000..97c8271f --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/MarkdownBuilder.cs @@ -0,0 +1,399 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using System.Text; + +namespace LlmsDocsGenerator; + +/// +/// Builds Markdown documentation for components +/// +public class MarkdownBuilder +{ + private const string GitHubBaseUrl = "https://github.com/dotnetcore/BootstrapBlazor/blob/main/"; + private readonly StringBuilder _sb = new(); + + /// + /// Build the main llms.txt index file + /// + public string BuildIndex(List components) + { + _sb.Clear(); + + _sb.AppendLine("# BootstrapBlazor"); + _sb.AppendLine(); + _sb.AppendLine("> Enterprise-class Blazor UI component library based on Bootstrap 5"); + _sb.AppendLine(); + + // Quick Start section + _sb.AppendLine("## Quick Start"); + _sb.AppendLine(); + _sb.AppendLine("```bash"); + _sb.AppendLine("dotnet add package BootstrapBlazor"); + _sb.AppendLine("```"); + _sb.AppendLine(); + _sb.AppendLine("### Configuration"); + _sb.AppendLine(); + _sb.AppendLine("```csharp"); + _sb.AppendLine("// Program.cs"); + _sb.AppendLine("builder.Services.AddBootstrapBlazor();"); + _sb.AppendLine("```"); + _sb.AppendLine(); + _sb.AppendLine("```razor"); + _sb.AppendLine("@* _Imports.razor *@"); + _sb.AppendLine("@using BootstrapBlazor.Components"); + _sb.AppendLine("```"); + _sb.AppendLine(); + _sb.AppendLine("```html"); + _sb.AppendLine(""); + _sb.AppendLine(""); + _sb.AppendLine(""); + _sb.AppendLine("```"); + _sb.AppendLine(); + + // Component List - grouped by category for easy navigation + _sb.AppendLine("## Components"); + _sb.AppendLine(); + _sb.AppendLine("Each component has its own documentation file in the `components/` directory."); + _sb.AppendLine("Use `components/{ComponentName}.txt` to get detailed API information."); + _sb.AppendLine(); + + // Group components by category for the index + var categorized = CategorizeComponents(components); + + var categoryDescriptions = new Dictionary + { + ["table"] = ("Data Display - Table", "Complex data table with sorting, filtering, paging, editing"), + ["input"] = ("Form Inputs", "Text input, number input, textarea, date picker"), + ["select"] = ("Selection Components", "Select, multi-select, autocomplete, cascader, transfer"), + ["button"] = ("Buttons", "Button, button group, dropdown button, split button"), + ["dialog"] = ("Dialogs & Feedback", "Modal, drawer, dialog service, message, toast"), + ["nav"] = ("Navigation", "Menu, tabs, breadcrumb, steps, pagination"), + ["card"] = ("Containers", "Card, collapse, group box, split, layout"), + ["treeview"] = ("Tree Components", "TreeView, tree select"), + ["form"] = ("Form Validation", "ValidateForm, editor form, validation rules"), + ["other"] = ("Other Components", "Miscellaneous components") + }; + + foreach (var (category, categoryComponents) in categorized.OrderBy(c => c.Key)) + { + if (categoryComponents.Count == 0) continue; + + var (title, description) = categoryDescriptions.GetValueOrDefault(category, (category, "")); + _sb.AppendLine($"### {title}"); + _sb.AppendLine(); + _sb.AppendLine($"{description}"); + _sb.AppendLine(); + + // List components with links to their individual docs + foreach (var component in categoryComponents.OrderBy(c => c.Name)) + { + var summary = !string.IsNullOrEmpty(component.Summary) + ? $" - {TruncateSummary(component.Summary, 60)}" + : ""; + _sb.AppendLine($"- [{component.Name}](components/{component.Name}.txt){summary}"); + } + _sb.AppendLine(); + } + + // Source Code Reference + _sb.AppendLine("## Source Code Reference"); + _sb.AppendLine(); + _sb.AppendLine("GitHub Repository: https://github.com/dotnetcore/BootstrapBlazor"); + _sb.AppendLine(); + _sb.AppendLine("When documentation is insufficient, consult the source code:"); + _sb.AppendLine(); + _sb.AppendLine("### File Structure"); + _sb.AppendLine(); + _sb.AppendLine("```"); + _sb.AppendLine($"{GitHubBaseUrl}src/BootstrapBlazor/Components/{{ComponentName}}/"); + _sb.AppendLine("├── {Component}.razor # Razor template"); + _sb.AppendLine("├── {Component}.razor.cs # Component logic & parameters"); + _sb.AppendLine("├── {Component}Base.cs # Base class (if exists)"); + _sb.AppendLine("├── {Component}Option.cs # Configuration options"); + _sb.AppendLine("└── {Component}Service.cs # Service class (Dialog, Toast, etc.)"); + _sb.AppendLine("```"); + _sb.AppendLine(); + _sb.AppendLine("### Examples"); + _sb.AppendLine(); + _sb.AppendLine("```"); + _sb.AppendLine($"{GitHubBaseUrl}src/BootstrapBlazor.Server/Components/Samples/{{ComponentName}}s.razor"); + _sb.AppendLine("```"); + _sb.AppendLine(); + _sb.AppendLine("### Reading Component Parameters"); + _sb.AppendLine(); + _sb.AppendLine("Look for properties with `[Parameter]` attribute:"); + _sb.AppendLine(); + _sb.AppendLine("```csharp"); + _sb.AppendLine("/// "); + _sb.AppendLine("/// Gets or sets whether to show the toolbar"); + _sb.AppendLine("/// "); + _sb.AppendLine("[Parameter]"); + _sb.AppendLine("public bool ShowToolbar { get; set; }"); + _sb.AppendLine("```"); + _sb.AppendLine(); + + // Footer + _sb.AppendLine("---"); + _sb.AppendLine($"Generated: {DateTime.UtcNow:yyyy-MM-dd}"); + _sb.AppendLine($"Total Components: {components.Count}"); + _sb.AppendLine($"Repository: {GitHubBaseUrl}"); + + return _sb.ToString(); + } + + private static Dictionary> CategorizeComponents(List components) + { + var categories = new Dictionary> + { + ["table"] = [], + ["input"] = [], + ["select"] = [], + ["button"] = [], + ["dialog"] = [], + ["nav"] = [], + ["card"] = [], + ["treeview"] = [], + ["form"] = [], + ["other"] = [] + }; + + foreach (var component in components) + { + var category = GetComponentCategory(component.Name); + if (categories.TryGetValue(category, out var list)) + { + list.Add(component); + } + else + { + categories["other"].Add(component); + } + } + + // Remove empty categories + return categories.Where(c => c.Value.Count > 0) + .ToDictionary(c => c.Key, c => c.Value); + } + + private static string GetComponentCategory(string componentName) + { + return componentName.ToLowerInvariant() switch + { + var n when n.Contains("table") => "table", + var n when n.Contains("input") || n.Contains("textarea") || + n.Contains("password") || n == "otpinput" => "input", + var n when n.Contains("select") || n.Contains("dropdown") || + n.Contains("autocomplete") || n.Contains("cascader") || + n.Contains("transfer") || n.Contains("multiselect") => "select", + var n when n.Contains("button") || n == "gotop" || + n.Contains("popconfirm") => "button", + var n when n.Contains("dialog") || n.Contains("modal") || + n.Contains("drawer") || n.Contains("swal") || + n.Contains("toast") || n.Contains("message") => "dialog", + var n when n.Contains("menu") || n.Contains("tab") || + n.Contains("breadcrumb") || n.Contains("step") || + n.Contains("anchor") || n.Contains("nav") => "nav", + var n when n.Contains("card") || n.Contains("collapse") || + n.Contains("groupbox") || n.Contains("panel") => "card", + var n when n.Contains("tree") => "treeview", + var n when n.Contains("validateform") || n.Contains("editorform") || + n.Contains("validator") => "form", + _ => "other" + }; + } + + private static string TruncateSummary(string summary, int maxLength) + { + if (string.IsNullOrEmpty(summary)) return ""; + summary = summary.Replace("\n", " ").Replace("\r", "").Trim(); + return summary.Length <= maxLength ? summary : summary[..(maxLength - 3)] + "..."; + } + + /// + /// Build documentation for a single component + /// + public string BuildComponentDoc(ComponentInfo component) + { + _sb.Clear(); + + _sb.AppendLine($"# BootstrapBlazor {component.Name}"); + _sb.AppendLine(); + + if (!string.IsNullOrEmpty(component.Summary)) + { + _sb.AppendLine($"> {component.Summary}"); + _sb.AppendLine(); + } + + BuildComponentSection(component, includeHeader: false); + + // Footer + _sb.AppendLine("---"); + _sb.AppendLine($""); + + return _sb.ToString(); + } + + private void BuildComponentSection(ComponentInfo component, bool includeHeader = true) + { + if (includeHeader) + { + _sb.AppendLine($"## {component.Name}"); + _sb.AppendLine(); + + if (!string.IsNullOrEmpty(component.Summary)) + { + _sb.AppendLine(component.Summary); + _sb.AppendLine(); + } + } + + // Type parameters + if (component.TypeParameters.Count > 0) + { + _sb.AppendLine("### Type Parameters"); + _sb.AppendLine(); + foreach (var tp in component.TypeParameters) + { + _sb.AppendLine($"- `{tp}` - Generic type parameter"); + } + _sb.AppendLine(); + } + + // Base class info + if (!string.IsNullOrEmpty(component.BaseClass)) + { + _sb.AppendLine($"**Inherits from**: `{component.BaseClass}`"); + _sb.AppendLine(); + } + + // Parameters table + if (component.Parameters.Count > 0) + { + _sb.AppendLine("### Parameters"); + _sb.AppendLine(); + _sb.AppendLine(""); + _sb.AppendLine(); + _sb.AppendLine("| Parameter | Type | Default | Description |"); + _sb.AppendLine("|-----------|------|---------|-------------|"); + + // Sort: required first, then events, then alphabetically + var sortedParams = component.Parameters + .OrderByDescending(p => p.IsRequired) + .ThenBy(p => p.IsEventCallback) + .ThenBy(p => p.Name); + + foreach (var param in sortedParams) + { + var required = param.IsRequired ? " **[Required]**" : ""; + var description = EscapeMarkdownCell(param.Description ?? "") + required; + var defaultVal = param.DefaultValue ?? "-"; + var type = EscapeMarkdownCell(param.Type); + + _sb.AppendLine($"| {param.Name} | `{type}` | {defaultVal} | {description} |"); + } + + _sb.AppendLine(); + _sb.AppendLine(""); + _sb.AppendLine(); + } + + // Event callbacks (separate section for clarity) + var eventCallbacks = component.Parameters.Where(p => p.IsEventCallback).ToList(); + if (eventCallbacks.Count > 0) + { + _sb.AppendLine("### Event Callbacks"); + _sb.AppendLine(); + _sb.AppendLine("| Event | Type | Description |"); + _sb.AppendLine("|-------|------|-------------|"); + + foreach (var evt in eventCallbacks.OrderBy(e => e.Name)) + { + var description = EscapeMarkdownCell(evt.Description ?? ""); + var type = EscapeMarkdownCell(evt.Type); + _sb.AppendLine($"| {evt.Name} | `{type}` | {description} |"); + } + + _sb.AppendLine(); + } + + // Public methods + if (component.PublicMethods.Count > 0) + { + _sb.AppendLine("### Public Methods"); + _sb.AppendLine(); + + foreach (var method in component.PublicMethods.OrderBy(m => m.Name)) + { + var paramStr = string.Join(", ", method.Parameters.Select(p => $"{p.Item1} {p.Item2}")); + _sb.AppendLine($"- `{method.ReturnType} {method.Name}({paramStr})`"); + if (!string.IsNullOrEmpty(method.Description)) + { + _sb.AppendLine($" - {method.Description}"); + } + if (method.IsJSInvokable) + { + _sb.AppendLine(" - *[JSInvokable]*"); + } + } + + _sb.AppendLine(); + } + + // Source reference with GitHub URLs + if (!string.IsNullOrEmpty(component.SourcePath)) + { + _sb.AppendLine("### Source"); + _sb.AppendLine(); + var sourceUrl = $"{GitHubBaseUrl}{component.SourcePath}"; + _sb.AppendLine($"- Component: [{component.SourcePath}]({sourceUrl})"); + if (!string.IsNullOrEmpty(component.SamplePath)) + { + var sampleUrl = $"{GitHubBaseUrl}{component.SamplePath}"; + _sb.AppendLine($"- Examples: [{component.SamplePath}]({sampleUrl})"); + } + _sb.AppendLine(); + } + } + + /// + /// Build a minimal parameter table for embedding in existing docs + /// + public string BuildParameterTable(List parameters) + { + _sb.Clear(); + + _sb.AppendLine("| Parameter | Type | Default | Description |"); + _sb.AppendLine("|-----------|------|---------|-------------|"); + + var sortedParams = parameters + .OrderByDescending(p => p.IsRequired) + .ThenBy(p => p.IsEventCallback) + .ThenBy(p => p.Name); + + foreach (var param in sortedParams) + { + var required = param.IsRequired ? " **[Required]**" : ""; + var description = EscapeMarkdownCell(param.Description ?? "") + required; + var defaultVal = param.DefaultValue ?? "-"; + var type = EscapeMarkdownCell(param.Type); + + _sb.AppendLine($"| {param.Name} | `{type}` | {defaultVal} | {description} |"); + } + + return _sb.ToString(); + } + + private static string EscapeMarkdownCell(string text) + { + if (string.IsNullOrEmpty(text)) return ""; + + return text + .Replace("|", "\\|") + .Replace("\n", " ") + .Replace("\r", ""); + } +} diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/Program.cs b/tools/BootstrapBlazor.LLMsDocsGenerator/Program.cs new file mode 100644 index 00000000..88eec57f --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/Program.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using LlmsDocsGenerator; +using System.CommandLine; + +var componentOption = new Option("--component") { Description = "Generate documentation for a specific component only" }; +var indexOnlyOption = new Option("--index-only") { Description = "Generate only the index file (llms.txt)" }; +var checkOption = new Option("--check") { Description = "Check if documentation is up-to-date (for CI/CD)" }; +var rootFolderOption = new Option("--root") { Description = "Set the root folder of project" }; +var debugOption = new Option("--debug") { Description = "Set the environment to development and display debugging information." }; + +var rootCommand = new RootCommand("BootstrapBlazor LLMs Documentation Generator") +{ + componentOption, + indexOnlyOption, + checkOption, + rootFolderOption, + debugOption +}; + +rootCommand.SetAction(async result => +{ + var debug = result.GetValue(debugOption); + var rootFolder = result.GetValue(rootFolderOption); + var generator = new DocsGenerator(rootFolder, debug); + + var check = result.GetValue(checkOption); + if (check) + { + var isUpToDate = await generator.CheckAsync(); + Environment.ExitCode = isUpToDate ? 0 : 1; + return; + } + + var indexOnly = result.GetValue(indexOnlyOption); + if (indexOnly) + { + await generator.GenerateIndexAsync(); + return; + } + + var component = result.GetValue(componentOption); + if (!string.IsNullOrEmpty(component)) + { + await generator.GenerateComponentAsync(component); + return; + } + + await generator.GenerateAllAsync(); +}); + +return await rootCommand.Parse(args).InvokeAsync(); diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/README.md b/tools/BootstrapBlazor.LLMsDocsGenerator/README.md new file mode 100644 index 00000000..246c5d66 --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/README.md @@ -0,0 +1,245 @@ +# LlmsDocsGenerator + +A tool that automatically generates LLM-friendly documentation for BootstrapBlazor components. + +## Purpose + +AI coding assistants (Claude Code, Cursor, GitHub Copilot) often generate incorrect UI code because they lack accurate component API information. This tool solves that problem by: + +1. **Auto-generating parameter tables** from source code using Roslyn +2. **Providing GitHub source links** for deeper reference +3. **Integrating with CI/CD** to keep docs synchronized with code + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ LlmsDocsGenerator │ +├─────────────────────────────────────────────────────────────┤ +│ ComponentAnalyzer → Roslyn-based source code parser │ +│ MarkdownBuilder → Generates markdown documentation │ +│ DocsGenerator → Orchestrates the generation flow │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Output: wwwroot/llms/ │ +├─────────────────────────────────────────────────────────────┤ +│ llms.txt → Index with quick start guide │ +│ components/ → Individual component documentation │ +│ ├── Button.txt → Button component API reference │ +│ ├── Table.txt → Table component API reference │ +│ ├── Select.txt → Select component API reference │ +│ ├── Modal.txt → Modal component API reference │ +│ └── ... → One file per component │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Why One File Per Component? + +This design optimizes for LLM and Code Agent consumption: + +| Aspect | Per-Category (Old) | Per-Component (New) | +|--------|-------------------|---------------------| +| **Precision** | ❌ Loads unrelated components | ✅ Only needed API info | +| **Token Efficiency** | ❌ Wastes tokens on irrelevant data | ✅ Minimal context loading | +| **Cache Friendly** | ❌ Regenerates entire category | ✅ Updates single file | +| **RAG Retrieval** | ❌ Coarse-grained matches | ✅ Fine-grained matches | +| **Incremental Updates** | ❌ Complex CI/CD checks | ✅ Simple file mapping | + +## How It Works + +### 1. Source Code Analysis + +The `ComponentAnalyzer` uses Roslyn to parse C# source files: + +```csharp +// Scans for [Parameter] attributes +var parameters = classDeclaration.DescendantNodes() + .OfType() + .Where(p => HasAttribute(p, "Parameter")); + +// Extracts XML documentation comments +var summary = ExtractXmlSummary(property); +``` + +### 2. Documentation Generation + +The `MarkdownBuilder` creates structured markdown with: + +- Parameter tables (name, type, default, description) +- Event callbacks section +- Public methods +- GitHub source links + +### 3. Component Organization + +Components are organized in the index by category for easy navigation, but each component has its own dedicated documentation file: + +| Category | Example Components | +|----------|-------------------| +| table | Table, SelectTable, TableToolbar | +| input | BootstrapInput, Textarea, OtpInput | +| select | Select, AutoComplete, Cascader | +| button | Button, PopConfirmButton | +| dialog | Modal, Drawer, Toast | +| nav | Menu, Tab, Breadcrumb | +| card | Card, Collapse, GroupBox | +| treeview | TreeView, Tree | +| form | ValidateForm, EditorForm | +| other | All other components | + +## Installation + +### Install as Global Tool + +```bash +dotnet pack tools/LlmsDocsGenerator +dotnet tool install --global --add-source ./tools/LlmsDocsGenerator/bin/Release BootstrapBlazor.LlmsDocsGenerator +``` + +Or install from NuGet (once published): + +```bash +dotnet tool install --global BootstrapBlazor.LlmsDocsGenerator +``` + +### Update Tool + +```bash +dotnet tool update --global BootstrapBlazor.LlmsDocsGenerator +``` + +### Uninstall Tool + +```bash +dotnet tool uninstall --global BootstrapBlazor.LlmsDocsGenerator +``` + +## Usage + +Once installed as a global tool, use the `llms-docs` command: + +### Generate All Documentation + +```bash +llms-docs +``` + +Or when running from source: + +```bash +dotnet run --project tools/LlmsDocsGenerator +``` + +### Generate Specific Component + +```bash +llms-docs --component Table +``` + +### Generate Index Only + +```bash +llms-docs --index-only +``` + +### Check Freshness (CI/CD) + +```bash +llms-docs --check +``` + +Returns exit code 1 if documentation is outdated. + +### Custom Output Directory + +```bash +llms-docs --output ./docs +``` + +### Show Help + +```bash +llms-docs --help +``` + +## CI/CD Integration + +### Build Workflow (build.yml) + +Checks if documentation is up-to-date on every push to main: + +```yaml +- name: Check LLM Documentation + run: dotnet run --project tools/LlmsDocsGenerator -- --check +``` + +### Docker Workflow (docker.yml) + +Regenerates documentation before building the doc site: + +```yaml +- name: Generate LLM Documentation + run: dotnet run --project tools/LlmsDocsGenerator +``` + +### Dockerfile + +Generates documentation during container build: + +```dockerfile +WORKDIR /tools/LlmsDocsGenerator +RUN dotnet run +``` + +## Output Format + +Each component documentation includes: + +```markdown +## ComponentName + +Description from XML comments + +### Type Parameters +- `TItem` - Generic type parameter + +### Parameters +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| Items | `List` | - | Data source | +| ShowToolbar | `bool` | false | Show toolbar | + +### Event Callbacks +| Event | Type | Description | +|-------|------|-------------| +| OnClick | `EventCallback` | Click handler | + +### Public Methods +- `Task RefreshAsync()` - Refresh data + +### Source +- Component: [src/.../Component.razor.cs](GitHub URL) +- Examples: [src/.../Samples/Components.razor](GitHub URL) +``` + +## For Library Users + +Users can reference this documentation in their own projects by creating a `llms.txt`: + +```markdown +# My Project + +## Dependencies + +### BootstrapBlazor +- Documentation Index: https://www.blazor.zone/llms/llms.txt +- Button: https://www.blazor.zone/llms/components/Button.txt +- Table: https://www.blazor.zone/llms/components/Table.txt +- Modal: https://www.blazor.zone/llms/components/Modal.txt +``` + +LLM agents can: +1. First read `llms.txt` to discover available components +2. Then fetch specific `components/{ComponentName}.txt` for detailed API info diff --git a/tools/BootstrapBlazor.LLMsDocsGenerator/README.zh-CN.md b/tools/BootstrapBlazor.LLMsDocsGenerator/README.zh-CN.md new file mode 100644 index 00000000..e3163ed3 --- /dev/null +++ b/tools/BootstrapBlazor.LLMsDocsGenerator/README.zh-CN.md @@ -0,0 +1,266 @@ +# LlmsDocsGenerator + +自动为 BootstrapBlazor 组件生成 LLM 友好文档的工具。 + +## 目的 + +AI 编程助手(Claude Code、Cursor、GitHub Copilot)经常因为缺乏准确的组件 API 信息而生成错误的 UI 代码。本工具通过以下方式解决这个问题: + +1. **使用 Roslyn 自动生成参数表** - 从源代码提取 +2. **提供 GitHub 源码链接** - 方便深入查阅 +3. **集成 CI/CD** - 确保文档与代码同步 + +## 架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ LlmsDocsGenerator │ +├─────────────────────────────────────────────────────────────┤ +│ ComponentAnalyzer → 基于 Roslyn 的源码解析器 │ +│ MarkdownBuilder → 生成 Markdown 文档 │ +│ DocsGenerator → 协调生成流程 │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 输出目录: wwwroot/llms/ │ +├─────────────────────────────────────────────────────────────┤ +│ llms.txt → 索引文件,包含快速入门指南 │ +│ components/ → 单独的组件文档目录 │ +│ ├── Button.txt → Button 组件 API 参考 │ +│ ├── Table.txt → Table 组件 API 参考 │ +│ ├── Select.txt → Select 组件 API 参考 │ +│ ├── Modal.txt → Modal 组件 API 参考 │ +│ └── ... → 每个组件一个文件 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 为什么每个组件单独一个文件? + +这种设计针对 LLM 和 Code Agent 进行了优化: + +| 方面 | 按分类(旧方案) | 按组件(新方案) | +|------|-----------------|-----------------| +| **精确性** | ❌ 加载无关组件 | ✅ 只加载需要的 API | +| **Token 效率** | ❌ 浪费 token 在无关数据上 | ✅ 最小化上下文加载 | +| **缓存友好** | ❌ 需重新生成整个分类 | ✅ 只更新单个文件 | +| **RAG 检索** | ❌ 粗粒度匹配 | ✅ 细粒度匹配 | +| **增量更新** | ❌ CI/CD 检查复杂 | ✅ 简单的文件映射 | + +## 工作原理 + +### 1. 源码分析 + +`ComponentAnalyzer` 使用 Roslyn 解析 C# 源文件: + +```csharp +// 扫描 [Parameter] 特性 +var parameters = classDeclaration.DescendantNodes() + .OfType() + .Where(p => HasAttribute(p, "Parameter")); + +// 提取 XML 文档注释 +var summary = ExtractXmlSummary(property); +``` + +### 2. 文档生成 + +`MarkdownBuilder` 生成结构化的 Markdown,包含: + +- 参数表(名称、类型、默认值、描述) +- 事件回调部分 +- 公共方法 +- GitHub 源码链接 + +### 3. 组件组织 + +组件在索引中按类别组织便于导航,但每个组件有独立的文档文件: + +| 类别 | 组件示例 | +|------|----------| +| table | Table, SelectTable, TableToolbar | +| input | BootstrapInput, Textarea, OtpInput | +| select | Select, AutoComplete, Cascader | +| button | Button, PopConfirmButton | +| dialog | Modal, Drawer, Toast | +| nav | Menu, Tab, Breadcrumb | +| card | Card, Collapse, GroupBox | +| treeview | TreeView, Tree | +| form | ValidateForm, EditorForm | +| other | 其他所有组件 | + +## 安装 + +### 作为全局工具安装 + +```bash +dotnet pack tools/LlmsDocsGenerator +dotnet tool install --global --add-source ./tools/LlmsDocsGenerator/bin/Release BootstrapBlazor.LlmsDocsGenerator +``` + +或从 NuGet 安装(发布后): + +```bash +dotnet tool install --global BootstrapBlazor.LlmsDocsGenerator +``` + +### 更新工具 + +```bash +dotnet tool update --global BootstrapBlazor.LlmsDocsGenerator +``` + +### 卸载工具 + +```bash +dotnet tool uninstall --global BootstrapBlazor.LlmsDocsGenerator +``` + +## 使用方法 + +安装为全局工具后,使用 `llms-docs` 命令: + +### 生成所有文档 + +```bash +llms-docs +``` + +或从源代码运行: + +```bash +dotnet run --project tools/LlmsDocsGenerator +``` + +### 生成特定组件 + +```bash +llms-docs --component Table +``` + +### 仅生成索引 + +```bash +llms-docs --index-only +``` + +### 检查文档是否过期(CI/CD) + +```bash +llms-docs --check +``` + +如果文档过期,返回退出码 1。 + +### 自定义输出目录 + +```bash +llms-docs --output ./docs +``` + +### 显示帮助 + +```bash +llms-docs --help +``` + +## CI/CD 集成 + +### 构建工作流 (build.yml) + +每次推送到 main 分支时检查文档是否最新: + +```yaml +- name: Check LLM Documentation + run: dotnet run --project tools/LlmsDocsGenerator -- --check +``` + +### Docker 工作流 (docker.yml) + +构建文档站点前重新生成文档: + +```yaml +- name: Generate LLM Documentation + run: dotnet run --project tools/LlmsDocsGenerator +``` + +### Dockerfile + +容器构建时生成文档: + +```dockerfile +WORKDIR /tools/LlmsDocsGenerator +RUN dotnet run +``` + +## 输出格式 + +每个组件的文档包含: + +```markdown +## 组件名称 + +来自 XML 注释的描述 + +### 类型参数 +- `TItem` - 泛型类型参数 + +### 参数 +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| Items | `List` | - | 数据源 | +| ShowToolbar | `bool` | false | 显示工具栏 | + +### 事件回调 +| 事件 | 类型 | 描述 | +|------|------|------| +| OnClick | `EventCallback` | 点击处理器 | + +### 公共方法 +- `Task RefreshAsync()` - 刷新数据 + +### 源码 +- 组件: [src/.../Component.razor.cs](GitHub 链接) +- 示例: [src/.../Samples/Components.razor](GitHub 链接) +``` + +## 库用户使用指南 + +用户可以在自己的项目中创建 `llms.txt` 来引用本文档: + +```markdown +# 我的项目 + +## 依赖 + +### BootstrapBlazor +- 文档索引: https://www.blazor.zone/llms/llms.txt +- Button: https://www.blazor.zone/llms/components/Button.txt +- Table: https://www.blazor.zone/llms/components/Table.txt +- Modal: https://www.blazor.zone/llms/components/Modal.txt +``` + +LLM 代理可以: +1. 先读取 `llms.txt` 了解有哪些组件 +2. 然后获取 `components/{ComponentName}.txt` 获取详细 API 信息 + +## 设计理念 + +### 为什么需要这个工具? + +| 问题 | 解决方案 | +|------|----------| +| AI 生成错误的组件代码 | 提供准确的参数文档 | +| 手动维护文档容易过期 | 自动从源码生成 | +| 文档太大占用上下文 | 每组件单独文件,按需加载 | +| 用户不知道如何引用 | 提供项目模板 | + +### 混合文档策略 + +``` +AI 代理工作流程: +1. 读取 llms.txt (轻量索引) → 快速了解组件列表 +2. 按需读取 components/{Component}.txt → 获取精确的 API 参考 +3. 不确定时查阅 GitHub 源码 → 获取准确信息 +4. 参考 Samples 目录 → 学习官方用法 +```