mjml

package module
v0.14.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2024 License: Apache-2.0 Imports: 19 Imported by: 3

README

mjml-go

Go Reference Tests Status Test Coverage

Compile MJML into HTML directly in your Go application!

Why?

MJML is a JavaScript library. In order to use it with other languages, the usual approach is to wrap the library in a Node.js HTTP server and provide an endpoint through which applications not written in JavaScript can make HTTP requests to compile MJML into HTML.

This approach poses some challenges, for example, if MJML is upgraded to a new major version in the deployed Node.js servers, applications calling these servers will need to be upgraded in a synchronized manner to avoid incompatibilities. In addition, running these extra servers introduces extra moving parts and the network into the mix.

This is why we built mjml-go and created an idiomatic Go API to compile MJML into HTML directly in Go applications that can be deployed as a single Go binary.

How?

We wrote a simple JavaScript wrapper that wraps around the MJML library by accepting input and returning output using JSON. This wrapper is then bundled using webpack and compiled into a WebAssembly module using Suborbital's Javy fork, a Javascript to WebAssembly compiler. The WebAssembly module is then compressed using Brotli to yield a 10x reduction in file size.

During runtime, the module is decompressed and loaded into a Wazero runtime on application start up to accept input in order to compile MJML into HTML.

Workers

As WebAssembly modules compiled using Javy are not thread-safe and cannot be called concurrently, the library maintains a pool of 1 to 10 instances to perform compilations. Idle instances are automatically destroyed and will be re-created when they are needed. This means that the library is thread-safe and you can use it concurrently in multiple goroutines.

Example

func main() {
	
	input := `<mjml><mj-body><mj-section><mj-column><mj-divider border-color="#F45E43"></mj-divider><mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text></mj-column></mj-section></mj-body></mjml>`
	
	output, err := mjml.ToHTML(context.Background(), input, mjml.WithMinify(true))
	
	var mjmlError mjml.Error
	
	if errors.As(err, &mjmlError){
	    fmt.Println(mjmlError.Message)
	    fmt.Println(mjmlError.Details)	
	}
	
	fmt.Println(output)
}

Options

The library provides a complete list of options to customize the MJML compilation process including options for html-minifier, js-beautify and juice.

These are all exposed via an idiomatic Go API and a complete list can be found in the Go documentation.

Defaults

If beautify and minify are enabled, but no options were passed in, the library defaults to using the same defaults as the MJML CLI application:

For minify:

option value
CaseSensitive true
CollapseWhitespace true
MinifyCSS false
RemoveEmptyAttributes true

For beautify:

option value
EndWithNewline true
IndentSize 2
PreserveNewlines false
WrapAttributesIndentSize 2

Limitations

The WebAssembly module is not able to access the filesystem, so <mj-include> tags are ignored. The solution is to flatten your templates during development and pass the flattened templates to mjml.ToHTML().

This example provides a good starting point to create a Node.js script to do this:

import mjml2html from 'mjml' // load default component
import components from 'mjml-core/lib/components.js'
import Parser from 'mjml-parser-xml'
import jsonToXML from 'mjml-core/lib/helpers/jsonToXML.js'

const xml = `<mjml>...</mjml>`

const mjml = Parser(xml, {
      components,
      filePath: '.',
      actualPath: '.'
    })

console.log(JSON.stringify(mjml))
console.log(jsonToXML(mjml))

Differences from the MJML JavaScript library

  • Beautify and minify will be removed from the library in MJML5 and will be moved into the MJML CLI. Therefore, to prepare for this move, the wrapper imports html-minifier and js-beautify directly to support minifying and beautifying the output.
  • In the current implementation of mjml, it is not possible to customize the output of js-beautify. In this library, we have exposed those options.

Benchmarks

We are benchmarking against a very minimal Node.js server serving a single API endpoint.

goos: linux
goarch: amd64
pkg: github.com/Boostport/mjml-go
cpu: 12th Gen Intel(R) Core(TM) i7-12700F
BenchmarkNodeJS/black-friday-20                      594           1865068 ns/op
BenchmarkNodeJS/one-page-20                          288           3978085 ns/op
BenchmarkNodeJS/reactivation-email-20                198           5969088 ns/op
BenchmarkNodeJS/real-estate-20                       153           7644823 ns/op
BenchmarkNodeJS/recast-20                            180           6747342 ns/op
BenchmarkNodeJS/receipt-email-20                     344           3396417 ns/op
BenchmarkMJMLGo/black-friday-20                       19          59296864 ns/op
BenchmarkMJMLGo/one-page-20                            8         136529250 ns/op
BenchmarkMJMLGo/reactivation-email-20                  9         121438942 ns/op
BenchmarkMJMLGo/real-estate-20                         4         265611426 ns/op
BenchmarkMJMLGo/recast-20                              5         213318243 ns/op
BenchmarkMJMLGo/receipt-email-20                       9         117782824 ns/op
PASS
ok      github.com/Boostport/mjml-go    31.910s

In its current state the Node.js implementation is significantly faster than mjml-go. However, with improvements to Wazero (in particular tetratelabs/wazero#618 and tetratelabs/wazero#179), module instantiation times should see great improvement, reducing worker spin-up times and improving the compilation performance.

Also, we should see improvements from Javy improve these numbers as well.

Development

Run tests

You can run tests using docker by running docker compose run test from the root of the repository.

Run benchmarks

From the root of the repository, run go test -bench=. ./.... Alternatively, you can run them in a docker container: docker compose run benchmark

Compile WebAssembly module and build Node.js test server

Run docker compose run build-js from the root of the repository.

Other languages

Since the MJML library is compiled into a WebAssembly module, it should be relatively easy to take the compiled module and drop it into languages with WebAssembly environments.

If you've created a library for another language, please let us know, so that we can add it to this list!

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetMaxWorkers added in v0.2.0

func SetMaxWorkers(maxSize int32) error

func ToHTML

func ToHTML(ctx context.Context, mjml string, toHTMLOptions ...ToHTMLOption) (string, error)

ToHTML converts a string containing mjml to HTML while using any of the optionally provided options

Types

type BeautifyBraceStyle

type BeautifyBraceStyle string
const (
	BeautifyBraceStyleCollapsePreserveInline BeautifyBraceStyle = "collapse-preserve-inline"
	BeautifyBraceStyleCollapse               BeautifyBraceStyle = "collapse"
	BeautifyBraceStyleExpand                 BeautifyBraceStyle = "expand"
	BeautifyBraceStyleEndExpand              BeautifyBraceStyle = "end-expand"
	BeautifyBraceStyleNone                   BeautifyBraceStyle = "none"
)

type BeautifyIndentScripts

type BeautifyIndentScripts string
const (
	BeautifyIndentScriptsKeep     BeautifyIndentScripts = "keep"
	BeautifyIndentScriptsSeparate BeautifyIndentScripts = "separate"
	BeautifyIndentScriptsNormal   BeautifyIndentScripts = "normal"
)

type BeautifyOptions

type BeautifyOptions interface {
	IndentSize(uint) BeautifyOptions
	IndentChar(string) BeautifyOptions
	IndentWithTabs(bool) BeautifyOptions
	Eol(string) BeautifyOptions
	EndWithNewline(bool) BeautifyOptions
	PreserveNewlines(bool) BeautifyOptions
	MaxPreserveNewlines(uint) BeautifyOptions
	IndentInnerHtml(bool) BeautifyOptions
	BraceStyle(BeautifyBraceStyle) BeautifyOptions
	IndentScripts(BeautifyIndentScripts) BeautifyOptions
	WrapLineLength(uint) BeautifyOptions
	WrapAttributes(BeautifyWrapAttributes) BeautifyOptions
	WrapAttributesIndentSize(uint) BeautifyOptions
	Inline([]string) BeautifyOptions
	Unformatted([]string) BeautifyOptions
	ContentUnformatted([]string) BeautifyOptions
	ExtraLiners([]string) BeautifyOptions
	UnformattedContentDelimiter(string) BeautifyOptions
	IndentEmptyLines(bool) BeautifyOptions
	Templating([]BeautifyTemplating) BeautifyOptions
}

BeautifyOptions is used to construct Beautify options to be passed to the MJML compiler Detailed explanations of the options are here: https://github.com/beautify-web/js-beautify#css--html

func NewBeautifyOptions

func NewBeautifyOptions() BeautifyOptions

type BeautifyTemplating

type BeautifyTemplating string
const (
	BeautifyTemplatingAuto       BeautifyTemplating = "auto"
	BeautifyTemplatingNone       BeautifyTemplating = "none"
	BeautifyTemplatingDjango     BeautifyTemplating = "django"
	BeautifyTemplatingERB        BeautifyTemplating = "erb"
	BeautifyTemplatingHandlebars BeautifyTemplating = "handlebars"
	BeautifyTemplatingPHP        BeautifyTemplating = "php"
	BeautifyTemplatingSmarty     BeautifyTemplating = "smarty"
)

type BeautifyWrapAttributes

type BeautifyWrapAttributes string
const (
	BeautifyWrapAttributesAuto                 BeautifyWrapAttributes = "auto"
	BeautifyWrapAttributesForce                BeautifyWrapAttributes = "force"
	BeautifyWrapAttributesForceAligned         BeautifyWrapAttributes = "force-aligned"
	BeautifyWrapAttributesForceExpandMultiline BeautifyWrapAttributes = "force-expand-multiline"
	BeautifyWrapAttributesAlignedMultiple      BeautifyWrapAttributes = "aligned-multiple"
	BeautifyWrapAttributesPreserve             BeautifyWrapAttributes = "preserve"
	BeautifyWrapAttributesPreserveAligned      BeautifyWrapAttributes = "preserved-aligned"
)

type Error

type Error struct {
	Message string `json:"message"`
	Details []struct {
		Line    int    `json:"line"`
		Message string `json:"message"`
		TagName string `json:"tagName"`
	} `json:"details"`
}

func (Error) Error

func (e Error) Error() string

type Fonts

type Fonts map[string]string

type HTMLMinifierOptions

type HTMLMinifierOptions interface {
	CaseSensitive(bool) HTMLMinifierOptions
	CollapseBooleanAttributes(bool) HTMLMinifierOptions
	CollapseInlineTagWhitespace(bool) HTMLMinifierOptions
	CollapseWhitespace(bool) HTMLMinifierOptions
	ConservativeCollapse(bool) HTMLMinifierOptions
	ContinueOnParseError(bool) HTMLMinifierOptions
	CustomAttrAssign([]string) HTMLMinifierOptions
	CustomAttrCollapse(string) HTMLMinifierOptions
	CustomAttrSurround([]string) HTMLMinifierOptions
	DecodeEntities(bool) HTMLMinifierOptions
	HTML5(bool) HTMLMinifierOptions
	IgnoreCustomComments([]string) HTMLMinifierOptions
	IgnoreCustomFragments([]string) HTMLMinifierOptions
	IncludeAutoGeneratedTags(bool) HTMLMinifierOptions
	KeepClosingSlash(bool) HTMLMinifierOptions
	MaxLineLength(lineLength uint) HTMLMinifierOptions
	MinifyCSS(bool) HTMLMinifierOptions
	MinifyURLs(bool) HTMLMinifierOptions
	PreserveLineBreaks(bool) HTMLMinifierOptions
	PreventAttributesEscaping(bool) HTMLMinifierOptions
	ProcessConditionalComments(bool) HTMLMinifierOptions
	ProcessScripts([]string) HTMLMinifierOptions
	QuoteCharacter(character HTMLMinifierQuoteCharacter) HTMLMinifierOptions
	RemoveAttributeQuotes(bool) HTMLMinifierOptions
	RemoveComments(bool) HTMLMinifierOptions
	RemoveEmptyAttributes(bool) HTMLMinifierOptions
	RemoveEmptyElements(bool) HTMLMinifierOptions
	RemoveOptionalTags(bool) HTMLMinifierOptions
	RemoveRedundantAttributes(bool) HTMLMinifierOptions
	RemoveScriptTypeAttributes(bool) HTMLMinifierOptions
	RemoveStyleLinkTypeAttributes(bool) HTMLMinifierOptions
	RemoveTagWhitespace(bool) HTMLMinifierOptions
	SortAttributes(bool) HTMLMinifierOptions
	SortClassName(bool) HTMLMinifierOptions
	TrimCustomFragments(bool) HTMLMinifierOptions
	UseShortDoctype(bool) HTMLMinifierOptions
}

HTMLMinifierOptions is used to construct HTMLMinifier options Detailed explanations of the options are here: https://github.com/kangax/html-minifier#options-quick-reference

func NewHTMLMinifierOptions

func NewHTMLMinifierOptions() HTMLMinifierOptions

type HTMLMinifierQuoteCharacter

type HTMLMinifierQuoteCharacter string
const (
	HTMLMinifierSingleQuote HTMLMinifierQuoteCharacter = "'"
	HTMLMinifierDoubleQuote HTMLMinifierQuoteCharacter = "\""
)

type JuiceOptions

type JuiceOptions interface {
	ApplyAttributesTableElements(bool) JuiceOptions
	ApplyHeightAttributes(bool) JuiceOptions
	ApplyStyleTags(bool) JuiceOptions
	ApplyWidthAttributes(bool) JuiceOptions
	ExtraCss(string) JuiceOptions
	InsertPreservedExtraCss(bool) JuiceOptions
	InlinePseudoElements(bool) JuiceOptions
	PreserveFontFaces(bool) JuiceOptions
	PreserveImportant(bool) JuiceOptions
	PreserveMediaQueries(bool) JuiceOptions
	PreserveKeyFrames(bool) JuiceOptions
	PreservePseudos(bool) JuiceOptions
	RemoveStyleTags(bool) JuiceOptions
	XmlMode(bool) JuiceOptions
}

JuiceOptions is used to construct Juice options to be passed to the MJML compiler Detailed explanations of the options are here: https://github.com/Automattic/juice#options

func NewJuiceOptions

func NewJuiceOptions() JuiceOptions

type JuiceTag

type JuiceTag struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

type ToHTMLOption

type ToHTMLOption func(options)

ToHTMLOption provides options to customize the compilation process Detailed explanations of each option is available here: https://github.com/mjmlio/mjml#inside-nodejs

func WithBeautify

func WithBeautify(beautify bool) ToHTMLOption

func WithBeautifyOptions

func WithBeautifyOptions(bOptions BeautifyOptions) ToHTMLOption

func WithFonts

func WithFonts(fonts Fonts) ToHTMLOption

func WithJuiceOptions

func WithJuiceOptions(jOptions JuiceOptions) ToHTMLOption

func WithJuicePreserveTags

func WithJuicePreserveTags(preserveTags map[string]JuiceTag) ToHTMLOption

func WithKeepComments

func WithKeepComments(keepComments bool) ToHTMLOption

func WithMinify

func WithMinify(minify bool) ToHTMLOption

func WithMinifyOptions

func WithMinifyOptions(minifyOptions HTMLMinifierOptions) ToHTMLOption

func WithPreprocessors

func WithPreprocessors(preprocessors []string) ToHTMLOption

func WithValidationLevel

func WithValidationLevel(validationLevel ValidationLevel) ToHTMLOption

type ValidationLevel

type ValidationLevel string
const (
	Strict ValidationLevel = "strict"
	Soft   ValidationLevel = "soft"
	Skip   ValidationLevel = "skip"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL