+
+
+
+
+
![]()
+
Asset summary
+
+
diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/src/webpack.config.js b/CSF.Screenplay.JsonToHtmlReport.Template/src/webpack.config.js
index 05da7cb7..d77fdd87 100644
--- a/CSF.Screenplay.JsonToHtmlReport.Template/src/webpack.config.js
+++ b/CSF.Screenplay.JsonToHtmlReport.Template/src/webpack.config.js
@@ -25,10 +25,7 @@ module.exports = {
]
},
optimization: {
- minimizer: [
- `...`,
- new CssMinimizerPlugin()
- ]
+ minimizer: [new CssMinimizerPlugin(), '...']
},
plugins: [
new HtmlWebpackPlugin({
diff --git a/CSF.Screenplay.JsonToHtmlReport/AssetEmbedder.cs b/CSF.Screenplay.JsonToHtmlReport/AssetEmbedder.cs
new file mode 100644
index 00000000..6f4059e4
--- /dev/null
+++ b/CSF.Screenplay.JsonToHtmlReport/AssetEmbedder.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using CSF.Screenplay.ReportModel;
+using System.Linq;
+using System.IO;
+
+namespace CSF.Screenplay.JsonToHtmlReport
+{
+ ///
+ /// Default implementation of .
+ ///
+ public class AssetEmbedder : IEmbedsReportAssets
+ {
+ ///
+ public async Task EmbedReportAssetsAsync(ScreenplayReport report, ReportConverterOptions options)
+ {
+ if (report is null)
+ throw new ArgumentNullException(nameof(report));
+ if (options is null)
+ throw new ArgumentNullException(nameof(options));
+
+ var applicableExtensions = options.GetEmbeddedFileExtensions();
+ var ignoreExtension = options.ShouldEmbedAllFileTypes();
+ var maxSizeKb = options.EmbeddedFileSizeThresholdKb;
+
+ var allAssets = GetAssets(report).ToList();
+ foreach(var asset in allAssets)
+ await EmbedAssetIfApplicable(asset, maxSizeKb, applicableExtensions, ignoreExtension);
+ }
+
+ static IEnumerable
GetAssets(ScreenplayReport report)
+ {
+ return from performance in report.Performances
+ from performable in GetAllPerformables(performance)
+ from asset in performable.Assets
+ select asset;
+ }
+
+ static List GetAllPerformables(PerformanceReport performance)
+ {
+ var open = new List();
+ var closed = new List();
+ open.AddRange(performance.Reportables.OfType());
+
+ while(open.Count > 0)
+ {
+ var current = open[0];
+ open.RemoveAt(0);
+ closed.Add(current);
+ open.AddRange(current.Reportables.OfType());
+ }
+
+ return closed;
+ }
+
+ static Task EmbedAssetIfApplicable(PerformableAsset asset,
+ int maxSizeKb,
+ IReadOnlyCollection applicableExtensions,
+ bool ignoreExtension)
+ {
+ if(!ShouldEmbed(asset, maxSizeKb, applicableExtensions, ignoreExtension)) return Task.CompletedTask;
+ return EmbedAssetIfApplicableAsync(asset);
+ }
+
+ static bool ShouldEmbed(PerformableAsset asset,
+ int maxSizeKb,
+ IReadOnlyCollection applicableExtensions,
+ bool ignoreExtension)
+ {
+ if(string.IsNullOrWhiteSpace(asset.FilePath)) return false;
+
+ var info = new FileInfo(asset.FilePath);
+ var fileSizeBytes = info.Length;
+ var extension = info.Extension;
+
+ return fileSizeBytes <= (maxSizeKb * 1000)
+ && (ignoreExtension || applicableExtensions.Contains(extension));
+ }
+
+#if !NETSTANDARD2_0 && !NET462
+ static async Task EmbedAssetIfApplicableAsync(PerformableAsset asset)
+ {
+ var bytes = await File.ReadAllBytesAsync(asset.FilePath).ConfigureAwait(false);
+#else
+ static Task EmbedAssetIfApplicableAsync(PerformableAsset asset)
+ {
+ var bytes = File.ReadAllBytes(asset.FilePath);
+#endif
+
+ asset.FileData = Convert.ToBase64String(bytes);
+ asset.FilePath = null;
+#if NETSTANDARD2_0 || NET462
+ return Task.CompletedTask;
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj b/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj
index b08e548a..d7c73c03 100644
--- a/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj
+++ b/CSF.Screenplay.JsonToHtmlReport/CSF.Screenplay.JsonToHtmlReport.csproj
@@ -4,7 +4,7 @@
- netcoreapp3.1;net462;netstandard2.0;net6.0;net8.0
+ net462;netstandard2.0;net6.0;net8.0
Exe
Library
NU1903,NU1902
@@ -18,7 +18,7 @@
-
+
@@ -26,6 +26,7 @@
false
+
diff --git a/CSF.Screenplay.JsonToHtmlReport/IEmbedsReportAssets.cs b/CSF.Screenplay.JsonToHtmlReport/IEmbedsReportAssets.cs
new file mode 100644
index 00000000..9d0c4397
--- /dev/null
+++ b/CSF.Screenplay.JsonToHtmlReport/IEmbedsReportAssets.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+using CSF.Screenplay.ReportModel;
+
+namespace CSF.Screenplay.JsonToHtmlReport
+{
+ ///
+ /// A service which reworks a , embedding assets within the report.
+ ///
+ public interface IEmbedsReportAssets
+ {
+ ///
+ /// Processes the specified , converting external file assets to embedded assets,
+ /// where they meet the criteria specified by the specified .
+ ///
+ /// A Screenplay report
+ /// A set of options for converting a report to HTML
+ /// A task which completes when the process is done.
+ Task EmbedReportAssetsAsync(ScreenplayReport report, ReportConverterOptions options);
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay.JsonToHtmlReport/ReportConverter.cs b/CSF.Screenplay.JsonToHtmlReport/ReportConverter.cs
index 016cfbd4..902267b1 100644
--- a/CSF.Screenplay.JsonToHtmlReport/ReportConverter.cs
+++ b/CSF.Screenplay.JsonToHtmlReport/ReportConverter.cs
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Threading.Tasks;
+using CSF.Screenplay.ReportModel;
+using CSF.Screenplay.Reporting;
namespace CSF.Screenplay.JsonToHtmlReport
{
@@ -10,23 +12,32 @@ namespace CSF.Screenplay.JsonToHtmlReport
public class ReportConverter : IConvertsReportJsonToHtml
{
readonly IGetsHtmlTemplate templateReader;
+ readonly IEmbedsReportAssets assetsEmbedder;
+ readonly IDeserializesReport reportReader;
+ readonly ISerializesReport reportWriter;
///
public async Task ConvertAsync(ReportConverterOptions options)
{
var report = await ReadReport(options.ReportPath).ConfigureAwait(false);
+ await assetsEmbedder.EmbedReportAssetsAsync(report, options).ConfigureAwait(false);
+ var reportJson = await GetModifiedReportJsonAsync(report).ConfigureAwait(false);
var template = await templateReader.ReadTemplate().ConfigureAwait(false);
- var assembledTemplate = template.Replace("", report);
+ var assembledTemplate = template.Replace("", reportJson);
await WriteReport(options.OutputPath, assembledTemplate).ConfigureAwait(false);
}
- static async Task ReadReport(string path)
+ async Task ReadReport(string path)
{
using (var stream = File.OpenRead(path))
- using (var reader = new StreamReader(stream))
- {
- return await reader.ReadToEndAsync().ConfigureAwait(false);
- }
+ return await reportReader.DeserializeAsync(stream).ConfigureAwait(false);
+ }
+
+ async Task GetModifiedReportJsonAsync(ScreenplayReport report)
+ {
+ var stream = await reportWriter.SerializeAsync(report).ConfigureAwait(false);
+ using(var textReader = new StreamReader(stream))
+ return await textReader.ReadToEndAsync().ConfigureAwait(false);
}
static async Task WriteReport(string path, string report)
@@ -42,9 +53,18 @@ static async Task WriteReport(string path, string report)
/// Initializes a new instance of the class.
///
/// The template reader used to get the HTML template.
- public ReportConverter(IGetsHtmlTemplate templateReader)
+ /// A service which embeds asset data into the JSON report.
+ /// A report deserializer
+ /// A report serializer
+ public ReportConverter(IGetsHtmlTemplate templateReader,
+ IEmbedsReportAssets assetsEmbedder,
+ IDeserializesReport reportReader,
+ ISerializesReport reportWriter)
{
this.templateReader = templateReader ?? throw new ArgumentNullException(nameof(templateReader));
+ this.assetsEmbedder = assetsEmbedder ?? throw new ArgumentNullException(nameof(assetsEmbedder));
+ this.reportReader = reportReader ?? throw new ArgumentNullException(nameof(reportReader));
+ this.reportWriter = reportWriter ?? throw new ArgumentNullException(nameof(reportWriter));
}
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay.JsonToHtmlReport/ReportConverterOptions.cs b/CSF.Screenplay.JsonToHtmlReport/ReportConverterOptions.cs
index 9e3caaca..c6e057d2 100644
--- a/CSF.Screenplay.JsonToHtmlReport/ReportConverterOptions.cs
+++ b/CSF.Screenplay.JsonToHtmlReport/ReportConverterOptions.cs
@@ -1,3 +1,7 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
namespace CSF.Screenplay.JsonToHtmlReport
{
///
@@ -14,5 +18,73 @@ public class ReportConverterOptions
/// Gets or sets the file system path where the HTML report will be saved.
///
public string OutputPath { get; set; } = "ScreenplayReport.html";
+
+ ///
+ /// Gets or sets a threshold (in Kilobytes) for files which should be embedded into the report.
+ ///
+ ///
+ ///
+ /// By default, the report converter will attempt to embed file data into the HTML report file.
+ /// This is desirable because it means that the report file is likely to be portable as a single file, even when
+ /// . Any asset files of supported file extensions are
+ /// embedded into the HTML file if their file size (in kilobytes) is less than or equal to this value.
+ ///
+ ///
+ /// The default value for this property is 500 (500 kilobytes, half a megabyte). Setting this value to zero
+ /// or a negative number will disable embedding of files into the report.
+ /// Setting this value to an arbitrarily high number (such as 1000000, meaning a gigabyte) will cause all files to be
+ /// embedded.
+ ///
+ ///
+ /// The supported file extensions are listed in the option property .
+ ///
+ ///
+ public int EmbeddedFileSizeThresholdKb { get; set; } = 500;
+
+ ///
+ /// Gets or sets a comma-separated list of file extensions which are supported for embedding into report.
+ ///
+ ///
+ ///
+ /// By default, the report converter will attempt to embed file data into the HTML report file.
+ /// This is desirable because it means that the report file is likely to be portable as a single file, even when
+ /// . Any asset files with a size less than or equal to the threshold
+ /// are embedded into the HTML file if their file extension is amongst those listed in this property.
+ ///
+ ///
+ /// The default value for this property is jpg,jpeg,png,gif,webp,svg,mp4,mov,avi,wmv,mkv,webm.
+ /// These are common image and video file types, seen on the web.
+ /// Note that the wildcard *, if included anywhere this property value, denotes that files of all (any) extension
+ /// should be embedded into the report.
+ /// Setting this value to an empty string will disable embedding of files into the report.
+ ///
+ ///
+ /// The file-size threshold for files which may be embedded into the report is controlled by the option property
+ /// .
+ ///
+ ///
+ public string EmbeddedFileExtensions { get; set; } = "jpg,jpeg,png,gif,webp,svg,mp4,mov,avi,wmv,mkv,webm";
+
+ ///
+ /// Gets a collection of file extensions (including the leading period) which should be embedded into HTML reports.
+ ///
+ /// A collection of the extensions to embed
+ public IReadOnlyCollection GetEmbeddedFileExtensions()
+ {
+ if(string.IsNullOrWhiteSpace(EmbeddedFileExtensions)) return Array.Empty();
+ return EmbeddedFileExtensions.Split(',').Select(x => string.Concat(".", x.Trim())).ToArray();
+ }
+
+ ///
+ /// Gets a value indicating whether all file types (regardless of extension) should be embedded.
+ ///
+ ///
+ ///
+ /// This method returns if contains the character *.
+ /// Note that a file must still have a size equal to or less than to be embedded.
+ ///
+ ///
+ /// if all file types are to be embedded; if not.
+ public bool ShouldEmbedAllFileTypes() => EmbeddedFileExtensions.Contains('*');
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay.JsonToHtmlReport/ServiceRegistrations.cs b/CSF.Screenplay.JsonToHtmlReport/ServiceRegistrations.cs
index a6d24289..ae2bee18 100644
--- a/CSF.Screenplay.JsonToHtmlReport/ServiceRegistrations.cs
+++ b/CSF.Screenplay.JsonToHtmlReport/ServiceRegistrations.cs
@@ -1,3 +1,4 @@
+using CSF.Screenplay.Reporting;
using Microsoft.Extensions.DependencyInjection;
namespace CSF.Screenplay.JsonToHtmlReport
@@ -19,8 +20,12 @@ public static class ServiceRegistrations
/// The service collection to which the services will be added.
public static void RegisterServices(IServiceCollection services)
{
- services.AddTransient();
- services.AddTransient();
+ services
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient();
}
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay/ReportModel/PerformableAsset.cs b/CSF.Screenplay/ReportModel/PerformableAsset.cs
index 75d8b2b9..b1a321cd 100644
--- a/CSF.Screenplay/ReportModel/PerformableAsset.cs
+++ b/CSF.Screenplay/ReportModel/PerformableAsset.cs
@@ -1,3 +1,5 @@
+using System.IO;
+
namespace CSF.Screenplay.ReportModel
{
///
@@ -5,19 +7,45 @@ namespace CSF.Screenplay.ReportModel
///
///
///
- /// Assets are files which are saved to disk, containing arbitrary information, recorded by a performable.
+ /// Assets are typically files which are saved to disk, containing arbitrary information, recorded by a performable.
/// This might be a screenshot, some generated content or diagnostic information. Its real content is arbitrary and
/// down to the implementation.
- /// An asset is described here by a file path and an optional human-readable summary.
+ ///
+ ///
+ /// Every asset has one or two properties in common, the and optionally .
+ /// The data for the asset file is then described by one of two ways:
+ ///
+ ///
+ /// - The content of the asset is located on disk at a path indicated by
+ /// - The content of the asset is embedded within this model as base64-encoded text, within
+ ///
+ ///
+ /// Typically when reports are first created, for expedience, assets are recorded to disk as files.
+ /// However, for portability, it is often useful to embed the asset data within the report so that the entire report may be transported as a single file.
///
///
public class PerformableAsset
{
+ ///
+ /// Gets the content type (aka MIME type) of the asset.
+ ///
+ public string ContentType { get; set; }
+
///
/// Gets or sets a full/absolute path to the asset file.
///
public string FilePath { get; set; }
+ ///
+ /// Gets or sets base64-encoded data which contains the data for the asset.
+ ///
+ public string FileData { get; set; }
+
+ ///
+ /// Gets or sets the name of the asset file, including its extension.
+ ///
+ public string FileName { get; set; }
+
///
/// Gets or sets an optional human-readable summary of what this asset represents. This should be one sentence at most, suitable
/// for display in a UI tool-tip.
diff --git a/CSF.Screenplay/Reporting/AssetPathProvider.cs b/CSF.Screenplay/Reporting/AssetPathProvider.cs
index fb793b5f..9edcf50b 100644
--- a/CSF.Screenplay/Reporting/AssetPathProvider.cs
+++ b/CSF.Screenplay/Reporting/AssetPathProvider.cs
@@ -1,5 +1,5 @@
using System;
-using System.Globalization;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using CSF.Screenplay.Performances;
@@ -36,6 +36,7 @@ namespace CSF.Screenplay.Reporting
///
public class AssetPathProvider : IGetsAssetFilePath
{
+ static readonly List invalidFilenameChars = Path.GetInvalidFileNameChars().Select(x => x.ToString()).ToList();
readonly IGetsReportPath reportPathProvider;
readonly IPerformance performance;
int assetNumber = 1;
@@ -55,14 +56,12 @@ public string GetAssetFilePath(string baseName)
var performanceId = performance.NamingHierarchy.LastOrDefault()?.Identifier ?? performance.PerformanceIdentity.ToString();
var sanitisedPerformanceId = RemoveInvalidFilenameChars(performanceId);
var sanitisedBaseFilename = RemoveInvalidFilenameChars(baseFilename);
- var filename = $"{GetTimestamp()}_{sanitisedPerformanceId}_{assetNumber++:000}_{sanitisedBaseFilename}";
- return Path.Combine(Path.GetDirectoryName(reportPath), filename);
+ var filename = $"{sanitisedPerformanceId}_{assetNumber++:000}_{sanitisedBaseFilename}";
+ return Path.Combine(reportPath, filename);
}
static string RemoveInvalidFilenameChars(string input)
- => Path.GetInvalidFileNameChars().Select(c => c.ToString()).Aggregate(input, (current, c) => current.Replace(c, string.Empty));
-
- static string GetTimestamp() => DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture);
+ => invalidFilenameChars.Aggregate(input, (current, c) => current.Replace(c, string.Empty));
///
/// Initializes a new instance of the class.
diff --git a/CSF.Screenplay/Reporting/ContentTypeProvider.cs b/CSF.Screenplay/Reporting/ContentTypeProvider.cs
new file mode 100644
index 00000000..72b6860c
--- /dev/null
+++ b/CSF.Screenplay/Reporting/ContentTypeProvider.cs
@@ -0,0 +1,11 @@
+namespace CSF.Screenplay.Reporting
+{
+ ///
+ /// Implementation of which makes use of the MimeTypes NuGet package.
+ ///
+ public class ContentTypeProvider : IGetsContentType
+ {
+ ///
+ public string GetContentType(string fileName) => MimeTypes.GetMimeType(fileName);
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/IGetsContentType.cs b/CSF.Screenplay/Reporting/IGetsContentType.cs
new file mode 100644
index 00000000..60c96af2
--- /dev/null
+++ b/CSF.Screenplay/Reporting/IGetsContentType.cs
@@ -0,0 +1,15 @@
+namespace CSF.Screenplay.Reporting
+{
+ ///
+ /// An object which can get the MIME type for a given filename.
+ ///
+ public interface IGetsContentType
+ {
+ ///
+ /// Gets the content type (aka MIME type) for a specified filename.
+ ///
+ /// The filename
+ /// The content type
+ string GetContentType(string fileName);
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/ISerializesReport.cs b/CSF.Screenplay/Reporting/ISerializesReport.cs
new file mode 100644
index 00000000..42602827
--- /dev/null
+++ b/CSF.Screenplay/Reporting/ISerializesReport.cs
@@ -0,0 +1,19 @@
+using System.IO;
+using System.Threading.Tasks;
+using CSF.Screenplay.ReportModel;
+
+namespace CSF.Screenplay.Reporting
+{
+ ///
+ /// An object which serializes a Screenplay report into a stream.
+ ///
+ public interface ISerializesReport
+ {
+ ///
+ /// Serializes a Screenplay report into a stream asynchronously.
+ ///
+ /// A Screenplay report.
+ /// A task that represents the asynchronous operation. The task result contains the serialized report stream.
+ Task SerializeAsync(ScreenplayReport report);
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/JsonScreenplayReportReader.cs b/CSF.Screenplay/Reporting/JsonScreenplayReportReader.cs
deleted file mode 100644
index 27784d23..00000000
--- a/CSF.Screenplay/Reporting/JsonScreenplayReportReader.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.IO;
-using System.Text.Json;
-using System.Threading.Tasks;
-using CSF.Screenplay.ReportModel;
-
-namespace CSF.Screenplay.Reporting
-{
-
- ///
- /// Implementation of that deserializes a Screenplay report from a JSON stream.
- ///
- public class JsonScreenplayReportReader : IDeserializesReport
- {
- ///
- public async Task DeserializeAsync(Stream stream)
- {
- if (stream == null)
- throw new ArgumentNullException(nameof(stream));
-
- return await JsonSerializer.DeserializeAsync(stream);
- }
- }
-}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/MimeTypes.cs b/CSF.Screenplay/Reporting/MimeTypes.cs
new file mode 100644
index 00000000..4aaafea1
--- /dev/null
+++ b/CSF.Screenplay/Reporting/MimeTypes.cs
@@ -0,0 +1,1291 @@
+//
+
+#pragma warning disable
+
+namespace CSF.Screenplay.Reporting
+{
+ using global::System;
+ using global::System.Linq;
+ using global::System.Collections.Generic;
+ using global::System.Diagnostics;
+ using global::System.Runtime.CompilerServices;
+
+ ///
+ /// Provides utilities for mapping file names and extensions to MIME-types.
+ ///
+ ///
+ ///
+ /// Note that this file was copied directly from the
+ /// source code for the MimeTypes package. I had tried to include it via NuGet in the past but the mechanism seems to be incompatible
+ /// with SonarScanner. As such I have abandoned including it via NuGet and I'm switching to including the file manually.
+ /// Obviously, to upgrade this package, I will need to re-download and include this file again.
+ ///
+ ///
+ [CompilerGenerated]
+ [DebuggerNonUserCode]
+ public static class MimeTypes
+ {
+ private const string DefaultFallbackMimeType = "application/octet-stream";
+ private static string s_fallbackMimeType;
+
+ ///
+ /// The fallback MIME-type. Defaults to application/octet-stream.
+ ///
+ public static string FallbackMimeType
+ {
+ get => s_fallbackMimeType;
+ set => s_fallbackMimeType = value ?? DefaultFallbackMimeType;
+ }
+
+ private static readonly Dictionary s_typeMap;
+
+ static MimeTypes()
+ {
+ s_fallbackMimeType = DefaultFallbackMimeType;
+
+ s_typeMap = new Dictionary(1180, StringComparer.OrdinalIgnoreCase)
+ {
+ { "123", "application/vnd.lotus-1-2-3" },
+ { "1km", "application/vnd.1000minds.decision-model+xml" },
+ { "3dml", "text/vnd.in3d.3dml" },
+ { "3ds", "image/x-3ds" },
+ { "3g2", "video/3gpp2" },
+ { "3gp", "video/3gpp" },
+ { "3gpp", "audio/3gpp" },
+ { "3mf", "model/3mf" },
+ { "7z", "application/x-7z-compressed" },
+ { "aab", "application/x-authorware-bin" },
+ { "aac", "audio/aac" },
+ { "aam", "application/x-authorware-map" },
+ { "aas", "application/x-authorware-seg" },
+ { "abw", "application/x-abiword" },
+ { "ac", "application/pkix-attr-cert" },
+ { "acc", "application/vnd.americandynamics.acc" },
+ { "ace", "application/x-ace-compressed" },
+ { "acu", "application/vnd.acucobol" },
+ { "acutc", "application/vnd.acucorp" },
+ { "adp", "audio/adpcm" },
+ { "adts", "audio/aac" },
+ { "aep", "application/vnd.audiograph" },
+ { "afm", "application/x-font-type1" },
+ { "afp", "application/vnd.ibm.modcap" },
+ { "age", "application/vnd.age" },
+ { "ahead", "application/vnd.ahead.space" },
+ { "ai", "application/postscript" },
+ { "aif", "audio/x-aiff" },
+ { "aifc", "audio/x-aiff" },
+ { "aiff", "audio/x-aiff" },
+ { "air", "application/vnd.adobe.air-application-installer-package+zip" },
+ { "ait", "application/vnd.dvb.ait" },
+ { "ami", "application/vnd.amiga.ami" },
+ { "aml", "application/automationml-aml+xml" },
+ { "amlx", "application/automationml-amlx+zip" },
+ { "amr", "audio/amr" },
+ { "apk", "application/vnd.android.package-archive" },
+ { "apng", "image/apng" },
+ { "appcache", "text/cache-manifest" },
+ { "appx", "application/appx" },
+ { "apr", "application/vnd.lotus-approach" },
+ { "arc", "application/x-freearc" },
+ { "arj", "application/x-arj" },
+ { "asc", "application/pgp-keys" },
+ { "asf", "video/x-ms-asf" },
+ { "asm", "text/x-asm" },
+ { "aso", "application/vnd.accpac.simply.aso" },
+ { "asx", "video/x-ms-asf" },
+ { "atc", "application/vnd.acucorp" },
+ { "atom", "application/atom+xml" },
+ { "atomcat", "application/atomcat+xml" },
+ { "atomsvc", "application/atomsvc+xml" },
+ { "atx", "application/vnd.antix.game-component" },
+ { "au", "audio/basic" },
+ { "avci", "image/avci" },
+ { "avcs", "image/avcs" },
+ { "avi", "video/x-msvideo" },
+ { "avif", "image/avif" },
+ { "aw", "application/applixware" },
+ { "azf", "application/vnd.airzip.filesecure.azf" },
+ { "azs", "application/vnd.airzip.filesecure.azs" },
+ { "azv", "image/vnd.airzip.accelerator.azv" },
+ { "azw", "application/vnd.amazon.ebook" },
+ { "b16", "image/vnd.pco.b16" },
+ { "bat", "application/x-msdownload" },
+ { "bcpio", "application/x-bcpio" },
+ { "bdf", "application/x-font-bdf" },
+ { "bdm", "application/vnd.syncml.dm+wbxml" },
+ { "bdoc", "application/bdoc" },
+ { "bed", "application/vnd.realvnc.bed" },
+ { "bh2", "application/vnd.fujitsu.oasysprs" },
+ { "bin", "application/octet-stream" },
+ { "blb", "application/x-blorb" },
+ { "blorb", "application/x-blorb" },
+ { "bmi", "application/vnd.bmi" },
+ { "bmml", "application/vnd.balsamiq.bmml+xml" },
+ { "bmp", "image/bmp" },
+ { "book", "application/vnd.framemaker" },
+ { "box", "application/vnd.previewsystems.box" },
+ { "boz", "application/x-bzip2" },
+ { "bpk", "application/octet-stream" },
+ { "bsp", "model/vnd.valve.source.compiled-map" },
+ { "btf", "image/prs.btif" },
+ { "btif", "image/prs.btif" },
+ { "buffer", "application/octet-stream" },
+ { "bz", "application/x-bzip" },
+ { "bz2", "application/x-bzip2" },
+ { "c", "text/x-c" },
+ { "c11amc", "application/vnd.cluetrust.cartomobile-config" },
+ { "c11amz", "application/vnd.cluetrust.cartomobile-config-pkg" },
+ { "c4d", "application/vnd.clonk.c4group" },
+ { "c4f", "application/vnd.clonk.c4group" },
+ { "c4g", "application/vnd.clonk.c4group" },
+ { "c4p", "application/vnd.clonk.c4group" },
+ { "c4u", "application/vnd.clonk.c4group" },
+ { "cab", "application/vnd.ms-cab-compressed" },
+ { "caf", "audio/x-caf" },
+ { "cap", "application/vnd.tcpdump.pcap" },
+ { "car", "application/vnd.curl.car" },
+ { "cat", "application/vnd.ms-pki.seccat" },
+ { "cb7", "application/x-cbr" },
+ { "cba", "application/x-cbr" },
+ { "cbr", "application/x-cbr" },
+ { "cbt", "application/x-cbr" },
+ { "cbz", "application/x-cbr" },
+ { "cc", "text/x-c" },
+ { "cco", "application/x-cocoa" },
+ { "cct", "application/x-director" },
+ { "ccxml", "application/ccxml+xml" },
+ { "cdbcmsg", "application/vnd.contact.cmsg" },
+ { "cdf", "application/x-netcdf" },
+ { "cdfx", "application/cdfx+xml" },
+ { "cdkey", "application/vnd.mediastation.cdkey" },
+ { "cdmia", "application/cdmi-capability" },
+ { "cdmic", "application/cdmi-container" },
+ { "cdmid", "application/cdmi-domain" },
+ { "cdmio", "application/cdmi-object" },
+ { "cdmiq", "application/cdmi-queue" },
+ { "cdx", "chemical/x-cdx" },
+ { "cdxml", "application/vnd.chemdraw+xml" },
+ { "cdy", "application/vnd.cinderella" },
+ { "cer", "application/pkix-cert" },
+ { "cfs", "application/x-cfs-compressed" },
+ { "cgm", "image/cgm" },
+ { "chat", "application/x-chat" },
+ { "chm", "application/vnd.ms-htmlhelp" },
+ { "chrt", "application/vnd.kde.kchart" },
+ { "cif", "chemical/x-cif" },
+ { "cii", "application/vnd.anser-web-certificate-issue-initiation" },
+ { "cil", "application/vnd.ms-artgalry" },
+ { "cjs", "application/node" },
+ { "cla", "application/vnd.claymore" },
+ { "class", "application/java-vm" },
+ { "cld", "model/vnd.cld" },
+ { "clkk", "application/vnd.crick.clicker.keyboard" },
+ { "clkp", "application/vnd.crick.clicker.palette" },
+ { "clkt", "application/vnd.crick.clicker.template" },
+ { "clkw", "application/vnd.crick.clicker.wordbank" },
+ { "clkx", "application/vnd.crick.clicker" },
+ { "clp", "application/x-msclip" },
+ { "cmc", "application/vnd.cosmocaller" },
+ { "cmdf", "chemical/x-cmdf" },
+ { "cml", "chemical/x-cml" },
+ { "cmp", "application/vnd.yellowriver-custom-menu" },
+ { "cmx", "image/x-cmx" },
+ { "cod", "application/vnd.rim.cod" },
+ { "coffee", "text/coffeescript" },
+ { "com", "application/x-msdownload" },
+ { "conf", "text/plain" },
+ { "cpio", "application/x-cpio" },
+ { "cpl", "application/cpl+xml" },
+ { "cpp", "text/x-c" },
+ { "cpt", "application/mac-compactpro" },
+ { "crd", "application/x-mscardfile" },
+ { "crl", "application/pkix-crl" },
+ { "crt", "application/x-x509-ca-cert" },
+ { "crx", "application/x-chrome-extension" },
+ { "csh", "application/x-csh" },
+ { "csl", "application/vnd.citationstyles.style+xml" },
+ { "csml", "chemical/x-csml" },
+ { "csp", "application/vnd.commonspace" },
+ { "css", "text/css" },
+ { "cst", "application/x-director" },
+ { "csv", "text/csv" },
+ { "cu", "application/cu-seeme" },
+ { "curl", "text/vnd.curl" },
+ { "cwl", "application/cwl" },
+ { "cww", "application/prs.cww" },
+ { "cxt", "application/x-director" },
+ { "cxx", "text/x-c" },
+ { "dae", "model/vnd.collada+xml" },
+ { "daf", "application/vnd.mobius.daf" },
+ { "dart", "application/vnd.dart" },
+ { "dataless", "application/vnd.fdsn.seed" },
+ { "davmount", "application/davmount+xml" },
+ { "dbf", "application/vnd.dbf" },
+ { "dbk", "application/docbook+xml" },
+ { "dcr", "application/x-director" },
+ { "dcurl", "text/vnd.curl.dcurl" },
+ { "dd2", "application/vnd.oma.dd2+xml" },
+ { "ddd", "application/vnd.fujixerox.ddd" },
+ { "ddf", "application/vnd.syncml.dmddf+xml" },
+ { "dds", "image/vnd.ms-dds" },
+ { "deb", "application/octet-stream" },
+ { "def", "text/plain" },
+ { "deploy", "application/octet-stream" },
+ { "der", "application/x-x509-ca-cert" },
+ { "dfac", "application/vnd.dreamfactory" },
+ { "dgc", "application/x-dgc-compressed" },
+ { "dib", "image/bmp" },
+ { "dic", "text/x-c" },
+ { "dir", "application/x-director" },
+ { "dis", "application/vnd.mobius.dis" },
+ { "dist", "application/octet-stream" },
+ { "distz", "application/octet-stream" },
+ { "djv", "image/vnd.djvu" },
+ { "djvu", "image/vnd.djvu" },
+ { "dll", "application/octet-stream" },
+ { "dmg", "application/octet-stream" },
+ { "dmp", "application/vnd.tcpdump.pcap" },
+ { "dms", "application/octet-stream" },
+ { "dna", "application/vnd.dna" },
+ { "doc", "application/msword" },
+ { "docm", "application/vnd.ms-word.document.macroenabled.12" },
+ { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
+ { "dot", "application/msword" },
+ { "dotm", "application/vnd.ms-word.template.macroenabled.12" },
+ { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { "dp", "application/vnd.osgi.dp" },
+ { "dpg", "application/vnd.dpgraph" },
+ { "dpx", "image/dpx" },
+ { "dra", "audio/vnd.dra" },
+ { "drle", "image/dicom-rle" },
+ { "dsc", "text/prs.lines.tag" },
+ { "dssc", "application/dssc+der" },
+ { "dtb", "application/x-dtbook+xml" },
+ { "dtd", "application/xml-dtd" },
+ { "dts", "audio/vnd.dts" },
+ { "dtshd", "audio/vnd.dts.hd" },
+ { "dump", "application/octet-stream" },
+ { "dvb", "video/vnd.dvb.file" },
+ { "dvi", "application/x-dvi" },
+ { "dwd", "application/atsc-dwd+xml" },
+ { "dwf", "model/vnd.dwf" },
+ { "dwg", "image/vnd.dwg" },
+ { "dxf", "image/vnd.dxf" },
+ { "dxp", "application/vnd.spotfire.dxp" },
+ { "dxr", "application/x-director" },
+ { "ear", "application/java-archive" },
+ { "ecma", "application/ecmascript" },
+ { "edm", "application/vnd.novadigm.edm" },
+ { "edx", "application/vnd.novadigm.edx" },
+ { "efif", "application/vnd.picsel" },
+ { "ei6", "application/vnd.pg.osasli" },
+ { "elc", "application/octet-stream" },
+ { "emf", "image/emf" },
+ { "eml", "message/rfc822" },
+ { "emma", "application/emma+xml" },
+ { "emz", "application/x-msmetafile" },
+ { "eol", "audio/vnd.digital-winds" },
+ { "eot", "application/vnd.ms-fontobject" },
+ { "eps", "application/postscript" },
+ { "epub", "application/epub+zip" },
+ { "es3", "application/vnd.eszigno3+xml" },
+ { "esa", "application/vnd.osgi.subsystem" },
+ { "esf", "application/vnd.epson.esf" },
+ { "et3", "application/vnd.eszigno3+xml" },
+ { "etx", "text/x-setext" },
+ { "eva", "application/x-eva" },
+ { "evy", "application/x-envoy" },
+ { "exe", "application/x-msdos-program" },
+ { "exi", "application/exi" },
+ { "exp", "application/express" },
+ { "exr", "image/aces" },
+ { "ext", "application/vnd.novadigm.ext" },
+ { "ez", "application/andrew-inset" },
+ { "ez2", "application/vnd.ezpix-album" },
+ { "ez3", "application/vnd.ezpix-package" },
+ { "f", "text/x-fortran" },
+ { "f4v", "video/x-f4v" },
+ { "f77", "text/x-fortran" },
+ { "f90", "text/x-fortran" },
+ { "fbs", "image/vnd.fastbidsheet" },
+ { "fcdt", "application/vnd.adobe.formscentral.fcdt" },
+ { "fcs", "application/vnd.isac.fcs" },
+ { "fdf", "application/fdf" },
+ { "fdt", "application/fdt+xml" },
+ { "fg5", "application/vnd.fujitsu.oasysgp" },
+ { "fgd", "application/x-director" },
+ { "fh", "image/x-freehand" },
+ { "fh4", "image/x-freehand" },
+ { "fh5", "image/x-freehand" },
+ { "fh7", "image/x-freehand" },
+ { "fhc", "image/x-freehand" },
+ { "fig", "application/x-xfig" },
+ { "fits", "image/fits" },
+ { "flac", "audio/x-flac" },
+ { "fli", "video/x-fli" },
+ { "flo", "application/vnd.micrografx.flo" },
+ { "flv", "video/x-flv" },
+ { "flw", "application/vnd.kde.kivio" },
+ { "flx", "text/vnd.fmi.flexstor" },
+ { "fly", "text/vnd.fly" },
+ { "fm", "application/vnd.framemaker" },
+ { "fnc", "application/vnd.frogans.fnc" },
+ { "fo", "application/vnd.software602.filler.form+xml" },
+ { "for", "text/x-fortran" },
+ { "fpx", "image/vnd.fpx" },
+ { "frame", "application/vnd.framemaker" },
+ { "fsc", "application/vnd.fsc.weblaunch" },
+ { "fst", "image/vnd.fst" },
+ { "ftc", "application/vnd.fluxtime.clip" },
+ { "fti", "application/vnd.anser-web-funds-transfer-initiation" },
+ { "fvt", "video/vnd.fvt" },
+ { "fxp", "application/vnd.adobe.fxp" },
+ { "fxpl", "application/vnd.adobe.fxp" },
+ { "fzs", "application/vnd.fuzzysheet" },
+ { "g2w", "application/vnd.geoplan" },
+ { "g3", "image/g3fax" },
+ { "g3w", "application/vnd.geospace" },
+ { "gac", "application/vnd.groove-account" },
+ { "gam", "application/x-tads" },
+ { "gbr", "application/rpki-ghostbusters" },
+ { "gca", "application/x-gca-compressed" },
+ { "gdl", "model/vnd.gdl" },
+ { "gdoc", "application/vnd.google-apps.document" },
+ { "ged", "text/vnd.familysearch.gedcom" },
+ { "geo", "application/vnd.dynageo" },
+ { "geojson", "application/geo+json" },
+ { "gex", "application/vnd.geometry-explorer" },
+ { "ggb", "application/vnd.geogebra.file" },
+ { "ggt", "application/vnd.geogebra.tool" },
+ { "ghf", "application/vnd.groove-help" },
+ { "gif", "image/gif" },
+ { "gim", "application/vnd.groove-identity-message" },
+ { "glb", "model/gltf-binary" },
+ { "gltf", "model/gltf+json" },
+ { "gml", "application/gml+xml" },
+ { "gmx", "application/vnd.gmx" },
+ { "gnumeric", "application/x-gnumeric" },
+ { "gph", "application/vnd.flographit" },
+ { "gpx", "application/gpx+xml" },
+ { "gqf", "application/vnd.grafeq" },
+ { "gqs", "application/vnd.grafeq" },
+ { "gram", "application/srgs" },
+ { "gramps", "application/x-gramps-xml" },
+ { "gre", "application/vnd.geometry-explorer" },
+ { "grv", "application/vnd.groove-injector" },
+ { "grxml", "application/srgs+xml" },
+ { "gsf", "application/x-font-ghostscript" },
+ { "gsheet", "application/vnd.google-apps.spreadsheet" },
+ { "gslides", "application/vnd.google-apps.presentation" },
+ { "gtar", "application/x-gtar" },
+ { "gtm", "application/vnd.groove-tool-message" },
+ { "gtw", "model/vnd.gtw" },
+ { "gv", "text/vnd.graphviz" },
+ { "gxf", "application/gxf" },
+ { "gxt", "application/vnd.geonext" },
+ { "gz", "application/gzip" },
+ { "h", "text/x-c" },
+ { "h261", "video/h261" },
+ { "h263", "video/h263" },
+ { "h264", "video/h264" },
+ { "hal", "application/vnd.hal+xml" },
+ { "hbci", "application/vnd.hbci" },
+ { "hbs", "text/x-handlebars-template" },
+ { "hdd", "application/x-virtualbox-hdd" },
+ { "hdf", "application/x-hdf" },
+ { "heic", "image/heic" },
+ { "heics", "image/heic-sequence" },
+ { "heif", "image/heif" },
+ { "heifs", "image/heif-sequence" },
+ { "hej2", "image/hej2k" },
+ { "held", "application/atsc-held+xml" },
+ { "hh", "text/x-c" },
+ { "hjson", "application/hjson" },
+ { "hlp", "application/winhlp" },
+ { "hpgl", "application/vnd.hp-hpgl" },
+ { "hpid", "application/vnd.hp-hpid" },
+ { "hps", "application/vnd.hp-hps" },
+ { "hqx", "application/mac-binhex40" },
+ { "hsj2", "image/hsj2" },
+ { "htc", "text/x-component" },
+ { "htke", "application/vnd.kenameaapp" },
+ { "htm", "text/html" },
+ { "html", "text/html" },
+ { "hvd", "application/vnd.yamaha.hv-dic" },
+ { "hvp", "application/vnd.yamaha.hv-voice" },
+ { "hvs", "application/vnd.yamaha.hv-script" },
+ { "i2g", "application/vnd.intergeo" },
+ { "icc", "application/vnd.iccprofile" },
+ { "ice", "x-conference/x-cooltalk" },
+ { "icm", "application/vnd.iccprofile" },
+ { "ico", "image/vnd.microsoft.icon" },
+ { "ics", "text/calendar" },
+ { "ief", "image/ief" },
+ { "ifb", "text/calendar" },
+ { "ifm", "application/vnd.shana.informed.formdata" },
+ { "iges", "model/iges" },
+ { "igl", "application/vnd.igloader" },
+ { "igm", "application/vnd.insors.igm" },
+ { "igs", "model/iges" },
+ { "igx", "application/vnd.micrografx.igx" },
+ { "iif", "application/vnd.shana.informed.interchange" },
+ { "img", "application/octet-stream" },
+ { "imp", "application/vnd.accpac.simply.imp" },
+ { "ims", "application/vnd.ms-ims" },
+ { "in", "text/plain" },
+ { "ini", "text/plain" },
+ { "ink", "application/inkml+xml" },
+ { "inkml", "application/inkml+xml" },
+ { "install", "application/x-install-instructions" },
+ { "iota", "application/vnd.astraea-software.iota" },
+ { "ipfix", "application/ipfix" },
+ { "ipk", "application/vnd.shana.informed.package" },
+ { "irm", "application/vnd.ibm.rights-management" },
+ { "irp", "application/vnd.irepository.package+xml" },
+ { "iso", "application/octet-stream" },
+ { "itp", "application/vnd.shana.informed.formtemplate" },
+ { "its", "application/its+xml" },
+ { "ivp", "application/vnd.immervision-ivp" },
+ { "ivu", "application/vnd.immervision-ivu" },
+ { "jad", "text/vnd.sun.j2me.app-descriptor" },
+ { "jade", "text/jade" },
+ { "jam", "application/vnd.jam" },
+ { "jar", "application/java-archive" },
+ { "jardiff", "application/x-java-archive-diff" },
+ { "java", "text/x-java-source" },
+ { "jhc", "image/jphc" },
+ { "jisp", "application/vnd.jisp" },
+ { "jls", "image/jls" },
+ { "jlt", "application/vnd.hp-jlyt" },
+ { "jng", "image/x-jng" },
+ { "jnlp", "application/x-java-jnlp-file" },
+ { "joda", "application/vnd.joost.joda-archive" },
+ { "jp2", "image/jp2" },
+ { "jpe", "image/jpeg" },
+ { "jpeg", "image/jpeg" },
+ { "jpf", "image/jpx" },
+ { "jpg", "image/jpeg" },
+ { "jpg2", "image/jp2" },
+ { "jpgm", "image/jpm" },
+ { "jpgv", "video/jpeg" },
+ { "jph", "image/jph" },
+ { "jpm", "image/jpm" },
+ { "jpx", "image/jpx" },
+ { "js", "application/javascript" },
+ { "json", "application/json" },
+ { "json5", "application/json5" },
+ { "jsonld", "application/ld+json" },
+ { "jsonml", "application/jsonml+json" },
+ { "jsx", "text/jsx" },
+ { "jt", "model/jt" },
+ { "jxr", "image/jxr" },
+ { "jxra", "image/jxra" },
+ { "jxrs", "image/jxrs" },
+ { "jxs", "image/jxs" },
+ { "jxsc", "image/jxsc" },
+ { "jxsi", "image/jxsi" },
+ { "jxss", "image/jxss" },
+ { "kar", "audio/midi" },
+ { "karbon", "application/vnd.kde.karbon" },
+ { "kdbx", "application/x-keepass2" },
+ { "key", "application/vnd.apple.keynote" },
+ { "kfo", "application/vnd.kde.kformula" },
+ { "kia", "application/vnd.kidspiration" },
+ { "kml", "application/vnd.google-earth.kml+xml" },
+ { "kmz", "application/vnd.google-earth.kmz" },
+ { "kne", "application/vnd.kinar" },
+ { "knp", "application/vnd.kinar" },
+ { "kon", "application/vnd.kde.kontour" },
+ { "kpr", "application/vnd.kde.kpresenter" },
+ { "kpt", "application/vnd.kde.kpresenter" },
+ { "kpxx", "application/vnd.ds-keypoint" },
+ { "ksp", "application/vnd.kde.kspread" },
+ { "ktr", "application/vnd.kahootz" },
+ { "ktx", "image/ktx" },
+ { "ktx2", "image/ktx2" },
+ { "ktz", "application/vnd.kahootz" },
+ { "kwd", "application/vnd.kde.kword" },
+ { "kwt", "application/vnd.kde.kword" },
+ { "lasxml", "application/vnd.las.las+xml" },
+ { "latex", "application/x-latex" },
+ { "lbd", "application/vnd.llamagraphics.life-balance.desktop" },
+ { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml" },
+ { "les", "application/vnd.hhe.lesson-player" },
+ { "less", "text/less" },
+ { "lgr", "application/lgr+xml" },
+ { "lha", "application/x-lzh-compressed" },
+ { "link66", "application/vnd.route66.link66+xml" },
+ { "list", "text/plain" },
+ { "list3820", "application/vnd.ibm.modcap" },
+ { "listafp", "application/vnd.ibm.modcap" },
+ { "lnk", "application/x-ms-shortcut" },
+ { "log", "text/plain" },
+ { "lostxml", "application/lost+xml" },
+ { "lrf", "application/octet-stream" },
+ { "lrm", "application/vnd.ms-lrm" },
+ { "ltf", "application/vnd.frogans.ltf" },
+ { "lua", "text/x-lua" },
+ { "luac", "application/x-lua-bytecode" },
+ { "lvp", "audio/vnd.lucent.voice" },
+ { "lwp", "application/vnd.lotus-wordpro" },
+ { "lzh", "application/x-lzh-compressed" },
+ { "m13", "application/x-msmediaview" },
+ { "m14", "application/x-msmediaview" },
+ { "m1v", "video/mpeg" },
+ { "m21", "application/mp21" },
+ { "m2a", "audio/mpeg" },
+ { "m2v", "video/mpeg" },
+ { "m3a", "audio/mpeg" },
+ { "m3u", "audio/x-mpegurl" },
+ { "m3u8", "application/vnd.apple.mpegurl" },
+ { "m4a", "audio/mp4" },
+ { "m4p", "application/mp4" },
+ { "m4s", "video/iso.segment" },
+ { "m4u", "video/vnd.mpegurl" },
+ { "m4v", "video/x-m4v" },
+ { "ma", "application/mathematica" },
+ { "mads", "application/mads+xml" },
+ { "maei", "application/mmt-aei+xml" },
+ { "mag", "application/vnd.ecowin.chart" },
+ { "maker", "application/vnd.framemaker" },
+ { "man", "text/troff" },
+ { "manifest", "text/cache-manifest" },
+ { "map", "application/json" },
+ { "mar", "application/octet-stream" },
+ { "markdown", "text/markdown" },
+ { "mathml", "application/mathml+xml" },
+ { "mb", "application/mathematica" },
+ { "mbk", "application/vnd.mobius.mbk" },
+ { "mbox", "application/mbox" },
+ { "mc1", "application/vnd.medcalcdata" },
+ { "mcd", "application/vnd.mcd" },
+ { "mcurl", "text/vnd.curl.mcurl" },
+ { "md", "text/markdown" },
+ { "mdb", "application/x-msaccess" },
+ { "mdi", "image/vnd.ms-modi" },
+ { "mdx", "text/mdx" },
+ { "me", "text/troff" },
+ { "mesh", "model/mesh" },
+ { "meta4", "application/metalink4+xml" },
+ { "metalink", "application/metalink+xml" },
+ { "mets", "application/mets+xml" },
+ { "mfm", "application/vnd.mfmp" },
+ { "mft", "application/rpki-manifest" },
+ { "mgp", "application/vnd.osgeo.mapguide.package" },
+ { "mgz", "application/vnd.proteus.magazine" },
+ { "mid", "audio/midi" },
+ { "midi", "audio/midi" },
+ { "mie", "application/x-mie" },
+ { "mif", "application/vnd.mif" },
+ { "mime", "message/rfc822" },
+ { "mj2", "video/mj2" },
+ { "mjp2", "video/mj2" },
+ { "mjs", "text/javascript" },
+ { "mk3d", "video/x-matroska" },
+ { "mka", "audio/x-matroska" },
+ { "mkd", "text/x-markdown" },
+ { "mks", "video/x-matroska" },
+ { "mkv", "video/x-matroska" },
+ { "mlp", "application/vnd.dolby.mlp" },
+ { "mmd", "application/vnd.chipnuts.karaoke-mmd" },
+ { "mmf", "application/vnd.smaf" },
+ { "mml", "text/mathml" },
+ { "mmr", "image/vnd.fujixerox.edmics-mmr" },
+ { "mng", "video/x-mng" },
+ { "mny", "application/x-msmoney" },
+ { "mobi", "application/x-mobipocket-ebook" },
+ { "mods", "application/mods+xml" },
+ { "mov", "video/quicktime" },
+ { "movie", "video/x-sgi-movie" },
+ { "mp2", "audio/mpeg" },
+ { "mp21", "application/mp21" },
+ { "mp2a", "audio/mpeg" },
+ { "mp3", "audio/mp3" },
+ { "mp4", "video/mp4" },
+ { "mp4a", "audio/mp4" },
+ { "mp4s", "application/mp4" },
+ { "mp4v", "video/mp4" },
+ { "mpc", "application/vnd.mophun.certificate" },
+ { "mpd", "application/dash+xml" },
+ { "mpe", "video/mpeg" },
+ { "mpeg", "video/mpeg" },
+ { "mpf", "application/media-policy-dataset+xml" },
+ { "mpg", "video/mpeg" },
+ { "mpg4", "video/mp4" },
+ { "mpga", "audio/mpeg" },
+ { "mpkg", "application/vnd.apple.installer+xml" },
+ { "mpm", "application/vnd.blueice.multipass" },
+ { "mpn", "application/vnd.mophun.application" },
+ { "mpp", "application/dash-patch+xml" },
+ { "mpt", "application/vnd.ms-project" },
+ { "mpy", "application/vnd.ibm.minipay" },
+ { "mqy", "application/vnd.mobius.mqy" },
+ { "mrc", "application/marc" },
+ { "mrcx", "application/marcxml+xml" },
+ { "ms", "text/troff" },
+ { "mscml", "application/mediaservercontrol+xml" },
+ { "mseed", "application/vnd.fdsn.mseed" },
+ { "mseq", "application/vnd.mseq" },
+ { "msf", "application/vnd.epson.msf" },
+ { "msg", "application/vnd.ms-outlook" },
+ { "msh", "model/mesh" },
+ { "msi", "application/octet-stream" },
+ { "msix", "application/msix" },
+ { "msl", "application/vnd.mobius.msl" },
+ { "msm", "application/octet-stream" },
+ { "msp", "application/octet-stream" },
+ { "msty", "application/vnd.muvee.style" },
+ { "mtl", "model/mtl" },
+ { "mts", "model/vnd.mts" },
+ { "mus", "application/vnd.musician" },
+ { "musd", "application/mmt-usd+xml" },
+ { "musicxml", "application/vnd.recordare.musicxml+xml" },
+ { "mvb", "application/x-msmediaview" },
+ { "mvt", "application/vnd.mapbox-vector-tile" },
+ { "mwf", "application/vnd.mfer" },
+ { "mxf", "application/mxf" },
+ { "mxl", "application/vnd.recordare.musicxml" },
+ { "mxmf", "audio/mobile-xmf" },
+ { "mxml", "application/xv+xml" },
+ { "mxs", "application/vnd.triscape.mxs" },
+ { "mxu", "video/vnd.mpegurl" },
+ { "n3", "text/n3" },
+ { "nb", "application/mathematica" },
+ { "nbp", "application/vnd.wolfram.player" },
+ { "nc", "application/x-netcdf" },
+ { "ncx", "application/x-dtbncx+xml" },
+ { "nfo", "text/x-nfo" },
+ { "ngdat", "application/vnd.nokia.n-gage.data" },
+ { "nitf", "application/vnd.nitf" },
+ { "nlu", "application/vnd.neurolanguage.nlu" },
+ { "nml", "application/vnd.enliven" },
+ { "nnd", "application/vnd.noblenet-directory" },
+ { "nns", "application/vnd.noblenet-sealer" },
+ { "nnw", "application/vnd.noblenet-web" },
+ { "npx", "image/vnd.net-fpx" },
+ { "nq", "application/n-quads" },
+ { "nsc", "application/x-conference" },
+ { "nsf", "application/vnd.lotus-notes" },
+ { "nt", "application/n-triples" },
+ { "ntf", "application/vnd.nitf" },
+ { "numbers", "application/vnd.apple.numbers" },
+ { "nzb", "application/x-nzb" },
+ { "oa2", "application/vnd.fujitsu.oasys2" },
+ { "oa3", "application/vnd.fujitsu.oasys3" },
+ { "oas", "application/vnd.fujitsu.oasys" },
+ { "obd", "application/x-msbinder" },
+ { "obgx", "application/vnd.openblox.game+xml" },
+ { "obj", "model/obj" },
+ { "oda", "application/oda" },
+ { "odb", "application/vnd.oasis.opendocument.database" },
+ { "odc", "application/vnd.oasis.opendocument.chart" },
+ { "odf", "application/vnd.oasis.opendocument.formula" },
+ { "odft", "application/vnd.oasis.opendocument.formula-template" },
+ { "odg", "application/vnd.oasis.opendocument.graphics" },
+ { "odi", "application/vnd.oasis.opendocument.image" },
+ { "odm", "application/vnd.oasis.opendocument.text-master" },
+ { "odp", "application/vnd.oasis.opendocument.presentation" },
+ { "ods", "application/vnd.oasis.opendocument.spreadsheet" },
+ { "odt", "application/vnd.oasis.opendocument.text" },
+ { "oga", "audio/ogg" },
+ { "ogex", "model/vnd.opengex" },
+ { "ogg", "audio/ogg" },
+ { "ogv", "video/ogg" },
+ { "ogx", "application/ogg" },
+ { "omdoc", "application/omdoc+xml" },
+ { "onepkg", "application/onenote" },
+ { "onetmp", "application/onenote" },
+ { "onetoc", "application/onenote" },
+ { "onetoc2", "application/onenote" },
+ { "opf", "application/oebps-package+xml" },
+ { "opml", "text/x-opml" },
+ { "oprc", "application/vnd.palm" },
+ { "opus", "audio/ogg" },
+ { "org", "application/vnd.lotus-organizer" },
+ { "osf", "application/vnd.yamaha.openscoreformat" },
+ { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml" },
+ { "osm", "application/vnd.openstreetmap.data+xml" },
+ { "otc", "application/vnd.oasis.opendocument.chart-template" },
+ { "otf", "font/otf" },
+ { "otg", "application/vnd.oasis.opendocument.graphics-template" },
+ { "oth", "application/vnd.oasis.opendocument.text-web" },
+ { "oti", "application/vnd.oasis.opendocument.image-template" },
+ { "otp", "application/vnd.oasis.opendocument.presentation-template" },
+ { "ots", "application/vnd.oasis.opendocument.spreadsheet-template" },
+ { "ott", "application/vnd.oasis.opendocument.text-template" },
+ { "ova", "application/x-virtualbox-ova" },
+ { "ovf", "application/x-virtualbox-ovf" },
+ { "owl", "application/rdf+xml" },
+ { "oxps", "application/oxps" },
+ { "oxt", "application/vnd.openofficeorg.extension" },
+ { "p", "text/x-pascal" },
+ { "p10", "application/pkcs10" },
+ { "p12", "application/x-pkcs12" },
+ { "p7b", "application/x-pkcs7-certificates" },
+ { "p7c", "application/pkcs7-mime" },
+ { "p7m", "application/pkcs7-mime" },
+ { "p7r", "application/x-pkcs7-certreqresp" },
+ { "p7s", "application/pkcs7-signature" },
+ { "p8", "application/pkcs8" },
+ { "pac", "application/x-ns-proxy-autoconfig" },
+ { "pages", "application/vnd.apple.pages" },
+ { "pas", "text/x-pascal" },
+ { "paw", "application/vnd.pawaafile" },
+ { "pbd", "application/vnd.powerbuilder6" },
+ { "pbm", "image/x-portable-bitmap" },
+ { "pcap", "application/vnd.tcpdump.pcap" },
+ { "pcf", "application/x-font-pcf" },
+ { "pcl", "application/vnd.hp-pcl" },
+ { "pclxl", "application/vnd.hp-pclxl" },
+ { "pct", "image/x-pict" },
+ { "pcurl", "application/vnd.curl.pcurl" },
+ { "pcx", "image/vnd.zbrush.pcx" },
+ { "pdb", "application/vnd.palm" },
+ { "pde", "text/x-processing" },
+ { "pdf", "application/pdf" },
+ { "pem", "application/x-x509-ca-cert" },
+ { "pfa", "application/x-font-type1" },
+ { "pfb", "application/x-font-type1" },
+ { "pfm", "application/x-font-type1" },
+ { "pfr", "application/font-tdpfr" },
+ { "pfx", "application/x-pkcs12" },
+ { "pgm", "image/x-portable-graymap" },
+ { "pgn", "application/x-chess-pgn" },
+ { "pgp", "application/pgp-encrypted" },
+ { "php", "application/x-httpd-php" },
+ { "pic", "image/x-pict" },
+ { "pkg", "application/octet-stream" },
+ { "pki", "application/pkixcmp" },
+ { "pkipath", "application/pkix-pkipath" },
+ { "pkpass", "application/vnd.apple.pkpass" },
+ { "pl", "application/x-perl" },
+ { "plb", "application/vnd.3gpp.pic-bw-large" },
+ { "plc", "application/vnd.mobius.plc" },
+ { "plf", "application/vnd.pocketlearn" },
+ { "pls", "application/pls+xml" },
+ { "pm", "application/x-perl" },
+ { "pml", "application/vnd.ctc-posml" },
+ { "png", "image/png" },
+ { "pnm", "image/x-portable-anymap" },
+ { "portpkg", "application/vnd.macports.portpkg" },
+ { "pot", "application/vnd.ms-powerpoint" },
+ { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12" },
+ { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
+ { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12" },
+ { "ppd", "application/vnd.cups-ppd" },
+ { "ppm", "image/x-portable-pixmap" },
+ { "pps", "application/vnd.ms-powerpoint" },
+ { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12" },
+ { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
+ { "ppt", "application/vnd.ms-powerpoint" },
+ { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12" },
+ { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
+ { "pqa", "application/vnd.palm" },
+ { "prc", "model/prc" },
+ { "pre", "application/vnd.lotus-freelance" },
+ { "prf", "application/pics-rules" },
+ { "provx", "application/provenance+xml" },
+ { "ps", "application/postscript" },
+ { "psb", "application/vnd.3gpp.pic-bw-small" },
+ { "psd", "image/vnd.adobe.photoshop" },
+ { "psf", "application/x-font-linux-psf" },
+ { "pskcxml", "application/pskc+xml" },
+ { "pti", "image/prs.pti" },
+ { "ptid", "application/vnd.pvi.ptid1" },
+ { "pub", "application/x-mspublisher" },
+ { "pvb", "application/vnd.3gpp.pic-bw-var" },
+ { "pwn", "application/vnd.3m.post-it-notes" },
+ { "pya", "audio/vnd.ms-playready.media.pya" },
+ { "pyo", "model/vnd.pytha.pyox" },
+ { "pyox", "model/vnd.pytha.pyox" },
+ { "pyv", "video/vnd.ms-playready.media.pyv" },
+ { "qam", "application/vnd.epson.quickanime" },
+ { "qbo", "application/vnd.intu.qbo" },
+ { "qfx", "application/vnd.intu.qfx" },
+ { "qps", "application/vnd.publishare-delta-tree" },
+ { "qt", "video/quicktime" },
+ { "qwd", "application/vnd.quark.quarkxpress" },
+ { "qwt", "application/vnd.quark.quarkxpress" },
+ { "qxb", "application/vnd.quark.quarkxpress" },
+ { "qxd", "application/vnd.quark.quarkxpress" },
+ { "qxl", "application/vnd.quark.quarkxpress" },
+ { "qxt", "application/vnd.quark.quarkxpress" },
+ { "ra", "audio/x-realaudio" },
+ { "ram", "audio/x-pn-realaudio" },
+ { "raml", "application/raml+yaml" },
+ { "rapd", "application/route-apd+xml" },
+ { "rar", "application/vnd.rar" },
+ { "ras", "image/x-cmu-raster" },
+ { "rdf", "application/rdf+xml" },
+ { "rdz", "application/vnd.data-vision.rdz" },
+ { "relo", "application/p2p-overlay+xml" },
+ { "rep", "application/vnd.businessobjects" },
+ { "res", "application/x-dtbresource+xml" },
+ { "rgb", "image/x-rgb" },
+ { "rif", "application/reginfo+xml" },
+ { "rip", "audio/vnd.rip" },
+ { "ris", "application/x-research-info-systems" },
+ { "rl", "application/resource-lists+xml" },
+ { "rlc", "image/vnd.fujixerox.edmics-rlc" },
+ { "rld", "application/resource-lists-diff+xml" },
+ { "rm", "application/vnd.rn-realmedia" },
+ { "rmi", "audio/midi" },
+ { "rmp", "audio/x-pn-realaudio-plugin" },
+ { "rms", "application/vnd.jcp.javame.midlet-rms" },
+ { "rmvb", "application/vnd.rn-realmedia-vbr" },
+ { "rnc", "application/relax-ng-compact-syntax" },
+ { "rng", "application/xml" },
+ { "roa", "application/rpki-roa" },
+ { "roff", "text/troff" },
+ { "rp9", "application/vnd.cloanto.rp9" },
+ { "rpm", "application/x-redhat-package-manager" },
+ { "rpss", "application/vnd.nokia.radio-presets" },
+ { "rpst", "application/vnd.nokia.radio-preset" },
+ { "rq", "application/sparql-query" },
+ { "rs", "application/rls-services+xml" },
+ { "rsat", "application/atsc-rsat+xml" },
+ { "rsd", "application/rsd+xml" },
+ { "rsheet", "application/urc-ressheet+xml" },
+ { "rss", "application/rss+xml" },
+ { "rtf", "text/rtf" },
+ { "rtx", "text/richtext" },
+ { "run", "application/x-makeself" },
+ { "rusd", "application/route-usd+xml" },
+ { "s", "text/x-asm" },
+ { "s3m", "audio/s3m" },
+ { "saf", "application/vnd.yamaha.smaf-audio" },
+ { "sass", "text/x-sass" },
+ { "sbml", "application/sbml+xml" },
+ { "sc", "application/vnd.ibm.secure-container" },
+ { "scd", "application/x-msschedule" },
+ { "scm", "application/vnd.lotus-screencam" },
+ { "scq", "application/scvp-cv-request" },
+ { "scs", "application/scvp-cv-response" },
+ { "scss", "text/x-scss" },
+ { "scurl", "text/vnd.curl.scurl" },
+ { "sda", "application/vnd.stardivision.draw" },
+ { "sdc", "application/vnd.stardivision.calc" },
+ { "sdd", "application/vnd.stardivision.impress" },
+ { "sdkd", "application/vnd.solent.sdkm+xml" },
+ { "sdkm", "application/vnd.solent.sdkm+xml" },
+ { "sdp", "application/sdp" },
+ { "sdw", "application/vnd.stardivision.writer" },
+ { "sea", "application/x-sea" },
+ { "see", "application/vnd.seemail" },
+ { "seed", "application/vnd.fdsn.seed" },
+ { "sema", "application/vnd.sema" },
+ { "semd", "application/vnd.semd" },
+ { "semf", "application/vnd.semf" },
+ { "senmlx", "application/senml+xml" },
+ { "sensmlx", "application/sensml+xml" },
+ { "ser", "application/java-serialized-object" },
+ { "setpay", "application/set-payment-initiation" },
+ { "setreg", "application/set-registration-initiation" },
+ { "sfs", "application/vnd.spotfire.sfs" },
+ { "sfv", "text/x-sfv" },
+ { "sgi", "image/sgi" },
+ { "sgl", "application/vnd.stardivision.writer-global" },
+ { "sgm", "text/sgml" },
+ { "sgml", "text/sgml" },
+ { "sh", "application/x-sh" },
+ { "shar", "application/x-shar" },
+ { "shex", "text/shex" },
+ { "shf", "application/shf+xml" },
+ { "shtml", "text/html" },
+ { "sid", "image/x-mrsid-image" },
+ { "sieve", "application/sieve" },
+ { "sig", "application/pgp-signature" },
+ { "sil", "audio/silk" },
+ { "silo", "model/mesh" },
+ { "sis", "application/vnd.symbian.install" },
+ { "sisx", "application/vnd.symbian.install" },
+ { "sit", "application/x-stuffit" },
+ { "sitx", "application/x-stuffitx" },
+ { "siv", "application/sieve" },
+ { "skd", "application/vnd.koan" },
+ { "skm", "application/vnd.koan" },
+ { "skp", "application/vnd.koan" },
+ { "skt", "application/vnd.koan" },
+ { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12" },
+ { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" },
+ { "slim", "text/slim" },
+ { "slm", "text/slim" },
+ { "sls", "application/route-s-tsid+xml" },
+ { "slt", "application/vnd.epson.salt" },
+ { "sm", "application/vnd.stepmania.stepchart" },
+ { "smf", "application/vnd.stardivision.math" },
+ { "smi", "application/smil+xml" },
+ { "smil", "application/smil+xml" },
+ { "smv", "video/x-smv" },
+ { "smzip", "application/vnd.stepmania.package" },
+ { "snd", "audio/basic" },
+ { "snf", "application/x-font-snf" },
+ { "so", "application/octet-stream" },
+ { "spc", "application/x-pkcs7-certificates" },
+ { "spdx", "text/spdx" },
+ { "spf", "application/vnd.yamaha.smaf-phrase" },
+ { "spl", "application/x-futuresplash" },
+ { "spot", "text/vnd.in3d.spot" },
+ { "spp", "application/scvp-vp-response" },
+ { "spq", "application/scvp-vp-request" },
+ { "spx", "audio/ogg" },
+ { "sql", "application/sql" },
+ { "src", "application/x-wais-source" },
+ { "srt", "application/x-subrip" },
+ { "sru", "application/sru+xml" },
+ { "srx", "application/sparql-results+xml" },
+ { "ssdl", "application/ssdl+xml" },
+ { "sse", "application/vnd.kodak-descriptor" },
+ { "ssf", "application/vnd.epson.ssf" },
+ { "ssml", "application/ssml+xml" },
+ { "st", "application/vnd.sailingtracker.track" },
+ { "stc", "application/vnd.sun.xml.calc.template" },
+ { "std", "application/vnd.sun.xml.draw.template" },
+ { "stf", "application/vnd.wt.stf" },
+ { "sti", "application/vnd.sun.xml.impress.template" },
+ { "stk", "application/hyperstudio" },
+ { "stl", "model/stl" },
+ { "stpx", "model/step+xml" },
+ { "stpxz", "model/step-xml+zip" },
+ { "stpz", "model/step+zip" },
+ { "str", "application/vnd.pg.format" },
+ { "stw", "application/vnd.sun.xml.writer.template" },
+ { "styl", "text/stylus" },
+ { "stylus", "text/stylus" },
+ { "sub", "text/vnd.dvb.subtitle" },
+ { "sus", "application/vnd.sus-calendar" },
+ { "susp", "application/vnd.sus-calendar" },
+ { "sv4cpio", "application/x-sv4cpio" },
+ { "sv4crc", "application/x-sv4crc" },
+ { "svc", "application/vnd.dvb.service" },
+ { "svd", "application/vnd.svd" },
+ { "svg", "image/svg+xml" },
+ { "svgz", "image/svg+xml" },
+ { "swa", "application/x-director" },
+ { "swf", "application/x-shockwave-flash" },
+ { "swi", "application/vnd.aristanetworks.swi" },
+ { "swidtag", "application/swid+xml" },
+ { "sxc", "application/vnd.sun.xml.calc" },
+ { "sxd", "application/vnd.sun.xml.draw" },
+ { "sxg", "application/vnd.sun.xml.writer.global" },
+ { "sxi", "application/vnd.sun.xml.impress" },
+ { "sxm", "application/vnd.sun.xml.math" },
+ { "sxw", "application/vnd.sun.xml.writer" },
+ { "t", "text/troff" },
+ { "t3", "application/x-t3vm-image" },
+ { "t38", "image/t38" },
+ { "taglet", "application/vnd.mynfc" },
+ { "tao", "application/vnd.tao.intent-module-archive" },
+ { "tap", "image/vnd.tencent.tap" },
+ { "tar", "application/x-tar" },
+ { "tcap", "application/vnd.3gpp2.tcap" },
+ { "tcl", "application/x-tcl" },
+ { "td", "application/urc-targetdesc+xml" },
+ { "teacher", "application/vnd.smart.teacher" },
+ { "tei", "application/tei+xml" },
+ { "tex", "application/x-tex" },
+ { "texi", "application/x-texinfo" },
+ { "texinfo", "application/x-texinfo" },
+ { "text", "text/plain" },
+ { "tfi", "application/thraud+xml" },
+ { "tfm", "application/x-tex-tfm" },
+ { "tfx", "image/tiff-fx" },
+ { "tga", "image/x-tga" },
+ { "thmx", "application/vnd.ms-officetheme" },
+ { "tif", "image/tiff" },
+ { "tiff", "image/tiff" },
+ { "tk", "application/x-tcl" },
+ { "tmo", "application/vnd.tmobile-livetv" },
+ { "toml", "application/toml" },
+ { "torrent", "application/x-bittorrent" },
+ { "tpl", "application/vnd.groove-tool-template" },
+ { "tpt", "application/vnd.trid.tpt" },
+ { "tr", "text/troff" },
+ { "tra", "application/vnd.trueapp" },
+ { "trig", "application/trig" },
+ { "trm", "application/x-msterminal" },
+ { "ts", "video/mp2t" },
+ { "tsd", "application/timestamped-data" },
+ { "tsv", "text/tab-separated-values" },
+ { "ttc", "font/collection" },
+ { "ttf", "font/ttf" },
+ { "ttl", "text/turtle" },
+ { "ttml", "application/ttml+xml" },
+ { "twd", "application/vnd.simtech-mindmapper" },
+ { "twds", "application/vnd.simtech-mindmapper" },
+ { "txd", "application/vnd.genomatix.tuxedo" },
+ { "txf", "application/vnd.mobius.txf" },
+ { "txt", "text/plain" },
+ { "u32", "application/x-authorware-bin" },
+ { "u3d", "model/u3d" },
+ { "u8dsn", "message/global-delivery-status" },
+ { "u8hdr", "message/global-headers" },
+ { "u8mdn", "message/global-disposition-notification" },
+ { "u8msg", "message/global" },
+ { "ubj", "application/ubjson" },
+ { "udeb", "application/x-debian-package" },
+ { "ufd", "application/vnd.ufdl" },
+ { "ufdl", "application/vnd.ufdl" },
+ { "ulx", "application/x-glulx" },
+ { "umj", "application/vnd.umajin" },
+ { "unityweb", "application/vnd.unity" },
+ { "uo", "application/vnd.uoml+xml" },
+ { "uoml", "application/vnd.uoml+xml" },
+ { "uri", "text/uri-list" },
+ { "uris", "text/uri-list" },
+ { "urls", "text/uri-list" },
+ { "usda", "model/vnd.usda" },
+ { "usdz", "model/vnd.usdz+zip" },
+ { "ustar", "application/x-ustar" },
+ { "utz", "application/vnd.uiq.theme" },
+ { "uu", "text/x-uuencode" },
+ { "uva", "audio/vnd.dece.audio" },
+ { "uvd", "application/vnd.dece.data" },
+ { "uvf", "application/vnd.dece.data" },
+ { "uvg", "image/vnd.dece.graphic" },
+ { "uvh", "video/vnd.dece.hd" },
+ { "uvi", "image/vnd.dece.graphic" },
+ { "uvm", "video/vnd.dece.mobile" },
+ { "uvp", "video/vnd.dece.pd" },
+ { "uvs", "video/vnd.dece.sd" },
+ { "uvt", "application/vnd.dece.ttml+xml" },
+ { "uvu", "video/vnd.uvvu.mp4" },
+ { "uvv", "video/vnd.dece.video" },
+ { "uvva", "audio/vnd.dece.audio" },
+ { "uvvd", "application/vnd.dece.data" },
+ { "uvvf", "application/vnd.dece.data" },
+ { "uvvg", "image/vnd.dece.graphic" },
+ { "uvvh", "video/vnd.dece.hd" },
+ { "uvvi", "image/vnd.dece.graphic" },
+ { "uvvm", "video/vnd.dece.mobile" },
+ { "uvvp", "video/vnd.dece.pd" },
+ { "uvvs", "video/vnd.dece.sd" },
+ { "uvvt", "application/vnd.dece.ttml+xml" },
+ { "uvvu", "video/vnd.uvvu.mp4" },
+ { "uvvv", "video/vnd.dece.video" },
+ { "uvvx", "application/vnd.dece.unspecified" },
+ { "uvvz", "application/vnd.dece.zip" },
+ { "uvx", "application/vnd.dece.unspecified" },
+ { "uvz", "application/vnd.dece.zip" },
+ { "vbox", "application/x-virtualbox-vbox" },
+ { "vcard", "text/vcard" },
+ { "vcd", "application/x-cdlink" },
+ { "vcf", "text/x-vcard" },
+ { "vcg", "application/vnd.groove-vcard" },
+ { "vcs", "text/x-vcalendar" },
+ { "vcx", "application/vnd.vcx" },
+ { "vdi", "application/x-virtualbox-vdi" },
+ { "vds", "model/vnd.sap.vds" },
+ { "vhd", "application/x-virtualbox-vhd" },
+ { "vis", "application/vnd.visionary" },
+ { "viv", "video/vnd.vivo" },
+ { "vmdk", "application/x-virtualbox-vmdk" },
+ { "vob", "video/x-ms-vob" },
+ { "vor", "application/vnd.stardivision.writer" },
+ { "vox", "application/x-authorware-bin" },
+ { "vrml", "model/vrml" },
+ { "vsd", "application/vnd.visio" },
+ { "vsf", "application/vnd.vsf" },
+ { "vss", "application/vnd.visio" },
+ { "vst", "application/vnd.visio" },
+ { "vsw", "application/vnd.visio" },
+ { "vtf", "image/vnd.valve.source.texture" },
+ { "vtt", "text/vtt" },
+ { "vtu", "model/vnd.vtu" },
+ { "vxml", "application/voicexml+xml" },
+ { "w3d", "application/x-director" },
+ { "wad", "application/x-doom" },
+ { "wadl", "application/vnd.sun.wadl+xml" },
+ { "war", "application/java-archive" },
+ { "wasm", "application/wasm" },
+ { "wav", "audio/wav" },
+ { "wax", "audio/x-ms-wax" },
+ { "wbmp", "image/vnd.wap.wbmp" },
+ { "wbs", "application/vnd.criticaltools.wbs+xml" },
+ { "wbxml", "application/vnd.wap.wbxml" },
+ { "wcm", "application/vnd.ms-works" },
+ { "wdb", "application/vnd.ms-works" },
+ { "wdp", "image/vnd.ms-photo" },
+ { "weba", "audio/webm" },
+ { "webapp", "application/x-web-app-manifest+json" },
+ { "webm", "video/webm" },
+ { "webp", "image/webp" },
+ { "wg", "application/vnd.pmi.widget" },
+ { "wgsl", "text/wgsl" },
+ { "wgt", "application/widget" },
+ { "wif", "application/watcherinfo+xml" },
+ { "wks", "application/vnd.ms-works" },
+ { "wm", "video/x-ms-wm" },
+ { "wma", "audio/x-ms-wma" },
+ { "wmd", "application/x-ms-wmd" },
+ { "wmf", "image/wmf" },
+ { "wml", "text/vnd.wap.wml" },
+ { "wmlc", "application/vnd.wap.wmlc" },
+ { "wmls", "text/vnd.wap.wmlscript" },
+ { "wmlsc", "application/vnd.wap.wmlscriptc" },
+ { "wmv", "video/x-ms-wmv" },
+ { "wmx", "video/x-ms-wmx" },
+ { "wmz", "application/x-ms-wmz" },
+ { "woff", "font/woff" },
+ { "woff2", "font/woff2" },
+ { "wpd", "application/vnd.wordperfect" },
+ { "wpl", "application/vnd.ms-wpl" },
+ { "wps", "application/vnd.ms-works" },
+ { "wqd", "application/vnd.wqd" },
+ { "wri", "application/x-mswrite" },
+ { "wrl", "model/vrml" },
+ { "wsc", "message/vnd.wfa.wsc" },
+ { "wsdl", "application/wsdl+xml" },
+ { "wspolicy", "application/wspolicy+xml" },
+ { "wtb", "application/vnd.webturbo" },
+ { "wvx", "video/x-ms-wvx" },
+ { "x32", "application/x-authorware-bin" },
+ { "x3d", "model/x3d+xml" },
+ { "x3db", "model/x3d+binary" },
+ { "x3dbz", "model/x3d+binary" },
+ { "x3dv", "model/x3d+vrml" },
+ { "x3dvz", "model/x3d+vrml" },
+ { "x3dz", "model/x3d+xml" },
+ { "xaml", "application/xaml+xml" },
+ { "xap", "application/x-silverlight-app" },
+ { "xar", "application/vnd.xara" },
+ { "xav", "application/xcap-att+xml" },
+ { "xbap", "application/x-ms-xbap" },
+ { "xbd", "application/vnd.fujixerox.docuworks.binder" },
+ { "xbm", "image/x-xbitmap" },
+ { "xca", "application/xcap-caps+xml" },
+ { "xcs", "application/calendar+xml" },
+ { "xdf", "application/xcap-diff+xml" },
+ { "xdm", "application/vnd.syncml.dm+xml" },
+ { "xdp", "application/vnd.adobe.xdp+xml" },
+ { "xdssc", "application/dssc+xml" },
+ { "xdw", "application/vnd.fujixerox.docuworks" },
+ { "xel", "application/xcap-el+xml" },
+ { "xenc", "application/xenc+xml" },
+ { "xer", "application/patch-ops-error+xml" },
+ { "xfdf", "application/xfdf" },
+ { "xfdl", "application/vnd.xfdl" },
+ { "xht", "application/xhtml+xml" },
+ { "xhtm", "application/vnd.pwg-xhtml-print+xml" },
+ { "xhtml", "application/xhtml+xml" },
+ { "xhvml", "application/xv+xml" },
+ { "xif", "image/vnd.xiff" },
+ { "xla", "application/vnd.ms-excel" },
+ { "xlam", "application/vnd.ms-excel.addin.macroenabled.12" },
+ { "xlc", "application/vnd.ms-excel" },
+ { "xlf", "application/xliff+xml" },
+ { "xlm", "application/vnd.ms-excel" },
+ { "xls", "application/vnd.ms-excel" },
+ { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12" },
+ { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12" },
+ { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
+ { "xlt", "application/vnd.ms-excel" },
+ { "xltm", "application/vnd.ms-excel.template.macroenabled.12" },
+ { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
+ { "xlw", "application/vnd.ms-excel" },
+ { "xm", "audio/xm" },
+ { "xml", "application/xml" },
+ { "xns", "application/xcap-ns+xml" },
+ { "xo", "application/vnd.olpc-sugar" },
+ { "xop", "application/xop+xml" },
+ { "xpi", "application/x-xpinstall" },
+ { "xpl", "application/xproc+xml" },
+ { "xpm", "image/x-xpixmap" },
+ { "xpr", "application/vnd.is-xpr" },
+ { "xps", "application/vnd.ms-xpsdocument" },
+ { "xpw", "application/vnd.intercon.formnet" },
+ { "xpx", "application/vnd.intercon.formnet" },
+ { "xsd", "application/xml" },
+ { "xsf", "application/prs.xsf+xml" },
+ { "xsl", "application/xml" },
+ { "xslt", "application/xslt+xml" },
+ { "xsm", "application/vnd.syncml+xml" },
+ { "xspf", "application/xspf+xml" },
+ { "xul", "application/vnd.mozilla.xul+xml" },
+ { "xvm", "application/xv+xml" },
+ { "xvml", "application/xv+xml" },
+ { "xwd", "image/x-xwindowdump" },
+ { "xyz", "chemical/x-xyz" },
+ { "xz", "application/x-xz" },
+ { "yaml", "text/yaml" },
+ { "yang", "application/yang" },
+ { "yin", "application/yin+xml" },
+ { "yml", "text/yaml" },
+ { "ymp", "text/x-suse-ymp" },
+ { "z1", "application/x-zmachine" },
+ { "z2", "application/x-zmachine" },
+ { "z3", "application/x-zmachine" },
+ { "z4", "application/x-zmachine" },
+ { "z5", "application/x-zmachine" },
+ { "z6", "application/x-zmachine" },
+ { "z7", "application/x-zmachine" },
+ { "z8", "application/x-zmachine" },
+ { "zaz", "application/vnd.zzazz.deck+xml" },
+ { "zip", "application/zip" },
+ { "zir", "application/vnd.zul" },
+ { "zirz", "application/vnd.zul" },
+ { "zmm", "application/vnd.handheld-entertainment+xml" },
+ };
+ }
+
+ ///
+ /// Attempts to fetch all available file extensions for a MIME-type.
+ ///
+ /// The name of the MIME-type
+ /// All available extensions for the given MIME-type
+ public static IEnumerable GetMimeTypeExtensions(string mimeType)
+ {
+ if (mimeType is null)
+ {
+ throw new ArgumentNullException(nameof(mimeType));
+ }
+
+ return s_typeMap
+ .Where(keyPair => string.Equals(keyPair.Value, mimeType, StringComparison.OrdinalIgnoreCase))
+ .Select(keyPair => keyPair.Key);
+ }
+
+ ///
+ /// Tries to get the MIME-type for the given file name.
+ ///
+ /// The name of the file.
+ /// The MIME-type for the given file name.
+ /// true if a MIME-type was found, false otherwise.
+ public static bool TryGetMimeType(string fileName, out string mimeType)
+ {
+ if (fileName is null)
+ {
+ mimeType = null;
+ return false;
+ }
+
+ var dotIndex = fileName.LastIndexOf('.');
+
+ if (dotIndex != -1 && fileName.Length > dotIndex + 1)
+ {
+ return s_typeMap.TryGetValue(fileName.Substring(dotIndex + 1), out mimeType);
+ }
+
+ mimeType = null;
+ return false;
+ }
+
+ ///
+ /// Gets the MIME-type for the given file name,
+ /// or if a mapping doesn't exist.
+ ///
+ /// The name of the file.
+ /// The MIME-type for the given file name.
+ public static string GetMimeType(string fileName)
+ {
+ if (fileName is null)
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ return TryGetMimeType(fileName, out var result) ? result : FallbackMimeType;
+ }
+ }
+}
+
+#pragma warning enable
diff --git a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
index d618c6e1..44b9398b 100644
--- a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
+++ b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using CSF.Screenplay.Performables;
using CSF.Screenplay.Performances;
@@ -22,6 +23,7 @@ public class PerformanceReportBuilder
readonly Stack performableStack = new Stack();
readonly IGetsValueFormatter valueFormatterProvider;
readonly IFormatsReportFragment formatter;
+ readonly IGetsContentType contentTypeProvider;
///
/// Gets a value indicating whether or not this builder has a 'current' performable that it is building.
@@ -187,7 +189,17 @@ public void BeginPerformable(object performable, Actor actor, string performance
/// The file path to the asset
/// The human readable summary of the asset
public void RecordAssetForCurrentPerformable(string assetPath, string assetSummary)
- => CurrentPerformable.Assets.Add(new PerformableAsset { FilePath = assetPath, FileSummary = assetSummary });
+ {
+ var fileName = Path.GetFileName(assetPath);
+ var asset = new PerformableAsset
+ {
+ FilePath = assetPath,
+ FileSummary = assetSummary,
+ FileName = fileName,
+ ContentType = contentTypeProvider.GetContentType(fileName),
+ };
+ CurrentPerformable.Assets.Add(asset);
+ }
///
/// Enriches the current performable with information about its result.
@@ -245,7 +257,7 @@ public void RecordFailureForCurrentPerformable(Exception exception)
performableStack.Pop();
}
-#endregion
+ #endregion
///
/// Initialises a new instance of .
@@ -253,17 +265,19 @@ public void RecordFailureForCurrentPerformable(Exception exception)
/// The naming hierarchy of the performance; see
/// A value formatter factory
/// A report-fragment formatter
+ /// A content type provider service
/// If any parameter is .
public PerformanceReportBuilder(List namingHierarchy,
IGetsValueFormatter valueFormatterProvider,
- IFormatsReportFragment formatter)
+ IFormatsReportFragment formatter,
+ IGetsContentType contentTypeProvider)
{
if (namingHierarchy is null)
throw new ArgumentNullException(nameof(namingHierarchy));
this.valueFormatterProvider = valueFormatterProvider ?? throw new ArgumentNullException(nameof(valueFormatterProvider));
this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
-
+ this.contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException(nameof(contentTypeProvider));
report = new PerformanceReport
{
NamingHierarchy = namingHierarchy.ToList(),
diff --git a/CSF.Screenplay/Reporting/ReportPathProvider.cs b/CSF.Screenplay/Reporting/ReportPathProvider.cs
index eb378b9d..7db689c0 100644
--- a/CSF.Screenplay/Reporting/ReportPathProvider.cs
+++ b/CSF.Screenplay/Reporting/ReportPathProvider.cs
@@ -15,7 +15,7 @@ namespace CSF.Screenplay.Reporting
///
///
/// If is a relative path then it is combined with the current working directory to form an
- /// absolute path, thus (if does not return null), its return value will always be an absolute path.
+ /// absolute path. Thus, if does not return null, its return value will always be an absolute path.
///
///
/// Because of the caching functionality, this class is stateful and should be used as a singleton.
@@ -28,14 +28,18 @@ public class ReportPathProvider : IGetsReportPath
bool hasCachedReportPath;
string cachedReportPath;
+ readonly object syncRoot = new object();
///
public string GetReportPath()
{
- if(!hasCachedReportPath)
+ lock(syncRoot)
{
- cachedReportPath = ShouldEnableReporting(out var reportPath) ? reportPath : null;
- hasCachedReportPath = true;
+ if(!hasCachedReportPath)
+ {
+ cachedReportPath = ShouldEnableReporting(out var reportPath) ? reportPath : null;
+ hasCachedReportPath = true;
+ }
}
return cachedReportPath;
diff --git a/CSF.Screenplay/Reporting/ScreenplayReportSerializer.cs b/CSF.Screenplay/Reporting/ScreenplayReportSerializer.cs
new file mode 100644
index 00000000..0f208591
--- /dev/null
+++ b/CSF.Screenplay/Reporting/ScreenplayReportSerializer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using CSF.Screenplay.ReportModel;
+
+namespace CSF.Screenplay.Reporting
+{
+
+ ///
+ /// Implementation of and
+ /// which serializes and/or deserializes a Screenplay report to/from a JSON stream.
+ ///
+ public class ScreenplayReportSerializer : IDeserializesReport, ISerializesReport
+ {
+ ///
+ public async Task DeserializeAsync(Stream stream)
+ {
+ if (stream is null)
+ throw new ArgumentNullException(nameof(stream));
+
+ return await JsonSerializer.DeserializeAsync(stream).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task SerializeAsync(ScreenplayReport report)
+ {
+ if (report is null)
+ throw new ArgumentNullException(nameof(report));
+
+ var stream = new BufferedStream(new MemoryStream());
+ await JsonSerializer.SerializeAsync(stream, report).ConfigureAwait(false);
+ stream.Position = 0;
+ return stream;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/WritePermissionTester.cs b/CSF.Screenplay/Reporting/WritePermissionTester.cs
index edf664e5..5b1f0577 100644
--- a/CSF.Screenplay/Reporting/WritePermissionTester.cs
+++ b/CSF.Screenplay/Reporting/WritePermissionTester.cs
@@ -9,7 +9,7 @@ namespace CSF.Screenplay.Reporting
public class WritePermissionTester : ITestsPathForWritePermissions
{
///
- /// Gets a value indicating whether or not the current process has write permission to the specified file path.
+ /// Gets a value indicating whether or not the current process has write permission to the specified path.
///
///
///
diff --git a/CSF.Screenplay/ScreenplayOptions.cs b/CSF.Screenplay/ScreenplayOptions.cs
index 649d7c77..f2865602 100644
--- a/CSF.Screenplay/ScreenplayOptions.cs
+++ b/CSF.Screenplay/ScreenplayOptions.cs
@@ -61,24 +61,27 @@ public sealed class ScreenplayOptions
};
///
- /// Gets a file system path at which a Screenplay report file will be written.
+ /// Gets a file system directory path at which a Screenplay report will be written.
///
///
///
/// As a executes each , it accumulates data relating to those performances, via its reporting
- /// mechanism. This information is then written to a JSON-formatted report file, which is saved at the path specified by this property.
- /// Once the Screenplay has completed this file may be inspected, converted into a different format and otherwise used to learn-about and diagnose the
+ /// mechanism. This information is then written to a JSON-formatted report file, which is saved into a directory specified by this property.
+ /// Once the Screenplay has completed the file may be inspected, converted into a different format and otherwise used to learn-about and diagnose the
/// Screenplay.
///
///
- /// If this value is set to a relative file path, then it will be relative to the current working directory.
- /// If using Screenplay with a software testing integration, then this directory might not be easily determined.
+ /// This value must indicate a directory, and not a file path, as a Screenplay Report may comprise of many files.
+ /// If this value is set to a relative path, then it will be relative to the current working directory.
+ /// If using Screenplay with a software testing integration, then the current working directory might not be easily determined.
+ /// It is strongly recommended that each Screenplay run should create its own directory, so files for the same report are kept together.
+ /// As such, it is advised that the directory name should contain some form of time-based value which will differ upon each run.
///
///
- /// The default value for this property is a relative file path in the current working directory, using the filename ScreenplayReport_[timestamp].json
- /// where [timestamp] is replaced by the current UTC date & time in a format which is similar to ISO 8601, except that the : characters separating
- /// the hours, minutes and second are omitted. This is because they are typically not legal filename characters. A sample of a Screenplay Report filename using
- /// this default path is ScreenplayReport_2024-10-04T192345Z.json.
+ /// The default value for this property is a relative directory path in the current working directory, using the format ScreenplayReport_[timestamp].
+ /// The [timestamp] portion is replaced by the current UTC date & time in a format which is similar to ISO 8601, except that the : characters
+ /// separating the hours, minutes and second are omitted. This is because they are typically not legal path characters. A sample of a Screenplay Report path using
+ /// this convention is ScreenplayReport_2024-10-04T192345Z.
///
///
/// If this property is set to , or an empty/whitespace-only string, or if the path is not writable, then the reporting functionality
@@ -88,7 +91,7 @@ public sealed class ScreenplayOptions
/// At runtime, do not read this value directly; instead use an implementation of service to get the report path.
///
///
- public string ReportPath { get; set; } = $"ScreenplayReport_{DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture)}.json";
+ public string ReportPath { get; set; } = $"ScreenplayReport_{DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture)}";
///
/// An optional callback/action which exposes the various which may be subscribed-to in order to be notified
diff --git a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
index 7264a480..66b32186 100644
--- a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
+++ b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
@@ -48,9 +48,10 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services)
.AddSingleton()
.AddSingleton(s =>
{
- var reportPath = s.GetRequiredService().GetReportPath();
+ var reportPath = s.GetRequiredService().GetReportFilePath();
if(reportPath is null) return new NoOpReporter();
+ Directory.CreateDirectory(Path.GetDirectoryName(reportPath));
var stream = File.Create(reportPath);
return ActivatorUtilities.CreateInstance(s, stream);
});
@@ -66,6 +67,7 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services)
.AddTransient()
.AddTransient()
.AddTransient()
+ .AddTransient()
.AddTransient()
.AddTransient()
.AddTransient()
diff --git a/Tests/CSF.Screenplay.Tests/CSF.Screenplay.Tests.csproj b/Tests/CSF.Screenplay.Tests/CSF.Screenplay.Tests.csproj
index 89c5a653..d58814bf 100644
--- a/Tests/CSF.Screenplay.Tests/CSF.Screenplay.Tests.csproj
+++ b/Tests/CSF.Screenplay.Tests/CSF.Screenplay.Tests.csproj
@@ -28,4 +28,13 @@
+
+
+ Always
+
+
+ Always
+
+
+
diff --git a/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/AssetEmbedderTests.cs b/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/AssetEmbedderTests.cs
new file mode 100644
index 00000000..4e9d2e74
--- /dev/null
+++ b/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/AssetEmbedderTests.cs
@@ -0,0 +1,80 @@
+using System.Linq;
+using CSF.Screenplay.ReportModel;
+using NUnit.Framework.Internal;
+
+namespace CSF.Screenplay.JsonToHtmlReport;
+
+[TestFixture, Parallelizable]
+public class AssetEmbedderTests
+{
+ const string
+ asset1Base64 = "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Wi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAJzFQ/kAAAIzSURBVFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDVErAAAFotAQsAoNUSsAAAWi0BCwCg1RKwAABaLQELAKDV+n+jC1muNUrXEgAAAABJRU5ErkJggg==",
+ asset2Base64 = "/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAMgAyADASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAcIBQYJBAMBAv/EAEUQAQABAwMBBQYCBgcGBgMAAAABAgMEBQYRBxIhMUFRCBNhcYGRFKEiMkJSYsEVM1NygpKxFiOistLhJGPD0fDxQ3Sz/8QAHAEBAAIDAQEBAAAAAAAAAAAAAAMEBQYHAgEI/8QAOBEBAAECBAMFBgUDBQEBAAAAAAECAwQFESEGMUESUWGBkRMicaHB0RQyUrHhFpLwByMkQvEVwv/aAAwDAQACEQMRAD8AsGA/NrMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//Z";
+
+ [Test, AutoMoqData]
+ public async Task EmbedReportAssetsAsyncShouldEmbedTwoEligibleAssets(AssetEmbedder sut, [SampleAssets] ScreenplayReport report)
+ {
+ await sut.EmbedReportAssetsAsync(report, new () { EmbeddedFileExtensions = "png,jpeg", EmbeddedFileSizeThresholdKb = 50 });
+ var performable = report.Performances.First().Reportables.OfType().First();
+ Assert.That(performable.Assets, Has.All.Matches(a => a is { FilePath: null, FileData: not null }), "Assets have been embedded");
+ }
+
+ [Test, AutoMoqData]
+ public async Task EmbedReportAssetsAsyncShouldNotEmbedAnAssetOfAnUnsupportedType(AssetEmbedder sut, [SampleAssets] ScreenplayReport report)
+ {
+ await sut.EmbedReportAssetsAsync(report, new () { EmbeddedFileExtensions = "jpeg", EmbeddedFileSizeThresholdKb = 50 });
+ var performable = report.Performances.First().Reportables.OfType().First();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(performable.Assets,
+ Has.One.Matches(a => a is { FileName: SampleAssetsCustomization.Asset2Filename, FileData: not null }),
+ "JPEG asset has been embedded");
+ Assert.That(performable.Assets,
+ Has.One.Matches(a => a is { FileName: SampleAssetsCustomization.Asset1Filename, FileData: null }),
+ "PNG asset has not been embedded");
+ });
+ }
+
+ [Test, AutoMoqData]
+ public async Task EmbedReportAssetsAsyncShouldEmbedAllAssetsIfFileExtensionIsAsterisk(AssetEmbedder sut, [SampleAssets] ScreenplayReport report)
+ {
+ await sut.EmbedReportAssetsAsync(report, new () { EmbeddedFileExtensions = "*", EmbeddedFileSizeThresholdKb = 50 });
+ var performable = report.Performances.First().Reportables.OfType().First();
+ Assert.That(performable.Assets, Has.All.Matches(a => a is { FilePath: null, FileData: not null }), "Assets have been embedded");
+ }
+
+ [Test, AutoMoqData]
+ public async Task EmbedReportAssetsAsyncShouldNotEmbedAnAssetWhichIsTooLarge(AssetEmbedder sut, [SampleAssets] ScreenplayReport report)
+ {
+ await sut.EmbedReportAssetsAsync(report, new () { EmbeddedFileExtensions = "png,jpeg", EmbeddedFileSizeThresholdKb = 10 });
+ var performable = report.Performances.First().Reportables.OfType().First();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(performable.Assets,
+ Has.One.Matches(a => a is { FileName: SampleAssetsCustomization.Asset2Filename, FileData: null }),
+ "JPEG asset has not been embedded");
+ Assert.That(performable.Assets,
+ Has.One.Matches(a => a is { FileName: SampleAssetsCustomization.Asset1Filename, FileData: not null }),
+ "PNG asset has been embedded");
+ });
+ }
+
+ [Test, AutoMoqData]
+ public async Task EmbedReportAssetsAsyncShouldUseCorrectBase64(AssetEmbedder sut, [SampleAssets] ScreenplayReport report)
+ {
+ await sut.EmbedReportAssetsAsync(report, new () { EmbeddedFileExtensions = "png,jpeg", EmbeddedFileSizeThresholdKb = 50 });
+ var performable = report.Performances.First().Reportables.OfType().First();
+
+ Assert.Multiple(() =>
+ {
+ var asset1Data = performable.Assets.FirstOrDefault(x => x.FileName == SampleAssetsCustomization.Asset1Filename)?.FileData;
+ Assert.That(asset1Data, Is.EqualTo(asset1Base64), "PNG asset encoded correctly");
+
+ var asset2Data = performable.Assets.FirstOrDefault(x => x.FileName == SampleAssetsCustomization.Asset2Filename)?.FileData;
+ Assert.That(asset2Data, Is.EqualTo(asset2Base64), "JPEG asset encoded correctly");
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/ReportConverterTests.cs b/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/ReportConverterTests.cs
new file mode 100644
index 00000000..26c3a5d0
--- /dev/null
+++ b/Tests/CSF.Screenplay.Tests/JsonToHtmlReport/ReportConverterTests.cs
@@ -0,0 +1,44 @@
+using System.IO;
+using System.Text;
+using CSF.Screenplay.Reporting;
+using CSF.Screenplay.ReportModel;
+
+namespace CSF.Screenplay.JsonToHtmlReport;
+
+[TestFixture, Parallelizable]
+public class ReportConverterTests
+{
+ [Test, AutoMoqData]
+ public async Task ConvertAsyncShouldWriteTheReportUsingATemplate([NoAutoProperties] ScreenplayReport report,
+ [Frozen] IDeserializesReport deserializer,
+ [Frozen] ISerializesReport serializer,
+ [Frozen] IGetsHtmlTemplate templateReader,
+ [Frozen] IEmbedsReportAssets assetsEmbedder,
+ ReportConverter sut)
+ {
+ var options = new ReportConverterOptions()
+ {
+ ReportPath = Path.Combine(Environment.CurrentDirectory, "SampleReport.json"),
+ OutputPath = Path.Combine(Environment.CurrentDirectory, "ReportConverterTestsOutput.html")
+ };
+ if(File.Exists(options.OutputPath))
+ File.Delete(options.OutputPath);
+
+ Mock.Get(deserializer).Setup(x => x.DeserializeAsync(It.IsAny())).Returns(Task.FromResult(report));
+ Mock.Get(assetsEmbedder).Setup(x => x.EmbedReportAssetsAsync(report, options)).Returns(Task.CompletedTask);
+ var bytes = Encoding.UTF8.GetBytes("SAMPLE REPORT");
+ using var stream = new MemoryStream(bytes);
+ Mock.Get(serializer).Setup(x => x.SerializeAsync(report)).Returns(Task.FromResult(stream));
+ Mock.Get(templateReader).Setup(x => x.ReadTemplate()).Returns(Task.FromResult(""));
+
+ await sut.ConvertAsync(options);
+
+ Assert.Multiple(async () =>
+ {
+ FileAssert.Exists(options.ReportPath, "Report exists");
+ using var reader = new StreamReader(File.Open(options.OutputPath, FileMode.Open));
+ var reportContent = await reader.ReadToEndAsync();
+ Assert.That(reportContent, Is.EqualTo(""), "Report has correct content");
+ });
+ }
+}
\ No newline at end of file
diff --git a/Tests/CSF.Screenplay.Tests/OptionsAttribute.cs b/Tests/CSF.Screenplay.Tests/OptionsAttribute.cs
new file mode 100644
index 00000000..f2199756
--- /dev/null
+++ b/Tests/CSF.Screenplay.Tests/OptionsAttribute.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Reflection;
+using AutoFixture;
+using AutoFixture.Kernel;
+using Microsoft.Extensions.Options;
+
+namespace CSF.Screenplay;
+
+public class OptionsAttribute : CustomizeAttribute
+{
+ public override ICustomization GetCustomization(ParameterInfo parameter)
+ {
+ if(!parameter.ParameterType.IsGenericType || parameter.ParameterType.GetGenericTypeDefinition() != typeof(IOptions<>))
+ throw new ArgumentException($"The parameter type must be a generic type of {nameof(IOptions