Compare commits
42 Commits
4085b0d99c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
58ba4b2a2f
|
|||
|
009e86b553
|
|||
|
d1ec3a51d1
|
|||
|
dab866f13a
|
|||
| 94421168c6 | |||
| 938fe1c715 | |||
| eedfc1ffce | |||
| 0f346d9ded | |||
| a662ecc14b | |||
| a254d0123d | |||
| 22d28e763d | |||
| d0a4f4b76b | |||
| 3126005731 | |||
| 2b9c374e8c | |||
| 4df3b98e6d | |||
| c293d2f6d7 | |||
| 132261831b | |||
| 043376c6d3 | |||
| 4682dacc79 | |||
| 383dd41695 | |||
| 7f3221fde9 | |||
| 05ea729950 | |||
| dcad453eb1 | |||
| dec2bc937a | |||
| b9c44408ad | |||
| e1c5362cf5 | |||
| bdcfed5506 | |||
| 3aae468e65 | |||
| 1ceaf30061 | |||
| 87204dab8e | |||
| 05d40ce3b6 | |||
| 309db7e5f1 | |||
| fb71ce64cf | |||
| d87e3830a5 | |||
| 32104bbfb8 | |||
| f77d2a47d1 | |||
| f6e43f466d | |||
| f889d51857 | |||
| 45c35d7442 | |||
| 9ebacf87e5 | |||
| bb5cb77b7b | |||
| 933eec0f7f |
@@ -15,6 +15,9 @@ trim_trailing_whitespace = true
|
||||
[project.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
# C# and Visual Basic files
|
||||
[*.{cs,vb}]
|
||||
charset = utf-8-bom
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1,5 @@
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.avif filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
name: Build blog docker image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- uses: https://git.rrricardo.top/actions/checkout@v4
|
||||
name: Check out code
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project
|
||||
run: |
|
||||
cd YaeBlog
|
||||
dotnet publish
|
||||
- name: Build docker image
|
||||
run: |
|
||||
cd YaeBlog
|
||||
docker build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
- name: Login aliyun docker registry
|
||||
uses: https://git.rrricardo.top/actions/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-beijing.aliyuncs.com
|
||||
username: 初冬的朝阳
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
- name: Push docker image
|
||||
run: docker push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- name: Check out code.
|
||||
uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project.
|
||||
run: |
|
||||
cd YaeBlog
|
||||
dotnet publish
|
||||
- name: Build docker image.
|
||||
run: |
|
||||
proxy
|
||||
podman pull mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
unproxy
|
||||
cd YaeBlog
|
||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$(git rev-parse --short=10 HEAD)
|
||||
- name: Workaround to make sure podman-login working.
|
||||
run: |
|
||||
mkdir /root/.docker
|
||||
- name: Login tencent cloud docker registry.
|
||||
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||
with:
|
||||
registry: ccr.ccs.tencentyun.com
|
||||
username: 100044380877
|
||||
password: ${{ secrets.TENCENT_REGISTRY_PASSWORD }}
|
||||
auth_file_path: /etc/containers/auth.json
|
||||
- name: Push docker image.
|
||||
run: podman push ccr.ccs.tencentyun.com/jackfiled/blog:latest
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -482,3 +482,6 @@ $RECYCLE.BIN/
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
|
||||
# Tailwind auto-generated stylesheet
|
||||
*.g.css
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class AboutInfo
|
||||
{
|
||||
public required string Introduction { get; set; }
|
||||
|
||||
public required string Description { get; set; }
|
||||
|
||||
public required string AvatarImage { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogContent
|
||||
{
|
||||
public required string FileName { get; init; }
|
||||
|
||||
public required MarkdownMetadata Metadata { get; init; }
|
||||
|
||||
public required string FileContent { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
|
||||
{
|
||||
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
|
||||
|
||||
public ConcurrentBag<BlogContent> Posts { get; } = posts;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
||||
@@ -1,29 +0,0 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
|
||||
{
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument document = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
IEnumerable<IElement> preElements = from e in document.All
|
||||
where e.LocalName == "pre"
|
||||
select e;
|
||||
|
||||
foreach (IElement element in preElements)
|
||||
{
|
||||
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
|
||||
}
|
||||
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(CodeBlockPostRenderProcessor);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
public class TablePostRenderProcessor: IPostRenderProcessor
|
||||
{
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext browsingContext = new(Configuration.Default);
|
||||
IDocument document = await browsingContext.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
|
||||
where item.LocalName == "table"
|
||||
select item as IHtmlTableElement;
|
||||
|
||||
foreach (IHtmlTableElement element in tableElements)
|
||||
{
|
||||
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
|
||||
divElement.InnerHtml = element.OuterHtml;
|
||||
divElement.ClassList.Add("py-2", "table-wrapper");
|
||||
|
||||
element.Replace(divElement);
|
||||
}
|
||||
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(TablePostRenderProcessor);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public sealed class BlogHotReloadService(
|
||||
RendererService rendererService,
|
||||
IEssayContentService essayContentService,
|
||||
BlogChangeWatcher watcher,
|
||||
ILogger<BlogHotReloadService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("BlogHotReloadService is starting.");
|
||||
|
||||
await rendererService.RenderAsync();
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug("Watching file changes...");
|
||||
string? changFile = await watcher.WaitForChange(stoppingToken);
|
||||
|
||||
if (changFile is null)
|
||||
{
|
||||
logger.LogInformation("BlogHotReloadService is stopping.");
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogInformation("{} changed, re-rendering.", changFile);
|
||||
essayContentService.Clear();
|
||||
await rendererService.RenderAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public partial class EssayScanService(
|
||||
ISerializer yamlSerializer,
|
||||
IDeserializer yamlDeserializer,
|
||||
IOptions<BlogOptions> blogOptions,
|
||||
ILogger<EssayScanService> logger) : IEssayScanService
|
||||
{
|
||||
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||
|
||||
public async Task<BlogContents> ScanContents()
|
||||
{
|
||||
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||
|
||||
return new BlogContents(
|
||||
await ScanContentsInternal(drafts),
|
||||
await ScanContentsInternal(posts));
|
||||
}
|
||||
|
||||
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
|
||||
{
|
||||
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||
|
||||
FileInfo targetFile = isDraft
|
||||
? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md"))
|
||||
: new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md"));
|
||||
|
||||
if (!isDraft)
|
||||
{
|
||||
content.Metadata.Date = DateTime.Now;
|
||||
}
|
||||
|
||||
if (targetFile.Exists)
|
||||
{
|
||||
logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
|
||||
}
|
||||
|
||||
await using StreamWriter writer = targetFile.CreateText();
|
||||
|
||||
await writer.WriteAsync("---\n");
|
||||
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
|
||||
await writer.WriteAsync("---\n");
|
||||
|
||||
if (isDraft)
|
||||
{
|
||||
await writer.WriteLineAsync("<!--more-->");
|
||||
}
|
||||
else
|
||||
{
|
||||
await writer.WriteAsync(content.FileContent);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
|
||||
{
|
||||
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
|
||||
where file.Extension == ".md"
|
||||
select file;
|
||||
|
||||
ConcurrentBag<(string, string)> fileContents = [];
|
||||
|
||||
await Parallel.ForEachAsync(markdownFiles, async (file, token) =>
|
||||
{
|
||||
using StreamReader reader = file.OpenText();
|
||||
fileContents.Add((file.Name, await reader.ReadToEndAsync(token)));
|
||||
});
|
||||
|
||||
ConcurrentBag<BlogContent> contents = [];
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach ((string filename, string content) in fileContents)
|
||||
{
|
||||
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
|
||||
if (!content.StartsWith("---") || endPos is -1 or 0)
|
||||
{
|
||||
logger.LogWarning("Failed to parse metadata from {}, skipped.", filename);
|
||||
return;
|
||||
}
|
||||
|
||||
string metadataString = content[4..endPos];
|
||||
|
||||
try
|
||||
{
|
||||
MarkdownMetadata metadata = yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
|
||||
logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename);
|
||||
|
||||
contents.Add(new BlogContent
|
||||
{
|
||||
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
|
||||
});
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
public async Task<ImageScanResult> ScanImages()
|
||||
{
|
||||
BlogContents contents = await ScanContents();
|
||||
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||
|
||||
List<FileInfo> unusedFiles = [];
|
||||
List<FileInfo> notFoundFiles = [];
|
||||
|
||||
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)
|
||||
{
|
||||
Regex imageRegex = ImageRegex();
|
||||
ConcurrentBag<FileInfo> unusedImage = [];
|
||||
ConcurrentBag<FileInfo> notFoundImage = [];
|
||||
|
||||
Parallel.ForEach(contents, content =>
|
||||
{
|
||||
MatchCollection result = imageRegex.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
|
||||
{
|
||||
usedDictionary = [];
|
||||
}
|
||||
|
||||
foreach (Match match in result)
|
||||
{
|
||||
string imageName = match.Groups[1].Value;
|
||||
|
||||
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 _))
|
||||
{
|
||||
usedDictionary[usedFile.FullName] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
notFoundImage.Add(usedFile);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
|
||||
{
|
||||
unusedImage.Add(new FileInfo(pair.Key));
|
||||
}
|
||||
});
|
||||
|
||||
return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList()));
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
|
||||
private static partial Regex ImageRegex();
|
||||
|
||||
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||
{
|
||||
root = Path.Combine(Environment.CurrentDirectory, root);
|
||||
DirectoryInfo rootDirectory = new(root);
|
||||
|
||||
if (!rootDirectory.Exists)
|
||||
{
|
||||
throw new BlogFileException($"'{root}' is not a directory.");
|
||||
}
|
||||
|
||||
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
|
||||
{
|
||||
throw new BlogFileException($"'{root}/drafts' not exists.");
|
||||
}
|
||||
|
||||
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
|
||||
{
|
||||
throw new BlogFileException($"'{root}/posts' not exists.");
|
||||
}
|
||||
|
||||
drafts = new DirectoryInfo(Path.Combine(root, "drafts"));
|
||||
posts = new DirectoryInfo(Path.Combine(root, "posts"));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||
<PackageReference Include="Markdig" Version="0.34.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
47
YaeBlog.sln
47
YaeBlog.sln
@@ -1,47 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
|
||||
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
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.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
|
||||
14
YaeBlog.slnx
Normal file
14
YaeBlog.slnx
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IEssayContentService
|
||||
{
|
||||
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
|
||||
public IEnumerable<BlogEssay> Essays { get; }
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
||||
|
||||
@@ -16,6 +18,8 @@ public interface IEssayContentService
|
||||
|
||||
public bool TryAdd(BlogEssay essay);
|
||||
|
||||
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
|
||||
|
||||
public void RefreshTags();
|
||||
|
||||
public void Clear();
|
||||
@@ -1,12 +1,10 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IEssayScanService
|
||||
{
|
||||
public Task<BlogContents> ScanContents();
|
||||
|
||||
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||
|
||||
public Task<ImageScanResult> ScanImages();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.CommandLine.Binding;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.CommandLine.Binding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
|
||||
21
YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
21
YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.CommandLine.Binding;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
public sealed class ImageCompressServiceBinder : BinderBase<ImageCompressService>
|
||||
{
|
||||
protected override ImageCompressService GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
bindingContext.AddService(provider =>
|
||||
{
|
||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||
ILogger<ImageCompressService> logger = provider.GetRequiredService<ILogger<ImageCompressService>>();
|
||||
|
||||
return new ImageCompressService(essayScanService, logger);
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<ImageCompressService>();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,35 @@
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Commands.Binders;
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Core.Extensions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Commands;
|
||||
|
||||
public static class CommandExtensions
|
||||
public sealed class YaeBlogCommand
|
||||
{
|
||||
public static void AddServeCommand(this RootCommand rootCommand)
|
||||
private readonly RootCommand _rootCommand = new("YaeBlog Cli");
|
||||
|
||||
public YaeBlogCommand()
|
||||
{
|
||||
AddServeCommand(_rootCommand);
|
||||
AddWatchCommand(_rootCommand);
|
||||
AddListCommand(_rootCommand);
|
||||
AddNewCommand(_rootCommand);
|
||||
AddPublishCommand(_rootCommand);
|
||||
AddScanCommand(_rootCommand);
|
||||
AddCompressCommand(_rootCommand);
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(string[] args)
|
||||
{
|
||||
return _rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static void AddServeCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command serveCommand = new("serve", "Start http server.");
|
||||
rootCommand.AddCommand(serveCommand);
|
||||
@@ -21,7 +41,6 @@ public static class CommandExtensions
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddServer();
|
||||
|
||||
@@ -40,7 +59,7 @@ public static class CommandExtensions
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddWatchCommand(this RootCommand rootCommand)
|
||||
private static void AddWatchCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -52,7 +71,6 @@ public static class CommandExtensions
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddWatcher();
|
||||
|
||||
@@ -71,7 +89,7 @@ public static class CommandExtensions
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddNewCommand(this RootCommand rootCommand)
|
||||
private static void AddNewCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command newCommand = new("new", "Create a new blog file and image directory.");
|
||||
rootCommand.AddCommand(newCommand);
|
||||
@@ -79,29 +97,27 @@ public static class CommandExtensions
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
||||
newCommand.AddArgument(filenameArgument);
|
||||
|
||||
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||
newCommand.SetHandler(async (file, blogOption, _, essayScanService) =>
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
if (contents.Posts.Any(content => content.FileName == file))
|
||||
if (contents.Posts.Any(content => content.BlogName == file))
|
||||
{
|
||||
Console.WriteLine("There exists the same title blog in posts.");
|
||||
return;
|
||||
}
|
||||
|
||||
await essayScanService.SaveBlogContent(new BlogContent
|
||||
{
|
||||
FileName = file,
|
||||
FileContent = string.Empty,
|
||||
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
|
||||
});
|
||||
await essayScanService.SaveBlogContent(new BlogContent(
|
||||
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
|
||||
new MarkdownMetadata { Title = file, Date = DateTime.Now },
|
||||
string.Empty, true, [], []));
|
||||
|
||||
Console.WriteLine($"Created new blog '{file}.");
|
||||
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
|
||||
new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddListCommand(this RootCommand rootCommand)
|
||||
private static void AddListCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("list", "List all blogs");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -111,20 +127,20 @@ public static class CommandExtensions
|
||||
BlogContents contents = await essyScanService.ScanContents();
|
||||
|
||||
Console.WriteLine($"All {contents.Posts.Count} Posts:");
|
||||
foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName))
|
||||
foreach (BlogContent content in contents.Posts.OrderBy(x => x.BlogName))
|
||||
{
|
||||
Console.WriteLine($" - {content.FileName}");
|
||||
Console.WriteLine($" - {content.BlogName}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
|
||||
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
|
||||
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName))
|
||||
{
|
||||
Console.WriteLine($" - {content.FileName}");
|
||||
Console.WriteLine($" - {content.BlogName}");
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddScanCommand(this RootCommand rootCommand)
|
||||
private static void AddScanCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("scan", "Scan unused and not found images.");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -135,37 +151,44 @@ public static class CommandExtensions
|
||||
|
||||
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
|
||||
{
|
||||
ImageScanResult result = await essayScanService.ScanImages();
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
List<BlogImageInfo> unusedImages = (from content in contents
|
||||
from image in content.Images
|
||||
where image is { IsUsed: false }
|
||||
select image).ToList();
|
||||
|
||||
if (result.UnusedImages.Count != 0)
|
||||
if (unusedImages.Count != 0)
|
||||
{
|
||||
Console.WriteLine("Found unused images:");
|
||||
Console.WriteLine("HINT: use '--rm' to remove unused images.");
|
||||
}
|
||||
|
||||
foreach (FileInfo image in result.UnusedImages)
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
Console.WriteLine($" - {image.FullName}");
|
||||
Console.WriteLine($" - {image.File.FullName}");
|
||||
}
|
||||
|
||||
if (removeOptionValue)
|
||||
{
|
||||
foreach (FileInfo image in result.UnusedImages)
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
image.Delete();
|
||||
image.File.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Used not existed images:");
|
||||
|
||||
foreach (FileInfo image in result.NotFoundImages)
|
||||
foreach (BlogContent content in contents)
|
||||
{
|
||||
Console.WriteLine($" - {image.FullName}");
|
||||
foreach (FileInfo file in content.NotfoundImages)
|
||||
{
|
||||
Console.WriteLine($"- {file.Name} in {content.BlogName}");
|
||||
}
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||
}
|
||||
|
||||
public static void AddPublishCommand(this RootCommand rootCommand)
|
||||
private static void AddPublishCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("publish", "Publish a new blog file.");
|
||||
rootCommand.AddCommand(command);
|
||||
@@ -178,7 +201,7 @@ public static class CommandExtensions
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = (from blog in contents.Drafts
|
||||
where blog.FileName == filename
|
||||
where blog.BlogName == filename
|
||||
select blog).FirstOrDefault();
|
||||
|
||||
if (content is null)
|
||||
@@ -187,14 +210,17 @@ public static class CommandExtensions
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置发布的时间
|
||||
content.Metadata.Date = DateTime.Now;
|
||||
|
||||
// 将选中的博客文件复制到posts
|
||||
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||
|
||||
// 复制图片文件夹
|
||||
DirectoryInfo sourceImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
|
||||
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
|
||||
DirectoryInfo targetImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
|
||||
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
|
||||
|
||||
if (sourceImageDirectory.Exists)
|
||||
{
|
||||
@@ -208,9 +234,30 @@ public static class CommandExtensions
|
||||
}
|
||||
|
||||
// 删除原始的文件
|
||||
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
|
||||
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md"));
|
||||
sourceBlogFile.Delete();
|
||||
}, new BlogOptionsBinder(),
|
||||
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
|
||||
}
|
||||
|
||||
private static void AddCompressCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
|
||||
rootCommand.Add(command);
|
||||
|
||||
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
|
||||
getDefaultValue: () => false);
|
||||
command.AddOption(dryRunOption);
|
||||
|
||||
command.SetHandler(ImageCommandHandler,
|
||||
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
|
||||
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
|
||||
}
|
||||
|
||||
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
|
||||
ILogger<ImageCompressService> _2,
|
||||
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
|
||||
{
|
||||
await imageCompressService.Compress(dryRun);
|
||||
}
|
||||
}
|
||||
9
YaeBlog/Components/Anchor.razor
Normal file
9
YaeBlog/Components/Anchor.razor
Normal file
@@ -0,0 +1,9 @@
|
||||
<a href="@Address" class="text-blue-600" target="@(NewPage ? "_blank" : "_self")">@Text</a>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Address { get; set; }
|
||||
|
||||
[Parameter] public string? Text { get; set; }
|
||||
|
||||
[Parameter] public bool NewPage { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
@@ -7,22 +7,14 @@
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||
<link rel="icon" href="images/favicon.ico"/>
|
||||
<link rel="stylesheet" href="bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
|
||||
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
|
||||
<link rel="stylesheet" href="globals.css"/>
|
||||
<link rel="stylesheet" href="tailwind.g.css"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes/>
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="bootstrap.bundle.min.js"></script>
|
||||
<script src="clipboard.min.js"></script>
|
||||
<script>
|
||||
const clipboard = new ClipboardJS('.btn');
|
||||
</script>
|
||||
<Routes/>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
29
YaeBlog/Components/AppreciationCode.razor
Normal file
29
YaeBlog/Components/AppreciationCode.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="flex flex-wrap justify-center gap-12 max-w-md md:max-w-lg">
|
||||
<div class="relative w-40 h-48 md:w-48 md:w-48 overflow-hidden
|
||||
transition-all duration-300 ease-out hover:scale-125 group">
|
||||
<img
|
||||
src="./images/wechat-code.jpeg"
|
||||
alt="微信赞赏码"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div class="absolute -bottom-8 left-0 right-0 text-center
|
||||
text-white bg-black opacity-60 text-sm font-medium
|
||||
backdrop-blur-sm group-hover:bottom-2 transition-all duration-300">
|
||||
请我喝奶茶<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-40 h-48 md:w-48 md:h-48 overflow-hidden
|
||||
transition-all duration-300 ease-out hover:scale-125 group">
|
||||
<img
|
||||
src="./images/alipay-code.jpeg"
|
||||
alt="支付宝赞赏码"
|
||||
class="w-full h-full object-cover"/>
|
||||
<div class="absolute -bottom-8 left-0 right-0 text-center
|
||||
text-white bg-black opacity-60 text-sm font-medium
|
||||
backdrop-blur-sm group-hover:bottom-2 transition-all duration-300">
|
||||
请我吃晚饭<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,57 +1,49 @@
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject BlogOptions Options
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto p-4">
|
||||
<Image Src="images/avatar.png" Alt="Ricardo's avatar"/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="p-10">
|
||||
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full"/>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center p-3">
|
||||
<div class="col-auto fs-4">
|
||||
“奇奇怪怪东西的聚合地”
|
||||
</div>
|
||||
<div class="px-10 py-2 text-xl">
|
||||
“奇奇怪怪东西的聚合地”
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
<div class="flex flex-row justify-between px-6 py-2 text-xl">
|
||||
<div>
|
||||
文章
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/archives">
|
||||
@(Contents.Essays.Count)
|
||||
</a>
|
||||
</div>
|
||||
<a href="/blog/archives/">
|
||||
<div>
|
||||
@(Contents.Count)
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
<div class="flex flex-row justify-between px-6 py-2 text-xl">
|
||||
<div>
|
||||
标签
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/tags">
|
||||
<a href="/blog/tags/">
|
||||
<div>
|
||||
@(Contents.Tags.Count)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-start fs-5" style="padding-top: 2em">
|
||||
<div class="col-auto">
|
||||
广而告之
|
||||
</div>
|
||||
<div class="text-xl px-2 py-2">
|
||||
广而告之
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p style="text-indent: 2em">
|
||||
@(Options.Announcement)
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<p class="text-lg">
|
||||
@(Options.Announcement)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Models
|
||||
|
||||
<div class="container p-3">
|
||||
<div class="row fs-2 fw-bold py-2 essay-title">
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="text-3xl font-bold py-2">
|
||||
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
|
||||
</div>
|
||||
|
||||
<div class="row p-2 justify-content-start">
|
||||
<div class="col-auto fw-light">
|
||||
<div class="p-2 flex flex-row justify-content-start gap-2">
|
||||
<div class="font-light">
|
||||
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
@foreach (string key in Essay.Tags)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<div class="text-sky-600">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
|
||||
# @key
|
||||
</a>
|
||||
@@ -21,20 +21,11 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
@(Essay.Description)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col border-bottom">
|
||||
|
||||
</div>
|
||||
<div class="p-2">
|
||||
@(Essay.Description)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required BlogEssay Essay { get; set; }
|
||||
[Parameter] public required BlogEssay Essay { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -1,14 +1,30 @@
|
||||
<div class="row align-items-end text-center">
|
||||
<div class="row">
|
||||
<p class="fs-6">
|
||||
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>,
|
||||
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。
|
||||
<div class="flex flex-col text-center py-2">
|
||||
<div>
|
||||
<p class="text-md">
|
||||
2021 - @(DateTimeOffset.Now.Year) ©
|
||||
<Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
|
||||
,由
|
||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||
驱动。
|
||||
</p>
|
||||
<p class="text-md">
|
||||
Build Commit #
|
||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-6">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
|
||||
<div>
|
||||
<p class="text-md">
|
||||
<Anchor Address="https://beian.miit.gov.cn" Text="蜀ICP备2022004429号-1" NewPage="true"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private string DotnetVersion => $".NET {Environment.Version}";
|
||||
|
||||
private string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
|
||||
|
||||
private string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
||||
}
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
@using YaeBlog.Core.Models
|
||||
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
|
||||
<div class="flex flex-col gap-3 text-md">
|
||||
<div>
|
||||
文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
|
||||
</div>
|
||||
|
||||
@inject BlogOptions Options
|
||||
<div>
|
||||
文章地址:
|
||||
<a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
|
||||
@($"https://rrricardo.top/blog/essays/{EssayFilename}")
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle">
|
||||
<div class="col">
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
|
||||
<div>
|
||||
版权声明:本博客所有文章除特别声明外,均采用
|
||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="text-blue-600">
|
||||
CC BY-NC-SA 4.0
|
||||
</a>
|
||||
许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
|
||||
<a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
|
||||
Ricardo's Blog
|
||||
</a>”。
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-center">
|
||||
<p>如果觉得不错的话,可以支持一下作者哦~</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<AppreciationCode/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
文章地址:
|
||||
<a href="/blog/essays/@(EssayAddress)" target="_blank">
|
||||
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-1">
|
||||
<div class="col">
|
||||
版权声明:本博客所有文章除特别声明外,均采用
|
||||
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a>
|
||||
许可协议,转载请注明来自
|
||||
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string? EssayAddress { get; set; }
|
||||
[Parameter] public string? EssayFilename { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.copyright {
|
||||
}
|
||||
22
YaeBlog/Components/PageAnchor.razor
Normal file
22
YaeBlog/Components/PageAnchor.razor
Normal file
@@ -0,0 +1,22 @@
|
||||
@if (Selected)
|
||||
{
|
||||
<div class="border rounded-lg shadow-neutral-500 bg-sky-400 w-8 h-8 inline-block leading-8 text-center">
|
||||
<span class="text-white">@(Text)</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="@Address">
|
||||
<div class="border rounded-lg shadow-neutral-500 w-8 h-8 inline-block leading-8 text-center">
|
||||
<span>@(Text)</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Address { get; set; }
|
||||
|
||||
[Parameter] public string? Text { get; set; }
|
||||
|
||||
[Parameter] public bool Selected { get; set; }
|
||||
}
|
||||
46
YaeBlog/Components/Pagination.razor
Normal file
46
YaeBlog/Components/Pagination.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="flex flex-row justify-center gap-3">
|
||||
@if (Page != 1)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="<"/>
|
||||
}
|
||||
|
||||
@if (Page == 1)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(1)" Text="1" Selected="@true"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(2)" Text="2"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(3)" Text="3"/>
|
||||
}
|
||||
else if (Page == PageCount)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(PageCount - 2)" Text="@($"{PageCount - 2}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(PageCount - 1)" Text="@($"{PageCount - 1}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(PageCount)" Text="@($"{PageCount}")" Selected="@true"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>
|
||||
|
||||
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page + 1}")"/>
|
||||
}
|
||||
|
||||
@if (Page != PageCount)
|
||||
{
|
||||
<PageAnchor Address="@GenerateAddress(Page + 1)" Text=">"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? BaseUrl { get; set; }
|
||||
|
||||
[Parameter] public int PageCount { get; set; }
|
||||
|
||||
[Parameter] public int Page { get; set; }
|
||||
|
||||
private string GenerateAddress(int page) => $"{BaseUrl}?page={page}";
|
||||
}
|
||||
@@ -9,12 +9,20 @@ public class FilesController : ControllerBase
|
||||
[HttpGet("{*filename}")]
|
||||
public IActionResult Images(string filename)
|
||||
{
|
||||
// 这里疑似有点太愚蠢了
|
||||
string contentType = "image/png";
|
||||
|
||||
if (filename.EndsWith("jpg") || filename.EndsWith("jpeg"))
|
||||
{
|
||||
contentType = "image/jpeg";
|
||||
}
|
||||
|
||||
if (filename.EndsWith("svg"))
|
||||
{
|
||||
contentType = "image/svg+xml";
|
||||
}
|
||||
|
||||
|
||||
FileInfo imageFile = new(filename);
|
||||
|
||||
if (!imageFile.Exists)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
|
||||
ARG COMMIT_ID
|
||||
ENV COMMIT_ID=${COMMIT_ID}
|
||||
|
||||
WORKDIR /app
|
||||
COPY bin/Release/net8.0/publish/ ./
|
||||
COPY bin/Release/net9.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
COPY appsettings.json .
|
||||
|
||||
|
||||
12
YaeBlog/Exceptions/BlogCommandException.cs
Normal file
12
YaeBlog/Exceptions/BlogCommandException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace YaeBlog.Core.Exceptions;
|
||||
|
||||
public class BlogCommandException : Exception
|
||||
{
|
||||
public BlogCommandException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public BlogCommandException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
18
YaeBlog/Extensions/AngleSharpExtensions.cs
Normal file
18
YaeBlog/Extensions/AngleSharpExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
@@ -1,13 +1,11 @@
|
||||
using AngleSharp;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Processors;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Processors;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
@@ -19,14 +17,13 @@ public static class WebApplicationBuilderExtensions
|
||||
|
||||
builder.Services.AddMarkdig();
|
||||
builder.Services.AddYamlParser();
|
||||
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
||||
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default);
|
||||
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
||||
builder.Services.AddSingleton<RendererService>();
|
||||
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
|
||||
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
||||
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
|
||||
builder.Services.AddTransient<TablePostRenderProcessor>();
|
||||
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
|
||||
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
|
||||
builder.Services.AddTransient<BlogOptions>(provider =>
|
||||
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Processors;
|
||||
using YaeBlog.Core.Services;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Processors;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static void UseYaeBlog(this WebApplication application)
|
||||
{
|
||||
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<TablePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
|
||||
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
|
||||
}
|
||||
|
||||
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
||||
@@ -2,60 +2,41 @@
|
||||
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<main class="container">
|
||||
<div class="row d-none d-xl-flex" style="height: 80px">
|
||||
<div class="px-2 col-9">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h4>Ricardo's Blog</h4>
|
||||
<main class="container mx-auto flex flex-col min-h-screen">
|
||||
<div class="grid grid-cols-3 mx-3">
|
||||
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
|
||||
<a href="/blog/">
|
||||
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-3 d-flex justify-content-around align-items-center">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h5>首页</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/archives/" class="p-2">
|
||||
<h5>归档</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/tags/" class="p-2">
|
||||
<h5>标签</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/about/" class="p-2">
|
||||
<h5>关于</h5>
|
||||
</a>
|
||||
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
|
||||
<div>
|
||||
<a href="/blog/archives/">
|
||||
<span class="text-xl text-blue-600">归档</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/blog/tags/">
|
||||
<span class="text-xl text-blue-600">标签</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/about/" target="_blank">
|
||||
<span class="text-xl text-blue-600">关于</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/friends/" target="_blank">
|
||||
<span class="text-xl text-blue-600">友链</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-xl-none">
|
||||
<div class="px-2 col-12">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h4>Ricardo's Blog</h4>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="px-2 col-12 justify-content-end d-flex">
|
||||
<a href="/blog/" class="p-2">
|
||||
<h5>首页</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/archives/" class="p-2">
|
||||
<h5>归档</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/tags/" class="p-2">
|
||||
<h5>标签</h5>
|
||||
</a>
|
||||
|
||||
<a href="/blog/about/" class="p-2">
|
||||
<h5>关于</h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-2">
|
||||
<div class="px-4 py-2 flex-grow">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main class="container">
|
||||
<div class="row" style="height: 80px">
|
||||
<div class="px-2 col-8">
|
||||
<a href="/" class="p-2">
|
||||
<h4>Ricardo's Index</h4>
|
||||
<main class="container mx-auto min-h-screen flex flex-col">
|
||||
<div class="grid grid-cols-4">
|
||||
<div class="px-2 md:col-span-3 col-span-4 h-20 flex items-center">
|
||||
<a href="/" class="text-2xl">
|
||||
<h4 class="text-blue-600">Ricardo's Index</h4>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-4 d-flex justify-content-around align-items-center">
|
||||
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank">
|
||||
<h5>E-mail</h5>
|
||||
</a>
|
||||
<div class="md:col-span-1 col-span-4 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
|
||||
<Anchor
|
||||
Address="/blog/"
|
||||
Text="博客"
|
||||
NewPage="@(true)"/>
|
||||
|
||||
<Anchor
|
||||
Address="/about/"
|
||||
Text="关于"
|
||||
NewPage="@(true)"/>
|
||||
|
||||
<Anchor
|
||||
Address="/friends"
|
||||
Text="友链"
|
||||
NewPage="@(true)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 center">
|
||||
<div class="px-4 mx-auto flex-grow">
|
||||
<div class="py-2">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
max-width: 48em;
|
||||
min-height: calc(100vh - 80px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
20
YaeBlog/Models/BlogContent.cs
Normal file
20
YaeBlog/Models/BlogContent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
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 string BlogName => BlogFile.Name.Split('.')[0];
|
||||
}
|
||||
12
YaeBlog/Models/BlogContents.cs
Normal file
12
YaeBlog/Models/BlogContents.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
|
||||
: IEnumerable<BlogContent>
|
||||
{
|
||||
public IEnumerator<BlogContent> GetEnumerator() => Posts.Concat(Drafts).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogEssay : IComparable<BlogEssay>
|
||||
{
|
||||
@@ -6,6 +6,8 @@ public class BlogEssay : IComparable<BlogEssay>
|
||||
|
||||
public required string FileName { get; init; }
|
||||
|
||||
public required bool IsDraft { get; init; }
|
||||
|
||||
public required DateTime PublishTime { get; init; }
|
||||
|
||||
public required string Description { get; init; }
|
||||
@@ -24,6 +26,7 @@ public class BlogEssay : IComparable<BlogEssay>
|
||||
{
|
||||
Title = Title,
|
||||
FileName = FileName,
|
||||
IsDraft = IsDraft,
|
||||
PublishTime = PublishTime,
|
||||
Description = Description,
|
||||
WordCount = WordCount,
|
||||
@@ -39,10 +42,16 @@ public class BlogEssay : IComparable<BlogEssay>
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return PublishTime.CompareTo(other.PublishTime);
|
||||
// 草稿文章应当排在前面
|
||||
if (IsDraft != other.IsDraft)
|
||||
{
|
||||
return IsDraft ? -1 : 1;
|
||||
}
|
||||
|
||||
return other.PublishTime.CompareTo(PublishTime);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
44
YaeBlog/Models/BlogImageInfo.cs
Normal file
44
YaeBlog/Models/BlogImageInfo.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogOptions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
@@ -1,141 +1,74 @@
|
||||
@page "/blog/about"
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject BlogOptions Options
|
||||
@page "/about"
|
||||
|
||||
<PageTitle>
|
||||
关于
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>关于</h1>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">关于</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-2">
|
||||
把字刻在石头上!(・’ω’・)
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<span class="italic">把字刻在石头上!(・’ω’・)</span>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于我</h3>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="pb-2">
|
||||
<h3 class="text-2xl">关于我</h3>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
|
||||
一般在互联网上使用<span class="fst-italic">初冬的朝阳</span>或者<span class="fst-italic">jackfiled</span>的名字活动。
|
||||
<span class="text-decoration-line-through">都是ICP备案过的人了,网名似乎没有太大的用处(</span>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
|
||||
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span class="italic">jackfiled</span>的名字活动。
|
||||
<span class="line-through">都是ICP备案过的人了,网名似乎没有太大的用处(</span>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
主要是一个C#程序员,目前也在尝试写一点Rust。
|
||||
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
|
||||
前后端分离的项目本当上手。
|
||||
常常因为现实的压力而写一些C/C++。
|
||||
<span class="text-decoration-line-through">对于Java和Go的评价很低。</span>
|
||||
日常使用ArchLinux。
|
||||
</div>
|
||||
<div class="py-2">
|
||||
主要是一个C#程序员,目前也在尝试写一点Rust。
|
||||
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
|
||||
前后端分离的项目本当上手。
|
||||
常常因为现实的压力而写一些C/C++。
|
||||
<span class="line-through">对于Java和Go的评价很低。</span>
|
||||
日常使用ArchLinux。
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
|
||||
</div>
|
||||
<div class="py-2">
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。
|
||||
兴趣活动是读书和看番,目前在玩原神和三角洲。
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
常常被人批评没有梦想,这里就随便瞎编一下。
|
||||
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
|
||||
完成第一个不是课程设计的个人开源项目。
|
||||
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
|
||||
<span class="text-decoration-line-through">什么时候天上才能掉美少女?</span>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
常常被人批评没有梦想,这里就随便瞎编一下。
|
||||
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
|
||||
完成第一个不是课程设计的个人开源项目。
|
||||
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
|
||||
<span class="line-through">什么时候天上才能掉美少女?</span>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
|
||||
也可以试试在各大平台搜索上面提到的名字。
|
||||
</div>
|
||||
<div class="py-2">
|
||||
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
|
||||
也可以试试在各大平台搜索上面提到的名字。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于本站</h3>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="pb-2">
|
||||
<h3 class="text-2xl">关于本站</h3>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
本站肇始于2021年下半年,在开始的两年中个人网站和博客是分别的两个网站,个人网站是裸HTML写的,博客是用
|
||||
<a href="https://hexo.io">Hexo</a>渲染的。
|
||||
</div>
|
||||
<div class="py-2">
|
||||
本站肇始于2021年下半年,在开始的两年中个人网站和博客是分别的两个网站,个人网站是裸HTML写的,博客是用
|
||||
<a href="https://hexo.io">Hexo</a>渲染的。
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
2024年,我们决定使用.NET技术完全重构两个网站,合二为一。虽然目前这个版本还是一个半成品,但是我们一定会努力的~(确信。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>友链</h3>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
2024年,我们决定使用.NET技术完全重构两个网站,合二为一。虽然目前这个版本还是一个半成品,但是我们一定会努力的~(确信。
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col fst-italic">
|
||||
欢迎所有人联系我添加友链!(´。✪ω✪。`)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
@foreach (FriendLink link in Options.Links)
|
||||
{
|
||||
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||
<a href="@(link.Link)" target="_blank" class="m-3">
|
||||
<div class="row link-item">
|
||||
<div class="col-4">
|
||||
<Image Src="@(link.AvatarImage)" Alt="@(link.Name)" Style="border-radius: 50%"/>
|
||||
</div>
|
||||
|
||||
<div class="col-8">
|
||||
<div class="row">
|
||||
<div class="col-auto fs-5">
|
||||
@(link.Name)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto fst-italic">
|
||||
@(link.Description)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<div class="py-2">
|
||||
2025年,我们将使用的样式库从Bootstrap迁移到Tailwind CSS,将现代的前端技术同Blazor结合起来。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.link-item {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog/archives"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
|
||||
@@ -8,68 +8,56 @@
|
||||
归档
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>归档</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-4">
|
||||
时光图书馆,黑历史集散地。(๑◔‿◔๑)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">归档</h1>
|
||||
</div>
|
||||
|
||||
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>@(group.Key.Year)</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<span class="italic">
|
||||
时光图书馆,黑历史集散地。(๑◔‿◔๑)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="container px-3 py-2">
|
||||
@foreach (KeyValuePair<string, BlogEssay> essay in group)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
@(essay.Value.PublishTime.ToString("MM-dd"))
|
||||
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
|
||||
{
|
||||
<div class="p-2">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h3 class="text-xl">@(group.Key.Year)</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 flex flex-col">
|
||||
@foreach (BlogEssay essay in group)
|
||||
{
|
||||
<a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
|
||||
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
|
||||
<div class="w-20">
|
||||
@(essay.PublishTime.ToString("MM月dd日"))
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/essays/@(essay.Key)">
|
||||
@(essay.Value.Title)
|
||||
</a>
|
||||
<div>
|
||||
<span class="text-blue-600">
|
||||
@(essay.Title)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
|
||||
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
_essays.AddRange(from essay in Contents.Essays
|
||||
orderby essay.Value.PublishTime descending
|
||||
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
|
||||
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -9,79 +9,18 @@
|
||||
Ricardo's Blog
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-9">
|
||||
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
|
||||
<div>
|
||||
<div class="grid grid-cols-4">
|
||||
<div class="col-span-4 md:col-span-3">
|
||||
@foreach (BlogEssay essay in _essays)
|
||||
{
|
||||
<EssayCard Essay="@(pair.Value)"/>
|
||||
<EssayCard Essay="@(essay)"/>
|
||||
}
|
||||
|
||||
<div class="row align-items-center justify-content-center p-3">
|
||||
@if (_page == 1)
|
||||
{
|
||||
<div class="col-auto fw-light">上一页</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page - 1)">上一页</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_page == 1)
|
||||
{
|
||||
<div class="col-auto">
|
||||
1
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=2">2</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=3">3</a>
|
||||
</div>
|
||||
}
|
||||
else if (_page == _pageCount)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@(_pageCount)
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@(_page)
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_page == _pageCount)
|
||||
{
|
||||
<div class="col-auto fw-light">
|
||||
下一页
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/?page=@(_page + 1)">下一页</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<div class="col-span-4 md:col-span-1">
|
||||
<BlogInformationCard/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +30,7 @@
|
||||
|
||||
[SupplyParameterFromQuery] private int? Page { get; set; }
|
||||
|
||||
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
|
||||
private readonly List<BlogEssay> _essays = [];
|
||||
private const int EssaysPerPage = 8;
|
||||
private int _pageCount = 1;
|
||||
private int _page = 1;
|
||||
@@ -99,16 +38,20 @@
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_page = Page ?? 1;
|
||||
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
|
||||
_pageCount = Contents.Count / EssaysPerPage + 1;
|
||||
(_pageCount, int reminder) = int.DivRem(Contents.Count, EssaysPerPage);
|
||||
if (reminder > 0)
|
||||
{
|
||||
_pageCount += 1;
|
||||
}
|
||||
|
||||
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
|
||||
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFount");
|
||||
return;
|
||||
}
|
||||
|
||||
_essays.AddRange(Contents.Essays
|
||||
.OrderByDescending(p => p.Value.PublishTime)
|
||||
.Skip((_page - 1) * EssaysPerPage)
|
||||
.Take(EssaysPerPage));
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.read-more a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/essays/{BlogKey}"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,96 +10,87 @@
|
||||
@(_essay!.Title)
|
||||
</PageTitle>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="flex flex-col py-8">
|
||||
<div>
|
||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||
<div class="col-auto">
|
||||
<h1 id="title">@(_essay!.Title)</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-1">
|
||||
<div class="col-auto fw-light">
|
||||
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
@foreach (string tag in _essay!.Tags)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||
# @(tag)
|
||||
</a>
|
||||
<div class="px-6 pt-4 pb-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="font-light">
|
||||
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
}
|
||||
|
||||
@foreach (string tag in _essay!.Tags)
|
||||
{
|
||||
<div class="text-sky-500">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||
# @(tag)
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-1">
|
||||
<div class="col-auto fw-light">
|
||||
<div class="px-6 pt-2 pb-4">
|
||||
<div class="font-light">
|
||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-12">
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
<div class="grid grid-cols-3">
|
||||
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
|
||||
<div>
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
</div>
|
||||
|
||||
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
||||
<div>
|
||||
<LicenseDisclaimer EssayFilename="@BlogKey"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="row sticky-lg-top justify-content-center">
|
||||
<div class="col-auto">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
|
||||
文章目录
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3 md:col-span-1">
|
||||
<div class="flex flex-col sticky top-0 px-8">
|
||||
<div>
|
||||
<h3 class="text-2xl">文章目录</h3>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-left: 10px">
|
||||
<div class="col-auto">
|
||||
@foreach (BlogHeadline level2 in _headline!.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level3 in level2.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
|
||||
@(level3.Title)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level4 in level3.Children)
|
||||
{
|
||||
<div class="row py-1">
|
||||
<div class="col-auto">
|
||||
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
|
||||
@(level4.Title)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_headline!.Children.Count == 0)
|
||||
<div>
|
||||
@foreach (BlogHeadline level2 in _headline!.Children)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col fst-italic">
|
||||
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
|
||||
</div>
|
||||
<div class="py-2 pl-3">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
|
||||
Text="@(level2.Title)"/>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level3 in level2.Children)
|
||||
{
|
||||
<div class="py-2 pl-6">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
|
||||
Text="@(level3.Title)"/>
|
||||
</div>
|
||||
|
||||
@foreach (BlogHeadline level4 in level3.Children)
|
||||
{
|
||||
<div class="py-2 pl-9">
|
||||
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
|
||||
Text="@(level4.Title)"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_headline!.Children.Count == 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col fst-italic">
|
||||
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,7 +114,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
|
||||
if (!Contents.TryGetEssay(BlogKey, out _essay))
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFound");
|
||||
}
|
||||
|
||||
49
YaeBlog/Pages/Friends.razor
Normal file
49
YaeBlog/Pages/Friends.razor
Normal file
@@ -0,0 +1,49 @@
|
||||
@page "/friends"
|
||||
@using YaeBlog.Models
|
||||
@inject BlogOptions Options
|
||||
|
||||
<PageTitle>
|
||||
友链
|
||||
</PageTitle>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">
|
||||
友链
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
欢迎所有人联系我添加友链!(´。✪ω✪。`)
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 g-4 p-2">
|
||||
@foreach (FriendLink link in Options.Links)
|
||||
{
|
||||
<div>
|
||||
<a href="@(link.Link)" target="_blank" class="mx-5">
|
||||
<div class="flex flex-row">
|
||||
<div class="basis-1/3">
|
||||
<img src="@(link.AvatarImage)" alt="@($"Avatar of {link.Name}")"
|
||||
class="w-full h-auto rounded-full">
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col basis-2/3 px-2">
|
||||
<div class="text-lg">
|
||||
@(link.Name)
|
||||
</div>
|
||||
|
||||
<div class="text-sm italic">
|
||||
@(link.Description)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -4,28 +4,28 @@
|
||||
Ricardo's Index
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row py-4">
|
||||
<div class="col-lg-4 col-12 p-5 p-lg-0">
|
||||
<Image Src="images/avatar.png" Alt="Ricardo's Avatar"/>
|
||||
<div class="mx-20">
|
||||
<div class="grid grid-cols-3 py-4">
|
||||
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
|
||||
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-12">
|
||||
<div class="container px-3">
|
||||
<div class="row">
|
||||
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
<div class="flex flex-col px-3 gap-y-3">
|
||||
<div class="">
|
||||
<div class="text-3xl font-bold">初冬的朝阳 (Ricardo Ren)</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">a.k.a jackfiled</p>
|
||||
<div class="">
|
||||
<p class="text-lg">a.k.a jackfiled</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
|
||||
<div class="">
|
||||
<p class="text-lg italic">世界很大,时间很长。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
<div class="">
|
||||
<p class="text-lg">
|
||||
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
|
||||
</p>
|
||||
</div>
|
||||
@@ -33,20 +33,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-top: 80px">
|
||||
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||
<div class="py-5">
|
||||
<p class="text-lg">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
|
||||
<div>
|
||||
<p class="text-lg py-1">
|
||||
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <Anchor Address="/blog/" Text="博客"/> 看看。
|
||||
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
|
||||
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>,
|
||||
<Anchor Address="https://jackfiled.github.io/wiki/" Text="我的学习笔记"/>,
|
||||
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
|
||||
如果您想批判一下我的代码,在 <a href="https://github.com/jackfiled" target="_blank">Github</a> 和
|
||||
<a href="https://git.rrricardo.top/jackfiled/" target="_blank">Gitea</a> 都可以找到。
|
||||
如果您想批判一下我的代码,在
|
||||
<Anchor Address="https://github.com/jackfiled/" Text="Github"/> 和
|
||||
<Anchor Address="https://git.rrricardo.top/jackfiled/" Text="Gitea"/>
|
||||
都可以找到。
|
||||
</p>
|
||||
<p class="fs-5">
|
||||
<p class="text-lg py-1">
|
||||
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
啊~ 页面走丢啦~
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<h3>NotFound!</h3>
|
||||
<div>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/tags/"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,24 +10,22 @@
|
||||
@(TagName ?? "标签")
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1>标签</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2>@(TagName)</h2>
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1 class="text-4xl">标签</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 class="text-2xl">@(TagName)</h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-4">
|
||||
<div class="py-4">
|
||||
<span class="italic">
|
||||
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (TagName is null)
|
||||
@@ -38,19 +36,17 @@
|
||||
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
||||
{
|
||||
<li class="p-2">
|
||||
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
||||
<div class="container fs-5">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
# @(pair.Key.TagName)
|
||||
</div>
|
||||
|
||||
<div class="col-auto tag-count">
|
||||
@(pair.Value.Count)
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
||||
<div class="text-sky-600 text-lg">
|
||||
# @(pair.Key.TagName)
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
|
||||
@(pair.Value.Count)
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.tag-count {
|
||||
background: var(--bs-secondary-bg);
|
||||
border-radius: 5px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
145
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal file
145
YaeBlog/Processors/EssayStylesPostRenderProcessor.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
|
||||
/// </summary>
|
||||
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
||||
{
|
||||
public string Name => nameof(EssayStylesPostRenderProcessor);
|
||||
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument document = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
ApplyGlobalCssStyles(document);
|
||||
BeatifyTable(document);
|
||||
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" },
|
||||
{ "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" },
|
||||
};
|
||||
|
||||
private void ApplyGlobalCssStyles(IDocument document)
|
||||
{
|
||||
foreach ((string tag, string style) in _globalCssStyles)
|
||||
{
|
||||
foreach (IElement element in document.GetElementsByTagName(tag))
|
||||
{
|
||||
element.ClassList.Add(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeatifyTable(IDocument document)
|
||||
{
|
||||
foreach (IElement element in from e in document.All
|
||||
where e.LocalName == "table"
|
||||
select e)
|
||||
{
|
||||
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
|
||||
|
||||
// thead元素
|
||||
foreach (IElement headElement in from e in element.Children
|
||||
where e.LocalName == "thead"
|
||||
select e)
|
||||
{
|
||||
headElement.ClassList.Add("bg-slate-200");
|
||||
|
||||
// tr in thead
|
||||
foreach (IElement trElement in from e in headElement.Children
|
||||
where e.LocalName == "tr"
|
||||
select e)
|
||||
{
|
||||
trElement.ClassList.Add("border border-slate-300");
|
||||
|
||||
// th in tr
|
||||
foreach (IElement thElement in from e in trElement.Children
|
||||
where e.LocalName == "th"
|
||||
select e)
|
||||
{
|
||||
thElement.ClassList.Add("px-4 py-1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tbody元素
|
||||
foreach (IElement bodyElement in from e in element.Children
|
||||
where e.LocalName == "tbody"
|
||||
select e)
|
||||
{
|
||||
// tr in tbody
|
||||
foreach (IElement trElement in from e in bodyElement.Children
|
||||
where e.LocalName == "tr"
|
||||
select e)
|
||||
{
|
||||
foreach (IElement tdElement in from e in trElement.Children
|
||||
where e.LocalName == "td"
|
||||
select e)
|
||||
{
|
||||
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
public class HeadlinePostRenderProcessor(
|
||||
IConfiguration angleConfiguration,
|
||||
AngleSharp.IConfiguration angleConfiguration,
|
||||
IEssayContentService essayContentService,
|
||||
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
|
||||
{
|
||||
@@ -1,24 +1,22 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Processors;
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||
public class ImagePostRenderProcessor(
|
||||
ILogger<ImagePostRenderProcessor> logger,
|
||||
IOptions<BlogOptions> options)
|
||||
: IPostRenderProcessor
|
||||
{
|
||||
private static readonly IConfiguration s_configuration = Configuration.Default;
|
||||
|
||||
private readonly BlogOptions _options = options.Value;
|
||||
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(s_configuration);
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument html = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
@@ -32,23 +30,27 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||
if (attr is not null)
|
||||
{
|
||||
logger.LogDebug("Found image link: '{}'", attr.Value);
|
||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
|
||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName, essay.IsDraft);
|
||||
}
|
||||
element.ClassList.Add("essay-image");
|
||||
}
|
||||
|
||||
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(ImagePostRenderProcessor);
|
||||
|
||||
private string GenerateImageLink(string filename, string essayFilename)
|
||||
private string GenerateImageLink(string filename, string essayFilename, bool isDraft)
|
||||
{
|
||||
// 如果图片路径中没有包含文件名
|
||||
// 则添加文件名
|
||||
if (!filename.Contains(essayFilename))
|
||||
{
|
||||
filename = Path.Combine(essayFilename, filename);
|
||||
}
|
||||
|
||||
filename = Path.Combine(_options.Root, "posts", filename);
|
||||
filename = isDraft
|
||||
? Path.Combine(_options.Root, "drafts", filename)
|
||||
: Path.Combine(_options.Root, "posts", filename);
|
||||
|
||||
if (!Path.Exists(filename))
|
||||
{
|
||||
@@ -1,13 +1,4 @@
|
||||
using System.CommandLine;
|
||||
using YaeBlog.Commands;
|
||||
|
||||
RootCommand rootCommand = new("YaeBlog CLI");
|
||||
|
||||
rootCommand.AddServeCommand();
|
||||
rootCommand.AddNewCommand();
|
||||
rootCommand.AddListCommand();
|
||||
rootCommand.AddWatchCommand();
|
||||
rootCommand.AddScanCommand();
|
||||
rootCommand.AddPublishCommand();
|
||||
|
||||
await rootCommand.InvokeAsync(args);
|
||||
YaeBlogCommand command = new();
|
||||
await command.RunAsync(args);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class BlogChangeWatcher : IDisposable
|
||||
{
|
||||
@@ -1,7 +1,4 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public class BlogHostedService(
|
||||
ILogger<BlogHostedService> logger,
|
||||
@@ -9,14 +6,12 @@ public class BlogHostedService(
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Welcome to YaeBlog!");
|
||||
|
||||
logger.LogInformation("Failed to load cache, render essays.");
|
||||
await rendererService.RenderAsync();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
50
YaeBlog/Services/BlogHotReloadService.cs
Normal file
50
YaeBlog/Services/BlogHotReloadService.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using YaeBlog.Abstraction;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class BlogHotReloadService(
|
||||
RendererService rendererService,
|
||||
IEssayContentService essayContentService,
|
||||
BlogChangeWatcher watcher,
|
||||
ILogger<BlogHotReloadService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("Hot reload is starting...");
|
||||
logger.LogInformation("Change essays will lead to hot reload!");
|
||||
logger.LogInformation("HINT: draft essays will be included.");
|
||||
|
||||
await rendererService.RenderAsync(true);
|
||||
|
||||
Task[] reloadTasks = [WatchFileAsync(stoppingToken)];
|
||||
await Task.WhenAll(reloadTasks);
|
||||
}
|
||||
|
||||
private async Task WatchFileAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
logger.LogInformation("Watching file changes...");
|
||||
string? changeFile = await watcher.WaitForChange(token);
|
||||
|
||||
if (changeFile is null)
|
||||
{
|
||||
logger.LogInformation("File watcher is stopping.");
|
||||
break;
|
||||
}
|
||||
|
||||
FileInfo changeFileInfo = new(changeFile);
|
||||
|
||||
if (changeFileInfo.Name.StartsWith('.'))
|
||||
{
|
||||
// Ignore dot-started file and directory.
|
||||
logger.LogDebug("Ignore hidden file: {}.", changeFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogInformation("{} changed, re-rendering.", changeFile);
|
||||
essayContentService.Clear();
|
||||
await rendererService.RenderAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,36 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public class EssayContentService : IEssayContentService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
|
||||
|
||||
private readonly List<BlogEssay> _sortedEssays = [];
|
||||
|
||||
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
|
||||
|
||||
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
|
||||
|
||||
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
|
||||
public bool TryAdd(BlogEssay essay)
|
||||
{
|
||||
_sortedEssays.Add(essay);
|
||||
return _essays.TryAdd(essay.FileName, essay);
|
||||
}
|
||||
|
||||
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
|
||||
|
||||
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
|
||||
public IEnumerable<BlogEssay> Essays => _sortedEssays;
|
||||
|
||||
public int Count => _sortedEssays.Count;
|
||||
|
||||
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
|
||||
{
|
||||
return _essays.TryGetValue(filename, out essay);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
|
||||
|
||||
237
YaeBlog/Services/EssayScanService.cs
Normal file
237
YaeBlog/Services/EssayScanService.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
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;
|
||||
using YaeBlog.Models;
|
||||
using YamlDotNet.Core;
|
||||
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,
|
||||
IDeserializer yamlDeserializer,
|
||||
IOptions<BlogOptions> blogOptions,
|
||||
ILogger<EssayScanService> logger)
|
||||
{
|
||||
_yamlSerializer = yamlSerializer;
|
||||
_yamlDeserializer = yamlDeserializer;
|
||||
_logger = logger;
|
||||
_blogOptions = blogOptions.Value;
|
||||
RootDirectory = ValidateRootDirectory();
|
||||
}
|
||||
|
||||
private DirectoryInfo RootDirectory { get; }
|
||||
|
||||
public async Task<BlogContents> ScanContents()
|
||||
{
|
||||
ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||
|
||||
return new BlogContents(
|
||||
await ScanContentsInternal(drafts, true),
|
||||
await ScanContentsInternal(posts, false));
|
||||
}
|
||||
|
||||
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
|
||||
{
|
||||
ValidateDirectory(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"));
|
||||
|
||||
if (targetFile.Exists)
|
||||
{
|
||||
_logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
|
||||
}
|
||||
|
||||
await using StreamWriter writer = targetFile.CreateText();
|
||||
|
||||
await writer.WriteAsync("---\n");
|
||||
await writer.WriteAsync(_yamlSerializer.Serialize(content.Metadata));
|
||||
await writer.WriteAsync("---\n");
|
||||
|
||||
if (string.IsNullOrEmpty(content.Content) && isDraft)
|
||||
{
|
||||
// 如果博客为操作且内容为空
|
||||
// 创建简介隔断符号
|
||||
await writer.WriteLineAsync("<!--more-->");
|
||||
}
|
||||
else
|
||||
{
|
||||
await writer.WriteAsync(content.Content);
|
||||
}
|
||||
|
||||
// 保存图片文件
|
||||
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结尾且不是隐藏文件的文件
|
||||
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
|
||||
where file.Extension == ".md" && !file.Name.StartsWith('.')
|
||||
select file;
|
||||
|
||||
ConcurrentBag<BlogResult> 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));
|
||||
});
|
||||
|
||||
ConcurrentBag<BlogContent> contents = [];
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (BlogResult blog in fileContents)
|
||||
{
|
||||
if (blog.BlogContent.Length < 4)
|
||||
{
|
||||
// Even not contains a legal header.
|
||||
continue;
|
||||
}
|
||||
|
||||
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
|
||||
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
|
||||
{
|
||||
_logger.LogWarning("Failed to parse metadata from {}, skipped.", blog.BlogFile.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
string metadataString = blog.BlogContent[4..endPos];
|
||||
|
||||
try
|
||||
{
|
||||
MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
|
||||
_logger.LogDebug("Scan metadata title: '{title}' for {name}.", metadata.Title, blog.BlogFile.Name);
|
||||
|
||||
contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft,
|
||||
blog.Images, blog.NotFoundImages));
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
_logger.LogWarning("Failed to parser metadata from {name} due to {exception}, skipping", blog.BlogFile.Name, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
private record struct ImageResult(List<BlogImageInfo> Images, List<FileInfo> NotfoundImages);
|
||||
|
||||
private async Task<ImageResult> ScanImagePreBlog(DirectoryInfo directory, string blogName, string content)
|
||||
{
|
||||
MatchCollection matchResult = ImagePattern.Matches(content);
|
||||
DirectoryInfo imageDirectory = new(Path.Combine(directory.FullName, blogName));
|
||||
|
||||
Dictionary<string, bool> usedImages = imageDirectory.Exists
|
||||
? imageDirectory.EnumerateFiles().ToDictionary(file => file.FullName, _ => false)
|
||||
: [];
|
||||
List<FileInfo> notFoundImages = [];
|
||||
|
||||
foreach (Match match in matchResult)
|
||||
{
|
||||
string imageName = match.Groups[1].Value;
|
||||
|
||||
// 判断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 _))
|
||||
{
|
||||
usedImages[usedFile.FullName] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
notFoundImages.Add(usedFile);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
byte[] image = await File.ReadAllBytesAsync(file.FullName);
|
||||
|
||||
if (file.Extension is ".jpg" or ".jpeg" or ".png")
|
||||
{
|
||||
ImageInfo imageInfo =
|
||||
await ImageJob.GetImageInfoAsync(MemorySource.Borrow(image), SourceLifetime.NowOwnedAndDisposedByTask);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
|
||||
private static partial Regex ImagePattern { get; }
|
||||
|
||||
|
||||
private DirectoryInfo ValidateRootDirectory()
|
||||
{
|
||||
DirectoryInfo rootDirectory = new(Path.Combine(Environment.CurrentDirectory, _blogOptions.Root));
|
||||
|
||||
if (!rootDirectory.Exists)
|
||||
{
|
||||
throw new BlogFileException($"'{_blogOptions.Root}' is not a directory.");
|
||||
}
|
||||
|
||||
return rootDirectory;
|
||||
}
|
||||
|
||||
private void ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||
{
|
||||
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
|
||||
{
|
||||
throw new BlogFileException($"'{_blogOptions.Root}/drafts' not exists.");
|
||||
}
|
||||
|
||||
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
|
||||
{
|
||||
throw new BlogFileException($"'{_blogOptions.Root}/posts' not exists.");
|
||||
}
|
||||
|
||||
drafts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "drafts"));
|
||||
posts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "posts"));
|
||||
}
|
||||
}
|
||||
119
YaeBlog/Services/ImageCompressService.cs
Normal file
119
YaeBlog/Services/ImageCompressService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public partial class RendererService(
|
||||
ILogger<RendererService> logger,
|
||||
@@ -22,40 +21,43 @@ public partial class RendererService(
|
||||
|
||||
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
|
||||
|
||||
public async Task RenderAsync()
|
||||
public async Task RenderAsync(bool includeDrafts = false)
|
||||
{
|
||||
_stopwatch.Start();
|
||||
logger.LogInformation("Render essays start.");
|
||||
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
List<BlogContent> posts = contents.Posts.ToList();
|
||||
if (includeDrafts)
|
||||
{
|
||||
posts.AddRange(contents.Drafts);
|
||||
}
|
||||
|
||||
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
|
||||
|
||||
List<BlogEssay> essays = [];
|
||||
await Task.Run(() =>
|
||||
foreach (BlogContent content in preProcessedContents)
|
||||
{
|
||||
foreach (BlogContent content in preProcessedContents)
|
||||
uint wordCount = GetWordCount(content);
|
||||
BlogEssay essay = new()
|
||||
{
|
||||
uint wordCount = GetWordCount(content);
|
||||
BlogEssay essay = new()
|
||||
{
|
||||
Title = content.Metadata.Title ?? content.FileName,
|
||||
FileName = content.FileName,
|
||||
Description = GetDescription(content),
|
||||
WordCount = wordCount,
|
||||
ReadTime = CalculateReadTime(wordCount),
|
||||
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||
HtmlContent = content.FileContent
|
||||
};
|
||||
Title = content.Metadata.Title ?? content.BlogName,
|
||||
FileName = content.BlogName,
|
||||
IsDraft = content.IsDraft,
|
||||
Description = GetDescription(content),
|
||||
WordCount = wordCount,
|
||||
ReadTime = CalculateReadTime(wordCount),
|
||||
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||
HtmlContent = content.Content
|
||||
};
|
||||
|
||||
if (content.Metadata.Tags is not null)
|
||||
{
|
||||
essay.Tags.AddRange(content.Metadata.Tags);
|
||||
}
|
||||
|
||||
essays.Add(essay);
|
||||
if (content.Metadata.Tags is not null)
|
||||
{
|
||||
essay.Tags.AddRange(content.Metadata.Tags);
|
||||
}
|
||||
});
|
||||
|
||||
essays.Add(essay);
|
||||
}
|
||||
|
||||
ConcurrentBag<BlogEssay> postProcessEssays = [];
|
||||
Parallel.ForEach(essays, essay =>
|
||||
@@ -67,7 +69,16 @@ public partial class RendererService(
|
||||
logger.LogDebug("Render markdown file {}.", newEssay);
|
||||
});
|
||||
|
||||
await PostProcess(postProcessEssays);
|
||||
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
|
||||
|
||||
foreach (BlogEssay essay in postProcessedEssays)
|
||||
{
|
||||
if (!essayContentService.TryAdd(essay))
|
||||
{
|
||||
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
|
||||
}
|
||||
}
|
||||
|
||||
essayContentService.RefreshTags();
|
||||
|
||||
_stopwatch.Stop();
|
||||
@@ -118,8 +129,10 @@ public partial class RendererService(
|
||||
return processedContents;
|
||||
}
|
||||
|
||||
private async Task PostProcess(IEnumerable<BlogEssay> essays)
|
||||
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
|
||||
{
|
||||
ConcurrentBag<BlogEssay> processedContents = [];
|
||||
|
||||
await Parallel.ForEachAsync(essays, async (essay, _) =>
|
||||
{
|
||||
foreach (IPostRenderProcessor processor in _postRenderProcessors)
|
||||
@@ -127,32 +140,34 @@ public partial class RendererService(
|
||||
essay = await processor.ProcessAsync(essay);
|
||||
}
|
||||
|
||||
if (!essayContentService.TryAdd(essay))
|
||||
{
|
||||
throw new BlogFileException(
|
||||
$"There are two essays with the same name: '{essay.FileName}'.");
|
||||
}
|
||||
processedContents.Add(essay);
|
||||
});
|
||||
|
||||
List<BlogEssay> result = processedContents.ToList();
|
||||
result.Sort();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
||||
private static partial Regex DescriptionPattern();
|
||||
// private static partial Regex DescriptionPattern();
|
||||
private static partial Regex DescriptionPattern { get; }
|
||||
|
||||
private string GetDescription(BlogContent content)
|
||||
{
|
||||
const string delimiter = "<!--more-->";
|
||||
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
|
||||
int pos = content.Content.IndexOf(delimiter, StringComparison.Ordinal);
|
||||
bool breakSentence = false;
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
// 自动截取前50个字符
|
||||
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
|
||||
pos = content.Content.Length < 50 ? content.Content.Length : 50;
|
||||
breakSentence = true;
|
||||
}
|
||||
|
||||
string rawContent = content.FileContent[..pos];
|
||||
MatchCollection matches = DescriptionPattern().Matches(rawContent);
|
||||
string rawContent = content.Content[..pos];
|
||||
MatchCollection matches = DescriptionPattern.Matches(rawContent);
|
||||
|
||||
StringBuilder builder = new();
|
||||
foreach (Match match in matches)
|
||||
@@ -167,18 +182,18 @@ public partial class RendererService(
|
||||
|
||||
string description = builder.ToString();
|
||||
|
||||
logger.LogDebug("Description of {} is {}.", content.FileName,
|
||||
logger.LogDebug("Description of {} is {}.", content.BlogName,
|
||||
description);
|
||||
return description;
|
||||
}
|
||||
|
||||
private uint GetWordCount(BlogContent content)
|
||||
{
|
||||
int count = (from c in content.FileContent
|
||||
int count = (from c in content.Content
|
||||
where char.IsLetterOrDigit(c)
|
||||
select c).Count();
|
||||
|
||||
logger.LogDebug("Word count of {} is {}", content.FileName,
|
||||
logger.LogDebug("Word count of {} is {}", content.BlogName,
|
||||
count);
|
||||
return (uint)count;
|
||||
}
|
||||
@@ -1,18 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ImageFlow.NativeRuntime.ubuntu-x86_64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('Linux'))"/>
|
||||
<PackageReference Include="ImageFlow.NativeRuntime.osx-arm64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('OSX'))"/>
|
||||
<PackageReference Include="ImageFlow.Net" Version="0.13.2"/>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0"/>
|
||||
<PackageReference Include="Markdig" Version="0.38.0"/>
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
|
||||
<Message Importance="low" Text="Ensure pnpm is installed..."/>
|
||||
<Exec Command="pnpm --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||
</Exec>
|
||||
|
||||
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
|
||||
|
||||
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
|
||||
<Exec Command="pnpm install"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
|
||||
<Message Importance="normal" Text="Generate css files using tailwind..."/>
|
||||
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorBootstrap
|
||||
@using YaeBlog
|
||||
@using YaeBlog.Components
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Tailwind": {
|
||||
"InputFile": "wwwroot/input.css",
|
||||
"OutputFile": "wwwroot/output.css"
|
||||
},
|
||||
"Blog": {
|
||||
"Root": "source",
|
||||
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
|
||||
@@ -24,10 +28,16 @@
|
||||
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "Chenxu",
|
||||
"Name": "不会写程序的晨旭",
|
||||
"Description": "一个普通大学生",
|
||||
"Link": "https://chenxutalk.top",
|
||||
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
|
||||
},
|
||||
{
|
||||
"Name": "万木长风",
|
||||
"Description": "世界渲染中...",
|
||||
"Link": "https://ryohai.fun",
|
||||
"AvatarImage": "https://ryohai.fun/icon.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
15
YaeBlog/package.json
Normal file
15
YaeBlog/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
545
YaeBlog/pnpm-lock.yaml
generated
Normal file
545
YaeBlog/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,545 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@tailwindcss/cli':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.15
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.15
|
||||
|
||||
packages:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@tailwindcss/cli@4.0.15':
|
||||
resolution: {integrity: sha512-52RdNZCpij4O8+25N9sfWZPG124e6ahmIS1uMHcJrdw10UdpPUFgSJtyMwf7COVOnkx0nkXfmp8CcYomPCrQ1Q==}
|
||||
hasBin: true
|
||||
|
||||
'@tailwindcss/node@4.0.15':
|
||||
resolution: {integrity: sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.0.15':
|
||||
resolution: {integrity: sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.0.15':
|
||||
resolution: {integrity: sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.0.15':
|
||||
resolution: {integrity: sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.0.15':
|
||||
resolution: {integrity: sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15':
|
||||
resolution: {integrity: sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.0.15':
|
||||
resolution: {integrity: sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.0.15':
|
||||
resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.0.15':
|
||||
resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.0.15':
|
||||
resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.0.15':
|
||||
resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.0.15':
|
||||
resolution: {integrity: sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.0.15':
|
||||
resolution: {integrity: sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
detect-libc@2.0.3:
|
||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
jiti@2.4.2:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.29.2:
|
||||
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
tailwindcss@4.0.15:
|
||||
resolution: {integrity: sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==}
|
||||
|
||||
tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
dependencies:
|
||||
detect-libc: 1.0.3
|
||||
is-glob: 4.0.3
|
||||
micromatch: 4.0.8
|
||||
node-addon-api: 7.1.1
|
||||
optionalDependencies:
|
||||
'@parcel/watcher-android-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-x64': 2.5.1
|
||||
'@parcel/watcher-freebsd-x64': 2.5.1
|
||||
'@parcel/watcher-linux-arm-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm-musl': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-musl': 2.5.1
|
||||
'@parcel/watcher-linux-x64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-x64-musl': 2.5.1
|
||||
'@parcel/watcher-win32-arm64': 2.5.1
|
||||
'@parcel/watcher-win32-ia32': 2.5.1
|
||||
'@parcel/watcher-win32-x64': 2.5.1
|
||||
|
||||
'@tailwindcss/cli@4.0.15':
|
||||
dependencies:
|
||||
'@parcel/watcher': 2.5.1
|
||||
'@tailwindcss/node': 4.0.15
|
||||
'@tailwindcss/oxide': 4.0.15
|
||||
enhanced-resolve: 5.18.1
|
||||
lightningcss: 1.29.2
|
||||
mri: 1.2.0
|
||||
picocolors: 1.1.1
|
||||
tailwindcss: 4.0.15
|
||||
|
||||
'@tailwindcss/node@4.0.15':
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.1
|
||||
jiti: 2.4.2
|
||||
tailwindcss: 4.0.15
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.0.15':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.0.15':
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.0.15
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.0.15
|
||||
'@tailwindcss/oxide-darwin-x64': 4.0.15
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.0.15
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.15
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.0.15
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.0.15
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.0.15
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.0.15
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.15
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.0.15
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.29.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
optionalDependencies:
|
||||
lightningcss-darwin-arm64: 1.29.2
|
||||
lightningcss-darwin-x64: 1.29.2
|
||||
lightningcss-freebsd-x64: 1.29.2
|
||||
lightningcss-linux-arm-gnueabihf: 1.29.2
|
||||
lightningcss-linux-arm64-gnu: 1.29.2
|
||||
lightningcss-linux-arm64-musl: 1.29.2
|
||||
lightningcss-linux-x64-gnu: 1.29.2
|
||||
lightningcss-linux-x64-musl: 1.29.2
|
||||
lightningcss-win32-arm64-msvc: 1.29.2
|
||||
lightningcss-win32-x64-msvc: 1.29.2
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
tailwindcss@4.0.15: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
title: 2021年终总结
|
||||
date: 2022-01-12 16:27:19
|
||||
date: 2022-01-12T16:27:19.0000000
|
||||
tags:
|
||||
- 随笔
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
---
|
||||
|
||||
|
||||
2021年已经过去,2022年已经来临。每每一年开始的时候,我都会展开一张纸或者新建一个文档,思量着又是一年时光,也该同诸大杂志一般,写几句意味深长的话语,怀念过去的时光,也祝福未来的自己。可往往脑海中已是三万字的长篇,落在笔头却又是一个字都没有了。
|
||||
如今跨年的时候已经过去,朋友圈中已经不见文案的踪影,我也该重新提笔,细说自己2021年中做过的种种。
|
||||
|
||||
@@ -22,7 +24,7 @@ tags:
|
||||
在前12年的学生生涯中,我们都在期待着这一次的暑假,以为在这个没有作业的假期里,我们就可以充分的享受人间的美好。可是,当时我们不知道,这人间的烦恼,可不止作业这一种,无论是突如其来的疫情导致开学延期,还是等待录取时的不安。
|
||||
虽说在暑假时,拥有了自己的笔记本电脑,可是在高中三年屯下的游戏还是没有玩几个,看来我也是“喜加一”的受害者。虽然在高考后入坑了原神,但是假期间我并没有太过投入的玩。
|
||||
暑假下定决心要好好的学一学,可是看着我gitee上暑假期间那稀疏的提交,我就知道我又摸了一个暑假的鱼。
|
||||

|
||||

|
||||
即使我想写的很多项目都没有被扎实的推进下来,但是学习的一些的C语言还是让我受益匪浅。
|
||||
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。
|
||||
|
||||
|
||||
BIN
YaeBlog/source/posts/2021-final/1.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2021-final/1.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/2021-final/1.webp
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2021-final/1.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -1,11 +1,13 @@
|
||||
---
|
||||
title: 2022年终总结
|
||||
date: 2022-12-30T14:58:12.0000000
|
||||
tags:
|
||||
- 随笔
|
||||
date: 2022-12-30 14:58:12
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
---
|
||||
|
||||
|
||||
|
||||
2022是困难的一年。我们需要为2023年做好准备。
|
||||
|
||||
<!--more-->
|
||||
@@ -56,11 +58,11 @@ date: 2022-12-30 14:58:12
|
||||
|
||||
小小的总结一下:2022年可以算得上是一事无成的一年,还搞砸了不少的事情。在写代码上进展有限,成绩上大幅倒退,说好的六级英语和大学物理竞赛都没有参加,在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
|
||||
|
||||

|
||||

|
||||
|
||||
关于今年的人际交往和社会关系,我愿意用QQ2022年年终总结中的一张截屏来总结,这张图片透漏出一种无可救药的悲伤。
|
||||
|
||||

|
||||

|
||||
|
||||
## 展望
|
||||
|
||||
|
||||
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -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 @@ date: 2022-08-22 15:39:13
|
||||
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
|
||||
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
|
||||
|
||||

|
||||

|
||||
|
||||
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
|
||||
|
||||
|
||||
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.webp
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user