diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index fb05427ba98fae2c578330802c3004954afabeb3..d11b59fb40f08371d5e510d54f2786f6f8a4a5e8 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. @@ -121,5 +121,4 @@ internal abstract class BaseGeneratorCommand } return false; } - } diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 9046589481ffd5b4b4485b0553ebf0cef9e6e5eb..92cceca8ac5ebd13e98ba1ef15c5c2dcc840a4b0 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; @@ -10,7 +11,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; @@ -45,30 +46,40 @@ internal 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 @@ internal 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/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 217a4bca5f95bdb5c719c56ae6e9ea35f5f8c99f..25c490e9d3189ab8536a19e97253e13f7c73a54b 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 3ec93a36162efb5aecb3c61526ea39a58948f256..910030ecfad55e478d44566955862ab383f25a31 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 d32782b8c0cfe96683660b0a1a38a42dd7662e25..d26b5c235a6b458734c224bda5ae2ec789618ed7 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 ec4c5a19581917a611aea5077ee73481737e3e3e..46219a6c64fb738048735e8662eb0957ab9e3b43 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 2af9da07d46201c7a1da3c55757ce918d1a2ac90..0b83bfdcfe8e1e8826c6b568f9268cb0b1ac1a69 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/Bundle.cs b/source/Models/Bundle.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a8d7db4105f5d945f26745b79e20c9dee475526 --- /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/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index 86f9748d2e856b773b246e5f18830b962cb75886..3710cdfb993737636f8c0a0ad0759407b387064b 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 8435eafc4d260e4d34e10954bc6cfd5b4d79e5b4..612284367e8e35440385a18b8d3b3b5de36f9d8a 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 8ea518bfffb81d392f720296f19c81ae3fb1ca25..9e2b5c969726d7babb800b916303453a30410b92 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 9c5629b95d5c7b235ddfbf8b601af9c119b5ce63..a3d82f718dbbad9c1a607aefb35083aa645fdc08 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,10 +45,13 @@ internal class FrontMatter : IFrontMatter /// public int Weight { get; init; } = 0; - + /// public List? Tags { get; init; } + /// + public List? ResourceDefinitions { get; set; } + /// [YamlIgnore] public string RawContent { get; set; } = string.Empty; @@ -59,15 +62,19 @@ internal class FrontMatter : IFrontMatter /// [YamlIgnore] - public string? SourcePath { get; set; } + public string? SourceRelativePath { get; set; } + + /// + [YamlIgnore] + public string SourceFullPath { get; set; } /// [YamlIgnore] - public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath); + public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath); /// [YamlIgnore] - public string? SourcePathDirectory => Path.GetDirectoryName(SourcePath); + public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourceRelativePath); /// [YamlIgnore] @@ -81,7 +88,10 @@ internal class FrontMatter : IFrontMatter /// /// Constructor /// - public FrontMatter() { } + public FrontMatter() + { + SourceFullPath = string.Empty; + } /// /// Constructor @@ -91,6 +101,7 @@ internal class FrontMatter : IFrontMatter public FrontMatter(string title, string sourcePath) { Title = title; - SourcePath = sourcePath; + SourceRelativePath = sourcePath; + SourceFullPath = sourcePath; } } diff --git a/source/Models/FrontMatterResources.cs b/source/Models/FrontMatterResources.cs new file mode 100644 index 0000000000000000000000000000000000000000..440d6dfac46b2a046153377a887cf3f4fb0b78e0 --- /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/IFile.cs b/source/Models/IFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad7bed8e3a090aeb14c9fff7549de80598ae4d7b --- /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 8da34d5244c95dbd2760f2814dd3ef116f020282..0bc369979d81302bdfe635a80d7101130dbc41ac 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,16 @@ 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; } + + /// + /// List of resource definitions. + /// + List? ResourceDefinitions { get; } /// /// Raw content from the Markdown file, bellow the front matter. @@ -107,21 +112,6 @@ public interface IFrontMatter : IParams /// Kind Kind { get; } - /// - /// The source filename, without the extension. ;) - /// - public string? SourcePath { get; } - - /// - /// The source filename, without the extension. ;) - /// - string? SourceFileNameWithoutExtension { get; } - - /// - /// The source directory of the file, without the file name. - /// - string? SourcePathDirectory { get; } - /// /// The date to be considered as the publish date. /// diff --git a/source/Models/IFrontMatterResources.cs b/source/Models/IFrontMatterResources.cs new file mode 100644 index 0000000000000000000000000000000000000000..315c703fc6a7f39f95bff3aeeee14ee70f9f0a7a --- /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/IOutput.cs b/source/Models/IOutput.cs new file mode 100644 index 0000000000000000000000000000000000000000..a2137312fc3053e20a9fbc077ff5a4c1beca42a9 --- /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 1f739e5be43d6dd9ed2fa61ca4bc51b574d372cd..cd300c5473fbc44dfd24c1d21f94d0f299a8c3db 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; } @@ -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. @@ -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. @@ -49,6 +44,16 @@ public interface IPage : IFrontMatter /// public IPage? Parent { get; set; } + /// + /// The bundle type of the page. + /// + public BundleType BundleType { get; set; } + + /// + /// Page resources. All files that accompany a page. + /// + public List? Resources { get; set; } + /// /// Plain markdown content, without HTML. /// @@ -97,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. @@ -112,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. @@ -120,4 +124,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/IResource.cs b/source/Models/IResource.cs new file mode 100644 index 0000000000000000000000000000000000000000..003191f9770688120e71badee448a6a25e56c8e4 --- /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, IParams +{ + /// + public string? Title { get; set; } + + /// + public string? FileName { get; set; } +} diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs index 99a833e8df241cf72514e457bbed3642473a24c8..5c96b67bcd3ca7663812167d4c7ec591971d54aa 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; @@ -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/ISystemClock.cs b/source/Models/ISystemClock.cs index f3f9274b03eb4991a05218a6aa26ce2bcd97e90e..bd389003fef397fc66e2864d71bf020b0d8ff043 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 35f0e909b00e7c45d90533375fd7454b02035737..5de36dfede7d84d9334b14c9afb308fe002eab24 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; @@ -12,7 +13,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; @@ -54,6 +55,9 @@ internal class Page : IPage /// public List? Tags => frontMatter.Tags; + /// + public List? ResourceDefinitions => frontMatter.ResourceDefinitions; + /// public string RawContent => frontMatter.RawContent; @@ -65,13 +69,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? SourceFullPathDirectory => frontMatter.SourceFullPathDirectory; /// - public string? SourcePathDirectory => frontMatter.SourcePathDirectory; + public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension; /// public Dictionary Params @@ -85,10 +95,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. @@ -100,9 +109,7 @@ internal class Page : IPage /// public List? AliasesProcessed { get; set; } - /// - /// The URL for the content. - /// + /// public string? Permalink { get; set; } /// @@ -111,12 +118,15 @@ 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; + + /// + public List? Resources { get; set; } + /// /// Plain markdown content, without HTML. /// @@ -172,7 +182,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. @@ -189,7 +198,11 @@ internal 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; } @@ -212,26 +225,44 @@ internal 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. /// @@ -320,6 +351,119 @@ 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 int counterInternal = 0; + private bool counterInternalLock; + private int counter + { + get + { + if (!counterInternalLock) + { + counterInternalLock = true; + } + return counterInternal; + } + } + + 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 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) + { + Title = title, + FileName = filename, + Permalink = Path.Combine(Permalink!, filename), + Params = resourceParams + }; + Resources.Add(resource); + } + } + 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 0000000000000000000000000000000000000000..66cf808245e2a8246559ba6cb4f5cdd048f52f95 --- /dev/null +++ b/source/Models/Resource.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace SuCoS.Models; + +/// +/// Page resources. All files that accompany a page. +/// +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; } + + /// + public Dictionary Params { get; set; } = new(); + + /// + /// Default constructor. + /// + /// + public Resource(string path) + { + SourceFullPath = path; + } +} diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 560163139aa85d7d0463c28e4f647af5ac7fb47c..eb71d04b48853a64a4b6d44ee696dc9f48307601 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; @@ -91,9 +91,10 @@ internal 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 @@ internal 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; } } @@ -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 indexFileUpperConst = "INDEX.MD"; + private const string indexBranchFileConst = "_index.md"; /// /// The synchronization lock object during ProstProcess. @@ -163,9 +163,9 @@ internal class Site : ISite /// private readonly IFrontMatterParser frontMatterParser; - private List? pagesCache; + private IEnumerable? pagesCache; - private List? regularPagesCache; + private IEnumerable? regularPagesCache; private readonly SiteSettings settings; @@ -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(); } @@ -202,7 +203,7 @@ internal class Site : ISite public void ResetCache() { CacheManager.ResetCache(); - PagesReferences.Clear(); + OutputReferences.Clear(); } /// @@ -223,7 +224,7 @@ internal class Site : ISite _ = Parallel.ForEach(markdownFiles, filePath => { - ParseSourceFile(parent, filePath); + ParseSourceFile(filePath, parent); }); var subdirectories = Directory.GetDirectories(directory); @@ -233,67 +234,6 @@ internal class Site : ISite }); } - private void ParseIndexPage(in 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) - { - markdownFiles = markdownFiles.Where(file => file != indexPage).ToArray(); - var page = ParseSourceFile(parent, indexPage); - if (level == 0) - { - PagesReferences.TryRemove(page!.Permalink!, out _); - Home = page; - page.Permalink = "/"; - page.Kind = Kind.index; - PagesReferences.GetOrAdd(page.Permalink, page); - } - else - { - 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) - { - var section = new DirectoryInfo(directory!).Name; - parent = CreateSystemPage(section, section); - } - } - - private IPage? ParseSourceFile(in IPage? parent, in string filePath) - { - Page? page = null; - try - { - var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath) - ?? throw new FormatException($"Error parsing front matter for {filePath}"); - - if (IsValidDate(frontMatter, Options)) - { - page = new(frontMatter, this); - PostProcessPage(page, parent, true); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Error parsing file {file}", filePath); - } - - // Use interlocked to safely increment the counter in a multi-threaded environment - _ = Interlocked.Increment(ref filesParsedToReport); - - return page; - } - /// /// Creates the page for the site index. /// @@ -302,7 +242,7 @@ internal class Site : ISite /// /// /// The created page for the index. - private IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) + public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) { sectionName ??= "section"; var isIndex = string.IsNullOrEmpty(relativePath); @@ -310,7 +250,8 @@ internal class Site : ISite { Kind = isIndex ? Kind.index : Kind.list, Section = isIndex ? "index" : sectionName, - SourcePath = Path.Combine(relativePath, indexFileConst), + SourceRelativePath = Path.Combine(relativePath, indexLeafFileConst), + SourceFullPath = Path.Combine(SourceContentPath, relativePath, indexLeafFileConst), Title = title, Type = isIndex ? "index" : sectionName, URL = relativePath @@ -323,13 +264,16 @@ internal class Site : ISite { IPage? parent = null; // Check if we need to create a section, even - var sections = (frontMatter.SourcePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries); + 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); + var newPage = new Page(frontMatter, this) + { + BundleType = BundleType.branch + }; PostProcessPage(newPage, parent); return newPage; })); @@ -356,6 +300,80 @@ internal class Site : ISite return page; } + private void ParseIndexPage(string? directory, int level, ref IPage? parent, ref string[] markdownFiles) + { + var indexLeafBundlePage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file) == indexLeafFileConst); + + 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 = selectedFile == indexLeafBundlePage ? BundleType.leaf : BundleType.branch; + + // Remove the selected file from markdownFiles + markdownFiles = bundleType == BundleType.leaf + ? new string[] { } + : markdownFiles.Where(file => file != selectedFile).ToArray(); + + page = ParseSourceFile(selectedFile!, parent, bundleType); + if (page is null) return; + + if (level == 0) + { + OutputReferences.TryRemove(page!.Permalink!, out _); + page.Permalink = "/"; + page.Kind = Kind.index; + + OutputReferences.GetOrAdd(page.Permalink, page); + Home = page; + } + else + { + parent = page; + } + } + else if (level == 0) + { + Home = CreateSystemPage(string.Empty, Title); + } + else if (level == 1 && directory is not null) + { + var section = new DirectoryInfo(directory).Name; + parent = CreateSystemPage(section, section); + } + } + + private IPage? ParseSourceFile(in string filePath, in IPage? parent, BundleType bundleType = BundleType.none) + { + Page? page = null; + try + { + var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath) + ?? throw new FormatException($"Error parsing front matter for {filePath}"); + + if (IsValidPage(frontMatter, Options)) + { + page = new(frontMatter, this) + { + BundleType = bundleType + }; + PostProcessPage(page, parent, true); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Error parsing file {file}", filePath); + } + + // Use interlocked to safely increment the counter in a multi-threaded environment + _ = Interlocked.Increment(ref filesParsedToReport); + + return page; + } + /// /// Extra calculation and automatic data for each page. /// @@ -373,46 +391,35 @@ internal 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) { - if (old?.PagesReferences is not null) - { - foreach (var pageOld in old.PagesReferences) - { - page.PagesReferences.Add(pageOld); - } - } + page.PostProcess(); - if (page.Aliases is not null) + // Replace the old page with the newly created one + if (oldOutput is IPage oldpage && oldpage?.PagesReferences is not null) { - page.AliasesProcessed ??= new(); - foreach (var alias in page.Aliases) + foreach (var pageOld in oldpage.PagesReferences) { - page.AliasesProcessed.Add(page.CreatePermalink(alias)); + 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 (page.Tags is not null) + if (!string.IsNullOrEmpty(page.Section) + && OutputReferences.TryGetValue('/' + page.Section!, out var output)) { - foreach (var tagName in page.Tags) + if (output is IPage section) { - CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", page); + section.PagesReferences.Add(page.Permalink!); } } - - if (!string.IsNullOrEmpty(page.Section) - && PagesReferences.TryGetValue('/' + page.Section!, out var section)) - { - section.PagesReferences.Add(page.Permalink!); - } } /// diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs index 5a523508a84501d0f198724cbe0330332e8d1e6d..1c137b33f8075ea3109d474f651e23048f43ea0f 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/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 0f1e023e91960d56cc6f6cd2f6eefebdfaa17863..67a536c412bc3f6965987fdc954f45b5e8662745 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 80b0c5292be382b8d6657a9dd19f91aab1065dfa..b827ce232f372596e92e52482026c0be3d4644b6 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 @@ -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/source/Program.cs b/source/Program.cs index 2a118e39bab66b647f0e40472d568ae5afa54c9a..e42eb4baca832535884fa3907ccd9ba97b9e769d 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 14d19857a8ebc9f4654cb46702c3d8c4671a8651..eba8335bfd4785109d866453d6a841658998c4d5 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; @@ -105,7 +105,8 @@ internal 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/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index c287887468b7785b844495097aed2b5d3e91dcbc..38887009030528fe97c885ebbd6a017a36a71272 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 927d701f480b30cc42f3ed2028ffe7162b323767..4ae76ff54e46b19677ed2efa86d57c4c38202788 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 61feaeacb6e945b77b0538101ae3a4ec48bc5cc6..b98870c6e461eb501f3c9e57a9a8b58b17456c04 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; @@ -26,7 +30,7 @@ internal class RegisteredPageRequest : IServerHandlers { throw new ArgumentNullException(nameof(requestPath)); } - return site.PagesReferences.TryGetValue(requestPath, out _); + return site.OutputReferences.TryGetValue(requestPath, out var item) && item is IPage _; } /// @@ -36,11 +40,18 @@ internal 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"; + + 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 + { + return "404"; + } } /// diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..595c02e27771f3e0c9f2a43cafed5a69421e634a --- /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; + } +} diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index bfa59c451f5d274896a75568741380d8acd62c94..9cf0e9100c9215c940fd635b47c37775a763638c 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) { diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index cea368f312f551b4b9ac5a1f0c87a7e1c372d5d4..5425482532e5e9145fbd3b57cfefe517fa05011a 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Models/FrontMatterTests.cs b/test/Models/FrontMatterTests.cs index b045efb4c4bc0484bca63cfa022612cbf7aece31..dcd4e813a1f7ef6c5a16958ff80f30a718ddc573 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 9e51d147213ac175053570ad8c87463449554fdf..30589d56c90a21b74002f7d844d94645a151fa93 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; @@ -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] @@ -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.AllOutputURLs); Assert.Equal(string.Empty, page.RawContent); Assert.Empty(page.TagsReference); Assert.Empty(page.PagesReferences); @@ -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); @@ -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); } @@ -102,8 +102,8 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, - ExpiryDate = systemClockMock.Object.Now.AddDays(days) + SourceRelativePath = sourcePathCONST, + ExpiryDate = systemClockMock.Now.AddDays(days) }, site); // Assert @@ -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,17 +172,17 @@ 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 }, 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] @@ -193,16 +193,16 @@ word03 word04 word05 6 7 eight var page = new Page(new FrontMatter { Title = titleCONST, - SourcePath = sourcePathCONST, - Date = systemClockMock.Object.Now.AddDays(1) + SourceRelativePath = sourcePathCONST, + 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] @@ -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 83403a1a411a8723814d3aebc573205766ead0aa..01ac6a06c46e68d794ab13399a4155b0756fcc82 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; @@ -24,7 +25,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); } @@ -45,7 +46,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(output => output is IPage page && page.IsHome)); } [Theory] @@ -64,13 +65,14 @@ 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(output => output is IPage page && page.IsSection).Count()); } [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() @@ -83,13 +85,14 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - Assert.Equal(expectedQuantity, site.PagesReferences.Count); + Assert.Equal(expectedQuantity, site.OutputReferences.Values.Where(output => output is IPage page).Count()); } [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() @@ -102,7 +105,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(output => output is IPage page && page.IsPage).Count()); } [Fact] @@ -127,7 +130,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; @@ -152,12 +155,13 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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); - 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); } @@ -174,7 +178,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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()); @@ -198,7 +203,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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); @@ -232,7 +238,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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); @@ -257,7 +264,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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); @@ -296,7 +304,8 @@ public class SiteTests : TestSetup site.ParseAndScanSourceFiles(null); // Assert - site.PagesReferences.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); diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index cd746f052d8ab219f6f9d52495aea8e91506a56b..4787750812841ad92dd135595bb491fbda65174e 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 @@ -52,13 +44,13 @@ 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(); - siteDefault = new Site(generateOptionsMock.Object, siteSettingsMock.Object, parser, loggerMock.Object, systemClockMock.Object); pageContent = @$"--- {pageFrontmaterCONST} --- @@ -89,7 +81,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 +101,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 +116,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 +136,7 @@ Title "; // Asset - Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(filePathCONST, fileContent)); + Assert.Throws(() => parser.ParseFrontmatterAndMarkdown(fileRelativePathCONST, fileFullPathCONST, fileContent)); } [Fact] @@ -167,8 +159,8 @@ Title var page = new Page(new FrontMatter { Title = "Test Title", - SourcePath = "/test.md" - }, siteDefault); + SourceRelativePath = "/test.md" + }, site); // Act parser.ParseParams(page, typeof(Page), pageFrontmaterCONST); @@ -182,11 +174,11 @@ Title public void ParseFrontmatter_ShouldParseContentInSiteFolder() { var date = DateTime.Parse("2023-07-01", CultureInfo.InvariantCulture); - var frontMatter = parser.ParseFrontmatterAndMarkdown("", pageContent); - Page page = new(frontMatter, siteDefault); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); // Act - siteDefault.PostProcessPage(page); + site.PostProcessPage(page); // Asset Assert.Equal(date, frontMatter.Date); @@ -196,11 +188,11 @@ Title public void ParseFrontmatter_ShouldCreateTags() { // Act - var frontMatter = parser.ParseFrontmatterAndMarkdown("", pageContent); - Page page = new(frontMatter, siteDefault); + var frontMatter = parser.ParseFrontmatterAndMarkdown("", "", pageContent); + Page page = new(frontMatter, site); // Act - siteDefault.PostProcessPage(page); + site.PostProcessPage(page); // Asset Assert.Equal(2, page.TagsReference.Count); @@ -209,7 +201,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 +230,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 +257,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); } @@ -279,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 13d1a2a1fdf3d3486463f85adfad9ecfc9ebc29d..080c54dca3b484ec5656284d72782cebb390f46c 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 151f2c4545636d1bacbc6625b987443cab0e6267..354967407eb63fbe8d72fa86aa39e2be90422216 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,14 +22,14 @@ 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, - SourcePath = sourcePathCONST + SourceRelativePath = sourcePathCONST }; protected readonly ISite site; @@ -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 24f5f9580651ee2a46f16ed2df2343165140a965..175556a9f69590245d3c23a21afa3a564a36e5c0 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -10,7 +10,7 @@ - +