diff --git a/Directory.Packages.props b/Directory.Packages.props index 05b99f9dd1..ae0429b944 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + 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 21d4aab5ac..aac7adbf5e 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 @@ -39,7 +39,6 @@ - @if (DocsUiOptions.Value.EnableEnlargeImage) @@ -72,7 +71,6 @@ - @if (DocsUiOptions.Value.EnableEnlargeImage) @@ -600,6 +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 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 @@ +