diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index 020fefd65..282195508 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -46,7 +46,8 @@ public DocumentationGenerator( IDocumentationFileOutputProvider? documentationFileOutputProvider = null, IDocumentationFileExporter? documentationExporter = null, IConversionCollector? conversionCollector = null, - IHistoryMapper? historyMapper = null + IHistoryMapper? historyMapper = null, + IPositionalNavigation? positionalNavigation = null ) { _documentationFileOutputProvider = documentationFileOutputProvider; @@ -57,7 +58,7 @@ public DocumentationGenerator( DocumentationSet = docSet; Context = docSet.Build; Resolver = docSet.LinkResolver; - HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter, historyMapper); + HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter, historyMapper, positionalNavigation); _documentationFileExporter = documentationExporter ?? docSet.Build.Configuration.EnabledExtensions.FirstOrDefault(e => e.FileExporter != null)?.FileExporter diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 566aca153..25c699069 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -24,6 +24,12 @@ public interface INavigationLookups FrozenDictionary FilesGroupedByFolder { get; } } +public interface IPositionalNavigation +{ + MarkdownFile? GetPrevious(MarkdownFile current); + MarkdownFile? GetNext(MarkdownFile current); +} + public record NavigationLookups : INavigationLookups { public required FrozenDictionary FlatMappedFiles { get; init; } @@ -33,7 +39,7 @@ public record NavigationLookups : INavigationLookups //public required FrozenDictionary IndexedTableOfContents { get; init; } } -public class DocumentationSet : INavigationLookups +public class DocumentationSet : INavigationLookups, IPositionalNavigation { public BuildContext Build { get; } public string Name { get; } diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 3db1e1bf6..5c89eb74f 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -144,7 +144,7 @@ public string Url } } - public int NavigationIndex { get; internal set; } = -1; + public int NavigationIndex { get; set; } = -1; public string? GroupId { get; set; } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 1c603253f..3fe812a70 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -68,12 +68,16 @@ public class HtmlWriter( IFileSystem writeFileSystem, IDescriptionGenerator descriptionGenerator, INavigationHtmlWriter? navigationHtmlWriter = null, - IHistoryMapper? historyMapper = null) + IHistoryMapper? historyMapper = null, + IPositionalNavigation? positionalNavigation = null +) { private DocumentationSet DocumentationSet { get; } = documentationSet; public INavigationHtmlWriter NavigationHtmlWriter { get; } = navigationHtmlWriter ?? new IsolatedBuildNavigationHtmlWriter(documentationSet); private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Build)); private IHistoryMapper HistoryMapper { get; } = historyMapper ?? new BypassHistoryMapper(); + private IPositionalNavigation PositionalNavigation { get; } = positionalNavigation ?? documentationSet; + public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { var document = await markdown.ParseFullAsync(ctx); @@ -87,8 +91,8 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument var navigationHtml = await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, markdown.NavigationSource, ctx); - var previous = DocumentationSet.GetPrevious(markdown); - var next = DocumentationSet.GetNext(markdown); + var previous = PositionalNavigation.GetPrevious(markdown); + var next = PositionalNavigation.GetNext(markdown); var remote = DocumentationSet.Build.Git.RepositoryName; var branch = DocumentationSet.Build.Git.Branch; diff --git a/src/docs-assembler/Building/AssemblerBuilder.cs b/src/docs-assembler/Building/AssemblerBuilder.cs index 70bf2478a..0ee958fe3 100644 --- a/src/docs-assembler/Building/AssemblerBuilder.cs +++ b/src/docs-assembler/Building/AssemblerBuilder.cs @@ -13,10 +13,10 @@ namespace Documentation.Assembler.Building; public class AssemblerBuilder( ILoggerFactory logger, AssembleContext context, + GlobalNavigation navigation, GlobalNavigationHtmlWriter writer, GlobalNavigationPathProvider pathProvider, - IHistoryMapper? historyMapper -) + IHistoryMapper? historyMapper) { private GlobalNavigationHtmlWriter HtmlWriter { get; } = writer; @@ -58,7 +58,13 @@ public async Task BuildAllAsync(FrozenDictionary BuildAll( var historyMapper = new PageHistoryMapper(assembleSources.HistoryMappings); - var builder = new AssemblerBuilder(logger, assembleContext, htmlWriter, pathProvider, historyMapper); + var builder = new AssemblerBuilder(logger, assembleContext, navigation, htmlWriter, pathProvider, historyMapper); await builder.BuildAllAsync(assembleSources.AssembleSets, ctx); var sitemapBuilder = new SitemapBuilder(navigation.NavigationItems, assembleContext.WriteFileSystem, assembleContext.OutputDirectory); diff --git a/src/docs-assembler/Navigation/GlobalNavigation.cs b/src/docs-assembler/Navigation/GlobalNavigation.cs index f2de93b80..f91eaf2c7 100644 --- a/src/docs-assembler/Navigation/GlobalNavigation.cs +++ b/src/docs-assembler/Navigation/GlobalNavigation.cs @@ -9,7 +9,7 @@ namespace Documentation.Assembler.Navigation; -public record GlobalNavigation +public record GlobalNavigation : IPositionalNavigation { private readonly AssembleSources _assembleSources; private readonly GlobalNavigationFile _navigationFile; @@ -25,8 +25,40 @@ public GlobalNavigation(AssembleSources assembleSources, GlobalNavigationFile na _assembleSources = assembleSources; _navigationFile = navigationFile; NavigationItems = BuildNavigation(navigationFile.TableOfContents, 0); + var navigationIndex = 0; + var markdownFiles = new HashSet(); + UpdateNavigationIndex(markdownFiles, NavigationItems, ref navigationIndex); TopLevelItems = NavigationItems.OfType().ToList(); NavigationLookup = TopLevelItems.ToDictionary(kv => kv.Source, kv => kv); + var grouped = markdownFiles.GroupBy(f => f.NavigationIndex).ToList(); + var files = grouped + .Select(g => g.First()) + .ToList(); + + MarkdownFiles = files.Where(f => f.NavigationIndex > -1).ToDictionary(i => i.NavigationIndex, i => i).ToFrozenDictionary(); + } + + public FrozenDictionary MarkdownFiles { get; set; } + + private static void UpdateNavigationIndex(HashSet markdownFiles, IReadOnlyCollection navigationItems, ref int navigationIndex) + { + foreach (var item in navigationItems) + { + switch (item) + { + case FileNavigationItem fileNavigationItem: + var fileIndex = Interlocked.Increment(ref navigationIndex); + fileNavigationItem.File.NavigationIndex = fileIndex; + _ = markdownFiles.Add(fileNavigationItem.File); + break; + case GroupNavigationItem { Group.Index: not null } groupNavigationItem: + var index = Interlocked.Increment(ref navigationIndex); + groupNavigationItem.Group.Index.NavigationIndex = index; + _ = markdownFiles.Add(groupNavigationItem.Group.Index); + UpdateNavigationIndex(markdownFiles, groupNavigationItem.Group.NavigationItems, ref navigationIndex); + break; + } + } } private IReadOnlyCollection BuildNavigation(IReadOnlyCollection node, int depth) @@ -46,8 +78,8 @@ private IReadOnlyCollection BuildNavigation(IReadOnlyCollection continue; } - // TODO passing DocumentationSet to TableOfContentsTree constructr is temporary - // We only build this fallback in order to aid with bootstrapping the navigaton + // TODO passing DocumentationSet to TableOfContentsTree constructor is temporary + // We only build this fallback in order to aid with bootstrapping the navigation if (!_assembleSources.TreeCollector.TryGetTableOfContentsTree(topLevel.TopLevelSource, out tree)) { _navigationFile.EmitError( @@ -86,13 +118,14 @@ private IReadOnlyCollection BuildNavigation(IReadOnlyCollection var cleanNavigationItems = new List(); var seenSources = new HashSet(); - foreach (var allNavigationItem in allNavigationItems) + foreach (var item in allNavigationItems) { - if (allNavigationItem is not TocNavigationItem tocNav) + if (item is not TocNavigationItem tocNav) { - cleanNavigationItems.Add(allNavigationItem); + cleanNavigationItems.Add(item); continue; } + if (seenSources.Contains(tocNav.Source)) continue; @@ -103,20 +136,47 @@ private IReadOnlyCollection BuildNavigation(IReadOnlyCollection continue; _ = seenSources.Add(tocNav.Source); - cleanNavigationItems.Add(allNavigationItem); + cleanNavigationItems.Add(item); } tree.NavigationItems = cleanNavigationItems.ToArray(); var navigationItem = new TocNavigationItem(i, depth, tree, toc.Source); - if (toc.Source == new Uri("docs-content://reference")) - { - } - - list.Add(navigationItem); i++; } return list.ToArray().AsReadOnly(); } + + public MarkdownFile? GetPrevious(MarkdownFile current) + { + var index = current.NavigationIndex; + do + { + var previous = MarkdownFiles.GetValueOrDefault(index - 1); + if (previous is null) + return null; + if (!previous.Hidden) + return previous; + index--; + } while (index >= 0); + + return null; + } + + public MarkdownFile? GetNext(MarkdownFile current) + { + var index = current.NavigationIndex; + do + { + var previous = MarkdownFiles.GetValueOrDefault(index + 1); + if (previous is null) + return null; + if (!previous.Hidden) + return previous; + index++; + } while (index <= MarkdownFiles.Count); + + return null; + } }