diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs index 35e26379c..6d8c7a6bf 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs @@ -21,16 +21,12 @@ public void CreateNavigationItem( List groups, List navigationItems, int depth, - bool inNav, ref int fileIndex, int index) { var detectionRulesFolder = (RulesFolderReference)tocItem; var children = detectionRulesFolder.Children; - var group = new DocumentationGroup(Build, lookups with { TableOfContents = children }, ref fileIndex, depth + 1, inNav) - { - Parent = parent - }; + var group = new DocumentationGroup(Build, lookups with { TableOfContents = children }, ref fileIndex, depth + 1, null, parent); groups.Add(group); navigationItems.Add(new GroupNavigation(index, depth, group)); } diff --git a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs index 3fa32ac4a..fef9e9129 100644 --- a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs @@ -19,7 +19,6 @@ void CreateNavigationItem( List groups, List navigationItems, int depth, - bool inNav, ref int fileIndex, int index ); diff --git a/src/Elastic.Markdown/Helpers/Htmx.cs b/src/Elastic.Markdown/Helpers/Htmx.cs index c4d4bd377..d67c768e2 100644 --- a/src/Elastic.Markdown/Helpers/Htmx.cs +++ b/src/Elastic.Markdown/Helpers/Htmx.cs @@ -9,47 +9,25 @@ namespace Elastic.Markdown.Helpers; public static class Htmx { - public static string GetHxSelectOob(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + public static string GetHxSelectOob(bool hasSameTopLevelGroup) { - if (features.IsPrimaryNavEnabled && currentUrl == pathPrefix + "/") - return "#main-container,#primary-nav,#secondary-nav"; - - var selectTargets = "#primary-nav,#secondary-nav,#content-container,#toc-nav"; - if (!HasSameTopLevelGroup(pathPrefix, currentUrl, targetUrl) && features.IsPrimaryNavEnabled) + var selectTargets = "#content-container,#toc-nav"; + if (!hasSameTopLevelGroup) selectTargets += ",#pages-nav"; return selectTargets; } - public static bool HasSameTopLevelGroup(string? pathPrefix, string currentUrl, string targetUrl) - { - if (string.IsNullOrEmpty(targetUrl) || string.IsNullOrEmpty(currentUrl)) - return false; - var startIndex = pathPrefix?.Length ?? 0; - - if (currentUrl.Length < startIndex) - throw new InvalidUrlException("Unexpected current URL", currentUrl, startIndex); - - if (targetUrl.Length < startIndex) - throw new InvalidUrlException("Unexpected target URL", targetUrl, startIndex); - - var currentSegments = GetSegments(currentUrl[startIndex..].Trim('/')); - var targetSegments = GetSegments(targetUrl[startIndex..].Trim('/')); - return currentSegments.Length >= 1 && targetSegments.Length >= 1 && currentSegments[0] == targetSegments[0]; - } - public static string GetPreload() => "true"; - public static string GetHxSwap() => "none"; - public static string GetHxPushUrl() => "true"; - public static string GetHxIndicator() => "#htmx-indicator"; + private static string GetHxSwap() => "none"; + private static string GetHxPushUrl() => "true"; + private static string GetHxIndicator() => "#htmx-indicator"; - private static string[] GetSegments(string url) => url.Split('/'); - - public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + public static string GetHxAttributes(string targetUrl, bool hasSameTopLevelGroup) { var attributes = new StringBuilder(); _ = attributes.Append($" hx-get={targetUrl}"); - _ = attributes.Append($" hx-select-oob={GetHxSelectOob(features, pathPrefix, currentUrl, targetUrl)}"); + _ = attributes.Append($" hx-select-oob={GetHxSelectOob(hasSameTopLevelGroup)}"); _ = attributes.Append($" hx-swap={GetHxSwap()}"); _ = attributes.Append($" hx-push-url={GetHxPushUrl()}"); _ = attributes.Append($" hx-indicator={GetHxIndicator()}"); @@ -57,14 +35,3 @@ public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, return attributes.ToString(); } } - - -internal sealed class InvalidUrlException : ArgumentException -{ - public InvalidUrlException(string message, string url, int startIndex) - : base($"{message} (Url: {url}, StartIndex: {startIndex})") - { - Data["Url"] = url; - Data["StartIndex"] = startIndex; - } -} diff --git a/src/Elastic.Markdown/IO/Configuration/ITocItem.cs b/src/Elastic.Markdown/IO/Configuration/ITocItem.cs index d80fef748..5aa3d025a 100644 --- a/src/Elastic.Markdown/IO/Configuration/ITocItem.cs +++ b/src/Elastic.Markdown/IO/Configuration/ITocItem.cs @@ -12,8 +12,8 @@ public interface ITocItem public record FileReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool Hidden, IReadOnlyCollection Children) : ITocItem; -public record FolderReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool InNav, IReadOnlyCollection Children) +public record FolderReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection Children) : ITocItem; -public record TocReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool InNav, IReadOnlyCollection Children) - : FolderReference(TableOfContentsScope, Path, Found, InNav, Children); +public record TocReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection Children) + : FolderReference(TableOfContentsScope, Path, Found, Children); diff --git a/src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs b/src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs index e5a8c6857..a94d04ade 100644 --- a/src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs +++ b/src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Runtime.InteropServices; using Elastic.Markdown.Extensions.DetectionRules; +using Elastic.Markdown.IO.Navigation; using YamlDotNet.RepresentationModel; namespace Elastic.Markdown.IO.Configuration; @@ -81,7 +82,6 @@ public IReadOnlyCollection ReadChildren(YamlStreamReader reader, KeyVa var folderFound = false; var detectionRulesFound = false; var hiddenFile = false; - var inNav = false; IReadOnlyCollection? children = null; foreach (var entry in tocEntry.Children) { @@ -91,10 +91,6 @@ public IReadOnlyCollection ReadChildren(YamlStreamReader reader, KeyVa case "toc": toc = ReadNestedToc(reader, entry, parentPath, out fileFound); break; - case "in_nav": - if (!bool.TryParse(reader.ReadString(entry), out inNav)) - throw new ArgumentException("in_nav must be a boolean"); - break; case "hidden": case "file": hiddenFile = key == "hidden"; @@ -122,7 +118,7 @@ public IReadOnlyCollection ReadChildren(YamlStreamReader reader, KeyVa foreach (var f in toc.Files) _ = Files.Add(f); - return [new TocReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, toc.TableOfContents)]; + return [new TocReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, toc.TableOfContents)]; } if (file is not null) @@ -151,7 +147,7 @@ public IReadOnlyCollection ReadChildren(YamlStreamReader reader, KeyVa if (children is null) _ = _configuration.ImplicitFolders.Add(parentPath.TrimStart(Path.DirectorySeparatorChar)); - return [new FolderReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, children ?? [])]; + return [new FolderReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, children ?? [])]; } return null; diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index fd0af16ff..3f3e74ef8 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -8,9 +8,7 @@ using Elastic.Markdown.CrossLinks; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Extensions; -using Elastic.Markdown.Extensions.DetectionRules; using Elastic.Markdown.IO.Configuration; -using Elastic.Markdown.IO.Discovery; using Elastic.Markdown.IO.Navigation; using Elastic.Markdown.Myst; using Microsoft.Extensions.Logging; @@ -111,10 +109,7 @@ public DocumentationSet(BuildContext build, ILoggerFactory logger, ICrossLinkRes FilesGroupedByFolder = FilesGroupedByFolder }; - Tree = new DocumentationGroup(Build, lookups, false, ref fileIndex) - { - Parent = null - }; + Tree = new DocumentationGroup(Build, lookups, ref fileIndex); var markdownFiles = Files.OfType().ToArray(); diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index c6fb57467..927939776 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -19,7 +19,7 @@ namespace Elastic.Markdown.IO; -public record MarkdownFile : DocumentationFile +public record MarkdownFile : DocumentationFile, INavigationScope, ITableOfContentsScope { private string? _navigationTitle; @@ -49,8 +49,13 @@ DocumentationSet set //may be updated by DocumentationGroup.ProcessTocItems //todo refactor mutability of MarkdownFile as a whole ScopeDirectory = build.Configuration.ScopeDirectory; + RootNavigation = set.Tree; } + public IDirectoryInfo ScopeDirectory { get; set; } + + public INavigation RootNavigation { get; set; } + public string Id { get; } = Guid.NewGuid().ToString("N")[..8]; private DiagnosticsCollector Collector { get; } @@ -130,28 +135,10 @@ public MarkdownFile[] YieldParents() return [.. parents]; } - public string[] YieldParentGroups() - { - var parents = new List(); - if (GroupId is not null) - parents.Add(GroupId); - var parent = Parent; - do - { - if (parent is not null) - parents.Add(parent.Id); - parent = parent?.Parent; - } while (parent != null); - - return [.. parents]; - } - /// this get set by documentationset when validating redirects /// because we need to minimally parse to see the anchors anchor validation is deferred. public IReadOnlyDictionary? AnchorRemapping { get; set; } - public IDirectoryInfo ScopeDirectory { get; set; } - private void ValidateAnchorRemapping() { if (AnchorRemapping is null) @@ -343,4 +330,5 @@ public string CreateHtml(MarkdownDocument document) _ = document.Remove(h1); return document.ToHtml(MarkdownParser.Pipeline); } + } diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index b8cf1049f..1cf7882ae 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -2,11 +2,7 @@ // 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.IO.Abstractions; -using System.Text.RegularExpressions; using Elastic.Markdown.Diagnostics; -using Elastic.Markdown.Extensions; -using Elastic.Markdown.Extensions.DetectionRules; using Elastic.Markdown.IO.Configuration; namespace Elastic.Markdown.IO.Navigation; @@ -28,10 +24,25 @@ public record FileNavigation(int Order, int Depth, MarkdownFile File) : INavigat public string Id { get; } = File.Id; } -public class DocumentationGroup +public interface INavigation +{ + string Id { get; } + IReadOnlyCollection NavigationItems { get; } + int Depth { get; } + string? IndexFileName { get; } +} + +public interface INavigationScope +{ + INavigation RootNavigation { get; } +} + +public class DocumentationGroup : INavigation { public string Id { get; } = Guid.NewGuid().ToString("N")[..8]; + public string NavigationRootId { get; } + public MarkdownFile? Index { get; set; } private IReadOnlyCollection FilesInOrder { get; } @@ -40,18 +51,14 @@ public class DocumentationGroup public IReadOnlyCollection NavigationItems { get; } - public required DocumentationGroup? Parent { get; init; } + public string? IndexFileName => Index?.FileName; public int Depth { get; } - public bool InNav { get; } + public DocumentationGroup? Parent { get; } - public DocumentationGroup( - BuildContext context, - NavigationLookups lookups, - bool inNav, - ref int fileIndex) - : this(context, lookups, ref fileIndex, depth: 0, inNav) + public DocumentationGroup(BuildContext context, NavigationLookups lookups, ref int fileIndex) + : this(context, lookups, ref fileIndex, depth: 0, null, null) { } @@ -60,15 +67,21 @@ internal DocumentationGroup( NavigationLookups lookups, ref int fileIndex, int depth, - bool inNav, - MarkdownFile? index = null) + DocumentationGroup? topLevelGroup, + DocumentationGroup? parent, + MarkdownFile? index = null + ) { Depth = depth; - Index = ProcessTocItems(context, index, lookups, depth, ref fileIndex, out var groups, out var files, out var navigationItems); + Parent = parent; + topLevelGroup ??= this; + if (parent?.Depth == 0) + topLevelGroup = this; + NavigationRootId = topLevelGroup.Id; + Index = ProcessTocItems(context, topLevelGroup, index, lookups, depth, ref fileIndex, out var groups, out var files, out var navigationItems); if (Index is not null) Index.GroupId = Id; - InNav = inNav; GroupsInOrder = groups; FilesInOrder = files; NavigationItems = navigationItems; @@ -79,6 +92,7 @@ internal DocumentationGroup( private MarkdownFile? ProcessTocItems( BuildContext context, + DocumentationGroup topLevelGroup, MarkdownFile? configuredIndex, NavigationLookups lookups, int depth, @@ -117,20 +131,20 @@ internal DocumentationGroup( var navigationIndex = Interlocked.Increment(ref fileIndex); md.NavigationIndex = navigationIndex; md.ScopeDirectory = file.TableOfContentsScope.ScopeDirectory; + md.RootNavigation = topLevelGroup; foreach (var extension in context.Configuration.EnabledExtensions) extension.Visit(d, tocItem); - if (file.Children.Count > 0 && d is MarkdownFile virtualIndex) { if (file.Hidden) context.EmitError(context.ConfigurationPath, $"The following file is hidden but has children: {file.Path}"); var group = new DocumentationGroup( - context, lookups with { TableOfContents = file.Children }, ref fileIndex, depth + 1, InNav, virtualIndex) - { - Parent = this - }; + context, lookups with + { + TableOfContents = file.Children + }, ref fileIndex, depth + 1, topLevelGroup, this, virtualIndex); groups.Add(group); navigationItems.Add(new GroupNavigation(index, depth, group)); continue; @@ -159,10 +173,10 @@ .. documentationFiles ]; } - var group = new DocumentationGroup(context, lookups with { TableOfContents = children }, ref fileIndex, depth + 1, folder.InNav) + var group = new DocumentationGroup(context, lookups with { - Parent = this - }; + TableOfContents = children + }, ref fileIndex, depth + 1, topLevelGroup, this); groups.Add(group); navigationItems.Add(new GroupNavigation(index, depth, group)); } @@ -171,7 +185,7 @@ .. documentationFiles foreach (var extension in lookups.EnabledExtensions) { if (extension.InjectsIntoNavigation(tocItem)) - extension.CreateNavigationItem(this, tocItem, lookups, groups, navigationItems, depth, false, ref fileIndex, index); + extension.CreateNavigationItem(this, tocItem, lookups, groups, navigationItems, depth, ref fileIndex, index); } } } diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index 4aacc0bc4..3e1e839f5 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -192,14 +192,23 @@ private static void ProcessInternalLink(LinkInline link, InlineProcessor process var file = ResolveFile(context, url); ValidateInternalUrl(processor, url, includeFrom, link, context); - if (link.IsImage && context.DocumentationFileLookup(context.MarkdownSourcePath) is MarkdownFile currentMarkdown) + if (context.DocumentationFileLookup(context.MarkdownSourcePath) is MarkdownFile currentMarkdown) { - //TODO make this an error once all offending repositories have been updated - if (!file.Directory!.FullName.StartsWith(currentMarkdown.ScopeDirectory.FullName + Path.DirectorySeparatorChar)) - processor.EmitHint(link, $"Image '{url}' is referenced out of table of contents scope '{currentMarkdown.ScopeDirectory}'."); + link.SetData(nameof(currentMarkdown.RootNavigation), currentMarkdown.RootNavigation); + + if (link.IsImage) + { + //TODO make this an error once all offending repositories have been updated + if (!file.Directory!.FullName.StartsWith(currentMarkdown.ScopeDirectory.FullName + Path.DirectorySeparatorChar)) + processor.EmitHint(link, $"Image '{url}' is referenced out of table of contents scope '{currentMarkdown.ScopeDirectory}'."); + } } + var linkMarkdown = context.DocumentationFileLookup(file) as MarkdownFile; + if (linkMarkdown is not null) + link.SetData($"Target{nameof(currentMarkdown.RootNavigation)}", linkMarkdown.RootNavigation); + ProcessLinkText(processor, link, linkMarkdown, anchor, url, file); UpdateLinkUrl(link, url, context, anchor, file); } diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index b2489f929..98414ac07 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -152,7 +152,7 @@ public MarkdownPipeline Pipeline .UseDirectives(this) .UseDefinitionLists() .UseEnhancedCodeBlocks() - .UseHtmxLinkInlineRenderer(Build) + .UseHtmxLinkInlineRenderer() .DisableHtml() .UseHardBreaks(); _ = builder.BlockParsers.TryRemove(); diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 8fae9da96..d44b68950 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; +using Elastic.Markdown.IO; using Elastic.Markdown.IO.Configuration; +using Elastic.Markdown.IO.Navigation; using Markdig; using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; @@ -11,7 +13,7 @@ namespace Elastic.Markdown.Myst.Renderers; -public class HtmxLinkInlineRenderer(BuildContext build) : LinkInlineRenderer +public class HtmxLinkInlineRenderer : LinkInlineRenderer { protected override void Write(HtmlRenderer renderer, LinkInline link) { @@ -24,14 +26,17 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) return; } + var currentRootNavigation = link.GetData(nameof(MarkdownFile.RootNavigation)) as INavigation; + var targetRootNavigation = link.GetData($"Target{nameof(MarkdownFile.RootNavigation)}") as INavigation; + _ = renderer.Write(" x is LinkInlineRenderer); - htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer(build)); + htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer()); } } } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 7fde86872..cd61dfe23 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -12,73 +12,76 @@ namespace Elastic.Markdown.Slices; -public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem) +public interface INavigationHtmlWriter { - private DocumentationSet DocumentationSet { get; } = documentationSet; - private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Build)); + Task RenderNavigation(INavigation currentRootNavigation, Cancel ctx = default); - private async Task RenderNavigation(string topLevelGroupId, MarkdownFile markdown, Cancel ctx = default) + async Task Render(NavigationViewModel model, Cancel ctx) { - var group = DocumentationSet.Tree.NavigationItems - .OfType() - .FirstOrDefault(i => i.Group.Id == topLevelGroupId)?.Group; - - var slice = Layout._TocTree.Create(new NavigationViewModel - { - Title = group?.Index?.NavigationTitle ?? DocumentationSet.Tree.Index?.NavigationTitle ?? "Docs", - TitleUrl = group?.Index?.Url ?? DocumentationSet.Tree.Index?.Url ?? DocumentationSet.Build.UrlPathPrefix ?? "/", - Tree = group ?? DocumentationSet.Tree, - CurrentDocument = markdown, - IsRoot = topLevelGroupId == DocumentationSet.Tree.Id, - Features = DocumentationSet.Configuration.Features, - TopLevelItems = DocumentationSet.Tree.NavigationItems.OfType().ToList() - }); + var slice = Layout._TocTree.Create(model); return await slice.RenderAsync(cancellationToken: ctx); } +} + +public class IsolatedBuildNavigationHtmlWriter(DocumentationSet set) : INavigationHtmlWriter +{ + private DocumentationSet Set { get; } = set; private readonly ConcurrentDictionary _renderedNavigationCache = []; + public async Task RenderNavigation(INavigation currentRootNavigation, Cancel ctx = default) + { + var navigation = Set.Configuration.Features.IsPrimaryNavEnabled + ? currentRootNavigation + : Set.Tree; + + if (_renderedNavigationCache.TryGetValue(navigation.Id, out var value)) + return value; + + var model = CreateNavigationModel(navigation); + value = await ((INavigationHtmlWriter)this).Render(model, ctx); + _renderedNavigationCache[navigation.Id] = value; + return value; + } + + private NavigationViewModel CreateNavigationModel(INavigation navigation) + { + if (navigation is not DocumentationGroup tree) + throw new InvalidOperationException("Expected a documentation group"); + + var isRoot = navigation.Id == tree.Id; + + return new NavigationViewModel + { + Title = tree.Index?.NavigationTitle ?? "Docs", + TitleUrl = tree.Index?.Url ?? Set.Build.UrlPathPrefix ?? "/", + Tree = tree, + IsRoot = isRoot, + IsPrimaryNavEnabled = Set.Configuration.Features.IsPrimaryNavEnabled, + TopLevelItems = Set.Tree.NavigationItems.OfType().ToList() + }; + } +} + +public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem, INavigationHtmlWriter? navigationHtmlWriter = null) +{ + private DocumentationSet DocumentationSet { get; } = documentationSet; + public INavigationHtmlWriter NavigationHtmlWriter { get; } = navigationHtmlWriter ?? new IsolatedBuildNavigationHtmlWriter(documentationSet); + private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Build)); + + public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { var document = await markdown.ParseFullAsync(ctx); return await RenderLayout(markdown, document, ctx); } - private static string GetTopLevelGroupId(MarkdownFile markdown) => - markdown.YieldParentGroups().Length > 1 - ? markdown.YieldParentGroups()[^2] - : markdown.YieldParentGroups()[0]; - - public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default) + private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default) { var html = markdown.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); - var topLevelNavigationItems = DocumentationSet.Tree.NavigationItems - .OfType() - .Select(i => i.Group); - - string? navigationHtml; - - if (DocumentationSet.Configuration.Features.IsPrimaryNavEnabled) - { - var topLevelGroupId = GetTopLevelGroupId(markdown); - if (!_renderedNavigationCache.TryGetValue(topLevelGroupId, out var value)) - { - value = await RenderNavigation(topLevelGroupId, markdown, ctx); - _renderedNavigationCache[topLevelGroupId] = value; - } - navigationHtml = value; - } - else - { - if (!_renderedNavigationCache.TryGetValue("root", out var value)) - { - value = await RenderNavigation(DocumentationSet.Tree.Id, markdown, ctx); - _renderedNavigationCache["root"] = value; - } - navigationHtml = value; - } + var navigationHtml = await NavigationHtmlWriter.RenderNavigation(markdown.RootNavigation, ctx); var previous = DocumentationSet.GetPrevious(markdown); var next = DocumentationSet.GetNext(markdown); @@ -97,7 +100,6 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, - TopLevelNavigationItems = [.. topLevelNavigationItems], NavigationHtml = navigationHtml, UrlPathPrefix = markdown.UrlPathPrefix, Applies = markdown.YamlFrontMatter?.AppliesTo, @@ -136,5 +138,4 @@ public async Task WriteAsync(IFileInfo outputFile, MarkdownFile markdown, IConve collector?.Collect(markdown, document, rendered); await writeFileSystem.File.WriteAllTextAsync(path, rendered, ctx); } - } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index 6a181cc02..04b17700e 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -6,12 +6,10 @@ { Title = $"Elastic Documentation: {Model.Title}", PageTocItems = Model.PageTocItems.Where(i => i is { Level: 2 or 3 }).ToList(), - Tree = Model.Tree, CurrentDocument = Model.CurrentDocument, Previous = Model.PreviousDocument, Next = Model.NextDocument, NavigationHtml = Model.NavigationHtml, - TopLevelNavigationItems = Model.TopLevelNavigationItems, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, AllowIndexing = Model.AllowIndexing, diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index c56dfb5b6..0610d39c3 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -22,7 +22,7 @@ @firstCrumb.NavigationTitle @@ -49,7 +49,7 @@ @item.NavigationTitle diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index 24afcafe0..ad7925436 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -1,106 +1,5 @@ -@using Elastic.Markdown.Helpers @inherits RazorSlice -@{ - string GetHxAttributes(string url) - { - return Htmx.GetHxAttributes(Model.Features, Model.UrlPathPrefix, Model.CurrentDocument.Url, url); - } - - var primaryNavViewModel = new PrimaryNavViewModel - { - Items = - [ - new PrimaryNavItemViewModel - { - Title = "Get Started", - HtmxAttributes = GetHxAttributes(Model.Link("/get-started")), - Url = Model.Link("/get-started"), - }, - new PrimaryNavItemViewModel - { - Title = "Solutions and use cases", - Url = Model.Link("/solutions"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions")), - DropdownItems = [ - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("elasticsearch-logo-color-64px.svg"), - IconAlt = "Search logo", - Title = "Search", - Description = "Build search experiences to help users find what they need instantly.", - Url = Model.Link("/solutions/search"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions/search")) - }, - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("observability-logo-color-64px.svg"), - IconAlt = "Observability logo", - Title = "Observability", - Description = "Unify monitoring for apps and infrastructure.", - Url = Model.Link("/solutions/observability"), - HtmxAttributes = Htmx.GetHxAttributes( - Model.Features, - Model.CurrentDocument.UrlPathPrefix, - Model.CurrentDocument.Url, - Model.Link("/solutions/observability") - ) - }, - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("security-logo-color-64px.svg"), - IconAlt = "Security logo", - Title = "Security", - Description = "Protect, investigate, and respond to cyber threats.", - Url = Model.Link("/solutions/security"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions/security")) - } - ] - }, - new PrimaryNavItemViewModel - { - Title = "Work with the Elastic Stack", - DropdownItems = [ - new PrimaryNavDropdownItemViewModel - { - Title = "Manage your data", - Description = "Ingest, enrich, and manage your data.", - Url = Model.Link("/manage-data"), - HtmxAttributes = GetHxAttributes(Model.Link("/manage-data")) - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Explore and analyze your data", - Description = "Query, shape, visualize, alert, and more.", - Url = Model.Link("/explore-analyze"), - HtmxAttributes = GetHxAttributes(Model.Link("/explore-analyze")) - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Deploy and manage Elastic", - Description = "Deploy, configure, manage, and upgrade clusters and deployments.", - Url = Model.Link("/deploy-manage"), - HtmxAttributes = GetHxAttributes(Model.Link("/deploy-manage")) - - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Manage your Cloud account", - Description = "Manage the settings for your Elastic Cloud account.", - Url = Model.Link("/cloud-account"), - HtmxAttributes = GetHxAttributes(Model.Link("/cloud-account")) - }, - ] - }, - new PrimaryNavItemViewModel - { - Title = "Reference", - HtmxAttributes = GetHxAttributes(Model.Link("/reference")), - Url = Model.Link("/reference"), - }, - ] - }; -}
@if (Model.Features.IsPrimaryNavEnabled) diff --git a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml index caa5cfef0..72753662f 100644 --- a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml @@ -4,8 +4,6 @@