Browse Source

Implement server-side Table of Contents generation

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
Ahmet Çelik 5 months ago
parent
commit
b8f72cf244
  1. 1
      Directory.Packages.props
  2. 3
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml
  3. 19
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  4. 2
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js
  5. 12
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss
  6. 43
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js
  7. 8
      modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs
  8. 154
      modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs
  9. 1
      modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj

1
Directory.Packages.props

@ -29,6 +29,7 @@
<PackageVersion Include="Dapper" Version="2.1.66" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.15.4" />
<PackageVersion Include="Dapr.Client" Version="1.15.4" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.2" />
<PackageVersion Include="MyCSharp.HttpUserAgentParser" Version="3.0.25" />
<PackageVersion Include="Devart.Data.Oracle.EFCore" Version="10.4.235.9" />
<PackageVersion Include="DistributedLock.Core" Version="1.0.8" />

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

@ -39,7 +39,6 @@
<abp-style-bundle name="@typeof(IndexModel).FullName">
<abp-style type="@typeof(PrismjsStyleBundleContributor)"/>
<abp-style type="@typeof(MalihuCustomScrollbarPluginStyleBundleContributor)"/>
<abp-style src="/Pages/Documents/Project/bootstrap-toc.css"/>
<abp-style src="/Pages/Documents/Shared/Styles/vs.css"/>
<abp-style src="/Pages/Documents/Project/index.css"/>
@if (DocsUiOptions.Value.EnableEnlargeImage)
@ -72,7 +71,6 @@
<abp-script type="@typeof(PopperJsScriptBundleContributor)"/>
<abp-script src="/client-proxies/docs-proxy.js"/>
<abp-script src="/client-proxies/docs-common-proxy.js"/>
<abp-script src="/Pages/Documents/Project/bootstrap-toc.js"/>
<abp-script src="/Pages/Documents/Shared/Scripts/vs.js"/>
<abp-script src="/Pages/Documents/Project/index.js"/>
@if (DocsUiOptions.Value.EnableEnlargeImage)
@ -600,6 +598,7 @@
<div id="scroll-index" class="">
<nav id="docs-sticky-index" class="navbar index-scroll">
@Html.Raw(Model.TocHtml)
</nav>
<div class="row">
<div class="col p-0">

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

@ -28,6 +28,7 @@ using Volo.Docs.Projects;
using Volo.Docs.GitHub.Documents.Version;
using Volo.Docs.Localization;
using Volo.Docs.Utils;
using Volo.Docs.TableOfContents;
namespace Volo.Docs.Pages.Documents.Project
{
@ -73,7 +74,9 @@ namespace Volo.Docs.Pages.Documents.Project
public VersionInfoViewModel LatestVersionInfo { get; private set; }
public string DocumentsUrlPrefix { get; set; }
public string DocumentsUrlPrefix { get; set; }
public string TocHtml { get; set; } = string.Empty;
public bool ShowProjectsCombobox { get; set; }
@ -105,6 +108,7 @@ namespace Volo.Docs.Pages.Documents.Project
private readonly DocsUiOptions _uiOptions;
private readonly IPermissionChecker _permissionChecker;
private readonly IDocumentPdfAppService _documentPdfAppService;
private readonly ITocGeneratorService _tocGeneratorService;
protected IDocsLinkGenerator DocsLinkGenerator => LazyServiceProvider.LazyGetRequiredService<IDocsLinkGenerator>();
@ -117,7 +121,8 @@ namespace Volo.Docs.Pages.Documents.Project
IOptions<DocsUiOptions> options,
IWebDocumentSectionRenderer webDocumentSectionRenderer,
IPermissionChecker permissionChecker,
IDocumentPdfAppService documentPdfAppService)
IDocumentPdfAppService documentPdfAppService,
ITocGeneratorService tocGeneratorService)
{
ObjectMapperContext = typeof(DocsWebModule);
@ -128,6 +133,7 @@ namespace Volo.Docs.Pages.Documents.Project
_permissionChecker = permissionChecker;
_documentPdfAppService = documentPdfAppService;
_uiOptions = options.Value;
_tocGeneratorService = tocGeneratorService;
LocalizationResourceType = typeof(DocsResource);
}
@ -535,6 +541,15 @@ namespace Volo.Docs.Pages.Documents.Project
DocumentNameWithExtension = Document.Name;
SetDocumentPageTitle();
await ConvertDocumentContentToHtmlAsync();
if (Document != null && !string.IsNullOrEmpty(Document.Content))
{
var (toc, processedContent) = _tocGeneratorService.GenerateTocAndProcessHeadings(Document.Content);
Document.Content = processedContent;
TocHtml = toc;
}
return true;
}
catch (DocumentNotFoundException e)

2
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js

@ -42,8 +42,6 @@ var doc = doc || {};
$ul.append($li);
$lazyLiElement.append($ul)
window.Toc.helpers.initNavEvent();
},
loadAll : function(lazyLiElements){
if(doc.lazyExpandableNavigation.isAllLoaded){

12
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss

@ -71,4 +71,16 @@ body {
background-color: transparent;
border-color: transparent;
font-size: 14px;
}
.toc-item-has-children > ul {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
}
.toc-item-has-children.open > ul {
max-height: 1000px;
opacity: 1;
}

43
modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js

@ -1,8 +1,6 @@
(function ($) {
$(function () {
window.Toc.helpers.initNavEvent();
var scrollTopBtn = $('.scroll-top-btn');
var enoughHeight = $('.docs-sidebar-wrapper > .docs-top').height();
var enoughHeightPlus = 500;
@ -64,10 +62,10 @@
handleCustomScrolls();
var $myNav = $('#docs-sticky-index');
Toc.init($myNav);
$('body').scrollspy({
target: $myNav,
offset:100
});
$('#docs-sticky-index a').on('click', function (event) {
@ -86,6 +84,23 @@
}
});
$("body").on('activate.bs.scrollspy', function (e) {
var $activeLink = $('.nav-link.active', $('#docs-sticky-index'));
var $activeLi = $activeLink.parent('li.nav-item');
$myNav.find('li.toc-item-has-children.open').each(function () {
if ($(this).has($activeLi).length === 0) {
$(this).removeClass('open');
}
});
var $parentToOpen = $activeLi.closest('li.toc-item-has-children');
if ($parentToOpen.length > 0) {
$parentToOpen.addClass('open');
}
});
$('.btn-toggle').on('click', function () {
$('.toggle-row').slideToggle(400);
$(this).toggleClass('less');
@ -99,6 +114,7 @@
$('.docs-tree-list').slideToggle();
});
initMenuToggle();
scrollToHashLink();
});
@ -125,26 +141,7 @@
});
}
window.Toc.helpers.createNavList = function () {
return $('<ul class="nav nav-pills flex-column"></ul>');
};
window.Toc.helpers.createChildNavList = function ($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
};
window.Toc.helpers.generateNavEl = function (anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr('href', '#' + anchor);
$a.text(text);
var $li = $('<li class="nav-item"></li>');
$li.append($a);
return $li;
};
window.Toc.helpers.initNavEvent = function () {
function initMenuToggle() {
$('li:not(.last-link) a.tree-toggle').off('click');
$('li:not(.last-link) span.plus-icon i.fa-chevron-right').off('click');

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

@ -0,0 +1,8 @@
using Volo.Abp.Application.Services;
namespace Volo.Docs.TableOfContents;
public interface ITocGeneratorService : IApplicationService
{
(string TocHtml, string ProcessedContent) GenerateTocAndProcessHeadings(string content);
}

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

@ -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();
}
}

1
modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj

@ -21,6 +21,7 @@
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Packages\Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj" />
<ProjectReference Include="..\Volo.Docs.Application.Contracts\Volo.Docs.Application.Contracts.csproj" />
<PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="Markdig.Signed" />
<PackageReference Include="Scriban" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />

Loading…
Cancel
Save