Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions tools/BootstrapBlazor.LLMsDocsGenerator/ArgumentsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) BootstrapBlazor & Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone

using BootstrapBlazorLLMsDocsGenerator;
using System.CommandLine;

namespace BootstrapBlazor.LLMsDocsGenerator;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a namespace inconsistency: line 5 uses BootstrapBlazorLLMsDocsGenerator (no dots between words) while line 8 declares the namespace as BootstrapBlazor.LLMsDocsGenerator (with dots). This mismatch will cause a compilation error. The using statement should match the actual namespace of the DocsGenerator class, which appears to be BootstrapBlazorLLMsDocsGenerator based on DocsGenerator.cs line 6.

Suggested change
namespace BootstrapBlazor.LLMsDocsGenerator;
namespace BootstrapBlazorLLMsDocsGenerator;

Copilot uses AI. Check for mistakes.

internal static class ArgumentsHelper
{
public static ParseResult Parse(string[] args)
{
var rootFolderOption = new Option<string?>("--root") { Description = "Set the root folder of project" };

var rootCommand = new RootCommand("BootstrapBlazor LLMs Documentation Generator")
{
rootFolderOption
};

rootCommand.SetAction(async result =>
{
var rootFolder = result.GetValue(rootFolderOption);
if (string.IsNullOrEmpty(rootFolder))
{
return;
}
Comment on lines +24 to +27
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the root folder is not provided, the command silently exits without any error message or indication of what went wrong. This makes it difficult for users to understand that the --root parameter is required. Consider adding a descriptive error message or making the option required.

Copilot uses AI. Check for mistakes.

await DocsGenerator.GenerateAllAsync(rootFolder);
Comment on lines +23 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Handling of missing --root is very silent and may confuse users.

Right now, omitting --root makes the command exit silently. Since rootFolder is required for any useful work, consider either marking the option as required or displaying usage/help or an explicit error when it’s missing so users understand why nothing happened.

});

return rootCommand.Parse(args);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>10.0.0</Version>
<Version>10.0.1</Version>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SatelliteResourceLanguages>false</SatelliteResourceLanguages>
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SatelliteResourceLanguages property should be a semicolon-separated list of language codes (e.g., "en;zh") or an empty string, not a boolean value. Setting it to "false" is invalid. If the intention is to disable satellite resource assembly generation, the correct value should be an empty string or the property should be removed entirely.

Suggested change
<SatelliteResourceLanguages>false</SatelliteResourceLanguages>
<SatelliteResourceLanguages></SatelliteResourceLanguages>

Copilot uses AI. Check for mistakes.
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text.RegularExpressions;

namespace LlmsDocsGenerator;
namespace BootstrapBlazorLLMsDocsGenerator;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a namespace inconsistency in this file. Line 11 declares the namespace as BootstrapBlazorLLMsDocsGenerator (without dots), but the proper format used in ArgumentsHelper.cs line 8 is BootstrapBlazor.LLMsDocsGenerator (with dots). All files should use a consistent namespace pattern to avoid confusion and potential compilation issues.

Suggested change
namespace BootstrapBlazorLLMsDocsGenerator;
namespace BootstrapBlazor.LLMsDocsGenerator;

Copilot uses AI. Check for mistakes.

/// <summary>
/// Analyzes Blazor component source files using Roslyn
Expand Down
2 changes: 1 addition & 1 deletion tools/BootstrapBlazor.LLMsDocsGenerator/ComponentInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

namespace LlmsDocsGenerator;
namespace BootstrapBlazorLLMsDocsGenerator;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a namespace inconsistency in this file. Line 6 declares the namespace as BootstrapBlazorLLMsDocsGenerator (without dots), but the proper format used in ArgumentsHelper.cs line 8 is BootstrapBlazor.LLMsDocsGenerator (with dots). All files should use a consistent namespace pattern to avoid confusion and potential compilation issues.

Suggested change
namespace BootstrapBlazorLLMsDocsGenerator;
namespace BootstrapBlazor.LLMsDocsGenerator;

Copilot uses AI. Check for mistakes.

/// <summary>
/// Represents information about a Blazor component
Expand Down
229 changes: 22 additions & 207 deletions tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,241 +3,56 @@
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

namespace LlmsDocsGenerator;
namespace BootstrapBlazorLLMsDocsGenerator;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a namespace inconsistency in this file. Line 6 declares the namespace as BootstrapBlazorLLMsDocsGenerator (without dots), but the proper format used in ArgumentsHelper.cs line 8 is BootstrapBlazor.LLMsDocsGenerator (with dots). All files should use a consistent namespace pattern to avoid confusion and potential compilation issues.

Suggested change
namespace BootstrapBlazorLLMsDocsGenerator;
namespace BootstrapBlazor.LLMsDocsGenerator;

Copilot uses AI. Check for mistakes.

/// <summary>
/// Main documentation generator class
/// </summary>
public class DocsGenerator
internal static 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.");
}

/// <summary>
/// Generate all documentation files
/// </summary>
public async Task GenerateAllAsync()
public static async Task GenerateAllAsync(string rootFolder)
{
var _sourcePath = Path.Combine(rootFolder, "..", "BootstrapBlazor");
var _outputPath = Path.Combine(rootFolder, "bin", "Release", "net10.0", "publish", "wwwroot", "llms");
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target framework version "net10.0" is hard-coded in the output path. This makes the tool brittle - if the framework version changes or if the tool needs to support multiple framework versions, this path will break. Consider making this configurable or dynamically detecting the framework version.

Suggested change
var _outputPath = Path.Combine(rootFolder, "bin", "Release", "net10.0", "publish", "wwwroot", "llms");
var releaseDir = Path.Combine(rootFolder, "bin", "Release");
string targetFrameworkDir;
if (Directory.Exists(releaseDir))
{
targetFrameworkDir = Path.Combine(releaseDir, "net10.0");
foreach (var dir in Directory.GetDirectories(releaseDir))
{
var frameworkName = Path.GetFileName(dir);
if (frameworkName.StartsWith("net", StringComparison.OrdinalIgnoreCase))
{
targetFrameworkDir = dir;
break;
}
}
}
else
{
targetFrameworkDir = Path.Combine(releaseDir, "net10.0");
}
var _outputPath = Path.Combine(targetFrameworkDir, "publish", "wwwroot", "llms");

Copilot uses AI. Check for mistakes.
var _componentsOutputPath = Path.Combine(_outputPath, "components");

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)
if (!Directory.Exists(_sourcePath))
{
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the source path doesn't exist, the method silently returns without any error message or logging. This makes it difficult to debug why documentation generation failed. Consider adding an error message or throwing an exception to inform the user that the source path was not found.

Suggested change
{
{
Logger($"Source path not found: {_sourcePath}. Documentation generation aborted.");

Copilot uses AI. Check for mistakes.
await GenerateComponentDocAsync(component);
}

Logger("Documentation generation complete!");
}

/// <summary>
/// Generate only the index file
/// </summary>
public async Task GenerateIndexAsync()
{
var components = await _analyzer.AnalyzeAllComponentsAsync();
await GenerateIndexAsync(components);
}

private async Task GenerateIndexAsync(List<ComponentInfo> 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}");
}

/// <summary>
/// Generate documentation for a specific component
/// </summary>
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(_outputPath);
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}");
}

/// <summary>
/// Check if documentation is up-to-date
/// </summary>
public async Task<bool> CheckAsync()
{
Logger("Checking documentation freshness...");
Logger("Analyzing components...");

var _analyzer = new ComponentAnalyzer(_sourcePath);
var components = await _analyzer.AnalyzeAllComponentsAsync();
Logger($"Found {components.Count} components");

// 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;
}
var content = MarkdownBuilder.BuildIndexDoc(components);
await File.WriteAllTextAsync(indexPath, content);
Logger($"Generated: {indexPath}");

// Check each component file
Logger("Generating individual component documentation...");
foreach (var component in components)
{
content = MarkdownBuilder.BuildComponentDoc(component);
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;
}
await File.WriteAllTextAsync(filePath, content);
Logger($"Generated: {filePath}");
}

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"
};
Logger("Documentation generation complete!");
}

private void Logger(string message)
private static void Logger(string message)
{
if (_debug)
{
Console.WriteLine(message);
}
Console.WriteLine(message);
}
}
Loading
Loading