add: watch command.
This commit is contained in:
parent
6cc74bf1e3
commit
01ebd84906
|
@ -9,5 +9,14 @@ public interface IEssayContentService
|
||||||
|
|
||||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
||||||
|
|
||||||
public bool SearchByUrlEncodedTag(string tag,[NotNullWhen(true)] out List<BlogEssay>? result);
|
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 TryAdd(BlogEssay essay);
|
||||||
|
|
||||||
|
public void RefreshTags();
|
||||||
|
|
||||||
|
public void Clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
using YaeBlog.Core.Models;
|
|
||||||
|
|
||||||
namespace YaeBlog.Core.Abstractions;
|
|
||||||
|
|
||||||
public interface ITableOfContentService
|
|
||||||
{
|
|
||||||
public IReadOnlyDictionary<string, BlogHeadline> Headlines { get; }
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
61
YaeBlog.Core/Services/BlogChangeWatcher.cs
Normal file
61
YaeBlog.Core/Services/BlogChangeWatcher.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user