diff --git a/.gitignore b/.gitignore
index 0de7bb9b2d0bc3d6b4c16b3316312fc91266b19d..ec824ea1c152d4a5f31277da4f30c2cd83ce944e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,4 +47,6 @@ artifacts/
output/
project.fragment.lock.json
project.lock.json
-**/coverage-results/
\ No newline at end of file
+**/coverage-results/
+.idea/
+qodana.yaml
diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs
index 76c17b0f9560eab587ed8f02ec1408a01094a407..dcafd2ef5fe0cda53f144b243fb47ddbcff3129a 100644
--- a/source/BaseGeneratorCommand.cs
+++ b/source/BaseGeneratorCommand.cs
@@ -6,6 +6,7 @@ using Fluid.Values;
using Serilog;
using SuCoS.Helpers;
using SuCoS.Models;
+using SuCoS.Models.CommandLineOptions;
using SuCoS.Parser;
namespace SuCoS;
@@ -26,9 +27,9 @@ public abstract class BaseGeneratorCommand
protected Site site;
///
- /// The frontmatter parser instance. The default is YAML.
+ /// The front matter parser instance. The default is YAML.
///
- protected readonly IFrontmatterParser frontmatterParser = new YAMLParser();
+ protected readonly IFrontMatterParser frontMatterParser = new YAMLParser();
///
/// The stopwatch reporter.
@@ -57,7 +58,7 @@ public abstract class BaseGeneratorCommand
logger.Information("Source path: {source}", propertyValue: options.Source);
- site = SiteHelper.Init(configFile, options, frontmatterParser, WhereParamsFilter, logger, stopwatch);
+ site = SiteHelper.Init(configFile, options, frontMatterParser, WhereParamsFilter, logger, stopwatch);
}
///
@@ -96,31 +97,29 @@ public abstract class BaseGeneratorCommand
private static bool CheckValueInDictionary(string[] array, IReadOnlyDictionary dictionary, string value)
{
- var key = array[0];
-
- // If the key doesn't exist or the value is not a dictionary, return false
- if (!dictionary.TryGetValue(key, out var dictionaryValue))
+ var currentDictionary = dictionary;
+ for (var i = 0; i < array.Length; i++)
{
- return false;
- }
+ var key = array[i];
- // 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);
- }
+ if (!currentDictionary.TryGetValue(key, out var dictionaryValue))
+ {
+ return false;
+ }
- // Check if the value is another dictionary
- if (dictionaryValue is not Dictionary nestedDictionary)
- {
- return false;
- }
+ if (i == array.Length - 1)
+ {
+ return dictionaryValue.Equals(value);
+ }
- // Create a new array without the current key
- var newArray = new string[array.Length - 1];
- Array.Copy(array, 1, newArray, 0, newArray.Length);
+ if (dictionaryValue is not Dictionary nestedDictionary)
+ {
+ return false;
+ }
- // Recursively call the method with the nested dictionary and the new array
- return CheckValueInDictionary(newArray, nestedDictionary, value);
+ currentDictionary = nestedDictionary;
+ }
+ return false;
}
+
}
diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs
index beffae0838b422f66bf14b13fbc45f1ad5306a09..64db95611b7cc6e1061b0943afd4df4ca8895e95 100644
--- a/source/BuildCommand.cs
+++ b/source/BuildCommand.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
+using SuCoS.Models.CommandLineOptions;
namespace SuCoS;
@@ -11,6 +12,8 @@ namespace SuCoS;
///
public class BuildCommand : BaseGeneratorCommand
{
+ private readonly BuildOptions options;
+
///
/// Entry point of the build command. It will be called by the main program
/// in case the build command is invoked (which is by default).
@@ -19,10 +22,7 @@ public class BuildCommand : BaseGeneratorCommand
/// The logger instance. Injectable for testing
public BuildCommand(BuildOptions options, ILogger logger) : base(options, logger)
{
- if (options is null)
- {
- throw new ArgumentNullException(nameof(options));
- }
+ this.options = options ?? throw new ArgumentNullException(nameof(options));
logger.Information("Output path: {output}", options.Output);
@@ -30,10 +30,10 @@ public class BuildCommand : BaseGeneratorCommand
CreateOutputFiles();
// Copy theme static folder files into the root of the output folder
- CopyFolder(site.SourceThemeStaticPath, site.OutputPath);
+ CopyFolder(site.SourceThemeStaticPath, options.Output);
// Copy static folder files into the root of the output folder
- CopyFolder(site.SourceStaticPath, site.OutputPath);
+ CopyFolder(site.SourceStaticPath, options.Output);
// Generate the build report
stopwatch.LogReport(site.Title);
@@ -47,13 +47,13 @@ public class BuildCommand : BaseGeneratorCommand
var pagesCreated = 0; // counter to keep track of the number of pages created
_ = Parallel.ForEach(site.PagesReferences, pair =>
{
- var (url, frontmatter) = pair;
- var result = frontmatter.CreateOutputFile();
+ var (url, page) = pair;
+ var result = page.CompleteContent;
var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/');
// Generate the output path
- var outputAbsolutePath = Path.Combine(site.OutputPath, path);
+ var outputAbsolutePath = Path.Combine(options.Output, path);
var outputDirectory = Path.GetDirectoryName(outputAbsolutePath);
if (!Directory.Exists(outputDirectory))
diff --git a/source/BuildOptions.cs b/source/BuildOptions.cs
deleted file mode 100644
index fc272146c7a6c445fc7de657df22e4ed66fbdec7..0000000000000000000000000000000000000000
--- a/source/BuildOptions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace SuCoS;
-
-///
-/// Command line options for the build command.
-///
-public class BuildOptions : IGenerateOptions
-{
- ///
- public string Source { get; set; } = ".";
-
- ///
- public string? Output { get; init; }
-
- ///
- public bool Future { get; set; }
-}
diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs
index 963b662eca34de27caf83bd1864b4c2539705725..25c490e9d3189ab8536a19e97253e13f7c73a54b 100644
--- a/source/Helpers/FileUtils.cs
+++ b/source/Helpers/FileUtils.cs
@@ -12,33 +12,33 @@ namespace SuCoS.Helpers;
public static class FileUtils
{
///
- /// Gets the content of a template file based on the frontmatter and the theme path.
+ /// Gets the content of a template file based on the page and the theme path.
///
/// The theme path.
- /// The frontmatter to determine the template index.
- /// Site data.
+ /// The page to determine the template index.
+ /// Site data.
/// Indicates whether the template is a base template.
/// The content of the template file.
- public static string GetTemplate(string themePath, Frontmatter frontmatter, Site site, bool isBaseTemplate = false)
+ public static string GetTemplate(string themePath, Page page, SiteCacheManager cacheManager, bool isBaseTemplate = false)
{
- if (frontmatter is null)
+ if (page is null)
{
- throw new ArgumentNullException(nameof(frontmatter));
+ throw new ArgumentNullException(nameof(page));
}
- if (site is null)
+ if (cacheManager is null)
{
- throw new ArgumentNullException(nameof(site));
+ throw new ArgumentNullException(nameof(cacheManager));
}
- var index = (frontmatter.Section, frontmatter.Kind, frontmatter.Type);
+ var index = (page.Section, page.Kind, page.Type);
- var cache = isBaseTemplate ? site.baseTemplateCache : site.contentTemplateCache;
+ var cache = isBaseTemplate ? cacheManager.baseTemplateCache : cacheManager.contentTemplateCache;
// Check if the template content is already cached
if (cache.TryGetValue(index, out var content))
return content;
- var templatePaths = GetTemplateLookupOrder(themePath, frontmatter, isBaseTemplate);
+ var templatePaths = GetTemplateLookupOrder(themePath, page, isBaseTemplate);
content = GetTemplate(templatePaths);
// Cache the template content for future use
@@ -72,25 +72,25 @@ public static class FileUtils
}
///
- /// Gets the lookup order for template files based on the theme path, frontmatter, and template type.
+ /// Gets the lookup order for template files based on the theme path, page, and template type.
///
/// The theme path.
- /// The frontmatter to determine the template index.
+ /// The page to determine the template index.
/// Indicates whether the template is a base template.
/// The list of template paths in the lookup order.
- private static List GetTemplateLookupOrder(string themePath, Frontmatter frontmatter, bool isBaseTemplate)
+ private static List GetTemplateLookupOrder(string themePath, Page page, bool isBaseTemplate)
{
- if (frontmatter is null)
+ if (page is null)
{
- throw new ArgumentNullException(nameof(frontmatter));
+ throw new ArgumentNullException(nameof(page));
}
- // Generate the lookup order for template files based on the theme path, frontmatter section, type, and kind
- var sections = frontmatter.Section is not null ? new[] { frontmatter.Section, string.Empty } : new[] { string.Empty };
- var types = new[] { frontmatter.Type, "_default" };
+ // Generate the lookup order for template files based on the theme path, page section, type, and kind
+ var sections = page.Section is not null ? new[] { page.Section, string.Empty } : new[] { string.Empty };
+ var types = new[] { page.Type, "_default" };
var kinds = isBaseTemplate
- ? new[] { frontmatter.Kind + "-baseof", "baseof" }
- : new[] { frontmatter.Kind.ToString() };
+ ? new[] { page.Kind + "-baseof", "baseof" }
+ : new[] { page.Kind.ToString() };
// for each section, each type and each kind
return (from section in sections
diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs
index 5dcc818b3161b6903aeb750c653483fea6443f8b..33d31b971e1027163fdc4af683a1cd7e06061085 100644
--- a/source/Helpers/SiteHelper.cs
+++ b/source/Helpers/SiteHelper.cs
@@ -1,9 +1,11 @@
using System;
using System.IO;
using Fluid;
+using Markdig;
using Microsoft.Extensions.FileProviders;
using Serilog;
using SuCoS.Models;
+using SuCoS.Models.CommandLineOptions;
using SuCoS.Parser;
namespace SuCoS.Helpers;
@@ -13,27 +15,41 @@ namespace SuCoS.Helpers;
///
public static class SiteHelper
{
+ ///
+ /// Markdig 20+ built-in extensions
+ ///
+ /// https://github.com/xoofx/markdig
+ public static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
+ .UseAdvancedExtensions()
+ .Build();
+
///
/// 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, IFrontMatterParser frontMatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch)
{
if (stopwatch is null)
{
throw new ArgumentNullException(nameof(stopwatch));
}
- Site site;
+ SiteSettings siteSettings;
try
{
- site = ParseSettings(configFile, options, frontmatterParser, whereParamsFilter, logger);
+ siteSettings = ParseSettings(configFile, options, frontMatterParser);
}
catch
{
throw new FormatException("Error reading app config");
}
+ var site = new Site(options, siteSettings, frontMatterParser, 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");
@@ -78,20 +94,18 @@ public static class SiteHelper
/// Reads the application settings.
///
/// The generate options.
- /// The frontmatter parser.
+ /// The front matter parser.
/// The site settings file.
- /// The method to be used in the whereParams.
- /// The logger instance. Injectable for testing
/// The site settings.
- private static Site ParseSettings(string configFile, IGenerateOptions options, IFrontmatterParser frontmatterParser, FilterDelegate whereParamsFilter, ILogger logger)
+ private static SiteSettings ParseSettings(string configFile, IGenerateOptions options, IFrontMatterParser frontMatterParser)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
- if (frontmatterParser is null)
+ if (frontMatterParser is null)
{
- throw new ArgumentNullException(nameof(frontmatterParser));
+ throw new ArgumentNullException(nameof(frontMatterParser));
}
try
@@ -104,22 +118,8 @@ public static class SiteHelper
}
var fileContent = File.ReadAllText(filePath);
- var site = frontmatterParser.ParseSiteSettings(fileContent);
-
- site.Logger = logger;
- site.options = options;
- site.SourceDirectoryPath = options.Source;
- site.OutputPath = options.Output!;
-
- // Liquid template options, needed to theme the content
- // but also parse URLs
- site.TemplateOptions.Filters.AddFilter("whereParams", whereParamsFilter);
-
- if (site is null)
- {
- throw new FormatException("Error reading app config");
- }
- return site;
+ var siteSettings = frontMatterParser.ParseSiteSettings(fileContent) ?? throw new FormatException("Error reading app config");
+ return siteSettings;
}
catch
{
diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs
index 1abc0ac7be750c79391773e2ca2f9dbfb2311cd9..0b83bfdcfe8e1e8826c6b568f9268cb0b1ac1a69 100644
--- a/source/Helpers/Urlizer.cs
+++ b/source/Helpers/Urlizer.cs
@@ -12,6 +12,7 @@ public static partial class Urlizer
{
[GeneratedRegex(@"[^a-zA-Z0-9]+")]
private static partial Regex UrlizeRegexAlpha();
+
[GeneratedRegex(@"[^a-zA-Z0-9.]+")]
private static partial Regex UrlizeRegexAlphaDot();
diff --git a/source/Models/BasicContent.cs b/source/Models/BasicContent.cs
deleted file mode 100644
index f0f6f81f19f22c7575cd45b01b23bfbc37fe4dbe..0000000000000000000000000000000000000000
--- a/source/Models/BasicContent.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace SuCoS.Models;
-
-///
-/// A scafold structure to help creating system-generated content, like
-/// tag, section or index pages
-///
-public class BasicContent : IBaseContent
-{
- ///
- public string Title { get; }
-
- ///
- public string Section { get; }
-
- ///
- public Kind Kind { get; }
-
- ///
- public string Type { get; }
-
- ///
- public string URL { get; }
-
- ///
- /// Constructor
- ///
- ///
- ///
- ///
- ///
- ///
- public BasicContent(string title, string section, string type, string url, Kind kind = Kind.list)
- {
- Title = title;
- Section = section;
- Kind = kind;
- Type = type;
- URL = url;
- }
-}
diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs
new file mode 100644
index 0000000000000000000000000000000000000000..208de262f6eac961e597487e3ef8f55d34ff1389
--- /dev/null
+++ b/source/Models/CommandLineOptions/BuildOptions.cs
@@ -0,0 +1,21 @@
+namespace SuCoS.Models.CommandLineOptions;
+
+///
+/// Command line options for the build command.
+///
+public class BuildOptions : GenerateOptions
+{
+ ///
+ /// The path of the output files.
+ ///
+ public string Output { get; }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public BuildOptions(string output)
+ {
+ Output = output;
+ }
+}
diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs
new file mode 100644
index 0000000000000000000000000000000000000000..709691b4f82b3c693badea9a3027d1b76402c4b2
--- /dev/null
+++ b/source/Models/CommandLineOptions/GenerateOptions.cs
@@ -0,0 +1,13 @@
+namespace SuCoS.Models.CommandLineOptions;
+
+///
+/// Basic Command line options for the serve and build command.
+///
+public class GenerateOptions : IGenerateOptions
+{
+ ///
+ public string Source { get; init; } = ".";
+
+ ///
+ public bool Future { get; init; }
+}
diff --git a/source/IGenerateOptions.cs b/source/Models/CommandLineOptions/IGenerateOptions.cs
similarity index 70%
rename from source/IGenerateOptions.cs
rename to source/Models/CommandLineOptions/IGenerateOptions.cs
index 585b7491a86aa6e47879a65a21ff866a21edcb7a..472bb541a09ad7e51b7681b168d6b40a33d74fd7 100644
--- a/source/IGenerateOptions.cs
+++ b/source/Models/CommandLineOptions/IGenerateOptions.cs
@@ -1,4 +1,4 @@
-namespace SuCoS;
+namespace SuCoS.Models.CommandLineOptions;
///
/// Command line options for the build and serve command.
@@ -10,11 +10,6 @@ public interface IGenerateOptions
///
string Source { get; }
- ///
- /// The path of the output files.
- ///
- public string? Output { get; }
-
///
/// Consider
///
diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs
new file mode 100644
index 0000000000000000000000000000000000000000..9e2b5c969726d7babb800b916303453a30410b92
--- /dev/null
+++ b/source/Models/CommandLineOptions/ServeOptions.cs
@@ -0,0 +1,8 @@
+namespace SuCoS.Models.CommandLineOptions;
+
+///
+/// Command line options for the serve command.
+///
+public class ServeOptions : GenerateOptions
+{
+}
diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs
new file mode 100644
index 0000000000000000000000000000000000000000..854c0460a6269423e128cbc2cf28fb42a44f54e1
--- /dev/null
+++ b/source/Models/FrontMatter.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using YamlDotNet.Serialization;
+
+namespace SuCoS.Models;
+
+///
+/// A scafold structure to help creating system-generated content, like
+/// tag, section or index pages
+///
+public class FrontMatter : IFrontMatter
+{
+ #region IFrontMatter
+
+ ///
+ public string? Title { get; init; } = string.Empty;
+
+ ///
+ public string? Type { get; set; } = "page";
+
+ ///
+ public string? URL { get; init; }
+
+ ///
+ public List? Aliases { get; init; }
+
+ ///
+ public string? Section { get; set; } = string.Empty;
+
+ ///
+ public DateTime? Date { get; init; }
+
+ ///
+ public DateTime? LastMod { get; init; }
+
+ ///
+ public DateTime? PublishDate { get; init; }
+
+ ///
+ public DateTime? ExpiryDate { get; init; }
+
+ ///
+ public int Weight { get; init; } = 0;
+
+ ///
+ public List? Tags { get; init; }
+
+ ///
+ [YamlIgnore]
+ public string RawContent { get; set; } = string.Empty;
+
+ ///
+ [YamlIgnore]
+ public Kind Kind { get; set; } = Kind.single;
+
+ ///
+ [YamlIgnore]
+ public string? SourcePath { get; set; }
+
+ ///
+ [YamlIgnore]
+ public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath);
+
+ ///
+ [YamlIgnore]
+ public string? SourcePathDirectory => Path.GetDirectoryName(SourcePath);
+
+ ///
+ [YamlIgnore]
+ public DateTime? GetPublishDate => PublishDate ?? Date;
+
+ ///
+ public Dictionary Params { get; set; } = new();
+
+ #endregion IFrontMatter
+
+ ///
+ /// Constructor
+ ///
+ public FrontMatter() { }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public FrontMatter(string title, string sourcePath)
+ {
+ Title = title;
+ SourcePath = sourcePath;
+ }
+}
diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs
deleted file mode 100644
index 6500798ce9d0a0d3423a82a4b0e2334db1990510..0000000000000000000000000000000000000000
--- a/source/Models/Frontmatter.cs
+++ /dev/null
@@ -1,465 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using Fluid;
-using Markdig;
-using SuCoS.Helpers;
-using YamlDotNet.Serialization;
-
-namespace SuCoS.Models;
-
-///
-/// The meta data about each content Markdown file.
-///
-public class Frontmatter : IBaseContent, IParams
-{
- #region IBaseContent
-
- ///
- public string? Title { get; set; } = string.Empty;
-
- ///
- public string? Section { get; set; } = string.Empty;
-
- ///
- public Kind Kind { get; set; } = Kind.single;
-
- ///
- public string? Type { get; set; } = "page";
-
- ///
- public string? URL { get; init; }
-
- #endregion IBaseContent
-
- #region IParams
-
- ///
- [YamlIgnore]
- public Dictionary Params { get; set; } = new();
-
- #endregion IParams
-
- ///
- /// Gets or sets the date of the page.
- ///
- public DateTime? Date { get; set; }
-
- ///
- /// Gets or sets the last modification date of the page.
- ///
- public DateTime? LastMod { get; set; }
-
- ///
- /// Gets or sets the publish date of the page.
- ///
- public DateTime? PublishDate { get; set; }
-
- ///
- /// Gets or sets the expiry date of the page.
- ///
- public DateTime? ExpiryDate { get; set; }
-
- ///
- /// The path of the file, if it's a file.
- ///
- public string? SourcePath { get; set; }
-
- ///
- /// Secondary URL patterns to be used to create the url.
- ///
- public List? Aliases { get; set; }
-
- ///
- /// Page weight. Useful for sorting.
- ///
- public int Weight { get; set; } = 0;
-
- ///
- /// A list of tags, if any.
- ///
- public List? Tags { get; set; }
-
- ///
- /// The source filename, without the extension. ;)
- ///
- [YamlIgnore]
- public string? SourceFileNameWithoutExtension { get; set; }
-
- ///
- /// The source directory of the file.
- ///
- [YamlIgnore]
- public string? SourcePathDirectory { get; set; }
-
- ///
- /// The source directory of the file.
- ///
- [YamlIgnore]
- public string? SourcePathLastDirectory => new DirectoryInfo(SourcePathDirectory ?? string.Empty).Name;
-
- ///
- /// Point to the site configuration.
- ///
- [YamlIgnore]
- public Site Site { get; set; }
-
- ///
- /// Secondary URL patterns to be used to create the url.
- ///
- [YamlIgnore]
- public List? AliasesProcessed { get; set; }
-
- ///
- /// The URL for the content.
- ///
- [YamlIgnore]
- public string? Permalink { get; set; }
-
- ///
- /// Raw content, from the Markdown file.
- ///
- [YamlIgnore]
- public string RawContent { get; set; } = string.Empty;
-
- ///
- /// Other content that mention this content.
- /// Used to create the tags list and Related Posts section.
- ///
- [YamlIgnore]
- public ConcurrentBag? PagesReferences { get; set; }
-
- ///
- /// Other content that mention this content.
- /// Used to create the tags list and Related Posts section.
- ///
- [YamlIgnore]
- public Frontmatter? Parent { get; set; }
-
- ///
- /// Plain markdown content, without HTML.
- ///
- [YamlIgnore]
- public string Plain => Markdown.ToPlainText(RawContent, Site.MarkdownPipeline);
-
- ///
- /// A list of tags, if any.
- ///
- [YamlIgnore]
- public List? TagsReference { get; set; }
-
- ///
- /// Check if the page is expired
- ///
- [YamlIgnore]
- public bool IsDateExpired => ExpiryDate is not null && ExpiryDate <= clock.Now;
-
- ///
- /// Check if the page is publishable
- ///
- [YamlIgnore]
- public bool IsDatePublishable => GetPublishDate is null || GetPublishDate <= clock.Now;
-
- ///
- /// Just a simple check if the current page is the home page
- ///
- [YamlIgnore]
- public bool IsHome => Site.Home == this;
-
- ///
- /// Just a simple check if the current page is a section page
- ///
- [YamlIgnore]
- public bool IsSection => Type == "section";
-
- ///
- /// Just a simple check if the current page is a "page"
- ///
- [YamlIgnore]
- public bool IsPage => Type == "page";
-
- ///
- /// The number of words in the main content
- ///
- [YamlIgnore]
- public int WordCount =>
- Plain.Split(new char[] { ' ', ',', ';', '.', '!', '"', '(', ')', '?' },
- StringSplitOptions.RemoveEmptyEntries).Length;
-
- ///
- /// The markdown content converted to HTML
- ///
- public string ContentPreRendered
- {
- get
- {
- contentPreRenderedCached ??= Markdown.ToHtml(RawContent, Site.MarkdownPipeline);
- return contentPreRenderedCached;
- }
- }
-
- ///
- /// The processed content.
- ///
- public string Content
- {
- get
- {
- if (contentCacheTime is not null && !(Site.IgnoreCacheBefore > contentCacheTime))
- {
- return contentCache!;
- }
- contentCache = CreateContent();
- contentCacheTime = clock.UtcNow;
- return contentCache!;
- }
- }
-
- ///
- /// Other content that mention this content.
- /// Used to create the tags list and Related Posts section.
- ///
- public IEnumerable Pages
- {
- get
- {
- if (PagesReferences is null)
- {
- return new List();
- }
-
- if (pagesCached is not null)
- {
- return pagesCached;
- }
-
- pagesCached ??= new();
- foreach (var permalink in PagesReferences)
- {
- pagesCached.Add(Site.PagesReferences[permalink]);
- }
- return pagesCached;
- }
- }
-
- ///
- /// List of pages from the content folder.
- ///
- public IEnumerable RegularPages
- {
- get
- {
- regularPagesCache ??= Pages
- .Where(frontmatter => frontmatter.Kind == Kind.single)
- .ToList();
- return regularPagesCache;
- }
- }
-
- ///
- /// Get all URLs related to this content.
- ///
- public List Urls
- {
- get
- {
- var urls = new List();
- if (Permalink is not null)
- {
- urls.Add(Permalink);
- }
-
- if (AliasesProcessed is not null)
- {
- urls.AddRange(from aliases in AliasesProcessed
- select aliases);
- }
-
- return urls;
- }
- }
-
- ///
- /// The markdown content.
- ///
- private string? contentPreRenderedCached { get; set; }
-
- ///
- /// The cached content.
- ///
- private string? contentCache { get; set; }
-
- ///
- /// The time when the content was cached.
- ///
- private DateTime? contentCacheTime { get; set; }
-
- private const string urlForIndex = @"{%- liquid
-if page.Parent
-echo page.Parent.Permalink
-echo '/'
-endif
-if page.Title != ''
-echo page.Title
-else
-echo page.SourcePathLastDirectory
-endif
--%}";
- private const string urlForNonIndex = @"{%- liquid
-if page.Parent
-echo page.Parent.Permalink
-echo '/'
-endif
-if page.Title != ''
-echo page.Title
-else
-echo page.SourceFileNameWithoutExtension
-endif
--%}";
-
- private List? regularPagesCache;
-
- private List? pagesCached { get; set; }
-
- private DateTime? GetPublishDate => PublishDate ?? Date;
-
- private ISystemClock clock => Site.Clock;
-
- ///
- /// Required.
- ///
- public Frontmatter(
- string title,
- string sourcePath,
- Site site,
- string? sourceFileNameWithoutExtension = null,
- string? sourcePathDirectory = null)
- {
- Title = title;
- Site = site;
- SourcePath = sourcePath;
- SourceFileNameWithoutExtension = sourceFileNameWithoutExtension ?? Path.GetFileNameWithoutExtension(sourcePath);
- SourcePathDirectory = sourcePathDirectory ?? Path.GetDirectoryName(sourcePath) ?? string.Empty;
- }
-
- ///
- /// Constructor
- ///
- public Frontmatter()
- {
- }
-
- ///
- /// Check if the page have a publishing date from the past.
- ///
- /// options
- ///
- public bool IsValidDate(IGenerateOptions? options)
- {
- return !IsDateExpired && (IsDatePublishable || (options?.Future ?? false));
- }
-
- ///
- /// Gets the Permalink path for the file.
- ///
- /// The URL to consider. If null, we get frontmatter.URL
- /// The output path.
- public string CreatePermalink(string? URLforce = null)
- {
- var isIndex = SourceFileNameWithoutExtension == "index";
-
- var permaLink = string.Empty;
-
- URLforce ??= URL ?? (isIndex ? urlForIndex : urlForNonIndex);
-
- try
- {
- if (Site.FluidParser.TryParse(URLforce, out var template, out var error))
- {
- var context = new TemplateContext(Site.TemplateOptions)
- .SetValue("page", this);
- permaLink = template.Render(context);
- }
- else
- {
- throw new FormatException(error);
- }
- }
- catch (Exception ex)
- {
- Site.Logger?.Error(ex, "Error converting URL: {URLforce}", URLforce);
- }
-
- if (!Path.IsPathRooted(permaLink) && !permaLink.StartsWith('/'))
- {
- permaLink = '/' + permaLink;
- }
-
- return Urlizer.UrlizePath(permaLink);
- }
-
- ///
- /// Creates the output file by applying the theme templates to the frontmatter content.
- ///
- /// The processed output file content.
- public string CreateOutputFile()
- {
- // Process the theme base template
- // If the theme base template file is available, parse and render the template using the frontmatter data
- // Otherwise, use the processed content as the final result
- // Any error during parsing is logged, and an empty string is returned
- // The final result is stored in the 'result' variable and returned
- var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site, true);
- if (string.IsNullOrEmpty(fileContents))
- {
- return Content;
- }
-
- if (Site.FluidParser.TryParse(fileContents, out var template, out var error))
- {
- var context = new TemplateContext(Site.TemplateOptions);
- _ = context.SetValue("page", this);
- return template.Render(context);
- }
-
- Site.Logger?.Error("Error parsing theme template: {Error}", error);
- return string.Empty;
- }
-
- ///
- /// Create the page content, with converted Markdown and themed.
- ///
- ///
- private string CreateContent()
- {
- var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site);
- // Theme content
- if (string.IsNullOrEmpty(fileContents))
- {
- return ContentPreRendered;
- }
-
- if (Site.FluidParser.TryParse(fileContents, out var template, out var error))
- {
- var context = new TemplateContext(Site.TemplateOptions)
- .SetValue("page", this);
- try
- {
- var rendered = template.Render(context);
- return rendered;
- }
- catch (Exception ex)
- {
- Site.Logger?.Error(ex, "Error rendering theme template: {Error}", error);
- return string.Empty;
- }
- }
-
- Site.Logger?.Error("Error parsing theme template: {Error}", error);
- return string.Empty;
-
- }
-}
diff --git a/source/Models/IBaseContent.cs b/source/Models/IBaseContent.cs
deleted file mode 100644
index 886cd6eef93267d7bd96b7bdff57fa380c8b7294..0000000000000000000000000000000000000000
--- a/source/Models/IBaseContent.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace SuCoS.Models;
-
-///
-/// Basic structure needed to generate user content and system content
-///
-public interface IBaseContent
-{
- ///
- /// The content Title.
- ///
- public string? Title { get; }
-
- ///
- /// The directory where the content is located.
- ///
- ///
- ///
- /// If the content is located at content/blog/2021-01-01-Hello-World.md,
- /// then the value of this property will be blog.
- ///
- string? Section { get; }
-
- ///
- /// The type of the page, if it's a single page, a list of pages or the home page.
- ///
- Kind Kind { get; }
-
- ///
- /// The type of content. It's the same of the Section, if not specified.
- ///
- string? Type { get; }
-
- ///
- /// The URL pattern to be used to create the url.
- ///
- string? URL { get; }
-}
diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs
new file mode 100644
index 0000000000000000000000000000000000000000..a06b57016e48a4ab3230a5935f480a0395d4ac57
--- /dev/null
+++ b/source/Models/IFrontMatter.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using SuCoS.Models.CommandLineOptions;
+
+namespace SuCoS.Models;
+
+///
+/// Basic structure needed to generate user content and system content
+///
+public interface IFrontMatter : IParams
+{
+ ///
+ /// The content Title.
+ ///
+ public string? Title { get; }
+
+ ///
+ /// The first directory where the content is located, inside content.
+ ///
+ ///
+ ///
+ /// If the content is located at content/blog/2021-01-01-Hello-World.md,
+ /// then the value of this property will be blog.
+ ///
+ string? Section { get; }
+
+ ///
+ /// The type of content. It's the will be "page", if not specified.
+ ///
+ string? Type { get; }
+
+ ///
+ /// The URL pattern to be used to create the url.
+ /// Liquid template can be used to use tokens.
+ ///
+ ///
+ ///
+ ///
+ /// URL: my-page
+ ///
+ /// will be converted to /my-page
, independetly of the page title.
+ ///
+ ///
+ ///
+ /// URL: "{{ page.Parent.Title }}/{{ page.Title }}"
+ ///
+ /// will try to convert page.Parent.Title
and page.Title
.
+ ///
+ string? URL { get; }
+
+ ///
+ /// Date of the post. Will be used as the if it's not set.
+ /// Unless the option is set to true,
+ /// the dates set from the future will be ignored.
+ ///
+ DateTime? Date { get; }
+
+ ///
+ /// Last modification date of the page.
+ /// Useful to notify users that the content was updated.
+ ///
+ DateTime? LastMod { get; }
+
+ ///
+ /// Publish date of the page. If not set, the will be used instead.
+ /// Unless the option is set to true,
+ /// the dates set from the future will be ignored.
+ ///
+ DateTime? PublishDate { get; }
+
+ ///
+ /// Expiry date of the page.
+ ///
+ DateTime? ExpiryDate { get; }
+
+ ///
+ /// 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; }
+
+ ///
+ /// Page weight. Used for sorting by default.
+ ///
+ int Weight { get; }
+
+ ///
+ /// A list of tags, if any.
+ ///
+ public List? Tags { get; }
+
+ ///
+ /// Raw content from the Markdown file, bellow the front matter.
+ ///
+ string RawContent { get; }
+
+ ///
+ /// The kind of the page, if it's a single page, a list of pages or the home page.
+ /// It's used to determine the proper theme file.
+ ///
+ Kind Kind { get; }
+
+ ///
+ /// The source filename, without the extension. ;)
+ ///
+ public string? SourcePath { get; }
+
+ ///
+ /// The source filename, without the extension. ;)
+ ///
+ string? SourceFileNameWithoutExtension { get; }
+
+ ///
+ /// The source directory of the file, without the file name.
+ ///
+ string? SourcePathDirectory { get; }
+
+ ///
+ /// The date to be considered as the publish date.
+ ///
+ DateTime? GetPublishDate => PublishDate ?? Date;
+}
diff --git a/source/Models/Page.cs b/source/Models/Page.cs
new file mode 100644
index 0000000000000000000000000000000000000000..fc793a40a5cff7bf7b549e87f7bc0b655185d822
--- /dev/null
+++ b/source/Models/Page.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Fluid;
+using Markdig;
+using SuCoS.Helpers;
+
+namespace SuCoS.Models;
+
+///
+/// Each page data created from source files or from the system.
+///
+public class Page : IFrontMatter
+{
+ private readonly IFrontMatter frontMatter;
+
+ #region IFrontMatter
+
+ ///
+ public string? Title => frontMatter.Title;
+
+ ///
+ public string? Type => frontMatter.Type;
+
+ ///
+ public string? URL => frontMatter.URL;
+
+ ///
+ public List? Aliases => frontMatter.Aliases;
+
+ ///
+ public string? Section => frontMatter.Section;
+
+ ///
+ public DateTime? Date => frontMatter.Date;
+
+ ///
+ public DateTime? LastMod => frontMatter.LastMod;
+
+ ///
+ public DateTime? PublishDate => frontMatter.PublishDate;
+
+ ///
+ public DateTime? ExpiryDate => frontMatter.ExpiryDate;
+
+ ///
+ public int Weight => frontMatter.Weight;
+
+ ///
+ public List? Tags => frontMatter.Tags;
+
+ ///
+ public string RawContent => frontMatter.RawContent;
+
+ ///
+ public Kind Kind
+ {
+ get => frontMatter.Kind;
+ set => (frontMatter as FrontMatter)!.Kind = value;
+ }
+
+ ///
+ public string? SourcePath => frontMatter.SourcePath;
+
+ ///
+ public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension;
+
+ ///
+ public string? SourcePathDirectory => frontMatter.SourcePathDirectory;
+
+ ///
+ public Dictionary Params
+ {
+ get => frontMatter.Params;
+ set => frontMatter.Params = value;
+ }
+
+ #endregion IFrontMatter
+
+ ///
+ /// The source directory of the file.
+ ///
+ public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory)
+ ? null
+ : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
+
+
+ ///
+ /// Point to the site configuration.
+ ///
+ public Site Site { get; }
+
+ ///
+ /// Secondary URL patterns to be used to create the url.
+ ///
+ public List? AliasesProcessed { get; set; }
+
+ ///
+ /// The URL for the content.
+ ///
+ public string? Permalink { get; set; }
+
+ ///
+ /// Other content that mention this content.
+ /// Used to create the tags list and Related Posts section.
+ ///
+ public ConcurrentBag PagesReferences { get; } = new();
+
+ ///
+ /// Other content that mention this content.
+ /// Used to create the tags list and Related Posts section.
+ ///
+ public Page? Parent { get; set; }
+
+ ///
+ /// Plain markdown content, without HTML.
+ ///
+ public string Plain => Markdown.ToPlainText(RawContent, SiteHelper.MarkdownPipeline);
+
+ ///
+ /// A list of tags, if any.
+ ///
+ public ConcurrentBag TagsReference { get; } = new();
+
+ ///
+ /// Just a simple check if the current page is the home page
+ ///
+ public bool IsHome => Site.Home == this;
+
+ ///
+ /// Just a simple check if the current page is a section page
+ ///
+ public bool IsSection => Type == "section";
+
+ ///
+ /// Just a simple check if the current page is a "page"
+ ///
+ public bool IsPage => Kind == Kind.single;
+
+ ///
+ /// The number of words in the main content
+ ///
+ public int WordCount => Plain.Split(nonWords, StringSplitOptions.RemoveEmptyEntries).Length;
+
+ private static readonly char[] nonWords = { ' ', ',', ';', '.', '!', '"', '(', ')', '?', '\n', '\r' };
+
+ ///
+ /// The markdown content converted to HTML
+ ///
+ public string ContentPreRendered => contentPreRenderedCached.Value;
+
+ ///
+ /// The processed content.
+ ///
+ public string Content
+ {
+ get
+ {
+ contentCache = ParseAndRenderTemplate(false, "Error rendering theme template: {Error}");
+ return contentCache!;
+ }
+ }
+
+ ///
+ /// Creates the output file by applying the theme templates to the page content.
+ ///
+ /// The processed output file content.
+ public string CompleteContent => ParseAndRenderTemplate(true, "Error parsing theme template: {Error}");
+
+
+ ///
+ /// Other content that mention this content.
+ /// Used to create the tags list and Related Posts section.
+ ///
+ public IEnumerable Pages
+ {
+ get
+ {
+ if (pagesCached is not null)
+ {
+ return pagesCached;
+ }
+
+ pagesCached ??= new();
+ foreach (var permalink in PagesReferences)
+ {
+ pagesCached.Add(Site.PagesReferences[permalink]);
+ }
+ return pagesCached;
+ }
+ }
+
+ ///
+ /// List of pages from the content folder.
+ ///
+ public IEnumerable RegularPages
+ {
+ get
+ {
+ regularPagesCache ??= Pages
+ .Where(page => page.Kind == Kind.single)
+ .ToList();
+ return regularPagesCache;
+ }
+ }
+
+ ///
+ /// Get all URLs related to this content.
+ ///
+ public List Urls
+ {
+ get
+ {
+ var urls = new List();
+ if (Permalink is not null)
+ {
+ urls.Add(Permalink);
+ }
+
+ if (AliasesProcessed is not null)
+ {
+ urls.AddRange(from aliases in AliasesProcessed
+ select aliases);
+ }
+
+ return urls;
+ }
+ }
+
+ ///
+ /// The markdown content.
+ ///
+ private Lazy contentPreRenderedCached => new(() => Markdown.ToHtml(RawContent, SiteHelper.MarkdownPipeline));
+
+ ///
+ /// The cached content.
+ ///
+ private string? contentCache { get; set; }
+
+ private const string urlForIndex = @"{%- liquid
+if page.Parent
+echo page.Parent.Permalink
+echo '/'
+endif
+if page.Title != ''
+echo page.Title
+else
+echo page.SourcePathLastDirectory
+endif
+-%}";
+ private const string urlForNonIndex = @"{%- liquid
+if page.Parent
+echo page.Parent.Permalink
+echo '/'
+endif
+if page.Title != ''
+echo page.Title
+else
+echo page.SourceFileNameWithoutExtension
+endif
+-%}";
+
+ private List? regularPagesCache;
+
+ private List? pagesCached { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public Page(in IFrontMatter frontMatter, in Site site)
+ {
+ this.frontMatter = frontMatter;
+ Site = site;
+ }
+
+ ///
+ /// Gets the Permalink path for the file.
+ ///
+ /// The URL to consider. If null use the predefined URL
+ /// The output path.
+ ///
+ ///
+ public string CreatePermalink(string? URLforce = null)
+ {
+ var isIndex = SourceFileNameWithoutExtension == "index";
+
+ var permaLink = string.Empty;
+
+ URLforce ??= URL ?? (isIndex ? urlForIndex : urlForNonIndex);
+
+ try
+ {
+ if (Site.FluidParser.TryParse(URLforce, out var template, out var error))
+ {
+ var context = new TemplateContext(Site.TemplateOptions)
+ .SetValue("page", this);
+ permaLink = template.Render(context);
+ }
+ else
+ {
+ throw new FormatException(error);
+ }
+ }
+ catch (Exception ex)
+ {
+ Site.Logger.Error(ex, "Error converting URL: {URLforce}", URLforce);
+ }
+
+ if (!Path.IsPathRooted(permaLink) && !permaLink.StartsWith('/'))
+ {
+ permaLink = $"/{permaLink}";
+ }
+
+ return Urlizer.UrlizePath(permaLink);
+ }
+ private string ParseAndRenderTemplate(bool isBaseTemplate, string errorMessage)
+ {
+ var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site.CacheManager, isBaseTemplate);
+ if (string.IsNullOrEmpty(fileContents))
+ {
+ return isBaseTemplate ? Content : ContentPreRendered;
+ }
+
+ if (Site.FluidParser.TryParse(fileContents, out var template, out var error))
+ {
+ var context = new TemplateContext(Site.TemplateOptions)
+ .SetValue("page", this);
+ try
+ {
+ var rendered = template.Render(context);
+ return rendered;
+ }
+ catch (Exception ex)
+ {
+ Site.Logger.Error(ex, errorMessage, error);
+ return string.Empty;
+ }
+ }
+
+ Site.Logger.Error(errorMessage, error);
+ return string.Empty;
+ }
+}
diff --git a/source/Models/Site.cs b/source/Models/Site.cs
index e950d0287211b8d9e30214ff7d76cfd82cd4e619..03fa08f0df03b3298aa29698695557f00ac8d657 100644
--- a/source/Models/Site.cs
+++ b/source/Models/Site.cs
@@ -1,15 +1,14 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Fluid;
-using Markdig;
using Serilog;
-using SuCoS.Helpers;
+using SuCoS.Models.CommandLineOptions;
using SuCoS.Parser;
-using YamlDotNet.Serialization;
namespace SuCoS.Models;
@@ -21,118 +20,98 @@ public class Site : IParams
#region IParams
///
- [YamlIgnore]
- public Dictionary Params { get; set; } = new();
+ ///
+ public Dictionary Params
+ {
+ get => settings.Params;
+ set => settings.Params = value;
+ }
#endregion IParams
///
- /// Site Title.
+ /// Command line options
///
- public string Title { get; set; } = string.Empty;
+ public IGenerateOptions Options;
+
+ #region SiteSettings
///
- /// The base URL that will be used to build internal links.
+ /// Site Title.
///
- public string BaseUrl { get; set; } = "./";
+ public string Title => settings.Title;
///
/// The appearance of a URL is either ugly or pretty.
///
- public bool UglyURLs { get; set; } = false;
+ public bool UglyURLs => settings.UglyURLs;
- ///
- /// The base path of the source site files.
- ///
- [YamlIgnore]
- public string SourceDirectoryPath { get; set; } = "./";
+ #endregion SiteSettings
///
/// The path of the content, based on the source path.
///
- public string SourceContentPath => Path.Combine(SourceDirectoryPath, "content");
+ public string SourceContentPath => Path.Combine(Options.Source, "content");
///
/// The path of the static content (that will be copied as is), based on the source path.
///
- public string SourceStaticPath => Path.Combine(SourceDirectoryPath, "static");
+ public string SourceStaticPath => Path.Combine(Options.Source, "static");
///
/// The path theme.
///
- public string SourceThemePath => Path.Combine(SourceDirectoryPath, "theme");
+ public string SourceThemePath => Path.Combine(Options.Source, "theme");
///
/// The path of the static content (that will be copied as is), based on the theme path.
///
public string SourceThemeStaticPath => Path.Combine(SourceThemePath, "static");
- ///
- /// The path where the generated site files will be saved.
- ///
- public string OutputPath { get; set; } = "./";
-
///
/// List of all pages, including generated.
///
- public List Pages
+ public IEnumerable Pages
{
get
{
- pagesCache ??= PagesReferences.Values.ToList();
+ pagesCache ??= PagesReferences.Values
+ .OrderBy(page => -page.Weight)
+ .ToList();
return pagesCache!;
}
}
- ///
- /// Expose a page getter to templates.
- ///
- ///
- ///
- public Frontmatter? GetPage(string permalink)
- {
- return PagesReferences.TryGetValue(permalink, out var page) ? page : null;
- }
-
///
/// List of all pages, including generated, by their permalink.
///
- public Dictionary PagesReferences { get; } = new();
+ public ConcurrentDictionary PagesReferences { get; } = new();
///
/// List of pages from the content folder.
///
- public List RegularPages
+ public List RegularPages
{
get
{
regularPagesCache ??= PagesReferences
- .Where(pair => pair.Value.Kind == Kind.single && pair.Key == pair.Value.Permalink)
+ .Where(pair => pair.Value.IsPage && pair.Key == pair.Value.Permalink)
.Select(pair => pair.Value)
+ .OrderBy(page => -page.Weight)
.ToList();
return regularPagesCache;
}
}
///
- /// The frontmatter of the home page;
+ /// The page of the home page;
///
- public Frontmatter? Home { get; private set; }
+ public Page? Home { get; private set; }
///
- /// Command line options
+ /// Manage all caching lists for the site
///
- public IGenerateOptions? options;
-
- ///
- /// Cache for content templates.
- ///
- public readonly Dictionary<(string?, Kind?, string?), string> contentTemplateCache = new();
-
- ///
- /// Cache for base templates.
- ///
- public readonly Dictionary<(string?, Kind?, string?), string> baseTemplateCache = new();
+ public readonly SiteCacheManager CacheManager = new();
///
/// The Fluid parser instance.
@@ -147,27 +126,16 @@ public class Site : IParams
///
/// The logger instance.
///
- public ILogger? Logger;
+ public ILogger Logger { get; }
///
- /// The time that the older cache should be ignored.
- ///
- public DateTime IgnoreCacheBefore { get; private set; }
-
- ///
- /// Datetime wrapper
+ /// Number of files parsed, used in the report.
///
- public readonly ISystemClock Clock;
+ public int filesParsedToReport;
- ///
- /// Cache for tag frontmatter.
- ///
- private readonly Dictionary automaticContentCache = new();
+ private const string indexFileConst = "index.md";
- ///
- /// The synchronization lock object.
- ///
- private readonly object syncLock = new();
+ private const string indexFileUpperConst = "INDEX.MD";
///
/// The synchronization lock object during ProstProcess.
@@ -175,45 +143,41 @@ public class Site : IParams
private readonly object syncLockPostProcess = new();
///
- /// The frontmatter parser instance. The default is YAML.
+ /// The front matter parser instance. The default is YAML.
///
- private readonly IFrontmatterParser frontmatterParser = new YAMLParser();
+ private readonly IFrontMatterParser frontMatterParser;
- private List? pagesCache;
+ private List? pagesCache;
- private List? regularPagesCache;
+ private List? regularPagesCache;
- ///
- /// Number of files parsed, used in the report.
- ///
- public int filesParsedToReport;
+ private readonly SiteSettings settings;
///
- /// Markdig 20+ built-in extensions
+ /// Datetime wrapper
///
- /// https://github.com/xoofx/markdig
- public readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
- .UseAdvancedExtensions()
- .Build();
+ private readonly ISystemClock clock;
///
/// Constructor
///
- public Site() : this(new SystemClock())
+ public Site(
+ in IGenerateOptions options,
+ in SiteSettings settings,
+ in IFrontMatterParser frontMatterParser,
+ in ILogger logger, ISystemClock? clock)
{
- }
+ Options = options;
+ this.settings = settings;
+ Logger = logger;
+ this.frontMatterParser = frontMatterParser;
- ///
- /// Constructor
- ///
- public Site(ISystemClock clock)
- {
// Liquid template options, needed to theme the content
// but also parse URLs
- TemplateOptions.MemberAccessStrategy.Register();
+ TemplateOptions.MemberAccessStrategy.Register();
TemplateOptions.MemberAccessStrategy.Register();
- Clock = clock;
+ this.clock = clock ?? new SystemClock();
}
///
@@ -221,11 +185,8 @@ public class Site : IParams
///
public void ResetCache()
{
- baseTemplateCache.Clear();
- contentTemplateCache.Clear();
- automaticContentCache.Clear();
+ CacheManager.ResetCache();
PagesReferences.Clear();
- IgnoreCacheBefore = DateTime.Now;
}
///
@@ -234,227 +195,246 @@ public class Site : IParams
///
/// Folder to scan
/// Folder recursive level
- /// Page of the upper directory
+ /// Page of the upper directory
///
- public void ParseAndScanSourceFiles(string directory, int level = 0, Frontmatter? pageParent = null)
+ public void ParseAndScanSourceFiles(string? directory, int level = 0, Page? parent = null)
{
directory ??= SourceContentPath;
var markdownFiles = Directory.GetFiles(directory, "*.md");
- var indexPath = markdownFiles.FirstOrDefault(file => Path.GetFileName(file).ToUpperInvariant() == "INDEX.MD");
- if (indexPath != null)
+ ParseIndexPage(directory, level, ref parent, ref markdownFiles);
+
+ _ = Parallel.ForEach(markdownFiles, filePath =>
+ {
+ ParseSourceFile(parent, filePath);
+ });
+
+ var subdirectories = Directory.GetDirectories(directory);
+ _ = Parallel.ForEach(subdirectories, subdirectory =>
+ {
+ ParseAndScanSourceFiles(subdirectory, level + 1, parent);
+ });
+ }
+
+ private void ParseIndexPage(in string? directory, int level, ref Page? parent, ref string[] markdownFiles)
+ {
+ // Check if the index.md file exists in the current directory
+ var indexPage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file).ToUpperInvariant() == indexFileUpperConst);
+ if (indexPage is not null)
{
- markdownFiles = markdownFiles.Where(file => file != indexPath).ToArray();
- var frontmatter = ParseSourceFile(pageParent, indexPath);
+ markdownFiles = markdownFiles.Where(file => file != indexPage).ToArray();
+ var page = ParseSourceFile(parent, indexPage);
if (level == 0)
{
- Home = frontmatter;
- frontmatter!.Permalink = "/";
- frontmatter.Kind = Kind.index;
- PagesReferences.Remove(frontmatter.Permalink);
- PagesReferences.Add(frontmatter.Permalink, frontmatter);
+ PagesReferences.TryRemove(page!.Permalink!, out _);
+ Home = page;
+ page.Permalink = "/";
+ page.Kind = Kind.index;
+ PagesReferences.GetOrAdd(page.Permalink, page);
}
else
{
- pageParent = frontmatter;
+ parent = page;
}
}
+
+ // If it's the home page
else if (level == 0)
{
- // TODO: unify the with section creation process
- Home = CreateIndexPage(string.Empty);
- }
- else if (level == 1)
- {
- var section = new DirectoryInfo(directory).Name;
- var contentTemplate = new BasicContent(
- title: section,
- section: "section",
- type: "section",
- url: section
- );
- pageParent = CreateAutomaticFrontmatter(contentTemplate, null);
+ Home = CreateSystemPage(string.Empty, Title);
}
- _ = Parallel.ForEach(markdownFiles, filePath =>
- {
- ParseSourceFile(pageParent, filePath);
- });
-
- var subdirectories = Directory.GetDirectories(directory);
- foreach (var subdirectory in subdirectories)
+ // Or a section page, which must be used as the parent for the next sub folder
+ else if (level == 1)
{
- ParseAndScanSourceFiles(subdirectory, level + 1, pageParent);
+ var section = new DirectoryInfo(directory!).Name;
+ parent = CreateSystemPage(section, section);
}
}
- private Frontmatter? ParseSourceFile(Frontmatter? pageParent, string filePath)
+ private Page? ParseSourceFile(in Page? parent, in string filePath)
{
- Frontmatter? frontmatter = null;
+ Page? page = null;
try
{
- frontmatter = frontmatterParser.ParseFrontmatterAndMarkdownFromFile(this, filePath, SourceContentPath)
- ?? throw new FormatException($"Error parsing frontmatter for {filePath}");
+ var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath)
+ ?? throw new FormatException($"Error parsing front matter for {filePath}");
- if (frontmatter.IsValidDate(options))
+ if (IsValidDate(frontMatter, Options))
{
- PostProcessFrontMatter(frontmatter, pageParent, true);
+ page = new(frontMatter, this);
+ PostProcessPage(page, parent, true);
}
}
catch (Exception ex)
{
- Logger?.Error(ex, "Error parsing file {file}", filePath);
+ Logger.Error(ex, "Error parsing file {file}", filePath);
}
// Use interlocked to safely increment the counter in a multi-threaded environment
_ = Interlocked.Increment(ref filesParsedToReport);
- return frontmatter;
+ return page;
}
///
- /// Create a page not from the content folder, but as part of the process.
- /// It's used to create tag pages, section list pages, etc.
+ /// Creates the page for the site index.
///
- public Frontmatter CreateAutomaticFrontmatter(BasicContent baseContent, Frontmatter? originalFrontmatter)
+ /// The relative path of the page.
+ ///
+ ///
+ ///
+ /// The created page for the index.
+ private Page CreateSystemPage(string relativePath, string title, string? sectionName = null, Page? originalPage = null)
{
- if (baseContent is null)
+ sectionName ??= "section";
+ var isIndex = string.IsNullOrEmpty(relativePath);
+ FrontMatter frontMatter = new()
{
- throw new ArgumentNullException(nameof(baseContent));
- }
+ Kind = isIndex ? Kind.index : Kind.list,
+ Section = isIndex ? "index" : sectionName,
+ SourcePath = Path.Combine(relativePath, indexFileConst),
+ Title = title,
+ Type = isIndex ? "index" : sectionName,
+ URL = relativePath
+ };
- var id = baseContent.URL;
- Frontmatter? frontmatter;
- lock (syncLock)
+ var id = frontMatter.URL;
+
+ // Get or create the page
+ var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() =>
{
- if (!automaticContentCache.TryGetValue(id, out frontmatter))
+ Page? parent = null;
+ // Check if we need to create a section, even
+ var sections = (frontMatter.SourcePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries);
+ if (sections.Length > 1)
{
- frontmatter = new(
- site: this,
- title: baseContent.Title,
- sourcePath: string.Empty,
- sourceFileNameWithoutExtension: string.Empty,
- sourcePathDirectory: null
- )
- {
- Section = baseContent.Section,
- Kind = baseContent.Kind,
- Type = baseContent.Type,
- URL = baseContent.URL,
- PagesReferences = new()
- };
- automaticContentCache.Add(id, frontmatter);
- PostProcessFrontMatter(frontmatter);
+ parent = CreateSystemPage(sections[0], sections[0]);
}
- }
- if (frontmatter.Kind != Kind.index && originalFrontmatter?.Permalink is not null)
- {
- frontmatter.PagesReferences!.Add(originalFrontmatter.Permalink!);
- }
+ var newPage = new Page(frontMatter, this);
+ PostProcessPage(newPage, parent);
+ return newPage;
+ }));
- // TODO: still too hardcoded
- if (frontmatter.Type != "tags" || originalFrontmatter is null)
+ // get the page from the lazy object
+ var page = lazyPage.Value;
+
+ if (originalPage is null || string.IsNullOrEmpty(originalPage.Permalink))
{
- return frontmatter;
+ return page;
}
- lock (originalFrontmatter)
+
+ if (page.Kind != Kind.index)
{
- originalFrontmatter.TagsReference ??= new();
- originalFrontmatter.TagsReference!.Add(frontmatter);
+ page.PagesReferences.Add(originalPage.Permalink);
}
- return frontmatter;
- }
- ///
- /// Creates the frontmatter for the index page.
- ///
- /// The relative path of the page.
- /// The created frontmatter for the index page.
- private Frontmatter CreateIndexPage(string relativePath)
- {
- Frontmatter frontmatter = new(
- title: Title,
- site: this,
- sourcePath: Path.Combine(relativePath, "index.md"),
- sourceFileNameWithoutExtension: "index",
- sourcePathDirectory: "/"
- )
+ // TODO: still too hardcoded to add the tags reference
+ if (page.Type != "tags")
{
- Kind = string.IsNullOrEmpty(relativePath) ? Kind.index : Kind.list,
- Section = (string.IsNullOrEmpty(relativePath) ? Kind.index : Kind.list).ToString(),
- URL = "/"
- };
-
- PostProcessFrontMatter(frontmatter);
- return frontmatter;
+ return page;
+ }
+ originalPage.TagsReference.Add(page);
+ return page;
}
///
- /// Extra calculation and automatic data for each frontmatter.
+ /// Extra calculation and automatic data for each page.
///
- /// The given page to be processed
+ /// The given page to be processed
/// The parent page, if any
///
- public void PostProcessFrontMatter(Frontmatter frontmatter, Frontmatter? parent = null, bool overwrite = false)
+ public void PostProcessPage(in Page page, Page? parent = null, bool overwrite = false)
{
- if (frontmatter is null)
+ if (page is null)
{
- throw new ArgumentNullException(nameof(frontmatter));
+ throw new ArgumentNullException(nameof(page));
}
- frontmatter.Parent = parent;
- frontmatter.Permalink = frontmatter.CreatePermalink();
+ page.Parent = parent;
+ page.Permalink = page.CreatePermalink();
lock (syncLockPostProcess)
{
- if (!PagesReferences.TryGetValue(frontmatter.Permalink, out var old) || overwrite)
+ if (!PagesReferences.TryGetValue(page.Permalink, out var old) || overwrite)
{
if (old?.PagesReferences is not null)
{
- frontmatter.PagesReferences ??= new();
- foreach (var page in old.PagesReferences)
+ foreach (var pageOld in old.PagesReferences)
{
- frontmatter.PagesReferences.Add(page);
+ page.PagesReferences.Add(pageOld);
}
}
- if (frontmatter.Aliases is not null)
+ if (page.Aliases is not null)
{
- frontmatter.AliasesProcessed ??= new();
- foreach (var alias in frontmatter.Aliases)
+ page.AliasesProcessed ??= new();
+ foreach (var alias in page.Aliases)
{
- frontmatter.AliasesProcessed.Add(frontmatter.CreatePermalink(alias));
+ page.AliasesProcessed.Add(page.CreatePermalink(alias));
}
}
// Register the page for all urls
- foreach (var url in frontmatter.Urls)
+ foreach (var url in page.Urls)
{
- PagesReferences[url] = frontmatter;
+ PagesReferences.TryAdd(url, page);
}
}
}
- if (frontmatter.Tags is not null)
+ if (page.Tags is not null)
{
- foreach (var tagName in frontmatter.Tags)
+ foreach (var tagName in page.Tags)
{
- var contentTemplate = new BasicContent(
- title: tagName,
- section: "tags",
- type: "tags",
- url: "tags/" + Urlizer.Urlize(tagName)
- );
- _ = CreateAutomaticFrontmatter(contentTemplate, frontmatter);
+ CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", page);
}
}
- if (!string.IsNullOrEmpty(frontmatter.Section)
- && PagesReferences.TryGetValue('/' + frontmatter.Section!, out var section))
+ if (!string.IsNullOrEmpty(page.Section)
+ && PagesReferences.TryGetValue('/' + page.Section!, out var section))
+ {
+ section.PagesReferences.Add(page.Permalink!);
+ }
+ }
+
+ ///
+ /// Check if the page have a publishing date from the past.
+ ///
+ /// Page or front matter
+ /// options
+ ///
+ public bool IsValidDate(in IFrontMatter frontMatter, IGenerateOptions? options)
+ {
+ if (frontMatter is null)
+ {
+ throw new ArgumentNullException(nameof(frontMatter));
+ }
+ return !IsDateExpired(frontMatter) && (IsDatePublishable(frontMatter) || (options?.Future ?? false));
+ }
+
+ ///
+ /// Check if the page is expired
+ ///
+ public bool IsDateExpired(in IFrontMatter frontMatter)
+ {
+ if (frontMatter is null)
+ {
+ throw new ArgumentNullException(nameof(frontMatter));
+ }
+ return frontMatter.ExpiryDate is not null && frontMatter.ExpiryDate <= clock.Now;
+ }
+
+ ///
+ /// Check if the page is publishable
+ ///
+ public bool IsDatePublishable(in IFrontMatter frontMatter)
+ {
+ if (frontMatter is null)
{
- section.PagesReferences ??= new();
- section.PagesReferences.Add(frontmatter.Permalink!);
+ throw new ArgumentNullException(nameof(frontMatter));
}
+ return frontMatter.GetPublishDate is null || frontMatter.GetPublishDate <= clock.Now;
}
}
\ No newline at end of file
diff --git a/source/Models/SiteCacheManager.cs b/source/Models/SiteCacheManager.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d984fe46d307d6dc9e0d061f6446d0a4ea7c23fe
--- /dev/null
+++ b/source/Models/SiteCacheManager.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace SuCoS.Models;
+
+///
+/// Manages all the lists and dictionaries used for cache for the site
+///
+public class SiteCacheManager
+{
+ ///
+ /// Cache for content templates.
+ ///
+ public readonly Dictionary<(string?, Kind?, string?), string> contentTemplateCache = new();
+
+ ///
+ /// Cache for base templates.
+ ///
+ public readonly Dictionary<(string?, Kind?, string?), string> baseTemplateCache = new();
+
+ ///
+ /// Cache for tag page.
+ ///
+ public readonly ConcurrentDictionary> automaticContentCache = new();
+
+ ///
+ /// Resets the template cache to force a reload of all templates.
+ ///
+ public void ResetCache()
+ {
+ baseTemplateCache.Clear();
+ contentTemplateCache.Clear();
+ automaticContentCache.Clear();
+ }
+}
\ No newline at end of file
diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs
new file mode 100644
index 0000000000000000000000000000000000000000..4de5802caa09343263c21079d9274456b7d2fb37
--- /dev/null
+++ b/source/Models/SiteSettings.cs
@@ -0,0 +1,32 @@
+
+using System.Collections.Generic;
+
+namespace SuCoS.Models;
+
+///
+/// The main configuration of the program, extracted from the app.yaml file.
+///
+public class SiteSettings : IParams
+{
+ ///
+ /// Site Title.
+ ///
+ public string Title { get; set; } = string.Empty;
+
+ ///
+ /// The base URL that will be used to build internal links.
+ ///
+ public string BaseUrl { get; set; } = "./";
+
+ ///
+ /// The appearance of a URL is either ugly or pretty.
+ ///
+ public bool UglyURLs { get; set; } = false;
+
+ #region IParams
+
+ ///
+ public Dictionary Params { get; set; } = new();
+
+ #endregion IParams
+}
\ No newline at end of file
diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs
index 0277a6069c92d63910e8f482452351a53037df69..1318dfb2959ae77fa672b43871eb54aaafd2ec38 100644
--- a/source/Parser/IFrontmatterParser.cs
+++ b/source/Parser/IFrontmatterParser.cs
@@ -3,32 +3,30 @@ using SuCoS.Models;
namespace SuCoS.Parser;
///
-/// Responsible for parsing the content frontmatter
+/// Responsible for parsing the content front matter
///
-public interface IFrontmatterParser
+public interface IFrontMatterParser
{
///
- /// Extract the frontmatter from the content file.
+ /// Extract the front matter from the content file.
///
- ///
///
///
///
- Frontmatter? ParseFrontmatterAndMarkdownFromFile(Site site, in string filePath, in string sourceContentPath);
+ FrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath);
///
- /// Extract the frontmatter from the content.
+ /// Extract the front matter from the content.
///
- ///
///
///
///
- Frontmatter? ParseFrontmatterAndMarkdown(Site site, in string filePath, in string fileContent);
+ FrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent);
///
/// Parse the app config file.
///
///
///
- Site ParseSiteSettings(string configFileContent);
+ SiteSettings ParseSiteSettings(string configFileContent);
}
\ No newline at end of file
diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs
index f166971463000810c0f19961fb1b208ca382b0e2..9804569b457ce90119723ba8c5ec1e347cde7ad6 100644
--- a/source/Parser/YAMLParser.cs
+++ b/source/Parser/YAMLParser.cs
@@ -10,9 +10,9 @@ using YamlDotNet.Serialization;
namespace SuCoS.Parser;
///
-/// Responsible for parsing the content frontmatter using YAML
+/// Responsible for parsing the content front matter using YAML
///
-public class YAMLParser : IFrontmatterParser
+public class YAMLParser : IFrontMatterParser
{
///
/// YamlDotNet parser, strictly set to allow automatically parse only known fields
@@ -29,12 +29,8 @@ public class YAMLParser : IFrontmatterParser
.Build();
///
- public Frontmatter ParseFrontmatterAndMarkdownFromFile(Site site, in string filePath, in string? sourceContentPath = null)
+ public FrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null)
{
- if (site is null)
- {
- throw new ArgumentNullException(nameof(site));
- }
if (filePath is null)
{
throw new ArgumentNullException(nameof(filePath));
@@ -52,70 +48,61 @@ public class YAMLParser : IFrontmatterParser
throw new FileNotFoundException(filePath, ex);
}
- return ParseFrontmatterAndMarkdown(site, fileRelativePath, fileContent);
+ return ParseFrontmatterAndMarkdown(fileRelativePath, fileContent);
}
///
- public Frontmatter ParseFrontmatterAndMarkdown(Site site, in string fileRelativePath, in string fileContent)
+ public FrontMatter ParseFrontmatterAndMarkdown(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();
+ var frontMatterBuilder = new StringBuilder();
string? line;
while ((line = content.ReadLine()) != null && line != "---") { }
while ((line = content.ReadLine()) != null && line != "---")
{
- frontmatterBuilder.AppendLine(line);
+ frontMatterBuilder.AppendLine(line);
}
- // Join the read lines to form the frontmatter
- var yaml = frontmatterBuilder.ToString();
+ // Join the read lines to form the front matter
+ var yaml = frontMatterBuilder.ToString();
var rawContent = content.ReadToEnd();
- // Now, you can parse the YAML frontmatter
- var page = ParseYAML(ref site, fileRelativePath, yaml, rawContent);
+ // Now, you can parse the YAML front matter
+ var page = ParseYAML(fileRelativePath, yaml, rawContent);
return page;
}
- private Frontmatter ParseYAML(ref Site site, in string filePath, string yaml, in string rawContent)
+ private FrontMatter ParseYAML(in string filePath, string yaml, in string rawContent)
{
- var page = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing frontmatter");
- var sourceFileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
+ var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter");
var section = SiteHelper.GetSection(filePath);
- page.RawContent = rawContent;
- page.Section = section;
- page.Site = site;
- page.SourceFileNameWithoutExtension = sourceFileNameWithoutExtension;
- page.SourcePath = filePath;
- page.SourcePathDirectory = Path.GetDirectoryName(filePath);
- page.Title ??= sourceFileNameWithoutExtension;
- page.Type ??= section;
+ frontMatter.RawContent = rawContent;
+ frontMatter.Section = section;
+ frontMatter.SourcePath = filePath;
+ frontMatter.Type ??= section;
var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml));
- if (yamlObject is not Dictionary