Compare commits
4 Commits
blog/async
...
feat/code-
| Author | SHA1 | Date | |
|---|---|---|---|
|
dfb66b4301
|
|||
|
05f99f4b79
|
|||
|
5fef951d36
|
|||
|
c9f6d638b9
|
@@ -12,6 +12,7 @@
|
||||
<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>
|
||||
|
||||
@@ -146,10 +146,6 @@ process {
|
||||
dotnet run -- serve
|
||||
break
|
||||
}
|
||||
"list" {
|
||||
dotnet run -- list
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
215
source/posts/tarjan-bridge.md
Normal file
215
source/posts/tarjan-bridge.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
title: Tarjan算法与实现
|
||||
date: 2026-03-28T21:53:45.1681856+08:00
|
||||
updateTime: 2026-03-28T21:53:45.1733146+08:00
|
||||
tags:
|
||||
- 技术笔记
|
||||
- 算法
|
||||
---
|
||||
|
||||
|
||||
Tarjan算法是一类用于无向图中割边和割点的算法。
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Tarjan算法
|
||||
|
||||
Tarjan算法是图论中非常常用的一种算法,基于深度优先搜索(DFS),基础版本的Tarjan算法用于求解无向图中的割点和桥。基于此可以求解图论中的一系列问题,例如无向图的双连通分量、有向图的强连通分量等问题。
|
||||
|
||||
Tarjan算法由计算机科学家Robert Tarjan在1972年于论文*Depth-First Search And Linear Graph Algorithms*中提出。Robert Tarjan是一位著名的计算机科学家,解决了图论中的一系列重大问题,同时也是斐波那契堆(Fibonacci Heap)和伸展树(Splay Tree)的开发者之一。他于1986年获得了图灵奖,目前仍在普林斯顿大学担任教职。
|
||||
|
||||
## 无向图的割点与桥
|
||||
|
||||
如果一个图中所有的边都是无向边,则称之为无向图。
|
||||
|
||||
### 割点
|
||||
|
||||
如果从无向图中删除节点x和所有与节点x关联的边之后,图将会被分成两个或者两个以上不相连的子图,那么节点x就是这个图的割点。下图中标注为红色的点就是该图的割点。
|
||||
|
||||

|
||||
|
||||
### 桥
|
||||
|
||||
如果从图中删除边e之后,图将分裂为两个不相连的子图,那么就称e是图的桥,或者割边。
|
||||
|
||||

|
||||
|
||||
图中被标注为红色的边就是该图的桥。
|
||||
|
||||
## 求解图中的割点
|
||||
|
||||
Tarjan算法中为了求解桥和割点,首先定义了如下几个概念。
|
||||
|
||||
### 时间戳
|
||||
|
||||
时间戳用来标记图中每个节点在进行深度优先搜索的过程中被访问的时间顺序,这个概念起始也就是在遍历的时候给每个节点编号。
|
||||
|
||||
这个编号用`search_number[x]`来表示,其中的x是节点。
|
||||
|
||||
### 搜索树
|
||||
|
||||
在图中,如果从一个节点x出发进行深度优先的搜索,在搜索的过程中每个节点只能访问一次,所有被访问的节点可以构成一棵树,这棵树就被称为无向连通图的搜索树。
|
||||
|
||||
### 追溯值
|
||||
|
||||
追溯值的定义和计算是Tarjan算法的核心。
|
||||
|
||||
追溯值被定义为,从当前节点x作为搜索树的根节点出现,能够访问到的所有节点中,时间戳的最小值,被记为`low[x]`。
|
||||
|
||||
定义中主要的限定条件是“能够访问到的所有节点”,主要考虑的是如下两种访问方式:
|
||||
|
||||
- 这个节点在以x为根的搜索树上
|
||||
- 通过一条不属于搜索树的边,可以到达搜索树的节点。
|
||||
|
||||
例如上图的例子中,考虑直接从节点1出发开始深度优先的遍历,此时使用的遍历顺序是节点1、节点2、节点3、节点4、节点5。
|
||||
|
||||

|
||||
|
||||
当遍历到节点5时,考虑以节点5为根的搜索树(可以认为此时的搜索树中只有节点5一个节点),可以发现有两条不属于搜索树的边(2, 5)和(1, 5),使得节点1和节点2成为了上述“可以访问到的节点”,因此将节点5的追溯值更新为1。
|
||||
|
||||

|
||||
|
||||
此时,算法按照深度优先搜索的顺序开始回溯,在回溯的过程中逐步更新当前节点的追溯值,此时就是按照上面“可以访问的所有节点”中的搜索树情形工作了。例如当回溯到节点3时,可以认为存在以节点3为根节点的搜索树{3, 4, 5},其中追溯值的最小值为1, 将节点3的追溯值更新为1。
|
||||
|
||||

|
||||
|
||||
### 桥的判定法则
|
||||
|
||||
在无向图中,对于一条边`e = (u ,v)`,如果满足`search_number[u] < low[v]`,那么该边就是图中的一个桥。
|
||||
|
||||
这个条件所蕴含的意思是,节点u被访问的时间,要小于(优先于)以下所有这些节点被访问的时间:
|
||||
|
||||
- 以节点v为根节点的搜索树中的所有节点
|
||||
- 通过一条非搜索树上的边,能否到达搜索树的所有节点。
|
||||
|
||||
## 实现
|
||||
|
||||
以下以[1192. 查找集群内的关键连接 - 力扣(LeetCode)](https://leetcode.cn/problems/critical-connections-in-a-network/description/)为例,给出Tarjan算法的实现。
|
||||
|
||||
```cpp
|
||||
namespace {
|
||||
/// Graph structure.
|
||||
/// Store the graph using linked forwarded stars.
|
||||
/// The linked forwarded stars store the graph using linked list.
|
||||
///
|
||||
/// To accelerate the loading and storing, use array to simulate the linked
|
||||
/// list.
|
||||
struct Graph {
|
||||
explicit Graph(const size_t nodeCount, const size_t edgeCount) {
|
||||
// The edgeID starts from 2, as 0 is used as null value.
|
||||
// And to find the reverse edge by i ^ 1, so 0 and 1 are both skipped.
|
||||
endNodes = vector<size_t>(edgeCount + 2, 0);
|
||||
nextEdges = vector<size_t>(edgeCount + 2, 0);
|
||||
headEdges = vector<size_t>(nodeCount, 0);
|
||||
}
|
||||
|
||||
void addEdge(const size_t x, const size_t y) {
|
||||
endNodes[edgeID] = y;
|
||||
nextEdges[edgeID] = headEdges[x];
|
||||
headEdges[x] = edgeID;
|
||||
|
||||
edgeID += 1;
|
||||
}
|
||||
|
||||
vector<bool> calculateBridges() {
|
||||
// Initialize values used by tarjan algorithm.
|
||||
const auto nodeCount = headEdges.size();
|
||||
bridges = vector(endNodes.size(), false);
|
||||
nodeIDs = vector<size_t>(nodeCount, 0);
|
||||
lowValues = vector<size_t>(nodeCount, 0);
|
||||
|
||||
number = 1;
|
||||
|
||||
for (auto i = 0; i < nodeCount; i++) {
|
||||
if (nodeIDs[i] == 0) {
|
||||
tarjan(i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return bridges;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t edgeID = 2;
|
||||
|
||||
/// Represent the end node of edge i.
|
||||
vector<size_t> endNodes;
|
||||
|
||||
/// Represent the next edge of edge i.
|
||||
vector<size_t> nextEdges;
|
||||
|
||||
/// Represent the head edge of node i.
|
||||
/// Also, head of simulated linked list.
|
||||
vector<size_t> headEdges;
|
||||
|
||||
vector<bool> bridges;
|
||||
|
||||
/// Represent timestamp of node i, 0 is used as unvisited.
|
||||
vector<size_t> nodeIDs;
|
||||
|
||||
vector<size_t> lowValues;
|
||||
|
||||
size_t number = 1;
|
||||
|
||||
void tarjan(const size_t node, const size_t inEdge) {
|
||||
nodeIDs[node] = lowValues[node] = number;
|
||||
number += 1;
|
||||
|
||||
for (auto i = headEdges[node]; i != 0; i = nextEdges[i]) {
|
||||
|
||||
// If the next node is not visited.
|
||||
if (const auto end = endNodes[i]; nodeIDs[end] == 0) {
|
||||
tarjan(end, i);
|
||||
|
||||
lowValues[node] = min(lowValues[node], lowValues[end]);
|
||||
if (lowValues[end] > nodeIDs[node]) {
|
||||
// Subtract 2 as the edge ID starts from 2.
|
||||
bridges[i - 2] = true;
|
||||
bridges[(i ^ 1) - 2] = true;
|
||||
}
|
||||
} else {
|
||||
// If edge i is visited and edge i is not the coming edge.
|
||||
if (i != (inEdge ^ 1)) {
|
||||
lowValues[node] = min(lowValues[node], nodeIDs[end]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
class Solution {
|
||||
public:
|
||||
vector<vector<int>> criticalConnections(int n,
|
||||
vector<vector<int>> &connections) {
|
||||
// To store the undirected graph, double the edge count.
|
||||
auto g = Graph{static_cast<size_t>(n), connections.size() * 2};
|
||||
|
||||
for (const auto &edge : connections) {
|
||||
g.addEdge(edge[0], edge[1]);
|
||||
g.addEdge(edge[1], edge[0]);
|
||||
}
|
||||
|
||||
auto bridges = g.calculateBridges();
|
||||
vector<vector<int>> result;
|
||||
for (auto i = 0; i < bridges.size(); i = i + 2) {
|
||||
if (bridges[i]) {
|
||||
const auto &edge = connections[i / 2];
|
||||
result.push_back(edge);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 链式前向星
|
||||
|
||||
在上面的实现中使用了一种较为高效的图存储方法-链式前向星(Linked Forward Star)。
|
||||
|
||||
链式前向星是一种类似于邻接表的图存储方法,提供了较为高效的边遍历方法。这种方法的本质上是按节点聚合的边链表,不过在上面的实现中使用了数组来存储链表的头结点和每个节点的下一个节点指针。
|
||||
|
||||
同时这种存储方法还提供了一种非常方便的反向边查找方法:考虑在存储无向图中的边时,将一条边成对的存储在数组中,由此针对任意一条边i,`i ^ 1`就是这条边的反向边。
|
||||
BIN
source/posts/tarjan-bridge/image-20260328213522542.webp
LFS
Normal file
BIN
source/posts/tarjan-bridge/image-20260328213522542.webp
LFS
Normal file
Binary file not shown.
BIN
source/posts/tarjan-bridge/image-20260328213554309.webp
LFS
Normal file
BIN
source/posts/tarjan-bridge/image-20260328213554309.webp
LFS
Normal file
Binary file not shown.
BIN
source/posts/tarjan-bridge/image-20260328213641303.webp
LFS
Normal file
BIN
source/posts/tarjan-bridge/image-20260328213641303.webp
LFS
Normal file
Binary file not shown.
BIN
source/posts/tarjan-bridge/image-20260328213702466.webp
LFS
Normal file
BIN
source/posts/tarjan-bridge/image-20260328213702466.webp
LFS
Normal file
Binary file not shown.
BIN
source/posts/tarjan-bridge/image-20260328213720719.webp
LFS
Normal file
BIN
source/posts/tarjan-bridge/image-20260328213720719.webp
LFS
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Abstractions;
|
||||
|
||||
public interface IEssayContentService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Abstractions;
|
||||
|
||||
public interface IEssayScanService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Abstractions;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Abstractions;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 单个博客文件的所有数据和元数据
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
|
||||
: IEnumerable<BlogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public record BlogEssay(
|
||||
string Title,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public record BlogImageInfo(FileInfo File, long Width, long Height, string MineType, byte[] Content, bool IsUsed)
|
||||
: IComparable<BlogImageInfo>
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public class GiteaOptions
|
||||
{
|
||||
@@ -8,7 +8,7 @@ public class GiteaOptions
|
||||
|
||||
[Required] public required string BaseAddress { get; init; }
|
||||
|
||||
[Required] public required string ApiKey { get; init; }
|
||||
public string? ApiKey { get; init; }
|
||||
|
||||
[Required] public required string HeatMapUsername { get; init; }
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public record GitContributionItem(DateOnly Time, long ContributionCount);
|
||||
public record GitContributionItem(DateOnly Time, long ContributionCount)
|
||||
{
|
||||
public string ItemId => $"item-{Time:yyyy-MM-dd}";
|
||||
}
|
||||
|
||||
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
9
src/YaeBlog.Abstractions/YaeBlog.Abstractions.csproj
Normal file
9
src/YaeBlog.Abstractions/YaeBlog.Abstractions.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,7 +1,8 @@
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Tests;
|
||||
@@ -9,6 +10,7 @@ namespace YaeBlog.Tests;
|
||||
public sealed class GiteaFetchServiceTests
|
||||
{
|
||||
private static readonly Mock<IOptions<GiteaOptions>> s_giteaOptionsMock = new();
|
||||
private static readonly Mock<ILogger<GiteaFetchService>> s_logger = new();
|
||||
private readonly GiteaFetchService _giteaFetchService;
|
||||
|
||||
public GiteaFetchServiceTests()
|
||||
@@ -16,12 +18,10 @@ public sealed class GiteaFetchServiceTests
|
||||
s_giteaOptionsMock.SetupGet(o => o.Value)
|
||||
.Returns(new GiteaOptions
|
||||
{
|
||||
BaseAddress = "https://git.rrricardo.top/api/v1/",
|
||||
ApiKey = "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||
HeatMapUsername = "jackfiled"
|
||||
BaseAddress = "https://git.rrricardo.top/api/v1/", HeatMapUsername = "jackfiled"
|
||||
});
|
||||
|
||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient());
|
||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient(), s_logger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Tests;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@using Microsoft.Extensions.Options
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject IOptions<BlogOptions> Options
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="text-3xl font-bold py-2">
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||
驱动。
|
||||
</p>
|
||||
<p class="text-md">
|
||||
Build Commit #
|
||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
|
||||
</p>
|
||||
@if (!string.IsNullOrEmpty(BuildCommitId))
|
||||
{
|
||||
<p class="text-md">
|
||||
Build Commit #
|
||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId" NewPage="true"/>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -24,7 +27,7 @@
|
||||
{
|
||||
private static string DotnetVersion => $".NET {Environment.Version}";
|
||||
|
||||
private static string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
|
||||
private static string? BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID");
|
||||
|
||||
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Services
|
||||
@inject GitHeapMapService GitHeapMapInstance
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<SvgGroup Transform="@GlobalMonthTransform">
|
||||
@foreach ((int i, string text) in _monthIndices)
|
||||
{
|
||||
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[10px]"/>
|
||||
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[8px] font-light"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
<SvgGroup Transform="@GlobalWeekTransform">
|
||||
@foreach ((int i, string text) in Weekdays.Index())
|
||||
{
|
||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[10px]"/>
|
||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[8px] font-light"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
<SvgGroup Transform="@GlobalMapTransform">
|
||||
@@ -23,7 +23,8 @@
|
||||
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
||||
{
|
||||
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
||||
Class="@(GetColorByContribution(item.ContributionCount))"/>
|
||||
Class="@(GetColorByContribution(item.ContributionCount))"
|
||||
Id="@(item.ItemId)"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
}
|
||||
@@ -118,5 +119,4 @@
|
||||
_ => "fill-blue-800"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
<Footer/>
|
||||
</main>
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
<Footer/>
|
||||
</main>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">关于</h1>
|
||||
<h1 class="text-4xl page-starter">关于</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog/archives"
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">归档</h1>
|
||||
<h1 class="text-4xl page-starter">归档</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog"
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,6 +10,7 @@
|
||||
</PageTitle>
|
||||
|
||||
<div>
|
||||
<div class="page-starter"></div>
|
||||
<div class="grid grid-cols-4">
|
||||
<div class="col-span-4 md:col-span-3">
|
||||
@foreach (BlogEssay essay in _essays)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/essays/{BlogKey}"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -14,7 +14,7 @@
|
||||
<div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div>
|
||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||
<h1 id="title" class="text-4xl page-starter">@(_essay!.Title)</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/friends"
|
||||
@using Microsoft.Extensions.Options
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@inject IOptions<BlogOptions> BlogOptionInstance
|
||||
|
||||
<PageTitle>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl">
|
||||
<h1 class="text-4xl page-starter">
|
||||
友链
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/"
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@inject IEssayContentService EssayContentInstance
|
||||
|
||||
<PageTitle>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
<div class="flex flex-col gap-y-3 items-center md:items-start md:px-6">
|
||||
<div class="">
|
||||
<div class="text-3xl font-bold">初冬的朝阳</div>
|
||||
<div class="text-3xl font-bold page-starter">初冬的朝阳</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PageTitle>
|
||||
|
||||
<div>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
<h3 class="text-3xl page-starter">NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/tags/"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -11,7 +11,7 @@
|
||||
</PageTitle>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div class="page-starter">
|
||||
@if (TagName is null)
|
||||
{
|
||||
<h1 class="text-4xl">标签</h1>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
|
||||
}
|
||||
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="page-starter"/>
|
||||
</Found>
|
||||
</Router>
|
||||
|
||||
@@ -18,5 +18,17 @@ public static class DateOnlyExtensions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public int DayNumberOfWeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return date.DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Sunday => 7,
|
||||
_ => (int)date.DayOfWeek + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,73 @@
|
||||
using AngleSharp;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Processors;
|
||||
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
public static class HostApplicationBuilderExtensions
|
||||
{
|
||||
extension(WebApplicationBuilder builder)
|
||||
extension(IHostApplicationBuilder builder)
|
||||
{
|
||||
public WebApplicationBuilder AddYaeBlog()
|
||||
public ConsoleInfoService AddYaeCommand(string[] arguments)
|
||||
{
|
||||
builder.ConfigureOptions<BlogOptions>(BlogOptions.OptionName)
|
||||
.ConfigureOptions<GiteaOptions>(GiteaOptions.OptionName);
|
||||
builder.AddCommonServices();
|
||||
|
||||
builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
|
||||
|
||||
builder.Services.AddTransient<ImageCompressService>();
|
||||
builder.Services.AddHostedService<YaeCommandService>(provider =>
|
||||
{
|
||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||
ImageCompressService imageCompressService = provider.GetRequiredService<ImageCompressService>();
|
||||
ConsoleInfoService consoleInfoService = provider.GetRequiredService<ConsoleInfoService>();
|
||||
IOptions<BlogOptions> blogOptions = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||
ILogger<YaeCommandService> logger = provider.GetRequiredService<ILogger<YaeCommandService>>();
|
||||
IHostApplicationLifetime hostApplicationLifetime =
|
||||
provider.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
return new YaeCommandService(arguments, essayScanService, imageCompressService, consoleInfoService,
|
||||
hostApplicationLifetime, blogOptions, logger);
|
||||
});
|
||||
|
||||
ConsoleInfoService infoService = new();
|
||||
builder.Services.AddSingleton<ConsoleInfoService>(_ => infoService);
|
||||
|
||||
return infoService;
|
||||
}
|
||||
|
||||
private void AddCommonServices()
|
||||
{
|
||||
builder.Services.AddHttpClient()
|
||||
.AddMarkdig()
|
||||
.AddYamlParser();
|
||||
|
||||
builder.ConfigureOptions<BlogOptions>(BlogOptions.OptionName)
|
||||
.ConfigureOptions<GiteaOptions>(GiteaOptions.OptionName);
|
||||
|
||||
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
||||
}
|
||||
|
||||
private IHostApplicationBuilder ConfigureOptions<T>(string optionSectionName) where T : class
|
||||
{
|
||||
builder.Services
|
||||
.AddOptions<T>()
|
||||
.Bind(builder.Configuration.GetSection(optionSectionName))
|
||||
.ValidateDataAnnotations();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
extension(WebApplicationBuilder builder)
|
||||
{
|
||||
public WebApplicationBuilder AddYaeServer(ConsoleInfoService consoleInfoService)
|
||||
{
|
||||
builder.AddCommonServices();
|
||||
|
||||
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default)
|
||||
.AddSingleton<ConsoleInfoService>(_ => consoleInfoService)
|
||||
.AddSingleton<IEssayScanService, EssayScanService>()
|
||||
.AddSingleton<RendererService>()
|
||||
.AddSingleton<IEssayContentService, EssayContentService>()
|
||||
@@ -32,31 +79,8 @@ public static class WebApplicationBuilderExtensions
|
||||
.AddTransient<BlogHotReloadService>()
|
||||
.AddSingleton<GitHeapMapService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
builder.Services.AddHostedService<StartServerService>();
|
||||
|
||||
public WebApplicationBuilder AddYaeCommand(string[] arguments)
|
||||
{
|
||||
builder.Services.AddHostedService<YaeCommandService>(provider =>
|
||||
{
|
||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||
IOptions<BlogOptions> blogOptions = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||
ILogger<YaeCommandService> logger = provider.GetRequiredService<ILogger<YaeCommandService>>();
|
||||
IHostApplicationLifetime applicationLifetime = provider.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
return new YaeCommandService(arguments, essayScanService, provider, blogOptions, logger,
|
||||
applicationLifetime);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private WebApplicationBuilder ConfigureOptions<T>(string optionSectionName) where T : class
|
||||
{
|
||||
builder.Services
|
||||
.AddOptions<T>()
|
||||
.Bind(builder.Configuration.GetSection(optionSectionName))
|
||||
.ValidateDataAnnotations();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Processors;
|
||||
using YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Services;
|
||||
|
||||
HostApplicationBuilder consoleBuilder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
ConsoleInfoService consoleInfoService = consoleBuilder.AddYaeCommand(args);
|
||||
|
||||
IHost consoleApp = consoleBuilder.Build();
|
||||
await consoleApp.RunAsync();
|
||||
|
||||
if (consoleInfoService.IsOneShotCommand)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddYaeCommand(args);
|
||||
builder.AddYaeServer(consoleInfoService);
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
14
src/YaeBlog/Services/ConsoleInfoService.cs
Normal file
14
src/YaeBlog/Services/ConsoleInfoService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public enum ServerCommand
|
||||
{
|
||||
Serve,
|
||||
Watch
|
||||
}
|
||||
|
||||
public sealed class ConsoleInfoService
|
||||
{
|
||||
public bool IsOneShotCommand { get; set; }
|
||||
|
||||
public ServerCommand Command { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
|
||||
using Imageflow.Bindings;
|
||||
using Imageflow.Fluent;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class GitHeapMapService(IServiceProvider serviceProvider, IOptions<GiteaOptions> giteaOptions,
|
||||
public sealed class GitHeapMapService(
|
||||
IServiceProvider serviceProvider,
|
||||
IOptions<GiteaOptions> giteaOptions,
|
||||
ILogger<GitHeapMapService> logger)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -83,7 +85,24 @@ public sealed class GitHeapMapService(IServiceProvider serviceProvider, IOptions
|
||||
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
||||
}
|
||||
|
||||
// Not fill the last item and add directly.
|
||||
// If the last contributing day is not today, fill the spacing.
|
||||
// But be careful here! If the last grouped contribution is current week, just fill the spacing until today.
|
||||
// If the last grouped contribution is before current week, first fill the blank week then fill until today.
|
||||
while (groupedContribution.Monday < today.LastMonday)
|
||||
{
|
||||
FillSpacing(groupedContribution, today);
|
||||
result.Add(groupedContribution);
|
||||
groupedContribution = new GitContributionGroupedByWeek(groupedContribution.Monday.AddDays(7), []);
|
||||
}
|
||||
|
||||
// Currently the grouped contribution must be current week.
|
||||
for (DateOnly date = groupedContribution.Monday.AddDays(groupedContribution.Contributions.Count);
|
||||
date <= today;
|
||||
date = date.AddDays(1))
|
||||
{
|
||||
groupedContribution.Contributions.Add(new GitContributionItem(date, 0));
|
||||
}
|
||||
|
||||
result.Add(groupedContribution);
|
||||
|
||||
_gitContributionsGroupedByWeek = result;
|
||||
|
||||
@@ -3,34 +3,45 @@ using System.Text.Json;
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class GiteaFetchService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<GiteaFetchService> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
RespectRequiredConstructorParameters = true, RespectNullableAnnotations = true
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
RespectRequiredConstructorParameters = true,
|
||||
RespectNullableAnnotations = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// For test only.
|
||||
/// </summary>
|
||||
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient)
|
||||
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient,
|
||||
ILogger<GiteaFetchService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", giteaOptions.Value.ApiKey);
|
||||
if (string.IsNullOrWhiteSpace(giteaOptions.Value.ApiKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Api Token is set.");
|
||||
httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("token", giteaOptions.Value.ApiKey);
|
||||
}
|
||||
|
||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory) : this(
|
||||
giteaOptions, httpClientFactory.CreateClient())
|
||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory,
|
||||
ILogger<GiteaFetchService> logger) : this(giteaOptions, httpClientFactory.CreateClient(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -50,6 +61,7 @@ public sealed class GiteaFetchService
|
||||
new GiteaFetchException("Failed to fetch valid data."));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetch new user heat map data.");
|
||||
return Result.FromValue(data.Select(i =>
|
||||
new GitContributionItem(DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(i.Timestamp).DateTime),
|
||||
i.Contributions)).ToList());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Imageflow.Fluent;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
if (needCompressContents.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No candidates found to be compressed.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
foreach (BlogImageInfo image in uncompressedImages)
|
||||
{
|
||||
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
|
||||
logger.LogInformation("Uncompressed image: {filename} belonging to blog {blog}.", image.File.Name,
|
||||
content.BlogName);
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
|
||||
|
||||
if (dryRun is false)
|
||||
if (!dryRun)
|
||||
{
|
||||
await Task.WhenAll(from content in compressedContent
|
||||
select essayScanService.SaveBlogContent(content, content.IsDraft));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
27
src/YaeBlog/Services/StartServerService.cs
Normal file
27
src/YaeBlog/Services/StartServerService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class StartServerService(ConsoleInfoService consoleInfoService,
|
||||
RendererService rendererService,
|
||||
BlogHotReloadService blogHotReloadService) : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
switch (consoleInfoService.Command)
|
||||
{
|
||||
case ServerCommand.Serve:
|
||||
{
|
||||
await rendererService.RenderAsync();
|
||||
break;
|
||||
}
|
||||
case ServerCommand.Watch:
|
||||
{
|
||||
await blogHotReloadService.StartAsync(cancellationToken);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -2,30 +2,31 @@
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public class YaeCommandService(
|
||||
public sealed class YaeCommandService(
|
||||
string[] arguments,
|
||||
IEssayScanService essayScanService,
|
||||
IServiceProvider serviceProvider,
|
||||
ImageCompressService imageCompressService,
|
||||
ConsoleInfoService consoleInfoService,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
IOptions<BlogOptions> blogOptions,
|
||||
ILogger<YaeCommandService> logger,
|
||||
IHostApplicationLifetime applicationLifetime)
|
||||
: IHostedService
|
||||
ILogger<YaeCommandService> logger)
|
||||
: BackgroundService
|
||||
{
|
||||
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||
private bool _oneShotCommandFlag = true;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
RootCommand rootCommand = new("YaeBlog CLI");
|
||||
|
||||
RegisterServeCommand(rootCommand);
|
||||
RegisterWatchCommand(rootCommand, cancellationToken);
|
||||
RegisterWatchCommand(rootCommand);
|
||||
|
||||
RegisterNewCommand(rootCommand);
|
||||
RegisterUpdateCommand(rootCommand);
|
||||
@@ -33,6 +34,10 @@ public class YaeCommandService(
|
||||
RegisterPublishCommand(rootCommand);
|
||||
RegisterCompressCommand(rootCommand);
|
||||
|
||||
// Shit code: wait for the application starting.
|
||||
// If the command service finished early before the application starting, there will be an ugly exception.
|
||||
await Task.Delay(500, stoppingToken);
|
||||
logger.LogInformation("Running YaeBlog Command.");
|
||||
int exitCode = await rootCommand.InvokeAsync(arguments);
|
||||
|
||||
if (exitCode != 0)
|
||||
@@ -40,13 +45,14 @@ public class YaeCommandService(
|
||||
throw new BlogCommandException($"YaeBlog command exited with no-zero code {exitCode}");
|
||||
}
|
||||
|
||||
if (_oneShotCommandFlag)
|
||||
{
|
||||
applicationLifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
consoleInfoService.IsOneShotCommand = _oneShotCommandFlag;
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
if (!consoleInfoService.IsOneShotCommand)
|
||||
{
|
||||
logger.LogInformation("Start YaeBlog command: {}", consoleInfoService.Command);
|
||||
}
|
||||
hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
private void RegisterServeCommand(RootCommand rootCommand)
|
||||
{
|
||||
@@ -59,27 +65,23 @@ public class YaeCommandService(
|
||||
rootCommand.SetHandler(HandleServeCommand);
|
||||
}
|
||||
|
||||
private async Task HandleServeCommand(InvocationContext context)
|
||||
private Task HandleServeCommand(InvocationContext context)
|
||||
{
|
||||
_oneShotCommandFlag = false;
|
||||
consoleInfoService.Command = ServerCommand.Serve;
|
||||
|
||||
logger.LogInformation("Failed to load cache, re-render essays.");
|
||||
RendererService rendererService = serviceProvider.GetRequiredService<RendererService>();
|
||||
await rendererService.RenderAsync();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RegisterWatchCommand(RootCommand rootCommand, CancellationToken cancellationToken)
|
||||
private void RegisterWatchCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
command.SetHandler(async _ =>
|
||||
command.SetHandler(_ =>
|
||||
{
|
||||
_oneShotCommandFlag = false;
|
||||
|
||||
// BlogHotReloadService is derived from BackgroundService, but we do not let framework trigger it.
|
||||
BlogHotReloadService blogHotReloadService = serviceProvider.GetRequiredService<BlogHotReloadService>();
|
||||
await blogHotReloadService.StartAsync(cancellationToken);
|
||||
consoleInfoService.Command = ServerCommand.Watch;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,12 +272,6 @@ public class YaeCommandService(
|
||||
getDefaultValue: () => false);
|
||||
command.AddOption(dryRunOption);
|
||||
|
||||
command.SetHandler(HandleCompressCommand, dryRunOption);
|
||||
}
|
||||
|
||||
private async Task HandleCompressCommand(bool dryRun)
|
||||
{
|
||||
ImageCompressService imageCompressService = serviceProvider.GetRequiredService<ImageCompressService>();
|
||||
await imageCompressService.Compress(dryRun);
|
||||
command.SetHandler(async dryRun => { await imageCompressService.Compress(dryRun); }, dryRunOption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
||||
<ProjectReference Include="..\YaeBlog.Abstractions\YaeBlog.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user