Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Navigation.Isolated.Leaf;
using Elastic.Documentation.Navigation.Isolated.Node;

namespace Elastic.Documentation.Navigation.Assembler;

[DebuggerDisplay("{Url}")]
public class SiteNavigation : IRootNavigationItem<IDocumentationFile, INavigationItem>
public class SiteNavigation : IRootNavigationItem<IDocumentationFile, INavigationItem>, INavigationTraversable
{
private readonly string? _sitePrefix;

Expand Down Expand Up @@ -51,7 +55,22 @@ public SiteNavigation(
UnseenNodes = [.. _nodes.Keys];
// Build NavigationItems from SiteTableOfContentsRef items
var items = new List<INavigationItem>();
var index = 0;
// The root file leafs of the narrative repository act as root leafs for the overall site
if (_nodes.TryGetValue(new Uri($"{NarrativeRepository.RepositoryName}://"), out var root))
{
if (root is INavigationHomeAccessor accessor)
accessor.HomeProvider = new NavigationHomeProvider(_sitePrefix ?? "/", this);
root.Parent = this;
root.Index.Parent = this;
items.Add(root.Index);
foreach (var leaf in root.NavigationItems.OfType<ILeafNavigationItem<INavigationModel>>())
{
leaf.Parent = root;
items.Add(leaf);
}
}

var index = items.Count;
foreach (var tocRef in siteNavigationFile.TableOfContents)
{
var navItem = CreateSiteTableOfContentsNavigation(
Expand Down Expand Up @@ -82,6 +101,9 @@ public SiteNavigation(
value.Parent = this;
}

// Build positional navigation lookup tables from all navigation items in a single traversal
NavigationDocumentationFileLookup = [];
NavigationIndexedByOrder = this.BuildNavigationLookups(NavigationDocumentationFileLookup);
}

public HashSet<Uri> DeclaredPhantoms { get; }
Expand Down Expand Up @@ -136,6 +158,12 @@ public SiteNavigation(
void IAssignableChildrenNavigation.SetNavigationItems(IReadOnlyCollection<INavigationItem> navigationItems) =>
throw new NotSupportedException("SetNavigationItems is not supported on SiteNavigation");

/// <inheritdoc />
public ConditionalWeakTable<IDocumentationFile, INavigationItem> NavigationDocumentationFileLookup { get; }

/// <inheritdoc />
public FrozenDictionary<int, INavigationItem> NavigationIndexedByOrder { get; }

/// <summary>
/// Normalizes the site prefix to ensure it has a leading slash and no trailing slash.
/// Returns null for null or empty/whitespace input.
Expand Down Expand Up @@ -270,4 +298,5 @@ void IAssignableChildrenNavigation.SetNavigationItems(IReadOnlyCollection<INavig
}
return node;
}

}
97 changes: 97 additions & 0 deletions src/Elastic.Documentation.Navigation/INavigationTraversable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using System.Runtime.CompilerServices;

namespace Elastic.Documentation.Navigation;

public interface INavigationTraversable
{
ConditionalWeakTable<IDocumentationFile, INavigationItem> NavigationDocumentationFileLookup { get; }
FrozenDictionary<int, INavigationItem> NavigationIndexedByOrder { get; }

IEnumerable<INavigationItem> YieldAll()
{
var current = NavigationIndexedByOrder.Values.First();
yield return current;
do
{
current = GetNext(current);
if (current is not null)
yield return current;

} while (current is not null);
}

/// <summary>
/// Type-safe helper to get navigation item for a specific documentation file type
/// </summary>
INavigationItem? GetNavigationItem<TFile>(TFile file) where TFile : IDocumentationFile =>
NavigationDocumentationFileLookup.TryGetValue(file, out var navigation) ? navigation : null;

INavigationItem? GetPrevious(IDocumentationFile current)
{
var currentNavigation = GetCurrent(current);
return GetPrevious(currentNavigation);
}

private INavigationItem? GetPrevious(INavigationItem currentNavigation)
{
var index = currentNavigation.NavigationIndex;
do
{
var previous = NavigationIndexedByOrder.GetValueOrDefault(index - 1);
if (previous is not null && !previous.Hidden && previous.Url != currentNavigation.Url)
return previous;
index--;
} while (index >= 0);

return null;
}

INavigationItem? GetNext(IDocumentationFile current)
{
var currentNavigation = GetCurrent(current);
return GetNext(currentNavigation);
}

private INavigationItem? GetNext(INavigationItem currentNavigation)
{
var index = currentNavigation.NavigationIndex;
do
{
var next = NavigationIndexedByOrder.GetValueOrDefault(index + 1);
if (next is not null && !next.Hidden && next.Url != currentNavigation.Url)
return next;
index++;
} while (index <= NavigationIndexedByOrder.Count - 1);

return null;
}

INavigationItem GetCurrent(IDocumentationFile file) =>
NavigationDocumentationFileLookup.TryGetValue(file, out var navigation)
? navigation : throw new InvalidOperationException($"Could not find {file.NavigationTitle} in navigation");

INavigationItem[] GetParents(INavigationItem current)
{
var parents = new List<INavigationItem>();
var parent = current.Parent;
do
{
if (parent is null)
continue;
if (parents.All(i => i.Url != parent.Url))
parents.Add(parent);

parent = parent.Parent;
} while (parent != null);

return [.. parents];
}

INavigationItem[] GetParentsOfMarkdownFile(IDocumentationFile file) =>
NavigationDocumentationFileLookup.TryGetValue(file, out var navigation) ? GetParents(navigation) : [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using System.Runtime.CompilerServices;
using Elastic.Documentation.Navigation.Isolated;
using Elastic.Documentation.Navigation.Isolated.Leaf;

namespace Elastic.Documentation.Navigation;

Expand Down Expand Up @@ -71,4 +74,55 @@ private static void ProcessNavigationItem(IDocumentationContext context, ref int
break;
}
}

/// <summary>
/// Builds navigation lookups by traversing the navigation tree and populating both the
/// NavigationDocumentationFileLookup and NavigationIndexedByOrder collections.
/// </summary>
/// <param name="rootItem">The root navigation item to start traversing from</param>
/// <param name="navigationDocumentationFileLookup">The ConditionalWeakTable to populate with file-to-navigation mappings</param>
/// <returns>A frozen dictionary mapping navigation indices to navigation items</returns>
public static FrozenDictionary<int, INavigationItem> BuildNavigationLookups(
this INavigationItem rootItem, ConditionalWeakTable<IDocumentationFile, INavigationItem> navigationDocumentationFileLookup
)
{
var navigationByOrder = new Dictionary<int, INavigationItem>();
BuildNavigationLookupsRecursive(rootItem, navigationDocumentationFileLookup, navigationByOrder);
return navigationByOrder.ToFrozenDictionary();
}

/// <summary>
/// Recursively builds both NavigationDocumentationFileLookup and NavigationIndexedByOrder in a single traversal
/// </summary>
private static void BuildNavigationLookupsRecursive(
INavigationItem item,
ConditionalWeakTable<IDocumentationFile, INavigationItem> navigationDocumentationFileLookup,
Dictionary<int, INavigationItem> navigationByOrder)
{
switch (item)
{
// CrossLinkNavigationLeaf is not added to NavigationDocumentationFileLookup or NavigationIndexedByOrder
case CrossLinkNavigationLeaf:
break;
case ILeafNavigationItem<IDocumentationFile> documentationFileLeaf:
_ = navigationDocumentationFileLookup.TryAdd(documentationFileLeaf.Model, documentationFileLeaf);
_ = navigationByOrder.TryAdd(documentationFileLeaf.NavigationIndex, documentationFileLeaf);
break;
case ILeafNavigationItem<INavigationModel> leaf:
_ = navigationByOrder.TryAdd(leaf.NavigationIndex, leaf);
break;
case INodeNavigationItem<IDocumentationFile, INavigationItem> documentationFileNode:
_ = navigationDocumentationFileLookup.TryAdd(documentationFileNode.Index.Model, documentationFileNode);
_ = navigationByOrder.TryAdd(documentationFileNode.NavigationIndex, documentationFileNode);
_ = navigationByOrder.TryAdd(documentationFileNode.Index.NavigationIndex, documentationFileNode.Index);
foreach (var child in documentationFileNode.NavigationItems)
BuildNavigationLookupsRecursive(child, navigationDocumentationFileLookup, navigationByOrder);
break;
case INodeNavigationItem<INavigationModel, INavigationItem> node:
_ = navigationByOrder.TryAdd(node.NavigationIndex, node);
foreach (var child in node.NavigationItems)
BuildNavigationLookupsRecursive(child, navigationDocumentationFileLookup, navigationByOrder);
break;
}
}
}
4 changes: 3 additions & 1 deletion src/Elastic.Markdown/DocumentationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration.LegacyUrlMappings;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Links;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Serialization;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
Expand Down Expand Up @@ -53,6 +54,7 @@ public class DocumentationGenerator
public DocumentationGenerator(
DocumentationSet docSet,
ILoggerFactory logFactory,
INavigationTraversable? positionalNavigation = null,
INavigationHtmlWriter? navigationHtmlWriter = null,
IDocumentationFileOutputProvider? documentationFileOutputProvider = null,
IMarkdownExporter[]? markdownExporters = null,
Expand All @@ -69,7 +71,7 @@ public DocumentationGenerator(
DocumentationSet = docSet;
Context = docSet.Context;
var productVersionInferrer = new ProductVersionInferrerService(DocumentationSet.Context.ProductsConfiguration, DocumentationSet.Context.VersionsConfiguration);
HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter, legacyUrlMapper, productVersionInferrer);
HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), positionalNavigation, navigationHtmlWriter, legacyUrlMapper, productVersionInferrer);
_documentationFileExporter =
docSet.Context.AvailableExporters.Contains(Exporter.Html)
? docSet.EnabledExtensions.FirstOrDefault(e => e.FileExporter != null)?.FileExporter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Synonyms;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Search;
using Elastic.Ingest.Elasticsearch;
using Elastic.Ingest.Elasticsearch.Indices;
Expand Down Expand Up @@ -382,7 +383,7 @@ private async ValueTask DoReindex(PostData request, string lexicalWriteAlias, st
public async ValueTask<bool> ExportAsync(MarkdownExportFileContext fileContext, Cancel ctx)
{
var file = fileContext.SourceFile;
IPositionalNavigation navigation = fileContext.DocumentationSet;
INavigationTraversable navigation = fileContext.DocumentationSet;
var currentNavigation = navigation.GetCurrent(file);
var url = currentNavigation.Url;

Expand Down
18 changes: 12 additions & 6 deletions src/Elastic.Markdown/HtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class HtmlWriter(
DocumentationSet documentationSet,
IFileSystem writeFileSystem,
IDescriptionGenerator descriptionGenerator,
INavigationTraversable? positionalNavigation = null,
INavigationHtmlWriter? navigationHtmlWriter = null,
ILegacyUrlMapper? legacyUrlMapper = null,
IVersionInferrerService? versionInferrerService = null
Expand All @@ -37,7 +38,7 @@ public class HtmlWriter(

private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Context));
private ILegacyUrlMapper LegacyUrlMapper { get; } = legacyUrlMapper ?? new NoopLegacyUrlMapper();
private IPositionalNavigation PositionalNavigation { get; } = documentationSet;
private INavigationTraversable NavigationTraversable { get; } = positionalNavigation ?? documentationSet;

private IVersionInferrerService VersionInferrerService { get; } = versionInferrerService ?? new NoopVersionInferrer();

Expand All @@ -59,18 +60,23 @@ private async Task<RenderResult> RenderLayout(MarkdownFile markdown, MarkdownDoc
{
var html = MarkdownFile.CreateHtml(document);
await DocumentationSet.ResolveDirectoryTree(ctx);
var navigationItem = DocumentationSet.FindNavigationByMarkdown(markdown);
var navigationItem = NavigationTraversable.GetNavigationItem(markdown);
if (navigationItem is null)
{
DocumentationSet.Context.EmitError(markdown.SourceFile, $"Unable to find navigation item for {markdown.RelativePath}");
throw new Exception($"Unable to find navigation item for {markdown.RelativePath}");
}

var root = navigationItem.NavigationRoot;

var navigationHtmlRenderResult = DocumentationSet.Context.Configuration.Features.LazyLoadNavigation
? await NavigationHtmlWriter.RenderNavigation(root, navigationItem, 1, ctx)
: await NavigationHtmlWriter.RenderNavigation(root, navigationItem, INavigationHtmlWriter.AllLevels, ctx);

var current = PositionalNavigation.GetCurrent(markdown);
var previous = PositionalNavigation.GetPrevious(markdown);
var next = PositionalNavigation.GetNext(markdown);
var parents = PositionalNavigation.GetParentsOfMarkdownFile(markdown);
var current = NavigationTraversable.GetCurrent(markdown);
var previous = NavigationTraversable.GetPrevious(markdown);
var next = NavigationTraversable.GetNext(markdown);
var parents = NavigationTraversable.GetParentsOfMarkdownFile(markdown);

var remote = DocumentationSet.Context.Git.RepositoryName;
var branch = DocumentationSet.Context.Git.Branch;
Expand Down
Loading
Loading