mirror of https://github.com/abpframework/abp.git
Browse Source
Replaces HTML-based TOC generation with a model-based approach by extracting heading data from markdown and rendering the table of contents via a new partial view. Removes the custom Markdig extension and related classes, updates the service interface, and adapts the page model and views to use the new heading list.pull/23666/head
8 changed files with 145 additions and 183 deletions
@ -0,0 +1,73 @@ |
|||
@model Volo.Docs.Pages.Documents.Project.IndexModel |
|||
@{ |
|||
if (Model.TocHeadings == null ||Model.TocHeadings.Count == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var relevantHeadings = Model.TocHeadings |
|||
.Where(h => h.Level is 2 or 3) |
|||
.ToList(); |
|||
|
|||
if (relevantHeadings.Count == 0) |
|||
{ |
|||
relevantHeadings = Model.TocHeadings |
|||
.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) |
|||
{ |
|||
@while (heading.Level < levelStack.Peek()) |
|||
{ |
|||
@:</li></ul> |
|||
levelStack.Pop(); |
|||
} |
|||
@:</li> |
|||
} |
|||
else if (heading.Level > previousLevel) |
|||
{ |
|||
@:<ul class="nav nav-pills flex-column"> |
|||
levelStack.Push(heading.Level); |
|||
} |
|||
else if (i > 0) |
|||
{ |
|||
@:</li> |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace Volo.Docs.Pages.Documents.Project; |
|||
|
|||
public class TableOfContentsModel : AbpPageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
} |
|||
} |
|||
|
|||
@ -1,73 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Markdig.Renderers; |
|||
using Markdig.Renderers.Html; |
|||
using Markdig.Syntax; |
|||
using Markdig.Syntax.Inlines; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public class CustomHeadingRenderer : MarkdownObjectRenderer<HtmlRenderer, HeadingBlock> |
|||
{ |
|||
private readonly HeadingExtractionExtension _extension; |
|||
private readonly HeadingRenderer _originalRenderer; |
|||
|
|||
public CustomHeadingRenderer(HeadingExtractionExtension extension, HeadingRenderer originalRenderer) |
|||
{ |
|||
_extension = extension; |
|||
_originalRenderer = originalRenderer ?? new HeadingRenderer(); |
|||
} |
|||
|
|||
protected override void Write(HtmlRenderer renderer, HeadingBlock headingBlock) |
|||
{ |
|||
var headingText = GetPlainText(headingBlock.Inline); |
|||
var headingId = headingBlock.TryGetAttributes()?.Id ?? string.Empty; |
|||
_extension.Headings.Add((headingBlock.Level, headingText, headingId)); |
|||
_originalRenderer.Write(renderer, headingBlock); |
|||
} |
|||
|
|||
private static string GetPlainText(ContainerInline container) |
|||
{ |
|||
if (container == null) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
var builder = new StringBuilder(); |
|||
|
|||
var inlinesToProcess = new Stack<Inline>(); |
|||
|
|||
// Push items in reverse for left-to-right processing (LIFO stack behavior)
|
|||
foreach (var inline in container.Reverse()) |
|||
{ |
|||
inlinesToProcess.Push(inline); |
|||
} |
|||
|
|||
while (inlinesToProcess.Count > 0) |
|||
{ |
|||
var currentInline = inlinesToProcess.Pop(); |
|||
|
|||
switch (currentInline) |
|||
{ |
|||
// Case 1: Simple leaf nodes with text content
|
|||
case LiteralInline literal: |
|||
builder.Append(literal.Content); |
|||
break; |
|||
case CodeInline code: |
|||
builder.Append(code.Content); |
|||
break; |
|||
|
|||
// Case 2: Container nodes - process their children next
|
|||
case ContainerInline childContainer: |
|||
foreach (var childInline in childContainer.Reverse()) |
|||
{ |
|||
inlinesToProcess.Push(childInline); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return builder.ToString(); |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Markdig; |
|||
using Markdig.Renderers; |
|||
using Markdig.Renderers.Html; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public class HeadingExtractionExtension : IMarkdownExtension |
|||
{ |
|||
public List<(int Level, string Text, string Id)> Headings { get; } = []; |
|||
|
|||
public void Setup(MarkdownPipelineBuilder pipeline) |
|||
{ |
|||
} |
|||
|
|||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) |
|||
{ |
|||
if (renderer is not HtmlRenderer) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var originalHeadingRenderer = renderer.ObjectRenderers.Find<HeadingRenderer>(); |
|||
if (originalHeadingRenderer != null) |
|||
{ |
|||
renderer.ObjectRenderers.Remove(originalHeadingRenderer); |
|||
} |
|||
renderer.ObjectRenderers.Add(new CustomHeadingRenderer(this, originalHeadingRenderer)); |
|||
} |
|||
} |
|||
@ -1,8 +1,10 @@ |
|||
using Volo.Abp.Application.Services; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Application.Services; |
|||
using static Volo.Docs.TableOfContents.TocGeneratorService; |
|||
|
|||
namespace Volo.Docs.TableOfContents; |
|||
|
|||
public interface ITocGeneratorService : IApplicationService |
|||
{ |
|||
string GenerateToc(string markdownContent); |
|||
List<Heading> GenerateTocHeadings(string markdownContent); |
|||
} |
|||
|
|||
Loading…
Reference in new issue