diff --git a/.build.Nuke/Build.Test.cs b/.build.Nuke/Build.Test.cs index 937b3f6fdd851c4352ba3fed5224d566b1403d81..c1d0c371cce12a76b54307f5994f66aa842895da 100644 --- a/.build.Nuke/Build.Test.cs +++ b/.build.Nuke/Build.Test.cs @@ -6,7 +6,6 @@ using Nuke.Common.Tools.ReportGenerator; using Nuke.Common.IO; using Nuke.Common.Tools.Coverlet; using static Nuke.Common.Tools.Coverlet.CoverletTasks; -using static Nuke.Common.IO.FileSystemTasks; using Serilog; using System; @@ -20,8 +19,6 @@ sealed partial class Build : NukeBuild { AbsolutePath testDirectory => RootDirectory / "test"; AbsolutePath testDLLDirectory => testDirectory / "bin" / "Debug" / "net7.0"; - AbsolutePath testSiteSourceDirectory => RootDirectory / "test" / ".TestSites"; - AbsolutePath testSiteDestinationDirectory => testDLLDirectory / ".TestSites"; AbsolutePath testAssembly => testDLLDirectory / "test.dll"; AbsolutePath coverageDirectory => RootDirectory / "coverage-results"; AbsolutePath coverageResultDirectory => coverageDirectory / "coverage"; @@ -29,16 +26,8 @@ sealed partial class Build : NukeBuild AbsolutePath coverageReportDirectory => coverageDirectory / "report"; AbsolutePath coverageReportSummaryDirectory => coverageReportDirectory / "Summary.txt"; - Target PrepareTestFiles => _ => _ - .After(Clean) - .Executes(() => - { - testSiteDestinationDirectory.CreateOrCleanDirectory(); - CopyDirectoryRecursively(testSiteSourceDirectory, testSiteDestinationDirectory, DirectoryExistsPolicy.Merge); - }); - Target Test => _ => _ - .DependsOn(Compile, PrepareTestFiles) + .DependsOn(Compile) .Executes(() => { coverageResultDirectory.CreateDirectory(); diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 9dbaef2a329f0d078efc6021c3c112e4abfcdf60..7def812ce0dc0a3e51e5560b0cd87f877e9eaf09 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -115,7 +115,6 @@ "CreatePackage", "GitLabCreateRelease", "GitLabCreateTag", - "PrepareTestFiles", "Publish", "Restore", "ShowCurrentVersion", @@ -141,7 +140,6 @@ "CreatePackage", "GitLabCreateRelease", "GitLabCreateTag", - "PrepareTestFiles", "Publish", "Restore", "ShowCurrentVersion", diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 57766406029fcfdaa3495e19ff42633f6ae9e55c..8f6c16e4f0a0aa6f394bc75e23708221ba97a783 100644 --- a/source/BuildCommand.cs +++ b/source/BuildCommand.cs @@ -65,7 +65,7 @@ public class BuildCommand : BaseGeneratorCommand File.WriteAllText(outputAbsolutePath, result); // Log - logger.Debug("Page created: {Permalink}", frontmatter.Permalink); + logger.Debug("Page created: {Permalink}", outputAbsolutePath); // Use interlocked to safely increment the counter in a multi-threaded environment _ = Interlocked.Increment(ref pagesCreated); diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index a098144fce5d9e0fab3c9a45a6f02063c0fe72a0..d69f404e9c9be596b17d66328ef17136f22729ca 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using SuCoS.Models; namespace SuCoS.Helper; @@ -14,20 +15,20 @@ public static class FileUtils /// Gets all Markdown files in the specified directory. /// /// The directory path. + /// The initial directory path. /// The list of Markdown file paths. - public static List GetAllMarkdownFiles(string directory) + public static List GetAllMarkdownFiles(string directory, string? basePath = null) { - var markdownFiles = new List(); - var files = Directory.GetFiles(directory, "*.md"); - markdownFiles.AddRange(files); + basePath ??= directory; + var files = Directory.GetFiles(directory, "*.md").ToList(); var subdirectories = Directory.GetDirectories(directory); foreach (var subdirectory in subdirectories) { - markdownFiles.AddRange(GetAllMarkdownFiles(subdirectory)); + files.AddRange(GetAllMarkdownFiles(subdirectory, basePath)); } - return markdownFiles; + return files; } /// diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 9993b2e46a0bfd246e72b4f78c9068a03894c221..ecaa537d678a0a4db7cebc4cf3b325e9e6f702d6 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -91,7 +91,9 @@ public static class SiteHelper site.ResetCache(); - site.ScanAllMarkdownFiles(); + // Scan content files + var markdownFiles = FileUtils.GetAllMarkdownFiles(site.SourceContentPath); + site.ContentPaths.AddRange(markdownFiles); site.ParseSourceFiles(stopwatch); diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 33cd6236d29b9e1591d06bd1bc161b3de5c50a92..466ea35b037d77bcb45f0b288c6e317893d24cd4 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -316,6 +316,7 @@ public class Frontmatter : IBaseContent, IParams URLforce ??= URL ?? (isIndex ? "{{ page.SourcePathDirectory }}" : "{{ page.SourcePathDirectory }}/{{ page.Title }}"); + try { if (Site.FluidParser.TryParse(URLforce, out var template, out var error)) diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 4ec4c888b438c3d6b7b008375269832bdb836967..3c59960c00887512c8464ad6213812392953570f 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -123,7 +123,7 @@ public class Site : IParams /// List of all content to be scanned and processed. /// [YamlIgnore] - public List<(string filePath, string content)> RawPages { get; set; } = new(); + public List ContentPaths { get; set; } = new(); /// /// Command line options @@ -217,22 +217,6 @@ public class Site : IParams this.Clock = clock; } - /// - /// Scans all markdown files in the source directory. - /// - public void ScanAllMarkdownFiles() - { - // Scan content files - var markdownFiles = FileUtils.GetAllMarkdownFiles(SourceContentPath); - - foreach (var fileAbsolutePath in markdownFiles) - { - var content = File.ReadAllText(fileAbsolutePath); - var relativePath = Path.GetRelativePath(SourceContentPath, fileAbsolutePath); - RawPages.Add((relativePath, content)); - } - } - /// /// Resets the template cache to force a reload of all templates. /// @@ -321,11 +305,12 @@ public class Site : IParams // Process the source files, extracting the frontmatter var filesParsed = 0; // counter to keep track of the number of files processed - _ = Parallel.ForEach(RawPages, file => + _ = Parallel.ForEach(ContentPaths, filePath => { try { - var frontmatter = ReadSourceFrontmatter(file.filePath, file.content, frontmatterParser); + var frontmatter = frontmatterParser.ParseFrontmatterAndMarkdownFromFile(this, filePath, SourceContentPath) + ?? throw new FormatException($"Error parsing frontmatter for {filePath}"); if (frontmatter.IsValidDate(options)) { @@ -334,7 +319,7 @@ public class Site : IParams } catch (Exception ex) { - Logger?.Error(ex, "Error parsing file {file}", file.filePath); + Logger?.Error(ex, "Error parsing file {file}", filePath); } // Use interlocked to safely increment the counter in a multi-threaded environment @@ -435,30 +420,4 @@ public class Site : IParams CreateAutomaticFrontmatter(contentTemplate, frontmatter); } } - - /// - /// Reads the frontmatter from the source file. - /// - /// The file path. - /// The file content. - /// The frontmatter parser. - /// The parsed frontmatter. - private Frontmatter ReadSourceFrontmatter(string filePath, string content, IFrontmatterParser frontmatterParser) - { - // test if filePath or config is null - if (filePath is null) - { - throw new ArgumentNullException(nameof(filePath)); - } - if (frontmatterParser is null) - { - throw new ArgumentNullException(nameof(frontmatterParser)); - } - - // Separate the YAML frontmatter from the file content - var frontmatter = frontmatterParser.ParseFrontmatter(this, filePath, content) - ?? throw new FormatException($"Error parsing frontmatter for {filePath}"); - - return frontmatter; - } } diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 045d9c5b38bff099f36693fd37339b8a8a6ad840..0277a6069c92d63910e8f482452351a53037df69 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IFrontmatterParser.cs @@ -11,10 +11,19 @@ public interface IFrontmatterParser /// Extract the frontmatter from the content file. /// /// + /// + /// + /// + Frontmatter? ParseFrontmatterAndMarkdownFromFile(Site site, in string filePath, in string sourceContentPath); + + /// + /// Extract the frontmatter from the content. + /// + /// /// /// /// - Frontmatter? ParseFrontmatter(Site site, in string filePath, in string fileContent); + Frontmatter? ParseFrontmatterAndMarkdown(Site site, in string filePath, in string fileContent); /// /// Parse the app config file. diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index f25235f97d3890584cdaba46327fd30422f920e8..897c27128e69662aebfbe531eb9d606047d13927 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; +using System.Text; using SuCoS.Helper; using SuCoS.Models; using YamlDotNet.Serialization; @@ -14,9 +14,6 @@ namespace SuCoS.Parser; /// public partial class YAMLParser : IFrontmatterParser { - [GeneratedRegex(@"^---\s*[\r\n](?.*?)[\r\n]---\s*", RegexOptions.Singleline)] - private partial Regex YAMLRegex(); - /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields /// @@ -32,7 +29,7 @@ public partial class YAMLParser : IFrontmatterParser .Build(); /// - public Frontmatter? ParseFrontmatter(Site site, in string filePath, in string fileContent) + public Frontmatter? ParseFrontmatterAndMarkdownFromFile(Site site, in string filePath, in string? sourceContentPath = null) { if (site is null) { @@ -43,20 +40,56 @@ public partial class YAMLParser : IFrontmatterParser throw new ArgumentNullException(nameof(filePath)); } - var match = YAMLRegex().Match(fileContent); - if (match.Success) + string? fileContent; + string? fileRelativePath; + try + { + fileContent = File.ReadAllText(filePath); + fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, filePath); + } + catch (Exception ex) + { + throw new FileNotFoundException(filePath, ex); + } + + return ParseFrontmatterAndMarkdown(site, fileRelativePath, fileContent); + } + + /// + public Frontmatter? ParseFrontmatterAndMarkdown(Site site, in string fileRelativePath, in string fileContent) + { + if (site is null) + { + throw new ArgumentNullException(nameof(site)); + } + if (fileRelativePath is null) + { + throw new ArgumentNullException(nameof(fileRelativePath)); + } + + using var content = new StringReader(fileContent); + var frontmatterBuilder = new StringBuilder(); + string? line; + + while ((line = content.ReadLine()) != null && line != "---") { } + while ((line = content.ReadLine()) != null && line != "---") { - var yaml = match.Groups["frontmatter"].Value; - var rawContent = fileContent[match.Length..].TrimStart('\n'); - var frontmatter = ParseYAML(ref site, filePath, yaml, rawContent); - return frontmatter; + frontmatterBuilder.AppendLine(line); } - return null; + + // Join the read lines to form the frontmatter + var yaml = frontmatterBuilder.ToString(); + var rawContent = content.ReadToEnd(); + + // Now, you can parse the YAML frontmatter + var page = ParseYAML(ref site, fileRelativePath, yaml, rawContent); + + return page; } - private Frontmatter ParseYAML(ref Site site, in string filePath, string frontmatter, string rawContent) + private Frontmatter ParseYAML(ref Site site, in string filePath, string yaml, in string rawContent) { - var page = yamlDeserializerRigid.Deserialize(frontmatter); + var page = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing frontmatter"); var sourceFileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath) ?? string.Empty; var section = SiteHelper.GetSection(filePath); page.RawContent = rawContent; @@ -68,13 +101,14 @@ public partial class YAMLParser : IFrontmatterParser page.Title ??= sourceFileNameWithoutExtension; page.Type ??= section; - var yamlObject = yamlDeserializer.Deserialize(new StringReader(frontmatter)); + var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); if (yamlObject is Dictionary yamlDictionary) { - if (yamlDictionary.TryGetValue("Tags", out var tags) && tags is List tagsValues) + if (yamlDictionary.TryGetValue("Tags", out var tags) && tags is List tagsValues) { - foreach (var tagName in tagsValues) + foreach (var tagObj in tagsValues) { + var tagName = (string)tagObj; var contentTemplate = new BasicContent( title: tagName, section: "tags", @@ -84,16 +118,16 @@ public partial class YAMLParser : IFrontmatterParser _ = site.CreateAutomaticFrontmatter(contentTemplate, page); } } - ParseParams(page, typeof(Frontmatter), frontmatter, yamlDictionary); + ParseParams(page, typeof(Frontmatter), yaml, yamlDictionary); } return page; } /// - public Site ParseSiteSettings(string content) + public Site ParseSiteSettings(string yaml) { - var settings = yamlDeserializerRigid.Deserialize(content); - ParseParams(settings, typeof(Site), content); + var settings = yamlDeserializerRigid.Deserialize(yaml); + ParseParams(settings, typeof(Site), yaml); return settings; } @@ -102,9 +136,9 @@ public partial class YAMLParser : IFrontmatterParser /// /// Site or Frontmatter object, that implements IParams /// The type (Site or Frontmatter) - /// YAML content + /// YAML content /// yamlObject already parsed if available - public void ParseParams(IParams settings, Type type, string content, object? yamlObject = null) + public void ParseParams(IParams settings, Type type, string yaml, object? yamlObject = null) { if (settings is null) { @@ -115,7 +149,7 @@ public partial class YAMLParser : IFrontmatterParser throw new ArgumentNullException(nameof(type)); } - yamlObject ??= yamlDeserializer.Deserialize(new StringReader(content)); + yamlObject ??= yamlDeserializer.Deserialize(new StringReader(yaml)); if (yamlObject is Dictionary yamlDictionary) { foreach (var key in yamlDictionary.Keys.Cast()) diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 3d97d0bc3d0bc8144089b95f97a74160f011514d..42e24df7dfedba4a3766535a4254326f7ff0f841 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -207,36 +207,42 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable var fileAbsolutePath = Path.Combine(site.SourceStaticPath, requestPath.TrimStart('/')); var fileAbsoluteThemePath = Path.Combine(site.SourceThemeStaticPath, requestPath.TrimStart('/')); - logger.Debug("Request received for {RequestPath}", requestPath); + string? resultType; // Return the server startup timestamp as the response if (requestPath == "/ping") { + resultType = "ping"; await HandlePingRequest(context); } // Check if it is one of the Static files (serve the actual file) else if (File.Exists(fileAbsolutePath)) { + resultType = "static"; await HandleStaticFileRequest(context, fileAbsolutePath); } // Check if it is one of the Static files (serve the actual file) else if (File.Exists(fileAbsoluteThemePath)) { + resultType = "themestatic"; await HandleStaticFileRequest(context, fileAbsoluteThemePath); } // Check if the requested file path corresponds to a registered page else if (site.PagesDict.TryGetValue(requestPath, out var frontmatter)) { + resultType = "dict"; await HandleRegisteredPageRequest(context, frontmatter); } else { + resultType = "404"; await HandleNotFoundRequest(context); } + logger.Debug("Request {type}\tfor {RequestPath}", resultType, requestPath); } private Task HandlePingRequest(HttpContext context) diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index 74489e6821a3f53d274694592061b566717db112..402b278dff269ddd49b437a4df7919b25e1ebcc8 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -2,6 +2,7 @@ using Xunit; using Moq; using SuCoS.Models; using System.Globalization; +using SuCoS.Helper; namespace SuCoS.Tests; @@ -14,6 +15,10 @@ public class SiteTests private readonly Mock systemClockMock; readonly string testSite1Path = ".TestSites/01"; + // based on the compiled test.dll path + // that is typically "bin/Debug/netX.0/test.dll" + readonly string testSitesPath = "../../.."; + public SiteTests() { systemClockMock = new Mock(); @@ -23,24 +28,18 @@ public class SiteTests } [Theory] - [InlineData("test01.md", @"--- -Title: Test Content 1 ---- - -Test Content 1 -")] - [InlineData("test02.md", @"--- -Title: Test Content 2 ---- - -Test Content 2 -")] - public void Test_ScanAllMarkdownFiles(string fileName, string fileContent) + [InlineData("test01.md")] + [InlineData("test02.md")] + public void Test_ScanAllMarkdownFiles(string fileName) { - site.SourceDirectoryPath = testSite1Path; - site.ScanAllMarkdownFiles(); + var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSite1Path)); + + // Act + var ContentPaths = FileUtils.GetAllMarkdownFiles(Path.Combine(siteFullPath, "content")); + var fileFullPath = Path.Combine(siteFullPath, "content", fileName); - Assert.Contains(site.RawPages, rp => rp.filePath == fileName && rp.content == fileContent); + // Assert + Assert.Contains(ContentPaths, rp => rp == fileFullPath); } [Theory] diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index 06eb559c1d351481e78d30c8dca65f57e045e118..68150174ac2359a96fb4ec842645f92abcf25f8a 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -11,22 +11,49 @@ public class YAMLParserTests { private readonly YAMLParser parser; private readonly Mock site; - private readonly string fileContent = @"--- -Title: Real Data Title -Date: 2023-07-01 -Categories: ['Test', 'Real Data'] -Tags: ['Test', 'Real Data'] ---- + private readonly string pageFrontmater = @"Title: Test Title +Type: post +Date: 2023-07-01 +LastMod: 2023-06-01 +PublishDate: 2023-06-01 +ExpiryDate: 2024-06-01 +Tags: + - Test + - Real Data +Categories: + - Test + - Real Data +NestedData: + Level2: + - Test + - Real Data +customParam: Custom Value +"; + private readonly string pageMarkdown = @" # Real Data Test This is a test using real data. Real Data Test "; + private readonly string siteContent = @" +Title: My Site +BaseUrl: https://www.example.com/ +customParam: Custom Value +NestedData: + Level2: + - Test + - Real Data +"; + private readonly string pageContent; public YAMLParserTests() { parser = new YAMLParser(); site = new Mock(); + pageContent = @$"--- +{pageFrontmater} +--- +{pageMarkdown}"; } [Fact] @@ -47,14 +74,15 @@ Title: Test Title --- ", "Test Title")] [InlineData(@"--- +Date: 2023-04-01 --- -", null)] +", "")] public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) { var filePath = "test.md"; // Act - var frontmatter = parser.ParseFrontmatter(site.Object, filePath, fileContent); + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, filePath, fileContent); // Asset Assert.Equal(expectedTitle, frontmatter?.Title); @@ -63,13 +91,8 @@ Title: Test Title [Fact] public void ParseFrontmatter_ShouldThrowException_WhenSiteIsNull() { - var fileContent = @"--- -Title: Test Title ---- -"; - // Asset - Assert.Throws(() => parser.ParseFrontmatter(null!, "test.md", fileContent)); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, "test.md", pageContent)); } [Theory] @@ -87,7 +110,7 @@ Date: 2023/01/01 var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); // Act - var frontmatter = parser.ParseFrontmatter(site.Object, filePath, fileContent); + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, filePath, fileContent); // Asset Assert.Equal(expectedDate, frontmatter?.Date); @@ -97,22 +120,13 @@ Date: 2023/01/01 public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() { var filePath = "test.md"; - var fileContent = @"--- -Title: Test Title -Type: post -Date: 2023-01-01 -LastMod: 2023-06-01 -PublishDate: 2023-06-01 -ExpiryDate: 2024-06-01 ---- -"; - var expectedDate = DateTime.Parse("2023-01-01", CultureInfo.InvariantCulture); + 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.ParseFrontmatter(site.Object, filePath, fileContent); + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, filePath, pageContent); // Asset Assert.Equal("Test Title", frontmatter?.Title); @@ -133,17 +147,12 @@ Title var filePath = "test.md"; // Asset - Assert.Throws(() => parser.ParseFrontmatter(site.Object, filePath, fileContent)); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(site.Object, filePath, fileContent)); } [Fact] public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() { - var siteContent = @" -BaseUrl: https://www.example.com/ -Title: My Site -"; - // Act var siteSettings = parser.ParseSiteSettings(siteContent); @@ -155,42 +164,130 @@ Title: My Site [Fact] public void ParseParams_ShouldFillParamsWithNonMatchingFields() { - var settings = new Frontmatter("Test Title", "/test.md", site.Object); - var content = @" -Title: Test Title -customParam: Custom Value -"; + var page = new Frontmatter("Test Title", "/test.md", site.Object); // Act - parser.ParseParams(settings, typeof(Frontmatter), content); + parser.ParseParams(page, typeof(Frontmatter), pageFrontmater); // Asset - Assert.True(settings.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", settings.Params["customParam"]); + Assert.True(page.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", page.Params["customParam"]); } - [Fact] public void ParseFrontmatter_ShouldParseContentInSiteFolder() { - var frontmatter = parser.ParseFrontmatter(site.Object, "", fileContent); + var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, "", pageContent); // Act site.Object.PostProcessFrontMatter(frontmatter!); // Asset - Assert.Equal(DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture), frontmatter?.Date); + Assert.Equal(date, frontmatter?.Date); } [Fact] - public void ParseFrontmatter_ShouldParseCategoriesCorrectly() + public void ParseFrontmatter_ShouldCreateTags() { - var frontmatter = parser.ParseFrontmatter(site.Object, "", fileContent); + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, "", pageContent); - // Act - site.Object.PostProcessFrontMatter(frontmatter!); + // Asset + Assert.Equal(2, frontmatter!.Tags?.Count); + } + + [Fact] + public void ParseFrontmatter_ShouldParseCategoriesCorrectly() + { + var frontmatter = parser.ParseFrontmatterAndMarkdown(site.Object, "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(site.Object, null!)); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist() + { + Assert.Throws(() => parser.ParseFrontmatterAndMarkdownFromFile(site.Object, "fakePath")); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() + { + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(site.Object, null!, "fakeContent")); + } + + [Fact] + public void ParseFrontmatter_ShouldHandleEmptyFileContent() + { + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(site.Object, "fakeFilePath", "")); + } + + [Fact] + public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() + { + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(site.Object, "fakeFilePath", "invalidFrontmatter")); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteSettings() + { + var site = parser.ParseSiteSettings(siteContent); + 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(site.Object, "fakeFilePath", pageContent); + + Assert.Equal(pageMarkdown, frontmatter?.RawContent); + } + + [Fact] + public void SiteParams_ShouldThrowExceptionWhenSettingsIsNull() + { + Assert.Throws(() => parser.ParseParams(null!, typeof(Site), siteContent)); + } + + [Fact] + public void SiteParams_ShouldThrowExceptionWhenTypeIsNull() + { + var site = new Site(); + Assert.Throws(() => parser.ParseParams(site, null!, siteContent)); + } + + [Fact] + public void SiteParams_ShouldHandleEmptyContent() + { + parser.ParseParams(site.Object, typeof(Site), string.Empty); + Assert.Empty(site.Object.Params); + } + + [Fact] + public void SiteParams_ShouldPopulateParamsWithExtraFields() + { + parser.ParseParams(site.Object, typeof(Site), siteContent); + Assert.NotEmpty(site.Object.Params); + Assert.True(site.Object.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", site.Object.Params["customParam"]); + Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)site.Object.Params["NestedData"])["Level2"]); + Assert.Equal("Test", ((site.Object?.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); } }