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.
236 lines
7.3 KiB
C#
236 lines
7.3 KiB
C#
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},
|
|
};
|
|
|
|
private static readonly string[] BaseUrlFiletypes = {
|
|
".md",
|
|
".html"
|
|
};
|
|
|
|
|
|
/// <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 (doing any baseurl replacements if needed)
|
|
/// </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 {
|
|
if (BaseUrlFiletypes.Contains(file.Extension))
|
|
{
|
|
File.WriteAllText(newPath.FullName, BaseUrlReplace(file, settings));
|
|
}
|
|
else
|
|
{
|
|
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){
|
|
// 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);
|
|
|
|
// 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
|
|
|
|
// 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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
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:\n{stdout}", program, stdout);
|
|
Log.Debug("{program} STDERR:\n{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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|