diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index 5894445ed20ec1dcdedfa37694f38c732516bca0..0e5dd123cc8a5f50f5ecc31ea175cabaa0bab53c 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -27,6 +27,9 @@ sealed partial class Build : NukeBuild [Parameter("publish-trimmed (default: false)")] readonly bool publishTrimmed = false; + [Parameter("publish-ready-to-run (default: true)")] + readonly bool publishReadyToRun = true; + Target Publish => td => td .After(Restore) .Executes(() => @@ -40,7 +43,7 @@ sealed partial class Build : NukeBuild .SetSelfContained(publishSelfContained) .SetPublishSingleFile(publishSingleFile) .SetPublishTrimmed(publishTrimmed) - .SetAuthors("Bruno Massa") + .SetPublishReadyToRun(publishReadyToRun) .SetVersion(CurrentVersion) .SetAssemblyVersion(CurrentVersion) .SetInformationalVersion(CurrentVersion) diff --git a/.editorconfig b/.editorconfig index ad36a90334510e08067810c742edcd495a8068f5..ca0a0b95304e3eba862b3e4cfa39df432cff49c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -68,3 +68,5 @@ dotnet_diagnostic.CS8019.severity = warning [**/obj/**/*.cs] dotnet_diagnostic.CS8019.severity = none # disable on debug genereated files +exclude = true +generated_code = true diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 930db2d70579deaefe5f4319137536fe0511186a..a534aacdcff8d3eabcb98ae272886bfad6f58371 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -82,6 +82,10 @@ "type": "string", "description": "publish-directory (default: ./publish/{runtimeIdentifier})" }, + "publishReadyToRun": { + "type": "boolean", + "description": "publish-ready-to-run (default: true)" + }, "publishSelfContained": { "type": "boolean", "description": "publish-self-contained (default: true)" diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index d70757e3f13daa2ea19b8b4377268cae6e553414..31710dde2c85c55179a1284218629ee3fceeb6c6 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -26,7 +26,7 @@ public abstract class BaseGeneratorCommand /// /// The front matter parser instance. The default is YAML. /// - protected IFrontMatterParser frontMatterParser { get; } = new YAMLParser(); + protected IMetadataParser frontMatterParser { get; } = new YAMLParser(); /// /// The stopwatch reporter. diff --git a/source/CheckLinkCommand.cs b/source/CheckLinkCommand.cs index f978458ce7f321e6f98832f204f3e6ce2ad6dfe0..a98dc92339aa191cc13a3cd6ea058b8348867e5c 100644 --- a/source/CheckLinkCommand.cs +++ b/source/CheckLinkCommand.cs @@ -14,8 +14,8 @@ public sealed partial class CheckLinkCommand(CheckLinkOptions settings, ILogger { [GeneratedRegex(@"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)")] - private static partial Regex MyRegex(); - private static readonly Regex linkRegex = MyRegex(); + private static partial Regex URLRegex(); + private static readonly Regex urlRegex = URLRegex(); private const int retriesCount = 3; private readonly TimeSpan retryInterval = TimeSpan.FromSeconds(1); private HttpClient httpClient = null!; @@ -77,7 +77,7 @@ public sealed partial class CheckLinkCommand(CheckLinkOptions settings, ILogger { var fileNameSanitized = filePath[directoryPath.Length..].Trim('/', '\\'); var fileText = File.ReadAllText(filePath); - var matches = linkRegex.Matches(fileText); + var matches = urlRegex.Matches(fileText); if (matches.Count == 0) { LogInformation("{fileName}: no links found", fileNameSanitized); diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index a6a6d37acab89bac8fc909b280883b60e57efbbc..06dd373281ad8ffd682721f86f6973725ec8a3f1 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -25,7 +25,7 @@ public static class SiteHelper /// Creates the pages dictionary. /// /// - public static Site Init(string configFile, IGenerateOptions options, IFrontMatterParser frontMatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) + public static Site Init(string configFile, IGenerateOptions options, IMetadataParser frontMatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) { ArgumentNullException.ThrowIfNull(stopwatch); @@ -92,7 +92,7 @@ public static class SiteHelper /// The front matter parser. /// The site settings file. /// The site settings. - private static SiteSettings ParseSettings(string configFile, IGenerateOptions options, IFrontMatterParser frontMatterParser) + private static SiteSettings ParseSettings(string configFile, IGenerateOptions options, IMetadataParser frontMatterParser) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(frontMatterParser); diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index 4da2e2da1ffe347746264be2243b1e2668e40344..bcacae6c145ccbad688b97f87c1c225668f9dd52 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -17,7 +17,7 @@ public sealed class SourceFileWatcher : IFileWatcher, IDisposable public void Start(string SourceAbsolutePath, Action OnSourceFileChanged) { ArgumentNullException.ThrowIfNull(OnSourceFileChanged); - + fileWatcher = new FileSystemWatcher { Path = SourceAbsolutePath, diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index f415158330e110020701d63ed88c093221d82360..b4ac972f334c5c0a939b9adb06ebf1b85829b326 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using YamlDotNet.Serialization; namespace SuCoS.Models; @@ -7,48 +6,49 @@ namespace SuCoS.Models; /// A scafold structure to help creating system-generated content, like /// tag, section or index pages /// +[YamlSerializable] public class FrontMatter : IFrontMatter { #region IFrontMatter /// - public string? Title { get; init; } = string.Empty; + public string? Title { get; set; } = string.Empty; /// public string? Type { get; set; } = "page"; /// - public string? URL { get; init; } + public string? URL { get; set; } /// - public bool? Draft { get; init; } + public bool? Draft { get; set; } /// - public Collection? Aliases { get; init; } + public List? Aliases { get; set; } /// public string? Section { get; set; } = string.Empty; /// - public DateTime? Date { get; init; } + public DateTime? Date { get; set; } /// - public DateTime? LastMod { get; init; } + public DateTime? LastMod { get; set; } /// - public DateTime? PublishDate { get; init; } + public DateTime? PublishDate { get; set; } /// - public DateTime? ExpiryDate { get; init; } + public DateTime? ExpiryDate { get; set; } /// - public int Weight { get; init; } + public int Weight { get; set; } /// - public Collection? Tags { get; init; } + public List? Tags { get; set; } /// - public Collection? ResourceDefinitions { get; set; } + public List? ResourceDefinitions { get; set; } /// [YamlIgnore] diff --git a/source/Models/FrontMatterResources.cs b/source/Models/FrontMatterResources.cs index 6e63b9e8689cb808353a0caca08a614f0da06978..093b0135253eb8baeb8f17e03addc9dd1f610724 100644 --- a/source/Models/FrontMatterResources.cs +++ b/source/Models/FrontMatterResources.cs @@ -1,10 +1,12 @@ using Microsoft.Extensions.FileSystemGlobbing; +using YamlDotNet.Serialization; namespace SuCoS.Models; /// /// Basic structure needed to generate user content and system content /// +[YamlSerializable] public class FrontMatterResources : IFrontMatterResources { /// diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 50d736125c7fe3e68137d3a9c9daff4d0ed19af2..27d6c2a1809c369d6387bcd26b9464cf3d07e69d 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -83,7 +83,7 @@ public interface IFrontMatter : IParams, IFile /// List URL, it will be parsed as liquid templates, so you can use page variables. /// /// - Collection? Aliases { get; } + List? Aliases { get; } /// /// Page weight. Used for sorting by default. @@ -93,12 +93,12 @@ public interface IFrontMatter : IParams, IFile /// /// A list of tags, if any. /// - Collection? Tags { get; } + List? Tags { get; } /// /// List of resource definitions. /// - Collection? ResourceDefinitions { get; } + List? ResourceDefinitions { get; set; } /// /// Raw content from the Markdown file, bellow the front matter. diff --git a/source/Models/Page.cs b/source/Models/Page.cs index ff2201dd34a31805c4f0aa1a949612ee9ac7d24e..503ef968ee0859588f427dcad310949caaa3daba 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -29,7 +29,7 @@ public class Page : IPage public bool? Draft => frontMatter.Draft; /// - public Collection? Aliases => frontMatter.Aliases; + public List? Aliases => frontMatter.Aliases; /// public string? Section => frontMatter.Section; @@ -50,10 +50,14 @@ public class Page : IPage public int Weight => frontMatter.Weight; /// - public Collection? Tags => frontMatter.Tags; + public List? Tags => frontMatter.Tags; /// - public Collection? ResourceDefinitions => frontMatter.ResourceDefinitions; + public List? ResourceDefinitions + { + get => frontMatter.ResourceDefinitions; + set => frontMatter.ResourceDefinitions = value; + } /// public string RawContent => frontMatter.RawContent; @@ -445,12 +449,13 @@ endif } filename = Path.GetFileNameWithoutExtension(filename) + extention; - var resource = new Resource(resourceFilename) + var resource = new Resource() { Title = title, FileName = filename, Permalink = Path.Combine(Permalink!, filename), - Params = resourceParams + Params = resourceParams, + SourceFullPath = resourceFilename }; Resources.Add(resource); } diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index 5082bd7569eeaa5d9c98e59554e4a18a09d68572..0bae647079d84993c1ad6e7d700477a0c4b4af44 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -12,23 +12,14 @@ public class Resource : IResource public string? FileName { get; set; } /// - public string SourceFullPath { get; set; } + public required string SourceFullPath { get; set; } /// - public string? SourceRelativePath => throw new NotImplementedException(); + public string? SourceRelativePath => null; /// public string? Permalink { get; set; } /// public Dictionary Params { get; set; } = []; - - /// - /// Default constructor. - /// - /// - public Resource(string path) - { - SourceFullPath = path; - } } diff --git a/source/Models/Site.cs b/source/Models/Site.cs index f6f26d77e9548f1f6847ea63f358570da327fdc3..81162ffa16ee243ea98de1be228b92809ecca479 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -166,7 +166,7 @@ public class Site : ISite /// /// The front matter parser instance. The default is YAML. /// - private readonly IFrontMatterParser frontMatterParser; + private readonly IMetadataParser frontMatterParser; private IEnumerable? pagesCache; @@ -185,7 +185,7 @@ public class Site : ISite public Site( in IGenerateOptions options, in SiteSettings settings, - in IFrontMatterParser frontMatterParser, + in IMetadataParser frontMatterParser, in ILogger logger, ISystemClock? clock) { Options = options; diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index e2efd0cc5a045133d99269ee52eb88ea13240949..29ec6514a332ffe6af3e69d85e878382467017c3 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -1,8 +1,11 @@ +using YamlDotNet.Serialization; + namespace SuCoS.Models; /// /// The main configuration of the program, extracted from the app.yaml file. /// +[YamlSerializable] public class SiteSettings : IParams { /// @@ -13,12 +16,12 @@ public class SiteSettings : IParams /// /// Site description /// - public string? Description { get; set; } + public string? Description { get; set; } = string.Empty; /// /// Copyright information /// - public string? Copyright { get; set; } + public string? Copyright { get; set; } = string.Empty; /// /// The base URL that will be used to build public links. @@ -36,4 +39,4 @@ public class SiteSettings : IParams public Dictionary Params { get; set; } = []; #endregion IParams -} \ No newline at end of file +} diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IMetadataParser.cs similarity index 91% rename from source/Parser/IFrontmatterParser.cs rename to source/Parser/IMetadataParser.cs index 67a536c412bc3f6965987fdc954f45b5e8662745..959e84c19569746dc28ee850ee50ffb85c3eda5f 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IMetadataParser.cs @@ -3,9 +3,9 @@ using SuCoS.Models; namespace SuCoS.Parser; /// -/// Responsible for parsing the content front matter +/// Responsible for parsing the content metadata /// -public interface IFrontMatterParser +public interface IMetadataParser { /// /// Extract the front matter from the content file. diff --git a/source/Parser/ParamsConverter.cs b/source/Parser/ParamsConverter.cs new file mode 100644 index 0000000000000000000000000000000000000000..952b3eaf21dc69e37327144538f30fc8b116054a --- /dev/null +++ b/source/Parser/ParamsConverter.cs @@ -0,0 +1,98 @@ +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace SuCoS.Parser; + +/// +/// A custom YAML type converter for dictionaries with string keys and object values. +/// +public class ParamsConverter : IYamlTypeConverter +{ + /// + /// Checks if the converter can handle the specified type. + /// + /// The type to check. + /// True if the type is a dictionary with string keys and object values, false otherwise. + public bool Accepts(Type type) + { + return type == typeof(Dictionary); + } + + /// + /// Reads a YAML stream and deserializes it into a dictionary. + /// + /// The YAML parser. + /// The type of the object to deserialize. + /// A dictionary deserialized from the YAML stream. + public object ReadYaml(IParser parser, Type type) + { + var dictionary = new Dictionary(); + + if (!parser.TryConsume(out _)) + { + // throw new YamlException("Expected a mapping start."); + } + + while (!parser.TryConsume(out _)) + { + if (!parser.TryConsume(out var key)) + { + throw new YamlException("Expected a key."); + } + + if (parser.TryConsume(out _)) + { + var list = new List(); + while (!parser.TryConsume(out _)) + { + if (parser?.Current is MappingStart) + { + list.Add(ReadYaml(parser, type)); + } + else if (parser?.Current is Scalar) + { + if (parser.TryConsume(out var scalar)) + { + list.Add(scalar.Value); + } + } + else + { + throw new YamlException( + "Expected a value, a nested mapping, or a sequence end." + ); + } + } + + dictionary[key.Value] = list; + } + else if (parser.TryConsume(out _)) + { + var nestedDictionary = + (Dictionary)ReadYaml(parser, type); + dictionary[key.Value] = nestedDictionary; + } + else + { + if (parser.TryConsume(out var value)) + { + dictionary[key.Value] = value.Value; + } + } + } + + return dictionary; + } + + /// + /// Writes an object to a YAML stream. + /// + /// The YAML emitter. + /// The object to serialize. + /// The type of the object to serialize. + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } +} diff --git a/source/Parser/StaticContext.cs b/source/Parser/StaticContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..c0bcf0c6a8e599a8ca577ca8223af83872a95d09 --- /dev/null +++ b/source/Parser/StaticContext.cs @@ -0,0 +1,11 @@ +using YamlDotNet.Serialization; + +namespace SuCoS.Parser; + +/// +/// The rest of this partial class gets generated at build time +/// +[YamlStaticContext] +public partial class StaticAOTContext : StaticContext +{ +} diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 0f8e42dc3f1a7348a22d394ac445678fd7a1a6a4..d37c5547cc7c28691d4ceac5bdccacfca7acca34 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -1,7 +1,6 @@ +using System.Text; using SuCoS.Helpers; using SuCoS.Models; -using System.Diagnostics.CodeAnalysis; -using System.Text; using YamlDotNet.Serialization; namespace SuCoS.Parser; @@ -9,24 +8,29 @@ namespace SuCoS.Parser; /// /// Responsible for parsing the content front matter using YAML /// -public class YAMLParser : IFrontMatterParser +public class YAMLParser : IMetadataParser { /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields /// - private readonly IDeserializer yamlDeserializerRigid = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .Build(); + private readonly IDeserializer yamlDeserializerRigid; /// - /// YamlDotNet parser to loosely parse the YAML file. Used to include all non-matching fields - /// into Params. + /// ctor /// - private readonly IDeserializer yamlDeserializer = new DeserializerBuilder() - .Build(); + public YAMLParser() + { + yamlDeserializerRigid = new StaticDeserializerBuilder(new StaticAOTContext()) + .WithTypeConverter(new ParamsConverter()) + .IgnoreUnmatchedProperties() + .Build(); + } /// - public IFrontMatter ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string? sourceContentPath = null) + public IFrontMatter ParseFrontmatterAndMarkdownFromFile( + in string fileFullPath, + in string? sourceContentPath = null + ) { ArgumentNullException.ThrowIfNull(fileFullPath); @@ -35,18 +39,29 @@ public class YAMLParser : IFrontMatterParser try { fileContent = File.ReadAllText(fileFullPath); - fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, fileFullPath); + fileRelativePath = Path.GetRelativePath( + sourceContentPath ?? string.Empty, + fileFullPath + ); } catch (Exception ex) { throw new FileNotFoundException(fileFullPath, ex); } - return ParseFrontmatterAndMarkdown(fileFullPath, fileRelativePath, fileContent); + return ParseFrontmatterAndMarkdown( + fileFullPath, + fileRelativePath, + fileContent + ); } /// - public IFrontMatter ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent) + public IFrontMatter ParseFrontmatterAndMarkdown( + in string fileFullPath, + in string fileRelativePath, + in string fileContent + ) { ArgumentNullException.ThrowIfNull(fileRelativePath); @@ -70,23 +85,23 @@ public class YAMLParser : IFrontMatterParser return page; } - private FrontMatter ParseYAML(in string fileFullPath, in string fileRelativePath, string yaml, in string rawContent) + private FrontMatter ParseYAML( + in string fileFullPath, + in string fileRelativePath, + string yaml, + in string rawContent + ) { - var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter"); + var frontMatter = + yamlDeserializerRigid.Deserialize( + new StringReader(yaml) + ) ?? throw new FormatException("Error parsing front matter"); var section = SiteHelper.GetSection(fileRelativePath); frontMatter.RawContent = rawContent; frontMatter.Section = section; frontMatter.SourceRelativePath = fileRelativePath; frontMatter.SourceFullPath = fileFullPath; frontMatter.Type ??= section; - - var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); - if (yamlObject is not Dictionary yamlDictionary) - { - return frontMatter; - } - ParseParams(frontMatter, typeof(Page), yaml, yamlDictionary); - return frontMatter; } @@ -94,45 +109,6 @@ public class YAMLParser : IFrontMatterParser public SiteSettings ParseSiteSettings(string yaml) { var settings = yamlDeserializerRigid.Deserialize(yaml); - ParseParams(settings, typeof(SiteSettings), yaml); return settings; } - - /// - /// Parse all YAML files for non-matching fields. - /// - /// Site or Frontmatter object, that implements IParams - /// The type (Site or Frontmatter) - /// YAML content - /// yamlObject already parsed if available - public void ParseParams(IParams settings, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string yaml, object? yamlObject = null) - { - ArgumentNullException.ThrowIfNull(settings); - ArgumentNullException.ThrowIfNull(type); - - yamlObject ??= yamlDeserializer.Deserialize(new StringReader(yaml)); - if (yamlObject is not Dictionary yamlDictionary) - { - return; - } - - foreach (var key in yamlDictionary.Keys.Cast()) - { - // If the property is not a standard Frontmatter property - if (type.GetProperty(key) != null) - { - continue; - } - - // Recursively create a dictionary structure for the value - if (yamlDictionary[key] is Dictionary valueDictionary) - { - settings.Params[key] = valueDictionary; - } - else - { - settings.Params[key] = yamlDictionary[key]; - } - } - } } diff --git a/source/Program.cs b/source/Program.cs index 6113c5e37ebe5cdf8dd730457f93270e36f64213..be6664dd57dca8d2ae559cf0c17617f6c959af3d 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -4,6 +4,7 @@ using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; using System.Reflection; using CommandLine; +using System.Diagnostics.CodeAnalysis; namespace SuCoS; @@ -39,6 +40,10 @@ public class Program(ILogger logger) /// /// /// + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GenerateOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ServeOptions))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CheckLinkOptions))] public async Task RunCommandLine(string[] args) { OutputLogo(); diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index ff63bde3691ebe2b8b31905130187818571adfe3..32eb4dcd8a28f9556a1009b2141a4d14eccc3549 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -7,6 +7,12 @@ enable true true + true + true + Bruno Massa + ../LICENSE + hhttps://sucos.brunomassa.com + static site generator;ssg;yaml/blog @@ -15,15 +21,16 @@ - - - + + + + diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index 2782a37295f0b2a8925c5c425ec00b3b7ffd75f5..8a4d577b096c1a420de667094c3d3f649c18b80b 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -1,16 +1,15 @@ using SuCoS.Helpers; using SuCoS.Models; -using SuCoS.Parser; using System.Globalization; using Xunit; -namespace Tests.Parser; +namespace Tests.YAMLParser; public class YAMLParserTests : TestSetup { - private readonly YAMLParser parser; + private readonly SuCoS.Parser.YAMLParser parser; - private const string pageFrontmaterCONST = @"Title: Test Title + private const string pageFrontmaterCONST = @"Title: Test Title Type: post Date: 2023-07-01 LastMod: 2023-06-01 @@ -27,13 +26,19 @@ NestedData: - Test - Real Data customParam: Custom Value +Params: + ParamsCustomParam: Custom Value + ParamsNestedData: + Level2: + - Test + - Real Data "; - private const string pageMarkdownCONST = @" + private const string pageMarkdownCONST = @" # Real Data Test This is a test using real data. Real Data Test "; - private const string siteContentCONST = @" + private const string siteContentCONST = @" Title: My Site BaseURL: https://www.example.com/ Description: Tastiest C# Static Site Generator of the World @@ -43,252 +48,244 @@ NestedData: Level2: - Test - Real Data +Params: + ParamsCustomParam: Custom Value + ParamsNestedData: + Level2: + - Test + - Real Data "; - private const string fileFullPathCONST = "test.md"; - private const string fileRelativePathCONST = "test.md"; - private readonly string pageContent; - - public YAMLParserTests() - { - parser = new YAMLParser(); - pageContent = @$"--- + private const string fileFullPathCONST = "test.md"; + private const string fileRelativePathCONST = "test.md"; + private readonly string pageContent; + + public YAMLParserTests() + { + parser = new SuCoS.Parser.YAMLParser(); + pageContent = @$"--- {pageFrontmaterCONST} --- {pageMarkdownCONST}"; - } + } - [Fact] - public void GetSection_ShouldReturnFirstFolderName() - { - var filePath = Path.Combine("folder1", "folder2", "file.md"); + [Fact] + public void GetSection_ShouldReturnFirstFolderName() + { + // Arrange + var filePath = Path.Combine("folder1", "folder2", "file.md"); - // Act - var section = SiteHelper.GetSection(filePath); + // Act + var section = SiteHelper.GetSection(filePath); - // Asset - Assert.Equal("folder1", section); - } + // Assert + Assert.Equal("folder1", section); + } - [Theory] - [InlineData(@"--- + [Theory] + [InlineData(@"--- Title: Test Title --- ", "Test Title")] - [InlineData(@"--- + [InlineData(@"--- Date: 2023-04-01 --- ", "")] - public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) - { - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); + public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); - // Asset - Assert.Equal(expectedTitle, frontMatter.Title); - } + // Assert + Assert.Equal(expectedTitle, frontMatter.Title); + } - [Theory] - [InlineData(@"--- + [Theory] + [InlineData(@"--- Date: 2023-01-01 --- ", "2023-01-01")] - [InlineData(@"--- + [InlineData(@"--- Date: 2023/01/01 --- ", "2023-01-01")] - public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) - { - var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); - - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); - - // Asset - Assert.Equal(expectedDate, frontMatter.Date); - } - - [Fact] - public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() - { - var expectedDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var expectedLastMod = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); - var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); - var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); - - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, pageContent); - - // Asset - Assert.Equal("Test Title", frontMatter.Title); - Assert.Equal("post", frontMatter.Type); - Assert.Equal(expectedDate, frontMatter.Date); - Assert.Equal(expectedLastMod, frontMatter.LastMod); - Assert.Equal(expectedPublishDate, frontMatter.PublishDate); - Assert.Equal(expectedExpiryDate, frontMatter.ExpiryDate); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowException_WhenInvalidYAMLSyntax() - { - const string fileContent = @"--- + public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) + { + // Arrange + var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); + + // Act + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); + + // Assert + Assert.Equal(expectedDate, frontMatter.Date); + } + + [Fact] + public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() + { + // Arrange + var expectedDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + var expectedLastMod = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); + + // Act + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, pageContent); + + // Assert + Assert.Equal("Test Title", frontMatter.Title); + Assert.Equal("post", frontMatter.Type); + Assert.Equal(expectedDate, frontMatter.Date); + Assert.Equal(expectedLastMod, frontMatter.LastMod); + Assert.Equal(expectedPublishDate, frontMatter.PublishDate); + Assert.Equal(expectedExpiryDate, frontMatter.ExpiryDate); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowException_WhenInvalidYAMLSyntax() + { + // Arrange + const string fileContent = @"--- Title --- "; - // Asset - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() - { - // Act - var site = parser.ParseSiteSettings(siteContentCONST); - - - // Asset - Assert.Equal("My Site", site.Title); - Assert.Equal("https://www.example.com/", site.BaseURL); - Assert.Equal("Tastiest C# Static Site Generator of the World", site.Description); - Assert.Equal("Copyright message", site.Copyright); - } - - [Fact] - public void ParseParams_ShouldFillParamsWithNonMatchingFields() - { - var page = new Page(new FrontMatter - { - Title = "Test Title", - SourceRelativePath = "/test.md" - }, site); - - // Act - parser.ParseParams(page, typeof(Page), pageFrontmaterCONST); - - // Asset - Assert.True(page.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", page.Params["customParam"]); - } - - [Fact] - public void ParseFrontmatter_ShouldParseContentInSiteFolder() - { - var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, site); - - // Act - site.PostProcessPage(page); - - // Asset - Assert.Equal(date, frontMatter.Date); - } - - [Fact] - public void ParseFrontmatter_ShouldCreateTags() - { - // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, site); - - // Act - site.PostProcessPage(page); - - // Asset - Assert.Equal(2, page.TagsReference.Count); - } - - [Fact] - public void ParseFrontmatter_ShouldParseCategoriesCorrectly() - { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); - - // Asset - Assert.Equal(new[] { "Test", "Real Data" }, frontMatter.Params["Categories"]); - Assert.Equal(new[] { "Test", "Real Data" }, (frontMatter.Params["NestedData"] as Dictionary)?["Level2"]); - Assert.Equal("Test", ((frontMatter.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenSiteIsNull() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!, "fakeFilePath")); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathIsNull() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!)); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile("fakePath")); - } - - [Fact] - public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, null!, "fakeContent")); - } - - [Fact] - public void ParseFrontmatter_ShouldHandleEmptyFileContent() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "")); - } - - [Fact] - public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() - { - _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "invalidFrontmatter")); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnSiteSettings() - { - var site = parser.ParseSiteSettings(siteContentCONST); - Assert.NotNull(site); - Assert.Equal("My Site", site.Title); - Assert.Equal("https://www.example.com/", site.BaseURL); - } - - [Fact] - public void ParseSiteSettings_ShouldReturnContent() - { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); - - Assert.Equal(pageMarkdownCONST, frontMatter.RawContent); - } - - [Fact] - public void SiteParams_ShouldThrowExceptionWhenSettingsIsNull() - { - _ = Assert.Throws(() => parser.ParseParams(null!, typeof(Site), siteContentCONST)); - } - - [Fact] - public void SiteParams_ShouldThrowExceptionWhenTypeIsNull() - { - _ = Assert.Throws(() => parser.ParseParams(site, null!, siteContentCONST)); - } - - [Fact] - public void SiteParams_ShouldHandleEmptyContent() - { - parser.ParseParams(site, typeof(Site), string.Empty); - Assert.Empty(site.Params); - } - - [Fact] - public void SiteParams_ShouldPopulateParamsWithExtraFields() - { - parser.ParseParams(site, typeof(Site), siteContentCONST); - Assert.NotEmpty(site.Params); - Assert.True(site.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", site.Params["customParam"]); - Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)site.Params["NestedData"])["Level2"]); - Assert.Equal("Test", ((site.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); - } + // Assert + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() + { + // Act + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + + + // Assert + Assert.Equal("My Site", siteSettings.Title); + Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + Assert.Equal("Tastiest C# Static Site Generator of the World", siteSettings.Description); + Assert.Equal("Copyright message", siteSettings.Copyright); + } + + [Fact] + public void ParseParams_ShouldFillParamsWithNonMatchingFields() + { + // Arrange + var page = new Page(parser.ParseFrontmatterAndMarkdown(string.Empty, string.Empty, pageContent), site); + + // Assert + Assert.False(page.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", page.Params["ParamsCustomParam"]); + } + + [Fact] + public void ParseFrontmatter_ShouldParseContentInSiteFolder() + { + // Arrange + var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); + + // Act + site.PostProcessPage(page); + + // Assert + Assert.Equal(date, frontMatter.Date); + } + + [Fact] + public void ParseFrontmatter_ShouldCreateTags() + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); + + // Act + site.PostProcessPage(page); + + // Assert + Assert.Equal(2, page.TagsReference.Count); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenSiteIsNull() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!, "fakeFilePath")); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathIsNull() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(null!)); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile("fakePath")); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, null!, "fakeContent")); + } + + [Fact] + public void ParseFrontmatter_ShouldHandleEmptyFileContent() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "")); + } + + [Fact] + public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() + { + _ = Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "invalidFrontmatter")); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteSettings() + { + // Arrange + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + + // Assert + Assert.NotNull(siteSettings); + Assert.Equal("My Site", siteSettings.Title); + Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnContent() + { + // Arrange + var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); + + // Assert + Assert.Equal(pageMarkdownCONST, frontMatter.RawContent); + } + + + [Fact] + public void SiteParams_ShouldHandleEmptyContent() + { + Assert.Empty(site.Params); + } + + [Fact] + public void SiteParams_ShouldPopulateParamsWithExtraFields() + { + // Arrange + var siteSettings = parser.ParseSiteSettings(siteContentCONST); + site = new Site(generateOptionsMock, siteSettings, frontMatterParser, loggerMock, systemClockMock); + + // Assert + Assert.NotEmpty(siteSettings.Params); + Assert.DoesNotContain("customParam", site.Params); + Assert.Contains("ParamsCustomParam", site.Params); + Assert.Equal("Custom Value", site.Params["ParamsCustomParam"]); + Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)siteSettings.Params["ParamsNestedData"])["Level2"]); + Assert.Equal("Test", ((siteSettings.Params["ParamsNestedData"] as Dictionary)?["Level2"] as List)?[0]); + } } diff --git a/test/TestSetup.cs b/test/TestSetup.cs index aa0f4d08a01b56a23e6672fa632213e273653b2b..ce5d9b9d837e0e315a4adfdadac91dd8d5821af1 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -23,7 +23,7 @@ public class TestSetup protected const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; protected const string testSitePathCONST08 = ".TestSites/08-theme-html"; - protected readonly IFrontMatterParser frontMatterParser = new YAMLParser(); + protected readonly IMetadataParser frontMatterParser = new SuCoS.Parser.YAMLParser(); protected readonly IGenerateOptions generateOptionsMock = Substitute.For(); protected readonly SiteSettings siteSettingsMock = Substitute.For(); protected readonly ILogger loggerMock = Substitute.For(); @@ -34,7 +34,7 @@ public class TestSetup SourceRelativePath = sourcePathCONST }; - protected readonly ISite site; + protected ISite site; // based on the compiled test.dll path // that is typically "bin/Debug/netX.0/test.dll" @@ -45,4 +45,10 @@ public class TestSetup _ = systemClockMock.Now.Returns(todayDate); site = new Site(generateOptionsMock, siteSettingsMock, frontMatterParser, loggerMock, systemClockMock); } + + public TestSetup(SiteSettings siteSettings) + { + _ = systemClockMock.Now.Returns(todayDate); + site = new Site(generateOptionsMock, siteSettings, frontMatterParser, loggerMock, systemClockMock); + } }