diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs
index fb05427ba98fae2c578330802c3004954afabeb3..d11b59fb40f08371d5e510d54f2786f6f8a4a5e8 100644
--- a/source/BaseGeneratorCommand.cs
+++ b/source/BaseGeneratorCommand.cs
@@ -14,7 +14,7 @@ namespace SuCoS;
///
/// Base class for build and serve commands.
///
-internal abstract class BaseGeneratorCommand
+public abstract class BaseGeneratorCommand
{
///
/// The configuration file name.
@@ -121,5 +121,4 @@ internal abstract class BaseGeneratorCommand
}
return false;
}
-
}
diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs
index 9046589481ffd5b4b4485b0553ebf0cef9e6e5eb..92cceca8ac5ebd13e98ba1ef15c5c2dcc840a4b0 100644
--- a/source/BuildCommand.cs
+++ b/source/BuildCommand.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
+using SuCoS.Models;
using SuCoS.Models.CommandLineOptions;
namespace SuCoS;
@@ -10,7 +11,7 @@ namespace SuCoS;
///
/// Build Command will build the site based on the source files.
///
-internal class BuildCommand : BaseGeneratorCommand
+public class BuildCommand : BaseGeneratorCommand
{
private readonly BuildOptions options;
@@ -45,30 +46,40 @@ internal class BuildCommand : BaseGeneratorCommand
// Print each page
var pagesCreated = 0; // counter to keep track of the number of pages created
- _ = Parallel.ForEach(site.PagesReferences, pair =>
+ _ = Parallel.ForEach(site.OutputReferences, pair =>
{
- var (url, page) = pair;
- var result = page.CompleteContent;
+ var (url, output) = pair;
- var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/');
+ if (output is IPage page)
+ {
+ var path = (url + (site.UglyURLs ? "" : "/index.html")).TrimStart('/');
- // Generate the output path
- var outputAbsolutePath = Path.Combine(options.Output, path);
+ // Generate the output path
+ var outputAbsolutePath = Path.Combine(options.Output, path);
- var outputDirectory = Path.GetDirectoryName(outputAbsolutePath);
- if (!Directory.Exists(outputDirectory))
- {
- _ = Directory.CreateDirectory(outputDirectory!);
- }
+ var outputDirectory = Path.GetDirectoryName(outputAbsolutePath);
+ if (!Directory.Exists(outputDirectory))
+ {
+ _ = Directory.CreateDirectory(outputDirectory!);
+ }
- // Save the processed output to the final file
- File.WriteAllText(outputAbsolutePath, result);
+ // Save the processed output to the final file
+ var result = page.CompleteContent;
+ File.WriteAllText(outputAbsolutePath, result);
- // Log
- logger.Debug("Page created: {Permalink}", outputAbsolutePath);
+ // Log
+ logger.Debug("Page created: {Permalink}", outputAbsolutePath);
- // Use interlocked to safely increment the counter in a multi-threaded environment
- _ = Interlocked.Increment(ref pagesCreated);
+ // Use interlocked to safely increment the counter in a multi-threaded environment
+ _ = Interlocked.Increment(ref pagesCreated);
+ }
+ else if (output is IResource resource)
+ {
+ var outputAbsolutePath = Path.Combine(options.Output, resource.Permalink!.TrimStart('/'));
+
+ // Copy the file to the output folder
+ File.Copy(resource.SourceFullPath, outputAbsolutePath, overwrite: true);
+ }
});
// Stop the stopwatch
@@ -94,16 +105,16 @@ internal class BuildCommand : BaseGeneratorCommand
// Get all files in the source folder
var files = Directory.GetFiles(source);
- foreach (var file in files)
+ foreach (var fileFullPath in files)
{
// Get the filename from the full path
- var fileName = Path.GetFileName(file);
+ var fileName = Path.GetFileName(fileFullPath);
// Create the destination path by combining the output folder and the filename
- var destinationPath = Path.Combine(output, fileName);
+ var destinationFullPath = Path.Combine(output, fileName);
// Copy the file to the output folder
- File.Copy(file, destinationPath, overwrite: true);
+ File.Copy(fileFullPath, destinationFullPath, overwrite: true);
}
}
}
diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs
index 217a4bca5f95bdb5c719c56ae6e9ea35f5f8c99f..25c490e9d3189ab8536a19e97253e13f7c73a54b 100644
--- a/source/Helpers/FileUtils.cs
+++ b/source/Helpers/FileUtils.cs
@@ -9,7 +9,7 @@ namespace SuCoS.Helpers;
///
/// Helper methods for scanning files.
///
-internal static class FileUtils
+public static class FileUtils
{
///
/// Gets the content of a template file based on the page and the theme path.
diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs
index 3ec93a36162efb5aecb3c61526ea39a58948f256..910030ecfad55e478d44566955862ab383f25a31 100644
--- a/source/Helpers/SiteHelper.cs
+++ b/source/Helpers/SiteHelper.cs
@@ -13,7 +13,7 @@ namespace SuCoS.Helpers;
///
/// Helper methods for scanning files.
///
-internal static class SiteHelper
+public static class SiteHelper
{
///
/// Markdig 20+ built-in extensions
diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs
index d32782b8c0cfe96683660b0a1a38a42dd7662e25..d26b5c235a6b458734c224bda5ae2ec789618ed7 100644
--- a/source/Helpers/SourceFileWatcher.cs
+++ b/source/Helpers/SourceFileWatcher.cs
@@ -4,7 +4,7 @@ using System.IO;
///
/// The FileSystemWatcher object that monitors the source directory for file changes.
///
-internal interface IFileWatcher
+public interface IFileWatcher
{
///
/// Starts the file watcher to monitor file changes in the specified source path.
@@ -23,7 +23,7 @@ internal interface IFileWatcher
///
/// The FileSystemWatcher object that monitors the source directory for file changes.
///
-internal class SourceFileWatcher : IFileWatcher
+public class SourceFileWatcher : IFileWatcher
{
///
/// The FileSystemWatcher object that monitors the source directory for file changes.
diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs
index ec4c5a19581917a611aea5077ee73481737e3e3e..46219a6c64fb738048735e8662eb0957ab9e3b43 100644
--- a/source/Helpers/StopwatchReporter.cs
+++ b/source/Helpers/StopwatchReporter.cs
@@ -12,7 +12,7 @@ namespace SuCoS.Helpers;
/// The stopwatch is started
/// and stopped around parts of the code that we want to measure.
///
-internal class StopwatchReporter
+public class StopwatchReporter
{
private readonly ILogger logger;
private readonly Dictionary stopwatches;
diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs
index 2af9da07d46201c7a1da3c55757ce918d1a2ac90..0b83bfdcfe8e1e8826c6b568f9268cb0b1ac1a69 100644
--- a/source/Helpers/Urlizer.cs
+++ b/source/Helpers/Urlizer.cs
@@ -8,7 +8,7 @@ namespace SuCoS.Helpers;
///
/// Helper class to convert a string to a URL-friendly string.
///
-internal static partial class Urlizer
+public static partial class Urlizer
{
[GeneratedRegex(@"[^a-zA-Z0-9]+")]
private static partial Regex UrlizeRegexAlpha();
@@ -68,7 +68,7 @@ internal static partial class Urlizer
/// Options for the class.
/// Basically to force lowercase and to change the replacement character.
///
-internal class UrlizerOptions
+public class UrlizerOptions
{
///
/// Force to generate lowercase URLs.
diff --git a/source/Models/Bundle.cs b/source/Models/Bundle.cs
new file mode 100644
index 0000000000000000000000000000000000000000..1a8d7db4105f5d945f26745b79e20c9dee475526
--- /dev/null
+++ b/source/Models/Bundle.cs
@@ -0,0 +1,22 @@
+namespace SuCoS.Models;
+
+///
+/// The type of content bundle.
+///
+public enum BundleType
+{
+ ///
+ /// Regular page. Not a bundle.
+ ///
+ none,
+
+ ///
+ /// Bundle with no childre
+ ///
+ leaf,
+
+ ///
+ /// Bundle with children embeded, like a home page, taxonomy term, taxonomy list
+ ///
+ branch
+}
diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs
index 86f9748d2e856b773b246e5f18830b962cb75886..3710cdfb993737636f8c0a0ad0759407b387064b 100644
--- a/source/Models/CommandLineOptions/BuildOptions.cs
+++ b/source/Models/CommandLineOptions/BuildOptions.cs
@@ -5,7 +5,7 @@ namespace SuCoS.Models.CommandLineOptions;
///
/// Command line options for the build command.
///
-internal class BuildOptions : GenerateOptions
+public class BuildOptions : GenerateOptions
{
///
/// The path of the output files.
diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs
index 8435eafc4d260e4d34e10954bc6cfd5b4d79e5b4..612284367e8e35440385a18b8d3b3b5de36f9d8a 100644
--- a/source/Models/CommandLineOptions/GenerateOptions.cs
+++ b/source/Models/CommandLineOptions/GenerateOptions.cs
@@ -3,7 +3,7 @@ namespace SuCoS.Models.CommandLineOptions;
///
/// Basic Command line options for the serve and build command.
///
-internal class GenerateOptions : IGenerateOptions
+public class GenerateOptions : IGenerateOptions
{
///
public string Source { get; init; } = ".";
diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs
index 8ea518bfffb81d392f720296f19c81ae3fb1ca25..9e2b5c969726d7babb800b916303453a30410b92 100644
--- a/source/Models/CommandLineOptions/ServeOptions.cs
+++ b/source/Models/CommandLineOptions/ServeOptions.cs
@@ -3,6 +3,6 @@ namespace SuCoS.Models.CommandLineOptions;
///
/// Command line options for the serve command.
///
-internal class ServeOptions : GenerateOptions
+public class ServeOptions : GenerateOptions
{
}
diff --git a/source/Models/FrontMatter.cs b/source/Models/FrontMatter.cs
index 9c5629b95d5c7b235ddfbf8b601af9c119b5ce63..a3d82f718dbbad9c1a607aefb35083aa645fdc08 100644
--- a/source/Models/FrontMatter.cs
+++ b/source/Models/FrontMatter.cs
@@ -9,7 +9,7 @@ namespace SuCoS.Models;
/// A scafold structure to help creating system-generated content, like
/// tag, section or index pages
///
-internal class FrontMatter : IFrontMatter
+public class FrontMatter : IFrontMatter
{
#region IFrontMatter
@@ -45,10 +45,13 @@ internal class FrontMatter : IFrontMatter
///
public int Weight { get; init; } = 0;
-
+
///
public List? Tags { get; init; }
+ ///
+ public List? ResourceDefinitions { get; set; }
+
///
[YamlIgnore]
public string RawContent { get; set; } = string.Empty;
@@ -59,15 +62,19 @@ internal class FrontMatter : IFrontMatter
///
[YamlIgnore]
- public string? SourcePath { get; set; }
+ public string? SourceRelativePath { get; set; }
+
+ ///
+ [YamlIgnore]
+ public string SourceFullPath { get; set; }
///
[YamlIgnore]
- public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourcePath);
+ public string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath);
///
[YamlIgnore]
- public string? SourcePathDirectory => Path.GetDirectoryName(SourcePath);
+ public string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourceRelativePath);
///
[YamlIgnore]
@@ -81,7 +88,10 @@ internal class FrontMatter : IFrontMatter
///
/// Constructor
///
- public FrontMatter() { }
+ public FrontMatter()
+ {
+ SourceFullPath = string.Empty;
+ }
///
/// Constructor
@@ -91,6 +101,7 @@ internal class FrontMatter : IFrontMatter
public FrontMatter(string title, string sourcePath)
{
Title = title;
- SourcePath = sourcePath;
+ SourceRelativePath = sourcePath;
+ SourceFullPath = sourcePath;
}
}
diff --git a/source/Models/FrontMatterResources.cs b/source/Models/FrontMatterResources.cs
new file mode 100644
index 0000000000000000000000000000000000000000..440d6dfac46b2a046153377a887cf3f4fb0b78e0
--- /dev/null
+++ b/source/Models/FrontMatterResources.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.FileSystemGlobbing;
+
+namespace SuCoS.Models;
+
+///
+/// Basic structure needed to generate user content and system content
+///
+public class FrontMatterResources : IFrontMatterResources
+{
+ ///
+ public string Src { get; set; } = string.Empty;
+
+ ///
+ public string? Title { get; set; }
+
+ ///
+ public string? Name { get; set; }
+
+ ///
+ public Dictionary Params { get; set; } = new();
+
+ ///
+ public Matcher? GlobMatcher { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public FrontMatterResources() { }
+}
diff --git a/source/Models/IFile.cs b/source/Models/IFile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ad7bed8e3a090aeb14c9fff7549de80598ae4d7b
--- /dev/null
+++ b/source/Models/IFile.cs
@@ -0,0 +1,61 @@
+using System.IO;
+using Microsoft.AspNetCore.StaticFiles;
+
+namespace SuCoS.Models;
+
+///
+/// Basic structure needed to generate user content and system content
+///
+public interface IFile
+{
+ ///
+ /// The source filename, without the extension. ;)
+ ///
+ string SourceFullPath { get; }
+
+ ///
+ /// The source file/folder, relative to content folder
+ ///
+ string? SourceRelativePath { get; }
+
+ ///
+ /// The source directory of the file, without the file name.
+ ///
+ string? SourceRelativePathDirectory => Path.GetDirectoryName(SourceRelativePath);
+
+ ///
+ /// The full source directory of the file, without the file name.
+ ///
+ string? SourceFullPathDirectory => Path.GetDirectoryName(SourceFullPath);
+
+ ///
+ /// The source filename, without the extension. ;)
+ ///
+ string? SourceFileNameWithoutExtension => Path.GetFileNameWithoutExtension(SourceRelativePath);
+
+ ///
+ /// File extension.
+ ///
+ string Extension => Path.GetExtension(SourceFullPath);
+
+ ///
+ /// File MIME type.
+ ///
+ string MimeType
+ {
+ get
+ {
+ var provider = new FileExtensionContentTypeProvider();
+ if (!provider.TryGetContentType(SourceFullPath, out var contentType))
+ {
+ contentType = "application/octet-stream";
+ }
+ return contentType;
+ }
+ }
+
+ ///
+ /// File size in bytes.
+ ///
+ long Size => new FileInfo(SourceFullPath).Length;
+}
diff --git a/source/Models/IFrontMatter.cs b/source/Models/IFrontMatter.cs
index 8da34d5244c95dbd2760f2814dd3ef116f020282..0bc369979d81302bdfe635a80d7101130dbc41ac 100644
--- a/source/Models/IFrontMatter.cs
+++ b/source/Models/IFrontMatter.cs
@@ -7,12 +7,12 @@ namespace SuCoS.Models;
///
/// Basic structure needed to generate user content and system content
///
-public interface IFrontMatter : IParams
+public interface IFrontMatter : IParams, IFile
{
///
/// The content Title.
///
- public string? Title { get; }
+ string? Title { get; }
///
/// The first directory where the content is located, inside content.
@@ -90,11 +90,16 @@ public interface IFrontMatter : IParams
/// Page weight. Used for sorting by default.
///
int Weight { get; }
-
+
///
/// A list of tags, if any.
///
- public List? Tags { get; }
+ List? Tags { get; }
+
+ ///
+ /// List of resource definitions.
+ ///
+ List? ResourceDefinitions { get; }
///
/// Raw content from the Markdown file, bellow the front matter.
@@ -107,21 +112,6 @@ public interface IFrontMatter : IParams
///
Kind Kind { get; }
- ///
- /// The source filename, without the extension. ;)
- ///
- public string? SourcePath { get; }
-
- ///
- /// The source filename, without the extension. ;)
- ///
- string? SourceFileNameWithoutExtension { get; }
-
- ///
- /// The source directory of the file, without the file name.
- ///
- string? SourcePathDirectory { get; }
-
///
/// The date to be considered as the publish date.
///
diff --git a/source/Models/IFrontMatterResources.cs b/source/Models/IFrontMatterResources.cs
new file mode 100644
index 0000000000000000000000000000000000000000..315c703fc6a7f39f95bff3aeeee14ee70f9f0a7a
--- /dev/null
+++ b/source/Models/IFrontMatterResources.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.FileSystemGlobbing;
+
+namespace SuCoS.Models;
+
+///
+/// Basic structure needed to generate user content and system content
+///
+public interface IFrontMatterResources : IParams
+{
+ ///
+ /// The resource filename search
+ ///
+ string Src { get; set; }
+
+ ///
+ /// The resource Title.
+ ///
+ string? Title { get; set; }
+
+ ///
+ /// The resource file name.
+ ///
+ string? Name { get; set; }
+
+ ///
+ /// Glob matcher that will parse the Src.
+ ///
+ public Matcher? GlobMatcher { get; set; }
+}
diff --git a/source/Models/IOutput.cs b/source/Models/IOutput.cs
new file mode 100644
index 0000000000000000000000000000000000000000..a2137312fc3053e20a9fbc077ff5a4c1beca42a9
--- /dev/null
+++ b/source/Models/IOutput.cs
@@ -0,0 +1,12 @@
+namespace SuCoS.Models;
+
+///
+/// Page or Resources (files) that will be considered as output.
+///
+public interface IOutput
+{
+ ///
+ /// The URL for the content.
+ ///
+ public string? Permalink { get; set; }
+}
diff --git a/source/Models/IPage.cs b/source/Models/IPage.cs
index 1f739e5be43d6dd9ed2fa61ca4bc51b574d372cd..cd300c5473fbc44dfd24c1d21f94d0f299a8c3db 100644
--- a/source/Models/IPage.cs
+++ b/source/Models/IPage.cs
@@ -10,7 +10,7 @@ namespace SuCoS.Models;
///
/// Each page data created from source files or from the system.
///
-public interface IPage : IFrontMatter
+public interface IPage : IFrontMatter, IOutput
{
///
new Kind Kind { get; set; }
@@ -18,9 +18,9 @@ public interface IPage : IFrontMatter
///
/// The source directory of the file.
///
- public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory)
+ public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourceRelativePathDirectory)
? null
- : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
+ : Path.GetFileName(Path.GetFullPath(SourceRelativePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
///
/// Point to the site configuration.
@@ -32,11 +32,6 @@ public interface IPage : IFrontMatter
///
public List? AliasesProcessed { get; set; }
- ///
- /// The URL for the content.
- ///
- public string? Permalink { get; set; }
-
///
/// Other content that mention this content.
/// Used to create the tags list and Related Posts section.
@@ -49,6 +44,16 @@ public interface IPage : IFrontMatter
///
public IPage? Parent { get; set; }
+ ///
+ /// The bundle type of the page.
+ ///
+ public BundleType BundleType { get; set; }
+
+ ///
+ /// Page resources. All files that accompany a page.
+ ///
+ public List? Resources { get; set; }
+
///
/// Plain markdown content, without HTML.
///
@@ -97,7 +102,6 @@ public interface IPage : IFrontMatter
/// The processed output file content.
public string CompleteContent { get; }
-
///
/// Other content that mention this content.
/// Used to create the tags list and Related Posts section.
@@ -112,7 +116,7 @@ public interface IPage : IFrontMatter
///
/// Get all URLs related to this content.
///
- public List Urls { get; }
+ public Dictionary AllOutputURLs { get; }
///
/// Gets the Permalink path for the file.
@@ -120,4 +124,9 @@ public interface IPage : IFrontMatter
/// The URL to consider. If null use the predefined URL
/// The output path.
public string CreatePermalink(string? URLforce = null);
+
+ ///
+ /// Final steps of parsing the content.
+ ///
+ public void PostProcess();
}
diff --git a/source/Models/IResource.cs b/source/Models/IResource.cs
new file mode 100644
index 0000000000000000000000000000000000000000..003191f9770688120e71badee448a6a25e56c8e4
--- /dev/null
+++ b/source/Models/IResource.cs
@@ -0,0 +1,13 @@
+namespace SuCoS.Models;
+
+///
+/// Page resources. All files that accompany a page.
+///
+public interface IResource : IFile, IOutput, IParams
+{
+ ///
+ public string? Title { get; set; }
+
+ ///
+ public string? FileName { get; set; }
+}
diff --git a/source/Models/ISite.cs b/source/Models/ISite.cs
index 99a833e8df241cf72514e457bbed3642473a24c8..5c96b67bcd3ca7663812167d4c7ec591971d54aa 100644
--- a/source/Models/ISite.cs
+++ b/source/Models/ISite.cs
@@ -60,12 +60,12 @@ public interface ISite : IParams
///
/// List of all pages, including generated, by their permalink.
///
- public ConcurrentDictionary PagesReferences { get; }
+ public ConcurrentDictionary OutputReferences { get; }
///
/// List of pages from the content folder.
///
- public List RegularPages { get; }
+ public IEnumerable RegularPages { get; }
///
/// The page of the home page;
@@ -140,4 +140,14 @@ public interface ISite : IParams
/// Check if the page is publishable
///
public bool IsDatePublishable(in IFrontMatter frontMatter);
+
+ ///
+ /// Creates the page for the site index.
+ ///
+ /// The relative path of the page.
+ ///
+ ///
+ ///
+ /// The created page for the index.
+ public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null);
}
\ No newline at end of file
diff --git a/source/Models/ISystemClock.cs b/source/Models/ISystemClock.cs
index f3f9274b03eb4991a05218a6aa26ce2bcd97e90e..bd389003fef397fc66e2864d71bf020b0d8ff043 100644
--- a/source/Models/ISystemClock.cs
+++ b/source/Models/ISystemClock.cs
@@ -21,7 +21,7 @@ public interface ISystemClock
///
/// Represents a concrete implementation of the ISystemClock interface using the system clock.
///
-internal class SystemClock : ISystemClock
+public class SystemClock : ISystemClock
{
///
/// Gets the current local date and time.
diff --git a/source/Models/Page.cs b/source/Models/Page.cs
index 35f0e909b00e7c45d90533375fd7454b02035737..5de36dfede7d84d9334b14c9afb308fe002eab24 100644
--- a/source/Models/Page.cs
+++ b/source/Models/Page.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using Fluid;
using Markdig;
+using Microsoft.Extensions.FileSystemGlobbing;
using SuCoS.Helpers;
namespace SuCoS.Models;
@@ -12,7 +13,7 @@ namespace SuCoS.Models;
///
/// Each page data created from source files or from the system.
///
-internal class Page : IPage
+public class Page : IPage
{
private readonly IFrontMatter frontMatter;
@@ -54,6 +55,9 @@ internal class Page : IPage
///
public List? Tags => frontMatter.Tags;
+ ///
+ public List? ResourceDefinitions => frontMatter.ResourceDefinitions;
+
///
public string RawContent => frontMatter.RawContent;
@@ -65,13 +69,19 @@ internal class Page : IPage
}
///
- public string? SourcePath => frontMatter.SourcePath;
+ public string? SourceRelativePath => frontMatter.SourceRelativePath;
///
- public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension;
+ public string? SourceRelativePathDirectory => frontMatter.SourceRelativePathDirectory;
+
+ ///
+ public string SourceFullPath => frontMatter.SourceFullPath;
+
+ ///
+ public string? SourceFullPathDirectory => frontMatter.SourceFullPathDirectory;
///
- public string? SourcePathDirectory => frontMatter.SourcePathDirectory;
+ public string? SourceFileNameWithoutExtension => frontMatter.SourceFileNameWithoutExtension;
///
public Dictionary Params
@@ -85,10 +95,9 @@ internal class Page : IPage
///
/// The source directory of the file.
///
- public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourcePathDirectory)
+ public string? SourcePathLastDirectory => string.IsNullOrEmpty(SourceRelativePathDirectory)
? null
- : Path.GetFileName(Path.GetFullPath(SourcePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
-
+ : Path.GetFileName(Path.GetFullPath(SourceRelativePathDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
///
/// Point to the site configuration.
@@ -100,9 +109,7 @@ internal class Page : IPage
///
public List? AliasesProcessed { get; set; }
- ///
- /// The URL for the content.
- ///
+ ///
public string? Permalink { get; set; }
///
@@ -111,12 +118,15 @@ internal class Page : IPage
///
public ConcurrentBag PagesReferences { get; } = new();
- ///
- /// Other content that mention this content.
- /// Used to create the tags list and Related Posts section.
- ///
+ ///
public IPage? Parent { get; set; }
+ ///
+ public BundleType BundleType { get; set; } = BundleType.none;
+
+ ///
+ public List? Resources { get; set; }
+
///
/// Plain markdown content, without HTML.
///
@@ -172,7 +182,6 @@ internal class Page : IPage
/// The processed output file content.
public string CompleteContent => ParseAndRenderTemplate(true, "Error parsing theme template: {Error}");
-
///
/// Other content that mention this content.
/// Used to create the tags list and Related Posts section.
@@ -189,7 +198,11 @@ internal class Page : IPage
pagesCached ??= new();
foreach (var permalink in PagesReferences)
{
- pagesCached.Add(Site.PagesReferences[permalink]);
+ var page = Site.OutputReferences[permalink] as IPage;
+ if (page is not null)
+ {
+ pagesCached.Add(page);
+ }
}
return pagesCached;
}
@@ -212,26 +225,44 @@ internal class Page : IPage
///
/// Get all URLs related to this content.
///
- public List Urls
+ public Dictionary AllOutputURLs
{
get
{
- var urls = new List();
+ var urls = new Dictionary();
+
if (Permalink is not null)
{
- urls.Add(Permalink);
+ urls.Add(Permalink, this);
}
if (AliasesProcessed is not null)
{
- urls.AddRange(from aliases in AliasesProcessed
- select aliases);
+ foreach (var alias in AliasesProcessed)
+ {
+ if (!urls.ContainsKey(alias))
+ {
+ urls.Add(alias, this);
+ }
+ }
+ }
+
+ if (Resources is not null)
+ {
+ foreach (var resource in Resources)
+ {
+ if (resource.Permalink is not null && !urls.ContainsKey(resource.Permalink))
+ {
+ urls.Add(resource.Permalink, resource);
+ }
+ }
}
return urls;
}
}
+
///
/// The markdown content.
///
@@ -320,6 +351,119 @@ endif
return Urlizer.UrlizePath(permaLink);
}
+ ///
+ public void PostProcess()
+ {
+ // Create all the aliases
+ if (Aliases is not null)
+ {
+ AliasesProcessed ??= new();
+ foreach (var alias in Aliases)
+ {
+ AliasesProcessed.Add(CreatePermalink(alias));
+ }
+ }
+
+ // Create tag pages, if any
+ if (Tags is not null)
+ {
+ foreach (var tagName in Tags)
+ {
+ Site.CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", this);
+ }
+ }
+
+ ScanForResources();
+ }
+
+ private int counterInternal = 0;
+ private bool counterInternalLock;
+ private int counter
+ {
+ get
+ {
+ if (!counterInternalLock)
+ {
+ counterInternalLock = true;
+ }
+ return counterInternal;
+ }
+ }
+
+ private void ScanForResources()
+ {
+ if (string.IsNullOrEmpty(SourceFullPathDirectory)) return;
+ if (BundleType == BundleType.none) return;
+
+ try
+ {
+ var resourceFiles = Directory.GetFiles(SourceFullPathDirectory)
+ .Where(file =>
+ file != SourceFullPath &&
+ (BundleType == BundleType.leaf || !file.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
+ );
+
+ foreach (var resourceFilename in resourceFiles)
+ {
+ Resources ??= new();
+ var filenameOriginal = Path.GetFileName(resourceFilename);
+ var filename = filenameOriginal;
+ var extention = Path.GetExtension(resourceFilename);
+ var title = filename;
+ Dictionary resourceParams = new();
+
+ if (ResourceDefinitions is not null)
+ {
+ if (counterInternalLock)
+ {
+ counterInternalLock = false;
+ ++counterInternal;
+ }
+ foreach (var resourceDefinition in ResourceDefinitions)
+ {
+ resourceDefinition.GlobMatcher ??= new();
+ resourceDefinition.GlobMatcher.AddInclude(resourceDefinition.Src);
+ var file = new InMemoryDirectoryInfo("./", new[] { filenameOriginal });
+ if (resourceDefinition.GlobMatcher.Execute(file).HasMatches)
+ {
+ if (Site.FluidParser.TryParse(resourceDefinition.Name, out var templateFileName, out var errorFileName))
+ {
+ var context = new TemplateContext(Site.TemplateOptions)
+ .SetValue("page", this)
+ .SetValue("site", Site)
+ .SetValue("counter", counter);
+ filename = templateFileName.Render(context);
+ }
+ if (Site.FluidParser.TryParse(resourceDefinition.Title, out var templateTitle, out var errorTitle))
+ {
+ var context = new TemplateContext(Site.TemplateOptions)
+ .SetValue("page", this)
+ .SetValue("site", Site)
+ .SetValue("counter", counter);
+ title = templateTitle.Render(context);
+ }
+ resourceParams = resourceDefinition.Params ?? new();
+ }
+ }
+ }
+
+ filename = Path.GetFileNameWithoutExtension(filename) + extention;
+ var resource = new Resource(resourceFilename)
+ {
+ Title = title,
+ FileName = filename,
+ Permalink = Path.Combine(Permalink!, filename),
+ Params = resourceParams
+ };
+ Resources.Add(resource);
+ }
+ }
+ catch
+ {
+ return;
+ }
+ }
+
private string ParseAndRenderTemplate(bool isBaseTemplate, string errorMessage)
{
var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site.CacheManager, isBaseTemplate);
diff --git a/source/Models/Resource.cs b/source/Models/Resource.cs
new file mode 100644
index 0000000000000000000000000000000000000000..66cf808245e2a8246559ba6cb4f5cdd048f52f95
--- /dev/null
+++ b/source/Models/Resource.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+
+namespace SuCoS.Models;
+
+///
+/// Page resources. All files that accompany a page.
+///
+public class Resource : IResource
+{
+ ///
+ public string? Title { get; set; }
+
+ ///
+ public string? FileName { get; set; }
+
+ ///
+ public string SourceFullPath { get; set; }
+
+ ///
+ public string? SourceRelativePath => throw new System.NotImplementedException();
+
+ ///
+ public string? Permalink { get; set; }
+
+ ///
+ public Dictionary Params { get; set; } = new();
+
+ ///
+ /// Default constructor.
+ ///
+ ///
+ public Resource(string path)
+ {
+ SourceFullPath = path;
+ }
+}
diff --git a/source/Models/Site.cs b/source/Models/Site.cs
index 560163139aa85d7d0463c28e4f647af5ac7fb47c..eb71d04b48853a64a4b6d44ee696dc9f48307601 100644
--- a/source/Models/Site.cs
+++ b/source/Models/Site.cs
@@ -16,7 +16,7 @@ namespace SuCoS.Models;
///
/// The main configuration of the program, primarily extracted from the app.yaml file.
///
-internal class Site : ISite
+public class Site : ISite
{
#region IParams
@@ -53,7 +53,7 @@ internal class Site : ISite
public string? Copyright => settings.Copyright;
///
- /// The base URL that will be used to build internal links.
+ /// The base URL that will be used to build public links.
///
public string BaseURL => settings.BaseURL;
@@ -91,9 +91,10 @@ internal class Site : ISite
{
get
{
- pagesCache ??= PagesReferences.Values
- .OrderBy(page => -page.Weight)
- .ToList();
+ pagesCache ??= OutputReferences.Values
+ .Where(output => output is IPage page)
+ .Select(output => (output as IPage)!)
+ .OrderBy(page => -page.Weight);
return pagesCache!;
}
}
@@ -101,20 +102,19 @@ internal class Site : ISite
///
/// List of all pages, including generated, by their permalink.
///
- public ConcurrentDictionary PagesReferences { get; } = new();
+ public ConcurrentDictionary OutputReferences { get; } = new();
///
/// List of pages from the content folder.
///
- public List RegularPages
+ public IEnumerable RegularPages
{
get
{
- regularPagesCache ??= PagesReferences
- .Where(pair => pair.Value.IsPage && pair.Key == pair.Value.Permalink)
- .Select(pair => pair.Value)
- .OrderBy(page => -page.Weight)
- .ToList();
+ regularPagesCache ??= OutputReferences
+ .Where(pair => pair.Value is IPage page && page.IsPage && pair.Key == page.Permalink)
+ .Select(pair => (pair.Value as IPage)!)
+ .OrderBy(page => -page.Weight);
return regularPagesCache;
}
}
@@ -149,9 +149,9 @@ internal class Site : ISite
///
public int filesParsedToReport;
- private const string indexFileConst = "index.md";
+ private const string indexLeafFileConst = "index.md";
- private const string indexFileUpperConst = "INDEX.MD";
+ private const string indexBranchFileConst = "_index.md";
///
/// The synchronization lock object during ProstProcess.
@@ -163,9 +163,9 @@ internal class Site : ISite
///
private readonly IFrontMatterParser frontMatterParser;
- private List? pagesCache;
+ private IEnumerable? pagesCache;
- private List? regularPagesCache;
+ private IEnumerable? regularPagesCache;
private readonly SiteSettings settings;
@@ -190,8 +190,9 @@ internal class Site : ISite
// Liquid template options, needed to theme the content
// but also parse URLs
- TemplateOptions.MemberAccessStrategy.Register();
TemplateOptions.MemberAccessStrategy.Register();
+ TemplateOptions.MemberAccessStrategy.Register();
+ TemplateOptions.MemberAccessStrategy.Register();
this.clock = clock ?? new SystemClock();
}
@@ -202,7 +203,7 @@ internal class Site : ISite
public void ResetCache()
{
CacheManager.ResetCache();
- PagesReferences.Clear();
+ OutputReferences.Clear();
}
///
@@ -223,7 +224,7 @@ internal class Site : ISite
_ = Parallel.ForEach(markdownFiles, filePath =>
{
- ParseSourceFile(parent, filePath);
+ ParseSourceFile(filePath, parent);
});
var subdirectories = Directory.GetDirectories(directory);
@@ -233,67 +234,6 @@ internal class Site : ISite
});
}
- private void ParseIndexPage(in string? directory, int level, ref IPage? parent, ref string[] markdownFiles)
- {
- // Check if the index.md file exists in the current directory
- var indexPage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file).ToUpperInvariant() == indexFileUpperConst);
- if (indexPage is not null)
- {
- markdownFiles = markdownFiles.Where(file => file != indexPage).ToArray();
- var page = ParseSourceFile(parent, indexPage);
- if (level == 0)
- {
- PagesReferences.TryRemove(page!.Permalink!, out _);
- Home = page;
- page.Permalink = "/";
- page.Kind = Kind.index;
- PagesReferences.GetOrAdd(page.Permalink, page);
- }
- else
- {
- parent = page;
- }
- }
-
- // If it's the home page
- else if (level == 0)
- {
- Home = CreateSystemPage(string.Empty, Title);
- }
-
- // Or a section page, which must be used as the parent for the next sub folder
- else if (level == 1)
- {
- var section = new DirectoryInfo(directory!).Name;
- parent = CreateSystemPage(section, section);
- }
- }
-
- private IPage? ParseSourceFile(in IPage? parent, in string filePath)
- {
- Page? page = null;
- try
- {
- var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath)
- ?? throw new FormatException($"Error parsing front matter for {filePath}");
-
- if (IsValidDate(frontMatter, Options))
- {
- page = new(frontMatter, this);
- PostProcessPage(page, parent, true);
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex, "Error parsing file {file}", filePath);
- }
-
- // Use interlocked to safely increment the counter in a multi-threaded environment
- _ = Interlocked.Increment(ref filesParsedToReport);
-
- return page;
- }
-
///
/// Creates the page for the site index.
///
@@ -302,7 +242,7 @@ internal class Site : ISite
///
///
/// The created page for the index.
- private IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null)
+ public IPage CreateSystemPage(string relativePath, string title, string? sectionName = null, IPage? originalPage = null)
{
sectionName ??= "section";
var isIndex = string.IsNullOrEmpty(relativePath);
@@ -310,7 +250,8 @@ internal class Site : ISite
{
Kind = isIndex ? Kind.index : Kind.list,
Section = isIndex ? "index" : sectionName,
- SourcePath = Path.Combine(relativePath, indexFileConst),
+ SourceRelativePath = Path.Combine(relativePath, indexLeafFileConst),
+ SourceFullPath = Path.Combine(SourceContentPath, relativePath, indexLeafFileConst),
Title = title,
Type = isIndex ? "index" : sectionName,
URL = relativePath
@@ -323,13 +264,16 @@ internal class Site : ISite
{
IPage? parent = null;
// Check if we need to create a section, even
- var sections = (frontMatter.SourcePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries);
+ var sections = (frontMatter.SourceRelativePathDirectory ?? string.Empty).Split('/', StringSplitOptions.RemoveEmptyEntries);
if (sections.Length > 1)
{
parent = CreateSystemPage(sections[0], sections[0]);
}
- var newPage = new Page(frontMatter, this);
+ var newPage = new Page(frontMatter, this)
+ {
+ BundleType = BundleType.branch
+ };
PostProcessPage(newPage, parent);
return newPage;
}));
@@ -356,6 +300,80 @@ internal class Site : ISite
return page;
}
+ private void ParseIndexPage(string? directory, int level, ref IPage? parent, ref string[] markdownFiles)
+ {
+ var indexLeafBundlePage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file) == indexLeafFileConst);
+
+ var indexBranchBundlePage = markdownFiles.FirstOrDefault(file => Path.GetFileName(file) == indexBranchFileConst);
+
+ IPage? page = null;
+ if (indexLeafBundlePage is not null || indexBranchBundlePage is not null)
+ {
+ // Determine the file to use and the bundle type
+ var selectedFile = indexLeafBundlePage ?? indexBranchBundlePage;
+ var bundleType = selectedFile == indexLeafBundlePage ? BundleType.leaf : BundleType.branch;
+
+ // Remove the selected file from markdownFiles
+ markdownFiles = bundleType == BundleType.leaf
+ ? new string[] { }
+ : markdownFiles.Where(file => file != selectedFile).ToArray();
+
+ page = ParseSourceFile(selectedFile!, parent, bundleType);
+ if (page is null) return;
+
+ if (level == 0)
+ {
+ OutputReferences.TryRemove(page!.Permalink!, out _);
+ page.Permalink = "/";
+ page.Kind = Kind.index;
+
+ OutputReferences.GetOrAdd(page.Permalink, page);
+ Home = page;
+ }
+ else
+ {
+ parent = page;
+ }
+ }
+ else if (level == 0)
+ {
+ Home = CreateSystemPage(string.Empty, Title);
+ }
+ else if (level == 1 && directory is not null)
+ {
+ var section = new DirectoryInfo(directory).Name;
+ parent = CreateSystemPage(section, section);
+ }
+ }
+
+ private IPage? ParseSourceFile(in string filePath, in IPage? parent, BundleType bundleType = BundleType.none)
+ {
+ Page? page = null;
+ try
+ {
+ var frontMatter = frontMatterParser.ParseFrontmatterAndMarkdownFromFile(filePath, SourceContentPath)
+ ?? throw new FormatException($"Error parsing front matter for {filePath}");
+
+ if (IsValidPage(frontMatter, Options))
+ {
+ page = new(frontMatter, this)
+ {
+ BundleType = bundleType
+ };
+ PostProcessPage(page, parent, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Error parsing file {file}", filePath);
+ }
+
+ // Use interlocked to safely increment the counter in a multi-threaded environment
+ _ = Interlocked.Increment(ref filesParsedToReport);
+
+ return page;
+ }
+
///
/// Extra calculation and automatic data for each page.
///
@@ -373,46 +391,35 @@ internal class Site : ISite
page.Permalink = page.CreatePermalink();
lock (syncLockPostProcess)
{
- if (!PagesReferences.TryGetValue(page.Permalink, out var old) || overwrite)
+ if (!OutputReferences.TryGetValue(page.Permalink, out var oldOutput) || overwrite)
{
- if (old?.PagesReferences is not null)
- {
- foreach (var pageOld in old.PagesReferences)
- {
- page.PagesReferences.Add(pageOld);
- }
- }
+ page.PostProcess();
- if (page.Aliases is not null)
+ // Replace the old page with the newly created one
+ if (oldOutput is IPage oldpage && oldpage?.PagesReferences is not null)
{
- page.AliasesProcessed ??= new();
- foreach (var alias in page.Aliases)
+ foreach (var pageOld in oldpage.PagesReferences)
{
- page.AliasesProcessed.Add(page.CreatePermalink(alias));
+ page.PagesReferences.Add(pageOld);
}
}
// Register the page for all urls
- foreach (var url in page.Urls)
+ foreach (var pageOutput in page.AllOutputURLs)
{
- PagesReferences.TryAdd(url, page);
+ OutputReferences.TryAdd(pageOutput.Key, pageOutput.Value);
}
}
}
- if (page.Tags is not null)
+ if (!string.IsNullOrEmpty(page.Section)
+ && OutputReferences.TryGetValue('/' + page.Section!, out var output))
{
- foreach (var tagName in page.Tags)
+ if (output is IPage section)
{
- CreateSystemPage(Path.Combine("tags", tagName), tagName, "tags", page);
+ section.PagesReferences.Add(page.Permalink!);
}
}
-
- if (!string.IsNullOrEmpty(page.Section)
- && PagesReferences.TryGetValue('/' + page.Section!, out var section))
- {
- section.PagesReferences.Add(page.Permalink!);
- }
}
///
diff --git a/source/Models/SiteSettings.cs b/source/Models/SiteSettings.cs
index 5a523508a84501d0f198724cbe0330332e8d1e6d..1c137b33f8075ea3109d474f651e23048f43ea0f 100644
--- a/source/Models/SiteSettings.cs
+++ b/source/Models/SiteSettings.cs
@@ -24,7 +24,7 @@ public class SiteSettings : IParams
public string? Copyright { get; set; }
///
- /// The base URL that will be used to build internal links.
+ /// The base URL that will be used to build public links.
///
public string BaseURL { get; set; } = string.Empty;
diff --git a/source/Parser/IFrontmatterParser.cs b/source/Parser/IFrontmatterParser.cs
index 0f1e023e91960d56cc6f6cd2f6eefebdfaa17863..67a536c412bc3f6965987fdc954f45b5e8662745 100644
--- a/source/Parser/IFrontmatterParser.cs
+++ b/source/Parser/IFrontmatterParser.cs
@@ -10,18 +10,19 @@ public interface IFrontMatterParser
///
/// Extract the front matter from the content file.
///
- ///
+ ///
///
///
- IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string filePath, in string sourceContentPath);
+ IFrontMatter? ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string sourceContentPath);
///
/// Extract the front matter from the content.
///
+ ///
///
- ///
+ ///
///
- IFrontMatter? ParseFrontmatterAndMarkdown(in string filePath, in string fileContent);
+ IFrontMatter? ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent);
///
/// Parse the app config file.
diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs
index 80b0c5292be382b8d6657a9dd19f91aab1065dfa..b827ce232f372596e92e52482026c0be3d4644b6 100644
--- a/source/Parser/YAMLParser.cs
+++ b/source/Parser/YAMLParser.cs
@@ -12,7 +12,7 @@ namespace SuCoS.Parser;
///
/// Responsible for parsing the content front matter using YAML
///
-internal class YAMLParser : IFrontMatterParser
+public class YAMLParser : IFrontMatterParser
{
///
/// YamlDotNet parser, strictly set to allow automatically parse only known fields
@@ -29,30 +29,30 @@ internal class YAMLParser : IFrontMatterParser
.Build();
///
- public IFrontMatter ParseFrontmatterAndMarkdownFromFile( in string filePath, in string? sourceContentPath = null)
+ public IFrontMatter ParseFrontmatterAndMarkdownFromFile(in string fileFullPath, in string? sourceContentPath = null)
{
- if (filePath is null)
+ if (fileFullPath is null)
{
- throw new ArgumentNullException(nameof(filePath));
+ throw new ArgumentNullException(nameof(fileFullPath));
}
string? fileContent;
string? fileRelativePath;
try
{
- fileContent = File.ReadAllText(filePath);
- fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, filePath);
+ fileContent = File.ReadAllText(fileFullPath);
+ fileRelativePath = Path.GetRelativePath(sourceContentPath ?? string.Empty, fileFullPath);
}
catch (Exception ex)
{
- throw new FileNotFoundException(filePath, ex);
+ throw new FileNotFoundException(fileFullPath, ex);
}
- return ParseFrontmatterAndMarkdown(fileRelativePath, fileContent);
+ return ParseFrontmatterAndMarkdown(fileFullPath, fileRelativePath, fileContent);
}
///
- public IFrontMatter ParseFrontmatterAndMarkdown(in string fileRelativePath, in string fileContent)
+ public IFrontMatter ParseFrontmatterAndMarkdown(in string fileFullPath, in string fileRelativePath, in string fileContent)
{
if (fileRelativePath is null)
{
@@ -74,18 +74,19 @@ internal class YAMLParser : IFrontMatterParser
var rawContent = content.ReadToEnd();
// Now, you can parse the YAML front matter
- var page = ParseYAML(fileRelativePath, yaml, rawContent);
+ var page = ParseYAML(fileFullPath, fileRelativePath, yaml, rawContent);
return page;
}
- private IFrontMatter ParseYAML(in string filePath, string yaml, in string rawContent)
+ private IFrontMatter ParseYAML(in string fileFullPath, in string fileRelativePath, string yaml, in string rawContent)
{
var frontMatter = yamlDeserializerRigid.Deserialize(new StringReader(yaml)) ?? throw new FormatException("Error parsing front matter");
- var section = SiteHelper.GetSection(filePath);
+ var section = SiteHelper.GetSection(fileRelativePath);
frontMatter.RawContent = rawContent;
frontMatter.Section = section;
- frontMatter.SourcePath = filePath;
+ frontMatter.SourceRelativePath = fileRelativePath;
+ frontMatter.SourceFullPath = fileFullPath;
frontMatter.Type ??= section;
var yamlObject = yamlDeserializer.Deserialize(new StringReader(yaml));
diff --git a/source/Program.cs b/source/Program.cs
index 2a118e39bab66b647f0e40472d568ae5afa54c9a..e42eb4baca832535884fa3907ccd9ba97b9e769d 100644
--- a/source/Program.cs
+++ b/source/Program.cs
@@ -10,9 +10,12 @@ namespace SuCoS;
///
/// The main entry point of the program.
///
-internal class Program
+public class Program
{
- internal const string helloWorld = @"
+ ///
+ /// Basic logo of the program, for fun
+ ///
+ public const string helloWorld = @"
____ ____ ____
/\ _`\ /\ _`\ /\ _`\
\ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\
@@ -44,7 +47,12 @@ internal class Program
this.logger = logger;
}
- internal int Run(string[] args)
+ ///
+ /// Actual entrypoint of the program
+ ///
+ ///
+ ///
+ public int Run(string[] args)
{
// Print the logo of the program.
OutputLogo();
@@ -121,7 +129,12 @@ internal class Program
return rootCommand.Invoke(args);
}
- internal static ILogger CreateLogger(bool verbose = false)
+ ///
+ /// Create a log (normally from Serilog), depending the verbose option
+ ///
+ ///
+ ///
+ public static ILogger CreateLogger(bool verbose = false)
{
return new LoggerConfiguration()
.MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information)
@@ -132,7 +145,7 @@ internal class Program
///
/// Print the name and version of the program.
///
- internal void OutputWelcome()
+ public void OutputWelcome()
{
var assembly = Assembly.GetEntryAssembly();
var assemblyName = assembly?.GetName();
@@ -141,7 +154,10 @@ internal class Program
logger.Information("{name} v{version}", appName, appVersion);
}
- internal void OutputLogo()
+ ///
+ /// Print the logo
+ ///
+ public void OutputLogo()
{
logger.Information(helloWorld);
}
diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs
index 14d19857a8ebc9f4654cb46702c3d8c4671a8651..eba8335bfd4785109d866453d6a841658998c4d5 100644
--- a/source/ServeCommand.cs
+++ b/source/ServeCommand.cs
@@ -15,7 +15,7 @@ namespace SuCoS;
///
/// Serve Command will live serve the site and watch any changes.
///
-internal class ServeCommand : BaseGeneratorCommand, IDisposable
+public class ServeCommand : BaseGeneratorCommand, IDisposable
{
private const string baseURLDefault = "http://localhost";
private const int portDefault = 1122;
@@ -105,7 +105,8 @@ internal class ServeCommand : BaseGeneratorCommand, IDisposable
new PingRequests(),
new StaticFileRequest(site.SourceStaticPath, false),
new StaticFileRequest(site.SourceThemeStaticPath, true),
- new RegisteredPageRequest(site)
+ new RegisteredPageRequest(site),
+ new RegisteredPageResourceRequest(site)
};
host = new WebHostBuilder()
diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs
index c287887468b7785b844495097aed2b5d3e91dcbc..38887009030528fe97c885ebbd6a017a36a71272 100644
--- a/source/ServerHandlers/IServerHandlers.cs
+++ b/source/ServerHandlers/IServerHandlers.cs
@@ -7,7 +7,7 @@ namespace SuCoS.ServerHandlers;
///
/// Handle server requests
///
-internal interface IServerHandlers
+public interface IServerHandlers
{
///
/// Check if the condition is met to handle the request
diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs
index 927d701f480b30cc42f3ed2028ffe7162b323767..4ae76ff54e46b19677ed2efa86d57c4c38202788 100644
--- a/source/ServerHandlers/PingRequests.cs
+++ b/source/ServerHandlers/PingRequests.cs
@@ -7,7 +7,7 @@ namespace SuCoS.ServerHandlers;
///
/// Return the server startup timestamp as the response
///
-internal class PingRequests : IServerHandlers
+public class PingRequests : IServerHandlers
{
///
public bool Check(string requestPath)
diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs
index 61feaeacb6e945b77b0538101ae3a4ec48bc5cc6..b98870c6e461eb501f3c9e57a9a8b58b17456c04 100644
--- a/source/ServerHandlers/RegisteredPageRequest.cs
+++ b/source/ServerHandlers/RegisteredPageRequest.cs
@@ -10,10 +10,14 @@ namespace SuCoS.ServerHandlers;
///
/// Return the server startup timestamp as the response
///
-internal class RegisteredPageRequest : IServerHandlers
+public class RegisteredPageRequest : IServerHandlers
{
readonly ISite site;
+ ///
+ /// Constructor
+ ///
+ ///
public RegisteredPageRequest(ISite site)
{
this.site = site;
@@ -26,7 +30,7 @@ internal class RegisteredPageRequest : IServerHandlers
{
throw new ArgumentNullException(nameof(requestPath));
}
- return site.PagesReferences.TryGetValue(requestPath, out _);
+ return site.OutputReferences.TryGetValue(requestPath, out var item) && item is IPage _;
}
///
@@ -36,11 +40,18 @@ internal class RegisteredPageRequest : IServerHandlers
{
throw new ArgumentNullException(nameof(context));
}
- site.PagesReferences.TryGetValue(requestPath, out var page);
- var content = page!.CompleteContent;
- content = InjectReloadScript(content);
- await context.Response.WriteAsync(content);
- return "dict";
+
+ if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IPage page)
+ {
+ var content = page.CompleteContent;
+ content = InjectReloadScript(content);
+ await context.Response.WriteAsync(content);
+ return "dict";
+ }
+ else
+ {
+ return "404";
+ }
}
///
diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..595c02e27771f3e0c9f2a43cafed5a69421e634a
--- /dev/null
+++ b/source/ServerHandlers/RegisteredPageResourceRequest.cs
@@ -0,0 +1,81 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using SuCoS.Models;
+
+namespace SuCoS.ServerHandlers;
+
+///
+/// Return the server startup timestamp as the response
+///
+public class RegisteredPageResourceRequest : IServerHandlers
+{
+ readonly ISite site;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public RegisteredPageResourceRequest(ISite site)
+ {
+ this.site = site;
+ }
+
+ ///
+ public bool Check(string requestPath)
+ {
+ if (requestPath is null)
+ {
+ throw new ArgumentNullException(nameof(requestPath));
+ }
+ return site.OutputReferences.TryGetValue(requestPath, out var item) && item is IResource _;
+ }
+
+ ///
+ public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IResource resource)
+ {
+ context.Response.ContentType = resource.MimeType;
+ await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ await fileStream.CopyToAsync(context.Response.Body);
+ return "resource";
+ }
+ else
+ {
+ return "404";
+ }
+ }
+
+ ///
+ /// Injects a reload script into the provided content.
+ /// The script is read from a JavaScript file and injected before the closing "body" tag.
+ ///
+ /// The content to inject the reload script into.
+ /// The content with the reload script injected.
+ private string InjectReloadScript(string content)
+ {
+ // Read the content of the JavaScript file
+ string scriptContent;
+ var assembly = Assembly.GetExecutingAssembly();
+ using var stream = assembly.GetManifestResourceStream("SuCoS.wwwroot.js.reload.js")
+ ?? throw new FileNotFoundException("Could not find the embedded JavaScript resource.");
+ using var reader = new StreamReader(stream);
+ scriptContent = reader.ReadToEnd();
+
+ // Inject the JavaScript content
+ var reloadScript = $"";
+
+ const string bodyClosingTag = "