3 Commits

Author SHA1 Message Date
c446012199 Merge branch 'master' into feat-hpc 2025-08-31 12:41:03 +08:00
3053b83bbf feat: a lot of notebooks. 2025-08-31 12:39:18 +08:00
457316971c draft: six notebooks 2025-06-16 00:45:16 +08:00
410 changed files with 1159 additions and 2944 deletions

View File

@@ -12,10 +12,7 @@ indent_style = space
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{project.json,appsettings.json,appsettings.*.json}] [project.json]
indent_size = 2
[*.{yaml,yml}]
indent_size = 2 indent_size = 2
# C# and Visual Basic files # C# and Visual Basic files

View File

@@ -7,24 +7,27 @@ jobs:
Build-Blog-Image: Build-Blog-Image:
runs-on: archlinux runs-on: archlinux
steps: steps:
- name: Check out code. - uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
uses: http://github-mirrors.infra.svc.cluster.local/actions/checkout.git@v4 name: Check out code
with: with:
lfs: true lfs: true
- name: Build project. - name: Build project
run: | run: |
git submodule update --init cd YaeBlog
podman pull mcr.azure.cn/dotnet/aspnet:10.0 dotnet publish
pwsh build.ps1 build - name: Build docker image
- name: Workaround to make sure podman-login working.
run: | run: |
mkdir -p /root/.docker cd YaeBlog
- name: Login tencent cloud docker registry. podman build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest --build-arg COMMIT_ID=$(git rev-parse --short=10 HEAD)
uses: http://github-mirrors.infra.svc.cluster.local/actions/podman-login.git@v1 - name: Workaround to make sure podman login succeed
run: |
mkdir /root/.docker
- name: Login aliyun docker registry
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
with: with:
registry: ccr.ccs.tencentyun.com registry: registry.cn-beijing.aliyuncs.com
username: 100044380877 username: 初冬的朝阳
password: ${{ secrets.TENCENT_REGISTRY_PASSWORD }} password: ${{ secrets.ALIYUN_PASSWORD }}
auth_file_path: /etc/containers/auth.json auth_file_path: /etc/containers/auth.json
- name: Push docker image. - name: Push docker image
run: podman push ccr.ccs.tencentyun.com/jackfiled/blog:latest run: podman push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest

1
.gitignore vendored
View File

@@ -184,7 +184,6 @@ DocProject/Help/html
# Click-Once directory # Click-Once directory
publish/ publish/
out/
# Publish Web Output # Publish Web Output
*.[Pp]ublish.xml *.[Pp]ublish.xml

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "third-party/BlazorSvgComponents"]
path = third-party/BlazorSvgComponents
url = https://git.rrricardo.top/jackfiled/BlazorSvgComponents.git

View File

@@ -7,17 +7,8 @@
<File Path=".editorconfig" /> <File Path=".editorconfig" />
<File Path=".gitattributes" /> <File Path=".gitattributes" />
<File Path=".gitignore" /> <File Path=".gitignore" />
<File Path="build.ps1" />
<File Path="LICENSE" /> <File Path="LICENSE" />
<File Path="README.md" /> <File Path="README.md" />
</Folder> </Folder>
<Folder Name="/src/"> <Project Path="YaeBlog/YaeBlog.csproj" />
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
<Project Path="src/YaeBlog/YaeBlog.csproj" />
</Folder>
<Folder Name="/third-party/" />
<Folder Name="/third-party/BlazorSvgComponents/" />
<Folder Name="/third-party/BlazorSvgComponents/src/">
<Project Path="third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
</Folder>
</Solution> </Solution>

View File

@@ -0,0 +1,10 @@
using YaeBlog.Models;
namespace YaeBlog.Abstraction;
public interface IEssayScanService
{
public Task<BlogContents> ScanContents();
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
}

View File

@@ -0,0 +1,36 @@
using System.CommandLine.Binding;
using System.Text.Json;
using Microsoft.Extensions.Options;
using YaeBlog.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>>();
}
}

View File

@@ -0,0 +1,32 @@
using System.CommandLine.Binding;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Models;
using YaeBlog.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>();
}
}

View File

@@ -0,0 +1,21 @@
using System.CommandLine.Binding;
using YaeBlog.Abstraction;
using YaeBlog.Services;
namespace YaeBlog.Commands.Binders;
public sealed class ImageCompressServiceBinder : BinderBase<ImageCompressService>
{
protected override ImageCompressService GetBoundValue(BindingContext bindingContext)
{
bindingContext.AddService(provider =>
{
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
ILogger<ImageCompressService> logger = provider.GetRequiredService<ILogger<ImageCompressService>>();
return new ImageCompressService(essayScanService, logger);
});
return bindingContext.GetRequiredService<ImageCompressService>();
}
}

View 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>>();
}
}

View File

@@ -0,0 +1,263 @@
using System.CommandLine;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Commands.Binders;
using YaeBlog.Components;
using YaeBlog.Extensions;
using YaeBlog.Models;
using YaeBlog.Services;
namespace YaeBlog.Commands;
public sealed class YaeBlogCommand
{
private readonly RootCommand _rootCommand = new("YaeBlog Cli");
public YaeBlogCommand()
{
AddServeCommand(_rootCommand);
AddWatchCommand(_rootCommand);
AddListCommand(_rootCommand);
AddNewCommand(_rootCommand);
AddPublishCommand(_rootCommand);
AddScanCommand(_rootCommand);
AddCompressCommand(_rootCommand);
}
public Task<int> RunAsync(string[] args)
{
return _rootCommand.InvokeAsync(args);
}
private static void AddServeCommand(RootCommand rootCommand)
{
Command serveCommand = new("serve", "Start http server.");
rootCommand.AddCommand(serveCommand);
serveCommand.SetHandler(async context =>
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
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);
});
}
private static void AddWatchCommand(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.AddYaeBlog();
builder.AddWatcher();
WebApplication application = builder.Build();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
application.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
application.MapControllers();
CancellationToken token = context.GetCancellationToken();
await application.RunAsync(token);
});
}
private static void AddNewCommand(RootCommand rootCommand)
{
Command newCommand = new("new", "Create a new blog file and image directory.");
rootCommand.AddCommand(newCommand);
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
newCommand.AddArgument(filenameArgument);
newCommand.SetHandler(async (file, blogOption, _, essayScanService) =>
{
BlogContents contents = await essayScanService.ScanContents();
if (contents.Posts.Any(content => content.BlogName == file))
{
Console.WriteLine("There exists the same title blog in posts.");
return;
}
await essayScanService.SaveBlogContent(new BlogContent(
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
new MarkdownMetadata { Title = file, Date = DateTime.Now },
string.Empty, true, [], []));
Console.WriteLine($"Created new blog '{file}.");
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
new EssayScanServiceBinder());
}
private static void AddListCommand(RootCommand rootCommand)
{
Command command = new("list", "List all blogs");
rootCommand.AddCommand(command);
command.SetHandler(async (_, _, essyScanService) =>
{
BlogContents contents = await essyScanService.ScanContents();
Console.WriteLine($"All {contents.Posts.Count} Posts:");
foreach (BlogContent content in contents.Posts.OrderBy(x => x.BlogName))
{
Console.WriteLine($" - {content.BlogName}");
}
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName))
{
Console.WriteLine($" - {content.BlogName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
private static void AddScanCommand(RootCommand rootCommand)
{
Command command = new("scan", "Scan unused and not found images.");
rootCommand.AddCommand(command);
Option<bool> removeOption =
new(name: "--rm", description: "Remove unused images.", getDefaultValue: () => false);
command.AddOption(removeOption);
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> unusedImages = (from content in contents
from image in content.Images
where image is { IsUsed: false }
select image).ToList();
if (unusedImages.Count != 0)
{
Console.WriteLine("Found unused images:");
Console.WriteLine("HINT: use '--rm' to remove unused images.");
}
foreach (BlogImageInfo image in unusedImages)
{
Console.WriteLine($" - {image.File.FullName}");
}
if (removeOptionValue)
{
foreach (BlogImageInfo image in unusedImages)
{
image.File.Delete();
}
}
Console.WriteLine("Used not existed images:");
foreach (BlogContent content in contents)
{
foreach (FileInfo file in content.NotfoundImages)
{
Console.WriteLine($"- {file.Name} in {content.BlogName}");
}
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
}
private static void AddPublishCommand(RootCommand rootCommand)
{
Command command = new("publish", "Publish a new blog file.");
rootCommand.AddCommand(command);
Argument<string> filenameArgument = new(name: "blog name", description: "The published blog filename.");
command.AddArgument(filenameArgument);
command.SetHandler(async (blogOptions, _, essayScanService, filename) =>
{
BlogContents contents = await essayScanService.ScanContents();
BlogContent? content = (from blog in contents.Drafts
where blog.BlogName == filename
select blog).FirstOrDefault();
if (content is null)
{
Console.WriteLine("Target blog does not exist.");
return;
}
// 设置发布的时间
content.Metadata.Date = DateTime.Now;
// 将选中的博客文件复制到posts
await essayScanService.SaveBlogContent(content, isDraft: false);
// 复制图片文件夹
DirectoryInfo sourceImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
DirectoryInfo targetImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
if (sourceImageDirectory.Exists)
{
targetImageDirectory.Create();
foreach (FileInfo file in sourceImageDirectory.EnumerateFiles())
{
file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true);
}
sourceImageDirectory.Delete(true);
}
// 删除原始的文件
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md"));
sourceBlogFile.Delete();
}, new BlogOptionsBinder(),
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
}
private static void AddCompressCommand(RootCommand rootCommand)
{
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
rootCommand.Add(command);
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
getDefaultValue: () => false);
command.AddOption(dryRunOption);
command.SetHandler(ImageCommandHandler,
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
}
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
ILogger<ImageCompressService> _2,
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
{
await imageCompressService.Compress(dryRun);
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="tailwind.g.css"/>
<HeadOutlet/>
</head>
<body>
<Routes/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -1,9 +1,8 @@
@using Microsoft.Extensions.Options
@using YaeBlog.Abstraction @using YaeBlog.Abstraction
@using YaeBlog.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject IOptions<BlogOptions> Options @inject BlogOptions Options
<div class="flex flex-col"> <div class="flex flex-col">
<div class="p-10"> <div class="p-10">
@@ -44,7 +43,7 @@
<div class="px-6"> <div class="px-6">
<p class="text-lg"> <p class="text-lg">
@(Options.Value.Announcement) @(Options.Announcement)
</p> </p>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col p-3"> <div class="flex flex-col p-3">
<div class="text-3xl font-bold py-2"> <div class="text-3xl font-bold py-2">
<a href="@(Essay.EssayLink)">@(Essay.Title)</a> <a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
</div> </div>
<div class="p-2 flex flex-row justify-content-start gap-2"> <div class="p-2 flex flex-row justify-content-start gap-2">
@@ -14,7 +14,9 @@
@foreach (string key in Essay.Tags) @foreach (string key in Essay.Tags)
{ {
<div class="text-sky-600"> <div class="text-sky-600">
<Anchor Address="@($"/blog/tags/?tagName={UrlEncoder.Default.Encode(key)}")" Text="@($"# {key}")"/> <a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
# @key
</a>
</div> </div>
} }
</div> </div>

View File

@@ -0,0 +1,30 @@
<div class="flex flex-col text-center py-2">
<div>
<p class="text-md">
2021 - @(DateTimeOffset.Now.Year) ©
<Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
,由
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
</p>
<p class="text-md">
Build Commit #
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
</p>
</div>
<div>
<p class="text-md">
<Anchor Address="https://beian.miit.gov.cn" Text="蜀ICP备2022004429号-1" NewPage="true"/>
</p>
</div>
</div>
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
private string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
private string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
}

View File

@@ -1,7 +1,7 @@
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200"> <div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
<div class="flex flex-col gap-3 text-md"> <div class="flex flex-col gap-3 text-md">
<div> <div>
文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">初冬的朝阳</a> 文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
</div> </div>
<div> <div>
@@ -18,7 +18,7 @@
</a> </a>
许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自 许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
<a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600"> <a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
Jackfiled's Blog Ricardo's Blog
</a>”。 </a>”。
</div> </div>

View File

@@ -9,6 +9,6 @@
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView> <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
} }
<FocusOnNavigate RouteData="routeData" Selector="page-starter"/> <FocusOnNavigate RouteData="routeData" Selector="h1"/>
</Found> </Found>
</Router> </Router>

View File

@@ -1,13 +1,11 @@
FROM mcr.azure.cn/dotnet/aspnet:10.0 FROM mcr.microsoft.com/dotnet/aspnet:9.0
ARG COMMIT_ID ARG COMMIT_ID
ENV COMMIT_ID=${COMMIT_ID} ENV COMMIT_ID=${COMMIT_ID}
WORKDIR /app WORKDIR /app
COPY out/ ./ COPY bin/Release/net9.0/publish/ ./
COPY source/ ./source/ COPY source/ ./source/
COPY src/YaeBlog/appsettings.json . COPY appsettings.json .
ENV BLOG__ROOT="./source"
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"] ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]

View File

@@ -0,0 +1,33 @@
using Markdig;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace YaeBlog.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMarkdig(this IServiceCollection collection)
{
MarkdownPipelineBuilder builder = new();
builder.UseAdvancedExtensions();
collection.AddSingleton<MarkdownPipeline>(_ => builder.Build());
return collection;
}
public static IServiceCollection AddYamlParser(this IServiceCollection collection)
{
DeserializerBuilder deserializerBuilder = new();
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
deserializerBuilder.IgnoreUnmatchedProperties();
collection.AddSingleton(deserializerBuilder.Build());
SerializerBuilder serializerBuilder = new();
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
collection.AddSingleton(serializerBuilder.Build());
return collection;
}
}

View File

@@ -0,0 +1,47 @@
using AngleSharp;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Services;
using YaeBlog.Models;
using YaeBlog.Processors;
namespace YaeBlog.Extensions;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder AddYaeBlog(this WebApplicationBuilder builder)
{
builder.Services.Configure<BlogOptions>(builder.Configuration.GetSection(BlogOptions.OptionName));
builder.Services.AddHttpClient();
builder.Services.AddMarkdig();
builder.Services.AddYamlParser();
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
builder.Services.AddSingleton<RendererService>();
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
builder.Services.AddTransient<ImagePostRenderProcessor>();
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
builder.Services.AddTransient<BlogOptions>(provider =>
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
return builder;
}
public static WebApplicationBuilder AddServer(this WebApplicationBuilder builder)
{
builder.Services.AddHostedService<BlogHostedService>();
return builder;
}
public static WebApplicationBuilder AddWatcher(this WebApplicationBuilder builder)
{
builder.Services.AddTransient<BlogChangeWatcher>();
builder.Services.AddHostedService<BlogHotReloadService>();
return builder;
}
}

View File

@@ -0,0 +1,44 @@
@inherits LayoutComponentBase
@attribute [StreamRendering]
<main class="container mx-auto flex flex-col min-h-screen">
<div class="grid grid-cols-3 mx-3">
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
<a href="/blog/">
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
</a>
</div>
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
<div>
<a href="/blog/archives/">
<span class="text-xl text-blue-600">归档</span>
</a>
</div>
<div>
<a href="/blog/tags/">
<span class="text-xl text-blue-600">标签</span>
</a>
</div>
<div>
<a href="/about/" target="_blank">
<span class="text-xl text-blue-600">关于</span>
</a>
</div>
<div>
<a href="/friends/" target="_blank">
<span class="text-xl text-blue-600">友链</span>
</a>
</div>
</div>
</div>
</div>
<div class="px-4 py-2 flex-grow">
@Body
</div>
<Foonter/>
</main>

View File

@@ -17,11 +17,13 @@
<Anchor <Anchor
Address="/about/" Address="/about/"
Text="关于"/> Text="关于"
NewPage="@(true)"/>
<Anchor <Anchor
Address="/friends" Address="/friends"
Text="友链"/> Text="友链"
NewPage="@(true)"/>
</div> </div>
</div> </div>
</div> </div>
@@ -32,5 +34,5 @@
</div> </div>
</div> </div>
<Footer/> <Foonter/>
</main> </main>

View File

@@ -0,0 +1,61 @@
namespace YaeBlog.Models;
public class BlogEssay : IComparable<BlogEssay>
{
public required string Title { get; init; }
public required string FileName { get; init; }
public required bool IsDraft { get; init; }
public required DateTime PublishTime { get; init; }
public required string Description { get; init; }
public required uint WordCount { get; init; }
public required string ReadTime { get; init; }
public List<string> Tags { get; } = [];
public required string HtmlContent { get; init; }
public BlogEssay WithNewHtmlContent(string newHtmlContent)
{
var essay = new BlogEssay
{
Title = Title,
FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime,
Description = Description,
WordCount = WordCount,
ReadTime = ReadTime,
HtmlContent = newHtmlContent
};
essay.Tags.AddRange(Tags);
return essay;
}
public int CompareTo(BlogEssay? other)
{
if (other is null)
{
return -1;
}
// 草稿文章应当排在前面
if (IsDraft != other.IsDraft)
{
return IsDraft ? -1 : 1;
}
return other.PublishTime.CompareTo(PublishTime);
}
public override string ToString()
{
return $"{Title}-{PublishTime}";
}
}

View File

@@ -0,0 +1,26 @@
namespace YaeBlog.Models;
public class BlogOptions
{
public const string OptionName = "Blog";
/// <summary>
/// 博客markdown文件的根目录
/// </summary>
public required string Root { get; set; }
/// <summary>
/// 博客正文的广而告之
/// </summary>
public required string Announcement { get; set; }
/// <summary>
/// 博客的起始年份
/// </summary>
public required int StartYear { get; set; }
/// <summary>
/// 博客的友链
/// </summary>
public required List<FriendLink> Links { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace YaeBlog.Models;
/// <summary>
/// 友链模型类
/// </summary>
public class FriendLink
{
/// <summary>
/// 友链名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 友链的简单介绍
/// </summary>
public required string Description { get; set; }
/// <summary>
/// 友链地址
/// </summary>
public required string Link { get; set; }
/// <summary>
/// 头像地址
/// </summary>
public required string AvatarImage { get; set; }
}

View File

@@ -4,9 +4,7 @@ public class MarkdownMetadata
{ {
public string? Title { get; set; } public string? Title { get; set; }
public string? Date { get; set; } public DateTime? Date { get; set; }
public string? UpdateTime { get; set; }
public List<string>? Tags { get; set; } public List<string>? Tags { get; set; }
} }

79
YaeBlog/Pages/About.razor Normal file
View File

@@ -0,0 +1,79 @@
@page "/about"
<PageTitle>
关于
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">关于</h1>
</div>
<div class="py-4">
<span class="italic">把字刻在石头上!(・’ω’・)</span>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于我</h3>
</div>
<div class="py-2">
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span class="italic">jackfiled</span>的名字活动。
<span class="line-through">都是ICP备案过的人了网名似乎没有太大的用处</span>
</div>
<div class="py-2">
主要是一个C#程序员目前也在尝试写一点Rust。
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
前后端分离的项目本当上手。
常常因为现实的压力而写一些C/C++。
<span class="line-through">对于Java和Go的评价很低。</span>
日常使用ArchLinux。
</div>
<div class="py-2">
100%社恐。日常生活是宅在电脑前面自言自语。
兴趣活动是读书和看番,目前在玩原神和三角洲。
</div>
<div class="py-4">
常常被人批评没有梦想,这里就随便瞎编一下。
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
完成第一个不是课程设计的个人开源项目。
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
<span class="line-through">什么时候天上才能掉美少女?</span>
</div>
<div class="py-2">
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
也可以试试在各大平台搜索上面提到的名字。
</div>
</div>
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于本站</h3>
</div>
<div class="py-2">
本站肇始于2021年下半年在开始的两年中个人网站和博客是分别的两个网站个人网站是裸HTML写的博客是用
<a href="https://hexo.io">Hexo</a>渲染的。
</div>
<div class="py-2">
2024年我们决定使用.NET技术完全重构两个网站合二为一。虽然目前这个版本还是一个半成品但是我们一定会努力的~(确信。
</div>
<div class="py-2">
2025年我们将使用的样式库从Bootstrap迁移到Tailwind CSS将现代的前端技术同Blazor结合起来。
</div>
</div>
</div>
</div>
@code {
}

View File

@@ -10,7 +10,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div> <div>
<h1 class="text-4xl page-starter">归档</h1> <h1 class="text-4xl">归档</h1>
</div> </div>
<div class="py-4"> <div class="py-4">
@@ -19,7 +19,7 @@
</span> </span>
</div> </div>
@foreach (IGrouping<DateTimeOffset, BlogEssay> group in _essays) @foreach (IGrouping<DateTime, BlogEssay> group in _essays)
{ {
<div class="p-2"> <div class="p-2">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -30,7 +30,7 @@
<div class="px-4 py-4 flex flex-col"> <div class="px-4 py-4 flex flex-col">
@foreach (BlogEssay essay in group) @foreach (BlogEssay essay in group)
{ {
<a href="@($"/blog/essays/{essay.FileName}")"> <a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300"> <div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
<div class="w-20"> <div class="w-20">
@(essay.PublishTime.ToString("MM月dd日")) @(essay.PublishTime.ToString("MM月dd日"))
@@ -51,13 +51,13 @@
</div> </div>
@code { @code {
private readonly List<IGrouping<DateTimeOffset, BlogEssay>> _essays = []; private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays _essays.AddRange(from essay in Contents.Essays
group essay by new DateTimeOffset(essay.PublishTime.Year, 1, 1,0, 0, 0, TimeSpan.Zero)); group essay by new DateTime(essay.PublishTime.Year, 1, 1));
} }
} }

View File

@@ -6,11 +6,10 @@
@inject NavigationManager NavigationInstance @inject NavigationManager NavigationInstance
<PageTitle> <PageTitle>
Jackfiled's Blog Ricardo's Blog
</PageTitle> </PageTitle>
<div> <div>
<div class="page-starter"></div>
<div class="grid grid-cols-4"> <div class="grid grid-cols-4">
<div class="col-span-4 md:col-span-3"> <div class="col-span-4 md:col-span-3">
@foreach (BlogEssay essay in _essays) @foreach (BlogEssay essay in _essays)
@@ -40,11 +39,6 @@
{ {
_page = Page ?? 1; _page = Page ?? 1;
_pageCount = Contents.Count / EssaysPerPage + 1; _pageCount = Contents.Count / EssaysPerPage + 1;
(_pageCount, int reminder) = int.DivRem(Contents.Count, EssaysPerPage);
if (reminder > 0)
{
_pageCount += 1;
}
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage) if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
{ {

View File

@@ -12,12 +12,17 @@
<div class="flex flex-col py-8"> <div class="flex flex-col py-8">
<div> <div>
<div class="flex flex-col items-center"> <h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div> <div class="col-auto">
<h1 id="title" class="text-4xl page-starter">@(_essay!.Title)</h1> </div>
</div>
<div class="px-6 pt-4 pb-2">
<div class="flex flex-row gap-4">
<div class="font-light">
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
</div> </div>
<div class="flex flex-row gap-4 py-2">
@foreach (string tag in _essay!.Tags) @foreach (string tag in _essay!.Tags)
{ {
<div class="text-sky-500"> <div class="text-sky-500">
@@ -27,27 +32,27 @@
</div> </div>
} }
</div> </div>
<div class="font-light pb-1">
发布于: @(_essay.PublishTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
</div> </div>
@if (_essay.UpdateTime != _essay.PublishTime) <div class="px-6 pt-2 pb-4">
{ <div class="font-light">
<div class="font-light pb-1"> 总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
</div>
}
<div class="font-light pb-1">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)
</div>
</div> </div>
</div> </div>
<div class="grid grid-cols-3"> <div class="grid grid-cols-3">
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
<div class="col-span-3 md:col-span-1"> <div class="col-span-3 md:col-span-1">
<div class="flex flex-col sticky top-20 px-8 pt-20"> <div class="flex flex-col sticky top-0 px-8">
<div> <div>
<h3 class="text-2xl">文章目录</h3> <h3 class="text-2xl">文章目录</h3>
</div> </div>
@@ -88,17 +93,8 @@
} }
</div> </div>
</div> </div>
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div> </div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
</div>
</div> </div>
@code { @code {

View File

@@ -1,7 +1,6 @@
@page "/friends" @page "/friends"
@using Microsoft.Extensions.Options
@using YaeBlog.Models @using YaeBlog.Models
@inject IOptions<BlogOptions> BlogOptionInstance @inject BlogOptions Options
<PageTitle> <PageTitle>
友链 友链
@@ -9,7 +8,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div> <div>
<h1 class="text-4xl page-starter"> <h1 class="text-4xl">
友链 友链
</h1> </h1>
</div> </div>
@@ -19,7 +18,7 @@
</div> </div>
<div class="grid grid-cols-4 g-4 p-2"> <div class="grid grid-cols-4 g-4 p-2">
@foreach (FriendLink link in BlogOptionInstance.Value.Links.Where(i => i is not null).Select(i => i!)) @foreach (FriendLink link in Options.Links)
{ {
<div> <div>
<a href="@(link.Link)" target="_blank" class="mx-5"> <a href="@(link.Link)" target="_blank" class="mx-5">
@@ -44,3 +43,7 @@
} }
</div> </div>
</div> </div>
@code {
}

59
YaeBlog/Pages/Index.razor Normal file
View File

@@ -0,0 +1,59 @@
@page "/"
<PageTitle>
Ricardo's Index
</PageTitle>
<div class="mx-20">
<div class="grid grid-cols-3 py-4">
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
</div>
<div class="col-span-3 md:col-span-2">
<div class="flex flex-col px-3 gap-y-3">
<div class="">
<div class="text-3xl font-bold">初冬的朝阳 (Ricardo Ren)</div>
</div>
<div class="">
<p class="text-lg">a.k.a jackfiled</p>
</div>
<div class="">
<p class="text-lg italic">世界很大,时间很长。</p>
</div>
<div class="">
<p class="text-lg">
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
</p>
</div>
</div>
</div>
</div>
<div class="py-5">
<p class="text-lg">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
</div>
<div>
<p class="text-lg py-1">
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <Anchor Address="/blog/" Text="博客"/> 看看。
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
<Anchor Address="https://jackfiled.github.io/wiki/" Text="我的学习笔记"/>
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
如果您想批判一下我的代码,在
<Anchor Address="https://github.com/jackfiled/" Text="Github"/> 和
<Anchor Address="https://git.rrricardo.top/jackfiled/" Text="Gitea"/>
都可以找到。
</p>
<p class="text-lg py-1">
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
</p>
</div>
</div>
@code {
}

View File

@@ -5,7 +5,7 @@
</PageTitle> </PageTitle>
<div> <div>
<h3 class="text-3xl page-starter">NotFound!</h3> <h3 class="text-3xl">NotFound!</h3>
</div> </div>
@code { @code {

View File

@@ -11,7 +11,7 @@
</PageTitle> </PageTitle>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="page-starter"> <div>
@if (TagName is null) @if (TagName is null)
{ {
<h1 class="text-4xl">标签</h1> <h1 class="text-4xl">标签</h1>

View File

@@ -16,14 +16,15 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
public async Task<BlogEssay> ProcessAsync(BlogEssay essay) public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{ {
BrowsingContext context = new(Configuration.Default); BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(req => req.Content(essay.HtmlContent)); IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document); ApplyGlobalCssStyles(document);
BeatifyTable(document); BeatifyTable(document);
BeatifyList(document); BeatifyList(document);
BeatifyInlineCode(document); BeatifyInlineCode(document);
return essay with { HtmlContent = document.DocumentElement.OuterHtml }; return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
} }
private readonly Dictionary<string, string> _globalCssStyles = new() private readonly Dictionary<string, string> _globalCssStyles = new()
@@ -35,7 +36,6 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
{ "h5", "text-lg font-bold py-1" }, { "h5", "text-lg font-bold py-1" },
{ "p", "p-2" }, { "p", "p-2" },
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" }, { "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
{ "a", "text-blue-600" }
}; };
private void ApplyGlobalCssStyles(IDocument document) private void ApplyGlobalCssStyles(IDocument document)
@@ -102,33 +102,17 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
} }
} }
/// <summary>
/// 美化各种列表元素
/// </summary>
/// <param name="document"></param>
private static void BeatifyList(IDocument document) private static void BeatifyList(IDocument document)
{ {
foreach (IElement listElement in from e in document.All foreach (IElement ulElement in from e in document.All
where e.LocalName is "ol" or "ul" where e.LocalName == "ul"
select e) select e)
{ {
// 给有序或者无序列表添加不同的样式 // 首先给<ul>元素添加样式
listElement.ClassList.Add("ml-10"); ulElement.ClassList.Add("list-disc ml-10");
switch (listElement.LocalName)
{
case "ul":
{
listElement.ClassList.Add("list-disc");
break;
}
case "ol":
{
listElement.ClassList.Add("list-decimal");
break;
}
}
foreach (IElement liElement in from e in listElement.Children
foreach (IElement liElement in from e in ulElement.Children
where e.LocalName == "li" where e.LocalName == "li"
select e) select e)
{ {

View File

@@ -67,7 +67,7 @@ public class HeadlinePostRenderProcessor(
logger.LogWarning("Failed to add headline of {}.", essay.FileName); logger.LogWarning("Failed to add headline of {}.", essay.FileName);
} }
return essay with { HtmlContent = document.DocumentElement.OuterHtml }; return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
} }
private static BlogHeadline ParserHeadlineElement(IElement element) private static BlogHeadline ParserHeadlineElement(IElement element)

View File

@@ -7,12 +7,6 @@ using YaeBlog.Models;
namespace YaeBlog.Processors; namespace YaeBlog.Processors;
/// <summary>
/// 图片地址路径后处理器
/// 将本地图片地址修改为图片API地址
/// </summary>
/// <param name="logger"></param>
/// <param name="options"></param>
public class ImagePostRenderProcessor( public class ImagePostRenderProcessor(
ILogger<ImagePostRenderProcessor> logger, ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options) IOptions<BlogOptions> options)
@@ -40,7 +34,7 @@ public class ImagePostRenderProcessor(
} }
} }
return essay with { HtmlContent = html.DocumentElement.OuterHtml }; return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
} }
public string Name => nameof(ImagePostRenderProcessor); public string Name => nameof(ImagePostRenderProcessor);
@@ -65,7 +59,7 @@ public class ImagePostRenderProcessor(
} }
string imageLink = "api/files/" + filename; string imageLink = "api/files/" + filename;
logger.LogDebug("Generate image link '{link}' for image file '{filename}'.", logger.LogDebug("Generate image link '{}' for image file '{}'.",
imageLink, filename); imageLink, filename);
return imageLink; return imageLink;

4
YaeBlog/Program.cs Normal file
View File

@@ -0,0 +1,4 @@
using YaeBlog.Commands;
YaeBlogCommand command = new();
await command.RunAsync(args);

View File

@@ -16,11 +16,11 @@ public sealed class BlogHotReloadService(
await rendererService.RenderAsync(true); await rendererService.RenderAsync(true);
Task[] reloadTasks = [WatchFileAsync(stoppingToken)]; Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks); await Task.WhenAll(reloadTasks);
} }
private async Task WatchFileAsync(CancellationToken token) private async Task FileWatchTask(CancellationToken token)
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
@@ -33,15 +33,6 @@ public sealed class BlogHotReloadService(
break; break;
} }
FileInfo changeFileInfo = new(changeFile);
if (changeFileInfo.Name.StartsWith('.'))
{
// Ignore dot-started file and directory.
logger.LogDebug("Ignore hidden file: {}.", changeFile);
continue;
}
logger.LogInformation("{} changed, re-rendering.", changeFile); logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear(); essayContentService.Clear();
await rendererService.RenderAsync(true); await rendererService.RenderAsync(true);

View File

@@ -109,12 +109,6 @@ public partial class EssayScanService : IEssayScanService
{ {
foreach (BlogResult blog in fileContents) foreach (BlogResult blog in fileContents)
{ {
if (blog.BlogContent.Length < 4)
{
// Even not contains a legal header.
continue;
}
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal); int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0) if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
{ {
@@ -127,15 +121,14 @@ public partial class EssayScanService : IEssayScanService
try try
{ {
MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString); MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
_logger.LogDebug("Scan metadata title: '{title}' for {name}.", metadata.Title, blog.BlogFile.Name); _logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, blog.BlogFile.Name);
contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft, contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft,
blog.Images, blog.NotFoundImages)); blog.Images, blog.NotFoundImages));
} }
catch (YamlException e) catch (YamlException e)
{ {
_logger.LogWarning("Failed to parser metadata from {name} due to {exception}, skipping", _logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", blog.BlogFile.Name, e);
blog.BlogFile.Name, e);
} }
} }
}); });
@@ -147,6 +140,7 @@ public partial class EssayScanService : IEssayScanService
private async Task<ImageResult> ScanImagePreBlog(DirectoryInfo directory, string blogName, string content) private async Task<ImageResult> ScanImagePreBlog(DirectoryInfo directory, string blogName, string content)
{ {
MatchCollection matchResult = ImagePattern.Matches(content);
DirectoryInfo imageDirectory = new(Path.Combine(directory.FullName, blogName)); DirectoryInfo imageDirectory = new(Path.Combine(directory.FullName, blogName));
Dictionary<string, bool> usedImages = imageDirectory.Exists Dictionary<string, bool> usedImages = imageDirectory.Exists
@@ -154,15 +148,10 @@ public partial class EssayScanService : IEssayScanService
: []; : [];
List<FileInfo> notFoundImages = []; List<FileInfo> notFoundImages = [];
// 同时扫描markdown格式和HTML格式的图片 foreach (Match match in matchResult)
MatchCollection markdownMatchResult = MarkdownImagePattern.Matches(content);
MatchCollection htmlMatchResult = HtmlImagePattern.Matches(content);
IEnumerable<string> imageNames = from match in markdownMatchResult.Concat(htmlMatchResult)
select match.Groups[1].Value;
foreach (string imageName in imageNames)
{ {
string imageName = match.Groups[1].Value;
// 判断md文件中的图片名称中是否包含文件夹名称 // 判断md文件中的图片名称中是否包含文件夹名称
// 例如 blog-1/image.png 或者 image.png // 例如 blog-1/image.png 或者 image.png
// 如果不带文件夹名称 // 如果不带文件夹名称
@@ -209,10 +198,7 @@ public partial class EssayScanService : IEssayScanService
} }
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")] [GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex MarkdownImagePattern { get; } private static partial Regex ImagePattern { get; }
[GeneratedRegex("""<img\s[^>]*?src\s*=\s*["']([^"']*)["'][^>]*>""")]
private static partial Regex HtmlImagePattern { get; }
private DirectoryInfo ValidateRootDirectory() private DirectoryInfo ValidateRootDirectory()

View File

@@ -9,7 +9,7 @@ using YaeBlog.Models;
namespace YaeBlog.Services; namespace YaeBlog.Services;
public sealed partial class RendererService( public partial class RendererService(
ILogger<RendererService> logger, ILogger<RendererService> logger,
IEssayScanService essayScanService, IEssayScanService essayScanService,
MarkdownPipeline markdownPipeline, MarkdownPipeline markdownPipeline,
@@ -34,32 +34,42 @@ public sealed partial class RendererService(
} }
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts); IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
ConcurrentBag<BlogEssay> essays = [];
Parallel.ForEach(preProcessedContents, content => List<BlogEssay> essays = [];
foreach (BlogContent content in preProcessedContents)
{ {
(uint wordCount, string readTime) = GetWordCount(content); uint wordCount = GetWordCount(content);
DateTimeOffset publishDate = content.Metadata.Date is null BlogEssay essay = new()
? DateTimeOffset.Now {
: DateTimeOffset.Parse(content.Metadata.Date); Title = content.Metadata.Title ?? content.BlogName,
// 如果不存在最后的更新时间,就把更新时间设置为发布时间 FileName = content.BlogName,
DateTimeOffset updateTime = content.Metadata.UpdateTime is null IsDraft = content.IsDraft,
? publishDate Description = GetDescription(content),
: DateTimeOffset.Parse(content.Metadata.UpdateTime); WordCount = wordCount,
string description = GetDescription(content); ReadTime = CalculateReadTime(wordCount),
List<string> tags = content.Metadata.Tags ?? []; PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.Content
};
string originalHtml = Markdown.ToHtml(content.Content, markdownPipeline); if (content.Metadata.Tags is not null)
{
BlogEssay essay = new( essay.Tags.AddRange(content.Metadata.Tags);
content.Metadata.Title ?? content.BlogName, content.BlogName, content.IsDraft, publishDate, updateTime, }
description, wordCount, readTime, tags, originalHtml);
logger.LogDebug("Render essay: {}", essay);
essays.Add(essay); essays.Add(essay);
}
ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay =>
{
BlogEssay newEssay =
essay.WithNewHtmlContent(Markdown.ToHtml(essay.HtmlContent, markdownPipeline));
postProcessEssays.Add(newEssay);
logger.LogDebug("Render markdown file {}.", newEssay);
}); });
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(essays); IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
foreach (BlogEssay essay in postProcessedEssays) foreach (BlogEssay essay in postProcessedEssays)
{ {
@@ -172,21 +182,28 @@ public sealed partial class RendererService(
string description = builder.ToString(); string description = builder.ToString();
logger.LogDebug("Description of {name} is {desc}.", content.BlogName, logger.LogDebug("Description of {} is {}.", content.BlogName,
description); description);
return description; return description;
} }
private (uint, string) GetWordCount(BlogContent content) private uint GetWordCount(BlogContent content)
{ {
uint count = MarkdownWordCounter.CountWord(content); int count = (from c in content.Content
where char.IsLetterOrDigit(c)
select c).Count();
logger.LogDebug("Word count of {blog} is {count}", content.BlogName, logger.LogDebug("Word count of {} is {}", content.BlogName,
count); count);
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟 return (uint)count;
uint second = count / 10; }
TimeSpan span = new(0, 0, (int)second);
return (count, span.ToString("mm'分'ss'秒'")); private static string CalculateReadTime(uint wordCount)
{
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟
int second = (int)wordCount / 10;
TimeSpan span = new(0, 0, second);
return span.ToString("mm'分 'ss'秒'");
} }
} }

40
YaeBlog/YaeBlog.csproj Normal file
View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="ImageFlow.NativeRuntime.ubuntu-x86_64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('Linux'))"/>
<PackageReference Include="ImageFlow.NativeRuntime.osx-arm64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('OSX'))"/>
<PackageReference Include="ImageFlow.Net" Version="0.13.2"/>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
<ItemGroup>
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
</ItemGroup>
</Target>
</Project>

View File

@@ -6,6 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using YaeBlog
@using YaeBlog.Components @using YaeBlog.Components
@using BlazorSvgComponents
@using BlazorSvgComponents.Models

44
YaeBlog/appsettings.json Normal file
View File

@@ -0,0 +1,44 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Tailwind": {
"InputFile": "wwwroot/input.css",
"OutputFile": "wwwroot/output.css"
},
"Blog": {
"Root": "source",
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
"StartYear": 2021,
"Links": [
{
"Name": "Ichirinko",
"Description": "这是个大哥",
"Link": "https://ichirinko.top",
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
},
{
"Name": "志田千陽",
"Description": "日出多值得",
"Link": "https://zzachary.top/",
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
},
{
"Name": "不会写程序的晨旭",
"Description": "一个普通大学生",
"Link": "https://chenxutalk.top",
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
},
{
"Name": "万木长风",
"Description": "世界渲染中...",
"Link": "https://ryohai.fun",
"AvatarImage": "https://ryohai.fun/icon.jpg"
}
]
}
}

View File

@@ -0,0 +1,13 @@
version: '3.8'
services:
blog:
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
- "traefik.http.routers.blog.tls=true"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"

View File

@@ -1,13 +1,12 @@
--- ---
title: High Performance Computing 25 SP NVIDIA title: High Performance Computing 25 SP NVIDIA
date: 2025-08-31T13:50:42.8639950+08:00 date: 2025-04-24T19:02:36.1077330+08:00
tags: tags:
- 高性能计算 - 高性能计算
- 学习资料 - 学习资料
--- ---
Fxxk you, NVIDIA! Fxxk you, NVIDIA!
<!--more--> <!--more-->

Some files were not shown because too many files have changed in this diff Show More