Compare commits
2 Commits
feat/code-
...
726d39bcc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 726d39bcc9 | |||
| f71a59f228 |
@@ -12,10 +12,7 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{project.json,appsettings.json,appsettings.*.json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yaml,yml}]
|
||||
[project.json]
|
||||
indent_size = 2
|
||||
|
||||
# C# and Visual Basic files
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,5 +1,2 @@
|
||||
*.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,30 +1,29 @@
|
||||
name: Build blog docker image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- name: Check out code.
|
||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/checkout.git@v4
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project.
|
||||
run: |
|
||||
git submodule update --init
|
||||
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
||||
pwsh build.ps1 build
|
||||
- name: Workaround to make sure podman-login working.
|
||||
run: |
|
||||
mkdir -p /root/.docker
|
||||
- name: Login tencent cloud docker registry.
|
||||
uses: http://github-mirrors.infra.svc.cluster.local/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
|
||||
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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -184,7 +184,6 @@ DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
out/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
@@ -483,6 +482,3 @@ $RECYCLE.BIN/
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
|
||||
# Tailwind auto-generated stylesheet
|
||||
*.g.css
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "third-party/BlazorSvgComponents"]
|
||||
path = third-party/BlazorSvgComponents
|
||||
url = https://git.rrricardo.top/jackfiled/BlazorSvgComponents.git
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
public interface IEssayContentService
|
||||
{
|
||||
public IEnumerable<BlogEssay> Essays { get; }
|
||||
|
||||
public int Count { get; }
|
||||
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
|
||||
|
||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
||||
|
||||
@@ -18,8 +16,6 @@ public interface IEssayContentService
|
||||
|
||||
public bool TryAdd(BlogEssay essay);
|
||||
|
||||
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
|
||||
|
||||
public void RefreshTags();
|
||||
|
||||
public void Clear();
|
||||
12
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
12
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
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.Abstractions.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
8
YaeBlog.Core/Components/_Imports.razor
Normal file
8
YaeBlog.Core/Components/_Imports.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@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
|
||||
34
YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs
Normal file
34
YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Markdig;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMarkdig(this IServiceCollection collection)
|
||||
{
|
||||
MarkdownPipelineBuilder builder = new();
|
||||
|
||||
builder.UseAdvancedExtensions();
|
||||
|
||||
collection.AddSingleton<MarkdownPipeline>(_ => builder.Build());
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddYamlParser(this IServiceCollection collection)
|
||||
{
|
||||
DeserializerBuilder deserializerBuilder = new();
|
||||
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||
collection.AddSingleton(deserializerBuilder.Build());
|
||||
|
||||
SerializerBuilder serializerBuilder = new();
|
||||
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||
collection.AddSingleton(serializerBuilder.Build());
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
50
YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs
Normal file
50
YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
|
||||
namespace YaeBlog.Core.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder AddYaeBlog(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<BlogOptions>(builder.Configuration.GetSection(BlogOptions.OptionName));
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
builder.Services.AddMarkdig();
|
||||
builder.Services.AddYamlParser();
|
||||
builder.Services.AddSingleton<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<BlogOptions>(provider =>
|
||||
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplicationBuilder AddServer(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddHostedService<BlogHostedService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplicationBuilder AddWatcher(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<BlogChangeWatcher>();
|
||||
builder.Services.AddHostedService<BlogHotReloadService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Processors;
|
||||
using YaeBlog.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Processors;
|
||||
using YaeBlog.Core.Services;
|
||||
|
||||
namespace YaeBlog.Extensions;
|
||||
namespace YaeBlog.Core.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
|
||||
10
YaeBlog.Core/Models/AboutInfo.cs
Normal file
10
YaeBlog.Core/Models/AboutInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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; }
|
||||
}
|
||||
10
YaeBlog.Core/Models/BlogContent.cs
Normal file
10
YaeBlog.Core/Models/BlogContent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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; }
|
||||
}
|
||||
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
}
|
||||
52
YaeBlog.Core/Models/BlogEssay.cs
Normal file
52
YaeBlog.Core/Models/BlogEssay.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogEssay : IComparable<BlogEssay>
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
|
||||
public required string FileName { get; init; }
|
||||
|
||||
public required DateTime PublishTime { get; init; }
|
||||
|
||||
public required string Description { get; init; }
|
||||
|
||||
public required uint WordCount { get; init; }
|
||||
|
||||
public required string ReadTime { get; init; }
|
||||
|
||||
public List<string> Tags { get; } = [];
|
||||
|
||||
public required string HtmlContent { get; init; }
|
||||
|
||||
public BlogEssay WithNewHtmlContent(string newHtmlContent)
|
||||
{
|
||||
var essay = new BlogEssay
|
||||
{
|
||||
Title = Title,
|
||||
FileName = FileName,
|
||||
PublishTime = PublishTime,
|
||||
Description = Description,
|
||||
WordCount = WordCount,
|
||||
ReadTime = ReadTime,
|
||||
HtmlContent = newHtmlContent
|
||||
};
|
||||
essay.Tags.AddRange(Tags);
|
||||
|
||||
return essay;
|
||||
}
|
||||
|
||||
public int CompareTo(BlogEssay? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return PublishTime.CompareTo(other.PublishTime);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Title}-{PublishTime}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
26
YaeBlog.Core/Models/BlogOptions.cs
Normal file
26
YaeBlog.Core/Models/BlogOptions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogOptions
|
||||
{
|
||||
public const string OptionName = "Blog";
|
||||
|
||||
/// <summary>
|
||||
/// 博客markdown文件的根目录
|
||||
/// </summary>
|
||||
public required string Root { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 博客正文的广而告之
|
||||
/// </summary>
|
||||
public required string Announcement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 博客的起始年份
|
||||
/// </summary>
|
||||
public required int StartYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 博客的友链
|
||||
/// </summary>
|
||||
public required List<FriendLink> Links { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
27
YaeBlog.Core/Models/FriendLink.cs
Normal file
27
YaeBlog.Core/Models/FriendLink.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
/// </summary>
|
||||
public class FriendLink
|
||||
{
|
||||
/// <summary>
|
||||
/// 友链名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 友链的简单介绍
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 友链地址
|
||||
/// </summary>
|
||||
public required string Link { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像地址
|
||||
/// </summary>
|
||||
public required string AvatarImage { get; set; }
|
||||
}
|
||||
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
||||
10
YaeBlog.Core/Models/MarkdownMetadata.cs
Normal file
10
YaeBlog.Core/Models/MarkdownMetadata.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
|
||||
public DateTime? Date { get; set; }
|
||||
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal file
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
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,12 +1,13 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
public class HeadlinePostRenderProcessor(
|
||||
AngleSharp.IConfiguration angleConfiguration,
|
||||
IConfiguration angleConfiguration,
|
||||
IEssayContentService essayContentService,
|
||||
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
|
||||
{
|
||||
@@ -67,7 +68,7 @@ public class HeadlinePostRenderProcessor(
|
||||
logger.LogWarning("Failed to add headline of {}.", essay.FileName);
|
||||
}
|
||||
|
||||
return essay with { HtmlContent = document.DocumentElement.OuterHtml };
|
||||
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
private static BlogHeadline ParserHeadlineElement(IElement element)
|
||||
@@ -1,28 +1,24 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
namespace YaeBlog.Core.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// 图片地址路径后处理器
|
||||
/// 将本地图片地址修改为图片API地址
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="options"></param>
|
||||
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(Configuration.Default);
|
||||
BrowsingContext context = new(s_configuration);
|
||||
IDocument html = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
@@ -36,27 +32,23 @@ public class ImagePostRenderProcessor(
|
||||
if (attr is not null)
|
||||
{
|
||||
logger.LogDebug("Found image link: '{}'", attr.Value);
|
||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName, essay.IsDraft);
|
||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
|
||||
}
|
||||
element.ClassList.Add("essay-image");
|
||||
}
|
||||
|
||||
return essay with { HtmlContent = html.DocumentElement.OuterHtml };
|
||||
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
|
||||
}
|
||||
|
||||
public string Name => nameof(ImagePostRenderProcessor);
|
||||
|
||||
private string GenerateImageLink(string filename, string essayFilename, bool isDraft)
|
||||
private string GenerateImageLink(string filename, string essayFilename)
|
||||
{
|
||||
// 如果图片路径中没有包含文件名
|
||||
// 则添加文件名
|
||||
if (!filename.Contains(essayFilename))
|
||||
{
|
||||
filename = Path.Combine(essayFilename, filename);
|
||||
}
|
||||
|
||||
filename = isDraft
|
||||
? Path.Combine(_options.Root, "drafts", filename)
|
||||
: Path.Combine(_options.Root, "posts", filename);
|
||||
filename = Path.Combine(_options.Root, "posts", filename);
|
||||
|
||||
if (!Path.Exists(filename))
|
||||
{
|
||||
@@ -65,7 +57,7 @@ public class ImagePostRenderProcessor(
|
||||
}
|
||||
|
||||
string imageLink = "api/files/" + filename;
|
||||
logger.LogDebug("Generate image link '{link}' for image file '{filename}'.",
|
||||
logger.LogDebug("Generate image link '{}' for image file '{}'.",
|
||||
imageLink, filename);
|
||||
|
||||
return imageLink;
|
||||
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal file
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
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,7 +1,8 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public sealed class BlogChangeWatcher : IDisposable
|
||||
{
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace YaeBlog.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public class BlogHostedService(
|
||||
ILogger<BlogHostedService> logger,
|
||||
@@ -6,12 +9,14 @@ public class BlogHostedService(
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Failed to load cache, render essays.");
|
||||
logger.LogInformation("Welcome to YaeBlog!");
|
||||
|
||||
await rendererService.RenderAsync();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
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,36 +1,23 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
namespace YaeBlog.Core.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)
|
||||
{
|
||||
_sortedEssays.Add(essay);
|
||||
return _essays.TryAdd(essay.FileName, essay);
|
||||
}
|
||||
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
|
||||
|
||||
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
|
||||
|
||||
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<string, BlogEssay> Essays => _essays;
|
||||
|
||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
|
||||
|
||||
210
YaeBlog.Core/Services/EssayScanService.cs
Normal file
210
YaeBlog.Core/Services/EssayScanService.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
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)
|
||||
{
|
||||
// 扫描以md结果的但是不是隐藏文件的文件
|
||||
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
|
||||
where file.Extension == ".md" && !file.Name.StartsWith('.')
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using YaeBlog.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
namespace YaeBlog.Core.Services;
|
||||
|
||||
public sealed partial class RendererService(
|
||||
public partial class RendererService(
|
||||
ILogger<RendererService> logger,
|
||||
IEssayScanService essayScanService,
|
||||
MarkdownPipeline markdownPipeline,
|
||||
@@ -21,54 +22,52 @@ public sealed partial class RendererService(
|
||||
|
||||
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
|
||||
|
||||
public async Task RenderAsync(bool includeDrafts = false)
|
||||
public async Task RenderAsync()
|
||||
{
|
||||
_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);
|
||||
ConcurrentBag<BlogEssay> essays = [];
|
||||
|
||||
Parallel.ForEach(preProcessedContents, content =>
|
||||
List<BlogEssay> essays = [];
|
||||
await Task.Run(() =>
|
||||
{
|
||||
(uint wordCount, string readTime) = GetWordCount(content);
|
||||
DateTimeOffset publishDate = content.Metadata.Date is null
|
||||
? DateTimeOffset.Now
|
||||
: DateTimeOffset.Parse(content.Metadata.Date);
|
||||
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
|
||||
DateTimeOffset updateTime = content.Metadata.UpdateTime is null
|
||||
? publishDate
|
||||
: DateTimeOffset.Parse(content.Metadata.UpdateTime);
|
||||
string description = GetDescription(content);
|
||||
List<string> tags = content.Metadata.Tags ?? [];
|
||||
foreach (BlogContent content in preProcessedContents)
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
string originalHtml = Markdown.ToHtml(content.Content, markdownPipeline);
|
||||
if (content.Metadata.Tags is not null)
|
||||
{
|
||||
essay.Tags.AddRange(content.Metadata.Tags);
|
||||
}
|
||||
|
||||
BlogEssay essay = new(
|
||||
content.Metadata.Title ?? content.BlogName, content.BlogName, content.IsDraft, publishDate, updateTime,
|
||||
description, wordCount, readTime, tags, originalHtml);
|
||||
logger.LogDebug("Render essay: {}", essay);
|
||||
|
||||
essays.Add(essay);
|
||||
essays.Add(essay);
|
||||
}
|
||||
});
|
||||
|
||||
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(essays);
|
||||
|
||||
foreach (BlogEssay essay in postProcessedEssays)
|
||||
ConcurrentBag<BlogEssay> postProcessEssays = [];
|
||||
Parallel.ForEach(essays, essay =>
|
||||
{
|
||||
if (!essayContentService.TryAdd(essay))
|
||||
{
|
||||
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
|
||||
}
|
||||
}
|
||||
BlogEssay newEssay =
|
||||
essay.WithNewHtmlContent(Markdown.ToHtml(essay.HtmlContent, markdownPipeline));
|
||||
|
||||
postProcessEssays.Add(newEssay);
|
||||
logger.LogDebug("Render markdown file {}.", newEssay);
|
||||
});
|
||||
|
||||
await PostProcess(postProcessEssays);
|
||||
essayContentService.RefreshTags();
|
||||
|
||||
_stopwatch.Stop();
|
||||
@@ -119,10 +118,8 @@ public sealed partial class RendererService(
|
||||
return processedContents;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
|
||||
private async Task PostProcess(IEnumerable<BlogEssay> essays)
|
||||
{
|
||||
ConcurrentBag<BlogEssay> processedContents = [];
|
||||
|
||||
await Parallel.ForEachAsync(essays, async (essay, _) =>
|
||||
{
|
||||
foreach (IPostRenderProcessor processor in _postRenderProcessors)
|
||||
@@ -130,34 +127,32 @@ public sealed partial class RendererService(
|
||||
essay = await processor.ProcessAsync(essay);
|
||||
}
|
||||
|
||||
processedContents.Add(essay);
|
||||
if (!essayContentService.TryAdd(essay))
|
||||
{
|
||||
throw new BlogFileException(
|
||||
$"There are two essays with the same name: '{essay.FileName}'.");
|
||||
}
|
||||
});
|
||||
|
||||
List<BlogEssay> result = processedContents.ToList();
|
||||
result.Sort();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
||||
// private static partial Regex DescriptionPattern();
|
||||
private static partial Regex DescriptionPattern { get; }
|
||||
private static partial Regex DescriptionPattern();
|
||||
|
||||
private string GetDescription(BlogContent content)
|
||||
{
|
||||
const string delimiter = "<!--more-->";
|
||||
int pos = content.Content.IndexOf(delimiter, StringComparison.Ordinal);
|
||||
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
|
||||
bool breakSentence = false;
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
// 自动截取前50个字符
|
||||
pos = content.Content.Length < 50 ? content.Content.Length : 50;
|
||||
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
|
||||
breakSentence = true;
|
||||
}
|
||||
|
||||
string rawContent = content.Content[..pos];
|
||||
MatchCollection matches = DescriptionPattern.Matches(rawContent);
|
||||
string rawContent = content.FileContent[..pos];
|
||||
MatchCollection matches = DescriptionPattern().Matches(rawContent);
|
||||
|
||||
StringBuilder builder = new();
|
||||
foreach (Match match in matches)
|
||||
@@ -172,21 +167,28 @@ public sealed partial class RendererService(
|
||||
|
||||
string description = builder.ToString();
|
||||
|
||||
logger.LogDebug("Description of {name} is {desc}.", content.BlogName,
|
||||
logger.LogDebug("Description of {} is {}.", content.FileName,
|
||||
description);
|
||||
return description;
|
||||
}
|
||||
|
||||
private (uint, string) GetWordCount(BlogContent content)
|
||||
private uint GetWordCount(BlogContent content)
|
||||
{
|
||||
uint count = MarkdownWordCounter.CountWord(content);
|
||||
int count = (from c in content.FileContent
|
||||
where char.IsLetterOrDigit(c)
|
||||
select c).Count();
|
||||
|
||||
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
|
||||
logger.LogDebug("Word count of {} is {}", content.FileName,
|
||||
count);
|
||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||
uint second = count / 10;
|
||||
TimeSpan span = new(0, 0, (int)second);
|
||||
return (uint)count;
|
||||
}
|
||||
|
||||
return (count, span.ToString("mm'分'ss'秒'"));
|
||||
private static string CalculateReadTime(uint wordCount)
|
||||
{
|
||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||
int second = (int)wordCount / 10;
|
||||
TimeSpan span = new(0, 0, second);
|
||||
|
||||
return span.ToString("mm'分 'ss'秒'");
|
||||
}
|
||||
}
|
||||
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal file
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<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
Normal file
47
YaeBlog.sln
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
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
|
||||
24
YaeBlog.slnx
24
YaeBlog.slnx
@@ -1,24 +0,0 @@
|
||||
<Solution>
|
||||
<Folder Name="/.gitea/" />
|
||||
<Folder Name="/.gitea/workflows/">
|
||||
<File Path=".gitea/workflows/build.yaml" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
<File Path=".gitattributes" />
|
||||
<File Path=".gitignore" />
|
||||
<File Path="build.ps1" />
|
||||
<File Path="LICENSE" />
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/YaeBlog.Abstractions/YaeBlog.Abstractions.csproj" />
|
||||
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
||||
<Project Path="src/YaeBlog/YaeBlog.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/third-party/" />
|
||||
<Folder Name="/third-party/BlazorSvgComponents/" />
|
||||
<Folder Name="/third-party/BlazorSvgComponents/src/">
|
||||
<Project Path="third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.CommandLine.Binding;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
public sealed class BlogOptionsBinder : BinderBase<IOptions<BlogOptions>>
|
||||
{
|
||||
protected override IOptions<BlogOptions> GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
bindingContext.AddService<IOptions<BlogOptions>>(_ =>
|
||||
{
|
||||
FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json"));
|
||||
if (!settings.Exists)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to load YaeBlog configurations.");
|
||||
}
|
||||
|
||||
using StreamReader reader = settings.OpenText();
|
||||
using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd());
|
||||
JsonElement root = document.RootElement;
|
||||
JsonElement optionSection = root.GetProperty(BlogOptions.OptionName);
|
||||
|
||||
BlogOptions? result = optionSection.Deserialize<BlogOptions>();
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.");
|
||||
}
|
||||
|
||||
return new OptionsWrapper<BlogOptions>(result);
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<IOptions<BlogOptions>>();
|
||||
}
|
||||
}
|
||||
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.CommandLine.Binding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Abstractions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
public sealed class EssayScanServiceBinder : BinderBase<IEssayScanService>
|
||||
{
|
||||
protected override IEssayScanService GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
bindingContext.AddService<IEssayScanService>(provider =>
|
||||
{
|
||||
DeserializerBuilder deserializerBuilder = new();
|
||||
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||
|
||||
SerializerBuilder serializerBuilder = new();
|
||||
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||
|
||||
IOptions<BlogOptions> options = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||
ILogger<EssayScanService> logger = provider.GetRequiredService<ILogger<EssayScanService>>();
|
||||
|
||||
return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger);
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<IEssayScanService>();
|
||||
}
|
||||
}
|
||||
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.CommandLine.Binding;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
public sealed class LoggerBinder<T> : BinderBase<ILogger<T>>
|
||||
{
|
||||
protected override ILogger<T> GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole()));
|
||||
bindingContext.AddService<ILogger<T>>(provider =>
|
||||
{
|
||||
ILoggerFactory factory = provider.GetRequiredService<ILoggerFactory>();
|
||||
return factory.CreateLogger<T>();
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<ILogger<T>>();
|
||||
}
|
||||
}
|
||||
216
YaeBlog/Commands/CommandExtensions.cs
Normal file
216
YaeBlog/Commands/CommandExtensions.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.CommandLine;
|
||||
using YaeBlog.Commands.Binders;
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Core.Extensions;
|
||||
using YaeBlog.Core.Models;
|
||||
using YaeBlog.Core.Services;
|
||||
|
||||
namespace YaeBlog.Commands;
|
||||
|
||||
public static class CommandExtensions
|
||||
{
|
||||
public static void AddServeCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command serveCommand = new("serve", "Start http server.");
|
||||
rootCommand.AddCommand(serveCommand);
|
||||
|
||||
serveCommand.SetHandler(async context =>
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddServer();
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.UseStaticFiles();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
application.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
application.MapControllers();
|
||||
|
||||
CancellationToken token = context.GetCancellationToken();
|
||||
await application.RunAsync(token);
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddWatchCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
command.SetHandler(async context =>
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddWatcher();
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.UseStaticFiles();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
application.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
application.MapControllers();
|
||||
|
||||
CancellationToken token = context.GetCancellationToken();
|
||||
await application.RunAsync(token);
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddNewCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command newCommand = new("new", "Create a new blog file and image directory.");
|
||||
rootCommand.AddCommand(newCommand);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
||||
newCommand.AddArgument(filenameArgument);
|
||||
|
||||
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
if (contents.Posts.Any(content => content.FileName == 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 }
|
||||
});
|
||||
|
||||
Console.WriteLine($"Created new blog '{file}.");
|
||||
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
|
||||
new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddListCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("list", "List all blogs");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
command.SetHandler(async (_, _, essyScanService) =>
|
||||
{
|
||||
BlogContents contents = await essyScanService.ScanContents();
|
||||
|
||||
Console.WriteLine($"All {contents.Posts.Count} Posts:");
|
||||
foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName))
|
||||
{
|
||||
Console.WriteLine($" - {content.FileName}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
|
||||
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
|
||||
{
|
||||
Console.WriteLine($" - {content.FileName}");
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
public static void AddScanCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("scan", "Scan unused and not found images.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
Option<bool> removeOption =
|
||||
new(name: "--rm", description: "Remove unused images.", getDefaultValue: () => false);
|
||||
command.AddOption(removeOption);
|
||||
|
||||
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
|
||||
{
|
||||
ImageScanResult result = await essayScanService.ScanImages();
|
||||
|
||||
if (result.UnusedImages.Count != 0)
|
||||
{
|
||||
Console.WriteLine("Found unused images:");
|
||||
Console.WriteLine("HINT: use '--rm' to remove unused images.");
|
||||
}
|
||||
|
||||
foreach (FileInfo image in result.UnusedImages)
|
||||
{
|
||||
Console.WriteLine($" - {image.FullName}");
|
||||
}
|
||||
|
||||
if (removeOptionValue)
|
||||
{
|
||||
foreach (FileInfo image in result.UnusedImages)
|
||||
{
|
||||
image.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Used not existed images:");
|
||||
|
||||
foreach (FileInfo image in result.NotFoundImages)
|
||||
{
|
||||
Console.WriteLine($" - {image.FullName}");
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||
}
|
||||
|
||||
public static void AddPublishCommand(this RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("publish", "Publish a new blog file.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The published blog filename.");
|
||||
command.AddArgument(filenameArgument);
|
||||
|
||||
command.SetHandler(async (blogOptions, _, essayScanService, filename) =>
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = (from blog in contents.Drafts
|
||||
where blog.FileName == filename
|
||||
select blog).FirstOrDefault();
|
||||
|
||||
if (content is null)
|
||||
{
|
||||
Console.WriteLine("Target blog does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将选中的博客文件复制到posts
|
||||
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||
|
||||
// 复制图片文件夹
|
||||
DirectoryInfo sourceImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
|
||||
DirectoryInfo targetImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
|
||||
|
||||
if (sourceImageDirectory.Exists)
|
||||
{
|
||||
targetImageDirectory.Create();
|
||||
foreach (FileInfo file in sourceImageDirectory.EnumerateFiles())
|
||||
{
|
||||
file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true);
|
||||
}
|
||||
|
||||
sourceImageDirectory.Delete(true);
|
||||
}
|
||||
|
||||
// 删除原始的文件
|
||||
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
|
||||
sourceBlogFile.Delete();
|
||||
}, new BlogOptionsBinder(),
|
||||
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
|
||||
}
|
||||
}
|
||||
28
YaeBlog/Components/App.razor
Normal file
28
YaeBlog/Components/App.razor
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||
<link rel="icon" href="images/favicon.ico"/>
|
||||
<link rel="stylesheet" href="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"/>
|
||||
<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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
57
YaeBlog/Components/BlogInformationCard.razor
Normal file
57
YaeBlog/Components/BlogInformationCard.razor
Normal file
@@ -0,0 +1,57 @@
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.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>
|
||||
|
||||
<div class="row justify-content-center p-3">
|
||||
<div class="col-auto fs-4">
|
||||
“奇奇怪怪东西的聚合地”
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
文章
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/archives">
|
||||
@(Contents.Essays.Count)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||
<div class="col-auto">
|
||||
标签
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/tags">
|
||||
@(Contents.Tags.Count)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-start fs-5" style="padding-top: 2em">
|
||||
<div class="col-auto">
|
||||
广而告之
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p style="text-indent: 2em">
|
||||
@(Options.Announcement)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
40
YaeBlog/Components/EssayCard.razor
Normal file
40
YaeBlog/Components/EssayCard.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
<div class="container p-3">
|
||||
<div class="row fs-2 fw-bold py-2 essay-title">
|
||||
<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">
|
||||
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
@foreach (string key in Essay.Tags)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
|
||||
# @key
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
@(Essay.Description)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col border-bottom">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required BlogEssay Essay { get; set; }
|
||||
}
|
||||
3
YaeBlog/Components/EssayCard.razor.css
Normal file
3
YaeBlog/Components/EssayCard.razor.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
14
YaeBlog/Components/Foonter.razor
Normal file
14
YaeBlog/Components/Foonter.razor
Normal file
@@ -0,0 +1,14 @@
|
||||
<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> 驱动。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-6">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
36
YaeBlog/Components/LicenseDisclaimer.razor
Normal file
36
YaeBlog/Components/LicenseDisclaimer.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject BlogOptions Options
|
||||
|
||||
<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>
|
||||
</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; }
|
||||
}
|
||||
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
@@ -0,0 +1,2 @@
|
||||
.copyright {
|
||||
}
|
||||
8
YaeBlog/Dockerfile
Normal file
8
YaeBlog/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
WORKDIR /app
|
||||
COPY bin/Release/net8.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
COPY appsettings.json .
|
||||
|
||||
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]
|
||||
63
YaeBlog/Layout/BlogLayout.razor
Normal file
63
YaeBlog/Layout/BlogLayout.razor
Normal file
@@ -0,0 +1,63 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@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>
|
||||
</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>
|
||||
</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">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
</main>
|
||||
0
YaeBlog/Layout/BlogLayout.razor.css
Normal file
0
YaeBlog/Layout/BlogLayout.razor.css
Normal file
25
YaeBlog/Layout/MainLayout.razor
Normal file
25
YaeBlog/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,25 @@
|
||||
@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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="row px-4 center">
|
||||
<div class="py-2">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
</main>
|
||||
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
max-width: 48em;
|
||||
min-height: calc(100vh - 80px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
146
YaeBlog/Pages/About.razor
Normal file
146
YaeBlog/Pages/About.razor
Normal file
@@ -0,0 +1,146 @@
|
||||
@page "/blog/about"
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject BlogOptions Options
|
||||
|
||||
<PageTitle>
|
||||
关于
|
||||
</PageTitle>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>关于</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-2">
|
||||
把字刻在石头上!(・’ω’・)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于我</h3>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
|
||||
也可以试试在各大平台搜索上面提到的名字。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-2">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>关于本站</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
本站肇始于2021年下半年,在开始的两年中个人网站和博客是分别的两个网站,个人网站是裸HTML写的,博客是用
|
||||
<a href="https://hexo.io">Hexo</a>渲染的。
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
8
YaeBlog/Pages/About.razor.css
Normal file
8
YaeBlog/Pages/About.razor.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.link-item {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
75
YaeBlog/Pages/Archives.razor
Normal file
75
YaeBlog/Pages/Archives.razor
Normal file
@@ -0,0 +1,75 @@
|
||||
@page "/blog/archives"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
|
||||
<PageTitle>
|
||||
归档
|
||||
</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>
|
||||
|
||||
@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="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"))
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<a href="/blog/essays/@(essay.Key)">
|
||||
@(essay.Value.Title)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<IGrouping<DateTime, KeyValuePair<string, 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));
|
||||
}
|
||||
}
|
||||
0
YaeBlog/Pages/Archives.razor.css
Normal file
0
YaeBlog/Pages/Archives.razor.css
Normal file
116
YaeBlog/Pages/BlogIndex.razor
Normal file
116
YaeBlog/Pages/BlogIndex.razor
Normal file
@@ -0,0 +1,116 @@
|
||||
@page "/blog"
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
|
||||
<PageTitle>
|
||||
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)
|
||||
{
|
||||
<EssayCard Essay="@(pair.Value)"/>
|
||||
}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<BlogInformationCard/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[SupplyParameterFromQuery] private int? Page { get; set; }
|
||||
|
||||
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
|
||||
private const int EssaysPerPage = 8;
|
||||
private int _pageCount = 1;
|
||||
private int _page = 1;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_page = Page ?? 1;
|
||||
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
|
||||
|
||||
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFount");
|
||||
return;
|
||||
}
|
||||
|
||||
_essays.AddRange(Contents.Essays
|
||||
.OrderByDescending(p => p.Value.PublishTime)
|
||||
.Skip((_page - 1) * EssaysPerPage)
|
||||
.Take(EssaysPerPage));
|
||||
}
|
||||
|
||||
}
|
||||
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.read-more a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
137
YaeBlog/Pages/Essays.razor
Normal file
137
YaeBlog/Pages/Essays.razor
Normal file
@@ -0,0 +1,137 @@
|
||||
@page "/blog/essays/{BlogKey}"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
|
||||
<PageTitle>
|
||||
@(_essay!.Title)
|
||||
</PageTitle>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row px-4 py-1">
|
||||
<div class="col-auto fw-light">
|
||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-12">
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
|
||||
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
||||
</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="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 class="row">
|
||||
<div class="col fst-italic">
|
||||
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? BlogKey { get; set; }
|
||||
|
||||
private BlogEssay? _essay;
|
||||
|
||||
private BlogHeadline? _headline;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(BlogKey))
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFound");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
|
||||
{
|
||||
NavigationInstance.NavigateTo("/NotFound");
|
||||
}
|
||||
|
||||
_headline = Contents.Headlines[BlogKey];
|
||||
}
|
||||
|
||||
private string GenerateSelectorUrl(string selectorId)
|
||||
=> $"/blog/essays/{BlogKey!}#{selectorId}";
|
||||
|
||||
}
|
||||
0
YaeBlog/Pages/Essays.razor.css
Normal file
0
YaeBlog/Pages/Essays.razor.css
Normal file
57
YaeBlog/Pages/Index.razor
Normal file
57
YaeBlog/Pages/Index.razor
Normal file
@@ -0,0 +1,57 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>
|
||||
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>
|
||||
|
||||
<div class="col-lg-8 col-12">
|
||||
<div class="container px-3">
|
||||
<div class="row">
|
||||
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">a.k.a jackfiled</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-top: 80px">
|
||||
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p class="fs-5">
|
||||
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
|
||||
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
|
||||
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>,
|
||||
<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> 都可以找到。
|
||||
</p>
|
||||
<p class="fs-5">
|
||||
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
0
YaeBlog/Pages/Index.razor.css
Normal file
0
YaeBlog/Pages/Index.razor.css
Normal file
@@ -4,8 +4,8 @@
|
||||
啊~ 页面走丢啦~
|
||||
</PageTitle>
|
||||
|
||||
<div>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
<div class="container">
|
||||
<h3>NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
0
YaeBlog/Pages/NotFound.razor.css
Normal file
0
YaeBlog/Pages/NotFound.razor.css
Normal file
@@ -1,7 +1,7 @@
|
||||
@page "/blog/tags/"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Core.Abstractions
|
||||
@using YaeBlog.Core.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,22 +10,24 @@
|
||||
@(TagName ?? "标签")
|
||||
</PageTitle>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1 class="text-4xl">标签</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 class="text-2xl">@(TagName)</h2>
|
||||
}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1>标签</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2>@(TagName)</h2>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
<span class="italic">
|
||||
<div class="row">
|
||||
<div class="col fst-italic py-4">
|
||||
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TagName is null)
|
||||
@@ -36,17 +38,19 @@
|
||||
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
||||
{
|
||||
<li class="p-2">
|
||||
<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>
|
||||
<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="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
|
||||
@(pair.Value.Count)
|
||||
<div class="col-auto tag-count">
|
||||
@(pair.Value.Count)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
6
YaeBlog/Pages/Tags.razor.css
Normal file
6
YaeBlog/Pages/Tags.razor.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.tag-count {
|
||||
background: var(--bs-secondary-bg);
|
||||
border-radius: 5px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
13
YaeBlog/Program.cs
Normal file
13
YaeBlog/Program.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
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);
|
||||
18
YaeBlog/YaeBlog.csproj
Normal file
18
YaeBlog/YaeBlog.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -6,6 +6,6 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorBootstrap
|
||||
@using YaeBlog
|
||||
@using YaeBlog.Components
|
||||
@using BlazorSvgComponents
|
||||
@using BlazorSvgComponents.Models
|
||||
40
YaeBlog/appsettings.json
Normal file
40
YaeBlog/appsettings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Blog": {
|
||||
"Root": "source",
|
||||
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
|
||||
"StartYear": 2021,
|
||||
"Links": [
|
||||
{
|
||||
"Name": "Ichirinko",
|
||||
"Description": "这是个大哥",
|
||||
"Link": "https://ichirinko.top",
|
||||
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
|
||||
},
|
||||
{
|
||||
"Name": "志田千陽",
|
||||
"Description": "日出多值得",
|
||||
"Link": "https://zzachary.top/",
|
||||
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
|
||||
},
|
||||
{
|
||||
"Name": "不会写程序的晨旭",
|
||||
"Description": "一个普通大学生",
|
||||
"Link": "https://chenxutalk.top",
|
||||
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
|
||||
},
|
||||
{
|
||||
"Name": "万木长风",
|
||||
"Description": "世界渲染中...",
|
||||
"Link": "https://ryohai.fun",
|
||||
"AvatarImage": "https://ryohai.fun/icon.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
YaeBlog/docker-compose.yaml
Normal file
13
YaeBlog/docker-compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
blog:
|
||||
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
|
||||
- "traefik.http.services.blog.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.blog.tls=true"
|
||||
- "traefik.http.routers.blog.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
0
YaeBlog/source/drafts/.gitkeep
Normal file
0
YaeBlog/source/drafts/.gitkeep
Normal file
@@ -1,13 +1,11 @@
|
||||
---
|
||||
title: 异构编程模型的昨天、今天与明天
|
||||
date: 2024-11-04T22:20:41.2571467+08:00
|
||||
tags:
|
||||
- 编译原理
|
||||
- 组会汇报
|
||||
date: 2024-10-16T13:34:49.0270134+08:00
|
||||
tags:
|
||||
- 编译原理
|
||||
- 组会汇报
|
||||
---
|
||||
|
||||
|
||||
|
||||
随着摩尔定律的逐渐失效,将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中,上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
|
||||
|
||||
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
|
||||
@@ -26,11 +24,11 @@ tags:
|
||||
|
||||
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中,比如多核CPU中,虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务,但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统,例如CPU、GPU和FPGA组成的异构系统,不同设备的微架构具有本质差异,其并行计算的模式和能力都完全不同,设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的,而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点,但是却给异构并行计算模型的设计带来了巨大的困难。
|
||||
|
||||

|
||||

|
||||
|
||||
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中,CPU片内的存储是对于软件透明的缓存架构,在片外则是一个共享内存模型,因此在这类系统中,数据仅可能分布在片外的共享存储中,具有存储位置单一的特点,也不需要进行显式的通信操作。但是在异构系统中,不仅在单个加速设备内部可能有软件可分配的快速局部存储,设备之间的连接方式差异也很大。目前,大多个加速设备都是通过PCIe总线的方式同CPU进行连接,这使得加速设备无法通过和CPU相同的方式完成地址映射,存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置,不仅使得编程模型的数据分布问题变得十分复杂,设备间的通信文件也可能需要显式进行。
|
||||
|
||||

|
||||

|
||||
|
||||
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题:在异构系统中数据可能分布在不同位置的条件下,同步操作需要在众多的位置上保证共享数据的一致性,这使得同步操作的范围变得十分复杂。同时,在一些特定的加速设备中,例如GPU,可能还会有局部的硬件同步机制,这更加提高了在异构系统的同步操作的设计和实现难度。
|
||||
|
||||
@@ -48,7 +46,7 @@ tags:
|
||||
|
||||
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
|
||||
|
||||

|
||||

|
||||
|
||||
### 异构任务划分机制研究
|
||||
|
||||
@@ -126,7 +124,7 @@ public class Result
|
||||
|
||||
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制,例如GPU的全局内存访存合并机制,这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程,会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法,例如`CUDA-lite`,这个运行时允许程序元在CUDA程序中加入简单的制导语句,数据分布的相关工作使用`CUDA-lite`的运行时系统完成,降低了CUDA程序的编写难度。
|
||||
|
||||

|
||||

|
||||
|
||||
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
|
||||
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
|
||||
@@ -139,7 +137,7 @@ public class Result
|
||||
|
||||
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
|
||||
|
||||

|
||||

|
||||
|
||||
### 异构编译/运行时优化
|
||||
|
||||
@@ -232,11 +230,11 @@ private:
|
||||
作为对比,一个使用CPU单线程计算的例子如下:
|
||||
|
||||
```cpp
|
||||
inline std::vector<int> cpuMatrixMultiply(
|
||||
const std::vector<int>& a,
|
||||
const std::vector<int>& b)
|
||||
std::vector<std::vector<int>> matrix_multiply(
|
||||
const std::vector<std::vector<int>>& a,
|
||||
const std::vector<std::vector<int>>& b)
|
||||
{
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
|
||||
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
@@ -245,10 +243,9 @@ inline std::vector<int> cpuMatrixMultiply(
|
||||
int temp = 0;
|
||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||
{
|
||||
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
|
||||
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
|
||||
temp += a[i][k] * b[k][j];
|
||||
}
|
||||
result[i * MATRIX_SIZE + j] = temp;
|
||||
result[i][j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +255,7 @@ inline std::vector<int> cpuMatrixMultiply(
|
||||
|
||||
### OpenMP
|
||||
|
||||
OpenMP是`Open MultiProcessing`的缩写,是一个使用编译器制导(Directives)来进行共享内存平行计算的框架,在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口,让程序员能够充分释放多核和多处理器系统性能。
|
||||
OpenMP是`Opem MultiProcessing`的缩写,是一个使用编译器制导(Directives)来进行共享内存平行计算的框架,在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口,让程序员能够充分释放多核和多处理器系统性能。
|
||||
|
||||
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型,但是第一,OpenMP作为一个经典的并行编程框架,研究价值还是非常高的,其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速,似乎也能算是一个异构并行编程模型。
|
||||
|
||||
@@ -449,21 +446,19 @@ std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<
|
||||
> - 首先是换了一台没有大小核异构设计的计算机进行实验,发现这下两次使用CPU计算的时间差异不大;
|
||||
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
|
||||
>
|
||||
> 综上所述,可以认为此现象和异构CPU之间存在着明显的关联,但是缺乏直接证据。
|
||||
>
|
||||
> 在我们调整了矩阵的数据布局之后,这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的,而在修改为使用一维数组(也就是现在提供的代码)之后,相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
|
||||
> 综上所述,可以认为此现象和异构CPU之间存在这明显的关联,但是缺乏直接证据。
|
||||
|
||||
### OpenCL
|
||||
|
||||
OpenCL是目前最为典型、发展最好的异构并行编程模型,毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
|
||||
|
||||

|
||||

|
||||
|
||||
从上图的OpenCL工作原理中可以看出,OpenCL和CUDA类似,也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写,编译之后在CPU上执行,而设备代码使用一个特定的C语言方言OpenCL C进行编写,这个方言针对并行编程进行了扩展,并提供了一系列封装好的数学计算函数。
|
||||
|
||||
设备代码上的编译方法有两种:在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码,离线编译则有两种表现形式,第一种是在线编译的扩展版,由驱动编译得到的可执行程序可以通过API获取并保存下来,当下一需要在同一设备上调用时可以直接使用而不是再次编译,第二种则是完全独立的编译过程,在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
|
||||
|
||||

|
||||

|
||||
|
||||
在提出离线编译之后,为了让驱动编译好的二进制文件可以在不同的设备之间复用,同时也是支持更为丰富的编译器生态系统,OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
|
||||
|
||||
@@ -659,7 +654,7 @@ AdaptiveCpp由四个部分组成,分别在不同的C++命名空间中提供。
|
||||
|
||||
- AdaptiveCpp Runtime:运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能,运行时负责同各种支持后端的运行时交互来实现上述的功能。
|
||||
|
||||

|
||||

|
||||
|
||||
- Compiler:考虑到在用户编写的代码中可能使用一些特定后端的方言,因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的,这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
|
||||
|
||||
@@ -673,7 +668,7 @@ AdaptiveCpp同时支持多种不同的编译流程。
|
||||
|
||||
第一种通用的编译流程显然是泛用性最广的一种编译流程,同时也是AdaptiveCpp推荐的编译流程。
|
||||
|
||||

|
||||

|
||||
|
||||
下面是一段使用SYCL进行矩阵乘法加速的代码:
|
||||
|
||||
@@ -777,93 +772,20 @@ OpenACC是作为一个标准的形式提供的,实现了该标准的编译器
|
||||
| GCC 12 | 支持到OpenACC 2.6 |
|
||||
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
|
||||
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
|
||||
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器,看上去还做了不少工作的样子,就是OpenACC官网上的链接已经失效了找起来比较麻烦,而且宣称是一个开源编译器,但是获取源代码和二进制文件需要联系他们(美国橡树岭国家实验室)创建账户,这看去对于我们这些Foreign Adversary有些抽象了。 |
|
||||
|
||||
在试验OpenACC时遇到了巨大的困难,不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速,多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
|
||||
|
||||
实验中编写的OpenACC加速代码如下:
|
||||
|
||||
```cpp
|
||||
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
|
||||
{
|
||||
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
|
||||
|
||||
const auto aBuffer = new int[length];
|
||||
const auto bBuffer = new int[length];
|
||||
const auto cBuffer = new int[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
aBuffer[i] = a[i];
|
||||
bBuffer[i] = b[i];
|
||||
cBuffer[i] = 0;
|
||||
}
|
||||
|
||||
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
|
||||
#pragma acc enter data create(bBuffer[0:length])
|
||||
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
|
||||
{
|
||||
#pragma acc kernels loop independent
|
||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||
{
|
||||
#pragma acc loop independent
|
||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
||||
{
|
||||
int temp = 0;
|
||||
#pragma acc loop independent reduction(+:temp)
|
||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||
{
|
||||
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
|
||||
}
|
||||
cBuffer[i * MATRIX_SIZE + j] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma acc exit data copyout(cBuffer[0:length])
|
||||
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
|
||||
|
||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
||||
|
||||
for (int i = 0; i < length; ++i)
|
||||
{
|
||||
result[i] = cBuffer[i];
|
||||
}
|
||||
|
||||
delete[] aBuffer;
|
||||
delete[] bBuffer;
|
||||
delete[] cBuffer;
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
|
||||
|
||||
| 编译器 | 类型 | 运行时间 |
|
||||
| -------------- | ------- | -------- |
|
||||
| NVIDIA HPC SDK | OpenACC | 19315ms |
|
||||
| NVIDIA HPC SDK | CPU | 22942ms |
|
||||
| GCC | OpenACC | 19999ms |
|
||||
| GCC | CPU | 22623ms |
|
||||
|
||||
### oneAPI
|
||||
|
||||
oneAPI是Intel公司提出的一套异构并行编程框架,该框架致力于达成如下几个目标:(1)定义一个跨架构、跨制造商的统一开放软件平台;(2)允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行;(3)提供一套覆盖多个编程领域的库API。为了实现这些目标,oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作,oneAPI也提供了一个SYCL的编译器和运行时;同时oneAPI也提供了一系列API库,包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
|
||||
|
||||

|
||||
|
||||
我对于oneAPI的理解就是Intel用来对标NVIDIA的CUDA的一套高性能编程工具箱。首先为了和NVIDIA完全闭源的CUDA形成鲜明的对比,Intel选择了OpenCL合作同时开发SYCL,当时也有可能是Intel知道自己的显卡技不如人,如果不兼容市面上其他的部件是没有出路的,同时为了和CUDA丰富的生态竞争,Intel再开发并开源了一系列的`oneXXX`。
|
||||
### Julia
|
||||
|
||||
这里我就把上面SYCL写的例子用Intel提供的`DPC++`编译运行一下,看看在效率上会不会有所变化。
|
||||
|
||||
| 类型 | 运行时间 | 比率 |
|
||||
| ----------------------------- | -------- | ----- |
|
||||
| Intel UHD Graphics 770 oneAPI | 429ms | 0.023 |
|
||||
| NVIDIA 4060 Ti oneAPI | 191ms | 0.010 |
|
||||
| Intel i5-13600K oneAPI | 198ms | 0.011 |
|
||||
| CPU | 18643ms | 1.000 |
|
||||
|
||||
在显卡上的计算时间没有明显的变化,但是我们Intel的编译器却在选择到使用Intel CPU进行计算时展现了不俗的实力。
|
||||
### Triton
|
||||
|
||||
|
||||
|
||||
|
||||
## 参考文献
|
||||
@@ -873,4 +795,5 @@ oneAPI是Intel公司提出的一套异构并行编程框架,该框架致力于
|
||||
3. Exploring the performance of SGEMM in OpenCL on NVIDIA GPUs. [https://github.com/CNugteren/myGEMM](https://github.com/CNugteren/myGEMM)
|
||||
4. OpenACC Programming and Best Practices Guide. [https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html](https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html)
|
||||
5. oneAPI What is it?. [https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html](https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html)
|
||||
6.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
YaeBlog/source/posts/.gitkeep
Normal file
0
YaeBlog/source/posts/.gitkeep
Normal file
@@ -1,12 +1,10 @@
|
||||
---
|
||||
title: 2021年终总结
|
||||
date: 2022-01-12T16:27:19.0000000
|
||||
date: 2022-01-12 16:27:19
|
||||
tags:
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
- 随笔
|
||||
---
|
||||
|
||||
|
||||
2021年已经过去,2022年已经来临。每每一年开始的时候,我都会展开一张纸或者新建一个文档,思量着又是一年时光,也该同诸大杂志一般,写几句意味深长的话语,怀念过去的时光,也祝福未来的自己。可往往脑海中已是三万字的长篇,落在笔头却又是一个字都没有了。
|
||||
如今跨年的时候已经过去,朋友圈中已经不见文案的踪影,我也该重新提笔,细说自己2021年中做过的种种。
|
||||
|
||||
@@ -24,7 +22,7 @@ tags:
|
||||
在前12年的学生生涯中,我们都在期待着这一次的暑假,以为在这个没有作业的假期里,我们就可以充分的享受人间的美好。可是,当时我们不知道,这人间的烦恼,可不止作业这一种,无论是突如其来的疫情导致开学延期,还是等待录取时的不安。
|
||||
虽说在暑假时,拥有了自己的笔记本电脑,可是在高中三年屯下的游戏还是没有玩几个,看来我也是“喜加一”的受害者。虽然在高考后入坑了原神,但是假期间我并没有太过投入的玩。
|
||||
暑假下定决心要好好的学一学,可是看着我gitee上暑假期间那稀疏的提交,我就知道我又摸了一个暑假的鱼。
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
|
||||
|
||||
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
LFS
Normal file
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
LFS
Normal file
Binary file not shown.
@@ -1,12 +1,10 @@
|
||||
---
|
||||
title: 2023年年终总结
|
||||
date: 2024-02-29T20:18:19.0000000
|
||||
tags:
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
- 随笔
|
||||
date: 2024-2-29 20:18:19
|
||||
---
|
||||
|
||||
|
||||
虽然2023年已经过去了两个月,但是年终总结还是要发的。
|
||||
|
||||
<!--more-->
|
||||
@@ -45,7 +43,7 @@ tags:
|
||||
|
||||
2023年最令我吃惊的事情是我刷B站的时长:
|
||||
|
||||

|
||||

|
||||
|
||||
容易计算得出,我一共看了64天的B站,接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯,但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变,将更多的时间放在看书上面去,~~虽然写这句话的时候我就在黑听B站~~。
|
||||
|
||||
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
LFS
Normal file
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
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