Skip to content
Merged
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,57 @@

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

namespace Elastic.Markdown.IO;
namespace Elastic.Documentation.Navigation;

public interface IPositionalNavigation
public interface INavigationTraversable
{
ConditionalWeakTable<MarkdownFile, INavigationItem> MarkdownNavigationLookup { get; }
ConditionalWeakTable<IDocumentationFile, INavigationItem> NavigationDocumentationFileLookup { get; }
FrozenDictionary<int, INavigationItem> NavigationIndexedByOrder { get; }
FrozenDictionary<string, ILeafNavigationItem<MarkdownFile>> NavigationIndexedByCrossLink { get; }

INavigationItem? GetPrevious(MarkdownFile current)
IEnumerable<INavigationItem> YieldAll()
{
if (NavigationIndexedByOrder.Count == 0)
yield break;
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);
}

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

private INavigationItem? GetPrevious(INavigationItem currentNavigation)
{
var currentNavigation = GetCurrent(current);
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);
} while (index >= 0);

return null;
}

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

private INavigationItem? GetNext(INavigationItem currentNavigation)
{
var currentNavigation = GetCurrent(current);
var index = currentNavigation.NavigationIndex;
do
{
Expand All @@ -44,9 +67,9 @@ public interface IPositionalNavigation
return null;
}

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

INavigationItem[] GetParents(INavigationItem current)
{
Expand All @@ -65,6 +88,6 @@ INavigationItem[] GetParents(INavigationItem current)
return [.. parents];
}

INavigationItem[] GetParentsOfMarkdownFile(MarkdownFile file) =>
MarkdownNavigationLookup.TryGetValue(file, out var navigation) ? GetParents(navigation) : [];
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,8 +383,8 @@ 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;
var currentNavigation = navigation.GetCurrent(file);
INavigationTraversable navigation = fileContext.DocumentationSet;
var currentNavigation = navigation.GetNavigationFor(file);
var url = currentNavigation.Url;

if (url is "/docs" or "/docs/404")
Expand Down
13 changes: 7 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,18 @@ 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.GetNavigationFor(markdown);

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.GetNavigationFor(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