feat: move TOBs to the left of essays.
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 3m32s
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:
@@ -13,9 +13,7 @@ jobs:
|
|||||||
lfs: true
|
lfs: true
|
||||||
- name: Build project.
|
- name: Build project.
|
||||||
run: |
|
run: |
|
||||||
proxy
|
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
||||||
podman pull mcr.microsoft.com/dotnet/aspnet:10.0
|
|
||||||
unproxy
|
|
||||||
cd YaeBlog
|
cd YaeBlog
|
||||||
pwsh build.ps1 build
|
pwsh build.ps1 build
|
||||||
- name: Workaround to make sure podman-login working.
|
- name: Workaround to make sure podman-login working.
|
||||||
|
|||||||
13
YaeBlog.Tests/DateTimeOffsetTests.cs
Normal file
13
YaeBlog.Tests/DateTimeOffsetTests.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
YaeBlog.Tests/YaeBlog.Tests.csproj
Normal file
25
YaeBlog.Tests/YaeBlog.Tests.csproj
Normal 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>
|
||||||
@@ -10,5 +10,6 @@
|
|||||||
<File Path="LICENSE" />
|
<File Path="LICENSE" />
|
||||||
<File Path="README.md" />
|
<File Path="README.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Project Path="YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
||||||
<Project Path="YaeBlog/YaeBlog.csproj" />
|
<Project Path="YaeBlog/YaeBlog.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -12,52 +12,42 @@
|
|||||||
|
|
||||||
<div class="flex flex-col py-8">
|
<div class="flex flex-col py-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
<div class="flex flex-col items-center">
|
||||||
<div class="col-auto">
|
<div>
|
||||||
</div>
|
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-6 pt-4 pb-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
<div class="flex flex-row gap-4">
|
@foreach (string tag in _essay!.Tags)
|
||||||
@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">
|
<div class="font-light pb-1">
|
||||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
|
||||||
# @(tag)
|
|
||||||
</a>
|
|
||||||
</div>
|
</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">
|
<div class="font-light pb-1">
|
||||||
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 hh:mm:ss"))
|
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<div class="font-light pb-1">
|
|
||||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3">
|
<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="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>
|
<div>
|
||||||
<h3 class="text-2xl">文章目录</h3>
|
<h3 class="text-2xl">文章目录</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,8 +88,17 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
FROM mcr.azure.cn/dotnet/aspnet:10.0
|
||||||
|
|
||||||
ARG COMMIT_ID
|
ARG COMMIT_ID
|
||||||
ENV COMMIT_ID=${COMMIT_ID}
|
ENV COMMIT_ID=${COMMIT_ID}
|
||||||
|
|||||||
62
YaeBlog/Services/MarkdownWordCounter.cs
Normal file
62
YaeBlog/Services/MarkdownWordCounter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public partial class RendererService(
|
|||||||
List<BlogEssay> essays = [];
|
List<BlogEssay> essays = [];
|
||||||
foreach (BlogContent content in preProcessedContents)
|
foreach (BlogContent content in preProcessedContents)
|
||||||
{
|
{
|
||||||
uint wordCount = GetWordCount(content);
|
(uint wordCount, string readTime) = GetWordCount(content);
|
||||||
BlogEssay essay = new()
|
BlogEssay essay = new()
|
||||||
{
|
{
|
||||||
Title = content.Metadata.Title ?? content.BlogName,
|
Title = content.Metadata.Title ?? content.BlogName,
|
||||||
@@ -46,7 +46,7 @@ public partial class RendererService(
|
|||||||
IsDraft = content.IsDraft,
|
IsDraft = content.IsDraft,
|
||||||
Description = GetDescription(content),
|
Description = GetDescription(content),
|
||||||
WordCount = wordCount,
|
WordCount = wordCount,
|
||||||
ReadTime = CalculateReadTime(wordCount),
|
ReadTime = readTime,
|
||||||
PublishTime = content.Metadata.Date == default ? DateTimeOffset.Now : content.Metadata.Date,
|
PublishTime = content.Metadata.Date == default ? DateTimeOffset.Now : content.Metadata.Date,
|
||||||
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
|
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
|
||||||
UpdateTime =
|
UpdateTime =
|
||||||
@@ -190,23 +190,16 @@ public partial class RendererService(
|
|||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint GetWordCount(BlogContent content)
|
private (uint, string) GetWordCount(BlogContent content)
|
||||||
{
|
{
|
||||||
int count = (from c in content.Content
|
uint count = MarkdownWordCounter.CountWord(content);
|
||||||
where char.IsLetterOrDigit(c)
|
|
||||||
select c).Count();
|
|
||||||
|
|
||||||
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
|
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
|
||||||
count);
|
count);
|
||||||
return (uint)count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CalculateReadTime(uint wordCount)
|
|
||||||
{
|
|
||||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||||
int second = (int)wordCount / 10;
|
uint second = count / 10;
|
||||||
TimeSpan span = new(0, 0, second);
|
TimeSpan span = new(0, 0, (int)second);
|
||||||
|
|
||||||
return span.ToString("mm'分 'ss'秒'");
|
return (count, span.ToString("mm'分'ss'秒'"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[cmdletbinding()]
|
[cmdletbinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
|
[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]$Target,
|
||||||
[string]$Output = "wwwroot",
|
[string]$Output = "wwwroot",
|
||||||
[string]$Essay,
|
[string]$Essay,
|
||||||
@@ -56,6 +56,28 @@ process {
|
|||||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
|
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)
|
switch ($Target)
|
||||||
{
|
{
|
||||||
"tailwind" {
|
"tailwind" {
|
||||||
@@ -85,6 +107,10 @@ process {
|
|||||||
Build-Image
|
Build-Image
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
"dev" {
|
||||||
|
Start-Develop
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user