4 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
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
51 changed files with 928 additions and 1357 deletions

View File

@@ -7,7 +7,7 @@ jobs:
Build-Blog-Image:
runs-on: archlinux
steps:
- uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
- uses: https://git.rrricardo.top/actions/checkout@v4
name: Check out code
with:
lfs: true
@@ -18,16 +18,12 @@ jobs:
- name: Build docker image
run: |
cd YaeBlog
podman build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
- name: Workaround to make sure podman login succeed
run: |
mkdir /root/.docker
docker build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
- name: Login aliyun docker registry
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
uses: https://git.rrricardo.top/actions/login-action@v3
with:
registry: registry.cn-beijing.aliyuncs.com
username: 初冬的朝阳
password: ${{ secrets.ALIYUN_PASSWORD }}
auth_file_path: /etc/containers/auth.json
- name: Push docker image
run: podman push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
run: docker push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest

2
.gitignore vendored
View File

@@ -484,4 +484,4 @@ $RECYCLE.BIN/
*.swp
# Tailwind auto-generated stylesheet
*.g.css
output.css

41
YaeBlog.sln Normal file
View File

@@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ADBC3DA8-F65C-4B5D-A97A-DC351F8E6592}"
ProjectSection(SolutionItems) = preProject
.gitea\workflows\build.yaml = .gitea\workflows\build.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{753B998C-1B9E-498F-B949-845CE86C4075}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
README.md = README.md
LICENSE = LICENSE
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{ADBC3DA8-F65C-4B5D-A97A-DC351F8E6592} = {9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}
EndGlobalSection
EndGlobal

View File

@@ -1,14 +0,0 @@
<Solution>
<Folder Name="/.gitea/" />
<Folder Name="/.gitea/workflows/">
<File Path=".gitea/workflows/build.yaml" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitattributes" />
<File Path=".gitignore" />
<File Path="LICENSE" />
<File Path="README.md" />
</Folder>
<Project Path="YaeBlog/YaeBlog.csproj" />
</Solution>

View File

@@ -7,4 +7,6 @@ public interface IEssayScanService
public Task<BlogContents> ScanContents();
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
public Task<ImageScanResult> ScanImages();
}

View File

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

View File

@@ -1,6 +1,4 @@
using System.CommandLine;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Commands.Binders;
using YaeBlog.Components;
using YaeBlog.Extensions;
@@ -21,7 +19,6 @@ public sealed class YaeBlogCommand
AddNewCommand(_rootCommand);
AddPublishCommand(_rootCommand);
AddScanCommand(_rootCommand);
AddCompressCommand(_rootCommand);
}
public Task<int> RunAsync(string[] args)
@@ -97,20 +94,22 @@ public sealed class YaeBlogCommand
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
newCommand.AddArgument(filenameArgument);
newCommand.SetHandler(async (file, blogOption, _, essayScanService) =>
newCommand.SetHandler(async (file, _, _, essayScanService) =>
{
BlogContents contents = await essayScanService.ScanContents();
if (contents.Posts.Any(content => content.BlogName == file))
if (contents.Posts.Any(content => content.FileName == file))
{
Console.WriteLine("There exists the same title blog in posts.");
return;
}
await essayScanService.SaveBlogContent(new BlogContent(
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
new MarkdownMetadata { Title = file, Date = DateTime.Now },
string.Empty, true, [], []));
await essayScanService.SaveBlogContent(new BlogContent
{
FileName = file,
FileContent = string.Empty,
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
});
Console.WriteLine($"Created new blog '{file}.");
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
@@ -127,15 +126,15 @@ public sealed class YaeBlogCommand
BlogContents contents = await essyScanService.ScanContents();
Console.WriteLine($"All {contents.Posts.Count} Posts:");
foreach (BlogContent content in contents.Posts.OrderBy(x => x.BlogName))
foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName))
{
Console.WriteLine($" - {content.BlogName}");
Console.WriteLine($" - {content.FileName}");
}
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName))
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
{
Console.WriteLine($" - {content.BlogName}");
Console.WriteLine($" - {content.FileName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
@@ -151,39 +150,32 @@ public sealed class YaeBlogCommand
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> unusedImages = (from content in contents
from image in content.Images
where image is { IsUsed: false }
select image).ToList();
ImageScanResult result = await essayScanService.ScanImages();
if (unusedImages.Count != 0)
if (result.UnusedImages.Count != 0)
{
Console.WriteLine("Found unused images:");
Console.WriteLine("HINT: use '--rm' to remove unused images.");
}
foreach (BlogImageInfo image in unusedImages)
foreach (FileInfo image in result.UnusedImages)
{
Console.WriteLine($" - {image.File.FullName}");
Console.WriteLine($" - {image.FullName}");
}
if (removeOptionValue)
{
foreach (BlogImageInfo image in unusedImages)
foreach (FileInfo image in result.UnusedImages)
{
image.File.Delete();
image.Delete();
}
}
Console.WriteLine("Used not existed images:");
foreach (BlogContent content in contents)
foreach (FileInfo image in result.NotFoundImages)
{
foreach (FileInfo file in content.NotfoundImages)
{
Console.WriteLine($"- {file.Name} in {content.BlogName}");
}
Console.WriteLine($" - {image.FullName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
}
@@ -201,7 +193,7 @@ public sealed class YaeBlogCommand
BlogContents contents = await essayScanService.ScanContents();
BlogContent? content = (from blog in contents.Drafts
where blog.BlogName == filename
where blog.FileName == filename
select blog).FirstOrDefault();
if (content is null)
@@ -210,17 +202,14 @@ public sealed class YaeBlogCommand
return;
}
// 设置发布的时间
content.Metadata.Date = DateTime.Now;
// 将选中的博客文件复制到posts
await essayScanService.SaveBlogContent(content, isDraft: false);
// 复制图片文件夹
DirectoryInfo sourceImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
DirectoryInfo targetImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
if (sourceImageDirectory.Exists)
{
@@ -234,30 +223,9 @@ public sealed class YaeBlogCommand
}
// 删除原始的文件
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md"));
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
sourceBlogFile.Delete();
}, new BlogOptionsBinder(),
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
}
private static void AddCompressCommand(RootCommand rootCommand)
{
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
rootCommand.Add(command);
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
getDefaultValue: () => false);
command.AddOption(dryRunOption);
command.SetHandler(ImageCommandHandler,
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
}
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
ILogger<ImageCompressService> _2,
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
{
await imageCompressService.Compress(dryRun);
}
}

View File

@@ -8,7 +8,7 @@
<link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="tailwind.g.css"/>
<link rel="stylesheet" href="output.css"/>
<HeadOutlet/>
</head>

View File

@@ -1,18 +0,0 @@
using AngleSharp.Dom;
namespace YaeBlog.Extensions;
public static class AngleSharpExtensions
{
public static IEnumerable<IElement> EnumerateParentElements(this IElement element)
{
IElement? e = element.ParentElement;
while (e is not null)
{
IElement c = e;
e = e.ParentElement;
yield return c;
}
}
}

View File

@@ -1,20 +1,12 @@
namespace YaeBlog.Models;
/// <summary>
/// 单个博客文件的所有数据和元数据
/// </summary>
/// <param name="BlogFile">博客文件</param>
/// <param name="Metadata">文件中的MD元数据</param>
/// <param name="Content">文件内容</param>
/// <param name="IsDraft">是否为草稿</param>
/// <param name="Images">博客中使用的文件</param>
public record BlogContent(
FileInfo BlogFile,
MarkdownMetadata Metadata,
string Content,
bool IsDraft,
List<BlogImageInfo> Images,
List<FileInfo> NotfoundImages)
public class BlogContent
{
public string BlogName => BlogFile.Name.Split('.')[0];
public required string FileName { get; init; }
public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
}

View File

@@ -1,15 +1,10 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
namespace YaeBlog.Models;
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
: IEnumerable<BlogContent>
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
{
IEnumerator<BlogContent> IEnumerable<BlogContent>.GetEnumerator()
{
return Posts.Concat(Drafts).GetEnumerator();
}
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
public IEnumerator GetEnumerator() => ((IEnumerable<BlogContent>)this).GetEnumerator();
public ConcurrentBag<BlogContent> Posts { get; } = posts;
}

View File

@@ -1,44 +0,0 @@
using System.Text;
namespace YaeBlog.Models;
public record BlogImageInfo(FileInfo File, long Width, long Height, string MineType, byte[] Content, bool IsUsed)
: IComparable<BlogImageInfo>
{
public int Size => Content.Length;
public override string ToString()
{
StringBuilder builder = new();
builder.AppendLine($"Blog image {File.Name}:");
builder.AppendLine($"\tWidth: {Width}; Height: {Height}");
builder.AppendLine($"\tSize: {FormatSize()}");
builder.AppendLine($"\tImage Format: {MineType}");
return builder.ToString();
}
public int CompareTo(BlogImageInfo? other)
{
if (other is null)
{
return -1;
}
return other.Size.CompareTo(Size);
}
private string FormatSize()
{
double size = Size;
if (size / 1024 > 3)
{
size /= 1024;
return size / 1024 > 3 ? $"{size / 1024}MB" : $"{size}KB";
}
return $"{size}B";
}
}

View File

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

View File

@@ -1,7 +1,6 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Extensions;
using YaeBlog.Models;
namespace YaeBlog.Processors;
@@ -21,21 +20,20 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
ApplyGlobalCssStyles(document);
BeatifyTable(document);
BeatifyList(document);
BeatifyInlineCode(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-gray-100 rounded-sm overflow-x-auto" },
{ "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)
@@ -101,45 +99,4 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
}
}
}
private static void BeatifyList(IDocument document)
{
foreach (IElement ulElement in from e in document.All
where e.LocalName == "ul"
select e)
{
// 首先给<ul>元素添加样式
ulElement.ClassList.Add("list-disc ml-10");
foreach (IElement liElement in from e in ulElement.Children
where e.LocalName == "li"
select e)
{
// 修改<li>元素中的<p>元素样式
// 默认的p-2间距有点太宽了
foreach (IElement pElement in from e in liElement.Children
where e.LocalName == "p"
select e)
{
pElement.ClassList.Remove("p-2");
pElement.ClassList.Add("p-1");
}
}
}
}
private static void BeatifyInlineCode(IDocument document)
{
// 选择不在<pre>元素内的<code>元素
// 即行内代码
IEnumerable<IElement> inlineCodes = from e in document.All
where e.LocalName == "code" && e.EnumerateParentElements().All(p => p.LocalName != "pre")
select e;
foreach (IElement e in inlineCodes)
{
e.ClassList.Add("bg-gray-100 inline p-1 rounded-xs");
}
}
}

View File

@@ -7,8 +7,7 @@ using YaeBlog.Models;
namespace YaeBlog.Processors;
public class ImagePostRenderProcessor(
ILogger<ImagePostRenderProcessor> logger,
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
@@ -30,27 +29,22 @@ public class ImagePostRenderProcessor(
if (attr is not null)
{
logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName, essay.IsDraft);
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
}
}
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
}
public string Name => nameof(ImagePostRenderProcessor);
private string GenerateImageLink(string filename, string essayFilename, bool isDraft)
private string GenerateImageLink(string filename, string essayFilename)
{
// 如果图片路径中没有包含文件名
// 则添加文件名
if (!filename.Contains(essayFilename))
{
filename = Path.Combine(essayFilename, filename);
}
filename = isDraft
? Path.Combine(_options.Root, "drafts", filename)
: Path.Combine(_options.Root, "posts", filename);
filename = Path.Combine(_options.Root, "posts", filename);
if (!Path.Exists(filename))
{

View File

@@ -1,7 +1,5 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Imageflow.Bindings;
using Imageflow.Fluent;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
@@ -11,30 +9,17 @@ using YamlDotNet.Serialization;
namespace YaeBlog.Services;
public partial class EssayScanService : IEssayScanService
{
private readonly BlogOptions _blogOptions;
private readonly ISerializer _yamlSerializer;
private readonly IDeserializer _yamlDeserializer;
private readonly ILogger<EssayScanService> _logger;
public EssayScanService(ISerializer yamlSerializer,
public partial class EssayScanService(
ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
ILogger<EssayScanService> logger)
ILogger<EssayScanService> logger) : IEssayScanService
{
_yamlSerializer = yamlSerializer;
_yamlDeserializer = yamlDeserializer;
_logger = logger;
_blogOptions = blogOptions.Value;
RootDirectory = ValidateRootDirectory();
}
private DirectoryInfo RootDirectory { get; }
private readonly BlogOptions _blogOptions = blogOptions.Value;
public async Task<BlogContents> ScanContents()
{
ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts);
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts, true),
@@ -43,92 +28,82 @@ public partial class EssayScanService : IEssayScanService
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
{
ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts);
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
FileInfo targetFile = isDraft
? new FileInfo(Path.Combine(drafts.FullName, content.BlogName + ".md"))
: new FileInfo(Path.Combine(posts.FullName, content.BlogName + ".md"));
? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md"))
: new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md"));
if (!isDraft)
{
content.Metadata.Date = DateTime.Now;
}
if (targetFile.Exists)
{
_logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
}
await using StreamWriter writer = targetFile.CreateText();
await writer.WriteAsync("---\n");
await writer.WriteAsync(_yamlSerializer.Serialize(content.Metadata));
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
await writer.WriteAsync("---\n");
if (string.IsNullOrEmpty(content.Content) && isDraft)
if (isDraft)
{
// 如果博客为操作且内容为空
// 创建简介隔断符号
await writer.WriteLineAsync("<!--more-->");
}
else
{
await writer.WriteAsync(content.Content);
await writer.WriteAsync(content.FileContent);
}
// 保存图片文件
await Task.WhenAll(from image in content.Images
select File.WriteAllBytesAsync(image.File.FullName, image.Content));
}
private record struct BlogResult(
FileInfo BlogFile,
string BlogContent,
List<BlogImageInfo> Images,
List<FileInfo> NotFoundImages);
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
{
// 扫描以md结尾且不是隐藏文件的文件
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
where file.Extension == ".md" && !file.Name.StartsWith('.')
select file;
ConcurrentBag<BlogResult> fileContents = [];
ConcurrentBag<(string, string)> fileContents = [];
await Parallel.ForEachAsync(markdownFiles, async (file, token) =>
{
using StreamReader reader = file.OpenText();
string blogName = file.Name.Split('.')[0];
string blogContent = await reader.ReadToEndAsync(token);
ImageResult imageResult =
await ScanImagePreBlog(directory, blogName,
blogContent);
fileContents.Add(new BlogResult(file, blogContent, imageResult.Images, imageResult.NotfoundImages));
fileContents.Add((file.Name, await reader.ReadToEndAsync(token)));
});
ConcurrentBag<BlogContent> contents = [];
await Task.Run(() =>
{
foreach (BlogResult blog in fileContents)
foreach ((string filename, string content) in fileContents)
{
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
if (!content.StartsWith("---") || endPos is -1 or 0)
{
_logger.LogWarning("Failed to parse metadata from {}, skipped.", blog.BlogFile.Name);
logger.LogWarning("Failed to parse metadata from {}, skipped.", filename);
return;
}
string metadataString = blog.BlogContent[4..endPos];
string metadataString = content[4..endPos];
try
{
MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
_logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, blog.BlogFile.Name);
MarkdownMetadata metadata = yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename);
contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft,
blog.Images, blog.NotFoundImages));
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
});
}
catch (YamlException e)
{
_logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", blog.BlogFile.Name, e);
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
}
}
});
@@ -136,96 +111,99 @@ public partial class EssayScanService : IEssayScanService
return contents;
}
private record struct ImageResult(List<BlogImageInfo> Images, List<FileInfo> NotfoundImages);
private async Task<ImageResult> ScanImagePreBlog(DirectoryInfo directory, string blogName, string content)
public async Task<ImageScanResult> ScanImages()
{
MatchCollection matchResult = ImagePattern.Matches(content);
DirectoryInfo imageDirectory = new(Path.Combine(directory.FullName, blogName));
BlogContents contents = await ScanContents();
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
Dictionary<string, bool> usedImages = imageDirectory.Exists
? imageDirectory.EnumerateFiles().ToDictionary(file => file.FullName, _ => false)
: [];
List<FileInfo> notFoundImages = [];
List<FileInfo> unusedFiles = [];
List<FileInfo> notFoundFiles = [];
foreach (Match match in matchResult)
ImageScanResult draftResult = await ScanUnusedImagesInternal(contents.Drafts, drafts);
ImageScanResult postResult = await ScanUnusedImagesInternal(contents.Posts, posts);
unusedFiles.AddRange(draftResult.UnusedImages);
notFoundFiles.AddRange(draftResult.NotFoundImages);
unusedFiles.AddRange(postResult.UnusedImages);
notFoundFiles.AddRange(postResult.NotFoundImages);
return new ImageScanResult(unusedFiles, notFoundFiles);
}
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
DirectoryInfo root)
{
string imageName = match.Groups[1].Value;
ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = [];
// 判断md文件中的图片名称中是否包含文件夹名称
// 例如 blog-1/image.png 或者 image.png
// 如果不带文件夹名称
// 默认添加同博客名文件夹
FileInfo usedFile = imageName.Contains(blogName)
? new FileInfo(Path.Combine(directory.FullName, imageName))
: new FileInfo(Path.Combine(directory.FullName, blogName, imageName));
if (usedImages.TryGetValue(usedFile.FullName, out _))
Parallel.ForEach(contents, content =>
{
usedImages[usedFile.FullName] = true;
MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
Dictionary<string, bool> usedDictionary;
if (imageDirectory.Exists)
{
usedDictionary = (from file in imageDirectory.EnumerateFiles()
select new KeyValuePair<string, bool>(file.FullName, false)).ToDictionary();
}
else
{
notFoundImages.Add(usedFile);
}
usedDictionary = [];
}
List<BlogImageInfo> images = (await Task.WhenAll((from pair in usedImages
select GetImageInfo(new FileInfo(pair.Key), pair.Value)).ToArray())).ToList();
return new ImageResult(images, notFoundImages);
}
private static async Task<BlogImageInfo> GetImageInfo(FileInfo file, bool isUsed)
foreach (Match match in result)
{
byte[] image = await File.ReadAllBytesAsync(file.FullName);
string imageName = match.Groups[1].Value;
if (file.Extension is ".jpg" or ".jpeg" or ".png")
FileInfo usedFile = imageName.Contains(content.FileName)
? new FileInfo(Path.Combine(root.FullName, imageName))
: new FileInfo(Path.Combine(root.FullName, content.FileName, imageName));
if (usedDictionary.TryGetValue(usedFile.FullName, out _))
{
ImageInfo imageInfo =
await ImageJob.GetImageInfoAsync(MemorySource.Borrow(image), SourceLifetime.NowOwnedAndDisposedByTask);
return new BlogImageInfo(file, imageInfo.ImageWidth, imageInfo.ImageWidth, imageInfo.PreferredMimeType,
image, isUsed);
usedDictionary[usedFile.FullName] = true;
}
else
{
notFoundImage.Add(usedFile);
}
}
return new BlogImageInfo(file, 0, 0, file.Extension switch
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
{
"svg" => "image/svg",
"avif" => "image/avif",
_ => string.Empty
}, image, isUsed);
unusedImage.Add(new FileInfo(pair.Key));
}
});
return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList()));
}
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex ImagePattern { get; }
private DirectoryInfo ValidateRootDirectory()
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
{
DirectoryInfo rootDirectory = new(Path.Combine(Environment.CurrentDirectory, _blogOptions.Root));
root = Path.Combine(Environment.CurrentDirectory, root);
DirectoryInfo rootDirectory = new(root);
if (!rootDirectory.Exists)
{
throw new BlogFileException($"'{_blogOptions.Root}' is not a directory.");
throw new BlogFileException($"'{root}' is not a directory.");
}
return rootDirectory;
}
private void ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts)
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
{
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
{
throw new BlogFileException($"'{_blogOptions.Root}/drafts' not exists.");
throw new BlogFileException($"'{root}/drafts' not exists.");
}
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
{
throw new BlogFileException($"'{_blogOptions.Root}/posts' not exists.");
throw new BlogFileException($"'{root}/posts' not exists.");
}
drafts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "drafts"));
posts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "posts"));
drafts = new DirectoryInfo(Path.Combine(root, "drafts"));
posts = new DirectoryInfo(Path.Combine(root, "posts"));
}
}

View File

@@ -1,108 +0,0 @@
using Imageflow.Fluent;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
namespace YaeBlog.Services;
public sealed class ImageCompressService(IEssayScanService essayScanService, ILogger<ImageCompressService> logger)
{
private record struct CompressResult(BlogImageInfo ImageInfo, byte[] CompressContent);
public async Task<List<BlogImageInfo>> ScanUsedImages()
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> originalImages = (from content in contents.Posts.Concat(contents.Drafts)
from image in content.Images
where image.IsUsed
select image).ToList();
originalImages.Sort();
return originalImages;
}
public async Task Compress(bool dryRun)
{
BlogContents contents = await essayScanService.ScanContents();
// 筛选需要压缩的图片
// 即图片被博客使用且是jpeg/png格式
List<BlogContent> needCompressContents = (from content in contents
where content.Images.Any(i => i is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" })
select content).ToList();
if (needCompressContents.Count == 0)
{
return;
}
int uncompressedSize = 0;
int compressedSize = 0;
List<BlogContent> compressedContent = new(needCompressContents.Count);
foreach (BlogContent content in needCompressContents)
{
List<BlogImageInfo> uncompressedImages = (from image in content.Images
where image is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" }
select image).ToList();
uncompressedSize += uncompressedImages.Select(i => i.Size).Sum();
foreach (BlogImageInfo image in uncompressedImages)
{
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
content.BlogName);
}
CompressResult[] compressedImages = (await Task.WhenAll(from image in uncompressedImages
select Task.Run(async () => new CompressResult(image, await ConvertToWebp(image.Content))))).ToArray();
compressedSize += compressedImages.Select(i => i.CompressContent.Length).Sum();
// 直接在原有的图片列表上添加图片
List<BlogImageInfo> images = content.Images.Concat(from r in compressedImages
select r.ImageInfo with
{
File = new FileInfo(r.ImageInfo.File.FullName.Split('.')[0] + ".webp"),
Content = r.CompressContent
}).ToList();
// 修改文本
string blogContent = compressedImages.Aggregate(content.Content, (c, r) =>
{
string originalName = r.ImageInfo.File.Name;
string outputName = originalName.Split('.')[0] + ".webp";
return c.Replace(originalName, outputName);
});
compressedContent.Add(content with { Images = images, Content = blogContent });
}
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
if (dryRun is false)
{
await Task.WhenAll(from content in compressedContent
select essayScanService.SaveBlogContent(content, content.IsDraft));
}
}
private static async Task<byte[]> ConvertToWebp(byte[] image)
{
using ImageJob job = new();
BuildJobResult result = await job.Decode(MemorySource.Borrow(image))
.EncodeToBytes(new WebPLossyEncoder(75))
.Finish()
.InProcessAsync();
ArraySegment<byte>? array = result.First?.TryGetBytes();
if (array.HasValue)
{
return array.Value.ToArray();
}
throw new BlogFileException();
}
}

View File

@@ -41,14 +41,14 @@ public partial class RendererService(
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.BlogName,
FileName = content.BlogName,
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.Content
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
@@ -156,17 +156,17 @@ public partial class RendererService(
private string GetDescription(BlogContent content)
{
const string delimiter = "<!--more-->";
int pos = content.Content.IndexOf(delimiter, StringComparison.Ordinal);
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
bool breakSentence = false;
if (pos == -1)
{
// 自动截取前50个字符
pos = content.Content.Length < 50 ? content.Content.Length : 50;
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
breakSentence = true;
}
string rawContent = content.Content[..pos];
string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern.Matches(rawContent);
StringBuilder builder = new();
@@ -182,18 +182,18 @@ public partial class RendererService(
string description = builder.ToString();
logger.LogDebug("Description of {} is {}.", content.BlogName,
logger.LogDebug("Description of {} is {}.", content.FileName,
description);
return description;
}
private uint GetWordCount(BlogContent content)
{
int count = (from c in content.Content
int count = (from c in content.FileContent
where char.IsLetterOrDigit(c)
select c).Count();
logger.LogDebug("Word count of {} is {}", content.BlogName,
logger.LogDebug("Word count of {} is {}", content.FileName,
count);
return (uint)count;
}

View File

@@ -1,8 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="ImageFlow.NativeRuntime.ubuntu-x86_64" Version="2.1.0-rc11"/>
<PackageReference Include="ImageFlow.Net" Version="0.13.2"/>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
@@ -15,7 +13,7 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
@@ -27,13 +25,9 @@
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
<ItemGroup>
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
</ItemGroup>
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
</Target>
</Project>

View File

@@ -1,15 +1,12 @@
{
"name": "yae-blog",
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css -w"
},
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0",
"@tailwindcss/cli": "^4.0.0"
"tailwindcss": "^3.4.16"
}
}

1103
YaeBlog/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,7 @@
title: 2021年终总结
date: 2022-01-12 16:27:19
tags:
- 杂谈
- 年终总结
- 随笔
---
2021年已经过去2022年已经来临。每每一年开始的时候我都会展开一张纸或者新建一个文档思量着又是一年时光也该同诸大杂志一般写几句意味深长的话语怀念过去的时光也祝福未来的自己。可往往脑海中已是三万字的长篇落在笔头却又是一个字都没有了。

View File

@@ -1,8 +1,7 @@
---
title: 2022年终总结
tags:
- 杂谈
- 年终总结
- 随笔
date: 2022-12-30 14:58:12
---

View File

@@ -1,7 +1,7 @@
---
title: 2022年暑假碎碎念
tags:
- 杂谈
- 随笔
typora-root-url: 2022-summer-vacation
date: 2022-08-22 15:39:13
---

View File

@@ -1,8 +1,7 @@
---
title: 2023年年终总结
tags:
- 杂谈
- 年终总结
- 随笔
date: 2024-2-29 20:18:19
---

View File

@@ -1,7 +1,7 @@
---
title: 人生代码大作业初体验
tags:
- 杂谈
- 随笔
typora-root-url: big-homework
date: 2022-07-27 11:34:49
---

View File

@@ -2,7 +2,7 @@
title: 日用Linux挑战 第0篇 初见Arch Linux
tags:
- Linux
- 杂谈
- 随笔
date: 2023-01-15 22:23:08
typora-root-url: daily-linux-0
---

View File

@@ -2,7 +2,7 @@
title: 日用Linux挑战 第1篇 问题与挑战
tags:
- Linux
- 杂谈
- 随笔
date: 2023-03-08 22:37:29
---

View File

@@ -1,7 +1,7 @@
---
title: 日用Linux挑战 第2篇 Wayland
tags:
- 杂谈
- 随笔
- Linux
date: 2023-07-23 11:44:34
typora-root-url: daily-linux-2

View File

@@ -1,7 +1,7 @@
---
title: 日用Linux挑战 第3篇 放弃Wayland
tags:
- 杂谈
- 随笔
- Linux
typora-root-url: daily-linux-3
date: 2023-09-04 14:47:46

View File

@@ -2,7 +2,7 @@
title: 日用Linux挑战 第4篇 新的开始
tags:
- Linux
- 杂谈
- 随笔
date: 2024/03/09 14:00:00
---

View File

@@ -1,100 +0,0 @@
---
title: High Performance Computing 25 SP CPU Architecture
date: 2025-03-13T23:59:08.8167680+08:00
tags:
- 学习资料
- 高性能计算
---
How to use the newly available transistors?
<!--more-->
Parallelsim:
Instruction Level Parallelism(ILP):
- **Implicit/transparent** to users/programmers.
- Instruction pipelining.
- Superscalar execution.
- Out of order execution.
- Register renaming.
- Speculative execution.
- Branch prediction.
Task Level Parallelism(TLP):
- **Explicit** to users/programmers.
- Multiple threads or processes executed simultaneously.
- Multi-core processors.
Data Parallelism:
- Vector processors and SIMD.
Von Neumann Architecture: the **stored-program** concept. Three components: processor, memory and data path.
Bandwidth: the gravity of modern computer system.
## Instruction Pipelining
Divide incoming instructions into a series of sequential steps performed by different processor unit to keep every part of the processor busy.
Superscalar execution can execute more than one instruction during a clock cycle.
Order of order execution.
Very long instruction word(VLIW): allows programs to explicitly specify instructions to execute at the same time.
EPIC: Explicit parallel instruction computing.
Move the complexity of instruction scheduling from the CPU hardware to the software compiler:
- Check dependencies between instructions.
- Assign instructions to the functional units.
- Determine when instructions are initiated placed together into a single word.
![image-20250313184421305](./hpc-2025-cpu-architecture/image-20250313184421305.png)
Comparisons between different architecture:
![image-20250313184732892](./hpc-2025-cpu-architecture/image-20250313184732892.png)
## Multi-Core Processor Gala
Symmetric multiprocessing(SMP): a multiprocessor computer hardware and software architecture.
Two or more identical processors are connected to a **single shared main memory** and have full access to all input and output devices.
> Current trend: computer clusters, SMP computers connected with network.
Multithreading: exploiting thread-level parallelism.
Multithreading allows multiple threads to share the functional units of a single processor in an overlapping fashion **duplicating only private state**. A thread switch should be much more efficient than a process switch.
Hardware approaches to multithreading:
**fine-grained multithreading**:
- Switches between threads on each clock.
- Hide the throughput losses that arise from the both short and long stalls.
- Disadvantages: slow down the execution of an individual thread.
**Coarse-grained multithreading**:
- Switch threads only on costly stalls.
- Limited in its ability to overcome throughput losses
**Simultaneous multithreading(SMT)**:
- A variation on fine-grained multithreading
![image-20250313190913475](./hpc-2025-cpu-architecture/image-20250313190913475.png)
## Data Parallelism: Vector Processors
Provides high-level operations that work on vectors.
Length of the array also varies depending on hardware.
SIMD and its generalization in vector parallelism approach improved efficiency by the same operation be performed on multiple data elements.

View File

@@ -1,33 +0,0 @@
---
title: High Performance Computing 25 SP Introduction
date: 2025-03-08T00:31:54.5775860+08:00
tags:
- 高性能计算
- 学习资料
---
High performance computing is the use of supercomputing and computing clusters to solve advanced problems.
<!--more-->
> High Performance Computing: Software execution speedup.
>
> High Throughput Computing: Resource utilization efficiency.
High performance computing:
- Provide computer power.
- Full-Domain HPC = Full-Stack HPC + Full-Network HPC.
Two key weapons: **partitioning** and **duplicating** to increase granularity.
HPC History:
- Mainframe computer
- Mini computer
- Cluster
- Grids
- Clouds

View File

@@ -1,7 +1,7 @@
---
title: 大学生用啥配置——计算机专业
tags:
- 杂谈
- 随笔
typora-root-url: laptop-for-computer
date: 2022-06-13 16:17:27
---

View File

@@ -1,163 +0,0 @@
---
title: 构建运行基于MLIR的独立项目
date: 2025-03-19T20:57:31.1928528+08:00
tags:
- 技术笔记
- LLVM
---
MLIR是多层次中间表示形式Multi-Level Intermediate Representation是LLVM项目中提供的一项新编译器开发基础设施使得编译器开发者能够在源代码和可执行代码之间定义多层IR来保留程序信息指导编译优化。本博客指导如何创建一个独立out-of-tree的MLIR项目。
<!--more-->
## 编译LLVM和MLIR
考虑到大多数的Linux发行版在打包LLVM时不会编译MLIR因此自行编译安装包括MLIR项目的LLVM就成为开发独立MLIR项目的前置条件。
首先在GitHub上下载LLVM的源代码包我这里选择最新的稳定版本`20.1.0`
```shell
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-20.1.0/llvm-project-20.1.0.src.tar.xz
```
下载之后解压进入,准备进行构建。
```shell
tar xvf llvm-project-20.1.0.src.tar.xz
cd llvm-project-20.1.0.src
```
创建`build`文件夹,使用下面的命令进行生成构建文件。在这里选择使用`Release`构建类型,安装的位置是`~/.local/share/llvm`文件夹,构建的项目包括`llvm``clang``mlir`三个项目,并指定使用系统上的`clang``clang++`编译器作为编译过程中使用的编译器。
```shell
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="/home/ricardo/.local/share/llvm-20.1.0" -DLLVM_ENABLE_PROJECTS="llvm;clang;mlir" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_INSTALL_UTILS=true ../llvm
```
![image-20250319192618697](./mlir-standalone/image-20250319192618697.png)
生成构建文件之后使用`ninja`进行构建。
```shell
ninja
```
![image-20250319194742171](./mlir-standalone/image-20250319194742171.png)
构建在我的i5-13600K上大约需要20分钟。
构建完成之后进行安装。
```shell
ninja install
```
## 编译MLIR官方的独立项目
MLIR的官方提供了一个独立项目项目文件夹在`mlir/examples/standalone`中,将这个文件夹中的内容复制到我们需要的地方,尝试使用上面构建的`mlir`进行构建。
```shell
cp -r ~/Downloads/llvm-project-20.1.0.src/mlir/examples/standalone mlir-standalone
cd mlir-standalone
```
### 不启用测试
编译过程中可能遇到的最大问题是`llvm-lit`,这个使用`python`编写的LLVM集成测试工具`standalone``README.md`中要求编译过程中使用`LLVM_EXTERNAL_LIT`变量指定到LLVM编译过程中生成的`llvm-lit`可执行文件。
> 也许就是因为`llvm-lit`是用Python撰写的所以`llvm-lit`不会安装到`PREFIX`指定的位置。
不过我们可以禁用测试(笑)。在`CMakeLists.txt`文件中注释对于测试文件夹的添加:
```cmake
add_subdirectory(include)
add_subdirectory(lib)
if(MLIR_ENABLE_BINDINGS_PYTHON)
message(STATUS "Enabling Python API")
add_subdirectory(python)
endif()
#add_subdirectory(test)
add_subdirectory(standalone-opt)
add_subdirectory(standalone-plugin)
add_subdirectory(standalone-translate)
```
回到构建文件夹,使用如下的`cmake`指令生成构建文件。
```shell
export LLVM_DIR=/home/ricardo/.local/share/llvm-20.1.0
cmake -G Ninja -DMLIR_DIR=$LLVM_DIR/lib/cmake/mlir ..
```
可以顺利通过编译。
![image-20250319202218503](./mlir-standalone/image-20250319202218503.png)
### 启用测试
但是测试还是非常重要的。我们尝试启动测试看看,取消对于测试文件夹的注释:
```shell
rm -rf build && mkdir build && cd build
cmake -G Ninja -DMLIR_DIR=$LLVM_DIR/lib/cmake/mlir ..
```
很好顺利报错,报错的提示是缺失`FileCheck``count``not`
![image-20250319202553644](./mlir-standalone/image-20250319202553644.png)
那么按照`README.md`中的提示添加上来自构建目录的`llvm-lit`会怎么样呢?
```shell
rm -rf build && mkdir build && cd build
export LLVM_BUILD_DIR=/home/ricardo/Downloads/llvm-project-20.1.0.src/build
cmake -G Ninja -DMLIR_DIR=$LLVM_DIR/lib/cmake/mlir -DLLVM_EXTERNAL_LIT=$LLVM_BUILD_DIR/bin/llvm-lit ..
```
同样的报错,看来问题不是出在这里。
经过对于LLVM文档的仔细研究发现原来是没有启动这个变量
![image-20250319204057832](./mlir-standalone/image-20250319204057832.png)
遂修改最初的LLVM编译指令。
重新运行来自`README.md`的构建文件生成指令之后,测试也完美运行通过:
```shell
ninja test-standalone
```
![image-20250319204522857](./mlir-standalone/image-20250319204522857.png)
不过这个还是有一点令我不是特别满意,这依赖了一个来自于构建目录的工具`llvm-lit`,如果我编译安装的时候眼疾手快的删除了编译目录不就完蛋了。而且我都**standalone**了还依赖似乎有点说不过去?
于是发现了一篇从`pip`上下载使用`llvm-lit`的[博客](https://medium.com/@mshockwave/using-llvm-lit-out-of-tree-5cddada85a78)和一个LLVM Discourse上面的[帖子](https://discourse.llvm.org/t/running-llvm-lit-on-external-project-test-file-derived-from-standalone-fails/67787),遂进行尝试。
首先在当前目录下创建一个虚拟环境,并下载安装`llvm-lit`
```shell
python -m venv .llvm-lit
source .llvm-lit/bin/activate
pip install lit
```
~~不过这个库似乎没有提供运行入口点,需要我们手动创建一个可执行的`python`文件:~~
```python
#!/usr/bin/env python
from lit.main import main
if __name__ == '__main__':
main()
```
经哥们纠正说`lit`包在某个版本之后会安装`lit`的可执行文件,在安装之后可以直接在命令行调用`lit`。因此在激活虚拟环境之后,`cmake`中直接在`LLVM_EXTERNAL_LIT`配置为`$(which lit)`即可。
```shell
cmake -G Ninja -DMLIR_DIR=$LLVM_DIR/lib/cmake/mlir -DLLVM_EXTERNAL_LIT=$(which lit) ..
ninja test-standalone
```
![image-20250319205520649](./mlir-standalone/image-20250319205520649.png)

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

@@ -1,117 +0,0 @@
---
title: 如何在MSBuild中复制生成的文件到发布目录中
date: 2025-03-20T22:33:21.6955385+08:00
tags:
- 技术笔记
- dotnet
---
如何使用`MSBuild`将构建过程中生成文件复制到生成目录中?
<!--more-->
### 遇到的问题
最近在尝试在`blazor`项目中使用`tailwindcss`作为`css`工具类的提供工具,而不是使用老旧的`bootstrap`框架,不过使用`tailwindcss`需要在项目构建时使用`tailwindcss`工具扫描文件中使用到的`css`属性并生成最终的`css`文件,这就带来了在构建时运行`tailwindcss`生成并复制到输出目录的需求。
由于我是使用`pnpm`作为前端管理工具,我在项目的`csproj`文件中添加了下面的`Target`来生成文件:
```xml
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css"/>
</Target>
```
这套生成逻辑在本地工作良好但是却在CI上运行时出现了问题`CI`上打包的`Docker`镜像中没有`tailwind.g.css`文件,导致最终部署的站点丢失了所有的格式。
### 产生问题的原因
经过反复实验,我发现只有在构建之前`wwwroot`目录中已经存在`tailwind.g.css`文件的情况下,`MSBuild`才会将生成的文件复制到最终的输出目录中。但是在`CI`环境下,因为使用`.gitignore`没有将`*.g.css`文件添加到代码管理,因此`CI`运行构建之前没有该文件,因此构建的结果中也没有该文件。
仔细研究`MSBuild`的文档和网络上的[分享](https://gist.github.com/BenVillalobos/c671baa1e32127f4ab582a5abd66b005),我意识到这是由于`MSBuild`的构建流程导致的MSBuild`的构建流程分成两个大的阶段:
- 评估阶段Evaluation Phase
在这个阶段,`MSBuild`将会运行读取所有的配置文件,创建需要的属性,展开所有的`glob`,建立好整个构建流程。
- 执行阶段Execution Phase
在这个阶段,`MSBuild`将按照上一阶段执行的属性执行实际的构建指令。
这两个阶段的划分就导致在生成阶段才生成的文件不会被包含在复制文件的指令中,因此他们不会被拷贝到最终的输出目录中。
>这和`cmake`的构建过程很像,首先调用`cmake`生成一些构建指令,在调用实际的构建指令构建二进制文件。
因此这类问题的推荐解决办法是手动将这些文件添加到构建流程中,即在`BeforeBuild`目标调用之前使用`Content`和`None`等项。
### 解决问题
总结上述的解决问题方法,我在上面的构建流程中添加了如下的`None`项:
```xml
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css"/>
<!-- Make sure generated file will be copied to output directory-->
<ItemGroup>
<Content Include="wwwroot/tailwind.g.css" Visible="false" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
</Target>
```
在运行构建之后,在最终的`publish`文件夹的`wwroot`文件夹中就可以找到`tailwind.g.css`文件。
不过我还想进行一点优化,`MSBuild`文档中建议将自动生成的文件放在`IntermediateOutputPath`,也就是`obj`文件加中,因此这里尝试将`tailwind.g.css`文件生成到`IntermediateOuputPath`中,优化之后的`Target`项长这个样子:
```xml
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
<!-- Make sure generated file will be copied to output directory-->
<ItemGroup>
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
</ItemGroup>
</Target>
```
经过测试,这套生成逻辑在`blazor`类库环境下也可以正常运行,类库的文件会被正确地生成到`wwwroot/_content/<ProjectName>/`文件夹下面。

View File

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

Binary file not shown.

BIN
YaeBlog/wwwroot/fonts/bootstrap-icons.woff2 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1 +0,0 @@
@import "tailwindcss";