diff --git a/source/AssemblyInfo.cs b/source/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..52f99c758e3bb4e1b0f5a3dc95f691d51c897051 --- /dev/null +++ b/source/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("test")] diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index dcafd2ef5fe0cda53f144b243fb47ddbcff3129a..fb05427ba98fae2c578330802c3004954afabeb3 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -14,7 +14,7 @@ namespace SuCoS; /// /// Base class for build and serve commands. /// -public abstract class BaseGeneratorCommand +internal abstract class BaseGeneratorCommand { /// /// The configuration file name. diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 64db95611b7cc6e1061b0943afd4df4ca8895e95..9046589481ffd5b4b4485b0553ebf0cef9e6e5eb 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. /// -public class BuildCommand : BaseGeneratorCommand +internal class BuildCommand : BaseGeneratorCommand { private readonly BuildOptions options; diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 25c490e9d3189ab8536a19e97253e13f7c73a54b..217a4bca5f95bdb5c719c56ae6e9ea35f5f8c99f 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -9,7 +9,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -public static class FileUtils +internal 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 33d31b971e1027163fdc4af683a1cd7e06061085..3ec93a36162efb5aecb3c61526ea39a58948f256 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -13,7 +13,7 @@ namespace SuCoS.Helpers; /// /// Helper methods for scanning files. /// -public static class SiteHelper +internal static class SiteHelper { /// /// Markdig 20+ built-in extensions @@ -41,7 +41,7 @@ public static class SiteHelper } catch { - throw new FormatException("Error reading app config"); + throw new FormatException($"Error reading app config {configFile}"); } var site = new Site(options, siteSettings, frontMatterParser, logger, null); diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs new file mode 100644 index 0000000000000000000000000000000000000000..d32782b8c0cfe96683660b0a1a38a42dd7662e25 --- /dev/null +++ b/source/Helpers/SourceFileWatcher.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; + +/// +/// The FileSystemWatcher object that monitors the source directory for file changes. +/// +internal interface IFileWatcher +{ + /// + /// Starts the file watcher to monitor file changes in the specified source path. + /// + /// The path to the source directory. + /// + /// The created FileSystemWatcher object. + void Start(string SourceAbsolutePath, Action OnSourceFileChanged); + + /// + /// Disposes the file watcher + /// + void Stop(); +} + +/// +/// The FileSystemWatcher object that monitors the source directory for file changes. +/// +internal class SourceFileWatcher : IFileWatcher +{ + /// + /// The FileSystemWatcher object that monitors the source directory for file changes. + /// When a change is detected, this triggers a server restart to ensure the served content + /// remains up-to-date. The FileSystemWatcher is configured with the source directory + /// at construction and starts watching immediately. + /// + private FileSystemWatcher? fileWatcher; + + /// + public void Start(string SourceAbsolutePath, Action OnSourceFileChanged) + { + if (OnSourceFileChanged is null) + { + throw new ArgumentNullException(nameof(OnSourceFileChanged)); + } + + fileWatcher = new FileSystemWatcher + { + Path = SourceAbsolutePath, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + IncludeSubdirectories = true, + EnableRaisingEvents = true + }; + + // Subscribe to the desired events + fileWatcher.Changed += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Created += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Deleted += new FileSystemEventHandler(OnSourceFileChanged.Invoke); + fileWatcher.Renamed += new RenamedEventHandler(OnSourceFileChanged); + } + + /// + public void Stop() + { + fileWatcher?.Dispose(); + } +} diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs index 46219a6c64fb738048735e8662eb0957ab9e3b43..ec4c5a19581917a611aea5077ee73481737e3e3e 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. /// -public class StopwatchReporter +internal class StopwatchReporter { private readonly ILogger logger; private readonly Dictionary stopwatches; diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs index 0b83bfdcfe8e1e8826c6b568f9268cb0b1ac1a69..2af9da07d46201c7a1da3c55757ce918d1a2ac90 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. /// -public static partial class Urlizer +internal static partial class Urlizer { [GeneratedRegex(@"[^a-zA-Z0-9]+")] private static partial Regex UrlizeRegexAlpha(); @@ -68,7 +68,7 @@ public static partial class Urlizer /// Options for the class. /// Basically to force lowercase and to change the replacement character. /// -public class UrlizerOptions +internal class UrlizerOptions { /// /// Force to generate lowercase URLs. diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index 208de262f6eac961e597487e3ef8f55d34ff1389..86f9748d2e856b773b246e5f18830b962cb75886 100644 --- a/source/Models/CommandLineOptions/BuildOptions.cs +++ b/source/Models/CommandLineOptions/BuildOptions.cs @@ -1,9 +1,11 @@ +using System.IO; + namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the build command. /// -public class BuildOptions : GenerateOptions +internal class BuildOptions : GenerateOptions { /// /// The path of the output files. @@ -13,9 +15,11 @@ public class BuildOptions : GenerateOptions /// /// Constructor /// + /// /// - public BuildOptions(string output) + public BuildOptions(string source, string output) { - Output = output; + Source = source; + Output = string.IsNullOrEmpty(output) ? Path.Combine(source, "public") : output; } } diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs index 709691b4f82b3c693badea9a3027d1b76402c4b2..d14eba2b74e03ce739621569b0e6d74a4337930c 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. /// -public class GenerateOptions : IGenerateOptions +internal class GenerateOptions : IGenerateOptions { /// public string Source { get; init; } = "."; diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs index 9e2b5c969726d7babb800b916303453a30410b92..8ea518bfffb81d392f720296f19c81ae3fb1ca25 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. /// -public class ServeOptions : GenerateOptions +internal class ServeOptions : GenerateOptions { } diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs index 854c0460a6269423e128cbc2cf28fb42a44f54e1..8ea9df48bf66a24c26d6b0a09ec24c87779dcd81 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 /// -public class FrontMatter : IFrontMatter +internal class FrontMatter : IFrontMatter { #region IFrontMatter diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs new file mode 100644 index 0000000000000000000000000000000000000000..98ad8105e845e62e5b17f366bb570a7084385f21 --- /dev/null +++ b/source/Models/IPage.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Markdig; +using SuCoS.Helpers; + +namespace SuCoS.Models; + +/// +/// Each page data created from source files or from the system. +/// +public interface IPage : IFrontMatter +{ + /// + new Kind Kind { get; set; } + + /// + /// The source directory of the file. + /// + public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory) + ? null + : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + + /// + /// Point to the site configuration. + /// + public ISite Site { get; } + + /// + /// Secondary URL patterns to be used to create the url. + /// + public List? AliasesProcessed { get; set; } + + /// + /// The URL for the content. + /// + public string? Permalink { get; set; } + + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + public ConcurrentBag PagesReferences { get; } + + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + public IPage? Parent { get; set; } + + /// + /// Plain markdown content, without HTML. + /// + public string Plain => Markdown.ToPlainText(RawContent, SiteHelper.MarkdownPipeline); + + /// + /// A list of tags, if any. + /// + public ConcurrentBag TagsReference { get; } + + /// + /// Just a simple check if the current page is the home page + /// + public bool IsHome => Site.Home == this; + + /// + /// Just a simple check if the current page is a section page + /// + public bool IsSection => Type == "section"; + + /// + /// Just a simple check if the current page is a "page" + /// + public bool IsPage => Kind == Kind.single; + + /// + /// The number of words in the main content + /// + public int WordCount => Plain.Split(nonWords, StringSplitOptions.RemoveEmptyEntries).Length; + + private static readonly char[] nonWords = { ' ', ',', ';', '.', '!', '"', '(', ')', '?', '\n', '\r' }; + + /// + /// The markdown content converted to HTML + /// + public string ContentPreRendered { get; } + + /// + /// The processed content. + /// + public string Content { get; } + + /// + /// Creates the output file by applying the theme templates to the page content. + /// + /// 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. + /// + public IEnumerable Pages { get; } + + /// + /// List of pages from the content folder. + /// + public IEnumerable RegularPages { get; } + + /// + /// Get all URLs related to this content. + /// + public List Urls + { + get; + } + + /// + /// Gets the Permalink path for the file. + /// + /// The URL to consider. If null use the predefined URL + /// The output path. + public string CreatePermalink(string? URLforce = null); +} diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs new file mode 100644 index 0000000000000000000000000000000000000000..a78e9e681a8b705d26a0f7a38b582fcb5ed13d13 --- /dev/null +++ b/source/Models/ISite.cs @@ -0,0 +1,133 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Fluid; +using Serilog; +using SuCoS.Models.CommandLineOptions; + +namespace SuCoS.Models; + +/// +/// The main configuration of the program, primarily extracted from the app.yaml file. +/// +public interface ISite : IParams +{ + /// + /// Command line options + /// + public IGenerateOptions Options { get; set; } + + #region SiteSettings + + /// + /// Site Title. + /// + public string Title { get; } + + /// + /// The appearance of a URL is either ugly or pretty. + /// + public bool UglyURLs { get; } + + #endregion SiteSettings + + /// + /// The path of the content, based on the source path. + /// + public string SourceContentPath { get; } + + /// + /// The path of the static content (that will be copied as is), based on the source path. + /// + public string SourceStaticPath { get; } + + /// + /// The path theme. + /// + public string SourceThemePath { get; } + + /// + /// The path of the static content (that will be copied as is), based on the theme path. + /// + public string SourceThemeStaticPath => Path.Combine(SourceThemePath, "static"); + + /// + /// List of all pages, including generated. + /// + public IEnumerable Pages { get; } + + /// + /// List of all pages, including generated, by their permalink. + /// + public ConcurrentDictionary PagesReferences { get; } + + /// + /// List of pages from the content folder. + /// + public List RegularPages { get; } + + /// + /// The page of the home page; + /// + public IPage? Home { get; } + + /// + /// Manage all caching lists for the site + /// + public SiteCacheManager CacheManager { get; } + + /// + /// The Fluid parser instance. + /// + public FluidParser FluidParser { get; } + + /// + /// The Fluid/Liquid template options. + /// + public TemplateOptions TemplateOptions { get; } + /// + /// The logger instance. + /// + public ILogger Logger { get; } + + /// + /// Resets the template cache to force a reload of all templates. + /// + public void ResetCache(); + + /// + /// Search recursively for all markdown files in the content folder, then + /// parse their content for front matter meta data and markdown. + /// + /// Folder to scan + /// Folder recursive level + /// Page of the upper directory + /// + public void ParseAndScanSourceFiles(string? directory, int level = 0, IPage? parent = null); + + /// + /// Extra calculation and automatic data for each page. + /// + /// The given page to be processed + /// The parent page, if any + /// + public void PostProcessPage(in IPage page, IPage? parent = null, bool overwrite = false); + + /// + /// Check if the page have a publishing date from the past. + /// + /// Page or front matter + /// options + /// + public bool IsValidDate(in IFrontMatter frontMatter, IGenerateOptions? options); + + /// + /// Check if the page is expired + /// + public bool IsDateExpired(in IFrontMatter frontMatter); + + /// + /// Check if the page is publishable + /// + public bool IsDatePublishable(in IFrontMatter frontMatter); +} \ No newline at end of file diff --git a/source/Models/ISystemClock.cs b/source/Models/ISystemClock.cs index bd389003fef397fc66e2864d71bf020b0d8ff043..f3f9274b03eb4991a05218a6aa26ce2bcd97e90e 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. /// -public class SystemClock : ISystemClock +internal class SystemClock : ISystemClock { /// /// Gets the current local date and time. diff --git a/source/Models/Page.cs b/source/Models/Page.cs index fc793a40a5cff7bf7b549e87f7bc0b655185d822..70f360ee2dea312c4871ea552ef331dc52320ccf 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. /// -public class Page : IFrontMatter +internal class Page : IPage { private readonly IFrontMatter frontMatter; @@ -90,7 +90,7 @@ public class Page : IFrontMatter /// /// Point to the site configuration. /// - public Site Site { get; } + public ISite Site { get; } /// /// Secondary URL patterns to be used to create the url. @@ -112,7 +112,7 @@ public class Page : IFrontMatter /// Other content that mention this content. /// Used to create the tags list and Related Posts section. /// - public Page? Parent { get; set; } + public IPage? Parent { get; set; } /// /// Plain markdown content, without HTML. @@ -122,7 +122,7 @@ public class Page : IFrontMatter /// /// A list of tags, if any. /// - public ConcurrentBag TagsReference { get; } = new(); + public ConcurrentBag TagsReference { get; } = new(); /// /// Just a simple check if the current page is the home page @@ -174,7 +174,7 @@ public class Page : IFrontMatter /// Other content that mention this content. /// Used to create the tags list and Related Posts section. /// - public IEnumerable Pages + public IEnumerable Pages { get { @@ -195,7 +195,7 @@ public class Page : IFrontMatter /// /// List of pages from the content folder. /// - public IEnumerable RegularPages + public IEnumerable RegularPages { get { @@ -262,14 +262,14 @@ echo page.SourceFileNameWithoutExtension endif -%}"; - private List? regularPagesCache; + private List? regularPagesCache; - private List? pagesCached { get; set; } + private List? pagesCached { get; set; } /// /// Constructor /// - public Page(in IFrontMatter frontMatter, in Site site) + public Page(in IFrontMatter frontMatter, in ISite site) { this.frontMatter = frontMatter; Site = site; diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 03fa08f0df03b3298aa29698695557f00ac8d657..65a517d68b4702c824bf96c398a35e97f3eb6b17 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -15,7 +15,7 @@ namespace SuCoS.Models; /// /// The main configuration of the program, primarily extracted from the app.yaml file. /// -public class Site : IParams +internal class Site : ISite { #region IParams @@ -32,7 +32,7 @@ public class Site : IParams /// /// Command line options /// - public IGenerateOptions Options; + public IGenerateOptions Options { get; set; } #region SiteSettings @@ -71,7 +71,7 @@ public class Site : IParams /// /// List of all pages, including generated. /// - public IEnumerable Pages + public IEnumerable Pages { get { @@ -85,12 +85,12 @@ public class Site : IParams /// /// List of all pages, including generated, by their permalink. /// - public ConcurrentDictionary PagesReferences { get; } = new(); + public ConcurrentDictionary PagesReferences { get; } = new(); /// /// List of pages from the content folder. /// - public List RegularPages + public List RegularPages { get { @@ -106,22 +106,22 @@ public class Site : IParams /// /// The page of the home page; /// - public Page? Home { get; private set; } + public IPage? Home { get; private set; } /// /// Manage all caching lists for the site /// - public readonly SiteCacheManager CacheManager = new(); + public SiteCacheManager CacheManager { get; } = new(); /// /// The Fluid parser instance. /// - public readonly FluidParser FluidParser = new(); + public FluidParser FluidParser { get; } = new(); /// /// The Fluid/Liquid template options. /// - public readonly TemplateOptions TemplateOptions = new(); + public TemplateOptions TemplateOptions { get; } = new(); /// /// The logger instance. @@ -147,9 +147,9 @@ public class Site : IParams /// private readonly IFrontMatterParser frontMatterParser; - private List? pagesCache; + private List? pagesCache; - private List? regularPagesCache; + private List? regularPagesCache; private readonly SiteSettings settings; @@ -163,8 +163,8 @@ public class Site : IParams /// public Site( in IGenerateOptions options, - in SiteSettings settings, - in IFrontMatterParser frontMatterParser, + in SiteSettings settings, + in IFrontMatterParser frontMatterParser, in ILogger logger, ISystemClock? clock) { Options = options; @@ -197,7 +197,7 @@ public class Site : IParams /// Folder recursive level /// Page of the upper directory /// - public void ParseAndScanSourceFiles(string? directory, int level = 0, Page? parent = null) + public void ParseAndScanSourceFiles(string? directory, int level = 0, IPage? parent = null) { directory ??= SourceContentPath; @@ -217,7 +217,7 @@ public class Site : IParams }); } - private void ParseIndexPage(in string? directory, int level, ref Page? parent, ref string[] markdownFiles) + 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); @@ -253,7 +253,7 @@ public class Site : IParams } } - private Page? ParseSourceFile(in Page? parent, in string filePath) + private IPage? ParseSourceFile(in IPage? parent, in string filePath) { Page? page = null; try @@ -286,7 +286,7 @@ public class Site : IParams /// /// /// The created page for the index. - private Page CreateSystemPage(string relativePath, string title, string? sectionName = null, Page? originalPage = null) + private IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null) { sectionName ??= "section"; var isIndex = string.IsNullOrEmpty(relativePath); @@ -303,9 +303,9 @@ public class Site : IParams var id = frontMatter.URL; // Get or create the page - var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => + var lazyPage = CacheManager.automaticContentCache.GetOrAdd(id, new Lazy(() => { - Page? parent = null; + 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) @@ -346,7 +346,7 @@ public class Site : IParams /// The given page to be processed /// The parent page, if any /// - public void PostProcessPage(in Page page, Page? parent = null, bool overwrite = false) + public void PostProcessPage(in IPage page, IPage? parent = null, bool overwrite = false) { if (page is null) { diff --git a/source/Models/SiteCacheManager.cs b/source/Models/SiteCacheManager.cs index d984fe46d307d6dc9e0d061f6446d0a4ea7c23fe..6694fbd32377114457e13658206c5f05a31af4ee 100644 --- a/source/Models/SiteCacheManager.cs +++ b/source/Models/SiteCacheManager.cs @@ -22,7 +22,7 @@ public class SiteCacheManager /// /// Cache for tag page. /// - public readonly ConcurrentDictionary> automaticContentCache = new(); + public readonly ConcurrentDictionary> automaticContentCache = new(); /// /// Resets the template cache to force a reload of all templates. diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs index 1318dfb2959ae77fa672b43871eb54aaafd2ec38..0f1e023e91960d56cc6f6cd2f6eefebdfaa17863 100644 --- a/source/Parser/IFrontmatterParser.cs +++ b/source/Parser/IFrontmatterParser.cs @@ -13,7 +13,7 @@ public interface IFrontMatterParser /// /// /// - FrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath); + IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath); /// /// Extract the front matter from the content. @@ -21,7 +21,7 @@ public interface IFrontMatterParser /// /// /// - FrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent); + IFrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent); /// /// Parse the app config file. diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 9804569b457ce90119723ba8c5ec1e347cde7ad6..80b0c5292be382b8d6657a9dd19f91aab1065dfa 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 /// -public class YAMLParser : IFrontMatterParser +internal class YAMLParser : IFrontMatterParser { /// /// YamlDotNet parser, strictly set to allow automatically parse only known fields @@ -29,7 +29,7 @@ public class YAMLParser : IFrontMatterParser .Build(); /// - public FrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null) + public IFrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null) { if (filePath is null) { @@ -52,7 +52,7 @@ public class YAMLParser : IFrontMatterParser } /// - public FrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent) + public IFrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent) { if (fileRelativePath is null) { @@ -79,7 +79,7 @@ public class YAMLParser : IFrontMatterParser return page; } - private FrontMatter ParseYAML(in string filePath, string yaml, in string rawContent) + private IFrontMatter ParseYAML(in string filePath, string yaml, in string rawContent) { var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter"); var section = SiteHelper.GetSection(filePath); diff --git a/source/Program.cs b/source/Program.cs index dfacd228a14a3baaec689c7710022c273fa2e0e5..cabc01ad470bc2a2d82a14b98970ffd9965308e1 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.IO; using System.Reflection; using System.Threading.Tasks; using Serilog; @@ -11,17 +10,19 @@ namespace SuCoS; /// /// The main entry point of the program. /// -public class Program +internal class Program { - private ILogger logger; + internal const string helloWorld = @" + ____ ____ ____ +/\ _`\ /\ _`\ /\ _`\ +\ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ + \/_\__ \ /\ \/\ \\ \ \/_/_ / __`\/_\__ \ + /\ \L\ \ \ \_\ \\ \ \L\ \/\ \L\ \/\ \L\ \ + \ `\____\ \____/ \ \____/\ \____/\ `\____\ + \/_____/\/___/ \/___/ \/___/ \/_____/ +"; - /// - /// Constructor - /// - private Program(ILogger logger) - { - this.logger = logger; - } + private ILogger logger; /// /// Entry point of the program @@ -30,25 +31,24 @@ public class Program /// public static int Main(string[] args) { - ILogger logger = new LoggerConfiguration() - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); - + var logger = CreateLogger(); var program = new Program(logger); return program.Run(args); } - private int Run(string[] args) + /// + /// Constructor + /// + public Program(ILogger logger) + { + this.logger = logger; + } + + internal int Run(string[] args) { // Print the logo of the program. OutputLogo(); - - // Print the name and version of the program. - var assembly = Assembly.GetEntryAssembly(); - var assemblyName = assembly?.GetName(); - var appName = assemblyName?.Name; - var appVersion = assemblyName?.Version; - logger.Information("{name} v{version}", appName, appVersion); + OutputWelcome(); // Shared options between the commands var sourceOption = new Option(new[] { "--source", "-s" }, () => ".", "Source directory path"); @@ -67,16 +67,14 @@ public class Program }; buildCommandHandler.SetHandler((source, output, future, verbose) => { + logger = CreateLogger(verbose); + BuildOptions buildOptions = new( - output: string.IsNullOrEmpty(output) ? Path.Combine(source, "public") : output) + source: source, + output: output) { - Source = source, Future = future }; - logger = new LoggerConfiguration() - .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); _ = new BuildCommand(buildOptions, logger); }, sourceOption, buildOutputOption, futureOption, verboseOption); @@ -90,17 +88,15 @@ public class Program }; serveCommandHandler.SetHandler(async (source, future, verbose) => { + logger = CreateLogger(verbose); + ServeOptions serverOptions = new() { Source = source, Future = future }; - logger = new LoggerConfiguration() - .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) - .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) - .CreateLogger(); - var serveCommand = new ServeCommand(serverOptions, logger); + var serveCommand = new ServeCommand(serverOptions, logger, new SourceFileWatcher()); await serveCommand.RunServer(); await Task.Delay(-1); // Wait forever. }, @@ -115,16 +111,28 @@ public class Program return rootCommand.Invoke(args); } - private void OutputLogo() + internal static ILogger CreateLogger(bool verbose = false) { - logger.Information(@" - ____ ____ ____ -/\ _`\ /\ _`\ /\ _`\ -\ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ - \/_\__ \ /\ \/\ \\ \ \/_/_ / __`\/_\__ \ - /\ \L\ \ \ \_\ \\ \ \L\ \/\ \L\ \/\ \L\ \ - \ `\____\ \____/ \ \____/\ \____/\ `\____\ - \/_____/\/___/ \/___/ \/___/ \/_____/ -"); + return new LoggerConfiguration() + .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) + .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) + .CreateLogger(); + } + + /// + /// Print the name and version of the program. + /// + internal void OutputWelcome() + { + var assembly = Assembly.GetEntryAssembly(); + var assemblyName = assembly?.GetName(); + var appName = assemblyName?.Name; + var appVersion = assemblyName?.Version; + logger.Information("{name} v{version}", appName, appVersion); + } + + internal void OutputLogo() + { + logger.Information(helloWorld); } } diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index fac2bd6d8dee1833557e5c2ad5c8679a1f21024e..14d19857a8ebc9f4654cb46702c3d8c4671a8651 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -1,23 +1,21 @@ using System; using System.IO; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles; using Serilog; using SuCoS.Helpers; -using SuCoS.Models; using SuCoS.Models.CommandLineOptions; +using SuCoS.ServerHandlers; namespace SuCoS; /// /// Serve Command will live serve the site and watch any changes. /// -public class ServeCommand : BaseGeneratorCommand, IDisposable +internal class ServeCommand : BaseGeneratorCommand, IDisposable { private const string baseURLDefault = "http://localhost"; private const int portDefault = 1122; @@ -45,13 +43,15 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private IWebHost? host; + private IServerHandlers[]? handlers; + /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// When a change is detected, this triggers a server restart to ensure the served content /// remains up-to-date. The FileSystemWatcher is configured with the source directory /// at construction and starts watching immediately. /// - private readonly FileSystemWatcher sourceFileWatcher; + private readonly IFileWatcher fileWatcher; /// /// A Timer that helps to manage the frequency of server restarts. @@ -101,6 +101,13 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable serverStartTime = DateTime.UtcNow; + handlers = new IServerHandlers[]{ + new PingRequests(), + new StaticFileRequest(site.SourceStaticPath, false), + new StaticFileRequest(site.SourceThemeStaticPath, true), + new RegisteredPageRequest(site) + }; + host = new WebHostBuilder() .UseKestrel() .UseUrls(baseURLGlobal) @@ -121,7 +128,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable public void Dispose() { host?.Dispose(); - sourceFileWatcher.Dispose(); + fileWatcher.Stop(); debounceTimer?.Dispose(); GC.SuppressFinalize(this); } @@ -131,40 +138,17 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// /// ServeOptions object specifying the serve options. /// The logger instance. Injectable for testing - public ServeCommand(ServeOptions options, ILogger logger) : base(options, logger) + /// + public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) { this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); baseURLGlobal = $"{baseURLDefault}:{portDefault}"; // Watch for file changes in the specified path - sourceFileWatcher = StartFileWatcher(options.Source); - } - - /// - /// Starts the file watcher to monitor file changes in the specified source path. - /// - /// The path to the source directory. - /// The created FileSystemWatcher object. - private FileSystemWatcher StartFileWatcher(string SourcePath) - { - var SourceAbsolutePath = Path.GetFullPath(SourcePath); - + var SourceAbsolutePath = Path.GetFullPath(options.Source); logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); - - var fileWatcher = new FileSystemWatcher - { - Path = SourceAbsolutePath, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, - IncludeSubdirectories = true, - EnableRaisingEvents = true - }; - - // Subscribe to the desired events - fileWatcher.Changed += OnSourceFileChanged; - fileWatcher.Created += OnSourceFileChanged; - fileWatcher.Deleted += OnSourceFileChanged; - fileWatcher.Renamed += OnSourceFileChanged; - return fileWatcher; + fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); } /// @@ -205,40 +189,20 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable requestPath = requestPath.TrimEnd('/'); } - var fileAbsolutePath = Path.Combine(site.SourceStaticPath, requestPath.TrimStart('/')); - var fileAbsoluteThemePath = Path.Combine(site.SourceThemeStaticPath, requestPath.TrimStart('/')); - - string? resultType; - - // Return the server startup timestamp as the response - if (requestPath == "/ping") + string? resultType = null; + if (handlers is not null) { - resultType = "ping"; - await HandlePingRequest(context); - } - - // Check if it is one of the Static files (serve the actual file) - else if (File.Exists(fileAbsolutePath)) - { - resultType = "static"; - await HandleStaticFileRequest(context, fileAbsolutePath); - } - - // Check if it is one of the Static files (serve the actual file) - else if (File.Exists(fileAbsoluteThemePath)) - { - resultType = "themestatic"; - await HandleStaticFileRequest(context, fileAbsoluteThemePath); - } - - // Check if the requested file path corresponds to a registered page - else if (site.PagesReferences.TryGetValue(requestPath, out var page)) - { - resultType = "dict"; - await HandleRegisteredPageRequest(context, page); + foreach (var item in handlers) + { + if (item.Check(requestPath)) + { + resultType = await item.Handle(context, requestPath, serverStartTime); + break; + } + } } - else + if (resultType is null) { resultType = "404"; await HandleNotFoundRequest(context); @@ -246,81 +210,12 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable logger.Debug("Request {type}\tfor {RequestPath}", resultType, requestPath); } - private Task HandlePingRequest(HttpContext context) - { - var content = serverStartTime.ToString("o"); - return context.Response.WriteAsync(content); - } - - private static async Task HandleStaticFileRequest(HttpContext context, string fileAbsolutePath) - { - context.Response.ContentType = GetContentType(fileAbsolutePath); - await using var fileStream = new FileStream(fileAbsolutePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); - } - - private async Task HandleRegisteredPageRequest(HttpContext context, Page page) - { - var content = page.CompleteContent; - content = InjectReloadScript(content); - await context.Response.WriteAsync(content); - } - private static async Task HandleNotFoundRequest(HttpContext context) { context.Response.StatusCode = 404; await context.Response.WriteAsync("404 - File Not Found"); } - /// - /// Retrieves the content type of a file based on its extension. - /// If the content type cannot be determined, the default value "application/octet-stream" is returned. - /// - /// The path of the file. - /// The content type of the file. - private static string GetContentType(string filePath) - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(filePath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType ?? "application/octet-stream"; - } - - /// - /// 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; - try - { - 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(); - } - catch (Exception ex) - { - logger.Error(ex, "Could not read the JavaScript file."); - throw; - } - - // Inject the JavaScript content - var reloadScript = $""; - - const string bodyClosingTag = ""; - content = content.Replace(bodyClosingTag, $"{reloadScript}{bodyClosingTag}", StringComparison.InvariantCulture); - - return content; - } - /// /// Handles the file change event from the file watcher. /// diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs new file mode 100644 index 0000000000000000000000000000000000000000..c287887468b7785b844495097aed2b5d3e91dcbc --- /dev/null +++ b/source/ServerHandlers/IServerHandlers.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace SuCoS.ServerHandlers; + +/// +/// Handle server requests +/// +internal interface IServerHandlers +{ + /// + /// Check if the condition is met to handle the request + /// + /// + /// + bool Check(string requestPath); + + /// + /// Process the request + /// + /// + /// + /// + /// + Task Handle(HttpContext context, string requestPath, DateTime serverStartTime); +} diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs new file mode 100644 index 0000000000000000000000000000000000000000..927d701f480b30cc42f3ed2028ffe7162b323767 --- /dev/null +++ b/source/ServerHandlers/PingRequests.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace SuCoS.ServerHandlers; + +/// +/// Return the server startup timestamp as the response +/// +internal class PingRequests : IServerHandlers +{ + /// + public bool Check(string requestPath) + { + return requestPath == "/ping"; + } + + /// + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + var content = serverStartTime.ToString("o"); + await context.Response.WriteAsync(content); + + return "ping"; + } +} diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs new file mode 100644 index 0000000000000000000000000000000000000000..61feaeacb6e945b77b0538101ae3a4ec48bc5cc6 --- /dev/null +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -0,0 +1,70 @@ +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 +/// +internal class RegisteredPageRequest : IServerHandlers +{ + readonly ISite site; + + public RegisteredPageRequest(ISite site) + { + this.site = site; + } + + /// + public bool Check(string requestPath) + { + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + return site.PagesReferences.TryGetValue(requestPath, out _); + } + + /// + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + { + if (context is null) + { + 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"; + } + + /// + /// 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 new file mode 100644 index 0000000000000000000000000000000000000000..a2e849a56d96a1fd2ed505113ba987f5a9f89884 --- /dev/null +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; + +namespace SuCoS.ServerHandlers; + +/// +/// Check if it is one of the Static files (serve the actual file) +/// +internal class StaticFileRequest : IServerHandlers +{ + private readonly string basePath; + private bool inTheme; + + /// + /// Constructor + /// + /// + /// + public StaticFileRequest(string basePath, bool inTheme) + { + this.basePath = basePath; + this.inTheme = inTheme; + } + + /// + public bool Check(string requestPath) + { + if (requestPath is null) + { + throw new ArgumentNullException(nameof(requestPath)); + } + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + return File.Exists(fileAbsolutePath); + } + + /// + public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + { + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + context.Response.ContentType = GetContentType(fileAbsolutePath!); + await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await fileStream.CopyToAsync(context.Response.Body); + return inTheme ? "themeSt" : "static"; + } + + /// + /// Retrieves the content type of a file based on its extension. + /// If the content type cannot be determined, the default value "application/octet-stream" is returned. + /// + /// The path of the file. + /// The content type of the file. + private static string GetContentType(string filePath) + { + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } + return contentType ?? "application/octet-stream"; + } +} diff --git a/test/.TestSites/01/sucos.yaml b/test/.TestSites/01/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/01/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/02-have-index/sucos.yaml b/test/.TestSites/02-have-index/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/02-have-index/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/03-section/sucos.yaml b/test/.TestSites/03-section/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/03-section/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/04-tags/sucos.yaml b/test/.TestSites/04-tags/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/04-tags/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/05-theme-no-baseof/sucos.yaml b/test/.TestSites/05-theme-no-baseof/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/05-theme-no-baseof/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/06-theme/sucos.yaml b/test/.TestSites/06-theme/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/06-theme/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/07-theme-no-baseof-error/sucos.yaml b/test/.TestSites/07-theme-no-baseof-error/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/07-theme-no-baseof-error/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/content/blog/alias.md b/test/.TestSites/08-theme-html/content/blog/alias.md new file mode 100644 index 0000000000000000000000000000000000000000..44e58b4dcb404c62bd28269e8f7002911827f750 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/alias.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias +Aliases: + - v123 + - "{{ page.Title }}-2" +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/categories.md b/test/.TestSites/08-theme-html/content/blog/categories.md new file mode 100644 index 0000000000000000000000000000000000000000..4065d6776966561b2a554f975ce3a4f8ddc12cc3 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/categories.md @@ -0,0 +1,6 @@ +--- +Title: Categories +Categories: ['Test', 'Real Data'] +--- + +Categories diff --git a/test/.TestSites/08-theme-html/content/blog/date-future.md b/test/.TestSites/08-theme-html/content/blog/date-future.md new file mode 100644 index 0000000000000000000000000000000000000000..5a6a90e5de9f9e1e24e3d30844dc0b1650093c53 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/date-future.md @@ -0,0 +1,8 @@ +--- +Title: Date Future +Date: 2023-07-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/date-ok.md b/test/.TestSites/08-theme-html/content/blog/date-ok.md new file mode 100644 index 0000000000000000000000000000000000000000..d4363e77c005f5f26ff9b868a88f76a44132ee40 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/date-ok.md @@ -0,0 +1,8 @@ +--- +Title: Date-OK +Date: 2023-01-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/expired.md b/test/.TestSites/08-theme-html/content/blog/expired.md new file mode 100644 index 0000000000000000000000000000000000000000..02ae1bb158ad5629aa30a2f2f865c35f0bd060d7 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/expired.md @@ -0,0 +1,8 @@ +--- +Title: Expired +ExpiryDate: 2020-04-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/publishdate-future.md b/test/.TestSites/08-theme-html/content/blog/publishdate-future.md new file mode 100644 index 0000000000000000000000000000000000000000..70e8da6865b549260a5781cb6981c0e51f2d2dda --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/publishdate-future.md @@ -0,0 +1,8 @@ +--- +Title: PublishDate Future +PublishDate: 2023-07-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md b/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md new file mode 100644 index 0000000000000000000000000000000000000000..f4507ff8076505302863b8b779b7063852b8b865 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/publishdate-ok.md @@ -0,0 +1,8 @@ +--- +Title: PublishDate OK +PublishDate: 2023-01-01 +--- + +## Real Data Test + +This is a test using real data.", "Real Data Test diff --git a/test/.TestSites/08-theme-html/content/blog/tags-01.md b/test/.TestSites/08-theme-html/content/blog/tags-01.md new file mode 100644 index 0000000000000000000000000000000000000000..0f79798ae02ae5f7f9b6ece7896dee8a6b52b973 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/tags-01.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias +Tags: + - tag1 + - tag 2 +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/tags-02.md b/test/.TestSites/08-theme-html/content/blog/tags-02.md new file mode 100644 index 0000000000000000000000000000000000000000..afcf002e2af3163a5172ccc937d262f9d6770faa --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/tags-02.md @@ -0,0 +1,8 @@ +--- +Title: Test Alias 2 +Tags: + - tag1 + - tag 2 +--- + +Test Alias diff --git a/test/.TestSites/08-theme-html/content/blog/test01.md b/test/.TestSites/08-theme-html/content/blog/test01.md new file mode 100644 index 0000000000000000000000000000000000000000..2d3c4158bf543cd81271fdb19c46fd6442f43d12 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/test01.md @@ -0,0 +1,5 @@ +--- +Title: Test Content 1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md b/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md new file mode 100644 index 0000000000000000000000000000000000000000..974d2c97c3820ad784a3f2add5338ba46ba1e1a5 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-negative-1.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: -1" +Weight: -1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md b/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md new file mode 100644 index 0000000000000000000000000000000000000000..e36b408ce5d772396153d019ccfd601033e68950 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-negative-100.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: -100" +Weight: -100 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md b/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md new file mode 100644 index 0000000000000000000000000000000000000000..2355b30a7a08557ee60e82a2529569fe856f94ef --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-positive-1.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: plus 1" +Weight: 1 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md b/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md new file mode 100644 index 0000000000000000000000000000000000000000..08cfa38c0f4abd16fdf23d6aec9d210855bb0192 --- /dev/null +++ b/test/.TestSites/08-theme-html/content/blog/weight-positive-100.md @@ -0,0 +1,6 @@ +--- +Title: "Weight: plus 100" +Weight: 100 +--- + +Test Content 1 diff --git a/test/.TestSites/08-theme-html/content/index.md b/test/.TestSites/08-theme-html/content/index.md new file mode 100644 index 0000000000000000000000000000000000000000..28806b1107b8fe86f837e4d7f37fe8cab4da73cb --- /dev/null +++ b/test/.TestSites/08-theme-html/content/index.md @@ -0,0 +1,5 @@ +--- +Title: My Home Page +--- + +Index Content diff --git a/test/.TestSites/08-theme-html/index.md b/test/.TestSites/08-theme-html/index.md new file mode 100644 index 0000000000000000000000000000000000000000..28806b1107b8fe86f837e4d7f37fe8cab4da73cb --- /dev/null +++ b/test/.TestSites/08-theme-html/index.md @@ -0,0 +1,5 @@ +--- +Title: My Home Page +--- + +Index Content diff --git a/test/.TestSites/08-theme-html/sucos.yaml b/test/.TestSites/08-theme-html/sucos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0addbe03e7787863648b62fcd497fffe7c490ce --- /dev/null +++ b/test/.TestSites/08-theme-html/sucos.yaml @@ -0,0 +1 @@ +Title: test \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/baseof.liquid b/test/.TestSites/08-theme-html/theme/_default/baseof.liquid new file mode 100644 index 0000000000000000000000000000000000000000..f3513fd353922345b4e72114875f6c966dea1c49 --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/baseof.liquid @@ -0,0 +1,7 @@ + + + + +BASEOF-{{ page.Content }} + + \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/index.liquid b/test/.TestSites/08-theme-html/theme/_default/index.liquid new file mode 100644 index 0000000000000000000000000000000000000000..6b9b3b0fab933af83895db7650711e913178379b --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/index.liquid @@ -0,0 +1 @@ +INDEX-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/list.liquid b/test/.TestSites/08-theme-html/theme/_default/list.liquid new file mode 100644 index 0000000000000000000000000000000000000000..df270cf40e8553db1f7e0fb27756bd2aa22b3e37 --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/list.liquid @@ -0,0 +1 @@ +LIST-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/.TestSites/08-theme-html/theme/_default/single.liquid b/test/.TestSites/08-theme-html/theme/_default/single.liquid new file mode 100644 index 0000000000000000000000000000000000000000..bea53d90e0fde93f4135fd6cd2c081f8317bf03b --- /dev/null +++ b/test/.TestSites/08-theme-html/theme/_default/single.liquid @@ -0,0 +1 @@ +SINGLE-{{ page.ContentPreRendered }} \ No newline at end of file diff --git a/test/Helpers/StopwatchReporterTests.cs b/test/Helpers/StopwatchReporterTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f68743e17096d3902bb857c6b011bb1bf16fd843 --- /dev/null +++ b/test/Helpers/StopwatchReporterTests.cs @@ -0,0 +1,79 @@ + +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using Serilog; +using Serilog.Sinks.InMemory; +using SuCoS.Helpers; +using Xunit; + +namespace Test.Helpers; + +public class StopwatchReporterTests +{ + private readonly ILogger logger; + private readonly StopwatchReporter stopwatchReporter; + private readonly InMemorySink inMemorySink; + + public StopwatchReporterTests() + { + inMemorySink = new InMemorySink(); + logger = new LoggerConfiguration().WriteTo.Sink(inMemorySink).CreateLogger(); + stopwatchReporter = new StopwatchReporter(logger); + } + + [Fact] + public void Start_InitializesAndStartsStopwatchForStep() + { + // Arrange + var stepName = "TestStep"; + var stopwatchReporter = new StopwatchReporter(new LoggerConfiguration().CreateLogger()); + + // Act + stopwatchReporter.Start(stepName); + + // Assert + var stopwatchField = stopwatchReporter.GetType().GetField("stopwatches", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(stopwatchField); + + var stopwatchDictionary = stopwatchField.GetValue(stopwatchReporter) as Dictionary; + Assert.NotNull(stopwatchDictionary); + + Assert.True(stopwatchDictionary.ContainsKey(stepName)); + Assert.True(stopwatchDictionary[stepName].IsRunning); + } + + [Fact] + public void LogReport_CorrectlyLogsElapsedTime() + { + var stepName = "TestStep"; + var siteTitle = "TestSite"; + + stopwatchReporter.Start(stepName); + Thread.Sleep(123); // Let's wait a bit to simulate some processing. + stopwatchReporter.Stop(stepName, 1); + + stopwatchReporter.LogReport(siteTitle); + + // Assert + var logEvents = inMemorySink.LogEvents; + Assert.NotEmpty(logEvents); + var logMessage = logEvents.First().RenderMessage(CultureInfo.InvariantCulture); + Assert.Contains($"Site '{siteTitle}' created!", logMessage, StringComparison.InvariantCulture); + Assert.Contains(stepName, logMessage, StringComparison.InvariantCulture); + Assert.Contains("123 ms", logMessage, StringComparison.InvariantCulture); // Ensure that our processing time was logged. + } + + [Fact] + public void Stop_ThrowsExceptionWhenStopCalledWithoutStart() + { + var stepName = "TestStep"; + + // Don't call Start for stepName + + // Assert that Stop throws an exception + var exception = Assert.Throws(() => stopwatchReporter.Stop(stepName, 1)); + Assert.Equal($"Step '{stepName}' has not been started.", exception.Message); + } + +} diff --git a/test/Models/FrontMatterTests.cs b/test/Models/FrontMatterTests.cs index 8c672f611a22ceba32b40238bf95bf83c0fff1ab..b045efb4c4bc0484bca63cfa022612cbf7aece31 100644 --- a/test/Models/FrontMatterTests.cs +++ b/test/Models/FrontMatterTests.cs @@ -4,7 +4,7 @@ using Xunit; namespace Test.Models; -public class FrontMatterTests +public class FrontMatterTests : TestSetup { [Theory] [InlineData("Title1", "Section1", "Type1", "URL1", Kind.single)] diff --git a/test/Models/PageTests.cs b/test/Models/PageTests.cs index fbf74317d8d3aa7fe009ed1336434f56e715f696..cc77b7b74d6ed5d11a63f1c05594bb58e6a450a7 100644 --- a/test/Models/PageTests.cs +++ b/test/Models/PageTests.cs @@ -2,27 +2,12 @@ using System.Globalization; using Moq; using SuCoS.Models; using Xunit; -using Serilog; using SuCoS.Models.CommandLineOptions; -using SuCoS.Parser; namespace Test.Models; -public class PageTests +public class PageTests : TestSetup { - private readonly IFrontMatterParser frontMatterParser = new YAMLParser(); - private readonly Mock generateOptionsMock = new(); - private readonly Mock siteSettingsMock = new(); - private readonly Mock loggerMock = new(); - private readonly Mock systemClockMock = new(); - private readonly FrontMatter frontMatterMock = new() - { - Title = titleCONST, - SourcePath = sourcePathCONST - }; - private readonly Site site; - private const string titleCONST = "Test Title"; - private const string sourcePathCONST = "/path/to/file.md"; private const string markdown1CONST = @" # word01 word02 @@ -46,13 +31,6 @@ console.WriteLine('hello word') word03 word04 word05 6 7 eight "; - public PageTests() - { - var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); - systemClockMock.Setup(c => c.Now).Returns(testDate); - site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); - } - [Theory] [InlineData("Test Title", "/path/to/file.md", "file", "/path/to")] public void Frontmatter_ShouldCreateWithCorrectProperties(string title, string sourcePath, string sourceFileNameWithoutExtension, string sourcePathDirectory) diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index cefd4df012854de03cec89f5a5f0291bbaba17bc..83403a1a411a8723814d3aebc573205766ead0aa 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -1,43 +1,13 @@ using Xunit; -using Moq; -using System.Globalization; -using SuCoS.Models; -using Serilog; using SuCoS.Models.CommandLineOptions; -using SuCoS.Parser; namespace Test.Models; /// /// Unit tests for the Site class. /// -public class SiteTests +public class SiteTests : TestSetup { - private readonly Site site; - private readonly Mock generateOptionsMock = new(); - private readonly Mock siteSettingsMock = new(); - private readonly Mock loggerMock = new(); - private readonly IFrontMatterParser frontMatterParser = new YAMLParser(); - private readonly Mock systemClockMock = new(); - private const string testSitePathCONST01 = ".TestSites/01"; - private const string testSitePathCONST02 = ".TestSites/02-have-index"; - private const string testSitePathCONST03 = ".TestSites/03-section"; - private const string testSitePathCONST04 = ".TestSites/04-tags"; - private const string testSitePathCONST05 = ".TestSites/05-theme-no-baseof"; - private const string testSitePathCONST06 = ".TestSites/06-theme"; - private const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; - - // based on the compiled test.dll path - // that is typically "bin/Debug/netX.0/test.dll" - private const string testSitesPath = "../../.."; - - public SiteTests() - { - var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); - systemClockMock.Setup(c => c.Now).Returns(testDate); - site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); - } - [Theory] [InlineData("test01.md")] [InlineData("date-ok.md")] diff --git a/test/ProgramTest.cs b/test/ProgramTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..13d1a2a1fdf3d3486463f85adfad9ecfc9ebc29d --- /dev/null +++ b/test/ProgramTest.cs @@ -0,0 +1,35 @@ +using Moq; +using Serilog.Events; +using SuCoS; +using Xunit; + +namespace Test; + +public class ProgramTests : TestSetup +{ + [Theory] + [InlineData(false, LogEventLevel.Information)] + [InlineData(true, LogEventLevel.Debug)] + public void CreateLogger_SetsLogLevel(bool verbose, LogEventLevel expected) + { + // Act + var logger = Program.CreateLogger(verbose); + + // Assert + Assert.True(logger.IsEnabled(expected)); + } + + [Fact] + public void OutputLogo_Should_LogHelloWorld() + { + // Arrange + var program = new Program(loggerMock.Object); + + // Act + program.OutputLogo(); + program.OutputWelcome(); + + // Assert + loggerMock.Verify(x => x.Information(Program.helloWorld), Times.Once); + } +} \ No newline at end of file diff --git a/test/ServerHandlers/PingRequestHandlerTests.cs b/test/ServerHandlers/PingRequestHandlerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..af5cfb2d3321f850b639ad8cf90fbd90b23524ce --- /dev/null +++ b/test/ServerHandlers/PingRequestHandlerTests.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class PingRequestHandlerTests : TestSetup +{ + [Fact] + public async Task Handle_ReturnsServerStartupTimestamp() + { + // Arrange + var context = new DefaultHttpContext(); + + var stream = new MemoryStream(); + context.Response.Body = stream; + + var pingRequests = new PingRequests(); + + // Act + await pingRequests.Handle(context, "ping", todayDate); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + Assert.Equal(todayDate.ToString("o"), content); + } + + + [Theory] + [InlineData("/ping", true)] + [InlineData("ping", false)] + [InlineData(null, false)] + public void Check_HandlesVariousRequestPaths(string requestPath, bool expectedResult) + { + // Arrange + var pingRequests = new PingRequests(); + + // Act + var result = pingRequests.Check(requestPath); + + // Assert + Assert.Equal(expectedResult, result); + } +} + diff --git a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..ccf7d538478d08352542436bb4b0251215eb76bc --- /dev/null +++ b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.Models; +using SuCoS.Models.CommandLineOptions; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class RegisteredPageRequestHandlerTests : TestSetup +{ + [Theory] + [InlineData("/", true)] + [InlineData("/testPage", false)] + public void Check_ReturnsTrueForRegisteredPage(string requestPath, bool exist) + { + // Arrange + var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePathCONST06)); + site.Options = new GenerateOptions + { + Source = siteFullPath + }; + var registeredPageRequest = new RegisteredPageRequest(site); + + // Act + site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); + + // Assert + Assert.Equal(exist, registeredPageRequest.Check(requestPath)); + } + + [Theory] + [InlineData("/", testSitePathCONST06, false)] + [InlineData("/", testSitePathCONST08, true)] + public async Task Handle_ReturnsExpectedContent2(string requestPath, string testSitePath, bool contains) + { + // Arrange + var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSitePath)); + site.Options = new GenerateOptions + { + Source = siteFullPath + }; + var registeredPageRequest = new RegisteredPageRequest(site); + + var context = new DefaultHttpContext(); + var stream = new MemoryStream(); + context.Response.Body = stream; + + // Act + site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); + registeredPageRequest.Check(requestPath); + await registeredPageRequest.Handle(context, requestPath, DateTime.Now); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + // You may want to adjust this assertion depending on the actual format of your injected script + if (contains) + { + Assert.Contains("", content, StringComparison.InvariantCulture); + } + else + { + Assert.DoesNotContain("", content, StringComparison.InvariantCulture); + Assert.DoesNotContain("", content, StringComparison.InvariantCulture); + } + Assert.Contains("Index Content", content, StringComparison.InvariantCulture); + + } + +} diff --git a/test/ServerHandlers/StaticFileRequestHandlerTests.cs b/test/ServerHandlers/StaticFileRequestHandlerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..eea246a045f4bdfe54c154edb00e50c96adef2ef --- /dev/null +++ b/test/ServerHandlers/StaticFileRequestHandlerTests.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Http; +using SuCoS.ServerHandlers; +using Xunit; + +namespace Tests.ServerHandlers; + +public class StaticFileRequestHandlerTests : TestSetup, IDisposable +{ + private readonly string tempFilePath; + + public StaticFileRequestHandlerTests() : base() + { + // Creating a temporary file for testing purposes + tempFilePath = Path.GetTempFileName(); + File.WriteAllText(tempFilePath, "test"); + } + + [Fact] + public void Check_ReturnsTrueForExistingFile() + { + // Arrange + var requestPath = Path.GetFileName(tempFilePath); + var basePath = Path.GetDirectoryName(tempFilePath) + ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); + + var staticFileRequest = new StaticFileRequest(basePath, false); + + // Act + var result = staticFileRequest.Check(requestPath); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task Handle_ReturnsExpectedContent() + { + // Arrange + var requestPath = Path.GetFileName(tempFilePath); + var basePath = Path.GetDirectoryName(tempFilePath) + ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); + var staticFileRequest = new StaticFileRequest(basePath, true); + + var context = new DefaultHttpContext(); + var stream = new MemoryStream(); + context.Response.Body = stream; + + // Act + staticFileRequest.Check(requestPath); + await staticFileRequest.Handle(context, requestPath, DateTime.Now); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + + Assert.Equal("test", content); + } + + public void Dispose() + { + // Cleaning up the temporary file after tests run + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } +} diff --git a/test/TestSetup.cs b/test/TestSetup.cs new file mode 100644 index 0000000000000000000000000000000000000000..151f2c4545636d1bacbc6625b987443cab0e6267 --- /dev/null +++ b/test/TestSetup.cs @@ -0,0 +1,46 @@ +using Serilog; +using Moq; +using SuCoS.Models; +using SuCoS.Models.CommandLineOptions; +using SuCoS.Parser; +using System.Globalization; + +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 const string testSitePathCONST01 = ".TestSites/01"; + protected const string testSitePathCONST02 = ".TestSites/02-have-index"; + protected const string testSitePathCONST03 = ".TestSites/03-section"; + protected const string testSitePathCONST04 = ".TestSites/04-tags"; + protected const string testSitePathCONST05 = ".TestSites/05-theme-no-baseof"; + protected const string testSitePathCONST06 = ".TestSites/06-theme"; + protected const string testSitePathCONST07 = ".TestSites/07-theme-no-baseof-error"; + 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 IFrontMatter frontMatterMock = new FrontMatter() + { + Title = titleCONST, + SourcePath = sourcePathCONST + }; + + protected readonly ISite site; + + // based on the compiled test.dll path + // that is typically "bin/Debug/netX.0/test.dll" + protected const string testSitesPath = "../../.."; + + public TestSetup() + { + systemClockMock.Setup(c => c.Now).Returns(todayDate); + site = new Site(generateOptionsMock.Object, siteSettingsMock.Object, frontMatterParser, loggerMock.Object, systemClockMock.Object); + } +} \ No newline at end of file diff --git a/test/test.csproj b/test/test.csproj index 520ac34b680d19ba7190debd0094713a0b1f89e6..24f5f9580651ee2a46f16ed2df2343165140a965 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -12,6 +12,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive