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
-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 = "