using System.Text; using System.Text.RegularExpressions; using AtomUI.Docs.LLMsGenerator.Catalog; using AtomUI.Docs.LLMsGenerator.Markdown; namespace AtomUI.Docs.LLMsGenerator.Reader; public static partial class ControlDocReader { private static readonly IReadOnlyDictionary CategoryGalleryNames = new Dictionary(StringComparer.Ordinal) { ["data-display"] = "DataDisplay", ["data-entry"] = "DataEntry", ["feedback"] = "Feedback", ["general"] = "General", ["layout"] = "Layout", ["navigation"] = "Navigation", ["other"] = "Other", ["window"] = "Window" }; public static ControlDocModel Read(string repositoryRoot, ControlDocumentInfo control) { var overviewText = File.ReadAllText(control.OverviewPath); var implementationText = File.ReadAllText(control.ImplementationPath); var overview = MarkdownSectionParser.Parse(overviewText, control.OverviewPath); var implementation = MarkdownSectionParser.Parse(implementationText, control.ImplementationPath); var metadata = ReadMetadata(overview.GetRequiredSection("1. 控件定位").Content); var displayName = ReadDisplayName(overview.Title, control.Name); var galleryPath = ReadMetadataValue(metadata, "Gallery 页面"); if (string.IsNullOrWhiteSpace(galleryPath)) { galleryPath = FindGalleryPath(repositoryRoot, control, displayName); } var semanticPartsMarkdown = ExtractSemanticPartsMarkdown(overviewText); var semanticParts = ParseMarkdownTable(semanticPartsMarkdown); var templatePartsMarkdown = ExtractTableByLeadText(overviewText, "template part"); var pseudoClassesMarkdown = ExtractPseudoClassesMarkdown(overviewText); var sourceIndex = implementation.GetRequiredSection("2. 源码文件结构").Content.Trim(); var hasTokenDoc = control.TokenPath is not null; var tokenSourceDescription = ReadTokenDescription(control.TokenPath, overviewText, displayName, hasTokenDoc); return new ControlDocModel { ControlName = control.Name, DisplayName = displayName, Category = control.Category, SourceOverviewPath = control.OverviewPath, SourceImplementationPath = control.ImplementationPath, SourceTokenPath = control.TokenPath, SourceChangelogPath = control.ChangelogPath, OutputIndexPath = control.OutputIndexPath, OutputSemanticPath = control.OutputSemanticPath, PackageName = ReadMetadataValue(metadata, "NuGet 包", InferPackageName(control.Name)), DotNetNamespace = ReadMetadataValue(metadata, ".NET 命名空间", InferNamespace(control.Name)), AxamlNamespace = ReadMetadataValue(metadata, "AXAML 命名空间", "https://atomui.net"), GalleryPath = galleryPath, Status = ReadMetadataValue(metadata, "控件状态", "Stable"), SemanticParts = semanticParts.Count > 0 ? semanticParts : BuildFallbackSemanticParts(displayName, templatePartsMarkdown, tokenSourceDescription), SemanticPartsMarkdown = !string.IsNullOrWhiteSpace(semanticPartsMarkdown) ? semanticPartsMarkdown : FormatSemanticParts(BuildFallbackSemanticParts(displayName, templatePartsMarkdown, tokenSourceDescription)), TemplatePartsMarkdown = !string.IsNullOrWhiteSpace(templatePartsMarkdown) ? templatePartsMarkdown : "源文档未声明稳定 Template Part。维护模板时应以源码和主题文件中的实际声明为准。", AbstractAxamlStructureMarkdown = ControlThemeStructureReader.Read( repositoryRoot, control, displayName, sourceIndex), CompositionModelMarkdown = CompositionModelReader.Read( repositoryRoot, control, displayName, sourceIndex), PseudoClassesMarkdown = !string.IsNullOrWhiteSpace(pseudoClassesMarkdown) ? pseudoClassesMarkdown : "源文档未声明控件专属伪类。控件仍可能消费 Avalonia 标准状态,例如 `:pointerover`、`:pressed`、`:disabled` 和 focus 相关状态。", SourceIndex = sourceIndex, HasTokenDoc = hasTokenDoc, TokenSourceDescription = tokenSourceDescription, OverviewSection = overview.GetRequiredSection("1. 控件定位").Content.Trim(), DesignLanguageSection = overview.GetRequiredSection("2. 设计语言").Content.Trim(), ApiSection = overview.GetRequiredSection("3. API 与契约模型").Content.Trim(), StateSection = overview.GetRequiredSection("4. 行为与状态模型").Content.Trim(), ThemeSection = overview.GetRequiredSection("5. 视觉与主题模型").Content.Trim(), CompatibilitySection = overview.GetRequiredSection("7. 兼容性不变量").Content.Trim(), VerificationSection = GetRequiredSection( overview, "9. 文档导航、LLMS 导出与验证策略", "9. 文档导航与验证策略").Content.Trim(), ImplementationLifecycleSection = GetRequiredSection( implementation, "6. 生命周期与模板接入", "5. 生命周期与模板接入").Content.Trim(), ImplementationAotSection = GetRequiredSection( implementation, "9. 资源、性能与 AOT 边界", "8. 资源、性能与 AOT 边界").Content.Trim(), ImplementationInvariantsSection = GetRequiredSection( implementation, "10. 维护不变量", "9. 维护不变量").Content.Trim(), ImplementationTestsSection = GetRequiredSection( implementation, "11. 测试与验证", "10. 测试与验证").Content.Trim(), GalleryExamplesMarkdown = GalleryExampleReader.ReadMarkdown(repositoryRoot, galleryPath, displayName), SourceOverviewRelativePath = ToRelativePath(repositoryRoot, control.OverviewPath), SourceImplementationRelativePath = ToRelativePath(repositoryRoot, control.ImplementationPath), SourceTokenRelativePath = control.TokenPath is null ? null : ToRelativePath(repositoryRoot, control.TokenPath), SourceChangelogRelativePath = ToRelativePath(repositoryRoot, control.ChangelogPath), HasExplicitMetadata = metadata.ContainsKey("NuGet 包") && metadata.ContainsKey(".NET 命名空间") && metadata.ContainsKey("AXAML 命名空间") && metadata.ContainsKey("Gallery 页面") && metadata.ContainsKey("控件状态"), HasExplicitSemanticParts = !string.IsNullOrWhiteSpace(semanticPartsMarkdown), HasExplicitExportSourceTable = overviewText.Contains("LLMS 导出来源", StringComparison.Ordinal) && overviewText.Contains($"controls/{control.Name}/index-cn.md", StringComparison.Ordinal) && overviewText.Contains($"controls/{control.Name}/semantic-cn.md", StringComparison.Ordinal), HasExplicitTokenSourceDescription = hasTokenDoc || overviewText.Contains("Token 说明", StringComparison.Ordinal) || overviewText.Contains("没有专属 Token", StringComparison.Ordinal) || overviewText.Contains("复用", StringComparison.Ordinal) }; } public static IReadOnlyList ReadAll(string repositoryRoot, IReadOnlyList controls) { return controls.Select(control => Read(repositoryRoot, control)).ToArray(); } private static IReadOnlyDictionary ReadMetadata(string section) { var result = new Dictionary(StringComparer.Ordinal); foreach (var row in ParseMarkdownTable(section)) { if (row.Cells.Count >= 2) { result[row.Cells[0]] = row.Cells[1].Trim('`'); } } return result; } private static MarkdownSection GetRequiredSection(MarkdownDocument document, params string[] headings) { foreach (var heading in headings) { var match = document.Sections.FirstOrDefault(section => section.Heading == heading); if (match is not null) { return match; } } return document.GetRequiredSection(headings[0]); } private static string ReadMetadataValue( IReadOnlyDictionary metadata, string key, string fallback = "") { return metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : fallback; } private static string ReadDisplayName(string title, string controlName) { if (!string.IsNullOrWhiteSpace(title)) { var normalized = title.Replace(" 桌面版架构设计", string.Empty, StringComparison.Ordinal) .Replace("架构设计", string.Empty, StringComparison.Ordinal) .Trim(); if (!string.IsNullOrWhiteSpace(normalized)) { return normalized; } } return ToPascalName(controlName); } private static string InferPackageName(string controlName) { return controlName switch { "data-grid" => "AtomUI.Desktop.Controls.DataGrid", "color-picker" => "AtomUI.Desktop.Controls.ColorPicker", _ => "AtomUI.Desktop.Controls" }; } private static string InferNamespace(string controlName) { return controlName switch { "data-grid" => "AtomUI.Desktop.Controls.DataGrid", "color-picker" => "AtomUI.Desktop.Controls.ColorPicker", _ => "AtomUI.Desktop.Controls" }; } private static string FindGalleryPath(string repositoryRoot, ControlDocumentInfo control, string displayName) { if (string.IsNullOrWhiteSpace(control.GalleryRoot) || !Directory.Exists(control.GalleryRoot)) { return string.Empty; } var categoryName = CategoryGalleryNames.GetValueOrDefault(control.Category, ToPascalName(control.Category)); var categoryRoot = Path.Combine(control.GalleryRoot, categoryName); if (!Directory.Exists(categoryRoot)) { return string.Empty; } var expectedNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ToPascalName(control.Name), displayName, control.Name.Replace("-", string.Empty, StringComparison.Ordinal) }; var match = Directory.GetDirectories(categoryRoot) .FirstOrDefault(directory => expectedNames.Contains(Path.GetFileName(directory))); return match is null ? string.Empty : ToRelativePath(repositoryRoot, match); } private static string ReadTokenDescription(string? tokenPath, string overviewText, string displayName, bool hasTokenDoc) { if (tokenPath is not null) { var tokenText = File.ReadAllText(tokenPath); var tokenDocument = MarkdownSectionParser.Parse(tokenText, tokenPath); return tokenDocument.Sections.FirstOrDefault()?.Content.Trim() ?? $"{displayName} 使用专属 token.md 说明组件 Token 语义。"; } var tokenNote = ExtractLeadParagraphAfter(overviewText, "Token 说明"); if (!string.IsNullOrWhiteSpace(tokenNote)) { return tokenNote; } return $"{displayName} 当前没有专属 token.md;LLMS 输出按源文档中的主题模型、家族 Token、SharedToken 和主题资源说明 Token 边界。"; } private static string ExtractSemanticPartsMarkdown(string markdown) { var markerIndex = markdown.IndexOf("LLMS 语义区域", StringComparison.Ordinal); if (markerIndex < 0) { markerIndex = markdown.IndexOf("Semantic Parts", StringComparison.Ordinal); } if (markerIndex < 0) { return string.Empty; } return ExtractNextTable(markdown[markerIndex..]); } private static string ExtractTableByLeadText(string markdown, string leadText) { var lines = markdown.ReplaceLineEndings("\n").Split('\n'); for (var index = 0; index < lines.Length; index++) { if (!lines[index].Contains(leadText, StringComparison.OrdinalIgnoreCase)) { continue; } var table = ExtractNextTable(string.Join('\n', lines.Skip(index))); if (!string.IsNullOrWhiteSpace(table)) { return table; } } return string.Empty; } private static string ExtractPseudoClassesMarkdown(string markdown) { var lines = markdown.ReplaceLineEndings("\n").Split('\n'); for (var index = 0; index < lines.Length; index++) { if (!lines[index].Contains("伪类", StringComparison.Ordinal)) { continue; } var builder = new StringBuilder(); for (var current = index; current < Math.Min(lines.Length, index + 12); current++) { if (current > index && lines[current].StartsWith("## ", StringComparison.Ordinal)) { break; } builder.AppendLine(lines[current]); } return builder.ToString().Trim(); } return string.Empty; } private static string ExtractNextTable(string markdown) { var lines = markdown.ReplaceLineEndings("\n").Split('\n'); var tableLines = new List(); var inTable = false; foreach (var line in lines) { if (line.TrimStart().StartsWith('|')) { inTable = true; tableLines.Add(line); continue; } if (inTable) { break; } } return string.Join('\n', tableLines).Trim(); } private static IReadOnlyList ParseMarkdownTable(string markdown) { var rows = new List(); foreach (var line in markdown.ReplaceLineEndings("\n").Split('\n')) { var trimmed = line.Trim(); if (!trimmed.StartsWith('|') || !trimmed.EndsWith('|')) { continue; } if (SeparatorRowRegex().IsMatch(trimmed)) { continue; } var cells = trimmed.Trim('|') .Split('|') .Select(cell => cell.Trim()) .ToArray(); rows.Add(new MarkdownTableRow(cells)); } return rows; } private static IReadOnlyList BuildFallbackSemanticParts( string displayName, string templatePartsMarkdown, string tokenSourceDescription) { var rows = new List { new([ "`root`", $"`{displayName}`", "控件根语义区域,承载 public API、状态归一、主题入口和 Gallery 可观察行为。", "见公共 API", "见主题与 Token 边界", "stable" ]) }; var templateRows = ParseMarkdownTable(templatePartsMarkdown) .Where(row => row.Cells.Count >= 3 && row.Cells[0] != "Template Part") .Take(8) .ToArray(); foreach (var row in templateRows) { rows.Add(new MarkdownTableRow([ NormalizeSemanticName(row.Cells[0]), row.Cells[0], row.Cells[2], "见公共 API", tokenSourceDescription.Contains("没有专属 token.md", StringComparison.Ordinal) ? "SharedToken / 主题资源" : "组件 Token / SharedToken", "stable" ])); } if (rows.Count == 1) { rows.Add(new MarkdownTableRow([ "`content`", "抽象内容区域", "承载用户内容、集合项、输入值或可视反馈。", "见公共 API", "见主题与 Token 边界", "stable" ])); } return rows; } private static string FormatSemanticParts(IReadOnlyList rows) { var builder = new StringBuilder(); builder.AppendLine("| Part | AtomUI 节点 | 职责 | 相关 API | 相关 Token | 稳定性 |"); builder.AppendLine("| --- | --- | --- | --- | --- | --- |"); foreach (var row in rows.Where(row => row.Cells.Count >= 6)) { builder.AppendLine($"| {string.Join(" | ", row.Cells.Take(6))} |"); } return builder.ToString().TrimEnd(); } private static string NormalizeSemanticName(string templatePart) { var value = templatePart.Trim('`'); value = value.StartsWith("PART_", StringComparison.Ordinal) ? value[5..] : value; value = value.Replace("_", string.Empty, StringComparison.Ordinal); if (string.IsNullOrWhiteSpace(value)) { return "`part`"; } return $"`{char.ToLowerInvariant(value[0])}{value[1..]}`"; } private static string ExtractLeadParagraphAfter(string markdown, string marker) { var index = markdown.IndexOf(marker, StringComparison.Ordinal); if (index < 0) { return string.Empty; } var lines = markdown[index..].ReplaceLineEndings("\n").Split('\n').Skip(1); var paragraph = new List(); foreach (var line in lines) { if (string.IsNullOrWhiteSpace(line)) { if (paragraph.Count > 0) { break; } continue; } if (line.StartsWith("## ", StringComparison.Ordinal)) { break; } paragraph.Add(line); } return string.Join('\n', paragraph).Trim(); } private static string ToPascalName(string value) { return string.Concat(value.Split('-', StringSplitOptions.RemoveEmptyEntries) .Select(part => char.ToUpperInvariant(part[0]) + part[1..])); } private static string ToRelativePath(string repositoryRoot, string path) { return Path.GetRelativePath(repositoryRoot, path).Replace(Path.DirectorySeparatorChar, '/'); } [GeneratedRegex(@"^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|$")] private static partial Regex SeparatorRowRegex(); }