feat: 从Bootstrap迁移到Tailwind css (#9)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m15s

Reviewed-on: #9
This commit is contained in:
2025-01-24 16:46:56 +08:00
parent 1ceaf30061
commit 3aae468e65
85 changed files with 1660 additions and 951 deletions

View File

@@ -0,0 +1,102 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
/// <summary>
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
/// </summary>
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
{
public string Name => nameof(EssayStylesPostRenderProcessor);
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document);
BeatifyTable(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-slate-300 rounded-sm overflow-x-auto" },
{ "h2", "text-3xl font-bold py-4" },
{ "h3", "text-2xl font-bold py-3" },
{ "h4", "text-xl font-bold py-2" },
{ "h5", "text-lg font-bold py-1" },
{ "p", "p-2" },
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
{ "ul", "list-disc pl-2" }
};
private void ApplyGlobalCssStyles(IDocument document)
{
foreach ((string tag, string style) in _globalCssStyles)
{
foreach (IElement element in document.GetElementsByTagName(tag))
{
element.ClassList.Add(style);
}
}
}
private static void BeatifyTable(IDocument document)
{
foreach (IElement element in from e in document.All
where e.LocalName == "table"
select e)
{
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
// thead元素
foreach (IElement headElement in from e in element.Children
where e.LocalName == "thead"
select e)
{
headElement.ClassList.Add("bg-slate-200");
// tr in thead
foreach (IElement trElement in from e in headElement.Children
where e.LocalName == "tr"
select e)
{
trElement.ClassList.Add("border border-slate-300");
// th in tr
foreach (IElement thElement in from e in trElement.Children
where e.LocalName == "th"
select e)
{
thElement.ClassList.Add("px-4 py-1");
}
}
}
// tbody元素
foreach (IElement bodyElement in from e in element.Children
where e.LocalName == "tbody"
select e)
{
// tr in tbody
foreach (IElement trElement in from e in bodyElement.Children
where e.LocalName == "tr"
select e)
{
foreach (IElement tdElement in from e in trElement.Children
where e.LocalName == "td"
select e)
{
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
}
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
public class HeadlinePostRenderProcessor(
AngleSharp.IConfiguration angleConfiguration,
IEssayContentService essayContentService,
ILogger<HeadlinePostRenderProcessor> logger) : 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);
if (!essayContentService.TryAddHeadline(essay.FileName, topHeadline))
{
logger.LogWarning("Failed to add headline of {}.", essay.FileName);
}
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);
}

View File

@@ -0,0 +1,61 @@
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
namespace YaeBlog.Processors;
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
private readonly BlogOptions _options = options.Value;
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument html = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IElement> imageElements = from node in html.All
where node.LocalName == "img"
select node;
foreach (IElement element in imageElements)
{
IAttr? attr = element.Attributes.GetNamedItem("src");
if (attr is not null)
{
logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
}
}
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
}
public string Name => nameof(ImagePostRenderProcessor);
private string GenerateImageLink(string filename, string essayFilename)
{
if (!filename.Contains(essayFilename))
{
filename = Path.Combine(essayFilename, filename);
}
filename = Path.Combine(_options.Root, "posts", filename);
if (!Path.Exists(filename))
{
logger.LogError("Failed to found image: {}.", filename);
throw new BlogFileException($"Image {filename} doesn't exist.");
}
string imageLink = "api/files/" + filename;
logger.LogDebug("Generate image link '{}' for image file '{}'.",
imageLink, filename);
return imageLink;
}
}