2 Commits

Author SHA1 Message Date
f49b4a3ea7 Merge branch 'master' into write-llvm-0 2024-08-23 20:49:00 +08:00
9d6112f987 write: llvm-naive-0 2024-08-21 14:23:00 +08:00
141 changed files with 1096 additions and 4870 deletions

3
.gitignore vendored
View File

@@ -482,6 +482,3 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
# Tailwind auto-generated stylesheet
output.css

View File

@@ -1,13 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Abstraction;
namespace YaeBlog.Core.Abstractions;
public interface IEssayContentService
{
public IEnumerable<BlogEssay> Essays { get; }
public int Count { get; }
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
@@ -18,8 +16,6 @@ public interface IEssayContentService
public bool TryAdd(BlogEssay essay);
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
public void RefreshTags();
public void Clear();

View File

@@ -1,12 +1,10 @@
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Abstraction;
namespace YaeBlog.Core.Abstractions;
public interface IEssayScanService
{
public Task<BlogContents> ScanContents();
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
public Task<ImageScanResult> ScanImages();
}

View File

@@ -1,6 +1,6 @@
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Abstraction;
namespace YaeBlog.Core.Abstractions;
public interface IPostRenderProcessor
{

View File

@@ -1,6 +1,6 @@
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Abstraction;
namespace YaeBlog.Core.Abstractions;
public interface IPreRenderProcessor
{

View File

@@ -0,0 +1,8 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop

View File

@@ -1,8 +1,9 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace YaeBlog.Extensions;
namespace YaeBlog.Core.Extensions;
public static class ServiceCollectionExtensions
{

View File

@@ -1,11 +1,13 @@
using AngleSharp;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Services;
using YaeBlog.Models;
using YaeBlog.Processors;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Processors;
using YaeBlog.Core.Services;
namespace YaeBlog.Extensions;
namespace YaeBlog.Core.Extensions;
public static class WebApplicationBuilderExtensions
{
@@ -17,13 +19,14 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddMarkdig();
builder.Services.AddYamlParser();
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
builder.Services.AddSingleton<RendererService>();
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
builder.Services.AddTransient<ImagePostRenderProcessor>();
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
builder.Services.AddTransient<TablePostRenderProcessor>();
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
builder.Services.AddTransient<BlogOptions>(provider =>
provider.GetRequiredService<IOptions<BlogOptions>>().Value);

View File

@@ -1,16 +1,19 @@
using YaeBlog.Abstraction;
using YaeBlog.Processors;
using YaeBlog.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Processors;
using YaeBlog.Core.Services;
namespace YaeBlog.Extensions;
namespace YaeBlog.Core.Extensions;
public static class WebApplicationExtensions
{
public static void UseYaeBlog(this WebApplication application)
{
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
application.UsePostRenderProcessor<TablePostRenderProcessor>();
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
}
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor

View File

@@ -0,0 +1,10 @@
namespace YaeBlog.Core.Models;
public class AboutInfo
{
public required string Introduction { get; set; }
public required string Description { get; set; }
public required string AvatarImage { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class BlogContent
{
@@ -7,6 +7,4 @@ public class BlogContent
public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
{

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class BlogEssay : IComparable<BlogEssay>
{
@@ -6,8 +6,6 @@ public class BlogEssay : IComparable<BlogEssay>
public required string FileName { get; init; }
public required bool IsDraft { get; init; }
public required DateTime PublishTime { get; init; }
public required string Description { get; init; }
@@ -26,7 +24,6 @@ public class BlogEssay : IComparable<BlogEssay>
{
Title = Title,
FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime,
Description = Description,
WordCount = WordCount,
@@ -42,16 +39,10 @@ public class BlogEssay : IComparable<BlogEssay>
{
if (other is null)
{
return -1;
return 1;
}
// 草稿文章应当排在前面
if (IsDraft != other.IsDraft)
{
return IsDraft ? -1 : 1;
}
return other.PublishTime.CompareTo(PublishTime);
return PublishTime.CompareTo(other.PublishTime);
}
public override string ToString()

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class BlogHeadline(string title, string selectorId)
{

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class BlogOptions
{

View File

@@ -1,6 +1,6 @@
using System.Text.Encodings.Web;
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class EssayTag(string tagName) : IEquatable<EssayTag>
{

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
/// <summary>
/// 友链模型类

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Models;
namespace YaeBlog.Core.Models;
public class MarkdownMetadata
{

View File

@@ -0,0 +1,29 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Processors;
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IElement> preElements = from e in document.All
where e.LocalName == "pre"
select e;
foreach (IElement element in preElements)
{
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(CodeBlockPostRenderProcessor);
}

View File

@@ -1,12 +1,13 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Processors;
namespace YaeBlog.Core.Processors;
public class HeadlinePostRenderProcessor(
AngleSharp.IConfiguration angleConfiguration,
IConfiguration angleConfiguration,
IEssayContentService essayContentService,
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
{

View File

@@ -1,21 +1,24 @@
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Processors;
namespace YaeBlog.Core.Processors;
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
private static readonly IConfiguration s_configuration = Configuration.Default;
private readonly BlogOptions _options = options.Value;
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
BrowsingContext context = new(s_configuration);
IDocument html = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
@@ -31,6 +34,7 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
}
element.ClassList.Add("essay-image");
}
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
}

View File

@@ -0,0 +1,34 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.Processors;
public class TablePostRenderProcessor: IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext browsingContext = new(Configuration.Default);
IDocument document = await browsingContext.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
where item.LocalName == "table"
select item as IHtmlTableElement;
foreach (IHtmlTableElement element in tableElements)
{
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
divElement.InnerHtml = element.OuterHtml;
divElement.ClassList.Add("py-2", "table-wrapper");
element.Replace(divElement);
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(TablePostRenderProcessor);
}

View File

@@ -1,7 +1,8 @@
using Microsoft.Extensions.Options;
using YaeBlog.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Models;
namespace YaeBlog.Services;
namespace YaeBlog.Core.Services;
public sealed class BlogChangeWatcher : IDisposable
{

View File

@@ -1,4 +1,7 @@
namespace YaeBlog.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace YaeBlog.Core.Services;
public class BlogHostedService(
ILogger<BlogHostedService> logger,
@@ -6,12 +9,14 @@ public class BlogHostedService(
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Failed to load cache, render essays.");
logger.LogInformation("Welcome to YaeBlog!");
await rendererService.RenderAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
namespace YaeBlog.Core.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("BlogHotReloadService is starting.");
await rendererService.RenderAsync();
while (!stoppingToken.IsCancellationRequested)
{
logger.LogDebug("Watching file changes...");
string? changFile = await watcher.WaitForChange(stoppingToken);
if (changFile is null)
{
logger.LogInformation("BlogHotReloadService is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changFile);
essayContentService.Clear();
await rendererService.RenderAsync();
}
}
}

View File

@@ -1,36 +1,23 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using YaeBlog.Abstraction;
using YaeBlog.Models;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Services;
namespace YaeBlog.Core.Services;
public class EssayContentService : IEssayContentService
{
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
private readonly List<BlogEssay> _sortedEssays = [];
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
public bool TryAdd(BlogEssay essay)
{
_sortedEssays.Add(essay);
return _essays.TryAdd(essay.FileName, essay);
}
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IEnumerable<BlogEssay> Essays => _sortedEssays;
public int Count => _sortedEssays.Count;
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
{
return _essays.TryGetValue(filename, out essay);
}
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;

View File

@@ -1,15 +1,15 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
using YaeBlog.Core.Models;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace YaeBlog.Services;
namespace YaeBlog.Core.Services;
public partial class EssayScanService(
public class EssayScanService(
ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
@@ -22,8 +22,8 @@ public partial class EssayScanService(
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts, true),
await ScanContentsInternal(posts, false));
await ScanContentsInternal(drafts),
await ScanContentsInternal(posts));
}
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
@@ -34,11 +34,6 @@ public partial class EssayScanService(
? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md"))
: new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md"));
if (!isDraft)
{
content.Metadata.Date = DateTime.Now;
}
if (targetFile.Exists)
{
logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
@@ -49,22 +44,13 @@ public partial class EssayScanService(
await writer.WriteAsync("---\n");
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
await writer.WriteAsync("---\n");
if (isDraft)
{
await writer.WriteLineAsync("<!--more-->");
}
else
{
await writer.WriteAsync(content.FileContent);
}
await writer.WriteAsync("<!--more-->\n");
}
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
{
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
where file.Extension == ".md" && !file.Name.StartsWith('.')
where file.Extension == ".md"
select file;
ConcurrentBag<(string, string)> fileContents = [];
@@ -97,8 +83,7 @@ public partial class EssayScanService(
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
});
}
catch (YamlException e)
@@ -111,78 +96,6 @@ public partial class EssayScanService(
return contents;
}
public async Task<ImageScanResult> ScanImages()
{
BlogContents contents = await ScanContents();
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
List<FileInfo> unusedFiles = [];
List<FileInfo> notFoundFiles = [];
ImageScanResult draftResult = await ScanUnusedImagesInternal(contents.Drafts, drafts);
ImageScanResult postResult = await ScanUnusedImagesInternal(contents.Posts, posts);
unusedFiles.AddRange(draftResult.UnusedImages);
notFoundFiles.AddRange(draftResult.NotFoundImages);
unusedFiles.AddRange(postResult.UnusedImages);
notFoundFiles.AddRange(postResult.NotFoundImages);
return new ImageScanResult(unusedFiles, notFoundFiles);
}
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
DirectoryInfo root)
{
ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = [];
Parallel.ForEach(contents, content =>
{
MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
Dictionary<string, bool> usedDictionary;
if (imageDirectory.Exists)
{
usedDictionary = (from file in imageDirectory.EnumerateFiles()
select new KeyValuePair<string, bool>(file.FullName, false)).ToDictionary();
}
else
{
usedDictionary = [];
}
foreach (Match match in result)
{
string imageName = match.Groups[1].Value;
FileInfo usedFile = imageName.Contains(content.FileName)
? new FileInfo(Path.Combine(root.FullName, imageName))
: new FileInfo(Path.Combine(root.FullName, content.FileName, imageName));
if (usedDictionary.TryGetValue(usedFile.FullName, out _))
{
usedDictionary[usedFile.FullName] = true;
}
else
{
notFoundImage.Add(usedFile);
}
}
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
{
unusedImage.Add(new FileInfo(pair.Key));
}
});
return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList()));
}
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex ImagePattern { get; }
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
{
root = Path.Combine(Environment.CurrentDirectory, root);

View File

@@ -3,11 +3,12 @@ using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using Markdig;
using YaeBlog.Abstraction;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Services;
namespace YaeBlog.Core.Services;
public partial class RendererService(
ILogger<RendererService> logger,
@@ -21,43 +22,40 @@ public partial class RendererService(
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
public async Task RenderAsync(bool includeDrafts = false)
public async Task RenderAsync()
{
_stopwatch.Start();
logger.LogInformation("Render essays start.");
BlogContents contents = await essayScanService.ScanContents();
List<BlogContent> posts = contents.Posts.ToList();
if (includeDrafts)
{
posts.AddRange(contents.Drafts);
}
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
List<BlogEssay> essays = [];
foreach (BlogContent content in preProcessedContents)
await Task.Run(() =>
{
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
foreach (BlogContent content in preProcessedContents)
{
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
}
essays.Add(essay);
}
essays.Add(essay);
}
});
ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay =>
@@ -69,16 +67,7 @@ public partial class RendererService(
logger.LogDebug("Render markdown file {}.", newEssay);
});
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
foreach (BlogEssay essay in postProcessedEssays)
{
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
}
}
await PostProcess(postProcessEssays);
essayContentService.RefreshTags();
_stopwatch.Stop();
@@ -129,10 +118,8 @@ public partial class RendererService(
return processedContents;
}
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
private async Task PostProcess(IEnumerable<BlogEssay> essays)
{
ConcurrentBag<BlogEssay> processedContents = [];
await Parallel.ForEachAsync(essays, async (essay, _) =>
{
foreach (IPostRenderProcessor processor in _postRenderProcessors)
@@ -140,18 +127,16 @@ public partial class RendererService(
essay = await processor.ProcessAsync(essay);
}
processedContents.Add(essay);
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
});
List<BlogEssay> result = processedContents.ToList();
result.Sort();
return result;
}
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
// private static partial Regex DescriptionPattern();
private static partial Regex DescriptionPattern { get; }
private static partial Regex DescriptionPattern();
private string GetDescription(BlogContent content)
{
@@ -167,7 +152,7 @@ public partial class RendererService(
}
string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern.Matches(rawContent);
MatchCollection matches = DescriptionPattern().Matches(rawContent);
StringBuilder builder = new();
foreach (Match match in matches)

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.0" />
<PackageReference Include="Markdig" Version="0.34.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
@@ -27,6 +29,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,7 +1,7 @@
using System.CommandLine.Binding;
using System.Text.Json;
using Microsoft.Extensions.Options;
using YaeBlog.Models;
using YaeBlog.Core.Models;
namespace YaeBlog.Commands.Binders;

View File

@@ -1,8 +1,8 @@
using System.CommandLine.Binding;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Models;
using YaeBlog.Services;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Services;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

View File

@@ -0,0 +1,118 @@
using System.CommandLine;
using YaeBlog.Commands.Binders;
using YaeBlog.Components;
using YaeBlog.Core.Extensions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Services;
namespace YaeBlog.Commands;
public static class CommandExtensions
{
public static void AddServeCommand(this RootCommand rootCommand)
{
Command serveCommand = new("serve", "Start http server.");
rootCommand.AddCommand(serveCommand);
serveCommand.SetHandler(async context =>
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog();
builder.AddServer();
WebApplication application = builder.Build();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
application.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
application.MapControllers();
CancellationToken token = context.GetCancellationToken();
await application.RunAsync(token);
});
}
public static void AddWatchCommand(this RootCommand rootCommand)
{
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
rootCommand.AddCommand(command);
command.SetHandler(async context =>
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog();
builder.AddWatcher();
WebApplication application = builder.Build();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
application.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
application.MapControllers();
CancellationToken token = context.GetCancellationToken();
await application.RunAsync(token);
});
}
public static void AddNewCommand(this RootCommand rootCommand)
{
Command newCommand = new("new", "Create a new blog file and image directory.");
rootCommand.AddCommand(newCommand);
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
newCommand.AddArgument(filenameArgument);
newCommand.SetHandler(async (file, _, _, essayScanService) =>
{
await essayScanService.SaveBlogContent(new BlogContent
{
FileName = file,
FileContent = string.Empty,
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
});
Console.WriteLine($"Created new blog '{file}.");
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
new EssayScanServiceBinder());
}
public static void AddListCommand(this RootCommand rootCommand)
{
Command command = new("list", "List all blogs");
rootCommand.Add(command);
command.SetHandler(async (_, _, essyScanService) =>
{
BlogContents contents = await essyScanService.ScanContents();
Console.WriteLine($"All {contents.Posts.Count} Posts:");
foreach (BlogContent content in contents.Posts)
{
Console.WriteLine($" - {content.FileName}");
}
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
foreach (BlogContent content in contents.Drafts)
{
Console.WriteLine($" - {content.FileName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
}

View File

@@ -1,231 +0,0 @@
using System.CommandLine;
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);
AddPublishCommand(_rootCommand);
AddScanCommand(_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.UseStaticFiles();
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.UseStaticFiles();
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, _, _, essayScanService) =>
{
BlogContents contents = await essayScanService.ScanContents();
if (contents.Posts.Any(content => content.FileName == file))
{
Console.WriteLine("There exists the same title blog in posts.");
return;
}
await essayScanService.SaveBlogContent(new BlogContent
{
FileName = file,
FileContent = string.Empty,
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
});
Console.WriteLine($"Created new blog '{file}.");
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
new EssayScanServiceBinder());
}
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.FileName))
{
Console.WriteLine($" - {content.FileName}");
}
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
{
Console.WriteLine($" - {content.FileName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
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) =>
{
ImageScanResult result = await essayScanService.ScanImages();
if (result.UnusedImages.Count != 0)
{
Console.WriteLine("Found unused images:");
Console.WriteLine("HINT: use '--rm' to remove unused images.");
}
foreach (FileInfo image in result.UnusedImages)
{
Console.WriteLine($" - {image.FullName}");
}
if (removeOptionValue)
{
foreach (FileInfo image in result.UnusedImages)
{
image.Delete();
}
}
Console.WriteLine("Used not existed images:");
foreach (FileInfo image in result.NotFoundImages)
{
Console.WriteLine($" - {image.FullName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
}
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.FileName == filename
select blog).FirstOrDefault();
if (content is null)
{
Console.WriteLine("Target blog does not exist.");
return;
}
// 将选中的博客文件复制到posts
await essayScanService.SaveBlogContent(content, isDraft: false);
// 复制图片文件夹
DirectoryInfo sourceImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
DirectoryInfo targetImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
if (sourceImageDirectory.Exists)
{
targetImageDirectory.Create();
foreach (FileInfo file in sourceImageDirectory.EnumerateFiles())
{
file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true);
}
sourceImageDirectory.Delete(true);
}
// 删除原始的文件
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
sourceBlogFile.Delete();
}, new BlogOptionsBinder(),
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
}
}

View File

@@ -1,9 +0,0 @@
<a href="@Address" class="text-blue-600" target="@(NewPage ? "_blank" : "_self")">@Text</a>
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool NewPage { get; set; }
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh">
<html lang="en">
<head>
<meta charset="utf-8"/>
@@ -7,14 +7,22 @@
<base href="/"/>
<link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="bootstrap.min.css"/>
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
<link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="output.css"/>
<HeadOutlet/>
</head>
<body>
<Routes/>
<script src="_framework/blazor.web.js"></script>
<Routes/>
<script src="_framework/blazor.web.js"></script>
<script src="bootstrap.bundle.min.js"></script>
<script src="clipboard.min.js"></script>
<script>
const clipboard = new ClipboardJS('.btn');
</script>
</body>
</html>

View File

@@ -1,49 +1,57 @@
@using YaeBlog.Abstraction
@using YaeBlog.Models
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@inject IEssayContentService Contents
@inject BlogOptions Options
<div class="flex flex-col">
<div class="p-10">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full"/>
<div class="container">
<div class="row justify-content-center">
<div class="col-auto p-4">
<Image Src="images/avatar.png" Alt="Ricardo's avatar"/>
</div>
</div>
<div class="px-10 py-2 text-xl">
“奇奇怪怪东西的聚合地”
<div class="row justify-content-center p-3">
<div class="col-auto fs-4">
“奇奇怪怪东西的聚合地”
</div>
</div>
<div class="flex flex-row justify-between px-6 py-2 text-xl">
<div>
<div class="row justify-content-between px-2 py-1 fs-5">
<div class="col-auto">
文章
</div>
<a href="/blog/archives/">
<div>
@(Contents.Count)
</div>
</a>
<div class="col-auto">
<a href="/blog/archives">
@(Contents.Essays.Count)
</a>
</div>
</div>
<div class="flex flex-row justify-between px-6 py-2 text-xl">
<div>
<div class="row justify-content-between px-2 py-1 fs-5">
<div class="col-auto">
标签
</div>
<a href="/blog/tags/">
<div>
<div class="col-auto">
<a href="/blog/tags">
@(Contents.Tags.Count)
</div>
</a>
</a>
</div>
</div>
<div class="text-xl px-2 py-2">
广而告之
<div class="row justify-content-start fs-5" style="padding-top: 2em">
<div class="col-auto">
广而告之
</div>
</div>
<div class="px-6">
<p class="text-lg">
@(Options.Announcement)
</p>
<div class="row">
<div class="col">
<p style="text-indent: 2em">
@(Options.Announcement)
</p>
</div>
</div>
</div>

View File

@@ -1,19 +1,19 @@
@using System.Text.Encodings.Web
@using YaeBlog.Models
@using YaeBlog.Core.Models
<div class="flex flex-col p-3">
<div class="text-3xl font-bold py-2">
<div class="container p-3">
<div class="row fs-2 fw-bold py-2 essay-title">
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
</div>
<div class="p-2 flex flex-row justify-content-start gap-2">
<div class="font-light">
<div class="row p-2 justify-content-start">
<div class="col-auto fw-light">
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
</div>
@foreach (string key in Essay.Tags)
{
<div class="text-sky-600">
<div class="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
# @key
</a>
@@ -21,11 +21,20 @@
}
</div>
<div class="p-2">
@(Essay.Description)
<div class="row p-2">
<div class="col">
@(Essay.Description)
</div>
</div>
<div class="row">
<div class="col border-bottom">
</div>
</div>
</div>
@code {
[Parameter] public required BlogEssay Essay { get; set; }
[Parameter]
public required BlogEssay Essay { get; set; }
}

View File

@@ -0,0 +1,3 @@
.essay-title a {
color: var(--bs-body-color);
}

View File

@@ -1,22 +1,14 @@
<div class="flex flex-col text-center py-2">
<div>
<p class="text-md">
2021 - @(DateTimeOffset.Now.Year) ©
<Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
,由
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
<div class="row align-items-end text-center">
<div class="row">
<p class="fs-6">
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。
</p>
</div>
<div>
<p class="text-md">
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
<div class="row">
<p class="fs-6">
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
</p>
</div>
</div>
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
}

View File

@@ -1,33 +1,36 @@
@using YaeBlog.Models
@using YaeBlog.Core.Models
@inject BlogOptions Options
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
<div class="flex flex-col gap-3 text-md">
<div>
文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle">
<div class="col">
<div class="row p-1">
<div class="col">
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
</div>
</div>
<div>
文章地址:
<a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
@($"https://rrricardo.top/blog/essays/{EssayFilename}")
</a>
<div class="row p-1">
<div class="col">
文章地址:
<a href="/blog/essays/@(EssayAddress)" target="_blank">
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
</a>
</div>
</div>
<div>
版权声明:本博客所有文章除特别声明外,均采用
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="text-blue-600">
CC BY-NC-SA 4.0
</a>
许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
<a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
Ricardo's Blog
</a>”。
<div class="row p-1">
<div class="col">
版权声明:本博客所有文章除特别声明外,均采用
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a>
许可协议,转载请注明来自
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>。
</div>
</div>
</div>
</div>
@code
{
[Parameter] public string? EssayFilename { get; set; }
[Parameter] public string? EssayAddress { get; set; }
}

View File

@@ -0,0 +1,2 @@
.copyright {
}

View File

@@ -1,22 +0,0 @@
@if (Selected)
{
<div class="border rounded-lg shadow-neutral-500 bg-sky-400 w-8 h-8 inline-block leading-8 text-center">
<span class="text-white">@(Text)</span>
</div>
}
else
{
<a href="@Address">
<div class="border rounded-lg shadow-neutral-500 w-8 h-8 inline-block leading-8 text-center">
<span>@(Text)</span>
</div>
</a>
}
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool Selected { get; set; }
}

View File

@@ -1,46 +0,0 @@
<div class="flex flex-row justify-center gap-3">
@if (Page != 1)
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="<"/>
}
@if (Page == 1)
{
<PageAnchor Address="@GenerateAddress(1)" Text="1" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(2)" Text="2"/>
<PageAnchor Address="@GenerateAddress(3)" Text="3"/>
}
else if (Page == PageCount)
{
<PageAnchor Address="@GenerateAddress(PageCount - 2)" Text="@($"{PageCount - 2}")"/>
<PageAnchor Address="@GenerateAddress(PageCount - 1)" Text="@($"{PageCount - 1}")"/>
<PageAnchor Address="@GenerateAddress(PageCount)" Text="@($"{PageCount}")" Selected="@true"/>
}
else
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page + 1}")"/>
}
@if (Page != PageCount)
{
<PageAnchor Address="@GenerateAddress(Page + 1)" Text=">"/>
}
</div>
@code {
[Parameter] public string? BaseUrl { get; set; }
[Parameter] public int PageCount { get; set; }
[Parameter] public int Page { get; set; }
private string GenerateAddress(int page) => $"{BaseUrl}?page={page}";
}

View File

@@ -9,20 +9,12 @@ public class FilesController : ControllerBase
[HttpGet("{*filename}")]
public IActionResult Images(string filename)
{
// 这里疑似有点太愚蠢了
string contentType = "image/png";
if (filename.EndsWith("jpg") || filename.EndsWith("jpeg"))
{
contentType = "image/jpeg";
}
if (filename.EndsWith("svg"))
{
contentType = "image/svg+xml";
}
FileInfo imageFile = new(filename);
if (!imageFile.Exists)

View File

@@ -1,7 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY bin/Release/net9.0/publish/ ./
COPY bin/Release/net8.0/publish/ ./
COPY source/ ./source/
COPY appsettings.json .

View File

@@ -2,41 +2,60 @@
@attribute [StreamRendering]
<main class="container mx-auto flex flex-col min-h-screen">
<div class="grid grid-cols-3 mx-3">
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
<a href="/blog/">
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
<main class="container">
<div class="row d-none d-xl-flex" style="height: 80px">
<div class="px-2 col-9">
<a href="/blog/" class="p-2">
<h4>Ricardo's Blog</h4>
</a>
</div>
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
<div>
<a href="/blog/archives/">
<span class="text-xl text-blue-600">归档</span>
</a>
</div>
<div>
<a href="/blog/tags/">
<span class="text-xl text-blue-600">标签</span>
</a>
</div>
<div>
<a href="/about/" target="_blank">
<span class="text-xl text-blue-600">关于</span>
</a>
</div>
<div>
<a href="/friends/" target="_blank">
<span class="text-xl text-blue-600">友链</span>
</a>
</div>
</div>
<div class="col-3 d-flex justify-content-around align-items-center">
<a href="/blog/" class="p-2">
<h5>首页</h5>
</a>
<a href="/blog/archives/" class="p-2">
<h5>归档</h5>
</a>
<a href="/blog/tags/" class="p-2">
<h5>标签</h5>
</a>
<a href="/blog/about/" class="p-2">
<h5>关于</h5>
</a>
</div>
</div>
<div class="px-4 py-2 flex-grow">
<div class="row d-xl-none">
<div class="px-2 col-12">
<a href="/blog/" class="p-2">
<h4>Ricardo's Blog</h4>
</a>
</div>
<div class="px-2 col-12 justify-content-end d-flex">
<a href="/blog/" class="p-2">
<h5>首页</h5>
</a>
<a href="/blog/archives/" class="p-2">
<h5>归档</h5>
</a>
<a href="/blog/tags/" class="p-2">
<h5>标签</h5>
</a>
<a href="/blog/about/" class="p-2">
<h5>关于</h5>
</a>
</div>
</div>
<div class="row px-4 py-2">
@Body
</div>

View File

View File

@@ -1,34 +1,21 @@
@inherits LayoutComponentBase
<main class="container mx-auto min-h-screen flex flex-col">
<div class="grid grid-cols-4">
<div class="px-2 md:col-span-3 col-span-4 h-20 flex items-center">
<a href="/" class="text-2xl">
<h4 class="text-blue-600">Ricardo's Index</h4>
<main class="container">
<div class="row" style="height: 80px">
<div class="px-2 col-8">
<a href="/" class="p-2">
<h4>Ricardo's Index</h4>
</a>
</div>
<div class="md:col-span-1 col-span-4 h-20 flex items-center">
<div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
<Anchor
Address="/blog/"
Text="博客"
NewPage="@(true)"/>
<Anchor
Address="/about/"
Text="关于"
NewPage="@(true)"/>
<Anchor
Address="/friends"
Text="友链"
NewPage="@(true)"/>
</div>
<div class="col-4 d-flex justify-content-around align-items-center">
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank">
<h5>E-mail</h5>
</a>
</div>
</div>
<div class="px-4 mx-auto flex-grow">
<div class="row px-4 center">
<div class="py-2">
@Body
</div>

View File

@@ -0,0 +1,8 @@
.center {
margin: 0 auto;
max-width: 48em;
min-height: calc(100vh - 80px);
position: relative;
display: flex;
flex-direction: column;
}

View File

@@ -1,3 +0,0 @@
namespace YaeBlog.Models;
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);

View File

@@ -1,74 +1,141 @@
@page "/about"
@page "/blog/about"
@using YaeBlog.Core.Models
@inject BlogOptions Options
<PageTitle>
关于
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">关于</h1>
<div class="container">
<div class="row">
<div class="col">
<h1>关于</h1>
</div>
</div>
<div class="py-4">
<span class="italic">把字刻在石头上!(・’ω’・)</span>
<div class="row">
<div class="col fst-italic py-2">
把字刻在石头上!(・’ω’・)
</div>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于我</h3>
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>关于我</h3>
</div>
</div>
<div class="py-2">
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span class="italic">jackfiled</span>的名字活动
<span class="line-through">都是ICP备案过的人了网名似乎没有太大的用处</span>
<div class="row py-2">
<div class="col">
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态
一般在互联网上使用<span class="fst-italic">初冬的朝阳</span>或者<span class="fst-italic">jackfiled</span>的名字活动。
<span class="text-decoration-line-through">都是ICP备案过的人了网名似乎没有太大的用处</span>
</div>
</div>
<div class="py-2">
主要是一个C#程序员目前也在尝试写一点Rust。
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”
前后端分离的项目本当上手
常常因为现实的压力而写一些C/C++
<span class="line-through">对于Java和Go的评价很低。</span>
日常使用ArchLinux。
<div class="row py-2">
<div class="col">
主要是一个C#程序员目前也在尝试写一点Rust
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”
前后端分离的项目本当上手
常常因为现实的压力而写一些C/C++。
<span class="text-decoration-line-through">对于Java和Go的评价很低。</span>
日常使用ArchLinux。
</div>
</div>
<div class="py-2">
100%社恐。日常生活是宅在电脑前面自言自语。
兴趣活动是读书和看番,目前在玩原神和三角洲
<div class="row py-2">
<div class="col">
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
</div>
</div>
<div class="py-4">
常常被人批评没有梦想,这里就随便瞎编一下。
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>
完成第一个不是课程设计的个人开源项目
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
<span class="line-through">什么时候天上才能掉美少女?</span>
<div class="row py-2">
<div class="col">
常常被人批评没有梦想,这里就随便瞎编一下
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>
完成第一个不是课程设计的个人开源项目。
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
<span class="text-decoration-line-through">什么时候天上才能掉美少女?</span>
</div>
</div>
<div class="py-2">
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
也可以试试在各大平台搜索上面提到的名字
<div class="row py-2">
<div class="col">
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>
也可以试试在各大平台搜索上面提到的名字。
</div>
</div>
</div>
</div>
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于本站</h3>
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>关于本站</h3>
</div>
</div>
<div class="py-2">
本站肇始于2021年下半年在开始的两年中个人网站和博客是分别的两个网站个人网站是裸HTML写的博客是用
<a href="https://hexo.io">Hexo</a>渲染的。
<div class="row py-2">
<div class="col">
本站肇始于2021年下半年在开始的两年中个人网站和博客是分别的两个网站个人网站是裸HTML写的博客是用
<a href="https://hexo.io">Hexo</a>渲染的。
</div>
</div>
<div class="py-2">
2024年我们决定使用.NET技术完全重构两个网站合二为一。虽然目前这个版本还是一个半成品但是我们一定会努力的~(确信。
<div class="row py-2">
<div class="col">
2024年我们决定使用.NET技术完全重构两个网站合二为一。虽然目前这个版本还是一个半成品但是我们一定会努力的~(确信。
</div>
</div>
</div>
</div>
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>友链</h3>
</div>
</div>
<div class="py-2">
2025年我们将使用的样式库从Bootstrap迁移到Tailwind CSS将现代的前端技术同Blazor结合起来。
<div class="row py-2">
<div class="col fst-italic">
欢迎所有人联系我添加友链!(´。✪ω✪。`)
</div>
</div>
<div class="row py-2">
@foreach (FriendLink link in Options.Links)
{
<div class="col-sm-12 col-md-4 col-lg-3">
<a href="@(link.Link)" target="_blank" class="m-3">
<div class="row link-item">
<div class="col-4">
<Image Src="@(link.AvatarImage)" Alt="@(link.Name)" Style="border-radius: 50%"/>
</div>
<div class="col-8">
<div class="row">
<div class="col-auto fs-5">
@(link.Name)
</div>
</div>
<div class="row">
<div class="col-auto fst-italic">
@(link.Description)
</div>
</div>
</div>
</div>
</a>
</div>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.link-item {
padding: 1rem;
border-radius: 4px;
}
.link-item:hover {
background-color: var(--bs-secondary-bg);
}

View File

@@ -1,6 +1,6 @@
@page "/blog/archives"
@using YaeBlog.Abstraction
@using YaeBlog.Models
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@inject IEssayContentService Contents
@@ -8,56 +8,68 @@
归档
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">归档</h1>
</div>
<div class="py-4">
<span class="italic">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</span>
</div>
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
{
<div class="p-2">
<div class="flex flex-col">
<div>
<h3 class="text-xl">@(group.Key.Year)</h3>
<div class="container">
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h1>归档</h1>
</div>
</div>
<div class="px-4 py-4 flex flex-col">
@foreach (BlogEssay essay in group)
{
<a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
<div class="w-20">
@(essay.PublishTime.ToString("MM月dd日"))
<div class="row">
<div class="col fst-italic py-4">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</div>
</div>
</div>
</div>
</div>
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
{
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h3>@(group.Key.Year)</h3>
</div>
</div>
<div class="container px-3 py-2">
@foreach (KeyValuePair<string, BlogEssay> essay in group)
{
<div class="row py-1">
<div class="col-auto">
@(essay.Value.PublishTime.ToString("MM-dd"))
</div>
<div>
<span class="text-blue-600">
@(essay.Title)
</span>
<div class="col-auto">
<a href="/blog/essays/@(essay.Key)">
@(essay.Value.Title)
</a>
</div>
</div>
</a>
}
}
</div>
</div>
</div>
</div>
}
</div>
@code {
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
protected override void OnInitialized()
{
base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
orderby essay.Value.PublishTime descending
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
}
}

View File

View File

@@ -1,6 +1,6 @@
@page "/blog"
@using YaeBlog.Abstraction
@using YaeBlog.Models
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@inject IEssayContentService Contents
@inject NavigationManager NavigationInstance
@@ -9,18 +9,79 @@
Ricardo's Blog
</PageTitle>
<div>
<div class="grid grid-cols-4">
<div class="col-span-4 md:col-span-3">
@foreach (BlogEssay essay in _essays)
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-9">
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
{
<EssayCard Essay="@(essay)"/>
<EssayCard Essay="@(pair.Value)"/>
}
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
<div class="row align-items-center justify-content-center p-3">
@if (_page == 1)
{
<div class="col-auto fw-light">上一页</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">上一页</a>
</div>
}
@if (_page == 1)
{
<div class="col-auto">
1
</div>
<div class="col-auto">
<a href="/blog/?page=2">2</a>
</div>
<div class="col-auto">
<a href="/blog/?page=3">3</a>
</div>
}
else if (_page == _pageCount)
{
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
</div>
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
</div>
<div class="col-auto">
@(_pageCount)
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
</div>
<div class="col-auto">
@(_page)
</div>
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
</div>
}
@if (_page == _pageCount)
{
<div class="col-auto fw-light">
下一页
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">下一页</a>
</div>
}
</div>
</div>
<div class="col-span-4 md:col-span-1">
<div class="col-sm-12 col-md-3">
<BlogInformationCard/>
</div>
</div>
@@ -30,7 +91,7 @@
[SupplyParameterFromQuery] private int? Page { get; set; }
private readonly List<BlogEssay> _essays = [];
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
private const int EssaysPerPage = 8;
private int _pageCount = 1;
private int _page = 1;
@@ -38,15 +99,16 @@
protected override void OnInitialized()
{
_page = Page ?? 1;
_pageCount = Contents.Count / EssaysPerPage + 1;
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
{
NavigationInstance.NavigateTo("/NotFount");
return;
}
_essays.AddRange(Contents.Essays
.OrderByDescending(p => p.Value.PublishTime)
.Skip((_page - 1) * EssaysPerPage)
.Take(EssaysPerPage));
}

View File

@@ -0,0 +1,7 @@
.essay-title a {
color: var(--bs-body-color);
}
.read-more a {
color: var(--bs-body-color);
}

View File

@@ -1,7 +1,7 @@
@page "/blog/essays/{BlogKey}"
@using System.Text.Encodings.Web
@using YaeBlog.Abstraction
@using YaeBlog.Models
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@inject IEssayContentService Contents
@inject NavigationManager NavigationInstance
@@ -10,87 +10,96 @@
@(_essay!.Title)
</PageTitle>
<div class="flex flex-col py-8">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="container py-4">
<div class="row">
<div class="col-auto">
<h1 id="title">@(_essay!.Title)</h1>
</div>
</div>
<div class="px-6 pt-4 pb-2">
<div class="flex flex-row gap-4">
<div class="font-light">
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
<div class="row px-4 py-1">
<div class="col-auto fw-light">
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
</div>
@foreach (string tag in _essay!.Tags)
{
<div class="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
@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>
<div class="px-6 pt-2 pb-4">
<div class="font-light">
<div class="row px-4 py-1">
<div class="col-auto fw-light">
总字数:@(_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 class="row">
<div class="col-lg-8 col-md-12">
@((MarkupString)_essay!.HtmlContent)
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
<LicenseDisclaimer EssayAddress="@BlogKey"/>
</div>
<div class="col-span-3 md:col-span-1">
<div class="flex flex-col sticky top-0 px-8">
<div>
<h3 class="text-2xl">文章目录</h3>
</div>
<div>
@foreach (BlogHeadline level2 in _headline!.Children)
{
<div class="py-2 pl-3">
<Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
Text="@(level2.Title)"/>
</div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="py-2 pl-6">
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
Text="@(level3.Title)"/>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="py-2 pl-9">
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
Text="@(level4.Title)"/>
</div>
}
}
}
</div>
@if (_headline!.Children.Count == 0)
{
<div class="col-lg-4 col-md-12">
<div class="row sticky-lg-top justify-content-center">
<div class="col-auto">
<div class="row">
<div class="col fst-italic">
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
<div class="col-auto">
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
文章目录
</h3>
</div>
</div>
}
<div class="row" style="padding-left: 10px">
<div class="col-auto">
@foreach (BlogHeadline level2 in _headline!.Children)
{
<div class="row py-1">
<div class="col-auto">
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
</div>
</div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
@(level3.Title)
</a>
</div>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
@(level4.Title)
</a>
</div>
</div>
}
}
}
</div>
</div>
@if (_headline!.Children.Count == 0)
{
<div class="row">
<div class="col fst-italic">
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
</div>
</div>
}
</div>
</div>
</div>
</div>
@@ -114,7 +123,7 @@
return;
}
if (!Contents.TryGetEssay(BlogKey, out _essay))
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
{
NavigationInstance.NavigateTo("/NotFound");
}

View File

View File

@@ -1,49 +0,0 @@
@page "/friends"
@using YaeBlog.Models
@inject BlogOptions Options
<PageTitle>
友链
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">
友链
</h1>
</div>
<div class="py-4">
欢迎所有人联系我添加友链!(´。✪ω✪。`)
</div>
<div class="grid grid-cols-4 g-4 p-2">
@foreach (FriendLink link in Options.Links)
{
<div>
<a href="@(link.Link)" target="_blank" class="mx-5">
<div class="flex flex-row">
<div class="basis-1/3">
<img src="@(link.AvatarImage)" alt="@($"Avatar of {link.Name}")"
class="w-full h-auto rounded-full">
</div>
<div class="flex flex-col basis-2/3 px-2">
<div class="text-lg">
@(link.Name)
</div>
<div class="text-sm italic">
@(link.Description)
</div>
</div>
</div>
</a>
</div>
}
</div>
</div>
@code {
}

View File

@@ -4,28 +4,28 @@
Ricardo's Index
</PageTitle>
<div class="mx-20">
<div class="grid grid-cols-3 py-4">
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
<div class="container">
<div class="row py-4">
<div class="col-lg-4 col-12 p-5 p-lg-0">
<Image Src="images/avatar.png" Alt="Ricardo's Avatar"/>
</div>
<div class="col-span-3 md:col-span-2">
<div class="flex flex-col px-3 gap-y-3">
<div class="">
<div class="text-3xl font-bold">初冬的朝阳 (Ricardo Ren)</div>
<div class="col-lg-8 col-12">
<div class="container px-3">
<div class="row">
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
</div>
<div class="">
<p class="text-lg">a.k.a jackfiled</p>
<div class="row">
<p class="fs-5">a.k.a jackfiled</p>
</div>
<div class="">
<p class="text-lg italic">世界很大,时间很长。</p>
<div class="row">
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
</div>
<div class="">
<p class="text-lg">
<div class="row">
<p class="fs-5">
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
</p>
</div>
@@ -33,22 +33,20 @@
</div>
</div>
<div class="py-5">
<p class="text-lg">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
<div class="row" style="padding-top: 80px">
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
</div>
<div>
<p class="text-lg py-1">
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <Anchor Address="/blog/" Text="博客"/> 看看。
<div class="row">
<p class="fs-5">
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
<Anchor Address="https://jackfiled.github.io/wiki/" Text="我的学习笔记"/>
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
如果您想批判一下我的代码,在
<Anchor Address="https://github.com/jackfiled/" Text="Github"/> 和
<Anchor Address="https://git.rrricardo.top/jackfiled/" Text="Gitea"/>
都可以找到。
如果您想批判一下我的代码,在 <a href="https://github.com/jackfiled" target="_blank">Github</a> 和
<a href="https://git.rrricardo.top/jackfiled/" target="_blank">Gitea</a> 都可以找到。
</p>
<p class="text-lg py-1">
<p class="fs-5">
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
</p>
</div>

View File

View File

@@ -4,8 +4,8 @@
啊~ 页面走丢啦~
</PageTitle>
<div>
<h3 class="text-3xl">NotFound!</h3>
<div class="container">
<h3>NotFound!</h3>
</div>
@code {

View File

View File

@@ -1,7 +1,7 @@
@page "/blog/tags/"
@using System.Text.Encodings.Web
@using YaeBlog.Abstraction
@using YaeBlog.Models
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@inject IEssayContentService Contents
@inject NavigationManager NavigationInstance
@@ -10,22 +10,24 @@
@(TagName ?? "标签")
</PageTitle>
<div class="flex flex-col">
<div>
@if (TagName is null)
{
<h1 class="text-4xl">标签</h1>
}
else
{
<h2 class="text-2xl">@(TagName)</h2>
}
<div class="container">
<div class="row">
<div class="col">
@if (TagName is null)
{
<h1>标签</h1>
}
else
{
<h2>@(TagName)</h2>
}
</div>
</div>
<div class="py-4">
<span class="italic">
<div class="row">
<div class="col fst-italic py-4">
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
</span>
</div>
</div>
@if (TagName is null)
@@ -36,17 +38,19 @@
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
{
<li class="p-2">
<div class="flex flex-row">
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
<div class="text-sky-600 text-lg">
# @(pair.Key.TagName)
</div>
</a>
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
<div class="container fs-5">
<div class="row">
<div class="col-auto">
# @(pair.Key.TagName)
</div>
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
@(pair.Value.Count)
<div class="col-auto tag-count">
@(pair.Value.Count)
</div>
</div>
</div>
</div>
</a>
</li>
}
</ul>

View File

@@ -0,0 +1,6 @@
.tag-count {
background: var(--bs-secondary-bg);
border-radius: 5px;
padding: 0 6px;
}

View File

@@ -1,102 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
/// <summary>
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
/// </summary>
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
{
public string Name => nameof(EssayStylesPostRenderProcessor);
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document);
BeatifyTable(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-slate-300 rounded-sm overflow-x-auto" },
{ "h2", "text-3xl font-bold py-4" },
{ "h3", "text-2xl font-bold py-3" },
{ "h4", "text-xl font-bold py-2" },
{ "h5", "text-lg font-bold py-1" },
{ "p", "p-2" },
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
{ "ul", "list-disc pl-2" }
};
private void ApplyGlobalCssStyles(IDocument document)
{
foreach ((string tag, string style) in _globalCssStyles)
{
foreach (IElement element in document.GetElementsByTagName(tag))
{
element.ClassList.Add(style);
}
}
}
private static void BeatifyTable(IDocument document)
{
foreach (IElement element in from e in document.All
where e.LocalName == "table"
select e)
{
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
// thead元素
foreach (IElement headElement in from e in element.Children
where e.LocalName == "thead"
select e)
{
headElement.ClassList.Add("bg-slate-200");
// tr in thead
foreach (IElement trElement in from e in headElement.Children
where e.LocalName == "tr"
select e)
{
trElement.ClassList.Add("border border-slate-300");
// th in tr
foreach (IElement thElement in from e in trElement.Children
where e.LocalName == "th"
select e)
{
thElement.ClassList.Add("px-4 py-1");
}
}
}
// tbody元素
foreach (IElement bodyElement in from e in element.Children
where e.LocalName == "tbody"
select e)
{
// tr in tbody
foreach (IElement trElement in from e in bodyElement.Children
where e.LocalName == "tr"
select e)
{
foreach (IElement tdElement in from e in trElement.Children
where e.LocalName == "td"
select e)
{
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
}
}
}
}
}
}

View File

@@ -1,4 +1,11 @@
using System.CommandLine;
using YaeBlog.Commands;
YaeBlogCommand command = new();
await command.RunAsync(args);
RootCommand rootCommand = new("YaeBlog CLI");
rootCommand.AddServeCommand();
rootCommand.AddNewCommand();
rootCommand.AddListCommand();
rootCommand.AddWatchCommand();
await rootCommand.InvokeAsync(args);

View File

@@ -1,41 +0,0 @@
using YaeBlog.Abstraction;
namespace YaeBlog.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Hot reload is starting...");
logger.LogInformation("Change essays will lead to hot reload!");
logger.LogInformation("HINT: draft essays will be included.");
await rendererService.RenderAsync(true);
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task FileWatchTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
logger.LogInformation("Watching file changes...");
string? changeFile = await watcher.WaitForChange(token);
if (changeFile is null)
{
logger.LogInformation("File watcher is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear();
await rendererService.RenderAsync(true);
}
}
}

View File

@@ -1,33 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
</Target>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -6,5 +6,6 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorBootstrap
@using YaeBlog
@using YaeBlog.Components

View File

@@ -6,10 +6,6 @@
}
},
"AllowedHosts": "*",
"Tailwind": {
"InputFile": "wwwroot/input.css",
"OutputFile": "wwwroot/output.css"
},
"Blog": {
"Root": "source",
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
@@ -28,16 +24,10 @@
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
},
{
"Name": "不会写程序的晨旭",
"Name": "Chenxu",
"Description": "一个普通大学生",
"Link": "https://chenxutalk.top",
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
},
{
"Name": "万木长风",
"Description": "世界渲染中...",
"Link": "https://ryohai.fun",
"AvatarImage": "https://ryohai.fun/icon.jpg"
}
]
}

View File

@@ -1,12 +0,0 @@
{
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.4.16"
}
}

836
YaeBlog/pnpm-lock.yaml generated
View File

@@ -1,836 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
tailwindcss:
specifier: ^3.4.16
version: 3.4.16
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-core-module@2.16.0:
resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.0.1:
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resolve@1.22.9:
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
hasBin: true
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwindcss@3.4.16:
resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yaml@2.6.1:
resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
engines: {node: '>= 14'}
hasBin: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@pkgjs/parseargs@0.11.0':
optional: true
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@5.0.2: {}
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
camelcase-css@2.0.1: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
commander@4.1.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
cssesc@3.0.0: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.17.1:
dependencies:
reusify: 1.0.4
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-core-module@2.16.0:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
isexe@2.0.0: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jiti@1.21.6: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lru-cache@10.4.3: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
minipass@7.1.2: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.8: {}
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
picocolors@1.1.1: {}
picomatch@2.3.1: {}
pify@2.3.0: {}
pirates@4.0.6: {}
postcss-import@15.1.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.9
postcss-js@4.0.1(postcss@8.4.49):
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.49):
dependencies:
lilconfig: 3.1.3
yaml: 2.6.1
optionalDependencies:
postcss: 8.4.49
postcss-nested@6.2.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.4.49:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
queue-microtask@1.2.3: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
resolve@1.22.9:
dependencies:
is-core-module: 2.16.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.0.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
commander: 4.1.1
glob: 10.4.5
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.6
ts-interface-checker: 0.1.13
supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@3.4.16:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)
postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
resolve: 1.22.9
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-interface-checker@0.1.13: {}
util-deprecate@1.0.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
yaml@2.6.1: {}

View File

@@ -0,0 +1,6 @@
---
title: test-essay
date: 2024-08-22T22:31:34.3177253+08:00
tags:
---
<!--more-->

View File

@@ -1,12 +1,10 @@
---
title: LLVM入门笔记
date: 2024-08-25T17:19:45.6572088+08:00
tags:
- 编译原理
- LLVM
- 技术笔记
- 编译原理
- LLVM
- 技术笔记
---
为什么说LLVM是神
<!--more-->
@@ -158,13 +156,13 @@ main: # @main
LLVM中间语言是一个基于静态单赋值的类型安全的低级别中间表示形式。中间语言一般情况下有三种表示形式内存中的数据结构、便于JIT编译器解析执行的字节码形式和人类可读的文本形式。
> **良好定义Well formed** 的中间语言:中间语言可以是在语义上没有问题的,但是并不是良好定义的。例如:
> **良好定义Well formed**的中间语言:中间语言可以是在语义上没有问题的,但是并不是良好定义的。例如:
>
> ```
> %x = add i32 1, %x
> ```
>
> 这段IR在语法上没有任何问题但是变量`%x`的定义并不在所有的使用之前
> 这段IR在语法上没有任何问题但是并不是静态单赋值形式的
>
> LLVM提供了一个Pass在运行所有的优化之前验证输入的IR是否是良好定义的。
@@ -209,14 +207,6 @@ LLVM中的关键词同其他语言中的关键词也非常类似例如对于
### 高级别表示
这里使用LLVM在Rust中的高级别封装[inkwell](https://github.com/TheDan64/inkwell)示范如何使用LLVM IR编写一个简单的程序。在这个程序中涉及到LLVM几个重要的基础概念。
- `context`LLVM中的上下文。这个对象中保存了LLVM IR中的一些重要全局状态借助这个变量我们可以方便的将LLVM并行运行起来。
- `module`LLVM的模块一个编译的单元可以包含各种函数和全局变量。
- `type`LLVM中对于数据类型的抽象通过基础类型的各种组合可以构建出更复杂的类型例如函数和结构体。
- `function`LLVM中的函数函数需要通过函数类型和名称来定义函数类型需要通过输入参数类型和返回类型来定义。函数中可以通过附加上基本块来定义函数的实现。
- `basic_block`LLVM中的基本块组成控制流的基本单元中间包含从上到下依次执行的一系列指令序列。
```rust
fn main() -> Result<(), Box<dyn Error>> {
let context = Context::create();
@@ -245,28 +235,3 @@ LLVM中的关键词同其他语言中的关键词也非常类似例如对于
}
```
执行上面的Rust代码可以得到一段生成的LLVM IR代码
```llvm
; ModuleID = 'main'
source_filename = "main"
@str = private unnamed_addr constant [13 x i8] c"Hello, LLVM!\00", align 1
@format = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
declare i32 @puts(ptr)
declare i32 @printf(ptr, i32, ...)
define i32 @main() {
entry:
%0 = call i32 @puts(ptr @str)
%1 = call i32 (ptr, i32, ...) @printf(ptr @format, i32 3)
ret i32 0
}
```
使用`lli`解释器可以直接运行这段代码:
![image-20240825171858276](./llvm-naive-0/image-20240825171858276.png)

View File

@@ -1,89 +0,0 @@
---
title: 2024年年终总结
date: 2025-01-16T17:15:05.8634370+08:00
tags:
- 杂谈
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
所以年终总结再次跳票,红豆泥私密马赛!
<!--more-->
### 压力
本年度的第一个关键词,我会选择压力。这一年总是被不同的压力笼罩着,先是有形的压力,然后是无形的压力,在不同的时间阶段有着不同的来源。
1月份起始的两周就是大三学年秋季学期的期末考试周而鄙人在下不才我在本学期面临着计算机科学四幻神的考验——老师不知所云之操作系统、抽象概念无法理解之编译原理、全英语授课之数据库系统原理和智商不够无法战胜之算法导论。挣扎在保研线上的我刚刚被上一学期的离散数学的~~75分~~74分和数据结构的79分拷打面对着如此沉重的考试压力加起来一共12学分呢可耻的失眠了。
过完年回来的三月份就是同论文奋斗的一个月。虽然只是一篇6页的EI检索论文但对于一个**纯洁**的本科生来说还是有点太困难了。这个过程就像是你先拉了一坨大的,然后在上面细细的涂上巧克力,在最后发表的过程中,需要在众人的面前大嚼这一坨东西,并且称赞“真是一道美食啊”!还没有开始的学术生涯就已经留下永恒的污点力(悲)。
搞完论文的四月和五月则是和大作业搏斗的两个月。首先是无法战胜的“编译原理课程设计”内容是设计一个Pascal-S到C语言的源到源编译器。这一大作业的主要压力来源是大作业本身的难度直到最后提交的时候全部95个测试点也没有能够完全通过然而其他人在祖传代码上缝缝补补却过来哭。虽然考虑到我们是全手写的编译器没有使用任何的编译器构建工具提出的解决方案也称不上是墨守成规老师给了我一个还算是可以的分数算是压力中的小小慰藉。
然后是风波不断的软件工程大作业明明只是一个相对简单的Web前后端开发但是我们前后进行了三次验收才通过一直拖到了学期的第16周。老师设计的联合验收制度给我们结结实实的上了一课要求联合验收小组的不同前后端需要能够任意组合使用导致我们为了适配另外一组的逻辑几乎是把核心代码写了两遍。虽然我不喜欢在背后攻击别人但是我不得不说这一年中最有压力的时刻往往不是自己的事情搞不定时而是看着别人搞砸事情你却无能为力的时候。
这两个月还夹杂这一个意义不明的专业实习,明明是计算机科学与技术专业的牛马,为什么会被中兴通讯的老师培训通信项目的项目管理?
应付完上面这些杂七杂八的内容,便是本科生生涯中的最后三场考试:人称计算机领域的政治之《软件工程》,通信领域科普课程之《现代交换原理》和永远的神之《计算机系统结构》。
不得不说《软件工程》,~~或者人们常说的肖概~~确实不愧于计算机领域的政治之称。毕竟政治的主要课题就是研究如何组织和动员人群以完成一个特定的目标,《软件工程》不过是将人员限制为了软件的开发人员,领域限制为了软件开发领域,基本的道理还是相通的。
《现代交换原理》则是一门在现有的课程体系下非常尴尬的一门课程,显然这门课的保留还是为了凸显“计算机+通信”的学科特色,但是大量前置知识的缺失和同其他课程的脱节使得这门课就显得非常的“脱节”。而且相对来说,通信技术的发展速度远远不如互联网的迭代技术,这门课也被同学们戏称为“古代交换原理”。令人最难受的,虽然知识古代,但是却一点都不简单,很多内容只能说是听了个概念,幸好最后的考试不难,靠死记硬背通过了考试。
《计算机系统结构》就是核心课中的核心课了。课程内容和《计算机组成原理》衔接的非常紧密,~~虽然我组成原理就学的很垃圾~~主要围绕着如何最大限度的并行化运行程序进行从指令级的并行一直到多机并行可以说是压力最大的一门考试。在准备的过程中做了很多套往年题博客上也发布了一部分的复习笔记最终幸好低空飞过。唯一的吐槽是实验什么时候可以从MIPS改成为RISC-V呢。
三门课的考试一结束,这些死线明确的、有形的压力便消失了,但是无形的压力——对于是否能保研的焦虑——便笼罩下来。
7月和8月都是在这种不安和恐慌中度过这种氛围在9月份保研名单出炉之前达到了顶峰。保研的流程开始之后则是通知推着人走各种交材料各种准备答辩各种等待公示直到最后的保研名单出炉。
不过现在回想起来,最后名单出炉,获得保研资格,复试通过之后,并没有一种如释重负的感觉,或者说终于实现了既定目标的快感。反而是一种“啊,结束了”的空落感,只想回去睡一觉。
然后新的~~风暴~~压力已经出现,在度过一个短短的国庆假期之后便正式进组,作为一个研究生的社畜生涯就此开始。
### 经历
虽然2024年的第一个关键词已经选择为“压力”但是众所周知高压锅里往往能压出好吃的。人也是这样。所以我将2024年的第二个关键词定为“经历”人生如逆旅我亦是行人各式各样的经历便是风格迥异的景点。
人生第一篇学术论文的撰写和发表无疑是今年最难忘的经历。虽然我在前面称之为“学术生涯上的污点”,但是污点也好过一片空白不是,还非常的引人夺目。而且这是一个完整的撰写-发表流程,从开始的选题、实验、撰写、投稿,到最后的接受、提交、发表、报销等等数个环节我均参加。这个过程不仅让我对于学术论文的诞生流程有力较为清晰的认识,也对学校的各种发表和报销流程有了深入的了解。
两个大作业编译原理课程设计和软件工程大作业也是非常难忘的经历。这两个项目的代码都已经整理好开源在Github上了。前者代表了目前我软件开发的最高水平而后者则是我本科阶段唯一一个差点失败的软件开发项目。这种冰火两重天的对比实在是很难令人忘记。
这两个项目中的收获有非常技术性的。相较于2023年面对各种大作业时的略显底气不足这次我在各种技术栈的选择上更加游刃有余选择了完全倒向.NET和React摈弃了之前的Java和Vue。各类现代软件开发技术也得到了充分的应用例如由Gitea Actions驱动的DevOps实践完全基于合并请求的多人协作流程。事实证明这些协作流程确实在一定程度上加速了项目的开发。
但是,“软件工程里没有银弹”,先进技术的堆叠并不能保证软件项目成功。虽然我这里~~自吹自擂~~有非常多新技术的帮助,软件工程大作业的差点失败的确说明了软件工程实际上还是人的工程,猪队友永远比凶恶的敌人更可怕。当然也不能将所有的锅都扔给别人,我在项目失控的过程中也没有能够采取有力的措施挽救整个项目,~~负有不可推卸的领导责任~~。
今年最后一个难忘的经历便是去横店镇参加CNCC 2024也单独出过[博客](https://rrricardo.top/blog/essays/cncc-2024)。虽然之前学术论文发表的过程中也是在学术会议上做过口头报告,不过是线上参加的,并没有特别的实感。现在线下参加,也不需要自己上去发表,顿感旅游真好玩,~~也有可能是因为CNCC比较水~~。
### 匆匆
2024年的第三个关键词我想定为”匆匆“虽然想找一个更加”有文化“的词汇奈何自己的文化造纸实在不够故定为”匆匆“。
可2024年确实是非常忙碌的一年现在回想起来几乎每一个月都是在为了某一件特定的事情而奔走着。还记得在新年伊始的时间里我还制订了各种各样的读书计划和补番计划现在看来定计划的目的不是为了实现而是为了安心。
不过匆匆之中还是读了几本书。首先是久负盛名的《置身事内——中国政府与经济发展》,这本书的开篇即言:“这本书是写给大学生和对经济话题感兴趣的读者”,细读下来也确实如此。然后是一本我从小便着迷的二战军史相关话题《美国陷阱:橙色计划始末》,其中若干的政治与军事细节之于我不过是走马观花,不过其中表达出的长期战略实在令人敬佩。
至于补番计划我则是表现出了同电子ED一样的症状对于新番没有兴趣对于补早就下载安装好的老番更是兴趣缺缺。反倒是电视剧由于12月韩国的惊天一变我又重新下载了《第五共和国》
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.png)
### 未来
> 定计划的目的不是为了实现,而是为了安心。
站在年关已经可以预见到2025年将会是更为繁忙的一年从一月份到十月份都已经有了或多或少的安排现在无法多言只能希望都能有良好的结果。
还是多说点可以说的罢。
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目一整本上总结下来也能有超过50本其中不乏超过一千页的大部头说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍
- 陆风,《光变:一个企业及其工业史》
- 吴军,《浪潮之巅》
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。

Binary file not shown.

View File

@@ -1,5 +1,5 @@
---
title: 日用Linux挑战 第5篇 标准安装流程
title: 日用Linux挑战第五篇 ArchLinux标准安装流程
date: 2024-7-16 20:08:37
tags:
- Linux

View File

@@ -1,407 +0,0 @@
---
title: 在ASP.NET Core中集成认证和授权流程
date: 2024-09-08T22:27:17.0328669+08:00
tags:
- ASP.NET Core
- 技术笔记
---
以[Martina](https://github.com/post-guard/Martina)为例记录如何典型的ASP.NET Core应用中集成认证和授权的流程。
<!--more-->
## 业务需求概述
[Martina](https://github.com/post-guard/Martina)系统是一个酒店的空调和入住管理系统,项目中对于认证和授权的要求是一个典型的多权限、多用户模式,具体来说:
- 系统中所有的接口均需要在登录之后才能调用;
- 系统中安装不同管理领域将用户的权限划分为一大类、三小类:一个超级管理员权限和客房、空调、账单三个领域管理员权限;
- 普通用户的权限有时间和使用房间的要求:只能在入住时间段内访问入住房间的空调相关接口。
可以看出上述这些要求基本上覆盖了一个常见系统的中所有关于认证和授权的使用场景因此本篇便以该系统为例介绍如何在ASP.NET Core框架中实现上述业务要求。
## 身份认证和授权的基础知识
身份认证是指由用户提供凭据,然后将其与存储在操作系统、数据库、应用和资源中的凭据进行比较的过程。而授权过程发生在身份认证成功之后:在凭据匹配成功之后,用户身份验证成功,可执行已向其授权的操作。授权就是判断允许用户执行操作的过程。
在ASPNET.Core中这是通过两个**中间件**`UseAuthenication``UseAuthorization`来完成的,还是来看这张经典的中间件工作流程:
![ASP.NET Core 中间件管道](./aspnet-authorization/middleware-pipeline.svg)
可以看到在中间件的管道中认证中间价将在授权中间件运行之前运行——这两个顺序是不能颠倒的如果授权中间件在认证中间件运行之前运行那授权中间件就无法为用户授予任何权限所有需要权限的接口均会返回401错误码。
> 为什么我知道的如此清楚捏?
>
> 因为我真的写反过最后还是在框架代码里面打断点才发现授权中间件拿不到用户登录的信息当时还在GitHub的工单里面翻找相关的bug感觉可以评选为人生十大傻逼bug之一。
概览完认证和授权之后,首先来谈谈认证。认证的基本过程就是一个开锁的过程:用户提供一个凭据,也就是钥匙,系统验证凭据的有效性,就是锁的工作。这里主要的问题就是这个钥匙的形状长什么样子,也就是凭据的表现形式。常见的凭据表现形式有`Cookies``JWT`两种。
`Cookies`是一种服务器发送到用户浏览器并保存在本地上的一小块文本文件,用户浏览器在保存这些文本文件之后会在每次向同一服务器发送请求时在请求体中携带一些文本文件信息。`Cookies`是一种非常古老的技术这种技术使得无状态的HTTP协议可以记录稳定的状态信息因此在这个技术常被应用来认证网络用户的身份。
`JWT`的全称是JSON Web Token是一种使用JSON对象表示格式在两方之前安全且有效的传输信息的方法使用该方法的信息可以使用指定的密钥或者是公钥-私钥对验证信息的有效性。因此`JWT`作为一种通用的、可验证的令牌格式用来完成网络中认证的过程。在服务器验证某一个用户的身份之后(例如通过验证账号密码、通过第三方的验证)可以签发一个`JWT`令牌给用户浏览器,浏览器可以使用`localstorage`等技术将该令牌存储在用户浏览器中并在每次向服务器发送请求的过程中将该令牌携带在一个特定的请求头`Authorization`中。
> 在`Authorization`请求头中常常会以`Bearer <JWT>`的格式进行,这其中的`Bearer`是指定的身份认证的模式Scheme这里的详细解释可以见[MDN文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)。
谈完认证之后,再来看看授权。授权的实现是一个和业务逻辑高度相关的过程,一个常见的业务逻辑是用户分为不同的层级——例如普通用户和管理员,而不同层级的用户可以调用的接口不同,这就是**基于策略的授权模式**的典型应用场景,该模式允许为每个接口指定一个或者多个认证策略。另外一个常见的业务逻辑是用户只能访问自己所拥有的资源——例如用户只能删除自己创建的记录,这就是**基于资源的授权模式**的典型应用场景,该模式允许为一种资源编写一段授权逻辑,并通过依赖注入的方式供服务器或者控制器使用。
## 身份认证和授权的实践
在本个系统中,身份认证将采用`JWT`令牌而授权的部分将会覆盖到上文中提到的两种典型模式通过研究本系统的实现可以理解在ASP.NET Core中集成身份认证和授权的流程。
在ASP.NET Core系统中集成`JWT`令牌的认证方式需要先安装一个包`Microsoft.AspNetCore.Authentication.JwtBearer`
### 身份认证部分
身份认证部分主要分为令牌签发和令牌验证两个部分,令牌认证的部分主要在于使用`AddAuthentication`向主机容器中注入服务,而令牌签发的部分则通常是实现一个接口,在验证用户输入的账号和密码之后生成该用户对于的令牌。这两个过程是高度关联的,在签发过程中设置的令牌信息需要在验证令牌的过程设置对应的部分,否则签发的令牌就无法验证。因此先介绍签发令牌的部分。
签发令牌之前先介绍一下`JWT`令牌的组成,一个兼容的`JWT`令牌一般有三个部分组成:
- 头部`Header`:头部在一般情况下只有两个字段组成,一个`tpy`字段存储固定值为`JWT`指定这是一个`JWT`令牌,一个`alg`字段指定验证该令牌的算法是`HMCA SHA256`还是`RSA`
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
- 负载`Payload`:包含各种关于实体(用户)的宣称列表。宣称可以分成三种类型,已注册的类型、公开的类型和私有的类型,这三种的类型的区别可以从[RFC7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1)中具体查看,简而言之就是已注册的类型就是推荐在签发令牌时设置的,包括签发者和到期时间等的内容,公开的类型是公开注册可以共享的名称,而私有的就是自行指定的。
```json
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
```
- 签名`signature`:验证令牌的签名部分,在使用`HMCA SHA256`算法的情况下,签名的计算公示如下所示:
```
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
```
在学习了这些`JWT`的基础知识之后就可以很容易的写出如下的令牌生成代码:
```csharp
public string GenerateJsonWebToken(User user)
{
List<Claim> claims =
[
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.NameIdentifier, user.UserId)
];
JwtSecurityToken token = new(
issuer: _option.Issuer,
audience: user.UserId,
notBefore: DateTime.Now,
expires: DateTime.Now.AddDays(7),
claims: claims,
signingCredentials: _signingCredentials
);
return _jwtSecurityTokenHandler.WriteToken(token);
}
```
签发令牌的凭据使用下面的方式创建:
```csharp
private readonly SigningCredentials _signingCredentials =
new(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.Value.JsonWebTokenKey)),
SecurityAlgorithms.HmacSha256);
```
签发的过程中部分重要的参数使用配置的方式提供,例如签发者和密钥,配置实体类如下所示:
```csharp
public class JsonWebTokenOption
{
public const string OptionName = "JWT";
/// <summary>
/// JWT令牌的签发者
/// </summary>
public required string Issuer { get; set; }
/// <summary>
/// JWT令牌的签发密钥
/// </summary>
public required string JsonWebTokenKey { get; set; }
}
```
签发好令牌之后就可以编写验证令牌的部分了:
```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
options =>
{
JsonWebTokenOption? jsonWebTokenOption = builder.Configuration.GetSection(JsonWebTokenOption.OptionName)
.Get<JsonWebTokenOption>();
if (jsonWebTokenOption is null)
{
throw new InvalidOperationException("Failed to get JWT options");
}
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jsonWebTokenOption.Issuer,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.JsonWebTokenKey)),
ValidAlgorithms = [SecurityAlgorithms.HmacSha256]
};
});
```
在验证令牌的部分,指定验证令牌的签发者和签名。
编写完上述代码之后就可以增加身份验证和授权的中间件验证上述代码的正确性了。
```csharp
application.UseAuthentication();
application.UseAuthorization();
```
### 授权的部分
#### 按照策略进行授权
系统中一个典型的场景就是不同级别的用户能访问的接口不同,例如在本系统中用户的级别分为:
```csharp
[Flags]
public enum Roles
{
User = 0b_0000_0000,
RoomAdministrator = 0b_0000_0001,
AirConditionerAdministrator = 0b_0000_0010,
BillAdministrator = 0b_0000_0100,
Administrator = 0b_0000_1000
}
```
为了方便给不同的接口指定不同的访问策略首先创建一个对用户级别的要求Requirement
```csharp
public class HotelRoleRequirement(Roles hotelRole) : IAuthorizationRequirement
{
public Roles HotelRole { get; } = hotelRole;
}
```
然后实现一个处理该要求的验证程序:
```csharp
public class HotelRoleHandler(MartinaDbContext dbContext) : AuthorizationHandler<HotelRoleRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
HotelRoleRequirement requirement)
{
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
if (userId is null)
{
return;
}
User? user = await dbContext.Users
.Include(u => u.Permission)
.Where(u => u.UserId == userId.Value)
.FirstOrDefaultAsync();
if (user is null)
{
return;
}
// 如果要求的权限是超级管理员
// 则判断是否是超级管理员
if ((requirement.HotelRole & Roles.Administrator) == Roles.Administrator)
{
if (user.Permission.IsAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
// 剩下的权限
// 如果用户是超级管理员则直接有权限
if (user.Permission.IsAdministrator)
{
context.Succeed(requirement);
return;
}
if ((requirement.HotelRole & Roles.BillAdministrator) == Roles.BillAdministrator)
{
if (user.Permission.BillAdminstrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
if ((requirement.HotelRole & Roles.RoomAdministrator) == Roles.RoomAdministrator)
{
if (user.Permission.RoomAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
if ((requirement.HotelRole & Roles.AirConditionerAdministrator) == Roles.AirConditionerAdministrator)
{
if (user.Permission.AirConditionorAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
}
```
框架要求在处理程序使用依赖注入到主机的容器中,这里因为在验证的过程中使用了数据库的服务`DbContext`因此被注册为一个范围内Scope服务。
```csharp
builder.Services.AddScoped<IAuthorizationHandler, HotelRoleHandler>();
```
为了方便在`[Authorize]`注解中使用字符串指定不同的授权策略,在`AddAuthoriztion`进行配置:
```csharp
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", policy =>
{
policy.AddRequirements(new HotelRoleRequirement(Roles.Administrator));
});
options.AddPolicy("RoomAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.RoomAdministrator)));
options.AddPolicy("AirConditionerAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.AirConditionerAdministrator)));
options.AddPolicy("BillAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.BillAdministrator)));
});
```
使用该方法注册之后就可以直接在`[Authorize]`注解中指定需要使用的授权策略:
```csharp
[HttpGet("revenue")]
[Authorize(policy: "BillAdministrator")]
[ProducesResponseType<ExceptionMessage>(400)]
[ProducesResponseType<RevenueTrend>(200)]
public async Task<IActionResult> QueryRevenueTrend([FromQuery] DateTimeOffset begin, [FromQuery] DateTimeOffset end)
{
if (begin >= end)
{
return BadRequest(new ExceptionMessage("开始时间不能晚于结束时间"));
}
RevenueTrend trend = new()
{
TotalUsers = await managerService.QueryCurrentUser(),
TotalCheckin = await managerService.QueryCurrentCheckin(),
DailyRevenues = await managerService.QueryDailyRevenue(begin, end)
};
return Ok(trend);
}
```
#### 按照资源进行授权
系统中一个典型的需求就是一个用户只能修改资源池中部分自己拥有权限的资源,在本系统中就是用户只能开启和关闭当前入住房间中的空调。
按照资源进行授权的总体流程和安装策略进行授权总体上差别不大,除了无法在注解中设置需要使用的策略。首先仍然是设计一个授权的要求:
```csharp
public class CheckinRequirement : IAuthorizationRequirement;
```
然后为该要求实现一个授权处理程序,注意在这里集成泛型基类`AuthorizationHandler`时除了需要指定要求类还需要指定资源类型:
```csharp
public class CheckinHandler(
RoomService roomService,
MartinaDbContext dbContext)
: AuthorizationHandler<CheckinRequirement, Room>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
CheckinRequirement requirement,
Room resource)
{
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
if (userId is null)
{
return;
}
User? user = await dbContext.Users.AsNoTracking()
.Where(u => u.UserId == userId.Value)
.FirstOrDefaultAsync();
if (user is { Permission.IsAdministrator: true } || user is { Permission.AirConditionorAdministrator: true })
{
context.Succeed(requirement);
return;
}
CheckinRecord? record = await roomService.QueryUserCurrentStatus(userId.Value);
if (record?.RoomId == resource.Id)
{
context.Succeed(requirement);
}
}
}
```
在使用该授权方法时,通过依赖注入获得一个`IAuthorizationService`的接口对象并调用对应的授权接口进行验证,传入需要访问的资源和当前`HttpContext`中的用户`User`,这个`User`实际上就是`JWT`令牌中的负载部分。
```csharp
AuthorizationResult result = await authorizationService.AuthorizeAsync(User, room, [new CheckinRequirement()]);
if (!result.Succeeded)
{
return Forbid();
}
if (!airConditionerManageService.VolidateAirConditionerRequest(roomObjectId, request, out string? message))
{
return BadRequest(new ExceptionMessage(message));
}
```
## 总结
通过清晰的定义身份认证和授权两个环节并提供了一个要求——处理程序的授权模型ASP.NET Core提供了一套简单易用、扩展性高的接口安全系统。

View File

@@ -1,324 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 706.35 390">
<defs>
<symbol id="New_Symbol_125" data-name="New Symbol 125" viewBox="0 0 32 16">
<path d="M31,8c0,3.85-3.6,7-8,7H9c-4.4,0-8-3.15-8-7S4.6,1,9,1H23C27.4,1,31,4.15,31,8Z" fill="#fff"/>
<path d="M23,16H9c-5,0-9-3.59-9-8S4,0,9,0H23c5,0,9,3.59,9,8S28,16,23,16ZM9,2C5.14,2,2,4.69,2,8s3.14,6,7,6H23c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#0072c6"/>
</symbol>
</defs>
<g id="Shapes">
<rect width="706.35" height="390" fill="#fff"/>
<g>
<rect x="376.35" y="287.58" width="215" height="85" fill="#fff"/>
<rect x="376.35" y="287.58" width="215" height="85" fill="#3c3c41" opacity="0.05"/>
<rect x="376.35" y="287.58" width="215" height="85" fill="none" stroke="#3c3c41" stroke-miterlimit="10" stroke-width="0.25"/>
</g>
<use width="32" height="16" transform="translate(73.58 22.58)" xlink:href="#New_Symbol_125"/>
<g>
<path d="M47.15,70.41c0,3.85-3.6,7-8,7h-14c-4.4,0-8-3.15-8-7s3.6-7,8-7h14C43.55,63.41,47.15,66.56,47.15,70.41Z" fill="#fff"/>
<path d="M39.15,78.41h-14c-5,0-9-3.59-9-8s4-8,9-8h14c5,0,9,3.59,9,8S44.11,78.41,39.15,78.41Zm-14-14c-3.86,0-7,2.69-7,6s3.14,6,7,6h14c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#76bc2d"/>
</g>
<g>
<rect x="79.78" y="57.58" width="110" height="25" fill="#fff"/>
<rect x="79.78" y="57.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="89.78" y1="39.15" x2="89.78" y2="52.2" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="86.04 51.1 89.78 57.58 93.52 51.1 86.04 51.1" fill="#005ba1"/>
</g>
<g>
<rect x="104.78" y="97.58" width="110" height="25" fill="#fff"/>
<rect x="104.78" y="97.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="114.78" y1="82.89" x2="114.78" y2="91.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="111.04 90.85 114.78 97.33 118.52 90.85 111.04 90.85" fill="#005ba1"/>
</g>
<g>
<line x1="79.78" y1="70.41" x2="50.91" y2="70.41" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M57.06,66.09a.44.44,0,0,1-.13.62l-5.81,3.7,5.81,3.7a.44.44,0,0,1,.13.62.45.45,0,0,1-.62.14L50,70.79a.44.44,0,0,1-.21-.38A.47.47,0,0,1,50,70L56.44,66a.55.55,0,0,1,.24-.07A.45.45,0,0,1,57.06,66.09Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="129.78" y="137.27" width="110" height="25" fill="#fff"/>
<rect x="129.78" y="137.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="139.78" y1="122.58" x2="139.78" y2="131.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="136.04 130.53 139.78 137.01 143.52 130.53 136.04 130.53" fill="#005ba1"/>
</g>
<g>
<path d="M129.78,148.93h-10a5,5,0,0,1-5-5V123.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M119.09,129.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,119.09,129.83Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M104.35,108.93h-10a5,5,0,0,1-5-5V83.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M93.67,89.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.7,5.8a.45.45,0,1,1-.76-.48L89,82.81a.45.45,0,0,1,.76,0l4.08,6.4a.42.42,0,0,1,.07.24A.45.45,0,0,1,93.67,89.83Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="154.78" y="177.58" width="110" height="25" fill="#fff"/>
<rect x="154.78" y="177.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="164.78" y1="162.89" x2="164.78" y2="171.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="161.04 170.85 164.78 177.32 168.52 170.85 161.04 170.85" fill="#005ba1"/>
</g>
<g>
<path d="M154.78,189.24h-10a5,5,0,0,1-5-5V164" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M144.09,170.15a.45.45,0,0,1-.62-.14l-3.7-5.81L136.08,170a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="179.78" y="217.27" width="110" height="25" fill="#fff"/>
<rect x="179.78" y="217.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="189.78" y1="202.58" x2="189.78" y2="211.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="186.04 210.53 189.78 217.01 193.52 210.53 186.04 210.53" fill="#005ba1"/>
</g>
<g>
<path d="M179.78,228.93h-10a5,5,0,0,1-5-5V203.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M169.09,209.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,169.09,209.83Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="204.78" y="257.58" width="110" height="25" fill="#fff"/>
<rect x="204.78" y="257.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="214.78" y1="242.89" x2="214.78" y2="251.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="211.04 250.85 214.78 257.32 218.52 250.85 211.04 250.85" fill="#005ba1"/>
</g>
<g>
<path d="M204.78,269.24h-10a5,5,0,0,1-5-5V244" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M194.09,250.15a.45.45,0,0,1-.62-.14l-3.7-5.81L186.08,250a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="229.78" y="297.89" width="110" height="25" fill="#fff"/>
<rect x="229.78" y="297.89" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="239.78" y1="283.21" x2="239.78" y2="292.26" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="236.04 291.16 239.78 297.64 243.52 291.16 236.04 291.16" fill="#005ba1"/>
</g>
<g>
<path d="M229.78,309.55h-10a5,5,0,0,1-5-5V284.31" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M219.09,290.46a.45.45,0,0,1-.62-.14l-3.7-5.81-3.69,5.81a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.44.44,0,0,1,219.09,290.46Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="254.78" y="337.58" width="110" height="25" fill="#fff"/>
<rect x="254.78" y="337.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="264.78" y1="322.89" x2="264.78" y2="331.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="261.04 330.85 264.78 337.32 268.52 330.85 261.04 330.85" fill="#005ba1"/>
</g>
<g>
<line x1="476.92" y1="349.24" x2="485.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="484.87 352.98 491.35 349.24 484.87 345.5 484.87 352.98" fill="#005ba1"/>
</g>
<g>
<line x1="364.78" y1="349.24" x2="380.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="379.87 352.98 386.35 349.24 379.87 345.5 379.87 352.98" fill="#005ba1"/>
</g>
<g>
<line x1="581.35" y1="349.24" x2="600.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="599.87 352.98 606.35 349.24 599.87 345.5 599.87 352.98" fill="#005ba1"/>
</g>
<g>
<path d="M254.78,349.24h-10a5,5,0,0,1-5-5V324" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M244.09,330.15a.45.45,0,0,1-.62-.14l-3.7-5.81L236.08,330a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M432.92,362.58v15a5,5,0,0,1-5,5H269.78a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M269.1,368.48a.45.45,0,0,1-.63-.13l-3.69-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.44.44,0,0,1,.38-.21.47.47,0,0,1,.38.21l4.07,6.4a.46.46,0,0,1-.13.62Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M656.35,362.08v15.5a5,5,0,0,1-5,5h-110a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M540.67,368.48a.44.44,0,0,1-.62-.13l-3.7-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.45.45,0,0,1,.76,0l4.08,6.4a.44.44,0,0,1,.07.24A.45.45,0,0,1,540.67,368.48Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M536.35,337.58v-10a5,5,0,0,0-5-5H437.92a5,5,0,0,0-5,5v8.92" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M428.6,330.35a.45.45,0,0,1,.63.13l3.69,5.81,3.7-5.81a.44.44,0,0,1,.62-.13.45.45,0,0,1,.14.62l-4.08,6.4a.44.44,0,0,1-.38.21.47.47,0,0,1-.38-.21l-4.07-6.4a.46.46,0,0,1,.13-.62Z" fill="#4f4f4f"/>
</g>
</g>
<g id="Text">
<g>
<path d="M75.51,16.47H74.34l-1.41-2.35a5,5,0,0,0-.37-.56,2.27,2.27,0,0,0-.38-.38,1.07,1.07,0,0,0-.41-.21,1.49,1.49,0,0,0-.49-.07h-.81v3.57h-1V8.07H72a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,1.84,1.84,0,0,1,.53.7,2.28,2.28,0,0,1,.2,1,2.53,2.53,0,0,1-.13.8,2.15,2.15,0,0,1-.38.66,2.37,2.37,0,0,1-.59.49,2.82,2.82,0,0,1-.77.31v0a2.13,2.13,0,0,1,.37.22,2.56,2.56,0,0,1,.3.28c.09.11.18.24.27.37s.2.3.31.49ZM70.47,9v3h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.83,1.83,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05A1.88,1.88,0,0,0,71.82,9Z" fill="#1e1e1e"/>
<path d="M81,13.71H76.77a2.19,2.19,0,0,0,.54,1.55,1.86,1.86,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.33,3.33,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.56,2.56,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76A3.19,3.19,0,0,1,81,13.21Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M87.58,19.23h-1V15.44h0a2.14,2.14,0,0,1-2,1.17,2.25,2.25,0,0,1-1.81-.8,3.27,3.27,0,0,1-.68-2.2,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.89,1.89,0,0,1,1.78,1h0v-.84h1Zm-1-5.46V12.9a1.73,1.73,0,0,0-.49-1.25,1.61,1.61,0,0,0-1.22-.51,1.66,1.66,0,0,0-1.37.64A2.85,2.85,0,0,0,83,13.59a2.48,2.48,0,0,0,.49,1.63,1.55,1.55,0,0,0,1.25.59,1.73,1.73,0,0,0,1.35-.58A2.17,2.17,0,0,0,86.62,13.77Z" fill="#1e1e1e"/>
<path d="M94.38,16.47h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55V10.47h.95v3.44c0,1.26.48,1.9,1.45,1.9A1.49,1.49,0,0,0,93,15.29a2,2,0,0,0,.45-1.36V10.47h1Z" fill="#1e1e1e"/>
<path d="M101.14,13.71H96.9a2.28,2.28,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.5,3.5,0,0,1-2.09.57,2.56,2.56,0,0,1-2-.81,3.37,3.37,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.58,2.58,0,0,1,2-.88,2.27,2.27,0,0,1,1.82.76,3.2,3.2,0,0,1,.65,2.12Zm-1-.81a1.91,1.91,0,0,0-.4-1.3,1.39,1.39,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M102.23,16.26v-1a2.88,2.88,0,0,0,1.73.58c.84,0,1.26-.29,1.26-.85a.7.7,0,0,0-.11-.41,1,1,0,0,0-.29-.29,2.18,2.18,0,0,0-.43-.23l-.54-.22a5.16,5.16,0,0,1-.7-.32,1.74,1.74,0,0,1-.5-.36,1.29,1.29,0,0,1-.31-.46,1.57,1.57,0,0,1-.1-.6,1.44,1.44,0,0,1,.19-.75,1.58,1.58,0,0,1,.52-.54,2.45,2.45,0,0,1,.73-.34,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.78,2.78,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.06,1.06,0,0,0-.37.18.66.66,0,0,0-.24.26.7.7,0,0,0-.09.35.8.8,0,0,0,.09.39.82.82,0,0,0,.25.28,1.67,1.67,0,0,0,.4.22l.53.22a6.85,6.85,0,0,1,.71.31,2.44,2.44,0,0,1,.54.37,1.41,1.41,0,0,1,.35.46,1.54,1.54,0,0,1,.12.63,1.5,1.5,0,0,1-.2.77,1.74,1.74,0,0,1-.53.55,2.43,2.43,0,0,1-.75.32,3.76,3.76,0,0,1-.9.1A3.49,3.49,0,0,1,102.23,16.26Z" fill="#1e1e1e"/>
<path d="M110.47,16.41a1.82,1.82,0,0,1-.9.19c-1,0-1.58-.58-1.58-1.76V11.29h-1v-.82h1V9l1-.31v1.77h1.52v.82H109v3.38a1.41,1.41,0,0,0,.21.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M14.05,55.66H12.88L11.47,53.3a4.53,4.53,0,0,0-.37-.56,2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,1.89,1.89,0,0,0-.5-.07H9v3.58H8v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7A2.61,2.61,0,0,1,13,50.3a2,2,0,0,1-.38.65,2.13,2.13,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48ZM9,48.15v3h1.34a1.87,1.87,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.32,1.32,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.35,1.35,0,0,0-.44-1.06,1.88,1.88,0,0,0-1.26-.37Z" fill="#1e1e1e"/>
<path d="M19.55,52.9H15.31a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.91,2.91,0,0,0,1.86-.67v.9A3.43,3.43,0,0,1,17,55.8,2.53,2.53,0,0,1,15,55a3.35,3.35,0,0,1-.72-2.3,3.28,3.28,0,0,1,.79-2.28,2.54,2.54,0,0,1,2-.88,2.24,2.24,0,0,1,1.82.76,3.17,3.17,0,0,1,.65,2.11Zm-1-.82a1.9,1.9,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.46,1.56,1.56,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M20.64,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L22.26,53c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.78.78,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6,6,0,0,1,.71.32,2,2,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.68,1.68,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,20.64,55.44Z" fill="#1e1e1e"/>
<path d="M27.08,54.79h0v3.63h-1V49.66h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,27.08,54.79Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.86,2.49,2.49,0,0,0-.46-1.57,1.56,1.56,0,0,0-1.26-.56,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,27.05,52.37Z" fill="#1e1e1e"/>
<path d="M35.66,55.8A2.79,2.79,0,0,1,33.53,55a3.12,3.12,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.29,3.29,0,0,1,.75,2.29,3.21,3.21,0,0,1-.81,2.3A2.82,2.82,0,0,1,35.66,55.8Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.46,2.46,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.77,1.77,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.74,1.74,0,0,0,35.73,50.33Z" fill="#1e1e1e"/>
<path d="M45.16,55.66h-1V52.24c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.18,2.18,0,0,1,2-1.13,1.85,1.85,0,0,1,1.51.63A2.84,2.84,0,0,1,45.16,52Z" fill="#1e1e1e"/>
<path d="M46.61,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L48.23,53a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.62.62,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6.81,6.81,0,0,1,.71.32,2.42,2.42,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.37,3.37,0,0,1,46.61,55.44Z" fill="#1e1e1e"/>
<path d="M56.88,52.9H52.65a2.21,2.21,0,0,0,.54,1.55A1.85,1.85,0,0,0,54.6,55a2.94,2.94,0,0,0,1.87-.67v.9a3.45,3.45,0,0,1-2.09.58,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.17,3.17,0,0,1,.64,2.11Zm-1-.82a2,2,0,0,0-.4-1.29,1.37,1.37,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M93.79,74.58H89.34v-8.4H93.6v.89H90.32v2.79h3v.89h-3v2.94h3.47Z" fill="#1e1e1e"/>
<path d="M99.66,68.58l-2,3,2,3H98.51l-1.18-2c-.08-.12-.16-.27-.27-.45h0l-.27.45-1.21,2h-1.1l2-2.94-2-3.06h1.11l1.16,2c.09.15.17.31.26.47h0l1.5-2.52Z" fill="#1e1e1e"/>
<path d="M104.88,74.3a3.14,3.14,0,0,1-1.65.42,2.72,2.72,0,0,1-2.07-.84,3,3,0,0,1-.78-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.26-.9,3.27,3.27,0,0,1,1.4.29v1a2.4,2.4,0,0,0-1.43-.47,1.92,1.92,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.55,1.66,1.91,1.91,0,0,0,1.49.61,2.43,2.43,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M111.15,71.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48,2.24,2.24,0,0,0-.58,1.28Z" fill="#1e1e1e"/>
<path d="M113.59,73.71h0v3.63h-1V68.58h1v1h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,113.59,73.71Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.45,2.45,0,0,0-.46-1.57,1.54,1.54,0,0,0-1.26-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,113.56,71.29Z" fill="#1e1e1e"/>
<path d="M122.44,74.52a1.76,1.76,0,0,1-.89.19c-1.06,0-1.58-.59-1.58-1.76V69.4h-1v-.82h1V67.11l1-.31v1.78h1.51v.82h-1.51v3.38a1.37,1.37,0,0,0,.21.86.8.8,0,0,0,.67.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M124.22,67.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.64.64,0,0,1,.45.18.6.6,0,0,1,.18.44.59.59,0,0,1-.18.44A.61.61,0,0,1,124.22,67.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M129.14,74.72a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.68,2.68,0,0,1,2.09.82,3.76,3.76,0,0,1-.05,4.6A2.88,2.88,0,0,1,129.14,74.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.63,2.63,0,0,0-.53,1.73,2.47,2.47,0,0,0,.54,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,129.21,69.25Z" fill="#1e1e1e"/>
<path d="M138.64,74.58h-1V71.16c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.84,1.84,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M146.9,74.58h-1V70.75h-4.34v3.83h-1v-8.4h1v3.68h4.34V66.18h1Z" fill="#1e1e1e"/>
<path d="M153.27,74.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a2.94,2.94,0,0,0-2,.74V69a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.9.9,0,0,0,.32.71,1.16,1.16,0,0,0,.83.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M160.06,74.58h-1V71.16c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M167,74.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V65.7h1Zm-1-2.71V71a1.73,1.73,0,0,0-.48-1.23,1.78,1.78,0,0,0-2.6.14,2.81,2.81,0,0,0-.51,1.78,2.51,2.51,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6,1.64,1.64,0,0,0,1.3-.58A2.17,2.17,0,0,0,166,71.87Z" fill="#1e1e1e"/>
<path d="M169.9,74.58h-1V65.7h1Z" fill="#1e1e1e"/>
<path d="M176.66,71.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M181.25,69.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M153.53,113.58h-1v-3.83H148.2v3.83h-1v-8.4h1v3.68h4.35v-3.68h1Z" fill="#1e1e1e"/>
<path d="M155.34,113.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.39,3.39,0,0,0,.61.15,3.09,3.09,0,0,0,.58.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2,2,0,0,1-.35-2.44,2.1,2.1,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,155.34,113.24Z" fill="#1e1e1e"/>
<path d="M167.08,106.07h-2.43v7.51h-1v-7.51h-2.42v-.89h5.83Z" fill="#1e1e1e"/>
<path d="M167.77,113.24v-1.16a2.23,2.23,0,0,0,.48.32,4.51,4.51,0,0,0,1.2.38,3.09,3.09,0,0,0,.58.06,2.27,2.27,0,0,0,1.35-.34,1.15,1.15,0,0,0,.45-1,1.1,1.1,0,0,0-.15-.59,1.76,1.76,0,0,0-.41-.46,4.43,4.43,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.2,3.2,0,0,1-.66-.51,2.1,2.1,0,0,1-.45-.62,2.07,2.07,0,0,1-.16-.82,2,2,0,0,1,.25-1,2.13,2.13,0,0,1,.67-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.42,1.42,0,0,0-.41.4,1.07,1.07,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.31,1.31,0,0,0,.36.43,3.31,3.31,0,0,0,.57.37c.22.12.48.26.78.4s.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.42,2.42,0,0,1,.48.67,1.84,1.84,0,0,1,.18.83,2.14,2.14,0,0,1-.24,1.05,1.93,1.93,0,0,1-.66.7,2.8,2.8,0,0,1-1,.39,5.24,5.24,0,0,1-1.14.12l-.49,0c-.19,0-.39-.06-.6-.1a4.74,4.74,0,0,1-.57-.15A2,2,0,0,1,167.77,113.24Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M147.86,153.26h-1v-3.83h-4.35v3.83h-1v-8.4h1v3.69h4.35v-3.69h1Z" fill="#1e1e1e"/>
<path d="M152.72,153.21a1.87,1.87,0,0,1-.89.18c-1.06,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.37,1.37,0,0,0,.21.86.81.81,0,0,0,.68.26,1,1,0,0,0,.62-.2Z" fill="#1e1e1e"/>
<path d="M156.79,153.21a2,2,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M159.06,152.4h0V156h-1v-8.76h1v1.06h0a2.26,2.26,0,0,1,2.07-1.2,2.19,2.19,0,0,1,1.81.81,3.37,3.37,0,0,1,.65,2.16,3.71,3.71,0,0,1-.73,2.41,2.46,2.46,0,0,1-2,.91A2,2,0,0,1,159.06,152.4Zm0-2.42v.84a1.77,1.77,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3.09,3.09,0,0,0,.5-1.86,2.41,2.41,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.71,1.71,0,0,0-1.35.59A2.11,2.11,0,0,0,159,150Z" fill="#1e1e1e"/>
<path d="M164.76,153.05v-1a2.9,2.9,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.85a.72.72,0,0,0-.11-.4,1,1,0,0,0-.29-.3,2.27,2.27,0,0,0-.44-.23l-.53-.22a6.87,6.87,0,0,1-.7-.31,2,2,0,0,1-.51-.37,1.41,1.41,0,0,1-.3-.46,1.57,1.57,0,0,1-.11-.6,1.45,1.45,0,0,1,.2-.75,1.65,1.65,0,0,1,.51-.54,2.26,2.26,0,0,1,.74-.33,3,3,0,0,1,.85-.12,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.44,1.75,1.75,0,0,0-.48.06,1.34,1.34,0,0,0-.38.18.85.85,0,0,0-.24.26.82.82,0,0,0-.08.35.74.74,0,0,0,.33.67,1.89,1.89,0,0,0,.4.22l.53.22c.27.1.51.21.72.31a2.44,2.44,0,0,1,.54.37,1.38,1.38,0,0,1,.34.46,1.54,1.54,0,0,1,.12.63,1.47,1.47,0,0,1-.19.77,1.85,1.85,0,0,1-.53.55,2.54,2.54,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,164.76,153.05Z" fill="#1e1e1e"/>
<path d="M176.37,153.26H175.2l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.5a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2.22,2.22,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,2,2,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38c.09.13.19.3.3.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M181.87,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.24,2.24,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M188.44,153.26h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.32,3.32,0,0,1-.68-2.19,3.6,3.6,0,0,1,.75-2.39,2.48,2.48,0,0,1,2-.9,1.94,1.94,0,0,1,1.8,1h0v-3.72h1Zm-1-2.71v-.88a1.7,1.7,0,0,0-.48-1.23,1.61,1.61,0,0,0-1.22-.51,1.64,1.64,0,0,0-1.38.65,2.79,2.79,0,0,0-.51,1.78,2.56,2.56,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6A1.66,1.66,0,0,0,187,152,2.2,2.2,0,0,0,187.48,150.55Z" fill="#1e1e1e"/>
<path d="M190.88,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,190.88,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M196.42,148.24a1.18,1.18,0,0,0-.73-.2,1.22,1.22,0,0,0-1,.58,2.68,2.68,0,0,0-.42,1.59v3h-1v-6h1v1.24h0a2.08,2.08,0,0,1,.62-1,1.49,1.49,0,0,1,.95-.35,1.65,1.65,0,0,1,.57.08Z" fill="#1e1e1e"/>
<path d="M202.29,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.26,2.26,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M207.83,153a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.33,3.33,0,0,1,.85-2.39,3,3,0,0,1,2.27-.9,3.09,3.09,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.39,2.39,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.45,2.45,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M212.07,153.21a2,2,0,0,1-.9.18c-1,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.52v.83h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M213.84,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.62.62,0,0,1,.63.63.6.6,0,0,1-.18.44A.61.61,0,0,1,213.84,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M218.76,153.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,218.76,153.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,218.83,147.93Z" fill="#1e1e1e"/>
<path d="M228.27,153.26h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.88,2.88,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M183,193.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.29,2.29,0,0,0,1.36-.34,1.14,1.14,0,0,0,.44-1,1.2,1.2,0,0,0-.14-.59,1.64,1.64,0,0,0-.42-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4,4,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,1.83,1.83,0,0,0-.57.22,1.31,1.31,0,0,0-.41.4,1,1,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.41,1.41,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.59.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.49,0c-.2,0-.4-.06-.6-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,183,193.24Z" fill="#1e1e1e"/>
<path d="M192,193.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78H192v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M197.6,193.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.92.92,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M202.19,193.52a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M204,186.06a.59.59,0,0,1-.44-.18.61.61,0,0,1-.18-.45.6.6,0,0,1,.18-.44.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,204,186.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M210.47,193.3a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.23,3.23,0,0,1,1.39.29v1a2.38,2.38,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.56,1.66,1.89,1.89,0,0,0,1.48.61,2.39,2.39,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M219.6,186.07h-3.28V189h3v.89h-3v3.72h-1v-8.4h4.26Z" fill="#1e1e1e"/>
<path d="M221.56,186.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M224.94,193.58h-1V184.7h1Z" fill="#1e1e1e"/>
<path d="M231.7,190.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M232.79,193.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.55,2.55,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.59,3.59,0,0,1,2.25.16v1a2.71,2.71,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.15,1.15,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.8.8,0,0,0,.24.28,1.91,1.91,0,0,0,.4.23l.54.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.5,1.5,0,0,1-.19.78,1.61,1.61,0,0,1-.53.54,2.3,2.3,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,232.79,193.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M221.16,233.26H220l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.51a3.62,3.62,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2,2,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,1.58,1.58,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38l.31.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.43,1.43,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M224.34,233.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,224.34,233.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74A2.46,2.46,0,0,0,223,232a1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,224.41,227.93Z" fill="#1e1e1e"/>
<path d="M233.72,233.26h-1v-.94h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.86-2.14-2.56v-3.59h.95v3.44c0,1.26.49,1.9,1.46,1.9a1.48,1.48,0,0,0,1.15-.52,2,2,0,0,0,.46-1.36v-3.46h1Z" fill="#1e1e1e"/>
<path d="M238.44,233.21a1.92,1.92,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V225.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M240.22,225.74a.62.62,0,0,1-.44-.17.65.65,0,0,1,0-.9.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,240.22,225.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M247.61,233.26h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.52,1.52,0,0,0-1.2.54,2.05,2.05,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.82,2.82,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M254.54,232.78q0,3.32-3.16,3.31a4.31,4.31,0,0,1-1.95-.42v-1a4.06,4.06,0,0,0,1.94.56c1.47,0,2.21-.79,2.21-2.36v-.65h0a2.43,2.43,0,0,1-3.87.35,3.24,3.24,0,0,1-.68-2.15,3.73,3.73,0,0,1,.74-2.43,2.44,2.44,0,0,1,2-.91,2,2,0,0,1,1.8,1h0v-.84h1Zm-1-2.23v-.88a1.73,1.73,0,0,0-.48-1.23,1.6,1.6,0,0,0-1.21-.51,1.68,1.68,0,0,0-1.39.65,2.86,2.86,0,0,0-.5,1.81,2.51,2.51,0,0,0,.48,1.61,1.56,1.56,0,0,0,1.28.6,1.63,1.63,0,0,0,1.31-.58A2.12,2.12,0,0,0,253.58,230.55Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M251.74,273.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M256.52,273.72a3.66,3.66,0,0,1-2.86-1.18,4.33,4.33,0,0,1-1.08-3.06,4.66,4.66,0,0,1,1.1-3.24,3.83,3.83,0,0,1,3-1.2,3.6,3.6,0,0,1,2.8,1.17,4.37,4.37,0,0,1,1.07,3.06,4.66,4.66,0,0,1-1.09,3.25A3.76,3.76,0,0,1,256.52,273.72Zm.07-7.79a2.73,2.73,0,0,0-2.15.95,4.24,4.24,0,0,0,0,5,2.61,2.61,0,0,0,2.1.95,2.77,2.77,0,0,0,2.18-.91,3.68,3.68,0,0,0,.79-2.52,3.81,3.81,0,0,0-.77-2.57A2.63,2.63,0,0,0,256.59,265.93Z" fill="#1e1e1e"/>
<path d="M268.2,273.58H267l-1.41-2.36c-.13-.21-.25-.4-.37-.56a2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,2,2,0,0,0-.5-.06h-.81v3.57h-1v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7,2.33,2.33,0,0,1,.19,1,2.28,2.28,0,0,1-.13.8,2,2,0,0,1-.38.65,2,2,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48Zm-5-7.51v3h1.34a1.9,1.9,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.28,1.28,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.32,1.32,0,0,0-.44-1.05,1.83,1.83,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M269,273.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,269,273.24Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M253.45,313.89h-1.09l-.89-2.35H247.9l-.84,2.35H246l3.22-8.4h1Zm-2.31-3.24-1.32-3.58a3.36,3.36,0,0,1-.12-.56h0a3.13,3.13,0,0,1-.13.56l-1.31,3.58Z" fill="#1e1e1e"/>
<path d="M259.4,313.89h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1v3.44c0,1.26.48,1.9,1.45,1.9a1.49,1.49,0,0,0,1.16-.52,2,2,0,0,0,.45-1.36v-3.46h1Z" fill="#1e1e1e"/>
<path d="M264.13,313.83a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.2.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M270.39,313.89h-1v-3.45c0-1.25-.46-1.88-1.39-1.88a1.53,1.53,0,0,0-1.19.54,2,2,0,0,0-.48,1.39v3.4h-1V305h1v3.88h0a2.17,2.17,0,0,1,2-1.14c1.36,0,2,.82,2,2.45Z" fill="#1e1e1e"/>
<path d="M277,311.13h-4.23a2.19,2.19,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.41.55,3,3,0,0,0,1.87-.67v.9a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.34,3.34,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.55,2.55,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76,3.19,3.19,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M283.46,313.89h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.51.64,2.86,2.86,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M288.05,313.83a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.58-1.57-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.49,1.49,0,0,0,.2.87.83.83,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M289.83,306.37a.59.59,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,289.83,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M296.33,313.62a3.1,3.1,0,0,1-1.64.41,2.68,2.68,0,0,1-2.07-.83,3,3,0,0,1-.79-2.17,3.32,3.32,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.06,3.06,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.41,2.41,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.4,2.4,0,0,0,1.48-.53Z" fill="#1e1e1e"/>
<path d="M302.08,313.89h-1V313h0a2,2,0,0,1-1.84,1.07,1.93,1.93,0,0,1-1.4-.47,1.65,1.65,0,0,1-.51-1.26c0-1.12.66-1.77,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a3,3,0,0,0-2,.74v-1a3.68,3.68,0,0,1,2-.57c1.41,0,2.12.75,2.12,2.24Zm-1-3-1.45.2a2.35,2.35,0,0,0-1,.33,1,1,0,0,0-.34.84.93.93,0,0,0,.32.72,1.22,1.22,0,0,0,.83.28,1.52,1.52,0,0,0,1.18-.51,1.77,1.77,0,0,0,.47-1.26Z" fill="#1e1e1e"/>
<path d="M306.67,313.83a1.76,1.76,0,0,1-.89.19c-1,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.21.87.81.81,0,0,0,.68.25.94.94,0,0,0,.62-.2Z" fill="#1e1e1e"/>
<path d="M308.45,306.37a.6.6,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,308.45,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M313.37,314a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.26,3.26,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.09.83,3.75,3.75,0,0,1-.05,4.59A2.83,2.83,0,0,1,313.37,314Zm.07-5.47a1.8,1.8,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.85,1.85,0,0,0,1.46.62,1.76,1.76,0,0,0,1.43-.61,2.58,2.58,0,0,0,.5-1.72,2.62,2.62,0,0,0-.5-1.73A1.73,1.73,0,0,0,313.44,308.56Z" fill="#1e1e1e"/>
<path d="M322.87,313.89h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.82,1.82,0,0,1,1.5.64,2.8,2.8,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M281.47,353.58h-1.09l-.89-2.36h-3.56l-.84,2.36H274l3.23-8.4h1Zm-2.3-3.24-1.32-3.58a3.65,3.65,0,0,1-.13-.56h0a3.49,3.49,0,0,1-.14.56l-1.31,3.58Z" fill="#1e1e1e"/>
<path d="M287.43,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h.95V351c0,1.27.49,1.9,1.46,1.9a1.45,1.45,0,0,0,1.15-.52,1.94,1.94,0,0,0,.46-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M292.15,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M298.41,353.58h-1v-3.46c0-1.25-.46-1.87-1.39-1.87a1.51,1.51,0,0,0-1.18.54,2,2,0,0,0-.48,1.39v3.4h-1V344.7h1v3.88h0a2.18,2.18,0,0,1,2-1.14c1.35,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M302.73,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,302.73,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.79,1.79,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.76,1.76,0,0,0,302.8,348.25Z" fill="#1e1e1e"/>
<path d="M310.39,348.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.64,2.64,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
<path d="M311.92,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M318.52,347.85,315,352.76h3.51v.82h-4.92v-.3l3.55-4.88h-3.22v-.82h4.63Z" fill="#1e1e1e"/>
<path d="M324.06,353.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.65,1.65,0,0,1-.51-1.26c0-1.13.67-1.78,2-2l1.79-.25q0-1.53-1.23-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.44.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.56,1.56,0,0,0,1.18-.5,1.79,1.79,0,0,0,.46-1.27Z" fill="#1e1e1e"/>
<path d="M328.65,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M330.42,346.06a.62.62,0,0,1-.44-.18.61.61,0,0,1-.18-.45A.6.6,0,0,1,330,345a.62.62,0,0,1,.44-.18.61.61,0,0,1,.45.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,330.42,346.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M335.35,353.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,335.35,353.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,335.42,348.25Z" fill="#1e1e1e"/>
<path d="M344.85,353.58h-1v-3.42c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M414.56,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M421,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h1V351c0,1.27.49,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52A2,2,0,0,0,420,351v-3.46h1Z" fill="#1e1e1e"/>
<path d="M422.57,353.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.29,2.29,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.41,3.41,0,0,1,.85-.11,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.33,1.33,0,0,0-.38.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.91,1.91,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.42,1.42,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.41,3.41,0,0,1,422.57,353.36Z" fill="#1e1e1e"/>
<path d="M430.81,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H429.3v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M434.6,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,434.6,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.17,3.17,0,0,0,0-3.45A1.73,1.73,0,0,0,434.67,348.25Z" fill="#1e1e1e"/>
<path d="M447.65,353.58h-1v-3.45a2.66,2.66,0,0,0-.3-1.44,1.19,1.19,0,0,0-1-.44,1.25,1.25,0,0,0-1,.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.76,1.76,0,0,1,1.08.34,1.73,1.73,0,0,1,.62.9,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M452.73,353.58h-1v-7.26a1.74,1.74,0,0,1-.33.25,5.06,5.06,0,0,1-.48.29,5.85,5.85,0,0,1-.56.26,5,5,0,0,1-.58.2v-1a5.46,5.46,0,0,0,.68-.23c.23-.1.46-.22.69-.34a5.88,5.88,0,0,0,.65-.39,4.87,4.87,0,0,0,.53-.39h.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M517.25,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.5,4.5,0,0,1,1.21-3.26,4.12,4.12,0,0,1,3.08-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M523.69,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1V351c0,1.27.48,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M525.26,353.36v-1a2.82,2.82,0,0,0,1.73.58c.84,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.49-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.35,1.35,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.68,2.68,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,525.26,353.36Z" fill="#1e1e1e"/>
<path d="M533.5,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H532v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M537.29,353.72a2.77,2.77,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.71,2.71,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,537.29,353.72Zm.07-5.47a1.81,1.81,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,537.36,348.25Z" fill="#1e1e1e"/>
<path d="M550.33,353.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.16,1.16,0,0,0-1-.44,1.28,1.28,0,0,0-1,.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.42,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M555.77,353.71a.63.63,0,0,1-.46-.2.6.6,0,0,1-.19-.46.62.62,0,0,1,.19-.46.63.63,0,0,1,.46-.2.65.65,0,0,1,.47.2.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.65.65,0,0,1,555.77,353.71Z" fill="#1e1e1e"/>
<path d="M558.37,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.64.64,0,0,1,.93,0,.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.63.63,0,0,1,558.37,353.71Z" fill="#1e1e1e"/>
<path d="M561,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.67.67,0,0,1,1.13.46.61.61,0,0,1-.2.46A.63.63,0,0,1,561,353.71Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M434.72,307.23a4.92,4.92,0,0,1-2.32.49,3.72,3.72,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.45,4.45,0,0,1,1.21-3.26,4.11,4.11,0,0,1,3.08-1.24,5,5,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M441.15,307.58h-1v-.95h0a2,2,0,0,1-1.86,1.09c-1.42,0-2.14-.85-2.14-2.55v-3.59h1V305c0,1.27.48,1.9,1.45,1.9a1.47,1.47,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M442.73,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23l-.54-.21a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.37,1.37,0,0,1,.47,1.09,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,442.73,307.36Z" fill="#1e1e1e"/>
<path d="M451,307.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.59-1.58-1.76V302.4h-1v-.82h1v-1.47l1-.31v1.78H451v.82h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M454.76,307.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,454.76,307.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,454.83,302.25Z" fill="#1e1e1e"/>
<path d="M467.8,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.46-1.77-1.37-1.77a1.24,1.24,0,0,0-1,.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.87-1.09,1.71,1.71,0,0,1,1.07.34,1.81,1.81,0,0,1,.63.9,2.13,2.13,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M481.42,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.45-1.77-1.36-1.77a1.27,1.27,0,0,0-1.05.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M483.73,300.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M491.27,307.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.7,1.7,0,0,0-.49-1.23,1.61,1.61,0,0,0-1.21-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.59,1.59,0,0,0,1.3.6,1.61,1.61,0,0,0,1.3-.58A2.13,2.13,0,0,0,490.31,304.87Z" fill="#1e1e1e"/>
<path d="M498.33,307.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.73,1.73,0,0,0-.48-1.23,1.64,1.64,0,0,0-1.22-.5,1.66,1.66,0,0,0-1.38.64,2.81,2.81,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.57,1.57,0,0,0,1.3.6,1.63,1.63,0,0,0,1.3-.58A2.17,2.17,0,0,0,497.37,304.87Z" fill="#1e1e1e"/>
<path d="M501.24,307.58h-1V298.7h1Z" fill="#1e1e1e"/>
<path d="M508,304.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.5,3.5,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.36,3.36,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.28,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.65,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M517,301.58l-1.8,6h-1l-1.23-4.3a2.84,2.84,0,0,1-.1-.55h0a2.44,2.44,0,0,1-.12.54l-1.35,4.31h-1l-1.81-6h1l1.24,4.51a3.12,3.12,0,0,1,.08.54H511a2.29,2.29,0,0,1,.1-.55l1.38-4.5h.88l1.25,4.52a5.29,5.29,0,0,1,.08.54h0a2.61,2.61,0,0,1,.1-.54l1.22-4.52Z" fill="#1e1e1e"/>
<path d="M522.43,307.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.64,1.64,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M527.37,302.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
<path d="M533.23,304.82H529a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48A2.24,2.24,0,0,0,529,304Z" fill="#1e1e1e"/>
<path d="M534.32,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.1-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.22,1.22,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.67,1.67,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.57,1.57,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,534.32,307.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M638,353.58h-4.46v-8.4h4.27v.89h-3.28v2.79h3v.89h-3v2.94H638Z" fill="#1e1e1e"/>
<path d="M644.48,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M651.42,353.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.67-2.19,3.57,3.57,0,0,1,.75-2.38,2.46,2.46,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V344.7h1Zm-1-2.71V350a1.73,1.73,0,0,0-.48-1.23,1.65,1.65,0,0,0-1.22-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.73,1.73,0,0,0,2.6,0A2.13,2.13,0,0,0,650.46,350.87Z" fill="#1e1e1e"/>
<path d="M654.35,352.71h0v3.63h-1v-8.76h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.21,2.21,0,0,1,1.81.8,3.37,3.37,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,654.35,352.71Zm0-2.42v.84a1.73,1.73,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.39,2.39,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,654.32,350.29Z" fill="#1e1e1e"/>
<path d="M662.92,353.72a2.79,2.79,0,0,1-2.12-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,662.92,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.84,1.84,0,0,0,1.45.61,1.77,1.77,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.75,1.75,0,0,0,663,348.25Z" fill="#1e1e1e"/>
<path d="M667.94,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M675.33,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M679.93,353.52a1.82,1.82,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.38,1.38,0,0,0,.2.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -44,7 +44,7 @@ date: 2022-07-27 11:34:49
而且采用 `Git`还有一个好处,采用 `Github``Insight`功能可以轻松的看出大家的贡献值()。
![img](1.png)
![img](1.png "贡献")
## 一些技术上的收获

View File

@@ -1,232 +0,0 @@
---
title: 交叉编译.NET到RISC-V平台
date: 2024-08-25T15:41:05.9519941+08:00
tags:
- dotnet
- 技术笔记
---
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
<!--more-->
这次我们打算在`x86_64`平台上交叉编译`.NET``riscv64`平台上。
首先从相关的[进度跟踪页面](https://github.com/dotnet/runtime/issues/84834)显示,.NET移植到RISC-V的进度还远远没有完成但是在整个SDK中除了AOT编译器的部分都可以在RISC-V平台上编译了。
## 环境准备
我们构建的环境是Arch Linux因此依赖包的安装使用`pacman`进行。综合[.NET官方文档](https://github.com/dotnet/runtime/blob/main/docs/workflow/requirements/linux-requirements.md)给出的信息和Arch Linux官方打包的脚本所需要安装的软件包如下
| 包名 | 备注 |
| ------------- | ------------------------------------------------------------ |
| bash | |
| clang | |
| lld | |
| cmake | |
| git | |
| icu | 第一次看见这个名词就想吐槽谁TM想得到重症监护室会是一个全球化支持库 |
| inetutils | 常见的网络工具库,官方文档没有但是构建脚本有 |
| krb5 | 一个网络通信认证库?不懂 |
| libgit2 | |
| libunwind | 解析程序运行堆栈的魔法工具 |
| libxml2 | |
| lldb | |
| llvm | |
| lttng-ust2.12 | 又是一个跟踪运行的魔法工具 |
| openssl | |
| systemd | |
| zlib | |
### 交叉编译工具链
在正式开始编译.NET之前先学习如何搭建一套C/C++的交叉编译工具链。
通常一份GNU工具链只能针对一个平台进行编译但是LLVM工具链是一套先天的交叉编译工具链例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.png)
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
但是直接使用`clang++ --target=riscv64-linux-gnu hello.cpp -o hello`时会爆出一个奇怪的找不到头文件错误:
```cpp
// File: hello.cpp
#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
return 0;
}
```
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.png)
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译因此尝试在本地自行编译一套LLVM工具链。
首先从[Github Release](https://github.com/llvm/llvm-project/releases)上下载最新的`llvm-project`源代码并解压到本地文件夹中。这里126M的压缩文件可以解压出一个1.8G大小的源代码文件夹。创建一个`build`文件夹,在该文件夹使用如下的配置进行编译,在配置中使用`LLVM_TARGETS_TO_BUILD`选择启用`X86``RISCV`的支持。
```bash
cmake ../llvm-project.src/llvm \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra"
make
sudo make install
```
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前因此调用时将会优先调用我们自行编译的LLVM工具链而不是系统中安装的LLVM工具链。
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.png)
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装LLVM工具链的问题。仔细一想也确实这里提示找不到对应的头文件应该是找不到RISC-V架构之下的头文件——这里的也是交叉编译的主要问题所在虽然LLVM工具链宣称自己是原生支持交叉编译的但是没人宣称说标准库和头文件是原生的。这里我们就需要一个根文件系统来提供这些头文件和各种库文件。
### 生成根文件系统
在.NET的构建文档中提供了一个自动生成头文件的脚本但是这个脚本似乎强依赖某个U开头的发行版身为Arch神教信徒的我似乎没有办法使用。直接使用预构建好的镜像又屏蔽了太多的技术细节感觉也不太好。因此打算尝试使用[arch-riscv](https://mirror.iscas.ac.cn/archriscv/)提供的移植Arch Linux系统作为根文件系统。
首先使用移植之后的根文件系统构建一个`archriscv`镜像:
```Dockerfile
FROM archriscv AS bootstrap
COPY etc /rootfs
COPY bootstrap/pacstrap-docker /usr/local/bin/
RUN pacstrap-docker /rootfs base
RUN rm /rootfs/var/lib/pacman/sync/*
FROM scratch AS root
COPY --from=bootstrap /rootfs /
COPY etc /etc
LABEL org.opencontainers.image.title="Arch Linux RISC-V"
LABEL org.opencontainers.image.description="This is an Arch Linux port to the RISC-V architecture."
ENV LANG=en_US.UTF-8
RUN ldconfig && locale-gen
RUN pacman-key --init && \
pacman-key --populate && \
bash -c "rm -rf etc/pacman.d/gnupg/{openpgp-revocs.d/,private-keys-v1.d/,pubring.gpg~,gnupg.S.}*"
CMD ["/usr/bin/bash"]
```
虽然这个镜像是一个自举的镜像,给出这个构建文件似乎没有什么用处(笑)。再在这个镜像的基础上新建一层镜像安装各种.NET的依赖项。
```dockerfile
FROM archriscv
RUN pacman -Syyu --noconfirm bash clang cmake git icu inetutils \
krb5 libgit2 libunwind libxml2 lldb llvm lttng-ust2.12 \
openssl systemd zlib
```
构建这个镜像,再将这个镜像根目录下的所有文件拷贝出来。
```bash
docker build . --platform linux/riscv64 -t archriscv:base-devel
mkdir rootfs
cid=$(docker run -d --platform linux/riscv64 archriscv:base-devel)
sudo docker cp $cid:/ rootfs
sudo chown $USER:$USER -R rootfs
```
新建一个`runtime-build`文件夹,使用下面的指令在`rootfs`文件系统中构建`libcxx``compiler-rt`
> `libcxx`和`compiler-rt`不是常规交叉编译需要的,而是编译.NET所需要的。
```bash
export TARGET_TRIPLE="riscv64-linux-gnu"
export CLANG_MAJOR_VERSION=18
export ROOTFS_DIR=<ROOTFS>
cmake -S ../llvm-project.src/runtimes \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_ASM_COMPILER=clang \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_ASM_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_C_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_CXX_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_SYSROOT="$ROOTFS_DIR" \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" \
-DLLVM_USE_LINKER=lld \
-DLLVM_ENABLE_RUNTIMES="libcxx;compiler-rt" \
-DLIBCXX_ENABLE_SHARED=OFF \
-DLIBCXX_CXX_ABI=libstdc++ \
-DLIBCXX_CXX_ABI_INCLUDE_PATHS="$ROOTFS_DIR/usr/include/c++/14.2.1/;$ROOTFS_DIR/usr/include/c++/14.2.1/riscv64-unknown-linux-gnu/" \
-DCOMPILER_RT_CXX_LIBRARY="libcxx" \
-DCOMPILER_RT_STATIC_CXX_LIBRARY=ON \
-DCOMPILER_RT_BUILD_SANITIZERS=OFF \
-DCOMPILER_RT_BUILD_MEMPROF=OFF \
-DCOMPILER_RT_BUILD_LIBFUZZER=OFF \
-DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
-DCOMPILER_RT_INSTALL_PATH="/usr/local/lib/clang/$CLANG_MAJOR_VERSION"
make -j20
sudo cmake --install . --prefix "$ROOTFS_DIR/usr"
```
在构建指令中需要根据安装的`gcc`版本调整`_DLIBCXX_CXX_ABI_INCLUDE_PATHS`的路径。
完成所有上述的工作之后,回到我们最开始的你好世界样例,使用下面这行神秘的代码进行编译:
```bash
clang++ --target=riscv64-linux-gnu --sysroot=$ROOTFS_DIR -fuse-ld=lld hello.cpp -o hello
```
这次编译不会出现问题,上面指定的三个参数依次为指定目标三元组、指定根文件系统的位置和指定使用`lld`作为链接器。使用Docker镜像进行测试确认编译之后的二进制文件可以正常运行。
### 复盘
在正式开始下一步之前,我们先复盘一下在搭建交叉编译环境时我们都做了什么:
- 使用`LLVM_TARGETS_TO_BUILD`编译了一套新的LLVM
- 将安装了基础依赖包的`archriscv`导出作为根文件系统,
- 使用该根文件系统在该根文件系统中编译了`libcxx``compiler-rt`两个库。
这三步也带来了三个问题:
1. Arch Linux自带的LLVM工具链难道不能交叉编译吗
2. Arch Linux 官方提供的`riscv64-linux-gnu-gcc`包能够作为根文件系统吗?
3. 能够在上述的根文件系统中安装我们需要的`libcxx``compiler-rt`两个库吗?
第一个问题的回答是Arch Linux安装的LLVM工具是可以交叉编译的。虽然在Arch Linux官方构建LLVM工具链的[构建脚本](https://gitlab.archlinux.org/archlinux/packaging/packages/clang/-/blob/main/PKGBUILD?ref_type=heads)中没有使用`LLVM_TARGETS_TO_BUILD`参数,但是这个参数的默认值是`all`。这一点我们也可以通过实验来验证。
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.png)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`
第二个问题的回答可以使用实验来验证,首先安装`riscv64-linux-gnu-gcc`,然后将根文件系统的位置设置为`/usr/riscv64-linux-gnu`,重新编译上面的你好世界样例。编译之后可以正常执行。
第三个问题的回答是还是新建一个根文件系统罢,随便往系统目录里面写东西感觉是一个不太好的习惯。
## 正式编译
首先进入克隆代码的目录,运行初始化脚本。
```bash
cd dotnet
./prep-source-build.sh
```
设置根文件系统的目录,这里仍然使用从安装了`base-devel`的Docker容器中导出并自行编译了`compiler-rt``libcxx`的根文件系统。
```bash
export ROOTFS_DIR=<rootfs>
```
然后使用下面这条神秘的命令开始交叉编译:
```bash
./build.sh -sb --clean-while-building /p:TargetOS=linux /p:TargetArchitecture=riscv64 /p:Crossbuild=true /p:BuildArgs="/p:BundleNativeAotCompiler=false"
```
上面的第一个参数是指定了`source-build`选项第二个参数指定了在编译的过程中清理不需要的文件以节省硬盘空间后面的几个MSBUILD参数则是指定为RISC-V架构上的Linux系统构建并且不构建AOT编译器。
但是现在的.NET在RISC-V平台上还是废物一个甚至连`dotnet new`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.png)

View File

@@ -1,67 +0,0 @@
---
title: 2024中国计算机大会
date: 2024-11-03T14:06:36.4212070+08:00
tags:
- 杂谈
---
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办而鄙人在下不才我有幸受到实验室资助前去参观学习。
<!--more-->
首先开幕式镇楼。
![image-20241102212738598](./cncc-2024/image-20241102212738598.png)
## 学术上
大会每天的日程是上午的大会特邀报告和大会论坛,下午的各个分论坛讨论。老实说,大会上午的报告和论坛我都没有特别感兴趣,因此这里将重点放在我参加的三个分论坛上。
### AI时代的异构融合操作系统聚散终有时融合亦有期
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路通过三层架构基于互联池化技术构建AI时代的融合算力系统。系统中的三层包括1池化基础底层包括多设备的融合和池化设备虚拟化2异构融合核心子系统例如异构融合调度系统、异构融合内存和异构融合存储系统3异构核心服务。总的来说这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能但是显然这一操作系统的实现还存在着明显的困难。
![image-20241102211959206](./cncc-2024/image-20241102211959206.png)
下面一个报告是较为有干货的报告北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性指出分布式共享内存Distributed Shared Memory可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意但是现在总结的时候才发现这个报告的思路似乎有点混乱尤其是最后一点和内存管理系统并没有什么直接的关系而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
![image-20241102212355390](./cncc-2024/image-20241102212355390.png)
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此提出了一个智能模型训练并行任务划分方法1基于符号算子的计算图定义方法2面向Transformer模型的流水线并行任务划分方法3异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术针对现有重计算技术re-computing和存储交换swapping技术存在的问题提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`
最后一个报告是上海交通大学杜冬冬老师的《软硬芯异构融合操作系统的多个维度》。报告伊始杜老师就抛出一个问题操作系统的演进应该是提供新的抽象还是兼容现有的抽象在回答这个问题之前杜老师首先介绍他们一个异构融合操作系统的设计思路层OS架构的思路通过设置两个层次——全局OS和本地OS全局OS在本地OS的基础上提供一层跨`XPU`的能力。杜老师设计的这个系统称作`XPU-Shim`,在设计这个系统时就面对着前面的问题,是提供新的抽象还是兼容现有的抽象。`XPU-Shim`的回答是兼容现有的抽象在底层的CXL、UB等内存语义总线的基础上实现了传统的Socket抽象提供了低时延、高吞吐的协同能力。在操作系统的抽象问题之外杜老师还就云上GPU应用的启动时延问题进行了讨论深入解释了通过状态复用完全跳过初始化阶段从而加速应用冷启动过程的思路。
Plane讨论没有参加。
### 编译系统前沿技术与应用
第一个报告是清华大学陈文光老师的《神经网络全同态编译器》。这个报告可以说证明了“编译技术的人才活跃在各行各业”,报告中的主要内容就是编译技术如何助力机密计算中的全同态加密应用在神经网络的推理中。全同态加密算法实现了“数据可用不可见”的概念,允许程序直接在密文上进行乘法和加法运算,但是限制也是只能进行加法和乘法运算,而且过多的乘法操作会造成计算之后解密失败。该编译器成为`ANT-ACE`首先通过设计新的五层中间表示IR实现了自动化全同态加密程序生成和面向性能的优化设计在实现基本的编译工作之外`ANT-ACE`提供了一定的调试支持,通过部分支持对于模型的部分加密支持和运行时校验为解决加密之后程序推理准确率下降的问题。
接下来三个报告都是关于如何将人工智能技术同编译技术解决起来。计算所冯晓兵老师的报告《人工智能编译领域的应用探索》介绍了大模型同编译后端的两个结合方向1使用大模型生成编译器的后端代码2使用大模型替换编译器的后端直接利用大模型生成汇编代码。华为毕昇编译器架构师魏伟的报告《AI for Compiler的技术探索和应用实践》则是介绍了毕昇编译器的自动调优器`Autotuner`,这个一个自动寻找最优化的编译参数组合工具。复旦大学张为华老师的报告《基于学习的编译优化技术》也是一个类似的工作,利用机器学习技术挖掘已有的编译系统中存在的相关知识来指导新的编译优化。
最后一个报告则是字节公司郑思泽研究员的《计算通信融合中的编译器设计》,该报告主要聚焦于如何实现在深度学习算子层的计算通信融合,这个报告主要由搞`MLIR`的同学听,我就摸鱼了。
### 智能终端操作系统OpenHarmony前沿研究
虽然名字叫作OpenHarmony但是感觉内容实际上和鸿蒙系统没有什么太大的关系。
第一个报告是软件所武延军老师的《万物智联时代基础软件如何驯服碎片化》。报告的标题非常的高大上但是实际上就讲了两件事情1RISCV架构或者说RISCV这个可扩展的思想是解决架构碎片化的思路2`openEular`系统可以作为系统软件适配的一个基线操作系统。总结一下,这其实就是一个广告,希望大家做基础软件的都来和大家一起做。
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛例如`triton`
![image-20241102212536635](./cncc-2024/image-20241102212536635.png)
第三个报告是在存算一体的芯片上做数据库的加速第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。
最后一个又是上交杜冬冬老师的报告,《面向下一代智能终端操作系统的渲染服务研究与挑战》。这是一个我感觉还挺有趣的报告,报告中介绍的主要背景是随着终端设备上屏幕刷新率的提高和操作系统动画变得更加精致复杂,用户会发现终端系统上的显示卡顿越来越多、越明显。这是因为目前的终端显示刷新机制是同步的,显示屏会按照当前刷新的频率从操纵系统中读取下一帧的画面,但是操作系统面对这越来越短的刷新时延和越来越复杂的动画常常不能按时把下一帧的画面渲染好。于是我们的杜冬冬老师就提出了一种动态、异步的渲染机制,考虑到系统中显示动画的时间还是占少部分的,于是就可以借用这些系统不繁忙的时间预先渲染(削峰填谷)。但是这种方式需要预知到系统后面会显示的内容,这使得这套技术只能在确定性的场景和部分简单交互场景下使用。
> 这里插入一个杜冬冬老师的八卦杜老师改过一次名字之前的名字是杜东Dong Du在查找论文的时候使用后面的名字会更好一些在[IPADS](https://ipads.se.sjtu.edu.cn/zh/members/)和[dblp](https://dblp.org/pid/48/331-3.html)上面都还没有改过来)。
## 其他
首先我要锐评一下浙江省金华市东阳市横店镇。横店镇感觉完全没有为一个旅游目的地做过准备,虽然说镇子上面的酒店还是挺多的,但是不管是吃的还是玩的感觉都非常少。而且镇上的交通简直就是一坨,尤其是我们从酒店到会议举办地圆明新园的一段路,完全被大货车摧残的不成样子,在上面坐车堪比过山车。
然后我要锐评一下会议的举办地横店圆明新园。在去之前听说这里是1:1复刻了被八国联军烧毁的圆明园结果去了才发现圆明新园分成春苑、夏苑和秋苑其中春苑是复刻的圆明园但是会议的举办地是在夏苑和秋苑感觉有点的被诈骗了。夏苑里面只复刻了圆明园长春园的部分景观比如海岳开襟、谐奇趣和大水法等而且还增设了英、法、美、俄、日、德、意和奥等国的特色建筑而会议就主要在这些特色建筑中进行属实感觉有点奇怪了。
最后我要锐评一下CNCC会议。名义上看这个会议有涵盖数十个方向的130余场论坛上万名注册参会者的大型会议但是这个会议却选在了一个看上去基本上不适合召开大型会议的横店镇圆明新园。同时会议进行的非常寒酸中午的午餐是横店提供给剧组的盒饭在主会场发给我们之后只能自己端着吃下午的茶歇更是少的可怜除了第三天有好哥们分了我一块蛋挞三天的茶歇我愣是一点都没见到有可能是第三天的人最少提高了我获得茶歇的概率

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More