Compare commits

..

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
303 changed files with 1440 additions and 2341 deletions

2
.gitattributes vendored
View File

@ -1,4 +1,2 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.avif filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text

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 --build-arg COMMIT_ID=$(git rev-parse --short=10 HEAD)
- 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

@ -7,15 +7,11 @@
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
</p>
<p class="text-md">
Build Commit #
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
</p>
</div>
<div>
<p class="text-md">
<Anchor Address="https://beian.miit.gov.cn" Text="蜀ICP备2022004429号-1" NewPage="true"/>
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
</p>
</div>
</div>
@ -23,8 +19,4 @@
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
private string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
private string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
}

View File

@ -1,8 +1,5 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0
ARG COMMIT_ID
ENV COMMIT_ID=${COMMIT_ID}
WORKDIR /app
COPY bin/Release/net9.0/publish/ ./
COPY source/ ./source/

View File

@ -1,12 +0,0 @@
namespace YaeBlog.Core.Exceptions;
public class BlogCommandException : Exception
{
public BlogCommandException(string message) : base(message)
{
}
public BlogCommandException(string message, Exception innerException) : base(message, innerException)
{
}
}

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
public partial class EssayScanService(
ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
ILogger<EssayScanService> logger) : IEssayScanService
{
private readonly BlogOptions _blogOptions;
private readonly ISerializer _yamlSerializer;
private readonly IDeserializer _yamlDeserializer;
private readonly ILogger<EssayScanService> _logger;
public EssayScanService(ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
ILogger<EssayScanService> logger)
{
_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)
{
ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = [];
Parallel.ForEach(contents, content =>
{
string imageName = match.Groups[1].Value;
MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
// 判断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));
Dictionary<string, bool> usedDictionary;
if (usedImages.TryGetValue(usedFile.FullName, out _))
if (imageDirectory.Exists)
{
usedImages[usedFile.FullName] = true;
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();
foreach (Match match in result)
{
string imageName = match.Groups[1].Value;
return new ImageResult(images, notFoundImages);
}
FileInfo usedFile = imageName.Contains(content.FileName)
? new FileInfo(Path.Combine(root.FullName, imageName))
: new FileInfo(Path.Combine(root.FullName, content.FileName, imageName));
private static async Task<BlogImageInfo> GetImageInfo(FileInfo file, bool isUsed)
{
byte[] image = await File.ReadAllBytesAsync(file.FullName);
if (usedDictionary.TryGetValue(usedFile.FullName, out _))
{
usedDictionary[usedFile.FullName] = true;
}
else
{
notFoundImage.Add(usedFile);
}
}
if (file.Extension is ".jpg" or ".jpeg" or ".png")
{
ImageInfo imageInfo =
await ImageJob.GetImageInfoAsync(MemorySource.Borrow(image), SourceLifetime.NowOwnedAndDisposedByTask);
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
{
unusedImage.Add(new FileInfo(pair.Key));
}
});
return new BlogImageInfo(file, imageInfo.ImageWidth, imageInfo.ImageWidth, imageInfo.PreferredMimeType,
image, isUsed);
}
return new BlogImageInfo(file, 0, 0, file.Extension switch
{
"svg" => "image/svg",
"avif" => "image/avif",
_ => string.Empty
}, image, isUsed);
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,119 +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))))).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,
MineType = "image/webp"
}).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(BlogImageInfo image)
{
using ImageJob job = new();
BuildJobResult result = await job.Decode(MemorySource.Borrow(image.Content))
.Branch(f => f.EncodeToBytes(new WebPLosslessEncoder()))
.EncodeToBytes(new WebPLossyEncoder(75))
.Finish()
.InProcessAsync();
// 超过128KB的图片使用有损压缩
// 反之使用无损压缩
ArraySegment<byte>? losslessImage = result.TryGet(1)?.TryGetBytes();
ArraySegment<byte>? lossyImage = result.TryGet(2)?.TryGetBytes();
if (image.Size <= 128 * 1024 && losslessImage.HasValue)
{
return losslessImage.Value.ToArray();
}
if (lossyImage.HasValue)
{
return lossyImage.Value.ToArray();
}
throw new BlogCommandException($"Failed to convert {image.File.Name} to webp format: return value is null.");
}
}

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,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="ImageFlow.NativeRuntime.ubuntu-x86_64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('Linux'))"/>
<PackageReference Include="ImageFlow.NativeRuntime.osx-arm64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('OSX'))"/>
<PackageReference Include="ImageFlow.Net" Version="0.13.2"/>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
@ -16,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"/>
@ -28,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",
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css -w"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0",
"@tailwindcss/cli": "^4.0.0"
}
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.4.16"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
---
title: 2021年终总结
date: 2022-01-12T16:27:19.0000000
date: 2022-01-12 16:27:19
tags:
- 杂谈
- 年终总结
- 随笔
---
2021年已经过去2022年已经来临。每每一年开始的时候我都会展开一张纸或者新建一个文档思量着又是一年时光也该同诸大杂志一般写几句意味深长的话语怀念过去的时光也祝福未来的自己。可往往脑海中已是三万字的长篇落在笔头却又是一个字都没有了。
如今跨年的时候已经过去朋友圈中已经不见文案的踪影我也该重新提笔细说自己2021年中做过的种种。
@ -24,7 +22,7 @@ tags:
在前12年的学生生涯中我们都在期待着这一次的暑假以为在这个没有作业的假期里我们就可以充分的享受人间的美好。可是当时我们不知道这人间的烦恼可不止作业这一种无论是突如其来的疫情导致开学延期还是等待录取时的不安。
虽说在暑假时,拥有了自己的笔记本电脑,可是在高中三年屯下的游戏还是没有玩几个,看来我也是“喜加一”的受害者。虽然在高考后入坑了原神,但是假期间我并没有太过投入的玩。
暑假下定决心要好好的学一学可是看着我gitee上暑假期间那稀疏的提交我就知道我又摸了一个暑假的鱼。
![gitee贡献](./2021-final/1.webp)
![gitee贡献](./2021-final/1.png)
即使我想写的很多项目都没有被扎实的推进下来但是学习的一些的C语言还是让我受益匪浅。
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。

BIN
YaeBlog/source/posts/2021-final/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/source/posts/2021-final/1.webp (Stored with Git LFS)

Binary file not shown.

View File

@ -1,13 +1,11 @@
---
title: 2022年终总结
date: 2022-12-30T14:58:12.0000000
tags:
- 杂谈
- 年终总结
- 随笔
date: 2022-12-30 14:58:12
---
2022是困难的一年。我们需要为2023年做好准备。
<!--more-->
@ -58,11 +56,11 @@ tags:
小小的总结一下2022年可以算得上是一事无成的一年还搞砸了不少的事情。在写代码上进展有限成绩上大幅倒退说好的六级英语和大学物理竞赛都没有参加在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
![](./2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp)
![](./2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg)
关于今年的人际交往和社会关系我愿意用QQ2022年年终总结中的一张截屏来总结这张图片透漏出一种无可救药的悲伤。
![](./2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp)
![](./2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg)
## 展望

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,11 @@
---
title: 2022年暑假碎碎念
date: 2022-08-22T15:39:13.0000000
tags:
- 杂谈
- 随笔
typora-root-url: 2022-summer-vacation
date: 2022-08-22 15:39:13
---
在8个月的漫长寒假的最后两个月~~也就是俗称的暑假中~~,我都干了些什么?
<!--more-->
@ -32,7 +32,7 @@ tags:
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
![唯一的一张成品](result1.webp)
![唯一的一张成品](result1.png)
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。

BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,10 @@
---
title: 2023年年终总结
date: 2024-02-29T20:18:19.0000000
tags:
- 杂谈
- 年终总结
- 随笔
date: 2024-2-29 20:18:19
---
虽然2023年已经过去了两个月但是年终总结还是要发的。
<!--more-->
@ -45,7 +43,7 @@ tags:
2023年最令我吃惊的事情是我刷B站的时长
![image-20240303165826486](2023-final/image-20240303165826486.webp)
![image-20240303165826486](2023-final/image-20240303165826486.png)
容易计算得出我一共看了64天的B站接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变将更多的时间放在看书上面去~~虽然写这句话的时候我就在黑听B站~~。

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

Binary file not shown.

Binary file not shown.

View File

@ -6,7 +6,6 @@ tags:
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
@ -71,7 +70,7 @@ tags:
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.webp)
![image-20250115171809775](./2024-final/image-20250115171809775.png)
### 未来

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

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,11 @@
---
title: 人生代码大作业初体验
date: 2022-07-27T11:34:49.0000000
tags:
- 杂谈
- 随笔
typora-root-url: big-homework
date: 2022-07-27 11:34:49
---
在大学也呆了一年了,终于遇上了第一个需要多人合作的写代码项目。从四月底分组完成,任务部署下来到七月初接近尾声,在这两个多月的时间里,也算是经历了不少,学到了不少。
<!--more-->
@ -44,7 +44,7 @@ tags:
而且采用 `Git`还有一个好处,采用 `Github``Insight`功能可以轻松的看出大家的贡献值()。
![img](1.webp)
![img](1.png)
## 一些技术上的收获

BIN
YaeBlog/source/posts/big-homework/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/source/posts/big-homework/1.webp (Stored with Git LFS)

Binary file not shown.

View File

@ -1,12 +1,12 @@
---
title: 建立博客过程的记录
date: 2022-04-08T11:52:32.0000000
typora-root-url: 建立博客过程的记录
date: 2022-04-08 11:52:32
tags:
- 技术笔记
- 技术笔记
---
当我已经在Python的浩瀚大海遨zhengzha了半个暑假后我决定尝试一下传说中程序员专用的学(zhuang)习(bi)手(fangfa)段(fa)——建立自己的个人博客。作为一个半懂不懂的Python程序员心中冒出的第一个想法自然是采用Python的Django作为开发自己的个人博客的手段。然而在阅读了[用Django搭建个人博客](https://www.dusaiphoto.com/article/2/)等的其他人搭建这类动态博客的过程记录之后我便义无反顾的转向了采用javascript开发的博客框架[Hexo](https://hexo.io)<del>说好的Python信仰呢</del>。无他,唯简单尔。
<!--more-->
@ -131,7 +131,7 @@ Hexo init blog
```
Hexo会以blog为名称创建一个博客文件夹这个文件夹的内容为
![文件夹截图](1.webp)
![文件夹截图](1.png)
`node_modules`文件夹是Hexo需要用到的一些npm依赖包的存放地址`public`文件夹下是由Hexo渲染产生的静态博客文件`scaffolds`文件夹是博客用到的模板文件,在默认情况下应该有`draft.md`,`page.md`,`post.md`三个模板文件。`themes`是Hexo中可以使用的主题文件。主题也是Hexo一个非常方便的设计我们可以方便使用其他人编写的Hexo Themes让自己的博客在不同的风格之间变换。`source`文件夹就是存放我们写作的博客的地方。一般这里面会有两个子文件夹,`_draft`, `_posts`。我们在里面在创建一个`img`文件夹,把自己的头像图片和网站的图标文件都放在里面,在之后的设置的时候使用。
@ -146,7 +146,7 @@ INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.
会在本地运行Hexo自带的一台静态博客服务器。我们用浏览器访问http://localhost:4000, 就可以看见Hexo博客的初始界面
![初始截图](2.webp)
![初始截图](2.png)
这便说明安装成功了,~~可以开香槟了~~

BIN
YaeBlog/source/posts/build-blog-record/1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
YaeBlog/source/posts/build-blog-record/2.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -7,7 +7,6 @@ tags:
---
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
<!--more-->
@ -46,7 +45,7 @@ tags:
通常一份GNU工具链只能针对一个平台进行编译但是LLVM工具链是一套先天的交叉编译工具链例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.webp)
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.png)
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
@ -63,7 +62,7 @@ int main()
}
```
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.webp)
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.png)
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译因此尝试在本地自行编译一套LLVM工具链。
@ -82,7 +81,7 @@ cmake ../llvm-project.src/llvm \
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前因此调用时将会优先调用我们自行编译的LLVM工具链而不是系统中安装的LLVM工具链。
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.webp)
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.png)
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装LLVM工具链的问题。仔细一想也确实这里提示找不到对应的头文件应该是找不到RISC-V架构之下的头文件——这里的也是交叉编译的主要问题所在虽然LLVM工具链宣称自己是原生支持交叉编译的但是没人宣称说标准库和头文件是原生的。这里我们就需要一个根文件系统来提供这些头文件和各种库文件。
@ -199,7 +198,7 @@ clang++ --target=riscv64-linux-gnu --sysroot=$ROOTFS_DIR -fuse-ld=lld hello.cpp
第一个问题的回答是Arch Linux安装的LLVM工具是可以交叉编译的。虽然在Arch Linux官方构建LLVM工具链的[构建脚本](https://gitlab.archlinux.org/archlinux/packaging/packages/clang/-/blob/main/PKGBUILD?ref_type=heads)中没有使用`LLVM_TARGETS_TO_BUILD`参数,但是这个参数的默认值是`all`。这一点我们也可以通过实验来验证。
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.webp)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`。
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.png)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`。
第二个问题的回答可以使用实验来验证,首先安装`riscv64-linux-gnu-gcc`,然后将根文件系统的位置设置为`/usr/riscv64-linux-gnu`,重新编译上面的你好世界样例。编译之后可以正常执行。
@ -230,4 +229,4 @@ export ROOTFS_DIR=<rootfs>
但是现在的.NET在RISC-V平台上还是废物一个甚至连`dotnet new`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.webp)
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.png)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,12 @@
---
title: C项目中有关头文件的一些问题
date: 2022-05-08T11:35:19.0000000
tags:
- 技术笔记
- C/C++
- 技术笔记
- C/C++
typora-root-url: c-include-problems
date: 2022-05-08 11:35:19
---
最近在完成一门`C`语言课程的大作业,课设老师要求我们将程序分模块的开发。在编写项目头文件的时候,遇到了一些令本菜鸡大开眼界的问题。
<!--more-->
@ -17,7 +17,7 @@ tags:
我项目的结构大致如图所示:
![](1.webp)
![](1.png)
在`include`的头文件目录下有两个头文件,`rail.h`和`bus.h`,这两个头文件分别定义了两个结构体`rail_node_t`和`bus_t`。
@ -68,7 +68,7 @@ typedef struct bus bus_t;
项目的`test`文件夹下是单元测试文件夹,但是在编译的时候会报错
![](2.webp)
![](2.png)
大意就是在一个google test内部的头文件中有几个函数找不到定义这个函数都位于`io.h`这个头文件中。

BIN
YaeBlog/source/posts/c-include-problems/1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
YaeBlog/source/posts/c-include-problems/2.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -5,14 +5,13 @@ tags:
- 杂谈
---
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办而鄙人在下不才我有幸受到实验室资助前去参观学习。
<!--more-->
首先开幕式镇楼。
![image-20241102212738598](./cncc-2024/image-20241102212738598.webp)
![image-20241102212738598](./cncc-2024/image-20241102212738598.png)
## 学术上
@ -22,11 +21,11 @@ tags:
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路通过三层架构基于互联池化技术构建AI时代的融合算力系统。系统中的三层包括1池化基础底层包括多设备的融合和池化设备虚拟化2异构融合核心子系统例如异构融合调度系统、异构融合内存和异构融合存储系统3异构核心服务。总的来说这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能但是显然这一操作系统的实现还存在着明显的困难。
![image-20241102211959206](./cncc-2024/image-20241102211959206.webp)
![image-20241102211959206](./cncc-2024/image-20241102211959206.png)
下面一个报告是较为有干货的报告北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性指出分布式共享内存Distributed Shared Memory可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意但是现在总结的时候才发现这个报告的思路似乎有点混乱尤其是最后一点和内存管理系统并没有什么直接的关系而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
![image-20241102212355390](./cncc-2024/image-20241102212355390.webp)
![image-20241102212355390](./cncc-2024/image-20241102212355390.png)
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此提出了一个智能模型训练并行任务划分方法1基于符号算子的计算图定义方法2面向Transformer模型的流水线并行任务划分方法3异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术针对现有重计算技术re-computing和存储交换swapping技术存在的问题提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`。
@ -50,7 +49,7 @@ Plane讨论没有参加。
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛例如`triton`。
![image-20241102212536635](./cncc-2024/image-20241102212536635.webp)
![image-20241102212536635](./cncc-2024/image-20241102212536635.png)
第三个报告是在存算一体的芯片上做数据库的加速第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,11 @@
---
title: 编译MediaPipe框架
date: 2022-11-11T22:20:25.0000000
tags:
- C/C++
- 技术笔记
- C/C++
- 技术笔记
date: 2022-11-11 22:20:25
---
编译MediaPipe框架。
<!--more-->
@ -199,7 +198,7 @@ bazel build -c opt --strip=ALWAYS \
如果在编译的过程中提示缺失`dx.jar`这个文件而且你用的SDK版本还是高于31的那可能是SDK中缺失了这个文件可以将SDk降级到30就含有这个文件了。我使用的解决办法比较离奇我是将30版本的SDK文件中的这个文件软链接过来解决了这个问题。
![](compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.webp)
![](compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png)
编译消耗的时间可能比较的长,耐心等待即可。
@ -228,7 +227,7 @@ bazel build -c opt //mediapipe/graphs/pose_tracking:pose_tracking_gpu_binary_gra
然后还需要从服务器上下载`tflite`文件,`Pose Tracking`这个解决方案需要两个`tflite`文件,第一个是[pose_detection.tflite](https://storage.googleapis.com/mediapipe-assets/pose_detection.tflite),第二个文件则有三个不同的选择,分别对于解决方案中提供的三个质量版本:
![](compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.webp)
![](compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png)
下载地址是[pose_landmark_full.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_full.tflite)[pose_landmark_heavy.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_heavy.tflite)和[pose_landmark_lite.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_lite.tflite)。

View File

@ -1,21 +1,20 @@
---
title: 计算机系统结构——流水线复习
date: 2024-06-12T20:27:25.0000000
tags:
- 计算机系统结构
- 学习资料
- 计算机系统结构
- 学习资料
date: 2024-06-12 20:27:25
---
让指令的各个执行阶段依次进行运行是一个简单而自然的想法,但是这种方式执行速度慢、运行效率低。因此一个很自然的想法就是将指令重叠起来运行,让执行功能部件被充分的利用起来,这就是**流水线**。
流水线的表示方法有两种。
![image-20240612184855300](computer-architecture-pipeline/image-20240612184855300.webp)
![image-20240612184855300](computer-architecture-pipeline/image-20240612184855300.png)
第一种被称作**连接图**,清晰的表达出了流水线内部的逻辑关系。
![image-20240612184949777](computer-architecture-pipeline/image-20240612184949777.webp)
![image-20240612184949777](computer-architecture-pipeline/image-20240612184949777.png)
> 上图中给出了两个流水线中的概念:通过时间和排空时间。其中通过时间又被称作装入时间,是指第一个任务进入流水线到完成的事件;排空时间则相反,是最后一个任务通过流水线的时间。
@ -41,7 +40,7 @@ tags:
- 静态流水线,同一时间内,多功能流水线的各段只能按照同一种功能的方式连接。
- 动态流水线,同一时间内,多功能流水线的各种可以按照不同的方式连接,执行不同的功能。
![image-20240612190426368](computer-architecture-pipeline/image-20240612190426368.webp)
![image-20240612190426368](computer-architecture-pipeline/image-20240612190426368.png)
按照流水线中是否存在反馈回路分类:
@ -59,7 +58,7 @@ tags:
- 加速比,同一任务,不使用流水线所使用时间与使用流水线所用时间比。
- 效率,流水线设备的利用率。
![image-20240612192700169](computer-architecture-pipeline/image-20240612192700169.webp)
![image-20240612192700169](computer-architecture-pipeline/image-20240612192700169.png)
在设计流水线的过程中存在若干问题。
@ -69,7 +68,7 @@ tags:
一个典型的五段流水线MIPS流水线
![image-20240612193301372](computer-architecture-pipeline/image-20240612193301372.webp)
![image-20240612193301372](computer-architecture-pipeline/image-20240612193301372.png)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,12 +1,12 @@
---
title: 日用Linux挑战 第0篇 初见Arch Linux
date: 2023-01-15T22:23:08.0000000
tags:
- Linux
- 杂谈
- Linux
- 随笔
date: 2023-01-15 22:23:08
typora-root-url: daily-linux-0
---
在将开发重心移到`WSL`上一年之后我最终还是决定完全抛弃Windows转向使用Linux作为我日常使用的主力系统。目前我已经使用Linux作为主力系统一个月了。
<!--more-->
@ -92,7 +92,7 @@ sudo systemctl enable sddm.service
我目前实现的效果大概长这样:
![](2023-01-12-13-28-38-Screenshot_20230112_132829.webp)
![](2023-01-12-13-28-38-Screenshot_20230112_132829.png)
颇有一种`Windows`和`MacOS`杂交的风格。
@ -106,7 +106,7 @@ sudo systemctl enable sddm.service
先上一张`shell`的系统概览截图:
![](2023-01-12-13-36-45-Screenshot_20230112_133628.webp)
![](2023-01-12-13-36-45-Screenshot_20230112_133628.png)
终端模拟器直接使用的`konsole`,目前没有进行改动。

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,13 +1,13 @@
---
title: 日用Linux挑战 第2篇 Wayland
date: 2023-07-23T11:44:34.0000000
tags:
- 杂谈
- Linux
- 随笔
- Linux
date: 2023-07-23 11:44:34
typora-root-url: daily-linux-2
---
使用`Linux`6个月我成功戒掉了原神。
<!--more-->
@ -18,7 +18,7 @@ tags:
最近恰好被平铺式的窗口管理器种草又在B站上看见一个动画绚丽的`wayland`合成器——[Hyprland](https://hyprland.org/),当即脑袋一热,就把`kde`干掉,装上了`hyprland`。
![img](df4211f6be2724b3b4725f7ce5a4078818844857.avif)
![img](df4211f6be2724b3b4725f7ce5a4078818844857.jpg)
安装`hyprland`的过程非常舒适,`hyprland`被打包为一个单独的二进制文件,使用`pacman`安装之后直接在`tty`下执行:
@ -46,7 +46,7 @@ Hyprland
各种在学习过程中遇到的工具软件基本上都工作运行良好。当然因为没有设置缩放的问题而导致字体都很小。因为如果在配置文件中设置缩放之后会导致字体发虚。下面的截图就是我将我的2K显示屏设置为150%缩放的效果,~~虽然在截图中的效果不明显~~。目前在常用软件中唯一让我十分不满意的软件是`wps`,使用体验完全无法和`offices`相提并论,目前我正在研究使用`wine`运行`offices`,如果成功了就再水一篇博客庆祝一下。
![image-20230702205919301](image-20230702205919301.webp)
![image-20230702205919301](image-20230702205919301.png)
> 最新的进展是使用`wine`没法安装学校提供的`office 2021`,同时我又不愿意使用古老的`office`版本,但是我发现一个称作`onlyoffice`的第三方软件蛮好用的,等我试用一段时间再说。
>

Binary file not shown.

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