From 44f23ded25e2a30199936fbd9279fe06bf9d4f37 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 23 Jul 2026 18:23:53 +0200 Subject: [PATCH 01/18] File system observer abstraction --- Source/BookGen.Cli/packages.lock.json | 5 +- Source/BookGen.Shellprog/packages.lock.json | 5 +- Source/BookGen.Vfs/BookGen.Vfs.csproj | 1 + Source/BookGen.Vfs/FileSystemChange.cs | 16 ++++ Source/BookGen.Vfs/FileSystemObserver.cs | 80 +++++++++++++++++++ Source/BookGen.Vfs/IFileSystemObserver.cs | 6 ++ Source/BookGen.Vfs/IReadOnlyFileSystem.cs | 3 + .../BookGen.Vfs/MultiReadScopeFileSystem.cs | 13 +++ Source/BookGen.Vfs/ReadOnlyFileSystem.cs | 9 +++ Source/BookGen/packages.lock.json | 5 +- Test/Bookgen.Tests/EmbeddedTestFolder.cs | 7 ++ 11 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 Source/BookGen.Vfs/FileSystemChange.cs create mode 100644 Source/BookGen.Vfs/FileSystemObserver.cs create mode 100644 Source/BookGen.Vfs/IFileSystemObserver.cs diff --git a/Source/BookGen.Cli/packages.lock.json b/Source/BookGen.Cli/packages.lock.json index 6510ee4d..6e7fe45f 100644 --- a/Source/BookGen.Cli/packages.lock.json +++ b/Source/BookGen.Cli/packages.lock.json @@ -24,7 +24,10 @@ "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "bookgen.vfs": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" + } } } } diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index 402e9f33..d3977c21 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -97,7 +97,10 @@ } }, "bookgen.vfs": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" + } }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", diff --git a/Source/BookGen.Vfs/BookGen.Vfs.csproj b/Source/BookGen.Vfs/BookGen.Vfs.csproj index 0eef9a3a..9a21a923 100644 --- a/Source/BookGen.Vfs/BookGen.Vfs.csproj +++ b/Source/BookGen.Vfs/BookGen.Vfs.csproj @@ -15,6 +15,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/BookGen.Vfs/FileSystemChange.cs b/Source/BookGen.Vfs/FileSystemChange.cs new file mode 100644 index 00000000..1a633876 --- /dev/null +++ b/Source/BookGen.Vfs/FileSystemChange.cs @@ -0,0 +1,16 @@ +namespace BookGen.Vfs; + +public sealed class FileSystemChangeEventArgs : EventArgs +{ + public required string FileName { get; init; } + public string? NewFileName { get; init; } + public required Change ChangeType { get; init; } + + public enum Change + { + Created, + Changed, + Deleted, + Renamed + } +} diff --git a/Source/BookGen.Vfs/FileSystemObserver.cs b/Source/BookGen.Vfs/FileSystemObserver.cs new file mode 100644 index 00000000..b04a2601 --- /dev/null +++ b/Source/BookGen.Vfs/FileSystemObserver.cs @@ -0,0 +1,80 @@ +using System.Diagnostics; + +using Microsoft.Extensions.Logging; + +namespace BookGen.Vfs; + +public sealed class FileSystemObserver : IFileSystemObserver +{ + private readonly FileSystemWatcher _watcher; + private readonly ILogger _logger; + + public FileSystemObserver(ILogger logger, string folder, string filter) + { + _watcher = new FileSystemWatcher + { + Filter = filter, + Path = folder, + NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastWrite + | NotifyFilters.Size, + IncludeSubdirectories = true, + EnableRaisingEvents = true + }; + _watcher.Changed += OnChanged; + _watcher.Created += OnChanged; + _watcher.Deleted += OnChanged; + _watcher.Renamed += OnRenamed; + _watcher.Error += OnError; + _logger = logger; + } + + public void Dispose() + { + _watcher.Changed -= OnChanged; + _watcher.Created -= OnChanged; + _watcher.Deleted -= OnChanged; + _watcher.Renamed -= OnRenamed; + _watcher.Error -= OnError; + _watcher.Dispose(); + } + + public event EventHandler? FileChanged; + + private void OnError(object sender, ErrorEventArgs e) + { + _logger.LogError(e.GetException(), "File system watcher error"); + } + + private void OnRenamed(object sender, RenamedEventArgs e) + { + FileChanged?.Invoke(this, new FileSystemChangeEventArgs + { + FileName = e.OldFullPath, + NewFileName = e.FullPath, + ChangeType = FileSystemChangeEventArgs.Change.Renamed, + }); + } + + private void OnChanged(object sender, FileSystemEventArgs e) + { + FileChanged?.Invoke(this, new FileSystemChangeEventArgs + { + FileName = e.FullPath, + ChangeType = Map(e.ChangeType), + }); + } + + private static FileSystemChangeEventArgs.Change Map(WatcherChangeTypes changeType) + { + return changeType switch + { + WatcherChangeTypes.Created => FileSystemChangeEventArgs.Change.Created, + WatcherChangeTypes.Changed => FileSystemChangeEventArgs.Change.Changed, + WatcherChangeTypes.Deleted => FileSystemChangeEventArgs.Change.Deleted, + _ => throw new UnreachableException() + }; + } +} diff --git a/Source/BookGen.Vfs/IFileSystemObserver.cs b/Source/BookGen.Vfs/IFileSystemObserver.cs new file mode 100644 index 00000000..267acd1e --- /dev/null +++ b/Source/BookGen.Vfs/IFileSystemObserver.cs @@ -0,0 +1,6 @@ +namespace BookGen.Vfs; + +public interface IFileSystemObserver : IDisposable +{ + event EventHandler? FileChanged; +} diff --git a/Source/BookGen.Vfs/IReadOnlyFileSystem.cs b/Source/BookGen.Vfs/IReadOnlyFileSystem.cs index 51cf4509..746e3e70 100644 --- a/Source/BookGen.Vfs/IReadOnlyFileSystem.cs +++ b/Source/BookGen.Vfs/IReadOnlyFileSystem.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using Microsoft.Extensions.Logging; + namespace BookGen.Vfs; public interface IReadOnlyFileSystem @@ -18,4 +20,5 @@ public interface IReadOnlyFileSystem bool DirectoryExists(string path); IEnumerable GetFiles(string path, string filter, bool recursive); IEnumerable GetDirectories(string path, bool recursive); + IFileSystemObserver CreateObserver(ILogger logger, string filter = "*.*"); } diff --git a/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs b/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs index 1f683814..a4cad351 100644 --- a/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs +++ b/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using Microsoft.Extensions.Logging; + namespace BookGen.Vfs; public sealed class MultiReadScopeFileSystem : IReadOnlyFileSystem @@ -119,4 +121,15 @@ public async Task ReadAllTextAsync(string path) var actualPath = Resolve(path); return await File.ReadAllTextAsync(actualPath); } + + public IFileSystemObserver CreateObserver(ILogger logger, string filter = "*.*") + { + if (_scopes.Count < 1) + throw new InvalidOperationException("Scope must be defined to create a file system observer."); + + if (_scopes.Count > 1) + throw new InvalidOperationException("MultiReadScopeFileSystem does not support file system observers with multiple scopes."); + + return new FileSystemObserver(logger, Path.GetFullPath(_scopes.First()), filter); + } } diff --git a/Source/BookGen.Vfs/ReadOnlyFileSystem.cs b/Source/BookGen.Vfs/ReadOnlyFileSystem.cs index 2a506fee..852cd9a1 100644 --- a/Source/BookGen.Vfs/ReadOnlyFileSystem.cs +++ b/Source/BookGen.Vfs/ReadOnlyFileSystem.cs @@ -5,6 +5,8 @@ using System.Diagnostics; +using Microsoft.Extensions.Logging; + namespace BookGen.Vfs; [DebuggerDisplay("{Scope}")] @@ -103,4 +105,11 @@ public async Task ReadAllTextAsync(string path) var actualPath = GetAndValidateFullNameInScope(path); return await File.ReadAllTextAsync(actualPath); } + + public IFileSystemObserver CreateObserver(ILogger logger, string filter = "*.*") + { + return string.IsNullOrEmpty(Scope) + ? throw new InvalidOperationException("Scope must be defined to create a file system observer.") + : (IFileSystemObserver)new FileSystemObserver(logger, Path.GetFullPath(Scope), filter); + } } diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index b7e2c379..72f446c7 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -314,7 +314,10 @@ } }, "bookgen.vfs": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" + } }, "AngleSharp": { "type": "CentralTransitive", diff --git a/Test/Bookgen.Tests/EmbeddedTestFolder.cs b/Test/Bookgen.Tests/EmbeddedTestFolder.cs index 453b7d5f..2cfa49d7 100644 --- a/Test/Bookgen.Tests/EmbeddedTestFolder.cs +++ b/Test/Bookgen.Tests/EmbeddedTestFolder.cs @@ -5,6 +5,8 @@ using BookGen.Vfs; +using Microsoft.Extensions.Logging; + namespace Bookgen.Tests; internal class EmbeddedTestFileSystem : IReadOnlyFileSystem @@ -21,6 +23,11 @@ public EmbeddedTestFileSystem() public string Scope { get; set; } + public IFileSystemObserver CreateObserver(ILogger logger, string filter = "*.*") + { + throw new NotImplementedException(); + } + public bool DirectoryExists(string path) { throw new NotImplementedException(); From 64cb4ac64f2cc14be6d00c6f404c23baf38c5753 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 23 Jul 2026 18:39:35 +0200 Subject: [PATCH 02/18] Work on previes server --- Source/BookGen.Lib/Http/HttpServer.cs | 1 - .../Bookgen.Lib/Preview/PreviewDataStore.cs | 69 +++++++++++++++++++ Source/Bookgen.Lib/Preview/PreviewServer.cs | 41 +++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Source/Bookgen.Lib/Preview/PreviewDataStore.cs create mode 100644 Source/Bookgen.Lib/Preview/PreviewServer.cs diff --git a/Source/BookGen.Lib/Http/HttpServer.cs b/Source/BookGen.Lib/Http/HttpServer.cs index fa7977c6..392a5667 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; diff --git a/Source/Bookgen.Lib/Preview/PreviewDataStore.cs b/Source/Bookgen.Lib/Preview/PreviewDataStore.cs new file mode 100644 index 00000000..48be062d --- /dev/null +++ b/Source/Bookgen.Lib/Preview/PreviewDataStore.cs @@ -0,0 +1,69 @@ +using BookGen.Vfs; + +using Microsoft.Extensions.Caching.Memory; + +namespace BookGen.Lib.Preview; + +internal class PreviewDataStore +{ + private readonly IMemoryCache _memoryCache; + private readonly List _availableFiles; + + public PreviewDataStore(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + _availableFiles = new List(); + } + + public IReadOnlyList Files + => _availableFiles; + + private string GetCacheKey(string path) + => $"preview_{path}"; + + public void UpdateAvailableFiles(IEnumerable enumerable) + { + _availableFiles.Clear(); + _availableFiles.AddRange(enumerable); + } + + public void UpdateWith(FileSystemChangeEventArgs e) + { + //Invalidate previous rendering + if (!string.IsNullOrEmpty(e.FileName)) + { + _memoryCache.Remove(GetCacheKey(e.FileName)); + } + if (!string.IsNullOrEmpty(e.NewFileName)) + { + _memoryCache.Remove(GetCacheKey(e.NewFileName)); + } + + switch (e.ChangeType) + { + case FileSystemChangeEventArgs.Change.Created: + _availableFiles.Add(e.FileName); + break; + case FileSystemChangeEventArgs.Change.Changed: + // No action needed for changed files + break; + case FileSystemChangeEventArgs.Change.Deleted: + _availableFiles.Remove(e.FileName); + break; + case FileSystemChangeEventArgs.Change.Renamed: + if (!string.IsNullOrEmpty(e.NewFileName)) + { + int idx = _availableFiles.IndexOf(e.FileName); + if (idx != -1) + { + _availableFiles[idx] = e.NewFileName; + } + else + { + _availableFiles.Add(e.NewFileName); + } + } + break; + } + } +} diff --git a/Source/Bookgen.Lib/Preview/PreviewServer.cs b/Source/Bookgen.Lib/Preview/PreviewServer.cs new file mode 100644 index 00000000..3686114a --- /dev/null +++ b/Source/Bookgen.Lib/Preview/PreviewServer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using BookGen.Vfs; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +namespace BookGen.Lib.Preview; + +internal class PreviewServer : IDisposable +{ + private readonly ILogger _logger; + private readonly PreviewDataStore _dataStore; + private IFileSystemObserver? _fileSystemObserver; + + public PreviewServer(ILogger logger, IMemoryCache memoryCache) + { + _logger = logger; + _dataStore = new PreviewDataStore(memoryCache); + } + + public void Dispose() + { + _fileSystemObserver?.FileChanged -= OnFileChange; + _fileSystemObserver?.Dispose(); + } + + public async Task Start(IReadOnlyFileSystem fileSystem) + { + _dataStore.UpdateAvailableFiles(fileSystem.GetFiles(fileSystem.Scope, "*.md", true)); + _fileSystemObserver = fileSystem.CreateObserver(_logger, "*.md"); + _fileSystemObserver.FileChanged += OnFileChange; + } + + private void OnFileChange(object? sender, FileSystemChangeEventArgs e) + { + _dataStore.UpdateWith(e); + } +} From 251a2372b8d229ee2351b271fbb78c434a517228 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 24 Jul 2026 17:13:08 +0200 Subject: [PATCH 03/18] Work on preview routing --- Source/BookGen.Lib/Http/HttpServer.cs | 22 ++- Source/BookGen.Lib/Http/MimeTypes.cs | 4 - Source/Bookgen.Lib/DisposableTracker.cs | 27 +++ Source/Bookgen.Lib/Http/PreviewRoutes.cs | 166 ++++++++++++++++++ Source/Bookgen.Lib/Http/RouteProvider.cs | 8 + .../Bookgen.Lib/Preview/PreviewDataStore.cs | 69 -------- Source/Bookgen.Lib/Preview/PreviewServer.cs | 41 ----- 7 files changed, 219 insertions(+), 118 deletions(-) create mode 100644 Source/Bookgen.Lib/DisposableTracker.cs create mode 100644 Source/Bookgen.Lib/Http/PreviewRoutes.cs create mode 100644 Source/Bookgen.Lib/Http/RouteProvider.cs delete mode 100644 Source/Bookgen.Lib/Preview/PreviewDataStore.cs delete mode 100644 Source/Bookgen.Lib/Preview/PreviewServer.cs diff --git a/Source/BookGen.Lib/Http/HttpServer.cs b/Source/BookGen.Lib/Http/HttpServer.cs index 392a5667..51ec26ab 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -19,6 +19,7 @@ namespace BookGen.Lib.Http; internal sealed class HttpServer : IHttpServer { private readonly WebApplication _app; + private readonly DisposableTracker _disposableTracker; public int Port { get; } @@ -30,6 +31,7 @@ public HttpServer(int port, ILogger logger) builder.Logging.AddProvider(new LoggerProvider(logger)); #pragma warning restore CA2000 // Dispose objects before losing scope builder.WebHost.ConfigureKestrel((context, serverOptions) => serverOptions.ListenAnyIP(port)); + _disposableTracker = new DisposableTracker(); _app = builder.Build(); Port = port; @@ -61,6 +63,12 @@ public HttpServer(int port, ILogger logger) }); } + public ValueTask DisposeAsync() + { + _disposableTracker.Dispose(); + return _app.DisposeAsync(); + } + public void AddStaticFiles(string directory, string requestPath, bool directoryBrowseEnabled) { var options = new FileServerOptions @@ -122,7 +130,7 @@ public void AddRoute(ApiMetaData metaData, RequestDelegate handler) } } - public void AddRoutes(IReadOnlyDictionary routes) + public void AddRoutes(IEnumerable> routes) { foreach (KeyValuePair route in routes) { @@ -130,6 +138,15 @@ public void AddRoutes(IReadOnlyDictionary routes) } } + public void AddRoutes(IRouteProvider provider) + { + if (provider is IDisposable disposable) + { + _disposableTracker.Track(disposable); + } + AddRoutes(provider.Routes); + } + public IEnumerable GetListenUrls() { foreach ((IPAddress? adress, IPAddress _) in GetIpAdresses()) @@ -160,7 +177,4 @@ public async Task StartAsync() public async Task StopAsync() => await _app.StopAsync(); - - public ValueTask DisposeAsync() - => _app.DisposeAsync(); } diff --git a/Source/BookGen.Lib/Http/MimeTypes.cs b/Source/BookGen.Lib/Http/MimeTypes.cs index 8143a5bf..7cab2c01 100644 --- a/Source/BookGen.Lib/Http/MimeTypes.cs +++ b/Source/BookGen.Lib/Http/MimeTypes.cs @@ -3,10 +3,6 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- -using System.Diagnostics.CodeAnalysis; - -using Microsoft.AspNetCore.StaticFiles; - namespace BookGen.Lib.Http; /// diff --git a/Source/Bookgen.Lib/DisposableTracker.cs b/Source/Bookgen.Lib/DisposableTracker.cs new file mode 100644 index 00000000..dcbfdd2f --- /dev/null +++ b/Source/Bookgen.Lib/DisposableTracker.cs @@ -0,0 +1,27 @@ +namespace BookGen.Lib; + +internal sealed class DisposableTracker : IDisposable +{ + private readonly List _items = new(); + private readonly Lock _lock = new(); + + public void Dispose() + { + lock (_lock) + { + foreach (IDisposable item in _items) + { + item.Dispose(); + } + _items.Clear(); + } + } + + public void Track(T item) where T : IDisposable + { + lock (_lock) + { + _items.Add(item); + } + } +} diff --git a/Source/Bookgen.Lib/Http/PreviewRoutes.cs b/Source/Bookgen.Lib/Http/PreviewRoutes.cs new file mode 100644 index 00000000..2fa6d124 --- /dev/null +++ b/Source/Bookgen.Lib/Http/PreviewRoutes.cs @@ -0,0 +1,166 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Mime; + +using BookGen.Lib.AppSettings; +using BookGen.Lib.Domain.IO.Configuration; +using BookGen.Lib.Rendering; +using BookGen.Lib.Rendering.Images; +using BookGen.Lib.Rendering.Markdown; +using BookGen.Lib.Rendering.Markdown.RenderInterop; +using BookGen.Lib.Rendering.Templates; +using BookGen.Vfs; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace BookGen.Lib.Http; + +internal sealed class PreviewRoutes : IDisposable, IRouteProvider +{ + private readonly IReadOnlyFileSystem _source; + private readonly ILogger _logger; + private readonly MarkdownRenderSettings _renderSettings; + private readonly MarkdownConverter _markdownConverter; + private readonly TemplateEngine _templateEngine; + private readonly string _template; + + private readonly IFileSystemObserver _observer; + private readonly List _allowedFiles; + private readonly Lock _lock; + + public PreviewRoutes(IReadOnlyFileSystem source, + ILogger logger, + IProgramPathResolver programPathResolver, + IAssetSource assetSource) + { + _allowedFiles = new List(); + _lock = new Lock(); + _source = source; + _logger = logger; + var imageConfig = new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough, + ResizeAndRecodeImages = ImgRecodeOption.Passtrough, + }; + + var imgService = new ImgService(source, _logger, imageConfig); + + _renderSettings = new MarkdownRenderSettings(imgService) + { + HostUrl = "http://localhost", + DeleteFirstH1 = false, + CssClasses = new CssClasses(), + OffsetHeadingsBy = 0, + AutoEmbedSupportedLinks = true, + RenderInterop = new RenderInterop(assetSource, programPathResolver, imageConfig) + }; + + _markdownConverter = new MarkdownConverter(_renderSettings); + _templateEngine = new TemplateEngine(_logger, assetSource); + _template = assetSource.GetAsset(BundledAssets.TemplateSinglePage); + + _observer = _source.CreateObserver(logger, "*.*"); + _observer.FileChanged += OnFileChange; + _allowedFiles.Clear(); + _allowedFiles.AddRange(_source.GetFiles(_source.Scope, "*.md", true)); + } + + private void OnFileChange(object? sender, FileSystemChangeEventArgs e) + { + lock (_lock) + { + switch (e.ChangeType) + { + case FileSystemChangeEventArgs.Change.Created: + _allowedFiles.Add(e.FileName); + break; + case FileSystemChangeEventArgs.Change.Changed: + break; + case FileSystemChangeEventArgs.Change.Renamed: + int oldindex = _allowedFiles.IndexOf(e.FileName); + if (oldindex != -1) + { + _allowedFiles[oldindex] = e.FileName; + } + else + { + _allowedFiles.Add(e.FileName); + } + break; + case FileSystemChangeEventArgs.Change.Deleted: + _allowedFiles.Remove(e.FileName); + break; + } + } + } + + public void Dispose() + { + _observer.FileChanged -= OnFileChange; + _observer.Dispose(); + _markdownConverter.Dispose(); + _renderSettings.Dispose(); + } + + public IEnumerable> Routes + { + get + { + var metaData = new ApiMetaData("/preview", MediaTypeNames.Text.Html, ApiMethod.Get); + yield return new KeyValuePair(metaData, RenderPreview); + } + } + + private async Task RenderPreview(HttpContext context) + { + string? fileName = context.Request.Query["file"]; + if (!CanServe(fileName)) + { + var viewData = new ViewData + { + Host = string.Empty, + Content = "Not a markdown file", + Title = "Error", + LastModified = DateTime.UtcNow, + }; + await SendData(context, + HttpStatusCode.BadRequest, + _templateEngine.Render(_template, viewData), + MediaTypeNames.Text.Html); + + return; + } + + var source = await _source.GetSourceFile(fileName, _logger); + + var data = new ViewData + { + Content = _markdownConverter.RenderMarkdownToHtml(source.Content), + Title = source.FrontMatter.Title, + LastModified = source.LastModified, + Host = "http://localhost" + }; + + await SendData(context, + HttpStatusCode.OK, + _templateEngine.Render(_template, data), + MediaTypeNames.Text.Html); + + } + + private bool CanServe([NotNullWhen(true)] string? fileName) + { + if (string.IsNullOrEmpty(fileName)) + return false; + + return _allowedFiles.Contains(fileName); + } + + private static async Task SendData(HttpContext httpContext, HttpStatusCode httpStatusCode, string data, string mimeType) + { + httpContext.Response.StatusCode = (int)httpStatusCode; + httpContext.Response.ContentType = mimeType; + await httpContext.Response.WriteAsync(data); + } +} diff --git a/Source/Bookgen.Lib/Http/RouteProvider.cs b/Source/Bookgen.Lib/Http/RouteProvider.cs new file mode 100644 index 00000000..851a5c55 --- /dev/null +++ b/Source/Bookgen.Lib/Http/RouteProvider.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace BookGen.Lib.Http; + +internal interface IRouteProvider +{ + IEnumerable> Routes { get; } +} diff --git a/Source/Bookgen.Lib/Preview/PreviewDataStore.cs b/Source/Bookgen.Lib/Preview/PreviewDataStore.cs deleted file mode 100644 index 48be062d..00000000 --- a/Source/Bookgen.Lib/Preview/PreviewDataStore.cs +++ /dev/null @@ -1,69 +0,0 @@ -using BookGen.Vfs; - -using Microsoft.Extensions.Caching.Memory; - -namespace BookGen.Lib.Preview; - -internal class PreviewDataStore -{ - private readonly IMemoryCache _memoryCache; - private readonly List _availableFiles; - - public PreviewDataStore(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - _availableFiles = new List(); - } - - public IReadOnlyList Files - => _availableFiles; - - private string GetCacheKey(string path) - => $"preview_{path}"; - - public void UpdateAvailableFiles(IEnumerable enumerable) - { - _availableFiles.Clear(); - _availableFiles.AddRange(enumerable); - } - - public void UpdateWith(FileSystemChangeEventArgs e) - { - //Invalidate previous rendering - if (!string.IsNullOrEmpty(e.FileName)) - { - _memoryCache.Remove(GetCacheKey(e.FileName)); - } - if (!string.IsNullOrEmpty(e.NewFileName)) - { - _memoryCache.Remove(GetCacheKey(e.NewFileName)); - } - - switch (e.ChangeType) - { - case FileSystemChangeEventArgs.Change.Created: - _availableFiles.Add(e.FileName); - break; - case FileSystemChangeEventArgs.Change.Changed: - // No action needed for changed files - break; - case FileSystemChangeEventArgs.Change.Deleted: - _availableFiles.Remove(e.FileName); - break; - case FileSystemChangeEventArgs.Change.Renamed: - if (!string.IsNullOrEmpty(e.NewFileName)) - { - int idx = _availableFiles.IndexOf(e.FileName); - if (idx != -1) - { - _availableFiles[idx] = e.NewFileName; - } - else - { - _availableFiles.Add(e.NewFileName); - } - } - break; - } - } -} diff --git a/Source/Bookgen.Lib/Preview/PreviewServer.cs b/Source/Bookgen.Lib/Preview/PreviewServer.cs deleted file mode 100644 index 3686114a..00000000 --- a/Source/Bookgen.Lib/Preview/PreviewServer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -using BookGen.Vfs; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; - -namespace BookGen.Lib.Preview; - -internal class PreviewServer : IDisposable -{ - private readonly ILogger _logger; - private readonly PreviewDataStore _dataStore; - private IFileSystemObserver? _fileSystemObserver; - - public PreviewServer(ILogger logger, IMemoryCache memoryCache) - { - _logger = logger; - _dataStore = new PreviewDataStore(memoryCache); - } - - public void Dispose() - { - _fileSystemObserver?.FileChanged -= OnFileChange; - _fileSystemObserver?.Dispose(); - } - - public async Task Start(IReadOnlyFileSystem fileSystem) - { - _dataStore.UpdateAvailableFiles(fileSystem.GetFiles(fileSystem.Scope, "*.md", true)); - _fileSystemObserver = fileSystem.CreateObserver(_logger, "*.md"); - _fileSystemObserver.FileChanged += OnFileChange; - } - - private void OnFileChange(object? sender, FileSystemChangeEventArgs e) - { - _dataStore.UpdateWith(e); - } -} From ab40d303e2b43aac9d5544eab3d446f21c31a41b Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Fri, 24 Jul 2026 19:05:44 +0200 Subject: [PATCH 04/18] Work on preview server --- Source/BookGen.Lib/Http/HttpServer.cs | 5 +- .../BookGen/Commands/Folder/PreviewCommand.cs | 58 +++++++++++++++++++ Source/BookGen/Commands/GuiCommand.cs | 2 +- .../BookGen/Commands/folder/ServeCommand.cs | 2 +- .../Http/HttpServerFactory.cs} | 17 +++++- .../{RouteProvider.cs => IRouteProvider.cs} | 2 +- Source/Bookgen.Lib/Http/PreviewRoutes.cs | 11 ++-- 7 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 Source/BookGen/Commands/Folder/PreviewCommand.cs rename Source/{BookGen.Lib/Http/ServerFactory.cs => Bookgen.Lib/Http/HttpServerFactory.cs} (75%) rename Source/Bookgen.Lib/Http/{RouteProvider.cs => IRouteProvider.cs} (56%) diff --git a/Source/BookGen.Lib/Http/HttpServer.cs b/Source/BookGen.Lib/Http/HttpServer.cs index 51ec26ab..397c756d 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -144,7 +144,10 @@ public void AddRoutes(IRouteProvider provider) { _disposableTracker.Track(disposable); } - AddRoutes(provider.Routes); + foreach ((ApiMetaData metaData, RequestDelegate handler) in provider.Routes) + { + AddRoute(metaData, handler); + } } public IEnumerable GetListenUrls() diff --git a/Source/BookGen/Commands/Folder/PreviewCommand.cs b/Source/BookGen/Commands/Folder/PreviewCommand.cs new file mode 100644 index 00000000..dbd6bbb1 --- /dev/null +++ b/Source/BookGen/Commands/Folder/PreviewCommand.cs @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using BookGen.Cli; +using BookGen.Cli.Annotations; +using BookGen.Lib; +using BookGen.Lib.AppSettings; +using BookGen.Lib.Http; +using BookGen.Vfs; + +using Microsoft.Extensions.Logging; + +namespace BookGen.Commands.Folder; + +[CommandName("folder preview")] +internal sealed class PreviewCommand : AsyncCommand +{ + private readonly IWritableFileSystem _fs; + private readonly ILogger _log; + private readonly IProgramPathResolver _programPathResolver; + private readonly IAssetSource _assetSource; + + public PreviewCommand(IWritableFileSystem source, + ILogger logger, + IProgramPathResolver programPathResolver, + IAssetSource assetSource) + { + _fs = source; + _log = logger; + _programPathResolver = programPathResolver; + _assetSource = assetSource; + } + + public override async Task ExecuteAsync(BookGenArgumentBase arguments, IReadOnlyList context, CancellationToken token) + { + _fs.Scope = arguments.Directory; + + using var folderLock = new FolderLock(_fs, FileNameConstants.LockFile); + + if (!folderLock.Initialize()) + { + _log.LogError("Failed to initialize folder lock. Another instance may be running or the directory is not writable."); + return ExitCodes.FolderLocked; + } + + await using (var runner = new ConsoleHttpServerRunner(HttpServerFactory.CreateServerForPreview(_fs, _log, _programPathResolver, _assetSource))) + { + var serverurls = string.Join(' ', runner.Server.GetListenUrls()); + _log.LogInformation("Preview server working in: {directory}", arguments.Directory); + _log.LogInformation("Preview server running on {urls}", serverurls); + await runner.RunServer(); + } + + return ExitCodes.Success; + } +} diff --git a/Source/BookGen/Commands/GuiCommand.cs b/Source/BookGen/Commands/GuiCommand.cs index 7c043a0c..3372e908 100644 --- a/Source/BookGen/Commands/GuiCommand.cs +++ b/Source/BookGen/Commands/GuiCommand.cs @@ -140,7 +140,7 @@ private async Task OnTest() if (_currentArgs == null) return ExitCodes.GeneralError; - int result = await Run("build web", "-o", "Output/Test", "-h", $"http://localhost:{ServerFactory.HostingPort}/"); + int result = await Run("build web", "-o", "Output/Test", "-h", $"http://localhost:{HttpServerFactory.HostingPort}/"); if (result == ExitCodes.Success) { diff --git a/Source/BookGen/Commands/folder/ServeCommand.cs b/Source/BookGen/Commands/folder/ServeCommand.cs index a06ded5a..d553b597 100644 --- a/Source/BookGen/Commands/folder/ServeCommand.cs +++ b/Source/BookGen/Commands/folder/ServeCommand.cs @@ -44,7 +44,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea #pragma warning disable CA2000 // Dispose objects before losing scope //runner is responsible for disposing the server - await using (var runner = new ConsoleHttpServerRunner(ServerFactory.CreateServerForDirectoryHosting(arguments.Directory, _log))) + await using (var runner = new ConsoleHttpServerRunner(HttpServerFactory.CreateServerForDirectoryHosting(arguments.Directory, _log))) { var serverurls = string.Join(' ', runner.Server.GetListenUrls()); var qrcodes = string.Join(' ', runner.Server.GetListenUrls().Select(x => $"{x}/qrcodelink")); diff --git a/Source/BookGen.Lib/Http/ServerFactory.cs b/Source/Bookgen.Lib/Http/HttpServerFactory.cs similarity index 75% rename from Source/BookGen.Lib/Http/ServerFactory.cs rename to Source/Bookgen.Lib/Http/HttpServerFactory.cs index ca0fe4cc..105e28ee 100644 --- a/Source/BookGen.Lib/Http/ServerFactory.cs +++ b/Source/Bookgen.Lib/Http/HttpServerFactory.cs @@ -6,14 +6,18 @@ using System.Net.Mime; using System.Net.NetworkInformation; +using BookGen.Lib.AppSettings; +using BookGen.Vfs; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace BookGen.Lib.Http; -public static class ServerFactory +public static class HttpServerFactory { public const int HostingPort = 8081; + public const int PreviewPort = 8181; private static int ChoosePort(int @default = HostingPort) { @@ -62,4 +66,15 @@ public static IHttpServer CreateServerForDirectoryHosting(string directoryToServ }); return server; } + + public static IHttpServer CreateServerForPreview(IReadOnlyFileSystem source, + ILogger logger, + IProgramPathResolver programPathResolver, + IAssetSource assetSource) + { + var server = new HttpServer(ChoosePort(), logger); + var previewRoutes = new PreviewRoutes(source, logger, programPathResolver, assetSource); + server.AddRoutes(previewRoutes); + return server; + } } diff --git a/Source/Bookgen.Lib/Http/RouteProvider.cs b/Source/Bookgen.Lib/Http/IRouteProvider.cs similarity index 56% rename from Source/Bookgen.Lib/Http/RouteProvider.cs rename to Source/Bookgen.Lib/Http/IRouteProvider.cs index 851a5c55..be6092a0 100644 --- a/Source/Bookgen.Lib/Http/RouteProvider.cs +++ b/Source/Bookgen.Lib/Http/IRouteProvider.cs @@ -4,5 +4,5 @@ namespace BookGen.Lib.Http; internal interface IRouteProvider { - IEnumerable> Routes { get; } + IEnumerable<(ApiMetaData metaData, RequestDelegate handler)> Routes { get; } } diff --git a/Source/Bookgen.Lib/Http/PreviewRoutes.cs b/Source/Bookgen.Lib/Http/PreviewRoutes.cs index 2fa6d124..0dc3f07b 100644 --- a/Source/Bookgen.Lib/Http/PreviewRoutes.cs +++ b/Source/Bookgen.Lib/Http/PreviewRoutes.cs @@ -103,12 +103,11 @@ public void Dispose() _renderSettings.Dispose(); } - public IEnumerable> Routes + public IEnumerable<(ApiMetaData metaData, RequestDelegate handler)> Routes { get { - var metaData = new ApiMetaData("/preview", MediaTypeNames.Text.Html, ApiMethod.Get); - yield return new KeyValuePair(metaData, RenderPreview); + yield return (new ApiMetaData("/preview", MediaTypeNames.Text.Html, ApiMethod.Get), RenderPreview); } } @@ -151,10 +150,8 @@ await SendData(context, private bool CanServe([NotNullWhen(true)] string? fileName) { - if (string.IsNullOrEmpty(fileName)) - return false; - - return _allowedFiles.Contains(fileName); + return !string.IsNullOrEmpty(fileName) + && _allowedFiles.Contains(fileName); } private static async Task SendData(HttpContext httpContext, HttpStatusCode httpStatusCode, string data, string mimeType) From e2efb9de69a2220eb7ef423c82222a6d51f507be Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 11:34:07 +0200 Subject: [PATCH 05/18] Working preview server --- Assets/BundledAssets/Preview.html | 61 +++++++++++++++++++ Docs/bookgen.lock | 1 + Source/BookGen.Lib/BundledAssets.cs | 1 + .../BookGen.Lib/Http/ErrorPageTemplate.html | 6 +- Source/BookGen.Lib/Http/HttpServer.cs | 20 +++++- Source/BookGen.Lib/Http/PageFactory.cs | 21 ++++++- Source/BookGen.Lib/Http/QRCodeTemplate.html | 2 +- Source/BookGen/Properties/launchSettings.json | 4 ++ Source/Bookgen.Lib/Http/HttpServerFactory.cs | 4 +- Source/Bookgen.Lib/Http/PreviewRoutes.cs | 43 +++++++++---- 10 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 Assets/BundledAssets/Preview.html create mode 100644 Docs/bookgen.lock diff --git a/Assets/BundledAssets/Preview.html b/Assets/BundledAssets/Preview.html new file mode 100644 index 00000000..593ed36c --- /dev/null +++ b/Assets/BundledAssets/Preview.html @@ -0,0 +1,61 @@ + + + + + + + + {{Title}} + + + + + + +
+
+ +
+
+ +
+
+
+ +
+ {{Content}} +
+ + + + + + + \ No newline at end of file diff --git a/Docs/bookgen.lock b/Docs/bookgen.lock new file mode 100644 index 00000000..78bcafcb --- /dev/null +++ b/Docs/bookgen.lock @@ -0,0 +1 @@ +12164 \ No newline at end of file diff --git a/Source/BookGen.Lib/BundledAssets.cs b/Source/BookGen.Lib/BundledAssets.cs index 0ba9fac3..fefd95ce 100644 --- a/Source/BookGen.Lib/BundledAssets.cs +++ b/Source/BookGen.Lib/BundledAssets.cs @@ -15,6 +15,7 @@ public static class BundledAssets public const string TemplatePrint = "Print.html"; public const string TemplateEpub = "Epub.html"; public const string TemplateBlank = "Blank.html"; + public const string TemplatePreview = "Preview.html"; public const string QrCodeJs = "qrcode.min.js"; public const string JsPageToc = "PageToc.js"; public const string NomnomlJs = "nomnoml.js"; diff --git a/Source/BookGen.Lib/Http/ErrorPageTemplate.html b/Source/BookGen.Lib/Http/ErrorPageTemplate.html index 329ded85..29385f06 100644 --- a/Source/BookGen.Lib/Http/ErrorPageTemplate.html +++ b/Source/BookGen.Lib/Http/ErrorPageTemplate.html @@ -157,13 +157,13 @@
-

Error {{code}}

+

Error {{Code}}


An error occured during serving your request.
- More info about this error | Return to home + More info about this error | Return to home

-

{{message}}

+

{{Message}}

    diff --git a/Source/BookGen.Lib/Http/HttpServer.cs b/Source/BookGen.Lib/Http/HttpServer.cs index 397c756d..d9a7ec4c 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -19,18 +19,28 @@ namespace BookGen.Lib.Http; internal sealed class HttpServer : IHttpServer { private readonly WebApplication _app; + private readonly bool _localhostOnly; private readonly DisposableTracker _disposableTracker; public int Port { get; } - public HttpServer(int port, ILogger logger) + public HttpServer(int port, ILogger logger, bool localHostOnly) { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); #pragma warning disable CA2000 // Dispose objects before losing scope builder.Logging.AddProvider(new LoggerProvider(logger)); #pragma warning restore CA2000 // Dispose objects before losing scope - builder.WebHost.ConfigureKestrel((context, serverOptions) => serverOptions.ListenAnyIP(port)); + + if (localHostOnly) + { + builder.WebHost.ConfigureKestrel((context, serverOptions) => serverOptions.ListenLocalhost(port)); + } + else + { + builder.WebHost.ConfigureKestrel((context, serverOptions) => serverOptions.ListenAnyIP(port)); + } + _localhostOnly = localHostOnly; _disposableTracker = new DisposableTracker(); _app = builder.Build(); Port = port; @@ -152,6 +162,12 @@ public void AddRoutes(IRouteProvider provider) public IEnumerable GetListenUrls() { + if (_localhostOnly) + { + yield return $"http://localhost:{Port}"; + yield break; + } + foreach ((IPAddress? adress, IPAddress _) in GetIpAdresses()) { yield return $"http://{adress}:{Port}"; diff --git a/Source/BookGen.Lib/Http/PageFactory.cs b/Source/BookGen.Lib/Http/PageFactory.cs index 10f408cd..5fc4aa58 100644 --- a/Source/BookGen.Lib/Http/PageFactory.cs +++ b/Source/BookGen.Lib/Http/PageFactory.cs @@ -22,10 +22,27 @@ private static string GetResource(string resoruceName) return content; } + public static string GetFiles(List allowedFiles) + { + var files = new StringBuilder(); + IEnumerable> groups = allowedFiles.GroupBy(f => Path.GetDirectoryName(f) ?? string.Empty); + foreach (IGrouping group in groups) + { + files.AppendLine($"

    {group.Key}

    "); + files.AppendLine("
      "); + foreach (var file in group) + { + files.AppendLine($"
    • {file}
    • "); + } + files.AppendLine("
    "); + } + return files.ToString(); + } + public static string GetErrorPage(int code, string message) { var page = GetResource($"BookGen.Lib.Http.ErrorPageTemplate.html"); - return page.Replace("{{code}}", code.ToString()).Replace("{{message}}", message); + return page.Replace("{{Code}}", code.ToString()).Replace("{{Message}}", message); } public static string GetQrCodePage(IEnumerable urls) @@ -39,6 +56,6 @@ public static string GetQrCodePage(IEnumerable urls) qrcodes.AppendLine(""); } - return GetResource("BookGen.Lib.Http.QRCodeTemplate.html").Replace("{{links}}", qrcodes.ToString()); + return GetResource("BookGen.Lib.Http.QRCodeTemplate.html").Replace("{{Links}}", qrcodes.ToString()); } } diff --git a/Source/BookGen.Lib/Http/QRCodeTemplate.html b/Source/BookGen.Lib/Http/QRCodeTemplate.html index 557f1a39..2ea1a047 100644 --- a/Source/BookGen.Lib/Http/QRCodeTemplate.html +++ b/Source/BookGen.Lib/Http/QRCodeTemplate.html @@ -163,7 +163,7 @@

    Liks for this server

    - {{links}} + {{Links}}
      diff --git a/Source/BookGen/Properties/launchSettings.json b/Source/BookGen/Properties/launchSettings.json index da26a92a..237c5735 100644 --- a/Source/BookGen/Properties/launchSettings.json +++ b/Source/BookGen/Properties/launchSettings.json @@ -64,6 +64,10 @@ "Build sampleplugin": { "commandName": "Project", "commandLineArgs": "build plugin ..\\..\\Source\\BookGen.SamplePlugin\\bin\\Debug\\netstandard2.1\\BookGen.SamplePlugin.dll -v -d ..\\..\\Docs -dev -o ..\\SamplePluginOutput" + }, + "folder preview": { + "commandName": "Project", + "commandLineArgs": "folder preview -d ..\\..\\Docs" } } } \ No newline at end of file diff --git a/Source/Bookgen.Lib/Http/HttpServerFactory.cs b/Source/Bookgen.Lib/Http/HttpServerFactory.cs index 105e28ee..bb9a025f 100644 --- a/Source/Bookgen.Lib/Http/HttpServerFactory.cs +++ b/Source/Bookgen.Lib/Http/HttpServerFactory.cs @@ -56,7 +56,7 @@ private static int ChoosePort(int @default = HostingPort) public static IHttpServer CreateServerForDirectoryHosting(string directoryToServe, ILogger logger) { - var server = new HttpServer(ChoosePort(), logger); + var server = new HttpServer(ChoosePort(), logger, localHostOnly: false); server.AddStaticFiles(directory: directoryToServe, requestPath: "", directoryBrowseEnabled: true); server.AddRoute(new ApiMetaData("/qrcodelink", MediaTypeNames.Text.Html), async context => { @@ -72,7 +72,7 @@ public static IHttpServer CreateServerForPreview(IReadOnlyFileSystem source, IProgramPathResolver programPathResolver, IAssetSource assetSource) { - var server = new HttpServer(ChoosePort(), logger); + var server = new HttpServer(ChoosePort(), logger, localHostOnly: true); var previewRoutes = new PreviewRoutes(source, logger, programPathResolver, assetSource); server.AddRoutes(previewRoutes); return server; diff --git a/Source/Bookgen.Lib/Http/PreviewRoutes.cs b/Source/Bookgen.Lib/Http/PreviewRoutes.cs index 0dc3f07b..afb2be2d 100644 --- a/Source/Bookgen.Lib/Http/PreviewRoutes.cs +++ b/Source/Bookgen.Lib/Http/PreviewRoutes.cs @@ -58,12 +58,16 @@ public PreviewRoutes(IReadOnlyFileSystem source, _markdownConverter = new MarkdownConverter(_renderSettings); _templateEngine = new TemplateEngine(_logger, assetSource); - _template = assetSource.GetAsset(BundledAssets.TemplateSinglePage); + _template = assetSource.GetAsset(BundledAssets.TemplatePreview); _observer = _source.CreateObserver(logger, "*.*"); _observer.FileChanged += OnFileChange; _allowedFiles.Clear(); - _allowedFiles.AddRange(_source.GetFiles(_source.Scope, "*.md", true)); + + var files = _source.GetFiles(_source.Scope, "*.md", true) + .Select(f => Path.GetRelativePath(_source.Scope, f)); + + _allowedFiles.AddRange(files); } private void OnFileChange(object? sender, FileSystemChangeEventArgs e) @@ -73,23 +77,23 @@ private void OnFileChange(object? sender, FileSystemChangeEventArgs e) switch (e.ChangeType) { case FileSystemChangeEventArgs.Change.Created: - _allowedFiles.Add(e.FileName); + _allowedFiles.Add(Path.GetRelativePath(_source.Scope, e.FileName)); break; case FileSystemChangeEventArgs.Change.Changed: break; case FileSystemChangeEventArgs.Change.Renamed: - int oldindex = _allowedFiles.IndexOf(e.FileName); - if (oldindex != -1) + int oldindex = _allowedFiles.IndexOf(Path.GetRelativePath(_source.Scope, e.FileName)); + if (oldindex != -1 && !string.IsNullOrEmpty(e.NewFileName)) { - _allowedFiles[oldindex] = e.FileName; + _allowedFiles[oldindex] = Path.GetRelativePath(_source.Scope, e.NewFileName); } - else + else if (!string.IsNullOrEmpty(e.NewFileName)) { - _allowedFiles.Add(e.FileName); + _allowedFiles.Add(Path.GetRelativePath(_source.Scope, e.NewFileName)); } break; case FileSystemChangeEventArgs.Change.Deleted: - _allowedFiles.Remove(e.FileName); + _allowedFiles.Remove(Path.GetRelativePath(_source.Scope, e.FileName)); break; } } @@ -108,9 +112,26 @@ public void Dispose() get { yield return (new ApiMetaData("/preview", MediaTypeNames.Text.Html, ApiMethod.Get), RenderPreview); + yield return (new ApiMetaData("/", MediaTypeNames.Text.Html, ApiMethod.Get), RenderIndex); } } + private async Task RenderIndex(HttpContext context) + { + var viewData = new ViewData + { + Host = "/", + Content = PageFactory.GetFiles(_allowedFiles), + Title = "Previewable files", + LastModified = DateTime.UtcNow, + }; + + await SendData(context, + HttpStatusCode.OK, + _templateEngine.Render(_template, viewData), + MediaTypeNames.Text.Html); + } + private async Task RenderPreview(HttpContext context) { string? fileName = context.Request.Query["file"]; @@ -118,7 +139,7 @@ private async Task RenderPreview(HttpContext context) { var viewData = new ViewData { - Host = string.Empty, + Host = "/", Content = "Not a markdown file", Title = "Error", LastModified = DateTime.UtcNow, @@ -138,7 +159,7 @@ await SendData(context, Content = _markdownConverter.RenderMarkdownToHtml(source.Content), Title = source.FrontMatter.Title, LastModified = source.LastModified, - Host = "http://localhost" + Host = "/" }; await SendData(context, From 1035ffb1c01962a5830d74109778f3a9e10b8af1 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 11:52:53 +0200 Subject: [PATCH 06/18] .NET 11 transition --- .github/workflows/codeql.yml | 6 +- .github/workflows/dotnet.yml | 6 +- Directory.Packages.props | 12 +-- Source/BookGen.Cli/BookGen.Cli.csproj | 4 +- Source/BookGen.Cli/packages.lock.json | 22 +---- .../BookGen.Contents/BookGen.Contents.csproj | 2 +- Source/BookGen.Lib/BookGen.Lib.csproj | 2 +- Source/BookGen.Lib/packages.lock.json | 8 +- .../BookGen.Shell.Shared.csproj | 3 +- .../BookGen.Shell.Shared/packages.lock.json | 17 +--- .../BookGen.Shellprog.csproj | 4 +- Source/BookGen.Shellprog/packages.lock.json | 60 ++---------- Source/BookGen.Vfs/BookGen.Vfs.csproj | 3 +- Source/BookGen/BookGen.csproj | 5 +- Source/BookGen/packages.lock.json | 93 ++++--------------- Test/Bookgen.Tests/Bookgen.Tests.csproj | 2 +- 16 files changed, 53 insertions(+), 196 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 24ad8d8a..7377db62 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,9 +47,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x - dotnet-quality: 'ga' - #dotnet-quality: 'preview' + dotnet-version: 11.0.x + #dotnet-quality: 'ga' + dotnet-quality: 'preview' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index acb0d6c3..5d4a0c26 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -21,9 +21,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x - dotnet-quality: 'ga' - #dotnet-quality: 'preview' + dotnet-version: 11.0.x + #dotnet-quality: 'ga' + dotnet-quality: 'preview' - name: get render dependencies run: sh ./download-assets-pipeline.sh diff --git a/Directory.Packages.props b/Directory.Packages.props index d5516913..b4afed65 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,13 +12,9 @@ - - - - - - - + + + @@ -33,7 +29,7 @@ - + diff --git a/Source/BookGen.Cli/BookGen.Cli.csproj b/Source/BookGen.Cli/BookGen.Cli.csproj index d9942b02..a5f01b26 100644 --- a/Source/BookGen.Cli/BookGen.Cli.csproj +++ b/Source/BookGen.Cli/BookGen.Cli.csproj @@ -1,6 +1,6 @@  - net10.0 + net11.0 enable enable ..\..\bin\$(Configuration)\ @@ -10,8 +10,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/BookGen.Cli/packages.lock.json b/Source/BookGen.Cli/packages.lock.json index 6e7fe45f..9a7eb3f9 100644 --- a/Source/BookGen.Cli/packages.lock.json +++ b/Source/BookGen.Cli/packages.lock.json @@ -1,22 +1,7 @@ { "version": 2, "dependencies": { - "net10.0": { - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" - } - }, + "net11.0": { "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.15.0, )", @@ -24,10 +9,7 @@ "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "bookgen.vfs": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" - } + "type": "Project" } } } diff --git a/Source/BookGen.Contents/BookGen.Contents.csproj b/Source/BookGen.Contents/BookGen.Contents.csproj index 76db8ef6..d07e6253 100644 --- a/Source/BookGen.Contents/BookGen.Contents.csproj +++ b/Source/BookGen.Contents/BookGen.Contents.csproj @@ -1,6 +1,6 @@  - net10.0 + net11.0 enable enable ..\..\bin\$(Configuration)\ diff --git a/Source/BookGen.Lib/BookGen.Lib.csproj b/Source/BookGen.Lib/BookGen.Lib.csproj index 513faa47..3a777f7e 100644 --- a/Source/BookGen.Lib/BookGen.Lib.csproj +++ b/Source/BookGen.Lib/BookGen.Lib.csproj @@ -1,7 +1,7 @@  - net10.0 + net11.0 enable enable preview diff --git a/Source/BookGen.Lib/packages.lock.json b/Source/BookGen.Lib/packages.lock.json index 802ed806..b6b6ae91 100644 --- a/Source/BookGen.Lib/packages.lock.json +++ b/Source/BookGen.Lib/packages.lock.json @@ -1,7 +1,7 @@ { "version": 2, "dependencies": { - "net10.0": { + "net11.0": { "AngleSharp": { "type": "Direct", "requested": "[1.5.2, )", @@ -95,9 +95,9 @@ }, "System.ServiceModel.Syndication": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "jU3WQG477Tk7RYlM2Ky8oySb6WHd744LgK7MYRMe4L8FcoJI3Gbpcf29sB2lIlJVS82ah9i4FUw3RfbXeJs8jw==" + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "StArv4DSO8I8QjDo1n+FzKeaq6UTNKvgkHHFaAt8ueexUWJZSEZ5/EasqwKt7413n3carQjaFzSULDI7sxwg0A==" }, "YamlDotNet": { "type": "Direct", diff --git a/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj b/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj index d8d56396..5f5685fd 100644 --- a/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj +++ b/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj @@ -1,7 +1,7 @@  - net10.0 + net11.0 enable enable true @@ -9,7 +9,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/BookGen.Shell.Shared/packages.lock.json b/Source/BookGen.Shell.Shared/packages.lock.json index f96ef418..77d5e1f3 100644 --- a/Source/BookGen.Shell.Shared/packages.lock.json +++ b/Source/BookGen.Shell.Shared/packages.lock.json @@ -1,16 +1,7 @@ { "version": 2, "dependencies": { - "net10.0": { - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" - } - }, + "net11.0": { "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.15.0, )", @@ -42,12 +33,6 @@ "type": "Transitive", "resolved": "0.57.2", "contentHash": "Y1+u73shwP+JYHmrkdN6bSt2kaGBEAafhi2nrecczbXdFRey+k7J/WF9YPfcB0rqivv2S8fmif6FSBq9m/AFvw==" - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" } } } diff --git a/Source/BookGen.Shellprog/BookGen.Shellprog.csproj b/Source/BookGen.Shellprog/BookGen.Shellprog.csproj index 2ccdc818..00796fc0 100644 --- a/Source/BookGen.Shellprog/BookGen.Shellprog.csproj +++ b/Source/BookGen.Shellprog/BookGen.Shellprog.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net11.0 enable enable ..\..\bin\$(Configuration)\ @@ -14,9 +14,7 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index d3977c21..e31f7120 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -1,40 +1,20 @@ { "version": 2, "dependencies": { - "net10.0": { + "net11.0": { "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "MwlfMwxXewuq+X85a31yYx6emRYIyJOboVXZtRH3Wvp/hZZWmlg1pu/tI+XJUm4ltoCNMzpcFF6zJcmbnbnuGA==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "lUAZ402asMsLYe483nXXQIphklpTNDP6LmhmrwB8V3gBTjXeUp40N6h/Ap5QOiGHqsAiooznL4lBETGSncYsxQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.6.26359.118" } }, "Roslynator.Analyzers": { @@ -58,20 +38,6 @@ "resolved": "1.0.0", "contentHash": "TojbwWASRf3PMUsotXa8MQ9SNwEwWZZ5bAYf1eLv+jiDsxaD/n7TKTi3da6z4+m8uZHri2i+BVRuLj5w+6j64Q==" }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" - }, "Spectre.Console.Ansi": { "type": "Transitive", "resolved": "0.57.2", @@ -80,9 +46,7 @@ "bookgen.cli": { "type": "Project", "dependencies": { - "BookGen.Vfs": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" + "BookGen.Vfs": "[1.0.0, )" } }, "bookgen.contents": { @@ -91,16 +55,12 @@ "bookgen.shell.shared": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Spectre.Console": "[0.57.2, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } }, "bookgen.vfs": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" - } + "type": "Project" }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", diff --git a/Source/BookGen.Vfs/BookGen.Vfs.csproj b/Source/BookGen.Vfs/BookGen.Vfs.csproj index 9a21a923..3264c7d2 100644 --- a/Source/BookGen.Vfs/BookGen.Vfs.csproj +++ b/Source/BookGen.Vfs/BookGen.Vfs.csproj @@ -1,7 +1,7 @@  - net10.0 + net11.0 enable enable ..\..\bin\$(Configuration)\ @@ -15,7 +15,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Source/BookGen/BookGen.csproj b/Source/BookGen/BookGen.csproj index 2c58dda1..596c336e 100644 --- a/Source/BookGen/BookGen.csproj +++ b/Source/BookGen/BookGen.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net11.0 enable enable true @@ -30,12 +30,9 @@ - - - all diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 72f446c7..7ceb1a33 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -1,7 +1,7 @@ { "version": 2, "dependencies": { - "net10.0": { + "net11.0": { "HtmlToOpenXml.dll": { "type": "Direct", "requested": "[3.5.0, )", @@ -9,8 +9,7 @@ "contentHash": "ULQqQidtvWfGhds11YWG0uAkRYGXFzn3mNA6ZVnByJUV/1pqkR4fVAF3f7V1cSbpdSWCwoPtFbAef//rUNCmKA==", "dependencies": { "AngleSharp": "1.5.0", - "DocumentFormat.OpenXml": "3.5.1", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0" + "DocumentFormat.OpenXml": "3.5.1" } }, "Markdig": { @@ -19,61 +18,25 @@ "resolved": "1.3.2", "contentHash": "fZgOC/3CswUrndjDTac70aQpYdtxbW5+5bRumR7vzvI2HJbkmgKisB1c9oT+GA6v0jB/JDR9BLa9FiPzQmaK6A==" }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.10" - } - }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" - } + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "/0XbQexXoSNQ7YfwhiI8XJvK05+rHTVxJZzuMCFc/l8owJ7aPwwE6cYwCDs1LvsklOWyfGm1LaAUHQbhYXNNcw==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "MwlfMwxXewuq+X85a31yYx6emRYIyJOboVXZtRH3Wvp/hZZWmlg1pu/tI+XJUm4ltoCNMzpcFF6zJcmbnbnuGA==" }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "lUAZ402asMsLYe483nXXQIphklpTNDP6LmhmrwB8V3gBTjXeUp40N6h/Ap5QOiGHqsAiooznL4lBETGSncYsxQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Direct", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + "Microsoft.Extensions.DependencyInjection": "11.0.0-preview.6.26359.118" } }, "Microsoft.IO.RecyclableMemoryStream": { @@ -195,20 +158,6 @@ "Microsoft.ClearScript.Core": "7.5.1" } }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" - }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -278,9 +227,7 @@ "bookgen.cli": { "type": "Project", "dependencies": { - "BookGen.Vfs": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" + "BookGen.Vfs": "[1.0.0, )" } }, "bookgen.contents": { @@ -301,23 +248,19 @@ "SkiaSharp.NativeAssets.Linux": "[4.150.1, )", "SkiaSharp.NativeAssets.Win32": "[4.150.1, )", "Svg.Skia": "[5.1.1, )", - "System.ServiceModel.Syndication": "[10.0.10, )", + "System.ServiceModel.Syndication": "[11.0.0-preview.6.26359.118, )", "YamlDotNet": "[18.1.0, )" } }, "bookgen.shell.shared": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Spectre.Console": "[0.57.2, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } }, "bookgen.vfs": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )" - } + "type": "Project" }, "AngleSharp": { "type": "CentralTransitive", @@ -400,9 +343,9 @@ }, "System.ServiceModel.Syndication": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "jU3WQG477Tk7RYlM2Ky8oySb6WHd744LgK7MYRMe4L8FcoJI3Gbpcf29sB2lIlJVS82ah9i4FUw3RfbXeJs8jw==" + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "StArv4DSO8I8QjDo1n+FzKeaq6UTNKvgkHHFaAt8ueexUWJZSEZ5/EasqwKt7413n3carQjaFzSULDI7sxwg0A==" }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", diff --git a/Test/Bookgen.Tests/Bookgen.Tests.csproj b/Test/Bookgen.Tests/Bookgen.Tests.csproj index 1265ed44..ec0b1525 100644 --- a/Test/Bookgen.Tests/Bookgen.Tests.csproj +++ b/Test/Bookgen.Tests/Bookgen.Tests.csproj @@ -1,7 +1,7 @@  - net10.0 + net11.0 latest enable enable From 56db5d2111b172a45af03818668783ecf05170d7 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 11:55:14 +0200 Subject: [PATCH 07/18] Changelog update --- Docs/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Docs/changelog.md b/Docs/changelog.md index 9046dd40..9f1a8b1c 100644 --- a/Docs/changelog.md +++ b/Docs/changelog.md @@ -9,6 +9,8 @@ tags: '' * Breaking: Removed edit command * Breaking: Removed terminalinstall command, now part of install command * Breaking: Removed Vstasks command and reworked it into a vscode command +* New: Based on .NET 11 +* New: New logo by Mariann Kobzi * New: Build system can be extended with custom build plugins * New: VSCode command that can generate VSCode tasks.json file for the current project and recomend extensions * New: Mermaid diagram rendering supported via mmdr @@ -19,6 +21,7 @@ tags: '' * New: Mixcloud media embeding supported * New: Html2OpenXml command to convert HTML to OpenXML format for Word processing * New: Template engine reworked, now supports functions and is more robust +* New: Preview command to preview the current project files in a browser * Change: Commands documentation generated from code and is now more complete * Change: App settings reworked * Change: Dropped mathjax as dependency for formula rendering. New formula rendering is based on katex and works with markdown syntax From f50c6c4cb9c67a16f3b93ca622620691727ebd3d Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 13:18:10 +0200 Subject: [PATCH 08/18] VS Code recomended extensions --- .vscode/extensions.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..c94c22d9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-dotnettools.csdevkit", + "ms-dotnettools.csharp", + "yzhang.markdown-all-in-one" + ] +} \ No newline at end of file From bb1e13f1deb7479c0fdc64126c5c742a540bf005 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 13:27:27 +0200 Subject: [PATCH 09/18] Code review fixes --- Source/BookGen.Api/V1/RenderTags.cs | 2 +- Source/BookGen.Lib/Http/HttpServer.cs | 2 ++ Source/BookGen.Lib/Http/PageFactory.cs | 4 ++-- Source/BookGen.Vfs/FileSystemObserver.cs | 4 ++-- Source/Bookgen.Lib/Http/PreviewRoutes.cs | 18 ++++++++++++++---- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Source/BookGen.Api/V1/RenderTags.cs b/Source/BookGen.Api/V1/RenderTags.cs index eed196f4..2178af06 100644 --- a/Source/BookGen.Api/V1/RenderTags.cs +++ b/Source/BookGen.Api/V1/RenderTags.cs @@ -33,5 +33,5 @@ public sealed class RenderTags /// /// Additional data that can be used in the rendering process. /// - public Dictionary AdditionalData { get; set; } = new(); + public Dictionary AdditionalData { get; set; } = []; } diff --git a/Source/BookGen.Lib/Http/HttpServer.cs b/Source/BookGen.Lib/Http/HttpServer.cs index d9a7ec4c..e9a4d224 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -14,6 +14,8 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; +using BookGen.Lib; + namespace BookGen.Lib.Http; internal sealed class HttpServer : IHttpServer diff --git a/Source/BookGen.Lib/Http/PageFactory.cs b/Source/BookGen.Lib/Http/PageFactory.cs index 5fc4aa58..3e21b102 100644 --- a/Source/BookGen.Lib/Http/PageFactory.cs +++ b/Source/BookGen.Lib/Http/PageFactory.cs @@ -28,11 +28,11 @@ public static string GetFiles(List allowedFiles) IEnumerable> groups = allowedFiles.GroupBy(f => Path.GetDirectoryName(f) ?? string.Empty); foreach (IGrouping group in groups) { - files.AppendLine($"

      {group.Key}

      "); + files.AppendLine($"

      {HttpUtility.HtmlEncode(group.Key)}

      "); files.AppendLine(""); } diff --git a/Source/BookGen.Vfs/FileSystemObserver.cs b/Source/BookGen.Vfs/FileSystemObserver.cs index b04a2601..f298b266 100644 --- a/Source/BookGen.Vfs/FileSystemObserver.cs +++ b/Source/BookGen.Vfs/FileSystemObserver.cs @@ -11,6 +11,7 @@ public sealed class FileSystemObserver : IFileSystemObserver public FileSystemObserver(ILogger logger, string folder, string filter) { + _logger = logger; _watcher = new FileSystemWatcher { Filter = filter, @@ -21,14 +22,13 @@ public FileSystemObserver(ILogger logger, string folder, string filter) | NotifyFilters.LastWrite | NotifyFilters.Size, IncludeSubdirectories = true, - EnableRaisingEvents = true }; _watcher.Changed += OnChanged; _watcher.Created += OnChanged; _watcher.Deleted += OnChanged; _watcher.Renamed += OnRenamed; _watcher.Error += OnError; - _logger = logger; + _watcher.EnableRaisingEvents = true; } public void Dispose() diff --git a/Source/Bookgen.Lib/Http/PreviewRoutes.cs b/Source/Bookgen.Lib/Http/PreviewRoutes.cs index afb2be2d..18d0ee19 100644 --- a/Source/Bookgen.Lib/Http/PreviewRoutes.cs +++ b/Source/Bookgen.Lib/Http/PreviewRoutes.cs @@ -3,6 +3,7 @@ using System.Net.Mime; using BookGen.Lib.AppSettings; +using BookGen.Lib.Domain; using BookGen.Lib.Domain.IO.Configuration; using BookGen.Lib.Rendering; using BookGen.Lib.Rendering.Images; @@ -118,10 +119,16 @@ public void Dispose() private async Task RenderIndex(HttpContext context) { + string content; + lock (_lock) + { + content = PageFactory.GetFiles(_allowedFiles); + } + var viewData = new ViewData { Host = "/", - Content = PageFactory.GetFiles(_allowedFiles), + Content = content, Title = "Previewable files", LastModified = DateTime.UtcNow, }; @@ -152,7 +159,7 @@ await SendData(context, return; } - var source = await _source.GetSourceFile(fileName, _logger); + SourceFile source = await _source.GetSourceFile(fileName, _logger); var data = new ViewData { @@ -171,8 +178,11 @@ await SendData(context, private bool CanServe([NotNullWhen(true)] string? fileName) { - return !string.IsNullOrEmpty(fileName) - && _allowedFiles.Contains(fileName); + lock (_lock) + { + return !string.IsNullOrEmpty(fileName) + && _allowedFiles.Contains(fileName); + } } private static async Task SendData(HttpContext httpContext, HttpStatusCode httpStatusCode, string data, string mimeType) From e55c6ed1d201421b547a46d53d8a0044c7b5aa6f Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 14:38:42 +0200 Subject: [PATCH 10/18] Folder fixes --- Source/{Bookgen.Lib => BookGen.Lib}/DisposableTracker.cs | 0 Source/{Bookgen.Lib => BookGen.Lib}/Http/HttpServerFactory.cs | 0 Source/{Bookgen.Lib => BookGen.Lib}/Http/IRouteProvider.cs | 0 Source/{Bookgen.Lib => BookGen.Lib}/Http/PreviewRoutes.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Source/{Bookgen.Lib => BookGen.Lib}/DisposableTracker.cs (100%) rename Source/{Bookgen.Lib => BookGen.Lib}/Http/HttpServerFactory.cs (100%) rename Source/{Bookgen.Lib => BookGen.Lib}/Http/IRouteProvider.cs (100%) rename Source/{Bookgen.Lib => BookGen.Lib}/Http/PreviewRoutes.cs (100%) diff --git a/Source/Bookgen.Lib/DisposableTracker.cs b/Source/BookGen.Lib/DisposableTracker.cs similarity index 100% rename from Source/Bookgen.Lib/DisposableTracker.cs rename to Source/BookGen.Lib/DisposableTracker.cs diff --git a/Source/Bookgen.Lib/Http/HttpServerFactory.cs b/Source/BookGen.Lib/Http/HttpServerFactory.cs similarity index 100% rename from Source/Bookgen.Lib/Http/HttpServerFactory.cs rename to Source/BookGen.Lib/Http/HttpServerFactory.cs diff --git a/Source/Bookgen.Lib/Http/IRouteProvider.cs b/Source/BookGen.Lib/Http/IRouteProvider.cs similarity index 100% rename from Source/Bookgen.Lib/Http/IRouteProvider.cs rename to Source/BookGen.Lib/Http/IRouteProvider.cs diff --git a/Source/Bookgen.Lib/Http/PreviewRoutes.cs b/Source/BookGen.Lib/Http/PreviewRoutes.cs similarity index 100% rename from Source/Bookgen.Lib/Http/PreviewRoutes.cs rename to Source/BookGen.Lib/Http/PreviewRoutes.cs From 6e14951bec32c6a37b06cc5e6d56001a85a3767c Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 14:47:19 +0200 Subject: [PATCH 11/18] Fix Preview command documentation --- Source/BookGen/Commands/Folder/PreviewCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/BookGen/Commands/Folder/PreviewCommand.cs b/Source/BookGen/Commands/Folder/PreviewCommand.cs index dbd6bbb1..861060f7 100644 --- a/Source/BookGen/Commands/Folder/PreviewCommand.cs +++ b/Source/BookGen/Commands/Folder/PreviewCommand.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.Annotations; using BookGen.Lib; @@ -15,6 +17,9 @@ namespace BookGen.Commands.Folder; [CommandName("folder preview")] +[Description("Starts a preview server for the specified folder.")] +[ExitCode(ExitCodes.FolderLocked, "A serve command is running in the given folder.")] +[ExitCode(ExitCodes.Success, "The command completed successfully.")] internal sealed class PreviewCommand : AsyncCommand { private readonly IWritableFileSystem _fs; From 0875749450fceb2d0884e882bae25de3e5c057f9 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 14:55:12 +0200 Subject: [PATCH 12/18] PVS Studio bugfixes --- .../Tooldownloaders/ChromaDownloader.cs | 19 +++++++++++-------- .../Tooldownloaders/CopyPartyDownloader.cs | 19 +++++++++++-------- .../Tooldownloaders/GithubDownloader.cs | 19 +++++++++++-------- .../BookGen/Tooldownloaders/GlowDownloader.cs | 19 +++++++++++-------- .../MicrosoftEditToolDownloader.cs | 19 +++++++++++-------- .../Tooldownloaders/PandocTooldownloader.cs | 19 +++++++++++-------- .../Tooldownloaders/TooldownloaderBase.cs | 5 +---- 7 files changed, 67 insertions(+), 52 deletions(-) diff --git a/Source/BookGen/Tooldownloaders/ChromaDownloader.cs b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs index 1b0f7bfd..a9d7efe8 100644 --- a/Source/BookGen/Tooldownloaders/ChromaDownloader.cs +++ b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs @@ -21,16 +21,19 @@ public ChromaDownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Chroma", - ApproximateSize = "8 MiB", - RepoOwner = "alecthomas", - RepoName = "chroma", - FolderName = "chroma", - }; + return new ToolInfo + { + Name = "Chroma", + ApproximateSize = "8 MiB", + RepoOwner = "alecthomas", + RepoName = "chroma", + FolderName = "chroma", + }; + } } protected override Task Extract(IDownloadUi ui, Stream stream) diff --git a/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs b/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs index a9a217fa..5df3a56f 100644 --- a/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs +++ b/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs @@ -21,16 +21,19 @@ public CopyPartyDownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Copyparty.exe", - ApproximateSize = "13 MiB", - RepoOwner = "9001", - RepoName = "copyparty", - FolderName = "copyparty", - }; + return new ToolInfo + { + Name = "Copyparty.exe", + ApproximateSize = "13 MiB", + RepoOwner = "9001", + RepoName = "copyparty", + FolderName = "copyparty", + }; + } } protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) diff --git a/Source/BookGen/Tooldownloaders/GithubDownloader.cs b/Source/BookGen/Tooldownloaders/GithubDownloader.cs index b1ce9e6b..fc24c800 100644 --- a/Source/BookGen/Tooldownloaders/GithubDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GithubDownloader.cs @@ -21,16 +21,19 @@ public GithubDownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Github CLI", - ApproximateSize = "38 MiB", - RepoOwner = "cli", - RepoName = "cli", - FolderName = "github-cli", - }; + return new ToolInfo + { + Name = "Github CLI", + ApproximateSize = "38 MiB", + RepoOwner = "cli", + RepoName = "cli", + FolderName = "github-cli", + }; + } } protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) diff --git a/Source/BookGen/Tooldownloaders/GlowDownloader.cs b/Source/BookGen/Tooldownloaders/GlowDownloader.cs index 1d896cee..575ea870 100644 --- a/Source/BookGen/Tooldownloaders/GlowDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GlowDownloader.cs @@ -21,16 +21,19 @@ public GlowDownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Glow", - ApproximateSize = "18 MiB", - RepoOwner = "charmbracelet", - RepoName = "glow", - FolderName = "glow", - }; + return new ToolInfo + { + Name = "Glow", + ApproximateSize = "18 MiB", + RepoOwner = "charmbracelet", + RepoName = "glow", + FolderName = "glow", + }; + } } protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) diff --git a/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs b/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs index 3b6fa854..26b97cc9 100644 --- a/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs +++ b/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs @@ -21,16 +21,19 @@ public MicrosoftEditToolDownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Microsoft Edit", - ApproximateSize = "3.6 MiB", - RepoOwner = "microsoft", - RepoName = "edit", - FolderName = "ms-edit", - }; + return new ToolInfo + { + Name = "Microsoft Edit", + ApproximateSize = "3.6 MiB", + RepoOwner = "microsoft", + RepoName = "edit", + FolderName = "ms-edit", + }; + } } protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) diff --git a/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs b/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs index acf2fd87..0557d81d 100644 --- a/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs +++ b/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs @@ -21,16 +21,19 @@ public PandocTooldownloader(IApiClient apiClient, { } - protected override ToolInfo CreateToolInfo() + public override ToolInfo ToolInfo { - return new ToolInfo + get { - Name = "Pandoc", - ApproximateSize = "217 MiB", - RepoOwner = "jgm", - RepoName = "pandoc", - FolderName = "pandoc", - }; + return new ToolInfo + { + Name = "Pandoc", + ApproximateSize = "217 MiB", + RepoOwner = "jgm", + RepoName = "pandoc", + FolderName = "pandoc", + }; + } } protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) diff --git a/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs b/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs index 26a18b59..906ec176 100644 --- a/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs +++ b/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs @@ -25,12 +25,9 @@ public TooldownloaderBase(IApiClient apiClient, _apiClient = apiClient; _memoryStreamManager = memoryStreamManager; _log = log; - ToolInfo = CreateToolInfo(); } - protected abstract ToolInfo CreateToolInfo(); - - public ToolInfo ToolInfo { get; } + public abstract ToolInfo ToolInfo { get; } protected abstract ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets); From 9bc6d5b669e3ee08efa8ec9617cf51d88d4293d2 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Sun, 26 Jul 2026 14:58:10 +0200 Subject: [PATCH 13/18] License headers --- Source/BookGen.Lib/DisposableTracker.cs | 7 ++++++- Source/BookGen.Lib/Http/IRouteProvider.cs | 7 ++++++- Source/BookGen.Lib/Http/PreviewRoutes.cs | 9 +++++++-- Source/BookGen.Vfs/FileSystemChange.cs | 7 ++++++- Source/BookGen.Vfs/FileSystemObserver.cs | 7 ++++++- Source/BookGen.Vfs/IFileSystemObserver.cs | 7 ++++++- 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Source/BookGen.Lib/DisposableTracker.cs b/Source/BookGen.Lib/DisposableTracker.cs index dcbfdd2f..49983bb3 100644 --- a/Source/BookGen.Lib/DisposableTracker.cs +++ b/Source/BookGen.Lib/DisposableTracker.cs @@ -1,4 +1,9 @@ -namespace BookGen.Lib; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Lib; internal sealed class DisposableTracker : IDisposable { diff --git a/Source/BookGen.Lib/Http/IRouteProvider.cs b/Source/BookGen.Lib/Http/IRouteProvider.cs index be6092a0..81ecb985 100644 --- a/Source/BookGen.Lib/Http/IRouteProvider.cs +++ b/Source/BookGen.Lib/Http/IRouteProvider.cs @@ -1,4 +1,9 @@ -using Microsoft.AspNetCore.Http; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using Microsoft.AspNetCore.Http; namespace BookGen.Lib.Http; diff --git a/Source/BookGen.Lib/Http/PreviewRoutes.cs b/Source/BookGen.Lib/Http/PreviewRoutes.cs index 18d0ee19..6defef68 100644 --- a/Source/BookGen.Lib/Http/PreviewRoutes.cs +++ b/Source/BookGen.Lib/Http/PreviewRoutes.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; using System.Net; using System.Net.Mime; @@ -61,7 +66,7 @@ public PreviewRoutes(IReadOnlyFileSystem source, _templateEngine = new TemplateEngine(_logger, assetSource); _template = assetSource.GetAsset(BundledAssets.TemplatePreview); - _observer = _source.CreateObserver(logger, "*.*"); + _observer = _source.CreateObserver(logger, "*.md"); _observer.FileChanged += OnFileChange; _allowedFiles.Clear(); diff --git a/Source/BookGen.Vfs/FileSystemChange.cs b/Source/BookGen.Vfs/FileSystemChange.cs index 1a633876..aec7d9a4 100644 --- a/Source/BookGen.Vfs/FileSystemChange.cs +++ b/Source/BookGen.Vfs/FileSystemChange.cs @@ -1,4 +1,9 @@ -namespace BookGen.Vfs; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Vfs; public sealed class FileSystemChangeEventArgs : EventArgs { diff --git a/Source/BookGen.Vfs/FileSystemObserver.cs b/Source/BookGen.Vfs/FileSystemObserver.cs index f298b266..ac3c7f47 100644 --- a/Source/BookGen.Vfs/FileSystemObserver.cs +++ b/Source/BookGen.Vfs/FileSystemObserver.cs @@ -1,4 +1,9 @@ -using System.Diagnostics; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Diagnostics; using Microsoft.Extensions.Logging; diff --git a/Source/BookGen.Vfs/IFileSystemObserver.cs b/Source/BookGen.Vfs/IFileSystemObserver.cs index 267acd1e..48315a0e 100644 --- a/Source/BookGen.Vfs/IFileSystemObserver.cs +++ b/Source/BookGen.Vfs/IFileSystemObserver.cs @@ -1,4 +1,9 @@ -namespace BookGen.Vfs; +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Vfs; public interface IFileSystemObserver : IDisposable { From 5eb17868bbd24d85ba65812300dd7197f6181285 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Wed, 29 Jul 2026 18:40:54 +0200 Subject: [PATCH 14/18] CSV table extension --- Docs/manual.md | 3 + .../Renderers/SvgMathBlockRenderer.cs | 2 +- .../Renderers/SvgMathInlineRenderer.cs | 2 +- .../SyntaxRenderPlugins/CsvRenderPlugin.cs | 113 ++++++++++++++++++ .../SyntaxRenderPlugins/LatexRenderPlugin.cs | 4 +- .../MermaidRenderPlugin.cs | 4 +- .../NomnomlRenderPlugin.cs | 4 +- .../PlantUmlRenderPlugin.cs | 4 +- .../SyntaxRenderPlugins/QrCodeRenderPlugin.cs | 4 +- .../SyntaxRendererPlugin.cs | 4 +- .../TerminalRenderPlugin.cs | 4 +- .../Markdown/Renderers/SyntaxRenderer.cs | 10 +- .../Lib/UT_MarkdownConverter_Specials.cs | 79 ++++++++++++ 13 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/CsvRenderPlugin.cs diff --git a/Docs/manual.md b/Docs/manual.md index 344ef518..397aed46 100644 --- a/Docs/manual.md +++ b/Docs/manual.md @@ -198,6 +198,9 @@ Markdown files are the main input format for BookGen. BookGen supports the follo * YAML front matter for metadata * Table of contents via the `[toc]` or `[toc maxlevel=n]` tag, where n is the level of headdings to include * Keyboard input via the `<<` and `>>` tags, which will render the text between them as keyboard input. +* Comma separated values tables via the `table-csv` language identifier for source code. It only works for comma separated values. +* Tab separated values tables via the `table-tsv` language identifier for source code. It only works for tab separated values. +* Semicolon separated values tables via the `table-ssv` language identifier for source code. It only works for semicolon separated values. ### YAML Front matter diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathBlockRenderer.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathBlockRenderer.cs index 21777471..a854a5b2 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathBlockRenderer.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathBlockRenderer.cs @@ -26,7 +26,7 @@ protected override void Write(HtmlRenderer renderer, MathBlock obj) renderer.EnsureLine(); renderer.Write(""); string code = obj.GetCode(); - renderer.Write(_latexRenderPlugin.Render(code)); + renderer.Write(_latexRenderPlugin.Render(code, "")); renderer.WriteLine("
    "); } } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathInlineRenderer.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathInlineRenderer.cs index 45986321..e9f47480 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathInlineRenderer.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SvgMathInlineRenderer.cs @@ -31,7 +31,7 @@ protected override void Write(HtmlRenderer renderer, MathInline obj) } renderer.Write(""); - renderer.Write(_latexRenderPlugin.Render(obj.Content.Text)); + renderer.Write(_latexRenderPlugin.Render(obj.Content.Text, "")); renderer.Write(""); } } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/CsvRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/CsvRenderPlugin.cs new file mode 100644 index 00000000..1a40225f --- /dev/null +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/CsvRenderPlugin.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Diagnostics; +using System.Text; +using System.Web; + +namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; + +internal sealed class CsvRenderPlugin : SyntaxRendererPlugin +{ + private const string ComaSeperated = "table-csv"; + private const string TabSeperated = "table-tsv"; + private const string SemicolonSeperated = "table-ssv"; + + public override string[] LanguageMonikers { get; } + = [ComaSeperated, TabSeperated, SemicolonSeperated]; + + public override string Render(string code, string parsedLanguageMoniker) + { + var rendered = new StringBuilder(code.Length); + char delimiter = GetDelimiter(parsedLanguageMoniker); + using var parser = new CsvParser(delimiter, code); + rendered.AppendLine(""); + foreach (List record in parser.GetRecords()) + { + rendered.AppendLine(""); + foreach (string field in record) + { + rendered.AppendLine($""); + } + rendered.AppendLine(""); + } + rendered.AppendLine("
    {HttpUtility.HtmlEncode(field)}
    "); + return rendered.ToString(); + } + + private static char GetDelimiter(string parsedLanguageMoniker) + { + return parsedLanguageMoniker switch + { + ComaSeperated => ',', + TabSeperated => '\t', + SemicolonSeperated => ';', + _ => throw new UnreachableException($"Unknown language moniker: {parsedLanguageMoniker}"), + }; + } + + internal sealed class CsvParser : IDisposable + { + private readonly StringReader _reader; + private readonly char _delimiter; + + public CsvParser(char delimiter, string input) + { + _reader = new StringReader(input); + _delimiter = delimiter; + } + + public void Dispose() + { + _reader.Dispose(); + } + + public IEnumerable> GetRecords() + { + string? line; + while ((line = _reader.ReadLine()) != null) + { + if (!string.IsNullOrWhiteSpace(line)) + yield return ParseLine(line); + } + } + + private List ParseLine(string line) + { + var result = new List(); + var current = new StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (c == '"') + { + if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') + { + current.Append('"'); + i++; + } + else + { + inQuotes = !inQuotes; + } + } + else if (c == _delimiter && !inQuotes) + { + result.Add(current.ToString()); + current.Clear(); + } + else + { + current.Append(c); + } + } + + result.Add(current.ToString()); + return result; + } + } +} diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/LatexRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/LatexRenderPlugin.cs index 38f680ac..4954ef73 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/LatexRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/LatexRenderPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal sealed class LatexRenderPlugin(IRenderInterop renderInterop) : SyntaxRendererPlugin { - public override string LanguageMoniker { get; } = "latex"; + public override string[] LanguageMonikers { get; } = ["latex"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { return RendererImgage(renderInterop.RenderLatex(code)); } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/MermaidRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/MermaidRenderPlugin.cs index 680fd04a..ad834cfd 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/MermaidRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/MermaidRenderPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal sealed class MermaidRenderPlugin(IRenderInterop renderInterop) : SyntaxRendererPlugin { - public override string LanguageMoniker { get; } = "mermaid"; + public override string[] LanguageMonikers { get; } = ["mermaid"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { return RendererImgage(renderInterop.RenderMermaid(code)); } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/NomnomlRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/NomnomlRenderPlugin.cs index 6f524cdf..61bdf0de 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/NomnomlRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/NomnomlRenderPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal sealed class NomnomlRenderPlugin(IRenderInterop renderInterop) : SyntaxRendererPlugin { - public override string LanguageMoniker { get; } = "nomnoml"; + public override string[] LanguageMonikers { get; } = ["nomnoml"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { return RendererImgage(renderInterop.RenderNomnoml(code)); } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/PlantUmlRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/PlantUmlRenderPlugin.cs index 7291fc33..de8401ba 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/PlantUmlRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/PlantUmlRenderPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal sealed class PlantUmlRenderPlugin(IRenderInterop renderInterop) : SyntaxRendererPlugin { - public override string LanguageMoniker => "plantuml"; + public override string[] LanguageMonikers { get; } = ["plantuml"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { return RendererImgage(renderInterop.RenderPlantUml(code)); } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/QrCodeRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/QrCodeRenderPlugin.cs index 3ff291d6..805686a9 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/QrCodeRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/QrCodeRenderPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal sealed class QrCodeRenderPlugin(IRenderInterop renderInterop) : SyntaxRendererPlugin { - public override string LanguageMoniker { get; } = "qrcode"; + public override string[] LanguageMonikers { get; } = ["qrcode"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { return RendererImgage(renderInterop.RenderQrCode(code)); } diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/SyntaxRendererPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/SyntaxRendererPlugin.cs index a4134219..55c2838c 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/SyntaxRendererPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/SyntaxRendererPlugin.cs @@ -9,9 +9,9 @@ namespace BookGen.Lib.Rendering.Markdown.Renderers.SyntaxRenderPlugins; internal abstract class SyntaxRendererPlugin { - public abstract string LanguageMoniker { get; } + public abstract string[] LanguageMonikers { get; } - public abstract string Render(string code); + public abstract string Render(string code, string parsedLanguageMoniker); protected static string RendererImgage(ImageResult img) { diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/TerminalRenderPlugin.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/TerminalRenderPlugin.cs index e885ec12..87d2400e 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/TerminalRenderPlugin.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderPlugins/TerminalRenderPlugin.cs @@ -18,9 +18,9 @@ internal sealed class TerminalRenderPlugin : SyntaxRendererPlugin
"""; - public override string LanguageMoniker { get; } = "terminal"; + public override string[] LanguageMonikers { get; } = ["terminal"]; - public override string Render(string code) + public override string Render(string code, string parsedLanguageMoniker) { const string codeTag = ""; return TerminalHtml.Replace(codeTag, TerminalRenderer.RenderAnsiCode(HttpUtility.HtmlEncode(code))); diff --git a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderer.cs b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderer.cs index 869305ab..55c46b36 100644 --- a/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderer.cs +++ b/Source/BookGen.Lib/Rendering/Markdown/Renderers/SyntaxRenderer.cs @@ -60,12 +60,16 @@ public SyntaxRenderer(CodeBlockRenderer underlyingRenderer, RegisterPlugin(new QrCodeRenderPlugin(_renderInterop)); RegisterPlugin(new MermaidRenderPlugin(_renderInterop)); RegisterPlugin(new PlantUmlRenderPlugin(_renderInterop)); + RegisterPlugin(new CsvRenderPlugin()); } private void RegisterPlugin(SyntaxRendererPlugin plugin) { - _plugins[plugin.LanguageMoniker] = plugin; - _supportedLanguages.Add(plugin.LanguageMoniker); + foreach (var languageMoniker in plugin.LanguageMonikers.Distinct()) + { + _plugins[languageMoniker] = plugin; + _supportedLanguages.Add(languageMoniker); + } } protected override void Write(HtmlRenderer renderer, CodeBlock obj) @@ -93,7 +97,7 @@ protected override void Write(HtmlRenderer renderer, CodeBlock obj) if (_plugins.TryGetValue(languageMoniker, out SyntaxRendererPlugin? plugin)) { - renderer.Write(plugin.Render(code)); + renderer.Write(plugin.Render(code, languageMoniker)); return; } diff --git a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs index 61499a5f..7be09646 100644 --- a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs +++ b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter_Specials.cs @@ -232,4 +232,83 @@ public void EnsureThat_Math_Block_Works() Assert.That(result, Is.EqualTo(expected).Using(comparer)); } + + private const string Tsv = """ + ```table-tsv + foo bar baz + "col11 >ong" col12 col13 + col21 col22 col23 + col31 col32 col33 + ``` + """; + + private const string Csv = """ + ```table-csv + foo,bar,baz + "col11 >ong",col12,col13 + col21,col22,col23 + col31,col32,col33 + ``` + """; + + private const string Ssv = """ + ```table-ssv + foo;bar;baz + "col11 >ong";col12;col13 + col21;col22;col23 + col31;col32;col33 + ``` + """; + + + [TestCase(Tsv)] + [TestCase(Csv)] + [TestCase(Ssv)] + public void EnsureThat_Language_Csv_Works(string input) + { + using 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 + }) + }; + + using var sut = new MarkdownConverter(settings); + + string result = sut.RenderMarkdownToHtml(input); + + string expected = """ + + + + + + + + + + + + + + + + + + + + + +
foobarbaz
col11 >ongcol12col13
col21col22col23
col31col32col33
+ + """; + + Assert.That(result, Is.EqualTo(expected).Using(comparer)); + + } } From 5e5edbcf99509f1059a691eb7da1996358343654 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Wed, 29 Jul 2026 19:08:42 +0200 Subject: [PATCH 15/18] Diagram2svg command --- Docs/changelog.md | 4 + .../Commands/Convert/Diagram2SvgCommand.cs | 101 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs diff --git a/Docs/changelog.md b/Docs/changelog.md index 9f1a8b1c..6cffaa41 100644 --- a/Docs/changelog.md +++ b/Docs/changelog.md @@ -16,12 +16,16 @@ tags: '' * New: Mermaid diagram rendering supported via mmdr * New: Nomnoml diagram rendering supported via nomnoml * New: PlantUML diagram rendering supported. Note: Requires seperate PlantUMl download and installation +* New: convert diagram2svg command to convert nomnoml and mermaid diagrams to SVG format * New: Search command to search text in bookgen project contents * New: Support for openCli specification * New: Mixcloud media embeding supported * New: Html2OpenXml command to convert HTML to OpenXML format for Word processing * New: Template engine reworked, now supports functions and is more robust * New: Preview command to preview the current project files in a browser +* New: table-csv syntax highlighting, that renders the data as a table in HTML output +* New: table-tsv syntax highlighting, that renders the data as a table in HTML output +* New: table-ssv syntax highlighting, that renders the data as a table in HTML output * Change: Commands documentation generated from code and is now more complete * Change: App settings reworked * Change: Dropped mathjax as dependency for formula rendering. New formula rendering is based on katex and works with markdown syntax diff --git a/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs new file mode 100644 index 00000000..76e328f5 --- /dev/null +++ b/Source/BookGen/Commands/Convert/Diagram2SvgCommand.cs @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +// (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 BookGen.Lib.Rendering.Images; +using BookGen.Lib.Rendering.Markdown.RenderInterop; +using BookGen.Vfs; + +using Microsoft.Extensions.Logging; + +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 Arguments : ArgumentsBase + { + internal enum DiagramType + { + Unknown, + Mermaid, + Nomnoml, + } + + [Switch("i", "input", Required = true)] + [Description("Input file containing the diagram definition.")] + public string InputFile { get; set; } = string.Empty; + + [Switch("o", "output", Required = true)] + [Description("Output file where the rendered SVG will be saved.")] + public string OutputFile { get; set; } = string.Empty; + + [Switch("t", "type", Required = true)] + [Description("Type of the diagram to render. Can be Mermaid or Nomnoml.")] + public DiagramType Type { get; set; } = DiagramType.Unknown; + + public override ValidationResult Validate(IValidationContext context) + { + ValidationResult result = new(); + + if (string.IsNullOrEmpty(InputFile)) + result.AddIssue("Input file can't be empty"); + + if (!context.FileSystem.FileExists(InputFile)) + result.AddIssue($"Input file '{InputFile}' does not exist"); + + if (string.IsNullOrEmpty(OutputFile)) + result.AddIssue("Output file must be specified"); + + if (Type == DiagramType.Unknown) + result.AddIssue("Diagram type must be specified"); + + return result; + } + + public override void ModifyAfterValidation() + { + OutputFile = Path.ChangeExtension(OutputFile, ".svg"); + } + } + + private readonly ILogger _log; + private readonly IWritableFileSystem _fileSystem; + private readonly IProgramPathResolver _programPathResolver; + private readonly IAssetSource _assetSource; + + public Diagram2SvgCommand(ILogger log, IWritableFileSystem fileSystem, IProgramPathResolver programPathResolver, IAssetSource assetSource) + { + _log = log; + _fileSystem = fileSystem; + _programPathResolver = programPathResolver; + _assetSource = assetSource; + } + + public override async Task ExecuteAsync(Arguments arguments, IReadOnlyList context, CancellationToken token) + { + using var render = IRenderInterop.CreateForSvg(_assetSource, _programPathResolver); + + string inputContent = await _fileSystem.ReadAllTextAsync(arguments.InputFile); + + ImageResult result = arguments.Type switch + { + Arguments.DiagramType.Mermaid => render.RenderMermaid(inputContent), + Arguments.DiagramType.Nomnoml => render.RenderNomnoml(inputContent), + _ => throw new InvalidOperationException($"Unsupported diagram type: {arguments.Type}"), + }; + + await _fileSystem.WriteAllTextAsync(arguments.OutputFile, result.Data); + + return ExitCodes.Success; + + } +} From 1ee5b54419e32b9b242a7f09bec7175bbaa47a6a Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 30 Jul 2026 19:01:06 +0200 Subject: [PATCH 16/18] Dependencies update --- Directory.Packages.props | 2 +- Source/BookGen/packages.lock.json | 6 +++--- download-assets-pipeline.sh | 6 +++--- download-assets.ps1 | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b4afed65..f85c5d70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 7ceb1a33..f454e4eb 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.IO.RecyclableMemoryStream": { "type": "Direct", - "requested": "[3.0.1, )", - "resolved": "3.0.1", - "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + "requested": "[4.0.0-preview, )", + "resolved": "4.0.0-preview", + "contentHash": "ueAhj4Qj4HgGyS0+efG2IjvqYHPWkayE+GWUnseWe6aJQagoxHWr2ljabmXd9k4QpSH2jpoTd7TTzgUsc2selA==" }, "Roslynator.Analyzers": { "type": "Direct", diff --git a/download-assets-pipeline.sh b/download-assets-pipeline.sh index 93cbcd40..321275e2 100644 --- a/download-assets-pipeline.sh +++ b/download-assets-pipeline.sh @@ -4,9 +4,9 @@ mkdir -p "./Assets/dictionaries" touch "./Assets/dictionaries/keep.file" cd "./Assets/tools" -curl -L -O "https://github.com/erweixin/RaTeX/releases/download/v0.1.13/ratex-cli-v0.1.13-x86_64-unknown-linux-musl.tar.gz" -tar -xzf ./ratex-cli-v0.1.13-x86_64-unknown-linux-musl.tar.gz -C . -mv ./ratex-cli-v0.1.13-x86_64-unknown-linux-musl/render-svg ./ratex-svg +curl -L -O "https://github.com/erweixin/RaTeX/releases/download/v0.1.14/ratex-cli-v0.1.14-x86_64-unknown-linux-musl.tar.gz" +tar -xzf ./ratex-cli-v0.1.14-x86_64-unknown-linux-musl.tar.gz -C . +mv ./ratex-cli-v0.1.14-x86_64-unknown-linux-musl/render-svg ./ratex-svg curl -L -O "https://github.com/1jehuang/mermaid-rs-renderer/releases/download/v0.3.1/mmdr-x86_64-unknown-linux-gnu.tar.gz" tar -xzf ./mmdr-x86_64-unknown-linux-gnu.tar.gz -C . diff --git a/download-assets.ps1 b/download-assets.ps1 index baf2fd65..9939b156 100644 --- a/download-assets.ps1 +++ b/download-assets.ps1 @@ -27,8 +27,8 @@ function download-dictionaries { function download-tools { Set-Location $toolsPath - curl.exe -L "https://github.com/erweixin/RaTeX/releases/download/v0.1.13/ratex-cli-v0.1.13-x86_64-pc-windows-msvc.zip" -o ratex-windows.zip - curl.exe -L "https://github.com/erweixin/RaTeX/releases/download/v0.1.13/ratex-cli-v0.1.13-x86_64-unknown-linux-musl.tar.gz" -o ratex-linux.tar.gz + curl.exe -L "https://github.com/erweixin/RaTeX/releases/download/v0.1.14/ratex-cli-v0.1.14-x86_64-pc-windows-msvc.zip" -o ratex-windows.zip + curl.exe -L "https://github.com/erweixin/RaTeX/releases/download/v0.1.14/ratex-cli-v0.1.14-x86_64-unknown-linux-musl.tar.gz" -o ratex-linux.tar.gz curl.exe -L "https://github.com/1jehuang/mermaid-rs-renderer/releases/download/v0.3.1/mmdr-x86_64-pc-windows-msvc.zip" -o mmdr-windows.zip curl.exe -L "https://github.com/1jehuang/mermaid-rs-renderer/releases/download/v0.3.1/mmdr-x86_64-unknown-linux-gnu.tar.gz" -o mmdr-linux.tar.gz @@ -40,14 +40,14 @@ function download-tools { # RaTeX Expand-Archive .\ratex-windows.zip -Force -DestinationPath . - Move-Item -Force .\ratex-cli-v0.1.13-x86_64-pc-windows-msvc\render-svg.exe .\ratex-svg.exe + Move-Item -Force .\ratex-cli-v0.1.14-x86_64-pc-windows-msvc\render-svg.exe .\ratex-svg.exe Remove-Item .\ratex-windows.zip - Remove-Item .\ratex-cli-v0.1.13-x86_64-pc-windows-msvc -Recurse -Force + Remove-Item .\ratex-cli-v0.1.14-x86_64-pc-windows-msvc -Recurse -Force tar -xzf .\ratex-linux.tar.gz -C . - Move-Item -Force .\ratex-cli-v0.1.13-x86_64-unknown-linux-musl\render-svg .\ratex-svg + Move-Item -Force .\ratex-cli-v0.1.14-x86_64-unknown-linux-musl\render-svg .\ratex-svg Remove-Item .\ratex-linux.tar.gz - Remove-Item .\ratex-cli-v0.1.13-x86_64-unknown-linux-musl -Recurse -Force + Remove-Item .\ratex-cli-v0.1.14-x86_64-unknown-linux-musl -Recurse -Force Set-Location $startdir } From c26a9e98532e131d9a1ec647c5acef2a51438222 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 30 Jul 2026 19:39:10 +0200 Subject: [PATCH 17/18] Fix review findings --- .../BookGen.Lib/Http/ErrorPageTemplate.html | 2 +- Source/BookGen.Lib/Http/HttpServerFactory.cs | 6 ++--- Source/BookGen.Lib/Http/QRCodeTemplate.html | 2 +- .../Tooldownloaders/ChromaDownloader.cs | 23 ++++++++----------- .../Tooldownloaders/CopyPartyDownloader.cs | 23 ++++++++----------- .../Tooldownloaders/GithubDownloader.cs | 23 ++++++++----------- .../BookGen/Tooldownloaders/GlowDownloader.cs | 23 ++++++++----------- .../MicrosoftEditToolDownloader.cs | 23 ++++++++----------- .../Tooldownloaders/PandocTooldownloader.cs | 23 ++++++++----------- 9 files changed, 59 insertions(+), 89 deletions(-) diff --git a/Source/BookGen.Lib/Http/ErrorPageTemplate.html b/Source/BookGen.Lib/Http/ErrorPageTemplate.html index 29385f06..edb073a9 100644 --- a/Source/BookGen.Lib/Http/ErrorPageTemplate.html +++ b/Source/BookGen.Lib/Http/ErrorPageTemplate.html @@ -160,7 +160,7 @@

Error {{Code}}


- An error occured during serving your request.
+ An error occurred during serving your request.
More info about this error | Return to home

{{Message}}

diff --git a/Source/BookGen.Lib/Http/HttpServerFactory.cs b/Source/BookGen.Lib/Http/HttpServerFactory.cs index bb9a025f..b30fb53b 100644 --- a/Source/BookGen.Lib/Http/HttpServerFactory.cs +++ b/Source/BookGen.Lib/Http/HttpServerFactory.cs @@ -19,7 +19,7 @@ public static class HttpServerFactory public const int HostingPort = 8081; public const int PreviewPort = 8181; - private static int ChoosePort(int @default = HostingPort) + private static int ChoosePort(int @default) { IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties(); @@ -56,7 +56,7 @@ private static int ChoosePort(int @default = HostingPort) public static IHttpServer CreateServerForDirectoryHosting(string directoryToServe, ILogger logger) { - var server = new HttpServer(ChoosePort(), logger, localHostOnly: false); + var server = new HttpServer(ChoosePort(HostingPort), logger, localHostOnly: false); server.AddStaticFiles(directory: directoryToServe, requestPath: "", directoryBrowseEnabled: true); server.AddRoute(new ApiMetaData("/qrcodelink", MediaTypeNames.Text.Html), async context => { @@ -72,7 +72,7 @@ public static IHttpServer CreateServerForPreview(IReadOnlyFileSystem source, IProgramPathResolver programPathResolver, IAssetSource assetSource) { - var server = new HttpServer(ChoosePort(), logger, localHostOnly: true); + var server = new HttpServer(ChoosePort(PreviewPort), logger, localHostOnly: true); var previewRoutes = new PreviewRoutes(source, logger, programPathResolver, assetSource); server.AddRoutes(previewRoutes); return server; diff --git a/Source/BookGen.Lib/Http/QRCodeTemplate.html b/Source/BookGen.Lib/Http/QRCodeTemplate.html index 2ea1a047..4f3f63ac 100644 --- a/Source/BookGen.Lib/Http/QRCodeTemplate.html +++ b/Source/BookGen.Lib/Http/QRCodeTemplate.html @@ -162,7 +162,7 @@
-

Liks for this server

+

Links for this server

{{Links}}
diff --git a/Source/BookGen/Tooldownloaders/ChromaDownloader.cs b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs index a9d7efe8..57959af8 100644 --- a/Source/BookGen/Tooldownloaders/ChromaDownloader.cs +++ b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs @@ -19,23 +19,18 @@ public ChromaDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Chroma", - ApproximateSize = "8 MiB", - RepoOwner = "alecthomas", - RepoName = "chroma", - FolderName = "chroma", - }; - } + Name = "Chroma", + ApproximateSize = "8 MiB", + RepoOwner = "alecthomas", + RepoName = "chroma", + FolderName = "chroma", + }; } + public override ToolInfo ToolInfo { get; } + protected override Task Extract(IDownloadUi ui, Stream stream) => Extractor.ExtractTarGz(ui, stream, ToolInfo.FolderName); diff --git a/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs b/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs index 5df3a56f..e216d80b 100644 --- a/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs +++ b/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs @@ -19,23 +19,18 @@ public CopyPartyDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Copyparty.exe", - ApproximateSize = "13 MiB", - RepoOwner = "9001", - RepoName = "copyparty", - FolderName = "copyparty", - }; - } + Name = "Copyparty.exe", + ApproximateSize = "13 MiB", + RepoOwner = "9001", + RepoName = "copyparty", + FolderName = "copyparty", + }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return releaseAssets diff --git a/Source/BookGen/Tooldownloaders/GithubDownloader.cs b/Source/BookGen/Tooldownloaders/GithubDownloader.cs index fc24c800..4b4f5a76 100644 --- a/Source/BookGen/Tooldownloaders/GithubDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GithubDownloader.cs @@ -19,23 +19,18 @@ public GithubDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Github CLI", - ApproximateSize = "38 MiB", - RepoOwner = "cli", - RepoName = "cli", - FolderName = "github-cli", - }; - } + Name = "Github CLI", + ApproximateSize = "38 MiB", + RepoOwner = "cli", + RepoName = "cli", + FolderName = "github-cli", + }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return releaseAssets diff --git a/Source/BookGen/Tooldownloaders/GlowDownloader.cs b/Source/BookGen/Tooldownloaders/GlowDownloader.cs index 575ea870..ec233250 100644 --- a/Source/BookGen/Tooldownloaders/GlowDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GlowDownloader.cs @@ -19,23 +19,18 @@ public GlowDownloader(IApiClient apiClient, ILogger log) : base(apiClient, memoryStreamManager, log) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Glow", - ApproximateSize = "18 MiB", - RepoOwner = "charmbracelet", - RepoName = "glow", - FolderName = "glow", - }; - } + Name = "Glow", + ApproximateSize = "18 MiB", + RepoOwner = "charmbracelet", + RepoName = "glow", + FolderName = "glow", + }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return releaseAssets diff --git a/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs b/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs index 26b97cc9..b871d28c 100644 --- a/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs +++ b/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs @@ -19,23 +19,18 @@ public MicrosoftEditToolDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Microsoft Edit", - ApproximateSize = "3.6 MiB", - RepoOwner = "microsoft", - RepoName = "edit", - FolderName = "ms-edit", - }; - } + Name = "Microsoft Edit", + ApproximateSize = "3.6 MiB", + RepoOwner = "microsoft", + RepoName = "edit", + FolderName = "ms-edit", + }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return releaseAssets diff --git a/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs b/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs index 0557d81d..c657daeb 100644 --- a/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs +++ b/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs @@ -19,23 +19,18 @@ public PandocTooldownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - public override ToolInfo ToolInfo - { - get + ToolInfo = new ToolInfo { - return new ToolInfo - { - Name = "Pandoc", - ApproximateSize = "217 MiB", - RepoOwner = "jgm", - RepoName = "pandoc", - FolderName = "pandoc", - }; - } + Name = "Pandoc", + ApproximateSize = "217 MiB", + RepoOwner = "jgm", + RepoName = "pandoc", + FolderName = "pandoc", + }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return releaseAssets From fc5b44a0fdc7b602c99864afd0ed25e29e463972 Mon Sep 17 00:00:00 2001 From: webmaster442 Date: Thu, 30 Jul 2026 19:44:40 +0200 Subject: [PATCH 18/18] Github actions update --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/dotnet.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7377db62..e9b17a9f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,14 +38,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: lfs: true submodules: recursive #https://github.com/actions/setup-dotnet/blob/main/README.md - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 11.0.x #dotnet-quality: 'ga' diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5d4a0c26..cba52035 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 with: lfs: true submodules: recursive #https://github.com/actions/setup-dotnet/blob/main/README.md - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 11.0.x #dotnet-quality: 'ga'