10 Commits

Author SHA1 Message Date
91501cd4d3 dev 2025-01-24 16:56:16 +08:00
10b4cef4c1 Merge branch 'master' into feat-highlight
# Conflicts:
#	YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
#	YaeBlog.Core/YaeBlog.Core.csproj
2025-01-24 16:53:57 +08:00
3aae468e65 feat: 从Bootstrap迁移到Tailwind css (#9)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m15s
Reviewed-on: #9
2025-01-24 16:46:56 +08:00
1ceaf30061 blog: 2024-final
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m47s
2025-01-16 17:27:11 +08:00
87204dab8e blog: rust-up-trait
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m28s
2024-12-15 15:52:58 +08:00
05d40ce3b6 bump: net8.0 -> net9.0
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 3m23s
2024-12-06 15:38:35 +08:00
309db7e5f1 blog: rust-drop-stack-overflow
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 50s
2024-11-06 14:00:46 +08:00
fb71ce64cf blog: 异构并行编程模型的昨天、今天和明天 (#8)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 2m37s
Reviewed-on: #8
2024-11-04 22:33:42 +08:00
4fd464fd34 Merge branch 'master' into feat-highlight 2024-09-08 22:44:57 +08:00
d9c17720dc dev: CSharp language highlight. 2024-08-28 20:26:41 +08:00
104 changed files with 2811 additions and 958 deletions

3
.gitignore vendored
View File

@@ -482,3 +482,6 @@ $RECYCLE.BIN/
# Vim temporary swap files # Vim temporary swap files
*.swp *.swp
# Tailwind auto-generated stylesheet
output.css

View File

@@ -1,8 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop

View File

@@ -1,10 +0,0 @@
namespace YaeBlog.Core.Models;
public class AboutInfo
{
public required string Introduction { get; set; }
public required string Description { get; set; }
public required string AvatarImage { get; set; }
}

View File

@@ -1,29 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Processors;
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IElement> preElements = from e in document.All
where e.LocalName == "pre"
select e;
foreach (IElement element in preElements)
{
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(CodeBlockPostRenderProcessor);
}

View File

@@ -1,34 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Processors;
public class TablePostRenderProcessor: IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext browsingContext = new(Configuration.Default);
IDocument document = await browsingContext.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
where item.LocalName == "table"
select item as IHtmlTableElement;
foreach (IHtmlTableElement element in tableElements)
{
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
divElement.InnerHtml = element.OuterHtml;
divElement.ClassList.Add("py-2", "table-wrapper");
element.Replace(divElement);
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(TablePostRenderProcessor);
}

View File

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

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.0" />
<PackageReference Include="Markdig" Version="0.34.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
@@ -29,10 +27,6 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU {20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU {20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,11 +1,13 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions; namespace YaeBlog.Abstraction;
public interface IEssayContentService public interface IEssayContentService
{ {
public IReadOnlyDictionary<string, BlogEssay> Essays { get; } public IEnumerable<BlogEssay> Essays { get; }
public int Count { get; }
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; } public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
@@ -16,6 +18,8 @@ public interface IEssayContentService
public bool TryAdd(BlogEssay essay); public bool TryAdd(BlogEssay essay);
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
public void RefreshTags(); public void RefreshTags();
public void Clear(); public void Clear();

View File

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions; namespace YaeBlog.Abstraction;
public interface IEssayScanService public interface IEssayScanService
{ {

View File

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions; namespace YaeBlog.Abstraction;
public interface IPostRenderProcessor public interface IPostRenderProcessor
{ {

View File

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions; namespace YaeBlog.Abstraction;
public interface IPreRenderProcessor public interface IPreRenderProcessor
{ {

View File

@@ -1,7 +1,7 @@
using System.CommandLine.Binding; using System.CommandLine.Binding;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Commands.Binders; namespace YaeBlog.Commands.Binders;

View File

@@ -1,8 +1,8 @@
using System.CommandLine.Binding; using System.CommandLine.Binding;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions; using YaeBlog.Abstraction;
using YaeBlog.Core.Models; using YaeBlog.Models;
using YaeBlog.Core.Services; using YaeBlog.Services;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;

View File

@@ -1,15 +1,32 @@
using System.CommandLine; using System.CommandLine;
using YaeBlog.Commands.Binders; using YaeBlog.Commands.Binders;
using YaeBlog.Components; using YaeBlog.Components;
using YaeBlog.Core.Extensions; using YaeBlog.Extensions;
using YaeBlog.Core.Models; using YaeBlog.Models;
using YaeBlog.Core.Services; using YaeBlog.Services;
namespace YaeBlog.Commands; namespace YaeBlog.Commands;
public static class CommandExtensions public sealed class YaeBlogCommand
{ {
public static void AddServeCommand(this RootCommand rootCommand) private readonly RootCommand _rootCommand = new("YaeBlog Cli");
public YaeBlogCommand()
{
AddServeCommand(_rootCommand);
AddWatchCommand(_rootCommand);
AddListCommand(_rootCommand);
AddNewCommand(_rootCommand);
AddPublishCommand(_rootCommand);
AddScanCommand(_rootCommand);
}
public Task<int> RunAsync(string[] args)
{
return _rootCommand.InvokeAsync(args);
}
private static void AddServeCommand(RootCommand rootCommand)
{ {
Command serveCommand = new("serve", "Start http server."); Command serveCommand = new("serve", "Start http server.");
rootCommand.AddCommand(serveCommand); rootCommand.AddCommand(serveCommand);
@@ -21,7 +38,6 @@ public static class CommandExtensions
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog(); builder.AddYaeBlog();
builder.AddServer(); builder.AddServer();
@@ -40,7 +56,7 @@ public static class CommandExtensions
}); });
} }
public static void AddWatchCommand(this RootCommand rootCommand) private static void AddWatchCommand(RootCommand rootCommand)
{ {
Command command = new("watch", "Start a blog watcher that re-render when file changes."); Command command = new("watch", "Start a blog watcher that re-render when file changes.");
rootCommand.AddCommand(command); rootCommand.AddCommand(command);
@@ -52,7 +68,6 @@ public static class CommandExtensions
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog(); builder.AddYaeBlog();
builder.AddWatcher(); builder.AddWatcher();
@@ -71,7 +86,7 @@ public static class CommandExtensions
}); });
} }
public static void AddNewCommand(this RootCommand rootCommand) private static void AddNewCommand(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);
@@ -101,7 +116,7 @@ public static class CommandExtensions
new EssayScanServiceBinder()); new EssayScanServiceBinder());
} }
public static void AddListCommand(this RootCommand rootCommand) private static void AddListCommand(RootCommand rootCommand)
{ {
Command command = new("list", "List all blogs"); Command command = new("list", "List all blogs");
rootCommand.AddCommand(command); rootCommand.AddCommand(command);
@@ -124,7 +139,7 @@ public static class CommandExtensions
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder()); }, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
} }
public static void AddScanCommand(this RootCommand rootCommand) private static void AddScanCommand(RootCommand rootCommand)
{ {
Command command = new("scan", "Scan unused and not found images."); Command command = new("scan", "Scan unused and not found images.");
rootCommand.AddCommand(command); rootCommand.AddCommand(command);
@@ -165,7 +180,7 @@ public static class CommandExtensions
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption); }, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
} }
public static void AddPublishCommand(this RootCommand rootCommand) private static void AddPublishCommand(RootCommand rootCommand)
{ {
Command command = new("publish", "Publish a new blog file."); Command command = new("publish", "Publish a new blog file.");
rootCommand.AddCommand(command); rootCommand.AddCommand(command);

View File

@@ -0,0 +1,9 @@
<a href="@Address" class="text-blue-600" target="@(NewPage ? "_blank" : "_self")">@Text</a>
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool NewPage { get; set; }
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
@@ -7,22 +7,14 @@
<base href="/"/> <base href="/"/>
<link rel="stylesheet" href="YaeBlog.styles.css"/> <link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/> <link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="bootstrap.min.css"/>
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
<link rel="stylesheet" href="globals.css"/> <link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="output.css"/>
<HeadOutlet/> <HeadOutlet/>
</head> </head>
<body> <body>
<Routes/> <Routes/>
<script src="_framework/blazor.web.js"></script>
<script src="_framework/blazor.web.js"></script>
<script src="bootstrap.bundle.min.js"></script>
<script src="clipboard.min.js"></script>
<script>
const clipboard = new ClipboardJS('.btn');
</script>
</body> </body>
</html> </html>

View File

@@ -1,57 +1,49 @@
@using YaeBlog.Core.Abstractions @using YaeBlog.Abstraction
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject BlogOptions Options @inject BlogOptions Options
<div class="container"> <div class="flex flex-col">
<div class="row justify-content-center"> <div class="p-10">
<div class="col-auto p-4"> <img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full"/>
<Image Src="images/avatar.png" Alt="Ricardo's avatar"/>
</div>
</div> </div>
<div class="row justify-content-center p-3"> <div class="px-10 py-2 text-xl">
<div class="col-auto fs-4"> “奇奇怪怪东西的聚合地”
“奇奇怪怪东西的聚合地”
</div>
</div> </div>
<div class="row justify-content-between px-2 py-1 fs-5"> <div class="flex flex-row justify-between px-6 py-2 text-xl">
<div class="col-auto"> <div>
文章 文章
</div> </div>
<div class="col-auto"> <a href="/blog/archives/">
<a href="/blog/archives"> <div>
@(Contents.Essays.Count) @(Contents.Count)
</a> </div>
</div> </a>
</div> </div>
<div class="row justify-content-between px-2 py-1 fs-5"> <div class="flex flex-row justify-between px-6 py-2 text-xl">
<div class="col-auto"> <div>
标签 标签
</div> </div>
<div class="col-auto"> <a href="/blog/tags/">
<a href="/blog/tags"> <div>
@(Contents.Tags.Count) @(Contents.Tags.Count)
</a> </div>
</div> </a>
</div> </div>
<div class="row justify-content-start fs-5" style="padding-top: 2em"> <div class="text-xl px-2 py-2">
<div class="col-auto"> 广而告之
广而告之
</div>
</div> </div>
<div class="row"> <div class="px-6">
<div class="col"> <p class="text-lg">
<p style="text-indent: 2em"> @(Options.Announcement)
@(Options.Announcement) </p>
</p>
</div>
</div> </div>
</div> </div>

View File

@@ -1,19 +1,19 @@
@using System.Text.Encodings.Web @using System.Text.Encodings.Web
@using YaeBlog.Core.Models @using YaeBlog.Models
<div class="container p-3"> <div class="flex flex-col p-3">
<div class="row fs-2 fw-bold py-2 essay-title"> <div class="text-3xl font-bold py-2">
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a> <a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
</div> </div>
<div class="row p-2 justify-content-start"> <div class="p-2 flex flex-row justify-content-start gap-2">
<div class="col-auto fw-light"> <div class="font-light">
@(Essay.PublishTime.ToString("yyyy-MM-dd")) @(Essay.PublishTime.ToString("yyyy-MM-dd"))
</div> </div>
@foreach (string key in Essay.Tags) @foreach (string key in Essay.Tags)
{ {
<div class="col-auto"> <div class="text-sky-600">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))"> <a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
# @key # @key
</a> </a>
@@ -21,20 +21,11 @@
} }
</div> </div>
<div class="row p-2"> <div class="p-2">
<div class="col"> @(Essay.Description)
@(Essay.Description)
</div>
</div>
<div class="row">
<div class="col border-bottom">
</div>
</div> </div>
</div> </div>
@code { @code {
[Parameter] [Parameter] public required BlogEssay Essay { get; set; }
public required BlogEssay Essay { get; set; }
} }

View File

@@ -1,3 +0,0 @@
.essay-title a {
color: var(--bs-body-color);
}

View File

@@ -1,14 +1,22 @@
<div class="row align-items-end text-center"> <div class="flex flex-col text-center py-2">
<div class="row"> <div>
<p class="fs-6"> <p class="text-md">
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a> 2021 - @(DateTimeOffset.Now.Year) ©
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。 <Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
,由
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
</p> </p>
</div> </div>
<div class="row"> <div>
<p class="fs-6"> <p class="text-md">
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a> <a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
</p> </p>
</div> </div>
</div> </div>
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
}

View File

@@ -1,36 +1,33 @@
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject BlogOptions Options @inject BlogOptions Options
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle"> <div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
<div class="col"> <div class="flex flex-col gap-3 text-md">
<div class="row p-1"> <div>
<div class="col"> 文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
</div>
</div> </div>
<div class="row p-1"> <div>
<div class="col"> 文章地址:
文章地址: <a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
<a href="/blog/essays/@(EssayAddress)" target="_blank"> @($"https://rrricardo.top/blog/essays/{EssayFilename}")
@($"https://rrricardo.top/blog/essays/{EssayAddress}") </a>
</a>
</div>
</div> </div>
<div class="row p-1"> <div>
<div class="col"> 版权声明:本博客所有文章除特别声明外,均采用
版权声明:本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="text-blue-600">
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> CC BY-NC-SA 4.0
许可协议,转载请注明来自 </a>
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>。 许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
</div> <a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
Ricardo's Blog
</a>”。
</div> </div>
</div> </div>
</div> </div>
@code @code
{ {
[Parameter] public string? EssayAddress { get; set; } [Parameter] public string? EssayFilename { get; set; }
} }

View File

@@ -1,2 +0,0 @@
.copyright {
}

View File

@@ -0,0 +1,22 @@
@if (Selected)
{
<div class="border rounded-lg shadow-neutral-500 bg-sky-400 w-8 h-8 inline-block leading-8 text-center">
<span class="text-white">@(Text)</span>
</div>
}
else
{
<a href="@Address">
<div class="border rounded-lg shadow-neutral-500 w-8 h-8 inline-block leading-8 text-center">
<span>@(Text)</span>
</div>
</a>
}
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool Selected { get; set; }
}

View File

@@ -0,0 +1,46 @@
<div class="flex flex-row justify-center gap-3">
@if (Page != 1)
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="<"/>
}
@if (Page == 1)
{
<PageAnchor Address="@GenerateAddress(1)" Text="1" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(2)" Text="2"/>
<PageAnchor Address="@GenerateAddress(3)" Text="3"/>
}
else if (Page == PageCount)
{
<PageAnchor Address="@GenerateAddress(PageCount - 2)" Text="@($"{PageCount - 2}")"/>
<PageAnchor Address="@GenerateAddress(PageCount - 1)" Text="@($"{PageCount - 1}")"/>
<PageAnchor Address="@GenerateAddress(PageCount)" Text="@($"{PageCount}")" Selected="@true"/>
}
else
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page + 1}")"/>
}
@if (Page != PageCount)
{
<PageAnchor Address="@GenerateAddress(Page + 1)" Text=">"/>
}
</div>
@code {
[Parameter] public string? BaseUrl { get; set; }
[Parameter] public int PageCount { get; set; }
[Parameter] public int Page { get; set; }
private string GenerateAddress(int page) => $"{BaseUrl}?page={page}";
}

View File

@@ -1,7 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app WORKDIR /app
COPY bin/Release/net8.0/publish/ ./ COPY bin/Release/net9.0/publish/ ./
COPY source/ ./source/ COPY source/ ./source/
COPY appsettings.json . COPY appsettings.json .

View File

@@ -1,9 +1,8 @@
using Markdig; using Markdig;
using Microsoft.Extensions.DependencyInjection;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
namespace YaeBlog.Core.Extensions; namespace YaeBlog.Extensions;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {

View File

@@ -1,13 +1,11 @@
using AngleSharp; using AngleSharp;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions; using YaeBlog.Abstraction;
using YaeBlog.Core.Models; using YaeBlog.Services;
using YaeBlog.Core.Processors; using YaeBlog.Models;
using YaeBlog.Core.Services; using YaeBlog.Processors;
namespace YaeBlog.Core.Extensions; namespace YaeBlog.Extensions;
public static class WebApplicationBuilderExtensions public static class WebApplicationBuilderExtensions
{ {
@@ -19,14 +17,13 @@ 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<AngleSharp.IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IEssayScanService, EssayScanService>(); builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
builder.Services.AddSingleton<RendererService>(); builder.Services.AddSingleton<RendererService>();
builder.Services.AddSingleton<IEssayContentService, EssayContentService>(); builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
builder.Services.AddTransient<ImagePostRenderProcessor>(); builder.Services.AddTransient<ImagePostRenderProcessor>();
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
builder.Services.AddTransient<TablePostRenderProcessor>();
builder.Services.AddTransient<HeadlinePostRenderProcessor>(); builder.Services.AddTransient<HeadlinePostRenderProcessor>();
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
builder.Services.AddTransient<BlogOptions>(provider => builder.Services.AddTransient<BlogOptions>(provider =>
provider.GetRequiredService<IOptions<BlogOptions>>().Value); provider.GetRequiredService<IOptions<BlogOptions>>().Value);

View File

@@ -1,19 +1,16 @@
using Microsoft.AspNetCore.Builder; using YaeBlog.Abstraction;
using Microsoft.Extensions.DependencyInjection; using YaeBlog.Processors;
using YaeBlog.Core.Abstractions; using YaeBlog.Services;
using YaeBlog.Core.Processors;
using YaeBlog.Core.Services;
namespace YaeBlog.Core.Extensions; namespace YaeBlog.Extensions;
public static class WebApplicationExtensions public static class WebApplicationExtensions
{ {
public static void UseYaeBlog(this WebApplication application) public static void UseYaeBlog(this WebApplication application)
{ {
application.UsePostRenderProcessor<ImagePostRenderProcessor>(); application.UsePostRenderProcessor<ImagePostRenderProcessor>();
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
application.UsePostRenderProcessor<TablePostRenderProcessor>();
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>(); application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
} }
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor

View File

@@ -2,60 +2,41 @@
@attribute [StreamRendering] @attribute [StreamRendering]
<main class="container"> <main class="container mx-auto flex flex-col min-h-screen">
<div class="row d-none d-xl-flex" style="height: 80px"> <div class="grid grid-cols-3 mx-3">
<div class="px-2 col-9"> <div class="md:col-span-2 col-span-3 h-20 flex items-center">
<a href="/blog/" class="p-2"> <a href="/blog/">
<h4>Ricardo's Blog</h4> <span class="text-blue-600 text-2xl">Ricardo's Blog</span>
</a> </a>
</div> </div>
<div class="col-3 d-flex justify-content-around align-items-center"> <div class="md:col-span-1 col-span-3 h-20 flex items-center">
<a href="/blog/" class="p-2"> <div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
<h5>首页</h5> <div>
</a> <a href="/blog/archives/">
<span class="text-xl text-blue-600">归档</span>
<a href="/blog/archives/" class="p-2"> </a>
<h5>归档</h5> </div>
</a> <div>
<a href="/blog/tags/">
<a href="/blog/tags/" class="p-2"> <span class="text-xl text-blue-600">标签</span>
<h5>标签</h5> </a>
</a> </div>
<div>
<a href="/blog/about/" class="p-2"> <a href="/about/" target="_blank">
<h5>关于</h5> <span class="text-xl text-blue-600">关于</span>
</a> </a>
</div>
<div>
<a href="/friends/" target="_blank">
<span class="text-xl text-blue-600">友链</span>
</a>
</div>
</div>
</div> </div>
</div> </div>
<div class="row d-xl-none"> <div class="px-4 py-2 flex-grow">
<div class="px-2 col-12">
<a href="/blog/" class="p-2">
<h4>Ricardo's Blog</h4>
</a>
</div>
<div class="px-2 col-12 justify-content-end d-flex">
<a href="/blog/" class="p-2">
<h5>首页</h5>
</a>
<a href="/blog/archives/" class="p-2">
<h5>归档</h5>
</a>
<a href="/blog/tags/" class="p-2">
<h5>标签</h5>
</a>
<a href="/blog/about/" class="p-2">
<h5>关于</h5>
</a>
</div>
</div>
<div class="row px-4 py-2">
@Body @Body
</div> </div>

View File

@@ -1,21 +1,34 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<main class="container"> <main class="container mx-auto min-h-screen flex flex-col">
<div class="row" style="height: 80px"> <div class="grid grid-cols-4">
<div class="px-2 col-8"> <div class="px-2 md:col-span-3 col-span-4 h-20 flex items-center">
<a href="/" class="p-2"> <a href="/" class="text-2xl">
<h4>Ricardo's Index</h4> <h4 class="text-blue-600">Ricardo's Index</h4>
</a> </a>
</div> </div>
<div class="col-4 d-flex justify-content-around align-items-center"> <div class="md:col-span-1 col-span-4 h-20 flex items-center">
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank"> <div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
<h5>E-mail</h5> <Anchor
</a> Address="/blog/"
Text="博客"
NewPage="@(true)"/>
<Anchor
Address="/about/"
Text="关于"
NewPage="@(true)"/>
<Anchor
Address="/friends"
Text="友链"
NewPage="@(true)"/>
</div>
</div> </div>
</div> </div>
<div class="row px-4 center"> <div class="px-4 mx-auto flex-grow">
<div class="py-2"> <div class="py-2">
@Body @Body
</div> </div>

View File

@@ -1,8 +0,0 @@
.center {
margin: 0 auto;
max-width: 48em;
min-height: calc(100vh - 80px);
position: relative;
display: flex;
flex-direction: column;
}

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class BlogContent public class BlogContent
{ {
@@ -7,4 +7,6 @@ public class BlogContent
public required MarkdownMetadata Metadata { get; init; } public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; } public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
} }

View File

@@ -1,6 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts) public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
{ {

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class BlogEssay : IComparable<BlogEssay> public class BlogEssay : IComparable<BlogEssay>
{ {
@@ -6,6 +6,8 @@ public class BlogEssay : IComparable<BlogEssay>
public required string FileName { get; init; } public required string FileName { get; init; }
public required bool IsDraft { get; init; }
public required DateTime PublishTime { get; init; } public required DateTime PublishTime { get; init; }
public required string Description { get; init; } public required string Description { get; init; }
@@ -24,6 +26,7 @@ public class BlogEssay : IComparable<BlogEssay>
{ {
Title = Title, Title = Title,
FileName = FileName, FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime, PublishTime = PublishTime,
Description = Description, Description = Description,
WordCount = WordCount, WordCount = WordCount,
@@ -39,10 +42,16 @@ public class BlogEssay : IComparable<BlogEssay>
{ {
if (other is null) if (other is null)
{ {
return 1; return -1;
} }
return PublishTime.CompareTo(other.PublishTime); // 草稿文章应当排在前面
if (IsDraft != other.IsDraft)
{
return IsDraft ? -1 : 1;
}
return other.PublishTime.CompareTo(PublishTime);
} }
public override string ToString() public override string ToString()

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class BlogHeadline(string title, string selectorId) public class BlogHeadline(string title, string selectorId)
{ {

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class BlogOptions public class BlogOptions
{ {

View File

@@ -1,6 +1,6 @@
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class EssayTag(string tagName) : IEquatable<EssayTag> public class EssayTag(string tagName) : IEquatable<EssayTag>
{ {

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
/// <summary> /// <summary>
/// 友链模型类 /// 友链模型类

View File

@@ -1,3 +1,3 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages); public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models; namespace YaeBlog.Models;
public class MarkdownMetadata public class MarkdownMetadata
{ {

View File

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

View File

@@ -1,8 +0,0 @@
.link-item {
padding: 1rem;
border-radius: 4px;
}
.link-item:hover {
background-color: var(--bs-secondary-bg);
}

View File

@@ -1,6 +1,6 @@
@page "/blog/archives" @page "/blog/archives"
@using YaeBlog.Core.Abstractions @using YaeBlog.Abstraction
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@@ -8,68 +8,56 @@
归档 归档
</PageTitle> </PageTitle>
<div class="container"> <div class="flex flex-col">
<div class="row"> <div>
<div class="col"> <h1 class="text-4xl">归档</h1>
<div class="container">
<div class="row">
<div class="col">
<h1>归档</h1>
</div>
</div>
<div class="row">
<div class="col fst-italic py-4">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</div>
</div>
</div>
</div>
</div> </div>
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays) <div class="py-4">
{ <span class="italic">
<div class="row"> 时光图书馆,黑历史集散地。(๑◔‿◔๑)
<div class="col"> </span>
<div class="container"> </div>
<div class="row">
<div class="col">
<h3>@(group.Key.Year)</h3>
</div>
</div>
<div class="container px-3 py-2"> @foreach (IGrouping<DateTime, BlogEssay> group in _essays)
@foreach (KeyValuePair<string, BlogEssay> essay in group) {
{ <div class="p-2">
<div class="row py-1"> <div class="flex flex-col">
<div class="col-auto"> <div>
@(essay.Value.PublishTime.ToString("MM-dd")) <h3 class="text-xl">@(group.Key.Year)</h3>
</div>
<div class="px-4 py-4 flex flex-col">
@foreach (BlogEssay essay in group)
{
<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="w-20">
@(essay.PublishTime.ToString("MM月dd日"))
</div> </div>
<div class="col-auto"> <div>
<a href="/blog/essays/@(essay.Key)"> <span class="text-blue-600">
@(essay.Value.Title) @(essay.Title)
</a> </span>
</div> </div>
</div> </div>
} </a>
</div> }
</div> </div>
</div> </div>
</div> </div>
} }
</div> </div>
@code { @code {
private readonly List<IGrouping<DateTime, KeyValuePair<string, 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
orderby essay.Value.PublishTime descending group essay by new DateTime(essay.PublishTime.Year, 1, 1));
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
} }
} }

View File

@@ -1,6 +1,6 @@
@page "/blog" @page "/blog"
@using YaeBlog.Core.Abstractions @using YaeBlog.Abstraction
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject NavigationManager NavigationInstance @inject NavigationManager NavigationInstance
@@ -9,79 +9,18 @@
Ricardo's Blog Ricardo's Blog
</PageTitle> </PageTitle>
<div class="container"> <div>
<div class="row"> <div class="grid grid-cols-4">
<div class="col-sm-12 col-md-9"> <div class="col-span-4 md:col-span-3">
@foreach (KeyValuePair<string, BlogEssay> pair in _essays) @foreach (BlogEssay essay in _essays)
{ {
<EssayCard Essay="@(pair.Value)"/> <EssayCard Essay="@(essay)"/>
} }
<div class="row align-items-center justify-content-center p-3"> <Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
@if (_page == 1)
{
<div class="col-auto fw-light">上一页</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">上一页</a>
</div>
}
@if (_page == 1)
{
<div class="col-auto">
1
</div>
<div class="col-auto">
<a href="/blog/?page=2">2</a>
</div>
<div class="col-auto">
<a href="/blog/?page=3">3</a>
</div>
}
else if (_page == _pageCount)
{
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
</div>
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
</div>
<div class="col-auto">
@(_pageCount)
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
</div>
<div class="col-auto">
@(_page)
</div>
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
</div>
}
@if (_page == _pageCount)
{
<div class="col-auto fw-light">
下一页
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">下一页</a>
</div>
}
</div>
</div> </div>
<div class="col-sm-12 col-md-3"> <div class="col-span-4 md:col-span-1">
<BlogInformationCard/> <BlogInformationCard/>
</div> </div>
</div> </div>
@@ -91,7 +30,7 @@
[SupplyParameterFromQuery] private int? Page { get; set; } [SupplyParameterFromQuery] private int? Page { get; set; }
private readonly List<KeyValuePair<string, BlogEssay>> _essays = []; private readonly List<BlogEssay> _essays = [];
private const int EssaysPerPage = 8; private const int EssaysPerPage = 8;
private int _pageCount = 1; private int _pageCount = 1;
private int _page = 1; private int _page = 1;
@@ -99,16 +38,15 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
_page = Page ?? 1; _page = Page ?? 1;
_pageCount = Contents.Essays.Count / EssaysPerPage + 1; _pageCount = Contents.Count / EssaysPerPage + 1;
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage) if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
{ {
NavigationInstance.NavigateTo("/NotFount"); NavigationInstance.NavigateTo("/NotFount");
return; return;
} }
_essays.AddRange(Contents.Essays _essays.AddRange(Contents.Essays
.OrderByDescending(p => p.Value.PublishTime)
.Skip((_page - 1) * EssaysPerPage) .Skip((_page - 1) * EssaysPerPage)
.Take(EssaysPerPage)); .Take(EssaysPerPage));
} }

View File

@@ -1,7 +0,0 @@
.essay-title a {
color: var(--bs-body-color);
}
.read-more a {
color: var(--bs-body-color);
}

View File

@@ -1,7 +1,7 @@
@page "/blog/essays/{BlogKey}" @page "/blog/essays/{BlogKey}"
@using System.Text.Encodings.Web @using System.Text.Encodings.Web
@using YaeBlog.Core.Abstractions @using YaeBlog.Abstraction
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject NavigationManager NavigationInstance @inject NavigationManager NavigationInstance
@@ -10,96 +10,87 @@
@(_essay!.Title) @(_essay!.Title)
</PageTitle> </PageTitle>
<div class="container py-4"> <div class="flex flex-col py-8">
<div class="row"> <div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="col-auto"> <div class="col-auto">
<h1 id="title">@(_essay!.Title)</h1>
</div> </div>
</div> </div>
<div class="row px-4 py-1"> <div class="px-6 pt-4 pb-2">
<div class="col-auto fw-light"> <div class="flex flex-row gap-4">
@(_essay!.PublishTime.ToString("yyyy-MM-dd")) <div class="font-light">
</div> @(_essay!.PublishTime.ToString("yyyy-MM-dd"))
@foreach (string tag in _essay!.Tags)
{
<div class="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div> </div>
}
@foreach (string tag in _essay!.Tags)
{
<div class="text-sky-500">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
}
</div>
</div> </div>
<div class="row px-4 py-1"> <div class="px-6 pt-2 pb-4">
<div class="col-auto fw-light"> <div class="font-light">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。 总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
</div> </div>
</div> </div>
<div class="row"> <div class="grid grid-cols-3">
<div class="col-lg-8 col-md-12"> <div class="col-span-3 md:col-span-2 flex flex-col gap-3">
@((MarkupString)_essay!.HtmlContent) <div>
@((MarkupString)_essay!.HtmlContent)
</div>
<LicenseDisclaimer EssayAddress="@BlogKey"/> <div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div> </div>
<div class="col-lg-4 col-md-12"> <div class="col-span-3 md:col-span-1">
<div class="row sticky-lg-top justify-content-center"> <div class="flex flex-col sticky top-0 px-8">
<div class="col-auto"> <div>
<div class="row"> <h3 class="text-2xl">文章目录</h3>
<div class="col-auto"> </div>
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
文章目录
</h3>
</div>
</div>
<div class="row" style="padding-left: 10px"> <div>
<div class="col-auto"> @foreach (BlogHeadline level2 in _headline!.Children)
@foreach (BlogHeadline level2 in _headline!.Children)
{
<div class="row py-1">
<div class="col-auto">
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
</div>
</div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
@(level3.Title)
</a>
</div>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
@(level4.Title)
</a>
</div>
</div>
}
}
}
</div>
</div>
@if (_headline!.Children.Count == 0)
{ {
<div class="row"> <div class="py-2 pl-3">
<div class="col fst-italic"> <Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录 Text="@(level2.Title)"/>
</div>
</div> </div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="py-2 pl-6">
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
Text="@(level3.Title)"/>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="py-2 pl-9">
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
Text="@(level4.Title)"/>
</div>
}
}
} }
</div> </div>
@if (_headline!.Children.Count == 0)
{
<div class="row">
<div class="col fst-italic">
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
</div>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -123,7 +114,7 @@
return; return;
} }
if (!Contents.Essays.TryGetValue(BlogKey, out _essay)) if (!Contents.TryGetEssay(BlogKey, out _essay))
{ {
NavigationInstance.NavigateTo("/NotFound"); NavigationInstance.NavigateTo("/NotFound");
} }

View File

@@ -0,0 +1,49 @@
@page "/friends"
@using YaeBlog.Models
@inject BlogOptions Options
<PageTitle>
友链
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">
友链
</h1>
</div>
<div class="py-4">
欢迎所有人联系我添加友链!(´。✪ω✪。`)
</div>
<div class="grid grid-cols-4 g-4 p-2">
@foreach (FriendLink link in Options.Links)
{
<div>
<a href="@(link.Link)" target="_blank" class="mx-5">
<div class="flex flex-row">
<div class="basis-1/3">
<img src="@(link.AvatarImage)" alt="@($"Avatar of {link.Name}")"
class="w-full h-auto rounded-full">
</div>
<div class="flex flex-col basis-2/3 px-2">
<div class="text-lg">
@(link.Name)
</div>
<div class="text-sm italic">
@(link.Description)
</div>
</div>
</div>
</a>
</div>
}
</div>
</div>
@code {
}

View File

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

View File

@@ -4,8 +4,8 @@
啊~ 页面走丢啦~ 啊~ 页面走丢啦~
</PageTitle> </PageTitle>
<div class="container"> <div>
<h3>NotFound!</h3> <h3 class="text-3xl">NotFound!</h3>
</div> </div>
@code { @code {

View File

@@ -1,7 +1,7 @@
@page "/blog/tags/" @page "/blog/tags/"
@using System.Text.Encodings.Web @using System.Text.Encodings.Web
@using YaeBlog.Core.Abstractions @using YaeBlog.Abstraction
@using YaeBlog.Core.Models @using YaeBlog.Models
@inject IEssayContentService Contents @inject IEssayContentService Contents
@inject NavigationManager NavigationInstance @inject NavigationManager NavigationInstance
@@ -10,24 +10,22 @@
@(TagName ?? "标签") @(TagName ?? "标签")
</PageTitle> </PageTitle>
<div class="container"> <div class="flex flex-col">
<div class="row"> <div>
<div class="col"> @if (TagName is null)
@if (TagName is null) {
{ <h1 class="text-4xl">标签</h1>
<h1>标签</h1> }
} else
else {
{ <h2 class="text-2xl">@(TagName)</h2>
<h2>@(TagName)</h2> }
}
</div>
</div> </div>
<div class="row"> <div class="py-4">
<div class="col fst-italic py-4"> <span class="italic">
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶ 在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
</div> </span>
</div> </div>
@if (TagName is null) @if (TagName is null)
@@ -38,19 +36,17 @@
Contents.Tags.OrderByDescending(pair => pair.Value.Count)) Contents.Tags.OrderByDescending(pair => pair.Value.Count))
{ {
<li class="p-2"> <li class="p-2">
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)"> <div class="flex flex-row">
<div class="container fs-5"> <a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
<div class="row"> <div class="text-sky-600 text-lg">
<div class="col-auto"> # @(pair.Key.TagName)
# @(pair.Key.TagName)
</div>
<div class="col-auto tag-count">
@(pair.Value.Count)
</div>
</div> </div>
</a>
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
@(pair.Value.Count)
</div> </div>
</a> </div>
</li> </li>
} }
</ul> </ul>

View File

@@ -1,6 +0,0 @@
.tag-count {
background: var(--bs-secondary-bg);
border-radius: 5px;
padding: 0 6px;
}

View File

@@ -0,0 +1,102 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
/// <summary>
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
/// </summary>
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
{
public string Name => nameof(EssayStylesPostRenderProcessor);
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document);
BeatifyTable(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-slate-300 rounded-sm overflow-x-auto" },
{ "h2", "text-3xl font-bold py-4" },
{ "h3", "text-2xl font-bold py-3" },
{ "h4", "text-xl font-bold py-2" },
{ "h5", "text-lg font-bold py-1" },
{ "p", "p-2" },
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
{ "ul", "list-disc pl-2" }
};
private void ApplyGlobalCssStyles(IDocument document)
{
foreach ((string tag, string style) in _globalCssStyles)
{
foreach (IElement element in document.GetElementsByTagName(tag))
{
element.ClassList.Add(style);
}
}
}
private static void BeatifyTable(IDocument document)
{
foreach (IElement element in from e in document.All
where e.LocalName == "table"
select e)
{
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
// thead元素
foreach (IElement headElement in from e in element.Children
where e.LocalName == "thead"
select e)
{
headElement.ClassList.Add("bg-slate-200");
// tr in thead
foreach (IElement trElement in from e in headElement.Children
where e.LocalName == "tr"
select e)
{
trElement.ClassList.Add("border border-slate-300");
// th in tr
foreach (IElement thElement in from e in trElement.Children
where e.LocalName == "th"
select e)
{
thElement.ClassList.Add("px-4 py-1");
}
}
}
// tbody元素
foreach (IElement bodyElement in from e in element.Children
where e.LocalName == "tbody"
select e)
{
// tr in tbody
foreach (IElement trElement in from e in bodyElement.Children
where e.LocalName == "tr"
select e)
{
foreach (IElement tdElement in from e in trElement.Children
where e.LocalName == "td"
select e)
{
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
}
}
}
}
}
}

View File

@@ -1,13 +1,12 @@
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using Microsoft.Extensions.Logging; using YaeBlog.Abstraction;
using YaeBlog.Core.Abstractions; using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Processors; namespace YaeBlog.Processors;
public class HeadlinePostRenderProcessor( public class HeadlinePostRenderProcessor(
IConfiguration angleConfiguration, AngleSharp.IConfiguration angleConfiguration,
IEssayContentService essayContentService, IEssayContentService essayContentService,
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
{ {

View File

@@ -1,24 +1,21 @@
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions; using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions; using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Processors; namespace YaeBlog.Processors;
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger, public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options) IOptions<BlogOptions> options)
: IPostRenderProcessor : IPostRenderProcessor
{ {
private static readonly IConfiguration s_configuration = Configuration.Default;
private readonly BlogOptions _options = options.Value; private readonly BlogOptions _options = options.Value;
public async Task<BlogEssay> ProcessAsync(BlogEssay essay) public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{ {
BrowsingContext context = new(s_configuration); BrowsingContext context = new(Configuration.Default);
IDocument html = await context.OpenAsync( IDocument html = await context.OpenAsync(
req => req.Content(essay.HtmlContent)); req => req.Content(essay.HtmlContent));
@@ -34,7 +31,6 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
logger.LogDebug("Found image link: '{}'", attr.Value); logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName); attr.Value = GenerateImageLink(attr.Value, essay.FileName);
} }
element.ClassList.Add("essay-image");
} }
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml); return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
} }

View File

@@ -1,13 +1,4 @@
using System.CommandLine;
using YaeBlog.Commands; using YaeBlog.Commands;
RootCommand rootCommand = new("YaeBlog CLI"); YaeBlogCommand command = new();
await command.RunAsync(args);
rootCommand.AddServeCommand();
rootCommand.AddNewCommand();
rootCommand.AddListCommand();
rootCommand.AddWatchCommand();
rootCommand.AddScanCommand();
rootCommand.AddPublishCommand();
await rootCommand.InvokeAsync(args);

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options; using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Services; namespace YaeBlog.Services;
public sealed class BlogChangeWatcher : IDisposable public sealed class BlogChangeWatcher : IDisposable
{ {

View File

@@ -1,7 +1,4 @@
using Microsoft.Extensions.Hosting; namespace YaeBlog.Services;
using Microsoft.Extensions.Logging;
namespace YaeBlog.Core.Services;
public class BlogHostedService( public class BlogHostedService(
ILogger<BlogHostedService> logger, ILogger<BlogHostedService> logger,
@@ -9,14 +6,12 @@ public class BlogHostedService(
{ {
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
logger.LogInformation("Welcome to YaeBlog!"); logger.LogInformation("Failed to load cache, render essays.");
await rendererService.RenderAsync(); await rendererService.RenderAsync();
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -0,0 +1,41 @@
using YaeBlog.Abstraction;
namespace YaeBlog.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Hot reload is starting...");
logger.LogInformation("Change essays will lead to hot reload!");
logger.LogInformation("HINT: draft essays will be included.");
await rendererService.RenderAsync(true);
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task FileWatchTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
logger.LogInformation("Watching file changes...");
string? changeFile = await watcher.WaitForChange(token);
if (changeFile is null)
{
logger.LogInformation("File watcher is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear();
await rendererService.RenderAsync(true);
}
}
}

View File

@@ -1,23 +1,36 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using YaeBlog.Core.Abstractions; using YaeBlog.Abstraction;
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Services; namespace YaeBlog.Services;
public class EssayContentService : IEssayContentService public class EssayContentService : IEssayContentService
{ {
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new(); private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
private readonly List<BlogEssay> _sortedEssays = [];
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = []; private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new(); private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay); public bool TryAdd(BlogEssay essay)
{
_sortedEssays.Add(essay);
return _essays.TryAdd(essay.FileName, essay);
}
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline); public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays; public IEnumerable<BlogEssay> Essays => _sortedEssays;
public int Count => _sortedEssays.Count;
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
{
return _essays.TryGetValue(filename, out essay);
}
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags; public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;

View File

@@ -1,14 +1,13 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions; using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions; using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models; using YaeBlog.Models;
using YamlDotNet.Core; using YamlDotNet.Core;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace YaeBlog.Core.Services; namespace YaeBlog.Services;
public partial class EssayScanService( public partial class EssayScanService(
ISerializer yamlSerializer, ISerializer yamlSerializer,
@@ -23,8 +22,8 @@ public partial class EssayScanService(
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts); ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents( return new BlogContents(
await ScanContentsInternal(drafts), await ScanContentsInternal(drafts, true),
await ScanContentsInternal(posts)); await ScanContentsInternal(posts, false));
} }
public async Task SaveBlogContent(BlogContent content, bool isDraft = true) public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
@@ -61,7 +60,7 @@ public partial class EssayScanService(
} }
} }
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory) private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
{ {
// 扫描以md结果的但是不是隐藏文件的文件 // 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles() IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
@@ -98,7 +97,8 @@ public partial class EssayScanService(
contents.Add(new BlogContent contents.Add(new BlogContent
{ {
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..] FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
}); });
} }
catch (YamlException e) catch (YamlException e)
@@ -133,13 +133,12 @@ public partial class EssayScanService(
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents, private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
DirectoryInfo root) DirectoryInfo root)
{ {
Regex imageRegex = ImageRegex();
ConcurrentBag<FileInfo> unusedImage = []; ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = []; ConcurrentBag<FileInfo> notFoundImage = [];
Parallel.ForEach(contents, content => Parallel.ForEach(contents, content =>
{ {
MatchCollection result = imageRegex.Matches(content.FileContent); MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName)); DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
Dictionary<string, bool> usedDictionary; Dictionary<string, bool> usedDictionary;
@@ -182,7 +181,7 @@ public partial class EssayScanService(
} }
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")] [GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex ImageRegex(); private static partial Regex ImagePattern { get; }
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts) private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
{ {

View File

@@ -3,12 +3,11 @@ using System.Diagnostics;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Markdig; using Markdig;
using Microsoft.Extensions.Logging; using YaeBlog.Abstraction;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Exceptions; using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models; using YaeBlog.Models;
namespace YaeBlog.Core.Services; namespace YaeBlog.Services;
public partial class RendererService( public partial class RendererService(
ILogger<RendererService> logger, ILogger<RendererService> logger,
@@ -22,40 +21,43 @@ public partial class RendererService(
private readonly List<IPostRenderProcessor> _postRenderProcessors = []; private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
public async Task RenderAsync() public async Task RenderAsync(bool includeDrafts = false)
{ {
_stopwatch.Start(); _stopwatch.Start();
logger.LogInformation("Render essays start."); logger.LogInformation("Render essays start.");
BlogContents contents = await essayScanService.ScanContents(); BlogContents contents = await essayScanService.ScanContents();
List<BlogContent> posts = contents.Posts.ToList(); List<BlogContent> posts = contents.Posts.ToList();
if (includeDrafts)
{
posts.AddRange(contents.Drafts);
}
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts); IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
List<BlogEssay> essays = []; List<BlogEssay> essays = [];
await Task.Run(() => foreach (BlogContent content in preProcessedContents)
{ {
foreach (BlogContent content in preProcessedContents) uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{ {
uint wordCount = GetWordCount(content); Title = content.Metadata.Title ?? content.FileName,
BlogEssay essay = new() FileName = content.FileName,
{ IsDraft = content.IsDraft,
Title = content.Metadata.Title ?? content.FileName, Description = GetDescription(content),
FileName = content.FileName, WordCount = wordCount,
Description = GetDescription(content), ReadTime = CalculateReadTime(wordCount),
WordCount = wordCount, PublishTime = content.Metadata.Date ?? DateTime.Now,
ReadTime = CalculateReadTime(wordCount), HtmlContent = content.FileContent
PublishTime = content.Metadata.Date ?? DateTime.Now, };
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null) if (content.Metadata.Tags is not null)
{ {
essay.Tags.AddRange(content.Metadata.Tags); essay.Tags.AddRange(content.Metadata.Tags);
}
essays.Add(essay);
} }
});
essays.Add(essay);
}
ConcurrentBag<BlogEssay> postProcessEssays = []; ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay => Parallel.ForEach(essays, essay =>
@@ -67,7 +69,16 @@ public partial class RendererService(
logger.LogDebug("Render markdown file {}.", newEssay); logger.LogDebug("Render markdown file {}.", newEssay);
}); });
await PostProcess(postProcessEssays); IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
foreach (BlogEssay essay in postProcessedEssays)
{
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
}
}
essayContentService.RefreshTags(); essayContentService.RefreshTags();
_stopwatch.Stop(); _stopwatch.Stop();
@@ -118,8 +129,10 @@ public partial class RendererService(
return processedContents; return processedContents;
} }
private async Task PostProcess(IEnumerable<BlogEssay> essays) private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
{ {
ConcurrentBag<BlogEssay> processedContents = [];
await Parallel.ForEachAsync(essays, async (essay, _) => await Parallel.ForEachAsync(essays, async (essay, _) =>
{ {
foreach (IPostRenderProcessor processor in _postRenderProcessors) foreach (IPostRenderProcessor processor in _postRenderProcessors)
@@ -127,16 +140,18 @@ public partial class RendererService(
essay = await processor.ProcessAsync(essay); essay = await processor.ProcessAsync(essay);
} }
if (!essayContentService.TryAdd(essay)) processedContents.Add(essay);
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
}); });
List<BlogEssay> result = processedContents.ToList();
result.Sort();
return result;
} }
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")] [GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
private static partial Regex DescriptionPattern(); // private static partial Regex DescriptionPattern();
private static partial Regex DescriptionPattern { get; }
private string GetDescription(BlogContent content) private string GetDescription(BlogContent content)
{ {
@@ -152,7 +167,7 @@ public partial class RendererService(
} }
string rawContent = content.FileContent[..pos]; string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern().Matches(rawContent); MatchCollection matches = DescriptionPattern.Matches(rawContent);
StringBuilder builder = new(); StringBuilder builder = new();
foreach (Match match in matches) foreach (Match match in matches)

View File

@@ -1,18 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
</ItemGroup> <PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
</ItemGroup>
<ItemGroup> <PropertyGroup>
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" /> <TargetFramework>net9.0</TargetFramework>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <Nullable>enable</Nullable>
</ItemGroup> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup> <Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
<TargetFramework>net8.0</TargetFramework> <Message Importance="low" Text="Ensure pnpm is installed..."/>
<Nullable>enable</Nullable> <Exec Command="pnpm --version" ContinueOnError="true">
<ImplicitUsings>enable</ImplicitUsings> <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</PropertyGroup> </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">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
</Target>
</Project> </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 BlazorBootstrap
@using YaeBlog @using YaeBlog
@using YaeBlog.Components @using YaeBlog.Components

View File

@@ -6,6 +6,10 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Tailwind": {
"InputFile": "wwwroot/input.css",
"OutputFile": "wwwroot/output.css"
},
"Blog": { "Blog": {
"Root": "source", "Root": "source",
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。", "Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",

12
YaeBlog/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.4.16"
}
}

836
YaeBlog/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,836 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
tailwindcss:
specifier: ^3.4.16
version: 3.4.16
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-core-module@2.16.0:
resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.0.1:
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resolve@1.22.9:
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
hasBin: true
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwindcss@3.4.16:
resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yaml@2.6.1:
resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
engines: {node: '>= 14'}
hasBin: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@pkgjs/parseargs@0.11.0':
optional: true
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@5.0.2: {}
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
camelcase-css@2.0.1: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
commander@4.1.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
cssesc@3.0.0: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.17.1:
dependencies:
reusify: 1.0.4
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-core-module@2.16.0:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
isexe@2.0.0: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jiti@1.21.6: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lru-cache@10.4.3: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
minipass@7.1.2: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.8: {}
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
picocolors@1.1.1: {}
picomatch@2.3.1: {}
pify@2.3.0: {}
pirates@4.0.6: {}
postcss-import@15.1.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.9
postcss-js@4.0.1(postcss@8.4.49):
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.49):
dependencies:
lilconfig: 3.1.3
yaml: 2.6.1
optionalDependencies:
postcss: 8.4.49
postcss-nested@6.2.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.4.49:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
queue-microtask@1.2.3: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
resolve@1.22.9:
dependencies:
is-core-module: 2.16.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.0.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
commander: 4.1.1
glob: 10.4.5
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.6
ts-interface-checker: 0.1.13
supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@3.4.16:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)
postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
resolve: 1.22.9
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-interface-checker@0.1.13: {}
util-deprecate@1.0.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
yaml@2.6.1: {}

View File

@@ -0,0 +1,89 @@
---
title: 2024年年终总结
date: 2025-01-16T17:15:05.8634370+08:00
tags:
- 杂谈
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
所以年终总结再次跳票,红豆泥私密马赛!
<!--more-->
### 压力
本年度的第一个关键词,我会选择压力。这一年总是被不同的压力笼罩着,先是有形的压力,然后是无形的压力,在不同的时间阶段有着不同的来源。
1月份起始的两周就是大三学年秋季学期的期末考试周而鄙人在下不才我在本学期面临着计算机科学四幻神的考验——老师不知所云之操作系统、抽象概念无法理解之编译原理、全英语授课之数据库系统原理和智商不够无法战胜之算法导论。挣扎在保研线上的我刚刚被上一学期的离散数学的~~75分~~74分和数据结构的79分拷打面对着如此沉重的考试压力加起来一共12学分呢可耻的失眠了。
过完年回来的三月份就是同论文奋斗的一个月。虽然只是一篇6页的EI检索论文但对于一个**纯洁**的本科生来说还是有点太困难了。这个过程就像是你先拉了一坨大的,然后在上面细细的涂上巧克力,在最后发表的过程中,需要在众人的面前大嚼这一坨东西,并且称赞“真是一道美食啊”!还没有开始的学术生涯就已经留下永恒的污点力(悲)。
搞完论文的四月和五月则是和大作业搏斗的两个月。首先是无法战胜的“编译原理课程设计”内容是设计一个Pascal-S到C语言的源到源编译器。这一大作业的主要压力来源是大作业本身的难度直到最后提交的时候全部95个测试点也没有能够完全通过然而其他人在祖传代码上缝缝补补却过来哭。虽然考虑到我们是全手写的编译器没有使用任何的编译器构建工具提出的解决方案也称不上是墨守成规老师给了我一个还算是可以的分数算是压力中的小小慰藉。
然后是风波不断的软件工程大作业明明只是一个相对简单的Web前后端开发但是我们前后进行了三次验收才通过一直拖到了学期的第16周。老师设计的联合验收制度给我们结结实实的上了一课要求联合验收小组的不同前后端需要能够任意组合使用导致我们为了适配另外一组的逻辑几乎是把核心代码写了两遍。虽然我不喜欢在背后攻击别人但是我不得不说这一年中最有压力的时刻往往不是自己的事情搞不定时而是看着别人搞砸事情你却无能为力的时候。
这两个月还夹杂这一个意义不明的专业实习,明明是计算机科学与技术专业的牛马,为什么会被中兴通讯的老师培训通信项目的项目管理?
应付完上面这些杂七杂八的内容,便是本科生生涯中的最后三场考试:人称计算机领域的政治之《软件工程》,通信领域科普课程之《现代交换原理》和永远的神之《计算机系统结构》。
不得不说《软件工程》,~~或者人们常说的肖概~~确实不愧于计算机领域的政治之称。毕竟政治的主要课题就是研究如何组织和动员人群以完成一个特定的目标,《软件工程》不过是将人员限制为了软件的开发人员,领域限制为了软件开发领域,基本的道理还是相通的。
《现代交换原理》则是一门在现有的课程体系下非常尴尬的一门课程,显然这门课的保留还是为了凸显“计算机+通信”的学科特色,但是大量前置知识的缺失和同其他课程的脱节使得这门课就显得非常的“脱节”。而且相对来说,通信技术的发展速度远远不如互联网的迭代技术,这门课也被同学们戏称为“古代交换原理”。令人最难受的,虽然知识古代,但是却一点都不简单,很多内容只能说是听了个概念,幸好最后的考试不难,靠死记硬背通过了考试。
《计算机系统结构》就是核心课中的核心课了。课程内容和《计算机组成原理》衔接的非常紧密,~~虽然我组成原理就学的很垃圾~~主要围绕着如何最大限度的并行化运行程序进行从指令级的并行一直到多机并行可以说是压力最大的一门考试。在准备的过程中做了很多套往年题博客上也发布了一部分的复习笔记最终幸好低空飞过。唯一的吐槽是实验什么时候可以从MIPS改成为RISC-V呢。
三门课的考试一结束,这些死线明确的、有形的压力便消失了,但是无形的压力——对于是否能保研的焦虑——便笼罩下来。
7月和8月都是在这种不安和恐慌中度过这种氛围在9月份保研名单出炉之前达到了顶峰。保研的流程开始之后则是通知推着人走各种交材料各种准备答辩各种等待公示直到最后的保研名单出炉。
不过现在回想起来,最后名单出炉,获得保研资格,复试通过之后,并没有一种如释重负的感觉,或者说终于实现了既定目标的快感。反而是一种“啊,结束了”的空落感,只想回去睡一觉。
然后新的~~风暴~~压力已经出现,在度过一个短短的国庆假期之后便正式进组,作为一个研究生的社畜生涯就此开始。
### 经历
虽然2024年的第一个关键词已经选择为“压力”但是众所周知高压锅里往往能压出好吃的。人也是这样。所以我将2024年的第二个关键词定为“经历”人生如逆旅我亦是行人各式各样的经历便是风格迥异的景点。
人生第一篇学术论文的撰写和发表无疑是今年最难忘的经历。虽然我在前面称之为“学术生涯上的污点”,但是污点也好过一片空白不是,还非常的引人夺目。而且这是一个完整的撰写-发表流程,从开始的选题、实验、撰写、投稿,到最后的接受、提交、发表、报销等等数个环节我均参加。这个过程不仅让我对于学术论文的诞生流程有力较为清晰的认识,也对学校的各种发表和报销流程有了深入的了解。
两个大作业编译原理课程设计和软件工程大作业也是非常难忘的经历。这两个项目的代码都已经整理好开源在Github上了。前者代表了目前我软件开发的最高水平而后者则是我本科阶段唯一一个差点失败的软件开发项目。这种冰火两重天的对比实在是很难令人忘记。
这两个项目中的收获有非常技术性的。相较于2023年面对各种大作业时的略显底气不足这次我在各种技术栈的选择上更加游刃有余选择了完全倒向.NET和React摈弃了之前的Java和Vue。各类现代软件开发技术也得到了充分的应用例如由Gitea Actions驱动的DevOps实践完全基于合并请求的多人协作流程。事实证明这些协作流程确实在一定程度上加速了项目的开发。
但是,“软件工程里没有银弹”,先进技术的堆叠并不能保证软件项目成功。虽然我这里~~自吹自擂~~有非常多新技术的帮助,软件工程大作业的差点失败的确说明了软件工程实际上还是人的工程,猪队友永远比凶恶的敌人更可怕。当然也不能将所有的锅都扔给别人,我在项目失控的过程中也没有能够采取有力的措施挽救整个项目,~~负有不可推卸的领导责任~~。
今年最后一个难忘的经历便是去横店镇参加CNCC 2024也单独出过[博客](https://rrricardo.top/blog/essays/cncc-2024)。虽然之前学术论文发表的过程中也是在学术会议上做过口头报告,不过是线上参加的,并没有特别的实感。现在线下参加,也不需要自己上去发表,顿感旅游真好玩,~~也有可能是因为CNCC比较水~~。
### 匆匆
2024年的第三个关键词我想定为”匆匆“虽然想找一个更加”有文化“的词汇奈何自己的文化造纸实在不够故定为”匆匆“。
可2024年确实是非常忙碌的一年现在回想起来几乎每一个月都是在为了某一件特定的事情而奔走着。还记得在新年伊始的时间里我还制订了各种各样的读书计划和补番计划现在看来定计划的目的不是为了实现而是为了安心。
不过匆匆之中还是读了几本书。首先是久负盛名的《置身事内——中国政府与经济发展》,这本书的开篇即言:“这本书是写给大学生和对经济话题感兴趣的读者”,细读下来也确实如此。然后是一本我从小便着迷的二战军史相关话题《美国陷阱:橙色计划始末》,其中若干的政治与军事细节之于我不过是走马观花,不过其中表达出的长期战略实在令人敬佩。
至于补番计划我则是表现出了同电子ED一样的症状对于新番没有兴趣对于补早就下载安装好的老番更是兴趣缺缺。反倒是电视剧由于12月韩国的惊天一变我又重新下载了《第五共和国》
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.png)
### 未来
> 定计划的目的不是为了实现,而是为了安心。
站在年关已经可以预见到2025年将会是更为繁忙的一年从一月份到十月份都已经有了或多或少的安排现在无法多言只能希望都能有良好的结果。
还是多说点可以说的罢。
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目一整本上总结下来也能有超过50本其中不乏超过一千页的大部头说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍
- 陆风,《光变:一个企业及其工业史》
- 吴军,《浪潮之巅》
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。

BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,875 @@
---
title: 异构编程模型的昨天、今天与明天
date: 2024-11-04T22:20:41.2571467+08:00
tags:
- 编译原理
- 组会汇报
---
随着摩尔定律的逐渐失效将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
<!--more-->
## 异构并行编程模型概述
异构并行编程模型是沟通上层应用和下层异构系统之间的桥梁,其的设计需要处理好下面五个问题:任务划分、任务映射、数据分布、同步和通信。
### 异构并行编程模型面临的技术挑战
异构并行编程模型面临的技术挑战主要是由两方面带来的:首先异构架构本身为编程模型带来的挑战,其次是上层应用带来的挑战。
异构并行编程模型需要解决的一个重要问题就是为上层应用的程序员提供一个合理的硬件平台抽象,使得其在编程是可以充分释放异构资源带来的计算能力,同时不需要考虑复杂的硬件细节。但是异构系统中各个计算设备在内部体系结构、设备间互联架构上的复杂性和多样性使得异构并行编程模型在提供建立统一的平台抽象上遇到了巨大的困难。具体来说,主要体现下述三点。
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中比如多核CPU中虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统例如CPU、GPU和FPGA组成的异构系统不同设备的微架构具有本质差异其并行计算的模式和能力都完全不同设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点但是却给异构并行计算模型的设计带来了巨大的困难。
![9eb06d8be92ddef3db33e040163c67a7.png](./heterogeneous-programming-model/9eb06d8be92ddef3db33e040163c67a7.png)
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中CPU片内的存储是对于软件透明的缓存架构在片外则是一个共享内存模型因此在这类系统中数据仅可能分布在片外的共享存储中具有存储位置单一的特点也不需要进行显式的通信操作。但是在异构系统中不仅在单个加速设备内部可能有软件可分配的快速局部存储设备之间的连接方式差异也很大。目前大多个加速设备都是通过PCIe总线的方式同CPU进行连接这使得加速设备无法通过和CPU相同的方式完成地址映射存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置不仅使得编程模型的数据分布问题变得十分复杂设备间的通信文件也可能需要显式进行。
![eab553f9e30d8d866a1ddd201b5e4c85.png](./heterogeneous-programming-model/eab553f9e30d8d866a1ddd201b5e4c85.png)
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题在异构系统中数据可能分布在不同位置的条件下同步操作需要在众多的位置上保证共享数据的一致性这使得同步操作的范围变得十分复杂。同时在一些特定的加速设备中例如GPU可能还会有局部的硬件同步机制这更加提高了在异构系统的同步操作的设计和实现难度。
上层应用带来的挑战主要集中在缺少良好的异构抽象和统一的编程接口上。例如在CPU上进行编程时通常使用Java、Python等高级语言而在进行GPU编程时则使用各种C语言的变体其中的核心计算函数Kernel Function则通常只支持一个C语言的子集而FPGA这些硬件设备又需要使用硬件描述语言进行编程。
### 异构并行编程接口和编译/运行时支持机制
异构并行编程接口是编程模型暴露给程序员使用的界面,它既需要为程序员提供合理的异构架构抽象,使程序员可以对异构计算资源加以合理利用,又需要保证接口的易用性,避免程序员陷入复杂的硬件细节中。编译/运行时系统是异构并行编程模型的软件工具层,它将程序员编写的加速器代码编译为可执行文件,并通过运行时系统完成任务的加速执行。
在任务划分、任务映射、数据分布、通信和同步这五个关键任务中,程序员往往只需要关注所编写应用程序的特点,因此显示的任务划分机制对应程序员来说可能是必不可少的,而其他的数据分布、通信和同步等任务只会加剧程序员开发应用程序的负担,但是这些任务通过接口暴露出来也为后续进行深度优化提供了空间。异构编译/运行时支持机制的主要任务就是保障任务映射,即明确任务将具体在哪个设备或者计算单元上执行,以何种顺序执行,同时在当程序员没有显式处理数据分布、通信和同步问题时进行自动处理并进行全系统级别的优化。
## 异构并行编程接口的研究
异构并行编程接口一般可以划分成两类新设计的异构编程语言和现有语言的异构并行扩展。对于现有语言进行的异构并行扩展一般通过库Library或者是制导Directive的方法进行。
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
![83ee1d254d638536d0fb4197ff63e758.png](./heterogeneous-programming-model/83ee1d254d638536d0fb4197ff63e758.png)
### 异构任务划分机制研究
在同构的并行编程语言中,并行编程接口需要提供一种面向单一设备的并行任务划分机制,这种并行任务划分机制有**任务并行**和**数据并行**等。数据并行指的是对源集合或者数组的元素同时执行相同操作的场景,一个数据并行的典型例子如下面计算两个矩阵的乘积:
```csharp
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
// A basic matrix multiplication.
// Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, i =>
{
for (int j = 0; j < matBCols; j++)
{
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] = temp;
}
}); // Parallel.For
}
```
任务并行的概念一般是指一个或者多个独立的任务同时运行,是一种比数据并行更高的抽象层级。
```csharp
public class Result
{
public static void Main()
{
Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
var results = new Double[taskArray.Length];
Double sum = 0;
for (int i = 0; i < taskArray.Length; i++) {
results[i] = taskArray[i].Result;
Console.Write("{0:N1} {1}", results[i],
i == taskArray.Length - 1 ? "= " : "+ ");
sum += results[i];
}
Console.WriteLine("{0:N1}", sum);
}
private static Double DoComputation(Double start)
{
Double sum = 0;
for (var value = start; value <= start + 10; value += .1)
sum += value;
return sum;
}
}
```
不论是高级或者是低级的异构并行编程接口都需要提供一种异构并行任务的划分机制。同传统的同构并行编程接口只需要提供面向单一设备的并行任务划分机制不同,异构并行编程接口还需要提供描述任务在不同设备间分配的机制。因此,异构并行编程接口的任务划分机制需要包括两个维度:异构特征描述和并行性表达两个维度。
一种典型异构任务划分机制是由`BrookGPU`编程语言提出的。该编程语言采用特殊函数`kernel`标记需要在GPU上执行的代码段`kernel`函数必须作用在流上。这个流Stream在并行性表达方面表达了细粒度的数据并行。后面的OpenCL和CUDA在C语言的基础上提供了异构扩展这种扩展的任务划分机制和`BrookGPU`的十分类似。但是OpenCL和CUDA在并行行表达的层面上支持了SPMD计算模型这个`BrookGPU`编程语言采用的流式编程模型不同。OpenCL在数据并行之外还提供了任务并行的机制。
`Lime`则是一门完全新的异构并行编程语言,通过语言结构为程序提供了丰富的操作符用于任务的划分。同时在异构特征描述方面,`Lime`也没有任何显式的接口,同`BrookGPU`等一系列需要手动指定设备代码段的编程模型完全不同,这也是因为`Lime`采用了基于任务的并行划分方式。同时在任务并行之外,`Lime`也通过`MapReduce`操作符提供了中粒度的数据并行机制。
`Merge`还是一门新的异构并行编程语言基于Intel提出的异构多核多线程系统编程环境`EXOCHI`。在并行性表达上,`Merge`使用`MapReduce`思想。而在异构特征描述方面,`Merge`则提供了一种成为平台变体Target Variant的机制程序员需要为异构系统中的不同设备提供不同版本的代码实现。
### 异构数据分布和通信机制
异构数据分布和通信机制主要分成显式和隐式两种,其中`OpenCL/CUDA`等使用了显式的数据分布的通信机制,为程序员提供了丰富的异构数据分布与通信接口。而`Lime``Merge`等语言则使用了隐式机制,运行时系统代为完成这部分的工作。
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制例如GPU的全局内存访存合并机制这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法例如`CUDA-lite`这个运行时允许程序元在CUDA程序中加入简单的制导语句数据分布的相关工作使用`CUDA-lite`的运行时系统完成降低了CUDA程序的编写难度。
![628804b3fe95d39013ff55ae84516d14.png](./heterogeneous-programming-model/Screenshot_20241016_214139.png)
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
- 异构数据分布和通知机制在传统并行编程模型的基础上增加了"设备内数据多层分布"和"设备间显式通信"接口;
- 异构同步机制在传统并行编程模型的基础上增加了"设备间同步"的机制。
## 异步编译/运行时的研究
### 异构任务映射机制
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
![](./heterogeneous-programming-model/Screenshot_20241016_214939.png)
### 异构编译/运行时优化
与同构平台类似,异构编译/运行时优化有两条路径:
- 平台相关的优化,其核心是挖掘系统的硬件优势;
- 应用导向的优化,其核心是实施特定领域的优化并解决应用的输入敏感问题。
在平台优化上,异构系统通常具有复杂且多变的硬件结构, 因此程序员仅负责编写正确实现程序功能的代码、由编译/运行时系统完成面向加速设备结构特点的优化是比较合理的方式, 这样也有利于程序在不同异构系统中获得良好的性能。
## 异构并行编程模型的研究方向
- 面向普通用户的异构并行编程接口
- 面向多种加速设备的异构编译/运行时优化
- 面向异构集群的异构并行编程模型
## 异构并行编程模型调研
为了调研各个异构并行编程模型的不同,使用不同的编程模型实现一个通用矩阵乘法算法,并通过计算`2048*2048`大小的矩阵乘法时间来比较各个模型的加速效果。
辅助计算的`Calculator`类如下所示:
```cpp
#define MATRIX_SIZE 2048
#include <chrono>
#include <functional>
#include <iostream>
#include <random>
class Calculator
{
public:
static void validate_matrix(const std::vector<std::vector<int>>& a, const std::vector<std::vector<int>>& b)
{
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
if (a[i][j] != b[i][j])
{
std::cout << "Two matrix must be the same." << std::endl;
}
}
}
}
std::vector<std::vector<int>> calculate(const std::string& method,
const std::function<std::vector<std::vector<int>>(
const std::vector<std::vector<int>>&,
const std::vector<std::vector<int>>&)>& calculator) const
{
std::cout << "Calculator '" << method << "' start." << std::endl;
const auto start_time = std::chrono::high_resolution_clock::now();
const auto result = calculator(a, b);
const auto end_time = std::chrono::high_resolution_clock::now();
const auto span = end_time - start_time;
std::cout << "Calculator '" << method << "' end, time is " << std::chrono::duration_cast<
std::chrono::milliseconds>(span).count() << " ms." << std::endl;
return result;
}
private:
std::vector<std::vector<int>> a = initialize_matrix();
std::vector<std::vector<int>> b = initialize_matrix();
static std::vector<std::vector<int>> initialize_matrix()
{
std::vector<std::vector<int>> matrix;
std::random_device seed;
std::ranlux48 engine(seed());
std::uniform_int_distribution distribute(0, 102400);
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector row(MATRIX_SIZE, 0);
for (int j = 0; j < MATRIX_SIZE; j++)
{
row[j] = distribute(engine);
}
matrix.emplace_back(row);
}
return matrix;
}
}
```
作为对比一个使用CPU单线程计算的例子如下
```cpp
inline std::vector<int> cpuMatrixMultiply(
const std::vector<int>& a,
const std::vector<int>& b)
{
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
}
result[i * MATRIX_SIZE + j] = temp;
}
}
return result;
}
```
### OpenMP
OpenMP是`Open MultiProcessing`的缩写是一个使用编译器制导Directives来进行共享内存平行计算的框架在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口让程序员能够充分释放多核和多处理器系统性能。
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型但是第一OpenMP作为一个经典的并行编程框架研究价值还是非常高的其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速似乎也能算是一个异构并行编程模型。
使用OpenMP进行并行加速的代码如下
```C++
std::vector<std::vector<int>> omp_matrix_multiply(
const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
#pragma omp parallel for shared(a, b, result) default(none)
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += a[i][k] * b[k][j];
}
result[i][j] = temp;
}
}
return result;
}
```
加速的结果如下:
| 运行方法 | 运行时间 | 比率 |
| ------------ | -------- | ---- |
| SingleThread | 21685 ms | 1.00 |
| OpenMP | 2268 ms | 0.10 |
### CUDA
CUDA是NVIDIA公司设计的一套GPU加速应用程序的编程框架是将NVIDIA GPU作为GPGPU使用的官方解决方案。
CUDA的异构编程接口是经典的Device-Host两元结构程序员需要编写两部分代码Device代码是实际运行在GPU上的逻辑部分而Host代码则负责将数据从内存中复制到GPU上的显存和复制回来等准备工作并负责以特定的参数调用GPU上的Device代码。
一个使用GPU的矩阵乘法程序如下所示
```c++
template <typename T>
void check(T result, char const* const func, const char* const file, int const line)
{
if (result)
{
std::cerr << "CUDA error at " << file << ":" << line << "code = " << result << "(" << cudaGetErrorString(result)
<< ") '" << func << "'" << std::endl;
exit(EXIT_FAILURE);
}
}
#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__)
__global__ void cudaMatrixMultiply(const int* a, const int* b, int* c)
{
const int totalSize = MATRIX_SIZE * MATRIX_SIZE;
int threadId = threadIdx.x + blockIdx.x * blockDim.x;
while (threadId < totalSize)
{
const int x = threadId / MATRIX_SIZE;
const int y = threadId % MATRIX_SIZE;
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[MATRIX_SIZE * x + y] = result;
threadId += gridDim.x * blockDim.x;
}
__syncthreads();
}
std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
constexpr unsigned int matrixSize = sizeof(int) * MATRIX_SIZE * MATRIX_SIZE;
// 在host上为a, b, c分配空间
int *hostA, *hostB, *hostC;
checkCudaErrors(cudaMallocHost(&hostA, matrixSize));
checkCudaErrors(cudaMallocHost(&hostB, matrixSize));
checkCudaErrors(cudaMallocHost(&hostC, matrixSize));
// 将数据复制到host上
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
hostA[i * MATRIX_SIZE + j] = a[i][j];
hostB[i * MATRIX_SIZE + j] = b[i][j];
}
}
// 在device上分配空间
int *deviceA, *deviceB, *deviceC;
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceA), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceB), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceC), matrixSize));
cudaStream_t stream;
checkCudaErrors(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking));
// 将数据从host复制到device
checkCudaErrors(cudaMemcpyAsync(deviceA, hostA, matrixSize, cudaMemcpyHostToDevice, stream));
checkCudaErrors(cudaMemcpyAsync(deviceB, hostB, matrixSize, cudaMemcpyHostToDevice, stream));
constexpr int threadSize = 32 * 32;
constexpr int grid = MATRIX_SIZE * MATRIX_SIZE / threadSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaStreamSynchronize(stream);
cudaEventRecord(start, stream);
cudaMatrixMultiply<<<grid, threadSize, 0, stream>>>(deviceA, deviceB, deviceC);
cudaEventRecord(stop, stream);
cudaEventSynchronize(stop);
float cudaRunTime = 0;
cudaEventElapsedTime(&cudaRunTime, start, stop);
std::cout << "CUDA actual run time is " << cudaRunTime << " ms" << std::endl;
// 将数据从device复制到host
cudaMemcpyAsync(hostC, deviceC, matrixSize, cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
std::vector<std::vector<int>> result;
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector<int> row;
for (int j = 0; j < MATRIX_SIZE; j++)
{
row.emplace_back(hostC[i * MATRIX_SIZE + j]);
}
result.emplace_back(row);
}
// 释放内存
cudaFreeHost(hostA);
cudaFreeHost(hostB);
cudaFreeHost(hostC);
cudaFree(deviceA);
cudaFree(deviceB);
cudaFree(deviceC);
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaStreamDestroy(stream);
return result;
}
```
加速的结果如下所示:
| 类型 | 运行时间 | 比率 |
| ---- | -------- | ----- |
| CPU | 22059ms | 1.000 |
| GPU | 32ms | 0.001 |
需要注意的是上面编写的CUDA代码还没有完全利用GPU的并行计算能力。
> 这里我遇到的一个非常奇怪的问题是相同的CPU计算代码在运行完OpenMP测试之后再运行就会比在CUDA运行之后再运行慢上一倍而且可复现性极高。这里我给出一个典型的运行时间比较CUDA计算的时间是323毫秒CUDA之后的CPU计算时间是38602毫秒OpenMP的计算时间是8721毫秒OpenMP之后的计算时间是76598毫秒。
>
> 针对这个比较奇怪的情况我觉得可以做出三个猜想:
>
> - 考虑到我使用的CPU是Intel的i7-13600K这是一个有性能核和效率核组成的大小核异构系统可能在两次计算的过程中调度到了不同的核上
> - 在进行CUDA计算的过程中提高了缓存的亲和性
> - 在测试中没有设计热身Warm up的过程而在CUDA计算的过程中部分起到了这个作用。
>
> 针对上面三个猜测做个两个实验:
>
> - 首先是换了一台没有大小核异构设计的计算机进行实验发现这下两次使用CPU计算的时间差异不大
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
>
> 综上所述可以认为此现象和异构CPU之间存在着明显的关联但是缺乏直接证据。
>
> 在我们调整了矩阵的数据布局之后这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的而在修改为使用一维数组也就是现在提供的代码之后相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
### OpenCL
OpenCL是目前最为典型、发展最好的异构并行编程模型毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
![image-20241020142938110](./heterogeneous-programming-model/image-20241020142938110.png)
从上图的OpenCL工作原理中可以看出OpenCL和CUDA类似也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写编译之后在CPU上执行而设备代码使用一个特定的C语言方言OpenCL C进行编写这个方言针对并行编程进行了扩展并提供了一系列封装好的数学计算函数。
设备代码上的编译方法有两种在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码离线编译则有两种表现形式第一种是在线编译的扩展版由驱动编译得到的可执行程序可以通过API获取并保存下来当下一需要在同一设备上调用时可以直接使用而不是再次编译第二种则是完全独立的编译过程在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
![image-20241020155656219](./heterogeneous-programming-model/image-20241020155656219.png)
在提出离线编译之后为了让驱动编译好的二进制文件可以在不同的设备之间复用同时也是支持更为丰富的编译器生态系统OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
下面是一个使用OpenCL进行矩阵计算的例子。
```cpp
struct ComputationContext
{
cl_platform_id platform;
cl_device_id device;
};
static std::unique_ptr<ComputationContext> selectDevice()
{
cl_uint platformCount;
checkOpenCLError(clGetPlatformIDs(0, nullptr, &platformCount));
std::cout << "Platform count: " << platformCount << std::endl;
std::vector<cl_platform_id> platforms(platformCount);
checkOpenCLError(clGetPlatformIDs(platformCount, platforms.data(), nullptr));
std::unique_ptr<ComputationContext> selectedDevice = nullptr;
for (const auto& platform : platforms)
{
cl_uint deviceCount = 0;
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &deviceCount));
std::vector<cl_device_id> devices(deviceCount);
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, deviceCount, devices.data(), nullptr));
for (const auto& device : devices)
{
size_t deviceNameLength;
checkOpenCLError(clGetDeviceInfo(device, CL_DEVICE_NAME, 0, nullptr, &deviceNameLength));
std::vector<char> deviceNameArray(deviceNameLength);
checkOpenCLError(
clGetDeviceInfo(device, CL_DEVICE_NAME, deviceNameLength, deviceNameArray.data(), nullptr));
std::string deviceName(deviceNameArray.data(), deviceNameArray.size() - 1);
std::cout << "Found device: " << deviceName << std::endl;
if (deviceName.find("4060") != std::string::npos)
{
std::cout << "Select device '" << deviceName << "' as runner." << std::endl;
selectedDevice = std::make_unique<ComputationContext>();
selectedDevice->platform = platform;
selectedDevice->device = device;
}
else
{
clReleaseDevice(device);
}
}
}
if (selectedDevice == nullptr)
{
std::cout << "Failed to find the target device." << std::endl;
std::exit(EXIT_FAILURE);
}
return selectedDevice;
}
std::vector<int> clCalculateMatrix(const std::vector<int>& a,
const std::vector<int>& b)
{
cl_int error;
const std::unique_ptr<ComputationContext> computationContext = selectDevice();
// A key-value list ends with 0
// See also https://www.khronos.org/registry/OpenCL/specs/3.0-unified/html/OpenCL_API.html#context-properties-table
std::array<cl_context_properties, 3> properties = {
CL_CONTEXT_PLATFORM,
reinterpret_cast<cl_context_properties>(computationContext->platform),
0
};
cl_context context = clCreateContext(properties.data(), 1, &computationContext->device, nullptr, nullptr,
&error);
checkOpenCLError(error);
cl_command_queue queue = clCreateCommandQueueWithProperties(context, computationContext->device, nullptr,
&error);
checkOpenCLError(error);
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
constexpr size_t matrixSize = MATRIX_SIZE * MATRIX_SIZE * sizeof(int);
cl_mem deviceA = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceB = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceC = clCreateBuffer(context, CL_MEM_READ_WRITE, matrixSize, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceA, CL_TRUE, 0, matrixSize, a.data(), 0, nullptr,
nullptr));
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceB, CL_TRUE, 0, matrixSize, b.data(), 0, nullptr,
nullptr));
// Copy result to erase the previous result
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr
));
auto source = R"(
#define MATRIX_SIZE 2048
__kernel void calculate(const __global int* a, const __global int* b, __global int* c)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[x * MATRIX_SIZE + y] = result;
})";
cl_program program = clCreateProgramWithSource(context, 1, &source, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(clBuildProgram(program, 0, nullptr, "", nullptr, nullptr));
size_t messageSize;
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, 0, nullptr, &messageSize));
std::vector<char> messageArray(messageSize);
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, messageSize, messageArray.data(
), nullptr));
std::string message(messageArray.data(), messageSize - 1);
std::cout << "Build log: " << message << std::endl;
cl_kernel kernel = clCreateKernel(program, "calculate", &error);
checkOpenCLError(error);
checkOpenCLError(clSetKernelArg(kernel, 0, sizeof(cl_mem), &deviceA));
checkOpenCLError(clSetKernelArg(kernel, 1, sizeof(cl_mem), &deviceB));
checkOpenCLError(clSetKernelArg(kernel, 2, sizeof(cl_mem), &deviceC));
cl_event event;
constexpr std::size_t globalSize[2] = {MATRIX_SIZE, MATRIX_SIZE};
checkOpenCLError(clEnqueueNDRangeKernel(queue, kernel, 2, nullptr,
globalSize, nullptr, 0, nullptr, &event));
checkOpenCLError(clWaitForEvents(1, &event));
checkOpenCLError(
clEnqueueReadBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr));
clReleaseMemObject(deviceA);
clReleaseMemObject(deviceB);
clReleaseMemObject(deviceC);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseCommandQueue(queue);
clReleaseContext(context);
clReleaseDevice(computationContext->device);
return result;
}
```
从上面的代码中可以看出两点:
- OpenCL的编程比CUDA的更为繁琐因为OpenCL支持的设备种类更多在主机代码上还需要多出一块选择运行设备的代码
- OpenCL在主机代码和核函数的解耦更为彻底核函数直接以字符串的形式存在于主机代码中而各个厂商提供的驱动才是真正的编译器。
测试的运行结果如下:
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ---- |
| NVIDIA 4060 Ti OpenCL | 173ms | 0.01 |
| Intel UHD Graphics 770 OpenCL | 1020ms | 0.04 |
| CPU | 21255ms | 1.00 |
### SYCL
SYCL是一个使用标准C++编写在各种异构计算设备上运行核函数的抽象层并提供了一套新的API来查找各种设备并管理这些设备上的内存资源和代码执行。这个标准是开发、无版税、跨平台的抽象标准。同时也是因为这是一个**标准**因此需要寻找支持这个标准的编译器才能使用这个标准。按照官网上的说明我们选择了两个看上去还在活跃开发的项目Intel的[oneAPI](https://www.intel.com/content/www/us/en/developer/tools/oneapi/overview.html)和开源的[AdaptiveCpp](https://github.com/AdaptiveCpp/AdaptiveCpp)进行调研考虑到在后文中还将继续介绍oneAPI相关的工作因此这里将重点放在AdaptiveCpp上。
AdaptiveCpp由四个部分组成分别在不同的C++命名空间中提供。
- SYCL Interface实现了SYCL标准中规定的各种类和函数是实际上同用户交互的接口。这些接口实际上可以仍然可以分成主机API和核函数库两个部分。主机API是普通的C++代码负责任务调度、任务管理和平台射别管理等。核函数库包括了这种在编写核函数时可以使用的类和函数这些接口暴露一些后端特定的功能其中的一些甚至需要使用后端特定的方言来编写例如CUDA。
- AdaptiveCpp Runtime运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能运行时负责同各种支持后端的运行时交互来实现上述的功能。
![image-20241029123308139](./heterogeneous-programming-model/image-20241029123308139.png)
- Compiler考虑到在用户编写的代码中可能使用一些特定后端的方言因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
- Glue将上述的各个部分连接在一起的胶水代码。一种典型的胶水代码是内核函数的启动代码`kernel launcher`由于启动器中往往涉及到一些后端特定的方言例如CUDA中的`<<<>>>`或者OpenMP中的各种`pragma`因此这些代码通常需要使用特定的编译器进行编译所以这些胶水代码直接以头文件的方式提供以方便在编译时被特定的编译器处理。这些胶水代码将会把核函数包裹为一个合法的C++函数对象,这样运行时就可以获得这个函数对象并控制代码在设备上的运行。
AdaptiveCpp同时支持多种不同的编译流程。
1. 一种通用的一遍编译流程,将核函数编译到一种统一的中间表示形式,这种中间表示形式将在运行时被编译到特定的后端架构上。这种编译流程提供了高度的可移植性和较快的编译速度。这种编译设施支持的后端有:通过`PTX`在NVIDIA的GPU上运行通过`amdgcn`在AMD的GPU上运行通过`SPIR-V`在Intel的GPU上运行通过`SPIR-V`在任何支持OpenCL驱动的设备上运行也可以通过LLVM直接在CPU上运行。
2. 一种为互操作性优化的多遍编译流程在这个流程中AdaptiveCpp将聚合现有的各种LLVM/Clang的编译工具链使得用户可以在单个代码文件中混合编写SYCL和各种特定的编程模型例如CUDA和HIP。使用这个编译流程的好处有亮点1在这种编译流程中可以直接在SYCL代码使用各个特定编译模型中提供最新设备内部优化Intrinsics不用等待SYCL标准的支持2在这种编译流程中可以使用各个厂商提供的优化模板库例如`rocPRIM`和`CUB`。这种编译流程是提供聚合`CUDA`的clang前端和`ROCm`的clang前端来实现的。
3. 一种只将AdaptiveCpp作为函数使用的编程流程。在这种情况AdaptiveCpp作为一个三方库被引入其他的编译器编译流程中。
第一种通用的编译流程显然是泛用性最广的一种编译流程同时也是AdaptiveCpp推荐的编译流程。
![image-20241029163654675](./heterogeneous-programming-model/image-20241029163654675.png)
下面是一段使用SYCL进行矩阵乘法加速的代码
```cpp
struct CustomDeviceSelector
{
explicit CustomDeviceSelector(std::string vendorName) : _vendorName(std::move(vendorName))
{
}
int operator()(const sycl::device& d) const
{
int deviceRating = 0;
if (d.is_gpu() && d.get_info<sycl::info::device::name>().find(_vendorName) != std::string::npos)
{
deviceRating = 3;
}
else if (d.is_cpu())
{
deviceRating = 1;
}
return deviceRating;
}
private:
std::string _vendorName;
};
static std::vector<int> syclCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b,
const std::string& hint)
{
const CustomDeviceSelector selector(hint);
sycl::queue queue(selector);
const std::string deviceName = queue.get_device().get_info<sycl::info::device::name>();
std::cout << "Select device: " << deviceName << std::endl;
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
sycl::buffer aBuffer(a);
sycl::buffer bBuffer(b);
sycl::buffer resultBuffer(result);
queue.submit([&](sycl::handler& h)
{
const sycl::accessor aBufferAccessor(aBuffer, h, sycl::read_only);
const sycl::accessor bBufferAccessor(bBuffer, h, sycl::read_only);
const sycl::accessor resultBufferAccessor(resultBuffer, h, sycl::write_only);
h.parallel_for(sycl::nd_range<2>({MATRIX_SIZE, MATRIX_SIZE}, {16, 16}), [=](const sycl::nd_item<2>& item)
{
const size_t x = item.get_global_id(0);
const size_t y = item.get_global_id(1);
int temp = 0;
for (size_t k = 0; k < MATRIX_SIZE; ++k)
{
temp += aBufferAccessor[x * MATRIX_SIZE + k] * bBufferAccessor[k * MATRIX_SIZE + y];
}
resultBufferAccessor[x * MATRIX_SIZE + y] = temp;
});
});
sycl::host_accessor resultHostAccessor(resultBuffer, sycl::read_only);
for (size_t i = 0; i < MATRIX_SIZE; ++i)
{
for (size_t j = 0; j < MATRIX_SIZE; ++j)
{
result[i * MATRIX_SIZE + j] = resultHostAccessor[i * MATRIX_SIZE + j];
}
}
return result;
}
```
测试之后的运行结果如下所示:
| 类型 | 运行时间 | 比率 |
| --------------------------- | -------- | ----- |
| Intel UHD Graphics 770 SYCL | 488ms | 0.023 |
| NVIDIA 4060 Ti SYCL | 180ms | 0.008 |
| OpenMP SYCL | 1591ms | 0.076 |
| CPU | 20930ms | 1.000 |
### OpenACC
OpenACC是一个通过编译器制导来在代码中表达并行性并利用并行编译器为多个并行加速器生成代码的编程模型。为了保证OpenACC可以适配于各种计算架构的加速设备OpenACC设计了一个各种并行层次和有着不同速度和寻址方式内存的编程模型。同时OpenACC主要的功能即是支持同时将计算和数据卸载到一个加速设备上考虑到加速设备可能有着同宿主设备完全不同的内存架构OpenACC编译器和运行时将会自动分析代码并负责加速器上内存的管理和加速器和主机之间的数据传输。
作为一个高等级、平台独立的加速器编程框架使用OpenACC进行开发能够使开发人员将一个源代码编译到一系列设备上运行并实现一个相对较好的性能但是这个简易性和移植性也在一定程度上造成使用OpenACC编程无法完全利用加速设备上的算力。
OpenACC是作为一个标准的形式提供的实现了该标准的编译器有
| 编译器名称 | 情况 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| NVIDIA HPC SDK | 支持在NVIDIA GPU和多核CPU上的OpenACC并行编程 |
| Sourcery CodeBench Lite | OpenACC官网上说支持针对AMD GPU的编译但是官网页面似乎改版了没有找到相关的内容 |
| GCC 12 | 支持到OpenACC 2.6 |
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器看上去还做了不少工作的样子就是OpenACC官网上的链接已经失效了找起来比较麻烦而且宣称是一个开源编译器但是获取源代码和二进制文件需要联系他们美国橡树岭国家实验室创建账户这看去对于我们这些Foreign Adversary有些抽象了。 |
在试验OpenACC时遇到了巨大的困难不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
实验中编写的OpenACC加速代码如下
```cpp
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
{
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
const auto aBuffer = new int[length];
const auto bBuffer = new int[length];
const auto cBuffer = new int[length];
for (int i = 0; i < length; i++)
{
aBuffer[i] = a[i];
bBuffer[i] = b[i];
cBuffer[i] = 0;
}
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
#pragma acc enter data create(bBuffer[0:length])
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
{
#pragma acc kernels loop independent
for (int i = 0; i < MATRIX_SIZE; i++)
{
#pragma acc loop independent
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
#pragma acc loop independent reduction(+:temp)
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
}
cBuffer[i * MATRIX_SIZE + j] = temp;
}
}
}
#pragma acc exit data copyout(cBuffer[0:length])
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < length; ++i)
{
result[i] = cBuffer[i];
}
delete[] aBuffer;
delete[] bBuffer;
delete[] cBuffer;
return result;
}
```
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
| 编译器 | 类型 | 运行时间 |
| -------------- | ------- | -------- |
| NVIDIA HPC SDK | OpenACC | 19315ms |
| NVIDIA HPC SDK | CPU | 22942ms |
| GCC | OpenACC | 19999ms |
| GCC | CPU | 22623ms |
### oneAPI
oneAPI是Intel公司提出的一套异构并行编程框架该框架致力于达成如下几个目标1定义一个跨架构、跨制造商的统一开放软件平台2允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行3提供一套覆盖多个编程领域的库API。为了实现这些目标oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作oneAPI也提供了一个SYCL的编译器和运行时同时oneAPI也提供了一系列API库包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
![image-20241103162259981](./heterogeneous-programming-model/image-20241103162259981.png)
我对于oneAPI的理解就是Intel用来对标NVIDIA的CUDA的一套高性能编程工具箱。首先为了和NVIDIA完全闭源的CUDA形成鲜明的对比Intel选择了OpenCL合作同时开发SYCL当时也有可能是Intel知道自己的显卡技不如人如果不兼容市面上其他的部件是没有出路的同时为了和CUDA丰富的生态竞争Intel再开发并开源了一系列的`oneXXX`。
这里我就把上面SYCL写的例子用Intel提供的`DPC++`编译运行一下,看看在效率上会不会有所变化。
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ----- |
| Intel UHD Graphics 770 oneAPI | 429ms | 0.023 |
| NVIDIA 4060 Ti oneAPI | 191ms | 0.010 |
| Intel i5-13600K oneAPI | 198ms | 0.011 |
| CPU | 18643ms | 1.000 |
在显卡上的计算时间没有明显的变化但是我们Intel的编译器却在选择到使用Intel CPU进行计算时展现了不俗的实力。
## 参考文献
1. 刘颖,吕方,王蕾,陈莉,崔慧敏,冯晓兵.异构并行编程模型研究与进展.软件学报,2014,25(7):1459-1475. [http://www.jos.org.cn/1000-9825/4608.htm](http://www.jos.org.cn/1000-9825/4608.htm)
2. AdaptiveCpp官方文档. [https://adaptivecpp.github.io/AdaptiveCpp/](https://adaptivecpp.github.io/AdaptiveCpp/)
3. Exploring the performance of SGEMM in OpenCL on NVIDIA GPUs. [https://github.com/CNugteren/myGEMM](https://github.com/CNugteren/myGEMM)
4. OpenACC Programming and Best Practices Guide. [https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html](https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html)
5. oneAPI What is it?. [https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html](https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,65 @@
---
title: 内存栈被Rust自动生成的Drop函数塞满了
date: 2024-11-05T20:36:07.3930374+08:00
tags:
- Rust
- 技术笔记
---
这辈子就是被Rust编译器害了.jpg
<!--more-->
最近在用Rust写一个[Sysy](https://gitlab.eduxiji.net/csc1/nscscc/compiler2022/-/blob/master/SysY2022%E8%AF%AD%E8%A8%80%E5%AE%9A%E4%B9%89-V1.pdf)语言的编译器,但是在实现完语法分析之后针对官方提供的测试用例进行测试时遇到的一个抽象的栈溢出报错。
事情是这样的,当我实现完`Sysy`语言的语法分析器并编写了一些白盒测试用例之后我便打算将官方提供的100个测试用例作为输入运行看看能不能**正常**的解析成抽象语法树(显然不可能手动检查生成的抽象语法树是否正确)。我首先在`main.rs`里面实现了读取所有的`.sy`文件进行词法分析和语法分析的逻辑程序在这里这正常的识别了大多数的输入文件在一些浮点数的输入上还存在一些问题。于是我便打算将这些逻辑重构到一个Rust的集成测试中方便在CI中使用`cargo test`进行运行测试。但是在重构完成之后使用`cargo test`进行运行时我去遇到了如下的运行时错误。
![image-20241105181144993](./rust-drop-stack-overflow/image-20241105181144993.png)
看到这个报错的第一瞬间,我怀疑是因为`cargo test``cargo run`的运行环境不同,导致测试程序读取到了其他其实不是`sysy`程序但是以`.sy`结尾的文件,而恰好这个文件又能被解析,使得解析器组合子工作的过程中调用链太长而导致栈溢出,于是我在`RustRover`中打断点调试运行,却发现程序正确的读取到输入文件。这就奇怪了,我于是让程序继续运行到报错,看看报错时候程序的调用栈是被什么东西填满了,然后发现程序的调用栈长这样:
![image-20241105181612954](./rust-drop-stack-overflow/image-20241105181612954.png)
并不是我程序中代码的调用太深导致的而是Rust编译器自动生成的`drop`函数导致的。于是尝试看看调用栈的底部,看看是在读取什么输入数据,`drop`什么神仙数据结构的时候发生的。调试器很快告诉我们,`drop`的数据结构是抽象语法树中的二元表达式而此时的输入代码则如下图所示而且图中的代码重复了400行。
![image-20241105182036975](./rust-drop-stack-overflow/image-20241105182036975.png)
我已经能想象到那棵高耸如云的抽象语法树了。
虽然找到了问题的根源,但是还有一个问题没有解决:为什么在`main.rs`上运行的时候程序并不会出现问题,但是在`cargo test`上运行时却会遇到栈溢出的问题?
这个问题其实在[Rust语言圣经](https://course.rs/compiler/pitfalls/stack-overflow.html)中就有记载不过问题的背景略有不同。Rust语言圣经中导致栈溢出的问题是尝试在栈上分配一个4MB的超大数组但是出现问题的原因是一致的。在`main.rs`中运行程序时,如果不使用多线程,那么程序的所有逻辑将运行在`main`线程上这个线程在Linux下的栈大小是8MB而当使用Rust提供的集成测试时Rust为了实现测试的并行运行会把所有的测试都运行在新线程上这就导致在使用`cargo test`时程序会出现问题。
解决这个问题的方案可以是设置环境变量设置创建新线程的栈大小:`RUST_MIN_STACK=8388608 cargo test`,但是这种方法总是不太优雅。合理的解决方案是重写造成问题数据结构的`drop`方法,避免使用编译器自动生成的`drop`方法。这里我提供的抽象语法树`drop`方法如下所示。通过广度优先搜索的方式遍历语法树,手动释放一些可能子节点可能较多的语法树节点(其中释放内存的方式来自于[reddit](https://www.reddit.com/r/rust/comments/x97a4a/stack_overflow_during_drop_of_huge_abstract/))。
```rust
fn collect_node_rubbishes(
rubbish: &mut Vec<Rc<RefCell<SyntaxNode>>>,
node_type: &mut SyntaxNodeType,
) {
match node_type {
SyntaxNodeType::BinaryExpression(node) => {
rubbish.push(std::mem::replace(&mut node.left, SyntaxNode::unit()));
rubbish.push(std::mem::replace(&mut node.right, SyntaxNode::unit()));
}
SyntaxNodeType::Block(nodes) => {
while let Some(child) = nodes.pop() {
rubbish.push(child);
}
}
_ => {}
}
}
impl Drop for SyntaxNode {
fn drop(&mut self) {
let mut rubbish = Vec::new();
collect_node_rubbishes(&mut rubbish, &mut self.node_type);
while let Some(node) = rubbish.pop() {
collect_node_rubbishes(&mut rubbish, &mut node.borrow_mut().node_type);
}
}
}
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,73 @@
---
title: Rust中将子特征的特征对象转换为父特征的特征对象
date: 2024-12-15T15:49:33.5102602+08:00
tags:
- Rust
- 技术笔记
---
这辈子就是被Rust编译器害了.jpg
<!--more-->
## 背景
还是在开发同[上一篇](https://rrricardo.top/blog/essays/rust-drop-stack-overflow)相同的项目——一个编译器。在编写语法分析构建抽象语法树的过程中设计了这样一种抽象语法树的数据结构:每一个抽象语法树节点都实现了一个基础的语法树节点特征`SyntaxNode`,同时每个可以参加运算、有返回类型的语法树节点都需要实现`ExpressionSyntaxNode`特征,该特征是`SyntaxNode`特征的子特征。因此,从特征对象`Rc<dyn ExpressionSyntaxNode>`到特征对象`Rc<dyn SyntaxNode>`的转换就成为在语法树构建过程中必然会遇到的问题。
这种数据结构的设计就是一个非常具有面向对象特色的设计思路但是我们伟大的Rust目前却不支持这种特征对象的转换。这种转换在Rust语言内部称作`trait-upcasting`,已经在[RFC3324](https://github.com/rust-lang/rfcs/blob/master/text/3324-dyn-upcasting.md)中完成了定义但其的实现从2021年开始一直到现在都处于`unstable`的状态,需要在`nightly`版本的编译器中开启`#[feature(trait_upcasting)]`。具体来说,这个特点允许当特征`Bar` 是另一个特征`Foo`的子特征`Bar : Foo`时是一个特征对象`dyn Bar`被转换为特征对象`dyn Foo`
## 当前条件下的实现方法
虽然我们可以在使用`nightly`编译器的条件下使用`feature`开关抢先实用这个功能,但是应该没有人会在生产环境下使用`nightly`编译器罢。
所以我们需要一个在当前环境下可以使用的解决方法。解决的思路是设计一个类型转换的辅助特征`CastHelper`,这个特征就提供了需要的转换方法:
```rust
trait CastHelper {
fn cast(&self) -> &dyn Foo;
}
```
然后在定义`Bar`特征时,让`CastHelper`也成为`Bar`特征的超特征。
```rust
trait Bar : Foo + CastHelper {
// Other method.
}
```
接下来使用泛型的方式为所有实现了`Bar` 的结构体实现`CastHelper`
```rust
impl<T> CastHelper for T
where
T : Bar + 'static
{
fn cast(&self) -> &dyn Foo {
self as _
}
}
```
`CastHelper`中也可以定义到`Rc<dyn Foo>``Box<dyn Foo>`等特征对象的转换。
所有的实现代码如下:
```rust
trait Foo {}
trait Bar: Foo + CastHelper {}
trait CastHelper {
fn cast(&self) -> &dyn Foo;
}
impl<T> CastHelper for T
where
T : Bar + 'static
{
fn cast(&self) -> &dyn Foo {
self as _
}
}
```

View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["**/*.razor", "**/*.cshtml", "**/*.html", "Processors/EssayStylesPostRenderProcessor.cs"],
theme: {
extend: {},
},
plugins: [],
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,78 +13,3 @@
font-display: block; font-display: block;
src: url(fonts/fa-solid-900.woff2) format("woff2"), url(fonts/fa-solid-900.ttf) format("truetype") src: url(fonts/fa-solid-900.woff2) format("woff2"), url(fonts/fa-solid-900.ttf) format("truetype")
} }
.essay-image {
width: 90%;
display: block;
margin: 1.5rem auto;
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18),
0 4px 15px 0 rgba(0, 0, 0, 0.15);
border-radius: 4px;
background-color: transparent;
}
.table-wrapper {
overflow-x: auto;
}
body a {
text-decoration: none;
}
body main {
flex: 1;
min-height: 100vh;
overflow-x: visible;
}
body p {
margin: 0;
margin-block: 1em;
}
h2, h3 {
margin-block-start: 1.5em;
margin-block-end: 1em;
}
h4, h5 {
margin-block-start: 1em;
margin-block-end: 1em;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px var(--bs-border-color) solid;
}
table thead {
background: var(--bs-secondary-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: 3px 20px;
}
table thead tr {
border: 1px var(--bs-border-color) solid;
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: var(--bs-body-color);
background-color: var(--bs-primary-bg-subtle);
border-block-start: .1em solid var(--bs-primary-border-subtle);
border-block-end: .1em solid var(--bs-primary-border-subtle);
}

BIN
YaeBlog/wwwroot/images/banner.png (Stored with Git LFS)

Binary file not shown.

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