From d5fcff553a1aef6fd2d75fbb6e53c3b20ddad53c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 14 Apr 2025 09:42:13 +0200 Subject: [PATCH 1/4] stage --- .../IO/Configuration/ConfigurationFile.cs | 2 +- src/Elastic.Markdown/IO/DocumentationFile.cs | 32 ++++--- src/Elastic.Markdown/IO/DocumentationSet.cs | 83 ++++++++++++++++--- src/Elastic.Markdown/IO/MarkdownFile.cs | 32 +------ .../IO/Navigation/DocumentationGroup.cs | 48 +++++------ .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 4 +- src/Elastic.Markdown/Slices/HtmlWriter.cs | 8 +- src/Elastic.Markdown/Slices/Index.cshtml | 3 +- .../Slices/Layout/_Breadcrumbs.cshtml | 13 +-- src/Elastic.Markdown/Slices/_ViewModels.cs | 18 +--- .../Navigation/GlobalNavigation.cs | 40 +++++++-- .../Navigation/GlobalNavigationHtmlWriter.cs | 2 +- .../DocSet/BreadCrumbTests.cs | 17 +++- .../DocSet/NestedTocTests.cs | 15 +++- .../GlobalNavigationTests.cs | 6 ++ 15 files changed, 206 insertions(+), 117 deletions(-) diff --git a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs index 2b42a5a30..d733e6287 100644 --- a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs +++ b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs @@ -57,7 +57,7 @@ Project is not null && Project.Equals("Elastic documentation", StringComparison.OrdinalIgnoreCase); public ConfigurationFile(BuildContext context) - : base(context.ConfigurationPath, context.DocumentationSourceDirectory) + : base(context.ConfigurationPath, context.DocumentationSourceDirectory, context.Git.RepositoryName) { _context = context; ScopeDirectory = context.ConfigurationPath.Directory!; diff --git a/src/Elastic.Markdown/IO/DocumentationFile.cs b/src/Elastic.Markdown/IO/DocumentationFile.cs index ce166b10f..7544feadd 100644 --- a/src/Elastic.Markdown/IO/DocumentationFile.cs +++ b/src/Elastic.Markdown/IO/DocumentationFile.cs @@ -8,27 +8,37 @@ namespace Elastic.Markdown.IO; -public abstract record DocumentationFile(IFileInfo SourceFile, IDirectoryInfo RootPath) +public abstract record DocumentationFile { - public string RelativePath { get; } = Path.GetRelativePath(RootPath.FullName, SourceFile.FullName); - public string RelativeFolder { get; } = Path.GetRelativePath(RootPath.FullName, SourceFile.Directory!.FullName); + protected DocumentationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, string repository) + { + SourceFile = sourceFile; + RelativePath = Path.GetRelativePath(rootPath.FullName, SourceFile.FullName); + RelativeFolder = Path.GetRelativePath(rootPath.FullName, SourceFile.Directory!.FullName); + CrossLink = $"{repository}://{RelativePath}"; + } + + public string RelativePath { get; } + public string RelativeFolder { get; } + public string CrossLink { get; } /// Allows documentation files of non markdown origins to advertise as their markdown equivalent in links.json public virtual string LinkReferenceRelativePath => RelativePath; + public IFileInfo SourceFile { get; } } -public record ImageFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string MimeType = "image/png") - : DocumentationFile(SourceFile, RootPath); +public record ImageFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string Repository, string MimeType = "image/png") + : DocumentationFile(SourceFile, RootPath, Repository); -public record StaticFile(IFileInfo SourceFile, IDirectoryInfo RootPath) - : DocumentationFile(SourceFile, RootPath); +public record StaticFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string Repository) + : DocumentationFile(SourceFile, RootPath, Repository); -public record ExcludedFile(IFileInfo SourceFile, IDirectoryInfo RootPath) - : DocumentationFile(SourceFile, RootPath); +public record ExcludedFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string Repository) + : DocumentationFile(SourceFile, RootPath, Repository); -public record SnippetFile(IFileInfo SourceFile, IDirectoryInfo RootPath) - : DocumentationFile(SourceFile, RootPath) +public record SnippetFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string Repository) + : DocumentationFile(SourceFile, RootPath, Repository) { private SnippetAnchors? Anchors { get; set; } private bool _parsed; diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 25c699069..eac36321c 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -26,8 +26,46 @@ public interface INavigationLookups public interface IPositionalNavigation { + FrozenDictionary MarkdownNavigationLookup { get; } + MarkdownFile? GetPrevious(MarkdownFile current); MarkdownFile? GetNext(MarkdownFile current); + + INavigationItem[] GetParents(INavigationItem current) + { + var parents = new List(); + var parent = current.Parent; + do + { + if (parent is null) + continue; + parents.Add(parent); + parent = parent.Parent; + } while (parent != null); + + return [.. parents]; + } + MarkdownFile[] GetParentMarkdownFiles(INavigationItem current) + { + var parents = new List(); + var navigationParents = GetParents(current); + foreach (var parent in navigationParents) + { + if (parent is FileNavigationItem f) + parents.Add(f.File); + if (parent is GroupNavigationItem { Group.Index: not null } g) + parents.Add(g.Group.Index); + if (parent is DocumentationGroup { Index: not null } dg) + parents.Add(dg.Index); + } + return [.. parents]; + } + MarkdownFile[] GetParentMarkdownFiles(MarkdownFile file) + { + if (MarkdownNavigationLookup.TryGetValue(file.CrossLink, out var navigationItem)) + return GetParentMarkdownFiles(navigationItem); + return []; + } } public record NavigationLookups : INavigationLookups @@ -71,6 +109,8 @@ public class DocumentationSet : INavigationLookups, IPositionalNavigation IReadOnlyCollection INavigationLookups.EnabledExtensions => Configuration.EnabledExtensions; + public FrozenDictionary MarkdownNavigationLookup { get; } + // FrozenDictionary? indexedTableOfContents = null public DocumentationSet( BuildContext build, @@ -137,9 +177,32 @@ public DocumentationSet( MarkdownFiles = markdownFiles.Where(f => f.NavigationIndex > -1).ToDictionary(i => i.NavigationIndex, i => i).ToFrozenDictionary(); + MarkdownNavigationLookup = Tree.NavigationItems + .SelectMany(Pairs) + .ToDictionary(kv => kv.Item1, kv => kv.Item2) + .ToFrozenDictionary(); + ValidateRedirectsExists(); } + public static (string, INavigationItem)[] Pairs(INavigationItem item) + { + if (item is FileNavigationItem f) + return [(f.File.CrossLink, item)]; + if (item is GroupNavigationItem g) + { + var index = new List<(string, INavigationItem)>(); + if (g.Group.Index is not null) + index.Add((g.Group.Index.CrossLink, g)); + + return index.Concat(g.Group.NavigationItems.SelectMany(Pairs).ToArray()) + .DistinctBy(kv => kv.Item1) + .ToArray(); + } + + return []; + } + private DocumentationFile[] ScanDocumentationFiles(BuildContext build, IDirectoryInfo sourceDirectory) => [.. build.ReadFileSystem.Directory .EnumerateFiles(sourceDirectory.FullName, "*.*", SearchOption.AllDirectories) @@ -150,11 +213,11 @@ [.. build.ReadFileSystem.Directory .Where(f => !Path.GetRelativePath(sourceDirectory.FullName, f.FullName).StartsWith('.')) .Select(file => file.Extension switch { - ".jpg" => new ImageFile(file, SourceDirectory, "image/jpeg"), - ".jpeg" => new ImageFile(file, SourceDirectory, "image/jpeg"), - ".gif" => new ImageFile(file, SourceDirectory, "image/gif"), - ".svg" => new ImageFile(file, SourceDirectory, "image/svg+xml"), - ".png" => new ImageFile(file, SourceDirectory), + ".jpg" => new ImageFile(file, SourceDirectory, build.Git.RepositoryName, "image/jpeg"), + ".jpeg" => new ImageFile(file, SourceDirectory, build.Git.RepositoryName, "image/jpeg"), + ".gif" => new ImageFile(file, SourceDirectory, build.Git.RepositoryName, "image/gif"), + ".svg" => new ImageFile(file, SourceDirectory, build.Git.RepositoryName, "image/svg+xml"), + ".png" => new ImageFile(file, SourceDirectory, build.Git.RepositoryName), ".md" => CreateMarkDownFile(file, build), _ => DefaultFileHandling(file, sourceDirectory) })]; @@ -167,7 +230,7 @@ private DocumentationFile DefaultFileHandling(IFileInfo file, IDirectoryInfo sou if (documentationFile is not null) return documentationFile; } - return new ExcludedFile(file, sourceDirectory); + return new ExcludedFile(file, sourceDirectory, Build.Git.RepositoryName); } private void ValidateRedirectsExists() @@ -265,15 +328,15 @@ private DocumentationFile CreateMarkDownFile(IFileInfo file, BuildContext contex { var relativePath = Path.GetRelativePath(SourceDirectory.FullName, file.FullName); if (Configuration.Exclude.Any(g => g.IsMatch(relativePath))) - return new ExcludedFile(file, SourceDirectory); + return new ExcludedFile(file, SourceDirectory, context.Git.RepositoryName); if (relativePath.Contains("_snippets")) - return new SnippetFile(file, SourceDirectory); + return new SnippetFile(file, SourceDirectory, context.Git.RepositoryName); // we ignore files in folders that start with an underscore var folder = Path.GetDirectoryName(relativePath); if (folder is not null && (folder.Contains($"{Path.DirectorySeparatorChar}_", StringComparison.Ordinal) || folder.StartsWith('_'))) - return new ExcludedFile(file, SourceDirectory); + return new ExcludedFile(file, SourceDirectory, context.Git.RepositoryName); if (Configuration.Files.Contains(relativePath)) return ExtensionOrDefaultMarkdown(); @@ -282,7 +345,7 @@ private DocumentationFile CreateMarkDownFile(IFileInfo file, BuildContext contex return ExtensionOrDefaultMarkdown(); context.EmitError(Configuration.SourceFile, $"Not linked in toc: {relativePath}"); - return new ExcludedFile(file, SourceDirectory); + return new ExcludedFile(file, SourceDirectory, context.Git.RepositoryName); MarkdownFile ExtensionOrDefaultMarkdown() { diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 5c89eb74f..4dfb624a7 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -37,7 +37,7 @@ public MarkdownFile( BuildContext build, DocumentationSet set ) - : base(sourceFile, rootPath) + : base(sourceFile, rootPath, build.Git.RepositoryName) { FileName = sourceFile.Name; FilePath = sourceFile.FullName; @@ -52,13 +52,14 @@ DocumentationSet set //may be updated by DocumentationGroup.ProcessTocItems //todo refactor mutability of MarkdownFile as a whole ScopeDirectory = build.Configuration.ScopeDirectory; + NavigationRoot = set.Tree; NavigationSource = set.Source; } public IDirectoryInfo ScopeDirectory { get; set; } - public INavigation NavigationRoot { get; set; } + public INavigationGroup NavigationRoot { get; set; } public Uri NavigationSource { get; set; } @@ -66,12 +67,6 @@ DocumentationSet set private DiagnosticsCollector Collector { get; } - public DocumentationGroup? Parent - { - get => FileName == "index.md" ? _parent?.Parent : _parent; - set => _parent = value; - } - public bool Hidden { get; internal set; } public string? UrlPathPrefix { get; } protected MarkdownParser MarkdownParser { get; } @@ -135,8 +130,7 @@ public string Url _url = DefaultUrlPath; return _url; } - var path = RelativePath; - var crossLink = new Uri($"{_set.Build.Git.RepositoryName}://{path}"); + var crossLink = new Uri(CrossLink); var uri = _set.LinkResolver.UriResolver.Resolve(crossLink, DefaultUrlPathSuffix); _url = uri.AbsolutePath; return _url; @@ -146,26 +140,9 @@ public string Url public int NavigationIndex { get; set; } = -1; - public string? GroupId { get; set; } - private bool _instructionsParsed; - private DocumentationGroup? _parent; private string? _title; - public MarkdownFile[] YieldParents() - { - var parents = new List(); - var parent = Parent; - do - { - if (parent is { Index: not null } && parent.Index != this) - parents.Add(parent.Index); - 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; } @@ -362,5 +339,4 @@ 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 229dd209e..1f5cbff63 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -11,41 +11,43 @@ namespace Elastic.Markdown.IO.Navigation; public interface INavigationItem { - int Order { get; } string Id { get; } + INavigationItem? Parent { get; set; } + int Depth { get; } } -[DebuggerDisplay("Toc >{Depth} #{Order} {Group.FolderName}")] -public record TocNavigationItem(int Order, int Depth, DocumentationGroup Group, Uri Source) - : GroupNavigationItem(Order, Depth, Group) +[DebuggerDisplay("Toc >{Depth} {Group.FolderName}")] +public record TocNavigationItem(int Depth, DocumentationGroup Group, Uri Source, INavigationItem? Parent) + : GroupNavigationItem(Depth, Group, Parent) { /// Only used for tests public IReadOnlyDictionary NavigationLookup => Group.NavigationItems.OfType().ToDictionary(i => i.Source, i => i); } -[DebuggerDisplay("Group >{Depth} #{Order} {Group.FolderName}")] -public record GroupNavigationItem(int Order, int Depth, DocumentationGroup Group) : INavigationItem +[DebuggerDisplay("Group >{Depth} {Group.FolderName}")] +public record GroupNavigationItem(int Depth, DocumentationGroup Group, INavigationItem? Parent) : INavigationItem { public string Id { get; } = Group.Id; + public INavigationItem? Parent { get; set; } = Parent; } -[DebuggerDisplay("File >{Depth} #{Order} {File.RelativePath}")] -public record FileNavigationItem(int Order, int Depth, MarkdownFile File) : INavigationItem +[DebuggerDisplay("File >{Depth} {File.RelativePath}")] +public record FileNavigationItem(int Depth, MarkdownFile File, INavigationItem? Parent) : INavigationItem { public string Id { get; } = File.Id; + public INavigationItem? Parent { get; set; } = Parent; } -public interface INavigation +public interface INavigationGroup : INavigationItem { - string Id { get; } IReadOnlyCollection NavigationItems { get; } string? IndexFileName { get; } } public interface INavigationScope { - INavigation NavigationRoot { get; } + INavigationGroup NavigationRoot { get; } } public class TableOfContentsTreeCollector @@ -118,7 +120,7 @@ internal TableOfContentsTree( } [DebuggerDisplay("Group >{Depth} {FolderName} ({NavigationItems.Count} items)")] -public class DocumentationGroup : INavigation +public class DocumentationGroup : INavigationGroup { private readonly TableOfContentsTreeCollector _treeCollector; @@ -126,7 +128,7 @@ public class DocumentationGroup : INavigation public string NavigationRootId => NavigationRoot.Id; - public INavigation NavigationRoot { get; set; } + public INavigationGroup NavigationRoot { get; set; } public Uri NavigationSource { get; set; } @@ -140,9 +142,9 @@ public class DocumentationGroup : INavigation public string? IndexFileName => Index?.FileName; - private int Depth { get; set; } + public int Depth { get; set; } - public DocumentationGroup? Parent { get; } + public INavigationItem? Parent { get; set; } public string FolderName { get; } @@ -161,7 +163,7 @@ ref int fileIndex _treeCollector = treeCollector; } - internal DocumentationGroup( + protected DocumentationGroup( string folderName, TableOfContentsTreeCollector treeCollector, BuildContext context, @@ -170,7 +172,7 @@ internal DocumentationGroup( ref int fileIndex, int depth, DocumentationGroup? toplevelTree, - DocumentationGroup? parent, + INavigationItem? parent, MarkdownFile? index = null ) { @@ -178,7 +180,6 @@ internal DocumentationGroup( NavigationSource = navigationSource; _treeCollector = treeCollector; Depth = depth; - Parent = parent; // Virtual call on purpose, implementations use no state // ReSharper disable VirtualMemberCallInConstructor toplevelTree ??= DefaultNavigation; @@ -187,8 +188,6 @@ internal DocumentationGroup( NavigationRoot = toplevelTree; // ReSharper restore VirtualMemberCallInConstructor Index = ProcessTocItems(context, toplevelTree, index, lookups, depth, ref fileIndex, out var groups, out var files, out var navigationItems); - if (Index is not null) - Index.GroupId = Id; GroupsInOrder = groups; FilesInOrder = files; @@ -234,7 +233,6 @@ internal DocumentationGroup( continue; - md.Parent = this; md.Hidden = file.Hidden; var navigationIndex = Interlocked.Increment(ref fileIndex); md.NavigationIndex = navigationIndex; @@ -255,7 +253,7 @@ internal DocumentationGroup( TableOfContents = file.Children }, NavigationSource, ref fileIndex, depth + 1, topLevelGroup, this, virtualIndex); groups.Add(group); - navigationItems.Add(new GroupNavigationItem(index, depth, group)); + navigationItems.Add(new GroupNavigationItem(depth, group, this)); indexFile ??= virtualIndex; continue; } @@ -269,7 +267,7 @@ internal DocumentationGroup( // explicit index page. E.g. when grouping related files together. // if the page is referenced as hidden in the TOC do not include it in the navigation if (indexFile != md && !md.Hidden) - navigationItems.Add(new FileNavigationItem(index, depth, md)); + navigationItems.Add(new FileNavigationItem(depth, md, this)); } else if (tocItem is FolderReference folder) { @@ -292,7 +290,7 @@ .. documentationFiles }, ref fileIndex, depth + 1, topLevelGroup, this); group = toc; - navigationItems.Add(new TocNavigationItem(index, depth, toc, tocReference.Source)); + navigationItems.Add(new TocNavigationItem(depth, toc, tocReference.Source, this)); } else { @@ -300,7 +298,7 @@ .. documentationFiles { TableOfContents = children }, NavigationSource, ref fileIndex, depth + 1, topLevelGroup, this); - navigationItems.Add(new GroupNavigationItem(index, depth, group)); + navigationItems.Add(new GroupNavigationItem(depth, group, this)); } groups.Add(group); diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index e410646b4..6b2dbca3d 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -35,8 +35,8 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) if (link.Url?.StartsWith('/') == true) { - var currentRootNavigation = link.GetData(nameof(MarkdownFile.NavigationRoot)) as INavigation; - var targetRootNavigation = link.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") as INavigation; + var currentRootNavigation = link.GetData(nameof(MarkdownFile.NavigationRoot)) as INavigationGroup; + var targetRootNavigation = link.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") as INavigationGroup; _ = renderer.Write(" hx-get=\""); _ = renderer.WriteEscapeUrl(url); _ = renderer.Write('"'); diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 3fe812a70..e0e9be822 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -16,7 +16,7 @@ namespace Elastic.Markdown.Slices; public interface INavigationHtmlWriter { - Task RenderNavigation(INavigation currentRootNavigation, Uri navigationSource, Cancel ctx = default); + Task RenderNavigation(INavigationGroup currentRootNavigation, Uri navigationSource, Cancel ctx = default); async Task Render(NavigationViewModel model, Cancel ctx) { @@ -31,7 +31,7 @@ public class IsolatedBuildNavigationHtmlWriter(DocumentationSet set) : INavigati private readonly ConcurrentDictionary _renderedNavigationCache = []; - public async Task RenderNavigation(INavigation currentRootNavigation, Uri navigationSource, Cancel ctx = default) + public async Task RenderNavigation(INavigationGroup currentRootNavigation, Uri navigationSource, Cancel ctx = default) { var navigation = Set.Configuration.Features.IsPrimaryNavEnabled ? currentRootNavigation @@ -46,7 +46,7 @@ public async Task RenderNavigation(INavigation currentRootNavigation, Ur return value; } - private NavigationViewModel CreateNavigationModel(INavigation navigation) + private NavigationViewModel CreateNavigationModel(INavigationGroup navigation) { if (navigation is not DocumentationGroup tree) throw new InvalidOperationException("Expected a documentation group"); @@ -93,6 +93,7 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument var previous = PositionalNavigation.GetPrevious(markdown); var next = PositionalNavigation.GetNext(markdown); + var parents = PositionalNavigation.GetParentMarkdownFiles(markdown); var remote = DocumentationSet.Build.Git.RepositoryName; var branch = DocumentationSet.Build.Git.Branch; @@ -126,6 +127,7 @@ private async Task RenderLayout(MarkdownFile markdown, MarkdownDocument CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, + Parents = parents, NavigationHtml = navigationHtml, UrlPathPrefix = markdown.UrlPathPrefix, AppliesTo = markdown.YamlFrontMatter?.AppliesTo, diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index db4df2442..e20fca996 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -6,12 +6,13 @@ public LayoutViewModel LayoutModel => new() { DocSetName = Model.DocSetName, - Title = Model.CurrentDocument.Parent == null ? Model.Title : $"{Model.Title} | {Model.SiteName}", + Title = $"{Model.Title} | {Model.SiteName}", Description = Model.Description, PageTocItems = Model.PageTocItems.Where(i => i is { Level: 2 or 3 }).ToList(), CurrentDocument = Model.CurrentDocument, Previous = Model.PreviousDocument, Next = Model.NextDocument, + Parents = Model.Parents, NavigationHtml = Model.NavigationHtml, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index 20c8006b6..882b47a7f 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -1,5 +1,10 @@ @using Elastic.Markdown.Helpers @inherits RazorSlice +@{ + var parents = Model.Parents.Reverse().DistinctBy(kv => kv.FileName).ToList(); + var crumbs = parents.Skip(1).TakeLast(2).ToList(); +} +