refact: Rewrite essay scan service and interface.
This commit is contained in:
parent
e6ed407285
commit
6cc74bf1e3
10
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
10
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IEssayScanService
|
||||||
|
{
|
||||||
|
public Task<BlogContents> ScanContents();
|
||||||
|
|
||||||
|
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||||
|
}
|
|
@ -20,12 +20,14 @@ public static class ServiceCollectionExtensions
|
||||||
|
|
||||||
public static IServiceCollection AddYamlParser(this IServiceCollection collection)
|
public static IServiceCollection AddYamlParser(this IServiceCollection collection)
|
||||||
{
|
{
|
||||||
DeserializerBuilder builder = new();
|
DeserializerBuilder deserializerBuilder = new();
|
||||||
|
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||||
|
collection.AddSingleton(deserializerBuilder.Build());
|
||||||
|
|
||||||
builder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
SerializerBuilder serializerBuilder = new();
|
||||||
builder.IgnoreUnmatchedProperties();
|
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
collection.AddSingleton(serializerBuilder.Build());
|
||||||
collection.AddSingleton<IDeserializer>(_ => builder.Build());
|
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ public static class WebApplicationBuilderExtensions
|
||||||
builder.Services.AddMarkdig();
|
builder.Services.AddMarkdig();
|
||||||
builder.Services.AddYamlParser();
|
builder.Services.AddYamlParser();
|
||||||
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
||||||
builder.Services.AddSingleton<EssayScanService>();
|
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
||||||
builder.Services.AddSingleton<RendererService>();
|
builder.Services.AddSingleton<RendererService>();
|
||||||
builder.Services.AddSingleton<EssayContentService>();
|
builder.Services.AddSingleton<EssayContentService>();
|
||||||
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
|
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(provider =>
|
||||||
|
|
|
@ -4,5 +4,7 @@ public class BlogContent
|
||||||
{
|
{
|
||||||
public required string FileName { get; init; }
|
public required string FileName { get; init; }
|
||||||
|
|
||||||
|
public required MarkdownMetadata Metadata { get; init; }
|
||||||
|
|
||||||
public required string FileContent { get; set; }
|
public required string FileContent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
|
||||||
|
{
|
||||||
|
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
|
||||||
|
|
||||||
|
public ConcurrentBag<BlogContent> Posts { get; } = posts;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ using AngleSharp.Dom;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Core.Abstractions;
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Core.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Core.Processors;
|
namespace YaeBlog.Core.Processors;
|
||||||
|
@ -47,12 +48,12 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||||
filename = Path.Combine(essayFilename, filename);
|
filename = Path.Combine(essayFilename, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = Path.Combine(_options.Root, filename);
|
filename = Path.Combine(_options.Root, "posts", filename);
|
||||||
|
|
||||||
if (!Path.Exists(filename))
|
if (!Path.Exists(filename))
|
||||||
{
|
{
|
||||||
logger.LogError("Failed to found image: {}.", filename);
|
logger.LogError("Failed to found image: {}.", filename);
|
||||||
throw new InvalidOperationException();
|
throw new BlogFileException($"Image {filename} doesn't exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string imageLink = "api/files/" + filename;
|
string imageLink = "api/files/" + filename;
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# YaeBlog.Core
|
|
||||||
|
|
||||||
A blog generation totally based on Blazor.
|
|
||||||
|
|
||||||
You can using this to create your blog all in .NET stack!
|
|
|
@ -1,20 +1,104 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Core.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace YaeBlog.Core.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public class EssayScanService(
|
public class EssayScanService(
|
||||||
|
ISerializer yamlSerializer,
|
||||||
|
IDeserializer yamlDeserializer,
|
||||||
IOptions<BlogOptions> blogOptions,
|
IOptions<BlogOptions> blogOptions,
|
||||||
ILogger<EssayContentService> logger)
|
ILogger<EssayScanService> logger) : IEssayScanService
|
||||||
{
|
{
|
||||||
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||||
|
|
||||||
public async Task<List<BlogContent>> ScanAsync()
|
public async Task<BlogContents> ScanContents()
|
||||||
{
|
{
|
||||||
string root = Path.Combine(Environment.CurrentDirectory, _blogOptions.Root);
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
return new BlogContents(
|
||||||
|
await ScanContentsInternal(drafts),
|
||||||
|
await ScanContentsInternal(posts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
|
||||||
|
{
|
||||||
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
FileInfo targetFile = isDraft
|
||||||
|
? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md"))
|
||||||
|
: new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md"));
|
||||||
|
|
||||||
|
if (targetFile.Exists)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using StreamWriter writer = targetFile.CreateText();
|
||||||
|
|
||||||
|
await writer.WriteAsync("---\n");
|
||||||
|
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
|
||||||
|
await writer.WriteAsync("---\n");
|
||||||
|
await writer.WriteAsync("<!--more-->\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
|
||||||
|
where file.Extension == ".md"
|
||||||
|
select file;
|
||||||
|
|
||||||
|
ConcurrentBag<(string, string)> fileContents = [];
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(markdownFiles, async (file, token) =>
|
||||||
|
{
|
||||||
|
using StreamReader reader = file.OpenText();
|
||||||
|
fileContents.Add((file.Name, await reader.ReadToEndAsync(token)));
|
||||||
|
});
|
||||||
|
|
||||||
|
ConcurrentBag<BlogContent> contents = [];
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
foreach ((string filename, string content) in fileContents)
|
||||||
|
{
|
||||||
|
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
|
||||||
|
if (!content.StartsWith("---") || endPos is -1 or 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parse metadata from {}, skipped.", filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string metadataString = content[4..endPos];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MarkdownMetadata metadata = yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
|
||||||
|
logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename);
|
||||||
|
|
||||||
|
contents.Add(new BlogContent
|
||||||
|
{
|
||||||
|
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (YamlException e)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||||
|
{
|
||||||
|
root = Path.Combine(Environment.CurrentDirectory, root);
|
||||||
DirectoryInfo rootDirectory = new(root);
|
DirectoryInfo rootDirectory = new(root);
|
||||||
|
|
||||||
if (!rootDirectory.Exists)
|
if (!rootDirectory.Exists)
|
||||||
|
@ -22,36 +106,17 @@ public class EssayScanService(
|
||||||
throw new BlogFileException($"'{root}' is not a directory.");
|
throw new BlogFileException($"'{root}' is not a directory.");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileInfo> markdownFiles = [];
|
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
|
||||||
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
foreach (FileInfo fileInfo in rootDirectory.EnumerateFiles())
|
throw new BlogFileException($"'{root}/drafts' not exists.");
|
||||||
{
|
|
||||||
if (fileInfo.Extension != ".md")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Scan markdown file: {}.", fileInfo.Name);
|
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
|
||||||
markdownFiles.Add(fileInfo);
|
{
|
||||||
|
throw new BlogFileException($"'{root}/posts' not exists.");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
ConcurrentBag<BlogContent> contents = [];
|
drafts = new DirectoryInfo(Path.Combine(root, "drafts"));
|
||||||
|
posts = new DirectoryInfo(Path.Combine(root, "posts"));
|
||||||
await Parallel.ForEachAsync(markdownFiles, async (info, token) =>
|
|
||||||
{
|
|
||||||
StreamReader reader = new(info.OpenRead());
|
|
||||||
|
|
||||||
BlogContent content = new()
|
|
||||||
{
|
|
||||||
FileName = info.Name.Split('.')[0], FileContent = await reader.ReadToEndAsync(token)
|
|
||||||
};
|
|
||||||
|
|
||||||
contents.Add(content);
|
|
||||||
});
|
|
||||||
|
|
||||||
return contents.ToList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,13 @@ using Microsoft.Extensions.Logging;
|
||||||
using YaeBlog.Core.Abstractions;
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Core.Models;
|
using YaeBlog.Core.Models;
|
||||||
using YamlDotNet.Core;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace YaeBlog.Core.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public partial class RendererService(
|
public partial class RendererService(
|
||||||
ILogger<RendererService> logger,
|
ILogger<RendererService> logger,
|
||||||
EssayScanService essayScanService,
|
IEssayScanService essayScanService,
|
||||||
MarkdownPipeline markdownPipeline,
|
MarkdownPipeline markdownPipeline,
|
||||||
IDeserializer yamlDeserializer,
|
|
||||||
EssayContentService essayContentService)
|
EssayContentService essayContentService)
|
||||||
{
|
{
|
||||||
private readonly Stopwatch _stopwatch = new();
|
private readonly Stopwatch _stopwatch = new();
|
||||||
|
@ -30,30 +27,30 @@ public partial class RendererService(
|
||||||
_stopwatch.Start();
|
_stopwatch.Start();
|
||||||
logger.LogInformation("Render essays start.");
|
logger.LogInformation("Render essays start.");
|
||||||
|
|
||||||
List<BlogContent> contents = await essayScanService.ScanAsync();
|
BlogContents contents = await essayScanService.ScanContents();
|
||||||
IEnumerable<BlogContent> preProcessedContents = await PreProcess(contents);
|
List<BlogContent> posts = contents.Posts.ToList();
|
||||||
|
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
|
||||||
|
|
||||||
List<BlogEssay> essays = [];
|
List<BlogEssay> essays = [];
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
foreach (BlogContent content in preProcessedContents)
|
foreach (BlogContent content in preProcessedContents)
|
||||||
{
|
{
|
||||||
MarkdownMetadata? metadata = TryParseMetadata(content);
|
|
||||||
uint wordCount = GetWordCount(content);
|
uint wordCount = GetWordCount(content);
|
||||||
BlogEssay essay = new()
|
BlogEssay essay = new()
|
||||||
{
|
{
|
||||||
Title = metadata?.Title ?? content.FileName,
|
Title = content.Metadata.Title ?? content.FileName,
|
||||||
FileName = content.FileName,
|
FileName = content.FileName,
|
||||||
Description = GetDescription(content),
|
Description = GetDescription(content),
|
||||||
WordCount = wordCount,
|
WordCount = wordCount,
|
||||||
ReadTime = CalculateReadTime(wordCount),
|
ReadTime = CalculateReadTime(wordCount),
|
||||||
PublishTime = metadata?.Date ?? DateTime.Now,
|
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||||
HtmlContent = content.FileContent
|
HtmlContent = content.FileContent
|
||||||
};
|
};
|
||||||
|
|
||||||
if (metadata?.Tags is not null)
|
if (content.Metadata.Tags is not null)
|
||||||
{
|
{
|
||||||
essay.Tags.AddRange(metadata.Tags);
|
essay.Tags.AddRange(content.Metadata.Tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
essays.Add(essay);
|
essays.Add(essay);
|
||||||
|
@ -138,45 +135,6 @@ public partial class RendererService(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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..];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
||||||
private static partial Regex DescriptionPattern();
|
private static partial Regex DescriptionPattern();
|
||||||
|
|
||||||
|
|
|
@ -6,16 +6,6 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<PackageId>YaeBlog.Core</PackageId>
|
|
||||||
<Version>0.1.0</Version>
|
|
||||||
<Authors>Ricardo Ren</Authors>
|
|
||||||
<Company>Ricardo Ren</Company>
|
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<SupportedPlatform Include="browser" />
|
<SupportedPlatform Include="browser" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -32,12 +22,6 @@
|
||||||
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="../.gitignore" />
|
|
||||||
<None Include="../.editorconfig" />
|
|
||||||
<None Include="./README.md" Pack="true" PackagePath="/"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="wwwroot\" />
|
<Folder Include="wwwroot\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -14,6 +14,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
||||||
.gitea\workflows\build.yaml = .gitea\workflows\build.yaml
|
.gitea\workflows\build.yaml = .gitea\workflows\build.yaml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{753B998C-1B9E-498F-B949-845CE86C4075}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.editorconfig = .editorconfig
|
||||||
|
.gitattributes = .gitattributes
|
||||||
|
.gitignore = .gitignore
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System.CommandLine.Binding;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class BlogOptionsBinder : BinderBase<IOptions<BlogOptions>>
|
||||||
|
{
|
||||||
|
protected override IOptions<BlogOptions> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IOptions<BlogOptions>>(_ =>
|
||||||
|
{
|
||||||
|
FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json"));
|
||||||
|
if (!settings.Exists)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to load YaeBlog configurations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using StreamReader reader = settings.OpenText();
|
||||||
|
using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd());
|
||||||
|
JsonElement root = document.RootElement;
|
||||||
|
JsonElement optionSection = root.GetProperty(BlogOptions.OptionName);
|
||||||
|
|
||||||
|
BlogOptions? result = optionSection.Deserialize<BlogOptions>();
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OptionsWrapper<BlogOptions>(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
}
|
||||||
|
}
|
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using System.CommandLine.Binding;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class EssayScanServiceBinder : BinderBase<IEssayScanService>
|
||||||
|
{
|
||||||
|
protected override IEssayScanService GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IEssayScanService>(provider =>
|
||||||
|
{
|
||||||
|
DeserializerBuilder deserializerBuilder = new();
|
||||||
|
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||||
|
|
||||||
|
SerializerBuilder serializerBuilder = new();
|
||||||
|
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
|
||||||
|
IOptions<BlogOptions> options = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
ILogger<EssayScanService> logger = provider.GetRequiredService<ILogger<EssayScanService>>();
|
||||||
|
|
||||||
|
return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IEssayScanService>();
|
||||||
|
}
|
||||||
|
}
|
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.CommandLine.Binding;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class LoggerBinder<T> : BinderBase<ILogger<T>>
|
||||||
|
{
|
||||||
|
protected override ILogger<T> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole()));
|
||||||
|
bindingContext.AddService<ILogger<T>>(provider =>
|
||||||
|
{
|
||||||
|
ILoggerFactory factory = provider.GetRequiredService<ILoggerFactory>();
|
||||||
|
return factory.CreateLogger<T>();
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<ILogger<T>>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
using System.CommandLine.Binding;
|
|
||||||
using System.Text.Json;
|
|
||||||
using YaeBlog.Core.Models;
|
|
||||||
|
|
||||||
namespace YaeBlog.Commands;
|
|
||||||
|
|
||||||
public sealed class BlogOptionsBinder : BinderBase<BlogOptions>
|
|
||||||
{
|
|
||||||
protected override BlogOptions GetBoundValue(BindingContext bindingContext)
|
|
||||||
{
|
|
||||||
FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json"));
|
|
||||||
if (!settings.Exists)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to load YaeBlog configurations.");
|
|
||||||
}
|
|
||||||
|
|
||||||
using StreamReader reader = settings.OpenText();
|
|
||||||
using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd());
|
|
||||||
JsonElement root = document.RootElement;
|
|
||||||
JsonElement optionSection = root.GetProperty(BlogOptions.OptionName);
|
|
||||||
|
|
||||||
BlogOptions? result = optionSection.Deserialize<BlogOptions>();
|
|
||||||
if (result is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,15 @@
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using YaeBlog.Commands.Binders;
|
||||||
using YaeBlog.Components;
|
using YaeBlog.Components;
|
||||||
using YaeBlog.Core.Extensions;
|
using YaeBlog.Core.Extensions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
|
||||||
namespace YaeBlog.Commands;
|
namespace YaeBlog.Commands;
|
||||||
|
|
||||||
public static class CommandExtensions
|
public static class CommandExtensions
|
||||||
{
|
{
|
||||||
public static Command AddServeCommand(this RootCommand rootCommand)
|
public static void AddServeCommand(this RootCommand rootCommand)
|
||||||
{
|
{
|
||||||
Command serveCommand = new("serve", "Start http server.");
|
Command serveCommand = new("serve", "Start http server.");
|
||||||
rootCommand.AddCommand(serveCommand);
|
rootCommand.AddCommand(serveCommand);
|
||||||
|
@ -34,11 +37,9 @@ public static class CommandExtensions
|
||||||
CancellationToken token = context.GetCancellationToken();
|
CancellationToken token = context.GetCancellationToken();
|
||||||
await application.RunAsync(token);
|
await application.RunAsync(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
return rootCommand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Command AddNewCommand(this RootCommand rootCommand)
|
public static void AddNewCommand(this RootCommand rootCommand)
|
||||||
{
|
{
|
||||||
Command newCommand = new("new", "Create a new blog file and image directory.");
|
Command newCommand = new("new", "Create a new blog file and image directory.");
|
||||||
rootCommand.AddCommand(newCommand);
|
rootCommand.AddCommand(newCommand);
|
||||||
|
@ -46,45 +47,40 @@ public static class CommandExtensions
|
||||||
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
||||||
newCommand.AddArgument(filenameArgument);
|
newCommand.AddArgument(filenameArgument);
|
||||||
|
|
||||||
newCommand.SetHandler(async (file, blogOptions) =>
|
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||||
{
|
{
|
||||||
string fileWithExtension;
|
await essayScanService.SaveBlogContent(new BlogContent
|
||||||
if (file.EndsWith(".md"))
|
|
||||||
{
|
{
|
||||||
fileWithExtension = file;
|
FileName = file,
|
||||||
file = fileWithExtension[..fileWithExtension.LastIndexOf('.')];
|
FileContent = string.Empty,
|
||||||
}
|
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
|
||||||
else
|
});
|
||||||
{
|
|
||||||
fileWithExtension = file + ".md";
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectoryInfo rootDir = new(Path.Combine(Environment.CurrentDirectory, blogOptions.Root));
|
|
||||||
if (!rootDir.Exists)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Blog source directory '{blogOptions.Root} doesn't exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootDir.EnumerateFiles().Any(f => f.Name == fileWithExtension))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Target blog '{file}' has been created!");
|
|
||||||
}
|
|
||||||
|
|
||||||
FileInfo newBlogFile = new(Path.Combine(rootDir.FullName, fileWithExtension));
|
|
||||||
await using StreamWriter newStream = newBlogFile.CreateText();
|
|
||||||
|
|
||||||
await newStream.WriteAsync($"""
|
|
||||||
---
|
|
||||||
title: {file}
|
|
||||||
tags:
|
|
||||||
---
|
|
||||||
<!--more-->
|
|
||||||
""");
|
|
||||||
|
|
||||||
Console.WriteLine($"Created new blog '{file}.");
|
Console.WriteLine($"Created new blog '{file}.");
|
||||||
}, filenameArgument, new BlogOptionsBinder());
|
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
|
||||||
|
new EssayScanServiceBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddListCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("list", "List all blogs");
|
||||||
|
rootCommand.Add(command);
|
||||||
|
|
||||||
return newCommand;
|
command.SetHandler(async (_, _, essyScanService) =>
|
||||||
|
{
|
||||||
|
BlogContents contents = await essyScanService.ScanContents();
|
||||||
|
|
||||||
|
Console.WriteLine($"All {contents.Posts.Count} Posts:");
|
||||||
|
foreach (BlogContent content in contents.Posts)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {content.FileName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
|
||||||
|
foreach (BlogContent content in contents.Drafts)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {content.FileName}");
|
||||||
|
}
|
||||||
|
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,6 @@ RootCommand rootCommand = new("YaeBlog CLI");
|
||||||
|
|
||||||
rootCommand.AddServeCommand();
|
rootCommand.AddServeCommand();
|
||||||
rootCommand.AddNewCommand();
|
rootCommand.AddNewCommand();
|
||||||
|
rootCommand.AddListCommand();
|
||||||
|
|
||||||
await rootCommand.InvokeAsync(args);
|
await rootCommand.InvokeAsync(args);
|
||||||
|
|
6
YaeBlog/source/drafts/test-essay.md
Normal file
6
YaeBlog/source/drafts/test-essay.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: test-essay
|
||||||
|
date: 2024-08-22T22:31:34.3177253+08:00
|
||||||
|
tags:
|
||||||
|
---
|
||||||
|
<!--more-->
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user