From ed6a64040452141b71b0e15027e4d5f66fb29719 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 15:26:55 +0200 Subject: [PATCH 01/32] Dotenv parsing experiment --- .../DotenvParserTests.cs | 140 ++++++++++++ .../Bookgen.Experiments.csproj | 4 + Experiments/Bookgen.Experiments/DotEnv.cs | 44 ++++ .../Bookgen.Experiments/DotEnvParser.cs | 213 ++++++++++++++++++ .../EnvironmentException.cs | 34 +++ Experiments/Bookgen.Experiments/IDotEnv.cs | 11 + 6 files changed, 446 insertions(+) create mode 100644 Experiments/Bookgen.Experiments.Tests/DotenvParserTests.cs create mode 100644 Experiments/Bookgen.Experiments/DotEnv.cs create mode 100644 Experiments/Bookgen.Experiments/DotEnvParser.cs create mode 100644 Experiments/Bookgen.Experiments/EnvironmentException.cs create mode 100644 Experiments/Bookgen.Experiments/IDotEnv.cs diff --git a/Experiments/Bookgen.Experiments.Tests/DotenvParserTests.cs b/Experiments/Bookgen.Experiments.Tests/DotenvParserTests.cs new file mode 100644 index 00000000..83120e4e --- /dev/null +++ b/Experiments/Bookgen.Experiments.Tests/DotenvParserTests.cs @@ -0,0 +1,140 @@ +namespace Bookgen.Experiments.Tests; + +[TestFixture] +internal class DotenvParserTests +{ + //https://github.com/env-lang/env/blob/main/env.md + + [Test] + public void EnsureThat_Comments_And_EmptyLines_AreIgnored() + { + string dotenv = """ + ; This is a semicolon comment + // This is a double-slash comment + + # This is a hash comment + + """; + + using var reader = new StringReader(dotenv); + + var env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + Assert.That(env.Keys, Is.Empty, "Expected no keys to be parsed from comments."); + } + + [TestCase("SECRET=\"password#123\" # The '#' is part of the value", "SECRET", "password#123")] + [TestCase("MESSAGE=\"Hello # World\" # The '#' after Hello is part of the value", "MESSAGE", "Hello # World")] + [TestCase("URL=https://example.com/path?foo=bar&baz=qux", "URL", "https://example.com/path?foo=bar&baz=qux")] + [TestCase("KEY=\"VAL//UE\" //comment", "KEY", "VAL//UE")] + [TestCase("KEY=\"VAL;UE\" ; comment", "KEY", "VAL;UE")] + [TestCase("FOO=value", "FOO", "value")] + [TestCase("foo=value", "foo", "value")] + [TestCase("FOO_BAR=value", "FOO_BAR", "value")] + [TestCase("_FOO=value", "_FOO", "value")] + [TestCase("FOO=bar", "FOO", "bar")] + [TestCase("FOO= bar", "FOO", " bar")] + [TestCase("FOO=bar baz", "FOO", "bar baz")] + [TestCase("FOO=\"BAR\"", "FOO", "BAR")] + [TestCase("FOO=\" bar \"", "FOO", " bar ")] + [TestCase("EMPTY=", "EMPTY", "")] + [TestCase("EMPTY=\"\"", "EMPTY", "")] + [TestCase("UNQUOTED=value with spaces", "UNQUOTED", "value with spaces")] + [TestCase("PATH=/usr/local/bin:/usr/bin:/bin", "PATH", "/usr/local/bin:/usr/bin:/bin")] + [TestCase("MESSAGE='Hello World'", "MESSAGE", "Hello World")] + [TestCase("PATH=\"C:\\Program Files\\App\"", "PATH", "C:\\Program Files\\App")] + [TestCase("HASH=\"my#password\"", "HASH", "my#password")] + public void EnsureThat_Keys_And_Values_ParsedCorrectly(string input, string expectedKey, string expectedValue) + { + using var reader = new StringReader(input); + + var env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key(expectedKey)); + Assert.That(env.GetValueOrDefault(expectedKey, string.Empty), Is.EqualTo(expectedValue)); + } + } + + [TestCase("123FOO=value")] + [TestCase("FOO-BAR=value")] + [TestCase(".FOO=value")] + [TestCase("FOO\\")] + [TestCase("BAR=value ")] + [TestCase("foo\nbar\nbaz=qux")] + public void EnsureThat_InvalidKeys_ThrowException(string input) + { + using var reader = new StringReader(input); + Assert.Throws(() => DotEnvParser.Parse(reader, StringComparer.Ordinal)); + } + + [Test] + public void EnsureThat_Quoted_MultiLine_Works() + { + string dotenv = """ + PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzrw + ... + -----END RSA PRIVATE KEY-----" + """; + using var reader = new StringReader(dotenv); + + var env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + string expectedValue = """ + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzrw + ... + -----END RSA PRIVATE KEY----- + """; + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key("PRIVATE_KEY")); + Assert.That(env.GetValueOrDefault("PRIVATE_KEY", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); + } + } + + [Test] + public void EnsureThat_BackSlashes_Work() + { + string dotenv = """ + LONG_MESSAGE=first line \ + second line \ + third line + """; + using var reader = new StringReader(dotenv); + + var env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + string expectedValue = """ + first line + second line + third line + """; + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key("LONG_MESSAGE")); + Assert.That(env.GetValueOrDefault("LONG_MESSAGE", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); + } + } + + [TestCase("foo\nbar\nbaz=qux", "ENV001: Invalid Line Format")] + [TestCase("foo=bar\r\nfoo=bar", "ENV002: Duplicate Key")] + [TestCase("123FOO=value", "ENV003: Invalid Key Format")] + [TestCase("FOO-BAR=value", "ENV003: Invalid Key Format")] + [TestCase(".FOO=value", "ENV003: Invalid Key Format")] + [TestCase("Foo=\"bar", "ENV004: Unclosed Quote")] + [TestCase("SECRET=password\\ # comment\r\nNEXT=value", "ENV005: Invalid Line Continuation")] + [TestCase("MULTI\\\r\nLINE_KEY=value", "ENV006: Multi-line Key")] + public void EnsureThat_Error_Handling_Works_AccordingToSpec(string input, string expectedMessage) + { + using var reader = new StringReader(input); + + EnvironmentException? ex = Assert.Throws(() => DotEnvParser.Parse(reader, StringComparer.Ordinal)); + + Assert.That(ex.Message, Does.StartWith(expectedMessage)); + } +} diff --git a/Experiments/Bookgen.Experiments/Bookgen.Experiments.csproj b/Experiments/Bookgen.Experiments/Bookgen.Experiments.csproj index b7601447..0af5ef91 100644 --- a/Experiments/Bookgen.Experiments/Bookgen.Experiments.csproj +++ b/Experiments/Bookgen.Experiments/Bookgen.Experiments.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Experiments/Bookgen.Experiments/DotEnv.cs b/Experiments/Bookgen.Experiments/DotEnv.cs new file mode 100644 index 00000000..ce77b3bd --- /dev/null +++ b/Experiments/Bookgen.Experiments/DotEnv.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bookgen.Experiments; + +internal sealed class DotEnv : IDotEnv +{ + private readonly Dictionary _keyValues; + + internal DotEnv(Dictionary keyValues) + { + _keyValues = keyValues; + } + + public IEnumerable Keys + => _keyValues.Keys; + + public bool ContainsKey(string key) + => _keyValues.ContainsKey(key); + + + public TValue GetValueOrDefault(string key, TValue defaultValue) + where TValue : IParsable + { + if (_keyValues.TryGetValue(key, out string? value) + && TValue.TryParse(value, null, out TValue? parsedValue)) + { + return parsedValue; + } + return defaultValue; + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue? value) + where TValue : IParsable + { + if (_keyValues.TryGetValue(key, out string? strValue) + && TValue.TryParse(strValue, null, out TValue? parsedValue)) + { + value = parsedValue; + return true; + } + value = default; + return false; + } +} diff --git a/Experiments/Bookgen.Experiments/DotEnvParser.cs b/Experiments/Bookgen.Experiments/DotEnvParser.cs new file mode 100644 index 00000000..830ae7dd --- /dev/null +++ b/Experiments/Bookgen.Experiments/DotEnvParser.cs @@ -0,0 +1,213 @@ +using System.Text; + +namespace Bookgen.Experiments; + +internal static class DotEnvParser +{ + public static DotEnv Parse(TextReader reader, StringComparer comparer) + { + var result = new Dictionary(comparer); + string? line; + + while ((line = reader.ReadLine()) is not null) + { + int pos = SkipWhitespace(line, 0); + + // Empty line + if (pos >= line.Length) + { + continue; + } + + // Comment line + if (IsCommentStart(line, pos)) + { + continue; + } + + // Key + int keyStart = pos; + if (!IsKeyStart(line[pos])) + { + throw EnvironmentException.Env003(line); + } + + pos++; + while (pos < line.Length && line[pos] != '=') + { + pos++; + } + + string key = line[keyStart..pos]; + if (!IsValidKey(key)) + { + if (key.Contains('\\')) + throw EnvironmentException.Env006(key); + + throw EnvironmentException.Env003(line); + } + + // Equals with optional whitespace + pos = SkipWhitespace(line, pos); + if (pos >= line.Length || line[pos] != '=') + { + throw EnvironmentException.Env001(line); + } + + pos++; // skip '=' + + string value = ParseValue(reader, line, ref pos); + + if (result.ContainsKey(key)) + { + throw EnvironmentException.Env002(key); + } + + result[key] = value; + } + + return new DotEnv(result); + } + + private static bool IsValidKey(string key) + { + foreach (char c in key) + { + if (!IsKeyChar(c)) + { + return false; + } + } + return true; + } + + private static string ParseValue(TextReader reader, string line, ref int pos) + { + if (pos >= line.Length) + { + return string.Empty; + } + + char c = line[pos]; + + return c is '"' or '\'' + ? ParseQuotedValue(reader, line, ref pos, c) + : ParseUnquotedOrContinuation(reader, line, ref pos); + } + + private static string ParseQuotedValue(TextReader reader, string line, ref int pos, char quote) + { + var sb = new StringBuilder(); + pos++; // skip opening quote + while (true) + { + while (pos < line.Length) + { + char c = line[pos]; + if (c == quote) + { + pos++; + return sb.ToString(); + } + sb.Append(c); + pos++; + } + + // Multi-line: append newline and read next line + string? next = reader.ReadLine() ?? throw EnvironmentException.Env004(); + sb.Append('\n'); + line = next; + pos = 0; + } + } + + private static string ParseUnquotedOrContinuation(TextReader reader, string line, ref int pos) + { + var sb = new StringBuilder(); + + while (true) + { + int valueEnd = pos; + int lastNonWs = pos - 1; + bool sawComment = false; + + while (valueEnd < line.Length) + { + char c = line[valueEnd]; + if (c == '#') + { + sawComment = true; + break; + } + if (c is not (' ' or '\t')) + { + lastNonWs = valueEnd; + } + valueEnd++; + } + + int end = lastNonWs + 1; + + // Continuation: value (trimmed) ends with backslash + if (end > pos && line[end - 1] == '\\') + { + // The backslash must be the final character on the line: + // no comment after it, no trailing whitespace after it. + bool backslashIsLineEnd = end == line.Length; + if (sawComment || !backslashIsLineEnd) + { + throw EnvironmentException.Env005(line); + } + + sb.Append(line, pos, end - 1 - pos); + sb.Append('\n'); + + string? next = reader.ReadLine(); + if (next is null) + { + // Continuation at EOF is invalid. + throw EnvironmentException.Env005(line); + } + + line = next; + pos = SkipWhitespace(line, 0); + continue; + } + + sb.Append(line, pos, end - pos); + pos = end; + + // Trailing whitespace on an unquoted value is not allowed. + if (valueEnd > end) + { + throw EnvironmentException.Env001(line); + } + + break; + } + + return sb.ToString(); + } + + private static bool IsCommentStart(string line, int pos) + { + return line[pos] == '#' + || line[pos] == ';' + || (line[pos] == '/' && pos + 1 < line.Length && line[pos + 1] == '/'); + } + + private static int SkipWhitespace(string line, int pos) + { + while (pos < line.Length && line[pos] is ' ' or '\t') + { + pos++; + } + return pos; + } + + private static bool IsKeyStart(char c) + => char.IsAsciiLetter(c) || c == '_'; + + private static bool IsKeyChar(char c) + => char.IsAsciiLetterOrDigit(c) || c == '_'; +} diff --git a/Experiments/Bookgen.Experiments/EnvironmentException.cs b/Experiments/Bookgen.Experiments/EnvironmentException.cs new file mode 100644 index 00000000..128698d0 --- /dev/null +++ b/Experiments/Bookgen.Experiments/EnvironmentException.cs @@ -0,0 +1,34 @@ +namespace Bookgen.Experiments; + +public sealed class EnvironmentException : Exception +{ + public static EnvironmentException Env001(string line) + => new($"ENV001: Invalid Line Format: {line}"); + + public static EnvironmentException Env002(string key) + => new($"ENV002: Duplicate Key: {key}"); + + public static EnvironmentException Env003(string line) + => new($"ENV003: Invalid Key Format: {line}"); + + public static EnvironmentException Env004() + => new($"ENV004: Unclosed Quote"); + + public static EnvironmentException Env005(string line) + => new($"ENV005: Invalid Line Continuation: {line}"); + + public static EnvironmentException Env006(string key) + => new($"ENV006: Multi-line Key: {key}"); + + public EnvironmentException(string message) : base(message) + { + } + + public EnvironmentException() + { + } + + public EnvironmentException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Experiments/Bookgen.Experiments/IDotEnv.cs b/Experiments/Bookgen.Experiments/IDotEnv.cs new file mode 100644 index 00000000..a665ceee --- /dev/null +++ b/Experiments/Bookgen.Experiments/IDotEnv.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bookgen.Experiments; + +public interface IDotEnv +{ + IEnumerable Keys { get; } + bool ContainsKey(string key); + TValue GetValueOrDefault(string key, TValue defaultValue) where TValue : IParsable; + bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue? value) where TValue : IParsable; +} From 5ed61afb1034519978d2c8daad828003269b4ec4 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 15:28:21 +0200 Subject: [PATCH 02/32] Removed JSON Args support, will replace it with scripting --- Source/BookGen.Cli/ArgumentJsonItem.cs | 12 --- Source/BookGen.Cli/CommandRunner.cs | 63 +++------------ .../Markdown/RenderInterop/RenderInterop.cs | 1 - Source/BookGen/Commands/JsonArgsCommand.cs | 79 ------------------- Source/BookGen/Properties/launchSettings.json | 4 +- 5 files changed, 16 insertions(+), 143 deletions(-) delete mode 100644 Source/BookGen.Cli/ArgumentJsonItem.cs delete mode 100644 Source/BookGen/Commands/JsonArgsCommand.cs diff --git a/Source/BookGen.Cli/ArgumentJsonItem.cs b/Source/BookGen.Cli/ArgumentJsonItem.cs deleted file mode 100644 index 7a4e36d9..00000000 --- a/Source/BookGen.Cli/ArgumentJsonItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -namespace BookGen.Cli; - -public sealed class ArgumentJsonItem -{ - public required string Name { get; init; } - public required string[] Arguments { get; init; } -} diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index d438272e..3e0b826a 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Text.Json; using BookGen.Cli.ArgumentParsing; using BookGen.Cli.Internals; @@ -20,7 +19,6 @@ namespace BookGen.Cli; public sealed class CommandRunner { - private readonly JsonSerializerOptions _serializerOptions; private readonly CommandTree _commands; private readonly IServiceProvider _serviceProvider; private readonly ICommandHelpProvider _helpProvider; @@ -28,6 +26,7 @@ public sealed class CommandRunner private readonly CommandRunnerSettings _settings; private readonly List _globalOptionParsers; private readonly SupportedOs _currentOs; + private readonly Dictionary _cachedCommands; public IValidationContext ValidationContext { get; set; } @@ -59,8 +58,13 @@ private bool TryProvideInternal(Type parameterType, string commandName, [NotNull return false; } - private ICommand CreateCommand(Type commandType, string commandName) + private ICommand GetOrCreateCommand(Type commandType, string commandName) { + if (_cachedCommands.ContainsKey(commandName)) + { + return _cachedCommands[commandName]; + } + ConstructorInfo constructor = commandType .GetConstructors(BindingFlags.Public | BindingFlags.Instance) .OrderByDescending(c => c.GetParameters().Length) @@ -84,10 +88,12 @@ private ICommand CreateCommand(Type commandType, string commandName) constructorParameters.Add(parameterInstance); } - var instance = Activator.CreateInstance(commandType, constructorParameters.ToArray()) + ICommand instance = Activator.CreateInstance(commandType, constructorParameters.ToArray()) as ICommand ?? throw new InvalidOperationException(); - return (ICommand)instance; + _cachedCommands.TryAdd(commandName, instance); + + return instance; } public CommandRunner(IServiceProvider serviceProvider, @@ -95,12 +101,7 @@ public CommandRunner(IServiceProvider serviceProvider, ILogger log, CommandRunnerSettings settings) { - _serializerOptions = new JsonSerializerOptions - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true, - WriteIndented = true - }; + _cachedCommands = new Dictionary(); _globalOptionParsers = new List(); _commands = new CommandTree(); _serviceProvider = serviceProvider; @@ -265,13 +266,6 @@ public async Task Run(IReadOnlyList args) } } - private async Task LoadFromJsonFile(string jsonFile) - { - await using FileStream stream = File.OpenRead(jsonFile); - return await JsonSerializer.DeserializeAsync(stream, _serializerOptions) - ?? throw new InvalidOperationException("Failed to load arguments from json"); - } - public async Task RunCommand(string commandName, IReadOnlyList argsToParse) { Type? commandType; @@ -285,7 +279,7 @@ public async Task RunCommand(string commandName, IReadOnlyList args } Type? argumentType = commandType.GetArgumentType(); - ICommand command = CreateCommand(commandType, commandName); + ICommand command = GetOrCreateCommand(commandType, commandName); if (!command.SupportedOs.HasFlag(_currentOs)) { @@ -298,18 +292,6 @@ public async Task RunCommand(string commandName, IReadOnlyList args if (argumentType == null) return await command.ExecuteAsync(ArgumentsBase.Empty, argsToParse, tokenSource.Token); - string jsonFileName = Path.ChangeExtension(commandName, ".json"); - - string argsJson = Path.Combine(Environment.CurrentDirectory, jsonFileName); - - if (argsToParse.Count < 1 - && File.Exists(argsJson)) - { - _log.LogInformation("Loading arguments from {filename}...", jsonFileName); - ArgumentJsonItem[] items = await LoadFromJsonFile(argsJson); - - return await ExecuteMultiple(items, argumentType, command, commandName, tokenSource.Token); - } return await ExecuteSingle(argsToParse, argumentType, command, commandName, tokenSource.Token); } } @@ -349,23 +331,4 @@ private async Task ExecuteSingle(IReadOnlyList argsToParse, return await command.ExecuteAsync(args, argsToParse, token); } - - private async Task ExecuteMultiple(ArgumentJsonItem[] items, - Type argumentType, - ICommand command, - string commandName, - CancellationToken token) - { - foreach (ArgumentJsonItem item in items) - { - _log.LogInformation("Executing {name} from json file...", item.Name); - int exitcode = await ExecuteSingle(item.Arguments, argumentType, command, commandName, token); - if (exitcode != 0) - { - _log.LogCritical("Failed to execute {name}. Exit code: {exitcode}", item.Name, exitcode); - return exitcode; - } - } - return 0; - } } diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs index d95654cd..6105b890 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Globalization; -using System.Text; using System.Web; using BookGen.Lib.AppSettings; diff --git a/Source/BookGen/Commands/JsonArgsCommand.cs b/Source/BookGen/Commands/JsonArgsCommand.cs deleted file mode 100644 index 941ad01b..00000000 --- a/Source/BookGen/Commands/JsonArgsCommand.cs +++ /dev/null @@ -1,79 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.ComponentModel; -using System.Text.Json; - -using BookGen.Cli; -using BookGen.Cli.Annotations; - -using Microsoft.Extensions.Logging; - -namespace BookGen.Commands; - -[CommandName("jsonargs")] -[Description("Creates an empty json arguments template file for a given bookgen command.")] -[ExitCode(ExitCodes.Success, "The command completed successfully.")] -internal sealed class JsonArgsCommand : Command -{ - internal sealed class Arguments : BookGenArgumentBase - { - [Switch("c", "command", Required = true)] - [Description("Specifies the command for which the json template will be created.")] - public string CommandName { get; set; } - - public Arguments() - { - CommandName = string.Empty; - } - - public override ValidationResult Validate(IValidationContext context) - { - if (string.IsNullOrWhiteSpace(CommandName)) - { - return ValidationResult.Error("CommandName is missing"); - } - return ValidationResult.Ok(); - } - } - - - private readonly HashSet _commandNames; - private readonly ILogger _log; - - public JsonArgsCommand(ICommandRunnerProxy runnerProxy, ILogger log) - { - _commandNames = [.. runnerProxy.CommandNames]; - _log = log; - } - - public override int Execute(Arguments arguments, IReadOnlyList context) - { - if (!_commandNames.Contains(arguments.CommandName)) - { - _log.LogCritical("Command not found: {commandname}", arguments.CommandName); - return ExitCodes.ArgumentsError; - } - - var empty = new ArgumentJsonItem[] - { - new ArgumentJsonItem - { - Name = "Log entry describing action", - Arguments = Array.Empty(), - } - }; - - _log.LogInformation("Creating json file for {command} command...", arguments.CommandName); - var fileName = Path.Combine(arguments.Directory, $"{arguments.CommandName}.json"); - var json = JsonSerializer.Serialize(empty, new JsonSerializerOptions - { - WriteIndented = true, - }); - File.WriteAllText(fileName, json); - - return ExitCodes.Success; - } -} diff --git a/Source/BookGen/Properties/launchSettings.json b/Source/BookGen/Properties/launchSettings.json index 237c5735..4d5ad469 100644 --- a/Source/BookGen/Properties/launchSettings.json +++ b/Source/BookGen/Properties/launchSettings.json @@ -5,7 +5,9 @@ "distributionName": "" }, "BookGen": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "convert md2html --svg -i .\\src\\075-generics.md -tf .\\template\\template.html -o .\\docs --title \"Generics\"", + "workingDirectory": "P:\\github\\ultimatedotnetcheatsheet" }, "Gui": { "commandName": "Project", From 6ee68dddfe5c8683a04b74fd557e528f5c1f2b02 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 15:48:09 +0200 Subject: [PATCH 03/32] Dotenv parsing integrated --- Source/BookGen.Cli/Dotenv/DotEnvParser.cs | 213 ++++++++++++++++++ Source/BookGen.Cli/Dotenv/DotEnvSettings.cs | 44 ++++ .../Dotenv/EnvironmentException.cs | 34 +++ Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs | 142 ++++++++++++ 4 files changed, 433 insertions(+) create mode 100644 Source/BookGen.Cli/Dotenv/DotEnvParser.cs create mode 100644 Source/BookGen.Cli/Dotenv/DotEnvSettings.cs create mode 100644 Source/BookGen.Cli/Dotenv/EnvironmentException.cs create mode 100644 Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs diff --git a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs new file mode 100644 index 00000000..7b2ebfd2 --- /dev/null +++ b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs @@ -0,0 +1,213 @@ +using System.Text; + +namespace BookGen.Cli.Dotenv; + +public static class DotEnvParser +{ + public static DotEnvSettings Parse(TextReader reader, StringComparer comparer) + { + var result = new Dictionary(comparer); + string? line; + + while ((line = reader.ReadLine()) is not null) + { + int pos = SkipWhitespace(line, 0); + + // Empty line + if (pos >= line.Length) + { + continue; + } + + // Comment line + if (IsCommentStart(line, pos)) + { + continue; + } + + // Key + int keyStart = pos; + if (!IsKeyStart(line[pos])) + { + throw EnvironmentException.Env003(line); + } + + pos++; + while (pos < line.Length && line[pos] != '=') + { + pos++; + } + + string key = line[keyStart..pos]; + if (!IsValidKey(key)) + { + if (key.Contains('\\')) + throw EnvironmentException.Env006(key); + + throw EnvironmentException.Env003(line); + } + + // Equals with optional whitespace + pos = SkipWhitespace(line, pos); + if (pos >= line.Length || line[pos] != '=') + { + throw EnvironmentException.Env001(line); + } + + pos++; // skip '=' + + string value = ParseValue(reader, line, ref pos); + + if (result.ContainsKey(key)) + { + throw EnvironmentException.Env002(key); + } + + result[key] = value; + } + + return new DotEnvSettings(result); + } + + private static bool IsValidKey(string key) + { + foreach (char c in key) + { + if (!IsKeyChar(c)) + { + return false; + } + } + return true; + } + + private static string ParseValue(TextReader reader, string line, ref int pos) + { + if (pos >= line.Length) + { + return string.Empty; + } + + char c = line[pos]; + + return c is '"' or '\'' + ? ParseQuotedValue(reader, line, ref pos, c) + : ParseUnquotedOrContinuation(reader, line, ref pos); + } + + private static string ParseQuotedValue(TextReader reader, string line, ref int pos, char quote) + { + var sb = new StringBuilder(); + pos++; // skip opening quote + while (true) + { + while (pos < line.Length) + { + char c = line[pos]; + if (c == quote) + { + pos++; + return sb.ToString(); + } + sb.Append(c); + pos++; + } + + // Multi-line: append newline and read next line + string? next = reader.ReadLine() ?? throw EnvironmentException.Env004(); + sb.Append('\n'); + line = next; + pos = 0; + } + } + + private static string ParseUnquotedOrContinuation(TextReader reader, string line, ref int pos) + { + var sb = new StringBuilder(); + + while (true) + { + int valueEnd = pos; + int lastNonWs = pos - 1; + bool sawComment = false; + + while (valueEnd < line.Length) + { + char c = line[valueEnd]; + if (c == '#') + { + sawComment = true; + break; + } + if (c is not (' ' or '\t')) + { + lastNonWs = valueEnd; + } + valueEnd++; + } + + int end = lastNonWs + 1; + + // Continuation: value (trimmed) ends with backslash + if (end > pos && line[end - 1] == '\\') + { + // The backslash must be the final character on the line: + // no comment after it, no trailing whitespace after it. + bool backslashIsLineEnd = end == line.Length; + if (sawComment || !backslashIsLineEnd) + { + throw EnvironmentException.Env005(line); + } + + sb.Append(line, pos, end - 1 - pos); + sb.Append('\n'); + + string? next = reader.ReadLine(); + if (next is null) + { + // Continuation at EOF is invalid. + throw EnvironmentException.Env005(line); + } + + line = next; + pos = SkipWhitespace(line, 0); + continue; + } + + sb.Append(line, pos, end - pos); + pos = end; + + // Trailing whitespace on an unquoted value is not allowed. + if (valueEnd > end) + { + throw EnvironmentException.Env001(line); + } + + break; + } + + return sb.ToString(); + } + + private static bool IsCommentStart(string line, int pos) + { + return line[pos] == '#' + || line[pos] == ';' + || (line[pos] == '/' && pos + 1 < line.Length && line[pos + 1] == '/'); + } + + private static int SkipWhitespace(string line, int pos) + { + while (pos < line.Length && line[pos] is ' ' or '\t') + { + pos++; + } + return pos; + } + + private static bool IsKeyStart(char c) + => char.IsAsciiLetter(c) || c == '_'; + + private static bool IsKeyChar(char c) + => char.IsAsciiLetterOrDigit(c) || c == '_'; +} diff --git a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs new file mode 100644 index 00000000..78048942 --- /dev/null +++ b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; + +namespace BookGen.Cli.Dotenv; + +public sealed class DotEnvSettings +{ + private readonly Dictionary _keyValues; + + internal DotEnvSettings(Dictionary keyValues) + { + _keyValues = keyValues; + } + + public IEnumerable Keys + => _keyValues.Keys; + + public bool ContainsKey(string key) + => _keyValues.ContainsKey(key); + + + public TValue GetValueOrDefault(string key, TValue defaultValue) + where TValue : IParsable + { + if (_keyValues.TryGetValue(key, out string? value) + && TValue.TryParse(value, null, out TValue? parsedValue)) + { + return parsedValue; + } + return defaultValue; + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue? value) + where TValue : IParsable + { + if (_keyValues.TryGetValue(key, out string? strValue) + && TValue.TryParse(strValue, null, out TValue? parsedValue)) + { + value = parsedValue; + return true; + } + value = default; + return false; + } +} diff --git a/Source/BookGen.Cli/Dotenv/EnvironmentException.cs b/Source/BookGen.Cli/Dotenv/EnvironmentException.cs new file mode 100644 index 00000000..61b6968c --- /dev/null +++ b/Source/BookGen.Cli/Dotenv/EnvironmentException.cs @@ -0,0 +1,34 @@ +namespace BookGen.Cli.Dotenv; + +public sealed class EnvironmentException : Exception +{ + public static EnvironmentException Env001(string line) + => new($"ENV001: Invalid Line Format: {line}"); + + public static EnvironmentException Env002(string key) + => new($"ENV002: Duplicate Key: {key}"); + + public static EnvironmentException Env003(string line) + => new($"ENV003: Invalid Key Format: {line}"); + + public static EnvironmentException Env004() + => new($"ENV004: Unclosed Quote"); + + public static EnvironmentException Env005(string line) + => new($"ENV005: Invalid Line Continuation: {line}"); + + public static EnvironmentException Env006(string key) + => new($"ENV006: Multi-line Key: {key}"); + + public EnvironmentException(string message) : base(message) + { + } + + public EnvironmentException() + { + } + + public EnvironmentException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs new file mode 100644 index 00000000..104dec7f --- /dev/null +++ b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs @@ -0,0 +1,142 @@ +using BookGen.Cli.Dotenv; + +namespace Bookgen.Tests.Cli; + +[TestFixture] +public class UT_DotEnvParser +{ + //https://github.com/env-lang/env/blob/main/env.md + + [Test] + public void EnsureThat_Comments_And_EmptyLines_AreIgnored() + { + const string dotenv = """ + ; This is a semicolon comment + // This is a double-slash comment + + # This is a hash comment + + """; + + using var reader = new StringReader(dotenv); + + DotEnvSettings env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + Assert.That(env.Keys, Is.Empty, "Expected no keys to be parsed from comments."); + } + + [TestCase("SECRET=\"password#123\" # The '#' is part of the value", "SECRET", "password#123")] + [TestCase("MESSAGE=\"Hello # World\" # The '#' after Hello is part of the value", "MESSAGE", "Hello # World")] + [TestCase("URL=https://example.com/path?foo=bar&baz=qux", "URL", "https://example.com/path?foo=bar&baz=qux")] + [TestCase("KEY=\"VAL//UE\" //comment", "KEY", "VAL//UE")] + [TestCase("KEY=\"VAL;UE\" ; comment", "KEY", "VAL;UE")] + [TestCase("FOO=value", "FOO", "value")] + [TestCase("foo=value", "foo", "value")] + [TestCase("FOO_BAR=value", "FOO_BAR", "value")] + [TestCase("_FOO=value", "_FOO", "value")] + [TestCase("FOO=bar", "FOO", "bar")] + [TestCase("FOO= bar", "FOO", " bar")] + [TestCase("FOO=bar baz", "FOO", "bar baz")] + [TestCase("FOO=\"BAR\"", "FOO", "BAR")] + [TestCase("FOO=\" bar \"", "FOO", " bar ")] + [TestCase("EMPTY=", "EMPTY", "")] + [TestCase("EMPTY=\"\"", "EMPTY", "")] + [TestCase("UNQUOTED=value with spaces", "UNQUOTED", "value with spaces")] + [TestCase("PATH=/usr/local/bin:/usr/bin:/bin", "PATH", "/usr/local/bin:/usr/bin:/bin")] + [TestCase("MESSAGE='Hello World'", "MESSAGE", "Hello World")] + [TestCase("PATH=\"C:\\Program Files\\App\"", "PATH", "C:\\Program Files\\App")] + [TestCase("HASH=\"my#password\"", "HASH", "my#password")] + public void EnsureThat_Keys_And_Values_ParsedCorrectly(string input, string expectedKey, string expectedValue) + { + using var reader = new StringReader(input); + + DotEnvSettings env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key(expectedKey)); + Assert.That(env.GetValueOrDefault(expectedKey, string.Empty), Is.EqualTo(expectedValue)); + } + } + + [TestCase("123FOO=value")] + [TestCase("FOO-BAR=value")] + [TestCase(".FOO=value")] + [TestCase("FOO\\")] + [TestCase("BAR=value ")] + [TestCase("foo\nbar\nbaz=qux")] + public void EnsureThat_InvalidKeys_ThrowException(string input) + { + using var reader = new StringReader(input); + Assert.Throws(() => DotEnvParser.Parse(reader, StringComparer.Ordinal)); + } + + [Test] + public void EnsureThat_Quoted_MultiLine_Works() + { + const string dotenv = """ + PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzrw + ... + -----END RSA PRIVATE KEY-----" + """; + using var reader = new StringReader(dotenv); + + DotEnvSettings env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + const string expectedValue = """ + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzrw + ... + -----END RSA PRIVATE KEY----- + """; + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key("PRIVATE_KEY")); + Assert.That(env.GetValueOrDefault("PRIVATE_KEY", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); + } + } + + [Test] + public void EnsureThat_BackSlashes_Work() + { + const string dotenv = """ + LONG_MESSAGE=first line \ + second line \ + third line + """; + using var reader = new StringReader(dotenv); + + DotEnvSettings env = DotEnvParser.Parse(reader, StringComparer.Ordinal); + + const string expectedValue = """ + first line + second line + third line + """; + + using (Assert.EnterMultipleScope()) + { + Assert.That(env.Keys, Contains.Key("LONG_MESSAGE")); + Assert.That(env.GetValueOrDefault("LONG_MESSAGE", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); + } + } + + [TestCase("foo\nbar\nbaz=qux", "ENV001: Invalid Line Format")] + [TestCase("foo=bar\r\nfoo=bar", "ENV002: Duplicate Key")] + [TestCase("123FOO=value", "ENV003: Invalid Key Format")] + [TestCase("FOO-BAR=value", "ENV003: Invalid Key Format")] + [TestCase(".FOO=value", "ENV003: Invalid Key Format")] + [TestCase("Foo=\"bar", "ENV004: Unclosed Quote")] + [TestCase("SECRET=password\\ # comment\r\nNEXT=value", "ENV005: Invalid Line Continuation")] + [TestCase("MULTI\\\r\nLINE_KEY=value", "ENV006: Multi-line Key")] + public void EnsureThat_Error_Handling_Works_AccordingToSpec(string input, string expectedMessage) + { + using var reader = new StringReader(input); + + EnvironmentException? ex = Assert.Throws(() => DotEnvParser.Parse(reader, StringComparer.Ordinal)); + + Assert.That(ex.Message, Does.StartWith(expectedMessage)); + } +} From f743c032bbe948b02dae9c9870f6df15b8010e00 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 16:14:38 +0200 Subject: [PATCH 04/32] Config: Transitioning to DotEnv --- Source/BookGen.Cli/Dotenv/DotEnvSettings.cs | 21 ++- Source/BookGen.Lib/AppSettings/AppSettings.cs | 125 ------------------ .../BookGen.Lib/AppSettings/IAppSettings.cs | 12 -- .../AppSettings/IReadOnlyAppSettings.cs | 20 --- Source/BookGen.Lib/BookEnvironment.cs | 1 - Source/BookGen.Lib/BookGen.Lib.csproj | 6 + Source/BookGen.Lib/Domain/IO/AppSetting.cs | 33 ----- Source/BookGen.Lib/Http/HttpServerFactory.cs | 1 - Source/BookGen.Lib/Http/PreviewRoutes.cs | 1 - .../{AppSettings => }/IProgramPathResolver.cs | 2 +- .../BookGen.Lib/Pipeline/IBookEnvironment.cs | 1 - .../Markdown/RenderInterop/IRenderInterop.cs | 1 - .../Markdown/RenderInterop/RenderInterop.cs | 1 - Source/BookGen/BuildCommandBase.cs | 1 - Source/BookGen/Commands/Book/LinksCommand.cs | 1 - Source/BookGen/Commands/Book/StatsCommand.cs | 1 - .../BookGen/Commands/Book/ValidateCommand.cs | 1 - Source/BookGen/Commands/Build/BuildEpub.cs | 2 +- .../Commands/Build/BuildExportCommand.cs | 2 +- .../Commands/Build/BuildFeedCommand.cs | 2 +- Source/BookGen/Commands/Build/BuildPlugin.cs | 1 - .../Commands/Build/BuildPrintCommand.cs | 2 +- .../BookGen/Commands/Build/BuildWebCommand.cs | 2 +- .../Commands/Build/BuildWordpressCommand.cs | 2 +- Source/BookGen/Commands/ConfigCommand.cs | 86 ------------ .../Commands/Convert/Diagram2SvgCommand.cs | 2 +- .../Commands/Convert/Math2SvgCommand.cs | 2 +- .../Commands/Convert/Md2HtmlCommand.cs | 1 - .../BookGen/Commands/Convert/QrCodeCommand.cs | 2 +- .../BookGen/Commands/Folder/PreviewCommand.cs | 1 - .../BookGen/Commands/folder/SearchCommand.cs | 1 - .../Infrastructure}/ProgramPathResolver.cs | 41 +++--- Source/BookGen/Program.cs | 6 +- .../Bookgen.Tests/Commands/CommandTestBase.cs | 2 +- Test/Bookgen.Tests/TestAppSettings.cs | 39 ------ Test/Bookgen.Tests/TestEnvironment.cs | 5 +- 36 files changed, 68 insertions(+), 362 deletions(-) delete mode 100644 Source/BookGen.Lib/AppSettings/AppSettings.cs delete mode 100644 Source/BookGen.Lib/AppSettings/IAppSettings.cs delete mode 100644 Source/BookGen.Lib/AppSettings/IReadOnlyAppSettings.cs delete mode 100644 Source/BookGen.Lib/Domain/IO/AppSetting.cs rename Source/BookGen.Lib/{AppSettings => }/IProgramPathResolver.cs (95%) delete mode 100644 Source/BookGen/Commands/ConfigCommand.cs rename Source/{BookGen.Lib/AppSettings => BookGen/Infrastructure}/ProgramPathResolver.cs (66%) delete mode 100644 Test/Bookgen.Tests/TestAppSettings.cs diff --git a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs index 78048942..fe4b1ff5 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs @@ -6,11 +6,30 @@ public sealed class DotEnvSettings { private readonly Dictionary _keyValues; - internal DotEnvSettings(Dictionary keyValues) + public DotEnvSettings(Dictionary keyValues) { _keyValues = keyValues; } + public DotEnvSettings() + { + _keyValues = new Dictionary(); + } + + public void AddRange(IEnumerable> keyValues) + { + foreach (var kvp in keyValues) + { + _keyValues[kvp.Key] = kvp.Value; + } + } + + public void Add(string key, string value) + => _keyValues.Add(key, value); + + public void Clear() + => _keyValues.Clear(); + public IEnumerable Keys => _keyValues.Keys; diff --git a/Source/BookGen.Lib/AppSettings/AppSettings.cs b/Source/BookGen.Lib/AppSettings/AppSettings.cs deleted file mode 100644 index dea6d97d..00000000 --- a/Source/BookGen.Lib/AppSettings/AppSettings.cs +++ /dev/null @@ -1,125 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.Json; - -using BookGen.Lib.Domain.IO; -using BookGen.Lib.Internals; - -using BookGen.Vfs; - -namespace BookGen.Lib.AppSettings; - -public sealed class AppSettings : IAppSettings -{ - private readonly AppSetting _appSetting; - private readonly string _appSettingsFilePath; - private readonly IWritableFileSystem _fileSystem; - private readonly SerializedObjectValidator _validator; - private readonly Dictionary> _problematicProperties; - private readonly PropertyInfo[] _properites; - - private AppSetting? LoadSettings() - { - return _fileSystem.FileExists(_appSettingsFilePath) - ? _fileSystem.Deserialize(_appSettingsFilePath) - : null; - } - - private void Validate() - { - if (_appSetting == null) - return; - - var issues = new List(); - _validator.ValidateSingleLevel(_appSetting, issues); - - _problematicProperties.Clear(); - - foreach (ValidationResult issue in issues) - { - foreach (string memberName in issue.MemberNames) - { - if (!_problematicProperties.TryGetValue(memberName, out List? value)) - { - value = []; - _problematicProperties[memberName] = value; - } - value.Add(issue.ErrorMessage ?? "Unknown error"); - } - } - } - - private static string GetPropertyName(Expression> expression) - { - return expression.Body is MemberExpression memberExpression - ? memberExpression.Member.Name - : throw new ArgumentException("Invalid expression", nameof(expression)); - } - - public AppSettings(IWritableFileSystem fileSystem) - { - _properites = typeof(AppSetting).GetProperties(BindingFlags.Public | BindingFlags.Instance); - _fileSystem = fileSystem; - _problematicProperties = new Dictionary>(); - _appSettingsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "bookgen.app.json"); - _validator = new SerializedObjectValidator(_fileSystem); - _appSetting = LoadSettings() ?? AppSetting.CreateDefault(); - Validate(); - } - - public IEnumerable<(string setting, Type type)> KnownSettings - { - get - { - foreach (PropertyInfo property in _properites) - { - yield return (property.Name, property.PropertyType); - } - } - } - - public T Get(Func selector) - => selector(_appSetting); - - public object? Get(string settingName) - { - var propery = _properites.First(x => x.Name == settingName); - return propery.GetValue(_appSetting); - } - - public bool IsSettingValid(string settingName, out IReadOnlyList issues) - { - bool ret = _problematicProperties.TryGetValue(settingName, out List? issueList); - issues = issueList ?? []; - return ret; - } - - public bool IsSettingValid(Expression> expression, out IReadOnlyList issues) - { - string propertyName = GetPropertyName(expression); - if (_problematicProperties.TryGetValue(propertyName, out List? value)) - { - issues = value; - return false; - } - issues = []; - return true; - } - - public void Set(string setting, string value) - { - PropertyInfo property = _properites.First(x => x.Name == setting); - object convertedValue = Convert.ChangeType(value, property.PropertyType); - property.SetValue(_appSetting, convertedValue); - Validate(); - } - - public void Save() - => _fileSystem.Serialize(_appSettingsFilePath, _appSetting); -} diff --git a/Source/BookGen.Lib/AppSettings/IAppSettings.cs b/Source/BookGen.Lib/AppSettings/IAppSettings.cs deleted file mode 100644 index 8f47d016..00000000 --- a/Source/BookGen.Lib/AppSettings/IAppSettings.cs +++ /dev/null @@ -1,12 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -namespace BookGen.Lib.AppSettings; - -public interface IAppSettings : IReadOnlyAppSettings -{ - void Save(); - void Set(string setting, string value); -} diff --git a/Source/BookGen.Lib/AppSettings/IReadOnlyAppSettings.cs b/Source/BookGen.Lib/AppSettings/IReadOnlyAppSettings.cs deleted file mode 100644 index 030c59aa..00000000 --- a/Source/BookGen.Lib/AppSettings/IReadOnlyAppSettings.cs +++ /dev/null @@ -1,20 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.Linq.Expressions; - -using BookGen.Lib.Domain.IO; - -namespace BookGen.Lib.AppSettings; - -public interface IReadOnlyAppSettings -{ - IEnumerable<(string setting, Type type)> KnownSettings { get; } - T Get(Func selector); - object? Get(string settingName); - bool IsSettingValid(string settingName, out IReadOnlyList issues); - bool IsSettingValid(Expression> expression, out IReadOnlyList issues); - -} diff --git a/Source/BookGen.Lib/BookEnvironment.cs b/Source/BookGen.Lib/BookEnvironment.cs index 55ecf3b5..473d8b3f 100644 --- a/Source/BookGen.Lib/BookEnvironment.cs +++ b/Source/BookGen.Lib/BookEnvironment.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; -using BookGen.Lib.AppSettings; using BookGen.Lib.Confighandling; using BookGen.Lib.Domain.IO; using BookGen.Lib.Domain.IO.Configuration; diff --git a/Source/BookGen.Lib/BookGen.Lib.csproj b/Source/BookGen.Lib/BookGen.Lib.csproj index 3a777f7e..568c8d1d 100644 --- a/Source/BookGen.Lib/BookGen.Lib.csproj +++ b/Source/BookGen.Lib/BookGen.Lib.csproj @@ -11,6 +11,12 @@ True + + + + + + diff --git a/Source/BookGen.Lib/Domain/IO/AppSetting.cs b/Source/BookGen.Lib/Domain/IO/AppSetting.cs deleted file mode 100644 index 87276ef1..00000000 --- a/Source/BookGen.Lib/Domain/IO/AppSetting.cs +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using BookGen.Lib.Domain.Validation; - -namespace BookGen.Lib.Domain.IO; - -public sealed record class AppSetting -{ - [WhenNotEmptyFileMustExist] - public string? NodeJsPath { get; set; } - - [WhenNotEmptyFileMustExist] - public string? PythonPath { get; set; } - - [WhenNotEmptyFileMustExist] - public string? RatexPath { get; set; } - - [WhenNotEmptyFileMustExist] - public string? MmdrPath { get; set; } - - [WhenNotEmptyFileMustExist] - public string? PlantUmlPath { get; set; } - - public static AppSetting CreateDefault() - { - return new AppSetting - { - }; - } -} diff --git a/Source/BookGen.Lib/Http/HttpServerFactory.cs b/Source/BookGen.Lib/Http/HttpServerFactory.cs index b30fb53b..10e49459 100644 --- a/Source/BookGen.Lib/Http/HttpServerFactory.cs +++ b/Source/BookGen.Lib/Http/HttpServerFactory.cs @@ -6,7 +6,6 @@ using System.Net.Mime; using System.Net.NetworkInformation; -using BookGen.Lib.AppSettings; using BookGen.Vfs; using Microsoft.AspNetCore.Http; diff --git a/Source/BookGen.Lib/Http/PreviewRoutes.cs b/Source/BookGen.Lib/Http/PreviewRoutes.cs index 6defef68..70f37fd0 100644 --- a/Source/BookGen.Lib/Http/PreviewRoutes.cs +++ b/Source/BookGen.Lib/Http/PreviewRoutes.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.Mime; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering; diff --git a/Source/BookGen.Lib/AppSettings/IProgramPathResolver.cs b/Source/BookGen.Lib/IProgramPathResolver.cs similarity index 95% rename from Source/BookGen.Lib/AppSettings/IProgramPathResolver.cs rename to Source/BookGen.Lib/IProgramPathResolver.cs index 39c7d33f..b729c50c 100644 --- a/Source/BookGen.Lib/AppSettings/IProgramPathResolver.cs +++ b/Source/BookGen.Lib/IProgramPathResolver.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; -namespace BookGen.Lib.AppSettings; +namespace BookGen.Lib; public interface IProgramPathResolver { diff --git a/Source/BookGen.Lib/Pipeline/IBookEnvironment.cs b/Source/BookGen.Lib/Pipeline/IBookEnvironment.cs index 49f8ae47..9b0be4e1 100644 --- a/Source/BookGen.Lib/Pipeline/IBookEnvironment.cs +++ b/Source/BookGen.Lib/Pipeline/IBookEnvironment.cs @@ -3,7 +3,6 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO; using BookGen.Lib.Domain.IO.Configuration; diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/IRenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/IRenderInterop.cs index e82d482f..3373747e 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/IRenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/IRenderInterop.cs @@ -3,7 +3,6 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering.Images; diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs index 6105b890..ac11b7d7 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Web; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering.Images; diff --git a/Source/BookGen/BuildCommandBase.cs b/Source/BookGen/BuildCommandBase.cs index 35d59a34..b703b702 100644 --- a/Source/BookGen/BuildCommandBase.cs +++ b/Source/BookGen/BuildCommandBase.cs @@ -7,7 +7,6 @@ using BookGen.Cli.Annotations; using BookGen.Infrastructure.Loging; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Book/LinksCommand.cs b/Source/BookGen/Commands/Book/LinksCommand.cs index c3727f87..3218e713 100644 --- a/Source/BookGen/Commands/Book/LinksCommand.cs +++ b/Source/BookGen/Commands/Book/LinksCommand.cs @@ -12,7 +12,6 @@ using BookGen.Cli.Annotations; using BookGen.Infrastructure.Loging; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Book/StatsCommand.cs b/Source/BookGen/Commands/Book/StatsCommand.cs index 95f3813b..6fc2c9f7 100644 --- a/Source/BookGen/Commands/Book/StatsCommand.cs +++ b/Source/BookGen/Commands/Book/StatsCommand.cs @@ -10,7 +10,6 @@ using BookGen.Infrastructure.Loging; using BookGen.Infrastructure.Terminal; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Book/ValidateCommand.cs b/Source/BookGen/Commands/Book/ValidateCommand.cs index 90963aa0..014889c1 100644 --- a/Source/BookGen/Commands/Book/ValidateCommand.cs +++ b/Source/BookGen/Commands/Book/ValidateCommand.cs @@ -8,7 +8,6 @@ using BookGen.Cli; using BookGen.Cli.Annotations; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Vfs; using Microsoft.Extensions.Logging; diff --git a/Source/BookGen/Commands/Build/BuildEpub.cs b/Source/BookGen/Commands/Build/BuildEpub.cs index b5bff721..37a05918 100644 --- a/Source/BookGen/Commands/Build/BuildEpub.cs +++ b/Source/BookGen/Commands/Build/BuildEpub.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Build/BuildExportCommand.cs b/Source/BookGen/Commands/Build/BuildExportCommand.cs index 22a80d9d..42caea23 100644 --- a/Source/BookGen/Commands/Build/BuildExportCommand.cs +++ b/Source/BookGen/Commands/Build/BuildExportCommand.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Build/BuildFeedCommand.cs b/Source/BookGen/Commands/Build/BuildFeedCommand.cs index a87afd07..a1c37831 100644 --- a/Source/BookGen/Commands/Build/BuildFeedCommand.cs +++ b/Source/BookGen/Commands/Build/BuildFeedCommand.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Build/BuildPlugin.cs b/Source/BookGen/Commands/Build/BuildPlugin.cs index cfef40ec..cf623497 100644 --- a/Source/BookGen/Commands/Build/BuildPlugin.cs +++ b/Source/BookGen/Commands/Build/BuildPlugin.cs @@ -14,7 +14,6 @@ using BookGen.Infrastructure.Plugins; using BookGen.Infrastructure.Plugins.V1; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Vfs; using Microsoft.Extensions.Caching.Memory; diff --git a/Source/BookGen/Commands/Build/BuildPrintCommand.cs b/Source/BookGen/Commands/Build/BuildPrintCommand.cs index ec2baad5..9567814c 100644 --- a/Source/BookGen/Commands/Build/BuildPrintCommand.cs +++ b/Source/BookGen/Commands/Build/BuildPrintCommand.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Build/BuildWebCommand.cs b/Source/BookGen/Commands/Build/BuildWebCommand.cs index 8e82fc9e..22093894 100644 --- a/Source/BookGen/Commands/Build/BuildWebCommand.cs +++ b/Source/BookGen/Commands/Build/BuildWebCommand.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Build/BuildWordpressCommand.cs b/Source/BookGen/Commands/Build/BuildWordpressCommand.cs index a3ae8217..712fbcb5 100644 --- a/Source/BookGen/Commands/Build/BuildWordpressCommand.cs +++ b/Source/BookGen/Commands/Build/BuildWordpressCommand.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Pipeline; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/ConfigCommand.cs b/Source/BookGen/Commands/ConfigCommand.cs deleted file mode 100644 index b5b3214f..00000000 --- a/Source/BookGen/Commands/ConfigCommand.cs +++ /dev/null @@ -1,86 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.ComponentModel; - -using BookGen.Cli; -using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; - -using Microsoft.Extensions.Logging; - -using Spectre.Console; - -namespace BookGen.Commands; - -[CommandName("config")] -[Description("Get or set the application settings. Without any arguments, the command will display the current settings.")] -[ExitCode(ExitCodes.Success, "The command completed successfully.")] -[ExitCode(ExitCodes.GeneralError, "An error occurred while executing the command.")] -internal sealed class ConfigCommand : Command -{ - internal sealed class Arguments : ArgumentsBase - { - [Argument(0, IsOptional = true)] - [Description("The setting to be configured.")] - public string Setting { get; set; } = string.Empty; - - [Argument(1, IsOptional = true)] - [Description("The value to set for the specified setting.")] - public string Value { get; set; } = string.Empty; - } - - private readonly ILogger _logger; - private readonly IAppSettings _appSettings; - - public ConfigCommand(ILogger logger, IAppSettings appSettings) - { - _logger = logger; - _appSettings = appSettings; - } - - public override int Execute(Arguments arguments, IReadOnlyList context) - { - if (string.IsNullOrEmpty(arguments.Setting) - && string.IsNullOrEmpty(arguments.Value)) - { - PrintCurrentSettings(); - return ExitCodes.Success; - } - - if (!_appSettings.KnownSettings.Any(x => x.setting == arguments.Setting)) - { - _logger.LogError("Unknown setting: {Setting}", arguments.Setting); - return ExitCodes.GeneralError; - } - - _appSettings.Set(arguments.Setting, arguments.Value); - - if (!_appSettings.IsSettingValid(arguments.Setting, out IReadOnlyList issues)) - { - foreach (var issue in issues) - { - _logger.LogError(issue); - } - return ExitCodes.GeneralError; - } - - _appSettings.Save(); - return ExitCodes.Success; - } - - private void PrintCurrentSettings() - { - var table = new Table(); - table.AddColumns("Setting", "Type", "Is Valid?", "Value"); - foreach ((string setting, Type type) setting in _appSettings.KnownSettings) - { - object? value = _appSettings.Get(setting.setting); - bool isValid = _appSettings.IsSettingValid(setting.setting, out _); - table.AddRow(setting.setting, setting.type.Name, isValid ? "Yes" : "No", value?.ToString() ?? "null"); - } - AnsiConsole.Write(table); - } -} diff --git a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs index 76e328f5..d446f9f4 100644 --- a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs +++ b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs @@ -7,7 +7,7 @@ using BookGen.Cli; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown.RenderInterop; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Convert/Math2SvgCommand.cs b/Source/BookGen/Commands/Convert/Math2SvgCommand.cs index 0651accd..b59e6c8b 100644 --- a/Source/BookGen/Commands/Convert/Math2SvgCommand.cs +++ b/Source/BookGen/Commands/Convert/Math2SvgCommand.cs @@ -7,7 +7,7 @@ using BookGen.Cli; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown.RenderInterop; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs index baf7f1c6..4ae3dc14 100644 --- a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs +++ b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs @@ -9,7 +9,6 @@ using BookGen.Cli; using BookGen.Cli.Annotations; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown; diff --git a/Source/BookGen/Commands/Convert/QrCodeCommand.cs b/Source/BookGen/Commands/Convert/QrCodeCommand.cs index 67d2a9f6..b49c3208 100644 --- a/Source/BookGen/Commands/Convert/QrCodeCommand.cs +++ b/Source/BookGen/Commands/Convert/QrCodeCommand.cs @@ -7,7 +7,7 @@ using BookGen.Cli; using BookGen.Cli.Annotations; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown.RenderInterop; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/Folder/PreviewCommand.cs b/Source/BookGen/Commands/Folder/PreviewCommand.cs index 861060f7..5ef523e3 100644 --- a/Source/BookGen/Commands/Folder/PreviewCommand.cs +++ b/Source/BookGen/Commands/Folder/PreviewCommand.cs @@ -8,7 +8,6 @@ using BookGen.Cli; using BookGen.Cli.Annotations; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Http; using BookGen.Vfs; diff --git a/Source/BookGen/Commands/folder/SearchCommand.cs b/Source/BookGen/Commands/folder/SearchCommand.cs index 6383e1bd..f024e9e4 100644 --- a/Source/BookGen/Commands/folder/SearchCommand.cs +++ b/Source/BookGen/Commands/folder/SearchCommand.cs @@ -11,7 +11,6 @@ using BookGen.Cli.Annotations; using BookGen.Infrastructure.Loging; using BookGen.Lib; -using BookGen.Lib.AppSettings; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown; diff --git a/Source/BookGen.Lib/AppSettings/ProgramPathResolver.cs b/Source/BookGen/Infrastructure/ProgramPathResolver.cs similarity index 66% rename from Source/BookGen.Lib/AppSettings/ProgramPathResolver.cs rename to Source/BookGen/Infrastructure/ProgramPathResolver.cs index 227e4e35..79a9665f 100644 --- a/Source/BookGen.Lib/AppSettings/ProgramPathResolver.cs +++ b/Source/BookGen/Infrastructure/ProgramPathResolver.cs @@ -4,15 +4,15 @@ //----------------------------------------------------------------------------- using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using BookGen.Lib.Domain.IO; +using BookGen.Cli.Dotenv; +using BookGen.Lib; -namespace BookGen.Lib.AppSettings; +namespace BookGen.Infrastructure; public sealed class ProgramPathResolver : IProgramPathResolver { - private readonly IReadOnlyAppSettings _appSettings; + private readonly DotEnvSettings _appSettings; private const string NodeJsExecutableName = "node"; private const string PythonExecutableName = "python"; @@ -20,18 +20,27 @@ public sealed class ProgramPathResolver : IProgramPathResolver private const string MmdrExecutableName = "mmdr"; private const string PlantUmlExecutableName = "plantuml"; - public ProgramPathResolver(IReadOnlyAppSettings appSettings) + private const string NodeJsKey = "NodeJsPath"; + private const string PythonKey = "PythonPath"; + private const string RatexKey = "RatexPath"; + private const string MmdrKey = "MmdrPath"; + private const string PlantUmlKey = "PlantUmlPath"; + + public ProgramPathResolver(DotEnvSettings appSettings) { _appSettings = appSettings; } - private string? Resolve(Expression> selector, string binaryName) + private string? Resolve(string key, string binaryName) { - // if App settings contains a valid path, return it - string? setting = _appSettings.Get(selector.Compile()); - if (setting != null && _appSettings.IsSettingValid(selector, out _)) + string settingValue = _appSettings.GetValueOrDefault(key, string.Empty); + if (!string.IsNullOrEmpty(settingValue)) { - return setting; + var fullPath = Path.GetFullPath(settingValue); + if (File.Exists(fullPath)) + { + return fullPath; + } } //if App path contains a valid path, return it @@ -39,6 +48,7 @@ public ProgramPathResolver(IReadOnlyAppSettings appSettings) ? Path.Combine(AppContext.BaseDirectory, $"{binaryName}.exe") : Path.Combine(AppContext.BaseDirectory, binaryName); + if (File.Exists(appBinaryPath)) return appBinaryPath; @@ -51,38 +61,37 @@ public ProgramPathResolver(IReadOnlyAppSettings appSettings) if (File.Exists(fullPath)) return fullPath; } - ; return null; } public bool TryResolvePythonPath([NotNullWhen(true)] out string? path) { - path = Resolve(x => x.PythonPath, PythonExecutableName); + path = Resolve(PythonKey, PythonExecutableName); return path != null; } public bool TryResolveNodeJsPath([NotNullWhen(true)] out string? path) { - path = Resolve(x => x.NodeJsPath, NodeJsExecutableName); + path = Resolve(NodeJsKey, NodeJsExecutableName); return path != null; } public bool TryResolveRatex([NotNullWhen(true)] out string? path) { - path = Resolve(x => x.RatexPath, RatexExecutableName); + path = Resolve(RatexKey, RatexExecutableName); return path != null; } public bool TryResolveMmdr([NotNullWhen(true)] out string? path) { - path = Resolve(x => x.MmdrPath, MmdrExecutableName); + path = Resolve(MmdrKey, MmdrExecutableName); return path != null; } public bool TryResolvePlantUml([NotNullWhen(true)] out string? path) { - path = Resolve(x => x.PlantUmlPath, PlantUmlExecutableName); + path = Resolve(PlantUmlKey, PlantUmlExecutableName); return path != null; } } diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index 6e6ed9eb..e94ac766 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -7,11 +7,12 @@ using BookGen; using BookGen.Cli; +using BookGen.Cli.Dotenv; using BookGen.Commands; using BookGen.GlobalOptionParsers; using BookGen.Infrastructure; using BookGen.Infrastructure.Loging; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Shell.Shared.Loging; using BookGen.Vfs; @@ -48,6 +49,7 @@ ioc.AddMemoryCache(); ioc.AddSingleton(logger); ioc.AddSingleton(info); +ioc.AddSingleton(); ioc.AddSingleton(runnerProxy); ioc.AddSingleton(); ioc.AddSingleton(ZipAssetSoruce.DefaultAssets()); @@ -55,8 +57,6 @@ ioc.AddTransient(); ioc.AddTransient(); ioc.AddTransient(); -ioc.AddTransient(); -ioc.AddTransient(); ioc.AddTransient(); ioc.AddKeyedSingleton("dictionaries", (provider, key) => { diff --git a/Test/Bookgen.Tests/Commands/CommandTestBase.cs b/Test/Bookgen.Tests/Commands/CommandTestBase.cs index c256d845..ec3012ea 100644 --- a/Test/Bookgen.Tests/Commands/CommandTestBase.cs +++ b/Test/Bookgen.Tests/Commands/CommandTestBase.cs @@ -4,7 +4,7 @@ //----------------------------------------------------------------------------- using BookGen.Cli; -using BookGen.Lib.AppSettings; +using BookGen.Lib; using BookGen.Vfs; using Microsoft.Extensions.Logging; diff --git a/Test/Bookgen.Tests/TestAppSettings.cs b/Test/Bookgen.Tests/TestAppSettings.cs deleted file mode 100644 index cd0f0563..00000000 --- a/Test/Bookgen.Tests/TestAppSettings.cs +++ /dev/null @@ -1,39 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.Linq.Expressions; - -using BookGen.Lib.AppSettings; -using BookGen.Lib.Domain.IO; - -namespace Bookgen.Tests; - -internal class TestAppSettings : IReadOnlyAppSettings -{ - public IEnumerable<(string setting, Type type)> KnownSettings - => Enumerable.Empty<(string setting, Type type)>(); - - public T Get(Func selector) - { - return default(T)!; - } - - public object? Get(string settingName) - { - return null; - } - - public bool IsSettingValid(string settingName, out IReadOnlyList issues) - { - issues = Array.Empty(); - return true; - } - - public bool IsSettingValid(Expression> expression, out IReadOnlyList issues) - { - issues = Array.Empty(); - return true; - } -} diff --git a/Test/Bookgen.Tests/TestEnvironment.cs b/Test/Bookgen.Tests/TestEnvironment.cs index c86cae56..59b0bea7 100644 --- a/Test/Bookgen.Tests/TestEnvironment.cs +++ b/Test/Bookgen.Tests/TestEnvironment.cs @@ -5,7 +5,8 @@ using System.Diagnostics.CodeAnalysis; -using BookGen.Lib.AppSettings; +using BookGen.Infrastructure; +using BookGen.Lib; using BookGen.Lib.Domain.IO; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Pipeline; @@ -40,7 +41,7 @@ public void Dispose() public TestEnvironment() { _assetSoruce = new ZipAssetSoruce(Path.Combine(AppContext.BaseDirectory, "assets.zip")); - ProgramPathResolver = new ProgramPathResolver(new TestAppSettings()); + ProgramPathResolver = new ProgramPathResolver(new BookGen.Cli.Dotenv.DotEnvSettings()); } public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) From 7f89a1628953f556ca9b25ffebc4b7bbfcfd428f Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 17:59:50 +0200 Subject: [PATCH 05/32] Settings from env files --- Source/BookGen.Cli/CommandRunner.cs | 7 +- Source/BookGen.Cli/Dotenv/DotEnvSettings.cs | 4 +- Source/BookGen.Cli/GlobalOptionParser.cs | 72 +++++++++++++++++++ Source/BookGen.Cli/IGlobalOptionParser.cs | 51 ------------- .../BookGen.Contents/BookGen.Contents.csproj | 3 + Source/BookGen.Contents/BookGen.env | 25 +++++++ .../AttachDebuggerParser.cs | 10 +-- .../GlobalOptionParsers/DotEnvFileParser.cs | 42 +++++++++++ .../GlobalOptionParsers/JsonLogParser.cs | 2 +- .../GlobalOptionParsers/LogToFileParser.cs | 2 +- .../RuntimePrintingParser.cs | 2 +- .../GlobalOptionParsers/WaitDebuggerParser.cs | 12 ++-- Source/BookGen/Program.cs | 26 ++++++- Test/Bookgen.Tests/UT_OpenCli.cs | 4 +- 14 files changed, 191 insertions(+), 71 deletions(-) create mode 100644 Source/BookGen.Cli/GlobalOptionParser.cs delete mode 100644 Source/BookGen.Cli/IGlobalOptionParser.cs create mode 100644 Source/BookGen.Contents/BookGen.env create mode 100644 Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index 3e0b826a..db1b9913 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -245,9 +245,12 @@ public async Task Run(IReadOnlyList args) HashSet parsedGlobals = new(); foreach (var parser in _globalOptionParsers) { - if (parser.TryParseGlobalOption(args, out string? globalOption)) + if (parser.TryParseGlobalOption(args, out List globalOptions)) { - parsedGlobals.Add(globalOption); + foreach (var option in globalOptions) + { + parsedGlobals.Add(option); + } } } diff --git a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs index fe4b1ff5..d3183e12 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs @@ -16,9 +16,9 @@ public DotEnvSettings() _keyValues = new Dictionary(); } - public void AddRange(IEnumerable> keyValues) + public void Merge(DotEnvSettings other) { - foreach (var kvp in keyValues) + foreach (KeyValuePair kvp in other._keyValues) { _keyValues[kvp.Key] = kvp.Value; } diff --git a/Source/BookGen.Cli/GlobalOptionParser.cs b/Source/BookGen.Cli/GlobalOptionParser.cs new file mode 100644 index 00000000..20e108a5 --- /dev/null +++ b/Source/BookGen.Cli/GlobalOptionParser.cs @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Cli; + +public abstract class GlobalOptionParser +{ + public string LongName { get; } + public string ShortName { get; } + public bool NeedsValue { get; } + + public GlobalOptionParser(string shortName, string longName, bool needsValue = false) + { + if (longName.StartsWith('-')) + throw new ArgumentException("Long name cannot start with '--'", nameof(longName)); + + if (shortName.StartsWith('-')) + throw new ArgumentException("Short name cannot start with '-'", nameof(shortName)); + + ShortName = $"-{shortName}"; + LongName = $"--{longName}"; + NeedsValue = needsValue; + } + + public bool TryParseGlobalOption(IReadOnlyList args, out List parsedValues) + { + bool handle = false; + parsedValues = new List(); + + int optionIndex = 0; + + for (int i=0; i args, [NotNullWhen(true)] out string? parsedOne) - { - bool handle = false; - parsedOne = null; - if (args.Contains(ShortName)) - { - handle = true; - parsedOne = ShortName; - } - else if (args.Contains(LongName)) - { - handle = true; - parsedOne = LongName; - } - - if (handle) - { - OnOptionWasPresent(); - } - - return handle; - } - - protected abstract void OnOptionWasPresent(); -} diff --git a/Source/BookGen.Contents/BookGen.Contents.csproj b/Source/BookGen.Contents/BookGen.Contents.csproj index d07e6253..61d33fdc 100644 --- a/Source/BookGen.Contents/BookGen.Contents.csproj +++ b/Source/BookGen.Contents/BookGen.Contents.csproj @@ -22,6 +22,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Source/BookGen.Contents/BookGen.env b/Source/BookGen.Contents/BookGen.env new file mode 100644 index 00000000..49aba69f --- /dev/null +++ b/Source/BookGen.Contents/BookGen.env @@ -0,0 +1,25 @@ +# ____ _ ____ _____ +# | __ ) ___ ___ | | __/ ___| ___ _ __ | ____|_ ____ __ +# | _ \ / _ \ / _ \| |/ / | _ / _ \ '_ \ | _| | '_ \ \ / / +# | |_) | (_) | (_) | <| |_| | __/ | | |_| |___| | | \ V / +# |____/ \___/ \___/|_|\_\\____|\___|_| |_(_)_____|_| |_|\_/ +# +# .env file example for configuring BookGen settings +# To specify a custom .env file use the -env or --env-file option when running BookGen. +# Uncomment and set the following variables to customize your BookGen environment. +# -------------------------------------------------------------------------------- + +# Custom Node.js path (optional. If not set, the default system Node.js will be used from PATH) +#NodeJsPath=/path/to/custom/nodejs + +# Custom Python path (optional. If not set, the default system Python will be used from PATH) +#PythonPath=/path/to/custom/python + +# Custom PlantUML path (optional. If not set, the default system PlantUML will be used from PATH) +#PlantUmlPath=/path/to/custom/plantuml + +# Custom Ratex path (optional. If not set, the default system Ratex will be used from Bookgen folder) +#RatexPath=/path/to/custom/ratex + +# Custom Mmdr path (optional. If not set, the default system Mmdr will be used from Bookgen folder) +#MmdrPath=/path/to/custom/mmdr diff --git a/Source/BookGen/GlobalOptionParsers/AttachDebuggerParser.cs b/Source/BookGen/GlobalOptionParsers/AttachDebuggerParser.cs index bafba4e6..447b97d0 100644 --- a/Source/BookGen/GlobalOptionParsers/AttachDebuggerParser.cs +++ b/Source/BookGen/GlobalOptionParsers/AttachDebuggerParser.cs @@ -8,7 +8,7 @@ using BookGen.Cli; -using Spectre.Console; +using Microsoft.Extensions.Logging; namespace BookGen.GlobalOptionParsers; @@ -17,15 +17,17 @@ internal sealed class AttachDebuggerParser : GlobalOptionParser { private const string DebuggerStartShort = "ad"; private const string DebuggerStartLong = "attach-debugger"; + private readonly ILogger _log; - public AttachDebuggerParser() + public AttachDebuggerParser(ILogger log) : base(DebuggerStartShort, DebuggerStartLong) { + _log = log; } - protected override void OnOptionWasPresent() + protected override void OnOptionWasPresent(string value) { - AnsiConsole.WriteLine("Attaching debugger..."); + _log.LogInformation("Attaching debugger..."); if (!Debugger.IsAttached) { Debugger.Launch(); diff --git a/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs new file mode 100644 index 00000000..cf445ba2 --- /dev/null +++ b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs @@ -0,0 +1,42 @@ +using BookGen.Cli; +using BookGen.Cli.Dotenv; + +using Microsoft.Extensions.Logging; + +namespace BookGen.GlobalOptionParsers; + +internal class DotEnvFileParser : GlobalOptionParser +{ + private readonly ILogger _log; + private readonly DotEnvSettings _dotEnvSettings; + + public DotEnvFileParser(ILogger log, DotEnvSettings dotEnvSettings) + : base("env", "env-file", true) + { + _log = log; + _dotEnvSettings = dotEnvSettings; + } + protected override void OnOptionWasPresent(string value) + { + _dotEnvSettings.Clear(); + var fileName = Path.GetFullPath(value); + if (!File.Exists(fileName)) + { + _log.LogWarning("The specified env-file '{fileName}' does not exist.", fileName); + Environment.Exit(ExitCodes.GeneralError); + return; + } + try + { + _log.LogInformation("Loading env-file '{fileName}'.", fileName); + using StreamReader reader = File.OpenText(fileName); + DotEnvSettings loaded = DotEnvParser.Parse(reader, StringComparer.Ordinal); + _dotEnvSettings.Merge(loaded); + } + catch (Exception ex) + { + _log.LogError(ex, "Failed to load env-file '{fileName}'.", fileName); + Environment.Exit(ExitCodes.GeneralError); + } + } +} diff --git a/Source/BookGen/GlobalOptionParsers/JsonLogParser.cs b/Source/BookGen/GlobalOptionParsers/JsonLogParser.cs index 67fe0dbd..5987e5fd 100644 --- a/Source/BookGen/GlobalOptionParsers/JsonLogParser.cs +++ b/Source/BookGen/GlobalOptionParsers/JsonLogParser.cs @@ -23,7 +23,7 @@ public JsonLogParser(ProgramInfo info) _info = info; } - protected override void OnOptionWasPresent() + protected override void OnOptionWasPresent(string value) { _info.JsonLogging = true; } diff --git a/Source/BookGen/GlobalOptionParsers/LogToFileParser.cs b/Source/BookGen/GlobalOptionParsers/LogToFileParser.cs index efd73153..a80ab6f1 100644 --- a/Source/BookGen/GlobalOptionParsers/LogToFileParser.cs +++ b/Source/BookGen/GlobalOptionParsers/LogToFileParser.cs @@ -23,7 +23,7 @@ public LogToFileParser(ProgramInfo info) _info = info; } - protected override void OnOptionWasPresent() + protected override void OnOptionWasPresent(string value) { _info.LogToFile = true; } diff --git a/Source/BookGen/GlobalOptionParsers/RuntimePrintingParser.cs b/Source/BookGen/GlobalOptionParsers/RuntimePrintingParser.cs index e1e2690b..9a94c071 100644 --- a/Source/BookGen/GlobalOptionParsers/RuntimePrintingParser.cs +++ b/Source/BookGen/GlobalOptionParsers/RuntimePrintingParser.cs @@ -24,7 +24,7 @@ public RuntimePrintingParser(ProgramInfo info) _info.PrintRuntime = true; } - protected override void OnOptionWasPresent() + protected override void OnOptionWasPresent(string value) { _info.PrintRuntime = false; } diff --git a/Source/BookGen/GlobalOptionParsers/WaitDebuggerParser.cs b/Source/BookGen/GlobalOptionParsers/WaitDebuggerParser.cs index 3eecc182..87cd1927 100644 --- a/Source/BookGen/GlobalOptionParsers/WaitDebuggerParser.cs +++ b/Source/BookGen/GlobalOptionParsers/WaitDebuggerParser.cs @@ -8,6 +8,8 @@ using BookGen.Cli; +using Microsoft.Extensions.Logging; + using Spectre.Console; namespace BookGen.GlobalOptionParsers; @@ -17,16 +19,18 @@ internal sealed class WaitDebuggerParser : GlobalOptionParser { private const string DebuggerShort = "wd"; private const string DebuggerLong = "wait-debugger"; + private readonly ILogger _log; - public WaitDebuggerParser() + public WaitDebuggerParser(ILogger log) : base(DebuggerShort, DebuggerLong) { + _log = log; } - protected override void OnOptionWasPresent() + protected override void OnOptionWasPresent(string value) { - AnsiConsole.WriteLine("Waiting for debugger to be attached..."); - AnsiConsole.WriteLine("ESC to cancel & contine execution..."); + _log.LogInformation("Waiting for debugger to be attached..."); + _log.LogInformation("ESC to cancel & continue execution..."); while (!Debugger.IsAttached) { if (Console.KeyAvailable) diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index e94ac766..e03b17fc 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -44,12 +44,31 @@ ILogger logger = factory.CreateLogger("Bookgen"); CommandRunnerProxy runnerProxy = new(); +DotEnvSettings settings = new(); + +string defaultEnvFile = Path.Combine(AppContext.BaseDirectory, "BookGen.env"); + +if (File.Exists(defaultEnvFile)) +{ + try + { + logger.LogInformation("Loading default env-file '{defaultEnvFile}'.", defaultEnvFile); + using StreamReader reader = File.OpenText(defaultEnvFile); + DotEnvSettings loaded = DotEnvParser.Parse(reader, StringComparer.Ordinal); + settings.Merge(loaded); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to load default env-file '{defaultEnvFile}'.", defaultEnvFile); + Environment.Exit(ExitCodes.GeneralError); + } +} var ioc = new ServiceCollection(); ioc.AddMemoryCache(); ioc.AddSingleton(logger); ioc.AddSingleton(info); -ioc.AddSingleton(); +ioc.AddSingleton(settings); ioc.AddSingleton(runnerProxy); ioc.AddSingleton(); ioc.AddSingleton(ZipAssetSoruce.DefaultAssets()); @@ -86,8 +105,9 @@ }; runner - .AddGlobalOptionParser() - .AddGlobalOptionParser() + .AddGlobalOptionParser(new DotEnvFileParser(logger, settings)) + .AddGlobalOptionParser(new AttachDebuggerParser(logger)) + .AddGlobalOptionParser(new WaitDebuggerParser(logger)) .AddGlobalOptionParser(new JsonLogParser(info)) .AddGlobalOptionParser(new LogToFileParser(info)) .AddGlobalOptionParser(new RuntimePrintingParser(info)); diff --git a/Test/Bookgen.Tests/UT_OpenCli.cs b/Test/Bookgen.Tests/UT_OpenCli.cs index 753900cc..1c0a8fbb 100644 --- a/Test/Bookgen.Tests/UT_OpenCli.cs +++ b/Test/Bookgen.Tests/UT_OpenCli.cs @@ -29,8 +29,8 @@ private static CommandRunner SetupCommandRunner() var info = new BookGen.ProgramInfo(); runner - .AddGlobalOptionParser() - .AddGlobalOptionParser() + .AddGlobalOptionParser(new AttachDebuggerParser(loggerMock.Object)) + .AddGlobalOptionParser(new WaitDebuggerParser(loggerMock.Object)) .AddGlobalOptionParser(new JsonLogParser(info)) .AddGlobalOptionParser(new LogToFileParser(info)) .AddGlobalOptionParser(new RuntimePrintingParser(info)); From 5cf1fdb30fa7d710e274f49202ccf983072954a7 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 7 Aug 2026 18:23:58 +0200 Subject: [PATCH 06/32] Scripts specification --- BookGen.slnx | 1 + Docs/bookgen.toc.json | 3 +- Docs/changelog.md | 3 + Docs/scripts.md | 86 ++++++++++++++++++++++++ Source/BookGen/Commands/ScriptCommand.cs | 20 ++++++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 Docs/scripts.md create mode 100644 Source/BookGen/Commands/ScriptCommand.cs diff --git a/BookGen.slnx b/BookGen.slnx index 60704f35..415dee62 100644 --- a/BookGen.slnx +++ b/BookGen.slnx @@ -4,6 +4,7 @@ + diff --git a/Docs/bookgen.toc.json b/Docs/bookgen.toc.json index 29967554..7a921d75 100644 --- a/Docs/bookgen.toc.json +++ b/Docs/bookgen.toc.json @@ -7,7 +7,8 @@ "Files": [ "manual.md", "changelog.md", - "plugin.md" + "plugin.md", + "scripts.md" ] } ] diff --git a/Docs/changelog.md b/Docs/changelog.md index f5fb43b0..ab76a566 100644 --- a/Docs/changelog.md +++ b/Docs/changelog.md @@ -5,6 +5,9 @@ tags: '' # 2026. 08. +* Breaking: Removed json args support for commands +* Breaking: Removed config command. Configuration is now loaded from the bookgen.env file or from a configuration specified by the -env option +* Breaking: Removed jsonargs command. * New: Md2html command output file now can be a folder. If a folder is specified, the output file name is generated from the input file name. * Fix: Shell atuocomplete now correctly handles command tree * Fix: Fixed md2html external template file validation diff --git a/Docs/scripts.md b/Docs/scripts.md new file mode 100644 index 00000000..5b2bd301 --- /dev/null +++ b/Docs/scripts.md @@ -0,0 +1,86 @@ +# Scripting + +Scripting in BookGen lets you automate common workflows for your book - building outputs, generating or transforming content. +You can run scripts from your shell (PowerShell, Bash, etc.) that invoke BookGen commands, or you can define and run scripts +directly inside BookGen. + +The advantage of scripting inside BookGen is performance: BookGen starts once and executes multiple script steps in the +same process, avoiding repeated startup overhead. Use shell scripts when you need OS integration or CI ties; +use BookGen scripts when you want faster, BookGen-focused automation. + +## Execution model + +BookGen scripts follow a bash `pipefail` style execution model. Commands run sequentially, and the script continues only +as long as each command succeeds. If any single command fails, execution stops immediately and the whole script is +considered failed - the remaining commands are not run. This fail-fast behavior ensures that a broken step never lets the +rest of the script proceed on invalid state, making script results reliable and easy to reason about. + +When a script fails, its exit code is the exit code of the last command that was executed - that is, the command that +failed and caused execution to stop. This lets you inspect the script's exit code from your shell or CI pipeline to +determine exactly which step went wrong and to react accordingly. + +## Syntax + +Beside the execution model described above, BookGen scripts follow the usual bash conventions. +Long arguments that contain spaces must be escaped by wrapping them in quotes. For example: + +``` +build --output "./my output folder" +``` + +Without the quotes the value would be split into multiple arguments and the command would not receive the intended input. + +Each line should contain only a single command together with its arguments. For readability, however, a command can be +split across multiple lines by ending a line with the `\` character. When a line ends with `\`, the following line is +treated as a continuation of the same command. For example: + +``` +build \ + --output "./my output folder" \ + --verbose +``` + +The example above is equivalent to writing the whole `build` command on one line. + +## Comments + +BookGen scripts support two comment styles, and only these two: + +- Shell-style comments that begin with a `#` mark. +- C++-style line comments that begin with `//`. + +Everything from the comment marker to the end of the line is ignored during execution. Both styles are line comments only - +there is no block or multi-line comment syntax. You can use either style (or mix them) to document your scripts. + +``` +# This is a comment +build --output ./out # trailing comment after a command + +// This is also a comment +clean // trailing comment using C++ style +``` + +### Adding extra logging + +In addition to regular comments, BookGen scripts let you inject your own messages into the log output while a script is +running. To emit a custom log message, start a line with the special `#log` or `//log` instruction, followed by the text +you want to display: + +``` +#log Starting the build step +build --output ./out + +//log Build finished, cleaning up +clean +``` + +These log instructions must appear at the very start of the line. If `#log` or `//log` is not at the beginning of the line +(for example when used as a trailing comment after a command), it is treated as an ordinary comment and its text is ignored +during execution instead of being written to the log: + +``` +build --output ./out #log this is NOT logged, it's just a regular trailing comment +``` + +Use these log instructions to make your scripts easier to follow, marking progress and highlighting important steps in the +runtime output. diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs new file mode 100644 index 00000000..fccbf8f8 --- /dev/null +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using BookGen.Cli; + +namespace BookGen.Commands; + +internal sealed class ScriptCommand : AsyncCommand +{ + public sealed class Arguments : ArgumentsBase + { + + } + + public override Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { + throw new NotImplementedException(); + } +} From 2aefa4c90be197f8d8e4b09ee3a1268abf32ddc3 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 15:06:37 +0200 Subject: [PATCH 07/32] Script command implementation --- Source/BookGen/Commands/ScriptCommand.cs | 237 ++++++++++++++++++++++- 1 file changed, 233 insertions(+), 4 deletions(-) diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index fccbf8f8..ff3d347b 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -1,20 +1,249 @@ -using System; -using System.Collections.Generic; +using System.ComponentModel; using System.Text; using BookGen.Cli; +using BookGen.Cli.Annotations; + +using Microsoft.Extensions.Logging; namespace BookGen.Commands; +[CommandName("script")] +[Description("Executes a script file.")] +[ExitCode(ExitCodes.Success, "The script completed successfully.")] +[ExitCode(ExitCodes.ArgumentsError, "The script file doesn't exist or a command couldn't be resolved.")] internal sealed class ScriptCommand : AsyncCommand { public sealed class Arguments : ArgumentsBase { + [Argument(0)] + [Description("The path to the script file to execute.")] + public string ScriptPath { get; set; } = string.Empty; + + public override ValidationResult Validate(IValidationContext context) + { + if (string.IsNullOrWhiteSpace(ScriptPath)) + return ValidationResult.Error("Script file path must be specified."); + + if (!context.FileSystem.FileExists(ScriptPath)) + return ValidationResult.Error($"Script file doesn't exist: {ScriptPath}"); + + return ValidationResult.Ok(); + } + } + + private readonly ICommandRunnerProxy _commandRunnerProxy; + private readonly ILogger _logger; + + public ScriptCommand(ICommandRunnerProxy commandRunnerProxy, ILogger logger) + { + _commandRunnerProxy = commandRunnerProxy; + _logger = logger; + } + + public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { + if (!File.Exists(arguments.ScriptPath)) + { + _logger.LogError("Script file doesn't exist: {path}", arguments.ScriptPath); + return ExitCodes.ArgumentsError; + } + + string[] rawLines = await File.ReadAllLinesAsync(arguments.ScriptPath, token); + + IReadOnlyList commandNames = _commandRunnerProxy.CommandNames + .OrderByDescending(x => x.Length) + .ToArray(); + + int exitCode = ExitCodes.Success; + + foreach (string logicalLine in JoinContinuations(rawLines)) + { + token.ThrowIfCancellationRequested(); + + // Handle log instructions: they must appear at the very start of the line. + if (TryGetLogMessage(logicalLine, out string? logMessage)) + { + _logger.LogInformation("{message}", logMessage); + continue; + } + + IReadOnlyList tokens = Tokenize(logicalLine); + + if (tokens.Count == 0) + continue; + + if (!TryResolveCommand(tokens, commandNames, out string? commandName, out IReadOnlyList commandArgs)) + { + _logger.LogError("Unknown command: {command}", string.Join(' ', tokens)); + return ExitCodes.ArgumentsError; + } + + _logger.LogInformation("Running: {command}", logicalLine.Trim()); + + exitCode = await _commandRunnerProxy.RunCommand(commandName, commandArgs); + + // pipefail style: stop on the first failing command and return its exit code. + if (exitCode != ExitCodes.Success) + { + _logger.LogError("Command failed with exit code {code}, stopping script execution.", exitCode); + return exitCode; + } + } + + return exitCode; + } + + private static IEnumerable JoinContinuations(IEnumerable lines) + { + StringBuilder? builder = null; + + foreach (string line in lines) + { + string current = line; + + if (current.EndsWith('\\')) + { + builder ??= new StringBuilder(); + builder.Append(current.AsSpan(0, current.Length - 1)); + builder.Append(' '); + continue; + } + + if (builder != null) + { + builder.Append(current); + yield return builder.ToString(); + builder = null; + } + else + { + yield return current; + } + } + + if (builder != null) + yield return builder.ToString(); + } + + private static bool TryGetLogMessage(string line, out string? message) + { + message = null; + string trimmed = line.TrimStart(); + + foreach (string marker in (ReadOnlySpan)["#log", "//log"]) + { + if (trimmed.StartsWith(marker, StringComparison.Ordinal)) + { + string rest = trimmed[marker.Length..]; + + // The marker must be followed by whitespace or the end of line to be treated as a log instruction. + if (rest.Length == 0 || char.IsWhiteSpace(rest[0])) + { + message = rest.Trim(); + return true; + } + } + } + + return false; + } + + private static List Tokenize(string line) + { + var tokens = new List(); + var current = new StringBuilder(); + bool inQuotes = false; + bool hasToken = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + + if (inQuotes) + { + if (c == '"') + inQuotes = false; + else + current.Append(c); + + continue; + } + switch (c) + { + case '"': + inQuotes = true; + hasToken = true; + break; + case '#': + // Rest of the line is a comment. + FlushToken(tokens, current, ref hasToken); + return tokens; + case '/' when i + 1 < line.Length && line[i + 1] == '/': + // Rest of the line is a comment. + FlushToken(tokens, current, ref hasToken); + return tokens; + default: + if (char.IsWhiteSpace(c)) + { + FlushToken(tokens, current, ref hasToken); + } + else + { + current.Append(c); + hasToken = true; + } + break; + } + } + + FlushToken(tokens, current, ref hasToken); + return tokens; + + static void FlushToken(List tokens, StringBuilder current, ref bool hasToken) + { + if (hasToken) + { + tokens.Add(current.ToString()); + current.Clear(); + hasToken = false; + } + } } - public override Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + private static bool TryResolveCommand(IReadOnlyList tokens, + IReadOnlyList commandNames, + out string commandName, + out IReadOnlyList commandArgs) { - throw new NotImplementedException(); + foreach (string candidate in commandNames) + { + string[] parts = candidate.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > tokens.Count) + continue; + + bool matches = true; + for (int i = 0; i < parts.Length; i++) + { + if (!string.Equals(parts[i], tokens[i], StringComparison.OrdinalIgnoreCase)) + { + matches = false; + break; + } + } + + if (matches) + { + commandName = candidate; + commandArgs = tokens.Skip(parts.Length).ToArray(); + return true; + } + } + + commandName = string.Empty; + commandArgs = []; + return false; } } From 9f6865f2ac98d840359aca2235a3be1a7a1558ca Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 15:19:17 +0200 Subject: [PATCH 08/32] License headers and solution update --- Directory.Packages.props | 10 +- Source/BookGen.Cli/Dotenv/DotEnvParser.cs | 7 +- Source/BookGen.Cli/Dotenv/DotEnvSettings.cs | 7 +- .../Dotenv/EnvironmentException.cs | 7 +- Source/BookGen.Lib/packages.lock.json | 114 ++++++++-------- Source/BookGen/Commands/ScriptCommand.cs | 7 +- .../GlobalOptionParsers/DotEnvFileParser.cs | 7 +- Source/BookGen/packages.lock.json | 124 +++++++++--------- Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs | 7 +- 9 files changed, 160 insertions(+), 130 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f85c5d70..5927044e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - + @@ -23,12 +23,12 @@ - - - + + + - + diff --git a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs index 7b2ebfd2..49901ac2 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs @@ -1,4 +1,9 @@ -using System.Text; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text; namespace BookGen.Cli.Dotenv; diff --git a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs index d3183e12..5bc51a87 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvSettings.cs @@ -1,4 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; namespace BookGen.Cli.Dotenv; diff --git a/Source/BookGen.Cli/Dotenv/EnvironmentException.cs b/Source/BookGen.Cli/Dotenv/EnvironmentException.cs index 61b6968c..cdf009d5 100644 --- a/Source/BookGen.Cli/Dotenv/EnvironmentException.cs +++ b/Source/BookGen.Cli/Dotenv/EnvironmentException.cs @@ -1,4 +1,9 @@ -namespace BookGen.Cli.Dotenv; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Cli.Dotenv; public sealed class EnvironmentException : Exception { diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index b6b6ae91..cfdf6eb5 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "AngleSharp": { "type": "Direct", - "requested": "[1.5.2, )", - "resolved": "1.5.2", - "contentHash": "LVZ7rrr6GHbhERhTgVDarVygl1e6KwE96pronA90LBOMacDhxR4lB+xYLjn/vlnUu+xbOB2RM9fuCPEx5bdyWA==" + "requested": "[1.7.1, )", + "resolved": "1.7.1", + "contentHash": "QBblGpRB3SYD66Mls7fHyl1mWkP93n7ErWDWHc1vd9sPHhdlTkmUoY3CjgfjcWNxGF9m7xjNyztRfErXqoBKOA==" }, "Markdig": { "type": "Direct", @@ -56,41 +56,41 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "5v3T8X1N62Dp+AkPO70GNBNS/NRBPGMOTiN+Prg33sZAcm/Ug3YOAH+3RTj/jxJV8NGTJs2idGpC2Qdae2mGLQ==", + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "FNlVsGZ9to3o9pObVqhIZanHjUGOoPb/Zxk5svCSLMJnKwxPKV7sIJrjs9BpB/jF3j9OdsaMOb4cfwSJbjueSw==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.150.1", - "SkiaSharp.NativeAssets.macOS": "4.150.1" + "SkiaSharp.NativeAssets.Win32": "4.151.1", + "SkiaSharp.NativeAssets.macOS": "4.151.1" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "7IZtljoTBp0EH7/6K3p3yey44db3W7hkV9UPdCOaQQ2TxFkmFOpXwOPiQ865DrG+szgp0aKpQs8vYrzRdF83Ng==" + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "4FM5fSV6W3nMOeawrxkgu3sf3uWUfQEizZ+ww6kcByf9XERiSbZftf5dyJxOUFDLcrtj9aslN2Jb7uZsO/lfWw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Direct", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "qrLSL8OonbkMJdSH8heK0Jl39Y2xxZGdd7Ru4cyBXk2ITdx+Fu1sCFLmvCKmnMaYs//mlN/YSpSpOurEXErcQw==" + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "G+nln30ZX2ZuhH+ungf6x/QMXCje38KTS0VTfQeMyeY8uyMBmsZwFz2D30ZxUjZqsVbEIcaC2bsYF65876s4Yg==" }, "Svg.Skia": { "type": "Direct", - "requested": "[5.1.1, )", - "resolved": "5.1.1", - "contentHash": "zBcxwGQlEOE1HKFOmcGl/Sl/0aeRajRv/yEE5lRFQqufhIERuuzCHmq6xWK0fyeA+B1uufOR/h5+MHGMatJz4w==", + "requested": "[5.2.0, )", + "resolved": "5.2.0", + "contentHash": "AABURGBPTwWNFPnETLgjteTqcGfoNMJu+z1LNcNc8R280oVRMHgCf8dwGkIDj9rDD6A4Xt5w/LLmcZh0Iv7tTw==", "dependencies": { - "HarfBuzzSharp": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.Win32": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.macOS": "8.3.1.3", - "SkiaSharp": "3.119.2", - "Svg.Animation": "5.1.1", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1", - "Svg.SceneGraph": "5.1.1" + "HarfBuzzSharp": "14.2.0", + "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", + "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", + "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", + "SkiaSharp": "4.148.0", + "Svg.Animation": "5.2.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0", + "Svg.SceneGraph": "5.2.0" } }, "System.ServiceModel.Syndication": { @@ -112,27 +112,27 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "NGZ2+ZVNPM+NdHB/asW0/ykWngyHWwcqjrbN2nDeH1B/aptPGlCUl8wkQ2cSJxw5fdWgdmIPmNuTPWpLwNVXWg==", + "resolved": "14.2.0", + "contentHash": "B9zczdZELTKyJoNZXJLTgq+Ho2Z6H8oFcFbelvmHixkA8/2sH9acE589KvOa951g1aN262pOU5ThhZxw33bGSg==", "dependencies": { - "HarfBuzzSharp.NativeAssets.Win32": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.macOS": "8.3.1.3" + "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", + "HarfBuzzSharp.NativeAssets.macOS": "14.2.0" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ==" + "resolved": "14.2.0", + "contentHash": "KgdkGoqCoRPAdekskR9asBbY5tzfqRcYQ0/+hz8rvPMAmbQqX1g57xr6rbGUyIVY5j5ioExZWUZ10QocUVpgwQ==" }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "KPTq0xnslkI6nAo0jh3ptcQPJvZZr7MWYXa2jUe4SnHc9q+JlHElmNXp0sfFoiTgoCX7WOYpYsurypuH9Gehxw==" + "resolved": "14.2.0", + "contentHash": "v2AfGa0Q0aNRz7glbRTvkrR8w2pipUpyRHDNjOCPXnnHE3pjY0ycTY0L7dL/AByd53aQ0AGPwIEP1H9hZPPywg==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "bx8CE8Js+XGX8PUxAHCBDEORt5aaBYtMN4Hr9QFs57Xithh6yjUyYqksizH6eRDhJkwsGI+SXWmPmMm8lZC9Pw==" + "resolved": "14.2.0", + "contentHash": "NCqaG3bJDl66JdmHeyOpRUf4eiGVwu8OSxGv6ukofBiukVyUpWj01btr/telMQLQ+XuQ83iDpi6NRDFoRIcojA==" }, "Microsoft.ClearScript.Core": { "type": "Transitive", @@ -182,13 +182,13 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "yYLSkTgGliFONplDPaSOpmTbV0VhbjHySYAqA3NMJilqPA/OBevX3/4sJ0k4FRhbuI8akyw6O2RMO/xUKwFBxQ==" + "resolved": "5.2.0", + "contentHash": "CNCbWDhCjuLTcbqsV2207BzCEr9+fWZ7veeS6/qZZp0h/GUzA0+aMGOEd2g+F1qllAIitvmdrNqiTOijQR/2+g==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.150.1", - "contentHash": "r755HVwaHZhyf1clWjrM2/RoOZYCzkQEmE9pu/mVsebPejWu52niNPUwtfyf112qoF0PIk6OndqVOUoITj6TwQ==" + "resolved": "4.151.1", + "contentHash": "nLXwdhi0oB7pqqNLbSOLX+FIZQOMAU/yFPspzcDTJjTmPbMWyp0JWyeVj8sFJ/cweurEfT0J/lvfgdXX2EFHXA==" }, "Spectre.Console.Ansi": { "type": "Transitive", @@ -197,40 +197,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "HUaU5Pa1fvE2CcYMHhA6nQH+4dcJsDEJ5feYfVoO3ha0u506ugOR0gR1fKHANIwEapYs20nLJQHLb39DK/pJlg==", + "resolved": "5.2.0", + "contentHash": "eUGhVM4dl+FQdsDM7N3zzKM1gYrNRfaCytNdD5VPGxooGc05GZlzoNVI8dwhh5jcYuCO2hxRPzsspyxfZdAgJA==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "SkiaSharp": "3.119.2", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "SkiaSharp": "4.148.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "i53zvhuauElnLtGzezyfzpAs/zJ3UaexqT+JuAQTL6yC5ym1nQXTnjntDhY4qrouJ5elGjEwjag1nzfdPPPGSQ==", + "resolved": "5.2.0", + "contentHash": "Up4hK0GphasgVzPOB5JT7Md3FP1dHaQK6AkBD7XH0uBWvu8d/DVVXp5qjWvpmBayIYWF4FAHnb2b/OPl+U2UkA==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "j9j4kqKL5Girwr/2qwzzDRJ7CDXvGipchbNQfxmmUAk70JG3/6mXaF/QJLiIat6p6NU38MBYaOSilVuqaZuC5w==", + "resolved": "5.2.0", + "contentHash": "G3ZXXnlo6eD+oU/e+a33oVkf27a3oYu+djAwIEt4BU8w4S9t5UWsjcrR3bsq6SdO9r154+KiflpW/eznmGCtZQ==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "Svg.Custom": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "Svg.Custom": "5.2.0" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "p0ti8uOikN7aEg/KDq042xBbWijbOl2sITWMpO9vJ2KEaBeB8jouTDQTK7QssMs8DynR1+FxOsn7WEftXF0jow==", + "resolved": "5.2.0", + "contentHash": "Yr4T6Nzwi+Hd2sKOKik1qbCteLNnuapzvKxMblIuJ2SHwEbMb+2FjFwA6FenD7CRWhlCEiIwTvAe3/IJswTYoQ==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0" } }, "bookgen.shell.shared": { diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index ff3d347b..d3e4c019 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -1,4 +1,9 @@ -using System.ComponentModel; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.ComponentModel; using System.Text; using BookGen.Cli; diff --git a/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs index cf445ba2..3ca924f5 100644 --- a/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs +++ b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs @@ -1,4 +1,9 @@ -using BookGen.Cli; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using BookGen.Cli; using BookGen.Cli.Dotenv; using Microsoft.Extensions.Logging; diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index f454e4eb..1f0b61a9 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -95,27 +95,27 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "NGZ2+ZVNPM+NdHB/asW0/ykWngyHWwcqjrbN2nDeH1B/aptPGlCUl8wkQ2cSJxw5fdWgdmIPmNuTPWpLwNVXWg==", + "resolved": "14.2.0", + "contentHash": "B9zczdZELTKyJoNZXJLTgq+Ho2Z6H8oFcFbelvmHixkA8/2sH9acE589KvOa951g1aN262pOU5ThhZxw33bGSg==", "dependencies": { - "HarfBuzzSharp.NativeAssets.Win32": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.macOS": "8.3.1.3" + "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", + "HarfBuzzSharp.NativeAssets.macOS": "14.2.0" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ==" + "resolved": "14.2.0", + "contentHash": "KgdkGoqCoRPAdekskR9asBbY5tzfqRcYQ0/+hz8rvPMAmbQqX1g57xr6rbGUyIVY5j5ioExZWUZ10QocUVpgwQ==" }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "KPTq0xnslkI6nAo0jh3ptcQPJvZZr7MWYXa2jUe4SnHc9q+JlHElmNXp0sfFoiTgoCX7WOYpYsurypuH9Gehxw==" + "resolved": "14.2.0", + "contentHash": "v2AfGa0Q0aNRz7glbRTvkrR8w2pipUpyRHDNjOCPXnnHE3pjY0ycTY0L7dL/AByd53aQ0AGPwIEP1H9hZPPywg==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "8.3.1.3", - "contentHash": "bx8CE8Js+XGX8PUxAHCBDEORt5aaBYtMN4Hr9QFs57Xithh6yjUyYqksizH6eRDhJkwsGI+SXWmPmMm8lZC9Pw==" + "resolved": "14.2.0", + "contentHash": "NCqaG3bJDl66JdmHeyOpRUf4eiGVwu8OSxGv6ukofBiukVyUpWj01btr/telMQLQ+XuQ83iDpi6NRDFoRIcojA==" }, "Microsoft.ClearScript.Core": { "type": "Transitive", @@ -165,13 +165,13 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "yYLSkTgGliFONplDPaSOpmTbV0VhbjHySYAqA3NMJilqPA/OBevX3/4sJ0k4FRhbuI8akyw6O2RMO/xUKwFBxQ==" + "resolved": "5.2.0", + "contentHash": "CNCbWDhCjuLTcbqsV2207BzCEr9+fWZ7veeS6/qZZp0h/GUzA0+aMGOEd2g+F1qllAIitvmdrNqiTOijQR/2+g==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.150.1", - "contentHash": "r755HVwaHZhyf1clWjrM2/RoOZYCzkQEmE9pu/mVsebPejWu52niNPUwtfyf112qoF0PIk6OndqVOUoITj6TwQ==" + "resolved": "4.151.1", + "contentHash": "nLXwdhi0oB7pqqNLbSOLX+FIZQOMAU/yFPspzcDTJjTmPbMWyp0JWyeVj8sFJ/cweurEfT0J/lvfgdXX2EFHXA==" }, "Spectre.Console.Ansi": { "type": "Transitive", @@ -180,40 +180,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "HUaU5Pa1fvE2CcYMHhA6nQH+4dcJsDEJ5feYfVoO3ha0u506ugOR0gR1fKHANIwEapYs20nLJQHLb39DK/pJlg==", + "resolved": "5.2.0", + "contentHash": "eUGhVM4dl+FQdsDM7N3zzKM1gYrNRfaCytNdD5VPGxooGc05GZlzoNVI8dwhh5jcYuCO2hxRPzsspyxfZdAgJA==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "SkiaSharp": "3.119.2", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "SkiaSharp": "4.148.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "i53zvhuauElnLtGzezyfzpAs/zJ3UaexqT+JuAQTL6yC5ym1nQXTnjntDhY4qrouJ5elGjEwjag1nzfdPPPGSQ==", + "resolved": "5.2.0", + "contentHash": "Up4hK0GphasgVzPOB5JT7Md3FP1dHaQK6AkBD7XH0uBWvu8d/DVVXp5qjWvpmBayIYWF4FAHnb2b/OPl+U2UkA==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "j9j4kqKL5Girwr/2qwzzDRJ7CDXvGipchbNQfxmmUAk70JG3/6mXaF/QJLiIat6p6NU38MBYaOSilVuqaZuC5w==", + "resolved": "5.2.0", + "contentHash": "G3ZXXnlo6eD+oU/e+a33oVkf27a3oYu+djAwIEt4BU8w4S9t5UWsjcrR3bsq6SdO9r154+KiflpW/eznmGCtZQ==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "Svg.Custom": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "Svg.Custom": "5.2.0" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "p0ti8uOikN7aEg/KDq042xBbWijbOl2sITWMpO9vJ2KEaBeB8jouTDQTK7QssMs8DynR1+FxOsn7WEftXF0jow==", + "resolved": "5.2.0", + "contentHash": "Yr4T6Nzwi+Hd2sKOKik1qbCteLNnuapzvKxMblIuJ2SHwEbMb+2FjFwA6FenD7CRWhlCEiIwTvAe3/IJswTYoQ==", "dependencies": { - "ShimSkiaSharp": "5.1.1", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1" + "ShimSkiaSharp": "5.2.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0" } }, "System.IO.Packaging": { @@ -236,7 +236,7 @@ "bookgen.lib": { "type": "Project", "dependencies": { - "AngleSharp": "[1.5.2, )", + "AngleSharp": "[1.7.1, )", "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", "Markdig": "[1.3.2, )", @@ -244,10 +244,10 @@ "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1, )", "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1, )", "PreMailer.Net": "[2.7.3, )", - "SkiaSharp": "[4.150.1, )", - "SkiaSharp.NativeAssets.Linux": "[4.150.1, )", - "SkiaSharp.NativeAssets.Win32": "[4.150.1, )", - "Svg.Skia": "[5.1.1, )", + "SkiaSharp": "[4.151.1, )", + "SkiaSharp.NativeAssets.Linux": "[4.151.1, )", + "SkiaSharp.NativeAssets.Win32": "[4.151.1, )", + "Svg.Skia": "[5.2.0, )", "System.ServiceModel.Syndication": "[11.0.0-preview.6.26359.118, )", "YamlDotNet": "[18.1.0, )" } @@ -264,9 +264,9 @@ }, "AngleSharp": { "type": "CentralTransitive", - "requested": "[1.5.2, )", - "resolved": "1.5.2", - "contentHash": "LVZ7rrr6GHbhERhTgVDarVygl1e6KwE96pronA90LBOMacDhxR4lB+xYLjn/vlnUu+xbOB2RM9fuCPEx5bdyWA==" + "requested": "[1.7.1, )", + "resolved": "1.7.1", + "contentHash": "QBblGpRB3SYD66Mls7fHyl1mWkP93n7ErWDWHc1vd9sPHhdlTkmUoY3CjgfjcWNxGF9m7xjNyztRfErXqoBKOA==" }, "Microsoft.ClearScript": { "type": "CentralTransitive", @@ -304,41 +304,41 @@ }, "SkiaSharp": { "type": "CentralTransitive", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "5v3T8X1N62Dp+AkPO70GNBNS/NRBPGMOTiN+Prg33sZAcm/Ug3YOAH+3RTj/jxJV8NGTJs2idGpC2Qdae2mGLQ==", + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "FNlVsGZ9to3o9pObVqhIZanHjUGOoPb/Zxk5svCSLMJnKwxPKV7sIJrjs9BpB/jF3j9OdsaMOb4cfwSJbjueSw==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.150.1", - "SkiaSharp.NativeAssets.macOS": "4.150.1" + "SkiaSharp.NativeAssets.Win32": "4.151.1", + "SkiaSharp.NativeAssets.macOS": "4.151.1" } }, "SkiaSharp.NativeAssets.Linux": { "type": "CentralTransitive", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "7IZtljoTBp0EH7/6K3p3yey44db3W7hkV9UPdCOaQQ2TxFkmFOpXwOPiQ865DrG+szgp0aKpQs8vYrzRdF83Ng==" + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "4FM5fSV6W3nMOeawrxkgu3sf3uWUfQEizZ+ww6kcByf9XERiSbZftf5dyJxOUFDLcrtj9aslN2Jb7uZsO/lfWw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "CentralTransitive", - "requested": "[4.150.1, )", - "resolved": "4.150.1", - "contentHash": "qrLSL8OonbkMJdSH8heK0Jl39Y2xxZGdd7Ru4cyBXk2ITdx+Fu1sCFLmvCKmnMaYs//mlN/YSpSpOurEXErcQw==" + "requested": "[4.151.1, )", + "resolved": "4.151.1", + "contentHash": "G+nln30ZX2ZuhH+ungf6x/QMXCje38KTS0VTfQeMyeY8uyMBmsZwFz2D30ZxUjZqsVbEIcaC2bsYF65876s4Yg==" }, "Svg.Skia": { "type": "CentralTransitive", - "requested": "[5.1.1, )", - "resolved": "5.1.1", - "contentHash": "zBcxwGQlEOE1HKFOmcGl/Sl/0aeRajRv/yEE5lRFQqufhIERuuzCHmq6xWK0fyeA+B1uufOR/h5+MHGMatJz4w==", + "requested": "[5.2.0, )", + "resolved": "5.2.0", + "contentHash": "AABURGBPTwWNFPnETLgjteTqcGfoNMJu+z1LNcNc8R280oVRMHgCf8dwGkIDj9rDD6A4Xt5w/LLmcZh0Iv7tTw==", "dependencies": { - "HarfBuzzSharp": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.Win32": "8.3.1.3", - "HarfBuzzSharp.NativeAssets.macOS": "8.3.1.3", - "SkiaSharp": "3.119.2", - "Svg.Animation": "5.1.1", - "Svg.Custom": "5.1.1", - "Svg.Model": "5.1.1", - "Svg.SceneGraph": "5.1.1" + "HarfBuzzSharp": "14.2.0", + "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", + "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", + "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", + "SkiaSharp": "4.148.0", + "Svg.Animation": "5.2.0", + "Svg.Custom": "5.2.0", + "Svg.Model": "5.2.0", + "Svg.SceneGraph": "5.2.0" } }, "System.ServiceModel.Syndication": { diff --git a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs index 104dec7f..e53072be 100644 --- a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs +++ b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs @@ -1,4 +1,9 @@ -using BookGen.Cli.Dotenv; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using BookGen.Cli.Dotenv; namespace Bookgen.Tests.Cli; From 3ff1cdca0d6d311413b27277429223700755f737 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 15:48:36 +0200 Subject: [PATCH 09/32] Command cache improvements for scripting --- Source/BookGen.Cli/CommandRunner.cs | 14 ++++++++++- .../Markdown/RenderInterop/RenderInterop.cs | 23 ++++++++++--------- Source/BookGen.Shellprog/Program.cs | 2 +- .../Commands/Convert/Diagram2SvgCommand.cs | 15 ++++++++---- .../Commands/Convert/Math2SvgCommand.cs | 14 +++++++---- .../Commands/Convert/Md2HtmlCommand.cs | 13 +++++++++-- .../BookGen/Commands/Convert/QrCodeCommand.cs | 13 +++++++---- Source/BookGen/Program.cs | 2 +- Test/Bookgen.Tests/Cli/UT_CommandRunner.cs | 6 +++++ 9 files changed, 73 insertions(+), 29 deletions(-) diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index db1b9913..ae260312 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -17,7 +17,7 @@ namespace BookGen.Cli; -public sealed class CommandRunner +public sealed class CommandRunner : IDisposable { private readonly CommandTree _commands; private readonly IServiceProvider _serviceProvider; @@ -116,6 +116,18 @@ public CommandRunner(IServiceProvider serviceProvider, Helpers.ConfigureUtfSupport(_settings.EnableUtf8Output); } + public void Dispose() + { + foreach (KeyValuePair cached in _cachedCommands) + { + if (cached.Value is IDisposable disposable) + { + disposable.Dispose(); + } + } + _cachedCommands.Clear(); + } + public Action ExceptionHandlerDelegate { get; set; } public CommandRunner AddCommand() where TCommand : ICommand diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs index ac11b7d7..925df4dd 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs @@ -24,19 +24,20 @@ public sealed class RenderInterop : IRenderInterop private readonly HashSet _loadedScripts; private bool _disposed; - private readonly ImageConfig _imageConfig; - public RenderInterop(IAssetSource assetSource, IProgramPathResolver programPathResolver, ImageConfig imageConfig) { _assetSource = assetSource; _programPathResolver = programPathResolver; - _imageConfig = imageConfig; + ImageConfig = imageConfig; _javascriptEngine = new JavascriptEngine(assetSource); _loadedScripts = new HashSet(); } + public ImageConfig ImageConfig { get; set; } + + public void Dispose() { _javascriptEngine.Dispose(); @@ -207,7 +208,7 @@ public ImageResult RenderNomnoml(string nomnomlCode) _javascriptEngine.Script.nomnomlCode = nomnomlCode; string svg = _javascriptEngine.ExecuteAndGetResult("nomnoml.renderSvg(nomnomlCode)"); - return EncodeSvg(svg, _imageConfig); + return EncodeSvg(svg, ImageConfig); } public ImageResult RenderQrCode(string url) @@ -217,7 +218,7 @@ public ImageResult RenderQrCode(string url) string cmd = $"new QRCode({{content: \"{url}\", padding: 2, color: \"#000000\"}}).svg();"; string svg = _javascriptEngine.ExecuteAndGetResult(cmd); - return EncodeSvg(svg, _imageConfig); + return EncodeSvg(svg, ImageConfig); } public ImageResult RenderLatex(string latex, double scale = 1.0) @@ -226,11 +227,11 @@ public ImageResult RenderLatex(string latex, double scale = 1.0) if (!_programPathResolver.TryResolveRatex(out string? ratexPath)) { - return EncodeSvg(ErrorSvg("Ratex binary not found"), _imageConfig); + return EncodeSvg(ErrorSvg("Ratex binary not found"), ImageConfig); } string svg = RunBinaryAndCaptureStdOut(ratexPath, $"--stdout --dpr {scale.ToString(CultureInfo.InvariantCulture)}", latex); - return EncodeSvg(svg, _imageConfig); + return EncodeSvg(svg, ImageConfig); } @@ -240,11 +241,11 @@ public ImageResult RenderMermaid(string mermaid) if (!_programPathResolver.TryResolveMmdr(out string? mmmdrPath)) { - return EncodeSvg(ErrorSvg("Mmmdr binary not found"), _imageConfig); + return EncodeSvg(ErrorSvg("Mmmdr binary not found"), ImageConfig); } string svg = RunBinaryAndCaptureStdOut(mmmdrPath, "-e svg --nodeSpacing 60 --rankSpacing 80 -i -", mermaid); - return EncodeSvg(svg, _imageConfig); + return EncodeSvg(svg, ImageConfig); } public ImageResult RenderPlantUml(string plantUml) @@ -253,10 +254,10 @@ public ImageResult RenderPlantUml(string plantUml) if (!_programPathResolver.TryResolvePlantUml(out string? plantUmlPath)) { - return EncodeSvg(ErrorSvg("PlantUML binary not found"), _imageConfig); + return EncodeSvg(ErrorSvg("PlantUML binary not found"), ImageConfig); } string svg = RunBinaryAndCaptureStdOut(plantUmlPath, "--svg -pipe", plantUml); - return EncodeSvg(svg, _imageConfig); + return EncodeSvg(svg, ImageConfig); } } diff --git a/Source/BookGen.Shellprog/Program.cs b/Source/BookGen.Shellprog/Program.cs index 7da6d496..f6e6caff 100644 --- a/Source/BookGen.Shellprog/Program.cs +++ b/Source/BookGen.Shellprog/Program.cs @@ -30,7 +30,7 @@ using ServiceProvider provider = ioc.BuildServiceProvider(); -CommandRunner runner = new(provider, new CommandHelpProvider(), logger, new CommandRunnerSettings +using CommandRunner runner = new(provider, new CommandHelpProvider(), logger, new CommandRunnerSettings { UnknownCommandCodeAndMessage = (-1, "Unknown command"), BadParametersExitCode = 2, diff --git a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs index d446f9f4..5565a572 100644 --- a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs +++ b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs @@ -19,7 +19,7 @@ namespace BookGen.Commands.Convert; [CommandName("convert diagram2svg")] [Description("Renders a diagram definition file to an SVG file.")] [ExitCode(ExitCodes.Success, "The command completed successfully.")] -internal sealed class Diagram2SvgCommand : AsyncCommand +internal sealed class Diagram2SvgCommand : AsyncCommand, IDisposable { internal sealed class Arguments : ArgumentsBase { @@ -71,6 +71,7 @@ public override void ModifyAfterValidation() private readonly IWritableFileSystem _fileSystem; private readonly IProgramPathResolver _programPathResolver; private readonly IAssetSource _assetSource; + private readonly IRenderInterop _renderInterop; public Diagram2SvgCommand(ILogger log, IWritableFileSystem fileSystem, IProgramPathResolver programPathResolver, IAssetSource assetSource) { @@ -78,18 +79,22 @@ public Diagram2SvgCommand(ILogger log, IWritableFileSystem fileSystem, IProgramP _fileSystem = fileSystem; _programPathResolver = programPathResolver; _assetSource = assetSource; + _renderInterop = IRenderInterop.CreateForSvg(_assetSource, _programPathResolver); } - public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + public void Dispose() { - using var render = IRenderInterop.CreateForSvg(_assetSource, _programPathResolver); + _renderInterop.Dispose(); + } + public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { string inputContent = await _fileSystem.ReadAllTextAsync(arguments.InputFile); ImageResult result = arguments.Type switch { - Arguments.DiagramType.Mermaid => render.RenderMermaid(inputContent), - Arguments.DiagramType.Nomnoml => render.RenderNomnoml(inputContent), + Arguments.DiagramType.Mermaid => _renderInterop.RenderMermaid(inputContent), + Arguments.DiagramType.Nomnoml => _renderInterop.RenderNomnoml(inputContent), _ => throw new InvalidOperationException($"Unsupported diagram type: {arguments.Type}"), }; diff --git a/Source/BookGen/Commands/Convert/Math2SvgCommand.cs b/Source/BookGen/Commands/Convert/Math2SvgCommand.cs index b59e6c8b..c4caf464 100644 --- a/Source/BookGen/Commands/Convert/Math2SvgCommand.cs +++ b/Source/BookGen/Commands/Convert/Math2SvgCommand.cs @@ -19,7 +19,7 @@ namespace BookGen.Commands.Convert; [CommandName("convert math2svg")] [Description("Renders a single markdown file containing Tex formulas to svg files.")] [ExitCode(ExitCodes.Success, "The command completed successfully.")] -internal sealed class Math2SvgCommand : AsyncCommand +internal sealed class Math2SvgCommand : AsyncCommand, IDisposable { private readonly ILogger _log; private readonly IWritableFileSystem _fileSystem; @@ -62,19 +62,25 @@ public override void ModifyAfterValidation() } } + private readonly IRenderInterop _renderInterop; + public Math2SvgCommand(ILogger log, IWritableFileSystem fileSystem, IProgramPathResolver programPathResolver, IAssetSource assetSource) { _log = log; _fileSystem = fileSystem; _programPathResolver = programPathResolver; _assets = assetSource; + _renderInterop = IRenderInterop.CreateForSvg(_assets, _programPathResolver); } - public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + public void Dispose() { - using var render = IRenderInterop.CreateForSvg(_assets, _programPathResolver); + _renderInterop.Dispose(); + } - ImageResult result = render.RenderLatex(arguments.Formula, arguments.Scale); + public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { + ImageResult result = _renderInterop.RenderLatex(arguments.Formula, arguments.Scale); await _fileSystem.WriteAllTextAsync(arguments.OutputFile, result.Data); diff --git a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs index 4ae3dc14..aa0fa614 100644 --- a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs +++ b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs @@ -25,7 +25,7 @@ namespace BookGen.Commands.Convert; [CommandName("convert md2html")] [Description("Renders a single markdown file to an HTML file.")] [ExitCode(ExitCodes.Success, "The command completed successfully.")] -internal sealed class Md2HtmlCommand : Command +internal sealed class Md2HtmlCommand : Command, IDisposable { internal sealed class Arguments : ArgumentsBase { @@ -116,6 +116,7 @@ public override void ModifyAfterValidation() private readonly IAssetSource _assetSource; private readonly TemplateEngine _templateEngine; private readonly IProgramPathResolver _programPathResolver; + private readonly RenderInterop _renderInterop; private const string TitleTag = "{{Title}}"; private const string ContentTag = "{{Content}}"; @@ -128,6 +129,12 @@ public Md2HtmlCommand(ILogger log, IFileSystemFactory fileSystemFactory, IProgra _programPathResolver = programPathResolver; _assetSource = assetSource; _templateEngine = new TemplateEngine(log, assetSource); + _renderInterop = new RenderInterop(_assetSource, _programPathResolver, new ImageConfig()); + } + + public void Dispose() + { + _renderInterop.Dispose(); } public override int Execute(Arguments arguments, IReadOnlyList context) @@ -163,6 +170,8 @@ public override int Execute(Arguments arguments, IReadOnlyList context) var imgService = new ImgService(inputFilesScope, _log, imgConfig); + _renderInterop.ImageConfig = imgConfig; + using var settings = new MarkdownRenderSettings(imgService) { HostUrl = string.Empty, @@ -170,7 +179,7 @@ public override int Execute(Arguments arguments, IReadOnlyList context) CssClasses = new CssClasses(), OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = !arguments.NoEmbed, - RenderInterop = new RenderInterop(_assetSource, _programPathResolver, imgConfig) + RenderInterop = _renderInterop }; settings.RenderInterop.PreRenderCode = !arguments.NoSyntax; diff --git a/Source/BookGen/Commands/Convert/QrCodeCommand.cs b/Source/BookGen/Commands/Convert/QrCodeCommand.cs index b49c3208..ccbc0662 100644 --- a/Source/BookGen/Commands/Convert/QrCodeCommand.cs +++ b/Source/BookGen/Commands/Convert/QrCodeCommand.cs @@ -19,7 +19,7 @@ namespace BookGen.Commands.Convert; [CommandName("convert qrcode")] [Description("Renders an url into a SVG QRCode image.")] [ExitCode(ExitCodes.Success, "The command completed successfully.")] -internal sealed class QrCodeCommand : AsyncCommand +internal sealed class QrCodeCommand : AsyncCommand, IDisposable { internal sealed class Arguments : ArgumentsBase { @@ -60,6 +60,7 @@ public override void ModifyAfterValidation() private readonly ILogger _log; private readonly IWritableFileSystem _fileSystem; private readonly IAssetSource _assetSource; + private readonly IRenderInterop _renderInteop; private readonly IProgramPathResolver _programPathResolver; public QrCodeCommand(ILogger log, IWritableFileSystem fileSystem, IProgramPathResolver programPathResolver, IAssetSource assetSource) @@ -68,13 +69,17 @@ public QrCodeCommand(ILogger log, IWritableFileSystem fileSystem, IProgramPathRe _fileSystem = fileSystem; _programPathResolver = programPathResolver; _assetSource = assetSource; + _renderInteop = IRenderInterop.CreateForSvg(_assetSource, _programPathResolver); } - public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + public void Dispose() { - using var render = IRenderInterop.CreateForSvg(_assetSource, _programPathResolver); + _renderInteop.Dispose(); + } - ImageResult result = render.RenderQrCode(arguments.Data); + public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { + ImageResult result = _renderInteop.RenderQrCode(arguments.Data); await _fileSystem.WriteAllTextAsync(arguments.Output, result.Data); diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index e03b17fc..65534d17 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -89,7 +89,7 @@ using ServiceProvider provider = ioc.BuildServiceProvider(); -CommandRunner runner = new(provider, new CommandHelpProvider(), logger, new CommandRunnerSettings +using CommandRunner runner = new(provider, new CommandHelpProvider(), logger, new CommandRunnerSettings { UnknownCommandCodeAndMessage = (-1, "Unknown command"), BadParametersExitCode = 2, diff --git a/Test/Bookgen.Tests/Cli/UT_CommandRunner.cs b/Test/Bookgen.Tests/Cli/UT_CommandRunner.cs index eafae942..917690f6 100644 --- a/Test/Bookgen.Tests/Cli/UT_CommandRunner.cs +++ b/Test/Bookgen.Tests/Cli/UT_CommandRunner.cs @@ -76,6 +76,12 @@ public void Setup() _sut.AddCommand(); } + [TearDown] + public void TearDown() + { + _sut.Dispose(); + } + [Test] public async Task EnsureThat_Run_Works_For_Command() { From 8cb1bdaea35330825393f4f2d0ebcf7b20394974 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 17:03:40 +0200 Subject: [PATCH 10/32] Fixes disposing of RenderInterop --- Source/BookGen.Lib/Http/PreviewRoutes.cs | 7 ++- .../Pipeline/Epub/CreateHtmlPages.cs | 6 +- .../BookGen.Lib/Pipeline/Feed/CreateItems.cs | 6 +- .../PostProcess/RenderPagesForPostProcess.cs | 6 +- .../BookGen.Lib/Pipeline/Print/RenderPages.cs | 6 +- .../Pipeline/StaticWebsite/RenderIndexPage.cs | 6 +- .../StaticWebsite/RenderStaticPages.cs | 6 +- .../Pipeline/Wordpress/CreateWpPages.cs | 6 +- .../Markdown/MarkdownRenderSettings.cs | 7 +-- .../Commands/Convert/Md2HtmlCommand.cs | 2 +- .../BookGen/Commands/folder/SearchCommand.cs | 6 +- .../Infrastructure/Plugins/V1/Renderer.cs | 7 ++- .../Bookgen.Tests/Lib/UT_MarkdownConverter.cs | 35 ++++++----- .../Lib/UT_MarkdownConverter_Specials.cs | 63 +++++++++++-------- 14 files changed, 102 insertions(+), 67 deletions(-) diff --git a/Source/BookGen.Lib/Http/PreviewRoutes.cs b/Source/BookGen.Lib/Http/PreviewRoutes.cs index 70f37fd0..add05d2b 100644 --- a/Source/BookGen.Lib/Http/PreviewRoutes.cs +++ b/Source/BookGen.Lib/Http/PreviewRoutes.cs @@ -25,6 +25,7 @@ internal sealed class PreviewRoutes : IDisposable, IRouteProvider { private readonly IReadOnlyFileSystem _source; private readonly ILogger _logger; + private readonly RenderInterop _renderInterop; private readonly MarkdownRenderSettings _renderSettings; private readonly MarkdownConverter _markdownConverter; private readonly TemplateEngine _templateEngine; @@ -51,6 +52,8 @@ public PreviewRoutes(IReadOnlyFileSystem source, var imgService = new ImgService(source, _logger, imageConfig); + _renderInterop = new RenderInterop(assetSource, programPathResolver, imageConfig); + _renderSettings = new MarkdownRenderSettings(imgService) { HostUrl = "http://localhost", @@ -58,7 +61,7 @@ public PreviewRoutes(IReadOnlyFileSystem source, CssClasses = new CssClasses(), OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(assetSource, programPathResolver, imageConfig) + RenderInterop = _renderInterop }; _markdownConverter = new MarkdownConverter(_renderSettings); @@ -109,7 +112,7 @@ public void Dispose() _observer.FileChanged -= OnFileChange; _observer.Dispose(); _markdownConverter.Dispose(); - _renderSettings.Dispose(); + _renderInterop.Dispose(); } public IEnumerable<(ApiMetaData metaData, RequestDelegate handler)> Routes diff --git a/Source/BookGen.Lib/Pipeline/Epub/CreateHtmlPages.cs b/Source/BookGen.Lib/Pipeline/Epub/CreateHtmlPages.cs index dafdef94..316eca7b 100644 --- a/Source/BookGen.Lib/Pipeline/Epub/CreateHtmlPages.cs +++ b/Source/BookGen.Lib/Pipeline/Epub/CreateHtmlPages.cs @@ -53,12 +53,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var imgService = new ImgService(environment.Source, logger, imgConfig); var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig), + RenderInterop = renderInterop, OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = false, ImageUrlRewriter = EpubImageRewrite diff --git a/Source/BookGen.Lib/Pipeline/Feed/CreateItems.cs b/Source/BookGen.Lib/Pipeline/Feed/CreateItems.cs index 7830c185..4919bbab 100644 --- a/Source/BookGen.Lib/Pipeline/Feed/CreateItems.cs +++ b/Source/BookGen.Lib/Pipeline/Feed/CreateItems.cs @@ -32,12 +32,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var imgService = new ImgService(environment.Source, logger, environment.Configuration.FeedConfig.Images); var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.FeedConfig.Images); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.FeedConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.FeedConfig.Images), + RenderInterop = renderInterop, OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = false, }; diff --git a/Source/BookGen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs b/Source/BookGen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs index 6f767e34..8e25d515 100644 --- a/Source/BookGen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs +++ b/Source/BookGen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs @@ -38,12 +38,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig), + RenderInterop = renderInterop, OffsetHeadingsBy = 1, AutoEmbedSupportedLinks = false, }; diff --git a/Source/BookGen.Lib/Pipeline/Print/RenderPages.cs b/Source/BookGen.Lib/Pipeline/Print/RenderPages.cs index a18e13bb..e109baaa 100644 --- a/Source/BookGen.Lib/Pipeline/Print/RenderPages.cs +++ b/Source/BookGen.Lib/Pipeline/Print/RenderPages.cs @@ -29,12 +29,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var imgService = new ImgService(environment.Source, logger, environment.Configuration.PrintConfig.Images); var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.PrintConfig.Images); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.PrintConfig.Images), + RenderInterop = renderInterop, OffsetHeadingsBy = 1, AutoEmbedSupportedLinks = false, }; diff --git a/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs b/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs index 0e051405..f91f7c73 100644 --- a/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs +++ b/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs @@ -30,12 +30,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.StaticWebsiteConfig.Images); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.StaticWebsiteConfig.CssClasses, DeleteFirstH1 = false, HostUrl = environment.Configuration.StaticWebsiteConfig.DeployHost, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.StaticWebsiteConfig.Images), + RenderInterop = renderInterop, AutoEmbedSupportedLinks = true, }; settings.RenderInterop.PreRenderCode = environment.Configuration.StaticWebsiteConfig.PreRenderCode; diff --git a/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs b/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs index f54ec3eb..5665a97a 100644 --- a/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs +++ b/Source/BookGen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs @@ -33,12 +33,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.StaticWebsiteConfig.Images); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.StaticWebsiteConfig.CssClasses, DeleteFirstH1 = false, HostUrl = environment.Configuration.StaticWebsiteConfig.DeployHost, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.StaticWebsiteConfig.Images), + RenderInterop = renderInterop, AutoEmbedSupportedLinks = true, }; settings.RenderInterop.PreRenderCode = false; diff --git a/Source/BookGen.Lib/Pipeline/Wordpress/CreateWpPages.cs b/Source/BookGen.Lib/Pipeline/Wordpress/CreateWpPages.cs index 1a79d214..1783430e 100644 --- a/Source/BookGen.Lib/Pipeline/Wordpress/CreateWpPages.cs +++ b/Source/BookGen.Lib/Pipeline/Wordpress/CreateWpPages.cs @@ -126,12 +126,14 @@ public override async Task ExecuteAsync(IBookEnvironment environment var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new MarkdownRenderSettings(cached) + using var renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.WordpressConfig.Images); + + var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.WordpressConfig.CssClasses, DeleteFirstH1 = true, HostUrl = environment.Configuration.WordpressConfig.DeployHost, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, environment.Configuration.WordpressConfig.Images), + RenderInterop = renderInterop, AutoEmbedSupportedLinks = true, }; settings.RenderInterop.PreRenderCode = false; diff --git a/Source/BookGen.Lib/Rendering/Markdown/MarkdownRenderSettings.cs b/Source/BookGen.Lib/Rendering/Markdown/MarkdownRenderSettings.cs index bcb4debe..de6a8473 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/MarkdownRenderSettings.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/MarkdownRenderSettings.cs @@ -9,7 +9,7 @@ namespace BookGen.Lib.Rendering.Markdown; -public sealed class MarkdownRenderSettings : IDisposable +public sealed class MarkdownRenderSettings { private readonly IImgService _imgService; @@ -41,9 +41,4 @@ public MarkdownRenderSettings(IImgService imgService) ImageUrlRewriter = EmbedImage; _imgService = imgService; } - - public void Dispose() - { - RenderInterop.Dispose(); - } } diff --git a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs index aa0fa614..21912f45 100644 --- a/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs +++ b/Source/BookGen/Commands/Convert/Md2HtmlCommand.cs @@ -172,7 +172,7 @@ public override int Execute(Arguments arguments, IReadOnlyList context) _renderInterop.ImageConfig = imgConfig; - using var settings = new MarkdownRenderSettings(imgService) + var settings = new MarkdownRenderSettings(imgService) { HostUrl = string.Empty, DeleteFirstH1 = false, diff --git a/Source/BookGen/Commands/folder/SearchCommand.cs b/Source/BookGen/Commands/folder/SearchCommand.cs index f024e9e4..6094f529 100644 --- a/Source/BookGen/Commands/folder/SearchCommand.cs +++ b/Source/BookGen/Commands/folder/SearchCommand.cs @@ -80,14 +80,16 @@ public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList< var imgService = new ImgService(env.Source, _logger, imageConfig); - using var settings = new MarkdownRenderSettings(imgService) + using var renderInterop = new RenderInterop(_assetSource, _programPathResolver, imageConfig); + + var settings = new MarkdownRenderSettings(imgService) { HostUrl = string.Empty, DeleteFirstH1 = false, CssClasses = new CssClasses(), OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = false, - RenderInterop = new RenderInterop(_assetSource, _programPathResolver, imageConfig), + RenderInterop = renderInterop, }; settings.RenderInterop.PreRenderCode = false; diff --git a/Source/BookGen/Infrastructure/Plugins/V1/Renderer.cs b/Source/BookGen/Infrastructure/Plugins/V1/Renderer.cs index 74bb3578..50fc524f 100644 --- a/Source/BookGen/Infrastructure/Plugins/V1/Renderer.cs +++ b/Source/BookGen/Infrastructure/Plugins/V1/Renderer.cs @@ -20,6 +20,7 @@ namespace BookGen.Infrastructure.Plugins.V1; internal sealed class Renderer : IRenderer { private readonly MarkdownRenderSettings _markdownRenderSettings; + private readonly RenderInterop _renderInterop; private readonly MarkdownConverter _markdownConverter; private readonly TemplateEngine _engine; private bool _disposed; @@ -41,12 +42,14 @@ public Renderer(RendererOptions options, var imgService = new ImgService(environment.Source, logger, imgConfig); var cachedImageService = new CachedImageService(imgService, memoryCache); + _renderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig); + _markdownRenderSettings = new MarkdownRenderSettings(cachedImageService) { CssClasses = Map(options.CssClasses), DeleteFirstH1 = options.DeleteFirstH1, HostUrl = options.HostUrl, - RenderInterop = new RenderInterop(environment, environment.ProgramPathResolver, imgConfig), + RenderInterop = _renderInterop, OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = options.AutoEmbedSupportedLinks, }; @@ -58,7 +61,7 @@ public Renderer(RendererOptions options, public void Dispose() { _markdownConverter.Dispose(); - _markdownRenderSettings.Dispose(); + _renderInterop.Dispose(); _disposed = true; } diff --git a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs index dbcebcec..d0678ab6 100644 --- a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs +++ b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs @@ -80,7 +80,8 @@ public void TearDown() [Test] public void EnsureThat_Css_ClassesAreAplied() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses { @@ -90,7 +91,7 @@ public void EnsureThat_Css_ClassesAreAplied() }, DeleteFirstH1 = false, HostUrl = null, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()), + RenderInterop = interop, AutoEmbedSupportedLinks = true, }; @@ -121,12 +122,13 @@ public void EnsureThat_Css_ClassesAreAplied() [Test] public void EnsureThat_DeleteFirstH1_HostUrlTargeting_Works() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = true, HostUrl = "https://my.domain", - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()), + RenderInterop = interop, AutoEmbedSupportedLinks = true, }; settings.RenderInterop.PreRenderCode = false; @@ -157,12 +159,13 @@ public void EnsureThat_DeleteFirstH1_HostUrlTargeting_Works() [Test] public void EnsureThat_SourceCode_Works() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = "https://my.domain", - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()), + RenderInterop = interop, AutoEmbedSupportedLinks = true, }; settings.RenderInterop.PreRenderCode = false; @@ -185,12 +188,13 @@ public void EnsureThat_SourceCode_Works() [Test] public void EnsureThat_SourceCode_PreRender_Works() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = "https://my.domain", - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()), + RenderInterop = interop, AutoEmbedSupportedLinks = true, }; @@ -211,13 +215,14 @@ public void EnsureThat_SourceCode_PreRender_Works() [Test] public void EnsureThat_Toc_NormalCase_RendersCorrectly() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()) + RenderInterop = interop, }; settings.RenderInterop.PreRenderCode = false; @@ -328,13 +333,14 @@ public void EnsureThat_Toc_NormalCase_RendersCorrectly() [Test] public void EnsureThat_Toc_WithTitle_RendersCorrectly() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()) + RenderInterop = interop, }; using var sut = new MarkdownConverter(settings); @@ -448,13 +454,14 @@ [toc] TOC*-*Title [Test] public void EnsureThat_Toc_Limited_RendersCorrectly() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()); + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig()) + RenderInterop = interop, }; using var sut = new MarkdownConverter(settings); diff --git a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs index 7be09646..648d19f3 100644 --- a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs +++ b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using AngleSharp.Html; + using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering.Images; using BookGen.Lib.Rendering.Markdown; @@ -18,6 +20,7 @@ internal sealed class UT_MarkdownConverter_Specials private Mock _imgServiceMock; private MarkdownRenderSettings _settings; private TestEnvironment _testEnvironment; + private RenderInterop _renderInterop; private readonly IEqualityComparer comparer = new LineEndingIgnoreComparer(); [SetUp] @@ -25,17 +28,17 @@ public void Setup() { _testEnvironment = new TestEnvironment(); _imgServiceMock = new Mock(MockBehavior.Strict); - + _renderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig() + { + SvgRecode = SvgRecodeOption.AsPng + }); _settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig() - { - SvgRecode = SvgRecodeOption.AsPng - }) + RenderInterop = _renderInterop, }; } @@ -43,7 +46,7 @@ public void Setup() [TearDown] public void Teardown() { - _settings.Dispose(); + _renderInterop.Dispose(); _testEnvironment.Dispose(); } @@ -95,16 +98,18 @@ public void EnsureThat_NomnomlRender_SvgWorks() ``` """; - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough + }); + + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig - { - SvgRecode = SvgRecodeOption.Passtrough - }) + RenderInterop = interop, }; using var sut = new MarkdownConverter(settings); @@ -179,16 +184,18 @@ public void EnsureThat_Math_Inline_Works() [TestCase("Press <> to open ", "

Press Ctrl+O to open http://www.example.com

\n")] public void EnsureThat_Kbd_Tag_Works(string input, string expected) { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough + }); + + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig - { - SvgRecode = SvgRecodeOption.Passtrough - }) + RenderInterop = interop, }; using var sut = new MarkdownConverter(settings); @@ -201,16 +208,18 @@ public void EnsureThat_Kbd_Tag_Works(string input, string expected) [Test] public void EnsureThat_Math_Block_Works() { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough + }); + + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig - { - SvgRecode = SvgRecodeOption.Passtrough - }) + RenderInterop = interop, }; string input = """ @@ -266,16 +275,18 @@ col31 col32 col33 [TestCase(Ssv)] public void EnsureThat_Language_Csv_Works(string input) { - using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + using var interop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough + }); + + var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, AutoEmbedSupportedLinks = true, - RenderInterop = new RenderInterop(_testEnvironment, _testEnvironment.ProgramPathResolver, new ImageConfig - { - SvgRecode = SvgRecodeOption.Passtrough - }) + RenderInterop = interop, }; using var sut = new MarkdownConverter(settings); From 51b617bd7d58313e7e7d903281fd884d2b3fc4e8 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 17:21:04 +0200 Subject: [PATCH 11/32] Some review comment fixes --- Docs/changelog.md | 2 +- Source/BookGen.Cli/Dotenv/DotEnvParser.cs | 5 ++++- Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs | 1 + Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs | 3 +++ Source/BookGen/Properties/launchSettings.json | 5 ----- Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs | 1 + 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Docs/changelog.md b/Docs/changelog.md index ab76a566..dd2690ae 100644 --- a/Docs/changelog.md +++ b/Docs/changelog.md @@ -6,7 +6,7 @@ tags: '' # 2026. 08. * Breaking: Removed json args support for commands -* Breaking: Removed config command. Configuration is now loaded from the bookgen.env file or from a configuration specified by the -env option +* Breaking: Removed config command. Configuration is now loaded from the `BookGen.env` file or from a configuration specified by the `-env` option * Breaking: Removed jsonargs command. * New: Md2html command output file now can be a folder. If a folder is specified, the output file name is generated from the input file name. * Fix: Shell atuocomplete now correctly handles command tree diff --git a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs index 49901ac2..0bd2e1b6 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs @@ -112,7 +112,10 @@ private static string ParseQuotedValue(TextReader reader, string line, ref int p if (c == quote) { pos++; - return sb.ToString(); + int suffixStart = SkipWhitespace(line, pos); + return suffixStart < line.Length && !IsCommentStart(line, suffixStart) + ? throw EnvironmentException.Env001(line) + : sb.ToString(); } sb.Append(c); pos++; diff --git a/Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs b/Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs index 0b1aaea9..7f43d497 100644 --- a/Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs +++ b/Source/BookGen.Cli/OpenCli/OpenCliDraftGenerator.cs @@ -103,6 +103,7 @@ public static Document GenerateOpenCli(string appName, Aliases = [globalOptionParser.LongName], Description = description?.Description, OpenClRequired = false, + Recursive = true, }); } return result.Count > 0 ? result : null; diff --git a/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs index 3ca924f5..15fbe2cd 100644 --- a/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs +++ b/Source/BookGen/GlobalOptionParsers/DotEnvFileParser.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.ComponentModel; + using BookGen.Cli; using BookGen.Cli.Dotenv; @@ -10,6 +12,7 @@ namespace BookGen.GlobalOptionParsers; +[Description("Loads BookGen settings from the specified environment file.")] internal class DotEnvFileParser : GlobalOptionParser { private readonly ILogger _log; diff --git a/Source/BookGen/Properties/launchSettings.json b/Source/BookGen/Properties/launchSettings.json index 4d5ad469..d87e850f 100644 --- a/Source/BookGen/Properties/launchSettings.json +++ b/Source/BookGen/Properties/launchSettings.json @@ -4,11 +4,6 @@ "commandName": "WSL2", "distributionName": "" }, - "BookGen": { - "commandName": "Project", - "commandLineArgs": "convert md2html --svg -i .\\src\\075-generics.md -tf .\\template\\template.html -o .\\docs --title \"Generics\"", - "workingDirectory": "P:\\github\\ultimatedotnetcheatsheet" - }, "Gui": { "commandName": "Project", "commandLineArgs": "Gui" diff --git a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs index e53072be..800560bf 100644 --- a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs +++ b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs @@ -129,6 +129,7 @@ third line } [TestCase("foo\nbar\nbaz=qux", "ENV001: Invalid Line Format")] + [TestCase("FOO=\"bar\"garbage", "ENV001: Invalid Line Format")] [TestCase("foo=bar\r\nfoo=bar", "ENV002: Duplicate Key")] [TestCase("123FOO=value", "ENV003: Invalid Key Format")] [TestCase("FOO-BAR=value", "ENV003: Invalid Key Format")] From d8566a137df37e4aea1de5149179f5f166139f46 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 19:33:54 +0200 Subject: [PATCH 12/32] Tests for Script command --- Source/BookGen.Vfs/Extensions.cs | 15 ++++++ Source/BookGen/Commands/ScriptCommand.cs | 11 ++-- .../Bookgen.Tests/Commands/CommandTestBase.cs | 2 + .../Commands/UT_Md2HtmlCommand.cs | 10 ++-- Test/Bookgen.Tests/Commands/UT_Script.cs | 53 +++++++++++++++++++ .../Bookgen.Tests/Commands/UT_ShellCommand.cs | 12 ++--- 6 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 Test/Bookgen.Tests/Commands/UT_Script.cs diff --git a/Source/BookGen.Vfs/Extensions.cs b/Source/BookGen.Vfs/Extensions.cs index e923465c..1f00a37d 100644 --- a/Source/BookGen.Vfs/Extensions.cs +++ b/Source/BookGen.Vfs/Extensions.cs @@ -184,4 +184,19 @@ public void Serialize(string path, T value) JsonSerializer.Serialize(stream, value, JsonOptions.SerializerOptions); } } + + extension(TextReader reader) + { + public async Task> ReadAllLinesAsync(CancellationToken cancellationToken) + { + string? line; + var lines = new List(); + while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null) + { + lines.Add(line); + cancellationToken.ThrowIfCancellationRequested(); + } + return lines; + } + } } diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index d3e4c019..53c4ee15 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -8,6 +8,7 @@ using BookGen.Cli; using BookGen.Cli.Annotations; +using BookGen.Vfs; using Microsoft.Extensions.Logging; @@ -39,22 +40,26 @@ public override ValidationResult Validate(IValidationContext context) private readonly ICommandRunnerProxy _commandRunnerProxy; private readonly ILogger _logger; + private readonly IReadOnlyFileSystem _fileSystem; - public ScriptCommand(ICommandRunnerProxy commandRunnerProxy, ILogger logger) + public ScriptCommand(ICommandRunnerProxy commandRunnerProxy, ILogger logger, IReadOnlyFileSystem fileSystem) { _commandRunnerProxy = commandRunnerProxy; _logger = logger; + _fileSystem = fileSystem; } public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) { - if (!File.Exists(arguments.ScriptPath)) + if (!_fileSystem.FileExists(arguments.ScriptPath)) { _logger.LogError("Script file doesn't exist: {path}", arguments.ScriptPath); return ExitCodes.ArgumentsError; } - string[] rawLines = await File.ReadAllLinesAsync(arguments.ScriptPath, token); + using TextReader reader = _fileSystem.OpenTextReader(arguments.ScriptPath); + + List rawLines = await reader.ReadAllLinesAsync(token); IReadOnlyList commandNames = _commandRunnerProxy.CommandNames .OrderByDescending(x => x.Length) diff --git a/Test/Bookgen.Tests/Commands/CommandTestBase.cs b/Test/Bookgen.Tests/Commands/CommandTestBase.cs index ec3012ea..28375bfd 100644 --- a/Test/Bookgen.Tests/Commands/CommandTestBase.cs +++ b/Test/Bookgen.Tests/Commands/CommandTestBase.cs @@ -25,6 +25,8 @@ internal abstract class CommandTestBase where TCommand : ICommand protected ICommand Command { get; private set; } + public const int TenSeconds = 10_000; + [SetUp] public void Setup() { diff --git a/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs b/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs index bd1ee52e..25e4c711 100644 --- a/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs +++ b/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs @@ -31,7 +31,8 @@ protected override Md2HtmlCommand CreateSut() => new Md2HtmlCommand(LoggerMock.Object, FilesystemFactoryMock.Object, ProgramPathResolverMock.Object, AssetSourceMock.Object); [Test] - public async Task EnsureThat_GenerateRawWorks() + [CancelAfter(TenSeconds)] + public async Task EnsureThat_GenerateRawWorks(CancellationToken token) { var arguments = new Md2HtmlCommand.Arguments { @@ -43,7 +44,7 @@ public async Task EnsureThat_GenerateRawWorks() Title = "Document title" }; - int exitCode = await Command.ExecuteAsync(arguments, Array.Empty(), CancellationToken.None); + int exitCode = await Command.ExecuteAsync(arguments, Array.Empty(), token); const string expectedContent = "

test

\n"; @@ -56,7 +57,8 @@ public async Task EnsureThat_GenerateRawWorks() } [Test] - public async Task EnsureThat_GenerateHtml_Works() + [CancelAfter(TenSeconds)] + public async Task EnsureThat_GenerateHtml_Works(CancellationToken token) { var arguments = new Md2HtmlCommand.Arguments { @@ -68,7 +70,7 @@ public async Task EnsureThat_GenerateHtml_Works() Title = "Document title" }; - int exitCode = await Command.ExecuteAsync(arguments, Array.Empty(), CancellationToken.None); + int exitCode = await Command.ExecuteAsync(arguments, Array.Empty(), token); using (Assert.EnterMultipleScope()) { diff --git a/Test/Bookgen.Tests/Commands/UT_Script.cs b/Test/Bookgen.Tests/Commands/UT_Script.cs new file mode 100644 index 00000000..f1a9e1ce --- /dev/null +++ b/Test/Bookgen.Tests/Commands/UT_Script.cs @@ -0,0 +1,53 @@ +using BookGen; +using BookGen.Commands; + +using Microsoft.Extensions.Logging; + +using Moq; + +namespace Bookgen.Tests.Commands; + +[TestFixture] +internal class UT_Script : CommandTestBase +{ + private string _samplescript = null!; + + protected override void SetupMocks() + { + _samplescript = """ + #log executing cmd1 + cmd1 arg1 arg2 #log this shouldn't be logged + #log done executing + #regular coment + """; + + CommandRunnerProxyMock.Setup(c => c.CommandNames).Returns(["cmd1", "cmd2"]); + FileSystemMock.Setup(x => x.FileExists("sample.script")).Returns(true); + FileSystemMock.Setup(x => x.OpenTextReader("sample.script")).Returns(new StringReader(_samplescript)); + LoggerMock.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); + CommandRunnerProxyMock.Setup(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); + } + + protected override ScriptCommand CreateSut() + { + return new ScriptCommand(CommandRunnerProxyMock.Object, + LoggerMock.Object, + FileSystemMock.Object); + } + + [Test] + [CancelAfter(TenSeconds)] + public async Task EnsureThat_SampleScript_Runs(CancellationToken token) + { + var args = new ScriptCommand.Arguments { ScriptPath = "sample.script" }; + int exitCode = await Command.ExecuteAsync(args, Array.Empty(), token); + + using (Assert.EnterMultipleScope()) + { + Assert.AreEqual(exitCode, ExitCodes.Success); + CommandRunnerProxyMock.Verify(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" }), Times.Once); + LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("executing cmd1")), null, It.IsAny>()), Times.Once); + LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("done executing")), null, It.IsAny>()), Times.Once); + } + } +} diff --git a/Test/Bookgen.Tests/Commands/UT_ShellCommand.cs b/Test/Bookgen.Tests/Commands/UT_ShellCommand.cs index 29a13972..9773773d 100644 --- a/Test/Bookgen.Tests/Commands/UT_ShellCommand.cs +++ b/Test/Bookgen.Tests/Commands/UT_ShellCommand.cs @@ -6,8 +6,6 @@ using BookGen.Cli; using BookGen.Commands; -using Spectre.Console; - namespace Bookgen.Tests.Commands; @@ -54,12 +52,13 @@ protected override void SetupMocks() [TestCase("BookGen book", 11, "book validate")] [TestCase("BookGen convert m", 16, "md2html")] [TestCase("BookGen convert md2html -i", 27, "")] - public async Task EnsureThat_Autocomplete_ReturnsExpected(string input, int index, string expected) + [CancelAfter(TenSeconds)] + public async Task EnsureThat_Autocomplete_ReturnsExpected(string input, int index, string expected, CancellationToken token) { using var writer = new StringWriter(); Console.SetOut(writer); - var result = await Command.ExecuteAsync(ArgumentsBase.Empty, [index.ToString(), input], CancellationToken.None); + var result = await Command.ExecuteAsync(ArgumentsBase.Empty, [index.ToString(), input], token); using (Assert.EnterMultipleScope()) { @@ -69,12 +68,13 @@ public async Task EnsureThat_Autocomplete_ReturnsExpected(string input, int inde } [Test] - public async Task Test_Execute_NoArgs() + [CancelAfter(TenSeconds)] + public async Task Test_Execute_NoArgs(CancellationToken token) { using var writer = new StringWriter(); Console.SetOut(writer); - var result = await Command.ExecuteAsync(ArgumentsBase.Empty, ["c"], CancellationToken.None); + var result = await Command.ExecuteAsync(ArgumentsBase.Empty, ["c"], token); using (Assert.EnterMultipleScope()) { From f24c62fbc821b49958a2fc718d60c9a9c6e9f5a7 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 8 Aug 2026 19:41:29 +0200 Subject: [PATCH 13/32] Script command fixes --- Source/BookGen/Commands/ScriptCommand.cs | 10 ++++------ Test/Bookgen.Tests/Commands/UT_Script.cs | 8 ++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index 53c4ee15..c8f5c430 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -139,13 +139,11 @@ private static IEnumerable JoinContinuations(IEnumerable lines) private static bool TryGetLogMessage(string line, out string? message) { message = null; - string trimmed = line.TrimStart(); - foreach (string marker in (ReadOnlySpan)["#log", "//log"]) { - if (trimmed.StartsWith(marker, StringComparison.Ordinal)) + if (line.StartsWith(marker, StringComparison.Ordinal)) { - string rest = trimmed[marker.Length..]; + string rest = line[marker.Length..]; // The marker must be followed by whitespace or the end of line to be treated as a log instruction. if (rest.Length == 0 || char.IsWhiteSpace(rest[0])) @@ -186,11 +184,11 @@ private static List Tokenize(string line) inQuotes = true; hasToken = true; break; - case '#': + case '#' when i == 0 || char.IsWhiteSpace(line[i - 1]): // Rest of the line is a comment. FlushToken(tokens, current, ref hasToken); return tokens; - case '/' when i + 1 < line.Length && line[i + 1] == '/': + case '/' when i + 1 < line.Length && line[i + 1] == '/' && (i == 0 || char.IsWhiteSpace(line[i - 1])): // Rest of the line is a comment. FlushToken(tokens, current, ref hasToken); return tokens; diff --git a/Test/Bookgen.Tests/Commands/UT_Script.cs b/Test/Bookgen.Tests/Commands/UT_Script.cs index f1a9e1ce..d747f17e 100644 --- a/Test/Bookgen.Tests/Commands/UT_Script.cs +++ b/Test/Bookgen.Tests/Commands/UT_Script.cs @@ -18,14 +18,17 @@ protected override void SetupMocks() #log executing cmd1 cmd1 arg1 arg2 #log this shouldn't be logged #log done executing + convert qrcode -d https://example.com/page#section #regular coment """; - CommandRunnerProxyMock.Setup(c => c.CommandNames).Returns(["cmd1", "cmd2"]); + CommandRunnerProxyMock.Setup(c => c.CommandNames).Returns(["cmd1", "cmd2", "convert qrcode"]); + CommandRunnerProxyMock.Setup(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); + CommandRunnerProxyMock.Setup(x => x.RunCommand("convert qrcode", new string[] { "-d", "https://example.com/page#section" })).Returns(Task.FromResult(0)); + FileSystemMock.Setup(x => x.FileExists("sample.script")).Returns(true); FileSystemMock.Setup(x => x.OpenTextReader("sample.script")).Returns(new StringReader(_samplescript)); LoggerMock.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - CommandRunnerProxyMock.Setup(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); } protected override ScriptCommand CreateSut() @@ -46,6 +49,7 @@ public async Task EnsureThat_SampleScript_Runs(CancellationToken token) { Assert.AreEqual(exitCode, ExitCodes.Success); CommandRunnerProxyMock.Verify(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" }), Times.Once); + CommandRunnerProxyMock.Verify(x => x.RunCommand("convert qrcode", new string[] { "-d", "https://example.com/page#section" }), Times.Once); LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("executing cmd1")), null, It.IsAny>()), Times.Once); LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("done executing")), null, It.IsAny>()), Times.Once); } From 76afe231606cf19a7d1849fdbcf9ad753011fd35 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 9 Aug 2026 14:56:11 +0200 Subject: [PATCH 14/32] Review fixes --- Source/BookGen.Cli/CommandRunnerProxy.cs | 2 +- Source/BookGen.Cli/ICommandRunnerProxy.cs | 2 +- Source/BookGen.Cli/Internals/BranchCommand.cs | 2 +- Source/BookGen/Commands/GuiCommand.cs | 2 +- Source/BookGen/Commands/ScriptCommand.cs | 4 ++-- Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs | 6 +++--- Test/Bookgen.Tests/Commands/CommandTestBase.cs | 9 +++++++++ Test/Bookgen.Tests/Commands/UT_Script.cs | 8 ++++---- 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Source/BookGen.Cli/CommandRunnerProxy.cs b/Source/BookGen.Cli/CommandRunnerProxy.cs index dac1795a..a7ae29d0 100644 --- a/Source/BookGen.Cli/CommandRunnerProxy.cs +++ b/Source/BookGen.Cli/CommandRunnerProxy.cs @@ -37,7 +37,7 @@ public string[] GetAutoCompleteItems(string commandName) => _autoComplete?.Invoke(commandName) ?? throw new InvalidOperationException("Provider hasn't been setup correctly"); - public async Task RunCommand(string commandName, IReadOnlyList argsToParse) + public async Task RunCommandAsync(string commandName, IReadOnlyList argsToParse) { if (_runCommand == null) throw new InvalidOperationException("Provider hasn't been setup correctly"); diff --git a/Source/BookGen.Cli/ICommandRunnerProxy.cs b/Source/BookGen.Cli/ICommandRunnerProxy.cs index cc8114da..ebca37fb 100644 --- a/Source/BookGen.Cli/ICommandRunnerProxy.cs +++ b/Source/BookGen.Cli/ICommandRunnerProxy.cs @@ -12,6 +12,6 @@ public interface ICommandRunnerProxy IEnumerable CommandNames { get; } IEnumerable GlobalOptions { get; } string[] GetAutoCompleteItems(string commandName); - Task RunCommand(string commandName, IReadOnlyList argsToParse); + Task RunCommandAsync(string commandName, IReadOnlyList argsToParse); Document GetOpenCliDocs(); } diff --git a/Source/BookGen.Cli/Internals/BranchCommand.cs b/Source/BookGen.Cli/Internals/BranchCommand.cs index 02019c4a..f1b13fee 100644 --- a/Source/BookGen.Cli/Internals/BranchCommand.cs +++ b/Source/BookGen.Cli/Internals/BranchCommand.cs @@ -70,7 +70,7 @@ public override async Task ExecuteAsync(IReadOnlyList context, Canc string commandToRun = $"{_branchItemsProvider.BranchName} {context[0]}"; if (_branchItemsProvider.BranchItems.Contains(commandToRun, StringComparer.OrdinalIgnoreCase)) { - return await _commandRunnerProxy.RunCommand(commandToRun, context.Skip(1).ToList()); + return await _commandRunnerProxy.RunCommandAsync(commandToRun, context.Skip(1).ToList()); } } diff --git a/Source/BookGen/Commands/GuiCommand.cs b/Source/BookGen/Commands/GuiCommand.cs index 3372e908..97c8874d 100644 --- a/Source/BookGen/Commands/GuiCommand.cs +++ b/Source/BookGen/Commands/GuiCommand.cs @@ -125,7 +125,7 @@ private async Task Run(string cmd, params string[] additionals) { return _currentArgs == null ? throw new InvalidOperationException("Command not initialized") - : await _commandRunnerProxy.RunCommand(cmd, _argsBuilder.New().Add(_currentArgs).Add(additionals).Build()); + : await _commandRunnerProxy.RunCommandAsync(cmd, _argsBuilder.New().Add(_currentArgs).Add(additionals).Build()); } private Task OnExit() diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index c8f5c430..d9984f97 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -89,9 +89,9 @@ public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList< return ExitCodes.ArgumentsError; } - _logger.LogInformation("Running: {command}", logicalLine.Trim()); + //_logger.LogInformation("Running: {command}", logicalLine.Trim()); - exitCode = await _commandRunnerProxy.RunCommand(commandName, commandArgs); + exitCode = await _commandRunnerProxy.RunCommandAsync(commandName, commandArgs); // pipefail style: stop on the first failing command and return its exit code. if (exitCode != ExitCodes.Success) diff --git a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs index 800560bf..643e152e 100644 --- a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs +++ b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs @@ -59,7 +59,7 @@ public void EnsureThat_Keys_And_Values_ParsedCorrectly(string input, string expe using (Assert.EnterMultipleScope()) { - Assert.That(env.Keys, Contains.Key(expectedKey)); + Assert.That(env.Keys, Does.Contain(expectedKey)); Assert.That(env.GetValueOrDefault(expectedKey, string.Empty), Is.EqualTo(expectedValue)); } } @@ -98,7 +98,7 @@ public void EnsureThat_Quoted_MultiLine_Works() using (Assert.EnterMultipleScope()) { - Assert.That(env.Keys, Contains.Key("PRIVATE_KEY")); + Assert.That(env.Keys, Does.Contain("PRIVATE_KEY")); Assert.That(env.GetValueOrDefault("PRIVATE_KEY", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); } } @@ -123,7 +123,7 @@ third line using (Assert.EnterMultipleScope()) { - Assert.That(env.Keys, Contains.Key("LONG_MESSAGE")); + Assert.That(env.Keys, Does.Contain("LONG_MESSAGE")); Assert.That(env.GetValueOrDefault("LONG_MESSAGE", string.Empty), Is.EqualTo(expectedValue).IgnoreLineEndingFormat); } } diff --git a/Test/Bookgen.Tests/Commands/CommandTestBase.cs b/Test/Bookgen.Tests/Commands/CommandTestBase.cs index 28375bfd..c1151c3a 100644 --- a/Test/Bookgen.Tests/Commands/CommandTestBase.cs +++ b/Test/Bookgen.Tests/Commands/CommandTestBase.cs @@ -34,6 +34,15 @@ public void Setup() Command = CreateSut(); } + [TearDown] + public virtual void Teardown() + { + if (Command is IDisposable disposable) + { + disposable.Dispose(); + } + } + protected abstract TCommand CreateSut(); protected abstract void SetupMocks(); diff --git a/Test/Bookgen.Tests/Commands/UT_Script.cs b/Test/Bookgen.Tests/Commands/UT_Script.cs index d747f17e..5cbb7cfa 100644 --- a/Test/Bookgen.Tests/Commands/UT_Script.cs +++ b/Test/Bookgen.Tests/Commands/UT_Script.cs @@ -23,8 +23,8 @@ cmd1 arg1 arg2 #log this shouldn't be logged """; CommandRunnerProxyMock.Setup(c => c.CommandNames).Returns(["cmd1", "cmd2", "convert qrcode"]); - CommandRunnerProxyMock.Setup(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); - CommandRunnerProxyMock.Setup(x => x.RunCommand("convert qrcode", new string[] { "-d", "https://example.com/page#section" })).Returns(Task.FromResult(0)); + CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); + CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("convert qrcode", new string[] { "-d", "https://example.com/page#section" })).Returns(Task.FromResult(0)); FileSystemMock.Setup(x => x.FileExists("sample.script")).Returns(true); FileSystemMock.Setup(x => x.OpenTextReader("sample.script")).Returns(new StringReader(_samplescript)); @@ -48,8 +48,8 @@ public async Task EnsureThat_SampleScript_Runs(CancellationToken token) using (Assert.EnterMultipleScope()) { Assert.AreEqual(exitCode, ExitCodes.Success); - CommandRunnerProxyMock.Verify(x => x.RunCommand("cmd1", new string[] { "arg1", "arg2" }), Times.Once); - CommandRunnerProxyMock.Verify(x => x.RunCommand("convert qrcode", new string[] { "-d", "https://example.com/page#section" }), Times.Once); + CommandRunnerProxyMock.Verify(x => x.RunCommandAsync("cmd1", new string[] { "arg1", "arg2" }), Times.Once); + CommandRunnerProxyMock.Verify(x => x.RunCommandAsync("convert qrcode", new string[] { "-d", "https://example.com/page#section" }), Times.Once); LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("executing cmd1")), null, It.IsAny>()), Times.Once); LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("done executing")), null, It.IsAny>()), Times.Once); } From 3b21a1ff5c567781487193da947da35ebc68bcc6 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 9 Aug 2026 15:09:54 +0200 Subject: [PATCH 15/32] Fix in Dotenv parser and Script command --- Source/BookGen.Cli/Dotenv/DotEnvParser.cs | 5 ++- Source/BookGen/Commands/ScriptCommand.cs | 49 +++++++++++++++++++++-- Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs | 1 + Test/Bookgen.Tests/Commands/UT_Script.cs | 30 +++++++++++++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs index 0bd2e1b6..a1629e33 100644 --- a/Source/BookGen.Cli/Dotenv/DotEnvParser.cs +++ b/Source/BookGen.Cli/Dotenv/DotEnvParser.cs @@ -185,8 +185,9 @@ private static string ParseUnquotedOrContinuation(TextReader reader, string line sb.Append(line, pos, end - pos); pos = end; - // Trailing whitespace on an unquoted value is not allowed. - if (valueEnd > end) + // Trailing whitespace on an unquoted value is not allowed, + // unless it precedes an inline comment. + if (valueEnd > end && !sawComment) { throw EnvironmentException.Env001(line); } diff --git a/Source/BookGen/Commands/ScriptCommand.cs b/Source/BookGen/Commands/ScriptCommand.cs index d9984f97..4531cd97 100644 --- a/Source/BookGen/Commands/ScriptCommand.cs +++ b/Source/BookGen/Commands/ScriptCommand.cs @@ -89,8 +89,6 @@ public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList< return ExitCodes.ArgumentsError; } - //_logger.LogInformation("Running: {command}", logicalLine.Trim()); - exitCode = await _commandRunnerProxy.RunCommandAsync(commandName, commandArgs); // pipefail style: stop on the first failing command and return its exit code. @@ -112,7 +110,12 @@ private static IEnumerable JoinContinuations(IEnumerable lines) { string current = line; - if (current.EndsWith('\\')) + // A trailing backslash only continues the logical line when it is + // syntactic — i.e. it is not inside a quoted string and not part + // of a trailing comment. Detect comments/quotes here to avoid + // consuming the next command when a backslash appears inside a + // comment (e.g. "cmd1 # note \"). + if (IsSyntacticFinalBackslash(current)) { builder ??= new StringBuilder(); builder.Append(current.AsSpan(0, current.Length - 1)); @@ -136,6 +139,46 @@ private static IEnumerable JoinContinuations(IEnumerable lines) yield return builder.ToString(); } + private static bool IsSyntacticFinalBackslash(string line) + { + if (line.Length == 0 || line[^1] != '\\') + return false; + + bool inQuotes = false; + + for (int i = 0; i < line.Length - 1; i++) + { + char c = line[i]; + + if (inQuotes) + { + if (c == '"') + inQuotes = false; + + continue; + } + + if (c == '"') + { + inQuotes = true; + continue; + } + + if (c == '#' && (i == 0 || char.IsWhiteSpace(line[i - 1]))) + return false; + + if (c == '/' && + i + 1 < line.Length && + line[i + 1] == '/' && + (i == 0 || char.IsWhiteSpace(line[i - 1]))) + { + return false; + } + } + + return !inQuotes; + } + private static bool TryGetLogMessage(string line, out string? message) { message = null; diff --git a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs index 643e152e..4bf055df 100644 --- a/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs +++ b/Test/Bookgen.Tests/Cli/UT_DotEnvParser.cs @@ -51,6 +51,7 @@ public void EnsureThat_Comments_And_EmptyLines_AreIgnored() [TestCase("MESSAGE='Hello World'", "MESSAGE", "Hello World")] [TestCase("PATH=\"C:\\Program Files\\App\"", "PATH", "C:\\Program Files\\App")] [TestCase("HASH=\"my#password\"", "HASH", "my#password")] + [TestCase("FOO=bar # comment", "FOO", "bar")] public void EnsureThat_Keys_And_Values_ParsedCorrectly(string input, string expectedKey, string expectedValue) { using var reader = new StringReader(input); diff --git a/Test/Bookgen.Tests/Commands/UT_Script.cs b/Test/Bookgen.Tests/Commands/UT_Script.cs index 5cbb7cfa..940452ad 100644 --- a/Test/Bookgen.Tests/Commands/UT_Script.cs +++ b/Test/Bookgen.Tests/Commands/UT_Script.cs @@ -1,4 +1,6 @@ -using BookGen; +using System.IO.Pipes; + +using BookGen; using BookGen.Commands; using Microsoft.Extensions.Logging; @@ -11,6 +13,7 @@ namespace Bookgen.Tests.Commands; internal class UT_Script : CommandTestBase { private string _samplescript = null!; + private string _multilineScript = null!; protected override void SetupMocks() { @@ -22,12 +25,20 @@ cmd1 arg1 arg2 #log this shouldn't be logged #regular coment """; + _multilineScript = """ + cmd1 # note \ + cmd2 + """; + CommandRunnerProxyMock.Setup(c => c.CommandNames).Returns(["cmd1", "cmd2", "convert qrcode"]); - CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("cmd1", new string[] { "arg1", "arg2" })).Returns(Task.FromResult(0)); + CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("cmd1", It.IsAny())).Returns(Task.FromResult(0)); + CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("cmd2", It.IsAny())).Returns(Task.FromResult(0)); CommandRunnerProxyMock.Setup(x => x.RunCommandAsync("convert qrcode", new string[] { "-d", "https://example.com/page#section" })).Returns(Task.FromResult(0)); FileSystemMock.Setup(x => x.FileExists("sample.script")).Returns(true); + FileSystemMock.Setup(x => x.FileExists("multi.script")).Returns(true); FileSystemMock.Setup(x => x.OpenTextReader("sample.script")).Returns(new StringReader(_samplescript)); + FileSystemMock.Setup(x => x.OpenTextReader("multi.script")).Returns(new StringReader(_multilineScript)); LoggerMock.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); } @@ -54,4 +65,19 @@ public async Task EnsureThat_SampleScript_Runs(CancellationToken token) LoggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("done executing")), null, It.IsAny>()), Times.Once); } } + + [Test] + [CancelAfter(TenSeconds)] + public async Task EnsureThat_MultiLineCommandsWork(CancellationToken token) + { + var args = new ScriptCommand.Arguments { ScriptPath = "multi.script" }; + int exitCode = await Command.ExecuteAsync(args, Array.Empty(), token); + + using (Assert.EnterMultipleScope()) + { + Assert.AreEqual(exitCode, ExitCodes.Success); + CommandRunnerProxyMock.Verify(x => x.RunCommandAsync("cmd1", It.IsAny()), Times.Once); + CommandRunnerProxyMock.Verify(x => x.RunCommandAsync("cmd2", It.IsAny()), Times.Once); + } + } } From 155750a19675a805e648933aca018f1556034d3a Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 9 Aug 2026 15:13:12 +0200 Subject: [PATCH 16/32] Global option parsing is now more robust --- Source/BookGen.Cli/CommandRunner.cs | 12 ++++++------ Source/BookGen.Cli/GlobalOptionParser.cs | 13 ++++++++----- Source/BookGen.Cli/Internals/Helpers.cs | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index ae260312..172dc4de 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -254,19 +254,19 @@ public async Task Run(IReadOnlyList args) skipCount = 0; } - HashSet parsedGlobals = new(); - foreach (var parser in _globalOptionParsers) + HashSet parsedGlobalIndexes = new(); + foreach (GlobalOptionParser parser in _globalOptionParsers) { - if (parser.TryParseGlobalOption(args, out List globalOptions)) + if (parser.TryParseGlobalOption(args, out List consumedIndexes)) { - foreach (var option in globalOptions) + foreach (var index in consumedIndexes) { - parsedGlobals.Add(option); + parsedGlobalIndexes.Add(index); } } } - List argsToParse = Helpers.GetArgsToParse(args, parsedGlobals, skipCount); + List argsToParse = Helpers.GetArgsToParse(args, parsedGlobalIndexes, skipCount); return await RunCommand(commandName, argsToParse); } diff --git a/Source/BookGen.Cli/GlobalOptionParser.cs b/Source/BookGen.Cli/GlobalOptionParser.cs index 20e108a5..e765b23c 100644 --- a/Source/BookGen.Cli/GlobalOptionParser.cs +++ b/Source/BookGen.Cli/GlobalOptionParser.cs @@ -24,10 +24,10 @@ public GlobalOptionParser(string shortName, string longName, bool needsValue = f NeedsValue = needsValue; } - public bool TryParseGlobalOption(IReadOnlyList args, out List parsedValues) + public bool TryParseGlobalOption(IReadOnlyList args, out List consumedIndexes) { bool handle = false; - parsedValues = new List(); + consumedIndexes = new List(); int optionIndex = 0; @@ -36,7 +36,6 @@ public bool TryParseGlobalOption(IReadOnlyList args, out List pa if (args[i] == ShortName || args[i] == LongName) { handle = true; - parsedValues.Add(args[i]); optionIndex = i; break; } @@ -45,23 +44,27 @@ public bool TryParseGlobalOption(IReadOnlyList args, out List pa if (handle) { string value = string.Empty; + int valueIndex = -1; if (NeedsValue) { int index = optionIndex + 1; if (index < args.Count) { value = args[index]; + valueIndex = index; } else { - Console.WriteLine($"Ignored Option '{parsedValues[0]}': requires a value, but none was provided."); + Console.WriteLine($"Ignored Option '{args[optionIndex]}': requires a value, but none was provided."); handle = false; } } if (handle) { OnOptionWasPresent(value); - parsedValues.Add(value); + consumedIndexes.Add(optionIndex); + if (valueIndex >= 0) + consumedIndexes.Add(valueIndex); } } diff --git a/Source/BookGen.Cli/Internals/Helpers.cs b/Source/BookGen.Cli/Internals/Helpers.cs index ab6ba97e..c9847360 100644 --- a/Source/BookGen.Cli/Internals/Helpers.cs +++ b/Source/BookGen.Cli/Internals/Helpers.cs @@ -32,13 +32,13 @@ public static void ConfigureUtfSupport(bool enableUtf8Output) // Skip the first argument (command name) and any parsed global options public static List GetArgsToParse(IReadOnlyList args, - HashSet parsedGlobals, + HashSet parsedGlobalIndexes, int skipCount) { List results = new(); for (int i = skipCount; i < args.Count; i++) { - if (!parsedGlobals.Contains(args[i])) + if (!parsedGlobalIndexes.Contains(i)) { results.Add(args[i]); } From a7b8f3469c80c245398b5f347c77b976913a3b67 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Mon, 10 Aug 2026 20:50:21 +0200 Subject: [PATCH 17/32] Diagram2svg infer diagram type from extension --- .../Commands/Convert/Diagram2SvgCommand.cs | 30 +++++++++++++++++-- Source/BookGen/Program.cs | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs index 5565a572..9f3e4f84 100644 --- a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs +++ b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs @@ -56,14 +56,40 @@ public override ValidationResult Validate(IValidationContext context) result.AddIssue("Output file must be specified"); if (Type == DiagramType.Unknown) - result.AddIssue("Diagram type must be specified"); + { + var extension = Path.GetExtension(InputFile).ToLower(); + switch (extension) + { + case ".mmd": + case ".mermaid": + case ".nomnoml": + // Infer the diagram type from the file extension + break; + default: + result.AddIssue("Diagram type must be specified"); + break; + } + } return result; } public override void ModifyAfterValidation() { - OutputFile = Path.ChangeExtension(OutputFile, ".svg"); + var extension = Path.GetExtension(InputFile).ToLower(); + + if (extension != ".svg") + OutputFile = Path.ChangeExtension(OutputFile, ".svg"); + + if (Type == DiagramType.Unknown) + { + Type = extension switch + { + ".mmd" or ".mermaid" => DiagramType.Mermaid, + ".nomnoml" => DiagramType.Nomnoml, + _ => throw new System.Diagnostics.UnreachableException("Shouldn't happen"), + }; + } } } diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index 65534d17..398b0f9d 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -52,7 +52,7 @@ { try { - logger.LogInformation("Loading default env-file '{defaultEnvFile}'.", defaultEnvFile); + logger.LogDebug("Loading default env-file '{defaultEnvFile}'.", defaultEnvFile); using StreamReader reader = File.OpenText(defaultEnvFile); DotEnvSettings loaded = DotEnvParser.Parse(reader, StringComparer.Ordinal); settings.Merge(loaded); From c793dabbb15016111a52e61198c39ccde3a9e553 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Mon, 10 Aug 2026 20:54:54 +0200 Subject: [PATCH 18/32] Removed prompt for env file loading --- Source/BookGen/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index 398b0f9d..eb78ca91 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -52,7 +52,6 @@ { try { - logger.LogDebug("Loading default env-file '{defaultEnvFile}'.", defaultEnvFile); using StreamReader reader = File.OpenText(defaultEnvFile); DotEnvSettings loaded = DotEnvParser.Parse(reader, StringComparer.Ordinal); settings.Merge(loaded); From 2d7783ce0c5a8a59d73f325dfdcb38a6eb85f42a Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Tue, 18 Aug 2026 16:35:14 +0200 Subject: [PATCH 19/32] Dependency Update --- Directory.Packages.props | 16 ++-- Source/BookGen.Cli/packages.lock.json | 6 +- Source/BookGen.Lib/packages.lock.json | 62 ++++++------- .../BookGen.Shell.Shared/packages.lock.json | 6 +- Source/BookGen.Shellprog/packages.lock.json | 20 ++-- Source/BookGen/packages.lock.json | 92 +++++++++---------- 6 files changed, 101 insertions(+), 101 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5927044e..40d04857 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,24 +12,24 @@ - - - - - + + + + + - + - - + + diff --git a/Source/BookGen.Cli/packages.lock.json b/Source/BookGen.Cli/packages.lock.json index 9a7eb3f9..9d5d8d69 100644 --- a/Source/BookGen.Cli/packages.lock.json +++ b/Source/BookGen.Cli/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.15.0, )", - "resolved": "4.15.0", - "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "bookgen.vfs": { "type": "Project" diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index cfdf6eb5..233c75ce 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -50,9 +50,9 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.15.0, )", - "resolved": "4.15.0", - "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "SkiaSharp": { "type": "Direct", @@ -78,26 +78,26 @@ }, "Svg.Skia": { "type": "Direct", - "requested": "[5.2.0, )", - "resolved": "5.2.0", - "contentHash": "AABURGBPTwWNFPnETLgjteTqcGfoNMJu+z1LNcNc8R280oVRMHgCf8dwGkIDj9rDD6A4Xt5w/LLmcZh0Iv7tTw==", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "es8F7MSeJJgc30Dmi0ZsrPpGQmLTQuS3DCvHcJ9wYlNAt3oHEoEzg5ugX3bwMPFejrPNLNjPpr1VOiMrpFyvIA==", "dependencies": { "HarfBuzzSharp": "14.2.0", "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", "SkiaSharp": "4.148.0", - "Svg.Animation": "5.2.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0", - "Svg.SceneGraph": "5.2.0" + "Svg.Animation": "5.2.1", + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1", + "Svg.SceneGraph": "5.2.1" } }, "System.ServiceModel.Syndication": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "StArv4DSO8I8QjDo1n+FzKeaq6UTNKvgkHHFaAt8ueexUWJZSEZ5/EasqwKt7413n3carQjaFzSULDI7sxwg0A==" + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "FEtlmar1uSt6u+qmefCO77alkcf4/IoK0qu0bmZ7GfL3vWDAXmBZUxM5uaJvoGoqGDOiKec7s9Qg9N1aH8dCrw==" }, "YamlDotNet": { "type": "Direct", @@ -182,8 +182,8 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "CNCbWDhCjuLTcbqsV2207BzCEr9+fWZ7veeS6/qZZp0h/GUzA0+aMGOEd2g+F1qllAIitvmdrNqiTOijQR/2+g==" + "resolved": "5.2.1", + "contentHash": "mUPSMxfW+vjtwDPrbhK3ILQg8D7aud7JO7p2Y5cqPmKf+s2erTyfEWohlVHCjSSDPvpT5L4aKLqnW02JCfGvag==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", @@ -197,40 +197,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "eUGhVM4dl+FQdsDM7N3zzKM1gYrNRfaCytNdD5VPGxooGc05GZlzoNVI8dwhh5jcYuCO2hxRPzsspyxfZdAgJA==", + "resolved": "5.2.1", + "contentHash": "/WKtsXMKAGIoiNzETQvQQtwoO3V2gEonkxqwrEdiGPSXuTG9Q0c6K34P06HhVXnLaZjjvxja+vOIKVSUucXOug==", "dependencies": { - "ShimSkiaSharp": "5.2.0", + "ShimSkiaSharp": "5.2.1", "SkiaSharp": "4.148.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0" + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "Up4hK0GphasgVzPOB5JT7Md3FP1dHaQK6AkBD7XH0uBWvu8d/DVVXp5qjWvpmBayIYWF4FAHnb2b/OPl+U2UkA==", + "resolved": "5.2.1", + "contentHash": "7qAG1I0FGlMwOJk0Js41TmvRJ0eLJPpz8Edd8DOzsotLikUhYGDZZlwXV+6sKvsApfIZEfarH0KwI38T9OHsmw==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "G3ZXXnlo6eD+oU/e+a33oVkf27a3oYu+djAwIEt4BU8w4S9t5UWsjcrR3bsq6SdO9r154+KiflpW/eznmGCtZQ==", + "resolved": "5.2.1", + "contentHash": "0+fG1BFw3QoXj+4sxpaXpLDZXz+Snb289epyP3HKjEjUUoWVsErMZb6teo4dVj8KElRJIeXjX6jD1C0B1vLRXw==", "dependencies": { - "ShimSkiaSharp": "5.2.0", - "Svg.Custom": "5.2.0" + "ShimSkiaSharp": "5.2.1", + "Svg.Custom": "5.2.1" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "Yr4T6Nzwi+Hd2sKOKik1qbCteLNnuapzvKxMblIuJ2SHwEbMb+2FjFwA6FenD7CRWhlCEiIwTvAe3/IJswTYoQ==", + "resolved": "5.2.1", + "contentHash": "vm2QxbPv5MraL7T2+1dh0m8lGQqtW37RK4iOHvBsJQHQzcuDlCR+uX/Oiun77kS2US2OC23zfaIFEYWGwocXLw==", "dependencies": { - "ShimSkiaSharp": "5.2.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0" + "ShimSkiaSharp": "5.2.1", + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1" } }, "bookgen.shell.shared": { diff --git a/Source/BookGen.Shell.Shared/packages.lock.json b/Source/BookGen.Shell.Shared/packages.lock.json index 77d5e1f3..6795bd8b 100644 --- a/Source/BookGen.Shell.Shared/packages.lock.json +++ b/Source/BookGen.Shell.Shared/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.15.0, )", - "resolved": "4.15.0", - "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "Spectre.Console": { "type": "Direct", diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index e31f7120..47e82957 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -4,24 +4,24 @@ "net11.0": { "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "MwlfMwxXewuq+X85a31yYx6emRYIyJOboVXZtRH3Wvp/hZZWmlg1pu/tI+XJUm4ltoCNMzpcFF6zJcmbnbnuGA==" + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "IgS1YfEd2i0p/zY8ytcW1zaFR3ivC0M4Ce3JnpZoKpzK9MogNkHb9BZPqYtq0mDuCe2ouirh6058glgPxhwD8g==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "lUAZ402asMsLYe483nXXQIphklpTNDP6LmhmrwB8V3gBTjXeUp40N6h/Ap5QOiGHqsAiooznL4lBETGSncYsxQ==", + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "VoYsG/1Gt7kpSeno7eUrnLKfusYZf9L8qSnYtMNh1ClwXKIfKU6+g+Lq23VeanJP2ieQyPBH0xtSzay8J7r9lQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.6.26359.118" + "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.7.26381.103" } }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.15.0, )", - "resolved": "4.15.0", - "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "Spectre.Console": { "type": "Direct", diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 1f0b61a9..7010d7b8 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -20,36 +20,36 @@ }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "/0XbQexXoSNQ7YfwhiI8XJvK05+rHTVxJZzuMCFc/l8owJ7aPwwE6cYwCDs1LvsklOWyfGm1LaAUHQbhYXNNcw==" + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "K3JeRtSGa85pWo+IrTHwv5PZvA7mHitQxvC83CT9gvrsZIJj0BTV0tLfHRAn3lL/o4gw7nJ6u681QkWhKISzlg==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "MwlfMwxXewuq+X85a31yYx6emRYIyJOboVXZtRH3Wvp/hZZWmlg1pu/tI+XJUm4ltoCNMzpcFF6zJcmbnbnuGA==" + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "IgS1YfEd2i0p/zY8ytcW1zaFR3ivC0M4Ce3JnpZoKpzK9MogNkHb9BZPqYtq0mDuCe2ouirh6058glgPxhwD8g==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "lUAZ402asMsLYe483nXXQIphklpTNDP6LmhmrwB8V3gBTjXeUp40N6h/Ap5QOiGHqsAiooznL4lBETGSncYsxQ==", + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "VoYsG/1Gt7kpSeno7eUrnLKfusYZf9L8qSnYtMNh1ClwXKIfKU6+g+Lq23VeanJP2ieQyPBH0xtSzay8J7r9lQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.6.26359.118" + "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.7.26381.103" } }, "Microsoft.IO.RecyclableMemoryStream": { "type": "Direct", - "requested": "[4.0.0-preview, )", - "resolved": "4.0.0-preview", - "contentHash": "ueAhj4Qj4HgGyS0+efG2IjvqYHPWkayE+GWUnseWe6aJQagoxHWr2ljabmXd9k4QpSH2jpoTd7TTzgUsc2selA==" + "requested": "[4.0.1-preview, )", + "resolved": "4.0.1-preview", + "contentHash": "V3alvRGpiW7bodGtgz+uOnR2K5mDQGCmqjR/vc76x9giVC1l6ucbEbOFbtojHYJfZpQ24gXIMDZDi0KNUPkQOg==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.15.0, )", - "resolved": "4.15.0", - "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "Spectre.Console": { "type": "Direct", @@ -165,8 +165,8 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "CNCbWDhCjuLTcbqsV2207BzCEr9+fWZ7veeS6/qZZp0h/GUzA0+aMGOEd2g+F1qllAIitvmdrNqiTOijQR/2+g==" + "resolved": "5.2.1", + "contentHash": "mUPSMxfW+vjtwDPrbhK3ILQg8D7aud7JO7p2Y5cqPmKf+s2erTyfEWohlVHCjSSDPvpT5L4aKLqnW02JCfGvag==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", @@ -180,40 +180,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "eUGhVM4dl+FQdsDM7N3zzKM1gYrNRfaCytNdD5VPGxooGc05GZlzoNVI8dwhh5jcYuCO2hxRPzsspyxfZdAgJA==", + "resolved": "5.2.1", + "contentHash": "/WKtsXMKAGIoiNzETQvQQtwoO3V2gEonkxqwrEdiGPSXuTG9Q0c6K34P06HhVXnLaZjjvxja+vOIKVSUucXOug==", "dependencies": { - "ShimSkiaSharp": "5.2.0", + "ShimSkiaSharp": "5.2.1", "SkiaSharp": "4.148.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0" + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "Up4hK0GphasgVzPOB5JT7Md3FP1dHaQK6AkBD7XH0uBWvu8d/DVVXp5qjWvpmBayIYWF4FAHnb2b/OPl+U2UkA==", + "resolved": "5.2.1", + "contentHash": "7qAG1I0FGlMwOJk0Js41TmvRJ0eLJPpz8Edd8DOzsotLikUhYGDZZlwXV+6sKvsApfIZEfarH0KwI38T9OHsmw==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "G3ZXXnlo6eD+oU/e+a33oVkf27a3oYu+djAwIEt4BU8w4S9t5UWsjcrR3bsq6SdO9r154+KiflpW/eznmGCtZQ==", + "resolved": "5.2.1", + "contentHash": "0+fG1BFw3QoXj+4sxpaXpLDZXz+Snb289epyP3HKjEjUUoWVsErMZb6teo4dVj8KElRJIeXjX6jD1C0B1vLRXw==", "dependencies": { - "ShimSkiaSharp": "5.2.0", - "Svg.Custom": "5.2.0" + "ShimSkiaSharp": "5.2.1", + "Svg.Custom": "5.2.1" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "Yr4T6Nzwi+Hd2sKOKik1qbCteLNnuapzvKxMblIuJ2SHwEbMb+2FjFwA6FenD7CRWhlCEiIwTvAe3/IJswTYoQ==", + "resolved": "5.2.1", + "contentHash": "vm2QxbPv5MraL7T2+1dh0m8lGQqtW37RK4iOHvBsJQHQzcuDlCR+uX/Oiun77kS2US2OC23zfaIFEYWGwocXLw==", "dependencies": { - "ShimSkiaSharp": "5.2.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0" + "ShimSkiaSharp": "5.2.1", + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1" } }, "System.IO.Packaging": { @@ -247,8 +247,8 @@ "SkiaSharp": "[4.151.1, )", "SkiaSharp.NativeAssets.Linux": "[4.151.1, )", "SkiaSharp.NativeAssets.Win32": "[4.151.1, )", - "Svg.Skia": "[5.2.0, )", - "System.ServiceModel.Syndication": "[11.0.0-preview.6.26359.118, )", + "Svg.Skia": "[5.2.1, )", + "System.ServiceModel.Syndication": "[11.0.0-preview.7.26381.103, )", "YamlDotNet": "[18.1.0, )" } }, @@ -326,26 +326,26 @@ }, "Svg.Skia": { "type": "CentralTransitive", - "requested": "[5.2.0, )", - "resolved": "5.2.0", - "contentHash": "AABURGBPTwWNFPnETLgjteTqcGfoNMJu+z1LNcNc8R280oVRMHgCf8dwGkIDj9rDD6A4Xt5w/LLmcZh0Iv7tTw==", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "es8F7MSeJJgc30Dmi0ZsrPpGQmLTQuS3DCvHcJ9wYlNAt3oHEoEzg5ugX3bwMPFejrPNLNjPpr1VOiMrpFyvIA==", "dependencies": { "HarfBuzzSharp": "14.2.0", "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", "SkiaSharp": "4.148.0", - "Svg.Animation": "5.2.0", - "Svg.Custom": "5.2.0", - "Svg.Model": "5.2.0", - "Svg.SceneGraph": "5.2.0" + "Svg.Animation": "5.2.1", + "Svg.Custom": "5.2.1", + "Svg.Model": "5.2.1", + "Svg.SceneGraph": "5.2.1" } }, "System.ServiceModel.Syndication": { "type": "CentralTransitive", - "requested": "[11.0.0-preview.6.26359.118, )", - "resolved": "11.0.0-preview.6.26359.118", - "contentHash": "StArv4DSO8I8QjDo1n+FzKeaq6UTNKvgkHHFaAt8ueexUWJZSEZ5/EasqwKt7413n3carQjaFzSULDI7sxwg0A==" + "requested": "[11.0.0-preview.7.26381.103, )", + "resolved": "11.0.0-preview.7.26381.103", + "contentHash": "FEtlmar1uSt6u+qmefCO77alkcf4/IoK0qu0bmZ7GfL3vWDAXmBZUxM5uaJvoGoqGDOiKec7s9Qg9N1aH8dCrw==" }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", From 92592f0709002919d6f69933981393137a2c5f50 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Wed, 19 Aug 2026 12:39:25 +0200 Subject: [PATCH 20/32] Rendering fix for nomnoml --- .../RenderInterop/JavascriptEngine.cs | 20 ++++++++++++++++++- .../Markdown/RenderInterop/RenderInterop.cs | 6 +++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs index 675fff9f..1208d32d 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Globalization; + using BookGen.Vfs; using Microsoft.ClearScript; @@ -29,10 +31,26 @@ public void Dispose() _disposed = true; } - public dynamic Script => _engine.Script; + public void SetVariable(string name, string value) + { + ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); + string script = $$""" + if ({{name}} === 'undefined') { + var {{name}} = `{{value}}`; + } + else { + {{name}} = `{{value}}`; + } + """; + _engine.Execute(script); + } + + public void SetVariable(string name, double value) + => SetVariable(name, value.ToString(CultureInfo.InvariantCulture)); public void Execute(string code) { + ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); _engine.Execute(code); diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs index 925df4dd..6dddeab7 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs @@ -192,8 +192,7 @@ public string PrismSyntaxHighlight(string code, string language) { ObjectDisposedException.ThrowIf(_disposed, nameof(RenderInterop)); LoadScriptIfNotLoaded(BundledAssets.PrismJs); - - _javascriptEngine.Script.code = code; + _javascriptEngine.SetVariable("code", code); return _javascriptEngine.ExecuteAndGetResult($"Prism.highlight(code, Prism.languages.{language}, '{language}');"); } @@ -206,7 +205,8 @@ public ImageResult RenderNomnoml(string nomnomlCode) LoadScriptIfNotLoaded(BundledAssets.GraphreJs); LoadScriptIfNotLoaded(BundledAssets.NomnomlJs); - _javascriptEngine.Script.nomnomlCode = nomnomlCode; + _javascriptEngine.SetVariable("nomnomlCode", nomnomlCode); + string svg = _javascriptEngine.ExecuteAndGetResult("nomnoml.renderSvg(nomnomlCode)"); return EncodeSvg(svg, ImageConfig); } From 570090e514b5ac0c7d072bbcca21fb9a0dcdaec5 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Tue, 25 Aug 2026 17:23:10 +0200 Subject: [PATCH 21/32] JS engine fix --- .../Rendering/Markdown/RenderInterop/JavascriptEngine.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs index 1208d32d..0ab28a16 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs @@ -34,12 +34,13 @@ public void Dispose() public void SetVariable(string name, string value) { ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); + string encoded = System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(value); string script = $$""" if ({{name}} === 'undefined') { - var {{name}} = `{{value}}`; + var {{name}} = "{{encoded}}"; } else { - {{name}} = `{{value}}`; + {{name}} = "{{encoded}}"; } """; _engine.Execute(script); From 319cd225e9943322da51806b01caea05b0d5a741 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Tue, 25 Aug 2026 18:30:36 +0200 Subject: [PATCH 22/32] Nomnoml: Fix line ending issues --- .../Markdown/RenderInterop/JavascriptEngine.cs | 12 +----------- .../Markdown/RenderInterop/RenderInterop.cs | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs index 0ab28a16..097113df 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs @@ -33,17 +33,7 @@ public void Dispose() public void SetVariable(string name, string value) { - ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); - string encoded = System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(value); - string script = $$""" - if ({{name}} === 'undefined') { - var {{name}} = "{{encoded}}"; - } - else { - {{name}} = "{{encoded}}"; - } - """; - _engine.Execute(script); + _engine.Script[name] = value; } public void SetVariable(string name, double value) diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs index 6dddeab7..c0fe6057 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/RenderInterop.cs @@ -205,7 +205,7 @@ public ImageResult RenderNomnoml(string nomnomlCode) LoadScriptIfNotLoaded(BundledAssets.GraphreJs); LoadScriptIfNotLoaded(BundledAssets.NomnomlJs); - _javascriptEngine.SetVariable("nomnomlCode", nomnomlCode); + _javascriptEngine.SetVariable("nomnomlCode", nomnomlCode.Replace("\r\n", "\n")); string svg = _javascriptEngine.ExecuteAndGetResult("nomnoml.renderSvg(nomnomlCode)"); return EncodeSvg(svg, ImageConfig); From efa343204a8bb3674c4de6d12baff65da2d62f4b Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Wed, 26 Aug 2026 18:27:16 +0200 Subject: [PATCH 23/32] Improved crash dump generation --- Source/BookGen.Cli/CrashDumpFactory.cs | 53 ---- .../CrashReporting/CrashDumpGenerator.cs | 243 ++++++++++++++++++ Source/BookGen.Cli/CrashReporting/LogEntry.cs | 9 + .../RenderInterop/JavascriptEngine.cs | 9 +- .../Loging/ConsoleLogProvider.cs | 13 +- .../Loging/DebugLoggerProvider.cs | 52 ++++ .../Infrastructure/Loging/RingBuffer.cs | 50 ++++ Source/BookGen/Program.cs | 15 +- 8 files changed, 374 insertions(+), 70 deletions(-) delete mode 100644 Source/BookGen.Cli/CrashDumpFactory.cs create mode 100644 Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs create mode 100644 Source/BookGen.Cli/CrashReporting/LogEntry.cs create mode 100644 Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs create mode 100644 Source/BookGen/Infrastructure/Loging/RingBuffer.cs diff --git a/Source/BookGen.Cli/CrashDumpFactory.cs b/Source/BookGen.Cli/CrashDumpFactory.cs deleted file mode 100644 index ea5d7130..00000000 --- a/Source/BookGen.Cli/CrashDumpFactory.cs +++ /dev/null @@ -1,53 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.Text; - -namespace BookGen.Cli; - -public static class CrashDumpFactory -{ - public static void TryCreateCrashDump(Exception ex) - { - var fileName = $"bookgen_crash_{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.txt"; - string dir = Path.Combine(Environment.CurrentDirectory, fileName); - StringBuilder content = new(); - - content - .AppendLine("BookGen Crash Dump") - .Append('-', 80).AppendLine() - .AppendLine("Environment:") - .AppendLine($"Command line: {string.Join(' ', Environment.GetCommandLineArgs())}") - .AppendLine($"Crash reason: {ex.Message}") - .Append('-', 80).AppendLine() - .AppendLine("Source:") - .AppendLine(ex.Source) - .Append('-', 80).AppendLine() - .AppendLine("Stack trace:") - .AppendLine(ex.StackTrace); - - - if (ex.InnerException != null) - { - content - .Append('-', 80).AppendLine() - .AppendLine($"Inner exception: {ex.InnerException.Message}") - .Append('-', 80).AppendLine() - .AppendLine(ex.InnerException.StackTrace); - } - - -#pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception - try - { - File.WriteAllText(fileName, content.ToString()); - } - catch (Exception) - { - //At this point we don't care - } -#pragma warning restore RCS1075 - } -} diff --git a/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs b/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs new file mode 100644 index 00000000..3a1b923a --- /dev/null +++ b/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs @@ -0,0 +1,243 @@ +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Web; + +namespace BookGen.Cli.CrashReporting; + +public sealed class CrashDumpGenerator +{ + private sealed class CrashHtmlBuilder + { + private readonly JsonSerializerOptions _options; + private readonly Exception _exception; + + private string _title = string.Empty; + private string _programArgs = string.Empty; + private string _osInformation = string.Empty; + private string _applicationVersion = string.Empty; + private string _applicationArchitecture = string.Empty; + private Version _runtime = Version.Parse("0.0.0.0"); + private string _logEntries = string.Empty; + private string _css; + + public CrashHtmlBuilder(Exception exception) + { + _options = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + }; + _exception = exception; + _css = """ + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #FCFCFC; + max-width: 1400px; + margin: 0 auto; + } + pre, code { + font-family: 'Cascadia Code', monospace; + } + details { + margin-left: 15px; + } + .exception-type { + font-weight: bold; + color: #E02A24; + } + .version { + color: #53735D; + } + .reason { + font-weight: bold; + color: #C08CF3; + } + """; + } + + public CrashHtmlBuilder Title(string title) + { + _title = title; + return this; + } + + public CrashHtmlBuilder ProgramArgs(string[] args) + { + _programArgs = string.Join(' ', args.Select(x => x.Contains(' ') ? $"\"{x}\"" : x)); + return this; + } + + public CrashHtmlBuilder WithOperatingSystem(string osInformation) + { + _osInformation = osInformation; + return this; + } + + public CrashHtmlBuilder WithApplicationVersion(string version) + { + _applicationVersion = version; + return this; + } + + public CrashHtmlBuilder WithApplicationArchitecture(string architecture) + { + _applicationArchitecture = architecture; + return this; + } + + public CrashHtmlBuilder WithRuntime(Version version) + { + _runtime = version; + return this; + } + + public CrashHtmlBuilder WithLogEntries(IEnumerable? logEntries) + { + if (logEntries != null) + { + _logEntries = JsonSerializer.Serialize(logEntries.ToArray(), _options); + } + return this; + } + + public CrashHtmlBuilder Css(string cssContent) + { + _css = cssContent; + return this; + } + + public override string ToString() + { + StringBuilder html = new(); + html.AppendLine($""" + + + + + {_title} + + + +

{_title}

+
+

Application Arguments: {HttpUtility.HtmlEncode(_programArgs)}

+

Application Version: {HttpUtility.HtmlEncode(_applicationVersion)}

+

Application Architecture: {HttpUtility.HtmlEncode(_applicationArchitecture)}

+

Operating System: {HttpUtility.HtmlEncode(_osInformation)}

+

Runtime Version: {HttpUtility.HtmlEncode(_runtime.ToString())}

+

Crash Reason: {HttpUtility.HtmlEncode(_exception.Message)}

+ """); + + if (_logEntries != string.Empty) + { + html.AppendLine("
"); + html.AppendLine("
"); + html.AppendLine($"

Recent Log Entries

"); + html.AppendLine($"
{HttpUtility.HtmlEncode(_logEntries)}
"); + html.AppendLine("
"); + } + + html + .AppendLine("
") + .AppendLine("

Exception details

"); + + WalkException(_exception, html); + html.AppendLine("
"); + + html.AppendLine(""" + + + """); + + return html.ToString(); + } + + private static void WalkException(Exception exception, StringBuilder html) + { + html + .AppendLine("
") + .AppendLine($"{HttpUtility.HtmlEncode(exception.GetType().Name)} - {HttpUtility.HtmlEncode(exception.Message)}") + .AppendLine("

Stack trace:

") + .AppendLine($"
{HttpUtility.HtmlEncode(exception.StackTrace)}
"); + + Exception? inner = exception?.InnerException; + int iopen = 0; + do + { + if (inner == null) + break; + + html + .Append("
") + .Append("

Inner Exception:

") + .AppendLine("
") + .AppendLine($"{HttpUtility.HtmlEncode(inner.GetType().Name)} - {HttpUtility.HtmlEncode(inner.Message)}") + .AppendLine("

Stack trace:

") + .AppendLine($"
{HttpUtility.HtmlEncode(inner.StackTrace)}
"); + + iopen++; + inner = inner.InnerException; + } + while (inner != null); + + for (int i = 0; i < iopen; i++) + { + html.AppendLine("
"); + } + + html.AppendLine("
"); + + } + } + + private readonly string _applicationName; + private readonly string _targetDirectory; + + public CrashDumpGenerator(string applicationName, string? targetDirectory = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(applicationName); + _applicationName = applicationName; + if (targetDirectory == null) + { + _targetDirectory = Environment.CurrentDirectory; + } + else + { + if (!Path.Exists(_targetDirectory)) + throw new ArgumentException($"The target directory '{_targetDirectory}' does not exist.", nameof(targetDirectory)); + + _targetDirectory = targetDirectory; + } + } + + public void GenerateCrashDump(Exception ex, IEnumerable? logEntries = null) + { + var fileName = Path.Combine(_targetDirectory, $"{_applicationName}_crash_{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.html"); + + CrashHtmlBuilder builder = new(ex); + var html = builder + .Title($"Crash Report - {_applicationName}") + .ProgramArgs(Environment.GetCommandLineArgs()) + .WithOperatingSystem($"{RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture}") + .WithApplicationVersion(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown") + .WithApplicationArchitecture(RuntimeInformation.ProcessArchitecture.ToString()) + .WithLogEntries(logEntries) + .WithRuntime(Environment.Version) + .ToString(); + +#pragma warning disable RCS1075 + try + { + File.WriteAllText(fileName, html); + } + catch (Exception) + { + // If writing the crash dump fails, we can't do much about it. + } +#pragma warning restore RCS1075 + } +} diff --git a/Source/BookGen.Cli/CrashReporting/LogEntry.cs b/Source/BookGen.Cli/CrashReporting/LogEntry.cs new file mode 100644 index 00000000..bc05a43a --- /dev/null +++ b/Source/BookGen.Cli/CrashReporting/LogEntry.cs @@ -0,0 +1,9 @@ +namespace BookGen.Cli.CrashReporting; + +public sealed record class LogEntry +{ + public required DateTimeOffset Timestamp { get; init; } + public required string LogLevel { get; init; } + public required string Message { get; init; } + public required string Exception { get; init; } +} diff --git a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs index 097113df..0922c23f 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/RenderInterop/JavascriptEngine.cs @@ -31,26 +31,21 @@ public void Dispose() _disposed = true; } - public void SetVariable(string name, string value) - { - _engine.Script[name] = value; - } + public void SetVariable(string name, string value) + => _engine.Script[name] = value; public void SetVariable(string name, double value) => SetVariable(name, value.ToString(CultureInfo.InvariantCulture)); public void Execute(string code) { - ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); - _engine.Execute(code); } public object Evaluate(string code) { ObjectDisposedException.ThrowIf(_disposed, nameof(_engine)); - return _engine.Evaluate(code); } diff --git a/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs b/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs index f9853101..765c9660 100644 --- a/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs +++ b/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs @@ -75,12 +75,12 @@ static string LevelToString(LogLevel logLevel) { return logLevel switch { - LogLevel.Trace => $"{DateTime.Now.ToShortTimeString()} [grey]TRACE[/]: ", - LogLevel.Debug => $"{DateTime.Now.ToShortTimeString()} [blue]DEBUG[/]: ", - LogLevel.Information => $"{DateTime.Now.ToShortTimeString()} [green]INFO[/]: ", - LogLevel.Warning => $"{DateTime.Now.ToShortTimeString()} [yellow]WARN[/]: ", - LogLevel.Error => $"{DateTime.Now.ToShortTimeString()} [red]ERROR[/]: ", - LogLevel.Critical => $"{DateTime.Now.ToShortTimeString()} [red bold]CRITICAL[/]: ", + LogLevel.Trace => $"{DateTime.Now:t} [grey]TRACE[/]: ", + LogLevel.Debug => $"{DateTime.Now:t} [blue]DEBUG[/]: ", + LogLevel.Information => $"{DateTime.Now:t} [green]INFO[/]: ", + LogLevel.Warning => $"{DateTime.Now:t} [yellow]WARN[/]: ", + LogLevel.Error => $"{DateTime.Now:t} [red]ERROR[/]: ", + LogLevel.Critical => $"{DateTime.Now:t} [red bold]CRITICAL[/]: ", LogLevel.None => string.Empty, _ => throw new UnreachableException(), }; @@ -88,7 +88,6 @@ static string LevelToString(LogLevel logLevel) var line = $"{LevelToString(logLevel)} {formatter(state, exception).EscapeMarkup()}"; AnsiConsole.MarkupLine(line); - } } } diff --git a/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs b/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs new file mode 100644 index 00000000..8ed1b24d --- /dev/null +++ b/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs @@ -0,0 +1,52 @@ +using BookGen.Cli.CrashReporting; + +using Microsoft.Extensions.Logging; + +namespace BookGen.Infrastructure.Loging; + +internal sealed class DebugLoggerProvider : ILoggerProvider +{ + private readonly RingBuffer _logBuffer; + private readonly DebugLogger _logger; + + public DebugLoggerProvider(int bufferSize) + { + _logBuffer = new RingBuffer(bufferSize); + _logger = new DebugLogger(_logBuffer); + } + + public ILogger CreateLogger(string categoryName) + => _logger; + + public void Dispose() + { + // No resources to dispose + } + + internal IEnumerable? GetEntries() + { + return _logBuffer.ToArray(); + } + + internal sealed class DebugLogger(RingBuffer logBuffer) : ILogger + { + private readonly RingBuffer _logBuffer = logBuffer; + + public IDisposable? BeginScope(TState state) where TState : notnull + => null; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _logBuffer.Add(new LogEntry + { + Timestamp = DateTimeOffset.UtcNow, + LogLevel = logLevel.ToString(), + Exception = exception?.ToString() ?? string.Empty, + Message = formatter(state, exception) + }); + } + } +} diff --git a/Source/BookGen/Infrastructure/Loging/RingBuffer.cs b/Source/BookGen/Infrastructure/Loging/RingBuffer.cs new file mode 100644 index 00000000..e826281d --- /dev/null +++ b/Source/BookGen/Infrastructure/Loging/RingBuffer.cs @@ -0,0 +1,50 @@ +namespace BookGen.Infrastructure.Loging; + +internal sealed class RingBuffer +{ + private readonly T[] _buffer; + private readonly Lock _lock; + private int _head; + private int _tail; + private int _count; + + public RingBuffer(int capacity) + { + ArgumentOutOfRangeException.ThrowIfLessThan(capacity, 0); + _lock = new Lock(); + _buffer = new T[capacity]; + _head = 0; + _tail = 0; + _count = 0; + } + + public void Add(T item) + { + lock (_lock) + { + _buffer[_tail] = item; + _tail = (_tail + 1) % _buffer.Length; + if (_count < _buffer.Length) + { + _count++; + } + else + { + _head = (_head + 1) % _buffer.Length; // Overwrite the oldest item + } + } + } + + public T[] ToArray() + { + lock (_lock) + { + var array = new T[_count]; + for (int i = 0; i < _count; i++) + { + array[i] = _buffer[(_head + i) % _buffer.Length]; + } + return array; + } + } +} diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index eb78ca91..cc6b1985 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -7,6 +7,7 @@ using BookGen; using BookGen.Cli; +using BookGen.Cli.CrashReporting; using BookGen.Cli.Dotenv; using BookGen.Commands; using BookGen.GlobalOptionParsers; @@ -23,11 +24,14 @@ ProgramInfo info = new(); +var debugLogProvider = new DebugLoggerProvider(30); +var crashDumpGenerator = new CrashDumpGenerator("BookGen"); + using ILoggerFactory factory = LoggerFactory .Create(builder => { builder.ClearProviders(); - builder.AddFilter(level => level >= info.LogLevel); + builder.AddProvider(debugLogProvider); if (info.JsonLogging) { builder.AddJsonConsole(); @@ -35,10 +39,12 @@ else { builder.AddProvider(new ConsoleLogProvider()); + builder.AddFilter(level => level >= info.LogLevel); } if (info.LogToFile) { builder.AddProvider(new FileLoggerProvider()); + builder.AddFilter(level => level >= info.LogLevel); } }); @@ -144,8 +150,11 @@ Task OnBeforeRun(ArgumentsBase @base, IReadOnlyList list) void OnException(Exception exception) { - logger.LogCritical(exception.Message); - CrashDumpFactory.TryCreateCrashDump(exception); + logger.LogCritical(exception, exception.Message); + + crashDumpGenerator + .GenerateCrashDump(exception, debugLogProvider.GetEntries()); + #if DEBUG AnsiConsole.WriteException(exception); #endif From 7d76e55dbb4a8c2a7c155f51c693055ff46ad8dc Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 29 Aug 2026 17:03:35 +0200 Subject: [PATCH 24/32] Updated packages --- Directory.Packages.props | 10 +-- Source/BookGen.Cli/packages.lock.json | 6 +- Source/BookGen.Lib/packages.lock.json | 70 ++++++++--------- .../BookGen.Shell.Shared/packages.lock.json | 6 +- Source/BookGen.Shellprog/packages.lock.json | 6 +- Source/BookGen/packages.lock.json | 76 +++++++++---------- 6 files changed, 87 insertions(+), 87 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 40d04857..e8642967 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - + @@ -20,15 +20,15 @@ - - - + + + - + diff --git a/Source/BookGen.Cli/packages.lock.json b/Source/BookGen.Cli/packages.lock.json index 9d5d8d69..a10abc1a 100644 --- a/Source/BookGen.Cli/packages.lock.json +++ b/Source/BookGen.Cli/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.1, )", - "resolved": "4.16.1", - "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "+J5kR/naCd62IwY2BG9P0QoHnJM8liBUFFAk6jga1t9s61JC3M+ofb3AY3wc73/pdROyKFCURqctQh8WRCIT/w==" }, "bookgen.vfs": { "type": "Project" diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index 233c75ce..7be7862a 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "AngleSharp": { "type": "Direct", - "requested": "[1.7.1, )", - "resolved": "1.7.1", - "contentHash": "QBblGpRB3SYD66Mls7fHyl1mWkP93n7ErWDWHc1vd9sPHhdlTkmUoY3CjgfjcWNxGF9m7xjNyztRfErXqoBKOA==" + "requested": "[1.7.2, )", + "resolved": "1.7.2", + "contentHash": "r1rb5Qo/0KPzmP0nbSiXPDVfh4Ctu0B+y1RyyUq73g4sgAmEW7q10RDyTq2ZsILwtO9O2LAvMrUles7eD4zz1g==" }, "Markdig": { "type": "Direct", @@ -41,18 +41,18 @@ }, "PreMailer.Net": { "type": "Direct", - "requested": "[2.7.3, )", - "resolved": "2.7.3", - "contentHash": "p6LB7Qu4ZsPmjiFtWmytiERhopuGxq3Ql/SVuOZueGzfrDNvvSi8sGaDVHVvJt0S+RCHU422TWLu2aMXDE9ejg==", + "requested": "[2.7.4, )", + "resolved": "2.7.4", + "contentHash": "SrmoWjabHeK4a5mrolGzLMi+z5DMTGzK/Jce0ORs/lGDzvNggDNJF260v4gj5k9aU4LRMp8qILCMCI71t+oa0g==", "dependencies": { - "AngleSharp": "1.5.1" + "AngleSharp": "1.7.2" } }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.1, )", - "resolved": "4.16.1", - "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "+J5kR/naCd62IwY2BG9P0QoHnJM8liBUFFAk6jga1t9s61JC3M+ofb3AY3wc73/pdROyKFCURqctQh8WRCIT/w==" }, "SkiaSharp": { "type": "Direct", @@ -78,19 +78,19 @@ }, "Svg.Skia": { "type": "Direct", - "requested": "[5.2.1, )", - "resolved": "5.2.1", - "contentHash": "es8F7MSeJJgc30Dmi0ZsrPpGQmLTQuS3DCvHcJ9wYlNAt3oHEoEzg5ugX3bwMPFejrPNLNjPpr1VOiMrpFyvIA==", + "requested": "[5.2.3, )", + "resolved": "5.2.3", + "contentHash": "slGSy5Aje/30Ou2a5aezbGGWLZspBIBYlEzqgtaIVex4FUYAR3mxaYrvMZj1tHy+ESDFCGY7+2FjF6UzqGUZew==", "dependencies": { "HarfBuzzSharp": "14.2.0", "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", "SkiaSharp": "4.148.0", - "Svg.Animation": "5.2.1", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1", - "Svg.SceneGraph": "5.2.1" + "Svg.Animation": "5.2.3", + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3", + "Svg.SceneGraph": "5.2.3" } }, "System.ServiceModel.Syndication": { @@ -182,8 +182,8 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "mUPSMxfW+vjtwDPrbhK3ILQg8D7aud7JO7p2Y5cqPmKf+s2erTyfEWohlVHCjSSDPvpT5L4aKLqnW02JCfGvag==" + "resolved": "5.2.3", + "contentHash": "BBmIvh70WduWEF8C9HeU12LmHkg6+h2SRwJzey/tmztRk4ttAxhyu9eAD/jlsNGlqquYpoCQyyTx+QynavyfVA==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", @@ -197,40 +197,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "/WKtsXMKAGIoiNzETQvQQtwoO3V2gEonkxqwrEdiGPSXuTG9Q0c6K34P06HhVXnLaZjjvxja+vOIKVSUucXOug==", + "resolved": "5.2.3", + "contentHash": "mlbLFKzSVL2wMvDxnlMKLbdbbCbZBjDwSgmCZ7dHE4l0DtVUX5kHixngZlUP8AuBoikzqsXZt4+IXJhbeJjAdA==", "dependencies": { - "ShimSkiaSharp": "5.2.1", + "ShimSkiaSharp": "5.2.3", "SkiaSharp": "4.148.0", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1" + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "7qAG1I0FGlMwOJk0Js41TmvRJ0eLJPpz8Edd8DOzsotLikUhYGDZZlwXV+6sKvsApfIZEfarH0KwI38T9OHsmw==", + "resolved": "5.2.3", + "contentHash": "epicuKTb55l7Y6c3f9pdP0RkoifVieS9sWvA9HsppTfYyV7RFZg89+fUA9TTPcuccqi0GUGwiPEi+9b881zlDw==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "0+fG1BFw3QoXj+4sxpaXpLDZXz+Snb289epyP3HKjEjUUoWVsErMZb6teo4dVj8KElRJIeXjX6jD1C0B1vLRXw==", + "resolved": "5.2.3", + "contentHash": "iZInP4/48rIkpFxVJNMmxDDjIBaUcGOHrzancQ3cmneHXrtH4MYkm6bzggL9hCiS2dxfLhk3JCiERoGf8j0MlA==", "dependencies": { - "ShimSkiaSharp": "5.2.1", - "Svg.Custom": "5.2.1" + "ShimSkiaSharp": "5.2.3", + "Svg.Custom": "5.2.3" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "vm2QxbPv5MraL7T2+1dh0m8lGQqtW37RK4iOHvBsJQHQzcuDlCR+uX/Oiun77kS2US2OC23zfaIFEYWGwocXLw==", + "resolved": "5.2.3", + "contentHash": "t9HrB1n499YCvziq+sdapDjMvgY7iSzyldEDalWnoZCP3cncu92gEktBqzhUgo8A+r9mfow2TNf0u5jgV5FvlQ==", "dependencies": { - "ShimSkiaSharp": "5.2.1", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1" + "ShimSkiaSharp": "5.2.3", + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3" } }, "bookgen.shell.shared": { diff --git a/Source/BookGen.Shell.Shared/packages.lock.json b/Source/BookGen.Shell.Shared/packages.lock.json index 6795bd8b..4ecc2dd9 100644 --- a/Source/BookGen.Shell.Shared/packages.lock.json +++ b/Source/BookGen.Shell.Shared/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.1, )", - "resolved": "4.16.1", - "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "+J5kR/naCd62IwY2BG9P0QoHnJM8liBUFFAk6jga1t9s61JC3M+ofb3AY3wc73/pdROyKFCURqctQh8WRCIT/w==" }, "Spectre.Console": { "type": "Direct", diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index 47e82957..1be70daa 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -19,9 +19,9 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.1, )", - "resolved": "4.16.1", - "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "+J5kR/naCd62IwY2BG9P0QoHnJM8liBUFFAk6jga1t9s61JC3M+ofb3AY3wc73/pdROyKFCURqctQh8WRCIT/w==" }, "Spectre.Console": { "type": "Direct", diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 7010d7b8..a26fd10e 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -47,9 +47,9 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.1, )", - "resolved": "4.16.1", - "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "+J5kR/naCd62IwY2BG9P0QoHnJM8liBUFFAk6jga1t9s61JC3M+ofb3AY3wc73/pdROyKFCURqctQh8WRCIT/w==" }, "Spectre.Console": { "type": "Direct", @@ -165,8 +165,8 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "mUPSMxfW+vjtwDPrbhK3ILQg8D7aud7JO7p2Y5cqPmKf+s2erTyfEWohlVHCjSSDPvpT5L4aKLqnW02JCfGvag==" + "resolved": "5.2.3", + "contentHash": "BBmIvh70WduWEF8C9HeU12LmHkg6+h2SRwJzey/tmztRk4ttAxhyu9eAD/jlsNGlqquYpoCQyyTx+QynavyfVA==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", @@ -180,40 +180,40 @@ }, "Svg.Animation": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "/WKtsXMKAGIoiNzETQvQQtwoO3V2gEonkxqwrEdiGPSXuTG9Q0c6K34P06HhVXnLaZjjvxja+vOIKVSUucXOug==", + "resolved": "5.2.3", + "contentHash": "mlbLFKzSVL2wMvDxnlMKLbdbbCbZBjDwSgmCZ7dHE4l0DtVUX5kHixngZlUP8AuBoikzqsXZt4+IXJhbeJjAdA==", "dependencies": { - "ShimSkiaSharp": "5.2.1", + "ShimSkiaSharp": "5.2.3", "SkiaSharp": "4.148.0", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1" + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "7qAG1I0FGlMwOJk0Js41TmvRJ0eLJPpz8Edd8DOzsotLikUhYGDZZlwXV+6sKvsApfIZEfarH0KwI38T9OHsmw==", + "resolved": "5.2.3", + "contentHash": "epicuKTb55l7Y6c3f9pdP0RkoifVieS9sWvA9HsppTfYyV7RFZg89+fUA9TTPcuccqi0GUGwiPEi+9b881zlDw==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "0+fG1BFw3QoXj+4sxpaXpLDZXz+Snb289epyP3HKjEjUUoWVsErMZb6teo4dVj8KElRJIeXjX6jD1C0B1vLRXw==", + "resolved": "5.2.3", + "contentHash": "iZInP4/48rIkpFxVJNMmxDDjIBaUcGOHrzancQ3cmneHXrtH4MYkm6bzggL9hCiS2dxfLhk3JCiERoGf8j0MlA==", "dependencies": { - "ShimSkiaSharp": "5.2.1", - "Svg.Custom": "5.2.1" + "ShimSkiaSharp": "5.2.3", + "Svg.Custom": "5.2.3" } }, "Svg.SceneGraph": { "type": "Transitive", - "resolved": "5.2.1", - "contentHash": "vm2QxbPv5MraL7T2+1dh0m8lGQqtW37RK4iOHvBsJQHQzcuDlCR+uX/Oiun77kS2US2OC23zfaIFEYWGwocXLw==", + "resolved": "5.2.3", + "contentHash": "t9HrB1n499YCvziq+sdapDjMvgY7iSzyldEDalWnoZCP3cncu92gEktBqzhUgo8A+r9mfow2TNf0u5jgV5FvlQ==", "dependencies": { - "ShimSkiaSharp": "5.2.1", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1" + "ShimSkiaSharp": "5.2.3", + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3" } }, "System.IO.Packaging": { @@ -236,18 +236,18 @@ "bookgen.lib": { "type": "Project", "dependencies": { - "AngleSharp": "[1.7.1, )", + "AngleSharp": "[1.7.2, )", "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", "Markdig": "[1.3.2, )", "Microsoft.ClearScript": "[7.5.1, )", "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1, )", "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1, )", - "PreMailer.Net": "[2.7.3, )", + "PreMailer.Net": "[2.7.4, )", "SkiaSharp": "[4.151.1, )", "SkiaSharp.NativeAssets.Linux": "[4.151.1, )", "SkiaSharp.NativeAssets.Win32": "[4.151.1, )", - "Svg.Skia": "[5.2.1, )", + "Svg.Skia": "[5.2.3, )", "System.ServiceModel.Syndication": "[11.0.0-preview.7.26381.103, )", "YamlDotNet": "[18.1.0, )" } @@ -264,9 +264,9 @@ }, "AngleSharp": { "type": "CentralTransitive", - "requested": "[1.7.1, )", - "resolved": "1.7.1", - "contentHash": "QBblGpRB3SYD66Mls7fHyl1mWkP93n7ErWDWHc1vd9sPHhdlTkmUoY3CjgfjcWNxGF9m7xjNyztRfErXqoBKOA==" + "requested": "[1.7.2, )", + "resolved": "1.7.2", + "contentHash": "r1rb5Qo/0KPzmP0nbSiXPDVfh4Ctu0B+y1RyyUq73g4sgAmEW7q10RDyTq2ZsILwtO9O2LAvMrUles7eD4zz1g==" }, "Microsoft.ClearScript": { "type": "CentralTransitive", @@ -295,11 +295,11 @@ }, "PreMailer.Net": { "type": "CentralTransitive", - "requested": "[2.7.3, )", - "resolved": "2.7.3", - "contentHash": "p6LB7Qu4ZsPmjiFtWmytiERhopuGxq3Ql/SVuOZueGzfrDNvvSi8sGaDVHVvJt0S+RCHU422TWLu2aMXDE9ejg==", + "requested": "[2.7.4, )", + "resolved": "2.7.4", + "contentHash": "SrmoWjabHeK4a5mrolGzLMi+z5DMTGzK/Jce0ORs/lGDzvNggDNJF260v4gj5k9aU4LRMp8qILCMCI71t+oa0g==", "dependencies": { - "AngleSharp": "1.5.1" + "AngleSharp": "1.7.2" } }, "SkiaSharp": { @@ -326,19 +326,19 @@ }, "Svg.Skia": { "type": "CentralTransitive", - "requested": "[5.2.1, )", - "resolved": "5.2.1", - "contentHash": "es8F7MSeJJgc30Dmi0ZsrPpGQmLTQuS3DCvHcJ9wYlNAt3oHEoEzg5ugX3bwMPFejrPNLNjPpr1VOiMrpFyvIA==", + "requested": "[5.2.3, )", + "resolved": "5.2.3", + "contentHash": "slGSy5Aje/30Ou2a5aezbGGWLZspBIBYlEzqgtaIVex4FUYAR3mxaYrvMZj1tHy+ESDFCGY7+2FjF6UzqGUZew==", "dependencies": { "HarfBuzzSharp": "14.2.0", "HarfBuzzSharp.NativeAssets.Linux": "14.2.0", "HarfBuzzSharp.NativeAssets.Win32": "14.2.0", "HarfBuzzSharp.NativeAssets.macOS": "14.2.0", "SkiaSharp": "4.148.0", - "Svg.Animation": "5.2.1", - "Svg.Custom": "5.2.1", - "Svg.Model": "5.2.1", - "Svg.SceneGraph": "5.2.1" + "Svg.Animation": "5.2.3", + "Svg.Custom": "5.2.3", + "Svg.Model": "5.2.3", + "Svg.SceneGraph": "5.2.3" } }, "System.ServiceModel.Syndication": { From 26f42d4166d02467ca162666eb0c5a937df51d4e Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 5 Sep 2026 19:00:58 +0200 Subject: [PATCH 25/32] Shell prompt improvements --- Source/BookGen.Contents/BookGenShell.ps1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/BookGen.Contents/BookGenShell.ps1 b/Source/BookGen.Contents/BookGenShell.ps1 index 4d092390..02018fa3 100644 --- a/Source/BookGen.Contents/BookGenShell.ps1 +++ b/Source/BookGen.Contents/BookGenShell.ps1 @@ -428,15 +428,22 @@ Register-ArgumentCompleter -Native -CommandName git -ScriptBlock { # set prompt function prompt { + $lastOk = $? + $lastExit = $LASTEXITCODE + $success = $lastOk -and (($null -eq $lastExit) -or ($lastExit -eq 0)) + $status = if ($success) { "`e[32m✓`e[0m" } else { "`e[31m✗`e[0m" } + $git = $(BookGen.Shellprog.exe "prompt" $(Get-Location).Path) $location = (Get-Location).Path - $topLine = "╭╴$location"+"`n" + $locationUri = [System.Uri]::new($location).AbsoluteUri + $link = "`e]8;;$locationUri`a$location`e]8;;`a" + $topLine = "╭╴$link"+"`n" if (-not [string]::IsNullOrWhiteSpace($git)) { - $topLine + '╰╴ PS ' + $git + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' + $topLine + '╰╴ PS ' + $git + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' ' + $status + ' > ' } else { - $topLine + '╰╴ PS ' + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' + $topLine + '╰╴ PS ' + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' ' + $status + ' > ' } } From 6660349210ded3abe3774f62531bb794db573745 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sat, 5 Sep 2026 19:07:07 +0200 Subject: [PATCH 26/32] Depenedencies update --- Directory.Packages.props | 14 ++-- Source/BookGen.Lib/packages.lock.json | 92 +++++++++++----------- Source/BookGen/packages.lock.json | 106 +++++++++++++------------- 3 files changed, 106 insertions(+), 106 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e8642967..61492120 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,14 +4,14 @@ true - + - - - + + + @@ -23,9 +23,9 @@ - - - + + + diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index 7be7862a..86213402 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "AngleSharp": { "type": "Direct", - "requested": "[1.7.2, )", - "resolved": "1.7.2", - "contentHash": "r1rb5Qo/0KPzmP0nbSiXPDVfh4Ctu0B+y1RyyUq73g4sgAmEW7q10RDyTq2ZsILwtO9O2LAvMrUles7eD4zz1g==" + "requested": "[1.7.3, )", + "resolved": "1.7.3", + "contentHash": "I8TpSvgOPAVQHjVvXaROZOkIGZdX0tn+e9CUix6mv5pMqfyKO+/zm/dofsXgsxVTJl/sSTnEMFwrdnqxTt2axQ==" }, "Markdig": { "type": "Direct", @@ -16,28 +16,28 @@ }, "Microsoft.ClearScript": { "type": "Direct", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "CyEBQDHC5b1B6n7Md3M6aGEs8bTtFHIeyY2jLCAJaE72gBOSdHOqAKX2cUQlYtPnQHPADTabigVXwwNtuebTQw==", + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "Ndl7gJiJzNLcazRJZyq7u6zXEKiZNcORFbeku7UjYCOLmJKZDzwFDigalOsJAsp32tef5bb65/xmsKtnYG2xVw==", "dependencies": { - "Microsoft.ClearScript.V8": "7.5.1", - "Microsoft.ClearScript.V8.ICUData": "7.5.1", - "Microsoft.ClearScript.V8.Native.win-x64": "7.5.1", - "Microsoft.ClearScript.V8.Native.win-x86": "7.5.1", - "Microsoft.ClearScript.Windows": "7.5.1" + "Microsoft.ClearScript.V8": "7.5.1.1", + "Microsoft.ClearScript.V8.ICUData": "7.5.1.1", + "Microsoft.ClearScript.V8.Native.win-x64": "7.5.1.1", + "Microsoft.ClearScript.V8.Native.win-x86": "7.5.1.1", + "Microsoft.ClearScript.Windows": "7.5.1.1" } }, "Microsoft.ClearScript.V8.Native.linux-x64": { "type": "Direct", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "B+JPK6ZnflitblwHl7WZ/6argp3+czixo10vYXbF9D5KgjclJbeppxj5FpJnH6Hv1Ua0Ql+TduoSX53fhYh5AQ==" + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "ZzX0d8KD3h1mkHji3qMZA5gllbArSJErOcgpmOgXp6MNVEcA08TED+InXAS/lnubEs1q0mflnXjHcXIptjDYSA==" }, "Microsoft.ClearScript.V8.Native.win-x64": { "type": "Direct", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "fkCJta+xF2W3+L6NwRzqIV0VYD/WVcrZHyxgbOe5iTl321Td0w5loBWGVXlgG5fsuv2FEcba1GLE1CbeJrzMRw==" + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "N79bR+RG7FD8wv/tdXZuOLDx3YAQ+mSB5P0gcUNMGEFO4DljRSPlJcEzHLrPvrlPzNnc3aV6Rx2v5V3qordPJg==" }, "PreMailer.Net": { "type": "Direct", @@ -56,25 +56,25 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "FNlVsGZ9to3o9pObVqhIZanHjUGOoPb/Zxk5svCSLMJnKwxPKV7sIJrjs9BpB/jF3j9OdsaMOb4cfwSJbjueSw==", + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "zqcEy5E17Tqoh2tEvb4uC2G3ulli5YdsljzZuf93pCc/9gQDMx9VrUInxiqkLMGt+MjQi8hNrf1Uxmq25yyU3A==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.151.1", - "SkiaSharp.NativeAssets.macOS": "4.151.1" + "SkiaSharp.NativeAssets.Win32": "4.151.2", + "SkiaSharp.NativeAssets.macOS": "4.151.2" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "4FM5fSV6W3nMOeawrxkgu3sf3uWUfQEizZ+ww6kcByf9XERiSbZftf5dyJxOUFDLcrtj9aslN2Jb7uZsO/lfWw==" + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "TSeXS5j73N9+avlMn72Q8DC+EGi2LW7Gh7INHTkTo4bVpkEQu32D7tTWvP5LhZ+Fto9Ld6lih7UMwm+V3AQyCQ==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Direct", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "G+nln30ZX2ZuhH+ungf6x/QMXCje38KTS0VTfQeMyeY8uyMBmsZwFz2D30ZxUjZqsVbEIcaC2bsYF65876s4Yg==" + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "sTUHSfWXVYThf/lTG81n8y1gMnr0TiN/2zrApwd0jbGfbsEt9g4+GPsj6UVjjgNLXCI9l5CyzxQ6Zh4fP9ZoVQ==" }, "Svg.Skia": { "type": "Direct", @@ -136,43 +136,43 @@ }, "Microsoft.ClearScript.Core": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "GuCo9rODubVJGR/cTS4+PIfLsXBz2JUtrUg3II3i+vb1GI0WWO4SLGZfnixtALeKhgBP+5hiKRE3pdmkC4pgFg==" + "resolved": "7.5.1.1", + "contentHash": "H3R4OaqQPfwxewnZcvTU6qUFyzJ05FchBD4IlO6Z0WO3iWh8PyAWHDcnt4FUmo/t0m40o8rkgnJ7g9pDT7pwJQ==" }, "Microsoft.ClearScript.V8": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "RflEJEMkidCKIOKALPwjvZXjjIUrIjgyXhgiyfrb8Ybnd2yxmNct7DOX3eWQYuHCyDe+wuTYBZaj3XaAlDMNDg==", + "resolved": "7.5.1.1", + "contentHash": "oEJxEMqzzC8XYE9kA1ajmfmloAuSlNIzNyrIfwROXkKjfeePptvUT22MqHfV6rRV4//B7XShlq7VM6o2ZM1eZQ==", "dependencies": { - "Microsoft.ClearScript.Core": "7.5.1", - "Microsoft.ClearScript.V8.ICUData": "7.5.1", + "Microsoft.ClearScript.Core": "7.5.1.1", + "Microsoft.ClearScript.V8.ICUData": "7.5.1.1", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ClearScript.V8.ICUData": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "QuyrzwPU54Np1zxF8NVKQjPghCGs3WEaEsgYWfdCwBHKWLjprtkKH1dl0+lE0gf8hkGalAOH5B5wOctrZ6jEoA==" + "resolved": "7.5.1.1", + "contentHash": "Jg3yFIWPkfsjOHzVUTbA0VgnxXBtX9BwSL00E8HIljFm3f203e+mlJYO0tTHnqetA0xZyLKbRjRuPotUP/47Nw==" }, "Microsoft.ClearScript.V8.Native.win-x86": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "v3w8vQJsByOOIimCIuU9oeu9OqGKtC300uBTsW/zoTz5/WYZZPsKeAday9CCZNbug/4DqDeJA9u/W/qmDpGEjg==" + "resolved": "7.5.1.1", + "contentHash": "Y2ThCEOpf0FI/CxoJcLTi04T8ghongf4xwOKglbqszhyDAdybg03hXoj59S8lg9Ym0ikXR2oh1+cZfskrX3eTA==" }, "Microsoft.ClearScript.Windows": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "Vxvu7yV9OSC+pafTn5QVABAnQulq9C731uarhyFaZPWCd3lOfDbtkI51PIolHKZ6araezH9kofdpnaJZf92vSQ==", + "resolved": "7.5.1.1", + "contentHash": "WRLieCurYIetStUMKAZS2bFZUzHYmfFs7uL/+xTrxNnoMymdkWg6wp2Rsy0M4rLlXOoz1E+sMzhJDi6G8Fz+DA==", "dependencies": { - "Microsoft.ClearScript.Windows.Core": "7.5.1" + "Microsoft.ClearScript.Windows.Core": "7.5.1.1" } }, "Microsoft.ClearScript.Windows.Core": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "miy+Z5jAe46nm4iCOcAHotYSzegtuEgmsp6wuEpDXjxAsepweKdeWMRKmvx+feBjIbHftWW25X7ne07Z26wHvw==", + "resolved": "7.5.1.1", + "contentHash": "ijhFErGPOWaTgCB9ywwqwyxtZhB15CC9OJ4UCjfWRgEHD7XFQ9r9UAdOfG1/C/nNFK7hELkFrxq9Dup3C3Npog==", "dependencies": { - "Microsoft.ClearScript.Core": "7.5.1" + "Microsoft.ClearScript.Core": "7.5.1.1" } }, "Newtonsoft.Json": { @@ -187,8 +187,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.151.1", - "contentHash": "nLXwdhi0oB7pqqNLbSOLX+FIZQOMAU/yFPspzcDTJjTmPbMWyp0JWyeVj8sFJ/cweurEfT0J/lvfgdXX2EFHXA==" + "resolved": "4.151.2", + "contentHash": "hDGy20k1DJZuKcdbHjwaOfsPeQYY9ty8wJeG1qzJWidPUzrwwvVtTLpDdTiP1M89UWbEYyWAhZo9qOaoYae6iQ==" }, "Spectre.Console.Ansi": { "type": "Transitive", diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index a26fd10e..5e42bc9a 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -119,43 +119,43 @@ }, "Microsoft.ClearScript.Core": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "GuCo9rODubVJGR/cTS4+PIfLsXBz2JUtrUg3II3i+vb1GI0WWO4SLGZfnixtALeKhgBP+5hiKRE3pdmkC4pgFg==" + "resolved": "7.5.1.1", + "contentHash": "H3R4OaqQPfwxewnZcvTU6qUFyzJ05FchBD4IlO6Z0WO3iWh8PyAWHDcnt4FUmo/t0m40o8rkgnJ7g9pDT7pwJQ==" }, "Microsoft.ClearScript.V8": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "RflEJEMkidCKIOKALPwjvZXjjIUrIjgyXhgiyfrb8Ybnd2yxmNct7DOX3eWQYuHCyDe+wuTYBZaj3XaAlDMNDg==", + "resolved": "7.5.1.1", + "contentHash": "oEJxEMqzzC8XYE9kA1ajmfmloAuSlNIzNyrIfwROXkKjfeePptvUT22MqHfV6rRV4//B7XShlq7VM6o2ZM1eZQ==", "dependencies": { - "Microsoft.ClearScript.Core": "7.5.1", - "Microsoft.ClearScript.V8.ICUData": "7.5.1", + "Microsoft.ClearScript.Core": "7.5.1.1", + "Microsoft.ClearScript.V8.ICUData": "7.5.1.1", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ClearScript.V8.ICUData": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "QuyrzwPU54Np1zxF8NVKQjPghCGs3WEaEsgYWfdCwBHKWLjprtkKH1dl0+lE0gf8hkGalAOH5B5wOctrZ6jEoA==" + "resolved": "7.5.1.1", + "contentHash": "Jg3yFIWPkfsjOHzVUTbA0VgnxXBtX9BwSL00E8HIljFm3f203e+mlJYO0tTHnqetA0xZyLKbRjRuPotUP/47Nw==" }, "Microsoft.ClearScript.V8.Native.win-x86": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "v3w8vQJsByOOIimCIuU9oeu9OqGKtC300uBTsW/zoTz5/WYZZPsKeAday9CCZNbug/4DqDeJA9u/W/qmDpGEjg==" + "resolved": "7.5.1.1", + "contentHash": "Y2ThCEOpf0FI/CxoJcLTi04T8ghongf4xwOKglbqszhyDAdybg03hXoj59S8lg9Ym0ikXR2oh1+cZfskrX3eTA==" }, "Microsoft.ClearScript.Windows": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "Vxvu7yV9OSC+pafTn5QVABAnQulq9C731uarhyFaZPWCd3lOfDbtkI51PIolHKZ6araezH9kofdpnaJZf92vSQ==", + "resolved": "7.5.1.1", + "contentHash": "WRLieCurYIetStUMKAZS2bFZUzHYmfFs7uL/+xTrxNnoMymdkWg6wp2Rsy0M4rLlXOoz1E+sMzhJDi6G8Fz+DA==", "dependencies": { - "Microsoft.ClearScript.Windows.Core": "7.5.1" + "Microsoft.ClearScript.Windows.Core": "7.5.1.1" } }, "Microsoft.ClearScript.Windows.Core": { "type": "Transitive", - "resolved": "7.5.1", - "contentHash": "miy+Z5jAe46nm4iCOcAHotYSzegtuEgmsp6wuEpDXjxAsepweKdeWMRKmvx+feBjIbHftWW25X7ne07Z26wHvw==", + "resolved": "7.5.1.1", + "contentHash": "ijhFErGPOWaTgCB9ywwqwyxtZhB15CC9OJ4UCjfWRgEHD7XFQ9r9UAdOfG1/C/nNFK7hELkFrxq9Dup3C3Npog==", "dependencies": { - "Microsoft.ClearScript.Core": "7.5.1" + "Microsoft.ClearScript.Core": "7.5.1.1" } }, "Newtonsoft.Json": { @@ -170,8 +170,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.151.1", - "contentHash": "nLXwdhi0oB7pqqNLbSOLX+FIZQOMAU/yFPspzcDTJjTmPbMWyp0JWyeVj8sFJ/cweurEfT0J/lvfgdXX2EFHXA==" + "resolved": "4.151.2", + "contentHash": "hDGy20k1DJZuKcdbHjwaOfsPeQYY9ty8wJeG1qzJWidPUzrwwvVtTLpDdTiP1M89UWbEYyWAhZo9qOaoYae6iQ==" }, "Spectre.Console.Ansi": { "type": "Transitive", @@ -236,17 +236,17 @@ "bookgen.lib": { "type": "Project", "dependencies": { - "AngleSharp": "[1.7.2, )", + "AngleSharp": "[1.7.3, )", "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", "Markdig": "[1.3.2, )", - "Microsoft.ClearScript": "[7.5.1, )", - "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1, )", - "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1, )", + "Microsoft.ClearScript": "[7.5.1.1, )", + "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1.1, )", + "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1.1, )", "PreMailer.Net": "[2.7.4, )", - "SkiaSharp": "[4.151.1, )", - "SkiaSharp.NativeAssets.Linux": "[4.151.1, )", - "SkiaSharp.NativeAssets.Win32": "[4.151.1, )", + "SkiaSharp": "[4.151.2, )", + "SkiaSharp.NativeAssets.Linux": "[4.151.2, )", + "SkiaSharp.NativeAssets.Win32": "[4.151.2, )", "Svg.Skia": "[5.2.3, )", "System.ServiceModel.Syndication": "[11.0.0-preview.7.26381.103, )", "YamlDotNet": "[18.1.0, )" @@ -264,34 +264,34 @@ }, "AngleSharp": { "type": "CentralTransitive", - "requested": "[1.7.2, )", - "resolved": "1.7.2", - "contentHash": "r1rb5Qo/0KPzmP0nbSiXPDVfh4Ctu0B+y1RyyUq73g4sgAmEW7q10RDyTq2ZsILwtO9O2LAvMrUles7eD4zz1g==" + "requested": "[1.7.3, )", + "resolved": "1.7.3", + "contentHash": "I8TpSvgOPAVQHjVvXaROZOkIGZdX0tn+e9CUix6mv5pMqfyKO+/zm/dofsXgsxVTJl/sSTnEMFwrdnqxTt2axQ==" }, "Microsoft.ClearScript": { "type": "CentralTransitive", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "CyEBQDHC5b1B6n7Md3M6aGEs8bTtFHIeyY2jLCAJaE72gBOSdHOqAKX2cUQlYtPnQHPADTabigVXwwNtuebTQw==", + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "Ndl7gJiJzNLcazRJZyq7u6zXEKiZNcORFbeku7UjYCOLmJKZDzwFDigalOsJAsp32tef5bb65/xmsKtnYG2xVw==", "dependencies": { - "Microsoft.ClearScript.V8": "7.5.1", - "Microsoft.ClearScript.V8.ICUData": "7.5.1", - "Microsoft.ClearScript.V8.Native.win-x64": "7.5.1", - "Microsoft.ClearScript.V8.Native.win-x86": "7.5.1", - "Microsoft.ClearScript.Windows": "7.5.1" + "Microsoft.ClearScript.V8": "7.5.1.1", + "Microsoft.ClearScript.V8.ICUData": "7.5.1.1", + "Microsoft.ClearScript.V8.Native.win-x64": "7.5.1.1", + "Microsoft.ClearScript.V8.Native.win-x86": "7.5.1.1", + "Microsoft.ClearScript.Windows": "7.5.1.1" } }, "Microsoft.ClearScript.V8.Native.linux-x64": { "type": "CentralTransitive", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "B+JPK6ZnflitblwHl7WZ/6argp3+czixo10vYXbF9D5KgjclJbeppxj5FpJnH6Hv1Ua0Ql+TduoSX53fhYh5AQ==" + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "ZzX0d8KD3h1mkHji3qMZA5gllbArSJErOcgpmOgXp6MNVEcA08TED+InXAS/lnubEs1q0mflnXjHcXIptjDYSA==" }, "Microsoft.ClearScript.V8.Native.win-x64": { "type": "CentralTransitive", - "requested": "[7.5.1, )", - "resolved": "7.5.1", - "contentHash": "fkCJta+xF2W3+L6NwRzqIV0VYD/WVcrZHyxgbOe5iTl321Td0w5loBWGVXlgG5fsuv2FEcba1GLE1CbeJrzMRw==" + "requested": "[7.5.1.1, )", + "resolved": "7.5.1.1", + "contentHash": "N79bR+RG7FD8wv/tdXZuOLDx3YAQ+mSB5P0gcUNMGEFO4DljRSPlJcEzHLrPvrlPzNnc3aV6Rx2v5V3qordPJg==" }, "PreMailer.Net": { "type": "CentralTransitive", @@ -304,25 +304,25 @@ }, "SkiaSharp": { "type": "CentralTransitive", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "FNlVsGZ9to3o9pObVqhIZanHjUGOoPb/Zxk5svCSLMJnKwxPKV7sIJrjs9BpB/jF3j9OdsaMOb4cfwSJbjueSw==", + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "zqcEy5E17Tqoh2tEvb4uC2G3ulli5YdsljzZuf93pCc/9gQDMx9VrUInxiqkLMGt+MjQi8hNrf1Uxmq25yyU3A==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.151.1", - "SkiaSharp.NativeAssets.macOS": "4.151.1" + "SkiaSharp.NativeAssets.Win32": "4.151.2", + "SkiaSharp.NativeAssets.macOS": "4.151.2" } }, "SkiaSharp.NativeAssets.Linux": { "type": "CentralTransitive", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "4FM5fSV6W3nMOeawrxkgu3sf3uWUfQEizZ+ww6kcByf9XERiSbZftf5dyJxOUFDLcrtj9aslN2Jb7uZsO/lfWw==" + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "TSeXS5j73N9+avlMn72Q8DC+EGi2LW7Gh7INHTkTo4bVpkEQu32D7tTWvP5LhZ+Fto9Ld6lih7UMwm+V3AQyCQ==" }, "SkiaSharp.NativeAssets.Win32": { "type": "CentralTransitive", - "requested": "[4.151.1, )", - "resolved": "4.151.1", - "contentHash": "G+nln30ZX2ZuhH+ungf6x/QMXCje38KTS0VTfQeMyeY8uyMBmsZwFz2D30ZxUjZqsVbEIcaC2bsYF65876s4Yg==" + "requested": "[4.151.2, )", + "resolved": "4.151.2", + "contentHash": "sTUHSfWXVYThf/lTG81n8y1gMnr0TiN/2zrApwd0jbGfbsEt9g4+GPsj6UVjjgNLXCI9l5CyzxQ6Zh4fP9ZoVQ==" }, "Svg.Skia": { "type": "CentralTransitive", From 5ecc770f945519d18d7efae3612ecff5a9ea898b Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 6 Sep 2026 11:21:27 +0200 Subject: [PATCH 27/32] Cli: Argument parsing bugfix --- .../ArgumentParsing/ArgumentBag.cs | 21 +++++++------------ .../ArgumentParsing/ArgumentParser.cs | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Source/BookGen.Cli/ArgumentParsing/ArgumentBag.cs b/Source/BookGen.Cli/ArgumentParsing/ArgumentBag.cs index b3363088..0bb0d66e 100644 --- a/Source/BookGen.Cli/ArgumentParsing/ArgumentBag.cs +++ b/Source/BookGen.Cli/ArgumentParsing/ArgumentBag.cs @@ -89,21 +89,16 @@ public string[] GetSwitchValues(SwitchAttribute @switch) public string? GetArgument(ArgumentAttribute argument) { - int notNullIndex = -1; - for (int i = 0; i < _arguments.Length; i++) + if (argument.Index < 0 + || argument.Index >= _arguments.Length + || _arguments[argument.Index] == null) { - if (_arguments[i] != null) - { - notNullIndex++; - } - if (notNullIndex == argument.Index) - { - string? returnValue = _arguments[i]; - _arguments[i] = null; - return returnValue; - } + return null; } - return null; + + string? returnValue = _arguments[argument.Index]; + _arguments[argument.Index] = null; + return returnValue; } public IEnumerable GetNotProcessed() diff --git a/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs b/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs index ad8c14f3..52b03294 100644 --- a/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs +++ b/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs @@ -11,7 +11,7 @@ namespace BookGen.Cli.ArgumentParsing; -internal class ArgumentParser +internal sealed class ArgumentParser { private readonly List _argumentPropertyInfos; private readonly List _switchPropertyInfos; From d030b89a9ed2815152812c42a4f9c283f99935cb Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 6 Sep 2026 12:15:23 +0200 Subject: [PATCH 28/32] Prompt command rework --- Source/BookGen.Contents/BookGenShell.ps1 | 15 +-- Source/BookGen.Shell.Shared/TerminalCell.cs | 42 +++++++ Source/BookGen.Shell.Shared/TerminalColor.cs | 20 ++++ .../TerminalOutputBuilder.cs | 57 ---------- Source/BookGen.Shellprog/GitCommandBase.cs | 65 ++--------- Source/BookGen.Shellprog/Program.cs | 6 +- Source/BookGen.Shellprog/Prompt.cs | 105 ++++++++++++++++++ Source/BookGen.Shellprog/PromptCommand.cs | 38 ------- .../Properties/launchSettings.json | 2 +- Source/BookGen.Shellprog/RepoWeb.cs | 7 +- 10 files changed, 189 insertions(+), 168 deletions(-) create mode 100644 Source/BookGen.Shell.Shared/TerminalCell.cs create mode 100644 Source/BookGen.Shell.Shared/TerminalColor.cs delete mode 100644 Source/BookGen.Shell.Shared/TerminalOutputBuilder.cs create mode 100644 Source/BookGen.Shellprog/Prompt.cs delete mode 100644 Source/BookGen.Shellprog/PromptCommand.cs diff --git a/Source/BookGen.Contents/BookGenShell.ps1 b/Source/BookGen.Contents/BookGenShell.ps1 index 02018fa3..eb8ab6d0 100644 --- a/Source/BookGen.Contents/BookGenShell.ps1 +++ b/Source/BookGen.Contents/BookGenShell.ps1 @@ -431,20 +431,7 @@ function prompt { $lastOk = $? $lastExit = $LASTEXITCODE $success = $lastOk -and (($null -eq $lastExit) -or ($lastExit -eq 0)) - $status = if ($success) { "`e[32m✓`e[0m" } else { "`e[31m✗`e[0m" } - - $git = $(BookGen.Shellprog.exe "prompt" $(Get-Location).Path) - $location = (Get-Location).Path - $locationUri = [System.Uri]::new($location).AbsoluteUri - $link = "`e]8;;$locationUri`a$location`e]8;;`a" - $topLine = "╭╴$link"+"`n" - - if (-not [string]::IsNullOrWhiteSpace($git)) { - $topLine + '╰╴ PS ' + $git + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' ' + $status + ' > ' - } - else { - $topLine + '╰╴ PS ' + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' ' + $status + ' > ' - } + (BookGen.Shellprog.exe "prompt" $(Get-Location).Path $success) -join "`n" } diff --git a/Source/BookGen.Shell.Shared/TerminalCell.cs b/Source/BookGen.Shell.Shared/TerminalCell.cs new file mode 100644 index 00000000..50f8b876 --- /dev/null +++ b/Source/BookGen.Shell.Shared/TerminalCell.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Shell.Shared; + +public sealed class TerminalCell +{ + public TerminalColor Foreground { get; set; } = TerminalColor.Default; + public TerminalColor Background { get; set; } = TerminalColor.Default; + public string Text { get; set; } = string.Empty; + + public static implicit operator string (TerminalCell cell) + { + int foregroundCode = cell.Foreground == TerminalColor.Reset + ? (int)cell.Foreground + : (int)cell.Foreground + 20; + + int backgroundCode = cell.Background == TerminalColor.Reset + ? (int)cell.Background + : (int)cell.Background + 30; + + if (cell.Foreground == TerminalColor.Default + && cell.Background == TerminalColor.Default) + { + return cell.Text; + } + else if (cell.Foreground == TerminalColor.Default) + { + return $"\e[{backgroundCode}m{cell.Text}\e[0m"; + } + else if (cell.Background == TerminalColor.Default) + { + return $"\e[{foregroundCode}m{cell.Text}\e[0m"; + } + else + { + return $"\e[{foregroundCode};{backgroundCode}m{cell.Text}\e[0m"; + } + } +} diff --git a/Source/BookGen.Shell.Shared/TerminalColor.cs b/Source/BookGen.Shell.Shared/TerminalColor.cs new file mode 100644 index 00000000..7bfee5fc --- /dev/null +++ b/Source/BookGen.Shell.Shared/TerminalColor.cs @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Shell.Shared; + +public enum TerminalColor +{ + Reset = 0, + Black = 10, + Red = 11, + Green = 12, + Yellow = 13, + Blue = 14, + Magenta = 15, + Cyan = 16, + White = 17, + Default = 19, +} diff --git a/Source/BookGen.Shell.Shared/TerminalOutputBuilder.cs b/Source/BookGen.Shell.Shared/TerminalOutputBuilder.cs deleted file mode 100644 index 27c1ea89..00000000 --- a/Source/BookGen.Shell.Shared/TerminalOutputBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using System.Text; - -namespace BookGen.Shell.Shared; - -public sealed class TerminalOutputBuilder -{ - private readonly StringBuilder _builder; - - public TerminalOutputBuilder() - { - _builder = new StringBuilder(); - } - - public enum ForegroundColor - { - Black = 30, - Red = 31, - Green = 32, - Yellow = 33, - Blue = 34, - Magenta = 35, - Cyan = 36, - White = 37, - Default = 39, - Reset = 0, - } - - public enum BackgroundColor - { - Black = 40, - Red = 41, - Green = 42, - Yellow = 43, - Blue = 44, - Magenta = 45, - Cyan = 46, - White = 47, - Default = 49, - Reset = 0, - } - - public TerminalOutputBuilder Append(ForegroundColor foreground, BackgroundColor? background, string text) - { - _builder.Append($"\x1b[{(int)foreground};{(int)(background ?? BackgroundColor.Default)}m{text}\x1b[0m"); - return this; - } - - public override string ToString() - { - return _builder.ToString(); - } -} diff --git a/Source/BookGen.Shellprog/GitCommandBase.cs b/Source/BookGen.Shellprog/GitCommandBase.cs index 0a103afa..ceeb5a61 100644 --- a/Source/BookGen.Shellprog/GitCommandBase.cs +++ b/Source/BookGen.Shellprog/GitCommandBase.cs @@ -13,26 +13,21 @@ namespace BookGen.Shellprog; -internal abstract class GitCommandBase : Command +internal class GitArguments : ArgumentsBase { - protected const int TimeOut = 10; - protected readonly IAnsiConsole _console; + [Description("Working directory for prompt")] + [Argument(0, IsOptional = true)] + public string WorkDirectory { get; set; } - internal sealed class GitArguments : ArgumentsBase + public GitArguments() { - [Description("Working directory to check for git repository")] - [Argument(0, IsOptional = true)] - public string WorkDirectory { get; set; } - public GitArguments() - { - WorkDirectory = string.Empty; - } + WorkDirectory = string.Empty; } +} - protected GitCommandBase(IAnsiConsole console) - { - _console = console; - } +internal abstract class GitCommandBase : Command where T : GitArguments +{ + protected const int TimeOut = 10; public enum GitDirectoryStatus { @@ -92,44 +87,4 @@ protected static string GetGitRemote(string workDirectory) return null; } } - - protected void PrintUntrusted() - { - TerminalOutputBuilder builder = new TerminalOutputBuilder() - .Append(TerminalOutputBuilder.ForegroundColor.Yellow, TerminalOutputBuilder.BackgroundColor.Black, ""); - - _console.WriteLine(builder.ToString()); - } - - protected void PrintStatus(GitStatus? status) - { - if (status is null) - { - return; - } - - TerminalOutputBuilder builder = new TerminalOutputBuilder() - .Append(TerminalOutputBuilder.ForegroundColor.Default, TerminalOutputBuilder.BackgroundColor.Green, $"({status.BranchName}) "); - - if (status.IncommingCommits > 0) - { - builder.Append(TerminalOutputBuilder.ForegroundColor.Black, - TerminalOutputBuilder.BackgroundColor.Magenta, - $"↓: {status.IncommingCommits}"); - } - if (status.OutGoingCommits > 0) - { - builder.Append(TerminalOutputBuilder.ForegroundColor.Black, - TerminalOutputBuilder.BackgroundColor.Yellow, - $" ↑: {status.OutGoingCommits}"); - } - if (status.NotCommitedChanges > 0) - { - builder.Append(TerminalOutputBuilder.ForegroundColor.Black, - TerminalOutputBuilder.BackgroundColor.Cyan, - $" M: {status.NotCommitedChanges}"); - } - - _console.WriteLine(builder.ToString()); - } } diff --git a/Source/BookGen.Shellprog/Program.cs b/Source/BookGen.Shellprog/Program.cs index f6e6caff..c249edee 100644 --- a/Source/BookGen.Shellprog/Program.cs +++ b/Source/BookGen.Shellprog/Program.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Diagnostics; + using BookGen.Cli; using BookGen.Shell.Shared.Loging; using BookGen.Shellprog; @@ -30,6 +32,8 @@ using ServiceProvider provider = ioc.BuildServiceProvider(); +//Debugger.Launch(); + using CommandRunner runner = new(provider, new CommandHelpProvider(), logger, new CommandRunnerSettings { UnknownCommandCodeAndMessage = (-1, "Unknown command"), @@ -43,7 +47,7 @@ runner .AddDefaultCommand() - .AddCommand() + .AddCommand() .AddCommand() .AddCommand() .AddCommand() diff --git a/Source/BookGen.Shellprog/Prompt.cs b/Source/BookGen.Shellprog/Prompt.cs new file mode 100644 index 00000000..24bb5d49 --- /dev/null +++ b/Source/BookGen.Shellprog/Prompt.cs @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.ComponentModel; + +using BookGen.Cli.Annotations; +using BookGen.Shell.Shared; + +namespace BookGen.Shellprog; + +[CommandName("prompt")] +internal sealed class Prompt : GitCommandBase +{ + internal sealed class Arguments : GitArguments + { + [Argument(1, IsOptional = false)] + [Description("Last command success status")] + public bool WasSuccess { get; set; } = false; + } + + public override int Execute(Arguments arguments, IReadOnlyList context) + { + if (string.IsNullOrWhiteSpace(arguments.WorkDirectory)) + return 0; + + List parts = + [ + "╭╴", + MakeLink(arguments.WorkDirectory, 110), + "\r\n", + "╰╴ PS ", + ]; + + GitDirectoryStatus result = TestIfGitDir(arguments.WorkDirectory); + if (result == GitDirectoryStatus.UntrustedGitDirectory) + { + parts.Add(new TerminalCell + { + Background = TerminalColor.Black, + Foreground = TerminalColor.Red, + Text = "", + }); + } + else if (result == GitDirectoryStatus.GitDirectory) + { + GitStatus? status = GetGitStatus(arguments.WorkDirectory); + if (status?.IncommingCommits > 0) + { + parts.Add(new TerminalCell + { + Background = TerminalColor.Black, + Foreground = TerminalColor.Magenta, + Text = $"↓: {status.IncommingCommits}", + }); + } + if (status?.OutGoingCommits > 0) + { + parts.Add(new TerminalCell + { + Background = TerminalColor.Black, + Foreground = TerminalColor.Yellow, + Text = $" ↑: {status.OutGoingCommits}", + }); + } + if (status?.NotCommitedChanges > 0) + { + parts.Add(new TerminalCell + { + Background = TerminalColor.Black, + Foreground = TerminalColor.Red, + Text = $" M: {status.NotCommitedChanges}", + }); + } + } + + parts.Add(MakeLastExitStatusDisplay(arguments.WasSuccess)); + parts.Add(" >"); + +#pragma warning disable Spectre1000 // Use AnsiConsole instead of System.Console + foreach (var part in parts) + { + Console.Write(part); + } +#pragma warning restore Spectre1000 // Use AnsiConsole instead of System.Console + + return 0; + + } + + private static string MakeLastExitStatusDisplay(bool wasSuccess) + { + return wasSuccess + ? "✅" + : "❌"; + } + + private static string MakeLink(string workDirectory, int maxWidth) + { + var uri = new Uri(workDirectory).AbsoluteUri; + string text = workDirectory.Length > maxWidth ? $"...{workDirectory[(workDirectory.Length - maxWidth + 3)..]}" : workDirectory; + return $"\e]8;;{uri}\a{text}\e]8;;\a"; + } +} diff --git a/Source/BookGen.Shellprog/PromptCommand.cs b/Source/BookGen.Shellprog/PromptCommand.cs deleted file mode 100644 index 7677491d..00000000 --- a/Source/BookGen.Shellprog/PromptCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2026 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -using BookGen.Cli.Annotations; -using BookGen.Shell.Shared; - -using Spectre.Console; - -namespace BookGen.Shellprog; - -[CommandName("prompt")] -internal sealed class PromptCommand : GitCommandBase -{ - public PromptCommand(IAnsiConsole console) : base(console) - { - } - - public override int Execute(GitArguments arguments, IReadOnlyList context) - { - if (!string.IsNullOrEmpty(arguments.WorkDirectory)) - { - GitDirectoryStatus result = TestIfGitDir(arguments.WorkDirectory); - if (result == GitDirectoryStatus.GitDirectory) - { - GitStatus? status = GetGitStatus(arguments.WorkDirectory); - PrintStatus(status); - } - else if (result == GitDirectoryStatus.UntrustedGitDirectory) - { - PrintUntrusted(); - } - } - - return 0; - } -} diff --git a/Source/BookGen.Shellprog/Properties/launchSettings.json b/Source/BookGen.Shellprog/Properties/launchSettings.json index 623d6cb8..bfd9f8dd 100644 --- a/Source/BookGen.Shellprog/Properties/launchSettings.json +++ b/Source/BookGen.Shellprog/Properties/launchSettings.json @@ -5,7 +5,7 @@ }, "Prompt": { "commandName": "Project", - "commandLineArgs": "prompt b:\\HelloCsharp\\C" + "commandLineArgs": "prompt c:\\Users\\ruzsi true" } } } \ No newline at end of file diff --git a/Source/BookGen.Shellprog/RepoWeb.cs b/Source/BookGen.Shellprog/RepoWeb.cs index 19cae8b2..b0df306d 100644 --- a/Source/BookGen.Shellprog/RepoWeb.cs +++ b/Source/BookGen.Shellprog/RepoWeb.cs @@ -11,10 +11,13 @@ namespace BookGen.Shellprog; [CommandName("repoweb")] -internal sealed class RepoWeb : GitCommandBase +internal sealed class RepoWeb : GitCommandBase { - public RepoWeb(IAnsiConsole console) : base(console) + private readonly IAnsiConsole _console; + + public RepoWeb(IAnsiConsole console) { + _console = console; } public override int Execute(GitArguments arguments, IReadOnlyList context) From c8f159b81bde6316fee998364df6d15190be9383 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 10 Sep 2026 21:47:40 +0200 Subject: [PATCH 29/32] Dependencies update --- Directory.Packages.props | 10 ++--- .../ArgumentParsing/ValueConverter.cs | 14 +++---- .../CrashReporting/CrashDumpGenerator.cs | 9 ++++- Source/BookGen.Cli/CrashReporting/LogEntry.cs | 7 +++- Source/BookGen.Lib/packages.lock.json | 32 +++++++-------- .../Loging/DebugLoggerProvider.cs | 7 +++- .../Infrastructure/Loging/RingBuffer.cs | 7 +++- Source/BookGen/packages.lock.json | 40 +++++++++---------- Test/Bookgen.Tests/Commands/UT_Script.cs | 7 +++- 9 files changed, 79 insertions(+), 54 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 61492120..b835e256 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - + @@ -16,16 +16,16 @@ - + - - - + + + diff --git a/Source/BookGen.Cli/ArgumentParsing/ValueConverter.cs b/Source/BookGen.Cli/ArgumentParsing/ValueConverter.cs index 5479e9ad..b99dd288 100644 --- a/Source/BookGen.Cli/ArgumentParsing/ValueConverter.cs +++ b/Source/BookGen.Cli/ArgumentParsing/ValueConverter.cs @@ -17,19 +17,19 @@ private static bool TryGetNullableType(Type input, [NotNullWhen(true)] out Type? return result != null; } - public static object? Convert(string? value, Type propertyType) + public static object? Convert(string? value, Type targetPropertyType) { if (string.IsNullOrEmpty(value)) return null; - Type currenttype = propertyType; - if (TryGetNullableType(currenttype, out Type? nullable)) + Type currentType = targetPropertyType; + if (TryGetNullableType(currentType, out Type? nullable)) { - currenttype = nullable; + currentType = nullable; } - if (currenttype.IsEnum - && Enum.TryParse(currenttype, value, true, out object? parsed)) + if (currentType.IsEnum + && Enum.TryParse(currentType, value, true, out object? parsed)) { return parsed; } @@ -38,7 +38,7 @@ private static bool TryGetNullableType(Type input, [NotNullWhen(true)] out Type? { checked { - object converted = System.Convert.ChangeType(value, currenttype, CultureInfo.InvariantCulture); + object converted = System.Convert.ChangeType(value, currentType, CultureInfo.InvariantCulture); return converted; } } diff --git a/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs b/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs index 3a1b923a..50424d4c 100644 --- a/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs +++ b/Source/BookGen.Cli/CrashReporting/CrashDumpGenerator.cs @@ -1,4 +1,9 @@ -using System.Runtime.InteropServices; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -164,7 +169,7 @@ private static void WalkException(Exception exception, StringBuilder html) .AppendLine("

Stack trace:

") .AppendLine($"
{HttpUtility.HtmlEncode(exception.StackTrace)}
"); - Exception? inner = exception?.InnerException; + Exception? inner = exception.InnerException; int iopen = 0; do { diff --git a/Source/BookGen.Cli/CrashReporting/LogEntry.cs b/Source/BookGen.Cli/CrashReporting/LogEntry.cs index bc05a43a..7589818a 100644 --- a/Source/BookGen.Cli/CrashReporting/LogEntry.cs +++ b/Source/BookGen.Cli/CrashReporting/LogEntry.cs @@ -1,4 +1,9 @@ -namespace BookGen.Cli.CrashReporting; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Cli.CrashReporting; public sealed record class LogEntry { diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index 86213402..5ccf2b40 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -4,9 +4,9 @@ "net11.0": { "AngleSharp": { "type": "Direct", - "requested": "[1.7.3, )", - "resolved": "1.7.3", - "contentHash": "I8TpSvgOPAVQHjVvXaROZOkIGZdX0tn+e9CUix6mv5pMqfyKO+/zm/dofsXgsxVTJl/sSTnEMFwrdnqxTt2axQ==" + "requested": "[1.8.1, )", + "resolved": "1.8.1", + "contentHash": "fTLPBxwYK5zmt+ernY3GL1kFBDA/2Cl72hpNnMHeNceZNJ/AT7Km9CXFaMHybuEFhXDy2Dv7Bjkez5G0Z4Io4g==" }, "Markdig": { "type": "Direct", @@ -56,25 +56,25 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "zqcEy5E17Tqoh2tEvb4uC2G3ulli5YdsljzZuf93pCc/9gQDMx9VrUInxiqkLMGt+MjQi8hNrf1Uxmq25yyU3A==", + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "TfYpxUV/Ihe5WyGeddJROZA2RhfGaSEKKNDHhREUVCVihtTlcavHQEi29Q0Mtn37AYXAf7EbC+StVPmWk63tkQ==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.151.2", - "SkiaSharp.NativeAssets.macOS": "4.151.2" + "SkiaSharp.NativeAssets.Win32": "4.152.0", + "SkiaSharp.NativeAssets.macOS": "4.152.0" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "TSeXS5j73N9+avlMn72Q8DC+EGi2LW7Gh7INHTkTo4bVpkEQu32D7tTWvP5LhZ+Fto9Ld6lih7UMwm+V3AQyCQ==" + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "B3713AE9SBHD55nMsaPlXY2C+GvsPsG50SxM/7iYayCvR3WqrQfgrSD4w/cJ9azGVrqrD2mx64esAQxVqK16dA==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Direct", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "sTUHSfWXVYThf/lTG81n8y1gMnr0TiN/2zrApwd0jbGfbsEt9g4+GPsj6UVjjgNLXCI9l5CyzxQ6Zh4fP9ZoVQ==" + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "PfyLS6TRP/9YghtW97le8bkzootCietlt7QUqFKGjGcZ6eRjeWNNHCjvxO41O/LYpV4gCthp2xiiUiYTOqfVyA==" }, "Svg.Skia": { "type": "Direct", @@ -187,8 +187,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.151.2", - "contentHash": "hDGy20k1DJZuKcdbHjwaOfsPeQYY9ty8wJeG1qzJWidPUzrwwvVtTLpDdTiP1M89UWbEYyWAhZo9qOaoYae6iQ==" + "resolved": "4.152.0", + "contentHash": "Vjx2sPleQDJXQjINxMzHaIU1KFyYAUerfBnwLAjMGSUCf0vCrSU2Y4LvCH0yAheeuu0YdlVfeSMiCKZYVJ12kA==" }, "Spectre.Console.Ansi": { "type": "Transitive", diff --git a/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs b/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs index 8ed1b24d..2fac2531 100644 --- a/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs +++ b/Source/BookGen/Infrastructure/Loging/DebugLoggerProvider.cs @@ -1,4 +1,9 @@ -using BookGen.Cli.CrashReporting; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using BookGen.Cli.CrashReporting; using Microsoft.Extensions.Logging; diff --git a/Source/BookGen/Infrastructure/Loging/RingBuffer.cs b/Source/BookGen/Infrastructure/Loging/RingBuffer.cs index e826281d..862e9593 100644 --- a/Source/BookGen/Infrastructure/Loging/RingBuffer.cs +++ b/Source/BookGen/Infrastructure/Loging/RingBuffer.cs @@ -1,4 +1,9 @@ -namespace BookGen.Infrastructure.Loging; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Infrastructure.Loging; internal sealed class RingBuffer { diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 5e42bc9a..56e955d8 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -170,8 +170,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.151.2", - "contentHash": "hDGy20k1DJZuKcdbHjwaOfsPeQYY9ty8wJeG1qzJWidPUzrwwvVtTLpDdTiP1M89UWbEYyWAhZo9qOaoYae6iQ==" + "resolved": "4.152.0", + "contentHash": "Vjx2sPleQDJXQjINxMzHaIU1KFyYAUerfBnwLAjMGSUCf0vCrSU2Y4LvCH0yAheeuu0YdlVfeSMiCKZYVJ12kA==" }, "Spectre.Console.Ansi": { "type": "Transitive", @@ -236,7 +236,7 @@ "bookgen.lib": { "type": "Project", "dependencies": { - "AngleSharp": "[1.7.3, )", + "AngleSharp": "[1.8.1, )", "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", "Markdig": "[1.3.2, )", @@ -244,9 +244,9 @@ "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1.1, )", "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1.1, )", "PreMailer.Net": "[2.7.4, )", - "SkiaSharp": "[4.151.2, )", - "SkiaSharp.NativeAssets.Linux": "[4.151.2, )", - "SkiaSharp.NativeAssets.Win32": "[4.151.2, )", + "SkiaSharp": "[4.152.0, )", + "SkiaSharp.NativeAssets.Linux": "[4.152.0, )", + "SkiaSharp.NativeAssets.Win32": "[4.152.0, )", "Svg.Skia": "[5.2.3, )", "System.ServiceModel.Syndication": "[11.0.0-preview.7.26381.103, )", "YamlDotNet": "[18.1.0, )" @@ -264,9 +264,9 @@ }, "AngleSharp": { "type": "CentralTransitive", - "requested": "[1.7.3, )", - "resolved": "1.7.3", - "contentHash": "I8TpSvgOPAVQHjVvXaROZOkIGZdX0tn+e9CUix6mv5pMqfyKO+/zm/dofsXgsxVTJl/sSTnEMFwrdnqxTt2axQ==" + "requested": "[1.8.1, )", + "resolved": "1.8.1", + "contentHash": "fTLPBxwYK5zmt+ernY3GL1kFBDA/2Cl72hpNnMHeNceZNJ/AT7Km9CXFaMHybuEFhXDy2Dv7Bjkez5G0Z4Io4g==" }, "Microsoft.ClearScript": { "type": "CentralTransitive", @@ -304,25 +304,25 @@ }, "SkiaSharp": { "type": "CentralTransitive", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "zqcEy5E17Tqoh2tEvb4uC2G3ulli5YdsljzZuf93pCc/9gQDMx9VrUInxiqkLMGt+MjQi8hNrf1Uxmq25yyU3A==", + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "TfYpxUV/Ihe5WyGeddJROZA2RhfGaSEKKNDHhREUVCVihtTlcavHQEi29Q0Mtn37AYXAf7EbC+StVPmWk63tkQ==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.151.2", - "SkiaSharp.NativeAssets.macOS": "4.151.2" + "SkiaSharp.NativeAssets.Win32": "4.152.0", + "SkiaSharp.NativeAssets.macOS": "4.152.0" } }, "SkiaSharp.NativeAssets.Linux": { "type": "CentralTransitive", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "TSeXS5j73N9+avlMn72Q8DC+EGi2LW7Gh7INHTkTo4bVpkEQu32D7tTWvP5LhZ+Fto9Ld6lih7UMwm+V3AQyCQ==" + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "B3713AE9SBHD55nMsaPlXY2C+GvsPsG50SxM/7iYayCvR3WqrQfgrSD4w/cJ9azGVrqrD2mx64esAQxVqK16dA==" }, "SkiaSharp.NativeAssets.Win32": { "type": "CentralTransitive", - "requested": "[4.151.2, )", - "resolved": "4.151.2", - "contentHash": "sTUHSfWXVYThf/lTG81n8y1gMnr0TiN/2zrApwd0jbGfbsEt9g4+GPsj6UVjjgNLXCI9l5CyzxQ6Zh4fP9ZoVQ==" + "requested": "[4.152.0, )", + "resolved": "4.152.0", + "contentHash": "PfyLS6TRP/9YghtW97le8bkzootCietlt7QUqFKGjGcZ6eRjeWNNHCjvxO41O/LYpV4gCthp2xiiUiYTOqfVyA==" }, "Svg.Skia": { "type": "CentralTransitive", diff --git a/Test/Bookgen.Tests/Commands/UT_Script.cs b/Test/Bookgen.Tests/Commands/UT_Script.cs index 940452ad..3a67f840 100644 --- a/Test/Bookgen.Tests/Commands/UT_Script.cs +++ b/Test/Bookgen.Tests/Commands/UT_Script.cs @@ -1,4 +1,9 @@ -using System.IO.Pipes; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.IO.Pipes; using BookGen; using BookGen.Commands; From 39f68c45bac9aaa6577d9d766eed32c01145ae72 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 20 Sep 2026 14:22:51 +0200 Subject: [PATCH 30/32] Package updates --- Directory.Packages.props | 22 +++--- Source/BookGen.Lib/packages.lock.json | 44 ++++++------ Source/BookGen.Shellprog/packages.lock.json | 14 ++-- Source/BookGen/packages.lock.json | 76 ++++++++++----------- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b835e256..14d95cc7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,32 +4,32 @@ true - + - + - - - + + + - + - + - - - + + + - + diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index 5ccf2b40..d026c1e3 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -4,15 +4,15 @@ "net11.0": { "AngleSharp": { "type": "Direct", - "requested": "[1.8.1, )", - "resolved": "1.8.1", - "contentHash": "fTLPBxwYK5zmt+ernY3GL1kFBDA/2Cl72hpNnMHeNceZNJ/AT7Km9CXFaMHybuEFhXDy2Dv7Bjkez5G0Z4Io4g==" + "requested": "[1.8.2, )", + "resolved": "1.8.2", + "contentHash": "iRlkfBIhi+gTgTxBlYNT4PlmaiVEy7+qX3BmprAK4cYE1UjfSNFI4mEDyxC1qgtAKYg5rjGYgL3tJlzGF+Ukdg==" }, "Markdig": { "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "fZgOC/3CswUrndjDTac70aQpYdtxbW5+5bRumR7vzvI2HJbkmgKisB1c9oT+GA6v0jB/JDR9BLa9FiPzQmaK6A==" + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "lLhaiI/mNDTNzgLqnmknhf4LjgTqOAyjx5+GNz0X3AxYMxUhmFppF2WVMdER+SDqwhE5wUF1pzs/cuLwFxs7xA==" }, "Microsoft.ClearScript": { "type": "Direct", @@ -56,25 +56,25 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "TfYpxUV/Ihe5WyGeddJROZA2RhfGaSEKKNDHhREUVCVihtTlcavHQEi29Q0Mtn37AYXAf7EbC+StVPmWk63tkQ==", + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "p3rj8SVoa+2Tmx1EITE4jIwCDPcYjwXdZ3c9yM6l9wNn6572hwxzrth6WOBK0iGnAoTGoJBZuZpy8wYjbov6sw==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.152.0", - "SkiaSharp.NativeAssets.macOS": "4.152.0" + "SkiaSharp.NativeAssets.Win32": "4.152.1", + "SkiaSharp.NativeAssets.macOS": "4.152.1" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "B3713AE9SBHD55nMsaPlXY2C+GvsPsG50SxM/7iYayCvR3WqrQfgrSD4w/cJ9azGVrqrD2mx64esAQxVqK16dA==" + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "1bB3eedb+4ZcyWigUzxXCE7LrNOlmyAs1crGCnX8UEHKl0nHOmzDt3E0d53IIgiguS88RKeCPy1ArDXK5FoDCw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Direct", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "PfyLS6TRP/9YghtW97le8bkzootCietlt7QUqFKGjGcZ6eRjeWNNHCjvxO41O/LYpV4gCthp2xiiUiYTOqfVyA==" + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "MLDP6XT8CHLHF6gZlpW06iswGNBlL/JB2+SKuSLz0Y0k1zzsfofNyUY6J6FC32A4/F67E9S8MwovRBL+ELw0lw==" }, "Svg.Skia": { "type": "Direct", @@ -95,9 +95,9 @@ }, "System.ServiceModel.Syndication": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "FEtlmar1uSt6u+qmefCO77alkcf4/IoK0qu0bmZ7GfL3vWDAXmBZUxM5uaJvoGoqGDOiKec7s9Qg9N1aH8dCrw==" + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "Dwf0Sck+i9Ucm3bKik3PHJSa8U91Of4dTT22VwOapjvEOfg/WeTukvLa//yEmgOniOg1GBnbfY9IG4TbvHqsSw==" }, "YamlDotNet": { "type": "Direct", @@ -187,8 +187,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.152.0", - "contentHash": "Vjx2sPleQDJXQjINxMzHaIU1KFyYAUerfBnwLAjMGSUCf0vCrSU2Y4LvCH0yAheeuu0YdlVfeSMiCKZYVJ12kA==" + "resolved": "4.152.1", + "contentHash": "gKaE2zNHsPLjlA88ZClRNmtZlHNGEa/W4iPy5GpP1TtUELIXzrSr2udCbl1GWYFuCdA36IycBk5YhUuK1xi9UA==" }, "Spectre.Console.Ansi": { "type": "Transitive", diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index 1be70daa..7968abaf 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -4,17 +4,17 @@ "net11.0": { "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "IgS1YfEd2i0p/zY8ytcW1zaFR3ivC0M4Ce3JnpZoKpzK9MogNkHb9BZPqYtq0mDuCe2ouirh6058glgPxhwD8g==" + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "r9VvtUpwM0MZ21DaTQWDqmsm6ABmuuXA/4aqa9LJVZET0HGlIG0Z5eN2esarFAQW2xtgGFyIYcnWs5uxUUizgg==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "VoYsG/1Gt7kpSeno7eUrnLKfusYZf9L8qSnYtMNh1ClwXKIfKU6+g+Lq23VeanJP2ieQyPBH0xtSzay8J7r9lQ==", + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "SxnI3xH/jZ11sRY5l1DqaqH4z7tyEqAa0ehQ6xsBGenst1mFy91iGWGZfUA0MnyPxQCdjmAlvPmuCvLsrv/ufw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.7.26381.103" + "Microsoft.Extensions.DependencyInjection": "11.0.0-rc.1.26425.128" } }, "Roslynator.Analyzers": { diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 56e955d8..d7e6f196 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -14,29 +14,29 @@ }, "Markdig": { "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "fZgOC/3CswUrndjDTac70aQpYdtxbW5+5bRumR7vzvI2HJbkmgKisB1c9oT+GA6v0jB/JDR9BLa9FiPzQmaK6A==" + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "lLhaiI/mNDTNzgLqnmknhf4LjgTqOAyjx5+GNz0X3AxYMxUhmFppF2WVMdER+SDqwhE5wUF1pzs/cuLwFxs7xA==" }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "K3JeRtSGa85pWo+IrTHwv5PZvA7mHitQxvC83CT9gvrsZIJj0BTV0tLfHRAn3lL/o4gw7nJ6u681QkWhKISzlg==" + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "vm0cIPeFy1TZWcr7xLY/aqvZmXZ63GvAuieN0Fec1Fz+P/vi1sPFdRTJEbJ/NrOP+DduYE/1Ba7Wv0V01g1hdQ==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "IgS1YfEd2i0p/zY8ytcW1zaFR3ivC0M4Ce3JnpZoKpzK9MogNkHb9BZPqYtq0mDuCe2ouirh6058glgPxhwD8g==" + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "r9VvtUpwM0MZ21DaTQWDqmsm6ABmuuXA/4aqa9LJVZET0HGlIG0Z5eN2esarFAQW2xtgGFyIYcnWs5uxUUizgg==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "VoYsG/1Gt7kpSeno7eUrnLKfusYZf9L8qSnYtMNh1ClwXKIfKU6+g+Lq23VeanJP2ieQyPBH0xtSzay8J7r9lQ==", + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "SxnI3xH/jZ11sRY5l1DqaqH4z7tyEqAa0ehQ6xsBGenst1mFy91iGWGZfUA0MnyPxQCdjmAlvPmuCvLsrv/ufw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.7.26381.103" + "Microsoft.Extensions.DependencyInjection": "11.0.0-rc.1.26425.128" } }, "Microsoft.IO.RecyclableMemoryStream": { @@ -170,8 +170,8 @@ }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "4.152.0", - "contentHash": "Vjx2sPleQDJXQjINxMzHaIU1KFyYAUerfBnwLAjMGSUCf0vCrSU2Y4LvCH0yAheeuu0YdlVfeSMiCKZYVJ12kA==" + "resolved": "4.152.1", + "contentHash": "gKaE2zNHsPLjlA88ZClRNmtZlHNGEa/W4iPy5GpP1TtUELIXzrSr2udCbl1GWYFuCdA36IycBk5YhUuK1xi9UA==" }, "Spectre.Console.Ansi": { "type": "Transitive", @@ -236,19 +236,19 @@ "bookgen.lib": { "type": "Project", "dependencies": { - "AngleSharp": "[1.8.1, )", + "AngleSharp": "[1.8.2, )", "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", - "Markdig": "[1.3.2, )", + "Markdig": "[1.4.0, )", "Microsoft.ClearScript": "[7.5.1.1, )", "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.1.1, )", "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.1.1, )", "PreMailer.Net": "[2.7.4, )", - "SkiaSharp": "[4.152.0, )", - "SkiaSharp.NativeAssets.Linux": "[4.152.0, )", - "SkiaSharp.NativeAssets.Win32": "[4.152.0, )", + "SkiaSharp": "[4.152.1, )", + "SkiaSharp.NativeAssets.Linux": "[4.152.1, )", + "SkiaSharp.NativeAssets.Win32": "[4.152.1, )", "Svg.Skia": "[5.2.3, )", - "System.ServiceModel.Syndication": "[11.0.0-preview.7.26381.103, )", + "System.ServiceModel.Syndication": "[11.0.0-rc.1.26425.128, )", "YamlDotNet": "[18.1.0, )" } }, @@ -264,9 +264,9 @@ }, "AngleSharp": { "type": "CentralTransitive", - "requested": "[1.8.1, )", - "resolved": "1.8.1", - "contentHash": "fTLPBxwYK5zmt+ernY3GL1kFBDA/2Cl72hpNnMHeNceZNJ/AT7Km9CXFaMHybuEFhXDy2Dv7Bjkez5G0Z4Io4g==" + "requested": "[1.8.2, )", + "resolved": "1.8.2", + "contentHash": "iRlkfBIhi+gTgTxBlYNT4PlmaiVEy7+qX3BmprAK4cYE1UjfSNFI4mEDyxC1qgtAKYg5rjGYgL3tJlzGF+Ukdg==" }, "Microsoft.ClearScript": { "type": "CentralTransitive", @@ -304,25 +304,25 @@ }, "SkiaSharp": { "type": "CentralTransitive", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "TfYpxUV/Ihe5WyGeddJROZA2RhfGaSEKKNDHhREUVCVihtTlcavHQEi29Q0Mtn37AYXAf7EbC+StVPmWk63tkQ==", + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "p3rj8SVoa+2Tmx1EITE4jIwCDPcYjwXdZ3c9yM6l9wNn6572hwxzrth6WOBK0iGnAoTGoJBZuZpy8wYjbov6sw==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "4.152.0", - "SkiaSharp.NativeAssets.macOS": "4.152.0" + "SkiaSharp.NativeAssets.Win32": "4.152.1", + "SkiaSharp.NativeAssets.macOS": "4.152.1" } }, "SkiaSharp.NativeAssets.Linux": { "type": "CentralTransitive", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "B3713AE9SBHD55nMsaPlXY2C+GvsPsG50SxM/7iYayCvR3WqrQfgrSD4w/cJ9azGVrqrD2mx64esAQxVqK16dA==" + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "1bB3eedb+4ZcyWigUzxXCE7LrNOlmyAs1crGCnX8UEHKl0nHOmzDt3E0d53IIgiguS88RKeCPy1ArDXK5FoDCw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "CentralTransitive", - "requested": "[4.152.0, )", - "resolved": "4.152.0", - "contentHash": "PfyLS6TRP/9YghtW97le8bkzootCietlt7QUqFKGjGcZ6eRjeWNNHCjvxO41O/LYpV4gCthp2xiiUiYTOqfVyA==" + "requested": "[4.152.1, )", + "resolved": "4.152.1", + "contentHash": "MLDP6XT8CHLHF6gZlpW06iswGNBlL/JB2+SKuSLz0Y0k1zzsfofNyUY6J6FC32A4/F67E9S8MwovRBL+ELw0lw==" }, "Svg.Skia": { "type": "CentralTransitive", @@ -343,9 +343,9 @@ }, "System.ServiceModel.Syndication": { "type": "CentralTransitive", - "requested": "[11.0.0-preview.7.26381.103, )", - "resolved": "11.0.0-preview.7.26381.103", - "contentHash": "FEtlmar1uSt6u+qmefCO77alkcf4/IoK0qu0bmZ7GfL3vWDAXmBZUxM5uaJvoGoqGDOiKec7s9Qg9N1aH8dCrw==" + "requested": "[11.0.0-rc.1.26425.128, )", + "resolved": "11.0.0-rc.1.26425.128", + "contentHash": "Dwf0Sck+i9Ucm3bKik3PHJSa8U91Of4dTT22VwOapjvEOfg/WeTukvLa//yEmgOniOg1GBnbfY9IG4TbvHqsSw==" }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", From 9e16495cd396f128af3361336d19bfd013574a52 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 24 Sep 2026 18:38:52 +0200 Subject: [PATCH 31/32] Architecture docs --- Architecture.md | 211 ++++++++++++++++++++++++++++++++++++++++++++++++ BookGen.slnx | 1 + 2 files changed, 212 insertions(+) create mode 100644 Architecture.md diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 00000000..c2d4ebcc --- /dev/null +++ b/Architecture.md @@ -0,0 +1,211 @@ +# BookGen Architecture + +## 1. Introduction and Goals + +BookGen is a command-line toolchain for generating books and documentation from Markdown sources. + +Primary goals: +- Provide reproducible builds for multiple output formats (web, epub, feed, print, WordPress export). +- Keep command handling extensible and testable. +- Support plugin-based custom builds through a stable API contract (`BookGen.Api`, netstandard2.1). +- Offer shell and script automation support for author workflows. + +## 2. Architecture Constraints + +- Core runtime targets `.NET 11` (`net11.0`) for executables and main libraries. +- Plugin API targets `.NET Standard 2.1` for compatibility (`BookGen.Api`). +- CLI-first architecture with dependency injection (`Microsoft.Extensions.DependencyInjection`). +- Rendering and pipeline logic is in reusable libraries, not in program entry points. +- Plugins are loaded dynamically and isolated via `AssemblyLoadContext`. + +## 3. System Scope and Context + +### 3.1 Business Context +Book authors and documentation teams use BookGen to transform Markdown + configuration into publishable outputs. + +```nomnoml +[Author/CI/CD] -> [BookGen CLI] +[BookGen CLI] -> [Book Sources\n(config.json, toc.json, md files)] +[BookGen CLI] -> [Generated Outputs\n(web, epub, feed, print, wordpress)] +[BookGen CLI] -> [Plugin Package (.plugin/.dll)] +``` + +### 3.2 Technical Context + +```nomnoml +[BookGen (Exe)] -> [BookGen.Cli] +[BookGen (Exe)] -> [BookGen.Lib] +[BookGen (Exe)] -> [BookGen.Vfs] +[BookGen (Exe)] -> [BookGen.Api] +[BookGen (Exe)] -> [BookGen.Shell.Shared] +[BookGen.Shellprog (Exe)] -> [BookGen.Cli] +[BookGen.Shellprog (Exe)] -> [BookGen.Shell.Shared] +[BookGen.Lib] -> [BookGen.Vfs] +[BookGen.SamplePlugin] -> [BookGen.Api] +[Test\nBookgen.Tests] -> [BookGen + Lib + Cli + Shell.Shared] +``` + +## 4. Solution Strategy + +- **Command orchestration layer**: `BookGen` + `BookGen.Cli` provide command discovery, parsing, validation, and execution. +- **Domain and build pipelines**: `BookGen.Lib` encapsulates environment initialization, config validation/upgrades, markdown rendering, and multi-step output pipelines. +- **I/O abstraction**: `BookGen.Vfs` provides scoped filesystem interfaces (`IReadOnlyFileSystem`, `IWritableFileSystem`) used across commands and pipelines. +- **Extensibility**: `BookGen.Api` defines plugin contracts; `BookGen.Infrastructure.Plugins` loads and executes plugin implementations. +- **Shell integration**: `BookGen.Shellprog` and `BookGen.Shell.Shared` support interactive and shell-oriented workflows. + +## 5. Building Block View + +### 5.1 Level 1 +```nomnoml +[BookGen Runtime| +- CLI command execution +- Build command entry points +- DI wiring] +[Library Core| +- BookEnvironment +- Pipeline steps +- Rendering] +[Plugin Surface| +- BookGen.Api contracts +- Plugin loader] +[Infrastructure Support| +- VFS +- Shell helpers +- Packaged assets] + +[BookGen Runtime] -> [Library Core] +[BookGen Runtime] -> [Plugin Surface] +[BookGen Runtime] -> [Infrastructure Support] +``` + +### 5.2 Level 2 (Main executable internals) +```nomnoml +[Program.cs] +[CommandRunner] +[BuildCommandBase + Commands] +[BookEnvironment] +[Pipeline] +[PluginRunner] + +[Program.cs] -> [CommandRunner] +[CommandRunner] -> [BuildCommandBase + Commands] +[BuildCommandBase + Commands] -> [BookEnvironment] +[BuildCommandBase + Commands] -> [Pipeline] +[BuildCommandBase + Commands] -> [PluginRunner] +``` + +### 5.3 Key Project Responsibilities + +- `Source/BookGen`: main executable, command implementations, DI composition, plugin invocation. +- `Source/BookGen.Cli`: command framework (`CommandRunner`, command tree, global options, validation). +- `Source/BookGen.Lib`: core domain model, config migration/validation, rendering, pipelines, preview HTTP support. +- `Source/BookGen.Vfs`: file system abstraction with scoped access. +- `Source/BookGen.Api`: stable plugin contracts for external builders. +- `Source/BookGen.Shellprog`: shell helper executable. +- `Source/BookGen.Shell.Shared`: shared shell/logging/browser/process helpers. +- `Source/BookGen.Contents`: distributable bundled content and tool assets. +- `Test/Bookgen.Tests`: NUnit test suite across CLI and command behavior. + +## 6. Runtime View + +### 6.1 Scenario: `build web` + +```nomnoml +[User] -> [BookGen Program] +[BookGen Program] -> [CommandRunner] +[CommandRunner] -> [BuildWebCommand] +[BuildWebCommand] -> [BookEnvironment.Initialize] +[BuildWebCommand] -> [Pipeline.CreateWebPipeLine] +[Pipeline] -> [Steps: CopyAssets -> Render -> Index -> Pager] +[Pipeline] -> [Output Folder] +``` + +Flow summary: +1. `Program.cs` configures logging and services, then runs `CommandRunner`. +2. `BuildWebCommand` (via `BuildCommandBase`) sets source/output scopes. +3. `BookEnvironment.Initialize` validates config and TOC, applies optional overlay, acquires lock. +4. Web pipeline executes ordered steps from `BookGen.Lib.Pipeline.StaticWebsite`. +5. Generated files are written through `IWritableFileSystem`. + +### 6.2 Scenario: `build plugin` + +```nomnoml +[User] -> [BuildPlugin Command] +[BuildPlugin Command] -> [PluginPathResolver] +[BuildPlugin Command] -> [BookEnvironment.Initialize] +[BuildPlugin Command] -> [PluginRunner] +[PluginRunner] -> [Extract .plugin + manifest] +[PluginRunner] -> [AssemblyLoadContext] +[AssemblyLoadContext] -> [IBuildPluginV1.Build] +[IBuildPluginV1.Build] -> [Output Folder] +``` + +Flow summary: +1. Plugin package path is resolved (`.plugin` or `.dll` in dev mode). +2. Environment is initialized like regular builds. +3. Plugin runner validates package manifest and extracts payload. +4. Plugin assembly is loaded in collectible `AssemblyLoadContext`. +5. Exactly one `IBuildPluginV1` implementation is instantiated and executed. +6. Context is unloaded and GC-assisted cleanup is performed. + +## 7. Deployment View + +BookGen is primarily a local/CI process architecture. + +```nomnoml +[Developer Workstation / CI Agent| +- BookGen.exe +- BookGen.Shellprog.exe +- assets.zip / dictionaries.zip +- optional plugins/*.plugin] + +[Developer Workstation / CI Agent] -> [File System Workspace| +book sources + config + output] +``` + +Deployment characteristics: +- No mandatory long-running backend service. +- Outputs are static artifacts suitable for hosting elsewhere. +- Optional preview/server capabilities are hosted in-process when used. + +## 8. Cross-cutting Concepts + +- **Dependency Injection**: service wiring in executable entry points. +- **Logging**: configurable console/json/file logging with shared providers. +- **Validation**: argument validation in command args + configuration schema/domain validation. +- **Pipeline pattern**: deterministic ordered steps per output target. +- **Filesystem abstraction**: commands and pipelines depend on VFS interfaces, not raw `System.IO` calls. +- **Plugin isolation**: dynamic loading with unloadable contexts. +- **Asset packaging**: content and dictionaries shipped as zipped artifacts. + +## 9. Architecture Decisions + +- Use separate projects to isolate concerns (CLI framework, domain/rendering, plugin API, VFS, shell helpers). +- Keep plugin API in `netstandard2.1` to lower compatibility friction for plugin authors. +- Use a step-based pipeline model for output generation to keep build stages explicit and composable. +- Use command auto-discovery (`AddCommandsFrom`) plus explicit default command registration. +- Use scoped filesystem wrappers to centralize path safety and testability. + +## 10. Quality Requirements + +Top quality goals: +1. **Reliability**: deterministic command execution and clear exit codes. +2. **Extensibility**: plugin contracts and build-command architecture. +3. **Maintainability**: modular projects with focused responsibilities. +4. **Usability**: strong CLI help, shell completion support, script command. +5. **Portability**: cross-platform runtime support for core tooling and assets. + +## 11. Risks and Technical Debt + +- Dynamic plugin loading can fail due to missing dependencies or malformed manifests. +- Pipeline failures abort generation; diagnostics quality depends on step-level logging. +- External native/tool dependencies (rendering helpers, conversion tools) can vary by OS/runtime packaging. +- Version drift between plugin implementations and API contracts must be managed carefully. + +## 12. Glossary + +- **BookEnvironment**: runtime context containing configuration, TOC, source/output scopes, and assets. +- **Pipeline**: ordered set of build steps producing one output format. +- **IBuildPluginV1**: plugin entry contract for custom build behavior. +- **VFS**: virtual/scoped file system abstraction used by commands and pipelines. +- **Shellprog**: helper executable for shell-oriented workflows and command interaction. \ No newline at end of file diff --git a/BookGen.slnx b/BookGen.slnx index 415dee62..edec6008 100644 --- a/BookGen.slnx +++ b/BookGen.slnx @@ -1,5 +1,6 @@ + From db0c1be3a3ef2b22c8e9110fe0f85a50520902cb Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 24 Sep 2026 18:40:39 +0200 Subject: [PATCH 32/32] Package updates --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 14d95cc7..e9ee94b1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - +