diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index 49168cca1..ff957f6c8 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -99,6 +99,36 @@ public async Task ResolveDirectoryTree(Cancel ctx) _logger.LogInformation("Resolving tree"); await DocumentationSet.Tree.Resolve(ctx); _logger.LogInformation("Resolved tree"); + ReportDuplicateTitles(DocumentationSet.Tree.ResolvedMarkdownFiles); + } + + private void ReportDuplicateTitles(List files) + { + // Create a dictionary where keys are the titles + // and values are files with that title + var titleMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var file in files) + { + if (string.IsNullOrWhiteSpace(file.Title)) + continue; + // If there is no entry for this title, create it and + // initialize it to an empty list + if (!titleMap.TryGetValue(file.Title, out var list)) + titleMap[file.Title] = []; + titleMap[file.Title].Add(file); + } + // Go through all the titles and if a title has multiple files, report it + foreach (var kv in titleMap) + { + var documentFiles = kv.Value; + if (documentFiles.Count > 1) + { + var fileList = string.Join(", ", documentFiles.Select(f => f.RelativePath)); + foreach (var documentFile in documentFiles) + Context.Collector.EmitHint(documentFile.RelativePath, + $"Duplicate titles found. The title '{kv.Key}' is used in files: {{{fileList}}}"); + } + } } public async Task GenerateAll(Cancel ctx) diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index 038b857be..442901c67 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -46,6 +46,8 @@ public class DocumentationGroup : INodeNavigationItem? _root; + public List ResolvedMarkdownFiles { get; set; } + protected virtual IRootNavigationItem DefaultNavigation => _root ?? throw new InvalidOperationException("root navigation's model is not of type MarkdownFile"); @@ -70,6 +72,7 @@ protected DocumentationGroup(string folderName, // We'll need to address this more structurally // ReSharper disable VirtualMemberCallInConstructor _root = toplevelTree; + ResolvedMarkdownFiles = []; toplevelTree ??= DefaultNavigation; if (parent?.Depth == 0) toplevelTree = DefaultNavigation; @@ -225,10 +228,22 @@ public async Task Resolve(Cancel ctx = default) if (_resolved) return; - await Parallel.ForEachAsync(FilesInOrder, ctx, async (file, token) => await file.MinimalParseAsync(token)); - await Parallel.ForEachAsync(GroupsInOrder, ctx, async (group, token) => await group.Resolve(token)); + // First add the index file + ResolvedMarkdownFiles.Add(Index); + // Then add all the files in this group + ResolvedMarkdownFiles.AddRange(FilesInOrder); + // Then add all files in subgroups, breadth first + var treeGroups = new Queue(GroupsInOrder); + while (treeGroups.Count > 0) + { + var group = treeGroups.Dequeue(); + ResolvedMarkdownFiles.Add(group.Index); + ResolvedMarkdownFiles.AddRange(group.FilesInOrder); + foreach (var subgroup in group.GroupsInOrder) + treeGroups.Enqueue(subgroup); + } - _ = await Index.MinimalParseAsync(ctx); + await Parallel.ForEachAsync(ResolvedMarkdownFiles, ctx, async (file, token) => await file.MinimalParseAsync(token)); _resolved = true; }