diff --git a/Directory.Packages.props b/Directory.Packages.props index 22cab4ff..fa1dbcc5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ true false - 1.1.1.4415 + 1.1.1.4520 true https://api.nuget.org/v3/index.json; diff --git a/EssentialCSharp.Web.Tests/SiteMappingTests.cs b/EssentialCSharp.Web.Tests/SiteMappingTests.cs index 140e982c..e3c753ac 100644 --- a/EssentialCSharp.Web.Tests/SiteMappingTests.cs +++ b/EssentialCSharp.Web.Tests/SiteMappingTests.cs @@ -5,7 +5,7 @@ namespace EssentialCSharp.Web.Tests; public class SiteMappingTests { static SiteMapping HelloWorldSiteMapping => new( - key: "hello-world", + keys: ["hello-world"], pagePath: [ "Chapters", @@ -15,6 +15,7 @@ public class SiteMappingTests ], chapterNumber: 1, pageNumber: 1, + orderOnPage: 1, chapterTitle: "Introducing C#", rawHeading: "Introduction", anchorId: "hello-world", @@ -22,7 +23,7 @@ public class SiteMappingTests ); static SiteMapping CSyntaxFundamentalsSiteMapping => new( - key: "c-syntax-fundamentals", + keys: ["c-syntax-fundamentals"], pagePath: [ "Chapters", @@ -32,6 +33,7 @@ public class SiteMappingTests ], chapterNumber: 1, pageNumber: 2, + orderOnPage: 1, chapterTitle: "Introducing C#", rawHeading: "C# Syntax Fundamentals", anchorId: "c-syntax-fundamentals", @@ -52,7 +54,7 @@ public void FindHelloWorldWithAnchorSlugReturnsCorrectSiteMap() { SiteMapping? foundSiteMap = GetSiteMap().Find("hello-world#hello-world"); Assert.NotNull(foundSiteMap); - Assert.Equal(HelloWorldSiteMapping, foundSiteMap); + Assert.Equivalent(HelloWorldSiteMapping, foundSiteMap); } [Fact] @@ -60,7 +62,7 @@ public void FindCSyntaxFundamentalsWithSpacesReturnsCorrectSiteMap() { SiteMapping? foundSiteMap = GetSiteMap().Find("C# Syntax Fundamentals"); Assert.NotNull(foundSiteMap); - Assert.Equal(CSyntaxFundamentalsSiteMapping, foundSiteMap); + Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap); } [Fact] @@ -68,7 +70,7 @@ public void FindCSyntaxFundamentalsWithSpacesAndAnchorReturnsCorrectSiteMap() { SiteMapping? foundSiteMap = GetSiteMap().Find("C# Syntax Fundamentals#hello-world"); Assert.NotNull(foundSiteMap); - Assert.Equal(CSyntaxFundamentalsSiteMapping, foundSiteMap); + Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap); } [Fact] @@ -76,6 +78,6 @@ public void FindCSyntaxFundamentalsSanitizedWithAnchorReturnsCorrectSiteMap() { SiteMapping? foundSiteMap = GetSiteMap().Find("c-syntax-fundamentals#hello-world"); Assert.NotNull(foundSiteMap); - Assert.Equal(CSyntaxFundamentalsSiteMapping, foundSiteMap); + Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap); } } diff --git a/EssentialCSharp.Web/Controllers/HomeController.cs b/EssentialCSharp.Web/Controllers/HomeController.cs index f20f0ee0..32237400 100644 --- a/EssentialCSharp.Web/Controllers/HomeController.cs +++ b/EssentialCSharp.Web/Controllers/HomeController.cs @@ -125,7 +125,7 @@ private string FlipPage(int currentChapter, int currentPage, bool next) return ""; } } - return $"{siteMap.Key}#{siteMap.AnchorId}"; + return $"{siteMap.Keys.First()}#{siteMap.AnchorId}"; } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] diff --git a/EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs b/EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs index 058e7ff7..28ae5643 100644 --- a/EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs +++ b/EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs @@ -16,7 +16,7 @@ public static class SiteMappingListExtensions } foreach (string? potentialMatch in key.GetPotentialMatches()) { - if (siteMappings.FirstOrDefault(x => x.Key == potentialMatch) is { } siteMap) + if (siteMappings.FirstOrDefault(x => x.Keys.Any(x => x == potentialMatch)) is { } siteMap) { return siteMap; } diff --git a/EssentialCSharp.Web/Services/ISiteMappingService.cs b/EssentialCSharp.Web/Services/ISiteMappingService.cs index fe8cfbc7..ac2a719e 100644 --- a/EssentialCSharp.Web/Services/ISiteMappingService.cs +++ b/EssentialCSharp.Web/Services/ISiteMappingService.cs @@ -3,4 +3,5 @@ public interface ISiteMappingService { IList SiteMappings { get; } + IEnumerable GetTocData(); } diff --git a/EssentialCSharp.Web/Services/SiteMappingDto.cs b/EssentialCSharp.Web/Services/SiteMappingDto.cs new file mode 100644 index 00000000..3b7ba91f --- /dev/null +++ b/EssentialCSharp.Web/Services/SiteMappingDto.cs @@ -0,0 +1,12 @@ +namespace EssentialCSharp.Web.Services; + +// Data transfer object to pass necessary SiteMapping data info +// to frontend for use in table of contents +public class SiteMappingDto +{ + public required int Level { get; set; } + public required string Key { get; set; } + public required string Href { get; set; } + public required string Title { get; set; } + public required IEnumerable Items { get; set; } +} diff --git a/EssentialCSharp.Web/Services/SiteMappingService.cs b/EssentialCSharp.Web/Services/SiteMappingService.cs index c086596f..9eafb3a9 100644 --- a/EssentialCSharp.Web/Services/SiteMappingService.cs +++ b/EssentialCSharp.Web/Services/SiteMappingService.cs @@ -12,4 +12,42 @@ public SiteMappingService(IWebHostEnvironment webHostEnvironment) List? siteMappings = System.Text.Json.JsonSerializer.Deserialize>(File.OpenRead(path)) ?? throw new InvalidOperationException("No table of contents found"); SiteMappings = siteMappings; } + + public IEnumerable GetTocData() + { + return SiteMappings.GroupBy(x => x.ChapterNumber).OrderBy(x => x.Key).Select(x => + { + IEnumerable orderedGrouping = x.OrderBy(i => i.PageNumber).ThenBy(i => i.OrderOnPage); + SiteMapping firstElement = orderedGrouping.First(); + return new SiteMappingDto() + { + Level = 0, + Key = firstElement.Keys.First(), + Href = $"{firstElement.Keys.First()}#{firstElement.AnchorId}", + Title = $"Chapter {x.Key}: {firstElement.ChapterTitle}", + Items = GetItems(orderedGrouping.Skip(1), 1) + }; + } + ); + } + + private static IEnumerable GetItems(IEnumerable chapterItems, int indentLevel) + { + return chapterItems + // Examine all items up until we move up to a level higher than where we're starting, + // which would indicate that we've reached the end of the entries nested under `indentationLevel` + .TakeWhile(i => i.IndentLevel >= indentLevel) + // Of all the multi-level descendants we found, take only those at the current level that we're wanting to render. + .Where(i => i.IndentLevel == indentLevel) + .Select(i => new SiteMappingDto() + { + Level = indentLevel, + Key = i.Keys.First(), + Href = $"{i.Keys.First()}#{i.AnchorId}", + Title = i.RawHeading, + // Any children of this node will be /after/ this node, + // so skip any items that are /before/ the current node. + Items = GetItems(chapterItems.SkipWhile(q => i.Keys.First() != q.Keys.First()).Skip(1), indentLevel + 1) + }); + } } diff --git a/EssentialCSharp.Web/Views/Shared/_Layout.cshtml b/EssentialCSharp.Web/Views/Shared/_Layout.cshtml index 04a9367b..7ccf9381 100644 --- a/EssentialCSharp.Web/Views/Shared/_Layout.cshtml +++ b/EssentialCSharp.Web/Views/Shared/_Layout.cshtml @@ -266,32 +266,7 @@ @await RenderSectionAsync("Scripts", required: false);