mirror of https://github.com/abpframework/abp.git
Browse Source
Replaces client-side TOC generation with a new ITocGeneratorService and TocGeneratorService using HtmlAgilityPack. Updates the project page to render the TOC from the server, removes bootstrap-toc dependencies, and adjusts related JS and CSS for the new TOC structure. Adds HtmlAgilityPack as a dependency.pull/23666/head
9 changed files with 214 additions and 29 deletions
@ -0,0 +1,8 @@ |
|||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace Volo.Docs.TableOfContents; |
||||
|
|
||||
|
public interface ITocGeneratorService : IApplicationService |
||||
|
{ |
||||
|
(string TocHtml, string ProcessedContent) GenerateTocAndProcessHeadings(string content); |
||||
|
} |
||||
@ -0,0 +1,154 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using HtmlAgilityPack; |
||||
|
|
||||
|
namespace Volo.Docs.TableOfContents; |
||||
|
|
||||
|
public class TocGeneratorService : ITocGeneratorService, ITransientDependency |
||||
|
{ |
||||
|
private readonly HashSet<string> _generatedIds = []; |
||||
|
|
||||
|
public (string TocHtml, string ProcessedContent) GenerateTocAndProcessHeadings(string content) |
||||
|
{ |
||||
|
if (content.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return (string.Empty, string.Empty); |
||||
|
} |
||||
|
|
||||
|
_generatedIds.Clear(); |
||||
|
var tocHeadings = new List<(int Level, string Text, string Id)>(); |
||||
|
|
||||
|
var doc = new HtmlDocument(); |
||||
|
doc.LoadHtml(content); |
||||
|
|
||||
|
var nodesWithId = doc.DocumentNode.SelectNodes("//*[@id]"); |
||||
|
if (nodesWithId != null) |
||||
|
{ |
||||
|
foreach (var node in nodesWithId) |
||||
|
{ |
||||
|
_generatedIds.Add(node.Id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var headingNodes = doc.DocumentNode.SelectNodes("//h1|//h2|//h3|//h4|//h5|//h6"); |
||||
|
if (headingNodes != null) |
||||
|
{ |
||||
|
foreach (var node in headingNodes) |
||||
|
{ |
||||
|
var id = node.Id; |
||||
|
|
||||
|
if (id.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
id = GenerateUniqueId(node.InnerText.Trim()); |
||||
|
node.SetAttributeValue("id", id); |
||||
|
} |
||||
|
|
||||
|
var level = int.Parse(node.Name.Substring(1)); |
||||
|
if (level == 2 || level == 3) |
||||
|
{ |
||||
|
tocHeadings.Add((level, node.InnerText.Trim(), id)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var tocHtml = BuildTocHtml(tocHeadings); |
||||
|
|
||||
|
var processedContent = doc.DocumentNode.OuterHtml; |
||||
|
|
||||
|
return (tocHtml, processedContent); |
||||
|
} |
||||
|
|
||||
|
private string GenerateUniqueId(string text) |
||||
|
{ |
||||
|
if (text.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return $"section-{Guid.NewGuid().ToString("N")[..8]}"; |
||||
|
} |
||||
|
|
||||
|
var baseId = text.ToLowerInvariant(); |
||||
|
|
||||
|
baseId = Regex.Replace(baseId, @"[^a-z0-9]+", "-", RegexOptions.Compiled); |
||||
|
|
||||
|
baseId = baseId.Trim('-'); |
||||
|
|
||||
|
if (baseId.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return $"section-{Guid.NewGuid().ToString("N")[..8]}"; |
||||
|
} |
||||
|
|
||||
|
var finalId = baseId; |
||||
|
var counter = 1; |
||||
|
|
||||
|
while (!_generatedIds.Add(finalId)) |
||||
|
{ |
||||
|
finalId = $"{baseId}-{++counter}"; |
||||
|
} |
||||
|
|
||||
|
return finalId; |
||||
|
} |
||||
|
|
||||
|
private static string BuildTocHtml(List<(int Level, string Text, string Id)> headings) |
||||
|
{ |
||||
|
if (headings == null || headings.Count == 0) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
var tocBuilder = new StringBuilder(); |
||||
|
tocBuilder.Append("<ul class=\"nav nav-pills flex-column\">"); |
||||
|
|
||||
|
var currentLevel = 0; |
||||
|
var isFirstH2 = true; |
||||
|
|
||||
|
foreach (var (index, heading) in headings.Select((h, i) => (i, h))) |
||||
|
{ |
||||
|
var isLastItem = index == headings.Count - 1; |
||||
|
var nextHeading = isLastItem ? default : headings[index + 1]; |
||||
|
var hasChildren = nextHeading.Level == 3 && heading.Level == 2; |
||||
|
|
||||
|
if (heading.Level < currentLevel) |
||||
|
{ |
||||
|
tocBuilder.Append("</ul></li>"); |
||||
|
} |
||||
|
else if (!isFirstH2 && heading.Level == 2 && currentLevel == 2) |
||||
|
{ |
||||
|
tocBuilder.Append("</li>"); |
||||
|
} |
||||
|
|
||||
|
if (heading.Level == 2) |
||||
|
{ |
||||
|
var liClass = hasChildren ? "nav-item toc-item-has-children" : "nav-item"; |
||||
|
tocBuilder.Append($"<li class=\"{liClass}\"><a class=\"nav-link\" href=\"#{heading.Id}\">{heading.Text}</a>"); |
||||
|
isFirstH2 = false; |
||||
|
} |
||||
|
else if (heading.Level == 3) |
||||
|
{ |
||||
|
if (currentLevel != 3) |
||||
|
{ |
||||
|
tocBuilder.Append("<ul class=\"nav nav-pills flex-column\">"); |
||||
|
} |
||||
|
|
||||
|
tocBuilder.Append($"<li class=\"nav-item\"><a class=\"nav-link\" href=\"#{heading.Id}\">{heading.Text}</a></li>"); |
||||
|
} |
||||
|
|
||||
|
currentLevel = heading.Level; |
||||
|
} |
||||
|
|
||||
|
if (currentLevel == 3) |
||||
|
{ |
||||
|
tocBuilder.Append("</ul></li>"); |
||||
|
} |
||||
|
else if (currentLevel == 2 && !isFirstH2) |
||||
|
{ |
||||
|
tocBuilder.Append("</li>"); |
||||
|
} |
||||
|
|
||||
|
tocBuilder.Append("</ul>"); |
||||
|
|
||||
|
return tocBuilder.ToString(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue