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 8ffe5c4c7a..d6e86c585d 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
@@ -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
();
@@ -117,7 +121,8 @@ namespace Volo.Docs.Pages.Documents.Project
IOptions 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)
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js
index d55d9f2ef9..2e3bdf1f30 100644
--- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.js
+++ b/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){
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss
index 9589b1fb73..1af1c41cf5 100644
--- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/index.scss
+++ b/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;
}
\ No newline at end of file
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js
index 5208ac99c0..0bddcf844c 100644
--- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js
+++ b/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 $('');
- };
-
- 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.attr('href', '#' + anchor);
- $a.text(text);
- var $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');
diff --git a/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs b/modules/docs/src/Volo.Docs.Web/TableOfContents/ITocGeneratorService.cs
new file mode 100644
index 0000000000..3c3e77e88a
--- /dev/null
+++ b/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);
+}
diff --git a/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs b/modules/docs/src/Volo.Docs.Web/TableOfContents/TocGeneratorService.cs
new file mode 100644
index 0000000000..d53bb0f91b
--- /dev/null
+++ b/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 _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("");
+
+ 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("
");
+ }
+ else if (!isFirstH2 && heading.Level == 2 && currentLevel == 2)
+ {
+ tocBuilder.Append("");
+ }
+
+ if (heading.Level == 2)
+ {
+ var liClass = hasChildren ? "nav-item toc-item-has-children" : "nav-item";
+ tocBuilder.Append($"{heading.Text}");
+ isFirstH2 = false;
+ }
+ else if (heading.Level == 3)
+ {
+ if (currentLevel != 3)
+ {
+ tocBuilder.Append("");
+ }
+
+ tocBuilder.Append($"- {heading.Text}
");
+ }
+
+ currentLevel = heading.Level;
+ }
+
+ if (currentLevel == 3)
+ {
+ tocBuilder.Append("
");
+ }
+ else if (currentLevel == 2 && !isFirstH2)
+ {
+ tocBuilder.Append("");
+ }
+
+ tocBuilder.Append("");
+
+ return tocBuilder.ToString();
+ }
+}
diff --git a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj
index 353f15811c..2215e85a92 100644
--- a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj
+++ b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj
@@ -21,6 +21,7 @@
+