feat: 美化文章界面 #3
8
YaeBlog.Core/Abstractions/ITableOfContentService.cs
Normal file
8
YaeBlog.Core/Abstractions/ITableOfContentService.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface ITableOfContentService
|
||||||
|
{
|
||||||
|
public IReadOnlyDictionary<string, BlogHeadline> Headlines { get; }
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.AspNetCore.Builder;
|
using AngleSharp;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Core.Abstractions;
|
using YaeBlog.Core.Abstractions;
|
||||||
|
@ -18,14 +19,19 @@ public static class WebApplicationBuilderExtensions
|
||||||
|
|
||||||
builder.Services.AddMarkdig();
|
builder.Services.AddMarkdig();
|
||||||
builder.Services.AddYamlParser();
|
builder.Services.AddYamlParser();
|
||||||
|
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
||||||
builder.Services.AddSingleton<EssayScanService>();
|
builder.Services.AddSingleton<EssayScanService>();
|
||||||
builder.Services.AddSingleton<RendererService>();
|
builder.Services.AddSingleton<RendererService>();
|
||||||
builder.Services.AddSingleton<EssayContentService>();
|
builder.Services.AddSingleton<EssayContentService>();
|
||||||
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
|
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
|
||||||
provider.GetRequiredService<EssayContentService>());
|
provider.GetRequiredService<EssayContentService>());
|
||||||
|
builder.Services.AddSingleton<TableOfContentService>();
|
||||||
|
builder.Services.AddSingleton<ITableOfContentService, TableOfContentService>(provider =>
|
||||||
|
provider.GetRequiredService<TableOfContentService>());
|
||||||
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
||||||
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
|
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
|
||||||
builder.Services.AddTransient<TablePostRenderProcessor>();
|
builder.Services.AddTransient<TablePostRenderProcessor>();
|
||||||
|
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
|
||||||
builder.Services.AddTransient<BlogOptions>(provider =>
|
builder.Services.AddTransient<BlogOptions>(provider =>
|
||||||
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ public static class WebApplicationExtensions
|
||||||
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
||||||
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
|
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
|
||||||
application.UsePostRenderProcessor<TablePostRenderProcessor>();
|
application.UsePostRenderProcessor<TablePostRenderProcessor>();
|
||||||
|
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
||||||
|
|
10
YaeBlog.Core/Models/BlogHeadline.cs
Normal file
10
YaeBlog.Core/Models/BlogHeadline.cs
Normal file
|
@ -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<BlogHeadline> Children { get; } = [];
|
||||||
|
}
|
105
YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs
Normal file
105
YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs
Normal file
|
@ -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<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||||
|
{
|
||||||
|
BrowsingContext browsingContext = new(angleConfiguration);
|
||||||
|
IDocument document = await browsingContext.OpenAsync(req => req.Content(essay.HtmlContent));
|
||||||
|
|
||||||
|
IEnumerable<IElement> elements = from item in document.All
|
||||||
|
where item.LocalName is "h2" or "h3" or "h4"
|
||||||
|
select item;
|
||||||
|
|
||||||
|
BlogHeadline topHeadline = new(essay.Title, "#title");
|
||||||
|
List<BlogHeadline> level2List = [];
|
||||||
|
List<BlogHeadline> level3List = [];
|
||||||
|
List<BlogHeadline> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 找到h4标题的父级标题
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topHeadline"></param>
|
||||||
|
/// <param name="level2"></param>
|
||||||
|
/// <param name="level3"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static BlogHeadline FindParentHeadline(BlogHeadline topHeadline, List<BlogHeadline> level2,
|
||||||
|
List<BlogHeadline> level3)
|
||||||
|
{
|
||||||
|
BlogHeadline? result = level3.LastOrDefault();
|
||||||
|
if (result is not null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return level2.LastOrDefault() ?? topHeadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 找到h3标题的父级标题
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topHeadline"></param>
|
||||||
|
/// <param name="level2"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static BlogHeadline FindParentHeadline(BlogHeadline topHeadline, List<BlogHeadline> level2) =>
|
||||||
|
FindParentHeadline(topHeadline, level2, []);
|
||||||
|
|
||||||
|
public string Name => nameof(HeadlinePostRenderProcessor);
|
||||||
|
}
|
20
YaeBlog.Core/Services/TableOfContentService.cs
Normal file
20
YaeBlog.Core/Services/TableOfContentService.cs
Normal file
|
@ -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<string, BlogHeadline> _headlines = [];
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, BlogHeadline> Headlines => _headlines;
|
||||||
|
|
||||||
|
public void AddHeadline(string filename, BlogHeadline headline)
|
||||||
|
{
|
||||||
|
if (!_headlines.TryAdd(filename, headline))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
@using YaeBlog.Core.Models
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
|
@inject ITableOfContentService TableOfContent
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<h1>@(_essay!.Title)</h1>
|
<h1 id="title">@(_essay!.Title)</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,16 +42,75 @@
|
||||||
|
|
||||||
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-12">
|
||||||
|
<div class="row sticky-lg-top justify-content-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
|
||||||
|
文章目录
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="padding-left: 10px">
|
||||||
|
<div class="col-auto">
|
||||||
|
@foreach (BlogHeadline level2 in _headline!.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (BlogHeadline level3 in level2.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
|
||||||
|
@(level3.Title)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (BlogHeadline level4 in level3.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
|
||||||
|
@(level4.Title)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_headline!.Children.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col fst-italic">
|
||||||
|
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter] public string? BlogKey { get; set; }
|
||||||
public string? BlogKey { get; set; }
|
|
||||||
|
|
||||||
private BlogEssay? _essay;
|
private BlogEssay? _essay;
|
||||||
|
|
||||||
|
private BlogHeadline? _headline;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
|
@ -65,6 +125,11 @@
|
||||||
{
|
{
|
||||||
NavigationInstance.NavigateTo("/NotFound");
|
NavigationInstance.NavigateTo("/NotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_headline = TableOfContent.Headlines[BlogKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GenerateSelectorUrl(string selectorId)
|
||||||
|
=> $"/blog/essays/{BlogKey!}#{selectorId}";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user