From 60bcead306c1695424f651153f993d9e2939357b Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 21 Jun 2023 20:13:22 -0300 Subject: [PATCH] feat: automatically create section pages --- source/BaseGeneratorCommand.cs | 215 ++++++++++++++++++++------------- source/BuildCommand.cs | 42 +++---- source/Models/BasicContent.cs | 41 +++++++ source/Models/Frontmatter.cs | 58 ++++----- source/Models/IBaseContent.cs | 37 ++++++ source/Models/Site.cs | 17 +-- source/Parser/YAMLParser.cs | 9 +- source/ServeCommand.cs | 40 +++--- 8 files changed, 295 insertions(+), 164 deletions(-) create mode 100644 source/Models/BasicContent.cs create mode 100644 source/Models/IBaseContent.cs diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index f2375ab..66094d5 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 0d12827..d10df26 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 0000000..d99dc16 --- /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 6ddb9ac..e9ac77f 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 0000000..d8e42c2 --- /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 82067a8..538ae81 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 8389b59..3cbdb63 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 dc7fd09..9004ede 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); -- GitLab