mirror of https://github.com/abpframework/abp.git
committed by
GitHub
10 changed files with 325 additions and 31 deletions
@ -0,0 +1,41 @@ |
|||||
|
@using Volo.Docs.TableOfContents |
||||
|
@model List<TocItem> |
||||
|
@{ |
||||
|
if (Model == null || Model.Count == 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
<ul class="nav nav-pills flex-column"> |
||||
|
@foreach (var item in Model) |
||||
|
{ |
||||
|
<li class="nav-item @(item.Children.Any() ? "toc-item-has-children" : "")"> |
||||
|
<a class="nav-link" href="#@item.Heading.Id">@item.Heading.Text</a> |
||||
|
|
||||
|
@if (item.Children.Any()) |
||||
|
{ |
||||
|
RenderChildrenRecursive(item.Children, 1); |
||||
|
} |
||||
|
</li> |
||||
|
} |
||||
|
</ul> |
||||
|
|
||||
|
@{ |
||||
|
void RenderChildrenRecursive(List<TocItem> children, int depth) |
||||
|
{ |
||||
|
<ul class="nav nav-pills flex-column toc-depth-@depth"> |
||||
|
@foreach (var child in children) |
||||
|
{ |
||||
|
<li class="nav-item @(child.Children.Any() ? "toc-item-has-children" : "")"> |
||||
|
<a class="nav-link" href="#@child.Heading.Id">@child.Heading.Text</a> |
||||
|
|
||||
|
@if (child.Children.Any()) |
||||
|
{ |
||||
|
RenderChildrenRecursive(child.Children, depth + 1); |
||||
|
} |
||||
|
</li> |
||||
|
} |
||||
|
</ul> |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace Volo.Docs.TableOfContents; |
||||
|
|
||||
|
public interface ITocGeneratorService : IApplicationService |
||||
|
{ |
||||
|
List<TocHeading> GenerateTocHeadings(string markdownContent); |
||||
|
|
||||
|
List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int levelCount); |
||||
|
|
||||
|
int GetTopLevel(List<TocHeading> tocHeadings); |
||||
|
|
||||
|
List<TocItem> GenerateTocItems(string markdownContent, int levelCount, int? topLevel = null); |
||||
|
} |
||||
@ -0,0 +1,209 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using Markdig; |
||||
|
using Markdig.Extensions.AutoIdentifiers; |
||||
|
using Markdig.Renderers.Html; |
||||
|
using Markdig.Syntax; |
||||
|
using Markdig.Syntax.Inlines; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Docs.TableOfContents; |
||||
|
|
||||
|
public class TocGeneratorService : ITocGeneratorService, ITransientDependency |
||||
|
{ |
||||
|
private const int MinHeadingLevel = 1; |
||||
|
private const int MaxHeadingLevel = 6; |
||||
|
|
||||
|
public virtual List<TocHeading> GenerateTocHeadings(string markdownContent) |
||||
|
{ |
||||
|
if (markdownContent.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return new List<TocHeading>(); |
||||
|
} |
||||
|
|
||||
|
var markdownPipeline = CreateMarkdownPipeline(); |
||||
|
var document = Markdig.Markdown.Parse(markdownContent, markdownPipeline); |
||||
|
var headingBlocks = document.Descendants<HeadingBlock>(); |
||||
|
|
||||
|
return headingBlocks |
||||
|
.Select(CreateTocHeading) |
||||
|
.ToList(); |
||||
|
} |
||||
|
|
||||
|
public virtual List<TocItem> GenerateTocItems(List<TocHeading> tocHeadings, int topLevel, int levelCount) |
||||
|
{ |
||||
|
var maxLevel = GetMaxLevel(tocHeadings, topLevel, levelCount); |
||||
|
|
||||
|
var filteredHeadings = tocHeadings |
||||
|
.Where(heading => heading.Level >= topLevel && heading.Level <= maxLevel) |
||||
|
.ToList(); |
||||
|
|
||||
|
return BuildHierarchicalStructure(filteredHeadings, topLevel); |
||||
|
} |
||||
|
|
||||
|
public virtual int GetTopLevel(List<TocHeading> headings) |
||||
|
{ |
||||
|
return Enumerable.Range(MinHeadingLevel, MaxHeadingLevel) |
||||
|
.FirstOrDefault(level => headings.Count(h => h.Level == level) > 1, MinHeadingLevel); |
||||
|
} |
||||
|
|
||||
|
public virtual List<TocItem> GenerateTocItems(string markdownContent, int levelCount, int? topLevel = null) |
||||
|
{ |
||||
|
var headings = GenerateTocHeadings(markdownContent); |
||||
|
if (headings.Count == 0) |
||||
|
{ |
||||
|
return new List<TocItem>(); |
||||
|
} |
||||
|
|
||||
|
var resolvedTopLevel = topLevel ?? GetTopLevel(headings); |
||||
|
return GenerateTocItems(headings, resolvedTopLevel, levelCount); |
||||
|
} |
||||
|
|
||||
|
protected virtual MarkdownPipeline CreateMarkdownPipeline() |
||||
|
{ |
||||
|
return new MarkdownPipelineBuilder() |
||||
|
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub) |
||||
|
.UseAdvancedExtensions() |
||||
|
.Build(); |
||||
|
} |
||||
|
|
||||
|
protected virtual TocHeading CreateTocHeading(HeadingBlock headingBlock) |
||||
|
{ |
||||
|
var plainText = GetPlainText(headingBlock.Inline); |
||||
|
var id = headingBlock.GetAttributes().Id; |
||||
|
return new TocHeading(headingBlock.Level, plainText, id); |
||||
|
} |
||||
|
|
||||
|
protected virtual List<TocItem> BuildHierarchicalStructure(List<TocHeading> headings, int topLevel) |
||||
|
{ |
||||
|
var result = new List<TocItem>(); |
||||
|
|
||||
|
for (var i = 0; i < headings.Count; i++) |
||||
|
{ |
||||
|
var currentHeading = headings[i]; |
||||
|
|
||||
|
if (currentHeading.Level != topLevel) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var children = GetDirectChildren(headings, i, currentHeading.Level); |
||||
|
result.Add(new TocItem(currentHeading, children)); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
protected virtual List<TocItem> GetDirectChildren(List<TocHeading> allHeadings, int parentIndex, int parentLevel) |
||||
|
{ |
||||
|
var targetChildLevel = parentLevel + 1; |
||||
|
var children = new List<TocItem>(); |
||||
|
|
||||
|
for (var i = parentIndex + 1; i < allHeadings.Count; i++) |
||||
|
{ |
||||
|
var heading = allHeadings[i]; |
||||
|
|
||||
|
// Stop if we encounter a heading at the same level or higher than parent
|
||||
|
if (heading.Level <= parentLevel) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// Only process direct children (not grandchildren)
|
||||
|
if (heading.Level != targetChildLevel) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var grandChildren = GetDirectChildren(allHeadings, i, heading.Level); |
||||
|
children.Add(new TocItem(heading, grandChildren)); |
||||
|
} |
||||
|
|
||||
|
return children; |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetPlainText(ContainerInline container) |
||||
|
{ |
||||
|
if (container == null) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
// Optimization for simple case with single literal inline
|
||||
|
if (HasExactCount(container, 1) && container.First() is LiteralInline singleLiteral) |
||||
|
{ |
||||
|
return singleLiteral.Content.ToString(); |
||||
|
} |
||||
|
|
||||
|
return ProcessInlineContent(container); |
||||
|
} |
||||
|
|
||||
|
protected virtual string ProcessInlineContent(ContainerInline container) |
||||
|
{ |
||||
|
var textBuilder = new StringBuilder(); |
||||
|
var processingQueue = new Queue<Inline>(); |
||||
|
|
||||
|
// Enqueue all initial inlines
|
||||
|
foreach (var inline in container) |
||||
|
{ |
||||
|
processingQueue.Enqueue(inline); |
||||
|
} |
||||
|
|
||||
|
// Process each inline in the queue
|
||||
|
while (processingQueue.Count > 0) |
||||
|
{ |
||||
|
var currentInline = processingQueue.Dequeue(); |
||||
|
AppendInlineText(currentInline, textBuilder, processingQueue); |
||||
|
} |
||||
|
|
||||
|
return textBuilder.ToString(); |
||||
|
} |
||||
|
|
||||
|
protected virtual void AppendInlineText(Inline inline, StringBuilder builder, Queue<Inline> processingQueue) |
||||
|
{ |
||||
|
switch (inline) |
||||
|
{ |
||||
|
case LiteralInline literal: |
||||
|
builder.Append(literal.Content); |
||||
|
break; |
||||
|
|
||||
|
case CodeInline code: |
||||
|
builder.Append(code.Content); |
||||
|
break; |
||||
|
|
||||
|
case ContainerInline containerInline: |
||||
|
foreach (var childInline in containerInline) |
||||
|
{ |
||||
|
processingQueue.Enqueue(childInline); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual int GetMaxLevel(List<TocHeading> tocHeadings, int topLevel, int levelCount) |
||||
|
{ |
||||
|
return tocHeadings.Where(h => h.Level >= topLevel) |
||||
|
.Select(h => h.Level) |
||||
|
.Distinct() |
||||
|
.OrderBy(level => level) |
||||
|
.Skip(levelCount - 1) |
||||
|
.FirstOrDefault(topLevel); |
||||
|
} |
||||
|
|
||||
|
protected virtual bool HasExactCount<T>(IEnumerable<T> enumerable, int count) |
||||
|
{ |
||||
|
var itemCount = 0; |
||||
|
foreach (var _ in enumerable) |
||||
|
{ |
||||
|
itemCount++; |
||||
|
if (itemCount > count) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return itemCount == count; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Docs.TableOfContents; |
||||
|
|
||||
|
public record TocHeading(int Level, string Text, string Id); |
||||
|
|
||||
|
public record TocItem(TocHeading Heading, List<TocItem> Children); |
||||
Loading…
Reference in new issue