Browse Source

Refactor table of contents to hierarchical structure

Replaces flat TocHeading list with hierarchical TocItem structure for document table of contents. Updates rendering logic, service interface, and implementation to support nested headings and configurable levels.
pull/23666/head
SALİH ÖZKARA 5 months ago
parent
commit
9d01a36891
  1. 2
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml
  2. 19
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  3. 91
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml
  4. 6
      modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs
  5. 81
      modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs
  6. 13
      modules/docs/src/Volo.Docs.Web/TableOfContents/TocHeading.cs

2
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml

@ -598,7 +598,7 @@
<div id="scroll-index" class="">
<nav id="toc" class="navbar index-scroll">
<partial name="TableOfContents" model="Model.TocHeadings" />
<partial name="/Pages/Documents/Project/TableOfContents.cshtml" model="Model.TocItems" />
</nav>
<div class="row">
<div class="col p-0">

19
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<TocHeading> TocHeadings { get; set; } = [];
public string DocumentsUrlPrefix { get; set; }
public List<TocItem> 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();

91
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/TableOfContents.cshtml

@ -1,74 +1,41 @@
@using Volo.Docs.TableOfContents
@model List<TocHeading>
@model List<TocItem>
@{
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<int>();
levelStack.Push(0);
}
@for (var i = 0; i < normalizedHeadings.Count; i++)
{
var heading = normalizedHeadings[i];
var previousLevel = levelStack.Peek();
if (heading.Level < previousLevel)
<ul class="nav nav-pills flex-column">
@foreach (var item in Model)
{
@while (heading.Level < levelStack.Peek())
{
@:</li></ul>
levelStack.Pop();
}
@:</li>
<li class="nav-item @(item.Children.Any() ? "toc-item-has-children" : "")">
<a class="nav-link" href="#@item.Heading.Id">@item.Heading.Text</a>
@if (item.Children.Any())
{
RenderChildrenRecursive(item.Children, 1);
}
</li>
}
else if (heading.Level > previousLevel)
{
@:<ul class="nav nav-pills flex-column">
levelStack.Push(heading.Level);
}
else if (i > 0)
</ul>
@{
void RenderChildrenRecursive(List<TocItem> children, int depth)
{
@:</li>
<ul class="nav nav-pills flex-column toc-depth-@depth">
@foreach (var child in children)
{
<li class="nav-item @(child.Children.Any() ? "toc-item-has-children" : "")">
<a class="nav-link" href="#@child.Heading.Id">@child.Heading.Text</a>
@if (child.Children.Any())
{
RenderChildrenRecursive(child.Children, depth + 1);
}
</li>
}
</ul>
}
var hasChildren = (i + 1 < normalizedHeadings.Count) &&
(normalizedHeadings[i + 1].Level > heading.Level);
var liClass = hasChildren ? "nav-item toc-item-has-children" : "nav-item";
@:<li class="@liClass"><a class="nav-link" href="#@heading.Id">@heading.Text</a>
}
@if (normalizedHeadings.Any())
{
@:</li>
}
@while (levelStack.Count > 1)
{
@:</ul>
levelStack.Pop();
}
}

6
modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs

@ -6,4 +6,10 @@ namespace Volo.Docs.TableOfContents;
public interface ITocGeneratorService : IApplicationService
{
List<TocHeading> GenerateTocHeadings(string markdownContent);
List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int maxLevel);
int GetTopLevel(List<TocHeading> tocHeadings);
List<TocItem> GenerateTocItems(string markdownContent, int maxLevel, int? topLevel = null);
}

81
modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs

@ -13,7 +13,7 @@ namespace Volo.Docs.TableOfContents;
public class TocGeneratorService : ITocGeneratorService, ITransientDependency
{
public List<TocHeading> GenerateTocHeadings(string markdownContent)
public virtual List<TocHeading> GenerateTocHeadings(string markdownContent)
{
if (markdownContent.IsNullOrWhiteSpace())
{
@ -26,25 +26,84 @@ public class TocGeneratorService : ITocGeneratorService, ITransientDependency
var pipeline = pipelineBuilder.Build();
var headings = new List<TocHeading>();
var document = Markdig.Markdown.Parse(markdownContent, pipeline);
var headingBlocks = document.Descendants<HeadingBlock>();
foreach (var headingBlock in headingBlocks)
return headingBlocks
.Select(hb => new TocHeading(hb.Level, GetPlainText(hb.Inline), hb.GetAttributes().Id)).ToList();
}
public virtual List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int maxLevel)
{
return BuildHierarchicalStructure(tocHeadings
.Where(h => h.Level >= topLevel && h.Level <= maxLevel).ToList(), topLevel);
}
public virtual int GetTopLevel(List<TocHeading> headings)
{
for (var i = 1; i <= 6; i++)
{
if (headings.Count(h => h.Level == i) > 1)
{
return i;
}
}
return 1;
}
public virtual List<TocItem> 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<TocItem> BuildHierarchicalStructure(List<TocHeading> headings, int topLevel)
{
var result = new List<TocItem>();
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<TocItem> GetDirectChildren(List<TocHeading> allHeadings, int parentIndex, int parentLevel)
{
var children = new List<TocItem>();
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)
{

13
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<TocItem> Children);

Loading…
Cancel
Save