2024-01-23 20:37:41 +08:00
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
using System.Diagnostics;
|
2024-01-24 14:00:55 +08:00
|
|
|
|
using System.Text;
|
2024-07-29 22:32:26 +08:00
|
|
|
|
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;
|
2024-01-23 20:37:41 +08:00
|
|
|
|
using YaeBlog.Core.Abstractions;
|
2024-01-17 13:20:32 +08:00
|
|
|
|
using YaeBlog.Core.Exceptions;
|
|
|
|
|
using YaeBlog.Core.Models;
|
2024-01-23 14:33:35 +08:00
|
|
|
|
using YamlDotNet.Core;
|
2024-01-19 20:33:41 +08:00
|
|
|
|
using YamlDotNet.Serialization;
|
2024-01-17 13:20:32 +08:00
|
|
|
|
|
|
|
|
|
namespace YaeBlog.Core.Services;
|
|
|
|
|
|
2024-07-29 22:32:26 +08:00
|
|
|
|
public partial class RendererService(
|
|
|
|
|
ILogger<RendererService> logger,
|
2024-01-17 13:20:32 +08:00
|
|
|
|
EssayScanService essayScanService,
|
|
|
|
|
MarkdownPipeline markdownPipeline,
|
2024-01-19 20:33:41 +08:00
|
|
|
|
IDeserializer yamlDeserializer,
|
2024-01-17 13:20:32 +08:00
|
|
|
|
EssayContentService essayContentService)
|
|
|
|
|
{
|
2024-01-23 14:33:35 +08:00
|
|
|
|
private readonly Stopwatch _stopwatch = new();
|
|
|
|
|
|
2024-01-23 20:37:41 +08:00
|
|
|
|
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.");
|
|
|
|
|
|
2024-01-17 13:20:32 +08:00
|
|
|
|
List<BlogContent> contents = await essayScanService.ScanAsync();
|
2024-01-25 11:53:08 +08:00
|
|
|
|
IEnumerable<BlogContent> preProcessedContents = await PreProcess(contents);
|
2024-01-17 13:20:32 +08:00
|
|
|
|
|
2024-01-23 14:33:35 +08:00
|
|
|
|
List<BlogEssay> essays = [];
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
2024-01-23 20:37:41 +08:00
|
|
|
|
foreach (BlogContent content in preProcessedContents)
|
2024-01-23 14:33:35 +08:00
|
|
|
|
{
|
|
|
|
|
MarkdownMetadata? metadata = TryParseMetadata(content);
|
2024-07-29 22:32:26 +08:00
|
|
|
|
uint wordCount = GetWordCount(content);
|
2024-01-23 14:33:35 +08:00
|
|
|
|
BlogEssay essay = new()
|
|
|
|
|
{
|
|
|
|
|
Title = metadata?.Title ?? content.FileName,
|
|
|
|
|
FileName = content.FileName,
|
2024-01-24 14:00:55 +08:00
|
|
|
|
Description = GetDescription(content),
|
2024-07-29 22:32:26 +08:00
|
|
|
|
WordCount = wordCount,
|
|
|
|
|
ReadTime = CalculateReadTime(wordCount),
|
2024-01-23 14:33:35 +08:00
|
|
|
|
PublishTime = metadata?.Date ?? DateTime.Now,
|
|
|
|
|
HtmlContent = content.FileContent
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (metadata?.Tags is not null)
|
|
|
|
|
{
|
|
|
|
|
essay.Tags.AddRange(metadata.Tags);
|
|
|
|
|
}
|
2024-07-29 22:32:26 +08:00
|
|
|
|
|
2024-01-23 14:33:35 +08:00
|
|
|
|
essays.Add(essay);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-01-23 20:37:41 +08:00
|
|
|
|
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
|
|
|
|
|
2024-01-23 20:37:41 +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 20:37:41 +08:00
|
|
|
|
|
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
|
|
|
|
|
2024-01-23 20:37: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)
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
|
|
|
|
ConcurrentBag<BlogContent> processedContents = [];
|
|
|
|
|
|
2024-01-25 11:53:08 +08:00
|
|
|
|
await Parallel.ForEachAsync(contents, async (content, _) =>
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
2024-01-24 21:32:14 +08:00
|
|
|
|
foreach (var processor in _preRenderProcessors)
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
2024-01-24 21:32:14 +08:00
|
|
|
|
content = await processor.ProcessAsync(content);
|
2024-01-23 20:37:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processedContents.Add(content);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return processedContents;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-25 11:53:08 +08:00
|
|
|
|
private async Task PostProcess(IEnumerable<BlogEssay> essays)
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
2024-01-25 11:53:08 +08:00
|
|
|
|
await Parallel.ForEachAsync(essays, async (essay, _) =>
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
|
|
|
|
foreach (IPostRenderProcessor processor in _postRenderProcessors)
|
|
|
|
|
{
|
2024-01-24 21:32:14 +08:00
|
|
|
|
essay = await processor.ProcessAsync(essay);
|
2024-01-23 20:37:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-29 22:32:26 +08:00
|
|
|
|
if (!essayContentService.TryAdd(essay))
|
2024-01-23 20:37:41 +08:00
|
|
|
|
{
|
|
|
|
|
throw new BlogFileException(
|
|
|
|
|
$"There are two essays with the same name: '{essay.FileName}'.");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-19 20:33:41 +08:00
|
|
|
|
private MarkdownMetadata? TryParseMetadata(BlogContent content)
|
|
|
|
|
{
|
|
|
|
|
string fileContent = content.FileContent.Trim();
|
|
|
|
|
|
|
|
|
|
if (!fileContent.StartsWith("---"))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除起始的---
|
|
|
|
|
fileContent = fileContent[3..];
|
|
|
|
|
|
|
|
|
|
int lastPos = fileContent.IndexOf("---", StringComparison.Ordinal);
|
|
|
|
|
if (lastPos is -1 or 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string yamlContent = fileContent[..lastPos];
|
|
|
|
|
// 返回去掉元数据之后的文本
|
|
|
|
|
lastPos += 3;
|
|
|
|
|
content.FileContent = fileContent[lastPos..];
|
|
|
|
|
|
2024-01-23 14:33:35 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
MarkdownMetadata metadata =
|
|
|
|
|
yamlDeserializer.Deserialize<MarkdownMetadata>(yamlContent);
|
|
|
|
|
logger.LogDebug("Title: {}, Publish Date: {}.",
|
|
|
|
|
metadata.Title, metadata.Date);
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
catch (YamlException e)
|
|
|
|
|
{
|
|
|
|
|
logger.LogWarning("Failed to parse '{}' metadata: {}", yamlContent, e);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-01-19 20:33:41 +08:00
|
|
|
|
}
|
2024-01-24 14:00:55 +08:00
|
|
|
|
|
2024-07-29 22:32:26 +08:00
|
|
|
|
[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);
|
2024-07-29 22:32:26 +08:00
|
|
|
|
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;
|
2024-07-29 22:32:26 +08:00
|
|
|
|
breakSentence = true;
|
2024-01-24 14:00:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-29 22:32:26 +08:00
|
|
|
|
string rawContent = content.FileContent[..pos];
|
|
|
|
|
MatchCollection matches = DescriptionPattern().Matches(rawContent);
|
2024-01-24 14:00:55 +08:00
|
|
|
|
|
2024-07-29 22:32:26 +08:00
|
|
|
|
StringBuilder builder = new();
|
|
|
|
|
foreach (Match match in matches)
|
|
|
|
|
{
|
|
|
|
|
builder.Append(match.Value);
|
|
|
|
|
}
|
2024-01-24 14:00:55 +08:00
|
|
|
|
|
2024-07-29 22:32:26 +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)
|
|
|
|
|
{
|
|
|
|
|
uint count = 0;
|
|
|
|
|
|
|
|
|
|
foreach (char c in content.FileContent)
|
|
|
|
|
{
|
|
|
|
|
if (char.IsControl(c) || char.IsSymbol(c)
|
2024-07-29 22:32:26 +08:00
|
|
|
|
|| char.IsSeparator(c))
|
2024-01-24 14:00:55 +08:00
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.LogDebug("Word count of {} is {}", content.FileName,
|
|
|
|
|
count);
|
|
|
|
|
return count;
|
|
|
|
|
}
|
2024-07-29 22:32:26 +08:00
|
|
|
|
|
|
|
|
|
private static string CalculateReadTime(uint wordCount)
|
|
|
|
|
{
|
|
|
|
|
// 据说语文教学大纲规定,中国高中问阅读现代文的速度是600字每分钟
|
|
|
|
|
int second = (int)wordCount / 10;
|
|
|
|
|
TimeSpan span = new TimeSpan(0, 0, second);
|
|
|
|
|
|
|
|
|
|
return span.ToString("mm'分 'ss'秒'");
|
|
|
|
|
}
|
2024-01-17 13:20:32 +08:00
|
|
|
|
}
|