Compare commits
4 Commits
master
...
4de644036f
| Author | SHA1 | Date | |
|---|---|---|---|
|
4de644036f
|
|||
|
0d10946ec1
|
|||
|
a3791596da
|
|||
|
2be09b8319
|
@@ -8,19 +8,19 @@ jobs:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- name: Check out code.
|
||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/checkout.git@v4
|
||||
uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project.
|
||||
run: |
|
||||
git submodule update --init
|
||||
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
||||
cd YaeBlog
|
||||
pwsh build.ps1 build
|
||||
- name: Workaround to make sure podman-login working.
|
||||
run: |
|
||||
mkdir -p /root/.docker
|
||||
mkdir /root/.docker
|
||||
- name: Login tencent cloud docker registry.
|
||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/podman-login.git@v1
|
||||
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||
with:
|
||||
registry: ccr.ccs.tencentyun.com
|
||||
username: 100044380877
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -184,7 +184,6 @@ DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
out/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<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>
|
||||
|
||||
21
build.ps1
21
build.ps1
@@ -3,15 +3,16 @@
|
||||
[cmdletbinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
|
||||
[ValidateSet("publish", "compress", "build", "dev", "new", "watch", "serve")]
|
||||
[ValidateSet("tailwind", "publish", "compress", "build", "dev", "new", "watch", "serve", "list")]
|
||||
[string]$Target,
|
||||
[string]$Output = "wwwroot",
|
||||
[string]$Essay,
|
||||
[switch]$Compress,
|
||||
[string]$Root = "source"
|
||||
)
|
||||
|
||||
begin {
|
||||
if (($Target -eq "tailwind") -or ($Target -eq "build"))
|
||||
if ($Target -eq "tailwind")
|
||||
{
|
||||
# Handle tailwind specially.
|
||||
return
|
||||
@@ -81,11 +82,8 @@ process {
|
||||
function Build-Image
|
||||
{
|
||||
$commitId = git rev-parse --short=10 HEAD
|
||||
dotnet publish ./src/YaeBlog/YaeBlog.csproj -o out
|
||||
Write-Host "Succeed to build blog appliocation."
|
||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId `
|
||||
-f ./src/YaeBlog/Dockerfile
|
||||
Write-Host "Succeed to build ccr.ccs.tencentyun.com/jackfiled/blog image."
|
||||
dotnet publish
|
||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
|
||||
}
|
||||
|
||||
function Start-Develop {
|
||||
@@ -113,6 +111,11 @@ process {
|
||||
|
||||
switch ($Target)
|
||||
{
|
||||
"tailwind" {
|
||||
Write-Host "Build tailwind css into $Output."
|
||||
pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css
|
||||
break
|
||||
}
|
||||
"publish" {
|
||||
Write-Host "Publish essay $Essay..."
|
||||
dotnet run -- publish $Essay
|
||||
@@ -146,6 +149,10 @@ process {
|
||||
dotnet run -- serve
|
||||
break
|
||||
}
|
||||
"list" {
|
||||
dotnet run -- list
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
title: 2025年终总结
|
||||
date: 2026-05-08T01:22:18.6904350+08:00
|
||||
updateTime: 2026-05-08T01:22:18.8859180+08:00
|
||||
tags:
|
||||
- 杂谈
|
||||
- 年终总结
|
||||
---
|
||||
|
||||
|
||||
|
||||
越来越晚的年终总结是本站不得不品的一大特色,在这样下去2026年的年终总结要到2028年才能面世了(绝望)。不过2025年确实是非常丰富多彩的一年,这一年的经历是如此的多样,以至于真正需要提笔写下来的时候反而不知道可以写什么。
|
||||
|
||||
<!--more-->
|
||||
|
||||
### 毕业
|
||||
|
||||
在2025年的6月,我从明光村幼儿园附属大学的括号学院毕业了。
|
||||
|
||||
毕业本身似乎并不是什么特别需要记录的事情,不过是一篇论文、两张证书和几次合照,现在回想起来只有一种如堕梦中的感觉。
|
||||
|
||||
虽然对于其他的事情都已经几乎淡忘,但是因为我提前自愿(?)选修了研究生课程《高性能计算》,而这门课的期末考试日期甚至在毕业典礼之后。这不得不使得我在毕业季各种事务缠身的情况下还得抽出时间准备考试,而这个《高性能计算》课程的内容又多又杂,实在是又难学又难背。这使得我对于这个毕业季影响最深刻的事情不是什么毕业合照,而是SMP和CUDA!
|
||||
|
||||
不过说都说到这里了,顺便回忆一下我的四年大学生活吧。
|
||||
|
||||
### 我的大学
|
||||
|
||||
还记得2021年的暑假,我还在和一个名叫Microsoft Visual Studio的神秘软件搏斗,并不知道这个软件中某个叫作.NET SDK的东西会成为这四年中的一个重要组成部分,当时还创作了本博客的[第一篇文章](https://rrricardo.top/blog/essays/question-in-install-vs-2019)。
|
||||
|
||||
2021年的9月第一次来到校园,不得不说该附属大学的偏远校区还是很符合我对于大学校园的刻板印象的。标准的四人间、高大的图书馆,迷宫一样的教学楼设计充分满足了我对于大学生活的一切想象,甚至到主校区需要坐一个半小时地铁也是刻板印象的一部分。
|
||||
|
||||
不过这样的“幸福大学生活”只持续了四个月。在2022年9月份就回到了宇宙中心的南边,中国最宽公路的东边,我们伟大的明光村。很高情商地说,在这个校区中学习和生活,可以**随时随地**地品味到这个学校深厚的历史底蕴。例如我现在居住的宿舍,在20年前也居住过我的导师。课间在窗边踱步,你可以欣赏到墙上的照片里,脚下的教学楼在1956年落成时的雄姿。
|
||||
|
||||
还是谈谈在这四年中我所修过的那些课程吧。本来在2025年6月毕业之后,我打算写一篇文章,结合我四年中的经历,详细分析一下我所经历的计算机科学本科教学。但是迟迟没有时间落笔,就在这里简单评述一下。
|
||||
|
||||

|
||||
|
||||
首先是培养方案中归类为“数学与自然科学”的课程:《高等数学A》和《线性代数》是必修课,其中《高等数学A》因为代课老师江彦是一个**认真负责**的好老师,这里给到一个夯。《线性代数》则是因为课程设计和老师只能给到一个NPC,说实话本来想给到一个拉完了,但是考虑到这门课算是计算机科学比较重要的数学基础,还是给到NPC。
|
||||
|
||||
两门概率论课程我选的是《概率论与随机过程》,同样因为代课老师鞠红杰给到一个顶级,而且随机过程本来就是一个非常有趣的研究课题,这门课的课程论文就是博客中的[原神抽卡研究](https://rrricardo.top/blog/essays/genshin-gacha-1)。至于《组合数学》、《运筹学》、《数学建模与模拟》和《矩阵理论与方法》四选一,我选择的是《数学建模与模拟》,这个课程只能给到NPC,听了和没听一样,老师脾气还不太好。《大学物理C》同样只能给到一个NPC,虽然我对于物理还是挺感兴趣的~~还参加了大学生物理竞赛~~,但是这实在和计算机学科关系不太大,安排这个不如多安排一些通信和电子的内容。
|
||||
|
||||
然后是培养方案中“学科基础”大类课程,这个课程组中的课程都是必修课程。《计算导论与程序设计》是大一上学期的唯一一门专业课,也是计算机科学的入门课,我会说它很好的完成了这个任务,虽然还有不少的优化空间,这里给到一个顶级。
|
||||
|
||||
《电路与电子学基础》我直接给到一个顶级,老师非常风趣幽默,教学内容不能说是和计算机科学直接相关,但也算是计算机的基石了。《离散数学》这里是一个拉完了,从理论上来说离散数学的教学内容可以算是实际上的计算机科学核心,但是正是这种极端重要性更加凸显出了这个课程拉完了的程度,而且还是中英文混合教学,我的评价是没有金刚钻就别揽瓷器活。
|
||||
|
||||
《数字逻辑与数字系统》,还可以,归类到人上人,课程内容本身比较重要,算是之后的计算机组成原理的先修课程。《形式语言与自动机》,这门课的代课老师石川老师算是北邮的风云人物,其他的东西不评价,但是他在这门课程上的表现还是值得肯定的,这里归到人上人。
|
||||
|
||||
下面正式进入计算机科学中的真·核心课程,即培养方案中的“专业基础”课程组。首先是《数据结构》,因为考试要求手写代码直接给到一个拉完了,我得承认这个有我自己的一些偏见。《算法设计与分析》本来想以同样的理由给到一个拉完了,~~但是考虑到我的成绩比《数据结构》高~~,这里给到一个NPC。《计算机系统基础》课程,使用的教材是大名鼎鼎的*Computer System: A Programmer Perspective* ,虽然课程并没有完全覆盖CSAPP中所有内容,但是还是给到一个夯!
|
||||
|
||||
《操作系统》,按道理讲也是计算机科学中的核心课程,但是课程内容我现在就记得一个信号量控制抢水果和挑水,老师也非常莫名其妙,直接给到拉完了,正所谓学完这门课你也不懂操作系统。然后是《编译原理与技术》,可以算是计算机科学中最难的课程之一,但是因为课程核心都放在了编译器的前端部分,教学语言还是老掉牙的Pascal,只能给到一个人上人。
|
||||
|
||||
《计算机组成原理》,考虑老师直接在课程推销王道考研,直接给到拉完了。《计算机系统结构》,算是《计算机组成原理》的进阶课,老师讲得也非常不错,和之前的《计算机组成原理》相比简直是天差地别,直接给到一个顶级,如果实验部分改进一下,和《计算机组成原理课程设计》结合一下,就可以直接给到夯了。《计算机网络》直接给到夯,算是课程与现实结合最紧密的课程,和《离散数学》同样都是中英文混合教学,只能说人与人之间亦有差距。《数据库系统原理》获得夯的理由和《计算机网络》比较一致,都是同现实紧密关联的课程,任课老师教学非常不错,也是中英文混合教学。
|
||||
|
||||
《软件工程》和《现代交换原理》则是难兄难弟,两门都是拉完了:《软件工程》被称为计算机科学中的政治,我认为与其死记硬背一些非常抽象的概念、参加两次考试不如多写几行代码来得体会深刻。而且这门课的实践部分也非常抽象,要求几个小组联合验收:而且这些小组的前后端之间需要可以随机组合。虽然这个要求可以说是非常的软件工程,但是和你合作的哥们可能并不是特别拟人,只能唉唉唉。至于《现代交换原理》,第一次看到这门课的时候我很怀疑这是否是打印错误,虽然代课老师挺不错的,但是它唯一的问题就是不该出现在计算机科学的培养方案上。退一步说就算学校设计培养方案的时候希望计算机科学的同学也懂一点通信,也是应该设计简单一点的通信原理而不是设计一门不知所云的“古代交换原理”课程(毕竟课程的主体部分是电话交换机)。
|
||||
|
||||
然后是专业课选修课组部分。在“网络&开发技术模块”,我选修的课程是《下一代Internet技术与协议》、《移动互联网技术及应用》和《Python程序设计》三门课。其中《下一代Internet技术与协议》是中规中矩的选修课程,只能归到NPC一档,课程的主要内容就是讲IPv6协议栈,考虑到现在IPv6协议栈已经在大规模的普及推进,建议把课程名称中的“下一代”去掉。《移动互联网技术及应用》就是教写Android应用,考虑到这门课的老师是北邮少数敢于在课程上打开IDE写代码的老师,出于对这位老师的敬佩,这里给到人上人。《Python程序设计》也是一门平平无奇的选修课,同时是出于对踢球骨折了还拄着拐杖来上课的老师之敬佩,这里给到人上人。
|
||||
|
||||
在“大数据技术模块”,我选修的课程是《大数据技术基础》和《信息与知识获取》。其中的《大数据技术基础》的实验部分非常抽象,要求使用容器技术模拟出多个节点来搭建大数据系统,比如Hadoop和Spark,还需要在华为的ARM云服务器上进行实验,与其叫作是《大数据技术基础》不如改名为《Linux系统运维基础》。不过考虑到这是我本科期间唯一一门愿意给100分的课程,这门还是给到人上人。《信息与知识获取》则是“大海呀,你全部都是水~”,这里给到NPC。
|
||||
|
||||
在“技术拓展模块”,我选修的课程是《人工智能原理》和《程序设计实践》。这两门都给到NPC,《人工智能原理》是因为它讲授的原理有点太古老了,连深度神经网络都没有涉及,还需要考试,只能给到NPC。《程序设计实践》则是普通的水课,项目开源在[github](https://github.com/jackfiled/Katheryne)上,看上去这个题目也是祖传题目了。
|
||||
|
||||
然后我们可以来谈论一下最激动人心的实践课课组。首先是《物理实验A》,由于是线上教学做实验,直接给到一个拉完了。《计算导论与程序设计课程设计》(不太可能会有“普通人”在大一的下学期就选择《程序设计竞赛基础》吧)是大一下学期开设,课程设计的题目是公交车的调度,对大一新人来说还是挺合适的,这里给到一个人上人。面向对象的程序设计实践我选择的是Java,这个只能说拉完了,Java感觉没学到什么有用的,最后交作业的时候还要求提交一大堆软件工程的UML图。
|
||||
|
||||
《计算机组成原理课程设计》和《数字逻辑与数字系统课程设计》二选一我选择是《计算机组成原理课程设计》,题目是设计一个单周期的CPU,非常好课程直接给到夯!唯一的问题似乎因为小学期时间安排的问题,导致最后只有三四天的时间来完成整个作业,这种课程就应该设计为一个必修实验,而且最好需要设计一个完整的五段流水线CPU。
|
||||
|
||||
《操作系统课程设计》和《编译原理课程设计》二选一我选择了《编译原理课程设计》,这更多是一个历史和“人民”的选择,当时我们课程班100多个人,最后只有一个小组大概8个人选择了《操作系统课程设计》。不过《编译原理课程设计》确实可以给到夯!首先是老师非常不错,然后课程设计的内容也确实可以说是循序渐进。唯一的问题是设计的编译器是从Pascal翻译到C,和编译原理课程本身的衔接也不是特别紧密,尤其是考虑到编译原理课程本身自带的两个实验就是词法分析和语法分析,课程设计还把重点放在前端上就有点不太好了。最好是把题目修改到C到某门汇编语言,例如RISC-V,然后和《编译原理》课程本身的实验衔接设计,课程设计的重点就可以放在代码生成和代码优化上面了。
|
||||
|
||||
至于最后的三选二选修课,在《数据结构课程设计》、《计算机网络课程设计》和《数据库系统原理课程设计》三门中我选择的前两门。《数据结构课程设计》有一个非常奇葩的要求,不能使用自带的数据结构实现(比如说标准库中的列表、哈希表等),而是要求自行实现,但是在最后验收时并没有突出这一点,这里只能给到一个人上人,算是实践课程中比较低的评价了。《计算机网络课程设计》要求设计一个DNS Relay服务器,比较典型的计算机网络课程设计的要求,唯一的问题是要求必须要用C语言完成,只能给到一个顶级。这里我感觉老师做出这个限制的主要理由是希望大家多钻研一下和网络相关的高并发设计,但是直接限制到C语言级别有点强人所难了。
|
||||
|
||||
最后锐评一下我选过的公选课和体育课,在我的培养方案中,我需要选修两门以上的公选课(人文艺术类型)和四门体育课(其中一门必修的《体育基础》)。我为了满足培养方案选修的公选课是《人工智能与社会发展》和《显示技术发展与游戏应用》两门,普通的水课没什么好说的,给到NPC。不过在培养方案之外我还选修了一门《基于Arduino的开源手机设计》,非常好的选修课,课程内容是基于ESP8266设计一个支持2G和Wi-Fi的按键手机。所有上课的同学都可以免费获得一个板子,值得选修,这里给到一个夯。
|
||||
|
||||
至于体育课,因为我本人体育苦手,评价略有失真之处,仅供参考。首先是在**线上**进行的《体育基础》课,直接给到NPC,至于《健美》和《乒乓球》,考虑到我学得不好应该是我的问题,给到一个顶级。至于最后的《太极拳》因为体育苦手的缘故,简直就是我等的福音,给到夯!
|
||||
|
||||
### 实习与工作
|
||||
|
||||
在毕业之后不能躲过的话题自然就是工作,虽然正式的找工作离我还有~~三年~~两年半的时间。尤其是考虑到我们伟大的学校并不打算为保研的同学提供宿舍,在6月被赶出校门之后,一个比较正确的选择就是找一份实习。
|
||||
|
||||
大概从2025年的5月份我就开始在BOSS直聘上找实习,首选的工作意向就是和我现在研究方向相关联的编译器、高性能计算和AI Infra方向,考虑到这些方向的工作岗位数量如同食堂番茄蛋花汤中的蛋花,我也填上了后端方向作为备选。不过找工作真的好痛苦啊:从事后统计来看,在那半个月的时间里我大概投出去了60多份简历,其中只有个位数的HR回复了消息,只约上了一场面试。这便是今天的主角——理想汽车。
|
||||
|
||||
说起来也奇怪,招聘的这个岗位,理想汽车的图编译器开发,并不是我自己找上门去的,而是对方的HR主动要走了我的简历。下面就简单记录一下理想汽车的图编译器开发实习生的面经吧,不过说实话这已经是接近一年之前的事情了,如果有错漏之处还请谅解。
|
||||
|
||||
上来还是自我介绍起手,并介绍自己简历上的一些项目。因为我当时没有任何的实习经历,简历上主要的项目其实是本科的毕业设计。不过因为这个毕业设计题目取得非常高大上,但是实现上非常一坨,所以拷打的过程个人感觉漏洞百出。
|
||||
|
||||
然后是一些基础的编译器知识,比如说你是否知道什么是静态单赋值形式,MLIR中的IR是否是SSA等等,还结合MLIR问了很多MLIR中的细节开发问题,如果你是否定义过Dialect,是否写过operation的parser和printer等等。还有MLIR中的pass分为什么,是否了解这些pass是如何运行的,有一些非常细节的问题我当时就直接回答不知道了(捂脸)。
|
||||
|
||||
中间还拷打了一些`C++`开发的知识,比如说CRTP这种在MLIR中非常常用的模板范式,不过我当时因为对`C++`开发还不是特别熟悉,将中间的`static_cast`说成了`dynamic_cast`,非常的尴尬。面试官还问了问我研究生和本科的主修课程。
|
||||
|
||||
编程的题目是手撕堆排序,但是面试官找了半天都没有找到测试的题目,只让我口述了算法的过程,不过我当时过度紧张(毕竟是人生的第一场面试),什么归并快排桶排堆排的都丢到九霄云外去了,只能阿巴阿巴地说一些建堆、排序之内的车轱辘话,幸好面试官也没有特别纠结这一块。反问的环节我是询问了一下我们工作的对象,理想汽车自研汽车芯片的情况,也就是现在即将发布的马赫100芯片。
|
||||
|
||||
幸好最终的结果还是比较好的,顺利拿到了offer并入职理想。不过当时暑假期间的工资是按照本科生算的,只有230一天,考虑到当时我在北京还需要租房居住,差点付费上班(幸好还有第二份兼职)。而且当时部门正处于芯片即将上车的集中开发阶段,8月份还把我们一起打包送到杭州去上班,血亏一个月房租,~~不过住一个月的酒店还是蛮爽的,谢谢想哥~~。
|
||||
|
||||
不过说来我还是非常感谢在理想的这段实习经历的。在进入理想实习之前,虽然我已经在导师的手下干了一年,但是感觉对于编译器,尤其是这种面向NPU的AI编译器,总感觉还是有一种雾里看花的感觉。但是在实习过程中,实际接触了编译器的开发和优化流程,瞬间感觉过程论文和课本中的编译器活了过来。同样也是在这段经历中,我对于MLIR的了解和认识也是飞速增长。
|
||||
|
||||
在理想的实习经历之外,~~为了赚房租~~为了进一步提高自己的能力,同时在为我在开源世界中开辟一块立锥之地,我报名并参加了2025年的开源之夏(OSPP)项目,中选的题目是[RustSBI原型系统引导生态完善](https://summer-ospp.ac.cn/org/prodetail/256590172?lang=zh&list=pro)。不过这次OSPP的经历我打算单出一篇文章来分享(挖坑x1),这里就不多赘述了。
|
||||
|
||||
### 研究生
|
||||
|
||||
讲道理,在9月份开学之后我就回到明光村幼儿园附属大学继续攻读我的硕士学位了,这段研究生时光也占据了2025年度三分之一的长度。但是我在撰写本年终总结的时候却对于这段时光一点回忆也没有,真是令人感到好奇!
|
||||
|
||||
### 第一台NAS
|
||||
|
||||
上面拖拖拉拉地讲了一堆令人悲伤的话题,还是一转聊一聊一些开心的话题吧。
|
||||
|
||||
经过若干个月的精心筹备,在2025年的一季度,我终于组建了我的个人NAS,虽然这台NAS里面目前只有1块4T 3.5英寸硬盘和几块从笔记本上拆下来的2.5英寸硬盘,显得我精心挑选的8 STAT接口主板和8个3.5英寸硬盘位的机箱显得很呆。不过还是很开心,谁叫组装好硬件就遇上了AI导致的硬件大涨价呢。
|
||||
|
||||

|
||||
|
||||
这里也是简单列一下硬件配置:
|
||||
|
||||
1. CPU:Intel Xeon E3-1245v5
|
||||
2. 内存:镁光DDR4 ECC 16G 2400 x2
|
||||
3. 主板:超微X11SSH-F
|
||||
4. 电源:全汉 蓝暴经典PLUS 450W
|
||||
5. 机箱:联宇 见方L
|
||||
6. CPU散热器:利民AXP90-X36
|
||||
|
||||
这里我也打算后面单独出一篇文章分享设计和配置这个NAS的点点滴滴(挖坑x2),这里就简单吐槽一件令我非常绷不住的事。这里选择E3-1245v5这颗带有核显的CPU简直就是我最大的错误,我设计的时候考虑的是这颗核显可以极大地加快NAS上各种媒体的解码速度。但是E3-1245v5这颗CPU带的核显简直就是垃圾中的垃圾:它不支持H265/HEVC 10 bit的解码,简直就是屁用没用!
|
||||
|
||||
而且,这颗CPU在秋季开学之后就挂掉了,还贡献了我的第一张Linux蓝屏~~扫码即可查看当时的Kernel日志~~:
|
||||
|
||||

|
||||
|
||||
我不得不购入了第二块CPU,也就是现在机器上使用的Intel Xeon E3-1240v5。谢天谢地,这颗CPU到目前还工作正常,而它比它带核显的兄弟足足便宜了三分之二。
|
||||
|
||||
### 第一台台式机
|
||||
|
||||
除了这台,我还在暑假的时候组装了我人生的第一台台式机:
|
||||
|
||||

|
||||
|
||||
这里也简单列举一下这台主机的配置:
|
||||
|
||||
- CPU: AMD 锐龙 5 9600X
|
||||
- 内存:光威龙武弈 DDR5 长鑫颗粒 6000 CL36 16G x2
|
||||
- 主板:华硕TX B650EM WIFI W
|
||||
- GPU:ONIX Intel ARC B580 LUMI 12G
|
||||
- 电源:鑫谷 GM650W 金牌全模组
|
||||
- CPU散热器:九州风神 玄冰400V5 晴雪白
|
||||
- 机箱散热器:酷冷至尊莫比乌斯120白色 x2
|
||||
- 立牌:psplive 2025校园主题亚克力立牌-[李豆沙_Channel](https://space.bilibili.com/1703797642)
|
||||
|
||||
组装这台主机的时间非常巧妙,我是在7月初的时候下定决心组装这台台式机,这几乎是在电子产品大涨价之前最后一个上车的时间窗口了。
|
||||
|
||||
关于我选择的这些配置我也想简单讨论一下。主要的争议点可能是我选择的显卡,当时我的决赛圈里面几乎就是了两张显卡:NVIDIA的RTX 5060和Intel的Arc B580。最终驱动我做出决定的因素主要是预算,RTX 5060的价格基本上都在2500元以上,而我拿下的这块Intel Arc B580只花了我1800元。虽然当时网上对于Intel显卡驱动的批评甚嚣尘上,但是我决定对Intel这家老牌的半导体厂商保持基本的信任(至少到目前为止,Intel还没有辜负我的信任)。而且考虑到我实际上也是一个Linux用户,NVIDIA的Linux驱动是个什么样子是不言自明的,至少Intel的Linux支持情况要好得多。
|
||||
|
||||
而且在我目前的日常游玩的所有游戏中这块显卡都表现正常,例如《三角洲行动》在我目前的2K分辨率下可以取得90~110FPS的水平。《原神》和《戴森球计划》这种对于显卡需求比较低的游戏更是轻松拿下。
|
||||
|
||||
### 玫瑰色生活
|
||||
|
||||
2025年也是一个旅行颇多的年份。
|
||||
|
||||
首先是趁着毕业的时候,去北京一个著名的小众冷门(这是否矛盾?)景点——中央礼品文物管理中心参观,算是一个非常有首都特色的景点了。然后是忙碌的8月份,在北京和杭州之间飞来飞去,很幸运地是在其中的一次坐到了国产大飞机C919前往杭州:
|
||||
|
||||

|
||||
|
||||
还前往了内蒙古的鄂尔多斯参加全国高性能计算学术年会CCF HPC China 2025。学术上的东西在下人微言轻不做评价,不过至少HPC China在茶歇上比之前去参加了CNCC 2024要慷慨的多。其他的内容可以参见同行者的[文章](https://www.chenxutalk.top/posts/life/travel/neimenggu/)。
|
||||
|
||||
正所谓“读万卷书,行万里路”。在2025年还读了下面这几本书:
|
||||
|
||||
- [*GOSICK*](https://mzh.moegirl.org.cn/GOSICK),樱庭一树著,不算是一般意义上的轻小说,而是比较严肃的推理小说,好看!
|
||||
- 《浪潮之巅》,吴军创作的科技产业发展史的书籍,对于计算机相关领域从业者来说可以说是必读的经典书籍了。
|
||||
|
||||
还看了不少的电影:一部是看上去比较冷门的电影《商海通牒》,原名*Margin Call*,记录的是2008年到2009年发生金融危机时一家华尔街投行中发生的故事。另一部则是学校组织放映的《窗外是蓝星》,记录的是神舟十三号乘组首次在中国空间站执行在轨驻留六个月任务的故事。岁月如梭啊,现在已经是神舟二十一号乘组了。
|
||||
|
||||
本来在年终总结的末尾,按照传统还是应该放一下B站的观看时长,但是在撰写本文时B站似乎已经下架了2025年度报告的查看页面了(悲)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,353 +0,0 @@
|
||||
---
|
||||
title: 使用System.Text.Json序列化和反序列化JSON
|
||||
date: 2026-01-21T22:07:38.4297603+08:00
|
||||
updateTime: 2026-04-03T17:16:16.0831040+08:00
|
||||
tags:
|
||||
- 技术笔记
|
||||
- dotnet
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
如何使用`System.Text.Json`高效地序列化和反序列化JSON?
|
||||
|
||||
<!--more-->
|
||||
|
||||
### 序列化
|
||||
|
||||
序列化JSON几乎总是简单的,直接使用`JsonSerializer.Serialize`就可以序列化为字符串。
|
||||
|
||||
唯一需要注意的是,JSON理论上唯一的数字类型`number`默认是双精度浮点数,只能**精确地**表示53位(二进制)以下的整数。在对于`long`类型进行序列化时,虽然框架可以输出正确的数值,但是JavaScript中无法正确的解析。
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void LongSerializeTest()
|
||||
{
|
||||
JsonBody body = new(long.MaxValue - 1);
|
||||
string output = JsonSerializer.Serialize(body);
|
||||
// Output: {"Number":9223372036854775806}
|
||||
outputHelper.WriteLine(output);
|
||||
}
|
||||
```
|
||||
|
||||
上述的JSON字符串中在JavaScript中将会被解析为:
|
||||
|
||||

|
||||
|
||||
因此在需要传递大整数的时候最好使用`String`。
|
||||
|
||||
### 反序列化
|
||||
|
||||
而反序列化中需要考虑的东西就很多了。
|
||||
|
||||
#### 使用记录声明反序列化的对象
|
||||
|
||||
在`System.Text.Json`的早期版本中,无法将JSON反序列化为`record`这类关键词声明的不可变类型,因为当时库的逻辑是首先调用类型的公共无参数构造函数构造对象,再使用setter为需要反序列化的属性赋值。在后来的版本中,序列化程序可以直接调用类型的构造函数进行反序列化,这就为反序列化到`record`和`struct`提供了方便。
|
||||
|
||||
例如可以使用如下的代码快速地进行反序列化:
|
||||
|
||||
```csharp
|
||||
private record JsonBody(int Code, string Username);
|
||||
|
||||
[Fact]
|
||||
public void DeserializeTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"code": 111,
|
||||
"username": "ricardo"
|
||||
}
|
||||
""";
|
||||
|
||||
JsonBody? body = JsonSerializer.Deserialize<JsonBody>(input, s_serializerOptions);
|
||||
Assert.NotNull(body);
|
||||
|
||||
Assert.Equal(111, body.Code);
|
||||
Assert.Equal("ricardo", body.Username);
|
||||
}
|
||||
```
|
||||
|
||||
但是这样进行反序列化有一个小小的坑,就是缺少对于空值的有效处理。例如对于下面的JSON,上面的代码都会正常地进行反序列化。
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void DeserializeFromNonexistFieldTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"code": 111
|
||||
}
|
||||
""";
|
||||
|
||||
JsonBody? body = JsonSerializer.Deserialize<JsonBody>(input, s_serializerOptions);
|
||||
Assert.NotNull(body);
|
||||
|
||||
Assert.Equal(111, body.Code);
|
||||
Assert.Equal("", body.Username);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void DeserializeFromNullValueTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"code": 111,
|
||||
"username": null
|
||||
}
|
||||
""";
|
||||
|
||||
JsonBody? body = JsonSerializer.Deserialize<JsonBody>(input, s_serializerOptions);
|
||||
Assert.NotNull(body);
|
||||
|
||||
Assert.Equal(111, body.Code);
|
||||
Assert.Equal("", body.Username);
|
||||
}
|
||||
```
|
||||
|
||||
但是对于返回结果的校验会发现`body.Username`实际上是一个空值。
|
||||
|
||||

|
||||
|
||||
幸好,在.NET 9中为`JsonSerializerOptions`添加了一个尊重可为空注释的选项`RespectNullableAnnotations`,将这个选项设置为`true`可以在**一定程度上**缓解这个问题。打开这个开关之后,对于`"username": null`的反序列化就会抛出异常了。
|
||||
|
||||
但是针对第一段JSON,也就是缺少了`username`字段的反序列化并不会报错,这就是反序列化的第二个坑,这里先按下不表。
|
||||
|
||||
因为在.NET运行时的设计初期并没有考虑空安全这一至关重要的特性,因此在IL中并没有针对引用类型的不可为空性的显式抽象(虽然后续的C#编译器会为所有不可为空的应用类型添加属性元数据)。所以,针对如下元素的不可为空约束是无效的:
|
||||
|
||||
1. 顶级类型;
|
||||
2. 集合的元素类型;
|
||||
3. 任何含有泛型的属性、字段和构造函数参数。
|
||||
|
||||
例如,针对下面这个反序列化代码并不会报错,需要程序员自行处理其中的空值:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void DeserializeListTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"names": [
|
||||
"1",
|
||||
null,
|
||||
"2"
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
JsonListBody? body = JsonSerializer.Deserialize<JsonListBody>(input, s_serializerOptions);
|
||||
Assert.NotNull(body);
|
||||
|
||||
foreach ((int i, string value) in body.Names.Index())
|
||||
{
|
||||
outputHelper.WriteLine($"{i} is null? {value is null}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行的输出结果提示第二个元素为空:
|
||||
|
||||

|
||||
|
||||
#### 需要才是需要,不为空并不一定不为空
|
||||
|
||||
在默认的反序列化行为中,如果反序列化对象的某一个属性并不在输入的JSON对象中,反序列化器并不为报错而是直接设置为null,这显然会给破环空安全的假定,即使打开了尊重空值注释也是这样。这在.NET文档中被称为**缺失值和空值**:
|
||||
|
||||
- **显式空值null**将会在`RespectNullableAnnontations=true`的情况下引发异常;
|
||||
- **缺少的属性**不会引发任何异常,即使对应的属性被声明为不可为空。
|
||||
|
||||
为了让序列化程序确保缺少属性时会报错,需要将这个属性声明为**需要的**。这一点可以通过C#的`required`关键词或者`[Required]`属性来实现。
|
||||
|
||||
而且,这两种属性对于C#语言和序列化程序来说是正交的,即:
|
||||
|
||||
1. 可以有一个可以为空的必需属性:
|
||||
|
||||
```csharp
|
||||
MyPoco poco = new() { Value = null }; // No compiler warnings.
|
||||
|
||||
class MyPoco
|
||||
{
|
||||
public required string? Value { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
2. 可以有一个不可为空的可选属性:
|
||||
|
||||
```csharp
|
||||
class MyPoco
|
||||
{
|
||||
public string Value { get; set; } = "default";
|
||||
}
|
||||
```
|
||||
|
||||
但是对于`record`类型来说,前者在语义上是冗余的,语法上是错误的,后者则对于程序员带来了额外的心智负担,需要手动给每一个字段加上一个额外的注解。
|
||||
|
||||
考虑到序列化程序也支持使用有参数的公共构造函数,上面这两个属性对于构造函数的参数来说也是成立的:
|
||||
|
||||
```csharp
|
||||
record MyPoco(
|
||||
string RequiredNonNullable,
|
||||
string? RequiredNullable,
|
||||
string OptionalNonNullable = "default",
|
||||
string? OptionalNullable = "default"
|
||||
);
|
||||
```
|
||||
|
||||
不过在.NET 9之前,所有构造函数的参数都被序列化程序认为是可选的。在.NET 9之后,`JsonSerializerOptions`添加了一个尊重必须构造函数参数的选项(别忘了对于`record`这类不可变对象的反序列化是通过构造函数来实现的)`RespectRequiredConstructorParameters`。在打开这个选项之后,针对缺少属性的反序列化就会正常报错了。
|
||||
|
||||
```csharp
|
||||
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
RespectNullableAnnotations = true,
|
||||
RespectRequiredConstructorParameters = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void DeserializeFromNonexistFieldTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"code": 111
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<JsonBody>(input, s_serializerOptions));
|
||||
}
|
||||
```
|
||||
|
||||
但是在实际的生产和生活中,并不会有人将可选的属性序列化为`null` 之后再返回,而是直接忽略这个属性。这就让“可为空”这个属性显得有点鸡肋,因此可为空的属性一般也需要提供了一个可选的构造函数参数:
|
||||
|
||||
```csharp
|
||||
record MyPocp(string RequiredField, string? NotRequiredField = null)
|
||||
```
|
||||
|
||||
更本质的说,这是因为Javascript中提供了两种空`undefined`和`null`,然而在C#中并没有提供`undefined`对应的语言构造,因此只能通过这种默认值为`null`的构造函数参数来模拟。
|
||||
|
||||
#### 反序列化为结构
|
||||
|
||||
结构作为值类型,虽然在函数之间传递时需要被拷贝而带来了额外的性能开销,但是也因为这一点而可以被直接分配在栈上,给GC带来的压力较小。因此在部分需要极端性能优化的场景可以直接针对`struct`进行反序列化。
|
||||
|
||||
`struct`的反序列化也是通过构造函数来实现的,序列化程序遵循如下的规则来选择构造函数:
|
||||
|
||||
1. 对于类,如果唯一的构造函数是参数化构造函数,则选择这一构造函数;
|
||||
2. 对于结构或者具有多个构造函数的类,需要使用`[JsonConstructor]`手动指定需要使用的构造函数,否则**只会**使用公共无参构造函数(如果存在)。
|
||||
|
||||
因此,如果需要针对不可变的结构进行反序列化,需要加上`[JsonConstructor]`注解。例如,针对下面的代码,如果不加上注解,反序列化又会静默地失败。
|
||||
|
||||
```csharp
|
||||
private struct JsonStruct
|
||||
{
|
||||
public int Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public JsonStruct(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializeToStructTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "ricardo"
|
||||
}
|
||||
""";
|
||||
|
||||
JsonStruct r = JsonSerializer.Deserialize<JsonStruct>(input, s_serializerOptions);
|
||||
Assert.Equal(1, r.Id);
|
||||
}
|
||||
```
|
||||
|
||||
为了简化语法,不可变的结构可以使用`readonly record struct`语法来替代:
|
||||
|
||||
```csharp
|
||||
private readonly record struct JsonRecordStruct(int Id, string Name);
|
||||
|
||||
[Fact]
|
||||
public void DeserializeToRecordStructTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "ricardo"
|
||||
}
|
||||
""";
|
||||
|
||||
JsonRecordStruct r = JsonSerializer.Deserialize<JsonRecordStruct>(input, s_serializerOptions);
|
||||
Assert.Equal(1, r.Id);
|
||||
Assert.Equal("ricardo", r.Name);
|
||||
}
|
||||
```
|
||||
|
||||
不过这里有一个很奇怪的点,使用`readonly record struct`语法之后就不需要`[JsonConstructor]`了。
|
||||
|
||||
可以实验一下是`readonly`还是`record`发挥了作用。
|
||||
|
||||
在仅仅添加了`readonly`的情况下,反序列化不会成功:
|
||||
|
||||
```csharp
|
||||
private readonly struct JsonReadonlyStruct
|
||||
{
|
||||
public int Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public JsonReadonlyStruct(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializeToReadonlyStructTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "ricardo"
|
||||
}
|
||||
""";
|
||||
|
||||
JsonReadonlyStruct r = JsonSerializer.Deserialize<JsonReadonlyStruct>(input, s_serializerOptions);
|
||||
Assert.Equal(0, r.Id);
|
||||
Assert.Null(r.Name);
|
||||
}
|
||||
```
|
||||
|
||||
而在仅仅加上`record`的情况下,序列化程序就可以选择正确的构造函数了:
|
||||
|
||||
```csharp
|
||||
private record struct JsonRecordStruct(int Id, string Name);
|
||||
|
||||
[Fact]
|
||||
public void DeserializeToRecordStructTest()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "ricardo"
|
||||
}
|
||||
""";
|
||||
|
||||
JsonRecordStruct r = JsonSerializer.Deserialize<JsonRecordStruct>(input, s_serializerOptions);
|
||||
Assert.Equal(1, r.Id);
|
||||
Assert.Equal("ricardo", r.Name);
|
||||
}
|
||||
```
|
||||
|
||||
> 不过这样说来`readonly record struct`中的`readonly`似乎是冗余的?
|
||||
>
|
||||
> 原来,`record struct`声明的对象是可变的。详见文档中对于[不可变性](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/record#immutability)的描述。
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,215 +0,0 @@
|
||||
---
|
||||
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`就是这条边的反向边。
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,8 +1,7 @@
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Tests;
|
||||
@@ -10,7 +9,6 @@ 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()
|
||||
@@ -18,10 +16,12 @@ public sealed class GiteaFetchServiceTests
|
||||
s_giteaOptionsMock.SetupGet(o => o.Value)
|
||||
.Returns(new GiteaOptions
|
||||
{
|
||||
BaseAddress = "https://git.rrricardo.top/api/v1/", HeatMapUsername = "jackfiled"
|
||||
BaseAddress = "https://git.rrricardo.top/api/v1/",
|
||||
ApiKey = "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||
HeatMapUsername = "jackfiled"
|
||||
});
|
||||
|
||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient(), s_logger.Object);
|
||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Tests;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IEssayContentService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IEssayScanService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Abstractions;
|
||||
namespace YaeBlog.Abstraction;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
33
src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
33
src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.CommandLine.Binding;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.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>();
|
||||
return result is null
|
||||
? throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.")
|
||||
: new OptionsWrapper<BlogOptions>(result);
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<IOptions<BlogOptions>>();
|
||||
}
|
||||
}
|
||||
32
src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
32
src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.CommandLine.Binding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.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>();
|
||||
}
|
||||
}
|
||||
21
src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
21
src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.CommandLine.Binding;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Commands.Binders;
|
||||
|
||||
public sealed class ImageCompressServiceBinder : BinderBase<ImageCompressService>
|
||||
{
|
||||
protected override ImageCompressService GetBoundValue(BindingContext bindingContext)
|
||||
{
|
||||
bindingContext.AddService(provider =>
|
||||
{
|
||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||
ILogger<ImageCompressService> logger = provider.GetRequiredService<ILogger<ImageCompressService>>();
|
||||
|
||||
return new ImageCompressService(essayScanService, logger);
|
||||
});
|
||||
|
||||
return bindingContext.GetRequiredService<ImageCompressService>();
|
||||
}
|
||||
}
|
||||
18
src/YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
18
src/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>>();
|
||||
}
|
||||
}
|
||||
296
src/YaeBlog/Commands/YaeBlogCommand.cs
Normal file
296
src/YaeBlog/Commands/YaeBlogCommand.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Commands.Binders;
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Services;
|
||||
|
||||
namespace YaeBlog.Commands;
|
||||
|
||||
public sealed class YaeBlogCommand
|
||||
{
|
||||
private readonly RootCommand _rootCommand = new("YaeBlog Cli");
|
||||
|
||||
public YaeBlogCommand()
|
||||
{
|
||||
AddServeCommand(_rootCommand);
|
||||
AddWatchCommand(_rootCommand);
|
||||
AddListCommand(_rootCommand);
|
||||
AddNewCommand(_rootCommand);
|
||||
AddUpdateCommand(_rootCommand);
|
||||
AddPublishCommand(_rootCommand);
|
||||
AddScanCommand(_rootCommand);
|
||||
AddCompressCommand(_rootCommand);
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(string[] args)
|
||||
{
|
||||
return _rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static void AddServeCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command serveCommand = new("serve", "Start http server.");
|
||||
rootCommand.AddCommand(serveCommand);
|
||||
|
||||
serveCommand.SetHandler(async context =>
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddControllers();
|
||||
builder.AddYaeBlog();
|
||||
builder.AddServer();
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.MapStaticAssets();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
application.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
application.MapControllers();
|
||||
|
||||
CancellationToken token = context.GetCancellationToken();
|
||||
await application.RunAsync(token);
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddWatchCommand(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.AddYaeBlog();
|
||||
builder.AddWatcher();
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.MapStaticAssets();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
application.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
application.MapControllers();
|
||||
|
||||
CancellationToken token = context.GetCancellationToken();
|
||||
await application.RunAsync(token);
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddNewCommand(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, blogOption, _, essayScanService) =>
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
if (contents.Posts.Any(content => content.BlogName == file))
|
||||
{
|
||||
Console.WriteLine("There exists the same title blog in posts.");
|
||||
return;
|
||||
}
|
||||
|
||||
await essayScanService.SaveBlogContent(new BlogContent(
|
||||
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
|
||||
new MarkdownMetadata
|
||||
{
|
||||
Title = file,
|
||||
Date = DateTimeOffset.Now.ToString("o"),
|
||||
UpdateTime = DateTimeOffset.Now.ToString("o")
|
||||
},
|
||||
string.Empty, true, [], []));
|
||||
|
||||
Console.WriteLine($"Created new blog '{file}.");
|
||||
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
|
||||
new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
private static void AddUpdateCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command newCommand = new("update", "Update the blog essay.");
|
||||
rootCommand.AddCommand(newCommand);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The blog filename to update.");
|
||||
newCommand.AddArgument(filenameArgument);
|
||||
|
||||
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||
{
|
||||
Console.WriteLine("HINT: The update command only consider published blogs.");
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == file);
|
||||
if (content is null)
|
||||
{
|
||||
Console.WriteLine($"Target essay {file} is not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
await essayScanService.SaveBlogContent(content, content.IsDraft);
|
||||
}, filenameArgument,
|
||||
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
private static void AddListCommand(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.BlogName))
|
||||
{
|
||||
Console.WriteLine($" - {content.BlogName}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
|
||||
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName))
|
||||
{
|
||||
Console.WriteLine($" - {content.BlogName}");
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
private static void AddScanCommand(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) =>
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
List<BlogImageInfo> unusedImages = (from content in contents
|
||||
from image in content.Images
|
||||
where image is { IsUsed: false }
|
||||
select image).ToList();
|
||||
|
||||
if (unusedImages.Count != 0)
|
||||
{
|
||||
Console.WriteLine("Found unused images:");
|
||||
Console.WriteLine("HINT: use '--rm' to remove unused images.");
|
||||
}
|
||||
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
Console.WriteLine($" - {image.File.FullName}");
|
||||
}
|
||||
|
||||
if (removeOptionValue)
|
||||
{
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
image.File.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Used not existed images:");
|
||||
|
||||
foreach (BlogContent content in contents)
|
||||
{
|
||||
foreach (FileInfo file in content.NotfoundImages)
|
||||
{
|
||||
Console.WriteLine($"- {file.Name} in {content.BlogName}");
|
||||
}
|
||||
}
|
||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||
}
|
||||
|
||||
private static void AddPublishCommand(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.BlogName == filename
|
||||
select blog).FirstOrDefault();
|
||||
|
||||
if (content is null)
|
||||
{
|
||||
Console.WriteLine("Target blog does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置发布的时间
|
||||
content.Metadata.Date = DateTimeOffset.Now.ToString("o");
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
|
||||
// 将选中的博客文件复制到posts
|
||||
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||
|
||||
// 复制图片文件夹
|
||||
DirectoryInfo sourceImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
|
||||
DirectoryInfo targetImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
|
||||
|
||||
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.BlogName + ".md"));
|
||||
sourceBlogFile.Delete();
|
||||
}, new BlogOptionsBinder(),
|
||||
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
|
||||
}
|
||||
|
||||
private static void AddCompressCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
|
||||
rootCommand.Add(command);
|
||||
|
||||
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
|
||||
getDefaultValue: () => false);
|
||||
command.AddOption(dryRunOption);
|
||||
|
||||
command.SetHandler(ImageCommandHandler,
|
||||
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
|
||||
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
|
||||
}
|
||||
|
||||
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
|
||||
ILogger<ImageCompressService> _2,
|
||||
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
|
||||
{
|
||||
await imageCompressService.Compress(dryRun);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@using Microsoft.Extensions.Options
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject IOptions<BlogOptions> Options
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Models
|
||||
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="text-3xl font-bold py-2">
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||
驱动。
|
||||
</p>
|
||||
@if (!string.IsNullOrEmpty(BuildCommitId))
|
||||
{
|
||||
<p class="text-md">
|
||||
Build Commit #
|
||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId" NewPage="true"/>
|
||||
</p>
|
||||
}
|
||||
<p class="text-md">
|
||||
Build Commit #
|
||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -27,7 +24,7 @@
|
||||
{
|
||||
private static string DotnetVersion => $".NET {Environment.Version}";
|
||||
|
||||
private static string? BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID");
|
||||
private static string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
|
||||
|
||||
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.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-[8px] font-light"/>
|
||||
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[10px]"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
<SvgGroup Transform="@GlobalWeekTransform">
|
||||
@foreach ((int i, string text) in Weekdays.Index())
|
||||
{
|
||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[8px] font-light"/>
|
||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[10px]"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
<SvgGroup Transform="@GlobalMapTransform">
|
||||
@@ -23,8 +23,7 @@
|
||||
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
||||
{
|
||||
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
||||
Class="@(GetColorByContribution(item.ContributionCount))"
|
||||
Id="@(item.ItemId)"/>
|
||||
Class="@(GetColorByContribution(item.ContributionCount))"/>
|
||||
}
|
||||
</SvgGroup>
|
||||
}
|
||||
@@ -119,4 +118,5 @@
|
||||
_ => "fill-blue-800"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<Footer/>
|
||||
<Foonter/>
|
||||
</main>
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer/>
|
||||
<Foonter/>
|
||||
</main>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl page-starter">关于</h1>
|
||||
<h1 class="text-4xl">关于</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
@@ -25,23 +25,16 @@
|
||||
正在明光村幼儿园附属研究生院攻读计算机科学与技术的硕士学位,研究AI编译器和异构编译器。
|
||||
</p>
|
||||
|
||||
<p class="my-1">
|
||||
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者
|
||||
<span class="italic">jackfiled</span>的名字活动。
|
||||
<p class="my-2">
|
||||
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span
|
||||
class="italic">jackfiled</span>的名字活动。
|
||||
<span class="line-through">(都是ICP备案过的人了,网名似乎没有太大的用处)</span>
|
||||
</p>
|
||||
<p class="my-1">
|
||||
Fun Fact:<span class="italic">jackfiled</span>这个名字来自于2020年我使用链接在树莓派上的9英寸屏幕注册
|
||||
GitHub的一时兴起,并没有任何特定的含义。
|
||||
<span class="italic">初冬的朝阳</span>则是源自初中,具体典故已不可考。
|
||||
至少到目前为止,还没有在要求唯一ID的平台遇见重名的情况。
|
||||
<span class="line-through">我的真实名字似乎也是如此。</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<p class="my-1">
|
||||
主要是一个.NET程序员,目前也在尝试写一点Rust。
|
||||
主要是一个C#程序员,目前也在尝试写一点Rust。
|
||||
<span class="line-through">
|
||||
总体上对于编程语言的态度是“大家都是我的翅膀.jpg”。
|
||||
</span>
|
||||
@@ -53,7 +46,7 @@
|
||||
常常因为现实的压力而写一些C/C++,现在就在和MLIR殊死搏斗。
|
||||
</p>
|
||||
<p class="my-1">
|
||||
日常使用Arch Linux,KISS的原则深得我心。
|
||||
日常使用Arch Linux。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +55,7 @@
|
||||
100%社恐。日常生活是宅在电脑前面自言自语。
|
||||
</p>
|
||||
<p class="my-1">
|
||||
兴趣活动是读书和看番,目前在玩戴森球计划和三角洲。2022年~2024年的时候沉迷于原神,现在偶尔还会登上去过一过剧情。
|
||||
兴趣活动是读书和看番,目前在玩戴森球计划和三角洲。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog/archives"
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl page-starter">归档</h1>
|
||||
<h1 class="text-4xl">归档</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/blog"
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -10,7 +10,6 @@
|
||||
</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.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.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 page-starter">@(_essay!.Title)</h1>
|
||||
<h1 id="title" class="text-4xl">@(_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.Abstractions.Models
|
||||
@using YaeBlog.Models
|
||||
@inject IOptions<BlogOptions> BlogOptionInstance
|
||||
|
||||
<PageTitle>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-4xl page-starter">
|
||||
<h1 class="text-4xl">
|
||||
友链
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/"
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.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 page-starter">初冬的朝阳</div>
|
||||
<div class="text-3xl font-bold">初冬的朝阳</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
@@ -57,17 +57,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5 pb-1">
|
||||
<div class="py-5">
|
||||
<p class="text-lg">恕我不能亲自为您沏茶,还是非常欢迎您来,能在广阔的互联网世界中发现这里实属不易。</p>
|
||||
</div>
|
||||
|
||||
<div class="text-lg">
|
||||
<div class="text-lg pt-2">
|
||||
<p class="py-1">
|
||||
正在攻读计算机科学与技术的硕士学位,研究方向是AI编译和异构编译!
|
||||
喜欢优雅的代码,香甜的蛋糕等等一切可爱的事物。
|
||||
更多的情报请见<Anchor Text="关于" Address="/about/"></Anchor>。
|
||||
</p>
|
||||
<p class="py-1">
|
||||
喜欢优雅的代码,香甜的蛋糕等等一切可爱的事物。
|
||||
</p>
|
||||
<p class="py-1">
|
||||
<Anchor Address="/blog/" Text="个人博客"/>中收集了我的各种奇思妙想,如果感兴趣欢迎移步。
|
||||
@@ -80,7 +79,7 @@
|
||||
</p>
|
||||
<p class="py-1">
|
||||
日常的代码开发使用自建的<Anchor Text="Gitea" Address="https://git.rrricardo.top" NewPage="@(true)"/>进行,个人
|
||||
开发的各种项目都可以在上面找到。下面的热力图展示了我在Git上的各种动态<span class="line-through">(Everything as Code)</span>。
|
||||
开发的各种项目都可以在上面找到。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PageTitle>
|
||||
|
||||
<div>
|
||||
<h3 class="text-3xl page-starter">NotFound!</h3>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/blog/tags/"
|
||||
@using System.Text.Encodings.Web
|
||||
@using YaeBlog.Abstractions
|
||||
@using YaeBlog.Abstractions.Models
|
||||
@using YaeBlog.Abstraction
|
||||
@using YaeBlog.Models
|
||||
|
||||
@inject IEssayContentService Contents
|
||||
@inject NavigationManager NavigationInstance
|
||||
@@ -11,7 +11,7 @@
|
||||
</PageTitle>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="page-starter">
|
||||
<div>
|
||||
@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="page-starter"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
|
||||
@@ -4,10 +4,8 @@ ARG COMMIT_ID
|
||||
ENV COMMIT_ID=${COMMIT_ID}
|
||||
|
||||
WORKDIR /app
|
||||
COPY out/ ./
|
||||
COPY bin/Release/net10.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
COPY src/YaeBlog/appsettings.json .
|
||||
|
||||
ENV BLOG__ROOT="./source"
|
||||
COPY appsettings.json .
|
||||
|
||||
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]
|
||||
|
||||
@@ -18,17 +18,5 @@ public static class DateOnlyExtensions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public int DayNumberOfWeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return date.DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Sunday => 7,
|
||||
_ => (int)date.DayOfWeek + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
using AngleSharp;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Processors;
|
||||
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class HostApplicationBuilderExtensions
|
||||
{
|
||||
extension(IHostApplicationBuilder builder)
|
||||
{
|
||||
public ConsoleInfoService AddYaeCommand(string[] arguments)
|
||||
{
|
||||
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>()
|
||||
.AddTransient<ImagePostRenderProcessor>()
|
||||
.AddTransient<HeadlinePostRenderProcessor>()
|
||||
.AddTransient<EssayStylesPostRenderProcessor>()
|
||||
.AddTransient<GiteaFetchService>()
|
||||
.AddTransient<BlogChangeWatcher>()
|
||||
.AddTransient<BlogHotReloadService>()
|
||||
.AddSingleton<GitHeapMapService>();
|
||||
|
||||
builder.Services.AddHostedService<StartServerService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs
Normal file
59
src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using AngleSharp;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Processors;
|
||||
|
||||
namespace YaeBlog.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
extension(WebApplicationBuilder builder)
|
||||
{
|
||||
public WebApplicationBuilder AddYaeBlog()
|
||||
{
|
||||
builder.ConfigureOptions<BlogOptions>(BlogOptions.OptionName)
|
||||
.ConfigureOptions<GiteaOptions>(GiteaOptions.OptionName);
|
||||
|
||||
builder.Services.AddHttpClient()
|
||||
.AddMarkdig()
|
||||
.AddYamlParser();
|
||||
|
||||
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default)
|
||||
.AddSingleton<IEssayScanService, EssayScanService>()
|
||||
.AddSingleton<RendererService>()
|
||||
.AddSingleton<IEssayContentService, EssayContentService>()
|
||||
.AddTransient<ImagePostRenderProcessor>()
|
||||
.AddTransient<HeadlinePostRenderProcessor>()
|
||||
.AddTransient<EssayStylesPostRenderProcessor>()
|
||||
.AddTransient<GiteaFetchService>()
|
||||
.AddSingleton<GitHeapMapService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public WebApplicationBuilder AddServer()
|
||||
{
|
||||
builder.Services.AddHostedService<BlogHostedService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public WebApplicationBuilder AddWatcher()
|
||||
{
|
||||
builder.Services.AddTransient<BlogChangeWatcher>();
|
||||
builder.Services.AddHostedService<BlogHotReloadService>();
|
||||
|
||||
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.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Processors;
|
||||
using YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 单个博客文件的所有数据和元数据
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
|
||||
: IEnumerable<BlogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record BlogEssay(
|
||||
string Title,
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.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.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class GiteaOptions
|
||||
{
|
||||
@@ -8,7 +8,7 @@ public class GiteaOptions
|
||||
|
||||
[Required] public required string BaseAddress { get; init; }
|
||||
|
||||
public string? ApiKey { get; init; }
|
||||
[Required] public required string ApiKey { get; init; }
|
||||
|
||||
[Required] public required string HeatMapUsername { get; init; }
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record GitContributionItem(DateOnly Time, long ContributionCount)
|
||||
{
|
||||
public string ItemId => $"item-{Time:yyyy-MM-dd}";
|
||||
}
|
||||
public record GitContributionItem(DateOnly Time, long ContributionCount);
|
||||
|
||||
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Abstractions.Models;
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
||||
|
||||
@@ -1,34 +1,4 @@
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Services;
|
||||
using YaeBlog.Commands;
|
||||
|
||||
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.AddYaeServer(consoleInfoService);
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.MapStaticAssets();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
application.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
application.MapControllers();
|
||||
|
||||
await application.RunAsync();
|
||||
YaeBlogCommand command = new();
|
||||
await command.RunAsync(args);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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.Abstractions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
|
||||
using Imageflow.Bindings;
|
||||
using Imageflow.Fluent;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.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>
|
||||
@@ -85,24 +83,7 @@ public sealed class GitHeapMapService(
|
||||
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// Not fill the last item and add directly.
|
||||
result.Add(groupedContribution);
|
||||
|
||||
_gitContributionsGroupedByWeek = result;
|
||||
|
||||
@@ -3,45 +3,34 @@ using System.Text.Json;
|
||||
using DotNext;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.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,
|
||||
ILogger<GiteaFetchService> logger)
|
||||
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
||||
if (string.IsNullOrWhiteSpace(giteaOptions.Value.ApiKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Api Token is set.");
|
||||
httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("token", giteaOptions.Value.ApiKey);
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", giteaOptions.Value.ApiKey);
|
||||
}
|
||||
|
||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory,
|
||||
ILogger<GiteaFetchService> logger) : this(giteaOptions, httpClientFactory.CreateClient(), logger)
|
||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory) : this(
|
||||
giteaOptions, httpClientFactory.CreateClient())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -61,7 +50,6 @@ 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.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
@@ -34,7 +34,6 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
if (needCompressContents.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No candidates found to be compressed.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +51,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
foreach (BlogImageInfo image in uncompressedImages)
|
||||
{
|
||||
logger.LogInformation("Uncompressed image: {filename} belonging to blog {blog}.", image.File.Name,
|
||||
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
|
||||
content.BlogName);
|
||||
}
|
||||
|
||||
@@ -83,7 +82,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
||||
|
||||
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
|
||||
|
||||
if (!dryRun)
|
||||
if (dryRun is false)
|
||||
{
|
||||
await Task.WhenAll(from content in compressedContent
|
||||
select essayScanService.SaveBlogContent(content, content.IsDraft));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstractions;
|
||||
using YaeBlog.Core.Exceptions;
|
||||
using YaeBlog.Abstractions.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed class YaeCommandService(
|
||||
string[] arguments,
|
||||
IEssayScanService essayScanService,
|
||||
ImageCompressService imageCompressService,
|
||||
ConsoleInfoService consoleInfoService,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
IOptions<BlogOptions> blogOptions,
|
||||
ILogger<YaeCommandService> logger)
|
||||
: BackgroundService
|
||||
{
|
||||
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||
private bool _oneShotCommandFlag = true;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
RootCommand rootCommand = new("YaeBlog CLI");
|
||||
|
||||
RegisterServeCommand(rootCommand);
|
||||
RegisterWatchCommand(rootCommand);
|
||||
|
||||
RegisterNewCommand(rootCommand);
|
||||
RegisterUpdateCommand(rootCommand);
|
||||
RegisterScanCommand(rootCommand);
|
||||
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)
|
||||
{
|
||||
throw new BlogCommandException($"YaeBlog command exited with no-zero code {exitCode}");
|
||||
}
|
||||
|
||||
consoleInfoService.IsOneShotCommand = _oneShotCommandFlag;
|
||||
|
||||
if (!consoleInfoService.IsOneShotCommand)
|
||||
{
|
||||
logger.LogInformation("Start YaeBlog command: {}", consoleInfoService.Command);
|
||||
}
|
||||
hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
private void RegisterServeCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("serve", "Start http server.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
command.SetHandler(HandleServeCommand);
|
||||
|
||||
// When invoking the root command without sub command, fallback to serve command.
|
||||
rootCommand.SetHandler(HandleServeCommand);
|
||||
}
|
||||
|
||||
private Task HandleServeCommand(InvocationContext context)
|
||||
{
|
||||
_oneShotCommandFlag = false;
|
||||
consoleInfoService.Command = ServerCommand.Serve;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RegisterWatchCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
command.SetHandler(_ =>
|
||||
{
|
||||
_oneShotCommandFlag = false;
|
||||
consoleInfoService.Command = ServerCommand.Watch;
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterNewCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("new", "Create a new blog file and image directory.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
||||
command.AddArgument(filenameArgument);
|
||||
|
||||
command.SetHandler(HandleNewCommand, filenameArgument);
|
||||
}
|
||||
|
||||
private async Task HandleNewCommand(string filename)
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
if (contents.Posts.Any(content => content.BlogName == filename))
|
||||
{
|
||||
throw new BlogCommandException("There exits the same title blog in posts.");
|
||||
}
|
||||
|
||||
await essayScanService.SaveBlogContent(new BlogContent(
|
||||
new FileInfo(Path.Combine(_blogOptions.Root, "drafts", filename + ".md")),
|
||||
new MarkdownMetadata
|
||||
{
|
||||
Title = filename,
|
||||
Date = DateTimeOffset.Now.ToString("o"),
|
||||
UpdateTime = DateTimeOffset.Now.ToString("o")
|
||||
},
|
||||
string.Empty, true, [], []
|
||||
));
|
||||
|
||||
logger.LogInformation("Create new blog '{}'", filename);
|
||||
}
|
||||
|
||||
private void RegisterUpdateCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("update", "Update the blog essay.");
|
||||
rootCommand.AddCommand(command);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The blog filename to update.");
|
||||
command.AddArgument(filenameArgument);
|
||||
|
||||
command.SetHandler(HandleUpdateCommand, filenameArgument);
|
||||
}
|
||||
|
||||
private async Task HandleUpdateCommand(string filename)
|
||||
{
|
||||
logger.LogInformation("The update command only considers published blogs.");
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == filename);
|
||||
if (content is null)
|
||||
{
|
||||
throw new BlogCommandException($"Target essay {filename} is not exist.");
|
||||
}
|
||||
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
await essayScanService.SaveBlogContent(content, content.IsDraft);
|
||||
logger.LogInformation("Update time of essay '{}' updated.", content.BlogName);
|
||||
}
|
||||
|
||||
private void RegisterScanCommand(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(HandleScanCommand, removeOption);
|
||||
}
|
||||
|
||||
private async Task HandleScanCommand(bool removeUnusedImages)
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
List<BlogImageInfo> unusedImages = (from content in contents
|
||||
from image in content.Images
|
||||
where image is { IsUsed: false }
|
||||
select image).ToList();
|
||||
|
||||
if (unusedImages.Count != 0)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.Append("Found unused images:").Append('\n');
|
||||
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
builder.Append('\t').Append("- ").Append(image.File.FullName).Append('\n');
|
||||
}
|
||||
|
||||
logger.LogInformation("{}", builder.ToString());
|
||||
logger.LogInformation("HINT: use '--rm' to remove unused images.");
|
||||
}
|
||||
|
||||
if (removeUnusedImages)
|
||||
{
|
||||
foreach (BlogImageInfo image in unusedImages)
|
||||
{
|
||||
image.File.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder infoBuilder = new();
|
||||
infoBuilder.Append("Used not existed images:\n");
|
||||
|
||||
bool flag = false;
|
||||
foreach (BlogContent content in contents)
|
||||
{
|
||||
foreach (FileInfo file in content.NotfoundImages)
|
||||
{
|
||||
flag = true;
|
||||
infoBuilder.Append('\t').Append("- ").Append(file.Name).Append(" in ").Append(content.BlogName)
|
||||
.Append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (flag)
|
||||
{
|
||||
logger.LogInformation("{}", infoBuilder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterPublishCommand(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(HandlePublishCommand, filenameArgument);
|
||||
}
|
||||
|
||||
private async Task HandlePublishCommand(string filename)
|
||||
{
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = (from blog in contents.Drafts
|
||||
where blog.BlogName == filename
|
||||
select blog).FirstOrDefault();
|
||||
|
||||
if (content is null)
|
||||
{
|
||||
throw new BlogCommandException("Target blog doest not exist.");
|
||||
}
|
||||
|
||||
logger.LogInformation("Publish blog {}", content.BlogName);
|
||||
|
||||
// 设置发布的时间
|
||||
content.Metadata.Date = DateTimeOffset.Now.ToString("o");
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
|
||||
// 将选中的博客文件复制到posts
|
||||
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||
|
||||
// 复制图片文件夹
|
||||
DirectoryInfo sourceImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
|
||||
DirectoryInfo targetImageDirectory =
|
||||
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
|
||||
|
||||
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.BlogName + ".md"));
|
||||
sourceBlogFile.Delete();
|
||||
}
|
||||
|
||||
private void RegisterCompressCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
|
||||
rootCommand.Add(command);
|
||||
|
||||
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
|
||||
getDefaultValue: () => false);
|
||||
command.AddOption(dryRunOption);
|
||||
|
||||
command.SetHandler(async dryRun => { await imageCompressService.Compress(dryRun); }, dryRunOption);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
||||
<ProjectReference Include="..\YaeBlog.Abstractions\YaeBlog.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -28,6 +27,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<ClientAssetsRestoreCommand>pnpm install</ClientAssetsRestoreCommand>
|
||||
<ClientAssetsBuildCommand>pwsh tailwind.ps1</ClientAssetsBuildCommand>
|
||||
<ClientAssetsBuildCommand>pwsh ../../build.ps1 tailwind</ClientAssetsBuildCommand>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"Links": [
|
||||
{
|
||||
"Name": "Ichirinko",
|
||||
"Description": "黑历史集合地,naive的代价",
|
||||
"Description": "这是个大哥",
|
||||
"Link": "https://ichirinko.top",
|
||||
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
|
||||
},
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"Gitea": {
|
||||
"BaseAddress": "https://git.rrricardo.top/api/v1/",
|
||||
"ApiKey": "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||
"HeatMapUsername": "jackfiled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/pwsh
|
||||
|
||||
[cmdletbinding()]
|
||||
param(
|
||||
[string]$Output = "wwwroot"
|
||||
)
|
||||
|
||||
end {
|
||||
Write-Host "Build tailwind css into $Output."
|
||||
pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css
|
||||
}
|
||||
Reference in New Issue
Block a user