diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 24ad8d8a..e9b17a9f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,18 +38,18 @@ 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: 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..cba52035 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,17 +13,17 @@ 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: 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/.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 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/Directory.Packages.props b/Directory.Packages.props index d5516913..f85c5d70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,14 +12,10 @@ - - - - - - - - + + + + @@ -33,7 +29,7 @@ - + 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/Docs/changelog.md b/Docs/changelog.md index 9046dd40..6cffaa41 100644 --- a/Docs/changelog.md +++ b/Docs/changelog.md @@ -9,16 +9,23 @@ 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 * 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/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.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.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 6510ee4d..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, )", 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/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/DisposableTracker.cs b/Source/BookGen.Lib/DisposableTracker.cs new file mode 100644 index 00000000..49983bb3 --- /dev/null +++ b/Source/BookGen.Lib/DisposableTracker.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// (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 +{ + 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/ErrorPageTemplate.html b/Source/BookGen.Lib/Http/ErrorPageTemplate.html index 329ded85..edb073a9 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 + An error occurred during serving your request.
+ 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 fa7977c6..e9a4d224 100644 --- a/Source/BookGen.Lib/Http/HttpServer.cs +++ b/Source/BookGen.Lib/Http/HttpServer.cs @@ -10,27 +10,40 @@ 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; +using BookGen.Lib; + 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; @@ -62,6 +75,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 @@ -123,7 +142,7 @@ public void AddRoute(ApiMetaData metaData, RequestDelegate handler) } } - public void AddRoutes(IReadOnlyDictionary routes) + public void AddRoutes(IEnumerable> routes) { foreach (KeyValuePair route in routes) { @@ -131,8 +150,26 @@ public void AddRoutes(IReadOnlyDictionary routes) } } + public void AddRoutes(IRouteProvider provider) + { + if (provider is IDisposable disposable) + { + _disposableTracker.Track(disposable); + } + foreach ((ApiMetaData metaData, RequestDelegate handler) in provider.Routes) + { + AddRoute(metaData, handler); + } + } + public IEnumerable GetListenUrls() { + if (_localhostOnly) + { + yield return $"http://localhost:{Port}"; + yield break; + } + foreach ((IPAddress? adress, IPAddress _) in GetIpAdresses()) { yield return $"http://{adress}:{Port}"; @@ -161,7 +198,4 @@ public async Task StartAsync() public async Task StopAsync() => await _app.StopAsync(); - - public ValueTask DisposeAsync() - => _app.DisposeAsync(); } diff --git a/Source/BookGen.Lib/Http/ServerFactory.cs b/Source/BookGen.Lib/Http/HttpServerFactory.cs similarity index 70% rename from Source/BookGen.Lib/Http/ServerFactory.cs rename to Source/BookGen.Lib/Http/HttpServerFactory.cs index ca0fe4cc..b30fb53b 100644 --- a/Source/BookGen.Lib/Http/ServerFactory.cs +++ b/Source/BookGen.Lib/Http/HttpServerFactory.cs @@ -6,16 +6,20 @@ 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) + private static int ChoosePort(int @default) { IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties(); @@ -52,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(HostingPort), logger, localHostOnly: false); server.AddStaticFiles(directory: directoryToServe, requestPath: "", directoryBrowseEnabled: true); server.AddRoute(new ApiMetaData("/qrcodelink", MediaTypeNames.Text.Html), async context => { @@ -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(PreviewPort), logger, localHostOnly: true); + var previewRoutes = new PreviewRoutes(source, logger, programPathResolver, assetSource); + server.AddRoutes(previewRoutes); + return server; + } } diff --git a/Source/BookGen.Lib/Http/IRouteProvider.cs b/Source/BookGen.Lib/Http/IRouteProvider.cs new file mode 100644 index 00000000..81ecb985 --- /dev/null +++ b/Source/BookGen.Lib/Http/IRouteProvider.cs @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +// (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; + +internal interface IRouteProvider +{ + IEnumerable<(ApiMetaData metaData, RequestDelegate handler)> Routes { get; } +} 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/Http/PageFactory.cs b/Source/BookGen.Lib/Http/PageFactory.cs index 10f408cd..3e21b102 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($"

    {HttpUtility.HtmlEncode(group.Key)}

    "); + 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/PreviewRoutes.cs b/Source/BookGen.Lib/Http/PreviewRoutes.cs new file mode 100644 index 00000000..6defef68 --- /dev/null +++ b/Source/BookGen.Lib/Http/PreviewRoutes.cs @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------- +// (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; + +using BookGen.Lib.AppSettings; +using BookGen.Lib.Domain; +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.TemplatePreview); + + _observer = _source.CreateObserver(logger, "*.md"); + _observer.FileChanged += OnFileChange; + _allowedFiles.Clear(); + + 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) + { + lock (_lock) + { + switch (e.ChangeType) + { + case FileSystemChangeEventArgs.Change.Created: + _allowedFiles.Add(Path.GetRelativePath(_source.Scope, e.FileName)); + break; + case FileSystemChangeEventArgs.Change.Changed: + break; + case FileSystemChangeEventArgs.Change.Renamed: + int oldindex = _allowedFiles.IndexOf(Path.GetRelativePath(_source.Scope, e.FileName)); + if (oldindex != -1 && !string.IsNullOrEmpty(e.NewFileName)) + { + _allowedFiles[oldindex] = Path.GetRelativePath(_source.Scope, e.NewFileName); + } + else if (!string.IsNullOrEmpty(e.NewFileName)) + { + _allowedFiles.Add(Path.GetRelativePath(_source.Scope, e.NewFileName)); + } + break; + case FileSystemChangeEventArgs.Change.Deleted: + _allowedFiles.Remove(Path.GetRelativePath(_source.Scope, e.FileName)); + break; + } + } + } + + public void Dispose() + { + _observer.FileChanged -= OnFileChange; + _observer.Dispose(); + _markdownConverter.Dispose(); + _renderSettings.Dispose(); + } + + public IEnumerable<(ApiMetaData metaData, RequestDelegate handler)> Routes + { + 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) + { + string content; + lock (_lock) + { + content = PageFactory.GetFiles(_allowedFiles); + } + + var viewData = new ViewData + { + Host = "/", + Content = content, + 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"]; + if (!CanServe(fileName)) + { + var viewData = new ViewData + { + Host = "/", + Content = "Not a markdown file", + Title = "Error", + LastModified = DateTime.UtcNow, + }; + await SendData(context, + HttpStatusCode.BadRequest, + _templateEngine.Render(_template, viewData), + MediaTypeNames.Text.Html); + + return; + } + + SourceFile source = await _source.GetSourceFile(fileName, _logger); + + var data = new ViewData + { + Content = _markdownConverter.RenderMarkdownToHtml(source.Content), + Title = source.FrontMatter.Title, + LastModified = source.LastModified, + Host = "/" + }; + + await SendData(context, + HttpStatusCode.OK, + _templateEngine.Render(_template, data), + MediaTypeNames.Text.Html); + + } + + private bool CanServe([NotNullWhen(true)] string? fileName) + { + lock (_lock) + { + return !string.IsNullOrEmpty(fileName) + && _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/QRCodeTemplate.html b/Source/BookGen.Lib/Http/QRCodeTemplate.html index 557f1a39..4f3f63ac 100644 --- a/Source/BookGen.Lib/Http/QRCodeTemplate.html +++ b/Source/BookGen.Lib/Http/QRCodeTemplate.html @@ -162,8 +162,8 @@
    -

    Liks for this server

    - {{links}} +

    Links for this server

    + {{Links}}
      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/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 402e9f33..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==", - "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==", + "requested": "[11.0.0-preview.6.26359.118, )", + "resolved": "11.0.0-preview.6.26359.118", + "contentHash": "lUAZ402asMsLYe483nXXQIphklpTNDP6LmhmrwB8V3gBTjXeUp40N6h/Ap5QOiGHqsAiooznL4lBETGSncYsxQ==", "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,7 +55,6 @@ "bookgen.shell.shared": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Spectre.Console": "[0.57.2, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } diff --git a/Source/BookGen.Vfs/BookGen.Vfs.csproj b/Source/BookGen.Vfs/BookGen.Vfs.csproj index 0eef9a3a..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)\ diff --git a/Source/BookGen.Vfs/FileSystemChange.cs b/Source/BookGen.Vfs/FileSystemChange.cs new file mode 100644 index 00000000..aec7d9a4 --- /dev/null +++ b/Source/BookGen.Vfs/FileSystemChange.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// (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 +{ + 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..ac3c7f47 --- /dev/null +++ b/Source/BookGen.Vfs/FileSystemObserver.cs @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +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) + { + _logger = logger; + _watcher = new FileSystemWatcher + { + Filter = filter, + Path = folder, + NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastWrite + | NotifyFilters.Size, + IncludeSubdirectories = true, + }; + _watcher.Changed += OnChanged; + _watcher.Created += OnChanged; + _watcher.Deleted += OnChanged; + _watcher.Renamed += OnRenamed; + _watcher.Error += OnError; + _watcher.EnableRaisingEvents = true; + } + + 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..48315a0e --- /dev/null +++ b/Source/BookGen.Vfs/IFileSystemObserver.cs @@ -0,0 +1,11 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +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/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/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; + + } +} diff --git a/Source/BookGen/Commands/Folder/PreviewCommand.cs b/Source/BookGen/Commands/Folder/PreviewCommand.cs new file mode 100644 index 00000000..861060f7 --- /dev/null +++ b/Source/BookGen/Commands/Folder/PreviewCommand.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +// (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; +using BookGen.Lib.AppSettings; +using BookGen.Lib.Http; +using BookGen.Vfs; + +using Microsoft.Extensions.Logging; + +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; + 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/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/Tooldownloaders/ChromaDownloader.cs b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs index 1b0f7bfd..57959af8 100644 --- a/Source/BookGen/Tooldownloaders/ChromaDownloader.cs +++ b/Source/BookGen/Tooldownloaders/ChromaDownloader.cs @@ -19,11 +19,7 @@ public ChromaDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Chroma", ApproximateSize = "8 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + 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 a9a217fa..e216d80b 100644 --- a/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs +++ b/Source/BookGen/Tooldownloaders/CopyPartyDownloader.cs @@ -19,11 +19,7 @@ public CopyPartyDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Copyparty.exe", ApproximateSize = "13 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + 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 b1ce9e6b..4b4f5a76 100644 --- a/Source/BookGen/Tooldownloaders/GithubDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GithubDownloader.cs @@ -19,11 +19,7 @@ public GithubDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Github CLI", ApproximateSize = "38 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + 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 1d896cee..ec233250 100644 --- a/Source/BookGen/Tooldownloaders/GlowDownloader.cs +++ b/Source/BookGen/Tooldownloaders/GlowDownloader.cs @@ -19,11 +19,7 @@ public GlowDownloader(IApiClient apiClient, ILogger log) : base(apiClient, memoryStreamManager, log) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Glow", ApproximateSize = "18 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + 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 3b6fa854..b871d28c 100644 --- a/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs +++ b/Source/BookGen/Tooldownloaders/MicrosoftEditToolDownloader.cs @@ -19,11 +19,7 @@ public MicrosoftEditToolDownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Microsoft Edit", ApproximateSize = "3.6 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + 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 acf2fd87..c657daeb 100644 --- a/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs +++ b/Source/BookGen/Tooldownloaders/PandocTooldownloader.cs @@ -19,11 +19,7 @@ public PandocTooldownloader(IApiClient apiClient, ILogger logger) : base(apiClient, memoryStreamManager, logger) { - } - - protected override ToolInfo CreateToolInfo() - { - return new ToolInfo + ToolInfo = new ToolInfo { Name = "Pandoc", ApproximateSize = "217 MiB", @@ -33,6 +29,8 @@ protected override ToolInfo CreateToolInfo() }; } + public override ToolInfo ToolInfo { get; } + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) { return 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); diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index b7e2c379..f454e4eb 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,68 +18,32 @@ "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": { "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", @@ -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,14 +248,13 @@ "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, )" } @@ -397,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 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(); 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)); + + } } 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 }