UNCONVENTIONAL COMMIT Bump to 0.0.2
While I would usually try and stick to the conventional commits standard this commit is a big one. This commit Bumps us to 0.0.2 And completely changes how you interact with the program. Now it is easier as you only need to specify the project directory on the commandline (OR be in the directory of the site you want to build) Also introduced is the cssitegen.json file that all projects must use. This means that static information such as the basename, source, and destination are kept with the files. ProjectSettings is used to hopefully make managing a site easier, although future refactoring may join the RuntimeSettings and ProjectSettings into one class. There are some obvious issues with the project in its current state but pending testing with a live domain, it does appear to actually work as intended. (if this is true then the code just needs refactoring and tidying to qualify for a 0.1.0 Release.) Future features planned include - Code to generate pages from data - Template nesting (or a custom template templating language) - Introduction of image conversion to webp (with fallback to RawCpy) - consistency enforcement, to ensure that deleted source files mean deleted destination files.
This commit is contained in:
parent
8717b1c677
commit
d868eac72f
540
Program.cs
540
Program.cs
|
|
@ -1,223 +1,317 @@
|
||||||
/*
|
/*
|
||||||
* csSiteGen - A static site generator written in c#
|
* csSiteGen - A static site generator written in c#
|
||||||
* Copyright © 2022 Robert Morrison<sherlock5512>
|
* Copyright © 2022 Robert Morrison<sherlock5512>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* 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.Events;
|
using Serilog.Events;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using System.CommandLine.Builder;
|
using System.CommandLine.Builder;
|
||||||
using System.CommandLine.Help;
|
using System.CommandLine.Help;
|
||||||
using System.CommandLine.Parsing;
|
using System.CommandLine.Parsing;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
|
using System.Text.Json;
|
||||||
namespace csSiteGen;
|
|
||||||
|
namespace csSiteGen;
|
||||||
class Program
|
|
||||||
{
|
class Program
|
||||||
|
{
|
||||||
static int Main(string[] args)
|
|
||||||
{
|
static int Main(string[] args)
|
||||||
|
{
|
||||||
// Get the current versiion number
|
|
||||||
string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
// Get the current version number
|
||||||
|
string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||||
// Configure logger
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
// Only log to file
|
||||||
.MinimumLevel.Debug()
|
// Logging to the console is BAD practice as it tends to be messy.
|
||||||
.WriteTo.File("log.log")
|
// TODO: Log to a known location using environment to find the correct location.
|
||||||
.CreateLogger();
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
Log.Information("Starting New Instance of csSiteGen");
|
.WriteTo.File("log.log")
|
||||||
|
.CreateLogger();
|
||||||
if (version is not null)
|
|
||||||
{
|
Log.Information("Starting New Instance of csSiteGen");
|
||||||
Log.Information("Version: {ver}", version);
|
|
||||||
}
|
if (version is not null)
|
||||||
else
|
{
|
||||||
{
|
Log.Information("Version: {ver}", version);
|
||||||
Log.Warning("Cannot get Version Information");
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
// It is very likely that this program will only work on linux. As such it is worth warning the user about this.
|
Log.Warning("Cannot get Version Information");
|
||||||
if (!OperatingSystem.IsLinux())
|
}
|
||||||
{
|
|
||||||
Log.Warning("This program has only been tested on linux and cannot be assumed to work on other Operating Systems");
|
// It is very likely that this program will only work on linux. As such it is worth warning the user about this.
|
||||||
// AnsiConsole.MarkupLine("[[[yellow]Warning[/]]] This program is only tested on linux systems, it may not work on this Operating System.");
|
// With further testing and handling of any edge cases it _may_ be possible to have this work anywhere.
|
||||||
}
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
Stopwatch TotalExecutionTime = Stopwatch.StartNew();
|
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.");
|
||||||
var inputDirectoryOption = new Option<DirectoryInfo>(
|
}
|
||||||
name: "--input",
|
|
||||||
description: "The directory that contains the site source.");
|
Stopwatch TotalExecutionTime = Stopwatch.StartNew();
|
||||||
inputDirectoryOption.IsRequired = true;
|
|
||||||
inputDirectoryOption.AddValidator(result =>
|
/* !! IMPORTANT !!
|
||||||
{
|
WARN:
|
||||||
if (!result.GetValueForOption(inputDirectoryOption)!.Exists)
|
This code uses system.commandline which is still in pre-release
|
||||||
{
|
the following section of code will contain comments to explain the intent of the programmer
|
||||||
result.ErrorMessage = $"Input directory {result.GetValueForOption(inputDirectoryOption)!.FullName} does not exist";
|
which may be useful if system.commandline has breaking changes
|
||||||
}
|
*/
|
||||||
});
|
|
||||||
|
|
||||||
var outputDirectoryOption = new Option<DirectoryInfo>(
|
// First the option for the project directory is created
|
||||||
name: "--output",
|
var ProjectDirectoryOption = new Option<DirectoryInfo>(
|
||||||
description: "The directory that the site should be output to.");
|
name: "--project",
|
||||||
outputDirectoryOption.IsRequired = true;
|
description: "The Directory for the project");
|
||||||
|
ProjectDirectoryOption.IsRequired = false; // it is not required as not providing it infers that the current directory is the project directory
|
||||||
var rootCommand = new RootCommand("csSiteGen");
|
// If the option is used then the input is validated before control passes to any of the actual code.
|
||||||
|
ProjectDirectoryOption.AddValidator(result =>
|
||||||
var cleanCommand = new Command("clean", "Clean the output directory");
|
{
|
||||||
cleanCommand.AddOption(outputDirectoryOption);
|
if (!result.GetValueForOption(ProjectDirectoryOption)!.Exists)
|
||||||
cleanCommand.SetHandler(async (directory) =>
|
{
|
||||||
{
|
result.ErrorMessage = $"Project directory {result.GetValueForOption(ProjectDirectoryOption)} does not exist.";
|
||||||
await Task.Run(() =>
|
}
|
||||||
{
|
}
|
||||||
Clean(directory);
|
);
|
||||||
});
|
|
||||||
} ,outputDirectoryOption);
|
// The root command is the entry point for commandline but otherwise does nothing.
|
||||||
|
var rootCommand = new RootCommand("csSiteGen");
|
||||||
var convertCommand = new Command("convert", "Convert the input directory and place the files in the output directory.");
|
|
||||||
convertCommand.AddOption(inputDirectoryOption);
|
|
||||||
convertCommand.AddOption(outputDirectoryOption);
|
// TODO: Verify if the use of async in these functions is necessary
|
||||||
convertCommand.SetHandler(async (inputDir, outputDir) =>
|
|
||||||
{
|
// This creates the command for cleaning a projects output directory.
|
||||||
await Task.Run(() =>
|
var cleanCommand = new Command("clean", "Clean the projects output directory");
|
||||||
{
|
cleanCommand.AddOption(ProjectDirectoryOption); // This command can use the project directory option we created earlier
|
||||||
Convert(inputDir, outputDir);
|
cleanCommand.SetHandler(async (ProjectDirectory) =>
|
||||||
});
|
{
|
||||||
}, inputDirectoryOption, outputDirectoryOption);
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
rootCommand.AddCommand(cleanCommand);
|
Clean(ProjectDirectory);
|
||||||
rootCommand.AddCommand(convertCommand);
|
});
|
||||||
|
},ProjectDirectoryOption);
|
||||||
var parser = new CommandLineBuilder(rootCommand)
|
|
||||||
.UseDefaults()
|
// This creates the command for actually converting the project.
|
||||||
.Build();
|
var convertCommand = new Command("convert", "Convert the projects input directory and place the files in the output directory.");
|
||||||
|
convertCommand.AddOption(ProjectDirectoryOption); // This command can use the project directory option
|
||||||
parser.Invoke(args);
|
convertCommand.SetHandler(async (ProjectDirectory) =>
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
TotalExecutionTime.Stop();
|
{
|
||||||
Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds);
|
Convert(ProjectDirectory);
|
||||||
|
});
|
||||||
Log.CloseAndFlush();
|
},ProjectDirectoryOption);
|
||||||
return 0;
|
|
||||||
}
|
// Adding the commands to the root command makes them actually callable on the commandline
|
||||||
|
rootCommand.AddCommand(cleanCommand);
|
||||||
static int Convert(DirectoryInfo inputDir, DirectoryInfo outputDir)
|
rootCommand.AddCommand(convertCommand);
|
||||||
{
|
|
||||||
AnsiConsole.Console.Profile.Capabilities.Ansi = true;
|
// The parser is what actually handles the arguments and dispatches them to the appropriate commands.
|
||||||
|
// This is used instead of the simpler method of just Invoking the root command as it automatically creates usage statements.
|
||||||
List<SiteFile> siteFiles = new();
|
// It also makes a user aware that a subcommand needs to be used.
|
||||||
|
var parser = new CommandLineBuilder(rootCommand)
|
||||||
Utils.GetFiles(inputDir).ForEach(x => siteFiles.Add(new SiteFile(x)));
|
.UseDefaults()
|
||||||
Log.Information("SiteFiles: {@sf} {count}", siteFiles, siteFiles.Count);
|
.Build();
|
||||||
|
|
||||||
Console.WriteLine($"Converting {siteFiles.Count} files from {inputDir.FullName} to {outputDir.FullName}");
|
parser.Invoke(args);
|
||||||
RuntimeSettings settings = new(inputDir,outputDir);
|
|
||||||
|
|
||||||
|
TotalExecutionTime.Stop();
|
||||||
Dictionary<string,bool> fileStatus = new();
|
Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds);
|
||||||
Log.Debug("fileStatus {@fileStatus}",fileStatus);
|
|
||||||
|
// Providing there were no early exits it is best to properly close the log before we exit.
|
||||||
AnsiConsole.Progress()
|
Log.CloseAndFlush();
|
||||||
.AutoRefresh(true)
|
return 0;
|
||||||
.Columns(new ProgressColumn[]
|
}
|
||||||
{
|
|
||||||
new TaskDescriptionColumn(),
|
static int Convert(DirectoryInfo? ProjectDirectory)
|
||||||
new ProgressBarColumn(),
|
{
|
||||||
new PercentageColumn(),
|
Log.Information("Convert command was called, beginning conversion.");
|
||||||
new RemainingTimeColumn(),
|
|
||||||
new SpinnerColumn()
|
// WARN: This is only temporary as Spectre.Console does not recognise some Linux terminals
|
||||||
})
|
// A better solution that checks the terminal value and sets this option should be added in the future.
|
||||||
.Start(ctx =>
|
AnsiConsole.Console.Profile.Capabilities.Ansi = true;
|
||||||
{
|
|
||||||
var tasks = siteFiles.Select(x => ctx.AddTask($"Converting {x.Name}")).ToList();
|
|
||||||
var overallTask = ctx.AddTask("[bold]Converting Files[/]");
|
// NOTE: Future refactors may merge ProjectSettings and RuntimeSettings
|
||||||
overallTask.MaxValue = siteFiles.Count();
|
ProjectSettings projectSettings = GetProjectSettings(ProjectDirectory);
|
||||||
|
|
||||||
for (int i = 0; i < siteFiles.Count; i++)
|
DirectoryInfo inputDir = new(projectSettings.Source);
|
||||||
{
|
DirectoryInfo outputDir = new(projectSettings.Destination);
|
||||||
tasks[i].MaxValue = 1;
|
|
||||||
Log.Information("Converting file {name} {i}/{count}",siteFiles[i].Name,i+1,siteFiles.Count());
|
RuntimeSettings settings = new(inputDir,outputDir);
|
||||||
bool res = siteFiles[i].Convert(settings);
|
settings.setBaseUrl(projectSettings.BaseUrl);
|
||||||
tasks[i].Increment(1);
|
|
||||||
tasks[i].StopTask();
|
List<SiteFile> siteFiles = new();
|
||||||
if (!res)
|
|
||||||
{
|
Utils.GetFiles(inputDir).ForEach(x => siteFiles.Add(new SiteFile(x)));
|
||||||
Log.Warning("{name} Failed...",siteFiles[i].FullName);
|
Log.Information("SiteFiles: {@sf} {count}", siteFiles, siteFiles.Count);
|
||||||
tasks[i].Description += " [red]FAILED[/]";
|
|
||||||
}
|
Console.WriteLine($"Converting {siteFiles.Count} files from {inputDir.FullName} to {outputDir.FullName}");
|
||||||
Log.Information("adding {@siteFile} conversion status to fileStatus", siteFiles[i]);
|
|
||||||
Log.Debug("FileStatus {@fileStatus}",fileStatus);
|
|
||||||
fileStatus.Add(siteFiles[i].FullName,res);
|
Dictionary<string,bool> fileStatus = new();
|
||||||
overallTask.Increment(1);
|
Log.Debug("fileStatus {@fileStatus}",fileStatus);
|
||||||
}
|
|
||||||
});
|
AnsiConsole.Progress()
|
||||||
|
.AutoRefresh(true)
|
||||||
Log.Information("Conversion Status {@status}", fileStatus);
|
.Columns(new ProgressColumn[]
|
||||||
|
{
|
||||||
var Failed = fileStatus.Where(x => x.Value == false);
|
new TaskDescriptionColumn(),
|
||||||
if ( Failed.Count() > 0)
|
new ProgressBarColumn(),
|
||||||
{
|
new PercentageColumn(),
|
||||||
foreach (var fail in Failed)
|
new RemainingTimeColumn(),
|
||||||
{
|
new SpinnerColumn()
|
||||||
AnsiConsole.MarkupLineInterpolated(
|
})
|
||||||
$"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]");
|
.Start(ctx =>
|
||||||
AnsiConsole.MarkupLine("[yellow]See log for more details[/]");
|
{
|
||||||
}
|
var tasks = siteFiles.Select(x => ctx.AddTask($"Converting {x.Name}")).ToList();
|
||||||
}
|
var overallTask = ctx.AddTask("[bold]Converting Files[/]");
|
||||||
|
overallTask.MaxValue = siteFiles.Count();
|
||||||
return 0;
|
|
||||||
}
|
for (int i = 0; i < siteFiles.Count; i++)
|
||||||
|
{
|
||||||
static int Clean(DirectoryInfo outputDir)
|
tasks[i].MaxValue = 1;
|
||||||
{
|
Log.Information("Converting file {name} {i}/{count}",siteFiles[i].Name,i+1,siteFiles.Count());
|
||||||
if (!outputDir.Exists)
|
bool res = siteFiles[i].Convert(settings);
|
||||||
{
|
tasks[i].Increment(1);
|
||||||
Log.Warning("Not deleting {dir} as it doesn't exist",outputDir.FullName);
|
tasks[i].StopTask();
|
||||||
AnsiConsole.MarkupLineInterpolated($"[bold][[[yellow]Warning[/]]][/] Not cleaning [blue]\"{outputDir}\"[/] as it does not exist.");
|
if (!res)
|
||||||
return 0; // success because it doesn't exist.
|
{
|
||||||
}
|
Log.Warning("{name} Failed...",siteFiles[i].FullName);
|
||||||
try
|
tasks[i].Description += " [red]FAILED[/]";
|
||||||
{
|
}
|
||||||
Log.Information("Cleaning {dir}",outputDir.FullName);
|
Log.Information("adding {@siteFile} conversion status to fileStatus", siteFiles[i]);
|
||||||
AnsiConsole.MarkupInterpolated($"Cleaning [blue]\"{outputDir.FullName}\"[/]");
|
Log.Debug("FileStatus {@fileStatus}",fileStatus);
|
||||||
outputDir.Delete(recursive: true);
|
fileStatus.Add(siteFiles[i].FullName,res);
|
||||||
outputDir.Create();
|
overallTask.Increment(1);
|
||||||
AnsiConsole.MarkupLine(" [bold][[[green]OK[/]]][/]");
|
}
|
||||||
AnsiConsole.MarkupLineInterpolated($"\t[grey]>>[/] [green]All files in {outputDir.FullName} purged successfully[/]");
|
});
|
||||||
return 0;
|
|
||||||
}
|
Log.Information("Conversion Status {@status}", fileStatus);
|
||||||
catch (System.Security.SecurityException e)
|
|
||||||
{
|
var Failed = fileStatus.Where(x => x.Value == false);
|
||||||
AnsiConsole.MarkupLine(" [bold][[[red]Fail[/]]][/]");
|
if ( Failed.Count() > 0)
|
||||||
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);
|
foreach (var fail in Failed)
|
||||||
return 1;
|
{
|
||||||
}
|
AnsiConsole.MarkupLineInterpolated(
|
||||||
catch (Exception e)
|
$"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]");
|
||||||
{
|
AnsiConsole.MarkupLine("[yellow]See log for more details[/]");
|
||||||
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;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
static int Clean(DirectoryInfo? ProjectDirectory)
|
||||||
|
{
|
||||||
|
Log.Information("Clean command was called, Beginning cleaning");
|
||||||
|
|
||||||
|
// NOTE: Future refactors may merge ProjectSettings and RuntimeSettings
|
||||||
|
ProjectSettings projectSettings = GetProjectSettings(ProjectDirectory);
|
||||||
|
|
||||||
|
DirectoryInfo inputDir = new(projectSettings.Source);
|
||||||
|
DirectoryInfo outputDir = new(projectSettings.Destination);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void EnforceConsistency(ProjectSettings projectSettings)
|
||||||
|
{
|
||||||
|
// Grab the metadata.
|
||||||
|
|
||||||
|
// Read the metadata.
|
||||||
|
|
||||||
|
// Find deleted files.
|
||||||
|
|
||||||
|
// Figure out what the new name for those files would be.
|
||||||
|
|
||||||
|
// Remove those files.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static ProjectSettings GetProjectSettings(DirectoryInfo? ProjectDirectory)
|
||||||
|
{
|
||||||
|
// TODO: implement proper error handling where file access is performed.
|
||||||
|
|
||||||
|
if (ProjectDirectory is null)
|
||||||
|
{
|
||||||
|
// use the current directory if no project directory is passed.
|
||||||
|
ProjectDirectory = new DirectoryInfo(".");
|
||||||
|
}
|
||||||
|
Log.Information("{projectdir} => fullname {pdfn}",ProjectDirectory, ProjectDirectory.FullName);
|
||||||
|
FileInfo projectFile = new (Path.Combine(ProjectDirectory.FullName,"cssitegen.json"));
|
||||||
|
|
||||||
|
|
||||||
|
if (!projectFile.Exists)
|
||||||
|
{
|
||||||
|
Log.Fatal("Cannot locate project file {pf} in {dir}",projectFile,ProjectDirectory);
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Located Project File {pf}",projectFile.FullName);
|
||||||
|
ProjectSettings? projectSettings = JsonSerializer
|
||||||
|
.Deserialize<ProjectSettings>(
|
||||||
|
projectFile
|
||||||
|
.OpenText()
|
||||||
|
.ReadToEnd()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (projectSettings is null)
|
||||||
|
{
|
||||||
|
Log.Fatal("Cannot deserialize projectFile");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
projectSettings.setProjectRoot(ProjectDirectory);
|
||||||
|
|
||||||
|
Log.Information("{@ps}",projectSettings);
|
||||||
|
|
||||||
|
return projectSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
44
ProjectSettings/ProjectSettings.cs
Normal file
44
ProjectSettings/ProjectSettings.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
// project settings is a user accessible config to set the Source and destination of a site
|
||||||
|
// This may include more scope in the future such as holding the site base address etc..
|
||||||
|
|
||||||
|
class ProjectSettings
|
||||||
|
{
|
||||||
|
private string _Source;
|
||||||
|
private string _Destination;
|
||||||
|
private DirectoryInfo? _ProjectRoot;
|
||||||
|
public string? BaseUrl {get; private set;}
|
||||||
|
|
||||||
|
public string Source {get {
|
||||||
|
if (_ProjectRoot is null)
|
||||||
|
{
|
||||||
|
return _Source;
|
||||||
|
}
|
||||||
|
return Path.Combine(_ProjectRoot.FullName,_Source);
|
||||||
|
}}
|
||||||
|
public string Destination {get {
|
||||||
|
if (_ProjectRoot is null)
|
||||||
|
{
|
||||||
|
return _Destination;
|
||||||
|
}
|
||||||
|
return Path.Combine(_ProjectRoot.FullName,_Destination);
|
||||||
|
}}
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public ProjectSettings(String source, String destination, string baseUrl) {
|
||||||
|
_Source = source;
|
||||||
|
_Destination = destination;
|
||||||
|
BaseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectRoot(string projectRoot) {
|
||||||
|
_ProjectRoot = new(projectRoot);
|
||||||
|
}
|
||||||
|
public void setProjectRoot(DirectoryInfo projectRoot) {
|
||||||
|
_ProjectRoot = projectRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseUrl(string baseUrl) {
|
||||||
|
BaseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,12 @@ namespace csSiteGen;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class <c>RuntimeSettings</p> Contains all the settings that could be loaded from the commandline.
|
/// Class <c>RuntimeSettings</c> Contains all the settings that could be loaded from the commandline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RuntimeSettings {
|
public class RuntimeSettings {
|
||||||
public DirectoryInfo InputDirectory {get; private set;}
|
public DirectoryInfo InputDirectory {get; private set;}
|
||||||
public DirectoryInfo OutputDirectory {get; private set;}
|
public DirectoryInfo OutputDirectory {get; private set;}
|
||||||
|
public string? BaseUrl {get; private set;}
|
||||||
|
|
||||||
|
|
||||||
public RuntimeSettings(string inputDirectory, string outputDirectory){
|
public RuntimeSettings(string inputDirectory, string outputDirectory){
|
||||||
|
|
@ -26,4 +27,7 @@ public class RuntimeSettings {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBaseUrl(string? baseurl) {
|
||||||
|
BaseUrl = baseurl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ public static class Conversions{
|
||||||
{".md", Pandoc},
|
{".md", Pandoc},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string[] BaseUrlFiletypes = {
|
||||||
|
".md",
|
||||||
|
".html"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TEST FUNCTION.
|
/// TEST FUNCTION.
|
||||||
|
|
@ -28,7 +33,7 @@ public static class Conversions{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the file verbatim
|
/// Copy the file verbatim (doing any baseurl replacements if needed)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool RawCpy(FileInfo file, RuntimeSettings settings){
|
public static bool RawCpy(FileInfo file, RuntimeSettings settings){
|
||||||
FileInfo newPath = new FileInfo(GetNewName(file,settings,null));
|
FileInfo newPath = new FileInfo(GetNewName(file,settings,null));
|
||||||
|
|
@ -41,7 +46,14 @@ public static class Conversions{
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
file.CopyTo(newPath.FullName, overwrite: true);
|
if (BaseUrlFiletypes.Contains(file.Extension))
|
||||||
|
{
|
||||||
|
File.WriteAllText(newPath.FullName, BaseUrlReplace(file, settings));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
file.CopyTo(newPath.FullName, overwrite: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e){
|
catch (Exception e){
|
||||||
Log.Fatal(e,"Copy Failed");
|
Log.Fatal(e,"Copy Failed");
|
||||||
|
|
@ -54,6 +66,11 @@ public static class Conversions{
|
||||||
/// Execute pandoc on the file, automatically detecting the template to use.
|
/// Execute pandoc on the file, automatically detecting the template to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool Pandoc(FileInfo file, RuntimeSettings settings){
|
public static bool Pandoc(FileInfo file, RuntimeSettings settings){
|
||||||
|
// NOTE: Some of the code later where the tmpfile is created for baseurl replacement may be too safe.
|
||||||
|
// the extension checks may be unnecessary, but this depends on if this function will be retooled to run pandoc for different conversions.
|
||||||
|
// for now I have take the safer approach, but the leaner approach may be used in the future when the project is more mature
|
||||||
|
|
||||||
|
|
||||||
Log.Information("Attempting to convert {file} using pandoc",file.Name);
|
Log.Information("Attempting to convert {file} using pandoc",file.Name);
|
||||||
|
|
||||||
// Look for pandoc
|
// Look for pandoc
|
||||||
|
|
@ -91,7 +108,37 @@ public static class Conversions{
|
||||||
searchDir = searchDir.Parent;
|
searchDir = searchDir.Parent;
|
||||||
} while (searchDir != settings.InputDirectory); // Check last as we want to search the InputDirectory
|
} while (searchDir != settings.InputDirectory); // Check last as we want to search the InputDirectory
|
||||||
|
|
||||||
string pandocArgs = $"{file.FullName} -o {GetNewName(file,settings,".html")}";
|
// the empty string is used as it has a defined identity
|
||||||
|
string tmpFile = string.Empty;
|
||||||
|
if (BaseUrlFiletypes.Contains(file.Extension))
|
||||||
|
{
|
||||||
|
Log.Information("Replacing baseurl for file {f}",file.FullName);
|
||||||
|
tmpFile = Path.Join(Path.GetTempPath(),"pandoc",file.Name);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(tmpFile)!); // NOTE: It is practially impossible that this would actually return null
|
||||||
|
File.Create(tmpFile).Close(); // TODO: Use the filestream provided by File.Create within a using block to write the text
|
||||||
|
File.WriteAllText(tmpFile,BaseUrlReplace(file,settings));
|
||||||
|
|
||||||
|
if (template is not null)
|
||||||
|
{
|
||||||
|
Log.Information("Replacing baseurl in template file");
|
||||||
|
string tmpTemplateFile = Path.Join(Path.GetTempPath(),"pandoc",template.Name);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(tmpTemplateFile)!); // NOTE: It is practially impossible that this would actually return null
|
||||||
|
File.Create(tmpTemplateFile).Close(); // TODO: Use the filestream provided by File.Create within a using block to write the text
|
||||||
|
File.WriteAllText(tmpTemplateFile,BaseUrlReplace(template,settings));
|
||||||
|
template = new(tmpTemplateFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string pandocArgs;
|
||||||
|
// If we have created a temporary file we need to ensure that we use it.
|
||||||
|
if (!string.IsNullOrEmpty(tmpFile))
|
||||||
|
{
|
||||||
|
pandocArgs = $"{tmpFile} -o {GetNewName(file,settings,".html")}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pandocArgs = $"{file.FullName} -o {GetNewName(file,settings,".html")}";
|
||||||
|
}
|
||||||
|
|
||||||
if (template is not null)
|
if (template is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -103,7 +150,18 @@ public static class Conversions{
|
||||||
Log.Warning("Pandoc template for {file} not found",file.Name);
|
Log.Warning("Pandoc template for {file} not found",file.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RunExternalProgram(pandoc,pandocArgs);
|
if (!Directory.Exists(Path.GetDirectoryName(GetNewName(file,settings,".html"))))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(GetNewName(file,settings,".html"))!);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pandocReturn = RunExternalProgram(pandoc,pandocArgs);
|
||||||
|
// If we made a tmpfile delete it after running pandoc against it.
|
||||||
|
if (!string.IsNullOrEmpty(tmpFile))
|
||||||
|
{
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
}
|
||||||
|
return pandocReturn;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,8 +196,8 @@ public static class Conversions{
|
||||||
|
|
||||||
RunProgram.WaitForExit();
|
RunProgram.WaitForExit();
|
||||||
|
|
||||||
Log.Debug("{program} stdout {stdout}", program, stdout);
|
Log.Debug("{program} STDOUT:\n{stdout}", program, stdout);
|
||||||
Log.Debug("{program} stderr {stderr}", program, stderr);
|
Log.Debug("{program} STDERR:\n{stderr}", program, stderr);
|
||||||
|
|
||||||
if (RunProgram.ExitCode != 0)
|
if (RunProgram.ExitCode != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -155,4 +213,23 @@ public static class Conversions{
|
||||||
.Replace(settings.InputDirectory.FullName, settings.OutputDirectory.FullName)
|
.Replace(settings.InputDirectory.FullName, settings.OutputDirectory.FullName)
|
||||||
.Replace(file.Extension,newExtension ?? file.Extension);
|
.Replace(file.Extension,newExtension ?? file.Extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? BaseUrlReplace(FileInfo file, RuntimeSettings settings){
|
||||||
|
Log.Information("Doing BaseUrlReplace for {f}", file.FullName);
|
||||||
|
// Read the file
|
||||||
|
using (StreamReader FileReader = file.OpenText())
|
||||||
|
{
|
||||||
|
string filestring = FileReader.ReadToEnd();
|
||||||
|
|
||||||
|
if (settings.BaseUrl is null)
|
||||||
|
{
|
||||||
|
Log.Warning("BaseUrl is null, replacing templateString with nothing.");
|
||||||
|
return filestring.Replace("%BASEURL%","");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Replacing templateString with {BaseUrl}",settings.BaseUrl);
|
||||||
|
return filestring.Replace("%BASEURL%",settings.BaseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,11 @@ public partial class SiteFile
|
||||||
|
|
||||||
// NOTE: By this point Metadata CANNOT be null as LoadMetadata would either have loaded the JSON or instantiated blank MetaData
|
// NOTE: By this point Metadata CANNOT be null as LoadMetadata would either have loaded the JSON or instantiated blank MetaData
|
||||||
// The compiler however is unaware of this as it uses side effects, so we tell it there is no possible null value here.
|
// The compiler however is unaware of this as it uses side effects, so we tell it there is no possible null value here.
|
||||||
return (Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) == info.LastWriteTimeUtc);
|
Log.Debug("Attempting to check metadata for file {f}",info.FullName);
|
||||||
|
Log.Debug("METADATA={MD}",Metadata!.GetValueOrDefault(info.FullName,DateTime.MinValue));
|
||||||
|
Log.Debug("FILETIME={FT}",info.LastWriteTimeUtc);
|
||||||
|
Log.Debug("RES={res}",(Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) != info.LastWriteTimeUtc));
|
||||||
|
return (Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) != info.LastWriteTimeUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
10
TODO
10
TODO
|
|
@ -1,5 +1,9 @@
|
||||||
|
- UPDATE README
|
||||||
|
|
||||||
|
- Make the code such that the metadata file knows what source file an output file came from, this will allow us to delete files from source
|
||||||
|
and force consistency by removing them from the dst directory too.
|
||||||
|
|
||||||
- Add a pre-commit or other type of hook to ensure that the Testing directory is properly cleaned and reset before commit.
|
- 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.
|
- Ensure that the .gitkeep file is placed into Testing/dst
|
||||||
- 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
|
- 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
|
||||||
|
|
|
||||||
5
Testing/cssitegen.json
Normal file
5
Testing/cssitegen.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"Source" : "./src",
|
||||||
|
"Destination" : "./dst",
|
||||||
|
"BaseUrl" : null
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,6 @@ public static partial class Utils {
|
||||||
|
|
||||||
// Abstract Directory.GetFiles to get a List as it will be easier to handle later.
|
// Abstract Directory.GetFiles to get a List as it will be easier to handle later.
|
||||||
public static List<FileInfo> GetFiles(DirectoryInfo dir){
|
public static List<FileInfo> GetFiles(DirectoryInfo dir){
|
||||||
return dir.GetFiles("*",SearchOption.AllDirectories).ToList();
|
return dir.GetFiles("*",SearchOption.AllDirectories).Where(x => x.Name != ".template").ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ namespace csSiteGen;
|
||||||
|
|
||||||
public static partial class Utils {
|
public static partial class Utils {
|
||||||
|
|
||||||
|
// As the PathSearch utility will be called for every convertible file
|
||||||
|
// It has been memoized which mean subsequent calls for the same argument just return the result
|
||||||
static Dictionary<string,string> PathSearchMemo = new();
|
static Dictionary<string,string> PathSearchMemo = new();
|
||||||
|
|
||||||
///<summary>
|
///<summary>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
|
||||||
<Version Condition="'$(RELEASE_VERSION)' != ''">$(RELEASE_VERSION)</Version>
|
<Version Condition="'$(RELEASE_VERSION)' != ''">$(RELEASE_VERSION)</Version>
|
||||||
<VersionPrefix Condition="'$(RELEASE_VERSION)' == ''">0.0.1</VersionPrefix>
|
<VersionPrefix Condition="'$(RELEASE_VERSION)' == ''">0.0.2</VersionPrefix>
|
||||||
<VersionSuffix Condition="'$(RELEASE_VERSION)' == ''">$([System.DateTime]::UtcNow.ToString(`yyyyMMdd-HHmm`))</VersionSuffix>
|
<VersionSuffix Condition="'$(RELEASE_VERSION)' == ''">$([System.DateTime]::UtcNow.ToString(`yyyyMMdd-HHmm`))</VersionSuffix>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user