feat: 从Bootstrap迁移到Tailwind css (#9)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m15s
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m15s
Reviewed-on: #9
This commit is contained in:
102
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal file
102
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
YaeBlog/Processors/HeadlinePostRenderProcessor.cs
Normal file
108
YaeBlog/Processors/HeadlinePostRenderProcessor.cs
Normal 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);
|
||||
}
|
||||
61
YaeBlog/Processors/ImagePostRenderProcessor.cs
Normal file
61
YaeBlog/Processors/ImagePostRenderProcessor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user