diff --git a/source/Commands/BaseGeneratorCommand.cs b/source/Commands/BaseGeneratorCommand.cs index 254146cccf809fed80ebf99e16610febde059a03..6eba9d381f5ef77475f19804c6f46f67d5b8cc63 100644 --- a/source/Commands/BaseGeneratorCommand.cs +++ b/source/Commands/BaseGeneratorCommand.cs @@ -1,5 +1,3 @@ -using Fluid; -using Fluid.Values; using Serilog; using SuCoS.Helpers; using SuCoS.Models; @@ -52,61 +50,6 @@ public abstract class BaseGeneratorCommand logger.Information("Source path: {source}", propertyValue: options.Source); - site = SiteHelper.Init(configFile, options, Parser, WhereParamsFilter, logger, stopwatch); - } - - /// - /// Fluid/Liquid filter to navigate Params dictionary - /// - /// - /// - /// - /// - /// - protected static ValueTask WhereParamsFilter(FluidValue input, FilterArguments arguments, TemplateContext context) - { - ArgumentNullException.ThrowIfNull(input); - ArgumentNullException.ThrowIfNull(arguments); - - List result = []; - var list = (input as ArrayValue)!.Values; - - var keys = arguments.At(0).ToStringValue().Split('.'); - foreach (var item in list) - { - if (item.ToObjectValue() is IParams param && CheckValueInDictionary(keys, param.Params, arguments.At(1).ToStringValue())) - { - result.Add(item); - } - } - - return new ArrayValue(result); - } - - private static bool CheckValueInDictionary(string[] array, IReadOnlyDictionary dictionary, string value) - { - var currentDictionary = dictionary; - for (var i = 0; i < array.Length; i++) - { - var key = array[i]; - - if (!currentDictionary.TryGetValue(key, out var dictionaryValue)) - { - return false; - } - - if (i == array.Length - 1) - { - return dictionaryValue.Equals(value); - } - - if (dictionaryValue is not Dictionary nestedDictionary) - { - return false; - } - - currentDictionary = nestedDictionary; - } - return false; + site = SiteHelper.Init(configFile, options, Parser, logger, stopwatch); } } diff --git a/source/Commands/CheckLinkCommand.cs b/source/Commands/CheckLinkCommand.cs index b8fd4c361ca135913329300c7665c7d0d864581b..2e9943053ffaa3aa06bb27354dcba748fd6472a2 100644 --- a/source/Commands/CheckLinkCommand.cs +++ b/source/Commands/CheckLinkCommand.cs @@ -223,4 +223,4 @@ public sealed partial class CheckLinkCommand(CheckLinkOptions settings, ILogger logger.Error(message, fileName, link, arg); } } -} \ No newline at end of file +} diff --git a/source/Commands/ServeCommand.cs b/source/Commands/ServeCommand.cs index 966593310831c7abf24bf2d826353f8f09cfceb0..fa6d7422031afa68c0c65cb6ef1af3899e85b79a 100644 --- a/source/Commands/ServeCommand.cs +++ b/source/Commands/ServeCommand.cs @@ -161,7 +161,7 @@ public sealed class ServeCommand : BaseGeneratorCommand, IDisposable } // Reinitialize the site - site = SiteHelper.Init(configFile, options, Parser, WhereParamsFilter, logger, stopwatch); + site = SiteHelper.Init(configFile, options, Parser, logger, stopwatch); StartServer(baseURLDefault, portDefault); }).ConfigureAwait(false); diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 2dcf23ede622249d91116d03b7ce2b139d94c873..5f0bcddfe0aeb1e3deadd5b381ca799f330aeea5 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -1,6 +1,4 @@ -using Fluid; using Markdig; -using Microsoft.Extensions.FileProviders; using Serilog; using SuCoS.Models; using SuCoS.Models.CommandLineOptions; @@ -25,7 +23,7 @@ public static class SiteHelper /// Creates the pages dictionary. /// /// - public static Site Init(string configFile, IGenerateOptions options, IMetadataParser parser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) + public static Site Init(string configFile, IGenerateOptions options, IMetadataParser parser, ILogger logger, StopwatchReporter stopwatch) { ArgumentNullException.ThrowIfNull(stopwatch); @@ -41,10 +39,6 @@ public static class SiteHelper var site = new Site(options, siteSettings, parser, logger, null); - // Liquid template options, needed to theme the content - // but also parse URLs - site.TemplateOptions.Filters.AddFilter("whereParams", whereParamsFilter); - site.ResetCache(); stopwatch.Start("Parse"); @@ -55,7 +49,7 @@ public static class SiteHelper if (Directory.Exists(Path.GetFullPath(site.SourceThemePath))) { - site.TemplateOptions.FileProvider = new PhysicalFileProvider(Path.GetFullPath(site.SourceThemePath)); + site.TemplateEngine.Initialize(site); } return site; diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs index fa40299be56c04e5030c9fab5a408c94d9ff8b09..40cab16bb5c6aae7c1288ac1fb846812e97b636a 100644 --- a/source/Models/ISite.cs +++ b/source/Models/ISite.cs @@ -1,8 +1,8 @@ -using Fluid; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; using SuCoS.Parser; +using SuCoS.TemplateEngine; using System.Collections.Concurrent; namespace SuCoS.Models; @@ -63,14 +63,9 @@ public interface ISite : ISiteSettings, IParams public IMetadataParser Parser { get; } /// - /// The Fluid parser instance. + /// The template engine. /// - public FluidParser FluidParser { get; } - - /// - /// The Fluid/Liquid template options. - /// - public TemplateOptions TemplateOptions { get; } + public ITemplateEngine TemplateEngine { get; } /// /// The logger instance. @@ -136,4 +131,4 @@ public interface ISite : ISiteSettings, IParams /// /// The created page for the index. public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null); -} \ No newline at end of file +} diff --git a/source/Models/Page.cs b/source/Models/Page.cs index cec47bfdfc377eb136ff52d2ac18e02129a8de9b..aaa16e5cd3d23f2f8e3217e29ca9849c866c5dd3 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -1,4 +1,3 @@ -using Fluid; using Markdig; using Microsoft.Extensions.FileSystemGlobbing; using SuCoS.Helpers; @@ -327,17 +326,7 @@ endif try { - if (Site.FluidParser.TryParse(URLforce, out var template, out var error)) - { - var context = new TemplateContext(Site.TemplateOptions) - .SetValue("page", this) - .SetValue("site", Site); - permaLink = template.Render(context); - } - else - { - throw new FormatException(error); - } + permaLink = Site.TemplateEngine.Parse(URLforce, Site, this); } catch (Exception ex) { @@ -433,22 +422,8 @@ endif var file = new InMemoryDirectoryInfo("./", new[] { filenameOriginal }); if (resourceDefinition.GlobMatcher.Execute(file).HasMatches) { - if (Site.FluidParser.TryParse(resourceDefinition.Name, out var templateFileName, out var errorFileName)) - { - var context = new TemplateContext(Site.TemplateOptions) - .SetValue("page", this) - .SetValue("site", Site) - .SetValue("counter", counter); - filename = templateFileName.Render(context); - } - if (Site.FluidParser.TryParse(resourceDefinition.Title, out var templateTitle, out var errorTitle)) - { - var context = new TemplateContext(Site.TemplateOptions) - .SetValue("page", this) - .SetValue("site", Site) - .SetValue("counter", counter); - title = templateTitle.Render(context); - } + filename = Site.TemplateEngine.ParseResource(resourceDefinition.Name, Site, this, counter) ?? filename; + title = Site.TemplateEngine.ParseResource(resourceDefinition.Title, Site, this, counter) ?? filename; resourceParams = resourceDefinition.Params ?? []; } } @@ -480,24 +455,14 @@ endif return isBaseTemplate ? Content : ContentPreRendered; } - if (Site.FluidParser.TryParse(fileContents, out var template, out var error)) + try { - var context = new TemplateContext(Site.TemplateOptions) - .SetValue("page", this) - .SetValue("site", Site); - try - { - var rendered = template.Render(context); - return rendered; - } - catch (Exception ex) - { - Site.Logger.Error(ex, errorMessage, error); - return string.Empty; - } + return Site.TemplateEngine.Parse(fileContents, Site, this); + } + catch (FormatException ex) + { + Site.Logger.Error(ex, errorMessage); + return string.Empty; } - - Site.Logger.Error(errorMessage, error); - return string.Empty; } } diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 53128c2892f630d66a093c76dec72facccb31aea..67f58c3b86d462efdad3dc788d531190757ab01d 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -1,8 +1,8 @@ -using Fluid; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; using SuCoS.Parser; +using SuCoS.TemplateEngine; using System.Collections.Concurrent; namespace SuCoS.Models; @@ -122,16 +122,6 @@ public class Site : ISite /// public SiteCacheManager CacheManager { get; } = new(); - /// - /// The Fluid parser instance. - /// - public FluidParser FluidParser { get; } = new(); - - /// - /// The Fluid/Liquid template options. - /// - public TemplateOptions TemplateOptions { get; } = new(); - /// /// The logger instance. /// @@ -167,6 +157,9 @@ public class Site : ISite /// private readonly ISystemClock clock; + /// + public ITemplateEngine TemplateEngine { get; set; } + /// /// Constructor /// @@ -180,13 +173,7 @@ public class Site : ISite this.settings = settings; Logger = logger; Parser = frontMatterParser; - - // Liquid template options, needed to theme the content - // but also parse URLs - TemplateOptions.MemberAccessStrategy.Register(); - TemplateOptions.MemberAccessStrategy.Register(); - TemplateOptions.MemberAccessStrategy.Register(); - TemplateOptions.MemberAccessStrategy.Register(); + TemplateEngine = new FluidTemplateEngine(); this.clock = clock ?? new SystemClock(); @@ -453,4 +440,4 @@ public class Site : ISite return frontMatter.GetPublishDate is null || frontMatter.GetPublishDate <= clock.Now; } -} \ No newline at end of file +} diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index 796c0461deecbba2ffd954bbdac590b9232be742..e24f897e15879e4cabdb58dd4713b54a6858fff3 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -9,26 +9,25 @@ namespace SuCoS.Models; public class SiteSettings : ISiteSettings { #region ISiteSettings - /// - /// Site Title/Name. - /// + + /// public string Title { get; set; } = string.Empty; - /// - /// Site description - /// + + /// public string? Description { get; set; } = string.Empty; - /// - /// Copyright information - /// + + /// public string? Copyright { get; set; } - /// - /// The base URL that will be used to build public links. - /// + + /// public string BaseURL { get; set; } = string.Empty; + /// + public Dictionary> Outputs { get; set; } = []; + #endregion ISiteSettings /// diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index 396eda4a5f35813900c8506ed07f386fb0724646..ea51196ad5c9ac3d977c070effc2d69c4272eacb 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -1,38 +1,38 @@ - - Exe - net8.0 - enable - enable - true - false - true - true - true - Bruno Massa - ../LICENSE - hhttps://sucos.brunomassa.com - static site generator;ssg;yaml/blog - + + Exe + net8.0 + enable + enable + true + false + true + true + true + Bruno Massa + ../LICENSE + hhttps://sucos.brunomassa.com + static site generator;ssg;yaml/blog + - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/source/TemplateEngine/FluidTemplateEngine.cs b/source/TemplateEngine/FluidTemplateEngine.cs new file mode 100644 index 0000000000000000000000000000000000000000..800a2d5701f4eb3669976a0fb69208dd5432a036 --- /dev/null +++ b/source/TemplateEngine/FluidTemplateEngine.cs @@ -0,0 +1,132 @@ +using Fluid; +using Fluid.Values; +using Microsoft.Extensions.FileProviders; +using SuCoS.Models; + +namespace SuCoS.TemplateEngine; + +/// +/// Fluid template engine +/// +public class FluidTemplateEngine : ITemplateEngine +{ + /// + /// The Fluid parser instance. + /// + private FluidParser FluidParser { get; } = new(); + + /// + /// The Fluid/Liquid template options. + /// + private TemplateOptions TemplateOptions { get; } = new(); + + /// + /// ctor + /// + public FluidTemplateEngine() + { + // Liquid template options, needed to theme the content + // but also parse URLs + TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); + + // Liquid template options, needed to theme the content + // but also parse URLs + TemplateOptions.Filters.AddFilter("whereParams", WhereParamsFilter); + } + + /// + public void Initialize(Site site) + { + ArgumentNullException.ThrowIfNull(site); + + TemplateOptions.FileProvider = new PhysicalFileProvider(Path.GetFullPath(site.SourceThemePath)); + } + + /// + public string Parse(string data, ISite site, IPage page) + { + if (FluidParser.TryParse(data, out var template, out var error)) + { + var context = new TemplateContext(TemplateOptions) + .SetValue("site", site) + .SetValue("page", page); + return template.Render(context); + } + else + { + throw new FormatException(error); + } + } + + /// + public string? ParseResource(string? data, ISite site, IPage page, int counter) + { + if (string.IsNullOrEmpty(data) || !FluidParser.TryParse(data, out var templateFileName, out var errorFileName)) + { + return null; + } + var context = new TemplateContext(TemplateOptions) + .SetValue("site", site) + .SetValue("page", page) + .SetValue("counter", counter); + return templateFileName.Render(context); + } + + /// + /// Fluid/Liquid filter to navigate Params dictionary + /// + /// + /// + /// + /// + /// + protected static ValueTask WhereParamsFilter(FluidValue input, FilterArguments arguments, TemplateContext context) + { + ArgumentNullException.ThrowIfNull(input); + ArgumentNullException.ThrowIfNull(arguments); + + List result = []; + var list = (input as ArrayValue)!.Values; + + var keys = arguments.At(0).ToStringValue().Split('.'); + foreach (var item in list) + { + if (item.ToObjectValue() is IParams param && CheckValueInDictionary(keys, param.Params, arguments.At(1).ToStringValue())) + { + result.Add(item); + } + } + + return new ArrayValue(result); + } + + private static bool CheckValueInDictionary(string[] array, IReadOnlyDictionary dictionary, string value) + { + var currentDictionary = dictionary; + for (var i = 0; i < array.Length; i++) + { + var key = array[i]; + + if (!currentDictionary.TryGetValue(key, out var dictionaryValue)) + { + return false; + } + + if (i == array.Length - 1) + { + return dictionaryValue.Equals(value); + } + + if (dictionaryValue is not Dictionary nestedDictionary) + { + return false; + } + + currentDictionary = nestedDictionary; + } + return false; + } +} diff --git a/source/TemplateEngine/ITemplateEngine.cs b/source/TemplateEngine/ITemplateEngine.cs new file mode 100644 index 0000000000000000000000000000000000000000..639ea2b766b702d92b4ff777091ffc2f05b0874f --- /dev/null +++ b/source/TemplateEngine/ITemplateEngine.cs @@ -0,0 +1,34 @@ +using SuCoS.Models; + +namespace SuCoS.TemplateEngine; + +/// +/// Interface for all Templace Engines (Liquid) +/// +public interface ITemplateEngine +{ + /// + /// Initialization + /// + /// + void Initialize(Site site); + + /// + /// Parse the content using the data template + /// + /// + /// + /// + /// + string Parse(string template, ISite site, IPage page); + + /// + /// Parse the content using the data template for Resource naming + /// + /// + /// + /// + /// + /// + string? ParseResource(string? template, ISite site, IPage page, int counter); +} diff --git a/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig b/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..0489b3488417452a4e0247f4c1ed7636c5706c04 --- /dev/null +++ b/source/YamlDotNet.Analyzers.StaticGenerator/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +dotnet_diagnostic.CS1591.severity = none # disable on debug genereated files diff --git a/test/BaseGeneratorCommandTests.cs b/test/BaseGeneratorCommandTests.cs index de99b7aa479cde381438e4ccbd73dc1cec06471b..4aa4d7c513f878f8cf46e02e2f30a5b398029309 100644 --- a/test/BaseGeneratorCommandTests.cs +++ b/test/BaseGeneratorCommandTests.cs @@ -1,6 +1,7 @@ using Serilog; using SuCoS; using SuCoS.Models.CommandLineOptions; +using SuCoS.TemplateEngine; using System.Reflection; using Xunit; @@ -36,7 +37,7 @@ public class BaseGeneratorCommandTests [Fact] public void CheckValueInDictionary_ShouldWorkCorrectly() { - var type = typeof(BaseGeneratorCommand); + var type = typeof(FluidTemplateEngine); var method = type.GetMethod("CheckValueInDictionary", BindingFlags.NonPublic | BindingFlags.Static); var parameters = new object[] { new[] { "key" }, new Dictionary { { "key", "value" } }, "value" };