mjml-rb 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +60 -22
- data/THIRD_PARTY_LICENSES.md +25 -0
- data/docs/ARCHITECTURE.md +363 -0
- data/docs/MIGRATING_TO_V5.md +112 -0
- data/docs/MJML_V5_SUPPORT_PLAN.md +325 -0
- data/docs/PARITY_AUDIT.md +124 -0
- data/docs/RELEASING.md +35 -0
- data/docs/USAGE.md +1258 -0
- data/docs/releases/v0.6.0.md +63 -0
- data/lib/mjml-rb/cli.rb +43 -31
- data/lib/mjml-rb/column_gutter.rb +81 -0
- data/lib/mjml-rb/compiler.rb +32 -122
- data/lib/mjml-rb/component_registry.rb +11 -1
- data/lib/mjml-rb/components/accordion.rb +11 -9
- data/lib/mjml-rb/components/attributes.rb +5 -5
- data/lib/mjml-rb/components/base.rb +5 -1
- data/lib/mjml-rb/components/body.rb +6 -3
- data/lib/mjml-rb/components/breakpoint.rb +1 -1
- data/lib/mjml-rb/components/carousel.rb +2 -2
- data/lib/mjml-rb/components/carousel_image.rb +2 -2
- data/lib/mjml-rb/components/column.rb +16 -11
- data/lib/mjml-rb/components/group.rb +13 -4
- data/lib/mjml-rb/components/head.rb +3 -3
- data/lib/mjml-rb/components/hero.rb +7 -2
- data/lib/mjml-rb/components/html_attributes.rb +3 -3
- data/lib/mjml-rb/components/image.rb +1 -3
- data/lib/mjml-rb/components/raw.rb +1 -1
- data/lib/mjml-rb/components/section.rb +23 -8
- data/lib/mjml-rb/components/social.rb +19 -28
- data/lib/mjml-rb/components/table.rb +2 -2
- data/lib/mjml-rb/config_file.rb +25 -15
- data/lib/mjml-rb/dependencies.rb +1 -0
- data/lib/mjml-rb/formatter.rb +113 -0
- data/lib/mjml-rb/node_compat.rb +19 -0
- data/lib/mjml-rb/options.rb +81 -0
- data/lib/mjml-rb/parser.rb +158 -131
- data/lib/mjml-rb/renderer.rb +84 -100
- data/lib/mjml-rb/result.rb +3 -2
- data/lib/mjml-rb/validator.rb +26 -34
- data/lib/mjml-rb/version.rb +2 -1
- data/lib/mjml-rb.rb +3 -3
- data/mjml-rb.gemspec +2 -2
- metadata +20 -2
- data/lib/mjml-rb/ast_node.rb +0 -37
data/docs/USAGE.md
ADDED
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
# mjml-rb Usage Guide
|
|
2
|
+
|
|
3
|
+
`mjml-rb` 0.6.0 is a pure-Ruby compiler targeting [MJML 5.4.1](https://mjml.io/).
|
|
4
|
+
Node.js/npm is only used for development reference checks, never at runtime.
|
|
5
|
+
|
|
6
|
+
- **Ruby**: >= 3.3 (CI-tested on 3.3, 3.4, and 4.0)
|
|
7
|
+
- **Dependencies**: css_parser (>= 1.17, < 4; CI-tested with 1.21, 2.x and 3.x), nokogiri (>= 1.13)
|
|
8
|
+
- **License**: MIT
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Ruby API](#ruby-api)
|
|
17
|
+
- [MjmlRb.mjml2html](#mjmlrbmjml2html)
|
|
18
|
+
- [Compiler](#compiler)
|
|
19
|
+
- [Result Object](#result-object)
|
|
20
|
+
- [Compiler Options](#compiler-options)
|
|
21
|
+
- [CLI](#cli)
|
|
22
|
+
- [Flags](#flags)
|
|
23
|
+
- [Examples](#cli-examples)
|
|
24
|
+
- [Config File (.mjmlconfig)](#config-file-mjmlconfig)
|
|
25
|
+
- [Rails Integration](#rails-integration)
|
|
26
|
+
- [Components Reference](#components-reference)
|
|
27
|
+
- [Document Structure](#document-structure)
|
|
28
|
+
- [Layout Components](#layout-components)
|
|
29
|
+
- [Content Components](#content-components)
|
|
30
|
+
- [Head-only Components](#head-only-components)
|
|
31
|
+
- [Component Hierarchy](#component-hierarchy)
|
|
32
|
+
- [Includes (mj-include)](#includes-mj-include)
|
|
33
|
+
- [Validation](#validation)
|
|
34
|
+
- [Custom Components](#custom-components)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Add to your Gemfile:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem "mjml-rb"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install directly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gem install mjml-rb
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`mjml-rb` requires Ruby 3.3 or newer.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require "mjml-rb"
|
|
60
|
+
|
|
61
|
+
result = MjmlRb.mjml2html(<<~MJML)
|
|
62
|
+
<mjml>
|
|
63
|
+
<mj-body>
|
|
64
|
+
<mj-section>
|
|
65
|
+
<mj-column>
|
|
66
|
+
<mj-text>Hello World!</mj-text>
|
|
67
|
+
</mj-column>
|
|
68
|
+
</mj-section>
|
|
69
|
+
</mj-body>
|
|
70
|
+
</mjml>
|
|
71
|
+
MJML
|
|
72
|
+
|
|
73
|
+
puts result[:html] # => "<!doctype html>..."
|
|
74
|
+
puts result[:errors] # => []
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Ruby API
|
|
80
|
+
|
|
81
|
+
### MjmlRb.mjml2html
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
MjmlRb.mjml2html(mjml_string, options = {}) → Hash
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Compiles an MJML string and returns a hash:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
{
|
|
91
|
+
html: "<!doctype html>...", # compiled HTML (empty string on fatal error)
|
|
92
|
+
errors: [], # array of error hashes
|
|
93
|
+
warnings: [] # array of warning hashes
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`MjmlRb.to_html` is an alias for `MjmlRb.mjml2html`.
|
|
98
|
+
|
|
99
|
+
### Compiler
|
|
100
|
+
|
|
101
|
+
For repeated compilations, instantiate a compiler to reuse configuration:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
compiler = MjmlRb::Compiler.new(
|
|
105
|
+
validation_level: "strict",
|
|
106
|
+
beautify: true,
|
|
107
|
+
keep_comments: false
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
result = compiler.compile(mjml_string)
|
|
111
|
+
result = compiler.compile(another_mjml_string) # same options
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You can also pass per-call overrides:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
result = compiler.compile(mjml_string, minify: true)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Result Object
|
|
121
|
+
|
|
122
|
+
`Compiler#compile` returns a `MjmlRb::Result`:
|
|
123
|
+
|
|
124
|
+
| Method | Return | Description |
|
|
125
|
+
|--------|--------|-------------|
|
|
126
|
+
| `html` | `String` | Compiled HTML output (empty on fatal error) |
|
|
127
|
+
| `errors` | `Array[Hash]` | Validation errors and parse failures |
|
|
128
|
+
| `warnings` | `Array[Hash]` | Non-blocking issues (soft mode include failures, etc.) |
|
|
129
|
+
| `success?` | `Boolean` | `true` if `errors` is empty |
|
|
130
|
+
| `to_h` | `Hash` | `{html:, errors:, warnings:}` |
|
|
131
|
+
|
|
132
|
+
Each error/warning hash contains:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
{
|
|
136
|
+
line: Integer or nil, # source line number
|
|
137
|
+
message: String, # human-readable message
|
|
138
|
+
tag_name: String or nil, # the MJML tag involved
|
|
139
|
+
formatted_message: String # display-ready message
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Compiler Options
|
|
144
|
+
|
|
145
|
+
| Option | Type | Default | Description |
|
|
146
|
+
|--------|------|---------|-------------|
|
|
147
|
+
| `validation_level` | `String` | `"soft"` | `"soft"`, `"strict"`, or `"skip"` |
|
|
148
|
+
| `beautify` | `Boolean` | `false` | Pretty-print output HTML |
|
|
149
|
+
| `minify` | `Boolean` | `false` | Minify output HTML |
|
|
150
|
+
| `keep_comments` | `Boolean` | `true` | Preserve source comments; Outlook conditionals always survive |
|
|
151
|
+
| `ignore_includes` | `Boolean` | `true` | Silently omit `mj-include`; set false to enable allowed files |
|
|
152
|
+
| `printer_support` | `Boolean` | `false` | Add `@media print` CSS rules |
|
|
153
|
+
| `preprocessors` | `Array` | `[]` | Callables applied to MJML before parsing |
|
|
154
|
+
| `file_path` | `String` | `"."` | Base directory for resolving includes |
|
|
155
|
+
| `actual_path` | `String` | `"."` | Source filename for diagnostics |
|
|
156
|
+
| `lang` | `String` | `"und"` | HTML `lang` attribute |
|
|
157
|
+
| `dir` | `String` | `"auto"` | HTML `dir` attribute |
|
|
158
|
+
| `fonts` | `Hash` | *(built-in)* | `{"Font Name" => "URL"}` for Google Fonts |
|
|
159
|
+
|
|
160
|
+
Snake_case is the native Ruby form; upstream camelCase and dashed keys are
|
|
161
|
+
accepted aliases. Conflicting aliases raise `ArgumentError`. The compatibility
|
|
162
|
+
reference is exposed as `MjmlRb::MJML_COMPATIBILITY_VERSION` and in `mjml --version`.
|
|
163
|
+
See [the v5 migration guide](MIGRATING_TO_V5.md).
|
|
164
|
+
|
|
165
|
+
| Additional option | Default | Meaning |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `include_path` | none | Additional canonical allowed include roots, string or array |
|
|
168
|
+
| `minify_options` | `{}` | `minify_css` boolean; also accepts `minifyCss` / `minifyCSS` |
|
|
169
|
+
| `sanitize_styles` | `false` | Validate and preserve CSS template tokens |
|
|
170
|
+
| `template_syntax` | `{{ }}` and `[[ ]]` pairs | Array of `{prefix:, suffix:}` objects |
|
|
171
|
+
| `allow_mixed_syntax` | `false` | Permit mixed block/property/value CSS template syntax |
|
|
172
|
+
| `mjml_config_path` | working directory | JSON config file or directory |
|
|
173
|
+
| `use_mjml_config_options` | `false` | Apply config options before explicit options |
|
|
174
|
+
| `juice_options`, `juice_preserve_tags` | none | Non-empty values are unsupported and raise |
|
|
175
|
+
|
|
176
|
+
`minify` wins over `beautify`. Formatting is conservative and preserves raw,
|
|
177
|
+
preformatted, script, VML and paired `htmlmin:ignore` content. Unsupported
|
|
178
|
+
`minify_options` keys raise instead of being ignored.
|
|
179
|
+
|
|
180
|
+
**Validation levels:**
|
|
181
|
+
|
|
182
|
+
| Level | Behavior |
|
|
183
|
+
|-------|----------|
|
|
184
|
+
| `"soft"` | Validation issues reported as warnings; compilation proceeds |
|
|
185
|
+
| `"strict"` | Validation issues reported as errors; compilation halts if any found |
|
|
186
|
+
| `"skip"` | Skip component validation; include security and diagnostics still apply |
|
|
187
|
+
|
|
188
|
+
**Preprocessors** are callables (lambdas, procs) that transform the raw MJML string before parsing:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
upcase_tags = ->(mjml) { mjml.gsub(/mj-text/, "mj-text") }
|
|
192
|
+
compiler = MjmlRb::Compiler.new(preprocessors: [upcase_tags])
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## CLI
|
|
198
|
+
|
|
199
|
+
The gem installs a `mjml` binary:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
mjml [options] [files]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Flags
|
|
206
|
+
|
|
207
|
+
| Flag | Alias | Argument | Description |
|
|
208
|
+
|------|-------|----------|-------------|
|
|
209
|
+
| `-r` | `--read` | `FILES` | Compile MJML file(s) (comma-separated or glob) |
|
|
210
|
+
| `-v` | `--validate` | `FILES` | Validate MJML file(s) with strict validation |
|
|
211
|
+
| `-w` | `--watch` | `FILES` | Watch file(s) and recompile on change |
|
|
212
|
+
| `-i` | `--stdin` | — | Read MJML from stdin |
|
|
213
|
+
| `-s` | `--stdout` | — | Write HTML to stdout (default output) |
|
|
214
|
+
| `-o` | `--output` | `PATH` | Write HTML to file or directory |
|
|
215
|
+
| `-c` | `--config` | `KEY=VALUE` | Set compiler option (repeatable) |
|
|
216
|
+
| | `--noStdoutFileComment` | — | Omit `<!-- FILE: ... -->` header on stdout |
|
|
217
|
+
| `-V` | `--version` | — | Print version and exit |
|
|
218
|
+
| `-h` | `--help` | — | Print help and exit |
|
|
219
|
+
|
|
220
|
+
Inline config is also supported: `--config.beautify=true`
|
|
221
|
+
|
|
222
|
+
### CLI Examples
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Compile a file to stdout
|
|
226
|
+
mjml -r email.mjml -s
|
|
227
|
+
|
|
228
|
+
# Compile to a file
|
|
229
|
+
mjml -r email.mjml -o email.html
|
|
230
|
+
|
|
231
|
+
# Compile all .mjml files in a directory
|
|
232
|
+
mjml -r "templates/*.mjml" -o output/
|
|
233
|
+
|
|
234
|
+
# Validate without compiling
|
|
235
|
+
mjml -v email.mjml
|
|
236
|
+
|
|
237
|
+
# Read from stdin, minify
|
|
238
|
+
mjml -i -s -c minify=true < email.mjml
|
|
239
|
+
|
|
240
|
+
# Watch mode
|
|
241
|
+
mjml -w email.mjml -o email.html
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`--validate` emits no HTML and never creates an output file. Watch mode compiles
|
|
245
|
+
initially and polls root files plus enabled recursive include dependencies.
|
|
246
|
+
`--filePath PATH` provides a base directory for stdin includes.
|
|
247
|
+
|
|
248
|
+
### Config File (.mjmlconfig)
|
|
249
|
+
|
|
250
|
+
Place a `.mjmlconfig` file (JSON) in your project root. `.mjmlrc` remains a legacy
|
|
251
|
+
alias. Enable config options with `use_mjml_config_options: true` or CLI
|
|
252
|
+
`--config.useMjmlConfigOptions`; explicit options take precedence and
|
|
253
|
+
`minify_options` merges by key. `.mjmlconfig.js` and JavaScript packages are
|
|
254
|
+
unsupported. Ruby package files must end in `.rb`.
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"packages": [
|
|
261
|
+
"./lib/my_custom_component.rb"
|
|
262
|
+
],
|
|
263
|
+
"options": {
|
|
264
|
+
"beautify": true,
|
|
265
|
+
"validation_level": "soft"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Key | Type | Description |
|
|
271
|
+
|-----|------|-------------|
|
|
272
|
+
| `packages` | `Array[String]` | Ruby files to `require` (for custom components) |
|
|
273
|
+
| `options` | `Hash` | Compiler options (same as Ruby API) |
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Rails Integration
|
|
278
|
+
|
|
279
|
+
mjml-rb auto-registers a `.mjml` template handler when Rails is loaded.
|
|
280
|
+
|
|
281
|
+
### Setup
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Gemfile
|
|
285
|
+
gem "mjml-rb"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
No additional configuration needed for raw XML MJML templates.
|
|
289
|
+
|
|
290
|
+
### Mailer Template
|
|
291
|
+
|
|
292
|
+
```xml
|
|
293
|
+
<!-- app/views/user_mailer/welcome.html.mjml -->
|
|
294
|
+
<mjml>
|
|
295
|
+
<mj-body>
|
|
296
|
+
<mj-section>
|
|
297
|
+
<mj-column>
|
|
298
|
+
<mj-text>Welcome to our service!</mj-text>
|
|
299
|
+
<mj-button href="https://example.com/dashboard">Go to Dashboard</mj-button>
|
|
300
|
+
</mj-column>
|
|
301
|
+
</mj-section>
|
|
302
|
+
</mj-body>
|
|
303
|
+
</mjml>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Raw XML `.mjml` templates are compiled as supplied; embedded ERB is not evaluated.
|
|
307
|
+
For dynamic content, load an ActionView handler such as `slim-rails`, configure
|
|
308
|
+
`config.mjml_rb.rails_template_language = :slim`, and write the `.html.mjml`
|
|
309
|
+
template using that language:
|
|
310
|
+
|
|
311
|
+
```slim
|
|
312
|
+
mjml
|
|
313
|
+
mj-body
|
|
314
|
+
mj-section
|
|
315
|
+
mj-column
|
|
316
|
+
mj-text = "Welcome, #{@user.name}!"
|
|
317
|
+
mj-button href=dashboard_url Go to Dashboard
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Configuration
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# config/initializers/mjml.rb
|
|
324
|
+
|
|
325
|
+
# Set compiler options for Rails (default: strict validation)
|
|
326
|
+
MjmlRb.rails_compiler_options = {
|
|
327
|
+
validation_level: "strict",
|
|
328
|
+
keep_comments: false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# For Slim/Haml templates instead of raw XML:
|
|
332
|
+
MjmlRb.rails_template_language = :slim # or :haml
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Or via Railtie config:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
# config/application.rb
|
|
339
|
+
config.mjml_rb.compiler_options = { validation_level: "strict" }
|
|
340
|
+
config.mjml_rb.rails_template_language = :slim
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Components Reference
|
|
346
|
+
|
|
347
|
+
### Document Structure
|
|
348
|
+
|
|
349
|
+
#### mj-body
|
|
350
|
+
|
|
351
|
+
Top-level content container. Required.
|
|
352
|
+
|
|
353
|
+
```xml
|
|
354
|
+
<mjml>
|
|
355
|
+
<mj-body background-color="#f4f4f4" width="600px">
|
|
356
|
+
...
|
|
357
|
+
</mj-body>
|
|
358
|
+
</mjml>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Attribute | Type | Default |
|
|
362
|
+
|-----------|------|---------|
|
|
363
|
+
| `background-color` | color | — |
|
|
364
|
+
| `id` | string | — |
|
|
365
|
+
| `width` | px | `600px` |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
### Layout Components
|
|
370
|
+
|
|
371
|
+
#### mj-section
|
|
372
|
+
|
|
373
|
+
Horizontal layout container. Direct child of `mj-body`.
|
|
374
|
+
|
|
375
|
+
```xml
|
|
376
|
+
<mj-section background-color="#ffffff" padding="20px 0">
|
|
377
|
+
<mj-column>...</mj-column>
|
|
378
|
+
</mj-section>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
| Attribute | Type | Default |
|
|
382
|
+
|-----------|------|---------|
|
|
383
|
+
| `background-color` | color | — |
|
|
384
|
+
| `background-url` | string | — |
|
|
385
|
+
| `background-repeat` | `repeat` / `no-repeat` | `repeat` |
|
|
386
|
+
| `background-size` | string | `auto` |
|
|
387
|
+
| `background-position` | string | `top center` |
|
|
388
|
+
| `background-position-x` | string | — |
|
|
389
|
+
| `background-position-y` | string | — |
|
|
390
|
+
| `border` | string | — |
|
|
391
|
+
| `border-bottom` | string | — |
|
|
392
|
+
| `border-left` | string | — |
|
|
393
|
+
| `border-right` | string | — |
|
|
394
|
+
| `border-top` | string | — |
|
|
395
|
+
| `border-radius` | string | — |
|
|
396
|
+
| `gutter` | px/% | — |
|
|
397
|
+
| `direction` | `ltr` / `rtl` | `ltr` |
|
|
398
|
+
| `full-width` | `full-width` / *(empty)* | — |
|
|
399
|
+
| `padding` | px/% (1-4 values) | `20px 0` |
|
|
400
|
+
| `padding-top` | px/% | — |
|
|
401
|
+
| `padding-right` | px/% | — |
|
|
402
|
+
| `padding-bottom` | px/% | — |
|
|
403
|
+
| `padding-left` | px/% | — |
|
|
404
|
+
| `text-align` | `left` / `center` / `right` | `center` |
|
|
405
|
+
| `text-padding` | px/% (1-4 values) | `4px 4px 4px 0` |
|
|
406
|
+
|
|
407
|
+
**Allowed children**: `mj-column`, `mj-group`, `mj-raw`
|
|
408
|
+
|
|
409
|
+
#### mj-wrapper
|
|
410
|
+
|
|
411
|
+
Like `mj-section` but wraps multiple sections. Supports section attributes except `gutter`, plus:
|
|
412
|
+
|
|
413
|
+
| Attribute | Type | Default |
|
|
414
|
+
|-----------|------|---------|
|
|
415
|
+
| `gap` | px | — |
|
|
416
|
+
|
|
417
|
+
**Allowed children**: `mj-hero`, `mj-raw`, `mj-section`
|
|
418
|
+
|
|
419
|
+
#### mj-column
|
|
420
|
+
|
|
421
|
+
Responsive column within a section. Width auto-calculated when omitted.
|
|
422
|
+
|
|
423
|
+
```xml
|
|
424
|
+
<mj-column width="50%" background-color="#f0f0f0">
|
|
425
|
+
<mj-text>Content</mj-text>
|
|
426
|
+
</mj-column>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
| Attribute | Type | Default |
|
|
430
|
+
|-----------|------|---------|
|
|
431
|
+
| `background-color` | color | — |
|
|
432
|
+
| `border` | string | — |
|
|
433
|
+
| `border-bottom` | string | — |
|
|
434
|
+
| `border-left` | string | — |
|
|
435
|
+
| `border-right` | string | — |
|
|
436
|
+
| `border-top` | string | — |
|
|
437
|
+
| `border-radius` | string | — |
|
|
438
|
+
| `direction` | `ltr` / `rtl` | `ltr` |
|
|
439
|
+
| `inner-background-color` | color | — |
|
|
440
|
+
| `inner-border` | string | — |
|
|
441
|
+
| `inner-border-bottom` | string | — |
|
|
442
|
+
| `inner-border-left` | string | — |
|
|
443
|
+
| `inner-border-right` | string | — |
|
|
444
|
+
| `inner-border-top` | string | — |
|
|
445
|
+
| `inner-border-radius` | string | — |
|
|
446
|
+
| `padding` | px/% (1-4 values) | — |
|
|
447
|
+
| `padding-top` | px/% | — |
|
|
448
|
+
| `padding-right` | px/% | — |
|
|
449
|
+
| `padding-bottom` | px/% | — |
|
|
450
|
+
| `padding-left` | px/% | — |
|
|
451
|
+
| `vertical-align` | `top` / `middle` / `bottom` | `top` |
|
|
452
|
+
| `width` | px/% | *(auto)* |
|
|
453
|
+
|
|
454
|
+
**Allowed children**: `mj-accordion`, `mj-button`, `mj-carousel`, `mj-divider`, `mj-image`, `mj-raw`, `mj-social`, `mj-spacer`, `mj-table`, `mj-text`, `mj-navbar`
|
|
455
|
+
|
|
456
|
+
#### mj-group
|
|
457
|
+
|
|
458
|
+
Groups columns to prevent wrapping on mobile.
|
|
459
|
+
|
|
460
|
+
| Attribute | Type | Default |
|
|
461
|
+
|-----------|------|---------|
|
|
462
|
+
| `background-color` | color | — |
|
|
463
|
+
| `direction` | `ltr` / `rtl` | `ltr` |
|
|
464
|
+
| `vertical-align` | `top` / `middle` / `bottom` | — |
|
|
465
|
+
| `width` | px/% | — |
|
|
466
|
+
|
|
467
|
+
**Allowed children**: `mj-column`, `mj-raw`
|
|
468
|
+
|
|
469
|
+
#### mj-hero
|
|
470
|
+
|
|
471
|
+
Hero banner with background image overlay.
|
|
472
|
+
|
|
473
|
+
```xml
|
|
474
|
+
<mj-hero mode="fluid-height" background-url="https://example.com/bg.jpg">
|
|
475
|
+
<mj-text>Overlay Text</mj-text>
|
|
476
|
+
</mj-hero>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
| Attribute | Type | Default |
|
|
480
|
+
|-----------|------|---------|
|
|
481
|
+
| `mode` | `fixed-height` / `fluid-height` | `fixed-height` |
|
|
482
|
+
| `height` | px/% | `0px` |
|
|
483
|
+
| `background-url` | string | — |
|
|
484
|
+
| `background-width` | px/% | — |
|
|
485
|
+
| `background-height` | px/% | — |
|
|
486
|
+
| `background-position` | string | `center center` |
|
|
487
|
+
| `background-color` | color | `#ffffff` |
|
|
488
|
+
| `border-radius` | string | — |
|
|
489
|
+
| `inner-background-color` | color | — |
|
|
490
|
+
| `inner-padding` | px/% (1-4 values) | — |
|
|
491
|
+
| `inner-padding-top` | px/% | — |
|
|
492
|
+
| `inner-padding-left` | px/% | — |
|
|
493
|
+
| `inner-padding-right` | px/% | — |
|
|
494
|
+
| `inner-padding-bottom` | px/% | — |
|
|
495
|
+
| `padding` | px/% (1-4 values) | `0px` |
|
|
496
|
+
| `padding-top` | px/% | — |
|
|
497
|
+
| `padding-right` | px/% | — |
|
|
498
|
+
| `padding-bottom` | px/% | — |
|
|
499
|
+
| `padding-left` | px/% | — |
|
|
500
|
+
| `vertical-align` | `top` / `middle` / `bottom` | `top` |
|
|
501
|
+
|
|
502
|
+
**Allowed children**: `mj-accordion`, `mj-button`, `mj-carousel`, `mj-divider`, `mj-image`, `mj-social`, `mj-spacer`, `mj-table`, `mj-text`, `mj-navbar`, `mj-raw`
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### Content Components
|
|
507
|
+
|
|
508
|
+
#### mj-text
|
|
509
|
+
|
|
510
|
+
Block of text/HTML content.
|
|
511
|
+
|
|
512
|
+
```xml
|
|
513
|
+
<mj-text font-size="16px" color="#333333" align="center">
|
|
514
|
+
<p>Hello <strong>World</strong>!</p>
|
|
515
|
+
</mj-text>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
| Attribute | Type | Default |
|
|
519
|
+
|-----------|------|---------|
|
|
520
|
+
| `align` | `left` / `right` / `center` / `justify` | `left` |
|
|
521
|
+
| `background-color` | color | — |
|
|
522
|
+
| `color` | color | `#000000` |
|
|
523
|
+
| `container-background-color` | color | — |
|
|
524
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
525
|
+
| `font-size` | px | `13px` |
|
|
526
|
+
| `font-style` | string | — |
|
|
527
|
+
| `font-weight` | string | — |
|
|
528
|
+
| `height` | px/% | — |
|
|
529
|
+
| `letter-spacing` | px/em (negative ok) | — |
|
|
530
|
+
| `line-height` | px/%/unitless | `1` |
|
|
531
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
532
|
+
| `padding-top` | px/% | — |
|
|
533
|
+
| `padding-right` | px/% | — |
|
|
534
|
+
| `padding-bottom` | px/% | — |
|
|
535
|
+
| `padding-left` | px/% | — |
|
|
536
|
+
| `text-decoration` | string | — |
|
|
537
|
+
| `text-transform` | string | — |
|
|
538
|
+
| `vertical-align` | `top` / `middle` / `bottom` | — |
|
|
539
|
+
|
|
540
|
+
#### mj-image
|
|
541
|
+
|
|
542
|
+
Responsive image with optional link.
|
|
543
|
+
|
|
544
|
+
```xml
|
|
545
|
+
<mj-image src="https://example.com/image.jpg"
|
|
546
|
+
alt="Description"
|
|
547
|
+
href="https://example.com"
|
|
548
|
+
width="300px" />
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
| Attribute | Type | Default |
|
|
552
|
+
|-----------|------|---------|
|
|
553
|
+
| `src` | string | — |
|
|
554
|
+
| `alt` | string | `""` |
|
|
555
|
+
| `href` | string | — |
|
|
556
|
+
| `name` | string | — |
|
|
557
|
+
| `title` | string | — |
|
|
558
|
+
| `rel` | string | — |
|
|
559
|
+
| `srcset` | string | — |
|
|
560
|
+
| `sizes` | string | — |
|
|
561
|
+
| `align` | `left` / `center` / `right` | `center` |
|
|
562
|
+
| `border` | string | `0` |
|
|
563
|
+
| `border-bottom` | string | — |
|
|
564
|
+
| `border-left` | string | — |
|
|
565
|
+
| `border-right` | string | — |
|
|
566
|
+
| `border-top` | string | — |
|
|
567
|
+
| `border-radius` | string | — |
|
|
568
|
+
| `container-background-color` | color | — |
|
|
569
|
+
| `fluid-on-mobile` | boolean | — |
|
|
570
|
+
| `full-width` | `full-width` | — |
|
|
571
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
572
|
+
| `padding-top` | px/% | — |
|
|
573
|
+
| `padding-right` | px/% | — |
|
|
574
|
+
| `padding-bottom` | px/% | — |
|
|
575
|
+
| `padding-left` | px/% | — |
|
|
576
|
+
| `target` | string | `_blank` |
|
|
577
|
+
| `width` | px | — |
|
|
578
|
+
| `height` | px/auto | `auto` |
|
|
579
|
+
| `max-height` | px/% | — |
|
|
580
|
+
| `font-size` | px | `13px` |
|
|
581
|
+
| `usemap` | string | — |
|
|
582
|
+
|
|
583
|
+
#### mj-button
|
|
584
|
+
|
|
585
|
+
Call-to-action button. Inner HTML is preserved.
|
|
586
|
+
|
|
587
|
+
```xml
|
|
588
|
+
<mj-button href="https://example.com"
|
|
589
|
+
background-color="#ff6600"
|
|
590
|
+
color="#ffffff"
|
|
591
|
+
border-radius="8px">
|
|
592
|
+
Sign Up Now
|
|
593
|
+
</mj-button>
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
| Attribute | Type | Default |
|
|
597
|
+
|-----------|------|---------|
|
|
598
|
+
| `href` | string | — |
|
|
599
|
+
| `name` | string | — |
|
|
600
|
+
| `title` | string | — |
|
|
601
|
+
| `rel` | string | — |
|
|
602
|
+
| `align` | `left` / `center` / `right` | `center` |
|
|
603
|
+
| `background-color` | color | `#414141` |
|
|
604
|
+
| `border` | string | `none` |
|
|
605
|
+
| `border-bottom` | string | — |
|
|
606
|
+
| `border-left` | string | — |
|
|
607
|
+
| `border-right` | string | — |
|
|
608
|
+
| `border-top` | string | — |
|
|
609
|
+
| `border-radius` | string | `3px` |
|
|
610
|
+
| `color` | color | `#ffffff` |
|
|
611
|
+
| `container-background-color` | color | — |
|
|
612
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
613
|
+
| `font-size` | px | `13px` |
|
|
614
|
+
| `font-style` | string | — |
|
|
615
|
+
| `font-weight` | string | `normal` |
|
|
616
|
+
| `height` | px/% | — |
|
|
617
|
+
| `inner-padding` | px/% (1-4 values) | `10px 25px` |
|
|
618
|
+
| `letter-spacing` | px/em (negative ok) | — |
|
|
619
|
+
| `line-height` | px/%/unitless | `120%` |
|
|
620
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
621
|
+
| `padding-top` | px/% | — |
|
|
622
|
+
| `padding-right` | px/% | — |
|
|
623
|
+
| `padding-bottom` | px/% | — |
|
|
624
|
+
| `padding-left` | px/% | — |
|
|
625
|
+
| `target` | string | `_blank` |
|
|
626
|
+
| `text-decoration` | string | `none` |
|
|
627
|
+
| `text-transform` | string | `none` |
|
|
628
|
+
| `text-align` | `left` / `right` / `center` | — |
|
|
629
|
+
| `vertical-align` | `top` / `middle` / `bottom` | `middle` |
|
|
630
|
+
| `width` | px/% | — |
|
|
631
|
+
|
|
632
|
+
When `href` is omitted, renders a `<p>` tag instead of `<a>`.
|
|
633
|
+
|
|
634
|
+
#### mj-divider
|
|
635
|
+
|
|
636
|
+
Horizontal rule/divider.
|
|
637
|
+
|
|
638
|
+
```xml
|
|
639
|
+
<mj-divider border-color="#cccccc" border-width="1px" border-style="dashed" />
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
| Attribute | Type | Default |
|
|
643
|
+
|-----------|------|---------|
|
|
644
|
+
| `align` | `left` / `center` / `right` | `center` |
|
|
645
|
+
| `border-color` | color | `#000000` |
|
|
646
|
+
| `border-style` | string | `solid` |
|
|
647
|
+
| `border-width` | px | `4px` |
|
|
648
|
+
| `container-background-color` | color | — |
|
|
649
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
650
|
+
| `padding-top` | px/% | — |
|
|
651
|
+
| `padding-right` | px/% | — |
|
|
652
|
+
| `padding-bottom` | px/% | — |
|
|
653
|
+
| `padding-left` | px/% | — |
|
|
654
|
+
| `width` | px/% | `100%` |
|
|
655
|
+
|
|
656
|
+
#### mj-spacer
|
|
657
|
+
|
|
658
|
+
Vertical spacing element.
|
|
659
|
+
|
|
660
|
+
```xml
|
|
661
|
+
<mj-spacer height="40px" />
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
| Attribute | Type | Default |
|
|
665
|
+
|-----------|------|---------|
|
|
666
|
+
| `height` | px/% | `20px` |
|
|
667
|
+
| `border` | string | — |
|
|
668
|
+
| `border-bottom` | string | — |
|
|
669
|
+
| `border-left` | string | — |
|
|
670
|
+
| `border-right` | string | — |
|
|
671
|
+
| `border-top` | string | — |
|
|
672
|
+
| `container-background-color` | color | — |
|
|
673
|
+
| `padding` | px/% (1-4 values) | — |
|
|
674
|
+
| `padding-top` | px/% | — |
|
|
675
|
+
| `padding-right` | px/% | — |
|
|
676
|
+
| `padding-bottom` | px/% | — |
|
|
677
|
+
| `padding-left` | px/% | — |
|
|
678
|
+
|
|
679
|
+
#### mj-table
|
|
680
|
+
|
|
681
|
+
HTML table with email-safe defaults. Inner HTML is preserved.
|
|
682
|
+
|
|
683
|
+
```xml
|
|
684
|
+
<mj-table>
|
|
685
|
+
<tr>
|
|
686
|
+
<th>Name</th>
|
|
687
|
+
<th>Price</th>
|
|
688
|
+
</tr>
|
|
689
|
+
<tr>
|
|
690
|
+
<td>Widget</td>
|
|
691
|
+
<td>$9.99</td>
|
|
692
|
+
</tr>
|
|
693
|
+
</mj-table>
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
| Attribute | Type | Default |
|
|
697
|
+
|-----------|------|---------|
|
|
698
|
+
| `align` | `left` / `right` / `center` | `left` |
|
|
699
|
+
| `border` | string | `none` |
|
|
700
|
+
| `cellpadding` | integer | `0` |
|
|
701
|
+
| `cellspacing` | integer | `0` |
|
|
702
|
+
| `color` | color | `#000000` |
|
|
703
|
+
| `container-background-color` | color | — |
|
|
704
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
705
|
+
| `font-size` | px | `13px` |
|
|
706
|
+
| `font-weight` | string | — |
|
|
707
|
+
| `line-height` | px/%/unitless | `22px` |
|
|
708
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
709
|
+
| `padding-top` | px/% | — |
|
|
710
|
+
| `padding-right` | px/% | — |
|
|
711
|
+
| `padding-bottom` | px/% | — |
|
|
712
|
+
| `padding-left` | px/% | — |
|
|
713
|
+
| `role` | `none` / `presentation` | — |
|
|
714
|
+
| `table-layout` | `auto` / `fixed` / `initial` / `inherit` | `auto` |
|
|
715
|
+
| `vertical-align` | `top` / `middle` / `bottom` | — |
|
|
716
|
+
| `width` | px/%/auto | `100%` |
|
|
717
|
+
|
|
718
|
+
#### mj-raw
|
|
719
|
+
|
|
720
|
+
Insert raw HTML directly. Not processed by the MJML engine.
|
|
721
|
+
|
|
722
|
+
```xml
|
|
723
|
+
<mj-raw>
|
|
724
|
+
<div class="custom">Any HTML here</div>
|
|
725
|
+
</mj-raw>
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
| Attribute | Type | Default |
|
|
729
|
+
|-----------|------|---------|
|
|
730
|
+
| `position` | `file-start` | — |
|
|
731
|
+
|
|
732
|
+
Use `position="file-start"` to insert content before the `<!doctype>`.
|
|
733
|
+
|
|
734
|
+
#### mj-social
|
|
735
|
+
|
|
736
|
+
Social media icon links.
|
|
737
|
+
|
|
738
|
+
```xml
|
|
739
|
+
<mj-social font-size="14px" icon-size="24px" mode="horizontal">
|
|
740
|
+
<mj-social-element name="facebook" href="https://facebook.com/page">
|
|
741
|
+
Facebook
|
|
742
|
+
</mj-social-element>
|
|
743
|
+
<mj-social-element name="twitter" href="https://twitter.com/handle">
|
|
744
|
+
Twitter
|
|
745
|
+
</mj-social-element>
|
|
746
|
+
</mj-social>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**mj-social attributes:**
|
|
750
|
+
|
|
751
|
+
| Attribute | Type | Default |
|
|
752
|
+
|-----------|------|---------|
|
|
753
|
+
| `align` | `left` / `center` / `right` | `center` |
|
|
754
|
+
| `border` | string | `0` on icons; inherited from parent |
|
|
755
|
+
| `border-radius` | string | `3px` |
|
|
756
|
+
| `color` | color | `#333333` |
|
|
757
|
+
| `container-background-color` | color | — |
|
|
758
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
759
|
+
| `font-size` | px | `13px` |
|
|
760
|
+
| `font-style` | string | — |
|
|
761
|
+
| `font-weight` | string | — |
|
|
762
|
+
| `icon-size` | px/% | `20px` |
|
|
763
|
+
| `icon-height` | px/% | — |
|
|
764
|
+
| `icon-padding` | px/% (1-4 values) | — |
|
|
765
|
+
| `inner-padding` | px/% (1-4 values) | — |
|
|
766
|
+
| `line-height` | px/%/unitless | `22px` |
|
|
767
|
+
| `mode` | `horizontal` / `vertical` | `horizontal` |
|
|
768
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
769
|
+
| `padding-top` | px/% | — |
|
|
770
|
+
| `padding-right` | px/% | — |
|
|
771
|
+
| `padding-bottom` | px/% | — |
|
|
772
|
+
| `padding-left` | px/% | — |
|
|
773
|
+
| `table-layout` | `auto` / `fixed` | — |
|
|
774
|
+
| `text-padding` | px/% (1-4 values) | — |
|
|
775
|
+
| `text-decoration` | string | `none` |
|
|
776
|
+
| `vertical-align` | `top` / `middle` / `bottom` | — |
|
|
777
|
+
|
|
778
|
+
**mj-social-element attributes:**
|
|
779
|
+
|
|
780
|
+
| Attribute | Type | Default |
|
|
781
|
+
|-----------|------|---------|
|
|
782
|
+
| `name` | string | — |
|
|
783
|
+
| `href` | string | — |
|
|
784
|
+
| `src` | string | — |
|
|
785
|
+
| `srcset` | string | — |
|
|
786
|
+
| `sizes` | string | — |
|
|
787
|
+
| `alt` | string | `""` |
|
|
788
|
+
| `title` | string | — |
|
|
789
|
+
| `target` | string | `_blank` |
|
|
790
|
+
| `rel` | string | — |
|
|
791
|
+
| `align` | `left` / `center` / `right` | `left` |
|
|
792
|
+
| `icon-position` | `left` / `right` | `left` |
|
|
793
|
+
| `background-color` | color | — |
|
|
794
|
+
| `border` | string | `0` on icons; inherited from parent |
|
|
795
|
+
| `border-radius` | string | `3px` |
|
|
796
|
+
| `color` | color | `#000` |
|
|
797
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
798
|
+
| `font-size` | px | `13px` |
|
|
799
|
+
| `font-style` | string | — |
|
|
800
|
+
| `font-weight` | string | — |
|
|
801
|
+
| `icon-size` | px/% | — |
|
|
802
|
+
| `icon-height` | px/% | — |
|
|
803
|
+
| `icon-padding` | px/% (1-4 values) | — |
|
|
804
|
+
| `line-height` | px/%/unitless | `1` |
|
|
805
|
+
| `padding` | px/% (1-4 values) | `4px` |
|
|
806
|
+
| `padding-top` | px/% | — |
|
|
807
|
+
| `padding-right` | px/% | — |
|
|
808
|
+
| `padding-bottom` | px/% | — |
|
|
809
|
+
| `padding-left` | px/% | — |
|
|
810
|
+
| `text-padding` | px/% (1-4 values) | `4px 4px 4px 0` |
|
|
811
|
+
| `text-decoration` | string | `none` |
|
|
812
|
+
| `vertical-align` | `top` / `middle` / `bottom` | `middle` |
|
|
813
|
+
|
|
814
|
+
**Built-in network names**: `facebook`, `twitter`, `x`, `google`, `pinterest`, `linkedin`, `instagram`, `web`, `snapchat`, `youtube`, `tumblr`, `github`, `xing`, `vimeo`, `medium`, `soundcloud`, `dribbble`. Each also has a `-noshare` variant (e.g. `facebook-noshare`).
|
|
815
|
+
|
|
816
|
+
#### mj-navbar
|
|
817
|
+
|
|
818
|
+
Navigation bar with optional hamburger menu for mobile.
|
|
819
|
+
|
|
820
|
+
```xml
|
|
821
|
+
<mj-navbar hamburger="hamburger">
|
|
822
|
+
<mj-navbar-link href="/home">Home</mj-navbar-link>
|
|
823
|
+
<mj-navbar-link href="/about">About</mj-navbar-link>
|
|
824
|
+
</mj-navbar>
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**mj-navbar attributes:**
|
|
828
|
+
|
|
829
|
+
| Attribute | Type | Default |
|
|
830
|
+
|-----------|------|---------|
|
|
831
|
+
| `align` | `left` / `center` / `right` | — |
|
|
832
|
+
| `base-url` | string | — |
|
|
833
|
+
| `hamburger` | `hamburger` | — |
|
|
834
|
+
| `ico-align` | `left` / `center` / `right` | — |
|
|
835
|
+
| `ico-open` | string | — |
|
|
836
|
+
| `ico-close` | string | — |
|
|
837
|
+
| `ico-color` | color | — |
|
|
838
|
+
| `ico-font-size` | px/% | — |
|
|
839
|
+
| `ico-font-family` | string | — |
|
|
840
|
+
| `ico-text-transform` | string | — |
|
|
841
|
+
| `ico-padding` | px/% (1-4 values) | — |
|
|
842
|
+
| `ico-padding-top` | px/% | — |
|
|
843
|
+
| `ico-padding-right` | px/% | — |
|
|
844
|
+
| `ico-padding-bottom` | px/% | — |
|
|
845
|
+
| `ico-padding-left` | px/% | — |
|
|
846
|
+
| `ico-text-decoration` | string | — |
|
|
847
|
+
| `ico-line-height` | px/%/unitless | — |
|
|
848
|
+
| `padding` | px/% (1-4 values) | — |
|
|
849
|
+
| `padding-top` | px/% | — |
|
|
850
|
+
| `padding-right` | px/% | — |
|
|
851
|
+
| `padding-bottom` | px/% | — |
|
|
852
|
+
| `padding-left` | px/% | — |
|
|
853
|
+
|
|
854
|
+
**mj-navbar-link attributes:**
|
|
855
|
+
|
|
856
|
+
| Attribute | Type | Default |
|
|
857
|
+
|-----------|------|---------|
|
|
858
|
+
| `href` | string | — |
|
|
859
|
+
| `name` | string | — |
|
|
860
|
+
| `target` | string | — |
|
|
861
|
+
| `rel` | string | — |
|
|
862
|
+
| `color` | color | — |
|
|
863
|
+
| `font-family` | string | — |
|
|
864
|
+
| `font-size` | px | — |
|
|
865
|
+
| `font-style` | string | — |
|
|
866
|
+
| `font-weight` | string | — |
|
|
867
|
+
| `letter-spacing` | px/em (negative ok) | — |
|
|
868
|
+
| `line-height` | px/%/unitless | — |
|
|
869
|
+
| `padding` | px/% (1-4 values) | — |
|
|
870
|
+
| `padding-top` | px/% | — |
|
|
871
|
+
| `padding-right` | px/% | — |
|
|
872
|
+
| `padding-bottom` | px/% | — |
|
|
873
|
+
| `padding-left` | px/% | — |
|
|
874
|
+
| `text-decoration` | string | — |
|
|
875
|
+
| `text-transform` | string | — |
|
|
876
|
+
|
|
877
|
+
#### mj-carousel
|
|
878
|
+
|
|
879
|
+
Image carousel with thumbnails.
|
|
880
|
+
|
|
881
|
+
```xml
|
|
882
|
+
<mj-carousel thumbnails="visible">
|
|
883
|
+
<mj-carousel-image src="https://example.com/1.jpg" />
|
|
884
|
+
<mj-carousel-image src="https://example.com/2.jpg" />
|
|
885
|
+
</mj-carousel>
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**mj-carousel attributes:**
|
|
889
|
+
|
|
890
|
+
| Attribute | Type | Default |
|
|
891
|
+
|-----------|------|---------|
|
|
892
|
+
| `align` | `left` / `center` / `right` | `center` |
|
|
893
|
+
| `border-radius` | string | `6px` |
|
|
894
|
+
| `container-background-color` | color | — |
|
|
895
|
+
| `icon-width` | px/% | `44px` |
|
|
896
|
+
| `left-icon` | string (URL) | *(imgur default)* |
|
|
897
|
+
| `right-icon` | string (URL) | *(imgur default)* |
|
|
898
|
+
| `thumbnails` | `visible` / `hidden` / `supported` | `visible` |
|
|
899
|
+
| `tb-border` | string | `2px solid transparent` |
|
|
900
|
+
| `tb-border-radius` | string | `6px` |
|
|
901
|
+
| `tb-hover-border-color` | color | `#fead0d` |
|
|
902
|
+
| `tb-selected-border-color` | color | `#ccc` |
|
|
903
|
+
| `tb-width` | px/% | — |
|
|
904
|
+
| `padding` | px/% (1-4 values) | — |
|
|
905
|
+
| `padding-top` | px/% | — |
|
|
906
|
+
| `padding-right` | px/% | — |
|
|
907
|
+
| `padding-bottom` | px/% | — |
|
|
908
|
+
| `padding-left` | px/% | — |
|
|
909
|
+
|
|
910
|
+
**mj-carousel-image attributes:**
|
|
911
|
+
|
|
912
|
+
| Attribute | Type | Default |
|
|
913
|
+
|-----------|------|---------|
|
|
914
|
+
| `src` | string | — |
|
|
915
|
+
| `alt` | string | `""` |
|
|
916
|
+
| `href` | string | — |
|
|
917
|
+
| `rel` | string | — |
|
|
918
|
+
| `target` | string | `_blank` |
|
|
919
|
+
| `title` | string | — |
|
|
920
|
+
| `thumbnails-src` | string | — |
|
|
921
|
+
| `border-radius` | string | — |
|
|
922
|
+
| `tb-border` | string | — |
|
|
923
|
+
| `tb-border-radius` | string | — |
|
|
924
|
+
|
|
925
|
+
#### mj-accordion
|
|
926
|
+
|
|
927
|
+
Expandable/collapsible sections.
|
|
928
|
+
|
|
929
|
+
```xml
|
|
930
|
+
<mj-accordion border="1px solid #ddd">
|
|
931
|
+
<mj-accordion-element>
|
|
932
|
+
<mj-accordion-title>Section 1</mj-accordion-title>
|
|
933
|
+
<mj-accordion-text>Content for section 1.</mj-accordion-text>
|
|
934
|
+
</mj-accordion-element>
|
|
935
|
+
</mj-accordion>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**mj-accordion attributes:**
|
|
939
|
+
|
|
940
|
+
| Attribute | Type | Default |
|
|
941
|
+
|-----------|------|---------|
|
|
942
|
+
| `border` | string | `2px solid black` |
|
|
943
|
+
| `container-background-color` | color | — |
|
|
944
|
+
| `font-family` | string | `Ubuntu, Helvetica, Arial, sans-serif` |
|
|
945
|
+
| `icon-align` | `top` / `middle` / `bottom` | `middle` |
|
|
946
|
+
| `icon-width` | px/% | `32px` |
|
|
947
|
+
| `icon-height` | px/% | `32px` |
|
|
948
|
+
| `icon-wrapped-url` | string | *(default +)* |
|
|
949
|
+
| `icon-wrapped-alt` | string | `+` |
|
|
950
|
+
| `icon-unwrapped-url` | string | *(default -)* |
|
|
951
|
+
| `icon-unwrapped-alt` | string | `-` |
|
|
952
|
+
| `icon-position` | `left` / `right` | `right` |
|
|
953
|
+
| `padding` | px/% (1-4 values) | `10px 25px` |
|
|
954
|
+
| `padding-top` | px/% | — |
|
|
955
|
+
| `padding-right` | px/% | — |
|
|
956
|
+
| `padding-bottom` | px/% | — |
|
|
957
|
+
| `padding-left` | px/% | — |
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
### Head-only Components
|
|
962
|
+
|
|
963
|
+
These go inside `<mj-head>` and configure the document.
|
|
964
|
+
|
|
965
|
+
#### mj-breakpoint
|
|
966
|
+
|
|
967
|
+
Set the mobile breakpoint width.
|
|
968
|
+
|
|
969
|
+
```xml
|
|
970
|
+
<mj-head>
|
|
971
|
+
<mj-breakpoint width="600px" />
|
|
972
|
+
</mj-head>
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
| Attribute | Type | Default |
|
|
976
|
+
|-----------|------|---------|
|
|
977
|
+
| `width` | px | `480px` |
|
|
978
|
+
|
|
979
|
+
#### mj-title
|
|
980
|
+
|
|
981
|
+
Set the HTML `<title>` tag.
|
|
982
|
+
|
|
983
|
+
```xml
|
|
984
|
+
<mj-title>My Email Subject</mj-title>
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
#### mj-preview
|
|
988
|
+
|
|
989
|
+
Set hidden preview text (shown in inbox list).
|
|
990
|
+
|
|
991
|
+
```xml
|
|
992
|
+
<mj-preview>This is the preview text shown in email clients.</mj-preview>
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
#### mj-font
|
|
996
|
+
|
|
997
|
+
Register a Google Font for use in the email.
|
|
998
|
+
|
|
999
|
+
```xml
|
|
1000
|
+
<mj-font name="Montserrat" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700" />
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
| Attribute | Type | Default |
|
|
1004
|
+
|-----------|------|---------|
|
|
1005
|
+
| `name` | string | — |
|
|
1006
|
+
| `href` | string | — |
|
|
1007
|
+
|
|
1008
|
+
**Built-in fonts** (no `mj-font` needed): Open Sans, Droid Sans, Lato, Roboto, Ubuntu.
|
|
1009
|
+
|
|
1010
|
+
#### mj-style
|
|
1011
|
+
|
|
1012
|
+
Inject custom CSS into the `<head>`.
|
|
1013
|
+
|
|
1014
|
+
```xml
|
|
1015
|
+
<mj-style>
|
|
1016
|
+
.custom-class { color: red; }
|
|
1017
|
+
</mj-style>
|
|
1018
|
+
|
|
1019
|
+
<!-- Inline styles into element style attributes: -->
|
|
1020
|
+
<mj-style inline="inline">
|
|
1021
|
+
.btn a { color: #00ada5; }
|
|
1022
|
+
</mj-style>
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
| Attribute | Type | Default |
|
|
1026
|
+
|-----------|------|---------|
|
|
1027
|
+
| `inline` | `inline` | — |
|
|
1028
|
+
|
|
1029
|
+
#### mj-attributes
|
|
1030
|
+
|
|
1031
|
+
Set default attributes for components globally or by class.
|
|
1032
|
+
|
|
1033
|
+
```xml
|
|
1034
|
+
<mj-head>
|
|
1035
|
+
<mj-attributes>
|
|
1036
|
+
<mj-all font-family="Helvetica" />
|
|
1037
|
+
<mj-text font-size="16px" color="#333" />
|
|
1038
|
+
<mj-class name="headline" font-size="24px" font-weight="bold" />
|
|
1039
|
+
</mj-attributes>
|
|
1040
|
+
</mj-head>
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
Use `mj-class="headline"` on any component to apply class defaults.
|
|
1044
|
+
|
|
1045
|
+
#### mj-html-attributes
|
|
1046
|
+
|
|
1047
|
+
Add HTML attributes to rendered elements via CSS selectors.
|
|
1048
|
+
|
|
1049
|
+
```xml
|
|
1050
|
+
<mj-head>
|
|
1051
|
+
<mj-html-attributes>
|
|
1052
|
+
<mj-selector path=".custom-table table">
|
|
1053
|
+
<mj-html-attribute name="role">presentation</mj-html-attribute>
|
|
1054
|
+
</mj-selector>
|
|
1055
|
+
</mj-html-attributes>
|
|
1056
|
+
</mj-head>
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
## Component Hierarchy
|
|
1062
|
+
|
|
1063
|
+
```
|
|
1064
|
+
mjml
|
|
1065
|
+
├── mj-head
|
|
1066
|
+
│ ├── mj-attributes
|
|
1067
|
+
│ ├── mj-breakpoint
|
|
1068
|
+
│ ├── mj-font
|
|
1069
|
+
│ ├── mj-html-attributes
|
|
1070
|
+
│ ├── mj-preview
|
|
1071
|
+
│ ├── mj-style
|
|
1072
|
+
│ ├── mj-title
|
|
1073
|
+
│ └── mj-raw
|
|
1074
|
+
└── mj-body
|
|
1075
|
+
├── mj-section
|
|
1076
|
+
│ ├── mj-column
|
|
1077
|
+
│ │ ├── mj-accordion
|
|
1078
|
+
│ │ ├── mj-button
|
|
1079
|
+
│ │ ├── mj-carousel
|
|
1080
|
+
│ │ ├── mj-divider
|
|
1081
|
+
│ │ ├── mj-image
|
|
1082
|
+
│ │ ├── mj-navbar
|
|
1083
|
+
│ │ ├── mj-raw
|
|
1084
|
+
│ │ ├── mj-social
|
|
1085
|
+
│ │ ├── mj-spacer
|
|
1086
|
+
│ │ ├── mj-table
|
|
1087
|
+
│ │ └── mj-text
|
|
1088
|
+
│ ├── mj-group
|
|
1089
|
+
│ │ └── mj-column
|
|
1090
|
+
│ └── mj-raw
|
|
1091
|
+
├── mj-wrapper
|
|
1092
|
+
│ ├── mj-section
|
|
1093
|
+
│ ├── mj-hero
|
|
1094
|
+
│ └── mj-raw
|
|
1095
|
+
├── mj-hero
|
|
1096
|
+
│ └── (same children as mj-column)
|
|
1097
|
+
└── mj-raw
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
**Global attributes** available on all components: `css-class`, `mj-class`.
|
|
1101
|
+
|
|
1102
|
+
---
|
|
1103
|
+
|
|
1104
|
+
## Includes (mj-include)
|
|
1105
|
+
|
|
1106
|
+
Includes are disabled by default. Enable them with `ignore_includes: false` or
|
|
1107
|
+
`--config.allowIncludes`. Set `file_path` to the template directory; the CLI does
|
|
1108
|
+
this for each file. `include_path` adds explicit allowed directories. Nested
|
|
1109
|
+
includes keep the original allowlist while resolving relative to their own file.
|
|
1110
|
+
Canonical path checks reject escapes through traversal or symlinks, as well as
|
|
1111
|
+
absolute, UNC, drive-letter and null-byte paths.
|
|
1112
|
+
|
|
1113
|
+
Include external MJML, HTML, or CSS files:
|
|
1114
|
+
|
|
1115
|
+
```xml
|
|
1116
|
+
<!-- Include MJML content (parsed and merged) -->
|
|
1117
|
+
<mj-include path="./partials/header.mjml" />
|
|
1118
|
+
|
|
1119
|
+
<!-- Include raw HTML (wrapped in mj-raw) -->
|
|
1120
|
+
<mj-include path="./snippets/tracking.html" type="html" />
|
|
1121
|
+
|
|
1122
|
+
<!-- Include CSS (added to <head>) -->
|
|
1123
|
+
<mj-include path="./styles/theme.css" type="css" />
|
|
1124
|
+
|
|
1125
|
+
<!-- Include CSS and inline it into style attributes -->
|
|
1126
|
+
<mj-include path="./styles/inline.css" type="css" css-inline="inline" />
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
| Attribute | Type | Description |
|
|
1130
|
+
|-----------|------|-------------|
|
|
1131
|
+
| `path` | string | File path to include (**required**) |
|
|
1132
|
+
| `type` | `html` / `css` | Include mode (default: MJML content) |
|
|
1133
|
+
| `css-inline` | `inline` | Inline CSS into style attributes (only with `type="css"`) |
|
|
1134
|
+
|
|
1135
|
+
- Paths are resolved relative to the including file
|
|
1136
|
+
- Circular includes are detected and raise an error
|
|
1137
|
+
- Missing or denied files produce structured diagnostics: warnings (soft/skip) or errors (strict)
|
|
1138
|
+
- Ignored includes are removed before validation; denied includes emit `<!-- mj-include denied -->`
|
|
1139
|
+
- CSS includes in `mj-head` retain source order
|
|
1140
|
+
- Nested includes are fully supported
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
## Validation
|
|
1145
|
+
|
|
1146
|
+
The validator checks:
|
|
1147
|
+
|
|
1148
|
+
1. **Root element** must be `<mjml>`
|
|
1149
|
+
2. **Required children** — `<mj-body>` must exist
|
|
1150
|
+
3. **Known tags** — only registered MJML components allowed
|
|
1151
|
+
4. **Hierarchy** — children must be valid for their parent
|
|
1152
|
+
5. **Supported attributes** — unknown attributes are flagged
|
|
1153
|
+
6. **Attribute types** — values validated against type definitions
|
|
1154
|
+
|
|
1155
|
+
Empty `mj-image` and `mj-font` elements are valid in v5; Ruby-only required
|
|
1156
|
+
attribute checks have been removed. Enabled `mj-include` elements still need a
|
|
1157
|
+
`path` for the parser to resolve the file.
|
|
1158
|
+
|
|
1159
|
+
**Attribute type definitions:**
|
|
1160
|
+
|
|
1161
|
+
| Type | Example Values |
|
|
1162
|
+
|------|---------------|
|
|
1163
|
+
| `string` | Any string |
|
|
1164
|
+
| `boolean` | `"true"`, `"false"` |
|
|
1165
|
+
| `integer` | `"42"` |
|
|
1166
|
+
| `color` | `#fff`, `#ff0000`, `rgb(0,0,0)`, `hsl(0,0%,0%)`, `red` |
|
|
1167
|
+
| `enum(a,b,c)` | One of the listed values |
|
|
1168
|
+
| `unit(px)` | `"13px"` |
|
|
1169
|
+
| `unit(px,%)` | `"13px"`, `"50%"` |
|
|
1170
|
+
| `unit(px,%){1,4}` | `"10px"`, `"10px 20px"`, `"10px 20px 30px 40px"` |
|
|
1171
|
+
| `unitWithNegative(px,em)` | `"-1px"`, `"0.5em"` |
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
## Custom Components
|
|
1176
|
+
|
|
1177
|
+
### Define a Component
|
|
1178
|
+
|
|
1179
|
+
```ruby
|
|
1180
|
+
class MjBadge < MjmlRb::Components::Base
|
|
1181
|
+
TAGS = ["mj-badge"].freeze
|
|
1182
|
+
|
|
1183
|
+
ALLOWED_ATTRIBUTES = {
|
|
1184
|
+
"text" => "string",
|
|
1185
|
+
"background-color" => "color",
|
|
1186
|
+
"color" => "color",
|
|
1187
|
+
"border-radius" => "unit(px)",
|
|
1188
|
+
"padding" => "unit(px,%){1,4}"
|
|
1189
|
+
}.freeze
|
|
1190
|
+
|
|
1191
|
+
DEFAULT_ATTRIBUTES = {
|
|
1192
|
+
"background-color" => "#e74c3c",
|
|
1193
|
+
"color" => "#ffffff",
|
|
1194
|
+
"border-radius" => "4px",
|
|
1195
|
+
"padding" => "4px 8px"
|
|
1196
|
+
}.freeze
|
|
1197
|
+
|
|
1198
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
1199
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
1200
|
+
style = style_join(
|
|
1201
|
+
"background-color" => a["background-color"],
|
|
1202
|
+
"color" => a["color"],
|
|
1203
|
+
"border-radius" => a["border-radius"],
|
|
1204
|
+
"padding" => a["padding"],
|
|
1205
|
+
"display" => "inline-block",
|
|
1206
|
+
"font-size" => "12px"
|
|
1207
|
+
)
|
|
1208
|
+
text = a["text"] || raw_inner(node)
|
|
1209
|
+
%(<tr><td><span style="#{style}">#{escape_html(text)}</span></td></tr>)
|
|
1210
|
+
end
|
|
1211
|
+
end
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### Register
|
|
1215
|
+
|
|
1216
|
+
```ruby
|
|
1217
|
+
MjmlRb.register_component(
|
|
1218
|
+
MjBadge,
|
|
1219
|
+
dependencies: { "mj-column" => ["mj-badge"] },
|
|
1220
|
+
ending_tags: [] # set to ["mj-badge"] if inner HTML should be preserved raw
|
|
1221
|
+
)
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
### Use
|
|
1225
|
+
|
|
1226
|
+
```xml
|
|
1227
|
+
<mjml>
|
|
1228
|
+
<mj-body>
|
|
1229
|
+
<mj-section>
|
|
1230
|
+
<mj-column>
|
|
1231
|
+
<mj-badge text="NEW" background-color="#27ae60" />
|
|
1232
|
+
</mj-column>
|
|
1233
|
+
</mj-section>
|
|
1234
|
+
</mj-body>
|
|
1235
|
+
</mjml>
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### Available Helper Methods
|
|
1239
|
+
|
|
1240
|
+
| Method | Description |
|
|
1241
|
+
|--------|-------------|
|
|
1242
|
+
| `render_node(node, context, parent:)` | Render a single child node |
|
|
1243
|
+
| `render_children(node, context, parent:)` | Render all children |
|
|
1244
|
+
| `resolved_attributes(node, context)` | Get merged attributes (defaults + classes + inline) |
|
|
1245
|
+
| `raw_inner(node)` | Get raw inner HTML (for ending-tag components) |
|
|
1246
|
+
| `html_inner(node)` | Get HTML-escaped inner content |
|
|
1247
|
+
| `style_join(hash)` | Build CSS style string from hash (skips nil values) |
|
|
1248
|
+
| `escape_html(value)` | HTML-escape a string |
|
|
1249
|
+
| `escape_attr(value)` | Attribute-escape a string |
|
|
1250
|
+
| `html_attrs(hash)` | Build HTML attribute string (skips nil values) |
|
|
1251
|
+
| `shorthand_value(parts, side)` | Parse CSS shorthand (from CssHelpers) |
|
|
1252
|
+
| `parse_border_width(str)` | Extract border width in px (from CssHelpers) |
|
|
1253
|
+
|
|
1254
|
+
### Reset Custom Components
|
|
1255
|
+
|
|
1256
|
+
```ruby
|
|
1257
|
+
MjmlRb.component_registry.reset!
|
|
1258
|
+
```
|