diff --git a/.build.Nuke/Build.Compile.cs b/.build.Nuke/Build.Compile.cs index ea4253ce873821b1e1b579a550e585714b35064f..718ee69e81ff1733a4f46273dfac00b108de023f 100644 --- a/.build.Nuke/Build.Compile.cs +++ b/.build.Nuke/Build.Compile.cs @@ -2,7 +2,6 @@ using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.Tools.DotNet; using Nuke.Common.Utilities.Collections; -using Serilog; using static Nuke.Common.Tools.DotNet.DotNetTasks; namespace SuCoS; diff --git a/.build.Nuke/Build.GitLab.cs b/.build.Nuke/Build.GitLab.cs index 5ee48725d6b176deb7b9dcd0b63558eab24216bb..69fc805d88039e42c2ab40ff48c0d274400614f1 100644 --- a/.build.Nuke/Build.GitLab.cs +++ b/.build.Nuke/Build.GitLab.cs @@ -49,13 +49,13 @@ sealed partial class Build : NukeBuild .Executes(async () => { // The package name constructed using packageName, runtimeIdentifier, and Version - var package = $"{packageName}-{runtimeIdentifier}-{CurrentVersion}"; + var package = $"{packageName}-{runtimeIdentifier}-{CurrentTag}"; // The filename of the package, constructed using the package variable var filename = $"{package}.zip"; // The URL for the package in the GitLab generic package registry - var packageLink = GitLabAPIUrl($"packages/generic/{packageName}/{CurrentVersion}/{filename}"); + var packageLink = GitLabAPIUrl($"packages/generic/{packageName}/{CurrentTag}/{filename}"); // Create the zip package var fullpath = Path.GetFullPath(filename); diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index add5c9e9aa0f259f8278be288baddd941ef9aa92..13bf9e700ac6e247b113773c973999aa155330fc 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -1,7 +1,6 @@ using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.Tools.DotNet; -using Serilog; using static Nuke.Common.Tools.DotNet.DotNetTasks; namespace SuCoS; @@ -42,6 +41,9 @@ sealed partial class Build : NukeBuild .SetPublishSingleFile(publishSingleFile) .SetPublishTrimmed(publishTrimmed) .SetAuthors("Bruno Massa") + .SetVersion(CurrentVersion) + .SetAssemblyVersion(CurrentVersion) + .SetInformationalVersion(CurrentVersion) ); }); } diff --git a/.build.Nuke/Build.Version.cs b/.build.Nuke/Build.Version.cs index 7bdc3f99fd55d1262db3163c9c98157ef6d2c1d4..a87d89bfbe85dfa143d363d7541def2c20bc5795 100644 --- a/.build.Nuke/Build.Version.cs +++ b/.build.Nuke/Build.Version.cs @@ -22,7 +22,7 @@ sealed partial class Build : NukeBuild string VersionMajor => $"{gitVersion.Major}"; - string VersionMajorMinor => $"{gitVersion.Major}.{gitVersion.Minor}"; + string VersionMajorMinor => $"{gitVersion.Major}.{gitVersion.Minor}"; /// /// The version in a format that can be used as a tag. @@ -35,7 +35,7 @@ sealed partial class Build : NukeBuild bool HasNewCommits => gitVersion.CommitsSinceVersionSource != "0"; string currentVersion; - string CurrentVersion + string CurrentTag { get { @@ -43,6 +43,7 @@ sealed partial class Build : NukeBuild return currentVersion; } } + string CurrentVersion => CurrentTag.TrimStart('v'); /// /// Prints the current version. @@ -52,8 +53,9 @@ sealed partial class Build : NukeBuild { var lastCommmit = GitTasks.Git("log -1").FirstOrDefault().Text; var status = GitTasks.Git("status").FirstOrDefault().Text; - Log.Information("Current version: {Version}", CurrentVersion); - Log.Information($"GitVersion before = {Version}"); + Log.Information("Current version: {Version}", CurrentVersion); + Log.Information("Current tag: {Version}", CurrentTag); + Log.Information("Next version: {Version}", Version); }); /// diff --git a/README.md b/README.md index 3304dbeda8af27f150021fd375de24f397fddb67..aaf02391d5b9c18493354bfdbea1afc51d6c3ae4 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,15 @@ The vision is to be a close substitute of [Hugo](https://gohugo.io/), but writte +[![Latest release](https://gitlab.com/sucos/sucos/-/badges/release.svg)](https://gitlab.com/sucos/sucos) +![Pipepline](https://gitlab.com/sucos/sucos/badges/main/pipeline.svg?ignore_skipped=true) + ## Usage First, navigate to the **SuCoS** folder. Chris Kibble Then, run the following command: + ```bash SuCoS --source YOUR_SITE_PATH ``` diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index 40feb65c7a602ce2c02c3fcd4f336da5dd26913e..f2375ab812613ec742fe84de073c2830d05a6f93 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Fluid; +using Fluid.Values; using Markdig; using Microsoft.Extensions.FileProviders; using Serilog; @@ -63,7 +64,7 @@ public abstract class BaseGeneratorCommand protected readonly object syncLock = new(); /// - /// The template options. + /// The Fluid/Liquid template options. /// protected readonly TemplateOptions templateOptions = new(); @@ -197,7 +198,7 @@ public abstract class BaseGeneratorCommand try { - site = ReadAppConfig(options: options, frontmatterParser); + site = ParseSiteSettings(options: options, frontmatterParser); if (site is null) { throw new FormatException("Error reading app config"); @@ -212,6 +213,7 @@ public abstract class BaseGeneratorCommand templateOptions.MemberAccessStrategy.Register(); templateOptions.MemberAccessStrategy.Register(); templateOptions.FileProvider = new PhysicalFileProvider(Path.GetFullPath(site.SourceThemePath)); + templateOptions.Filters.AddFilter("whereParams", WhereParamsFilter); // Configure Markdig with the Bibliography extension markdownPipeline = new MarkdownPipelineBuilder() @@ -260,7 +262,7 @@ public abstract class BaseGeneratorCommand site.RegularPages.Add(frontmatter); frontmatter.Permalink = "/" + CreatePermalink(file.filePath, site, frontmatter); - if (site.HomePage is null && frontmatter.SourceFileNameWithoutExtension == "index") + if (site.HomePage is null && frontmatter.SourcePath == "index.md") { site.HomePage = frontmatter; frontmatter.Kind = Kind.index; @@ -300,7 +302,7 @@ public abstract class BaseGeneratorCommand /// The generate options. /// The frontmatter parser. /// The site configuration. - protected static Site ReadAppConfig(IGenerateOptions options, IFrontmatterParser frontmatterParser) + protected static Site ParseSiteSettings(IGenerateOptions options, IFrontmatterParser frontmatterParser) { if (options is null) { @@ -312,18 +314,18 @@ public abstract class BaseGeneratorCommand } // Read the main configation - var configFilePath = Path.Combine(options.Source, configFile); - if (!File.Exists(configFilePath)) + var filePath = Path.Combine(options.Source, configFile); + if (!File.Exists(filePath)) { throw new FileNotFoundException($"The {configFile} file was not found in the specified source directory: {options.Source}"); } - var configFileContent = File.ReadAllText(configFilePath); - var config = frontmatterParser.ParseAppConfig(configFileContent); - config.SourcePath = options.Source; - config.OutputPath = options.Output; + var fileContent = File.ReadAllText(filePath); + var settings = frontmatterParser.ParseSiteSettings(fileContent); + settings.SourcePath = options.Source; + settings.OutputPath = options.Output; - return config; + return settings; } /// @@ -343,7 +345,7 @@ public abstract class BaseGeneratorCommand baseGeneratorCommand: this, title: site.Title, site: site, - sourcePath: Path.Combine(relativePath, "index"), + sourcePath: Path.Combine(relativePath, "/index.md"), sourceFileNameWithoutExtension: "index", sourcePathDirectory: null ) @@ -620,4 +622,67 @@ public abstract class BaseGeneratorCommand tagFrontmatterCache.Clear(); IgnoreCacheBefore = DateTime.Now; } + + /// + /// Fluid/Liquid filter to navigate Params dictionary + /// + /// + /// + /// + /// + /// + public static ValueTask WhereParamsFilter(FluidValue input, FilterArguments arguments, TemplateContext context) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + if (arguments is null) + { + throw new ArgumentNullException(nameof(arguments)); + } + + List result = new(); + 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 ValueTask(new ArrayValue((IEnumerable)result)); + } + + static bool CheckValueInDictionary(string[] array, Dictionary dictionary, string value) + { + var key = array[0]; + + // Check if the key exists in the dictionary + if (dictionary.TryGetValue(key, out var dictionaryValue)) + { + // If it's the last element in the array, check if the dictionary value matches the value parameter + if (array.Length == 1) + { + return dictionaryValue.Equals(value); + } + + // Check if the value is another dictionary + else if (dictionaryValue is Dictionary nestedDictionary) + { + // Create a new array without the current key + var newArray = new string[array.Length - 1]; + Array.Copy(array, 1, newArray, 0, newArray.Length); + + // Recursively call the method with the nested dictionary and the new array + return CheckValueInDictionary(newArray, nestedDictionary, value); + } + } + + // If the key doesn't exist or the value is not a dictionary, return false + return false; + } } diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 58a328bbc2334577c23b35e1192aadeb25346e2b..6ddb9acb3199869ead89f34b0ed5a5d779c823ad 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -9,7 +9,7 @@ namespace SuCoS; /// /// The meta data about each content Markdown file. /// -public class Frontmatter +public class Frontmatter : IParams { /// /// The content Title. @@ -94,6 +94,9 @@ public class Frontmatter } } + /// + public Dictionary Params { get; set; } = new(); + /// /// Raw content, from the Markdown file. /// diff --git a/source/Models/IParams.cs b/source/Models/IParams.cs new file mode 100644 index 0000000000000000000000000000000000000000..4045f9ad326890c48f7b2430b4499eb99072dc4c --- /dev/null +++ b/source/Models/IParams.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace SuCoS; + +/// +/// Interface for all classes that will implement a catch-all YAML +/// values. +/// +public interface IParams +{ + /// + /// Recursive dictionary with non-standard values + /// + Dictionary Params { get; set; } +} \ No newline at end of file diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 8bad961856840a89e647038dfcf0d49902de3d5b..82067a85be3a17af2c46c1b3c6da9284af9945b2 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -6,7 +6,7 @@ namespace SuCoS.Models; /// /// The main configuration of the program, primarily extracted from the app.yaml file. /// -public class Site +public class Site : IParams { /// /// Site Title. @@ -67,4 +67,7 @@ public class Site /// List of all content to be scanned and processed. /// public List<(string filePath, string content)> RawPages { get; set; } = new(); + + /// + public Dictionary Params { get; set; } = new(); } diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 5f6554a4f7218e631dac7e7a85c23d5c88849156..64a48dc1bb01c46ed39c1015c8fa79efce3c3aec 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IFrontmatterParser.cs @@ -22,5 +22,5 @@ public interface IFrontmatterParser /// /// /// - Site ParseAppConfig(string configFileContent); + Site ParseSiteSettings(string configFileContent); } \ No newline at end of file diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index d53544ad9b66783665d86864af3d8001a5a3c2d0..8389b596b02f920dd933d61e0222f9e50ac4e3d7 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; +using Serilog; using SuCoS.Models; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -13,8 +15,24 @@ namespace SuCoS.Parser; /// public partial class YAMLParser : IFrontmatterParser { + [GeneratedRegex(@"^---\s*[\r\n](?.*?)[\r\n]---\s*", RegexOptions.Singleline)] - private partial Regex regex(); + private partial Regex YAMLRegex(); + + /// + /// YamlDotNet parser, strictly set to allow automatically parse only known fields + /// + readonly IDeserializer yamlDeserializerRigid = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + /// + /// YamlDotNet parser to loosely parse the YAML file. Used to include all non-matching fields + /// into Params. + /// + readonly IDeserializer yamlDeserializer = new DeserializerBuilder() + .Build(); /// public Frontmatter? ParseFrontmatter(Site site, string filePath, ref string fileContent, BaseGeneratorCommand frontmatterManager) @@ -25,16 +43,15 @@ public partial class YAMLParser : IFrontmatterParser } Frontmatter? frontmatter = null; - var match = regex().Match(fileContent); + var match = YAMLRegex().Match(fileContent); if (match.Success) { - var frontmatterString = match.Groups["frontmatter"].Value; + var content = match.Groups["frontmatter"].Value; fileContent = fileContent[match.Length..].TrimStart('\n'); // Parse the front matter string into Frontmatter properties - var yamlDeserializer = new DeserializerBuilder().Build(); - var yamlObject = yamlDeserializer.Deserialize(new StringReader(frontmatterString)); + var yamlObject = yamlDeserializer.Deserialize(new StringReader(content)); if (yamlObject is Dictionary frontmatterDictionary) { @@ -93,6 +110,8 @@ public partial class YAMLParser : IFrontmatterParser } } + ParseParams(frontmatter, typeof(Frontmatter), content); + foreach (var tagName in tags) { _ = frontmatterManager.CreateTagFrontmatter(site, tagName: tagName, frontmatter); @@ -118,13 +137,40 @@ public partial class YAMLParser : IFrontmatterParser } /// - public Site ParseAppConfig(string configFileContent) + public Site ParseSiteSettings(string content) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(PascalCaseNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); - var config = deserializer.Deserialize(configFileContent); - return config; + var settings = yamlDeserializerRigid.Deserialize(content); + ParseParams(settings, typeof(Site), content); + return settings; + } + + /// + /// Parse all YAML files for non-matching fields. + /// + /// Site or Frontmatter object, that implements IParams + /// The type (Site or Frontmatter) + /// YAML content + void ParseParams(IParams settings, Type type, string content) + { + var yamlObject = yamlDeserializer.Deserialize(new StringReader(content)); + if (yamlObject is Dictionary yamlDictionary) + { + foreach (var key in yamlDictionary.Keys.Cast()) + { + // If the property is not a standard Frontmatter property + if (type.GetProperty(key) == null) + { + // 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/ServeCommand.cs b/source/ServeCommand.cs index 62a62bdf889e8055239f4c58ae106c6901053aa7..dc7fd095bcc5bd400f20041c7d66872d16973ac9 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -98,7 +98,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable { try { - site = ReadAppConfig(options: options, frontmatterParser); + site = ParseSiteSettings(options: options, frontmatterParser); if (site is null) { throw new FormatException("Error reading app config"); @@ -126,11 +126,6 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable if (url != null) { _ = pages.TryAdd(url, frontmatter); - if (Path.GetFileName(url) == "index.html") - { - var path = Path.GetDirectoryName(url); - _ = pages.TryAdd(path!, frontmatter); - } } else {