refactor(EVERYTHING): Refactor all the things.

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.
This commit is contained in:
Robert Morrison 2024-01-16 11:00:35 +00:00
parent 4ce8672a68
commit 18ed0534b7
Signed by: robert
GPG Key ID: 73E012EB3F4EC696
9 changed files with 633 additions and 252 deletions

View File

@ -16,12 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
using Serilog;
using Serilog.Events; using Serilog.Events;
using System.Reflection; using Serilog;
using System.Text.RegularExpressions; using System.CommandLine.Builder;
using System.CommandLine.Help;
using System.CommandLine.Parsing;
using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using System.Text.Json; using System.Reflection;
using Spectre.Console;
namespace csSiteGen; namespace csSiteGen;
@ -30,21 +33,6 @@ class Program
static int Main(string[] args) static int Main(string[] args)
{ {
// Default values
List<string> _verbatimFileTypes = new List<string>{ // 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<string> _conversionTypes = new List<string>{ // file extenstions we want to convert to html
"md"
};
// Get the current versiion number // Get the current versiion number
string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
@ -52,7 +40,6 @@ class Program
// Configure logger // Configure logger
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .MinimumLevel.Debug()
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.WriteTo.File("log.log") .WriteTo.File("log.log")
.CreateLogger(); .CreateLogger();
@ -64,264 +51,173 @@ class Program
} }
else else
{ {
Log.Error("Cannot get Version Information"); Log.Warning("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;
} }
// It is very likely that this program will only work on linux. As such it is worth warning the user about this. // 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()) if (!OperatingSystem.IsLinux())
{ {
Log.Warning("This program has only been tested on linux and cannot be assumed to work on other Operating Systems"); 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 Stopwatch TotalExecutionTime = Stopwatch.StartNew();
string _inputDirectory = args[0];
string _outputDirectory = args[1];
if (!Directory.Exists(_inputDirectory)) var inputDirectoryOption = new Option<DirectoryInfo>(
name: "--input",
description: "The directory that contains the site source.");
inputDirectoryOption.IsRequired = true;
inputDirectoryOption.AddValidator(result =>
{ {
Log.Error("Input directory '{i}' Does not exist or is not a directory you have access to" , _inputDirectory); if (!result.GetValueForOption(inputDirectoryOption)!.Exists)
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); result.ErrorMessage = $"Input directory {result.GetValueForOption(inputDirectoryOption)!.FullName} does not exist";
Environment.Exit(1);
} }
});
// Test for dependencies var outputDirectoryOption = new Option<DirectoryInfo>(
List<string> deps = new List<string>{"pandoc"}; name: "--output",
var dep = CheckDeps(deps); description: "The directory that the site should be output to.");
Log.Debug("CheckDeps result: {@dep}",dep); outputDirectoryOption.IsRequired = true;
// Deal with the dependency test results var rootCommand = new RootCommand("csSiteGen");
Dictionary<string,string> 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<string> ConvertableInputFiles = new(); var cleanCommand = new Command("clean", "Clean the output directory");
foreach (var ext in _conversionTypes) cleanCommand.AddOption(outputDirectoryOption);
cleanCommand.SetHandler(async (directory) =>
{ {
ConvertableInputFiles.AddRange(GetAllFilesMatching($".*{ext}",_inputDirectory)); await Task.Run(() =>
} {
Log.Debug("Files matching conversiontypes: {@conversionType} found {count} \n {files} ",_conversionTypes,ConvertableInputFiles.Count(),ConvertableInputFiles); Clean(directory);
});
} ,outputDirectoryOption);
Dictionary<string,DateTime>? metadata = null; var convertCommand = new Command("convert", "Convert the input directory and place the files in the output directory.");
if (File.Exists($"{_inputDirectory}/.files")) convertCommand.AddOption(inputDirectoryOption);
convertCommand.AddOption(outputDirectoryOption);
convertCommand.SetHandler(async (inputDir, outputDir) =>
{ {
Log.Information("Loading metadata for {src}",_inputDirectory); await Task.Run(() =>
metadata = new();
string metaJSON = File.ReadAllText($"{_inputDirectory}/.files");
try
{ {
Type metadataType = metadata.GetType(); Convert(inputDir, outputDir);
metadata = JsonSerializer.Deserialize<Dictionary<string,DateTime>>(metaJSON); });
} }, inputDirectoryOption, outputDirectoryOption);
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) rootCommand.AddCommand(cleanCommand);
{ rootCommand.AddCommand(convertCommand);
// 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<string> shadow = new List<string>(ConvertableInputFiles);
foreach (string file in shadow)
{
if (metadata[file] == new FileInfo(file).LastWriteTimeUtc)
{
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);
var parser = new CommandLineBuilder(rootCommand)
.UseDefaults()
.Build();
parser.Invoke(args);
TotalExecutionTime.Stop();
Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds);
Log.CloseAndFlush(); Log.CloseAndFlush();
return 0; return 0;
} }
/// <summary> static int Convert(DirectoryInfo inputDir, DirectoryInfo outputDir)
/// Returns a list of all the files in the given directory and all subdirectories recursive
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="directory">
/// The directory to find files in.
/// <param>
/// <returns>
/// A string list of every file in the provided directory and all subdirectories
/// </returns>
static List<string> GetAllFiles(string directory)
{ {
List<string> res = new(); AnsiConsole.Console.Profile.Capabilities.Ansi = true;
Stack<string> dirs = new();
dirs.Push(directory); List<SiteFile> siteFiles = new();
Log.Debug("Dirs Starting as: {DirStack}",dirs );
Stopwatch timer = new(); Utils.GetFiles(inputDir).ForEach(x => siteFiles.Add(new SiteFile(x)));
timer.Start(); Log.Information("SiteFiles: {@sf} {count}", siteFiles, siteFiles.Count);
while (dirs.Count > 0)
Console.WriteLine($"Converting {siteFiles.Count} files from {inputDir.FullName} to {outputDir.FullName}");
RuntimeSettings settings = new(inputDir,outputDir);
Dictionary<string,bool> fileStatus = new();
Log.Debug("fileStatus {@fileStatus}",fileStatus);
AnsiConsole.Progress()
.AutoRefresh(true)
.Columns(new ProgressColumn[]
{ {
var dir = dirs.Pop(); new TaskDescriptionColumn(),
res.AddRange(Directory.GetFiles(dir)); new ProgressBarColumn(),
foreach (string subdir in Directory.GetDirectories(dir)) new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn()
})
.Start(ctx =>
{ {
if(File.GetAttributes(subdir).HasFlag(FileAttributes.ReparsePoint)) 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;
* FIXME: This is only a temporary measure and should not be relied upon Log.Information("Converting file {name} {i}/{count}",siteFiles[i].Name,i+1,siteFiles.Count());
* if possible there should be a depth detection system that allows for bool res = siteFiles[i].Convert(settings);
* safe symlink following. tasks[i].Increment(1);
*/ tasks[i].StopTask();
Log.Debug("Directory {subdir} is a ReparsePoint(symlink) and has been ignored",subdir); if (!res)
continue; {
Log.Warning("{name} Failed...",siteFiles[i].FullName);
tasks[i].Description += " [red]FAILED[/]";
} }
dirs.Push(subdir); Log.Information("adding {@siteFile} conversion status to fileStatus", siteFiles[i]);
Log.Debug("Adding Directory {subdir} to dirs\nResult: {dirs}",subdir,dirs); Log.Debug("FileStatus {@fileStatus}",fileStatus);
} fileStatus.Add(siteFiles[i].FullName,res);
} overallTask.Increment(1);
timer.Stop();
Log.Information("Crawled {directory} in {time}ms with {number} results",directory,timer.ElapsedMilliseconds,res.Count());
return res;
} }
});
static List<string> GetAllFilesMatching(string pattern, string directory) Log.Information("Conversion Status {@status}", fileStatus);
var Failed = fileStatus.Where(x => x.Value == false);
if ( Failed.Count() > 0)
{ {
List<string> res = new(); foreach (var fail in Failed)
List<string> files = GetAllFiles(directory);
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()
{ {
/* AnsiConsole.MarkupLineInterpolated(
* This function will be called when argument errors happen. This will $"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]");
* also be called if the user triggers the help flag currently this AnsiConsole.MarkupLine("[yellow]See log for more details[/]");
* function throws a not implemented exception since it I cannot write
* the usage until I have other functionality
*/
throw new NotImplementedException();
}
static Tuple<bool,Dictionary<string,string>?> CheckDeps(List<string>? dependencies)
{
bool success = true;
Stopwatch timer = new();
if (! (dependencies?.Count() > 0)) // assume success as no dependencies
{
return new Tuple<bool,Dictionary<string,string>?>(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<string> 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.
{
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<string,string>? result = new();
Dictionary<string,string>? 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);
} }
} }
if (success) return 0;
{
return new Tuple<bool,Dictionary<string,string>?>(success,result);
}
else
{
return new Tuple<bool,Dictionary<string,string>?>(success,failures);
}
} }
static bool Convert(Dictionary<string,string> files) static int Clean(DirectoryInfo outputDir)
{ {
if (!outputDir.Exists)
return false; {
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);
// }
// }

View File

@ -0,0 +1,29 @@
namespace csSiteGen;
/// <summary>
/// Class <c>RuntimeSettings</p> Contains all the settings that could be loaded from the commandline.
/// </summary>
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.
*/
}
}

View File

@ -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);
/// <summary>
/// A Mapping of filetype to ConvertFunc.
/// </summary>
public static readonly Dictionary<string,ConvertFunc> Mappings = new(){
{".md", Pandoc},
};
/// <summary>
/// TEST FUNCTION.
/// </summary>
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;
}
/// <summary>
/// Copy the file verbatim
/// </summary>
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;
}
/// <summary>
/// Execute pandoc on the file, automatically detecting the template to use.
/// </summary>
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);
}
}

138
SiteFile/SiteFile.cs Normal file
View File

@ -0,0 +1,138 @@
using System.Text.Json;
using Serilog;
namespace csSiteGen;
/// <summary>
/// A <c>SiteFile</c> represents an individual file to be converted for the static site.
/// </summary>
public partial class SiteFile
{
FileInfo info;
Conversions.ConvertFunc ConverterFunction;
static Dictionary<string, DateTime>? Metadata = null;
/// <summary>
/// The name of the file, Not Guaranteed to be unique.
/// Use only for output and logging, never file operations,
/// nor as a Dictionary key.
/// </summary>
public string Name => info.Name;
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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<Dictionary<string, DateTime>>(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<Dictionary<string, DateTime>>(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);
}
}

5
TODO Normal file
View File

@ -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

92
Testing/src/.template Executable file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
$-- #### STATIC HEAD ELEMENTS
<meta charset="UTF-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="Robert's Place">
<meta name="twitter:card" content="summary_large_image" />
<link rel="shortcut icon" href="https://www.closedless.xyz/~robert/favicon.png" type="image/png">
<link rel="stylesheet" href="https://www.closedless.xyz/~robert/css/blog.css" type="text/css" media="all">
$-- #### VARIABLE HEAD ELEMENTS
<title>$if(title-prefix)$$title-prefix$ $endif$$pagetitle$</title>
<meta property="og:title" content="$title$"/>
<meta property="og:url" content="http://euro-travel-example.com/index.htm" />
$for(author-meta)$ $-- Include author information
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$ $-- Include date information
<meta name="dcterms.date" content="$date-meta$" />
$endif$
$if(keywords)$ $-- Include Keywords
<meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
$endif$
$if(description-meta)$ $-- Include description
<meta name="description" content="$description-meta$" />
<meta property="og:description" content="$description-meta$">
$endif$
$if(image)$ $-- OPTIONAL set image for link cards
<meta property="og:image" content="$image$" />
$else$ $-- Defaults to favicon
<meta property="og:image" content="https://closedless.xyz/~robert/favicon.png">
$endif$
$if(alt-text)$ $-- OPTIONAL Include alt text for twitter
<meta name="twitter:image:alt" content="$alt-text$">
$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
<link rel="stylesheet" href="https://closedless.xyz/resources/CSS/hljs-gruvbox-CSSVARS.css" type="text/css" media="all">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/highlight.min.js"></script>
$endif$
$if(style)$ $-- Include page specfic style (To reduce size of page load on other pages)
<style>$style$</style>
$endif$
</head>
<body class="sans"> $-- default to sans-serif --$
<nav class="navbar mono"> $-- navbar in monospace --$
<div class="bar-left">
<ul>
<li>
<a href="https://www.closedless.xyz/~robert">
<img class="minipic" src="https://www.closedless.xyz/~robert/favicon.png">
<span class="site-title">Robert's Place</span>
</a>
</li>
</ul>
</div>
<div class="bar-right">
<ul>
<li>
<a id="dark-toggle" class="nf nf-fa-moon_o" onclick="changeTheme()"></a>
</li>
<li>
<a href="https://instagram.com/sherlock5512" class="nf nf-mdi-instagram"> Instagram</a>
</li>
</ul>
</div>
</nav>
$body$
<footer class="Ilow">
<p>
Design based on <a href="https://notthebe.ee/">Wolfgang's blog</a> (<a href="https://creativecommons.org/licenses/by-nc/4.0">CC-BY-NC 4.0</a>)
<br>
Pages generated from markdown from markdown using <a class="mono" href="https://romanzolotarev.com/ssg.html">ssg5</a> by Roman Zolotarev
</p>
</footer>
<script charset="utf-8">
$if(needs-syntax-highlighting)$hljs.highlightAll();$endif$
console.log("TEST");
initLoad();
</script>
</body>
</html>

9
Utils/Utils.GetFiles.cs Normal file
View File

@ -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<FileInfo> GetFiles(DirectoryInfo dir){
return dir.GetFiles("*",SearchOption.AllDirectories).ToList();
}
}

52
Utils/Utils.PathSearch.cs Normal file
View File

@ -0,0 +1,52 @@
using Serilog;
namespace csSiteGen;
public static partial class Utils {
static Dictionary<string,string> PathSearchMemo = new();
///<summary>
/// Find executable in path
///</summary>
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<string> 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;
}
}

View File

@ -10,15 +10,17 @@
<OutputPath>bin\$(Configuration)\</OutputPath> <OutputPath>bin\$(Configuration)\</OutputPath>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="spectre.console" Version="0.48.0" />
<PackageReference Include="system.commandline" Version="2.0.0-beta4.22272.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">