From 18ed0534b744a11acf4b6c3cd1fa7c1a7a799578 Mon Sep 17 00:00:00 2001
From: Robert Morrison
Date: Tue, 16 Jan 2024 11:00:35 +0000
Subject: [PATCH 1/2] refactor(EVERYTHING): Refactor all the things.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
With a little bit of OOP and a monster commit, I too can make a an ssg,
Ka-chow...
Changes:
- ➕ added class to represent SiteFile
- ➕ enabled conversion semi-automatic based on file type.
- ➕ added template to Testing.
- ➖ Removed awful code for dependency search
- ➖ Removed awful code for enumerating directory
- ➕ arguments to a class to allow for easier passing to other
parts of the code.
TODO:
- 🐞Test and debug with a copy of a live site,
- ✍️ Add handling for Pandoc errors on stderr
- ❓Look into parallelising as much as possible.
---
Program.cs | 394 +++++++++---------------
RuntimeSettings/RuntimeSettings.cs | 29 ++
SiteFile/SiteFile.ConverterFunctions.cs | 158 ++++++++++
SiteFile/SiteFile.cs | 138 +++++++++
TODO | 5 +
Testing/src/.template | 92 ++++++
Utils/Utils.GetFiles.cs | 9 +
Utils/Utils.PathSearch.cs | 52 ++++
csSiteGen.csproj | 8 +-
9 files changed, 633 insertions(+), 252 deletions(-)
create mode 100644 RuntimeSettings/RuntimeSettings.cs
create mode 100644 SiteFile/SiteFile.ConverterFunctions.cs
create mode 100644 SiteFile/SiteFile.cs
create mode 100644 TODO
create mode 100755 Testing/src/.template
create mode 100644 Utils/Utils.GetFiles.cs
create mode 100644 Utils/Utils.PathSearch.cs
diff --git a/Program.cs b/Program.cs
index d4a9119..edfe1f8 100644
--- a/Program.cs
+++ b/Program.cs
@@ -16,12 +16,15 @@
* along with this program. If not, see .
*/
-using Serilog;
using Serilog.Events;
-using System.Reflection;
-using System.Text.RegularExpressions;
+using Serilog;
+using System.CommandLine.Builder;
+using System.CommandLine.Help;
+using System.CommandLine.Parsing;
+using System.CommandLine;
using System.Diagnostics;
-using System.Text.Json;
+using System.Reflection;
+using Spectre.Console;
namespace csSiteGen;
@@ -30,21 +33,6 @@ class Program
static int Main(string[] args)
{
- // Default values
- List _verbatimFileTypes = new List{ // file extenstions we want to have copied verbatim
- "html", // Premade html pages
- "css", // Style Sheets
- // IMAGE TYPES TODO: add webp conversion for most images.
- "jpg",
- "png",
- "webp",
- "gif"
- };
- // TODO: Make this a dictionary/tuple to show a FROM -> TO filetype relationship (Possibly with program)
- List _conversionTypes = new List{ // file extenstions we want to convert to html
- "md"
- };
-
// Get the current versiion number
string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion;
@@ -52,7 +40,6 @@ class Program
// Configure logger
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
- .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.WriteTo.File("log.log")
.CreateLogger();
@@ -64,264 +51,173 @@ class Program
}
else
{
- Log.Error("Cannot get Version Information");
- }
-
- // Exit on argument errors
- if (args.Length < 2)
- {
- Log.Error("Too Few Args: Expected at least 2 but received {Count}",args.Length);
- return 1;
+ Log.Warning("Cannot get Version Information");
}
// It is very likely that this program will only work on linux. As such it is worth warning the user about this.
if (!OperatingSystem.IsLinux())
{
Log.Warning("This program has only been tested on linux and cannot be assumed to work on other Operating Systems");
+ // AnsiConsole.MarkupLine("[[[yellow]Warning[/]]] This program is only tested on linux systems, it may not work on this Operating System.");
}
- // Get and check the directories passed to the program
- string _inputDirectory = args[0];
- string _outputDirectory = args[1];
+ Stopwatch TotalExecutionTime = Stopwatch.StartNew();
- if (!Directory.Exists(_inputDirectory))
- {
- Log.Error("Input directory '{i}' Does not exist or is not a directory you have access to" , _inputDirectory);
- Environment.Exit(1);
- }
- if (!Directory.Exists(_outputDirectory))
- {
- Log.Error("Output directory '{o}' Does not exist or is not a directory you have access to" , _outputDirectory);
- Environment.Exit(1);
- }
-
- // Test for dependencies
- List deps = new List{"pandoc"};
- var dep = CheckDeps(deps);
- Log.Debug("CheckDeps result: {@dep}",dep);
-
- // Deal with the dependency test results
- Dictionary depsDict = new();
- if (dep.Item2 is null)
- {
- Log.Debug("Ignoring dependency check as no dependencies listed");
- }
- else
- {
- depsDict = dep.Item2;
- }
- if (!dep.Item1)
- {
- foreach (var dependency in depsDict)
- {
- Log.Error("Dependency {dependency} Cannot be found\nPlease install it or check it is in your PATH",dependency.Key);
- }
- Log.CloseAndFlush();
- Environment.Exit(1);
- }
-
- List ConvertableInputFiles = new();
- foreach (var ext in _conversionTypes)
- {
- ConvertableInputFiles.AddRange(GetAllFilesMatching($".*{ext}",_inputDirectory));
- }
- Log.Debug("Files matching conversiontypes: {@conversionType} found {count} \n {files} ",_conversionTypes,ConvertableInputFiles.Count(),ConvertableInputFiles);
-
- Dictionary? metadata = null;
- if (File.Exists($"{_inputDirectory}/.files"))
- {
- Log.Information("Loading metadata for {src}",_inputDirectory);
- metadata = new();
- string metaJSON = File.ReadAllText($"{_inputDirectory}/.files");
- try
- {
- Type metadataType = metadata.GetType();
- metadata = JsonSerializer.Deserialize>(metaJSON);
- }
- catch (JsonException e)
- {
- Log.Debug(e,"Cannot deserialize .files Json data");
- Log.Debug("As this is possibly caused by an internal interface change the file will be deleted");
- File.Delete($"{_inputDirectory}/.files");
- }
- }
-
- if (metadata is not null)
- {
- // Use metadata to determine if files have been updated
- // A shadow list is used here so we can iterate and modify at the same time
- List shadow = new List(ConvertableInputFiles);
- foreach (string file in shadow)
- {
- if (metadata[file] == new FileInfo(file).LastWriteTimeUtc)
+ var inputDirectoryOption = new Option(
+ name: "--input",
+ description: "The directory that contains the site source.");
+ inputDirectoryOption.IsRequired = true;
+ inputDirectoryOption.AddValidator(result =>
{
- Log.Debug("File {file} has not been updated since last run. ignoring",file);
- ConvertableInputFiles.Remove(file);
- }
- }
- }
- Log.Information("{count} Files need converting",ConvertableInputFiles.Count());
- Log.Debug("Files: {files}",ConvertableInputFiles);
+ if (!result.GetValueForOption(inputDirectoryOption)!.Exists)
+ {
+ result.ErrorMessage = $"Input directory {result.GetValueForOption(inputDirectoryOption)!.FullName} does not exist";
+ }
+ });
+ var outputDirectoryOption = new Option(
+ name: "--output",
+ description: "The directory that the site should be output to.");
+ outputDirectoryOption.IsRequired = true;
+
+ var rootCommand = new RootCommand("csSiteGen");
+
+ var cleanCommand = new Command("clean", "Clean the output directory");
+ cleanCommand.AddOption(outputDirectoryOption);
+ cleanCommand.SetHandler(async (directory) =>
+ {
+ await Task.Run(() =>
+ {
+ Clean(directory);
+ });
+ } ,outputDirectoryOption);
+
+ var convertCommand = new Command("convert", "Convert the input directory and place the files in the output directory.");
+ convertCommand.AddOption(inputDirectoryOption);
+ convertCommand.AddOption(outputDirectoryOption);
+ convertCommand.SetHandler(async (inputDir, outputDir) =>
+ {
+ await Task.Run(() =>
+ {
+ Convert(inputDir, outputDir);
+ });
+ }, inputDirectoryOption, outputDirectoryOption);
+
+ rootCommand.AddCommand(cleanCommand);
+ rootCommand.AddCommand(convertCommand);
+
+ var parser = new CommandLineBuilder(rootCommand)
+ .UseDefaults()
+ .Build();
+
+ parser.Invoke(args);
+
+
+ TotalExecutionTime.Stop();
+ Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds);
Log.CloseAndFlush();
return 0;
}
-///
-/// Returns a list of all the files in the given directory and all subdirectories recursive
-///
-///
-/// Currently this function ignores reparse points (symlinks) so as to avoid infinite loops.
-/// It is probably a good idea to make the function aware of loops so it can read symlinks.
-///
-///
-/// The directory to find files in.
-///
-///
-/// A string list of every file in the provided directory and all subdirectories
-///
- static List GetAllFiles(string directory)
+ static int Convert(DirectoryInfo inputDir, DirectoryInfo outputDir)
{
- List res = new();
- Stack dirs = new();
+ AnsiConsole.Console.Profile.Capabilities.Ansi = true;
- dirs.Push(directory);
- Log.Debug("Dirs Starting as: {DirStack}",dirs );
- Stopwatch timer = new();
- timer.Start();
- while (dirs.Count > 0)
- {
- var dir = dirs.Pop();
- res.AddRange(Directory.GetFiles(dir));
- foreach (string subdir in Directory.GetDirectories(dir))
- {
- if(File.GetAttributes(subdir).HasFlag(FileAttributes.ReparsePoint))
+ List siteFiles = new();
+
+ Utils.GetFiles(inputDir).ForEach(x => siteFiles.Add(new SiteFile(x)));
+ Log.Information("SiteFiles: {@sf} {count}", siteFiles, siteFiles.Count);
+
+ Console.WriteLine($"Converting {siteFiles.Count} files from {inputDir.FullName} to {outputDir.FullName}");
+ RuntimeSettings settings = new(inputDir,outputDir);
+
+
+ Dictionary fileStatus = new();
+ Log.Debug("fileStatus {@fileStatus}",fileStatus);
+
+ AnsiConsole.Progress()
+ .AutoRefresh(true)
+ .Columns(new ProgressColumn[]
{
- /*
- * FIXME: This is only a temporary measure and should not be relied upon
- * if possible there should be a depth detection system that allows for
- * safe symlink following.
- */
- Log.Debug("Directory {subdir} is a ReparsePoint(symlink) and has been ignored",subdir);
- continue;
+ new TaskDescriptionColumn(),
+ new ProgressBarColumn(),
+ new PercentageColumn(),
+ new RemainingTimeColumn(),
+ new SpinnerColumn()
+ })
+ .Start(ctx =>
+ {
+ var tasks = siteFiles.Select(x => ctx.AddTask($"Converting {x.Name}")).ToList();
+ var overallTask = ctx.AddTask("[bold]Converting Files[/]");
+ overallTask.MaxValue = siteFiles.Count();
+
+ for (int i = 0; i < siteFiles.Count; i++)
+ {
+ tasks[i].MaxValue = 1;
+ Log.Information("Converting file {name} {i}/{count}",siteFiles[i].Name,i+1,siteFiles.Count());
+ bool res = siteFiles[i].Convert(settings);
+ tasks[i].Increment(1);
+ tasks[i].StopTask();
+ if (!res)
+ {
+ Log.Warning("{name} Failed...",siteFiles[i].FullName);
+ tasks[i].Description += " [red]FAILED[/]";
+ }
+ Log.Information("adding {@siteFile} conversion status to fileStatus", siteFiles[i]);
+ Log.Debug("FileStatus {@fileStatus}",fileStatus);
+ fileStatus.Add(siteFiles[i].FullName,res);
+ overallTask.Increment(1);
}
- dirs.Push(subdir);
- Log.Debug("Adding Directory {subdir} to dirs\nResult: {dirs}",subdir,dirs);
- }
- }
- timer.Stop();
- Log.Information("Crawled {directory} in {time}ms with {number} results",directory,timer.ElapsedMilliseconds,res.Count());
- return res;
- }
+ });
- static List GetAllFilesMatching(string pattern, string directory)
- {
- List res = new();
- List files = GetAllFiles(directory);
+ Log.Information("Conversion Status {@status}", fileStatus);
- Regex expression = new Regex(pattern,
- RegexOptions.Compiled);
-
- res = files.FindAll( x => expression.IsMatch(x)); // Here a lambda function is used as a predicate to do regex across the whole file list
-
- return res;
- }
-
- void Usage()
- {
- /*
- * This function will be called when argument errors happen. This will
- * also be called if the user triggers the help flag currently this
- * function throws a not implemented exception since it I cannot write
- * the usage until I have other functionality
- */
- throw new NotImplementedException();
- }
-
- static Tuple?> CheckDeps(List? dependencies)
- {
- bool success = true;
- Stopwatch timer = new();
- if (! (dependencies?.Count() > 0)) // assume success as no dependencies
+ var Failed = fileStatus.Where(x => x.Value == false);
+ if ( Failed.Count() > 0)
{
- return new Tuple?>(true,null);
- }
-
-
- var path = System.Environment.GetEnvironmentVariable("PATH")?.Split(':');
- if (path is null)
- {
- Log.Error("Cannot read system path.");
- Environment.Exit(1);
- }
- Log.Debug("Path is: {path}",path);
-
- List executables = new();
- timer = Stopwatch.StartNew();
- foreach (string dir in path)
- {
- if (Directory.Exists(dir)) // Sometimes People fuck up their path variables and include directories that don't exist.
+ foreach (var fail in Failed)
{
- executables.AddRange(GetAllFiles(dir));
- }
- }
- timer.Stop();
- Log.Verbose("Executables are: {executables}",executables);
- Log.Information("Got executables list in {timer:000}ms",timer.ElapsedMilliseconds);
-
- // Do some regex magic to find an executable in the path.
- Dictionary? result = new();
- Dictionary? failures = new();
-
- foreach (string dependency in dependencies)
- {
- timer = Stopwatch.StartNew();
- string regex = $"/(.*/)*{dependency}";
- Regex expr = new Regex(regex,RegexOptions.Compiled);
- Log.Debug("Compiled regex: {expr}",expr);
- string? match = executables.Find( x => expr.IsMatch(x));
- if (match is not null && match != "")
- {
- timer.Stop();
- Log.Information("Dependency {dep} found at {path} in {timer:000}ms",dependency,match,timer.ElapsedMilliseconds);
- result.Add(dependency,match); // Create a dictionary of paths to found executables
- }
- else
- {
- timer.Stop();
- Log.Information("Dependency {dep} cannot be found, search took {timer:000ms}",dependency,timer.ElapsedMilliseconds);
- success = false;
- failures.Add(dependency,dependency);
+ AnsiConsole.MarkupLineInterpolated(
+ $"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]");
+ AnsiConsole.MarkupLine("[yellow]See log for more details[/]");
}
}
- if (success)
- {
- return new Tuple?>(success,result);
- }
- else
- {
- return new Tuple?>(success,failures);
- }
+ return 0;
}
- static bool Convert(Dictionary files)
+ static int Clean(DirectoryInfo outputDir)
{
-
- return false;
+ if (!outputDir.Exists)
+ {
+ Log.Warning("Not deleting {dir} as it doesn't exist",outputDir.FullName);
+ AnsiConsole.MarkupLineInterpolated($"[bold][[[yellow]Warning[/]]][/] Not cleaning [blue]\"{outputDir}\"[/] as it does not exist.");
+ return 0; // success because it doesn't exist.
+ }
+ try
+ {
+ Log.Information("Cleaning {dir}",outputDir.FullName);
+ AnsiConsole.MarkupInterpolated($"Cleaning [blue]\"{outputDir.FullName}\"[/]");
+ outputDir.Delete(recursive: true);
+ outputDir.Create();
+ AnsiConsole.MarkupLine(" [bold][[[green]OK[/]]][/]");
+ AnsiConsole.MarkupLineInterpolated($"\t[grey]>>[/] [green]All files in {outputDir.FullName} purged successfully[/]");
+ return 0;
+ }
+ catch (System.Security.SecurityException e)
+ {
+ AnsiConsole.MarkupLine(" [bold][[[red]Fail[/]]][/]");
+ AnsiConsole.MarkupLine("[orangered1]See log for more details about what went wrong.[/]");
+ Log.Error(e, "Failed to delete directory {dir} due to permission error.", outputDir.FullName);
+ return 1;
+ }
+ catch (Exception e)
+ {
+ AnsiConsole.MarkupLine(" [red][[[bold]Fail[/]]][/]");
+ AnsiConsole.MarkupLine("[orangered1]See log for more details about what went wrong.[/]");
+ Log.Error(e, "Failed to delete/create directory {dir}", outputDir.FullName);
+ return 1;
+ }
}
}
- // if (!File.Exists($"{_inputDirectory}/.files"))
- // {
- // Log.Information("Creating metadata for {src}",_inputDirectory);
- // metadata = new();
- // foreach (var file in ConvertableInputFiles)
- // {
- // FileInfo info = new FileInfo(file);
- // DateTime LastMod = info.LastWriteTimeUtc;
- // metadata.Add(file,LastMod);
- // string metaJSON = JsonSerializer.Serialize(metadata);
- // File.WriteAllText($"{_inputDirectory}/.files",metaJSON);
- // }
- // }
diff --git a/RuntimeSettings/RuntimeSettings.cs b/RuntimeSettings/RuntimeSettings.cs
new file mode 100644
index 0000000..92aff3e
--- /dev/null
+++ b/RuntimeSettings/RuntimeSettings.cs
@@ -0,0 +1,29 @@
+namespace csSiteGen;
+
+
+///
+/// Class RuntimeSettings
Contains all the settings that could be loaded from the commandline.
+///
+public class RuntimeSettings {
+ public DirectoryInfo InputDirectory {get; private set;}
+ public DirectoryInfo OutputDirectory {get; private set;}
+
+
+ public RuntimeSettings(string inputDirectory, string outputDirectory){
+ InputDirectory = new DirectoryInfo(inputDirectory);
+ OutputDirectory = new DirectoryInfo(outputDirectory);
+
+ /* NOTE: it is the responisbility of the UI code to check the values passed are good.
+ */
+ }
+
+
+ public RuntimeSettings(DirectoryInfo inputDirectory, DirectoryInfo outputDirectory){
+ InputDirectory = inputDirectory;
+ OutputDirectory = outputDirectory;
+
+ /* NOTE: it is the responisbility of the UI code to check the values passed are good.
+ */
+ }
+
+}
diff --git a/SiteFile/SiteFile.ConverterFunctions.cs b/SiteFile/SiteFile.ConverterFunctions.cs
new file mode 100644
index 0000000..d15a414
--- /dev/null
+++ b/SiteFile/SiteFile.ConverterFunctions.cs
@@ -0,0 +1,158 @@
+using Serilog;
+using Spectre.Console;
+using System.Diagnostics;
+namespace csSiteGen;
+
+public static class Conversions{
+
+
+ public delegate bool ConvertFunc(FileInfo file, RuntimeSettings settings);
+
+ ///
+ /// A Mapping of filetype to ConvertFunc.
+ ///
+ public static readonly Dictionary Mappings = new(){
+ {".md", Pandoc},
+ };
+
+
+ ///
+ /// TEST FUNCTION.
+ ///
+ public static bool NoOp(FileInfo file, RuntimeSettings settings){
+ Log.Information("Performing NoOp Conversion");
+ string newName = GetNewName(file,settings,"NoOp");
+ Log.Debug("{FullName} -> {newName}",file.FullName,newName);
+ Thread.Sleep(1500);
+ return true;
+ }
+
+ ///
+ /// Copy the file verbatim
+ ///
+ public static bool RawCpy(FileInfo file, RuntimeSettings settings){
+ FileInfo newPath = new FileInfo(GetNewName(file,settings,null));
+
+ Log.Information("RawCpy: Copying {file} to {dest}",file.FullName, newPath.FullName);
+
+ if (!newPath.Directory!.Exists)
+ {
+ newPath.Directory.Create();
+ }
+
+ try {
+ file.CopyTo(newPath.FullName, overwrite: true);
+ }
+ catch (Exception e){
+ Log.Fatal(e,"Copy Failed");
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Execute pandoc on the file, automatically detecting the template to use.
+ ///
+ public static bool Pandoc(FileInfo file, RuntimeSettings settings){
+ Log.Information("Attempting to convert {file} using pandoc",file.Name);
+
+ // Look for pandoc
+ string pandoc = Utils.PathSearch("pandoc");
+ if (string.IsNullOrEmpty(pandoc))
+ {
+ Console.WriteLine("Conversion failed due to dependency being unavailable.");
+ return false;
+ }
+ Log.Information("Located pandoc binary.");
+
+ // Look for template file.
+ FileInfo? template = null;
+ DirectoryInfo? searchDir = file.Directory;
+ do
+ {
+ // This loop starts searching at the directory of the file,
+ // and if a template is not found works up to the InputDirectory.
+ // If no template is found at any level we simply run pandoc with no template.
+
+ if (searchDir is null) // While it is unlikely this could happen the check is here to please the compiler.
+ {
+ break;
+ }
+
+ var result = searchDir.GetFiles(".template");
+ if (result.Length > 0)
+ {
+ template = result.First();
+ Log.Information("Found template {template}", template);
+ break;
+ }
+
+ // Go up the tree.
+ searchDir = searchDir.Parent;
+ } while (searchDir != settings.InputDirectory); // Check last as we want to search the InputDirectory
+
+ string pandocArgs = $"{file.FullName} -o {GetNewName(file,settings,".html")}";
+
+ if (template is not null)
+ {
+ pandocArgs += $" --template={template.FullName}";
+ }
+ else
+ {
+ AnsiConsole.MarkupLine("[bold][[[orange1] Warning [/]]][/] Pandoc Template was not located.");
+ Log.Warning("Pandoc template for {file} not found",file.Name);
+ }
+
+ return RunExternalProgram(pandoc,pandocArgs);
+
+ }
+
+ private static bool RunExternalProgram(string program, string args)
+ {
+
+ Log.Information("Executing {program}", program);
+ Log.Debug("Full arguments {args}",args);
+
+ using (Process RunProgram = new())
+ {
+ // Reading stderr and stdout needs to be done carefully.
+ string? stdout = null;
+ string? stderr = null;
+
+ RunProgram.StartInfo.UseShellExecute = false;
+ RunProgram.StartInfo.FileName = program;
+ RunProgram.StartInfo.CreateNoWindow = true;
+ RunProgram.StartInfo.Arguments = args;
+ RunProgram.StartInfo.RedirectStandardOutput = true;
+ RunProgram.StartInfo.RedirectStandardError = true;
+
+ // Add a handler to append stderr to the stderr string
+ RunProgram.ErrorDataReceived += new DataReceivedEventHandler((sender, e) =>
+ {stderr += e.Data;});
+ RunProgram.OutputDataReceived += new DataReceivedEventHandler((sender, o) =>
+ {stdout += o.Data;});
+
+ RunProgram.Start();
+ RunProgram.BeginErrorReadLine();
+ RunProgram.BeginOutputReadLine();
+
+ RunProgram.WaitForExit();
+
+ Log.Debug("{program} stdout {stdout}", program, stdout);
+ Log.Debug("{program} stderr {stderr}", program, stderr);
+
+ if (RunProgram.ExitCode != 0)
+ {
+ Log.Error("{program} execution Failed. Check debug data for more information", program);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static string GetNewName(FileInfo file, RuntimeSettings settings, string? newExtension){
+ return file.FullName
+ .Replace(settings.InputDirectory.FullName, settings.OutputDirectory.FullName)
+ .Replace(file.Extension,newExtension ?? file.Extension);
+ }
+}
diff --git a/SiteFile/SiteFile.cs b/SiteFile/SiteFile.cs
new file mode 100644
index 0000000..8271bbe
--- /dev/null
+++ b/SiteFile/SiteFile.cs
@@ -0,0 +1,138 @@
+using System.Text.Json;
+using Serilog;
+namespace csSiteGen;
+
+///
+/// A SiteFile represents an individual file to be converted for the static site.
+///
+public partial class SiteFile
+{
+ FileInfo info;
+ Conversions.ConvertFunc ConverterFunction;
+ static Dictionary? Metadata = null;
+
+ ///
+ /// The name of the file, Not Guaranteed to be unique.
+ /// Use only for output and logging, never file operations,
+ /// nor as a Dictionary key.
+ ///
+ public string Name => info.Name;
+
+ ///
+ /// The FullName (or Path) of the file, This is unique as each file can only be found once.
+ /// Use for file operations and Dictionary keys, or anywhere else you need to avoid ambiguity.
+ ///
+ public string FullName => info.FullName;
+
+ public SiteFile(FileInfo fileInfo)
+ {
+ info = fileInfo;
+
+ Log.Information("{file} extension is {ext}",fileInfo.FullName, fileInfo.Extension);
+ // Using this Ensures that the ConverterFunction is Always set.
+ // ConverterFunctions ALWAYS accept just the FileInfo, and RuntimeSettings passed at convert time.
+ ConverterFunction = Conversions.Mappings.GetValueOrDefault(info.Extension, Conversions.RawCpy);
+ }
+
+ ///
+ /// Convert the file, placing it in the correct place in the output directory.
+ /// If a filetype conversion is not needed, or specified, then the file is simply copied.
+ ///
+ public bool Convert(RuntimeSettings settings)
+ {
+ if (!NeedsUpdating(settings))
+ {
+ Log.Information("Ignoring {name}, as it has not been changed since last run.", info.FullName);
+ return true;
+ }
+ bool res = ConverterFunction(info, settings);
+
+ if (res)
+ {
+ Log.Information("Converted sucessfully, updating metadata");
+ if (Metadata!.ContainsKey(info.FullName))
+ {
+ Metadata[info.FullName] = info.LastWriteTimeUtc;
+ }
+ else
+ {
+ Metadata.Add(info.FullName,info.LastWriteTimeUtc);
+ }
+ Log.Debug("Metadata Dictionary now {@Metadata}", Metadata);
+ SaveMetadata(settings);
+ }
+
+ return res;
+ }
+
+ private bool NeedsUpdating(RuntimeSettings settings)
+ {
+ if (Metadata is null)
+ {
+ LoadMetadata(settings);
+ }
+
+ // NOTE: By this point Metadata CANNOT be null as LoadMetadata would either have loaded the JSON or instantiated MetaData
+ if (Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) == info.LastWriteTimeUtc)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /*
+ * NOTE: It may seem wrong to save the metadata in the output directory.
+ * But that ensures that if you remove the output directory the site will be
+ * Fully recreated.
+ */
+ private void LoadMetadata(RuntimeSettings settings)
+ {
+ string metaFile = $"{settings.OutputDirectory}/.files";
+ Log.Information("Loading Metadata from {file}",metaFile);
+ try
+ {
+ string metaJson = File.ReadAllText(metaFile);
+ Log.Debug("Read Json {metaJson}", metaJson);
+ Metadata = JsonSerializer.Deserialize>(metaJson);
+ Log.Debug("Deserialized to {@Metadata}",Metadata);
+ }
+ catch (IOException e)
+ {
+ Log.Warning(e,"Error reading .files metafile");
+ Metadata = new();
+ return;
+ }
+ catch (JsonException e)
+ {
+ Log.Information(e, "Failed to deserialize .files Json data");
+ Log.Information("The metadata file will be deleted as it is likely corrupted.");
+ File.Delete(metaFile);
+ Metadata = new();
+ }
+ }
+
+ private void SaveMetadata(RuntimeSettings settings)
+ {
+ if (Metadata is null)
+ {
+ Log.Warning("Attempted to save null Metadata");
+ return;
+ }
+ string metaFile = $"{settings.OutputDirectory}/.files";
+ string metaJson;
+ try
+ {
+ metaJson = JsonSerializer.Serialize>(Metadata);
+ Log.Debug("metaJson: {metaJson}",metaJson);
+ }
+ catch (JsonException e)
+ {
+ Log.Warning(e, "Failed to serialize .files Json data");
+ return;
+ }
+ Log.Information("Writing metadata");
+ File.WriteAllText(metaFile, metaJson);
+ }
+}
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..01c937f
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+- Add a pre-commit or other type of hook to ensure that the Testing directory is properly cleaned and reset before commit.
+- Double check that links are processed correctly by testing against a copy of my blog source.
+ - Also add the appropriate metadata to the markdown files to make sure that they are properly detected.
+ - Potentially host a local Test version to fully ensure that everything can be made to work properly.
+- Look into the possiblity of using a TemplateTemplate, And generating the pandoc template on the fly making it possible to switch out domian/prefix stuff
diff --git a/Testing/src/.template b/Testing/src/.template
new file mode 100755
index 0000000..9f2ef31
--- /dev/null
+++ b/Testing/src/.template
@@ -0,0 +1,92 @@
+
+
+
+$-- #### STATIC HEAD ELEMENTS
+
+
+
+
+
+
+
+
+$-- #### VARIABLE HEAD ELEMENTS
+ $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$
+
+
+$for(author-meta)$ $-- Include author information
+
+$endfor$
+$if(date-meta)$ $-- Include date information
+
+$endif$
+$if(keywords)$ $-- Include Keywords
+
+$endif$
+$if(description-meta)$ $-- Include description
+
+
+$endif$
+$if(image)$ $-- OPTIONAL set image for link cards
+
+$else$ $-- Defaults to favicon
+
+$endif$
+$if(alt-text)$ $-- OPTIONAL Include alt text for twitter
+
+$endif$
+
+$if(math)$ $-- I think this includes some kind of css
+ $math$
+$endif$
+
+$for(header-includes)$ $-- Include any external data
+ $header-includes$
+$endfor$
+
+$if(needs-syntax-highlighting)$ $-- If set then include css and JS for syntax highlighting
+
+
+$endif$
+$if(style)$ $-- Include page specfic style (To reduce size of page load on other pages)
+
+$endif$
+
+ $-- default to sans-serif --$
+
+$body$
+
+
+
+
diff --git a/Utils/Utils.GetFiles.cs b/Utils/Utils.GetFiles.cs
new file mode 100644
index 0000000..a1ee635
--- /dev/null
+++ b/Utils/Utils.GetFiles.cs
@@ -0,0 +1,9 @@
+namespace csSiteGen;
+
+public static partial class Utils {
+
+ // Abstract Directory.GetFiles to get a List as it will be easier to handle later.
+ public static List GetFiles(DirectoryInfo dir){
+ return dir.GetFiles("*",SearchOption.AllDirectories).ToList();
+ }
+}
diff --git a/Utils/Utils.PathSearch.cs b/Utils/Utils.PathSearch.cs
new file mode 100644
index 0000000..fdb687b
--- /dev/null
+++ b/Utils/Utils.PathSearch.cs
@@ -0,0 +1,52 @@
+using Serilog;
+namespace csSiteGen;
+
+public static partial class Utils {
+
+ static Dictionary PathSearchMemo = new();
+
+ ///
+ /// Find executable in path
+ ///
+ public static string PathSearch(string Program){
+
+ if (PathSearchMemo.TryGetValue(Program, out string? result))
+ {
+ Log.Debug("Memo Hit for {program}", Program);
+ return result;
+ }
+
+ var path = System.Environment.GetEnvironmentVariable("PATH")?.Split(':');
+ if (path is null)
+ {
+ Log.Error("Failed to read PATH environment variable.");
+ return string.Empty;
+ }
+
+ List candidateExecutables = new();
+
+ foreach (var dir in path)
+ {
+ // Directories do not need to exist for them to be in path
+ // This check avoids attempting to look in directories that
+ // Do not exist.
+ if (Directory.Exists(dir))
+ {
+ Log.Information("Searching for {Program} in {dir}", Program, dir);
+ candidateExecutables.AddRange(Directory.GetFiles(dir,$"{Program}"));
+ }
+ }
+
+ if (candidateExecutables.Count == 0)
+ {
+ Log.Warning("Dependency {Program} not found",Program);
+ return string.Empty;
+ }
+
+ result = candidateExecutables.First();
+ PathSearchMemo.Add(Program,result);
+ Log.Information("Found {program} at {path}",Program, result);
+ Log.Debug("Adding {@entry} to PathSearchMemo", PathSearchMemo.Last());
+ return result;
+ }
+}
diff --git a/csSiteGen.csproj b/csSiteGen.csproj
index 6e06db1..8f3ade6 100644
--- a/csSiteGen.csproj
+++ b/csSiteGen.csproj
@@ -10,15 +10,17 @@
bin\$(Configuration)\
- net6.0
+ net7.0enableenable
-
-
+
+
+
+
From 85331e8701ca31d14d2f5b4f675a896cc26c19bc Mon Sep 17 00:00:00 2001
From: Robert Morrison
Date: Tue, 5 Mar 2024 15:45:17 +0000
Subject: [PATCH 2/2] chore: Bump TargetFramework to 8.0
---
csSiteGen.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/csSiteGen.csproj b/csSiteGen.csproj
index 8f3ade6..a142bd7 100644
--- a/csSiteGen.csproj
+++ b/csSiteGen.csproj
@@ -10,7 +10,7 @@
bin\$(Configuration)\
- net7.0
+ net8.0enableenable