mjml-rb 0.5.2 → 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/base.rb +1 -1
- data/lib/mjml-rb/components/body.rb +6 -3
- 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/hero.rb +7 -2
- 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 +18 -27
- data/lib/mjml-rb/components/table.rb +1 -1
- 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 +4 -0
- data/lib/mjml-rb/options.rb +81 -0
- data/lib/mjml-rb/parser.rb +138 -94
- data/lib/mjml-rb/renderer.rb +50 -76
- data/lib/mjml-rb/result.rb +3 -2
- data/lib/mjml-rb/validator.rb +14 -33
- data/lib/mjml-rb/version.rb +2 -1
- data/lib/mjml-rb.rb +2 -2
- data/mjml-rb.gemspec +2 -2
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b17ed30bd6634734e144e636e50a7a940cb074a355b85d0021bf60d4cf12232
|
|
4
|
+
data.tar.gz: d6c58cb0e3b7e5d23315eab2fa71cbbfc7598ac6d1acbaf9b3eb1abace8473c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7240ddaa993999e89a757206971805019f40b0092db716a355c9b41aa711dbb4ae49808ac3eb498123c39844762483d8f3680271f9f836dc5fccee231c56d9dd
|
|
7
|
+
data.tar.gz: 80adca7028ef0dafb1d91b5b13b2a9f420bbb4a1c5922d3305249f07f4691438e29f73664387dbfe6f89bfcbebba119aaea98c7f4eb3d5918ee748b60f1b0489
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
Target MJML 5.4.1 with a development-only pinned reference compiler. Runtime
|
|
6
|
+
compilation remains pure Ruby and synchronous.
|
|
7
|
+
|
|
8
|
+
Requires Ruby >= 3.3, `css_parser` >= 1.17 and < 4, and Nokogiri >= 1.13.
|
|
9
|
+
|
|
10
|
+
- Support `css_parser` 3.x alongside 1.x and 2.x, with all three dependency
|
|
11
|
+
lines in the Ruby 3.3, 3.4 and 4.0 CI matrix.
|
|
12
|
+
- Disable includes by default; add canonical root allowlists and structured
|
|
13
|
+
include diagnostics. Track recursive dependencies in watch mode.
|
|
14
|
+
- Move body id/class and preview ownership to `mj-body`; add responsive section
|
|
15
|
+
gutters, social icon borders, string radii and v5 hero/accordion behavior.
|
|
16
|
+
Body-targeted CSS can change the background below short emails in browser
|
|
17
|
+
previews; update selectors that previously targeted the inner article.
|
|
18
|
+
- Preserve Outlook conditionals and raw/template whitespace during formatting;
|
|
19
|
+
add CSS template validation and configurable CSS compaction.
|
|
20
|
+
- Accept upstream option aliases, opt-in `.mjmlconfig` options, and explicit
|
|
21
|
+
unsupported-option errors. Make CLI validation silent and render-free.
|
|
22
|
+
- Fix default column widths, raw siblings, social ending content, image width,
|
|
23
|
+
CSS include order and shared component registration metadata.
|
|
24
|
+
- Include release and usage documentation in the gem so relative README links
|
|
25
|
+
work after installation.
|
|
26
|
+
|
|
27
|
+
See [the release notes](docs/releases/v0.6.0.md),
|
|
28
|
+
[the migration guide](docs/MIGRATING_TO_V5.md) for breaking changes and
|
|
29
|
+
[the compatibility audit](docs/PARITY_AUDIT.md) for remaining differences.
|
data/README.md
CHANGED
|
@@ -2,33 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
# MJML Ruby Implementation
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
> We recommend testing your templates when migrating from the npm renderer.
|
|
11
|
-
> This is a **fully open source project** — feedback, bug reports, test cases,
|
|
12
|
-
> and pull requests are welcome!
|
|
13
|
-
|
|
14
|
-
A pure-Ruby MJML v4 compiler — no Node.js required.
|
|
15
|
-
|
|
16
|
-
- Library API compatible with `mjml2html`
|
|
5
|
+
`mjml-rb` **0.6.0** is a pure-Ruby compiler targeting **MJML 5.4.1**.
|
|
6
|
+
The gem version and the upstream MJML version are separate. Node.js/npm is used
|
|
7
|
+
only for the development reference checks; compilation requires neither.
|
|
8
|
+
|
|
9
|
+
- Synchronous `mjml2html` library API returning HTML, errors and warnings
|
|
17
10
|
- Command-line interface (`mjml`)
|
|
18
11
|
- Rails integration (ActionView template handler for `.mjml` views)
|
|
19
12
|
- Validation (soft, strict, skip)
|
|
20
13
|
- Custom component support
|
|
21
14
|
- Pure Ruby parser, AST, validator, and renderer
|
|
15
|
+
- CSS inlining tested with `css_parser` 1.21, 2.x and 3.x
|
|
22
16
|
|
|
23
17
|
**[Full Usage Guide](docs/USAGE.md)** — API reference, all compiler options, component attribute tables, CLI flags, Rails setup, custom components, and more.
|
|
24
18
|
|
|
19
|
+
See the [0.6.0 release notes](docs/releases/v0.6.0.md) and [changelog](CHANGELOG.md).
|
|
20
|
+
|
|
25
21
|
## Installation
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
Requires Ruby **>= 3.3**, `css_parser` **>= 1.17, < 4**, and Nokogiri **>= 1.13**.
|
|
24
|
+
CI covers Ruby 3.3, 3.4 and 4.0 with `css_parser` 1.21, 2.x and 3.x.
|
|
28
25
|
|
|
29
26
|
```ruby
|
|
30
27
|
# Gemfile
|
|
31
|
-
gem "mjml-rb"
|
|
28
|
+
gem "mjml-rb", "~> 0.6.0"
|
|
32
29
|
```
|
|
33
30
|
|
|
34
31
|
## Quick Start
|
|
@@ -50,6 +47,7 @@ MJML
|
|
|
50
47
|
|
|
51
48
|
puts result[:html] # compiled HTML
|
|
52
49
|
puts result[:errors] # validation errors (if any)
|
|
50
|
+
puts result[:warnings] # non-blocking issues in soft validation
|
|
53
51
|
```
|
|
54
52
|
|
|
55
53
|
## CLI
|
|
@@ -63,17 +61,43 @@ mjml -i -s < email.mjml # stdin → stdout
|
|
|
63
61
|
|
|
64
62
|
See the [Usage Guide — CLI section](docs/USAGE.md#cli) for all flags and config options.
|
|
65
63
|
|
|
64
|
+
## Upgrading from mjml-rb 0.5.x (MJML v4)
|
|
65
|
+
|
|
66
|
+
The 0.6.x line uses v5 defaults. `mj-include` is silently omitted by default.
|
|
67
|
+
Enable local includes explicitly and supply the template's directory:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
MjmlRb.mjml2html(source,
|
|
71
|
+
ignore_includes: false,
|
|
72
|
+
file_path: File.dirname(template_path),
|
|
73
|
+
actual_path: template_path
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For the CLI, use `mjml email.mjml --config.allowIncludes -o email.html`.
|
|
78
|
+
Includes stay within the template's canonical root and any explicit `include_path`
|
|
79
|
+
roots; absolute paths and escapes are denied.
|
|
80
|
+
|
|
81
|
+
`mj-body` now owns the HTML `<body>`, including its `id` and `css-class`.
|
|
82
|
+
Update CSS selectors that expected the class on the inner article. Background
|
|
83
|
+
painting below short emails can consequently change in browser previews.
|
|
84
|
+
`minify` takes precedence over `beautify`; raw whitespace and Outlook conditionals
|
|
85
|
+
are preserved, but formatted output can differ from earlier releases.
|
|
86
|
+
|
|
87
|
+
See [the migration guide](docs/MIGRATING_TO_V5.md) for the breaking changes and
|
|
88
|
+
[the compatibility audit](docs/PARITY_AUDIT.md) for verified scope and limitations.
|
|
89
|
+
|
|
66
90
|
## Rails
|
|
67
91
|
|
|
68
92
|
Add the gem to your Gemfile — that's it. The `.mjml` template handler is registered automatically.
|
|
69
93
|
|
|
70
|
-
```
|
|
94
|
+
```xml
|
|
71
95
|
<!-- app/views/user_mailer/welcome.html.mjml -->
|
|
72
96
|
<mjml>
|
|
73
97
|
<mj-body>
|
|
74
98
|
<mj-section>
|
|
75
99
|
<mj-column>
|
|
76
|
-
<mj-text>Welcome
|
|
100
|
+
<mj-text>Welcome to our service!</mj-text>
|
|
77
101
|
</mj-column>
|
|
78
102
|
</mj-section>
|
|
79
103
|
</mj-body>
|
|
@@ -83,28 +107,42 @@ Add the gem to your Gemfile — that's it. The `.mjml` template handler is regis
|
|
|
83
107
|
```ruby
|
|
84
108
|
class UserMailer < ApplicationMailer
|
|
85
109
|
def welcome(user)
|
|
86
|
-
@user = user
|
|
87
110
|
mail(to: user.email, subject: "Welcome")
|
|
88
111
|
end
|
|
89
112
|
end
|
|
90
113
|
```
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
Raw XML `.mjml` templates are compiled as supplied; embedded ERB is not evaluated.
|
|
116
|
+
For dynamic templates, load a Slim or Haml ActionView handler and set
|
|
117
|
+
`config.mjml_rb.rails_template_language = :slim` (or `:haml`). See the
|
|
118
|
+
[Usage Guide — Rails section](docs/USAGE.md#rails-integration) for examples.
|
|
93
119
|
|
|
94
120
|
## Architecture
|
|
95
121
|
|
|
96
122
|
```
|
|
97
|
-
MJML string → Parser → AST → Validator → Renderer → HTML
|
|
123
|
+
MJML string → Parser → AST → Validator → Renderer → Formatter → HTML
|
|
98
124
|
```
|
|
99
125
|
|
|
100
|
-
1. **Parser** — normalizes source, expands `mj-include`, produces a Nokogiri XML node tree
|
|
126
|
+
1. **Parser** — normalizes source, expands explicitly enabled `mj-include`, produces a Nokogiri XML node tree
|
|
101
127
|
2. **Validator** — checks structure, hierarchy, and attribute types
|
|
102
128
|
3. **Renderer** — resolves head metadata, applies defaults, emits responsive HTML
|
|
129
|
+
4. **Formatter** — applies the selected formatting and CSS template checks
|
|
103
130
|
|
|
104
131
|
For the full internal walkthrough, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
105
132
|
|
|
106
|
-
|
|
133
|
+
The [compatibility audit](docs/PARITY_AUDIT.md) records pinned upstream checks and
|
|
134
|
+
maintainer-reported downstream Chrome comparisons. These are representative
|
|
135
|
+
browser renders, not certification across email clients. Customer screenshots
|
|
136
|
+
remain private; public examples use synthetic content.
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
This is a fully open source project — feedback, bug reports, test cases, and pull requests are welcome! Feel free to use it in your projects and let me know how it goes.
|
|
141
|
+
|
|
142
|
+
## Disclaimer
|
|
143
|
+
|
|
144
|
+
This is an unofficial Ruby port of the MJML email framework, not affiliated with or endorsed by the MJML team. Compatibility is checked against the pinned `mjml@5.4.1` compiler. This port remains experimental: the parser, CSS inliner and formatter have documented differences, and identical rendering across all email clients is not guaranteed.
|
|
107
145
|
|
|
108
146
|
## License
|
|
109
147
|
|
|
110
|
-
|
|
148
|
+
See [LICENSE](LICENSE) and [third-party notices](THIRD_PARTY_LICENSES.md).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# MJML
|
|
2
|
+
|
|
3
|
+
The v5 gutter arithmetic and reference cases are adapted from MJML.
|
|
4
|
+
|
|
5
|
+
The MIT License (MIT)
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2016 Mailjet SAS, https://mjml.io
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`mjml-rb` is a pure Ruby implementation of the MJML compilation pipeline. The
|
|
6
|
+
project does not shell out to the Node.js MJML renderer. Instead it parses MJML
|
|
7
|
+
into an internal AST, validates that AST, renders HTML through Ruby component
|
|
8
|
+
classes, and applies a small amount of post-processing before returning the
|
|
9
|
+
result.
|
|
10
|
+
|
|
11
|
+
At a high level the system is organized into five layers:
|
|
12
|
+
|
|
13
|
+
1. Public entry points: library API and CLI.
|
|
14
|
+
2. Parsing: XML normalization, preprocessors, include expansion, AST creation.
|
|
15
|
+
3. Validation: document structure, allowed children, supported attributes, and
|
|
16
|
+
basic attribute type checks.
|
|
17
|
+
4. Rendering: head/body context assembly plus component-specific HTML output.
|
|
18
|
+
5. Result packaging: final HTML string plus structured errors and warnings.
|
|
19
|
+
|
|
20
|
+
## Public Entry Points
|
|
21
|
+
|
|
22
|
+
### Library API
|
|
23
|
+
|
|
24
|
+
The gem entry point is `lib/mjml-rb.rb`.
|
|
25
|
+
|
|
26
|
+
- `MjmlRb.mjml2html(mjml, options = {})`
|
|
27
|
+
- Main public API.
|
|
28
|
+
- Instantiates `MjmlRb::Compiler`.
|
|
29
|
+
- Returns a hash with `:html`, `:errors`, and `:warnings`.
|
|
30
|
+
- `MjmlRb.to_html`
|
|
31
|
+
- Alias of `mjml2html`.
|
|
32
|
+
|
|
33
|
+
Internally the compiler works with `MjmlRb::Result`, but the top-level module
|
|
34
|
+
returns `Result#to_h` for compatibility with the expected `mjml2html` shape.
|
|
35
|
+
|
|
36
|
+
### CLI
|
|
37
|
+
|
|
38
|
+
`bin/mjml` delegates to `MjmlRb::CLI`, which supports three main modes:
|
|
39
|
+
|
|
40
|
+
- Read/compile files.
|
|
41
|
+
- Validate files.
|
|
42
|
+
- Watch files and recompile them on change.
|
|
43
|
+
|
|
44
|
+
The CLI accepts both explicit mode flags and stdin/stdout usage. Watch mode is
|
|
45
|
+
implemented as a polling loop based on file modification times rather than OS
|
|
46
|
+
file notifications.
|
|
47
|
+
|
|
48
|
+
## Core Compile Flow
|
|
49
|
+
|
|
50
|
+
`MjmlRb::Compiler` is the orchestrator. `Compiler#compile` follows this flow:
|
|
51
|
+
|
|
52
|
+
1. Canonicalize aliases with `Options`, then merge defaults, opted-in config, constructor and call options.
|
|
53
|
+
2. Create a per-call parser and a `ParseResult` containing AST, diagnostics and include dependencies.
|
|
54
|
+
3. Run validation unless `validation_level` is `skip`.
|
|
55
|
+
4. Stop early when strict validation fails.
|
|
56
|
+
5. Render HTML from the AST.
|
|
57
|
+
6. Run the conservative `Formatter`; source comments were already handled by parsing.
|
|
58
|
+
7. Return a `Result`.
|
|
59
|
+
|
|
60
|
+
Default compiler options live in `MjmlRb::Compiler::DEFAULT_OPTIONS`. Important
|
|
61
|
+
ones:
|
|
62
|
+
|
|
63
|
+
- `validation_level`: `soft`, `strict`, or `skip`.
|
|
64
|
+
- `keep_comments`: preserve comment nodes through parsing and rendering.
|
|
65
|
+
- `ignore_includes`: defaults to true; omitted includes disappear before validation.
|
|
66
|
+
- `include_path`: extra canonical allowed roots for explicitly enabled includes.
|
|
67
|
+
- `preprocessors`: callable objects applied to the raw source before parsing.
|
|
68
|
+
- `file_path` and `actual_path`: used for include resolution.
|
|
69
|
+
|
|
70
|
+
Error handling is intentionally forgiving:
|
|
71
|
+
|
|
72
|
+
- Parse errors are wrapped as structured errors in the result.
|
|
73
|
+
- Unexpected runtime errors are also converted into structured errors.
|
|
74
|
+
- Soft validation and include issues populate `warnings`; strict issues populate `errors`.
|
|
75
|
+
|
|
76
|
+
## Parsing Layer
|
|
77
|
+
|
|
78
|
+
`MjmlRb::Parser` converts MJML source into a Nokogiri XML node tree.
|
|
79
|
+
|
|
80
|
+
### Pre-parse normalization
|
|
81
|
+
|
|
82
|
+
Before building the XML document, the parser performs several cleanup steps:
|
|
83
|
+
|
|
84
|
+
- Runs user-provided preprocessors.
|
|
85
|
+
- Wraps registered ending-tag content in CDATA; structural exceptions are `mj-table` and `mj-carousel-image`.
|
|
86
|
+
- Normalizes HTML void tags so XML parsing can succeed.
|
|
87
|
+
- Expands `mj-include` nodes unless includes are disabled.
|
|
88
|
+
- Escapes bare `&` only outside protected CDATA.
|
|
89
|
+
|
|
90
|
+
These steps make the parser more tolerant of email-template input that is valid
|
|
91
|
+
for MJML or HTML but not strict XML.
|
|
92
|
+
|
|
93
|
+
### Include expansion
|
|
94
|
+
|
|
95
|
+
`mj-include` is expanded recursively before AST conversion.
|
|
96
|
+
|
|
97
|
+
- `type="html"` includes are wrapped into `mj-raw`.
|
|
98
|
+
- MJML includes are inserted as parsed XML fragments.
|
|
99
|
+
- Paths resolve against `file_path`, updated to the including directory during recursion.
|
|
100
|
+
- The original realpath root plus explicit `include_path` roots remain fixed.
|
|
101
|
+
- Canonical containment rejects traversal and symlink escapes before reading.
|
|
102
|
+
- CSS includes in the head are replaced in place.
|
|
103
|
+
- `ParseResult` carries file, line, tag, diagnostic type and dependencies.
|
|
104
|
+
|
|
105
|
+
Include expansion is implemented inside the parser rather than in the renderer,
|
|
106
|
+
so downstream stages always operate on a single expanded document tree.
|
|
107
|
+
|
|
108
|
+
### Node tree
|
|
109
|
+
|
|
110
|
+
The parser returns the root `Nokogiri::XML::Node` directly. A small
|
|
111
|
+
`MjmlRb::NodeCompat` module is prepended to `Nokogiri::XML::Node` to add
|
|
112
|
+
convenience methods (`tag_name` as an alias for `name`, and `file` to read
|
|
113
|
+
the `data-mjml-file` annotation).
|
|
114
|
+
|
|
115
|
+
- Element nodes are `Nokogiri::XML::Element` instances.
|
|
116
|
+
- Text nodes respond to `text?` and store their content in `content`.
|
|
117
|
+
- Comment nodes respond to `comment?`.
|
|
118
|
+
- Ending-tag components (e.g. `mj-text`, `mj-button`) are marked with a
|
|
119
|
+
`data-mjml-ending-tag` attribute; their raw HTML is preserved as CDATA children.
|
|
120
|
+
|
|
121
|
+
Component behavior is not embedded in the node type; it is provided later by
|
|
122
|
+
the renderer and validator.
|
|
123
|
+
|
|
124
|
+
## Validation Layer
|
|
125
|
+
|
|
126
|
+
`MjmlRb::Validator` validates either raw MJML or a prebuilt AST.
|
|
127
|
+
|
|
128
|
+
### What it validates today
|
|
129
|
+
|
|
130
|
+
- Root element must be `mjml`.
|
|
131
|
+
- A document must contain `mj-body`.
|
|
132
|
+
- Child placement must match `MjmlRb::Dependencies::RULES`.
|
|
133
|
+
- Empty `mj-image` and `mj-font` are accepted, matching the v5 validator.
|
|
134
|
+
- Component-declared attributes are checked for support and basic type validity.
|
|
135
|
+
|
|
136
|
+
Type validation currently covers:
|
|
137
|
+
|
|
138
|
+
- strings
|
|
139
|
+
- colors
|
|
140
|
+
- enums
|
|
141
|
+
- unit-based values such as `px` and `%`
|
|
142
|
+
|
|
143
|
+
### How attribute validation works
|
|
144
|
+
|
|
145
|
+
The validator discovers supported attributes from component classes under
|
|
146
|
+
`MjmlRb::Components`. Most components declare `ALLOWED_ATTRIBUTES`, and some
|
|
147
|
+
provide `allowed_attributes_for(tag_name)` when multiple tags share the same
|
|
148
|
+
class.
|
|
149
|
+
|
|
150
|
+
This means attribute support is mostly decentralized:
|
|
151
|
+
|
|
152
|
+
- structure rules live in `lib/mjml-rb/dependencies.rb`
|
|
153
|
+
- attribute rules live in component classes
|
|
154
|
+
- validation logic lives in `lib/mjml-rb/validator.rb`
|
|
155
|
+
|
|
156
|
+
That split keeps component behavior close to its validation metadata, but it
|
|
157
|
+
also means any new component must be wired into more than one place.
|
|
158
|
+
|
|
159
|
+
## Rendering Layer
|
|
160
|
+
|
|
161
|
+
`MjmlRb::Renderer` turns the AST into the final HTML document. This is the
|
|
162
|
+
largest part of the system.
|
|
163
|
+
|
|
164
|
+
### Render entry
|
|
165
|
+
|
|
166
|
+
`Renderer#render(document, options)`:
|
|
167
|
+
|
|
168
|
+
1. Finds `mj-head` and `mj-body`.
|
|
169
|
+
2. Builds a rendering context from head metadata and options.
|
|
170
|
+
3. Collects component-provided head CSS.
|
|
171
|
+
4. Renders the body tree.
|
|
172
|
+
5. Appends responsive column-width CSS.
|
|
173
|
+
6. Builds the final HTML document string.
|
|
174
|
+
7. Applies HTML-attribute and inline-style transformations with Nokogiri.
|
|
175
|
+
|
|
176
|
+
### Rendering context
|
|
177
|
+
|
|
178
|
+
The renderer passes a mutable `context` hash through component rendering. It is
|
|
179
|
+
the main shared state mechanism in the project.
|
|
180
|
+
|
|
181
|
+
Important context keys include:
|
|
182
|
+
|
|
183
|
+
- `:title`, `:preview`
|
|
184
|
+
- `:breakpoint`
|
|
185
|
+
- `:head_raw`, `:head_styles`, `:inline_styles`
|
|
186
|
+
- `:html_attributes`
|
|
187
|
+
- `:fonts`
|
|
188
|
+
- `:global_defaults`, `:tag_defaults`
|
|
189
|
+
- `:classes`, `:classes_default`
|
|
190
|
+
- `:background_color`, `:container_width`
|
|
191
|
+
- `:column_widths`
|
|
192
|
+
- `:inherited_mj_class`
|
|
193
|
+
|
|
194
|
+
This design keeps the component API small, but it also means correctness
|
|
195
|
+
depends on components mutating shared state in a disciplined way.
|
|
196
|
+
|
|
197
|
+
### Head processing
|
|
198
|
+
|
|
199
|
+
Head nodes are not rendered into HTML directly. Instead supported head
|
|
200
|
+
components mutate the context through `handle_head`.
|
|
201
|
+
|
|
202
|
+
Current head-related responsibilities:
|
|
203
|
+
|
|
204
|
+
- `mj-title` sets document title.
|
|
205
|
+
- `mj-preview` sets preview text.
|
|
206
|
+
- `mj-style` contributes either head CSS or inline CSS rules.
|
|
207
|
+
- `mj-font` registers font URLs.
|
|
208
|
+
- `mj-breakpoint` controls responsive breakpoint CSS.
|
|
209
|
+
- `mj-attributes` builds default attribute maps and class maps.
|
|
210
|
+
- `mj-html-attributes` registers selector-to-attribute rules.
|
|
211
|
+
- `mj-raw` can inject raw markup into `<head>` or before the doctype.
|
|
212
|
+
|
|
213
|
+
### Component registry
|
|
214
|
+
|
|
215
|
+
Most renderable tags are implemented as subclasses of `MjmlRb::Components::Base`
|
|
216
|
+
and registered explicitly in `Renderer#component_registry`.
|
|
217
|
+
|
|
218
|
+
Currently implemented as component classes:
|
|
219
|
+
|
|
220
|
+
- `mj-body`
|
|
221
|
+
- `mj-head`, `mj-title`, `mj-preview`, `mj-style`, `mj-font`
|
|
222
|
+
- `mj-attributes`, `mj-all`, `mj-class`
|
|
223
|
+
- `mj-breakpoint`
|
|
224
|
+
- `mj-accordion`, `mj-accordion-element`, `mj-accordion-title`,
|
|
225
|
+
`mj-accordion-text`
|
|
226
|
+
- `mj-button`
|
|
227
|
+
- `mj-hero`
|
|
228
|
+
- `mj-image`
|
|
229
|
+
- `mj-navbar`, `mj-navbar-link`
|
|
230
|
+
- `mj-raw`
|
|
231
|
+
- `mj-text`
|
|
232
|
+
- `mj-divider`
|
|
233
|
+
- `mj-html-attributes`, `mj-selector`, `mj-html-attribute`
|
|
234
|
+
- `mj-table`
|
|
235
|
+
- `mj-social`, `mj-social-element`
|
|
236
|
+
- `mj-section`, `mj-wrapper`
|
|
237
|
+
- `mj-group`
|
|
238
|
+
- `mj-column`
|
|
239
|
+
- `mj-spacer`
|
|
240
|
+
|
|
241
|
+
### Component responsibilities
|
|
242
|
+
|
|
243
|
+
Components are responsible for HTML generation for their tags. The base class
|
|
244
|
+
exposes shared renderer helpers such as:
|
|
245
|
+
|
|
246
|
+
- child rendering
|
|
247
|
+
- attribute resolution
|
|
248
|
+
- HTML escaping
|
|
249
|
+
- raw-inner vs HTML-inner extraction
|
|
250
|
+
- style merging helpers
|
|
251
|
+
|
|
252
|
+
Some components also participate in head behavior by exposing:
|
|
253
|
+
|
|
254
|
+
- `handle_head`
|
|
255
|
+
- `head_style`
|
|
256
|
+
- `head_style_tags`
|
|
257
|
+
|
|
258
|
+
### Attribute resolution order
|
|
259
|
+
|
|
260
|
+
Resolved attributes for a node are built in this order:
|
|
261
|
+
|
|
262
|
+
1. global defaults from `mj-all`
|
|
263
|
+
2. per-tag defaults from `mj-attributes`
|
|
264
|
+
3. direct `mj-class` attributes
|
|
265
|
+
4. inherited descendant defaults from parent `mj-class`
|
|
266
|
+
5. node attributes from the MJML source
|
|
267
|
+
|
|
268
|
+
This order is implemented in `Renderer#resolved_attributes`.
|
|
269
|
+
|
|
270
|
+
### Document assembly
|
|
271
|
+
|
|
272
|
+
After body rendering, the renderer assembles:
|
|
273
|
+
|
|
274
|
+
- doctype
|
|
275
|
+
- `<html>` with `lang` and `dir`
|
|
276
|
+
- font `<link>` tags and `@import` CSS for actually used fonts
|
|
277
|
+
- a reset stylesheet plus component CSS
|
|
278
|
+
- the complete body returned by `Components::Body`, including preview and accessible article
|
|
279
|
+
|
|
280
|
+
Selector-based HTML attributes are applied to the real body before skeleton
|
|
281
|
+
assembly. Inline CSS runs on the complete document. `Formatter` protects raw
|
|
282
|
+
regions and conditionals without reserializing the entire HTML document.
|
|
283
|
+
|
|
284
|
+
This is an important architectural detail: the project is not purely
|
|
285
|
+
string-based. It renders to HTML as a string, then reparses that HTML for
|
|
286
|
+
selector-based mutations.
|
|
287
|
+
|
|
288
|
+
## Components of Note
|
|
289
|
+
|
|
290
|
+
Some behaviors are important enough to call out because they shape the rest of
|
|
291
|
+
the architecture:
|
|
292
|
+
|
|
293
|
+
- `Body`
|
|
294
|
+
- Owns `<body>`, its id/class/background, preview and accessible article; sets container width.
|
|
295
|
+
- `Section`
|
|
296
|
+
- Renders the main MJML section structure, including Outlook conditional
|
|
297
|
+
table wrappers and width calculations for columns/groups.
|
|
298
|
+
- `Column`
|
|
299
|
+
- Computes child container widths so inner components such as `mj-image` can
|
|
300
|
+
size themselves correctly.
|
|
301
|
+
- `Head`
|
|
302
|
+
- Acts as a dispatcher for supported head child tags.
|
|
303
|
+
- `Attributes`
|
|
304
|
+
- Builds the default and class-based styling maps used later by the renderer.
|
|
305
|
+
- `HtmlAttributes`
|
|
306
|
+
- Defers selector-based mutations until body rendering is complete.
|
|
307
|
+
- `Raw`
|
|
308
|
+
- Preserves raw markup and supports `position="file-start"` before the
|
|
309
|
+
doctype.
|
|
310
|
+
|
|
311
|
+
## Test Coverage Snapshot
|
|
312
|
+
|
|
313
|
+
The repository has broader test coverage than the previous version of this
|
|
314
|
+
document suggested. Current tests cover:
|
|
315
|
+
|
|
316
|
+
- compiler behavior and include expansion
|
|
317
|
+
- validation
|
|
318
|
+
- head features
|
|
319
|
+
- HTML attribute injection
|
|
320
|
+
- navbar rendering
|
|
321
|
+
- column and image sizing details
|
|
322
|
+
- `mj-all` / `mj-class` attribute precedence
|
|
323
|
+
|
|
324
|
+
Component fixtures coexist with pinned v5 differential contracts in
|
|
325
|
+
`test/reference`. CI compares these contracts against `mjml@5.4.1`, installed
|
|
326
|
+
only for development. See `docs/PARITY_AUDIT.md` for the exact coverage and limits.
|
|
327
|
+
|
|
328
|
+
## Extension Workflow
|
|
329
|
+
|
|
330
|
+
To add a new component safely, the usual steps are:
|
|
331
|
+
|
|
332
|
+
1. Create a new class under `lib/mjml-rb/components/` that inherits from
|
|
333
|
+
`MjmlRb::Components::Base`.
|
|
334
|
+
2. Declare `TAGS` and attribute metadata.
|
|
335
|
+
3. Implement `render`.
|
|
336
|
+
4. Implement `handle_head` and/or `head_style` if the component affects head
|
|
337
|
+
processing.
|
|
338
|
+
5. Register through `MjmlRb.register_component`, including custom ending tags and dependencies.
|
|
339
|
+
6. Add parent/child rules to `Dependencies::RULES`.
|
|
340
|
+
7. Add tests that cover rendering and validation behavior.
|
|
341
|
+
|
|
342
|
+
If a tag is allowed by dependency rules but has no renderer implementation, it
|
|
343
|
+
may validate successfully but still not produce correct output. That risk now
|
|
344
|
+
applies mainly to future dependency-only tags rather than the current carousel
|
|
345
|
+
implementation.
|
|
346
|
+
|
|
347
|
+
## Shared metadata and layout
|
|
348
|
+
|
|
349
|
+
`ComponentRegistry` supplies the classes, dependency rules and ending-tag set to
|
|
350
|
+
parser, validator and renderer. Its revision invalidates renderer caches when
|
|
351
|
+
components are registered or reset; collisions with another class are rejected.
|
|
352
|
+
A compiler creates per-call parser and renderer instances, so concurrent
|
|
353
|
+
compilations do not share include diagnostics or rendering context.
|
|
354
|
+
|
|
355
|
+
`ColumnGutter` calculates width reduction and edge-aware desktop/mobile/Outlook
|
|
356
|
+
padding from the section or group context. The responsive rule registry stores
|
|
357
|
+
width or padding rules; a layout value is shared by a column and its Outlook cell.
|
|
358
|
+
|
|
359
|
+
## Current limitations
|
|
360
|
+
|
|
361
|
+
See [the compatibility audit](PARITY_AUDIT.md) for the tested MJML 5.4.1 scope,
|
|
362
|
+
strict structural XML parsing, custom inliner/formatter differences, unsupported
|
|
363
|
+
options and watch limitations. Node is used only by the development oracle.
|