feat: move TOBs to the left of essays.
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 3m32s

Fix the word counter to not count the characters in code blocks.

Signed-off-by: jackfiled <xcrenchangjun@outlook.com>
This commit is contained in:
2026-01-10 19:46:06 +08:00
parent 80e48a2043
commit 35f069f40a
9 changed files with 171 additions and 54 deletions

View File

@@ -13,9 +13,7 @@ jobs:
lfs: true
- name: Build project.
run: |
proxy
podman pull mcr.microsoft.com/dotnet/aspnet:10.0
unproxy
podman pull mcr.azure.cn/dotnet/aspnet:10.0
cd YaeBlog
pwsh build.ps1 build
- name: Workaround to make sure podman-login working.

View File

@@ -0,0 +1,13 @@
namespace YaeBlog.Tests;
public class DateTimeOffsetTests
{
[Fact]
public void DateTimeOffsetParseTest()
{
const string input = "2026-01-04T16:36:36.5629759+08:00";
DateTimeOffset time = DateTimeOffset.Parse(input);
Assert.Equal("2026年01月04日 16:36:36", time.ToString("yyyy年MM月dd日 HH:mm:ss"));
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YaeBlog\YaeBlog.csproj" />
</ItemGroup>
</Project>

View File

@@ -10,5 +10,6 @@
<File Path="LICENSE" />
<File Path="README.md" />
</Folder>
<Project Path="YaeBlog.Tests/YaeBlog.Tests.csproj" />
<Project Path="YaeBlog/YaeBlog.csproj" />
</Solution>

View File

@@ -12,52 +12,42 @@
<div class="flex flex-col py-8">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="col-auto">
</div>
</div>
<div class="flex flex-col items-center">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
</div>
<div class="px-6 pt-4 pb-2">
<div class="flex flex-row gap-4">
@foreach (string tag in _essay!.Tags)
<div class="flex flex-row gap-4 py-2">
@foreach (string tag in _essay!.Tags)
{
<div class="text-sky-500">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
}
</div>
<div class="font-light pb-1">
发布于: @(_essay.PublishTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
</div>
@if (_essay.UpdateTime != _essay.PublishTime)
{
<div class="text-sky-500">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
<div class="font-light pb-1">
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
</div>
}
</div>
<div class="font-light py-1">
发布于: @(_essay.PublishTime.ToString("yyyy年MM月dd日 hh:mm:ss"))
</div>
@if (_essay.UpdateTime != _essay.PublishTime)
{
<div class="font-light pb-1">
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 hh:mm:ss"))
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)
</div>
}
<div class="font-light pb-1">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
</div>
</div>
<div class="grid grid-cols-3">
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
<div class="col-span-3 md:col-span-1">
<div class="flex flex-col sticky top-0 px-8">
<div class="flex flex-col sticky top-20 px-8 pt-20">
<div>
<h3 class="text-2xl">文章目录</h3>
</div>
@@ -98,8 +88,17 @@
}
</div>
</div>
</div>
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
</div>
</div>
@code {

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0
FROM mcr.azure.cn/dotnet/aspnet:10.0
ARG COMMIT_ID
ENV COMMIT_ID=${COMMIT_ID}

View File

@@ -0,0 +1,62 @@
using YaeBlog.Extensions;
using YaeBlog.Models;
namespace YaeBlog.Services
{
public class MarkdownWordCounter
{
private bool _inCodeBlock;
private int _index;
private readonly string _content;
private uint WordCount { get; set; }
private MarkdownWordCounter(BlogContent content)
{
_content = content.Content;
}
private void CountWordInner()
{
while (_index < _content.Length)
{
if (IsCodeBlockTag())
{
_inCodeBlock = !_inCodeBlock;
}
if (!_inCodeBlock && char.IsLetterOrDigit(_content, _index))
{
WordCount += 1;
}
_index++;
}
}
private bool IsCodeBlockTag()
{
// 首先考虑识别代码块
bool outerCodeBlock =
Enumerable.Range(0, 3)
.Select(i => _index + i < _content.Length && _content.AsSpan().Slice(_index + i, 1) is "`")
.All(i => i);
if (outerCodeBlock)
{
return true;
}
// 然后识别行内代码
return _index < _content.Length && _content.AsSpan().Slice(_index, 1) is "`";
}
public static uint CountWord(BlogContent content)
{
MarkdownWordCounter counter = new(content);
counter.CountWordInner();
return counter.WordCount;
}
}
}

View File

@@ -38,7 +38,7 @@ public partial class RendererService(
List<BlogEssay> essays = [];
foreach (BlogContent content in preProcessedContents)
{
uint wordCount = GetWordCount(content);
(uint wordCount, string readTime) = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.BlogName,
@@ -46,7 +46,7 @@ public partial class RendererService(
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
ReadTime = readTime,
PublishTime = content.Metadata.Date == default ? DateTimeOffset.Now : content.Metadata.Date,
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
UpdateTime =
@@ -190,23 +190,16 @@ public partial class RendererService(
return description;
}
private uint GetWordCount(BlogContent content)
private (uint, string) GetWordCount(BlogContent content)
{
int count = (from c in content.Content
where char.IsLetterOrDigit(c)
select c).Count();
uint count = MarkdownWordCounter.CountWord(content);
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
count);
return (uint)count;
}
private static string CalculateReadTime(uint wordCount)
{
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟
int second = (int)wordCount / 10;
TimeSpan span = new(0, 0, second);
uint second = count / 10;
TimeSpan span = new(0, 0, (int)second);
return span.ToString("mm'分 'ss'秒'");
return (count, span.ToString("mm'分'ss'秒'"));
}
}

View File

@@ -3,7 +3,7 @@
[cmdletbinding()]
param(
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
[ValidateSet("tailwind", "watch", "publish", "compress", "build")]
[ValidateSet("tailwind", "watch", "publish", "compress", "build", "dev")]
[string]$Target,
[string]$Output = "wwwroot",
[string]$Essay,
@@ -56,6 +56,28 @@ process {
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
}
function Start-Develop {
Write-Host "Start tailwindcss and dotnet watch servers..."
$pnpmProcess = Start-Process pnpm "tailwindcss -i wwwroot/tailwind.css -o obj/Debug/net10.0/ClientAssets/tailwind.g.css -w" `
-PassThru
try
{
Write-Host "Started pnpm process exit? " $pnpmProcess.HasExited
Start-Process dotnet "watch -- serve" -PassThru | Wait-Process
}
finally
{
if ($pnpmProcess.HasExited)
{
Write-Error "pnpm process has exited!"
exit 1
}
Write-Host "Kill tailwindcss and dotnet watch servers..."
$pnpmProcess | Stop-Process
}
}
switch ($Target)
{
"tailwind" {
@@ -85,6 +107,10 @@ process {
Build-Image
break
}
"dev" {
Start-Develop
break
}
}
}