Skip to content

Commit

Permalink
Feature: add mermaid in memory http server (#1794)
Browse files Browse the repository at this point in the history
* initial import of mermaid logic

* add logging wrappers and rework http server injection

* Update ci.yml

* Update ci.yml

* Update ci.yml

* Update Whipstaff.UnitTests.csproj

* Update Whipstaff.UnitTests.csproj

* add browser and channel support

* Update Whipstaff.UnitTests.csproj

* Update PlaywrightRendererTests.cs

* refactor http message logic

* vs file save issue

* reduce pkgs used for mermaid proj

* add browser type test

* test ToHttpRequestMessage method

* add npm restore to csproj

* add gzip creation of files

* fix files not being copied to wwwroot after npm restore

* logic to decompress result back to playwright

* add content type lookup

* recalc embedded files on clean build
  • Loading branch information
dpvreony authored Jun 28, 2024
1 parent c9ddf70 commit b7ccf70
Show file tree
Hide file tree
Showing 24 changed files with 3,455 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ jobs:
npm install
popd
- name: NPM package restore
run: |
pushd src\Whipstaff.Mermaid\
npm install
popd
- name: Changelog
uses: glennawatson/ChangeLog@v1
id: changelog
Expand Down
1 change: 1 addition & 0 deletions src/Whipstaff.Mermaid/HttpServer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wwwroot/lib
59 changes: 59 additions & 0 deletions src/Whipstaff.Mermaid/HttpServer/EmbeddedGzipFileProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

#if TBC
using System.Reflection;
using Microsoft.Extensions.FileProviders;

namespace Whipstaff.Mermaid.HttpServer
{
/// <summary>
/// Wrapper for the <see cref="EmbeddedFileProvider"/> that provides Gzip decompression for embedded resources.
/// </summary>
public class EmbeddedGzipFileProvider : EmbeddedFileProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="EmbeddedGzipFileProvider" /> class using the specified
/// assembly with the base namespace defaulting to the assembly name.
/// </summary>
/// <param name="assembly">The assembly that contains the embedded resources.</param>
public EmbeddedGzipFileProvider(Assembly assembly)
: base(assembly)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="EmbeddedGzipFileProvider" /> class using the specified
/// assembly and base namespace.
/// </summary>
/// <param name="assembly">The assembly that contains the embedded resources.</param>
/// <param name="baseNamespace">The base namespace that contains the embedded resources.</param>
public EmbeddedGzipFileProvider(Assembly assembly, string? baseNamespace)
: base(assembly, baseNamespace)
{
}

/// <summary>
/// Locates a file at the given path.
/// </summary>
/// <param name="subpath">The path that identifies the file. </param>
/// <returns>
/// The file information. Caller must check Exists property. A <see cref="NotFoundFileInfo" /> if the file could
/// not be found.
/// </returns>
// ReSharper disable once IdentifierTypo
public new IFileInfo GetFileInfo(string subpath)
{
var baseResponse = base.GetFileInfo(subpath);

if (!baseResponse.Exists)
{
return baseResponse;
}

return new EmbeddedResourceGzipFileInfo(baseResponse);
}
}
}
#endif
70 changes: 70 additions & 0 deletions src/Whipstaff.Mermaid/HttpServer/EmbeddedResourceGzipFileInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

#if TBC
using System;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using Microsoft.Extensions.FileProviders;

namespace Whipstaff.Mermaid.HttpServer
{
/// <summary>
/// Represents a file in the embedded resources that is compressed with Gzip.
/// </summary>
public sealed class EmbeddedResourceGzipFileInfo : IFileInfo
{
private readonly IFileInfo _baseResponse;
private long? _length;

/// <summary>
/// Initializes a new instance of the <see cref="EmbeddedResourceGzipFileInfo"/> class.
/// </summary>
/// <param name="baseResponse">The compressed response to handle.</param>
public EmbeddedResourceGzipFileInfo(IFileInfo baseResponse)
{
_baseResponse = baseResponse;
}

/// <inheritdoc />
public bool Exists => _baseResponse.Exists;

/// <inheritdoc />
public bool IsDirectory => _baseResponse.IsDirectory;

/// <inheritdoc />
public DateTimeOffset LastModified => _baseResponse.LastModified;

/// <inheritdoc />
public long Length
{
get
{
if (!_length.HasValue)
{
using var stream = CreateReadStream();
_length = stream.Length;
}

return _length.Value;
}
}

/// <inheritdoc />
public string Name => _baseResponse.Name;

/// <inheritdoc />
public string? PhysicalPath => _baseResponse.PhysicalPath;

/// <inheritdoc />
public Stream CreateReadStream()
{
var stream = _baseResponse.CreateReadStream();
var gZipStream = new GZipStream(stream, CompressionMode.Decompress);
return gZipStream;
}
}
}
#endif
24 changes: 24 additions & 0 deletions src/Whipstaff.Mermaid/HttpServer/MermaidHttpServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;

namespace Whipstaff.Mermaid.HttpServer
{
/// <summary>
/// In memory HTTP server for Mermaid.
/// </summary>
public class MermaidHttpServer : TestServer
{
/// <summary>
/// Initializes a new instance of the <see cref="MermaidHttpServer"/> class.
/// </summary>
/// <param name="builder">Web host builder used to create a server instance.</param>
public MermaidHttpServer(IWebHostBuilder builder)
: base(builder)
{
}
}
}
177 changes: 177 additions & 0 deletions src/Whipstaff.Mermaid/HttpServer/MermaidHttpServerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;

namespace Whipstaff.Mermaid.HttpServer
{
/// <summary>
/// Factory for the Mermaid in memory HTTP server.
/// </summary>
public static class MermaidHttpServerFactory
{
/// <summary>
/// Gets the In Memory Test Server.
/// </summary>
/// <param name="loggerFactory">Logging Factory.</param>
/// <returns>In memory HTTP server instance.</returns>
public static MermaidHttpServer GetTestServer(ILoggerFactory loggerFactory)
{
var builder = GetWebHostBuilder(loggerFactory);
var testServer = new MermaidHttpServer(builder);
return testServer;
}

private static IWebHostBuilder GetWebHostBuilder(ILoggerFactory loggerFactory)
{
var embeddedProvider = new EmbeddedFileProvider(
typeof(MermaidHttpServerFactory).Assembly,
typeof(MermaidHttpServerFactory).Namespace + ".wwwroot");

var builder = new WebHostBuilder()
.ConfigureLogging(loggingBuilder => ConfigureLogging(
loggingBuilder,
loggerFactory))
.ConfigureServices((_, serviceCollection) => ConfigureServices(
serviceCollection,
embeddedProvider))
.Configure((_, applicationBuilder) => ConfigureApp(
applicationBuilder,
embeddedProvider));

return builder;
}

private static void ConfigureApp(IApplicationBuilder applicationBuilder, EmbeddedFileProvider embeddedFileProvider)
{
_ = applicationBuilder.Use(async (context, next) =>
{
var url = context.Request.Path.Value;

if (url != null
&& url.Contains("/lib/mermaid", StringComparison.OrdinalIgnoreCase)
&& !url.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
{
// rewrite and continue processing
context.Request.Path += ".gz";
}

await next();
});

_ = applicationBuilder.UseStaticFiles(new StaticFileOptions
{
FileProvider = embeddedFileProvider,
OnPrepareResponseAsync = OnPrepareResponseAsync,
HttpsCompression = HttpsCompressionMode.DoNotCompress
});

_ = applicationBuilder.MapWhen(IsMermaidPost, AppConfiguration);
}

private static Task OnPrepareResponseAsync(StaticFileResponseContext arg)
{
if (!arg.File.Name.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
{
return Task.CompletedTask;
}

var filename = arg.File.Name[..^3];

var headers = arg.Context.Response.Headers;
headers["Content-Encoding"] = "gzip";
if (new FileExtensionContentTypeProvider().TryGetContentType(filename, out var contentType))
{
headers["Content-Type"] = contentType;
}

return Task.CompletedTask;
}

private static void AppConfiguration(IApplicationBuilder applicationBuilder)
{
applicationBuilder.Run(Handler);
}

private static async Task Handler(HttpContext context)
{
var request = context.Request;
var response = context.Response;

var diagramFormStringValues = request.Form["diagram"];
if (diagramFormStringValues.Count < 1)
{
await WriteNoDiagramResponse(response).ConfigureAwait(false);
return;
}

var diagram = diagramFormStringValues[0];
if (string.IsNullOrWhiteSpace(diagram))
{
await WriteNoDiagramResponse(response).ConfigureAwait(false);
return;
}

response.StatusCode = 200;
response.ContentType = "text/html";

var sb = new System.Text.StringBuilder();
_ = sb.AppendLine(@"<!DOCTYPE html>");
_ = sb.AppendLine(@"<html lang=""en"" xmlns=""http://www.w3.org/1999/xhtml"">");
_ = sb.AppendLine(@"<head>");
_ = sb.AppendLine(@" <meta charset=""utf-8"" />");
_ = sb.AppendLine(@" <title>MermaidJS factory</title>");
_ = sb.AppendLine(@"</head>");
_ = sb.AppendLine(@"<body>");
_ = sb.AppendLine(@" <pre class=""mermaid"" name=""mermaid-element"" id=""mermaid-element"">");
_ = sb.AppendLine(HtmlEncoder.Default.Encode(diagram));
_ = sb.AppendLine(@" </pre>");
_ = sb.AppendLine(@" <script type=""module"">");
_ = sb.AppendLine(@" import mermaid from '/lib/mermaid/mermaid.esm.min.mjs';");
_ = sb.AppendLine(@" mermaid.initialize({ startOnLoad: false });");
_ = sb.AppendLine(@" await mermaid.run();");
_ = sb.AppendLine(@" </script>");
_ = sb.AppendLine(@"</body>");
_ = sb.AppendLine(@"</html>");

await response.WriteAsync(sb.ToString())
.ConfigureAwait(false);
}

private static async Task WriteNoDiagramResponse(HttpResponse response)
{
response.StatusCode = 400;
response.ContentType = "text/plain";
await response.WriteAsync("No diagram passed in request").ConfigureAwait(false);
}

private static bool IsMermaidPost(HttpContext arg)
{
var request = arg.Request;

return request.Method.Equals("POST", StringComparison.Ordinal) &&
request.Path.Equals("/index.html", StringComparison.OrdinalIgnoreCase);
}

private static void ConfigureServices(IServiceCollection serviceCollection, EmbeddedFileProvider embeddedFileProvider)
{
_ = serviceCollection.AddSingleton<IFileProvider>(embeddedFileProvider);
}

private static void ConfigureLogging(ILoggingBuilder loggingBuilder, ILoggerFactory loggerFactory)
{
_ = loggingBuilder.Services.AddSingleton(loggerFactory);
}
}
}
17 changes: 17 additions & 0 deletions src/Whipstaff.Mermaid/Playwright/GetDiagramResponseModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Whipstaff.Mermaid.Playwright
{
/// <summary>
/// Response Model for <see cref="PlaywrightRenderer.GetDiagram"/>.
/// </summary>
/// <param name="Svg">The diagram in SVG format.</param>
/// <param name="Png">PNG image of the diagram as a byte array.</param>
#pragma warning disable CA1819 // Properties should not return arrays
public sealed record GetDiagramResponseModel(string Svg, byte[] Png)
#pragma warning restore CA1819 // Properties should not return arrays
{
}
}
Loading

0 comments on commit b7ccf70

Please sign in to comment.