diff --git a/YaeBlog/Abstraction/IEssayContentService.cs b/YaeBlog/Abstraction/IEssayContentService.cs index 2845c75..8c95cce 100644 --- a/YaeBlog/Abstraction/IEssayContentService.cs +++ b/YaeBlog/Abstraction/IEssayContentService.cs @@ -5,7 +5,9 @@ namespace YaeBlog.Abstraction; public interface IEssayContentService { - public IReadOnlyDictionary Essays { get; } + public IEnumerable Essays { get; } + + public int Count { get; } public IReadOnlyDictionary> Tags { get; } @@ -16,6 +18,8 @@ public interface IEssayContentService public bool TryAdd(BlogEssay essay); + public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay); + public void RefreshTags(); public void Clear(); diff --git a/YaeBlog/Components/BlogInformationCard.razor b/YaeBlog/Components/BlogInformationCard.razor index b8b3383..72de03a 100644 --- a/YaeBlog/Components/BlogInformationCard.razor +++ b/YaeBlog/Components/BlogInformationCard.razor @@ -19,7 +19,7 @@
- @(Contents.Essays.Count) + @(Contents.Count)
diff --git a/YaeBlog/Components/Pagination.razor b/YaeBlog/Components/Pagination.razor index 10889d2..46263ee 100644 --- a/YaeBlog/Components/Pagination.razor +++ b/YaeBlog/Components/Pagination.razor @@ -22,7 +22,7 @@ } else { - + diff --git a/YaeBlog/Exceptions/ProcessInteropException.cs b/YaeBlog/Exceptions/ProcessInteropException.cs deleted file mode 100644 index d38f808..0000000 --- a/YaeBlog/Exceptions/ProcessInteropException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace YaeBlog.Core.Exceptions; - -public sealed class ProcessInteropException : Exception -{ - public ProcessInteropException(string message) : base(message) - { - } - - public ProcessInteropException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/YaeBlog/Models/AboutInfo.cs b/YaeBlog/Models/AboutInfo.cs deleted file mode 100644 index f5e9a97..0000000 --- a/YaeBlog/Models/AboutInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace YaeBlog.Models; - -public class AboutInfo -{ - public required string Introduction { get; set; } - - public required string Description { get; set; } - - public required string AvatarImage { get; set; } -} diff --git a/YaeBlog/Models/BlogContent.cs b/YaeBlog/Models/BlogContent.cs index 775f37b..6ab5b7d 100644 --- a/YaeBlog/Models/BlogContent.cs +++ b/YaeBlog/Models/BlogContent.cs @@ -7,4 +7,6 @@ public class BlogContent public required MarkdownMetadata Metadata { get; init; } public required string FileContent { get; set; } + + public bool IsDraft { get; set; } = false; } diff --git a/YaeBlog/Models/BlogEssay.cs b/YaeBlog/Models/BlogEssay.cs index dceaea9..0af5cf6 100644 --- a/YaeBlog/Models/BlogEssay.cs +++ b/YaeBlog/Models/BlogEssay.cs @@ -6,6 +6,8 @@ public class BlogEssay : IComparable public required string FileName { get; init; } + public required bool IsDraft { get; init; } + public required DateTime PublishTime { get; init; } public required string Description { get; init; } @@ -24,6 +26,7 @@ public class BlogEssay : IComparable { Title = Title, FileName = FileName, + IsDraft = IsDraft, PublishTime = PublishTime, Description = Description, WordCount = WordCount, @@ -39,10 +42,16 @@ public class BlogEssay : IComparable { if (other is null) { - return 1; + return -1; } - return PublishTime.CompareTo(other.PublishTime); + // 草稿文章应当排在前面 + if (IsDraft != other.IsDraft) + { + return IsDraft ? -1 : 1; + } + + return other.PublishTime.CompareTo(PublishTime); } public override string ToString() diff --git a/YaeBlog/Models/TailwindOptions.cs b/YaeBlog/Models/TailwindOptions.cs deleted file mode 100644 index abafd5c..0000000 --- a/YaeBlog/Models/TailwindOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace YaeBlog.Models; - -public class TailwindOptions -{ - public const string OptionName = "Tailwind"; - - public required string InputFile { get; set; } - - public required string OutputFile { get; set; } -} diff --git a/YaeBlog/Pages/Archives.razor b/YaeBlog/Pages/Archives.razor index d094f84..883f33b 100644 --- a/YaeBlog/Pages/Archives.razor +++ b/YaeBlog/Pages/Archives.razor @@ -19,7 +19,7 @@ - @foreach (IGrouping> group in _essays) + @foreach (IGrouping group in _essays) {
@@ -28,9 +28,9 @@
- @foreach ((String name, BlogEssay essay) in group) + @foreach (BlogEssay essay in group) { - +
@(essay.PublishTime.ToString("MM月dd日")) @@ -51,14 +51,13 @@
@code { - private readonly List>> _essays = []; + private readonly List> _essays = []; protected override void OnInitialized() { base.OnInitialized(); _essays.AddRange(from essay in Contents.Essays - orderby essay.Value.PublishTime descending - group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1)); + group essay by new DateTime(essay.PublishTime.Year, 1, 1)); } } diff --git a/YaeBlog/Pages/BlogIndex.razor b/YaeBlog/Pages/BlogIndex.razor index 6ce7b1d..7c8b683 100644 --- a/YaeBlog/Pages/BlogIndex.razor +++ b/YaeBlog/Pages/BlogIndex.razor @@ -12,9 +12,9 @@
- @foreach (KeyValuePair pair in _essays) + @foreach (BlogEssay essay in _essays) { - + } @@ -30,7 +30,7 @@ [SupplyParameterFromQuery] private int? Page { get; set; } - private readonly List> _essays = []; + private readonly List _essays = []; private const int EssaysPerPage = 8; private int _pageCount = 1; private int _page = 1; @@ -38,16 +38,15 @@ protected override void OnInitialized() { _page = Page ?? 1; - _pageCount = Contents.Essays.Count / EssaysPerPage + 1; + _pageCount = Contents.Count / EssaysPerPage + 1; - if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage) + if (EssaysPerPage * _page > Contents.Count + EssaysPerPage) { NavigationInstance.NavigateTo("/NotFount"); return; } _essays.AddRange(Contents.Essays - .OrderByDescending(p => p.Value.PublishTime) .Skip((_page - 1) * EssaysPerPage) .Take(EssaysPerPage)); } diff --git a/YaeBlog/Pages/Essays.razor b/YaeBlog/Pages/Essays.razor index f53577b..e423c1d 100644 --- a/YaeBlog/Pages/Essays.razor +++ b/YaeBlog/Pages/Essays.razor @@ -110,7 +110,7 @@ return; } - if (!Contents.Essays.TryGetValue(BlogKey, out _essay)) + if (!Contents.TryGetEssay(BlogKey, out _essay)) { NavigationInstance.NavigateTo("/NotFound"); } diff --git a/YaeBlog/Services/BlogHostedService.cs b/YaeBlog/Services/BlogHostedService.cs index 0dfe573..490c93b 100644 --- a/YaeBlog/Services/BlogHostedService.cs +++ b/YaeBlog/Services/BlogHostedService.cs @@ -6,14 +6,12 @@ public class BlogHostedService( { public async Task StartAsync(CancellationToken cancellationToken) { - logger.LogInformation("Welcome to YaeBlog!"); - + logger.LogInformation("Failed to load cache, render essays."); await rendererService.RenderAsync(); } public Task StopAsync(CancellationToken cancellationToken) { - logger.LogInformation("YaeBlog stopped!\nHave a nice day!"); return Task.CompletedTask; } } diff --git a/YaeBlog/Services/BlogHotReloadService.cs b/YaeBlog/Services/BlogHotReloadService.cs index c7106b6..581c1b4 100644 --- a/YaeBlog/Services/BlogHotReloadService.cs +++ b/YaeBlog/Services/BlogHotReloadService.cs @@ -10,24 +10,32 @@ public sealed class BlogHotReloadService( { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogInformation("BlogHotReloadService is starting."); + logger.LogInformation("Hot reload is starting..."); + logger.LogInformation("Change essays will lead to hot reload!"); + logger.LogInformation("HINT: draft essays will be included."); - await rendererService.RenderAsync(); + await rendererService.RenderAsync(true); - while (!stoppingToken.IsCancellationRequested) + Task[] reloadTasks = [FileWatchTask(stoppingToken)]; + await Task.WhenAll(reloadTasks); + } + + private async Task FileWatchTask(CancellationToken token) + { + while (!token.IsCancellationRequested) { - logger.LogDebug("Watching file changes..."); - string? changFile = await watcher.WaitForChange(stoppingToken); + logger.LogInformation("Watching file changes..."); + string? changeFile = await watcher.WaitForChange(token); - if (changFile is null) + if (changeFile is null) { - logger.LogInformation("BlogHotReloadService is stopping."); + logger.LogInformation("File watcher is stopping."); break; } - logger.LogInformation("{} changed, re-rendering.", changFile); + logger.LogInformation("{} changed, re-rendering.", changeFile); essayContentService.Clear(); - await rendererService.RenderAsync(); + await rendererService.RenderAsync(true); } } } diff --git a/YaeBlog/Services/EssayContentService.cs b/YaeBlog/Services/EssayContentService.cs index db08e42..ab86ada 100644 --- a/YaeBlog/Services/EssayContentService.cs +++ b/YaeBlog/Services/EssayContentService.cs @@ -9,15 +9,28 @@ public class EssayContentService : IEssayContentService { private readonly ConcurrentDictionary _essays = new(); + private readonly List _sortedEssays = []; + private readonly Dictionary> _tags = []; private readonly ConcurrentDictionary _headlines = new(); - public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay); + public bool TryAdd(BlogEssay essay) + { + _sortedEssays.Add(essay); + return _essays.TryAdd(essay.FileName, essay); + } public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline); - public IReadOnlyDictionary Essays => _essays; + public IEnumerable Essays => _sortedEssays; + + public int Count => _sortedEssays.Count; + + public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay) + { + return _essays.TryGetValue(filename, out essay); + } public IReadOnlyDictionary> Tags => _tags; diff --git a/YaeBlog/Services/EssayScanService.cs b/YaeBlog/Services/EssayScanService.cs index 8492ff8..1ef9b13 100644 --- a/YaeBlog/Services/EssayScanService.cs +++ b/YaeBlog/Services/EssayScanService.cs @@ -22,8 +22,8 @@ public partial class EssayScanService( ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts); return new BlogContents( - await ScanContentsInternal(drafts), - await ScanContentsInternal(posts)); + await ScanContentsInternal(drafts, true), + await ScanContentsInternal(posts, false)); } public async Task SaveBlogContent(BlogContent content, bool isDraft = true) @@ -60,7 +60,7 @@ public partial class EssayScanService( } } - private async Task> ScanContentsInternal(DirectoryInfo directory) + private async Task> ScanContentsInternal(DirectoryInfo directory, bool isDraft) { // 扫描以md结果的但是不是隐藏文件的文件 IEnumerable markdownFiles = from file in directory.EnumerateFiles() @@ -97,7 +97,8 @@ public partial class EssayScanService( contents.Add(new BlogContent { - FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..] + FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..], + IsDraft = isDraft }); } catch (YamlException e) diff --git a/YaeBlog/Services/ProcessInteropService.cs b/YaeBlog/Services/ProcessInteropService.cs deleted file mode 100644 index 7df9c44..0000000 --- a/YaeBlog/Services/ProcessInteropService.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using YaeBlog.Core.Exceptions; - -namespace YaeBlog.Services; - -public class ProcessInteropService(ILogger logger) -{ - public Process StartProcess(string command, string arguments) - { - string commandName; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !command.EndsWith(".exe")) - { - commandName = command + ".exe"; - } - else - { - commandName = command; - } - - try - { - ProcessStartInfo startInfo = new() - { - FileName = commandName, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - Process? process = Process.Start(startInfo); - - if (process is null) - { - throw new ProcessInteropException( - $"Failed to start process: {commandName}, the return process is null."); - } - - process.OutputDataReceived += (_, data) => - { - if (!string.IsNullOrEmpty(data.Data)) - { - logger.LogInformation("Receive output from process '{}': '{}'", commandName, data.Data); - } - }; - - process.ErrorDataReceived += (_, data) => - { - if (!string.IsNullOrEmpty(data.Data)) - { - logger.LogWarning("Receive error from process '{}': '{}'", commandName, data.Data); - } - }; - - return process; - } - catch (Exception innerException) - { - throw new ProcessInteropException($"Failed to start process '{command}' with arguments '{arguments}", - innerException); - } - } -} diff --git a/YaeBlog/Services/RendererService.cs b/YaeBlog/Services/RendererService.cs index 8386399..dda44ee 100644 --- a/YaeBlog/Services/RendererService.cs +++ b/YaeBlog/Services/RendererService.cs @@ -21,40 +21,43 @@ public partial class RendererService( private readonly List _postRenderProcessors = []; - public async Task RenderAsync() + public async Task RenderAsync(bool includeDrafts = false) { _stopwatch.Start(); logger.LogInformation("Render essays start."); BlogContents contents = await essayScanService.ScanContents(); List posts = contents.Posts.ToList(); + if (includeDrafts) + { + posts.AddRange(contents.Drafts); + } + IEnumerable preProcessedContents = await PreProcess(posts); List essays = []; - await Task.Run(() => + foreach (BlogContent content in preProcessedContents) { - foreach (BlogContent content in preProcessedContents) + uint wordCount = GetWordCount(content); + BlogEssay essay = new() { - uint wordCount = GetWordCount(content); - BlogEssay essay = new() - { - Title = content.Metadata.Title ?? content.FileName, - FileName = content.FileName, - Description = GetDescription(content), - WordCount = wordCount, - ReadTime = CalculateReadTime(wordCount), - PublishTime = content.Metadata.Date ?? DateTime.Now, - HtmlContent = content.FileContent - }; + Title = content.Metadata.Title ?? content.FileName, + FileName = content.FileName, + IsDraft = content.IsDraft, + Description = GetDescription(content), + WordCount = wordCount, + ReadTime = CalculateReadTime(wordCount), + PublishTime = content.Metadata.Date ?? DateTime.Now, + HtmlContent = content.FileContent + }; - if (content.Metadata.Tags is not null) - { - essay.Tags.AddRange(content.Metadata.Tags); - } - - essays.Add(essay); + if (content.Metadata.Tags is not null) + { + essay.Tags.AddRange(content.Metadata.Tags); } - }); + + essays.Add(essay); + } ConcurrentBag postProcessEssays = []; Parallel.ForEach(essays, essay => @@ -66,7 +69,16 @@ public partial class RendererService( logger.LogDebug("Render markdown file {}.", newEssay); }); - await PostProcess(postProcessEssays); + IEnumerable postProcessedEssays = await PostProcess(postProcessEssays); + + foreach (BlogEssay essay in postProcessedEssays) + { + if (!essayContentService.TryAdd(essay)) + { + throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'."); + } + } + essayContentService.RefreshTags(); _stopwatch.Stop(); @@ -117,8 +129,10 @@ public partial class RendererService( return processedContents; } - private async Task PostProcess(IEnumerable essays) + private async Task> PostProcess(IEnumerable essays) { + ConcurrentBag processedContents = []; + await Parallel.ForEachAsync(essays, async (essay, _) => { foreach (IPostRenderProcessor processor in _postRenderProcessors) @@ -126,12 +140,13 @@ public partial class RendererService( essay = await processor.ProcessAsync(essay); } - if (!essayContentService.TryAdd(essay)) - { - throw new BlogFileException( - $"There are two essays with the same name: '{essay.FileName}'."); - } + processedContents.Add(essay); }); + + List result = processedContents.ToList(); + result.Sort(); + + return result; } [GeneratedRegex(@"(? -/// 在应用程序运行的过程中启动Tailwind watch -/// 在程序退出时自动结束进程 -/// 只在Development模式下启动 -/// -public sealed class TailwindRefreshService( - IOptions options, - ProcessInteropService processInteropService, - IHostEnvironment hostEnvironment, - ILogger logger) : IHostedService, IDisposable -{ - private Process? _tailwindProcess; - - public Task StartAsync(CancellationToken cancellationToken) - { - if (!hostEnvironment.IsDevelopment()) - { - return Task.CompletedTask; - } - - logger.LogInformation("Try to start tailwind watcher with input {} and output {}", options.Value.InputFile, - options.Value.OutputFile); - - _tailwindProcess = processInteropService.StartProcess("pnpm", - $"tailwind -i {options.Value.InputFile} -o {options.Value.OutputFile} --watch"); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _tailwindProcess?.Kill(); - return Task.CompletedTask; - } - - public void Dispose() - { - _tailwindProcess?.Dispose(); - } -}