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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +60 -22
  4. data/THIRD_PARTY_LICENSES.md +25 -0
  5. data/docs/ARCHITECTURE.md +363 -0
  6. data/docs/MIGRATING_TO_V5.md +112 -0
  7. data/docs/MJML_V5_SUPPORT_PLAN.md +325 -0
  8. data/docs/PARITY_AUDIT.md +124 -0
  9. data/docs/RELEASING.md +35 -0
  10. data/docs/USAGE.md +1258 -0
  11. data/docs/releases/v0.6.0.md +63 -0
  12. data/lib/mjml-rb/cli.rb +43 -31
  13. data/lib/mjml-rb/column_gutter.rb +81 -0
  14. data/lib/mjml-rb/compiler.rb +32 -122
  15. data/lib/mjml-rb/component_registry.rb +11 -1
  16. data/lib/mjml-rb/components/accordion.rb +11 -9
  17. data/lib/mjml-rb/components/base.rb +1 -1
  18. data/lib/mjml-rb/components/body.rb +6 -3
  19. data/lib/mjml-rb/components/carousel.rb +2 -2
  20. data/lib/mjml-rb/components/carousel_image.rb +2 -2
  21. data/lib/mjml-rb/components/column.rb +16 -11
  22. data/lib/mjml-rb/components/group.rb +13 -4
  23. data/lib/mjml-rb/components/hero.rb +7 -2
  24. data/lib/mjml-rb/components/image.rb +1 -3
  25. data/lib/mjml-rb/components/raw.rb +1 -1
  26. data/lib/mjml-rb/components/section.rb +23 -8
  27. data/lib/mjml-rb/components/social.rb +18 -27
  28. data/lib/mjml-rb/components/table.rb +1 -1
  29. data/lib/mjml-rb/config_file.rb +25 -15
  30. data/lib/mjml-rb/dependencies.rb +1 -0
  31. data/lib/mjml-rb/formatter.rb +113 -0
  32. data/lib/mjml-rb/node_compat.rb +4 -0
  33. data/lib/mjml-rb/options.rb +81 -0
  34. data/lib/mjml-rb/parser.rb +138 -94
  35. data/lib/mjml-rb/renderer.rb +50 -76
  36. data/lib/mjml-rb/result.rb +3 -2
  37. data/lib/mjml-rb/validator.rb +14 -33
  38. data/lib/mjml-rb/version.rb +2 -1
  39. data/lib/mjml-rb.rb +2 -2
  40. data/mjml-rb.gemspec +2 -2
  41. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffb73c167fce50a47b0513aa071f478625bd28de94a47904980746905f731e8e
4
- data.tar.gz: 67977b5a7c57afb011f6e1f7f1b1de4d1872cca1847eb0f61c6413a60b93c919
3
+ metadata.gz: 8b17ed30bd6634734e144e636e50a7a940cb074a355b85d0021bf60d4cf12232
4
+ data.tar.gz: d6c58cb0e3b7e5d23315eab2fa71cbbfc7598ac6d1acbaf9b3eb1abace8473c2
5
5
  SHA512:
6
- metadata.gz: 1eb9c9b43804484c8c0a5e89e04c83f85bcd0699ada32f6e77b566341795372386bac5fc41766bdc98d6f2844be69e6c456b461c9649971f42a4dbc97016226e
7
- data.tar.gz: d49c215799a867e70ced384893081933c596ea65cc2e325782110c796f8f690b410f5a24cb1858e91c3699d635893a3ce273154cd12900575331899c108af54b
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
- > **Note:** This is an unofficial Ruby port of the MJML email framework,
6
- > not affiliated with or endorsed by the MJML team.
7
- > All 22 MJML v4 components are implemented and tested against npm MJML v4.18.0.
8
- > Output may differ from the npm renderer in cosmetic ways (whitespace, attribute ordering)
9
- > but renders identically across email clients.
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
- `mjml-rb` requires Ruby 3.3 or newer.
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
- ```erb
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, <%= @user.name %>!</mj-text>
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
- Supports Slim and Haml via `config.mjml_rb.rails_template_language = :slim`. See the [Usage Guide — Rails section](docs/USAGE.md#rails-integration) for full configuration.
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
- Remaining parity work is tracked in [npm ↔ Ruby Parity Audit](docs/PARITY_AUDIT.md).
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
- MIT
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.