diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cdbe0b --- /dev/null +++ b/LICENSE @@ -0,0 +1,438 @@ +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e4fe70 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# YaeBlog + +Ricardo Ren 的个人主页和博客仓库。 + +主页地址:[rrricardo.top](https://rrricardo.top) + +博客地址:[rrricardo.top/blog/](https://rrricardo.top/blog/) + +欢迎各位来玩~ 未来也会添加更多有趣的功能~ diff --git a/YaeBlog.Core/Abstractions/IEssayContentService.cs b/YaeBlog.Core/Abstractions/IEssayContentService.cs index 3fbbb42..9be561c 100644 --- a/YaeBlog.Core/Abstractions/IEssayContentService.cs +++ b/YaeBlog.Core/Abstractions/IEssayContentService.cs @@ -9,5 +9,14 @@ public interface IEssayContentService public IReadOnlyDictionary> Tags { get; } - public bool SearchByUrlEncodedTag(string tag,[NotNullWhen(true)] out List? result); + public IReadOnlyDictionary Headlines { get; } + + public bool TryAddHeadline(string filename, BlogHeadline headline); + public bool SearchByUrlEncodedTag(string tag, [NotNullWhen(true)] out List? result); + + public bool TryAdd(BlogEssay essay); + + public void RefreshTags(); + + public void Clear(); } diff --git a/YaeBlog.Core/Abstractions/IEssayScanService.cs b/YaeBlog.Core/Abstractions/IEssayScanService.cs new file mode 100644 index 0000000..56957c8 --- /dev/null +++ b/YaeBlog.Core/Abstractions/IEssayScanService.cs @@ -0,0 +1,10 @@ +using YaeBlog.Core.Models; + +namespace YaeBlog.Core.Abstractions; + +public interface IEssayScanService +{ + public Task ScanContents(); + + public Task SaveBlogContent(BlogContent content, bool isDraft = true); +} diff --git a/YaeBlog.Core/Abstractions/ITableOfContentService.cs b/YaeBlog.Core/Abstractions/ITableOfContentService.cs deleted file mode 100644 index 08b997c..0000000 --- a/YaeBlog.Core/Abstractions/ITableOfContentService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using YaeBlog.Core.Models; - -namespace YaeBlog.Core.Abstractions; - -public interface ITableOfContentService -{ - public IReadOnlyDictionary Headlines { get; } -} diff --git a/YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs b/YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs index 2b617fa..ab9a02e 100644 --- a/YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs +++ b/YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs @@ -20,12 +20,14 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddYamlParser(this IServiceCollection collection) { - DeserializerBuilder builder = new(); + DeserializerBuilder deserializerBuilder = new(); + deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); + deserializerBuilder.IgnoreUnmatchedProperties(); + collection.AddSingleton(deserializerBuilder.Build()); - builder.WithNamingConvention(CamelCaseNamingConvention.Instance); - builder.IgnoreUnmatchedProperties(); - - collection.AddSingleton(_ => builder.Build()); + SerializerBuilder serializerBuilder = new(); + serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); + collection.AddSingleton(serializerBuilder.Build()); return collection; } diff --git a/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs b/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs index d469a33..f6cd91a 100644 --- a/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs +++ b/YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs @@ -20,14 +20,9 @@ public static class WebApplicationBuilderExtensions builder.Services.AddMarkdig(); builder.Services.AddYamlParser(); builder.Services.AddSingleton(_ => Configuration.Default); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(provider => - provider.GetRequiredService()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(provider => - provider.GetRequiredService()); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -35,8 +30,21 @@ public static class WebApplicationBuilderExtensions builder.Services.AddTransient(provider => provider.GetRequiredService>().Value); + return builder; + } + + public static WebApplicationBuilder AddServer(this WebApplicationBuilder builder) + { builder.Services.AddHostedService(); return builder; } + + public static WebApplicationBuilder AddWatcher(this WebApplicationBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddHostedService(); + + return builder; + } } diff --git a/YaeBlog.Core/Models/BlogContent.cs b/YaeBlog.Core/Models/BlogContent.cs index f7999d6..4cf6c22 100644 --- a/YaeBlog.Core/Models/BlogContent.cs +++ b/YaeBlog.Core/Models/BlogContent.cs @@ -4,5 +4,7 @@ public class BlogContent { public required string FileName { get; init; } + public required MarkdownMetadata Metadata { get; init; } + public required string FileContent { get; set; } } diff --git a/YaeBlog.Core/Models/BlogContents.cs b/YaeBlog.Core/Models/BlogContents.cs new file mode 100644 index 0000000..9514903 --- /dev/null +++ b/YaeBlog.Core/Models/BlogContents.cs @@ -0,0 +1,10 @@ +using System.Collections.Concurrent; + +namespace YaeBlog.Core.Models; + +public sealed class BlogContents(ConcurrentBag drafts, ConcurrentBag posts) +{ + public ConcurrentBag Drafts { get; } = drafts; + + public ConcurrentBag Posts { get; } = posts; +} diff --git a/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs b/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs index ddfed50..74dd5d3 100644 --- a/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs +++ b/YaeBlog.Core/Processors/HeadlinePostRenderProcessor.cs @@ -1,14 +1,15 @@ using AngleSharp; using AngleSharp.Dom; +using Microsoft.Extensions.Logging; using YaeBlog.Core.Abstractions; using YaeBlog.Core.Models; -using YaeBlog.Core.Services; namespace YaeBlog.Core.Processors; public class HeadlinePostRenderProcessor( IConfiguration angleConfiguration, - TableOfContentService tableOfContentService) : IPostRenderProcessor + IEssayContentService essayContentService, + ILogger logger) : IPostRenderProcessor { public async Task ProcessAsync(BlogEssay essay) { @@ -62,7 +63,10 @@ public class HeadlinePostRenderProcessor( FindParentHeadline(topHeadline, level2List).Children.AddRange(level3List); topHeadline.Children.AddRange(level2List); - tableOfContentService.AddHeadline(essay.FileName, topHeadline); + if (!essayContentService.TryAddHeadline(essay.FileName, topHeadline)) + { + logger.LogWarning("Failed to add headline of {}.", essay.FileName); + } return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml); } diff --git a/YaeBlog.Core/Processors/ImagePostRenderProcessor.cs b/YaeBlog.Core/Processors/ImagePostRenderProcessor.cs index 0592899..e0f3ab0 100644 --- a/YaeBlog.Core/Processors/ImagePostRenderProcessor.cs +++ b/YaeBlog.Core/Processors/ImagePostRenderProcessor.cs @@ -3,6 +3,7 @@ using AngleSharp.Dom; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YaeBlog.Core.Abstractions; +using YaeBlog.Core.Exceptions; using YaeBlog.Core.Models; namespace YaeBlog.Core.Processors; @@ -47,12 +48,12 @@ public class ImagePostRenderProcessor(ILogger logger, filename = Path.Combine(essayFilename, filename); } - filename = Path.Combine(_options.Root, filename); + filename = Path.Combine(_options.Root, "posts", filename); if (!Path.Exists(filename)) { logger.LogError("Failed to found image: {}.", filename); - throw new InvalidOperationException(); + throw new BlogFileException($"Image {filename} doesn't exist."); } string imageLink = "api/files/" + filename; diff --git a/YaeBlog.Core/README.md b/YaeBlog.Core/README.md deleted file mode 100644 index 20e3510..0000000 --- a/YaeBlog.Core/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# YaeBlog.Core - -A blog generation totally based on Blazor. - -You can using this to create your blog all in .NET stack! diff --git a/YaeBlog.Core/Services/BlogChangeWatcher.cs b/YaeBlog.Core/Services/BlogChangeWatcher.cs new file mode 100644 index 0000000..a6d4136 --- /dev/null +++ b/YaeBlog.Core/Services/BlogChangeWatcher.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using YaeBlog.Core.Models; + +namespace YaeBlog.Core.Services; + +public sealed class BlogChangeWatcher : IDisposable +{ + private readonly FileSystemWatcher _fileSystemWatcher; + private readonly ILogger _logger; + + public BlogChangeWatcher(IOptions options, ILogger logger) + { + _logger = logger; + _fileSystemWatcher = new FileSystemWatcher(Path.Combine(Environment.CurrentDirectory, options.Value.Root)); + + _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | + NotifyFilters.DirectoryName | NotifyFilters.Size; + _fileSystemWatcher.IncludeSubdirectories = true; + _fileSystemWatcher.EnableRaisingEvents = true; + } + + public async Task WaitForChange(CancellationToken cancellationToken = default) + { + TaskCompletionSource tcs = new(); + cancellationToken.Register(() => tcs.TrySetResult(null)); + + _logger.LogDebug("Register file change handle."); + _fileSystemWatcher.Changed += FileChangedCallback; + _fileSystemWatcher.Created += FileChangedCallback; + _fileSystemWatcher.Deleted += FileChangedCallback; + _fileSystemWatcher.Renamed += FileChangedCallback; + + string? result; + try + { + result = await tcs.Task; + } + finally + { + _logger.LogDebug("Unregister file change handle."); + _fileSystemWatcher.Changed -= FileChangedCallback; + _fileSystemWatcher.Created -= FileChangedCallback; + _fileSystemWatcher.Deleted -= FileChangedCallback; + _fileSystemWatcher.Renamed -= FileChangedCallback; + } + + return result; + + void FileChangedCallback(object _, FileSystemEventArgs e) + { + _logger.LogDebug("File {} change detected.", e.Name); + tcs.TrySetResult(e.Name); + } + } + + public void Dispose() + { + _fileSystemWatcher.Dispose(); + } +} diff --git a/YaeBlog.Core/Services/BlogHotReloadService.cs b/YaeBlog.Core/Services/BlogHotReloadService.cs new file mode 100644 index 0000000..f112e10 --- /dev/null +++ b/YaeBlog.Core/Services/BlogHotReloadService.cs @@ -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 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(); + } + } +} diff --git a/YaeBlog.Core/Services/EssayContentService.cs b/YaeBlog.Core/Services/EssayContentService.cs index aa800f2..05f49b6 100644 --- a/YaeBlog.Core/Services/EssayContentService.cs +++ b/YaeBlog.Core/Services/EssayContentService.cs @@ -11,12 +11,18 @@ public class EssayContentService : IEssayContentService private readonly Dictionary> _tags = []; + private readonly ConcurrentDictionary _headlines = new(); + public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay); + public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline); + public IReadOnlyDictionary Essays => _essays; public IReadOnlyDictionary> Tags => _tags; + public IReadOnlyDictionary Headlines => _headlines; + public void RefreshTags() { _tags.Clear(); @@ -45,4 +51,11 @@ public class EssayContentService : IEssayContentService return result is not null; } + + public void Clear() + { + _essays.Clear(); + _tags.Clear(); + _headlines.Clear(); + } } diff --git a/YaeBlog.Core/Services/EssayScanService.cs b/YaeBlog.Core/Services/EssayScanService.cs index b486c8f..5ac4036 100644 --- a/YaeBlog.Core/Services/EssayScanService.cs +++ b/YaeBlog.Core/Services/EssayScanService.cs @@ -1,20 +1,104 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using YaeBlog.Core.Abstractions; using YaeBlog.Core.Exceptions; using YaeBlog.Core.Models; +using YamlDotNet.Core; +using YamlDotNet.Serialization; namespace YaeBlog.Core.Services; public class EssayScanService( + ISerializer yamlSerializer, + IDeserializer yamlDeserializer, IOptions blogOptions, - ILogger logger) + ILogger logger) : IEssayScanService { private readonly BlogOptions _blogOptions = blogOptions.Value; - public async Task> ScanAsync() + public async Task ScanContents() { - string root = Path.Combine(Environment.CurrentDirectory, _blogOptions.Root); + ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts); + + return new BlogContents( + await ScanContentsInternal(drafts), + await ScanContentsInternal(posts)); + } + + public async Task SaveBlogContent(BlogContent content, bool isDraft = true) + { + ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts); + + FileInfo targetFile = isDraft + ? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md")) + : new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md")); + + if (targetFile.Exists) + { + logger.LogWarning("Blog {} exists, overriding.", targetFile.Name); + } + + await using StreamWriter writer = targetFile.CreateText(); + + await writer.WriteAsync("---\n"); + await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata)); + await writer.WriteAsync("---\n"); + await writer.WriteAsync("\n"); + } + + private async Task> ScanContentsInternal(DirectoryInfo directory) + { + IEnumerable markdownFiles = from file in directory.EnumerateFiles() + where file.Extension == ".md" + select file; + + ConcurrentBag<(string, string)> fileContents = []; + + await Parallel.ForEachAsync(markdownFiles, async (file, token) => + { + using StreamReader reader = file.OpenText(); + fileContents.Add((file.Name, await reader.ReadToEndAsync(token))); + }); + + ConcurrentBag contents = []; + + await Task.Run(() => + { + foreach ((string filename, string content) in fileContents) + { + int endPos = content.IndexOf("---", 4, StringComparison.Ordinal); + if (!content.StartsWith("---") || endPos is -1 or 0) + { + logger.LogWarning("Failed to parse metadata from {}, skipped.", filename); + return; + } + + string metadataString = content[4..endPos]; + + try + { + MarkdownMetadata metadata = yamlDeserializer.Deserialize(metadataString); + logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename); + + contents.Add(new BlogContent + { + FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..] + }); + } + catch (YamlException e) + { + logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e); + } + } + }); + + return contents; + } + + private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts) + { + root = Path.Combine(Environment.CurrentDirectory, root); DirectoryInfo rootDirectory = new(root); if (!rootDirectory.Exists) @@ -22,36 +106,17 @@ public class EssayScanService( throw new BlogFileException($"'{root}' is not a directory."); } - List markdownFiles = []; - - await Task.Run(() => + if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts")) { - foreach (FileInfo fileInfo in rootDirectory.EnumerateFiles()) - { - if (fileInfo.Extension != ".md") - { - continue; - } + throw new BlogFileException($"'{root}/drafts' not exists."); + } - logger.LogDebug("Scan markdown file: {}.", fileInfo.Name); - markdownFiles.Add(fileInfo); - } - }); - - ConcurrentBag contents = []; - - await Parallel.ForEachAsync(markdownFiles, async (info, token) => + if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts")) { - StreamReader reader = new(info.OpenRead()); + throw new BlogFileException($"'{root}/posts' not exists."); + } - BlogContent content = new() - { - FileName = info.Name.Split('.')[0], FileContent = await reader.ReadToEndAsync(token) - }; - - contents.Add(content); - }); - - return contents.ToList(); + drafts = new DirectoryInfo(Path.Combine(root, "drafts")); + posts = new DirectoryInfo(Path.Combine(root, "posts")); } } diff --git a/YaeBlog.Core/Services/RendererService.cs b/YaeBlog.Core/Services/RendererService.cs index 3195efb..8ed7103 100644 --- a/YaeBlog.Core/Services/RendererService.cs +++ b/YaeBlog.Core/Services/RendererService.cs @@ -7,17 +7,14 @@ using Microsoft.Extensions.Logging; using YaeBlog.Core.Abstractions; using YaeBlog.Core.Exceptions; using YaeBlog.Core.Models; -using YamlDotNet.Core; -using YamlDotNet.Serialization; namespace YaeBlog.Core.Services; public partial class RendererService( ILogger logger, - EssayScanService essayScanService, + IEssayScanService essayScanService, MarkdownPipeline markdownPipeline, - IDeserializer yamlDeserializer, - EssayContentService essayContentService) + IEssayContentService essayContentService) { private readonly Stopwatch _stopwatch = new(); @@ -30,30 +27,30 @@ public partial class RendererService( _stopwatch.Start(); logger.LogInformation("Render essays start."); - List contents = await essayScanService.ScanAsync(); - IEnumerable preProcessedContents = await PreProcess(contents); + BlogContents contents = await essayScanService.ScanContents(); + List posts = contents.Posts.ToList(); + IEnumerable preProcessedContents = await PreProcess(posts); List essays = []; await Task.Run(() => { foreach (BlogContent content in preProcessedContents) { - MarkdownMetadata? metadata = TryParseMetadata(content); uint wordCount = GetWordCount(content); BlogEssay essay = new() { - Title = metadata?.Title ?? content.FileName, + Title = content.Metadata.Title ?? content.FileName, FileName = content.FileName, Description = GetDescription(content), WordCount = wordCount, ReadTime = CalculateReadTime(wordCount), - PublishTime = metadata?.Date ?? DateTime.Now, + PublishTime = content.Metadata.Date ?? DateTime.Now, HtmlContent = content.FileContent }; - if (metadata?.Tags is not null) + if (content.Metadata.Tags is not null) { - essay.Tags.AddRange(metadata.Tags); + essay.Tags.AddRange(content.Metadata.Tags); } essays.Add(essay); @@ -138,45 +135,6 @@ public partial class RendererService( }); } - private MarkdownMetadata? TryParseMetadata(BlogContent content) - { - string fileContent = content.FileContent.Trim(); - - if (!fileContent.StartsWith("---")) - { - return null; - } - - // 移除起始的--- - fileContent = fileContent[3..]; - - int lastPos = fileContent.IndexOf("---", StringComparison.Ordinal); - if (lastPos is -1 or 0) - { - return null; - } - - string yamlContent = fileContent[..lastPos]; - // 返回去掉元数据之后的文本 - lastPos += 3; - content.FileContent = fileContent[lastPos..]; - - try - { - MarkdownMetadata metadata = - yamlDeserializer.Deserialize(yamlContent); - logger.LogDebug("Title: {}, Publish Date: {}.", - metadata.Title, metadata.Date); - - return metadata; - } - catch (YamlException e) - { - logger.LogWarning("Failed to parse '{}' metadata: {}", yamlContent, e); - return null; - } - } - [GeneratedRegex(@"(? _headlines = []; - - public IReadOnlyDictionary Headlines => _headlines; - - public void AddHeadline(string filename, BlogHeadline headline) - { - if (!_headlines.TryAdd(filename, headline)) - { - throw new InvalidOperationException(); - } - } -} diff --git a/YaeBlog.Core/YaeBlog.Core.csproj b/YaeBlog.Core/YaeBlog.Core.csproj index 844ae04..ef44f03 100644 --- a/YaeBlog.Core/YaeBlog.Core.csproj +++ b/YaeBlog.Core/YaeBlog.Core.csproj @@ -6,20 +6,6 @@ enable - - YaeBlog.Core - 0.1.0 - Ricardo Ren - Ricardo Ren - MIT - README.md - - - - - - - @@ -32,12 +18,6 @@ - - - - - - diff --git a/YaeBlog.sln b/YaeBlog.sln index c74cbfb..78aae25 100644 --- a/YaeBlog.sln +++ b/YaeBlog.sln @@ -14,6 +14,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .gitea\workflows\build.yaml = .gitea\workflows\build.yaml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{753B998C-1B9E-498F-B949-845CE86C4075}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + README.md = README.md + LICENSE = LICENSE + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/YaeBlog/Commands/Binders/BlogOptionsBinder.cs b/YaeBlog/Commands/Binders/BlogOptionsBinder.cs new file mode 100644 index 0000000..9f3cb54 --- /dev/null +++ b/YaeBlog/Commands/Binders/BlogOptionsBinder.cs @@ -0,0 +1,36 @@ +using System.CommandLine.Binding; +using System.Text.Json; +using Microsoft.Extensions.Options; +using YaeBlog.Core.Models; + +namespace YaeBlog.Commands.Binders; + +public sealed class BlogOptionsBinder : BinderBase> +{ + protected override IOptions GetBoundValue(BindingContext bindingContext) + { + bindingContext.AddService>(_ => + { + FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json")); + if (!settings.Exists) + { + throw new InvalidOperationException("Failed to load YaeBlog configurations."); + } + + using StreamReader reader = settings.OpenText(); + using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd()); + JsonElement root = document.RootElement; + JsonElement optionSection = root.GetProperty(BlogOptions.OptionName); + + BlogOptions? result = optionSection.Deserialize(); + if (result is null) + { + throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json."); + } + + return new OptionsWrapper(result); + }); + + return bindingContext.GetRequiredService>(); + } +} diff --git a/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs b/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs new file mode 100644 index 0000000..b0613c8 --- /dev/null +++ b/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs @@ -0,0 +1,32 @@ +using System.CommandLine.Binding; +using Microsoft.Extensions.Options; +using YaeBlog.Core.Abstractions; +using YaeBlog.Core.Models; +using YaeBlog.Core.Services; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace YaeBlog.Commands.Binders; + +public sealed class EssayScanServiceBinder : BinderBase +{ + protected override IEssayScanService GetBoundValue(BindingContext bindingContext) + { + bindingContext.AddService(provider => + { + DeserializerBuilder deserializerBuilder = new(); + deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); + deserializerBuilder.IgnoreUnmatchedProperties(); + + SerializerBuilder serializerBuilder = new(); + serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); + + IOptions options = provider.GetRequiredService>(); + ILogger logger = provider.GetRequiredService>(); + + return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger); + }); + + return bindingContext.GetRequiredService(); + } +} diff --git a/YaeBlog/Commands/Binders/LoggerBinder.cs b/YaeBlog/Commands/Binders/LoggerBinder.cs new file mode 100644 index 0000000..47bd24c --- /dev/null +++ b/YaeBlog/Commands/Binders/LoggerBinder.cs @@ -0,0 +1,18 @@ +using System.CommandLine.Binding; + +namespace YaeBlog.Commands.Binders; + +public sealed class LoggerBinder : BinderBase> +{ + protected override ILogger GetBoundValue(BindingContext bindingContext) + { + bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole())); + bindingContext.AddService>(provider => + { + ILoggerFactory factory = provider.GetRequiredService(); + return factory.CreateLogger(); + }); + + return bindingContext.GetRequiredService>(); + } +} diff --git a/YaeBlog/Commands/CommandExtensions.cs b/YaeBlog/Commands/CommandExtensions.cs new file mode 100644 index 0000000..0ea6ac6 --- /dev/null +++ b/YaeBlog/Commands/CommandExtensions.cs @@ -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() + .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() + .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 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(), + 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(), new EssayScanServiceBinder()); + } +} diff --git a/YaeBlog/Components/EssayCard.razor b/YaeBlog/Components/EssayCard.razor index 6c4c34e..02fe971 100644 --- a/YaeBlog/Components/EssayCard.razor +++ b/YaeBlog/Components/EssayCard.razor @@ -1,3 +1,4 @@ +@using System.Text.Encodings.Web @using YaeBlog.Core.Models
@@ -13,7 +14,7 @@ @foreach (string key in Essay.Tags) { diff --git a/YaeBlog/Dockerfile b/YaeBlog/Dockerfile index 162308e..301432e 100644 --- a/YaeBlog/Dockerfile +++ b/YaeBlog/Dockerfile @@ -5,4 +5,4 @@ COPY bin/Release/net8.0/publish/ ./ COPY source/ ./source/ COPY appsettings.json . -ENTRYPOINT ["dotnet", "YaeBlog.dll"] +ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"] diff --git a/YaeBlog/Pages/About.razor b/YaeBlog/Pages/About.razor index d3847a0..7a7c784 100644 --- a/YaeBlog/Pages/About.razor +++ b/YaeBlog/Pages/About.razor @@ -38,10 +38,11 @@
- 主要是一个C#程序员,目前也在尝试写一点Rust。总体上对于编程语言的态度是“大家都是我的翅膀.jpg”。 + 主要是一个C#程序员,目前也在尝试写一点Rust。 + 总体上对于编程语言的态度是“大家都是我的翅膀.jpg”。 前后端分离的项目本当上手。 常常因为现实的压力而写一些C/C++。 - 对于Java和Go的评价很低。 + 对于Java和Go的评价很低。 日常使用ArchLinux。
diff --git a/YaeBlog/Pages/Archives.razor b/YaeBlog/Pages/Archives.razor index fcf87e9..d8d991b 100644 --- a/YaeBlog/Pages/Archives.razor +++ b/YaeBlog/Pages/Archives.razor @@ -20,7 +20,7 @@
- 时光图书馆,黑历史集散地。 + 时光图书馆,黑历史集散地。(๑◔‿◔๑)
@@ -47,7 +47,7 @@ diff --git a/YaeBlog/Pages/Essays.razor b/YaeBlog/Pages/Essays.razor index 0a05d69..9fdb685 100644 --- a/YaeBlog/Pages/Essays.razor +++ b/YaeBlog/Pages/Essays.razor @@ -1,9 +1,9 @@ @page "/blog/essays/{BlogKey}" +@using System.Text.Encodings.Web @using YaeBlog.Core.Abstractions @using YaeBlog.Core.Models @inject IEssayContentService Contents -@inject ITableOfContentService TableOfContent @inject NavigationManager NavigationInstance @@ -25,7 +25,9 @@ @foreach (string tag in _essay!.Tags) { } @@ -126,7 +128,7 @@ NavigationInstance.NavigateTo("/NotFound"); } - _headline = TableOfContent.Headlines[BlogKey]; + _headline = Contents.Headlines[BlogKey]; } private string GenerateSelectorUrl(string selectorId) diff --git a/YaeBlog/Pages/Tags.razor b/YaeBlog/Pages/Tags.razor index c7b75bc..c333621 100644 --- a/YaeBlog/Pages/Tags.razor +++ b/YaeBlog/Pages/Tags.razor @@ -26,7 +26,7 @@
- 在野外游荡的指针,走向未知的方向。 + 在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
diff --git a/YaeBlog/Program.cs b/YaeBlog/Program.cs index dd92dde..4cd3540 100644 --- a/YaeBlog/Program.cs +++ b/YaeBlog/Program.cs @@ -1,22 +1,11 @@ -using YaeBlog.Components; -using YaeBlog.Core.Extensions; +using System.CommandLine; +using YaeBlog.Commands; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +RootCommand rootCommand = new("YaeBlog CLI"); -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); -builder.Services.AddControllers(); -builder.Services.AddBlazorBootstrap(); -builder.AddYaeBlog(); +rootCommand.AddServeCommand(); +rootCommand.AddNewCommand(); +rootCommand.AddListCommand(); +rootCommand.AddWatchCommand(); -WebApplication application = builder.Build(); - -application.UseStaticFiles(); -application.UseAntiforgery(); -application.UseYaeBlog(); - -application.MapRazorComponents() - .AddInteractiveServerRenderMode(); -application.MapControllers(); - -await application.RunAsync(); +await rootCommand.InvokeAsync(args); diff --git a/YaeBlog/Properties/launchSettings.json b/YaeBlog/Properties/launchSettings.json index 37fd6e7..53831cc 100644 --- a/YaeBlog/Properties/launchSettings.json +++ b/YaeBlog/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "http://localhost:5275", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/YaeBlog/YaeBlog.csproj b/YaeBlog/YaeBlog.csproj index f43a4e6..976e76e 100644 --- a/YaeBlog/YaeBlog.csproj +++ b/YaeBlog/YaeBlog.csproj @@ -6,6 +6,7 @@ + diff --git a/YaeBlog/source/drafts/test-essay.md b/YaeBlog/source/drafts/test-essay.md new file mode 100644 index 0000000..2eb5d88 --- /dev/null +++ b/YaeBlog/source/drafts/test-essay.md @@ -0,0 +1,6 @@ +--- +title: test-essay +date: 2024-08-22T22:31:34.3177253+08:00 +tags: +--- + diff --git a/YaeBlog/source/2021-final.md b/YaeBlog/source/posts/2021-final.md similarity index 100% rename from YaeBlog/source/2021-final.md rename to YaeBlog/source/posts/2021-final.md diff --git a/YaeBlog/source/2021-final/1.png b/YaeBlog/source/posts/2021-final/1.png similarity index 100% rename from YaeBlog/source/2021-final/1.png rename to YaeBlog/source/posts/2021-final/1.png diff --git a/YaeBlog/source/2022-final.md b/YaeBlog/source/posts/2022-final.md similarity index 100% rename from YaeBlog/source/2022-final.md rename to YaeBlog/source/posts/2022-final.md diff --git a/YaeBlog/source/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg b/YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg similarity index 100% rename from YaeBlog/source/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg rename to YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg diff --git a/YaeBlog/source/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg b/YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg similarity index 100% rename from YaeBlog/source/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg rename to YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg diff --git a/YaeBlog/source/2022-summer-vacation.md b/YaeBlog/source/posts/2022-summer-vacation.md similarity index 100% rename from YaeBlog/source/2022-summer-vacation.md rename to YaeBlog/source/posts/2022-summer-vacation.md diff --git a/YaeBlog/source/2022-summer-vacation/result1.png b/YaeBlog/source/posts/2022-summer-vacation/result1.png similarity index 100% rename from YaeBlog/source/2022-summer-vacation/result1.png rename to YaeBlog/source/posts/2022-summer-vacation/result1.png diff --git a/YaeBlog/source/2023-final.md b/YaeBlog/source/posts/2023-final.md similarity index 100% rename from YaeBlog/source/2023-final.md rename to YaeBlog/source/posts/2023-final.md diff --git a/YaeBlog/source/2023-final/image-20240303165826486.png b/YaeBlog/source/posts/2023-final/image-20240303165826486.png similarity index 100% rename from YaeBlog/source/2023-final/image-20240303165826486.png rename to YaeBlog/source/posts/2023-final/image-20240303165826486.png diff --git a/YaeBlog/source/archlinux-sop.md b/YaeBlog/source/posts/archlinux-sop.md similarity index 100% rename from YaeBlog/source/archlinux-sop.md rename to YaeBlog/source/posts/archlinux-sop.md diff --git a/YaeBlog/source/big-homework.md b/YaeBlog/source/posts/big-homework.md similarity index 99% rename from YaeBlog/source/big-homework.md rename to YaeBlog/source/posts/big-homework.md index f8df144..040cb05 100644 --- a/YaeBlog/source/big-homework.md +++ b/YaeBlog/source/posts/big-homework.md @@ -2,7 +2,6 @@ title: 人生代码大作业初体验 tags: - 随笔 - - 感悟 typora-root-url: big-homework date: 2022-07-27 11:34:49 --- diff --git a/YaeBlog/source/big-homework/1.png b/YaeBlog/source/posts/big-homework/1.png similarity index 100% rename from YaeBlog/source/big-homework/1.png rename to YaeBlog/source/posts/big-homework/1.png diff --git a/YaeBlog/source/build-blog-record.md b/YaeBlog/source/posts/build-blog-record.md similarity index 99% rename from YaeBlog/source/build-blog-record.md rename to YaeBlog/source/posts/build-blog-record.md index e57c557..248b787 100644 --- a/YaeBlog/source/build-blog-record.md +++ b/YaeBlog/source/posts/build-blog-record.md @@ -3,7 +3,7 @@ title: 建立博客过程的记录 typora-root-url: 建立博客过程的记录 date: 2022-04-08 11:52:32 tags: - - 技术笔记 + - 技术笔记 --- diff --git a/YaeBlog/source/build-blog-record/1.png b/YaeBlog/source/posts/build-blog-record/1.png similarity index 100% rename from YaeBlog/source/build-blog-record/1.png rename to YaeBlog/source/posts/build-blog-record/1.png diff --git a/YaeBlog/source/build-blog-record/2.png b/YaeBlog/source/posts/build-blog-record/2.png similarity index 100% rename from YaeBlog/source/build-blog-record/2.png rename to YaeBlog/source/posts/build-blog-record/2.png diff --git a/YaeBlog/source/c-include-problems.md b/YaeBlog/source/posts/c-include-problems.md similarity index 100% rename from YaeBlog/source/c-include-problems.md rename to YaeBlog/source/posts/c-include-problems.md diff --git a/YaeBlog/source/c-include-problems/1.png b/YaeBlog/source/posts/c-include-problems/1.png similarity index 100% rename from YaeBlog/source/c-include-problems/1.png rename to YaeBlog/source/posts/c-include-problems/1.png diff --git a/YaeBlog/source/c-include-problems/2.png b/YaeBlog/source/posts/c-include-problems/2.png similarity index 100% rename from YaeBlog/source/c-include-problems/2.png rename to YaeBlog/source/posts/c-include-problems/2.png diff --git a/YaeBlog/source/compile-mediapipe.md b/YaeBlog/source/posts/compile-mediapipe.md similarity index 99% rename from YaeBlog/source/compile-mediapipe.md rename to YaeBlog/source/posts/compile-mediapipe.md index 15bf441..56efebb 100644 --- a/YaeBlog/source/compile-mediapipe.md +++ b/YaeBlog/source/posts/compile-mediapipe.md @@ -2,7 +2,7 @@ title: 编译MediaPipe框架 tags: - C/C++ - - 动作捕捉 + - 技术笔记 date: 2022-11-11 22:20:25 --- diff --git a/YaeBlog/source/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png b/YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png similarity index 100% rename from YaeBlog/source/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png rename to YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png diff --git a/YaeBlog/source/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png b/YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png similarity index 100% rename from YaeBlog/source/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png rename to YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png diff --git a/YaeBlog/source/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png b/YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png similarity index 100% rename from YaeBlog/source/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png rename to YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png diff --git a/YaeBlog/source/computer-architecture-pipeline.md b/YaeBlog/source/posts/computer-architecture-pipeline.md similarity index 99% rename from YaeBlog/source/computer-architecture-pipeline.md rename to YaeBlog/source/posts/computer-architecture-pipeline.md index 1947b6b..6228525 100644 --- a/YaeBlog/source/computer-architecture-pipeline.md +++ b/YaeBlog/source/posts/computer-architecture-pipeline.md @@ -2,7 +2,7 @@ title: 计算机系统结构——流水线复习 tags: - 计算机系统结构 - - 笔记 + - 学习资料 date: 2024-06-12 20:27:25 --- diff --git a/YaeBlog/source/computer-architecture-pipeline/image-20240612184855300.png b/YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.png similarity index 100% rename from YaeBlog/source/computer-architecture-pipeline/image-20240612184855300.png rename to YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.png diff --git a/YaeBlog/source/computer-architecture-pipeline/image-20240612184949777.png b/YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.png similarity index 100% rename from YaeBlog/source/computer-architecture-pipeline/image-20240612184949777.png rename to YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.png diff --git a/YaeBlog/source/computer-architecture-pipeline/image-20240612190426368.png b/YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.png similarity index 100% rename from YaeBlog/source/computer-architecture-pipeline/image-20240612190426368.png rename to YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.png diff --git a/YaeBlog/source/computer-architecture-pipeline/image-20240612192700169.png b/YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.png similarity index 100% rename from YaeBlog/source/computer-architecture-pipeline/image-20240612192700169.png rename to YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.png diff --git a/YaeBlog/source/computer-architecture-pipeline/image-20240612193301372.png b/YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.png similarity index 100% rename from YaeBlog/source/computer-architecture-pipeline/image-20240612193301372.png rename to YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.png diff --git a/YaeBlog/source/daily-linux-0.md b/YaeBlog/source/posts/daily-linux-0.md similarity index 100% rename from YaeBlog/source/daily-linux-0.md rename to YaeBlog/source/posts/daily-linux-0.md diff --git a/YaeBlog/source/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png b/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png similarity index 100% rename from YaeBlog/source/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png rename to YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png diff --git a/YaeBlog/source/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png b/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png similarity index 100% rename from YaeBlog/source/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png rename to YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png diff --git a/YaeBlog/source/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png b/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png similarity index 100% rename from YaeBlog/source/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png rename to YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png diff --git a/YaeBlog/source/daily-linux-1.md b/YaeBlog/source/posts/daily-linux-1.md similarity index 100% rename from YaeBlog/source/daily-linux-1.md rename to YaeBlog/source/posts/daily-linux-1.md diff --git a/YaeBlog/source/daily-linux-2.md b/YaeBlog/source/posts/daily-linux-2.md similarity index 100% rename from YaeBlog/source/daily-linux-2.md rename to YaeBlog/source/posts/daily-linux-2.md diff --git a/YaeBlog/source/daily-linux-2/df4211f6be2724b3b4725f7ce5a4078818844857.jpg b/YaeBlog/source/posts/daily-linux-2/df4211f6be2724b3b4725f7ce5a4078818844857.jpg similarity index 100% rename from YaeBlog/source/daily-linux-2/df4211f6be2724b3b4725f7ce5a4078818844857.jpg rename to YaeBlog/source/posts/daily-linux-2/df4211f6be2724b3b4725f7ce5a4078818844857.jpg diff --git a/YaeBlog/source/daily-linux-2/image-20230702205919301.png b/YaeBlog/source/posts/daily-linux-2/image-20230702205919301.png similarity index 100% rename from YaeBlog/source/daily-linux-2/image-20230702205919301.png rename to YaeBlog/source/posts/daily-linux-2/image-20230702205919301.png diff --git a/YaeBlog/source/daily-linux-2/image-20230720202327220.png b/YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png similarity index 100% rename from YaeBlog/source/daily-linux-2/image-20230720202327220.png rename to YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png diff --git a/YaeBlog/source/daily-linux-3.md b/YaeBlog/source/posts/daily-linux-3.md similarity index 100% rename from YaeBlog/source/daily-linux-3.md rename to YaeBlog/source/posts/daily-linux-3.md diff --git a/YaeBlog/source/daily-linux-3/Screenshot_20230904_144149.png b/YaeBlog/source/posts/daily-linux-3/Screenshot_20230904_144149.png similarity index 100% rename from YaeBlog/source/daily-linux-3/Screenshot_20230904_144149.png rename to YaeBlog/source/posts/daily-linux-3/Screenshot_20230904_144149.png diff --git a/YaeBlog/source/daily-linux-4.md b/YaeBlog/source/posts/daily-linux-4.md similarity index 100% rename from YaeBlog/source/daily-linux-4.md rename to YaeBlog/source/posts/daily-linux-4.md diff --git a/YaeBlog/source/daily-linux-4/Screenshot_20240309_115143.png b/YaeBlog/source/posts/daily-linux-4/Screenshot_20240309_115143.png similarity index 100% rename from YaeBlog/source/daily-linux-4/Screenshot_20240309_115143.png rename to YaeBlog/source/posts/daily-linux-4/Screenshot_20240309_115143.png diff --git a/YaeBlog/source/daily-linux-4/cfd17cff0701a8e8c69fecf247f17fc1-1709963611271-2.jpg b/YaeBlog/source/posts/daily-linux-4/cfd17cff0701a8e8c69fecf247f17fc1-1709963611271-2.jpg similarity index 100% rename from YaeBlog/source/daily-linux-4/cfd17cff0701a8e8c69fecf247f17fc1-1709963611271-2.jpg rename to YaeBlog/source/posts/daily-linux-4/cfd17cff0701a8e8c69fecf247f17fc1-1709963611271-2.jpg diff --git a/YaeBlog/source/daily-linux-4/image-20240309130329784.png b/YaeBlog/source/posts/daily-linux-4/image-20240309130329784.png similarity index 100% rename from YaeBlog/source/daily-linux-4/image-20240309130329784.png rename to YaeBlog/source/posts/daily-linux-4/image-20240309130329784.png diff --git a/YaeBlog/source/daily-linux-4/image-20240309131750535.png b/YaeBlog/source/posts/daily-linux-4/image-20240309131750535.png similarity index 100% rename from YaeBlog/source/daily-linux-4/image-20240309131750535.png rename to YaeBlog/source/posts/daily-linux-4/image-20240309131750535.png diff --git a/YaeBlog/source/daily-linux-4/image-20240309134847166.png b/YaeBlog/source/posts/daily-linux-4/image-20240309134847166.png similarity index 100% rename from YaeBlog/source/daily-linux-4/image-20240309134847166.png rename to YaeBlog/source/posts/daily-linux-4/image-20240309134847166.png diff --git a/YaeBlog/source/environment-setting.md b/YaeBlog/source/posts/environment-setting.md similarity index 100% rename from YaeBlog/source/environment-setting.md rename to YaeBlog/source/posts/environment-setting.md diff --git a/YaeBlog/source/environment-setting/6.png b/YaeBlog/source/posts/environment-setting/6.png similarity index 100% rename from YaeBlog/source/environment-setting/6.png rename to YaeBlog/source/posts/environment-setting/6.png diff --git a/YaeBlog/source/genshin-gacha-1.md b/YaeBlog/source/posts/genshin-gacha-1.md similarity index 99% rename from YaeBlog/source/genshin-gacha-1.md rename to YaeBlog/source/posts/genshin-gacha-1.md index 278895a..5b033de 100644 --- a/YaeBlog/source/genshin-gacha-1.md +++ b/YaeBlog/source/posts/genshin-gacha-1.md @@ -2,6 +2,7 @@ title: 原神抽卡研究一 tags: - 原神 + - 学习资料 date: 2022-12-31 13:38:19 --- diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-13-06-36-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-06-36-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-13-06-36-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-06-36-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-13-20-46-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-20-46-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-13-20-46-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-20-46-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-13-21-11-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-21-11-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-13-21-11-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-21-11-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-13-24-26-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-24-26-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-13-24-26-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-13-24-26-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-15-59-20-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-15-59-20-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-15-59-20-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-15-59-20-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-15-59-42-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-15-59-42-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-15-59-42-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-15-59-42-image.png diff --git a/YaeBlog/source/genshin-gacha-1/2022-12-31-16-00-10-image.png b/YaeBlog/source/posts/genshin-gacha-1/2022-12-31-16-00-10-image.png similarity index 100% rename from YaeBlog/source/genshin-gacha-1/2022-12-31-16-00-10-image.png rename to YaeBlog/source/posts/genshin-gacha-1/2022-12-31-16-00-10-image.png diff --git a/YaeBlog/source/install-pytorch.md b/YaeBlog/source/posts/install-pytorch.md similarity index 100% rename from YaeBlog/source/install-pytorch.md rename to YaeBlog/source/posts/install-pytorch.md diff --git a/YaeBlog/source/install-pytorch/1.png b/YaeBlog/source/posts/install-pytorch/1.png similarity index 100% rename from YaeBlog/source/install-pytorch/1.png rename to YaeBlog/source/posts/install-pytorch/1.png diff --git a/YaeBlog/source/install-pytorch/2.png b/YaeBlog/source/posts/install-pytorch/2.png similarity index 100% rename from YaeBlog/source/install-pytorch/2.png rename to YaeBlog/source/posts/install-pytorch/2.png diff --git a/YaeBlog/source/install-pytorch/3.png b/YaeBlog/source/posts/install-pytorch/3.png similarity index 100% rename from YaeBlog/source/install-pytorch/3.png rename to YaeBlog/source/posts/install-pytorch/3.png diff --git a/YaeBlog/source/install-pytorch/4.png b/YaeBlog/source/posts/install-pytorch/4.png similarity index 100% rename from YaeBlog/source/install-pytorch/4.png rename to YaeBlog/source/posts/install-pytorch/4.png diff --git a/YaeBlog/source/laptop-for-computer.md b/YaeBlog/source/posts/laptop-for-computer.md similarity index 100% rename from YaeBlog/source/laptop-for-computer.md rename to YaeBlog/source/posts/laptop-for-computer.md diff --git a/YaeBlog/source/laptop-for-computer/c.png b/YaeBlog/source/posts/laptop-for-computer/c.png similarity index 100% rename from YaeBlog/source/laptop-for-computer/c.png rename to YaeBlog/source/posts/laptop-for-computer/c.png diff --git a/YaeBlog/source/laptop-for-computer/clion.png b/YaeBlog/source/posts/laptop-for-computer/clion.png similarity index 100% rename from YaeBlog/source/laptop-for-computer/clion.png rename to YaeBlog/source/posts/laptop-for-computer/clion.png diff --git a/YaeBlog/source/laptop-for-computer/csharp.png b/YaeBlog/source/posts/laptop-for-computer/csharp.png similarity index 100% rename from YaeBlog/source/laptop-for-computer/csharp.png rename to YaeBlog/source/posts/laptop-for-computer/csharp.png diff --git a/YaeBlog/source/laptop-for-computer/web.png b/YaeBlog/source/posts/laptop-for-computer/web.png similarity index 100% rename from YaeBlog/source/laptop-for-computer/web.png rename to YaeBlog/source/posts/laptop-for-computer/web.png diff --git a/YaeBlog/source/linux-genshin-cloud.md b/YaeBlog/source/posts/linux-genshin-cloud.md similarity index 100% rename from YaeBlog/source/linux-genshin-cloud.md rename to YaeBlog/source/posts/linux-genshin-cloud.md diff --git a/YaeBlog/source/minecraft-wayland.md b/YaeBlog/source/posts/minecraft-wayland.md similarity index 100% rename from YaeBlog/source/minecraft-wayland.md rename to YaeBlog/source/posts/minecraft-wayland.md diff --git a/YaeBlog/source/minecraft-wayland/image-20240105212744116.png b/YaeBlog/source/posts/minecraft-wayland/image-20240105212744116.png similarity index 100% rename from YaeBlog/source/minecraft-wayland/image-20240105212744116.png rename to YaeBlog/source/posts/minecraft-wayland/image-20240105212744116.png diff --git a/YaeBlog/source/minecraft-wayland/image-20240105213439528.png b/YaeBlog/source/posts/minecraft-wayland/image-20240105213439528.png similarity index 100% rename from YaeBlog/source/minecraft-wayland/image-20240105213439528.png rename to YaeBlog/source/posts/minecraft-wayland/image-20240105213439528.png diff --git a/YaeBlog/source/minecraft-wayland/image-20240105213942445.png b/YaeBlog/source/posts/minecraft-wayland/image-20240105213942445.png similarity index 100% rename from YaeBlog/source/minecraft-wayland/image-20240105213942445.png rename to YaeBlog/source/posts/minecraft-wayland/image-20240105213942445.png diff --git a/YaeBlog/source/posts/parser-combinator-performance.md b/YaeBlog/source/posts/parser-combinator-performance.md new file mode 100644 index 0000000..fe49651 --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator-performance.md @@ -0,0 +1,287 @@ +--- +title: 解析器组合子和LR(1)分析方法的性能比较 +tags: + - 编译原理 + - 技术笔记 +date: 2024-08-19 14:31:00 +--- + +在使用解析器组合子编写编译器前端时,其与LR分析方法之间的性能差距是开发人员关心的重点问题之一。 + + + +## 背景 + +解析器组合子是一种使用函数式编程思想编写编译器前端中的词法分析器和语法分析器的编程范式。这种方式通过利用函数式编程语言将词法和语法直接嵌入在解析器代码中,大大提高了编写分析器的效率,降低了编写的难度。 + +但是这种以函数的嵌套和递归为核心的编程范式却带来了分析器运行效率下降的问题,在运行效率和编写效率之前的取舍成为了编译器研究人员十分关心的话题。因此,为了实际对比两种编写方式的运行效率,并为解析器组合子库的后续优化提升指明方向,本文中通过使用两种方式编写针对Pascal-S语言的解析器,设计了多组Benchmark对比两个解析器的运行效率。 + +### Pascal-S语言 + +本次测试使用的语言为Pascal-S语言,这是Pascal语言的一个简化版本,语言的具体语法如下所示。 + +```haskell +ProgramStart -> ProgramStruct +ProgramStruct -> ProgramHead ; ProgramBody . +ProgramHead -> program id (IdList) | program id +ProgramBody -> ConstDeclarations + VarDeclarations + SubprogramDeclarations + CompoundStatement +IdList -> , id IdList | : Type +ConstDeclarations -> $\epsilon$ | const ConstDeclaration ; +ConstDeclaration -> id = ConstValue | ConstDeclaration ; id = ConstValue +ConstValue -> +num | -num | num | 'letter' | true | false +VarDeclarations -> | var VarDeclaration ; +VarDeclaration -> id IdList | VarDeclaration ; id IdList +Type -> BasicType | array [ Period ] of BasicType +BasicType -> integer | real | boolean | char +Period -> digits .. digits | Period , digits .. digits +SubprogramDeclarations -> $\epsilon$ | SubprogramDeclarations Subprogram ; +Subprogram -> SubprogramHead ; SubprogramBody +SubprogramHead -> procedure id FormalParameter + | function id FormalParameter : BasicType +FormalParameter -> $\epsilon$ | () | ( ParameterList ) +ParameterList -> Parameter | ParameterList ; Parameter +Parameter -> VarParameter | ValueParameter +VarParameter -> var ValueParameter +ValueParameter -> id IdList +SubprogramBody -> ConstDeclarations + VarDeclarations + CompoundStatement +CompoundStatement -> begin StatementList end +StatementList -> Statement | StatementList ; Statement +Statement -> $\epsilon$ + | Variable assignOp Expression + | ProcedureCall + | CompoundStatement + | if Expression then Statement ElsePart + | for id assignOp Expression to Expression do Statement + | while Expression do Statement +Variable -> id IdVarPart +IdVarPart -> $\epsilon$ | [ ExpressionList ] +ProcedureCall -> id | id () | id ( ExpressionList ) +ElsePart -> $\epsilon$ | else Statement +ExpressionList -> Expression | ExpressionList , Expression +Expression -> SimpleExpression | SimpleExpression RelationOperator SimpleExpression +SimpleExpression -> Term | SimpleExpression AddOperator Term +Term -> Factor | Term MultiplyOperator Factor +Factor -> num + | true + | false + | Variable + | ( Expression ) + | id () + | id ( ExpressionList ) + | not Factor + | - Factor + | + Factor +AddOperator -> + | - | or +MultiplyOperator -> * | / | div | mod | and +RelationOperator -> = | <> | < | <= | > | >= +``` + +### 分析器的实现 + +本次测试中涉及到两个分析器的实现。两个分析器都是使用C#语言编写,运行在.NET 8.0.7平台上,使用X64 RyuJIT AVX2配置进行运行。 + +第一个分析器称为`Canon`。词法分析器使用朴素的自动机实现,没有使用任何的高端技术。将所有的词法规则写在一个巨大的自动机中,代码长度600行,是一个在可读性上堪称地狱,但是在效率上做到极致的方式。语法分析器虽然使用LR(1)分析方式,但是没有使用任何成熟的语法分析工具,而是自行实现的LR(1)语法分析器,在构建LR(1)分析表之后将分析表生成为C#代码编译到最终的程序集中,以求获得和传统语法分析器工具近似的运行效率。本分析器具体的实现可以在[jackfiled/Canon](https://git.rrricardo.top/post-guard/Canon)获得。 + +第二个分析器称为`CanonSharp`,词法分析器和语法分析都是使用自行实现的解析器组合子实现,解析器组合子的实现可以参考我的[上一篇文章](https://rrricardo.top/blog/essays/parser-combinator)。词法分析器的解析器以字符作为输入,解析之后输出词法令牌。语法分析器的解析器以词法令牌作为输入,解析之后输出抽象语法树。这个分析器的代码可以在[jackfiled/CanonSharp](https://git.rrricardo.top/jackfiled/CanonSharp)获得。需要说明的是,两个分析器最终的输出有一定的不同,`Canon`分析器的输出是完整的语法树,在语法定义中的每一个非终结节点都在语法树上存在,`CanonSharp`分析器的输出是抽象语法树,语法定义中的一些冗余的节点在抽象语法树中均不存在。这种实现上的差异可能会导致两个分析器在占用内存上存在一定的差异,但是并不会在运行效率上造成明显的影响。 + +## 基准测试程序的编写 + +测试程序使用[BenchmarkDotnet](https://github.com/dotnet/BenchmarkDotNet)作为驱动程序。这是一个.NET平台上简单易用的基准测试框架,可以按照编写单元测试的方式编写基准测试程序。本文中使用的测试代码如下所示: + +```csharp +public class GrammarParserBenchmark +{ + private readonly List _inputFiles = []; + + // CanonSharp + private readonly LexicalScanner _scanner = new(); + private readonly GrammarParser _parser = new(); + + // Canon + private readonly IGrammarParser _grammarParser = GeneratedGrammarParser.Instance; + + public GrammarParserBenchmark() + { + // 读取文件系统中的程序文件 + _inputFiles.AddRange(ReadOpenSet()); + } + + [Benchmark] + [ArgumentsSource(nameof(InputFiles))] + public Pascal.SyntaxTree.Program CanonSharpParse(int index) + { + IEnumerable tokens = _scanner.Tokenize(new StringReadState(_inputFiles[index])); + return _parser.Parse(tokens); + } + + [Benchmark] + [ArgumentsSource(nameof(InputFiles))] + public ProgramStruct CanonParse(int index) + { + Lexer lexer = new(); + IEnumerable tokens = lexer.Tokenize(new StringSourceReader(_inputFiles[index])); + + return _grammarParser.Analyse(tokens); + } + + public IEnumerable InputFiles() + { + for (int i = 0; i < _inputFiles.Count; i++) + { + yield return i; + } + } +} + +``` + +在编写测试程序中需要说明的是:在创建类的过程中,`CanonSharp`解析器的创建并没有计算在测试程序的运行时间中,但是`Canon`解析器的创建被计算在了测试程序的运行时间中。这是因为`Canon`解析器的词法分析器并不是一个无状态的解析器,不能重复的使用,只能在每次使用之前重新创建。 + +## 测试结果 + +在对原始数据进行数据处理之后我们绘制了如下的图。图中的横轴是输入测试文件的编号,图中的纵轴是`CanonSharp`解析器和`Canon`解析器运行时间的比值。从图中可以看出`CanonSharp`解析器的运行时间大约是`Canon`解析器运行时间的65到75倍,在某些极端的情况下可能会达到90倍。 + +![image-20240819140523087](./parser-combinator-performance/image-20240819140523087.png) + +## 结论 + +从相对的效率上说,使用解析器组合子编写的分析器在运行效率上大约要比使用表驱动的分析器慢上两个数量级;从绝对的运行时间上看,解析器组合子在面向一般长度的输入代码时运行的时间大约在毫秒量级。因此从Amdahl定律的角度和实际用户体验的角度出发,使用解析器组合子编写分析器不会导致最终的编译器在运行效率上的实际下降。同时,使用编译器组合子可以在保证代码高度可读性的条件下敏捷开发针对不同语言的编译器,在编写以教学和实验为目的的编译器中有着非常巨大的应用空间。 + +虽然在运行效率上,使用解析器组合子不会导致过多的担忧,但是从解析器组合子库的设计者角度出发,如何尽可能的提高解析器组合子库运行的效率并避免在运行时发生栈溢出错误都是重要的研究课题。 + +## 原始数据 + +| Method | index | Mean | Error | StdDev | +| ------------------- | ------ | ---------------: | -------------: | -------------: | +| **CanonSharpParse** | **0** | **353.627 μs** | **1.8258 μs** | **1.7078 μs** | +| CanonParse | 0 | 4.917 μs | 0.0130 μs | 0.0115 μs | +| **CanonSharpParse** | **1** | **653.161 μs** | **1.7194 μs** | **1.6084 μs** | +| CanonParse | 1 | 8.798 μs | 0.0280 μs | 0.0234 μs | +| **CanonSharpParse** | **2** | **630.299 μs** | **1.4253 μs** | **1.1902 μs** | +| CanonParse | 2 | 8.724 μs | 0.0179 μs | 0.0149 μs | +| **CanonSharpParse** | **3** | **387.579 μs** | **1.5613 μs** | **1.3038 μs** | +| CanonParse | 3 | 5.649 μs | 0.0098 μs | 0.0082 μs | +| **CanonSharpParse** | **4** | **356.240 μs** | **2.3247 μs** | **2.1745 μs** | +| CanonParse | 4 | 4.716 μs | 0.0062 μs | 0.0055 μs | +| CanonParse | 5 | 4.980 μs | 0.0154 μs | 0.0136 μs | +| **CanonSharpParse** | **6** | **979.392 μs** | **4.0016 μs** | **3.5473 μs** | +| CanonParse | 6 | 13.479 μs | 0.0428 μs | 0.0379 μs | +| **CanonSharpParse** | **7** | **600.507 μs** | **2.1920 μs** | **1.9431 μs** | +| CanonParse | 7 | 8.072 μs | 0.0134 μs | 0.0125 μs | +| **CanonSharpParse** | **8** | **524.578 μs** | **1.6822 μs** | **1.4047 μs** | +| CanonParse | 8 | 7.695 μs | 0.0254 μs | 0.0225 μs | +| **CanonSharpParse** | **9** | **315.395 μs** | **0.2694 μs** | **0.2250 μs** | +| CanonParse | 9 | 4.780 μs | 0.0156 μs | 0.0138 μs | +| **CanonSharpParse** | **10** | **510.408 μs** | **1.3935 μs** | **1.2353 μs** | +| CanonParse | 10 | 6.968 μs | 0.0203 μs | 0.0190 μs | +| **CanonSharpParse** | **11** | **444.388 μs** | **1.0900 μs** | **0.9663 μs** | +| CanonParse | 11 | 5.952 μs | 0.0116 μs | 0.0091 μs | +| **CanonSharpParse** | **12** | **523.964 μs** | **4.2651 μs** | **3.9896 μs** | +| CanonParse | 12 | 7.391 μs | 0.0106 μs | 0.0100 μs | +| **CanonSharpParse** | **13** | **290.775 μs** | **0.9223 μs** | **0.8176 μs** | +| CanonParse | 13 | 4.373 μs | 0.0096 μs | 0.0090 μs | +| **CanonSharpParse** | **14** | **669.914 μs** | **5.7419 μs** | **5.3710 μs** | +| CanonParse | 14 | 9.321 μs | 0.0195 μs | 0.0173 μs | +| **CanonSharpParse** | **15** | **329.593 μs** | **0.9726 μs** | **0.9098 μs** | +| CanonParse | 15 | 4.759 μs | 0.0124 μs | 0.0103 μs | +| **CanonSharpParse** | **16** | **389.419 μs** | **1.2592 μs** | **1.1778 μs** | +| CanonParse | 16 | 5.889 μs | 0.0188 μs | 0.0167 μs | +| **CanonSharpParse** | **17** | **390.669 μs** | **0.8737 μs** | **0.7295 μs** | +| CanonParse | 17 | 5.677 μs | 0.0114 μs | 0.0106 μs | +| **CanonSharpParse** | **18** | **1,783.017 μs** | **10.1009 μs** | **9.4484 μs** | +| CanonParse | 18 | 18.990 μs | 0.0787 μs | 0.0736 μs | +| **CanonSharpParse** | **19** | **1,832.231 μs** | **6.9808 μs** | **6.5299 μs** | +| CanonParse | 19 | 19.032 μs | 0.0968 μs | 0.0906 μs | +| **CanonSharpParse** | **20** | **1,397.956 μs** | **7.1686 μs** | **6.7055 μs** | +| CanonParse | 20 | 19.161 μs | 0.0301 μs | 0.0282 μs | +| **CanonSharpParse** | **21** | **1,920.760 μs** | **5.6297 μs** | **5.2661 μs** | +| CanonParse | 21 | 24.183 μs | 0.0531 μs | 0.0497 μs | +| **CanonSharpParse** | **22** | **1,937.031 μs** | **7.3762 μs** | **6.5388 μs** | +| CanonParse | 22 | 24.551 μs | 0.0411 μs | 0.0385 μs | +| **CanonSharpParse** | **23** | **827.700 μs** | **4.8717 μs** | **4.5570 μs** | +| CanonParse | 23 | 12.082 μs | 0.0233 μs | 0.0194 μs | +| **CanonSharpParse** | **24** | **901.852 μs** | **4.4028 μs** | **3.6765 μs** | +| CanonParse | 24 | 12.923 μs | 0.0282 μs | 0.0250 μs | +| **CanonSharpParse** | **25** | **664.637 μs** | **1.8827 μs** | **1.4699 μs** | +| CanonParse | 25 | 10.172 μs | 0.0288 μs | 0.0240 μs | +| CanonParse | 26 | 30.176 μs | 0.0517 μs | 0.0484 μs | +| **CanonSharpParse** | **27** | **2,655.310 μs** | **11.8520 μs** | **11.0864 μs** | +| CanonParse | 27 | 33.174 μs | 0.1600 μs | 0.1497 μs | +| **CanonSharpParse** | **28** | **355.309 μs** | **2.3985 μs** | **2.2436 μs** | +| CanonParse | 28 | 5.229 μs | 0.0153 μs | 0.0143 μs | +| CanonParse | 29 | 19.519 μs | 0.0676 μs | 0.0599 μs | +| CanonParse | 30 | 15.602 μs | 0.0260 μs | 0.0217 μs | +| **CanonSharpParse** | **31** | **419.458 μs** | **1.0384 μs** | **0.9714 μs** | +| CanonParse | 31 | 5.272 μs | 0.0073 μs | 0.0061 μs | +| **CanonSharpParse** | **32** | **2,148.625 μs** | **10.1403 μs** | **9.4852 μs** | +| CanonParse | 32 | 30.954 μs | 0.0335 μs | 0.0280 μs | +| CanonParse | 33 | 38.046 μs | 0.0833 μs | 0.0738 μs | +| CanonParse | 34 | 58.688 μs | 0.2072 μs | 0.1938 μs | +| CanonParse | 35 | 132.349 μs | 0.3033 μs | 0.2689 μs | +| **CanonSharpParse** | **36** | **2,251.311 μs** | **11.0098 μs** | **9.7599 μs** | +| CanonParse | 36 | 29.074 μs | 0.1068 μs | 0.0999 μs | +| CanonParse | 37 | 59.152 μs | 0.1773 μs | 0.1659 μs | +| CanonParse | 38 | 58.291 μs | 0.1733 μs | 0.1621 μs | +| CanonParse | 39 | 78.906 μs | 0.2064 μs | 0.1830 μs | +| CanonParse | 40 | 141.906 μs | 0.4749 μs | 0.4210 μs | +| CanonParse | 41 | 142.309 μs | 0.4521 μs | 0.4229 μs | +| CanonParse | 42 | 185.218 μs | 0.6247 μs | 0.5538 μs | +| CanonParse | 43 | 50.808 μs | 0.2504 μs | 0.2342 μs | +| **CanonSharpParse** | **44** | **1,461.460 μs** | **4.7521 μs** | **4.2126 μs** | +| CanonParse | 44 | 19.431 μs | 0.0391 μs | 0.0347 μs | +| CanonParse | 45 | 103.431 μs | 0.4603 μs | 0.4306 μs | +| CanonParse | 46 | 1,031.768 μs | 20.4417 μs | 29.3168 μs | +| CanonParse | 47 | 38.072 μs | 0.1364 μs | 0.1276 μs | +| CanonParse | 48 | 80.956 μs | 0.1958 μs | 0.1736 μs | +| CanonParse | 49 | 152.900 μs | 0.8712 μs | 0.7723 μs | +| CanonParse | 50 | 49.622 μs | 0.1501 μs | 0.1404 μs | +| **CanonSharpParse** | **51** | **1,150.554 μs** | **4.0367 μs** | **3.5784 μs** | +| CanonParse | 51 | 16.331 μs | 0.0426 μs | 0.0378 μs | +| CanonParse | 52 | 87.271 μs | 0.1576 μs | 0.1475 μs | +| CanonParse | 53 | 25.740 μs | 0.0657 μs | 0.0583 μs | +| CanonParse | 54 | 94.270 μs | 0.1679 μs | 0.1571 μs | +| CanonParse | 55 | 54.342 μs | 0.2107 μs | 0.1971 μs | +| CanonParse | 56 | 53,845.477 μs | 1,047.6308 μs | 1,661.6535 μs | +| CanonParse | 57 | 453.672 μs | 1.2739 μs | 1.1293 μs | +| CanonParse | 58 | 83.867 μs | 0.2897 μs | 0.2709 μs | +| CanonParse | 59 | 190.913 μs | 0.6662 μs | 0.5906 μs | +| CanonParse | 60 | 155.175 μs | 0.3931 μs | 0.3677 μs | +| CanonParse | 61 | 113.926 μs | 0.8090 μs | 0.7567 μs | +| CanonParse | 62 | 368.975 μs | 0.9096 μs | 0.8064 μs | +| CanonParse | 63 | 142.392 μs | 0.3123 μs | 0.2922 μs | +| CanonParse | 64 | 168.598 μs | 0.3496 μs | 0.3099 μs | +| CanonParse | 65 | 102.763 μs | 0.3745 μs | 0.3503 μs | +| CanonParse | 66 | 73.413 μs | 0.4360 μs | 0.3865 μs | +| CanonParse | 67 | 77.734 μs | 0.2080 μs | 0.1945 μs | +| CanonParse | 68 | 91.893 μs | 0.3471 μs | 0.3077 μs | +| CanonParse | 69 | 76.277 μs | 0.1656 μs | 0.1468 μs | +| CanonParse | 70 | 2.991 μs | 0.0050 μs | 0.0044 μs | +| **CanonSharpParse** | **71** | **1,225.389 μs** | **8.1085 μs** | **7.1880 μs** | +| CanonParse | 71 | 14.829 μs | 0.0372 μs | 0.0330 μs | +| CanonParse | 72 | 76.865 μs | 0.1630 μs | 0.1445 μs | +| **CanonSharpParse** | **73** | **838.646 μs** | **5.7775 μs** | **5.4043 μs** | +| CanonParse | 73 | 11.358 μs | 0.0200 μs | 0.0187 μs | +| **CanonSharpParse** | **74** | **850.399 μs** | **4.4444 μs** | **4.1573 μs** | +| CanonParse | 74 | 10.957 μs | 0.0126 μs | 0.0118 μs | +| CanonParse | 75 | 369.698 μs | 0.5347 μs | 0.4740 μs | +| CanonParse | 76 | 168.135 μs | 0.3753 μs | 0.3134 μs | +| CanonParse | 78 | 105.286 μs | 0.4302 μs | 0.3813 μs | +| CanonParse | 80 | 96.353 μs | 0.3182 μs | 0.2977 μs | +| CanonParse | 81 | 89.891 μs | 0.2942 μs | 0.2608 μs | +| CanonParse | 82 | 110.896 μs | 0.6884 μs | 0.6440 μs | +| CanonParse | 83 | 135.258 μs | 0.1460 μs | 0.1366 μs | +| CanonParse | 84 | 66.339 μs | 0.1386 μs | 0.1229 μs | +| CanonParse | 86 | 300.468 μs | 0.3804 μs | 0.3177 μs | +| CanonParse | 87 | 299.640 μs | 0.6466 μs | 0.6048 μs | +| CanonParse | 88 | 80.081 μs | 0.1701 μs | 0.1508 μs | +| CanonParse | 89 | 2,762.159 μs | 33.4829 μs | 31.3199 μs | +| CanonParse | 90 | 751.693 μs | 3.4209 μs | 3.1999 μs | +| CanonParse | 91 | 56.290 μs | 0.3105 μs | 0.2752 μs | +| CanonParse | 93 | 258.514 μs | 0.6600 μs | 0.5851 μs | + +> 原始数据中缺失的数据为对应测试运行失败。 \ No newline at end of file diff --git a/YaeBlog/source/posts/parser-combinator-performance/image-20240819140523087.png b/YaeBlog/source/posts/parser-combinator-performance/image-20240819140523087.png new file mode 100644 index 0000000..a06cdf6 --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator-performance/image-20240819140523087.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a34416d22a7b254db9904d2bb5ec5b7082a76df514c0316812f3477618f5c5eb +size 30728 diff --git a/YaeBlog/source/posts/parser-combinator.md b/YaeBlog/source/posts/parser-combinator.md new file mode 100644 index 0000000..661499a --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator.md @@ -0,0 +1,658 @@ +--- +title: 使用Parser Combinator编写编译器前端 +tags: + - 编译原理 + - C# + - 技术笔记 +date: 2024-08-13 18:09:10 +--- + +在函数式编程思想的指导下,使用Parser Combinator编写编译器前端。 + + + +在编译原理的课程上,我们往往会学习两种编写语法分析器的方式,分别是自顶向下的递归下降分析和`LL(1)`语法分析器和自底向上的`LR(1)`语法分析器。在课堂上,我们通常更加聚焦于学习`LL(1)`和`LR(1)`等几种给定BNF范式的语法就能自动生成表驱动分析器的分析技术。在实践中,以`Yacc`和`ANTLR`为代表语法分析器就是上述思想的经典实现。 + +但是如果我们调研实际中的编译器,会发现大多数的编译器都是使用递归下降的方式手动编写解析器,而不是使用各种的解析器生成工具。这往往是因为解析器生成器是难以扩展的,且生成的代码通常质量不高,在阅读和调试上都存在一系列的困难。但是手写解析器通常是一个非常枯燥的过程,写出来的代码也不能直观的表示语法。 + +因此我们希望能够有一种方法,在给定一种语言的语法之后就可以明确、简单地设计出该语言的词法分析器和语法分析器。上个世纪末出现了抽象能力极强函数式编程语言,有人注意到可以直接使用函数式编程语言的代码来表示各种文法的产生式,这类技术就被成为解析器组合子(Parser Combinator)。 + +## Parser Combinator初见 + +在没有学习编译原理的课程之前,如果我们需要编写一个识别文本的程序,我们往往会这样编写程序。例如识别一个布尔表达式: + +``` +bool_value := true | false; +``` + +我们会写出下面这种代码: + +```csharp +bool ParserBoolValue(string input) +{ + if (string.StartsWith("true")) + { + return true; + } + + if (string.StartsWith("false")) + { + return false; + } + + throw new InvalidInputException(); +} +``` + +这就是一个**解析器(Parser)**的原型实现:输入需要识别的字符串,输出识别的结果。在这个原型方法中,我们可以很容易发现两个问题: + +1. 解析器并不会**消耗**输入的字符串,缺少一个读取字符串的游标。 +2. 解析器输出结果的方式并不高明,输出的结果只能是解析成功情况的结果,解析失败的情况需要通过抛出一个自定义的异常`InvalidInputException`,但是考虑到一个解析器解析失败是非常常见的情况,在这里使用异常方式返回错误结果会导致非常严重的性能问题。 + +从上述两点出现,我们需要设计一个输入状态的抽象和一个解析结果的抽象才能设计出一个良好的解析器抽象。 + +首先是输入状态的抽象,这个输入状态需要具有一个*读取游标*的功能,可以读取当前值并向后移动,容易想到**链表**是一个非常适合该场景的数据结构(实际上在部分函数式编程语言中,列表的默认实现就是链表)。然后是解析结果的抽象,解析结果至少需要能够被表示两个状态——成功状态和失败状态。从面向对象设计的角度出发,我们可以设计一个解析结果基类,并分别实现一个成功解析结果基类和一个失败解析结果基类。但是从函数式编程的角度出发,我们可以设计一个类似于Rust中的枚举或者F#中的可区分联合之类的数据结构。(实际上目前C#的设计者正在设计并实现一个类似的东西[Discriminated Unions](https://github.com/dotnet/csharplang/issues/113),毕竟在代码中需要返回成功或者失败结果的函数太常见了,而使用异常是一个非常昂贵的方式,使用`Try`和`out`关键词也不是一个非常优雅的方式。) + +然后我们便可以设计出如下的解析器原型函数: + +```csharp +public abstract class Parser +{ + public ParseResult Parse(TState state) where TState : IReadState; +} +``` + +上面解析器基类中,类上的泛型参数`T`表示该解析器最终解析结果的类型,解析函数`Parse`的泛型参数`TState`是实现了输入状态`IReadState`的类型,返回的类型`ParseResult`就是上文中提到的解析结果基类。 + +在设计完解析器之后,该谈一谈**组合子(Combinator)**了。实际上组合子就是将多个解析器组合到一起的一系列函数,输入一个或者多个解析器,输出一个合并之后的解析器。容易想到,各种解析器组合在一起的方式千千万万,但是实际上我们只需要实现一系列基本的组合子,就可以通过综合使用各种解析器和组合子将各种需要的解析器和组合子实现出现。实际上,这也是解析器组合子思想的集中体现,通过基础的“砖块”(解析器)和“水泥“(组合子)设计和实现各种构建,最终建造出宏伟的高楼。 + +基础解析器和组合子的选择因人而异,但是一个常见的组合是: + +- 空解析器(empty),一个总是成功并返回指定值的解析器。 +- 短语解析器(term),解析指定的短语并返回对应结果的解析器。 +- 选择组合子(alternate),输入两个解析器,在相同的输入上执行并返回两者的结果。 +- 连接组合子(sequence),输入两个解析器,依次应用这两个解析器并返回最终的结果。 + +好了,让我们来建设高楼吧! + +## 实现一个C#的Parser Combinator库 + +### 解析器基类、输入状态接口和解析结果基类 + +在正式开始设计解析器和组合子之前,还请允许我再啰唆一下库中最为重要的那些接口和基类设计。 + +#### 输入状态接口 + +库中的输入状态接口详细定义如下: + +```csharp +/// +/// 输入流的读取状态 +/// +/// 输入流元素类型 +public interface IReadState +{ + public TToken Current { get; } + + public bool HasValue { get; } +} + +/// +/// 输入流的读取状态 +/// +/// 输入流元素类型 +/// 下一个读取状态的类型 +public interface IReadState : IReadState, IEquatable + where TState : IReadState +{ + /// + /// 下一个读取状态 + /// + TState Next { get; } +} +``` + +这是一个**泛型**的链表接口定义,而且将链表的数据部分和指针部分分拆到两个接口中。分拆的好处在于在定义对于某一个特定输入状态的处理函数是可以使用`IReadState`接口而不用考虑下一个节点的指针类型。 + +同时这个输入状态接口没有限制输入类型,而是使用一个泛型类型`TToken`。这大大增加了解析器组合子库的泛用性,不仅可以用于处理各种字符串解析的场景,对于二进制比特流也可以进行解析。在实际的编译器设计过程中也可以首先针对`char`类型的输入流设计一个词法分析器,然后在设计一个针对词法令牌类型的输入流设计一个语法分析器。 + +#### 解析结果基类 + +```csharp +/// +/// 解析器结果 +/// +/// 输入流类型 +/// 实际结果类型 +public abstract class ParseResult +{ + /// + /// 实际结果对象 + /// + public abstract T Value { get; } + + /// + /// 在当前结果上应用下一个解析器 + /// + /// 下一个解析器的函数 + /// 处理解析结果的后继函数 + /// 下一个解析器函数返回的解析结果类型 + /// 最终的解析结果类型 + /// + internal abstract ParseResult Next(Func> nextParser, + Func, ParseResult> continuation); + + /// + /// 映射结果 + /// + /// 映射结果的函数 + /// 映射结果函数返回解析结果的类型 + /// 最终的解析结果 + public abstract ParseResult Map(Func map); + + /// + /// 在成功或者失败解析结果上应用不同的后继函数 + /// + /// 在成功解析结果上应用的函数 + /// 在失败解析结构上应用的函数 + /// 最后返回解析结果的类型 + /// 最后的解析结果 + public abstract TResult CaseOf(Func, TResult> successfulHandler, + Func, TResult> failedHandler); +} +``` + +#### 解析器基类 + +```csharp +/// +/// 解析器抽象基类 +/// +/// 输入流类型 +/// 解析结果类型 +public abstract class Parser +{ + /// + /// 解析器运行函数 + /// + /// 解析的输入流状态 + /// 运行之后的后继函数 + /// 输入流状态类型 + /// 后继函数运行之后的解析结果类型 + /// + internal abstract ParseResult Run(TState state, + Func, ParseResult> continuation) + where TState : IReadState; + + public ParseResult Parse(TState state) where TState : IReadState + { + return Run(state); + } + + private ParseResult Run(TState state) where TState : IReadState + { + try + { + return Run(state, result => result); + } + catch (Exception e) + { + return ParseResultBuilder.Fail(e, state); + } + } + + public static Parser operator |(Parser a, Parser b) + => a.Alternative(b); +} +``` + +在解析器基类的设计上仍然使用了常见的CPS(Continuous Passing Style)设计范式,在解析器运行函数中需要传入一**后继函数**,对该解析器返回的解析结果进行进一个的处理之后再返回。在后续`sequence`类别的组合子设计中也将会利用这一点。 + +### 基础解析器和组合子的选择与设计 + +在设计解析器和组合子时,我们分成三类,分别是基础组合子(Basic),修改解析结果的解析器组合子(Modified Parser)和原组合子(Primitive Parser)。 + +基础组合子是将输入的一个或者多个解析器组合为新解析器的组合子,主要有选择组合子(Alternative Parser)、单子组合子(Bind Parser)、映射组合子(Map Parser)和下一个组合子(Next Parser)等。这里需要指出的是,上面选择的一些组合子理论上完全可以通过其他的组合子组合出来,但是有些非常常用的组合子使用其他组合子进行组合会导致运行时栈的嵌套深度过深,降低运行效率,因此选择这部分组合子单独实现可以在一定长度上提高整个库的运行效率。在库提供的若干个组合个中选择关键的组合子作为基础组合子进行实现也是一个效率的平衡。 + +这里贴一下两个重要但不复杂的基础组合子实现:选择组合子和单子组合子。 + +```csharp +/// +/// 选择解析器 +/// 如果第一个不成功则调用第二个 +/// +/// 第一个解析器 +/// 第二个解析器 +/// 输入流类型 +/// 解析器结果类型 +internal sealed class AlternativeParser(Parser first, Parser second) + : Parser +{ + internal override ParseResult Run(TState state, + Func, ParseResult> continuation) + { + return first.Run(state, result => result.CaseOf(continuation, _ => second.Run(state, continuation))); + } +} +``` + +```csharp +/// +/// 选择解析器 +/// 如果第一个不成功则调用第二个 +/// +/// 第一个解析器 +/// 第二个解析器 +/// 输入流类型 +/// 解析器结果类型 +internal sealed class AlternativeParser(Parser first, Parser second) + : Parser +{ + internal override ParseResult Run(TState state, + Func, ParseResult> continuation) + { + return first.Run(state, result => result.CaseOf(continuation, _ => second.Run(state, continuation))); + } +} +``` + +同时还有一个常用且复杂的组合子:修改组合子(Fix Parser)。这个组合子通过传入一个针对自己的修改函数获得一个新的解析器,在后面的组合子设计作为一个递归的实现出现。 + +```csharp +/// +/// 修正?解析器 +/// 感觉是一种递归的高级实现? +/// +/// +/// +/// +internal sealed class FixParser : Parser +{ + private readonly Parser _parser; + + public FixParser(Func, Parser> func) + { + _parser = func(this); + } + + internal override ParseResult Run(TState state, + Func, ParseResult> continuation) + => _parser.Run(state, continuation); +} +``` + +>设计出这种组合子的人确实很牛逼,感觉可以加入“这种代码我一辈子也写不出来”系列。 + +修改组合子是在传入解析器的基础上修改解析器结果的一类组合子。在这类组合子中较为重要的有:向前看组合子(Look Ahead Parser)和翻转组合子(Reverse Parser)。 + +向前看组合子在解析成功之后不会将输入状态向下移动,以此来达到向前看的效果。 + +```csharp +/// +/// 向前看解析器 +/// 使用传入的解析器向前解析 +/// 但是返回的结果中输入流读取状态不前移 +/// +/// 需要向前看的解析器 +/// 输入流令牌 +/// 返回的解析结果类型 +internal sealed class LookAheadParser(Parser parser) : ModifiedParser(parser) +{ + protected override ParseResult Succeed(TState state, + SuccessfulResult successfulResult) + => ParseResultBuilder.Succeed(successfulResult.Value, state); + + protected override ParseResult Fail(TState state, FailedResult failedResult) + => ParseResultBuilder.Fail($"Failed when looking ahead: {failedResult}", state); +} +``` + +翻转组合子负责翻转传入解析器的解析结果,常常和向前看组合子配合使用,达到向前看不到期望输入的效果。 + +```csharp +/// +/// 翻转结果的解析器 +/// 当成功时失败 +/// 当失败时返回指定的成功结果 +/// +/// 上游解析器 +/// 期望中的结果 +/// 输入流的类型 +/// 上游解析器结果类型 +/// 最终的返回结果 +internal sealed class ReverseParser(Parser parser, T result) + : ModifiedParser(parser) +{ + protected override ParseResult Succeed(TState state, + SuccessfulResult successfulResult) + => ParseResultBuilder.Fail($"Unexpected successful result: {successfulResult.Value}", + state); + + protected override ParseResult Fail(TState state, + FailedResult failedResult) + => ParseResultBuilder.Succeed(result, state); +} +``` + +元组合子理论上只有成功特定短语的`term`解析器和不识别任何内容直接成功的`empty`解析器两种,但是在这里我们还是额外多实现了一些,仍然是出现效率的考量。同时我们把短语解析器修改为了满足解析器(Satisfy Parser),通过传入一个判断谓词进行解析,提高了编写的灵活性。 + +```csharp +/// +/// 满足指定条件即成功的解析器 +/// +/// 满足的条件谓词 +/// 输入流类型 +internal sealed class SatisfyParser(Func predicate) : PrimitiveParser +{ + protected override ParseResult Run(TState state) + { + return state.HasValue && predicate(state.Current) + ? ParseResultBuilder.Succeed(state.Current, state.Next) + : ParseResultBuilder.Fail(state); + } +} +``` + +### 进阶组合子的设计和实现 + +目前的组合子库中大致一共实现了50个组合子,这里并不会解释涉及到的所有组合子,只列举一些我们实现过程中比较迷惑的组合子。 + +首先是顺序组合子(Sequence Parser)的实现,这个组合子输入一系列组合子,顺序应用所有的组合子之后输出结果。我们最开始的实现是: + +```csharp +public static Parser> Sequence(IEnumerable> parsers) + => parsers.Aggregate(Pure>([]), + (result, parser) => result.Bind( + x => parser.Map(x.Append))); +``` + +但是我们发现类似开源库对于这个组合子的实现是: + +```csharp +public static Parser> Sequence(IEnumerable> parsers) + => parsers.Reverse().Aggregate(Pure>([]), + (next, parser) => parser.Bind( + x => next.Map(result => result.Prepend(x)))); +``` + +造成上述实现差异的可能原因是闭包中捕获元素的不同:在我们的实现中传给`Map`函数的闭包需要捕获的变量`x`是`IEnumerable`类型,在开源库中的需要捕获的变量就是`T`类型的,这可能在一定程度上造成我们实现的运行效率不如开源库的效率。 + +使用指定解析器识别零次到多次的组合子`Many`的实现是一个典型的递归实现: + +```csharp + private static Parser> ManyRecursively(this Parser parser, + IEnumerable result) + => parser.Next(x => parser.ManyRecursively(result.Append(x)), result); + + public static Parser> Many(this Parser parser) + => parser.ManyRecursively([]); +``` + +这里一个挺有趣的问题是,在使用`IEnumerable`作为目标类型是,`[]`将会被初始化为哪个类型? + +我使用[sharplab](https://sharplab.io/#v2:D4AQTAjAsAUCAMACEEAsBuWsQGZlkQGFEBvWRC5PEVRAWQAoBKU8y9lHAHgEsA7AC4A+RAEMATuNEBPRAF5EAbQgAaMCpwBdTDHYBfWHqA==)进行了一波实验,发现C#编译器会默认实现为一个数组类型。 + +```csharp +public class C { + public void M() { + IEnumerable array = [1,2,3]; + } +} +``` + + + +![image-20240813214315576](./parser-combinator/image-20240813214315576.png) + +跳过组合子的实现则是使用我们之前提过的修改组合子(Fix Parser)进行的。 + +```csharp +public static Parser SkipMany(this Parser parser) + => Fix(self => parser.Next(_ => self, Unit.Instance)); +``` + +实际上这段Magic Code也可以使用显式递归函数的方式实现为: + +```csharp +public static Parser SkipManyRecursively(this Parser parser) + => parser.Next(_ => parser.SkipManyRecursively(), Unit.Instance); +``` + +在跳过直到组合子中也是使用修改组合子(Fix Parser)实现的: + +```csharp +public static Parser SkipTill(this Parser parser, + Parser terminator) + => Fix(self => terminator | parser.Right(self)); +``` + +在这段代码中使用到一个非常有趣的组合子 Right。这个组合子和它的孪生兄弟Left其实都是单子组合子的封装: + +```csharp +public static Parser Left(this Parser left, + Parser right) + => left.Bind(right.Map); + +public static Parser Right(this Parser left, + Parser right) + => left.Bind(_ => right) +``` + +实际的作用是Left 组合子返回左侧解析器返回的结果作为最终结果,Right 组合子返回右侧解析器返回的结果作为最终结果。 + +最后一个有趣的组合子是引用组合子(Quote Parser),这个组合子输入三个解析器,负责解析由左解析器和右解析器限定范围内的所有元素。非常适合与解析各种封闭范围的元素,例如字符串和注释。 + +```csharp +public static Parser> Quote(this Parser parser, + Parser left, Parser right) + => left.Right(parser.ManyTill(right)) +``` + +## Pascal词法分析器实战 + +在准备好丰富的砖瓦之后,我们可以先尝试盖一栋小楼,编写一个可以解析Pascal-S语言词法的词法分析器。 + +Pascal-S语言的词法约定如下所示: + +![image-20240813220521028](./parser-combinator/image-20240813220521028.png) + +![image-20240813220530717](./parser-combinator/image-20240813220530717.png) + +据此,我们可以开始编写对应的词法分析器。首先给出一个词法令牌的规定,将词法令牌分类为: + +- 关键词、 +- 整型常数、 +- 浮点常数、 +- 操作符、 +- 分隔符、 +- 标识符、 +- 字符、 +- 字符串。 + +使用枚举表示出上述的种类,将词法令牌类实现为: + +```csharp +public sealed class LexicalToken(LexicalTokenType type, string literalValue) : IEquatable +{ + public LexicalTokenType TokenType { get; } = type; + + public string LiteralValue { get; } = literalValue; + + public bool Equals(LexicalToken? other) => + other is not null && TokenType == other.TokenType && LiteralValue == other.LiteralValue; + + public override bool Equals(object? obj) => obj is LexicalToken other && Equals(other); + + public override int GetHashCode() => TokenType.GetHashCode() ^ LiteralValue.GetHashCode(); + + public override string ToString() => $"<{TokenType}>'{LiteralValue}'"; +} +``` + +对于不同的词法令牌种类实现对应的解析器。 + +首先是识别关键词的解析器: + +```csharp +public static Parser KeywordParser() + { + return from value in Choice(StringIgnoreCase("program"), + StringIgnoreCase("const"), + StringIgnoreCase("var"), + StringIgnoreCase("procedure"), + StringIgnoreCase("function"), + StringIgnoreCase("begin"), + StringIgnoreCase("end"), + StringIgnoreCase("array"), + StringIgnoreCase("of"), + StringIgnoreCase("if"), + StringIgnoreCase("then"), + StringIgnoreCase("else"), + StringIgnoreCase("for"), + StringIgnoreCase("to"), + StringIgnoreCase("do"), + StringIgnoreCase("integer"), + StringIgnoreCase("real"), + StringIgnoreCase("boolean"), + StringIgnoreCase("char"), + StringIgnoreCase("divide"), + StringIgnoreCase("not"), + StringIgnoreCase("mod"), + StringIgnoreCase("and"), + StringIgnoreCase("or"), + StringIgnoreCase("true"), + StringIgnoreCase("false"), + StringIgnoreCase("while")) + from _ in (AsciiLetter() | AsciiDigit() | Char('_')).LookAhead().Not() + select new LexicalToken(LexicalTokenType.Keyword, value); + } +``` + +考虑到在Pascal中关键词不区分大小,使用`StringIgnoreCase`作为定义关键词的解析器组合子,同时向前看识别下一个字符不是任何的字母、数字或者下划线——虽然在词法定义中说下一个字符应该是空格或者换行符,但是考虑到起始和结束关键词以及空格和换行符的统一处理,这里就采用了识别下一个字符不是字母、数字和下划线的方法。 + +然后是分隔符和运算法的解析器: + +```csharp +public static Parser DelimiterParser() + { + Parser semicolonParser = from token in Char(':') + from _ in Char('=').LookAhead().Not() + select new LexicalToken(LexicalTokenType.Delimiter, token.ToString()); + Parser periodParser = from token in Char('.') + from _ in Char('.').LookAhead().Not() + select new LexicalToken(LexicalTokenType.Delimiter, "."); + + Parser singleCharTokenParser = from token in Choice( + String(","), + String(";"), + String("("), + String(")"), + String("["), + String("]"), + String("..")) + select new LexicalToken(LexicalTokenType.Delimiter, token); + + return singleCharTokenParser | semicolonParser | periodParser; + } + +public static Parser OperatorParser() + { + Parser lessParser = from token in Char('<') + from _ in Char('=').LookAhead().Not() + select new LexicalToken(LexicalTokenType.Operator, "<"); + + Parser greaterParser = from token in Char('>') + from _ in Char('=').LookAhead().Not() + select new LexicalToken(LexicalTokenType.Operator, ">"); + + Parser otherParsers = from token in Choice( + String("="), + String("!="), + String("<="), + String(">="), + String("+"), + String("-"), + String("*"), + String("/"), + String(":=")) + select new LexicalToken(LexicalTokenType.Operator, token); + + return otherParsers | lessParser | greaterParser; + } +``` + +这两个解析器的编写主要是需要注意前缀相同符号的处理,比如冒号和赋值号、点和两个点、大于和大于等于以及小于和小于等于几个符号。 + +常数的识别就按照表达式编写就好: + +```csharp +public static Parser ConstIntegerParser() + { + return from nums in AsciiDigit().Many1() + from _ in Char('.').LookAhead().Not() + select new LexicalToken(LexicalTokenType.ConstInteger, new string(nums.ToArray())); + } + +public static Parser ConstFloatParser() + { + return from integer in AsciiDigit().Many1() + from _ in Char('.') + from fraction in AsciiDigit().Many1() + select new LexicalToken(LexicalTokenType.ConstFloat, + new string(integer.ToArray()) + '.' + new string(fraction.ToArray())); + } +``` + +标识符的识别和常数的识别类似: + +```csharp +public static Parser IdentifierParser() + { + return from first in AsciiLetter() | Char('_') + from second in (AsciiLetter() | AsciiDigit() | Char('_')).Many() + select new LexicalToken(LexicalTokenType.Identifier, first + new string(second.ToArray())); + } +``` + +注释的识别和字符串的识别使用我们前文中提到的引用组合子编写非常的方便: + +```csharp +public static Parser CommentParser() + { + return Any().Quote(Char('{'), Char('}')).Map(_ => Unit.Instance); + } + +public static Parser CharParser() + { + return from str in Any().Quote(Char('\'')).Map(x => new string(x.ToArray())) + select str.Length <= 1 + ? new LexicalToken(LexicalTokenType.Character, str) + : new LexicalToken(LexicalTokenType.String, str); + } +``` + +将注释、空格和换行符都作为无用的符号聚合到同一个解析器中: + +```csharp +public static Parser JunkParser() + { + return Space().Map(_ => Unit.Instance) | LineBreak().Map(_ => Unit.Instance) | CommentParser(); + } +``` + +最终将上述解析器组合到一起就构成了完整的词法分析器: + +```csharp +public static Parser> PascalParser() + { + return JunkParser().SkipTill(Choice(KeywordParser(), + DelimiterParser(), + OperatorParser(), + ConstIntegerParser(), + ConstFloatParser(), + CharParser(), + IdentifierParser())).Many(); + } +``` + +​ diff --git a/YaeBlog/source/posts/parser-combinator/image-20240813214315576.png b/YaeBlog/source/posts/parser-combinator/image-20240813214315576.png new file mode 100644 index 0000000..3cae470 --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator/image-20240813214315576.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8565dc29005087be0fff55f3c56331cdcc70cc977ccbf9ef8ae047f90e5b8f3 +size 29655 diff --git a/YaeBlog/source/posts/parser-combinator/image-20240813220521028.png b/YaeBlog/source/posts/parser-combinator/image-20240813220521028.png new file mode 100644 index 0000000..dc4ba0b --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator/image-20240813220521028.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:608d183ed3511d9c42ad2cd79f576c42609c99fb94a3014f8c841d5d95bf6a6d +size 164153 diff --git a/YaeBlog/source/posts/parser-combinator/image-20240813220530717.png b/YaeBlog/source/posts/parser-combinator/image-20240813220530717.png new file mode 100644 index 0000000..7f01189 --- /dev/null +++ b/YaeBlog/source/posts/parser-combinator/image-20240813220530717.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3db9d6b10173fd0bfa8dab9ae7c52b4f4ac151d2f2322cf34c055029e1560e69 +size 106064 diff --git a/YaeBlog/source/program-design-introduction.md b/YaeBlog/source/posts/program-design-introduction.md similarity index 100% rename from YaeBlog/source/program-design-introduction.md rename to YaeBlog/source/posts/program-design-introduction.md diff --git a/YaeBlog/source/program-design-introduction/1.png b/YaeBlog/source/posts/program-design-introduction/1.png similarity index 100% rename from YaeBlog/source/program-design-introduction/1.png rename to YaeBlog/source/posts/program-design-introduction/1.png diff --git a/YaeBlog/source/program-design-introduction/2.png b/YaeBlog/source/posts/program-design-introduction/2.png similarity index 100% rename from YaeBlog/source/program-design-introduction/2.png rename to YaeBlog/source/posts/program-design-introduction/2.png diff --git a/YaeBlog/source/program-design-introduction/3.png b/YaeBlog/source/posts/program-design-introduction/3.png similarity index 100% rename from YaeBlog/source/program-design-introduction/3.png rename to YaeBlog/source/posts/program-design-introduction/3.png diff --git a/YaeBlog/source/qt-learning.md b/YaeBlog/source/posts/qt-learning.md similarity index 100% rename from YaeBlog/source/qt-learning.md rename to YaeBlog/source/posts/qt-learning.md diff --git a/YaeBlog/source/qt-learning/1.png b/YaeBlog/source/posts/qt-learning/1.png similarity index 100% rename from YaeBlog/source/qt-learning/1.png rename to YaeBlog/source/posts/qt-learning/1.png diff --git a/YaeBlog/source/question-in-install-vs-2019.md b/YaeBlog/source/posts/question-in-install-vs-2019.md similarity index 100% rename from YaeBlog/source/question-in-install-vs-2019.md rename to YaeBlog/source/posts/question-in-install-vs-2019.md diff --git a/YaeBlog/source/question-in-install-vs-2019/1.png b/YaeBlog/source/posts/question-in-install-vs-2019/1.png similarity index 100% rename from YaeBlog/source/question-in-install-vs-2019/1.png rename to YaeBlog/source/posts/question-in-install-vs-2019/1.png diff --git a/YaeBlog/source/software-engineer.md b/YaeBlog/source/posts/software-engineer.md similarity index 99% rename from YaeBlog/source/software-engineer.md rename to YaeBlog/source/posts/software-engineer.md index 2c20caa..a5b0229 100644 --- a/YaeBlog/source/software-engineer.md +++ b/YaeBlog/source/posts/software-engineer.md @@ -2,7 +2,7 @@ title: 软件工程 tags: - 软件工程 - - 笔记 + - 学习资料 date: 2024-06-12 20:27:25 --- diff --git a/YaeBlog/source/software-engineer/image-20240620211321957.png b/YaeBlog/source/posts/software-engineer/image-20240620211321957.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620211321957.png rename to YaeBlog/source/posts/software-engineer/image-20240620211321957.png diff --git a/YaeBlog/source/software-engineer/image-20240620212101864.png b/YaeBlog/source/posts/software-engineer/image-20240620212101864.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620212101864.png rename to YaeBlog/source/posts/software-engineer/image-20240620212101864.png diff --git a/YaeBlog/source/software-engineer/image-20240620214307906.png b/YaeBlog/source/posts/software-engineer/image-20240620214307906.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620214307906.png rename to YaeBlog/source/posts/software-engineer/image-20240620214307906.png diff --git a/YaeBlog/source/software-engineer/image-20240620214739548.png b/YaeBlog/source/posts/software-engineer/image-20240620214739548.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620214739548.png rename to YaeBlog/source/posts/software-engineer/image-20240620214739548.png diff --git a/YaeBlog/source/software-engineer/image-20240620215022645.png b/YaeBlog/source/posts/software-engineer/image-20240620215022645.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620215022645.png rename to YaeBlog/source/posts/software-engineer/image-20240620215022645.png diff --git a/YaeBlog/source/software-engineer/image-20240620220155962.png b/YaeBlog/source/posts/software-engineer/image-20240620220155962.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620220155962.png rename to YaeBlog/source/posts/software-engineer/image-20240620220155962.png diff --git a/YaeBlog/source/software-engineer/image-20240620224318982.png b/YaeBlog/source/posts/software-engineer/image-20240620224318982.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620224318982.png rename to YaeBlog/source/posts/software-engineer/image-20240620224318982.png diff --git a/YaeBlog/source/software-engineer/image-20240620224519243.png b/YaeBlog/source/posts/software-engineer/image-20240620224519243.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240620224519243.png rename to YaeBlog/source/posts/software-engineer/image-20240620224519243.png diff --git a/YaeBlog/source/software-engineer/image-20240621114540033.png b/YaeBlog/source/posts/software-engineer/image-20240621114540033.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240621114540033.png rename to YaeBlog/source/posts/software-engineer/image-20240621114540033.png diff --git a/YaeBlog/source/software-engineer/image-20240623160826903.png b/YaeBlog/source/posts/software-engineer/image-20240623160826903.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240623160826903.png rename to YaeBlog/source/posts/software-engineer/image-20240623160826903.png diff --git a/YaeBlog/source/software-engineer/image-20240623162229404.png b/YaeBlog/source/posts/software-engineer/image-20240623162229404.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240623162229404.png rename to YaeBlog/source/posts/software-engineer/image-20240623162229404.png diff --git a/YaeBlog/source/software-engineer/image-20240623163647935.png b/YaeBlog/source/posts/software-engineer/image-20240623163647935.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240623163647935.png rename to YaeBlog/source/posts/software-engineer/image-20240623163647935.png diff --git a/YaeBlog/source/software-engineer/image-20240623170058679.png b/YaeBlog/source/posts/software-engineer/image-20240623170058679.png similarity index 100% rename from YaeBlog/source/software-engineer/image-20240623170058679.png rename to YaeBlog/source/posts/software-engineer/image-20240623170058679.png diff --git a/YaeBlog/source/spring-boot-custom-authorize.md b/YaeBlog/source/posts/spring-boot-custom-authorize.md similarity index 100% rename from YaeBlog/source/spring-boot-custom-authorize.md rename to YaeBlog/source/posts/spring-boot-custom-authorize.md diff --git a/YaeBlog/source/spring-boot-custom-authorize/image-20230727175807814.png b/YaeBlog/source/posts/spring-boot-custom-authorize/image-20230727175807814.png similarity index 100% rename from YaeBlog/source/spring-boot-custom-authorize/image-20230727175807814.png rename to YaeBlog/source/posts/spring-boot-custom-authorize/image-20230727175807814.png diff --git a/YaeBlog/source/spring-boot-custom-authorize/image-20230727175955817.png b/YaeBlog/source/posts/spring-boot-custom-authorize/image-20230727175955817.png similarity index 100% rename from YaeBlog/source/spring-boot-custom-authorize/image-20230727175955817.png rename to YaeBlog/source/posts/spring-boot-custom-authorize/image-20230727175955817.png diff --git a/YaeBlog/source/update-windows-break-archlinux.md b/YaeBlog/source/posts/update-windows-break-archlinux.md similarity index 100% rename from YaeBlog/source/update-windows-break-archlinux.md rename to YaeBlog/source/posts/update-windows-break-archlinux.md diff --git a/YaeBlog/source/using-vpn-elegant.md b/YaeBlog/source/posts/using-vpn-elegant.md similarity index 100% rename from YaeBlog/source/using-vpn-elegant.md rename to YaeBlog/source/posts/using-vpn-elegant.md diff --git a/YaeBlog/source/vscode-in-browser.md b/YaeBlog/source/posts/vscode-in-browser.md similarity index 100% rename from YaeBlog/source/vscode-in-browser.md rename to YaeBlog/source/posts/vscode-in-browser.md diff --git a/YaeBlog/source/vscode-in-browser/1.png b/YaeBlog/source/posts/vscode-in-browser/1.png similarity index 100% rename from YaeBlog/source/vscode-in-browser/1.png rename to YaeBlog/source/posts/vscode-in-browser/1.png diff --git a/YaeBlog/source/vscode-in-browser/2.png b/YaeBlog/source/posts/vscode-in-browser/2.png similarity index 100% rename from YaeBlog/source/vscode-in-browser/2.png rename to YaeBlog/source/posts/vscode-in-browser/2.png diff --git a/YaeBlog/source/whats-new-of-dotnet-8.md b/YaeBlog/source/posts/whats-new-of-dotnet-8.md similarity index 100% rename from YaeBlog/source/whats-new-of-dotnet-8.md rename to YaeBlog/source/posts/whats-new-of-dotnet-8.md diff --git a/YaeBlog/source/whats-new-of-dotnet-8/AOTOptimizations4.png b/YaeBlog/source/posts/whats-new-of-dotnet-8/AOTOptimizations4.png similarity index 100% rename from YaeBlog/source/whats-new-of-dotnet-8/AOTOptimizations4.png rename to YaeBlog/source/posts/whats-new-of-dotnet-8/AOTOptimizations4.png diff --git a/YaeBlog/source/whats-new-of-dotnet-8/image-20231122100930849.png b/YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122100930849.png similarity index 100% rename from YaeBlog/source/whats-new-of-dotnet-8/image-20231122100930849.png rename to YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122100930849.png diff --git a/YaeBlog/source/whats-new-of-dotnet-8/image-20231122101012416.png b/YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122101012416.png similarity index 100% rename from YaeBlog/source/whats-new-of-dotnet-8/image-20231122101012416.png rename to YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122101012416.png diff --git a/YaeBlog/source/whats-new-of-dotnet-8/image-20231122101142080.png b/YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122101142080.png similarity index 100% rename from YaeBlog/source/whats-new-of-dotnet-8/image-20231122101142080.png rename to YaeBlog/source/posts/whats-new-of-dotnet-8/image-20231122101142080.png diff --git a/YaeBlog/source/wsl-setup-csapp.md b/YaeBlog/source/posts/wsl-setup-csapp.md similarity index 100% rename from YaeBlog/source/wsl-setup-csapp.md rename to YaeBlog/source/posts/wsl-setup-csapp.md diff --git a/YaeBlog/source/wsl-setup-csapp/1.png b/YaeBlog/source/posts/wsl-setup-csapp/1.png similarity index 100% rename from YaeBlog/source/wsl-setup-csapp/1.png rename to YaeBlog/source/posts/wsl-setup-csapp/1.png diff --git a/YaeBlog/source/wsl-setup-csapp/2.png b/YaeBlog/source/posts/wsl-setup-csapp/2.png similarity index 100% rename from YaeBlog/source/wsl-setup-csapp/2.png rename to YaeBlog/source/posts/wsl-setup-csapp/2.png diff --git a/YaeBlog/source/wsl-setup-csapp/3.png b/YaeBlog/source/posts/wsl-setup-csapp/3.png similarity index 100% rename from YaeBlog/source/wsl-setup-csapp/3.png rename to YaeBlog/source/posts/wsl-setup-csapp/3.png