YaeBlog/YaeBlog.Core/Services/RendererService.cs

195 lines
6.0 KiB
C#
Raw Normal View History

using System.Collections.Concurrent;
using System.Diagnostics;
2024-01-24 14:00:55 +08:00
using System.Text;
using System.Text.RegularExpressions;
2024-01-23 14:33:35 +08:00
using Markdig;
2024-01-17 13:20:32 +08:00
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
2024-01-17 13:20:32 +08:00
using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Services;
public partial class RendererService(
ILogger<RendererService> logger,
IEssayScanService essayScanService,
2024-01-17 13:20:32 +08:00
MarkdownPipeline markdownPipeline,
IEssayContentService essayContentService)
2024-01-17 13:20:32 +08:00
{
2024-01-23 14:33:35 +08:00
private readonly Stopwatch _stopwatch = new();
private readonly List<IPreRenderProcessor> _preRenderProcessors = [];
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
2024-01-17 13:20:32 +08:00
public async Task RenderAsync()
{
2024-01-23 14:33:35 +08:00
_stopwatch.Start();
logger.LogInformation("Render essays start.");
BlogContents contents = await essayScanService.ScanContents();
List<BlogContent> posts = contents.Posts.ToList();
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
2024-01-17 13:20:32 +08:00
2024-01-23 14:33:35 +08:00
List<BlogEssay> essays = [];
await Task.Run(() =>
{
foreach (BlogContent content in preProcessedContents)
2024-01-23 14:33:35 +08:00
{
uint wordCount = GetWordCount(content);
2024-01-23 14:33:35 +08:00
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.FileName,
2024-01-23 14:33:35 +08:00
FileName = content.FileName,
2024-01-24 14:00:55 +08:00
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
2024-01-23 14:33:35 +08:00
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
2024-01-23 14:33:35 +08:00
{
essay.Tags.AddRange(content.Metadata.Tags);
2024-01-23 14:33:35 +08:00
}
2024-01-23 14:33:35 +08:00
essays.Add(essay);
}
});
ConcurrentBag<BlogEssay> postProcessEssays = [];
2024-01-23 14:33:35 +08:00
Parallel.ForEach(essays, essay =>
2024-01-17 13:20:32 +08:00
{
2024-01-25 11:53:08 +08:00
BlogEssay newEssay =
essay.WithNewHtmlContent(Markdown.ToHtml(essay.HtmlContent, markdownPipeline));
2024-01-17 13:20:32 +08:00
postProcessEssays.Add(newEssay);
2024-01-23 14:33:35 +08:00
logger.LogDebug("Render markdown file {}.", newEssay);
2024-01-17 13:20:32 +08:00
});
2024-01-23 14:33:35 +08:00
2024-01-25 11:53:08 +08:00
await PostProcess(postProcessEssays);
2024-01-26 17:29:37 +08:00
essayContentService.RefreshTags();
2024-01-23 14:33:35 +08:00
_stopwatch.Stop();
logger.LogInformation("Render finished, consuming {} s.",
_stopwatch.Elapsed.ToString("s\\.fff"));
2024-01-17 13:20:32 +08:00
}
2024-01-19 20:33:41 +08:00
public void AddPreRenderProcessor(IPreRenderProcessor processor)
{
bool exist = _preRenderProcessors.Any(p => p.Name == processor.Name);
if (exist)
{
throw new InvalidOperationException("There exists one pre-render processor " +
$"with the same name: {processor.Name}.");
}
_preRenderProcessors.Add(processor);
}
public void AddPostRenderProcessor(IPostRenderProcessor processor)
{
bool exist = _postRenderProcessors.Any(p => p.Name == processor.Name);
if (exist)
{
throw new InvalidCastException("There exists one post-render processor " +
$"with the same name: {processor.Name}.");
}
_postRenderProcessors.Add(processor);
}
2024-01-25 11:53:08 +08:00
private async Task<IEnumerable<BlogContent>> PreProcess(IEnumerable<BlogContent> contents)
{
ConcurrentBag<BlogContent> processedContents = [];
2024-01-25 11:53:08 +08:00
await Parallel.ForEachAsync(contents, async (content, _) =>
{
foreach (var processor in _preRenderProcessors)
{
content = await processor.ProcessAsync(content);
}
processedContents.Add(content);
});
return processedContents;
}
2024-01-25 11:53:08 +08:00
private async Task PostProcess(IEnumerable<BlogEssay> essays)
{
2024-01-25 11:53:08 +08:00
await Parallel.ForEachAsync(essays, async (essay, _) =>
{
foreach (IPostRenderProcessor processor in _postRenderProcessors)
{
essay = await processor.ProcessAsync(essay);
}
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
});
}
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
private static partial Regex DescriptionPattern();
2024-01-24 14:00:55 +08:00
private string GetDescription(BlogContent content)
{
const string delimiter = "<!--more-->";
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
bool breakSentence = false;
2024-01-24 14:00:55 +08:00
if (pos == -1)
{
// 自动截取前50个字符
2024-07-12 15:48:53 +08:00
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
breakSentence = true;
2024-01-24 14:00:55 +08:00
}
string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern().Matches(rawContent);
2024-01-24 14:00:55 +08:00
StringBuilder builder = new();
foreach (Match match in matches)
{
builder.Append(match.Value);
}
2024-01-24 14:00:55 +08:00
if (breakSentence)
{
builder.Append("……");
2024-01-24 14:00:55 +08:00
}
string description = builder.ToString();
logger.LogDebug("Description of {} is {}.", content.FileName,
description);
return description;
}
private uint GetWordCount(BlogContent content)
{
int count = (from c in content.FileContent
where char.IsLetterOrDigit(c)
select c).Count();
2024-01-24 14:00:55 +08:00
logger.LogDebug("Word count of {} is {}", content.FileName,
count);
return (uint)count;
2024-01-24 14:00:55 +08:00
}
private static string CalculateReadTime(uint wordCount)
{
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟
int second = (int)wordCount / 10;
TimeSpan span = new(0, 0, second);
return span.ToString("mm'分 'ss'秒'");
}
2024-01-17 13:20:32 +08:00
}