diff --git a/CHANGELOG.md b/CHANGELOG.md index 2431ac4c0710efb43f197964fabb5e73ec1f76e2..7aac50a6c3a23c5080865856eb598a77b0bdf890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [Unreleased] +- `term.liquid` and `taxonomy.liquid` are now available for theming +- Added: YAML front matter and site settings are now case-insensitive + v[4.0.2] 2024-05-09 - Fixed crashes on serve @@ -104,8 +107,8 @@ v[1.0.0] 2023-07-15 - Born to be Wild - Added First Commit! [Unreleased]: https://gitlab.com/sucos/sucos/-/compare/v4.0.2...HEAD -[4.0.0]: https://gitlab.com/sucos/sucos/-/compare/v4.0.1...v4.0.2 -[4.0.0]: https://gitlab.com/sucos/sucos/-/compare/v4.0.0...v4.0.1 +[4.0.2]: https://gitlab.com/sucos/sucos/-/compare/v4.0.1...v4.0.2 +[4.0.1]: https://gitlab.com/sucos/sucos/-/compare/v4.0.0...v4.0.1 [4.0.0]: https://gitlab.com/sucos/sucos/-/compare/v3.0.0...v4.0.0 [3.0.0]: https://gitlab.com/sucos/sucos/-/compare/v2.4.0...v3.0.0 [2.4.0]: https://gitlab.com/sucos/sucos/-/compare/v2.3.0...v2.4.0 diff --git a/Tests/Models/FrontMatterTests.cs b/Tests/Models/FrontMatterTests.cs index 657a15c9f7a3e9966c96219c610586336b8c15ce..7336a7c1cc06bce725f8f9d489749d4be59bfb33 100644 --- a/Tests/Models/FrontMatterTests.cs +++ b/Tests/Models/FrontMatterTests.cs @@ -18,14 +18,14 @@ public class FrontMatterTests : TestSetup Title = title, Section = section, Type = type, - URL = url + Url = url }; // Assert Assert.Equal(title, basicContent.Title); Assert.Equal(section, basicContent.Section); Assert.Equal(type, basicContent.Type); - Assert.Equal(url, basicContent.URL); + Assert.Equal(url, basicContent.Url); } [Theory] diff --git a/Tests/Models/PageTests.cs b/Tests/Models/PageTests.cs index d3d0516d6d5eb732effd3042b67dd38a30257ab0..42dec2b5cb870468ace823480b53efa71c1323d8 100644 --- a/Tests/Models/PageTests.cs +++ b/Tests/Models/PageTests.cs @@ -1,7 +1,7 @@ +using System.Globalization; using NSubstitute; using SuCoS.Models; using SuCoS.Models.CommandLineOptions; -using System.Globalization; using Xunit; namespace Tests.Models; @@ -61,7 +61,7 @@ word03 word04 word05 6 7 [eight](https://example.com) Assert.Equal(string.Empty, page.Section); Assert.Equal(Kind.Single, page.Kind); Assert.Equal("page", page.Type); - Assert.Null(page.URL); + Assert.Null(page.Url); Assert.Empty(page.Params); Assert.Null(page.Date); Assert.Null(page.LastMod); @@ -235,7 +235,7 @@ word03 word04 word05 6 7 [eight](https://example.com) { Title = TitleConst, SourceRelativePath = SourcePathConst, - URL = urlTemplate + Url = urlTemplate }, Site); var actualPermalink = page.CreatePermalink(); diff --git a/Tests/Parser/YAMLParserTests.cs b/Tests/Parser/YAMLParserTests.cs index c63fdd37a171bb88ac918de569ea4e642c99cd7e..ef6708986ad87fa99b34ed712ea46b9bde12a014 100644 --- a/Tests/Parser/YAMLParserTests.cs +++ b/Tests/Parser/YAMLParserTests.cs @@ -1,6 +1,6 @@ +using System.Globalization; using SuCoS.Helpers; using SuCoS.Models; -using System.Globalization; using SuCoS.Parsers; using Xunit; @@ -37,14 +37,12 @@ public class YamlParserTests : TestSetup """; private const string PageMarkdownConst = """ - # Real Data Test This is a test using real data. Real Data Test """; private const string SiteContentConst = """ - Title: My Site BaseURL: https://www.example.com/ Description: Tastiest C# Static Site Generator of the World @@ -179,7 +177,7 @@ public class YamlParserTests : TestSetup // Assert Assert.Equal("My Site", siteSettings.Title); - Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + 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); } @@ -257,7 +255,7 @@ public class YamlParserTests : TestSetup // Assert Assert.NotNull(siteSettings); Assert.Equal("My Site", siteSettings.Title); - Assert.Equal("https://www.example.com/", siteSettings.BaseURL); + Assert.Equal("https://www.example.com/", siteSettings.BaseUrl); } @@ -282,4 +280,71 @@ public class YamlParserTests : TestSetup Assert.Equal(Expected, ((Dictionary)siteSettings.Params["ParamsNestedData"])["Level2"]); Assert.Equal("Test", ((siteSettings.Params["ParamsNestedData"] as Dictionary)?["Level2"] as List)?[0]); } + + [Theory] + [InlineData(""" + --- + Title: title-test + Url: my-page + --- + """)] + [InlineData(""" + --- + title: title-test + url: my-page + --- + """)] + [InlineData(""" + --- + tiTle: title-test + URL: my-page + --- + """)] + [InlineData(""" + --- + tiTle: title-test-old + title: title-test # the last on is used + Url: my-page + url: my-page + --- + """)] + public void FrontMatter_ShouldIgnoreCase(string fileContent) + { + // Arrange + var frontMatter = FrontMatter.Parse(FileRelativePathConst, FileFullPathConst, _parser, fileContent); + + // Assert + Assert.Equal("title-test", frontMatter.Title); + Assert.Equal("my-page", frontMatter.Url); + } + + [Theory] + [InlineData(""" + Title: title-test + BaseURL: https://www.example.com/ + """)] + [InlineData(""" + title: title-test + baseurl: https://www.example.com/ + """)] + [InlineData(""" + tiTle: title-test + baseUrl: https://www.example.com/ + """)] + [InlineData(""" + tiTle: title-test-old + Title: title-test # the last on is used + baseurl: https://www.example2.com/ + BaseURL: https://www.example.com/ # the last on is used + """)] + public void SiteSettings_ShouldIgnoreCase(string fileContent) + { + // Arrange + var siteSettings = _parser.Parse(fileContent); + Site = new Site(GenerateOptionsMock, siteSettings, FrontMatterParser, LoggerMock, SystemClockMock); + + // Assert + Assert.Equal("title-test", Site.Title); + Assert.Equal("https://www.example.com/", Site.BaseUrl); + } } diff --git a/source/Commands/BuildCommand.cs b/source/Commands/BuildCommand.cs index 1ad4bb141434088d65eeacec3b9672c5e49bb3ab..5530d7ce4d625f27817e75764ab996d7bc766bcc 100644 --- a/source/Commands/BuildCommand.cs +++ b/source/Commands/BuildCommand.cs @@ -61,7 +61,7 @@ public class BuildCommand : BaseGeneratorCommand if (output is IPage page) { - var path = (url + (Site.UglyURLs ? string.Empty : "/index.html")).TrimStart('/'); + var path = (url + (Site.UglyUrLs ? string.Empty : "/index.html")).TrimStart('/'); // Generate the output path var outputAbsolutePath = Path.Combine(_options.Output, path); diff --git a/source/Commands/NewSiteCommand.cs b/source/Commands/NewSiteCommand.cs index 0d7dd1eedc4e45c4771b3b481c4d14f8724017da..876153ae58b34c0a154c3eb06bb9af6f26fbddec 100644 --- a/source/Commands/NewSiteCommand.cs +++ b/source/Commands/NewSiteCommand.cs @@ -28,7 +28,7 @@ public sealed class NewSiteCommand(NewSiteOptions options, ILogger logger, IFile { Title = options.Title, Description = options.Description, - BaseURL = options.BaseUrl + BaseUrl = options.BaseUrl }; var site = new Site(new GenerateOptions() { SourceOption = options.Output }, _siteSettings, new YamlParser(), null!, null); diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 6f8587d31675d6a2b8fc6a33571fa6d828916d02..1df4b3607237fe8108d45f52537257b06d7ce1af 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -20,7 +20,7 @@ public class FrontMatter : IFrontMatter public string? Type { get; set; } = "page"; /// - public string? URL { get; set; } + public string? Url { get; set; } /// public bool? Draft { get; set; } @@ -66,7 +66,7 @@ public class FrontMatter : IFrontMatter /// [YamlIgnore] - public string? SourceRelativePathDirectory => (Path.GetDirectoryName(SourceRelativePath) ?? string.Empty) + public string SourceRelativePathDirectory => (Path.GetDirectoryName(SourceRelativePath) ?? string.Empty) .Replace('\\', '/'); /// diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 29e704e2e30a4f4f5bfdcdc74c5c54c5629c6ea0..24b007fb94c196e0aa7a745efa57034e2e29c0d5 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -44,7 +44,7 @@ public interface IFrontMatter : IParams, IFile /// /// will try to convert page.Parent.Title and page.Title. /// - string? URL { get; } + string? Url { get; } /// /// True for draft content. It will not be rendered unless @@ -81,7 +81,7 @@ public interface IFrontMatter : IParams, IFile /// A List of secondary URL patterns to be used to create the url. /// List URL, it will be parsed as liquid templates, so you can use page variables. /// - /// + /// List? Aliases { get; } /// diff --git a/source/Models/ISiteSettings.cs b/source/Models/ISiteSettings.cs index 2bbda14fdd7b0e898d1bfe141c4f8c4083093fd8..8dd224888ee3d2acb3219608b0053d43d8c551a2 100644 --- a/source/Models/ISiteSettings.cs +++ b/source/Models/ISiteSettings.cs @@ -23,10 +23,10 @@ public interface ISiteSettings : IParams /// /// The base URL that will be used to build public links. /// - public string BaseURL { get; } + public string BaseUrl { get; } /// /// The appearance of a URL is either ugly or pretty. /// - public bool UglyURLs { get; } + public bool UglyUrLs { get; } } diff --git a/source/Models/Page.cs b/source/Models/Page.cs index 83f04f3c193e41fd6a4d7ddca86097b77dacf1f9..0ec9e608d85e907a087faf1f80244d29a5a66706 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -22,7 +22,7 @@ public class Page : IPage public string? Type => _frontMatter.Type; /// - public string? URL => _frontMatter.URL; + public string? Url => _frontMatter.Url; /// public bool? Draft => _frontMatter.Draft; @@ -318,7 +318,7 @@ endif var permalink = string.Empty; - urlForce ??= URL ?? (isIndex ? UrlForIndex : UrlForNonIndex); + urlForce ??= Url ?? (isIndex ? UrlForIndex : UrlForNonIndex); try { diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 0fdacafea590010a85e93a09cf733a2f83a14c5d..96f5a2720f94787b0668b53e3d16eb8ce5188370 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -40,10 +40,10 @@ public class Site : ISite public string? Copyright => _settings.Copyright; /// - public string BaseURL => _settings.BaseURL; + public string BaseUrl => _settings.BaseUrl; /// - public bool UglyURLs => _settings.UglyURLs; + public bool UglyUrLs => _settings.UglyUrLs; #endregion SiteSettings @@ -236,7 +236,7 @@ public class Site : ISite SourceFullPath = Urlizer.Path(Path.Combine(SourceContentPath, relativePath, IndexLeafFileConst)), Title = title, Type = kind == Kind.Home ? "index" : sectionName, - URL = relativePath + Url = relativePath }; IPage? parent = null; diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index e24f897e15879e4cabdb58dd4713b54a6858fff3..af284784dd3fab2df64fd73ed24e653d1abbfb78 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -23,7 +23,7 @@ public class SiteSettings : ISiteSettings /// - public string BaseURL { get; set; } = string.Empty; + public string BaseUrl { get; set; } = string.Empty; /// public Dictionary> Outputs { get; set; } = []; @@ -43,7 +43,7 @@ public class SiteSettings : ISiteSettings /// /// The appearance of a URL is either ugly or pretty. /// - public bool UglyURLs { get; set; } + public bool UglyUrLs { get; set; } #region IParams diff --git a/source/Parsers/YAMLParser.cs b/source/Parsers/YAMLParser.cs index 133d978febb7a1b7e085d876a435d8c045e334ba..e8b9e19c910e3e597cc3cd2fee6d31c65062bea9 100644 --- a/source/Parsers/YAMLParser.cs +++ b/source/Parsers/YAMLParser.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using FolkerKinzel.Strings; using YamlDotNet.Serialization; @@ -21,9 +22,49 @@ public class YamlParser : IMetadataParser { _deserializer = new StaticDeserializerBuilder(new StaticAotContext()) .WithTypeConverter(new ParamsConverter()) + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .WithTypeInspector(n => new IgnoreCaseTypeInspector(n)) .IgnoreUnmatchedProperties() .Build(); } + private class IgnoreCaseTypeInspector(ITypeInspector innerTypeInspector) + : ITypeInspector + { + private readonly ITypeInspector _innerTypeInspector = innerTypeInspector ?? throw new ArgumentNullException(nameof(innerTypeInspector)); + + public IEnumerable GetProperties(Type type, object? container) + { + return _innerTypeInspector.GetProperties(type, container ?? null); + } + + public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched) + { + var candidates = GetProperties(type, container) + .Where(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList(); + + var property = candidates.FirstOrDefault(); + + if (property == null) + { + if (ignoreUnmatched) + { + return null; + } + + throw new SerializationException($"Property '{name}' not found on type '{type.FullName}'."); + } + + if (candidates.Count > 1) + { + throw new SerializationException( + $"Multiple properties with the name/alias '{name}' already exists on type '{type.FullName}', maybe you're misusing YamlAlias or maybe you are using the wrong naming convention? The matching properties are: {string.Join(", ", candidates.Select(p => p.Name))}" + ); + } + + return property; + } + } + /// public T Parse(string content)