From a90535a770798a5fd6552fb0d009149752ffaee4 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 8 Aug 2023 12:58:11 -0300 Subject: [PATCH 1/9] feat: page bundle types --- source/Models/Bundle.cs | 22 ++++++++++++++++++++ source/Models/IPage.cs | 5 +++++ source/Models/Page.cs | 13 +++++------- source/Models/Site.cs | 43 +++++++++++++++++++++++++--------------- test/Models/SiteTests.cs | 8 +++++--- 5 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 source/Models/Bundle.cs diff --git a/source/Models/Bundle.cs b/source/Models/Bundle.cs new file mode 100644 index 0000000..1a8d7db --- /dev/null +++ b/source/Models/Bundle.cs @@ -0,0 +1,22 @@ +namespace SuCoS.Models; + +/// +/// The type of content bundle. +/// +public enum BundleType +{ + /// + /// Regular page. Not a bundle. + /// + none, + + /// + /// Bundle with no childre + /// + leaf, + + /// + /// Bundle with children embeded, like a home page, taxonomy term, taxonomy list + /// + branch +} diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs index 1f739e5..05445aa 100644 --- a/source/Models/IPage.cs +++ b/source/Models/IPage.cs @@ -49,6 +49,11 @@ public interface IPage : IFrontMatter /// public IPage? Parent { get; set; } + /// + /// The bundle type of the page. + /// + public BundleType BundleType { get; set; } + /// /// Plain markdown content, without HTML. /// diff --git a/source/Models/Page.cs b/source/Models/Page.cs index 35f0e90..c2e4dc6 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -89,7 +89,6 @@ internal class Page : IPage ? null : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); - /// /// Point to the site configuration. /// @@ -100,9 +99,7 @@ internal class Page : IPage /// public List? AliasesProcessed { get; set; } - /// - /// The URL for the content. - /// + /// public string? Permalink { get; set; } /// @@ -111,12 +108,12 @@ internal class Page : IPage /// public ConcurrentBag PagesReferences { get; } = new(); - /// - /// Other content that mention this content. - /// Used to create the tags list and Related Posts section. - /// + /// public IPage? Parent { get; set; } + /// + public BundleType BundleType { get; set; } = BundleType.none; + /// /// Plain markdown content, without HTML. /// diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 5601631..e210836 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -151,7 +151,7 @@ internal class Site : ISite private const string indexFileConst = "index.md"; - private const string indexFileUpperConst = "INDEX.MD"; + private const string index_FileConst = "_index.md"; /// /// The synchronization lock object during ProstProcess. @@ -223,7 +223,7 @@ internal class Site : ISite _ = Parallel.ForEach(markdownFiles, filePath => { - ParseSourceFile(parent, filePath); + ParseSourceFile(filePath, parent); }); var subdirectories = Directory.GetDirectories(directory); @@ -233,14 +233,29 @@ internal class Site : ISite }); } - private void ParseIndexPage(in string? directory, int level, ref IPage? parent, ref string[] markdownFiles) + private void ParseIndexPage(string? directory, int level, ref IPage? 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) + var indexLeafBundlePage = markdownFiles.FirstOrDefault(file => + file.AsSpan()[Math.Max(0, file.Length - indexFileConst.Length)..].SequenceEqual(indexFileConst.AsSpan())); + + var indexBranchBundlePage = markdownFiles.FirstOrDefault(file => + file.AsSpan()[Math.Max(0, file.Length - index_FileConst.Length)..].SequenceEqual(index_FileConst.AsSpan())); + + if (indexLeafBundlePage is not null || indexBranchBundlePage is not null) { - markdownFiles = markdownFiles.Where(file => file != indexPage).ToArray(); - var page = ParseSourceFile(parent, indexPage); + // Determine the file to use and the bundle type + var selectedFile = indexLeafBundlePage ?? indexBranchBundlePage; + var bundleType = indexLeafBundlePage is not null ? BundleType.leaf : BundleType.branch; + + // Remove the selected file from markdownFiles + markdownFiles = indexLeafBundlePage is not null + ? new string[] { } + : markdownFiles.Where(file => file != selectedFile).ToArray(); + + var page = ParseSourceFile(selectedFile!, parent); + if (page is null) return; + page.BundleType = bundleType; + if (level == 0) { PagesReferences.TryRemove(page!.Permalink!, out _); @@ -254,22 +269,18 @@ internal class Site : ISite parent = page; } } - - // If it's the home page else if (level == 0) { Home = CreateSystemPage(string.Empty, Title); } - - // Or a section page, which must be used as the parent for the next sub folder - else if (level == 1) + else if (level == 1 && directory is not null) { - var section = new DirectoryInfo(directory!).Name; + var section = new DirectoryInfo(directory).Name; parent = CreateSystemPage(section, section); } } - private IPage? ParseSourceFile(in IPage? parent, in string filePath) + private IPage? ParseSourceFile(in string filePath, in IPage? parent) { Page? page = null; try @@ -277,7 +288,7 @@ internal class Site : ISite var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath) ?? throw new FormatException($"Error parsing front matter for {filePath}"); - if (IsValidDate(frontMatter, Options)) + if (IsValidPage(frontMatter, Options)) { page = new(frontMatter, this); PostProcessPage(page, parent, true); diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index 83403a1..13e30a7 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -69,8 +69,9 @@ public class SiteTests : TestSetup [Theory] [InlineData(testSitePathCONST01, 5)] - [InlineData(testSitePathCONST02, 8)] + [InlineData(testSitePathCONST02, 1)] [InlineData(testSitePathCONST03, 13)] + [InlineData(testSitePathCONST04, 26)] public void PagesReference_ShouldReturnExpectedQuantityOfPages(string sitePath, int expectedQuantity) { GenerateOptions options = new() @@ -88,8 +89,9 @@ public class SiteTests : TestSetup [Theory] [InlineData(testSitePathCONST01, 4)] - [InlineData(testSitePathCONST02, 7)] + [InlineData(testSitePathCONST02, 0)] [InlineData(testSitePathCONST03, 11)] + [InlineData(testSitePathCONST04, 21)] public void Page_IsPage_ShouldReturnExpectedQuantityOfPages(string sitePath, int expectedQuantity) { GenerateOptions options = new() @@ -127,7 +129,7 @@ public class SiteTests : TestSetup { GenerateOptions options = new() { - Source = Path.GetFullPath(Path.Combine(testSitesPath, testSitePathCONST02)) + Source = Path.GetFullPath(Path.Combine(testSitesPath, testSitePathCONST01)) }; site.Options = options; -- GitLab From 22196226c847d322c69beaeb49f9b0f0d0bdd1bb Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 8 Aug 2023 16:18:37 -0300 Subject: [PATCH 2/9] feat: page resources --- source/Models/FrontMatter.cs | 16 ++- source/Models/IFrontMatter.cs | 20 +++- source/Models/IPage.cs | 14 ++- source/Models/ISite.cs | 10 ++ source/Models/Page.cs | 69 ++++++++++- source/Models/Resource.cs | 54 +++++++++ source/Models/Site.cs | 179 +++++++++++++--------------- source/Parser/IFrontmatterParser.cs | 9 +- source/Parser/YAMLParser.cs | 25 ++-- test/Models/FrontMatterTests.cs | 2 +- test/Models/PageTests.cs | 18 +-- test/Models/SiteTests.cs | 6 +- test/Parser/YAMLParserTests.cs | 28 +++-- test/TestSetup.cs | 2 +- 14 files changed, 299 insertions(+), 153 deletions(-) create mode 100644 source/Models/Resource.cs diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 9c5629b..e335f06 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -59,15 +59,23 @@ internal class FrontMatter : IFrontMatter /// [YamlIgnore] - public string? SourcePath { get; set; } + public string? SourceRelativePath { get; set; } /// [YamlIgnore] - public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath); + public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); /// [YamlIgnore] - public string? SourcePathDirectory => Path.GetDirectoryName(SourcePath); + public string? SourceFullPath { get; set; } + + /// + [YamlIgnore] + public string? SourceFullPathDirectory => Path.GetDirectoryName(SourceFullPath); + + /// + [YamlIgnore] + public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourceRelativePath); /// [YamlIgnore] @@ -91,6 +99,6 @@ internal class FrontMatter : IFrontMatter public FrontMatter(string title, string sourcePath) { Title = title; - SourcePath = sourcePath; + SourceRelativePath = sourcePath; } } diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 8da34d5..1ce3141 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -108,19 +108,29 @@ public interface IFrontMatter : IParams Kind Kind { get; } /// - /// The source filename, without the extension. ;) + /// The source file/folder, relative to content folder + /// + public string? SourceRelativePath { get; } + + /// + /// The source directory of the file, without the file name. /// - public string? SourcePath { get; } + string? SourceRelativePathDirectory { get; } /// /// The source filename, without the extension. ;) /// - string? SourceFileNameWithoutExtension { get; } + public string? SourceFullPath { get; } /// - /// The source directory of the file, without the file name. + /// The full source directory of the file, without the file name. + /// + string? SourceFullPathDirectory { get; } + + /// + /// The source filename, without the extension. ;) /// - string? SourcePathDirectory { get; } + string? SourceFileNameWithoutExtension { get; } /// /// The date to be considered as the publish date. diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs index 05445aa..ab26f47 100644 --- a/source/Models/IPage.cs +++ b/source/Models/IPage.cs @@ -18,9 +18,9 @@ public interface IPage : IFrontMatter /// /// The source directory of the file. /// - public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory) + public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourceRelativePathDirectory) ? null - : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + : Path.GetFileName(Path.GetFullPath(SourceRelativePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); /// /// Point to the site configuration. @@ -54,6 +54,11 @@ public interface IPage : IFrontMatter /// public BundleType BundleType { get; set; } + /// + /// Page resources. All files that accompany a page. + /// + public List? Resources { get; set; } + /// /// Plain markdown content, without HTML. /// @@ -125,4 +130,9 @@ public interface IPage : IFrontMatter /// The URL to consider. If null use the predefined URL /// The output path. public string CreatePermalink(string? URLforce = null); + + /// + /// Final steps of parsing the content. + /// + public void PostProcess(); } diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs index 99a833e..180ccae 100644 --- a/source/Models/ISite.cs +++ b/source/Models/ISite.cs @@ -140,4 +140,14 @@ public interface ISite : IParams /// Check if the page is publishable /// public bool IsDatePublishable(in IFrontMatter frontMatter); + + /// + /// Creates the page for the site index. + /// + /// The relative path of the page. + /// + /// + /// + /// The created page for the index. + public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null); } \ No newline at end of file diff --git a/source/Models/Page.cs b/source/Models/Page.cs index c2e4dc6..e8c48ac 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -65,13 +65,19 @@ internal class Page : IPage } /// - public string? SourcePath => frontMatter.SourcePath; + public string? SourceRelativePath => frontMatter.SourceRelativePath; /// - public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension; + public string? SourceRelativePathDirectory => frontMatter.SourceRelativePathDirectory; + + /// + public string? SourceFullPath => frontMatter.SourceFullPath; /// - public string? SourcePathDirectory => frontMatter.SourcePathDirectory; + public string? SourceFullPathDirectory => frontMatter.SourceFullPathDirectory; + + /// + public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension; /// public Dictionary Params @@ -85,9 +91,9 @@ internal class Page : IPage /// /// The source directory of the file. /// - public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory) + public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourceRelativePathDirectory) ? null - : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + : Path.GetFileName(Path.GetFullPath(SourceRelativePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); /// /// Point to the site configuration. @@ -114,6 +120,9 @@ internal class Page : IPage /// public BundleType BundleType { get; set; } = BundleType.none; + /// + public List? Resources { get; set; } + /// /// Plain markdown content, without HTML. /// @@ -169,7 +178,6 @@ internal class Page : IPage /// 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. @@ -317,6 +325,55 @@ endif return Urlizer.UrlizePath(permaLink); } + public void PostProcess() + { + // Create all the aliases + if (Aliases is not null) + { + AliasesProcessed ??= new(); + foreach (var alias in Aliases) + { + AliasesProcessed.Add(CreatePermalink(alias)); + } + } + + // Create tag pages, if any + if (Tags is not null) + { + foreach (var tagName in Tags) + { + Site.CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", this); + } + } + + ScanForResources(); + } + + private void ScanForResources() + { + if (string.IsNullOrEmpty(SourceFullPathDirectory)) return; + if (BundleType == BundleType.none) return; + + try + { + var resourceFiles = Directory.GetFiles(SourceFullPathDirectory) + .Where(file => + file != SourceFullPath && + (BundleType == BundleType.leaf || !file.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) + ); + + foreach (var resourceFile in resourceFiles) + { + Resources ??= new(); + Resources.Add(new Resource(resourceFile)); + } + } + catch + { + return; + } + } + private string ParseAndRenderTemplate(bool isBaseTemplate, string errorMessage) { var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site.CacheManager, isBaseTemplate); diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs new file mode 100644 index 0000000..4ac606b --- /dev/null +++ b/source/Models/Resource.cs @@ -0,0 +1,54 @@ +using System.IO; +using Microsoft.AspNetCore.StaticFiles; + +namespace SuCoS.Models; + +/// +/// Page resources. All files that accompany a page. +/// +public class Resource +{ + /// + /// File path + /// + public string SourcePath { get; set; } + + /// + /// File name, without the extension. + /// + public string SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath); + + /// + /// File extension. + /// + public string Extension => Path.GetExtension(SourcePath); + + /// + /// File MIME type. + /// + public string MimeType => GetMimeType(SourcePath); + + /// + /// File size in bytes. + /// + public long Size => new FileInfo(SourcePath).Length; + + /// + /// Default constructor. + /// + /// + public Resource(string path) + { + SourcePath = path; + } + + private string GetMimeType(string path) + { + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(path, out var contentType)) + { + contentType = "application/octet-stream"; + } + return contentType; + } +} diff --git a/source/Models/Site.cs b/source/Models/Site.cs index e210836..fd5eed3 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -149,9 +149,9 @@ internal class Site : ISite /// public int filesParsedToReport; - private const string indexFileConst = "index.md"; + private const string indexLeafFileConst = "index.md"; - private const string index_FileConst = "_index.md"; + private const string indexBranchFileConst = "_index.md"; /// /// The synchronization lock object during ProstProcess. @@ -190,8 +190,9 @@ internal class Site : ISite // Liquid template options, needed to theme the content // but also parse URLs - TemplateOptions.MemberAccessStrategy.Register(); TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); this.clock = clock ?? new SystemClock(); } @@ -233,36 +234,101 @@ internal class Site : ISite }); } + /// + /// Creates the page for the site index. + /// + /// The relative path of the page. + /// + /// + /// + /// The created page for the index. + public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) + { + sectionName ??= "section"; + var isIndex = string.IsNullOrEmpty(relativePath); + FrontMatter frontMatter = new() + { + Kind = isIndex ? Kind.index : Kind.list, + Section = isIndex ? "index" : sectionName, + SourceRelativePath = Path.Combine(relativePath, indexLeafFileConst), + SourceFullPath = Path.Combine(SourceContentPath, relativePath, indexLeafFileConst), + Title = title, + Type = isIndex ? "index" : sectionName, + URL = relativePath + }; + + var id = frontMatter.URL; + + // Get or create the page + var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => + { + IPage? parent = null; + // Check if we need to create a section, even + var sections = (frontMatter.SourceRelativePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries); + if (sections.Length > 1) + { + parent = CreateSystemPage(sections[0], sections[0]); + } + + var newPage = new Page(frontMatter, this) + { + BundleType = BundleType.branch + }; + PostProcessPage(newPage, parent); + return newPage; + })); + + // get the page from the lazy object + var page = lazyPage.Value; + + if (originalPage is null || string.IsNullOrEmpty(originalPage.Permalink)) + { + return page; + } + + if (page.Kind != Kind.index) + { + page.PagesReferences.Add(originalPage.Permalink); + } + + // TODO: still too hardcoded to add the tags reference + if (page.Type != "tags") + { + return page; + } + originalPage.TagsReference.Add(page); + return page; + } + private void ParseIndexPage(string? directory, int level, ref IPage? parent, ref string[] markdownFiles) { - var indexLeafBundlePage = markdownFiles.FirstOrDefault(file => - file.AsSpan()[Math.Max(0, file.Length - indexFileConst.Length)..].SequenceEqual(indexFileConst.AsSpan())); + var indexLeafBundlePage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file) == indexLeafFileConst); - var indexBranchBundlePage = markdownFiles.FirstOrDefault(file => - file.AsSpan()[Math.Max(0, file.Length - index_FileConst.Length)..].SequenceEqual(index_FileConst.AsSpan())); + var indexBranchBundlePage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file) == indexBranchFileConst); + IPage? page = null; if (indexLeafBundlePage is not null || indexBranchBundlePage is not null) { // Determine the file to use and the bundle type var selectedFile = indexLeafBundlePage ?? indexBranchBundlePage; - var bundleType = indexLeafBundlePage is not null ? BundleType.leaf : BundleType.branch; + var bundleType = selectedFile == indexLeafBundlePage ? BundleType.leaf : BundleType.branch; // Remove the selected file from markdownFiles - markdownFiles = indexLeafBundlePage is not null + markdownFiles = bundleType == BundleType.leaf ? new string[] { } : markdownFiles.Where(file => file != selectedFile).ToArray(); - var page = ParseSourceFile(selectedFile!, parent); + page = ParseSourceFile(selectedFile!, parent, bundleType); if (page is null) return; - page.BundleType = bundleType; if (level == 0) { PagesReferences.TryRemove(page!.Permalink!, out _); - Home = page; page.Permalink = "/"; page.Kind = Kind.index; + PagesReferences.GetOrAdd(page.Permalink, page); + Home = page; } else { @@ -280,7 +346,7 @@ internal class Site : ISite } } - private IPage? ParseSourceFile(in string filePath, in IPage? parent) + private IPage? ParseSourceFile(in string filePath, in IPage? parent, BundleType bundleType = BundleType.none) { Page? page = null; try @@ -290,7 +356,10 @@ internal class Site : ISite if (IsValidPage(frontMatter, Options)) { - page = new(frontMatter, this); + page = new(frontMatter, this) + { + BundleType = bundleType + }; PostProcessPage(page, parent, true); } } @@ -305,68 +374,6 @@ internal class Site : ISite return page; } - /// - /// Creates the page for the site index. - /// - /// The relative path of the page. - /// - /// - /// - /// The created page for the index. - private IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) - { - sectionName ??= "section"; - var isIndex = string.IsNullOrEmpty(relativePath); - FrontMatter frontMatter = new() - { - 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 = frontMatter.URL; - - // Get or create the page - var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => - { - IPage? parent = null; - // Check if we need to create a section, even - var sections = (frontMatter.SourcePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries); - if (sections.Length > 1) - { - parent = CreateSystemPage(sections[0], sections[0]); - } - - var newPage = new Page(frontMatter, this); - PostProcessPage(newPage, parent); - return newPage; - })); - - // get the page from the lazy object - var page = lazyPage.Value; - - if (originalPage is null || string.IsNullOrEmpty(originalPage.Permalink)) - { - return page; - } - - if (page.Kind != Kind.index) - { - page.PagesReferences.Add(originalPage.Permalink); - } - - // TODO: still too hardcoded to add the tags reference - if (page.Type != "tags") - { - return page; - } - originalPage.TagsReference.Add(page); - return page; - } - /// /// Extra calculation and automatic data for each page. /// @@ -386,6 +393,9 @@ internal class Site : ISite { if (!PagesReferences.TryGetValue(page.Permalink, out var old) || overwrite) { + page.PostProcess(); + + // Replace the old page with the newly created one if (old?.PagesReferences is not null) { foreach (var pageOld in old.PagesReferences) @@ -394,15 +404,6 @@ internal class Site : ISite } } - if (page.Aliases is not null) - { - page.AliasesProcessed ??= new(); - foreach (var alias in page.Aliases) - { - page.AliasesProcessed.Add(page.CreatePermalink(alias)); - } - } - // Register the page for all urls foreach (var url in page.Urls) { @@ -411,14 +412,6 @@ internal class Site : ISite } } - if (page.Tags is not null) - { - foreach (var tagName in page.Tags) - { - CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", page); - } - } - if (!string.IsNullOrEmpty(page.Section) && PagesReferences.TryGetValue('/' + page.Section!, out var section)) { diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 0f1e023..67a536c 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IFrontmatterParser.cs @@ -10,18 +10,19 @@ public interface IFrontMatterParser /// /// Extract the front matter from the content file. /// - /// + /// /// /// - IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath); + IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string sourceContentPath); /// /// Extract the front matter from the content. /// + /// /// - /// + /// /// - IFrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent); + IFrontMatter? ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent); /// /// Parse the app config file. diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 80b0c52..51ab894 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -29,30 +29,30 @@ internal class YAMLParser : IFrontMatterParser .Build(); /// - public IFrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null) + public IFrontMatter ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string? sourceContentPath = null) { - if (filePath is null) + if (fileFullPath is null) { - throw new ArgumentNullException(nameof(filePath)); + throw new ArgumentNullException(nameof(fileFullPath)); } string? fileContent; string? fileRelativePath; try { - fileContent = File.ReadAllText(filePath); - fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, filePath); + fileContent = File.ReadAllText(fileFullPath); + fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, fileFullPath); } catch (Exception ex) { - throw new FileNotFoundException(filePath, ex); + throw new FileNotFoundException(fileFullPath, ex); } - return ParseFrontmatterAndMarkdown(fileRelativePath, fileContent); + return ParseFrontmatterAndMarkdown(fileFullPath, fileRelativePath, fileContent); } /// - public IFrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent) + public IFrontMatter ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent) { if (fileRelativePath is null) { @@ -74,18 +74,19 @@ internal class YAMLParser : IFrontMatterParser var rawContent = content.ReadToEnd(); // Now, you can parse the YAML front matter - var page = ParseYAML(fileRelativePath, yaml, rawContent); + var page = ParseYAML(fileFullPath, fileRelativePath, yaml, rawContent); return page; } - private IFrontMatter ParseYAML(in string filePath, string yaml, in string rawContent) + private IFrontMatter ParseYAML(in string fileFullPath, in string fileRelativePath, string yaml, in string rawContent) { var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter"); - var section = SiteHelper.GetSection(filePath); + var section = SiteHelper.GetSection(fileRelativePath); frontMatter.RawContent = rawContent; frontMatter.Section = section; - frontMatter.SourcePath = filePath; + frontMatter.SourceRelativePath = fileRelativePath; + frontMatter.SourceFullPath = fileFullPath; frontMatter.Type ??= section; var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml)); diff --git a/test/Models/FrontMatterTests.cs b/test/Models/FrontMatterTests.cs index b045efb..dcd4e81 100644 --- a/test/Models/FrontMatterTests.cs +++ b/test/Models/FrontMatterTests.cs @@ -58,7 +58,7 @@ public class FrontMatterTests : TestSetup var frontMatter = new FrontMatter("Title", sourcePath); // Assert - Assert.Equal(expectedDirectory, frontMatter.SourcePathDirectory); + Assert.Equal(expectedDirectory, frontMatter.SourceRelativePathDirectory); } [Theory] diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index 9e51d14..cbca303 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -39,10 +39,10 @@ word03 word04 word05 6 7 eight // Assert Assert.Equal(title, page.Title); - Assert.Equal(sourcePath, page.SourcePath); + Assert.Equal(sourcePath, page.SourceRelativePath); Assert.Same(site, page.Site); Assert.Equal(sourceFileNameWithoutExtension, page.SourceFileNameWithoutExtension); - Assert.Equal(sourcePathDirectory, page.SourcePathDirectory); + Assert.Equal(sourcePathDirectory, page.SourceRelativePathDirectory); } [Fact] @@ -80,7 +80,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, Aliases = new() { "v123", "{{ page.Title }}", "{{ page.Title }}-2" } }, site); @@ -102,7 +102,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, ExpiryDate = systemClockMock.Object.Now.AddDays(days) }, site); @@ -121,7 +121,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, PublishDate = publishDate is null ? null : DateTime.Parse(publishDate, CultureInfo.InvariantCulture), Date = date is null ? null : DateTime.Parse(date, CultureInfo.InvariantCulture) }, site); @@ -172,7 +172,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, PublishDate = publishDate is null ? null : DateTime.Parse(publishDate, CultureInfo.InvariantCulture), Date = date is null ? null : DateTime.Parse(date, CultureInfo.InvariantCulture), Draft = draft @@ -193,7 +193,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, Date = systemClockMock.Object.Now.AddDays(1) }, site); @@ -213,7 +213,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePath + SourceRelativePath = sourcePath }, site); // Assert @@ -228,7 +228,7 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, + SourceRelativePath = sourcePathCONST, URL = urlTemplate }, site); var actualPermalink = page.CreatePermalink(); diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index 13e30a7..8fce43a 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -24,7 +24,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); // Assert - Assert.Contains(site.Pages, page => page.SourcePathDirectory!.Length == 0); + Assert.Contains(site.Pages, page => page.SourceRelativePathDirectory!.Length == 0); Assert.Contains(site.Pages, page => page.SourceFileNameWithoutExtension == fileNameWithoutExtension); } @@ -158,8 +158,8 @@ public class SiteTests : TestSetup Assert.NotNull(tagSectionPage); Assert.Equal(2, tagSectionPage.Pages.Count()); Assert.Empty(tagSectionPage.RegularPages); - Assert.Equal("tags/index.md", tagSectionPage.SourcePath); - Assert.Equal("tags", tagSectionPage.SourcePathDirectory); + Assert.Equal("tags/index.md", tagSectionPage.SourceRelativePath); + Assert.Equal("tags", tagSectionPage.SourceRelativePathDirectory); Assert.Equal("tags", tagSectionPage.SourcePathLastDirectory); } diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index cd746f0..b001011 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -52,12 +52,14 @@ NestedData: - Test - Real Data "; - private const string filePathCONST = "test.md"; + private const string fileFullPathCONST = "test.md"; + private const string fileRelativePathCONST = "test.md"; private readonly string pageContent; public YAMLParserTests() { parser = new YAMLParser(); + generateOptionsMock.Setup(o => o.Source).Returns("/"); siteDefault = new Site(generateOptionsMock.Object, siteSettingsMock.Object, parser, loggerMock.Object, systemClockMock.Object); pageContent = @$"--- {pageFrontmaterCONST} @@ -89,7 +91,7 @@ Date: 2023-04-01 public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) { // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(filePathCONST, fileContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); // Asset Assert.Equal(expectedTitle, frontMatter.Title); @@ -109,7 +111,7 @@ Date: 2023/01/01 var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(filePathCONST, fileContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent); // Asset Assert.Equal(expectedDate, frontMatter.Date); @@ -124,7 +126,7 @@ Date: 2023/01/01 var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown(filePathCONST, pageContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, pageContent); // Asset Assert.Equal("Test Title", frontMatter.Title); @@ -144,7 +146,7 @@ Title "; // Asset - Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(filePathCONST, fileContent)); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); } [Fact] @@ -167,7 +169,7 @@ Title var page = new Page(new FrontMatter { Title = "Test Title", - SourcePath = "/test.md" + SourceRelativePath = "/test.md" }, siteDefault); // Act @@ -182,7 +184,7 @@ Title public void ParseFrontmatter_ShouldParseContentInSiteFolder() { var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var frontMatter = parser.ParseFrontmatterAndMarkdown("", pageContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); Page page = new(frontMatter, siteDefault); // Act @@ -196,7 +198,7 @@ Title public void ParseFrontmatter_ShouldCreateTags() { // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown("", pageContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); Page page = new(frontMatter, siteDefault); // Act @@ -209,7 +211,7 @@ Title [Fact] public void ParseFrontmatter_ShouldParseCategoriesCorrectly() { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", pageContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); // Asset Assert.Equal(new[] { "Test", "Real Data" }, frontMatter.Params["Categories"]); @@ -238,19 +240,19 @@ Title [Fact] public void ParseFrontmatter_ShouldThrowExceptionWhenFilePathDoesNotExist2() { - Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, "fakeContent")); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(null!, null!, "fakeContent")); } [Fact] public void ParseFrontmatter_ShouldHandleEmptyFileContent() { - Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "")); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "")); } [Fact] public void ParseYAML_ShouldThrowExceptionWhenFrontmatterIsInvalid() { - Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "invalidFrontmatter")); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", "invalidFrontmatter")); } [Fact] @@ -265,7 +267,7 @@ Title [Fact] public void ParseSiteSettings_ShouldReturnContent() { - var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", pageContent); + var frontMatter = parser.ParseFrontmatterAndMarkdown("fakeFilePath", "/fakeFilePath", pageContent); Assert.Equal(pageMarkdownCONST, frontMatter.RawContent); } diff --git a/test/TestSetup.cs b/test/TestSetup.cs index 151f2c4..d0b1908 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -29,7 +29,7 @@ public class TestSetup protected readonly IFrontMatter frontMatterMock = new FrontMatter() { Title = titleCONST, - SourcePath = sourcePathCONST + SourceRelativePath = sourcePathCONST }; protected readonly ISite site; -- GitLab From 097e60da0ea95721eab9c72962906d303d15e0dd Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 8 Aug 2023 16:18:42 -0300 Subject: [PATCH 3/9] feat: page resources --- source/Models/Page.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Models/Page.cs b/source/Models/Page.cs index e8c48ac..a80297a 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -325,6 +325,7 @@ endif return Urlizer.UrlizePath(permaLink); } + /// public void PostProcess() { // Create all the aliases -- GitLab From 682c326b0c5894ce3710c1868532d074e7934267 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 9 Aug 2023 12:00:49 -0300 Subject: [PATCH 4/9] refactor: IFile to unify the file description --- source/BaseGeneratorCommand.cs | 2 +- source/BuildCommand.cs | 2 +- source/Helpers/FileUtils.cs | 2 +- source/Helpers/SiteHelper.cs | 2 +- source/Helpers/SourceFileWatcher.cs | 4 +- source/Helpers/StopwatchReporter.cs | 2 +- source/Helpers/Urlizer.cs | 4 +- .../Models/CommandLineOptions/BuildOptions.cs | 2 +- .../CommandLineOptions/GenerateOptions.cs | 2 +- .../Models/CommandLineOptions/ServeOptions.cs | 2 +- source/Models/FrontMatter.cs | 20 +++--- source/Models/IFile.cs | 61 +++++++++++++++++++ source/Models/IFrontMatter.cs | 33 ++-------- source/Models/ISystemClock.cs | 2 +- source/Models/Page.cs | 4 +- source/Models/Resource.cs | 44 ++----------- source/Models/Site.cs | 4 +- source/Models/SiteSettings.cs | 2 +- source/Parser/YAMLParser.cs | 2 +- source/Program.cs | 28 +++++++-- source/ServeCommand.cs | 2 +- source/ServerHandlers/IServerHandlers.cs | 2 +- source/ServerHandlers/PingRequests.cs | 2 +- .../ServerHandlers/RegisteredPageRequest.cs | 6 +- source/ServerHandlers/StaticFileRequest.cs | 7 ++- 25 files changed, 136 insertions(+), 107 deletions(-) create mode 100644 source/Models/IFile.cs diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index fb05427..dcafd2e 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -14,7 +14,7 @@ namespace SuCoS; /// /// Base class for build and serve commands. /// -internal abstract class BaseGeneratorCommand +public abstract class BaseGeneratorCommand { /// /// The configuration file name. diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 9046589..64db956 100644 --- a/source/BuildCommand.cs +++ b/source/BuildCommand.cs @@ -10,7 +10,7 @@ namespace SuCoS; /// /// Build Command will build the site based on the source files. /// -internal class BuildCommand : BaseGeneratorCommand +public class BuildCommand : BaseGeneratorCommand { private readonly BuildOptions options; diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 217a4bc..25c490e 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -9,7 +9,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -internal static class FileUtils +public static class FileUtils { /// /// Gets the content of a template file based on the page and the theme path. diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 3ec93a3..910030e 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -13,7 +13,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -internal static class SiteHelper +public static class SiteHelper { /// /// Markdig 20+ built-in extensions diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index d32782b..d26b5c2 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -4,7 +4,7 @@ using System.IO; /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// -internal interface IFileWatcher +public interface IFileWatcher { /// /// Starts the file watcher to monitor file changes in the specified source path. @@ -23,7 +23,7 @@ internal interface IFileWatcher /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// -internal class SourceFileWatcher : IFileWatcher +public class SourceFileWatcher : IFileWatcher { /// /// The FileSystemWatcher object that monitors the source directory for file changes. diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs index ec4c5a1..46219a6 100644 --- a/source/Helpers/StopwatchReporter.cs +++ b/source/Helpers/StopwatchReporter.cs @@ -12,7 +12,7 @@ namespace SuCoS.Helpers; /// The stopwatch is started /// and stopped around parts of the code that we want to measure. /// -internal class StopwatchReporter +public class StopwatchReporter { private readonly ILogger logger; private readonly Dictionary stopwatches; diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs index 2af9da0..0b83bfd 100644 --- a/source/Helpers/Urlizer.cs +++ b/source/Helpers/Urlizer.cs @@ -8,7 +8,7 @@ namespace SuCoS.Helpers; /// /// Helper class to convert a string to a URL-friendly string. /// -internal static partial class Urlizer +public static partial class Urlizer { [GeneratedRegex(@"[^a-zA-Z0-9]+")] private static partial Regex UrlizeRegexAlpha(); @@ -68,7 +68,7 @@ internal static partial class Urlizer /// Options for the class. /// Basically to force lowercase and to change the replacement character. /// -internal class UrlizerOptions +public class UrlizerOptions { /// /// Force to generate lowercase URLs. diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index 86f9748..3710cdf 100644 --- a/source/Models/CommandLineOptions/BuildOptions.cs +++ b/source/Models/CommandLineOptions/BuildOptions.cs @@ -5,7 +5,7 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the build command. /// -internal class BuildOptions : GenerateOptions +public class BuildOptions : GenerateOptions { /// /// The path of the output files. diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs index 8435eaf..6122843 100644 --- a/source/Models/CommandLineOptions/GenerateOptions.cs +++ b/source/Models/CommandLineOptions/GenerateOptions.cs @@ -3,7 +3,7 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Basic Command line options for the serve and build command. /// -internal class GenerateOptions : IGenerateOptions +public class GenerateOptions : IGenerateOptions { /// public string Source { get; init; } = "."; diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs index 8ea518b..9e2b5c9 100644 --- a/source/Models/CommandLineOptions/ServeOptions.cs +++ b/source/Models/CommandLineOptions/ServeOptions.cs @@ -3,6 +3,6 @@ namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the serve command. /// -internal class ServeOptions : GenerateOptions +public class ServeOptions : GenerateOptions { } diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index e335f06..6eed010 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -9,7 +9,7 @@ namespace SuCoS.Models; /// A scafold structure to help creating system-generated content, like /// tag, section or index pages /// -internal class FrontMatter : IFrontMatter +public class FrontMatter : IFrontMatter { #region IFrontMatter @@ -45,7 +45,7 @@ internal class FrontMatter : IFrontMatter /// public int Weight { get; init; } = 0; - + /// public List? Tags { get; init; } @@ -63,15 +63,11 @@ internal class FrontMatter : IFrontMatter /// [YamlIgnore] - public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); - - /// - [YamlIgnore] - public string? SourceFullPath { get; set; } - + public string SourceFullPath { get; set; } + /// [YamlIgnore] - public string? SourceFullPathDirectory => Path.GetDirectoryName(SourceFullPath); + public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); /// [YamlIgnore] @@ -89,7 +85,10 @@ internal class FrontMatter : IFrontMatter /// /// Constructor /// - public FrontMatter() { } + public FrontMatter() + { + SourceFullPath = string.Empty; + } /// /// Constructor @@ -100,5 +99,6 @@ internal class FrontMatter : IFrontMatter { Title = title; SourceRelativePath = sourcePath; + SourceFullPath = sourcePath; } } diff --git a/source/Models/IFile.cs b/source/Models/IFile.cs new file mode 100644 index 0000000..ad7bed8 --- /dev/null +++ b/source/Models/IFile.cs @@ -0,0 +1,61 @@ +using System.IO; +using Microsoft.AspNetCore.StaticFiles; + +namespace SuCoS.Models; + +/// +/// Basic structure needed to generate user content and system content +/// +public interface IFile +{ + /// + /// The source filename, without the extension. ;) + /// + string SourceFullPath { get; } + + /// + /// The source file/folder, relative to content folder + /// + string? SourceRelativePath { get; } + + /// + /// The source directory of the file, without the file name. + /// + string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); + + /// + /// The full source directory of the file, without the file name. + /// + string? SourceFullPathDirectory => Path.GetDirectoryName(SourceFullPath); + + /// + /// The source filename, without the extension. ;) + /// + string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourceRelativePath); + + /// + /// File extension. + /// + string Extension => Path.GetExtension(SourceFullPath); + + /// + /// File MIME type. + /// + string MimeType + { + get + { + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(SourceFullPath, out var contentType)) + { + contentType = "application/octet-stream"; + } + return contentType; + } + } + + /// + /// File size in bytes. + /// + long Size => new FileInfo(SourceFullPath).Length; +} diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 1ce3141..35bede2 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -7,12 +7,12 @@ namespace SuCoS.Models; /// /// Basic structure needed to generate user content and system content /// -public interface IFrontMatter : IParams +public interface IFrontMatter : IParams, IFile { /// /// The content Title. /// - public string? Title { get; } + string? Title { get; } /// /// The first directory where the content is located, inside content. @@ -90,11 +90,11 @@ public interface IFrontMatter : IParams /// Page weight. Used for sorting by default. /// int Weight { get; } - + /// /// A list of tags, if any. /// - public List? Tags { get; } + List? Tags { get; } /// /// Raw content from the Markdown file, bellow the front matter. @@ -107,31 +107,6 @@ public interface IFrontMatter : IParams /// Kind Kind { get; } - /// - /// The source file/folder, relative to content folder - /// - public string? SourceRelativePath { get; } - - /// - /// The source directory of the file, without the file name. - /// - string? SourceRelativePathDirectory { get; } - - /// - /// The source filename, without the extension. ;) - /// - public string? SourceFullPath { get; } - - /// - /// The full source directory of the file, without the file name. - /// - string? SourceFullPathDirectory { get; } - - /// - /// The source filename, without the extension. ;) - /// - string? SourceFileNameWithoutExtension { get; } - /// /// The date to be considered as the publish date. /// diff --git a/source/Models/ISystemClock.cs b/source/Models/ISystemClock.cs index f3f9274..bd38900 100644 --- a/source/Models/ISystemClock.cs +++ b/source/Models/ISystemClock.cs @@ -21,7 +21,7 @@ public interface ISystemClock /// /// Represents a concrete implementation of the ISystemClock interface using the system clock. /// -internal class SystemClock : ISystemClock +public class SystemClock : ISystemClock { /// /// Gets the current local date and time. diff --git a/source/Models/Page.cs b/source/Models/Page.cs index a80297a..e0f34d0 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -12,7 +12,7 @@ namespace SuCoS.Models; /// /// Each page data created from source files or from the system. /// -internal class Page : IPage +public class Page : IPage { private readonly IFrontMatter frontMatter; @@ -71,7 +71,7 @@ internal class Page : IPage public string? SourceRelativePathDirectory => frontMatter.SourceRelativePathDirectory; /// - public string? SourceFullPath => frontMatter.SourceFullPath; + public string SourceFullPath => frontMatter.SourceFullPath; /// public string? SourceFullPathDirectory => frontMatter.SourceFullPathDirectory; diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index 4ac606b..1f82889 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -1,37 +1,15 @@ -using System.IO; -using Microsoft.AspNetCore.StaticFiles; - namespace SuCoS.Models; /// /// Page resources. All files that accompany a page. /// -public class Resource +public class Resource : IFile { - /// - /// File path - /// - public string SourcePath { get; set; } + /// + public string SourceFullPath { get; set; } - /// - /// File name, without the extension. - /// - public string SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath); - - /// - /// File extension. - /// - public string Extension => Path.GetExtension(SourcePath); - - /// - /// File MIME type. - /// - public string MimeType => GetMimeType(SourcePath); - - /// - /// File size in bytes. - /// - public long Size => new FileInfo(SourcePath).Length; + /// + public string? SourceRelativePath => throw new System.NotImplementedException(); /// /// Default constructor. @@ -39,16 +17,6 @@ public class Resource /// public Resource(string path) { - SourcePath = path; - } - - private string GetMimeType(string path) - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(path, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType; + SourceFullPath = path; } } diff --git a/source/Models/Site.cs b/source/Models/Site.cs index fd5eed3..e2b6b1f 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -16,7 +16,7 @@ namespace SuCoS.Models; /// /// The main configuration of the program, primarily extracted from the app.yaml file. /// -internal class Site : ISite +public class Site : ISite { #region IParams @@ -53,7 +53,7 @@ internal class Site : ISite public string? Copyright => settings.Copyright; /// - /// The base URL that will be used to build internal links. + /// The base URL that will be used to build public links. /// public string BaseURL => settings.BaseURL; diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index 5a52350..1c137b3 100644 --- a/source/Models/SiteSettings.cs +++ b/source/Models/SiteSettings.cs @@ -24,7 +24,7 @@ public class SiteSettings : IParams public string? Copyright { get; set; } /// - /// The base URL that will be used to build internal links. + /// The base URL that will be used to build public links. /// public string BaseURL { get; set; } = string.Empty; diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 51ab894..b827ce2 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -12,7 +12,7 @@ namespace SuCoS.Parser; /// /// Responsible for parsing the content front matter using YAML /// -internal class YAMLParser : IFrontMatterParser +public class YAMLParser : IFrontMatterParser { /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields diff --git a/source/Program.cs b/source/Program.cs index 2a118e3..e42eb4b 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -10,9 +10,12 @@ namespace SuCoS; /// /// The main entry point of the program. /// -internal class Program +public class Program { - internal const string helloWorld = @" + /// + /// Basic logo of the program, for fun + /// + public const string helloWorld = @" ____ ____ ____ /\ _`\ /\ _`\ /\ _`\ \ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ @@ -44,7 +47,12 @@ internal class Program this.logger = logger; } - internal int Run(string[] args) + /// + /// Actual entrypoint of the program + /// + /// + /// + public int Run(string[] args) { // Print the logo of the program. OutputLogo(); @@ -121,7 +129,12 @@ internal class Program return rootCommand.Invoke(args); } - internal static ILogger CreateLogger(bool verbose = false) + /// + /// Create a log (normally from Serilog), depending the verbose option + /// + /// + /// + public static ILogger CreateLogger(bool verbose = false) { return new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) @@ -132,7 +145,7 @@ internal class Program /// /// Print the name and version of the program. /// - internal void OutputWelcome() + public void OutputWelcome() { var assembly = Assembly.GetEntryAssembly(); var assemblyName = assembly?.GetName(); @@ -141,7 +154,10 @@ internal class Program logger.Information("{name} v{version}", appName, appVersion); } - internal void OutputLogo() + /// + /// Print the logo + /// + public void OutputLogo() { logger.Information(helloWorld); } diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 14d1985..2b8cca7 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -15,7 +15,7 @@ namespace SuCoS; /// /// Serve Command will live serve the site and watch any changes. /// -internal class ServeCommand : BaseGeneratorCommand, IDisposable +public class ServeCommand : BaseGeneratorCommand, IDisposable { private const string baseURLDefault = "http://localhost"; private const int portDefault = 1122; diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index c287887..3888700 100644 --- a/source/ServerHandlers/IServerHandlers.cs +++ b/source/ServerHandlers/IServerHandlers.cs @@ -7,7 +7,7 @@ namespace SuCoS.ServerHandlers; /// /// Handle server requests /// -internal interface IServerHandlers +public interface IServerHandlers { /// /// Check if the condition is met to handle the request diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs index 927d701..4ae76ff 100644 --- a/source/ServerHandlers/PingRequests.cs +++ b/source/ServerHandlers/PingRequests.cs @@ -7,7 +7,7 @@ namespace SuCoS.ServerHandlers; /// /// Return the server startup timestamp as the response /// -internal class PingRequests : IServerHandlers +public class PingRequests : IServerHandlers { /// public bool Check(string requestPath) diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index 61feaea..44a422e 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -10,10 +10,14 @@ namespace SuCoS.ServerHandlers; /// /// Return the server startup timestamp as the response /// -internal class RegisteredPageRequest : IServerHandlers +public class RegisteredPageRequest : IServerHandlers { readonly ISite site; + /// + /// Constructor + /// + /// public RegisteredPageRequest(ISite site) { this.site = site; diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index bfa59c4..9cf0e91 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -9,7 +9,7 @@ namespace SuCoS.ServerHandlers; /// /// Check if it is one of the Static files (serve the actual file) /// -internal class StaticFileRequest : IServerHandlers +public class StaticFileRequest : IServerHandlers { private readonly string basePath; private readonly bool inTheme; @@ -39,6 +39,11 @@ internal class StaticFileRequest : IServerHandlers /// public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) { + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); if (context is null) { -- GitLab From 366e067cd623e0313a1b89f9cfd36407cf2d2721 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 9 Aug 2023 12:15:21 -0300 Subject: [PATCH 5/9] fix: replace Moq by NSubstitute due known spyware --- test/Models/PageTests.cs | 22 +++++++++---------- test/Parser/YAMLParserTests.cs | 40 +++++++++++++--------------------- test/ProgramTest.cs | 8 +++---- test/TestSetup.cs | 16 +++++++------- test/test.csproj | 2 +- 5 files changed, 39 insertions(+), 49 deletions(-) diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index cbca303..bf5da47 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -1,8 +1,8 @@ -using System.Globalization; -using Moq; +using NSubstitute; using SuCoS.Models; -using Xunit; using SuCoS.Models.CommandLineOptions; +using System.Globalization; +using Xunit; namespace Test.Models; @@ -103,7 +103,7 @@ word03 word04 word05 6 7 eight { Title = titleCONST, SourceRelativePath = sourcePathCONST, - ExpiryDate = systemClockMock.Object.Now.AddDays(days) + ExpiryDate = systemClockMock.Now.AddDays(days) }, site); // Assert @@ -178,11 +178,11 @@ word03 word04 word05 6 7 eight Draft = draft }, site); - var options = new Mock(); - options.Setup(o => o.Draft).Returns(draftOption); + var options = Substitute.For(); + options.Draft.Returns(draftOption); // Assert - Assert.Equal(expectedValue, site.IsValidPage(page, options.Object)); + Assert.Equal(expectedValue, site.IsValidPage(page, options)); } [Theory] @@ -194,15 +194,15 @@ word03 word04 word05 6 7 eight { Title = titleCONST, SourceRelativePath = sourcePathCONST, - Date = systemClockMock.Object.Now.AddDays(1) + Date = systemClockMock.Now.AddDays(1) }, site); // Act - var options = new Mock(); - options.Setup(o => o.Future).Returns(futureOption); + var options = Substitute.For(); + options.Future.Returns(futureOption); // Assert - Assert.Equal(expected, site.IsValidDate(page, options.Object)); + Assert.Equal(expected, site.IsValidDate(page, options)); } [Theory] diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index b001011..4787750 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -1,22 +1,14 @@ using Xunit; -using Moq; using SuCoS.Parser; using System.Globalization; using SuCoS.Helpers; using SuCoS.Models; -using Serilog; -using SuCoS.Models.CommandLineOptions; namespace Test.Parser; -public class YAMLParserTests +public class YAMLParserTests : TestSetup { private readonly YAMLParser parser; - private readonly Site siteDefault; - private readonly Mock generateOptionsMock = new(); - private readonly Mock siteSettingsMock = new(); - private readonly Mock loggerMock = new(); - private readonly Mock systemClockMock = new(); private const string pageFrontmaterCONST = @"Title: Test Title Type: post @@ -59,8 +51,6 @@ NestedData: public YAMLParserTests() { parser = new YAMLParser(); - generateOptionsMock.Setup(o => o.Source).Returns("/"); - siteDefault = new Site(generateOptionsMock.Object, siteSettingsMock.Object, parser, loggerMock.Object, systemClockMock.Object); pageContent = @$"--- {pageFrontmaterCONST} --- @@ -170,7 +160,7 @@ Title { Title = "Test Title", SourceRelativePath = "/test.md" - }, siteDefault); + }, site); // Act parser.ParseParams(page, typeof(Page), pageFrontmaterCONST); @@ -185,10 +175,10 @@ Title { var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, siteDefault); + Page page = new(frontMatter, site); // Act - siteDefault.PostProcessPage(page); + site.PostProcessPage(page); // Asset Assert.Equal(date, frontMatter.Date); @@ -199,10 +189,10 @@ Title { // Act var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); - Page page = new(frontMatter, siteDefault); + Page page = new(frontMatter, site); // Act - siteDefault.PostProcessPage(page); + site.PostProcessPage(page); // Asset Assert.Equal(2, page.TagsReference.Count); @@ -281,24 +271,24 @@ Title [Fact] public void SiteParams_ShouldThrowExceptionWhenTypeIsNull() { - Assert.Throws(() => parser.ParseParams(siteDefault, null!, siteContentCONST)); + Assert.Throws(() => parser.ParseParams(site, null!, siteContentCONST)); } [Fact] public void SiteParams_ShouldHandleEmptyContent() { - parser.ParseParams(siteDefault, typeof(Site), string.Empty); - Assert.Empty(siteDefault.Params); + parser.ParseParams(site, typeof(Site), string.Empty); + Assert.Empty(site.Params); } [Fact] public void SiteParams_ShouldPopulateParamsWithExtraFields() { - parser.ParseParams(siteDefault, typeof(Site), siteContentCONST); - Assert.NotEmpty(siteDefault.Params); - Assert.True(siteDefault.Params.ContainsKey("customParam")); - Assert.Equal("Custom Value", siteDefault.Params["customParam"]); - Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)siteDefault.Params["NestedData"])["Level2"]); - Assert.Equal("Test", ((siteDefault.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); + parser.ParseParams(site, typeof(Site), siteContentCONST); + Assert.NotEmpty(site.Params); + Assert.True(site.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", site.Params["customParam"]); + Assert.Equal(new[] { "Test", "Real Data" }, ((Dictionary)site.Params["NestedData"])["Level2"]); + Assert.Equal("Test", ((site.Params["NestedData"] as Dictionary)?["Level2"] as List)?[0]); } } diff --git a/test/ProgramTest.cs b/test/ProgramTest.cs index 13d1a2a..080c54d 100644 --- a/test/ProgramTest.cs +++ b/test/ProgramTest.cs @@ -1,4 +1,4 @@ -using Moq; +using NSubstitute; using Serilog.Events; using SuCoS; using Xunit; @@ -23,13 +23,13 @@ public class ProgramTests : TestSetup public void OutputLogo_Should_LogHelloWorld() { // Arrange - var program = new Program(loggerMock.Object); + var program = new Program(loggerMock); // Act program.OutputLogo(); program.OutputWelcome(); // Assert - loggerMock.Verify(x => x.Information(Program.helloWorld), Times.Once); + loggerMock.Received(1).Information(Program.helloWorld); } -} \ No newline at end of file +} diff --git a/test/TestSetup.cs b/test/TestSetup.cs index d0b1908..3549674 100644 --- a/test/TestSetup.cs +++ b/test/TestSetup.cs @@ -1,5 +1,5 @@ using Serilog; -using Moq; +using NSubstitute; using SuCoS.Models; using SuCoS.Models.CommandLineOptions; using SuCoS.Parser; @@ -10,7 +10,7 @@ public class TestSetup protected const string titleCONST = "Test Title"; protected const string sourcePathCONST = "/path/to/file.md"; protected readonly DateTime todayDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); - protected readonly DateTime futureDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); + protected readonly DateTime futureDate = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); protected const string testSitePathCONST01 = ".TestSites/01"; protected const string testSitePathCONST02 = ".TestSites/02-have-index"; @@ -22,10 +22,10 @@ public class TestSetup protected const string testSitePathCONST08 = ".TestSites/08-theme-html"; protected readonly IFrontMatterParser frontMatterParser = new YAMLParser(); - protected readonly Mock generateOptionsMock = new(); - protected readonly Mock siteSettingsMock = new(); - protected readonly Mock loggerMock = new(); - protected readonly Mock systemClockMock = new(); + protected readonly IGenerateOptions generateOptionsMock = Substitute.For(); + protected readonly SiteSettings siteSettingsMock = Substitute.For(); + protected readonly ILogger loggerMock = Substitute.For(); + protected readonly ISystemClock systemClockMock = Substitute.For(); protected readonly IFrontMatter frontMatterMock = new FrontMatter() { Title = titleCONST, @@ -40,7 +40,7 @@ public class TestSetup public TestSetup() { - systemClockMock.Setup(c => c.Now).Returns(todayDate); - site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); + systemClockMock.Now.Returns(todayDate); + site = new Site(generateOptionsMock, siteSettingsMock, frontMatterParser, loggerMock, systemClockMock); } } \ No newline at end of file diff --git a/test/test.csproj b/test/test.csproj index 24f5f95..175556a 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -10,7 +10,7 @@ - + -- GitLab From 7baa4630249952df876c972bc198768d2a0ba7d4 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 9 Aug 2023 15:57:24 -0300 Subject: [PATCH 6/9] feat: serve/build the page resource files --- source/BaseGeneratorCommand.cs | 1 - source/BuildCommand.cs | 53 +++++++++++-------- source/Models/IOutput.cs | 12 +++++ source/Models/IPage.cs | 10 +--- source/Models/IResource.cs | 13 +++++ source/Models/ISite.cs | 4 +- source/Models/Page.cs | 42 ++++++++++++--- source/Models/Resource.cs | 11 +++- source/Models/Site.cs | 47 ++++++++-------- .../ServerHandlers/RegisteredPageRequest.cs | 27 +++++++--- test/Models/PageTests.cs | 6 +-- test/Models/SiteTests.cs | 20 +++---- 12 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 source/Models/IOutput.cs create mode 100644 source/Models/IResource.cs diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index dcafd2e..d11b59f 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -121,5 +121,4 @@ public abstract class BaseGeneratorCommand } return false; } - } diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 64db956..92cceca 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; using SuCoS.Models.CommandLineOptions; namespace SuCoS; @@ -45,30 +46,40 @@ public class BuildCommand : BaseGeneratorCommand // Print each page var pagesCreated = 0; // counter to keep track of the number of pages created - _ = Parallel.ForEach(site.PagesReferences, pair => + _ = Parallel.ForEach(site.OutputReferences, pair => { - var (url, page) = pair; - var result = page.CompleteContent; + var (url, output) = pair; - var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/'); + if (output is IPage page) + { + var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/'); - // Generate the output path - var outputAbsolutePath = Path.Combine(options.Output, path); + // Generate the output path + var outputAbsolutePath = Path.Combine(options.Output, path); - var outputDirectory = Path.GetDirectoryName(outputAbsolutePath); - if (!Directory.Exists(outputDirectory)) - { - _ = Directory.CreateDirectory(outputDirectory!); - } + var outputDirectory = Path.GetDirectoryName(outputAbsolutePath); + if (!Directory.Exists(outputDirectory)) + { + _ = Directory.CreateDirectory(outputDirectory!); + } - // Save the processed output to the final file - File.WriteAllText(outputAbsolutePath, result); + // Save the processed output to the final file + var result = page.CompleteContent; + File.WriteAllText(outputAbsolutePath, result); - // Log - logger.Debug("Page created: {Permalink}", outputAbsolutePath); + // Log + logger.Debug("Page created: {Permalink}", outputAbsolutePath); - // Use interlocked to safely increment the counter in a multi-threaded environment - _ = Interlocked.Increment(ref pagesCreated); + // Use interlocked to safely increment the counter in a multi-threaded environment + _ = Interlocked.Increment(ref pagesCreated); + } + else if (output is IResource resource) + { + var outputAbsolutePath = Path.Combine(options.Output, resource.Permalink!.TrimStart('/')); + + // Copy the file to the output folder + File.Copy(resource.SourceFullPath, outputAbsolutePath, overwrite: true); + } }); // Stop the stopwatch @@ -94,16 +105,16 @@ public class BuildCommand : BaseGeneratorCommand // Get all files in the source folder var files = Directory.GetFiles(source); - foreach (var file in files) + foreach (var fileFullPath in files) { // Get the filename from the full path - var fileName = Path.GetFileName(file); + var fileName = Path.GetFileName(fileFullPath); // Create the destination path by combining the output folder and the filename - var destinationPath = Path.Combine(output, fileName); + var destinationFullPath = Path.Combine(output, fileName); // Copy the file to the output folder - File.Copy(file, destinationPath, overwrite: true); + File.Copy(fileFullPath, destinationFullPath, overwrite: true); } } } diff --git a/source/Models/IOutput.cs b/source/Models/IOutput.cs new file mode 100644 index 0000000..a213731 --- /dev/null +++ b/source/Models/IOutput.cs @@ -0,0 +1,12 @@ +namespace SuCoS.Models; + +/// +/// Page or Resources (files) that will be considered as output. +/// +public interface IOutput +{ + /// + /// The URL for the content. + /// + public string? Permalink { get; set; } +} diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs index ab26f47..cd300c5 100644 --- a/source/Models/IPage.cs +++ b/source/Models/IPage.cs @@ -10,7 +10,7 @@ namespace SuCoS.Models; /// /// Each page data created from source files or from the system. /// -public interface IPage : IFrontMatter +public interface IPage : IFrontMatter, IOutput { /// new Kind Kind { get; set; } @@ -32,11 +32,6 @@ public interface IPage : IFrontMatter /// 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. @@ -107,7 +102,6 @@ public interface IPage : IFrontMatter /// The processed output file content. public string CompleteContent { get; } - /// /// Other content that mention this content. /// Used to create the tags list and Related Posts section. @@ -122,7 +116,7 @@ public interface IPage : IFrontMatter /// /// Get all URLs related to this content. /// - public List Urls { get; } + public Dictionary AllOutputURLs { get; } /// /// Gets the Permalink path for the file. diff --git a/source/Models/IResource.cs b/source/Models/IResource.cs new file mode 100644 index 0000000..028f1ad --- /dev/null +++ b/source/Models/IResource.cs @@ -0,0 +1,13 @@ +namespace SuCoS.Models; + +/// +/// Page resources. All files that accompany a page. +/// +public interface IResource : IFile, IOutput +{ + /// + public string? Title { get; set; } + + /// + public string? FileName { get; set; } +} diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs index 180ccae..5c96b67 100644 --- a/source/Models/ISite.cs +++ b/source/Models/ISite.cs @@ -60,12 +60,12 @@ public interface ISite : IParams /// /// List of all pages, including generated, by their permalink. /// - public ConcurrentDictionary PagesReferences { get; } + public ConcurrentDictionary OutputReferences { get; } /// /// List of pages from the content folder. /// - public List RegularPages { get; } + public IEnumerable RegularPages { get; } /// /// The page of the home page; diff --git a/source/Models/Page.cs b/source/Models/Page.cs index e0f34d0..e167770 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -194,7 +194,11 @@ public class Page : IPage pagesCached ??= new(); foreach (var permalink in PagesReferences) { - pagesCached.Add(Site.PagesReferences[permalink]); + var page = Site.OutputReferences[permalink] as IPage; + if (page is not null) + { + pagesCached.Add(page); + } } return pagesCached; } @@ -217,26 +221,44 @@ public class Page : IPage /// /// Get all URLs related to this content. /// - public List Urls + public Dictionary AllOutputURLs { get { - var urls = new List(); + var urls = new Dictionary(); + if (Permalink is not null) { - urls.Add(Permalink); + urls.Add(Permalink, this); } if (AliasesProcessed is not null) { - urls.AddRange(from aliases in AliasesProcessed - select aliases); + foreach (var alias in AliasesProcessed) + { + if (!urls.ContainsKey(alias)) + { + urls.Add(alias, this); + } + } + } + + if (Resources is not null) + { + foreach (var resource in Resources) + { + if (resource.Permalink is not null && !urls.ContainsKey(resource.Permalink)) + { + urls.Add(resource.Permalink, resource); + } + } } return urls; } } + /// /// The markdown content. /// @@ -363,10 +385,14 @@ endif (BundleType == BundleType.leaf || !file.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) ); - foreach (var resourceFile in resourceFiles) + foreach (var resourceFilename in resourceFiles) { Resources ??= new(); - Resources.Add(new Resource(resourceFile)); + var resource = new Resource(resourceFilename) + { + Permalink = Path.Combine(Permalink!, Path.GetFileName(resourceFilename)) + }; + Resources.Add(resource); } } catch diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index 1f82889..da3b40f 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -3,13 +3,22 @@ namespace SuCoS.Models; /// /// Page resources. All files that accompany a page. /// -public class Resource : IFile +public class Resource : IResource { + /// + public string? Title { get; set; } + + /// + public string? FileName { get; set; } + /// public string SourceFullPath { get; set; } /// public string? SourceRelativePath => throw new System.NotImplementedException(); + + /// + public string? Permalink { get; set; } /// /// Default constructor. diff --git a/source/Models/Site.cs b/source/Models/Site.cs index e2b6b1f..eb71d04 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -91,9 +91,10 @@ public class Site : ISite { get { - pagesCache ??= PagesReferences.Values - .OrderBy(page => -page.Weight) - .ToList(); + pagesCache ??= OutputReferences.Values + .Where(output => output is IPage page) + .Select(output => (output as IPage)!) + .OrderBy(page => -page.Weight); return pagesCache!; } } @@ -101,20 +102,19 @@ public class Site : ISite /// /// List of all pages, including generated, by their permalink. /// - public ConcurrentDictionary PagesReferences { get; } = new(); + public ConcurrentDictionary OutputReferences { get; } = new(); /// /// List of pages from the content folder. /// - public List RegularPages + public IEnumerable RegularPages { get { - regularPagesCache ??= PagesReferences - .Where(pair => pair.Value.IsPage && pair.Key == pair.Value.Permalink) - .Select(pair => pair.Value) - .OrderBy(page => -page.Weight) - .ToList(); + regularPagesCache ??= OutputReferences + .Where(pair => pair.Value is IPage page && page.IsPage && pair.Key == page.Permalink) + .Select(pair => (pair.Value as IPage)!) + .OrderBy(page => -page.Weight); return regularPagesCache; } } @@ -163,9 +163,9 @@ public class Site : ISite /// private readonly IFrontMatterParser frontMatterParser; - private List? pagesCache; + private IEnumerable? pagesCache; - private List? regularPagesCache; + private IEnumerable? regularPagesCache; private readonly SiteSettings settings; @@ -203,7 +203,7 @@ public class Site : ISite public void ResetCache() { CacheManager.ResetCache(); - PagesReferences.Clear(); + OutputReferences.Clear(); } /// @@ -323,11 +323,11 @@ public class Site : ISite if (level == 0) { - PagesReferences.TryRemove(page!.Permalink!, out _); + OutputReferences.TryRemove(page!.Permalink!, out _); page.Permalink = "/"; page.Kind = Kind.index; - PagesReferences.GetOrAdd(page.Permalink, page); + OutputReferences.GetOrAdd(page.Permalink, page); Home = page; } else @@ -391,31 +391,34 @@ public class Site : ISite page.Permalink = page.CreatePermalink(); lock (syncLockPostProcess) { - if (!PagesReferences.TryGetValue(page.Permalink, out var old) || overwrite) + if (!OutputReferences.TryGetValue(page.Permalink, out var oldOutput) || overwrite) { page.PostProcess(); // Replace the old page with the newly created one - if (old?.PagesReferences is not null) + if (oldOutput is IPage oldpage && oldpage?.PagesReferences is not null) { - foreach (var pageOld in old.PagesReferences) + foreach (var pageOld in oldpage.PagesReferences) { page.PagesReferences.Add(pageOld); } } // Register the page for all urls - foreach (var url in page.Urls) + foreach (var pageOutput in page.AllOutputURLs) { - PagesReferences.TryAdd(url, page); + OutputReferences.TryAdd(pageOutput.Key, pageOutput.Value); } } } if (!string.IsNullOrEmpty(page.Section) - && PagesReferences.TryGetValue('/' + page.Section!, out var section)) + && OutputReferences.TryGetValue('/' + page.Section!, out var output)) { - section.PagesReferences.Add(page.Permalink!); + if (output is IPage section) + { + section.PagesReferences.Add(page.Permalink!); + } } } diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index 44a422e..f932623 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -30,7 +30,7 @@ public class RegisteredPageRequest : IServerHandlers { throw new ArgumentNullException(nameof(requestPath)); } - return site.PagesReferences.TryGetValue(requestPath, out _); + return site.OutputReferences.TryGetValue(requestPath, out _); } /// @@ -40,11 +40,26 @@ public class RegisteredPageRequest : IServerHandlers { throw new ArgumentNullException(nameof(context)); } - site.PagesReferences.TryGetValue(requestPath, out var page); - var content = page!.CompleteContent; - content = InjectReloadScript(content); - await context.Response.WriteAsync(content); - return "dict"; + site.OutputReferences.TryGetValue(requestPath, out var output); + + if (output is IPage page) + { + var content = page.CompleteContent; + content = InjectReloadScript(content); + await context.Response.WriteAsync(content); + return "dict"; + } + else if (output is IResource resource) + { + context.Response.ContentType = resource.MimeType; + await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await fileStream.CopyToAsync(context.Response.Body); + return "resource"; + } + else + { + return "404"; + } } /// diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index bf5da47..a22473b 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -63,7 +63,7 @@ word03 word04 word05 6 7 eight Assert.Null(page.ExpiryDate); Assert.Null(page.AliasesProcessed); Assert.Null(page.Permalink); - Assert.Empty(page.Urls); + Assert.Empty(page.AllURLs); Assert.Equal(string.Empty, page.RawContent); Assert.Empty(page.TagsReference); Assert.Empty(page.PagesReferences); @@ -88,8 +88,8 @@ word03 word04 word05 6 7 eight site.PostProcessPage(page); // Assert - Assert.Equal(3, site.PagesReferences.Count); - site.PagesReferences.TryGetValue(url, out var pageOther); + Assert.Equal(3, site.OutputReferences.Count); + site.OutputReferences.TryGetValue(url, out var pageOther); Assert.NotNull(pageOther); Assert.Same(page, pageOther); } diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index 8fce43a..b8bdfbe 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -45,7 +45,7 @@ public class SiteTests : TestSetup // Assert Assert.NotNull(site.Home); Assert.True(site.Home.IsHome); - Assert.Single(site.PagesReferences.Values.Where(page => page.IsHome)); + Assert.Single(site.OutputReferences.Values.Where(page => page.IsHome)); } [Theory] @@ -64,7 +64,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.PagesReferences.Values.Where(page => page.IsSection).Count()); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(page => page.IsSection).Count()); } [Theory] @@ -84,7 +84,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.PagesReferences.Count); + Assert.Equal(expectedQuantity, site.OutputReferences.Count); } [Theory] @@ -104,7 +104,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.PagesReferences.Values.Where(page => page.IsPage).Count()); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(page => page.IsPage).Count()); } [Fact] @@ -154,7 +154,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue("/tags", out var tagSectionPage); + site.OutputReferences.TryGetValue("/tags", out var tagSectionPage); Assert.NotNull(tagSectionPage); Assert.Equal(2, tagSectionPage.Pages.Count()); Assert.Empty(tagSectionPage.RegularPages); @@ -176,7 +176,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue("/tags/tag1", out var page); + site.OutputReferences.TryGetValue("/tags/tag1", out var page); Assert.NotNull(page); Assert.Equal(10, page.Pages.Count()); Assert.Equal(10, page.RegularPages.Count()); @@ -200,7 +200,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var page); Assert.NotNull(page); Assert.Equal(expectedContent, page.Content); Assert.Equal(page.ContentPreRendered, page.Content); @@ -234,7 +234,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var page); Assert.NotNull(page); Assert.Equal(expectedContentPreRendered, page.ContentPreRendered); Assert.Equal(expectedContent, page.Content); @@ -259,7 +259,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var page); Assert.NotNull(page); Assert.Equal(string.Empty, page.Content); Assert.Equal(string.Empty, page.CompleteContent); @@ -298,7 +298,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var page); Assert.NotNull(page); Assert.Equal(expectedContentPreRendered, page.ContentPreRendered); Assert.Equal(expectedContent, page.Content); -- GitLab From 96e628b7fdc81665f62a6683167ad74e2940be18 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 9 Aug 2023 16:16:30 -0300 Subject: [PATCH 7/9] fix: tests fixed --- test/Models/PageTests.cs | 2 +- test/Models/SiteTests.cs | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index a22473b..30589d5 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -63,7 +63,7 @@ word03 word04 word05 6 7 eight Assert.Null(page.ExpiryDate); Assert.Null(page.AliasesProcessed); Assert.Null(page.Permalink); - Assert.Empty(page.AllURLs); + Assert.Empty(page.AllOutputURLs); Assert.Equal(string.Empty, page.RawContent); Assert.Empty(page.TagsReference); Assert.Empty(page.PagesReferences); diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index b8bdfbe..01ac6a0 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -1,5 +1,6 @@ using Xunit; using SuCoS.Models.CommandLineOptions; +using SuCoS.Models; namespace Test.Models; @@ -45,7 +46,7 @@ public class SiteTests : TestSetup // Assert Assert.NotNull(site.Home); Assert.True(site.Home.IsHome); - Assert.Single(site.OutputReferences.Values.Where(page => page.IsHome)); + Assert.Single(site.OutputReferences.Values.Where(output => output is IPage page && page.IsHome)); } [Theory] @@ -64,7 +65,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(page => page.IsSection).Count()); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(output => output is IPage page && page.IsSection).Count()); } [Theory] @@ -84,7 +85,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.OutputReferences.Count); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(output => output is IPage page).Count()); } [Theory] @@ -104,7 +105,7 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(page => page.IsPage).Count()); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(output => output is IPage page && page.IsPage).Count()); } [Fact] @@ -154,7 +155,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue("/tags", out var tagSectionPage); + site.OutputReferences.TryGetValue("/tags", out var output); + var tagSectionPage = output as IPage; Assert.NotNull(tagSectionPage); Assert.Equal(2, tagSectionPage.Pages.Count()); Assert.Empty(tagSectionPage.RegularPages); @@ -176,7 +178,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue("/tags/tag1", out var page); + site.OutputReferences.TryGetValue("/tags/tag1", out var output); + var page = output as IPage; Assert.NotNull(page); Assert.Equal(10, page.Pages.Count()); Assert.Equal(10, page.RegularPages.Count()); @@ -200,7 +203,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var output); + var page = output as IPage; Assert.NotNull(page); Assert.Equal(expectedContent, page.Content); Assert.Equal(page.ContentPreRendered, page.Content); @@ -234,7 +238,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var output); + var page = output as IPage; Assert.NotNull(page); Assert.Equal(expectedContentPreRendered, page.ContentPreRendered); Assert.Equal(expectedContent, page.Content); @@ -259,7 +264,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var output); + var page = output as IPage; Assert.NotNull(page); Assert.Equal(string.Empty, page.Content); Assert.Equal(string.Empty, page.CompleteContent); @@ -298,7 +304,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.OutputReferences.TryGetValue(url, out var page); + site.OutputReferences.TryGetValue(url, out var output); + var page = output as IPage; Assert.NotNull(page); Assert.Equal(expectedContentPreRendered, page.ContentPreRendered); Assert.Equal(expectedContent, page.Content); -- GitLab From 15b257af6407a2988b17a2867b43eb21cc87b1a1 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Thu, 10 Aug 2023 09:50:51 -0300 Subject: [PATCH 8/9] refactor: RegisteredPageResourceRequest --- source/ServeCommand.cs | 3 +- .../ServerHandlers/RegisteredPageRequest.cs | 14 +--- .../RegisteredPageResourceRequest.cs | 81 +++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 source/ServerHandlers/RegisteredPageResourceRequest.cs diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 2b8cca7..eba8335 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -105,7 +105,8 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable new PingRequests(), new StaticFileRequest(site.SourceStaticPath, false), new StaticFileRequest(site.SourceThemeStaticPath, true), - new RegisteredPageRequest(site) + new RegisteredPageRequest(site), + new RegisteredPageResourceRequest(site) }; host = new WebHostBuilder() diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index f932623..b98870c 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -30,7 +30,7 @@ public class RegisteredPageRequest : IServerHandlers { throw new ArgumentNullException(nameof(requestPath)); } - return site.OutputReferences.TryGetValue(requestPath, out _); + return site.OutputReferences.TryGetValue(requestPath, out var item) && item is IPage _; } /// @@ -40,22 +40,14 @@ public class RegisteredPageRequest : IServerHandlers { throw new ArgumentNullException(nameof(context)); } - site.OutputReferences.TryGetValue(requestPath, out var output); - - if (output is IPage page) + + if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IPage page) { var content = page.CompleteContent; content = InjectReloadScript(content); await context.Response.WriteAsync(content); return "dict"; } - else if (output is IResource resource) - { - context.Response.ContentType = resource.MimeType; - await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); - return "resource"; - } else { return "404"; diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs new file mode 100644 index 0000000..595c02e --- /dev/null +++ b/source/ServerHandlers/RegisteredPageResourceRequest.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using SuCoS.Models; + +namespace SuCoS.ServerHandlers; + +/// +/// Return the server startup timestamp as the response +/// +public class RegisteredPageResourceRequest : IServerHandlers +{ + readonly ISite site; + + /// + /// Constructor + /// + /// + public RegisteredPageResourceRequest(ISite site) + { + this.site = site; + } + + /// + public bool Check(string requestPath) + { + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + return site.OutputReferences.TryGetValue(requestPath, out var item) && item is IResource _; + } + + /// + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IResource resource) + { + context.Response.ContentType = resource.MimeType; + await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await fileStream.CopyToAsync(context.Response.Body); + return "resource"; + } + else + { + return "404"; + } + } + + /// + /// Injects a reload script into the provided content. + /// The script is read from a JavaScript file and injected before the closing "body" tag. + /// + /// The content to inject the reload script into. + /// The content with the reload script injected. + private string InjectReloadScript(string content) + { + // Read the content of the JavaScript file + string scriptContent; + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream("SuCoS.wwwroot.js.reload.js") + ?? throw new FileNotFoundException("Could not find the embedded JavaScript resource."); + using var reader = new StreamReader(stream); + scriptContent = reader.ReadToEnd(); + + // Inject the JavaScript content + var reloadScript = $""; + + const string bodyClosingTag = ""; + content = content.Replace(bodyClosingTag, $"{reloadScript}{bodyClosingTag}", StringComparison.InvariantCulture); + + return content; + } +} -- GitLab From 7a551844ee6ebc0a3c311d745541b6461ac09d77 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Thu, 10 Aug 2023 15:24:21 -0300 Subject: [PATCH 9/9] feat: resource front matter definitions --- source/Models/FrontMatter.cs | 5 +- source/Models/FrontMatterResources.cs | 30 ++++++++++++ source/Models/IFrontMatter.cs | 5 ++ source/Models/IFrontMatterResources.cs | 29 ++++++++++++ source/Models/IResource.cs | 2 +- source/Models/Page.cs | 65 +++++++++++++++++++++++++- source/Models/Resource.cs | 5 ++ source/SuCoS.csproj | 1 + 8 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 source/Models/FrontMatterResources.cs create mode 100644 source/Models/IFrontMatterResources.cs diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 6eed010..a3d82f7 100644 --- a/source/Models/FrontMatter.cs +++ b/source/Models/FrontMatter.cs @@ -49,6 +49,9 @@ public class FrontMatter : IFrontMatter /// public List? Tags { get; init; } + /// + public List? ResourceDefinitions { get; set; } + /// [YamlIgnore] public string RawContent { get; set; } = string.Empty; @@ -64,7 +67,7 @@ public class FrontMatter : IFrontMatter /// [YamlIgnore] public string SourceFullPath { get; set; } - + /// [YamlIgnore] public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); diff --git a/source/Models/FrontMatterResources.cs b/source/Models/FrontMatterResources.cs new file mode 100644 index 0000000..440d6df --- /dev/null +++ b/source/Models/FrontMatterResources.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Microsoft.Extensions.FileSystemGlobbing; + +namespace SuCoS.Models; + +/// +/// Basic structure needed to generate user content and system content +/// +public class FrontMatterResources : IFrontMatterResources +{ + /// + public string Src { get; set; } = string.Empty; + + /// + public string? Title { get; set; } + + /// + public string? Name { get; set; } + + /// + public Dictionary Params { get; set; } = new(); + + /// + public Matcher? GlobMatcher { get; set; } + + /// + /// Constructor + /// + public FrontMatterResources() { } +} diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs index 35bede2..0bc3699 100644 --- a/source/Models/IFrontMatter.cs +++ b/source/Models/IFrontMatter.cs @@ -96,6 +96,11 @@ public interface IFrontMatter : IParams, IFile /// List? Tags { get; } + /// + /// List of resource definitions. + /// + List? ResourceDefinitions { get; } + /// /// Raw content from the Markdown file, bellow the front matter. /// diff --git a/source/Models/IFrontMatterResources.cs b/source/Models/IFrontMatterResources.cs new file mode 100644 index 0000000..315c703 --- /dev/null +++ b/source/Models/IFrontMatterResources.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.FileSystemGlobbing; + +namespace SuCoS.Models; + +/// +/// Basic structure needed to generate user content and system content +/// +public interface IFrontMatterResources : IParams +{ + /// + /// The resource filename search + /// + string Src { get; set; } + + /// + /// The resource Title. + /// + string? Title { get; set; } + + /// + /// The resource file name. + /// + string? Name { get; set; } + + /// + /// Glob matcher that will parse the Src. + /// + public Matcher? GlobMatcher { get; set; } +} diff --git a/source/Models/IResource.cs b/source/Models/IResource.cs index 028f1ad..003191f 100644 --- a/source/Models/IResource.cs +++ b/source/Models/IResource.cs @@ -3,7 +3,7 @@ namespace SuCoS.Models; /// /// Page resources. All files that accompany a page. /// -public interface IResource : IFile, IOutput +public interface IResource : IFile, IOutput, IParams { /// public string? Title { get; set; } diff --git a/source/Models/Page.cs b/source/Models/Page.cs index e167770..5de36df 100644 --- a/source/Models/Page.cs +++ b/source/Models/Page.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Fluid; using Markdig; +using Microsoft.Extensions.FileSystemGlobbing; using SuCoS.Helpers; namespace SuCoS.Models; @@ -54,6 +55,9 @@ public class Page : IPage /// public List? Tags => frontMatter.Tags; + /// + public List? ResourceDefinitions => frontMatter.ResourceDefinitions; + /// public string RawContent => frontMatter.RawContent; @@ -372,6 +376,20 @@ endif ScanForResources(); } + private int counterInternal = 0; + private bool counterInternalLock; + private int counter + { + get + { + if (!counterInternalLock) + { + counterInternalLock = true; + } + return counterInternal; + } + } + private void ScanForResources() { if (string.IsNullOrEmpty(SourceFullPathDirectory)) return; @@ -388,9 +406,54 @@ endif foreach (var resourceFilename in resourceFiles) { Resources ??= new(); + var filenameOriginal = Path.GetFileName(resourceFilename); + var filename = filenameOriginal; + var extention = Path.GetExtension(resourceFilename); + var title = filename; + Dictionary resourceParams = new(); + + if (ResourceDefinitions is not null) + { + if (counterInternalLock) + { + counterInternalLock = false; + ++counterInternal; + } + foreach (var resourceDefinition in ResourceDefinitions) + { + resourceDefinition.GlobMatcher ??= new(); + resourceDefinition.GlobMatcher.AddInclude(resourceDefinition.Src); + var file = new InMemoryDirectoryInfo("./", new[] { filenameOriginal }); + if (resourceDefinition.GlobMatcher.Execute(file).HasMatches) + { + if (Site.FluidParser.TryParse(resourceDefinition.Name, out var templateFileName, out var errorFileName)) + { + var context = new TemplateContext(Site.TemplateOptions) + .SetValue("page", this) + .SetValue("site", Site) + .SetValue("counter", counter); + filename = templateFileName.Render(context); + } + if (Site.FluidParser.TryParse(resourceDefinition.Title, out var templateTitle, out var errorTitle)) + { + var context = new TemplateContext(Site.TemplateOptions) + .SetValue("page", this) + .SetValue("site", Site) + .SetValue("counter", counter); + title = templateTitle.Render(context); + } + resourceParams = resourceDefinition.Params ?? new(); + } + } + } + + filename = Path.GetFileNameWithoutExtension(filename) + extention; var resource = new Resource(resourceFilename) { - Permalink = Path.Combine(Permalink!, Path.GetFileName(resourceFilename)) + Title = title, + FileName = filename, + Permalink = Path.Combine(Permalink!, filename), + Params = resourceParams }; Resources.Add(resource); } diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs index da3b40f..66cf808 100644 --- a/source/Models/Resource.cs +++ b/source/Models/Resource.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace SuCoS.Models; /// @@ -20,6 +22,9 @@ public class Resource : IResource /// public string? Permalink { get; set; } + /// + public Dictionary Params { get; set; } = new(); + /// /// Default constructor. /// diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index cea368f..5425482 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -17,6 +17,7 @@ + -- GitLab