Compare commits
2 Commits
write-asyn
...
f49b4a3ea7
| Author | SHA1 | Date | |
|---|---|---|---|
| f49b4a3ea7 | |||
| 9d6112f987 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -482,6 +482,3 @@ $RECYCLE.BIN/
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
|
||||
# Tailwind auto-generated stylesheet
|
||||
output.css
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
public interface IPostRenderProcessor
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using YaeBlog.Models;
|
||||
using YaeBlog.Core.Models;
|
||||
|
||||
namespace YaeBlog.Abstraction;
|
||||
namespace YaeBlog.Core.Abstractions;
|
||||
|
||||
public interface IPreRenderProcessor
|
||||
{
|
||||
8
YaeBlog.Core/Components/_Imports.razor
Normal file
8
YaeBlog.Core/Components/_Imports.razor
Normal 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
|
||||
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
10
YaeBlog.Core/Models/AboutInfo.cs
Normal file
10
YaeBlog.Core/Models/AboutInfo.cs
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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()
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogHeadline(string title, string selectorId)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class BlogOptions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 友链模型类
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace YaeBlog.Models;
|
||||
namespace YaeBlog.Core.Models;
|
||||
|
||||
public class MarkdownMetadata
|
||||
{
|
||||
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal file
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal file
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal file
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
118
YaeBlog/Commands/CommandExtensions.cs
Normal file
118
YaeBlog/Commands/CommandExtensions.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
3
YaeBlog/Components/EssayCard.razor.css
Normal file
3
YaeBlog/Components/EssayCard.razor.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
@@ -0,0 +1,2 @@
|
||||
.copyright {
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 .
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
0
YaeBlog/Layout/BlogLayout.razor.css
Normal file
0
YaeBlog/Layout/BlogLayout.razor.css
Normal 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>
|
||||
|
||||
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
max-width: 48em;
|
||||
min-height: calc(100vh - 80px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace YaeBlog.Models;
|
||||
|
||||
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
||||
@@ -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>
|
||||
|
||||
8
YaeBlog/Pages/About.razor.css
Normal file
8
YaeBlog/Pages/About.razor.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.link-item {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
0
YaeBlog/Pages/Archives.razor.css
Normal file
0
YaeBlog/Pages/Archives.razor.css
Normal 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));
|
||||
}
|
||||
|
||||
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.essay-title a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.read-more a {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
0
YaeBlog/Pages/Essays.razor.css
Normal file
0
YaeBlog/Pages/Essays.razor.css
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
0
YaeBlog/Pages/Index.razor.css
Normal file
0
YaeBlog/Pages/Index.razor.css
Normal file
@@ -4,8 +4,8 @@
|
||||
啊~ 页面走丢啦~
|
||||
</PageTitle>
|
||||
|
||||
<div>
|
||||
<h3 class="text-3xl">NotFound!</h3>
|
||||
<div class="container">
|
||||
<h3>NotFound!</h3>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
0
YaeBlog/Pages/NotFound.razor.css
Normal file
0
YaeBlog/Pages/NotFound.razor.css
Normal 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>
|
||||
|
||||
6
YaeBlog/Pages/Tags.razor.css
Normal file
6
YaeBlog/Pages/Tags.razor.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.tag-count {
|
||||
background: var(--bs-secondary-bg);
|
||||
border-radius: 5px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
836
YaeBlog/pnpm-lock.yaml
generated
@@ -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: {}
|
||||
@@ -1,884 +0,0 @@
|
||||
---
|
||||
title: async/await究竟是如何工作的?
|
||||
tags:
|
||||
- dotnet
|
||||
- 技术笔记
|
||||
- 译文
|
||||
---
|
||||
|
||||
### 译者按
|
||||
|
||||
如何正确而快速的编写异步运行的代码一直是软件工程界的难题,而C#提出的`async/await`范式无疑是探索道路上的先行者。本篇文章便是翻译自.NET开发者博客上一篇名为“How async/await really works in C#”的文章,希望能够让读者在阅读之后明白`async/await`编程范式的前世今生和`.NET`实现方式。另外,.Net开发者中文博客也翻译了[这篇文章](https://devblogs.microsoft.com/dotnet-ch/async-await%e5%9c%a8-c%e8%af%ad%e8%a8%80%e4%b8%ad%e6%98%af%e5%a6%82%e4%bd%95%e5%b7%a5%e4%bd%9c%e7%9a%84/),一并供读者参考。
|
||||
|
||||
---
|
||||
|
||||
数周前,[.NET开发者博客](https://devblogs.microsoft.com/dotnet/)发布了一篇题为[什么是.NET,为什么你应该选择.NET](https://devblogs.microsoft.com/dotnet/why-dotnet/)的文章。文章中从宏观上概览了整个`dotnet`生态系统,总结了系统中的各个部分和其中的设计决定;文章还承诺在未来推出一系列的深度文章介绍涉及到的方方面面。这篇文章便是这系列文章中的第一篇,深入介绍C#和.NET中`async/await`的历史、设计决定和实现细节。
|
||||
|
||||
对于`async/await`的支持大约在十年前就提供了。在这段时间里,`async/await`语法大幅改变了编写可扩展.NET代码的方式,同时该语法使得在不了解`async/await`工作原理的情况下使用它提供的功能编写异步代码也是十分容易和常见的。以下面的**同步**方法为例:(因为这个方法的调用者在整个操作完成之前、将控制权返回给它之前都不能进行任何操作,所以这个方法被称为**同步**)
|
||||
|
||||
```csharp
|
||||
// 将数据同步地从源复制到目的地
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个方法的基础上,你只需要修改几个关键词、改变几个方法的名称,就可以得到一个**异步**的方法(因为这个方法将很快,往往实在所有的工作完成之前,就会将控制权返回给它的调用者,所以被称作异步方法)。
|
||||
|
||||
```csharp
|
||||
// 将数据异步地从源复制到目的地
|
||||
public async Task CopyStreamToStreamAsync(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有着几乎相同的语法,类似的控制流结构,但是现在这个方法在执行过程中不会阻塞,有着完全不同的底层执行模型,而且C#编译器和核心库帮你完成所有这些复杂的工作。
|
||||
|
||||
尽管在不了解底层原理的基础上使用这类技术是十分普遍的,但是我们坚持认为了解这些事务的运行原理将会帮助我们更好的利用它们。之于`async/await`,了解这些原理将在你需要深入探究时十分有用,例如当你需要调试一段错误的代码或者优化某段正确运行代码的运行效率时。在这篇文章中,我们将深入了解`async/await`具体如何在语言、编译器和库层面运行,然后你将更好地利用这些优秀的设计。
|
||||
|
||||
为了更好的理解这一切,我们将回到没有`async/await`的时代,看看在没有它们的情况下最优秀的异步代码是如何编写的。平心而论,这些代码看上去并不好。
|
||||
|
||||
### 原初的历史
|
||||
|
||||
回到.NET框架1.0时代,当时流行的异步编程范式是**异步编程模型**,“Asynchronous Programming Model”,也被称作`APM`范式、`Being/End`范式或者`IAsyncResult`范式。从宏观上来看,这种范式是相当简单的。例如对于一个同步操作`DoStuff`:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
}
|
||||
```
|
||||
|
||||
在这种编程模型下会存在两个相关的方法:一个`BeginStuff`方法和一个`EndStuff`方法:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
|
||||
public int EndDoStuff(IAsyncResult asyncResult);
|
||||
}
|
||||
```
|
||||
|
||||
`BeginStuff`方法首先会接受所有`DoStuff`方法会接受的参数,同时其会接受一个`AsyncCallback`回调和一个**不透明**的状态对象`state`,而且这两个参数都可以为空。这个“开始”方法将负责异步操作的初始化,而且如果提供了回调函数,这个函数还会负责在异步操作完成之后调用这个回调函数,因此这个回调函数也常常被称为初始化操作的“下一步”。开始方法还会负责构建一个实现了`IAsyncResult`接口的对象,这个对象中的`AsyncState`属性由可选的`state`参数提供:
|
||||
|
||||
```csharp
|
||||
namespace System
|
||||
{
|
||||
public interface IAsyncResult
|
||||
{
|
||||
object? AsyncState { get; }
|
||||
WaitHandle AsyncWaitHandle { get; }
|
||||
bool IsCompleted { get; }
|
||||
bool CompletedSynchronously { get; }
|
||||
}
|
||||
|
||||
public delegate void AsyncCallback(IAsyncResult ar);
|
||||
}
|
||||
```
|
||||
|
||||
这个`IAsynResult`实例将会被开始方法返回,在调用`AsyncCallback`时这个实例也会被传递过去。当准备好使用该异步操作的结果时,调用者也会将这个`IAsyncResult`实例传递给结束方法,同时结束方法也会负责保证这个异步操作完成,如果没有完成该方法就会阻塞代码的运行直到完成。结束方法会返回异步操作的结果,异步操作过程中引发的各种错误和异常也会通过该方法传递出来。因此,对于下面这种同步的操作:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int i = handler.DoStuff(arg);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 在这里处理DoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
可以使用开始/结束方法改写为异步运行的形式:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // 处理从EndDoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
}, handler);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 处理从同步调用BeginDoStuff方法引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
对于熟悉使用含有回调`API`语言的开发者来说,这样的代码应该会显得相当眼熟。
|
||||
|
||||
但是事情在这里变得更加复杂了。例如,这段代码存在“栈堆积”`stack dive`的问题。栈堆积就是代码在重复的调用方法中使得栈越来越深,直到发生栈溢出的现象。如果“异步”操作同步完成,开始方法将会使同步的调用回调方法,这就意味着对于开始方法的调用就会直接调用回调方法。同时考虑到“异步”方法同步完成却是一种非常常见的现象,它们只是承诺会异步的完成操作而不是只被允许异步的完成。例如一个对于某个网络操作的异步操作,比如读取一个套接字,如果你只需要从一次操作中读取少量的数据,例如在一次回答中只需要读取少量响应头的数据,你可能会直接读取大量数据存储在缓冲区中。相比于每次使用都使用系统调用但是只读取少量的数据,你一次读取了大量数据在缓冲区中,并在缓冲区失效之前都是从缓冲区中读取,这样就减少了需要调用昂贵的系统调用来和套接字交互的次数。像这样的缓冲区可能在你进行任何异步调用之后存在,例如第一次操作异步的完成对于缓冲区的填充,之后的若干次“异步”操作都不需要同I/O进行任何交互而直接通过与缓冲区的同步交互完成,直到缓冲区失效之后再次异步的填充缓冲区。因此当开始方法进行上述的一次调用时,开始方法会发现操作同步地完成了,因此开始方法同步地调用回调方法。此时,你有一个调用了开始方法的栈帧和一个调用了回调方法的栈帧。想想看如果回调方法再次调用了开始方法会发生什么?如果开始方法和回调方法都是被同步调用的,现在你就会在站上得到多个重复的栈帧,如此重复下去直到将栈上的空间耗尽。
|
||||
|
||||
这并不是杞人忧天,使用下面这段代码就可以很容易的复现这个问题:
|
||||
|
||||
```csharp
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
listener.Listen();
|
||||
|
||||
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
client.Connect(listener.LocalEndPoint!);
|
||||
|
||||
using Socket server = listener.Accept();
|
||||
_ = server.SendAsync(new byte[100_000]);
|
||||
|
||||
var mres = new ManualResetEventSlim();
|
||||
byte[] buffer = new byte[1];
|
||||
|
||||
var stream = new NetworkStream(client);
|
||||
|
||||
void ReadAgain()
|
||||
{
|
||||
stream.BeginRead(buffer, 0, 1, iar =>
|
||||
{
|
||||
if (stream.EndRead(iar) != 0)
|
||||
{
|
||||
ReadAgain(); // uh oh!
|
||||
}
|
||||
else
|
||||
{
|
||||
mres.Set();
|
||||
}
|
||||
}, null);
|
||||
};
|
||||
ReadAgain();
|
||||
|
||||
mres.Wait();
|
||||
```
|
||||
|
||||
在代码中我们建立一个简单的客户端套接字和一个简单的服务端套接字并让它们连接。服务端会向客户端发送十万字节的信息,而客户端会使用开始/结束方法尝试去“异步的”接收这些信息(需要注意这样做是十分低效的,在教学实例之外的地方都不应该这样编写代码)。传递给`BeingRead`的回调函数通过调用`EndRead`方法停止读取,如果在读取过程中读取到数据(意味着还没有读取完成),就通过对于本地方法`ReadAgain`的递归调用来再次调用`BeingRead`方法继续读取。值得指出的是,在.NET Core中套接字操作比原来在.NET Framework中的版本快上许多,同时如果操作系统可以同步的完成这些操作,那么.NET Core中的操作也会同步完成(需要注意操作系统内核也有一个缓冲区来完成套接字接收操作)。因此,运行这段代码就会出现栈溢出。
|
||||
|
||||
鉴于这个问题非常容易出现,因此`APM`模型中内建了缓解这个问题的方法。容易想到有两种方法可以缓解这个问题:
|
||||
|
||||
1. 不允许`AsyncCallback`被同步调用。如果该回调方法始终都是被异步调用的,即使操作是异步完成的,栈堆叠的方法也就不存在了。但是这样做会降低性能,因为同步完成的操作(或者快到难以注意到的操作)是相当的常见的,强制这些操作的回调排队完成会增加相当可观的开销。
|
||||
2. 引入一个机制让调用者而不是回调函数在工作异步完成时完成剩余的工作。在这种情况下,我们就避免了引入额外的栈帧,在不增加栈深度的情况下完成了余下的工作。
|
||||
|
||||
`APM`模型使用了第二种方法。为了实现这个方法,`IAsyncResult`接口提供了另外两个成员:`IsCompleted`和`CompletedSynchronusly`。`IsCompeleted`成员告诉我们操作是否完成,在程序中可以反复检查这个成员直到它从`false`变成`true`。相对的,`CompletedSynchronously`在运行过程中不会变化,(或者它存在一个还未被发现的`bug`会导致这个值变化,笑),这个值的主要作用是判断后续的工作是应该由开始方法的调用者还是`AsyncCallback`来进行。如果`CompletedSynchronously`的值是`false`,说明这个操作是异步进行的,所有后续的工作应该由回调函数来进行处理;毕竟,如果工作是异步完成的,开始方法的调用者不能知道工作是何时完成的(如果开始方法的调用者调用了结束方法,那么结束方法就会阻塞直到工作完成)。反之,如果`CompletedSynchronously`的值是`true`,如果此时使用回调方法处理后续的工作就会引发栈堆叠问题,因为此时回调方法会在栈上比开始它更低的位置上进行后续的操作。因此任何在意栈堆叠问题的实现需要关注`CompletedSynchronously`的值,当为真的时候,让开始方法的调用者处理后续的工作,而回调方法在此时不应处理任何工作。这也是为什么`CompletedSynchronously`的值不能改变——开始方法的调用者和回调方法需要相同的值来保证后续工作在任何情况下都进行且只进行一次。
|
||||
|
||||
因此我们之前的`DoStuff`实例就需要被修改为:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
IAsyncResult ar = handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // handle exceptions from EndDoStuff and Use
|
||||
}
|
||||
}
|
||||
}, handler);
|
||||
if (ar.CompletedSynchronously)
|
||||
{
|
||||
int i = handler.EndDoStuff(ar);
|
||||
Use(i);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // handle exceptions that emerge synchronously from BeginDoStuff and possibly EndDoStuff/Use
|
||||
}
|
||||
```
|
||||
|
||||
这里的代码已经~~显得冗长~~,而且我们还只研究了如何使用这种范式,还没有涉及如何实现这种范式。尽管大部分的开发者并不需要在这些子调用(例如实现`Socket.BeginReceive/EndReceive`这些方法去和操作系统交互),但是很多开发者需要组合这些操作(从一个“较大的”的异步操作调用多个异步操作),而这不仅需要使用其他的开始/结束方法,还需要自行实现你自己的开始/结束方法,这样你才能在其他的地方使用这个操作。同时,你还会注意到在上述的`DoStuff`范例中没有任何的控制流代码。如果需要引入一些控制流代码——即使是一个简单的循环——这也会立刻变成~~抖M才会编写的代码~~,同时也给无数的博客作者提供水`CSDN`的好题材。
|
||||
|
||||
所以让我们现在就来写一篇`CSDN`,给出一个完成的实例。在文章的开头我展示了一个`CopyStreamToStream`方法,这个方式会将一个流中的数据复制到另外一个流中(就是`Stream.CopyTo`方法所完成的工作,但是为了说明,让我们假设这个方法并不存在):
|
||||
|
||||
```csharp
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
直白的说,我们只需要不停的从一个流中读取数据然后写入到另外一个流中,直到我们没法从第一个流中读取到任何数据。现在让我们使用`APM`模型使用这个操作的异步模式吧:
|
||||
|
||||
```csharp
|
||||
public IAsyncResult BeginCopyStreamToStream(
|
||||
Stream source, Stream destination,
|
||||
AsyncCallback callback, object state)
|
||||
{
|
||||
var ar = new MyAsyncResult(state);
|
||||
var buffer = new byte[0x1000];
|
||||
|
||||
Action<IAsyncResult?> readWriteLoop = null!;
|
||||
readWriteLoop = iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (bool isRead = iar == null; ; isRead = !isRead)
|
||||
{
|
||||
if (isRead)
|
||||
{
|
||||
iar = source.BeginRead(buffer, 0, buffer.Length, static readResult =>
|
||||
{
|
||||
if (!readResult.CompletedSynchronously)
|
||||
{
|
||||
((Action<IAsyncResult?>)readResult.AsyncState!)(readResult);
|
||||
}
|
||||
}, readWriteLoop);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int numRead = source.EndRead(iar!);
|
||||
if (numRead == 0)
|
||||
{
|
||||
ar.Complete(null);
|
||||
callback?.Invoke(ar);
|
||||
return;
|
||||
}
|
||||
|
||||
iar = destination.BeginWrite(buffer, 0, numRead, writeResult =>
|
||||
{
|
||||
if (!writeResult.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination.EndWrite(writeResult);
|
||||
readWriteLoop(null);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destination.EndWrite(iar);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
};
|
||||
|
||||
readWriteLoop(null);
|
||||
|
||||
return ar;
|
||||
}
|
||||
|
||||
public void EndCopyStreamToStream(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is not MyAsyncResult ar)
|
||||
{
|
||||
throw new ArgumentException(null, nameof(asyncResult));
|
||||
}
|
||||
|
||||
ar.Wait();
|
||||
}
|
||||
|
||||
private sealed class MyAsyncResult : IAsyncResult
|
||||
{
|
||||
private bool _completed;
|
||||
private int _completedSynchronously;
|
||||
private ManualResetEvent? _event;
|
||||
private Exception? _error;
|
||||
|
||||
public MyAsyncResult(object? state) => AsyncState = state;
|
||||
|
||||
public object? AsyncState { get; }
|
||||
|
||||
public void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_completed = true;
|
||||
_error = error;
|
||||
_event?.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Wait()
|
||||
{
|
||||
WaitHandle? h = null;
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
h = _event ??= new ManualResetEvent(false);
|
||||
}
|
||||
|
||||
h.WaitOne();
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
}
|
||||
|
||||
public WaitHandle AsyncWaitHandle
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _event ??= new ManualResetEvent(_completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompletedSynchronously
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completedSynchronously == 0)
|
||||
{
|
||||
_completedSynchronously = _completed ? 1 : -1;
|
||||
}
|
||||
|
||||
return _completedSynchronously == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
~~Yowsers~~。即使写完了这些繁文缛节,这实际上仍然不是一个完美的实现。例如,`IAsyncResult`的实现会在每次操作时上锁,而不是在任何可能的时候都使用无锁的实现;异常也是以原始的模型存储,如果使用`ExceptionDispatchInfo`可以让异常在传播的过程中含有调用栈的信息,在每次操作中都分配了大量的空间来存储变量(例如在每次`BeingWrite`调用时都会分配一片空间来存储委托),如此等等。现在想象这就是你每次编写方法时需要做的工作,每次当你需要编写一个可重用的异步方法来使用另外一个异步方法时,你需要自己完成上述所有的工作。而且如果你需要编写使用多个不同的`IAsyncResult`的可重用代码——就像在`async/await`范式中`Task.WhenAll`所完成的那样,难度又上升了一个等级;每个不同操作都会实现并暴露针对相关的`API`,这让编写一套逻辑代码并简单的复用它们也变得不可能(尽管一些库作者可能会通过提供一层针对回调方法的新抽象来方便开发者编写需要访问暴露`API`的回调方法)。
|
||||
|
||||
上述这些复杂性也说明只有很少的一部分人尝试过这样编写代码,而且对于这些人来说,`bug`也往往如影随形。而且这并不是一个`APM`范式的黑点,这是所有使用基于回调的异步方法都具有的缺点。我们已经十分习惯现代语言都有的控制流结构所带来的强大和便利,因此使用会破坏这种结构的基于回调的异步方式会带来大量的复杂性也是可以理解的。同时,也没有任何主流的语言提供了更好的替代。
|
||||
|
||||
我们需要一种更好的办法,一个既继承了我们在`APM`范式中所学习到所有经验也规避了其所有的各种缺点的方式。一个有趣的点是,`APM`范式只是一种编程范式,运行时、核心库和编译器在使用或者实现这种范式的过程中没有提供任何协助。
|
||||
|
||||
### 基于事件的异步范式
|
||||
|
||||
在.NET Framework 2.0中提供了一系列的`API`来实现一种不同的异步编程范式,当时设想这种范式的主要应用场景是客户端应用程序。这种基于事件的异步范式,也被称作`EAP`范式,也是以提供一系列成员的方式提供的,包含一个用于初始化异步操作的方式和一个监听异步操作是否完成的事件。因此上述的`DoStuff`示例可能会暴露如下的一系列成员:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
首先通过`DoStuffCompleted`事件注册需要在完成异步操作时进行的工作然后调用`DoStuff`方法,这个方法将初始化异步操作,一旦异步操作完成,`DoStuffCompleted`事件将会被调用者引发。已经注册的回调方法可以运行剩余的工作,例如验证提供的`userToken`是否是期望的`userToken`,同时我们可以注册多个回调方法在异步操作完成的时候运行。
|
||||
|
||||
这个范式确实让一系列用例的编写更好编写,同时也让一系列用例变得更加复杂(例如上述的`CopyStreamToStream`例子)。这种范式的影响范围并不大,只在一次.NET Framework的更新中引入便匆匆地消失了,除了留下了一系列为了支持这种范式而实现的`API`,例如:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
但是这种编程范式确实在`APM`范式所没有注意到的地方前进了一大步,并且这一点还保留到了我们今天所介绍的模型中:[同步上下文](https://github.com/dotnet/runtime/blob/967a59712996c2cdb8ce2f65fb3167afbd8b01f3/src/libraries/System.Private.CoreLib/src/System/Threading/SynchronizationContext.cs#L6) (`SynchronizationContext`)。
|
||||
|
||||
同步上下文作为一个对于通用调度器的实现,也是在.NET Framework中引入的。在实践中,同步上下文最常用的方法是`Post`,这个方法将一个工作实现传递给上下文所代表的一种调度器。举例来说,一个基础的同步上下文实现是一个线程池`ThreadPool`,因此`Post`方法的典型实现就是`ThreadPool.QueueUserWorkItem`方法,这个方法将让线程池在池中任意的线程上以指定的状态调用指定的委托。然而,同步上下文的巧妙之处不仅在于提供了对于不同调度器的支持,而是提供了一种针对不同的应用模型使用不同调度方法的抽象能力。
|
||||
|
||||
考虑像Windows Forms之类的`UI`框架。对于大多数工作在Windows上的`UI`框架来说,控件往往关联到一个特定的线程,这个线程负责运行一个消息管理中心,这个中心用来运行那些需要同控件交互的工作:只有这个控件有能力来修改控件,任何其他试图同控件进行交互的线程都需要发送消息到这个消息控制中心。Windows Forms通过一系列方法来实现这一点,例如`Control.BeingInvoke`,这类方法将会把提供的委托和参数传递给同这个控件相关联的线程来运行。你可以写出如下的代码:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.BeginInvoke(() =>
|
||||
{
|
||||
button1.Text = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
这段代码首先将`ComputeMessage`方法交给线程池中的一个线程运行(这样可以保证该方法在运行时`UI`界面不会卡死),当上述工作完成之后,再将一个更新`button1`标签的委托传递给关联到`button1`的线程运行。简单而易于理解。在`WPF`框架中也是类似的逻辑,使用一个被称为`Dispatcher`的类型:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
button1.Content = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`.NET MAUI`亦然。但是如果我想将这部分的逻辑封装到一个独立的辅助函数中,例如下面这种:
|
||||
|
||||
```csharp
|
||||
// 调用ComputeMessage然后触发更新逻辑
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }
|
||||
```
|
||||
|
||||
这样我就可以直接:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ComputeMessageAndInvokeUpdate(message => button1.Text = message);
|
||||
}
|
||||
```
|
||||
|
||||
但是`ComputerMessageAndInvokeUpdate`应该如何实现才能适配各种类型的应用程序呢?难道需要硬编码所有可能涉及的`UI`框架吗?这就是`SynchronizationContext`大显神威的地方,我们可以这样实现这个方法:
|
||||
|
||||
```csharp
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update)
|
||||
{
|
||||
SynchronizationContext? sc = SynchronizationContext.Current;
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
if (sc is not null)
|
||||
{
|
||||
sc.Post(_ => update(message), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
update(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
在这个实现中将`SynchronizationContext`作为同`UI`进行交互的调度器之抽象。任何应用程序模型都需要保证在`SynchronizationContext.Current`属性上注册一个继承了`SynchronizationContext`的类,这个就会完成调度相关的工作。例如在`Windows Forms`中:
|
||||
|
||||
```csharp
|
||||
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state) =>
|
||||
_controlToSendTo?.BeginInvoke(d, new object?[] { state });
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
在`WPF`中有:
|
||||
|
||||
```
|
||||
public sealed class DispatcherSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, Object state) =>
|
||||
_dispatcher.BeginInvoke(_priority, d, state);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`ASP.NET`*曾经*也有过一个实现,尽管Web框架实际上并不关心是哪个线程在运行指定的工作,但是非常关心指定工作和那个请求相关,因此该实现主要负责保证多个线程不会在同时访问同一个`HttpContext`。
|
||||
|
||||
```csharp
|
||||
internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase
|
||||
{
|
||||
public override void Post(SendOrPostCallback callback, Object state) =>
|
||||
_state.Helper.QueueAsynchronous(() => callback(state));
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这个概念也并不局限于像上面的主流应用程序模型。例如在[xunit](https://github.com/xunit/xunit),一个流行的单元测试框架(`.NET`核心代码仓库也使用了)中也实现了需要自定义的`SynchronizationContext`。例如限制同步运行单元测试时同时运行单元测试数量就可以用`SynchroniaztionContext`实现:
|
||||
|
||||
```
|
||||
public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state)
|
||||
{
|
||||
var context = ExecutionContext.Capture();
|
||||
workQueue.Enqueue((d, state, context));
|
||||
workReady.Set();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`MaxConcurrentSyncContext`中的`Post`方法只是将需要完成的工作压入其内部的工作队列中,这样就能够控制同时多少工作能够并行的运行。
|
||||
|
||||
那么同步上下文这个概念时如何同基于事件的异步范式关联起来的呢?`EAP`范式和同步上下文都是在同一时间引入的,而`EAP`范式要求当异步操作启动的时候,完成事件需要由当前`SynchronizationContext`进行调度。为了简化这个过程(可能反而引入多余的复杂性),在`System.ComponentModel`命名控件中引入了一些帮助程序,具体来说是`AsyncOperation`和`AsyncOperationManager`。其中前者是一个由用户提供的状态对象和捕获到的`SynchronizationContext`组成的元组,后者是一个捕获`SynchronizationContext`和创建`AsyncOperation`对象的工厂类。`EAP`范式会在实现中使用上述帮助类,例如`Ping.SendAsync`会首先调用`AsyncOperationManager.CreateOperationi`来捕获同步上下文,然后当异步操作完成的时候调用`AsyncOperation.PostOperationCompleted`方法来调用捕获到的`SynchronizationContext.Post`方法。
|
||||
|
||||
`SynchronizationContext`还提供了其他一些后面会用到的小工具。这个类暴露了`OperationStarted`和`OperationCompleted`两个方法。这个虚方法在基类中的实现都是空的,并不完成任何工作。但是继承其的实现可能会重载这些来了解运行中的操作。`EAP`的实现就会在每个操作开始和结束的时候调用`OperationStarted`和`OperationCompleted`,来方便可能存在的同步上下文跟踪工作的进度。鉴于在`EAP`范式中启动异步操作的方法往往不会返回任何东西,不能指望可以获得任何帮助你跟踪工作进度的东西,因而可能获得工作进度的同步上下文就显得很有价值了。
|
||||
|
||||
综上所说,我们需要一些比`APM`编程范式更好的东西,而`EAP`范式引入了一些新的东西,但是没有解决我们面对的核心问题,我们仍然需要一些更好的东西。
|
||||
|
||||
### 进入Task时代
|
||||
|
||||
在.NET Framework 4.0中引入了`System.Threading.Tasks.Task`类型。当时`Task`类型还只代表某些异步操作的最终完成(在其他编程框架中可能成称为`promise`或者`future`)。当一个操作开始时,创建一个`Task`来表示这个操作,当这个操作完成之后,操作的结果就会被保存在这个`Task`中。简单而明确。但是`Task`相较于`IAsyncResult`提供的重要特点是其蕴含了一个任务在持续运行的状态。这个特点让你能够随意找到一个`Task`,让它在异步操作完成的时候异步的通知你,而不用你关注任务当前是处在已经完成、没有完成、正在完成等各种状态。为什么这点非常重要?首先想想`APM`范式中存在的两个主要问题:
|
||||
|
||||
1. 你需要对每个操作实现一个自定义的`IAsycResult`实现:库中没有任何内置开箱即用的`IAsycResult`实现。
|
||||
2. 你需要在调用开始方法之前就知道在操作结束的时候需要做什么。这让编写使用任意异步操作的组合代码或者通用运行时非常困难。
|
||||
|
||||
相对的,`Task`提供了一个通用的接口让你在启动一个异步操作之后“接触”这个操作,还提供了针对“持续”的抽象,这样你就不需要为启动异步操作的方法提供一个持续性。任何需要进行异步操作的人都可以产生一个`Task`,任何人需要使用异步操作的人都可以使用一个`Task`,在这个过程中不用自定义任何东西,`Task`成为了沟通异步操作的生产者和消费者之间最重要的桥梁。这一点大大改变了.NET框架。
|
||||
|
||||
现在让我们深入理解`Task`所带来的重要意义。与其直接去研究错综复杂的`Task`源代码,我们将尝试去实现一个`Task`的简单版本。这不会是一个完善的实现,只会完成基础的功能来让我们更好的理解什么是`Task`,即一个负责协调设置和存储完成信号的数据结构。
|
||||
|
||||
开始时`Task`中只有很少的字段:
|
||||
|
||||
```csharp
|
||||
class MyTask
|
||||
{
|
||||
private bool _completed;
|
||||
private Exception? _error;
|
||||
private Action<MyTask>? _continuation;
|
||||
private ExecutionContext? _ec;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
我们首先需要一个字段告诉我们任务是否完成`_completed`,一个字段存储造成任务执行失败的错误`_error`;如果我们需要实现一个泛型的`MyTask<TResult>`,还需要一个`private TResult _result`字段来存储操作运行完成之后的结果。到目前为止的实现和`IAsyncResult`相关的实现非常类似(当然这不是一个巧合)。`_continuation`字段时实现中最重要的字段。在这个简单的实现中,我们只支持一个简单的后续过程,在真正的`Task`实现中是一个`object`类型的字段,这样既可以是一个独立的后续过程,也可以是一个后续过程的列表。这个委托会在任务完成的时候调用。
|
||||
|
||||
让我们继续深入。如上所述,`Task`相较于之前的异步执行模型一个基础的优势是在异步操作开始之后再提供后续需要完成的工作。因此我们需要一个方法来实现这个功能:
|
||||
|
||||
```csharp
|
||||
public void ContinueWith(Action<MyTask> action)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ => action(this));
|
||||
}
|
||||
else if (_continuation is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation = action;
|
||||
_ec = ExecutionContext.Capture();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果在调用`ContinueWith`的时候异步操作已经完成,那么就直接将该委托的执行加入执行队列。反之,这个方法就会将存储这个委托,当异步任务完成的时候进行执行(这个方法同时也存储一个被称为`ExecutionContext`的对象,会在后续调用委托的涉及到,我们后续会继续介绍)。
|
||||
|
||||
然后我们需要能够在异步过程完成的时候标记任务已经完成。我们将添加两个方法,一个负责标记任务成功完成,一个负责标记任务报错退出。
|
||||
|
||||
```csharp
|
||||
public void SetResult() => Complete(null);
|
||||
|
||||
public void SetException(Exception error) => Complete(error);
|
||||
|
||||
private void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
throw new InvalidOperationException("Already completed");
|
||||
}
|
||||
|
||||
_error = error;
|
||||
_completed = true;
|
||||
|
||||
if (_continuation is not null)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
if (_ec is not null)
|
||||
{
|
||||
ExecutionContext.Run(_ec, _ => _continuation(this), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们会存储任何的错误、标记任务已经完成,如果已经注册的任何的后续过程,我们也会引发其进行执行。
|
||||
|
||||
最后我们还需要一个方法将在工作中发生的任何传递出来,(如果是泛型类型,还需要将执行结果返回),为了方便某些特定的场景,我们将允许这个方法阻塞直到异步操作完成(通过调用`ContinueWith`注册一个`ManualResetEventSlim`实现)。
|
||||
|
||||
```csharp
|
||||
public void Wait()
|
||||
{
|
||||
ManualResetEventSlim? mres = null;
|
||||
lock (this)
|
||||
{
|
||||
if (!_completed)
|
||||
{
|
||||
mres = new ManualResetEventSlim();
|
||||
ContinueWith(_ => mres.Set());
|
||||
}
|
||||
}
|
||||
|
||||
mres?.Wait();
|
||||
if (_error is not null)
|
||||
{
|
||||
ExceptionDispatchInfo.Throw(_error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这就是一个基础的`Task`实现。当然需要指出的是实际的`Task`会复杂很多:
|
||||
|
||||
- 支持设置任意数量的后续工作;
|
||||
- 支持配置其的工作行为(例如配置后续工作是应该进入工作队列等待执行还是作为任务完成的一部分同步被调用);
|
||||
- 支持存储多个错误;
|
||||
- 支持取消异步操作;
|
||||
- 一系列的帮助函数(例如`Task.Run`创建一个代表在线程池上运行委托的`Task`)。
|
||||
|
||||
但是这些内容中没有什么奥秘,核心工作原理和我们自行实现的是一样的。
|
||||
|
||||
你可以会注意到我们自行实现的`MyTask`直接公开了`SetResult/SetException`方法,而`Task`没有;这是因为`Task`是以`internal`声明了上述两个方法,同时`System.Threading.Tasks.TaskCompletionSource`类型负责作为一个独立的`Task`生产者和管理任务的完成。这样做的目的并不是出于技术目的,只是将负责控制完成的方法从消费`Task`的方法中分离出来。这样你就可以通过保留`TaskCompletionSource`对象来控制`Task`的完成,不必担心你创建的`Task`在你不知道的地方被完成。(`CancellationToken`和`CanellationTokenSource`也是处于同样的设计考虑,`CancellationToken`是一个包装`CancellationTokenSource`的结构,只暴露了和接受消费信号相关的结构而缺少产生一个取消信号的能力,这样就限制只有`CancellationToeknSource`可以产生取消信号。)
|
||||
|
||||
当前我们也可以像`Task`一样为我们自己的`MyTask`添加各种工具函数。例如我们添加一个`MyTask.WhenAll`:
|
||||
|
||||
```csharp
|
||||
public static MyTask WhenAll(MyTask t1, MyTask t2)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
int remaining = 2;
|
||||
Exception? e = null;
|
||||
|
||||
Action<MyTask> continuation = completed =>
|
||||
{
|
||||
e ??= completed._error; // just store a single exception for simplicity
|
||||
if (Interlocked.Decrement(ref remaining) == 0)
|
||||
{
|
||||
if (e is not null) t.SetException(e);
|
||||
else t.SetResult();
|
||||
}
|
||||
};
|
||||
|
||||
t1.ContinueWith(continuation);
|
||||
t2.ContinueWith(continuation);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
然后是一个`MyTask.Run`的示例:
|
||||
|
||||
```csharp
|
||||
public static MyTask Run(Action action)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
t.SetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
t.SetException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
还有一个简单的`MyTask.Delay`:
|
||||
|
||||
```csharp
|
||||
public static MyTask Delay(TimeSpan delay)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
var timer = new Timer(_ => t.SetResult());
|
||||
timer.Change(delay, Timeout.InfiniteTimeSpan);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
在`Task`横空出世之后,之前的所有异步编程范式都成为了过去式。任何使用过去的编程范式暴露的异步`API`,现在都提供了返回`Task`的方法。
|
||||
|
||||
### 添加Value Task
|
||||
|
||||
直到现在,`Task`都是.NET异步编程中的主力军,在每次新版本发布或者社区发布的新`API`都会返回`Task`或者`Task<TResult>`。但是,`Task`是一个类,而每次创建一个类是都需要分配一次内存。在大多数情况下,为一个会长期存在的异步操作进行一次内存分配时无关紧要的,并不会操作明显的性能影响。但是正如之前所说的,同步完成的异步操作十分创建。例如,`Stream.ReadAsync`会返回一个`Task<int>`,但是如果是在一个类似与`BufferedStream`的实现上调用该方法,那么你的调用由很大概率就会是同步完成的,因为大多数读取只需要从内存中的缓冲区中读取数据而不需要通过系统调用访问`I/O`。在这种情况下还需要分配一个额外的对象显然是不划算的(而且在`APM`范式中也存在这个问题)。对于返回非泛型类型的方法来说,还可以通过返回一个预先分配的已完成单例来缓解这个问题,而且`Task`也提供了一个`Task.CompletedTask`。但是对于泛型的`Task<TResult>`则不行,因为不可能针对每个不同的`TResult`都创建一个对应的单例。那么我们可以如何让这个同步操作更快呢?
|
||||
|
||||
我们可以试图缓存一个常见的`Task<TResult>`。例如`Task<bool>`就非常的常见,而且也只存在两种需要缓存的情况:当结果为真时的一个对象和结果为假时的一个对象。同样的,尽管我们可能不想尝试(也不太可能)去缓存数亿个`Task<int>`对象以覆盖所有可能出现的值,但是鉴于很小的`Int32`值时非常常见的,我们可以尝试去缓存给一些较小的结果,例如从-1到8的结果。 而且对于其他任意的类型来说,`default`就是一个常常出现的值,因此缓存一个结果是`default(TResult)`的`Task`。而且 在最近的.NET版本中添加了一个称作`Task.FromResult`辅助函数,该函数就会完成与上述类似的工作,如果存在可以重复使用的`Task<Result>`单例就返回该单例,反之再创建一个新的`Task`对象。对于其他常常出现的值也也可以设计方法进行缓存。还是以`Stream.ReadAsync`为例子,这个方法常常会在同一个流上调用多次,而且每次读取的值都是允许读取的字节数量`count`。再考虑到使用者往往只需要读取到这个`count`值,因此`Stream.ReadAsync`操作常常会重复返回有着相同`int`值的`Task`对象。为了避免在这种情况下重复的内存分配,许多`Stream`的实现(例如`MemoryStream`)会缓存上一次成功缓存的`Task<int>`对象,如果下一次读取仍然是同步返回的且返回了相同的数值,该方法就会返回上一次读取创建的`Task<int>`对象。但是仍然会存在许多无法覆盖的其他情况,能不能找到一种更加优雅的解决方案来来避免在异步操作同步完成的时候避免创建新的对象,尤其是在性能非常重要的场景下。
|
||||
|
||||
这就是`ValueTask<TResult>`诞生的背景([这篇博客](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)详细测试了`ValueTask<TResult>`的性能)。`ValueTask<TResult>`在诞生之初是`TResult`和`Task<TResult>`的歧视性联合。在这些争论尘埃落定之后,`ValueTask<TResult>`便不是一个立刻可以返回的结果就是一个对未来结果的承诺:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly Task<TResult>? _task;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
一个方法可以通过返回`ValueTask<TResult>`来避免在`TResult`已知的情况下创建新的`Task<Result>`对象,当然返回的类型会更大、返回的结果更加不直接。
|
||||
|
||||
当然,实际应用中也存在对性能需求相当高的场合,甚至你会想在操作异步完成的时候也避免`Task<TResult>`对象的分配。例如`Socket`作为整个网络栈的最底层,对于网络中的大多数服务来说`SendAsync`和`ReceiveAsync`都是绝对的热点代码路径,不论是同步操作还是异步操作都是非常常见的(鉴于内核中的缓存,大多数发送请求都会同步完成,部分接受请求会同步完成)。因此对于像`Socket`这类的工具,如果我们可以在异步我弄成和同步完成的情况下都实现无内存分配的调用是十分有意义的。
|
||||
|
||||
这就是`System.Threading.Tasks.Sources.IValueTaskSource<TResult>`产生的背景:
|
||||
|
||||
```csharp
|
||||
public interface IValueTaskSource<out TResult>
|
||||
{
|
||||
ValueTaskSourceStatus GetStatus(short token);
|
||||
void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
|
||||
TResult GetResult(short token);
|
||||
}
|
||||
```
|
||||
|
||||
该接口允许自行为`ValueTask<TResult>`实现一个“背后“的对象,并且让这个对象提供了获得操作结构的`GetResult`方法和设置操作后续工作的`OnCompleted`。在这个接口出现之后,`ValueTask<TResult>`也小小修改了定义,`Task<TResult>? _task`字段被一个`object? _obj`字段替换了:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly object? _obj;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
现在`_obj`字段就可以存储一个`IValueTaskSource<TReuslt>`对象了。而且相较于`Task<TResult>`在完成之后就只能保持完成的状态,不能变回未完成的状态,`IValueTaskSource<TResult>`的实现有着完全的控制权,可以在已完成和未完成的状态之间双向变化。但是`ValueTask<TResult>`要求一个特定的实例只能被使用一次,不能观察到这个实例在使用之后的任何变化,这也是分析规则[CA2012](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012)存在的意义。这就让让类似于`Socket`的工具为重复的调用建立一个`IValueTaskSource<TResult>`对象池。从实现上来说,`Socket`会至多缓存两个类似的实例,一个用于读取操作一个用于写入操作,因为在99.999%的情况下同时只会有一个发送请求和一个接受请求。
|
||||
|
||||
值得说明的是我只提到了`ValueTask<TResult>`却没有提到`ValueTask`。因为如果只是为了在操作同步完成的时候避免内存分配,非泛型类型的`ValueTask`指挥提供很少的性能提升,因为在同样的条件下可以使用`Task.CompletedTask`。但是如果要考虑在异步完成的时候通过缓存对象避免内存分配,非泛型类型也有作用。因而,在引入`IValueTaskSource<TResult>`的同时,`IValueTaskSource`和`ValueTask`也被引入了。
|
||||
|
||||
到目前我们,我们已经可以利用`Task`,`Task<TResult>`,`ValueTask`,`ValueTask<TResult>`表示各种各样的异步操作,并注册在操作完成之前和之后注册后续的操作。
|
||||
|
||||
但是这些后续操作仍然是回调方法,我们仍然陷入了基于回调的异步控制流程。该怎么办?
|
||||
|
||||
### 迭代器成为大救星
|
||||
|
||||
解决方案的先声实际上在`Task`诞生之前就出现了,在C# 2.0引入迭代器语法的时候。
|
||||
|
||||
你可能会问,迭代器就是`IEnumerable<T>`吗?这是其中的一个。迭代器是一个让编译器将你编写的方法自动实现`IEnumerable<T>`或者`IEnumertor<T>`的语法。例如我可以用迭代器语法编写一个产生斐波那契数列的可遍历对象:
|
||||
|
||||
```csharp
|
||||
public static IEnumerable<int> Fib()
|
||||
{
|
||||
int prev = 0, next = 1;
|
||||
yield return prev;
|
||||
yield return next;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int sum = prev + next;
|
||||
yield return sum;
|
||||
prev = next;
|
||||
next = sum;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个方法可以直接用`foreach`遍历,也可以和`System.Linq.Enumerable`中提供的各种方法组合,也可以直接用一个`IEnumerator<T>`对象遍历。
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib())
|
||||
{
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib().Take(12))
|
||||
{
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
using IEnumerator<int> e = Fib().GetEnumerator();
|
||||
while (e.MoveNext())
|
||||
{
|
||||
int i = e.Current;
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
6
YaeBlog/source/drafts/test-essay.md
Normal file
6
YaeBlog/source/drafts/test-essay.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: test-essay
|
||||
date: 2024-08-22T22:31:34.3177253+08:00
|
||||
tags:
|
||||
---
|
||||
<!--more-->
|
||||
@@ -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`解释器可以直接运行这段代码:
|
||||
|
||||

|
||||
|
||||
@@ -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站,鬼知道你匆匆在哪了~~。
|
||||
|
||||

|
||||
|
||||
### 未来
|
||||
|
||||
> 定计划的目的不是为了实现,而是为了安心。
|
||||
|
||||
站在年关,已经可以预见到2025年将会是更为繁忙的一年,从一月份到十月份都已经有了或多或少的安排,现在无法多言,只能希望都能有良好的结果。
|
||||
|
||||
还是多说点可以说的罢。
|
||||
|
||||
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目,一整本上总结下来也能有超过50本,其中不乏超过一千页的大部头,说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍:
|
||||
|
||||
- 陆风,《光变:一个企业及其工业史》
|
||||
- 吴军,《浪潮之巅》
|
||||
|
||||
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。
|
||||
|
||||
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 日用Linux挑战 第5篇 标准安装流程
|
||||
title: 日用Linux挑战第五篇 ArchLinux标准安装流程
|
||||
date: 2024-7-16 20:08:37
|
||||
tags:
|
||||
- Linux
|
||||
|
||||
@@ -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`来完成的,还是来看这张经典的中间件工作流程:
|
||||
|
||||

|
||||
|
||||
可以看到在中间件的管道中,认证中间价将在授权中间件运行之前运行——这两个顺序是不能颠倒的,如果授权中间件在认证中间件运行之前运行,那授权中间件就无法为用户授予任何权限,所有需要权限的接口均会返回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提供了一套简单易用、扩展性高的接口安全系统。
|
||||
@@ -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 |
@@ -44,7 +44,7 @@ date: 2022-07-27 11:34:49
|
||||
|
||||
而且采用 `Git`还有一个好处,采用 `Github`的 `Insight`功能可以轻松的看出大家的贡献值()。
|
||||
|
||||

|
||||

|
||||
|
||||
## 一些技术上的收获
|
||||
|
||||
|
||||
@@ -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`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
|
||||
|
||||

|
||||
|
||||
在使用`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;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的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工具链。
|
||||
|
||||

|
||||
|
||||
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装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`。这一点我们也可以通过实验来验证。
|
||||
|
||||
于是回到编译`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`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
|
||||
|
||||

|
||||
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
Binary file not shown.
@@ -1,67 +0,0 @@
|
||||
---
|
||||
title: 2024中国计算机大会
|
||||
date: 2024-11-03T14:06:36.4212070+08:00
|
||||
tags:
|
||||
- 杂谈
|
||||
---
|
||||
|
||||
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办,而鄙人在下不才我,有幸受到实验室资助前去参观学习。
|
||||
|
||||
<!--more-->
|
||||
|
||||
首先开幕式镇楼。
|
||||
|
||||

|
||||
|
||||
## 学术上
|
||||
|
||||
大会每天的日程是上午的大会特邀报告和大会论坛,下午的各个分论坛讨论。老实说,大会上午的报告和论坛我都没有特别感兴趣,因此这里将重点放在我参加的三个分论坛上。
|
||||
|
||||
### AI时代的异构融合操作系统:聚散终有时,融合亦有期
|
||||
|
||||
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴,从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路:通过三层架构,基于互联池化技术,构建AI时代的融合算力系统。系统中的三层包括:(1)池化基础底层,包括多设备的融合和池化设备虚拟化;(2)异构融合核心子系统,例如异构融合调度系统、异构融合内存和异构融合存储系统;(3)异构核心服务。总的来说,这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能,但是显然这一操作系统的实现还存在着明显的困难。
|
||||
|
||||

|
||||
|
||||
下面一个报告是较为有干货的报告,北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统,是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性,指出分布式共享内存(Distributed Shared Memory)可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意,但是现在总结的时候才发现这个报告的思路似乎有点混乱,尤其是最后一点和内存管理系统并没有什么直接的关系,而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
|
||||
|
||||

|
||||
|
||||
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此,提出了一个智能模型训练并行任务划分方法:(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,但是感觉内容实际上和鸿蒙系统没有什么太大的关系。
|
||||
|
||||
第一个报告是软件所武延军老师的《万物智联时代基础软件如何驯服碎片化》。报告的标题非常的高大上,但是实际上就讲了两件事情:(1)RISCV架构,或者说RISCV这个可扩展的思想,是解决架构碎片化的思路;(2)`openEular`系统可以作为系统软件适配的一个基线操作系统。总结一下,这其实就是一个广告,希望大家做基础软件的都来和大家一起做。
|
||||
|
||||
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》,同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力,而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景,仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言,但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛,例如`triton`。
|
||||
|
||||

|
||||
|
||||
第三个报告是在存算一体的芯片上做数据库的加速,第四个报告是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余场论坛,上万名注册参会者的大型会议,但是这个会议却选在了一个看上去基本上不适合召开大型会议的横店镇圆明新园。同时会议进行的非常寒酸,中午的午餐是横店提供给剧组的盒饭,在主会场发给我们之后只能自己端着吃,下午的茶歇更是少的可怜,除了第三天有好哥们分了我一块蛋挞,三天的茶歇我愣是一点都没见到(有可能是第三天的人最少,提高了我获得茶歇的概率)。
|
||||
|
||||
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user