MjmlProcessor 1.0.2
dotnet add package MjmlProcessor --version 1.0.2
NuGet\Install-Package MjmlProcessor -Version 1.0.2
<PackageReference Include="MjmlProcessor" Version="1.0.2" />
<PackageVersion Include="MjmlProcessor" Version="1.0.2" />
<PackageReference Include="MjmlProcessor" />
paket add MjmlProcessor --version 1.0.2
#r "nuget: MjmlProcessor, 1.0.2"
#:package MjmlProcessor@1.0.2
#addin nuget:?package=MjmlProcessor&version=1.0.2
#tool nuget:?package=MjmlProcessor&version=1.0.2
MjmlProcessor
A dependency-free MJML to HTML converter for .NET.
Give it MJML markup, get back responsive, Outlook-compatible email HTML. There is no Node.js
process to install, no mjml binary to shell out to, and no call to the hosted MJML API — the
whole renderer is C#, so it runs anywhere your app runs.
using MjmlProcessor;
var html = Mjml.ToHtml("""
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello world</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
""");
Install
dotnet add package MjmlProcessor
Targets netstandard2.0 and net8.0, so it works on .NET Framework 4.6.1+, .NET Core, and
current .NET.
Usage
One-off conversion
string html = Mjml.ToHtml(mjmlSource);
Conversion with validation warnings
Render returns the HTML plus anything questionable the converter found — an unknown tag, an
mj-class that was never declared, an image with no src.
MjmlResult result = Mjml.Render(mjmlSource);
Console.WriteLine(result.Html);
foreach (MjmlWarning warning in result.Warnings)
{
// "Line 12, column 9 (mj-image): mj-image requires a src attribute."
Console.WriteLine(warning);
}
Reuse a converter
MjmlConverter snapshots its options at construction and holds no per-call state, so a single
instance can be registered as a singleton and shared across threads.
services.AddSingleton(new MjmlConverter(new MjmlOptions { Minify = true }));
// ...
string html = converter.ConvertToHtml(mjmlSource);
Rendering from a file
RenderFile resolves mj-include paths relative to the file being rendered.
string html = Mjml.FileToHtml("templates/welcome.mjml");
Options
| Option | Default | What it does |
|---|---|---|
Beautify |
true |
Indents and line-breaks the generated markup. |
Minify |
false |
Collapses the whitespace between tags. Text content is left alone. |
IncludeDocumentSkeleton |
true |
Set to false to emit only the body markup, with no <!doctype> or <head>. CSS that needs a <style> block is then dropped, with a warning. |
Language |
"und" |
The lang attribute of <html>. |
Direction |
"auto" |
The dir attribute of <html>. |
ValidationLevel |
Soft |
Skip ignores problems, Soft collects them as warnings, Strict throws. |
FileLoader |
null |
Resolves mj-include paths. See below. |
Fonts |
Google's 5 | Font family to stylesheet URL, imported only when the family is actually used. |
var options = new MjmlOptions
{
Beautify = false,
Minify = true,
ValidationLevel = MjmlValidationLevel.Strict,
};
options.Fonts["Inter"] = "https://fonts.googleapis.com/css?family=Inter:400,700";
string html = Mjml.ToHtml(source, options);
Includes
mj-include needs a loader, because the library never touches the filesystem on its own.
Mjml.RenderFile wires one up automatically; supply your own to load partials from anywhere
else — embedded resources, a database, blob storage.
var options = new MjmlOptions { FileLoader = new DirectoryFileLoader("templates/partials") };
string html = Mjml.ToHtml(source, options);
DirectoryFileLoader refuses to read outside its configured root, appends .mjml when the
path has no extension, and supports type="html" and type="css" includes.
public sealed class EmbeddedLoader : IMjmlFileLoader
{
public string? Load(string path) => /* return the partial, or null when not found */;
}
Supported elements
Head: mj-head, mj-title, mj-preview, mj-attributes (mj-all, per-tag defaults, and
named mj-class sets), mj-style (including inline="inline"), mj-font, mj-breakpoint,
mj-raw, mj-include.
Layout: mj-body, mj-wrapper, mj-section (including full-width and background
images with a VML fallback), mj-group, mj-column, mj-hero.
Content: mj-text, mj-button, mj-image, mj-divider, mj-spacer, mj-table,
mj-raw, mj-social / mj-social-element, mj-navbar / mj-navbar-link (including the
hamburger menu), mj-accordion / mj-accordion-element / -title / -text.
CSS inlining
Gmail and several other clients strip <style> blocks, so CSS often has to live in style
attributes. mj-style inline="inline" does that for you — the rules are merged into the
matching elements and the block itself disappears:
<mj-head>
<mj-style inline="inline">
.card p { margin: 0 0 12px 0; font-family: Arial, sans-serif; }
.card p:last-child { margin-bottom: 0; }
</mj-style>
</mj-head>
Inlining runs over the finished document, so it reaches your own HTML inside mj-text and
mj-raw — which is usually the point — as well as elements the renderer generates and the
<body> element itself. The normal cascade applies: specificity, then source order, with a
style attribute that is already on the element beating the stylesheet unless the rule is
!important.
Supported selectors: type, *, .class, #id, attribute selectors ([a], [a=b], ~=,
|=, ^=, $=, *=), the descendant, >, + and ~ combinators, and the structural
pseudo-classes :first-child, :last-child, :only-child, :first-of-type, :last-of-type,
:nth-child(), :nth-last-child() and :not().
Anything with no static equivalent — @media, @font-face, :hover, ::before — cannot be
inlined and is kept in a <style> block instead of being dropped. A selector group is split, so
p, a:hover { ... } inlines the p half and preserves the a:hover half.
Two things worth knowing:
- CSS is inlined, not shortened. Declarations are merged per element; no shorthand collapsing.
css-classon a component lands where MJML puts it, which is not always the element you expect. Onmj-buttonit goes on the wrapping<td>, so target the link with.cta a { ... }rather thana.cta { ... }.
Not implemented
mj-carousel— the gallery component. Documents using it render without it and get a warning.mj-html-attributes— ignored, with a warning.
Anything unrecognised is skipped and reported as a warning rather than failing the render,
unless ValidationLevel is Strict.
What the output looks like
The renderer produces the same shape of markup the reference MJML implementation does:
- a centred, fixed-width table layout (600px by default, set with
<mj-body width="...">) - Outlook and IE ghost tables in
<!--[if mso | IE]>conditionals, so columns sit side by side in Word-based clients and stack everywhere else @mediarules keyed to the breakpoint (<mj-breakpoint>), duplicated outside the media query for Thunderbird's.moz-text-htmlmode- VML fallbacks for section and hero background images
- the usual client resets:
#outlook a,mso-table-lspace,-ms-interpolation-mode, and so on
It is not a byte-for-byte match with the JavaScript implementation, so don't diff the two — compare how they render.
Error handling
- Syntactically broken markup throws
MjmlParseException, carryingLineandColumn. - With
ValidationLevel.Strict, the first semantic problem throwsMjmlException. ConvertFilethrowsFileNotFoundExceptionwhen the template is missing.
try
{
var html = Mjml.ToHtml(source);
}
catch (MjmlParseException ex)
{
Console.WriteLine($"Bad template at line {ex.Line}, column {ex.Column}: {ex.Message}");
}
Building
dotnet build
dotnet test
dotnet pack src/MjmlProcessor -c Release
Package icon
assets/icon.svg is the source artwork; assets/icon.png is the 128×128 render that ships in the
package as PackageIcon. Regenerate the PNG from the SVG after changing the artwork — any SVG
rasteriser will do, for example:
rsvg-convert -w 128 -h 128 -b white assets/icon.svg -o assets/icon.png
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.