add: watch command.

This commit is contained in:
jackfiled 2024-08-23 20:12:34 +08:00
parent 6cc74bf1e3
commit 01ebd84906
13 changed files with 175 additions and 45 deletions

View File

@ -9,5 +9,14 @@ public interface IEssayContentService
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; } public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
public IReadOnlyDictionary<string, BlogHeadline> Headlines { get; }
public bool TryAddHeadline(string filename, BlogHeadline headline);
public bool SearchByUrlEncodedTag(string tag, [NotNullWhen(true)] out List<BlogEssay>? result); public bool SearchByUrlEncodedTag(string tag, [NotNullWhen(true)] out List<BlogEssay>? result);
public bool TryAdd(BlogEssay essay);
public void RefreshTags();
public void Clear();
} }

View File

@ -1,8 +0,0 @@
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Abstractions;
public interface ITableOfContentService
{
public IReadOnlyDictionary<string, BlogHeadline> Headlines { get; }
}

View File

@ -22,12 +22,7 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default); builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IEssayScanService, EssayScanService>(); builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
builder.Services.AddSingleton<RendererService>(); builder.Services.AddSingleton<RendererService>();
builder.Services.AddSingleton<EssayContentService>(); builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
provider.GetRequiredService<EssayContentService>());
builder.Services.AddSingleton<TableOfContentService>();
builder.Services.AddSingleton<ITableOfContentService, TableOfContentService>(provider =>
provider.GetRequiredService<TableOfContentService>());
builder.Services.AddTransient<ImagePostRenderProcessor>(); builder.Services.AddTransient<ImagePostRenderProcessor>();
builder.Services.AddTransient<CodeBlockPostRenderProcessor>(); builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
builder.Services.AddTransient<TablePostRenderProcessor>(); builder.Services.AddTransient<TablePostRenderProcessor>();
@ -35,8 +30,21 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddTransient<BlogOptions>(provider => builder.Services.AddTransient<BlogOptions>(provider =>
provider.GetRequiredService<IOptions<BlogOptions>>().Value); provider.GetRequiredService<IOptions<BlogOptions>>().Value);
return builder;
}
public static WebApplicationBuilder AddServer(this WebApplicationBuilder builder)
{
builder.Services.AddHostedService<BlogHostedService>(); builder.Services.AddHostedService<BlogHostedService>();
return builder; return builder;
} }
public static WebApplicationBuilder AddWatcher(this WebApplicationBuilder builder)
{
builder.Services.AddTransient<BlogChangeWatcher>();
builder.Services.AddHostedService<BlogHotReloadService>();
return builder;
}
} }

View File

@ -1,14 +1,15 @@
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions; using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models; using YaeBlog.Core.Models;
using YaeBlog.Core.Services;
namespace YaeBlog.Core.Processors; namespace YaeBlog.Core.Processors;
public class HeadlinePostRenderProcessor( public class HeadlinePostRenderProcessor(
IConfiguration angleConfiguration, IConfiguration angleConfiguration,
TableOfContentService tableOfContentService) : IPostRenderProcessor IEssayContentService essayContentService,
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
{ {
public async Task<BlogEssay> ProcessAsync(BlogEssay essay) public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{ {
@ -62,7 +63,10 @@ public class HeadlinePostRenderProcessor(
FindParentHeadline(topHeadline, level2List).Children.AddRange(level3List); FindParentHeadline(topHeadline, level2List).Children.AddRange(level3List);
topHeadline.Children.AddRange(level2List); topHeadline.Children.AddRange(level2List);
tableOfContentService.AddHeadline(essay.FileName, topHeadline); if (!essayContentService.TryAddHeadline(essay.FileName, topHeadline))
{
logger.LogWarning("Failed to add headline of {}.", essay.FileName);
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml); return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
} }

View File

@ -0,0 +1,61 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Services;
public sealed class BlogChangeWatcher : IDisposable
{
private readonly FileSystemWatcher _fileSystemWatcher;
private readonly ILogger<BlogChangeWatcher> _logger;
public BlogChangeWatcher(IOptions<BlogOptions> options, ILogger<BlogChangeWatcher> logger)
{
_logger = logger;
_fileSystemWatcher = new FileSystemWatcher(Path.Combine(Environment.CurrentDirectory, options.Value.Root));
_fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName |
NotifyFilters.DirectoryName | NotifyFilters.Size;
_fileSystemWatcher.IncludeSubdirectories = true;
_fileSystemWatcher.EnableRaisingEvents = true;
}
public async Task<string?> WaitForChange(CancellationToken cancellationToken = default)
{
TaskCompletionSource<string?> tcs = new();
cancellationToken.Register(() => tcs.TrySetResult(null));
_logger.LogDebug("Register file change handle.");
_fileSystemWatcher.Changed += FileChangedCallback;
_fileSystemWatcher.Created += FileChangedCallback;
_fileSystemWatcher.Deleted += FileChangedCallback;
_fileSystemWatcher.Renamed += FileChangedCallback;
string? result;
try
{
result = await tcs.Task;
}
finally
{
_logger.LogDebug("Unregister file change handle.");
_fileSystemWatcher.Changed -= FileChangedCallback;
_fileSystemWatcher.Created -= FileChangedCallback;
_fileSystemWatcher.Deleted -= FileChangedCallback;
_fileSystemWatcher.Renamed -= FileChangedCallback;
}
return result;
void FileChangedCallback(object _, FileSystemEventArgs e)
{
_logger.LogDebug("File {} change detected.", e.Name);
tcs.TrySetResult(e.Name);
}
}
public void Dispose()
{
_fileSystemWatcher.Dispose();
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
namespace YaeBlog.Core.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("BlogHotReloadService is starting.");
await rendererService.RenderAsync();
while (!stoppingToken.IsCancellationRequested)
{
logger.LogDebug("Watching file changes...");
string? changFile = await watcher.WaitForChange(stoppingToken);
if (changFile is null)
{
logger.LogInformation("BlogHotReloadService is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changFile);
essayContentService.Clear();
await rendererService.RenderAsync();
}
}
}

View File

@ -11,12 +11,18 @@ public class EssayContentService : IEssayContentService
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = []; private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay); public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays; public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags; public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
public IReadOnlyDictionary<string, BlogHeadline> Headlines => _headlines;
public void RefreshTags() public void RefreshTags()
{ {
_tags.Clear(); _tags.Clear();
@ -45,4 +51,11 @@ public class EssayContentService : IEssayContentService
return result is not null; return result is not null;
} }
public void Clear()
{
_essays.Clear();
_tags.Clear();
_headlines.Clear();
}
} }

View File

@ -14,7 +14,7 @@ public partial class RendererService(
ILogger<RendererService> logger, ILogger<RendererService> logger,
IEssayScanService essayScanService, IEssayScanService essayScanService,
MarkdownPipeline markdownPipeline, MarkdownPipeline markdownPipeline,
EssayContentService essayContentService) IEssayContentService essayContentService)
{ {
private readonly Stopwatch _stopwatch = new(); private readonly Stopwatch _stopwatch = new();

View File

@ -1,20 +0,0 @@
using System.Collections.Concurrent;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Services;
public class TableOfContentService : ITableOfContentService
{
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = [];
public IReadOnlyDictionary<string, BlogHeadline> Headlines => _headlines;
public void AddHeadline(string filename, BlogHeadline headline)
{
if (!_headlines.TryAdd(filename, headline))
{
throw new InvalidOperationException();
}
}
}

View File

@ -6,10 +6,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>

View File

@ -23,6 +23,38 @@ public static class CommandExtensions
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap(); builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog(); builder.AddYaeBlog();
builder.AddServer();
WebApplication application = builder.Build();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
application.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
application.MapControllers();
CancellationToken token = context.GetCancellationToken();
await application.RunAsync(token);
});
}
public static void AddWatchCommand(this RootCommand rootCommand)
{
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
rootCommand.AddCommand(command);
command.SetHandler(async context =>
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog();
builder.AddWatcher();
WebApplication application = builder.Build(); WebApplication application = builder.Build();

View File

@ -4,7 +4,6 @@
@using YaeBlog.Core.Models @using YaeBlog.Core.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject ITableOfContentService TableOfContent
@inject NavigationManager NavigationInstance @inject NavigationManager NavigationInstance
<PageTitle> <PageTitle>
@ -129,7 +128,7 @@
NavigationInstance.NavigateTo("/NotFound"); NavigationInstance.NavigateTo("/NotFound");
} }
_headline = TableOfContent.Headlines[BlogKey]; _headline = Contents.Headlines[BlogKey];
} }
private string GenerateSelectorUrl(string selectorId) private string GenerateSelectorUrl(string selectorId)

View File

@ -6,5 +6,6 @@ RootCommand rootCommand = new("YaeBlog CLI");
rootCommand.AddServeCommand(); rootCommand.AddServeCommand();
rootCommand.AddNewCommand(); rootCommand.AddNewCommand();
rootCommand.AddListCommand(); rootCommand.AddListCommand();
rootCommand.AddWatchCommand();
await rootCommand.InvokeAsync(args); await rootCommand.InvokeAsync(args);