diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 30291094a2..ec59d25db6 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -598,7 +598,7 @@
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index a8833e963a..4f78353d07 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -74,9 +74,9 @@ namespace Volo.Docs.Pages.Documents.Project public VersionInfoViewModel LatestVersionInfo { get; private set; } - public string DocumentsUrlPrefix { get; set; } - - public List TocHeadings { get; set; } = []; + public string DocumentsUrlPrefix { get; set; } + + public List TocItems { get; set; } = []; public bool ShowProjectsCombobox { get; set; } @@ -101,6 +101,7 @@ namespace Volo.Docs.Pages.Documents.Project public DocumentNavigationsDto DocumentNavigationsDto { get; private set; } private const int MaxDescriptionMetaTagLength = 200; + private const int MaxTocLevel = 2; private readonly IDocumentAppService _documentAppService; private readonly IDocumentToHtmlConverterFactory _documentToHtmlConverterFactory; private readonly IProjectAppService _projectAppService; @@ -539,12 +540,12 @@ namespace Volo.Docs.Pages.Documents.Project Document = await GetSpecificDocumentOrDefaultAsync(language); DocumentLanguageCode = language; DocumentNameWithExtension = Document.Name; - SetDocumentPageTitle(); - - if (Document != null && !Document.Content.IsNullOrEmpty()) - { - TocHeadings = _tocGeneratorService.GenerateTocHeadings(Document.Content); - } + SetDocumentPageTitle(); + + if (Document != null && !Document.Content.IsNullOrEmpty()) + { + TocItems = _tocGeneratorService.GenerateTocItems(Document.Content, MaxTocLevel); + } await ConvertDocumentContentToHtmlAsync(); diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml index 56f040dcc9..51a1bbe59c 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml @@ -1,74 +1,41 @@ @using Volo.Docs.TableOfContents -@model List +@model List @{ if (Model == null || Model.Count == 0) { return; } - - var relevantHeadings = Model - .Where(h => h.Level is 2 or 3) - .ToList(); - - if (relevantHeadings.Count == 0) - { - relevantHeadings = Model - .Where(h => h.Level == 1) - .ToList(); - } - - if (relevantHeadings.Count == 0) - { - return; - } - - var baseLevel = relevantHeadings.Min(h => h.Level); - var normalizedHeadings = relevantHeadings - .Select(h => h with { Level = h.Level - baseLevel + 1 }) - .ToList(); - - var levelStack = new Stack(); - levelStack.Push(0); } -@for (var i = 0; i < normalizedHeadings.Count; i++) -{ - var heading = normalizedHeadings[i]; - var previousLevel = levelStack.Peek(); - - if (heading.Level < previousLevel) + - levelStack.Pop(); - } - @: + } - else if (heading.Level > previousLevel) - { - @: + +@{ + void RenderChildrenRecursive(List children, int depth) { - @: + } - - var hasChildren = (i + 1 < normalizedHeadings.Count) && - (normalizedHeadings[i + 1].Level > heading.Level); - - var liClass = hasChildren ? "nav-item toc-item-has-children" : "nav-item"; - - @:
  • @heading.Text -} - -@if (normalizedHeadings.Any()) -{ - @:
  • -} -@while (levelStack.Count > 1) -{ - @: - levelStack.Pop(); -} +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs b/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs index 3ccb0faa56..a33fb92e69 100644 --- a/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs +++ b/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs @@ -6,4 +6,10 @@ namespace Volo.Docs.TableOfContents; public interface ITocGeneratorService : IApplicationService { List GenerateTocHeadings(string markdownContent); + + List GenerateTocItems(List tocHeadings, int topLevel, int maxLevel); + + int GetTopLevel(List tocHeadings); + + List GenerateTocItems(string markdownContent, int maxLevel, int? topLevel = null); } diff --git a/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs b/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs index 956b9f7e09..0f2ac84a0d 100644 --- a/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs +++ b/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs @@ -13,7 +13,7 @@ namespace Volo.Docs.TableOfContents; public class TocGeneratorService : ITocGeneratorService, ITransientDependency { - public List GenerateTocHeadings(string markdownContent) + public virtual List GenerateTocHeadings(string markdownContent) { if (markdownContent.IsNullOrWhiteSpace()) { @@ -26,25 +26,84 @@ public class TocGeneratorService : ITocGeneratorService, ITransientDependency var pipeline = pipelineBuilder.Build(); - var headings = new List(); - var document = Markdig.Markdown.Parse(markdownContent, pipeline); var headingBlocks = document.Descendants(); - foreach (var headingBlock in headingBlocks) + return headingBlocks + .Select(hb => new TocHeading(hb.Level, GetPlainText(hb.Inline), hb.GetAttributes().Id)).ToList(); + } + + public virtual List GenerateTocItems(List tocHeadings, int topLevel, int maxLevel) + { + return BuildHierarchicalStructure(tocHeadings + .Where(h => h.Level >= topLevel && h.Level <= maxLevel).ToList(), topLevel); + } + + public virtual int GetTopLevel(List headings) + { + for (var i = 1; i <= 6; i++) + { + if (headings.Count(h => h.Level == i) > 1) + { + return i; + } + } + return 1; + } + + public virtual List GenerateTocItems(string markdownContent, int maxLevel, int? topLevel = null) + { + var headings = GenerateTocHeadings(markdownContent); + var topLevelToUse = topLevel ?? GetTopLevel(headings); + return GenerateTocItems(headings, topLevelToUse, maxLevel); + } + + protected virtual List BuildHierarchicalStructure(List headings, int topLevel) + { + var result = new List(); + + for (var i = 0; i < headings.Count; i++) { - headings.Add(new TocHeading { - Level = headingBlock.Level, - Text = GetPlainText(headingBlock.Inline), - Id = headingBlock.GetAttributes()?.Id - }); + var currentHeading = headings[i]; + + if (currentHeading.Level != topLevel) + { + continue; + } + + result.Add(new TocItem(currentHeading, GetDirectChildren(headings, i, currentHeading.Level))); + } + + return result; + } + + protected virtual List GetDirectChildren(List allHeadings, int parentIndex, int parentLevel) + { + var children = new List(); + var targetChildLevel = parentLevel + 1; + + for (var i = parentIndex + 1; i < allHeadings.Count; i++) + { + var heading = allHeadings[i]; + + if (heading.Level <= parentLevel) + { + break; + } + + if (heading.Level != targetChildLevel) + { + continue; + } + + children.Add(new TocItem(heading, GetDirectChildren(allHeadings, i, heading.Level))); } - return headings; + return children; } - private static string GetPlainText(ContainerInline container) + protected virtual string GetPlainText(ContainerInline container) { if (container == null) { diff --git a/modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs b/modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs index 3fdb775752..790b556e9d 100644 --- a/modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs +++ b/modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs @@ -1,8 +1,7 @@ -namespace Volo.Docs.TableOfContents; +using System.Collections.Generic; -public record TocHeading -{ - public int Level { get; set; } - public string Text { get; set; } - public string Id { get; set; } -} +namespace Volo.Docs.TableOfContents; + +public record TocHeading(int Level, string Text, string Id); + +public record TocItem(TocHeading Heading, List Children);