diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs
index f2375ab812613ec742fe84de073c2830d05a6f93..66094d5861b291d3894a50b257ceab0da68f49f6 100644
--- a/source/BaseGeneratorCommand.cs
+++ b/source/BaseGeneratorCommand.cs
@@ -31,7 +31,7 @@ public abstract class BaseGeneratorCommand
///
/// The Fluid parser instance.
///
- protected static readonly FluidParser parser = new();
+ protected static readonly FluidParser fluidParser = new();
///
/// The site configuration.
@@ -56,13 +56,18 @@ public abstract class BaseGeneratorCommand
///
/// Cache for tag frontmatter.
///
- protected readonly Dictionary tagFrontmatterCache = new();
+ protected readonly Dictionary automaticContentCache = new();
///
/// The synchronization lock object.
///
protected readonly object syncLock = new();
+ ///
+ /// The synchronization lock object during ProstProcess.
+ ///
+ protected readonly object syncLockPostProcess = new();
+
///
/// The Fluid/Liquid template options.
///
@@ -84,51 +89,66 @@ public abstract class BaseGeneratorCommand
///
public DateTime IgnoreCacheBefore { get; set; }
- ///
- public Frontmatter CreateTagFrontmatter(Site site, string tagName, Frontmatter originalFrontmatter)
+ ///
+ /// 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.
+ ///
+ public Frontmatter CreateAutomaticFrontmatter(BasicContent baseContent, Frontmatter originalFrontmatter)
{
+ if (baseContent is null)
+ {
+ throw new ArgumentNullException(nameof(baseContent));
+ }
if (originalFrontmatter is null)
{
throw new ArgumentNullException(nameof(originalFrontmatter));
}
+ var id = baseContent.URL ?? baseContent.Section;
Frontmatter? frontmatter = null;
lock (syncLock)
{
- if (!tagFrontmatterCache.TryGetValue(tagName, out frontmatter))
+ if (!automaticContentCache.TryGetValue(id, out frontmatter))
{
- if (site is null)
- {
- throw new ArgumentNullException(nameof(site));
- }
-
frontmatter = new(
baseGeneratorCommand: this,
site: site,
- title: tagName,
- sourcePath: "tags",
+ title: baseContent.Title,
+ sourcePath: string.Empty,
sourceFileNameWithoutExtension: string.Empty,
sourcePathDirectory: null
)
{
- Section = "tags",
- Kind = Kind.list,
- Type = "tags",
- URL = "tags/" + Urlizer.Urlize(tagName),
- RawContent = $"# {tagName}",
+ Section = baseContent.Section,
+ Kind = baseContent.Kind,
+ Type = baseContent.Type,
+ URL = baseContent.URL,
Pages = new()
};
- frontmatter.Permalink = "/" + CreatePermalink(frontmatter.URL, site, frontmatter);
- site.Pages.Add(item: frontmatter);
- tagFrontmatterCache.Add(tagName, frontmatter);
+
+ automaticContentCache.Add(id, frontmatter);
+ PostProcessFrontMatter(frontmatter);
}
}
- lock (frontmatter?.Pages!)
+
+ if (frontmatter.Kind != Kind.index)
{
frontmatter.Pages!.Add(originalFrontmatter);
- originalFrontmatter.Tags ??= new();
- originalFrontmatter.Tags!.Add(frontmatter);
}
+
+ // TODO: still too hardcoded
+ if (frontmatter.Type == "tags" && originalFrontmatter is not null)
+ {
+ lock (originalFrontmatter!)
+ {
+ if (frontmatter.Type == "tags")
+ {
+ originalFrontmatter.Tags ??= new();
+ originalFrontmatter.Tags!.Add(frontmatter);
+ }
+ }
+ }
+
return frontmatter;
}
@@ -145,7 +165,7 @@ public abstract class BaseGeneratorCommand
{
return frontmatter.Content;
}
- else if (parser.TryParse(fileContents, out var template, out var error))
+ else if (fluidParser.TryParse(fileContents, out var template, out var error))
{
var context = new TemplateContext(templateOptions)
.SetValue("page", frontmatter);
@@ -258,22 +278,7 @@ public abstract class BaseGeneratorCommand
if (IsValidDate(frontmatter))
{
- site.Pages.Add(frontmatter);
- site.RegularPages.Add(frontmatter);
- frontmatter.Permalink = "/" + CreatePermalink(file.filePath, site, frontmatter);
-
- if (site.HomePage is null && frontmatter.SourcePath == "index.md")
- {
- site.HomePage = frontmatter;
- frontmatter.Kind = Kind.index;
- }
- if (frontmatter.Aliases is not null)
- {
- for (var i = 0; i < frontmatter.Aliases.Count; i++)
- {
- frontmatter.Aliases[i] = "/" + CreatePermalink(file.filePath, site, frontmatter, frontmatter.Aliases[i]);
- }
- }
+ PostProcessFrontMatter(frontmatter, true);
}
}
catch (Exception ex)
@@ -288,14 +293,77 @@ public abstract class BaseGeneratorCommand
// If the home page is not yet created, create it!
if (site.HomePage is null)
{
- var home = CreateIndexPage(site, string.Empty);
+ var home = CreateIndexPage(string.Empty);
site.HomePage = home;
- site.Pages.Add(home);
}
stopwatch.Stop("Parse", filesParsed);
}
+ ///
+ /// Extra calculation and automatic data for each frontmatter.
+ ///
+ ///
+ ///
+ private void PostProcessFrontMatter(Frontmatter frontmatter, bool overwrite = false)
+ {
+ frontmatter.Permalink = CreatePermalink(site, frontmatter);
+ lock (syncLockPostProcess)
+ {
+ if (!site.Pages.TryGetValue(frontmatter.Permalink, out var old) || overwrite)
+ {
+ if (old is not null)
+ {
+ if (old?.Pages is not null)
+ {
+ frontmatter.Pages ??= new();
+ foreach (var page in old.Pages)
+ {
+ frontmatter.Pages.Add(page);
+ }
+ }
+ }
+
+ // Register the page for all urls
+ foreach (var url in frontmatter.Urls)
+ {
+ site.Pages[url] = frontmatter;
+ }
+
+ if (frontmatter.Kind == Kind.single)
+ {
+ site.RegularPages.Add(frontmatter.Permalink, frontmatter);
+ }
+
+ if (site.HomePage is null && frontmatter.SourcePath == "index.md")
+ {
+ site.HomePage = frontmatter;
+ frontmatter.Kind = Kind.index;
+ }
+
+ if (frontmatter.Aliases is not null)
+ {
+ for (var i = 0; i < frontmatter.Aliases.Count; i++)
+ {
+ frontmatter.Aliases[i] = "/" + CreatePermalink(site, frontmatter, frontmatter.Aliases[i]);
+ }
+ }
+ }
+ }
+
+ // Create a section page when due
+ if (frontmatter.Type != "section")
+ {
+ var contentTemplate = new BasicContent(
+ title: frontmatter.Section,
+ section: frontmatter.Section,
+ type: "section",
+ url: frontmatter.Section
+ );
+ CreateAutomaticFrontmatter(contentTemplate, frontmatter);
+ }
+ }
+
///
/// Reads the application configuration.
///
@@ -331,16 +399,10 @@ public abstract class BaseGeneratorCommand
///
/// Creates the frontmatter for the index page.
///
- /// The site instance.
/// The relative path of the page.
/// The created frontmatter for the index page.
- protected Frontmatter CreateIndexPage(Site site, string relativePath)
+ protected Frontmatter CreateIndexPage(string relativePath)
{
- if (site is null)
- {
- throw new ArgumentNullException(nameof(site));
- }
-
Frontmatter frontmatter = new(
baseGeneratorCommand: this,
title: site.Title,
@@ -353,7 +415,8 @@ public abstract class BaseGeneratorCommand
Kind = string.IsNullOrEmpty(relativePath) ? Kind.index : Kind.list,
Section = (string.IsNullOrEmpty(relativePath) ? Kind.index : Kind.list).ToString()
};
- frontmatter.Permalink = CreatePermalink(frontmatter.SourcePath, site, frontmatter);
+
+ PostProcessFrontMatter(frontmatter);
return frontmatter;
}
@@ -424,21 +487,16 @@ public abstract class BaseGeneratorCommand
///
/// Gets the Permalink path for the file.
///
- /// The file's relative path.
/// The site instance.
/// The frontmatter.
/// The URL to consider. If null, we get frontmatter.URL
/// The output path.
- public string CreatePermalink(string fileRelativePath, Site site, Frontmatter frontmatter, string? URL = null)
+ public string CreatePermalink(Site site, Frontmatter frontmatter, string? URL = null)
{
if (frontmatter is null)
{
throw new ArgumentNullException(nameof(frontmatter));
}
- if (fileRelativePath is null)
- {
- throw new ArgumentNullException(nameof(fileRelativePath));
- }
if (site is null)
{
throw new ArgumentNullException(nameof(site));
@@ -450,37 +508,29 @@ public abstract class BaseGeneratorCommand
URL ??= frontmatter.URL
?? (isIndex ? "{{ page.SourcePathDirectory }}" : "{{ page.SourcePathDirectory }}/{{ page.Title }}");
- // TODO: Tokenize the URL instead of hardcoding the usage of the title
- if (!string.IsNullOrEmpty(URL))
- {
- outputRelativePath = URL;
+ outputRelativePath = URL;
- if (parser.TryParse(URL, out var template, out var error))
+ if (fluidParser.TryParse(URL, out var template, out var error))
+ {
+ var context = new TemplateContext(templateOptions)
+ .SetValue("page", frontmatter);
+ try
{
- var context = new TemplateContext(templateOptions)
- .SetValue("page", frontmatter);
- try
- {
- outputRelativePath = template.Render(context);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error converting URL: {Error}", error);
- }
+ outputRelativePath = template.Render(context);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error converting URL: {Error}", error);
}
- }
- else
- {
- var folderRelativePath = Path.GetDirectoryName(fileRelativePath.Replace(site.SourceContentPath, string.Empty, StringComparison.InvariantCultureIgnoreCase)) ?? string.Empty;
- var extraPath = isIndex
- ? ""
- : frontmatter.SourceFileNameWithoutExtension;
-
- outputRelativePath = Path.Combine(folderRelativePath, extraPath) + (site.UglyURLs ? "/index.html" : "");
}
outputRelativePath = Urlizer.UrlizePath(outputRelativePath);
+ if (!string.IsNullOrEmpty(outputRelativePath) && !Path.IsPathRooted(outputRelativePath) && !outputRelativePath.StartsWith("/"))
+ {
+ outputRelativePath = "/" + outputRelativePath;
+ }
+
return outputRelativePath;
}
@@ -504,7 +554,7 @@ public abstract class BaseGeneratorCommand
{
result = frontmatter.Content;
}
- else if (parser.TryParse(fileContents, out var template, out var error))
+ else if (fluidParser.TryParse(fileContents, out var template, out var error))
{
var context = new TemplateContext(templateOptions);
_ = context.SetValue("page", frontmatter);
@@ -619,7 +669,8 @@ public abstract class BaseGeneratorCommand
{
baseTemplateCache.Clear();
contentTemplateCache.Clear();
- tagFrontmatterCache.Clear();
+ automaticContentCache.Clear();
+ site.Pages.Clear();
IgnoreCacheBefore = DateTime.Now;
}
diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs
index 0d12827f181b4b3337b55b4d26aa3b6da3237b51..d10df2660f80df0c3bc2e1fca9e166f03e236242 100644
--- a/source/BuildCommand.cs
+++ b/source/BuildCommand.cs
@@ -44,35 +44,33 @@ public class BuildCommand : BaseGeneratorCommand
// Print each page
var pagesCreated = 0; // counter to keep track of the number of pages created
- _ = Parallel.ForEach(site.Pages, frontmatter =>
+ _ = Parallel.ForEach(site.Pages, pair =>
{
- foreach (var url in frontmatter.Urls)
- {
- var result = CreateOutputFile(frontmatter);
-
- var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/');
+ var (url, frontmatter) = pair;
+ var result = CreateOutputFile(frontmatter);
- // Generate the output path
- var outputAbsolutePath = Path.Combine(site.OutputPath, path);
+ var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/');
- var outputDirectory = Path.GetDirectoryName(outputAbsolutePath);
- if (!Directory.Exists(outputDirectory))
- {
- _ = Directory.CreateDirectory(outputDirectory!);
- }
+ // Generate the output path
+ var outputAbsolutePath = Path.Combine(site.OutputPath, path);
- // Save the processed output to the final file
- File.WriteAllText(outputAbsolutePath, result);
+ var outputDirectory = Path.GetDirectoryName(outputAbsolutePath);
+ if (!Directory.Exists(outputDirectory))
+ {
+ _ = Directory.CreateDirectory(outputDirectory!);
+ }
- // Log
- if (options.Verbose)
- {
- Log.Information("Page created: {Permalink}", frontmatter.Permalink);
- }
+ // Save the processed output to the final file
+ File.WriteAllText(outputAbsolutePath, result);
- // Use interlocked to safely increment the counter in a multi-threaded environment
- _ = Interlocked.Increment(ref pagesCreated);
+ // Log
+ if (options.Verbose)
+ {
+ Log.Information("Page created: {Permalink}", frontmatter.Permalink);
}
+
+ // Use interlocked to safely increment the counter in a multi-threaded environment
+ _ = Interlocked.Increment(ref pagesCreated);
});
// Stop the stopwatch
diff --git a/source/Models/BasicContent.cs b/source/Models/BasicContent.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d99dc164dd2d4cea7685fcb7ef2647cef1f6a275
--- /dev/null
+++ b/source/Models/BasicContent.cs
@@ -0,0 +1,41 @@
+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/Frontmatter.cs b/source/Models/Frontmatter.cs
index 6ddb9acb3199869ead89f34b0ed5a5d779c823ad..e9ac77f55089f600d525e06e0573dcce63f93c42 100644
--- a/source/Models/Frontmatter.cs
+++ b/source/Models/Frontmatter.cs
@@ -9,13 +9,8 @@ namespace SuCoS;
///
/// The meta data about each content Markdown file.
///
-public class Frontmatter : IParams
+public class Frontmatter : IBaseContent, IParams
{
- ///
- /// The content Title.
- ///
- public string Title { get; init; }
-
///
/// Gets or sets the date of the page.
///
@@ -56,11 +51,32 @@ public class Frontmatter : IParams
///
public Site Site { get; init; }
- ///
- /// The URL pattern to be used to create the url.
- ///
+ #region IBaseContent
+
+ ///
+ public string Title { get; init; }
+
+ ///
+ public string Section { get; set; } = string.Empty;
+
+ ///
+ public Kind Kind { get; set; } = Kind.single;
+
+ ///
+ public string Type { get; set; } = string.Empty;
+
+ ///
public string? URL { get; set; }
+ #endregion IBaseContent
+
+ #region IParams
+
+ ///
+ public Dictionary Params { get; set; } = new();
+
+ #endregion IParams
+
///
/// Secondary URL patterns to be used to create the url.
///
@@ -94,9 +110,6 @@ public class Frontmatter : IParams
}
}
- ///
- public Dictionary Params { get; set; } = new();
-
///
/// Raw content, from the Markdown file.
///
@@ -122,7 +135,6 @@ public class Frontmatter : IParams
}
}
-
///
/// The cached content.
///
@@ -160,26 +172,6 @@ public class Frontmatter : IParams
///
public ConcurrentBag? Pages { get; set; }
- ///
- /// 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.
- ///
- public string Section { get; set; } = string.Empty;
-
- ///
- /// The type of content. It's the same of the Section, if not specified.
- ///
- public string Type { get; set; } = string.Empty;
-
- ///
- /// The type of the page, if it's a single page, a list of pages or the home page.
- ///
- public Kind Kind { get; set; } = Kind.single;
-
///
/// Language of the content.
///
diff --git a/source/Models/IBaseContent.cs b/source/Models/IBaseContent.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d8e42c2ac059698ee77bee5f559b092a07d09202
--- /dev/null
+++ b/source/Models/IBaseContent.cs
@@ -0,0 +1,37 @@
+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/Site.cs b/source/Models/Site.cs
index 82067a85be3a17af2c46c1b3c6da9284af9945b2..538ae81508461acdeda6f0431c1b228d5dbefded 100644
--- a/source/Models/Site.cs
+++ b/source/Models/Site.cs
@@ -1,4 +1,3 @@
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
namespace SuCoS.Models;
@@ -51,23 +50,27 @@ public class Site : IParams
///
/// List of all pages, including generated.
///
- public ConcurrentBag Pages { get; set; } = new();
+ public Dictionary Pages { get; set; } = new();
///
- /// The frontmatter of the home page;
+ /// List of pages from the content folder.
///
- public Frontmatter? HomePage { get; set; }
+ public Dictionary RegularPages { get; set; } = new();
///
- /// List of pages from the content folder.
+ /// The frontmatter of the home page;
///
- public ConcurrentBag RegularPages { get; set; } = new();
+ public Frontmatter? HomePage { get; set; }
///
/// List of all content to be scanned and processed.
///
public List<(string filePath, string content)> RawPages { get; set; } = new();
-
+
+ #region IParams
+
///
public Dictionary Params { get; set; } = new();
+
+ #endregion IParams
}
diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs
index 8389b596b02f920dd933d61e0222f9e50ac4e3d7..3cbdb634b290f3dcdb548e2c3f29bd1f6c4621f1 100644
--- a/source/Parser/YAMLParser.cs
+++ b/source/Parser/YAMLParser.cs
@@ -3,7 +3,6 @@ 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;
@@ -114,7 +113,13 @@ public partial class YAMLParser : IFrontmatterParser
foreach (var tagName in tags)
{
- _ = frontmatterManager.CreateTagFrontmatter(site, tagName: tagName, frontmatter);
+ var contentTemplate = new BasicContent(
+ title: tagName,
+ section: "tags",
+ type: "tags",
+ url: "tags/" + Urlizer.Urlize(tagName)
+ );
+ _ = frontmatterManager.CreateAutomaticFrontmatter(contentTemplate, frontmatter);
}
}
}
diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs
index dc7fd095bcc5bd400f20041c7d66872d16973ac9..9004ede2ab42fe9b96575994dba26fec826de799 100644
--- a/source/ServeCommand.cs
+++ b/source/ServeCommand.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -75,7 +74,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable
/// page content. This could be replaced with more complex logic, such as loading the content
/// from .html files.
///
- private readonly Dictionary pages = new();
+ // private readonly Dictionary pages = new();
private DateTime serverStartTime;
@@ -109,7 +108,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable
throw new FormatException("Error reading app config");
}
- pages.Clear();
+ // pages.Clear();
ResetCache();
ScanAllMarkdownFiles();
@@ -119,20 +118,20 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable
// Generate the build report
stopwatch.LogReport(site.Title);
- foreach (var frontmatter in site.Pages)
- {
- foreach (var url in frontmatter.Urls)
- {
- if (url != null)
- {
- _ = pages.TryAdd(url, frontmatter);
- }
- else
- {
- Log.Error("No permalink for {Title}", frontmatter.Title);
- }
- }
- }
+ // foreach (var frontmatter in site.Pages)
+ // {
+ // foreach (var url in frontmatter.Urls)
+ // {
+ // if (url != null)
+ // {
+ // _ = pages.TryAdd(url, frontmatter);
+ // }
+ // else
+ // {
+ // Log.Error("No permalink for {Title}", frontmatter.Title);
+ // }
+ // }
+ // }
}
///
@@ -237,6 +236,11 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable
private async Task HandleRequest(HttpContext context)
{
var requestPath = context.Request.Path.Value;
+ if (string.IsNullOrEmpty(Path.GetExtension(context.Request.Path.Value)) && (requestPath.Length > 1))
+ {
+ requestPath = requestPath.TrimEnd('/');
+ }
+
var fileAbsolutePath = Path.Combine(options.Source, "static", requestPath.TrimStart('/'));
if (options.Verbose)
@@ -263,7 +267,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable
}
// Check if the requested file path corresponds to a registered page
- else if (pages.TryGetValue(requestPath, out var frontmatter))
+ else if (site.Pages.TryGetValue(requestPath, out var frontmatter))
{
// Generate the output content for the frontmatter
var content = CreateOutputFile(frontmatter);