From 91ae6d8ac1a64cb9754b9d3630937e989ee8734a Mon Sep 17 00:00:00 2001 From: jackfiled Date: Mon, 29 Jul 2024 21:52:23 +0800 Subject: [PATCH] add: table of contents --- .../Abstractions/ITableOfContentService.cs | 8 ++ .../WebApplicationBuilderExtensions.cs | 8 +- .../Extensions/WebApplicationExtensions.cs | 1 + YaeBlog.Core/Models/BlogHeadline.cs | 10 ++ .../Processors/HeadlinePostRenderProcessor.cs | 105 ++++++++++++++++++ .../Services/TableOfContentService.cs | 20 ++++ YaeBlog/Pages/Essays.razor | 71 +++++++++++- 7 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 YaeBlog.Core/Abstractions/ITableOfContentService.cs create mode 100644 YaeBlog.Core/Models/BlogHeadline.cs create mode 100644 YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs create mode 100644 YaeBlog.Core/Services/TableOfContentService.cs diff --git a/YaeBlog.Core/Abstractions/ITableOfContentService.cs b/YaeBlog.Core/Abstractions/ITableOfContentService.cs new file mode 100644 index 0000000..08b997c --- /dev/null +++ b/YaeBlog.Core/Abstractions/ITableOfContentService.cs @@ -0,0 +1,8 @@ +using YaeBlog.Core.Models; + +namespace YaeBlog.Core.Abstractions; + +public interface ITableOfContentService +{ + public IReadOnlyDictionary Headlines { get; } +} diff --git a/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs b/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs index ff8c570..d469a33 100644 --- a/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs +++ b/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Builder; +using AngleSharp; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using YaeBlog.Core.Abstractions; @@ -18,14 +19,19 @@ public static class WebApplicationBuilderExtensions builder.Services.AddMarkdig(); builder.Services.AddYamlParser(); + builder.Services.AddSingleton(_ => Configuration.Default); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => provider.GetRequiredService()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(provider => + provider.GetRequiredService()); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(provider => provider.GetRequiredService>().Value); diff --git a/YaeBlog.Core/Extensions/WebApplicationExtensions.cs b/YaeBlog.Core/Extensions/WebApplicationExtensions.cs index 4ab3d8e..563395c 100644 --- a/YaeBlog.Core/Extensions/WebApplicationExtensions.cs +++ b/YaeBlog.Core/Extensions/WebApplicationExtensions.cs @@ -13,6 +13,7 @@ public static class WebApplicationExtensions application.UsePostRenderProcessor(); application.UsePostRenderProcessor(); application.UsePostRenderProcessor(); + application.UsePostRenderProcessor(); } private static void UsePreRenderProcessor(this WebApplication application) where T : IPreRenderProcessor diff --git a/YaeBlog.Core/Models/BlogHeadline.cs b/YaeBlog.Core/Models/BlogHeadline.cs new file mode 100644 index 0000000..f328290 --- /dev/null +++ b/YaeBlog.Core/Models/BlogHeadline.cs @@ -0,0 +1,10 @@ +namespace YaeBlog.Core.Models; + +public class BlogHeadline(string title, string selectorId) +{ + public string Title { get; } = title; + + public string SelectorId { get; set; } = selectorId; + + public List Children { get; } = []; +} diff --git a/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs b/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs new file mode 100644 index 0000000..ddfed50 --- /dev/null +++ b/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs @@ -0,0 +1,105 @@ +using AngleSharp; +using AngleSharp.Dom; +using YaeBlog.Core.Abstractions; +using YaeBlog.Core.Models; +using YaeBlog.Core.Services; + +namespace YaeBlog.Core.Processors; + +public class HeadlinePostRenderProcessor( + IConfiguration angleConfiguration, + TableOfContentService tableOfContentService) : IPostRenderProcessor +{ + public async Task ProcessAsync(BlogEssay essay) + { + BrowsingContext browsingContext = new(angleConfiguration); + IDocument document = await browsingContext.OpenAsync(req => req.Content(essay.HtmlContent)); + + IEnumerable elements = from item in document.All + where item.LocalName is "h2" or "h3" or "h4" + select item; + + BlogHeadline topHeadline = new(essay.Title, "#title"); + List level2List = []; + List level3List = []; + List level4List = []; + + foreach (IElement element in elements) + { + switch (element.LocalName) + { + case "h2": + { + FindParentHeadline(topHeadline, level2List, level3List).Children.AddRange(level4List); + level4List.Clear(); + FindParentHeadline(topHeadline, level2List).Children.AddRange(level3List); + level3List.Clear(); + + BlogHeadline headline = ParserHeadlineElement(element); + level2List.Add(headline); + break; + } + case "h3": + { + FindParentHeadline(topHeadline, level2List, level3List).Children.AddRange(level4List); + level4List.Clear(); + + BlogHeadline headline = ParserHeadlineElement(element); + level3List.Add(headline); + break; + } + case "h4": + { + BlogHeadline headline = ParserHeadlineElement(element); + level4List.Add(headline); + break; + } + } + } + + // 太抽象了((( + FindParentHeadline(topHeadline, level2List, level3List).Children.AddRange(level4List); + FindParentHeadline(topHeadline, level2List).Children.AddRange(level3List); + topHeadline.Children.AddRange(level2List); + + tableOfContentService.AddHeadline(essay.FileName, topHeadline); + + return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml); + } + + private static BlogHeadline ParserHeadlineElement(IElement element) + { + element.Id ??= element.TextContent; + return new BlogHeadline(element.TextContent, element.Id); + } + + /// + /// 找到h4标题的父级标题 + /// + /// + /// + /// + /// + private static BlogHeadline FindParentHeadline(BlogHeadline topHeadline, List level2, + List level3) + { + BlogHeadline? result = level3.LastOrDefault(); + if (result is not null) + { + return result; + } + + return level2.LastOrDefault() ?? topHeadline; + } + + /// + /// 找到h3标题的父级标题 + /// + /// + /// + /// + private static BlogHeadline FindParentHeadline(BlogHeadline topHeadline, List level2) => + FindParentHeadline(topHeadline, level2, []); + + public string Name => nameof(HeadlinePostRenderProcessor); +} diff --git a/YaeBlog.Core/Services/TableOfContentService.cs b/YaeBlog.Core/Services/TableOfContentService.cs new file mode 100644 index 0000000..4426016 --- /dev/null +++ b/YaeBlog.Core/Services/TableOfContentService.cs @@ -0,0 +1,20 @@ +using System.Collections.Concurrent; +using YaeBlog.Core.Abstractions; +using YaeBlog.Core.Models; + +namespace YaeBlog.Core.Services; + +public class TableOfContentService : ITableOfContentService +{ + private readonly ConcurrentDictionary _headlines = []; + + public IReadOnlyDictionary Headlines => _headlines; + + public void AddHeadline(string filename, BlogHeadline headline) + { + if (!_headlines.TryAdd(filename, headline)) + { + throw new InvalidOperationException(); + } + } +} diff --git a/YaeBlog/Pages/Essays.razor b/YaeBlog/Pages/Essays.razor index 6616712..84adb50 100644 --- a/YaeBlog/Pages/Essays.razor +++ b/YaeBlog/Pages/Essays.razor @@ -3,6 +3,7 @@ @using YaeBlog.Core.Models @inject IEssayContentService Contents +@inject ITableOfContentService TableOfContent @inject NavigationManager NavigationInstance @@ -12,7 +13,7 @@
-

@(_essay!.Title)

+

@(_essay!.Title)

@@ -41,16 +42,75 @@
+ +
+
+
+
+
+

+ 文章目录 +

+
+
+ +
+
+ @foreach (BlogHeadline level2 in _headline!.Children) + { +
+ +
+ + @foreach (BlogHeadline level3 in level2.Children) + { + + + @foreach (BlogHeadline level4 in level3.Children) + { + + } + } + } +
+
+ + @if (_headline!.Children.Count == 0) + { +
+
+ 坏了(* Ŏ∀Ŏ),没有在文章中识别到目录 +
+
+ } +
+
+
@code { - [Parameter] - public string? BlogKey { get; set; } + [Parameter] public string? BlogKey { get; set; } private BlogEssay? _essay; + private BlogHeadline? _headline; + protected override void OnInitialized() { base.OnInitialized(); @@ -65,6 +125,11 @@ { NavigationInstance.NavigateTo("/NotFound"); } + + _headline = TableOfContent.Headlines[BlogKey]; } + private string GenerateSelectorUrl(string selectorId) + => $"/blog/essays/{BlogKey!}#{selectorId}"; + }