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.
Files changed (46) 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/attributes.rb +5 -5
  18. data/lib/mjml-rb/components/base.rb +5 -1
  19. data/lib/mjml-rb/components/body.rb +6 -3
  20. data/lib/mjml-rb/components/breakpoint.rb +1 -1
  21. data/lib/mjml-rb/components/carousel.rb +2 -2
  22. data/lib/mjml-rb/components/carousel_image.rb +2 -2
  23. data/lib/mjml-rb/components/column.rb +16 -11
  24. data/lib/mjml-rb/components/group.rb +13 -4
  25. data/lib/mjml-rb/components/head.rb +3 -3
  26. data/lib/mjml-rb/components/hero.rb +7 -2
  27. data/lib/mjml-rb/components/html_attributes.rb +3 -3
  28. data/lib/mjml-rb/components/image.rb +1 -3
  29. data/lib/mjml-rb/components/raw.rb +1 -1
  30. data/lib/mjml-rb/components/section.rb +23 -8
  31. data/lib/mjml-rb/components/social.rb +19 -28
  32. data/lib/mjml-rb/components/table.rb +2 -2
  33. data/lib/mjml-rb/config_file.rb +25 -15
  34. data/lib/mjml-rb/dependencies.rb +1 -0
  35. data/lib/mjml-rb/formatter.rb +113 -0
  36. data/lib/mjml-rb/node_compat.rb +19 -0
  37. data/lib/mjml-rb/options.rb +81 -0
  38. data/lib/mjml-rb/parser.rb +158 -131
  39. data/lib/mjml-rb/renderer.rb +84 -100
  40. data/lib/mjml-rb/result.rb +3 -2
  41. data/lib/mjml-rb/validator.rb +26 -34
  42. data/lib/mjml-rb/version.rb +2 -1
  43. data/lib/mjml-rb.rb +3 -3
  44. data/mjml-rb.gemspec +2 -2
  45. metadata +20 -2
  46. data/lib/mjml-rb/ast_node.rb +0 -37
@@ -0,0 +1,112 @@
1
+ # Migrating to MJML v5
2
+
3
+ `mjml-rb` 0.6.0 targets **MJML 5.4.1**. The 0.5.x line retains v4 behavior.
4
+ There is no v4/v5 runtime switch. The library remains synchronous and requires
5
+ no Node.js packages at runtime.
6
+
7
+ Requires Ruby >= 3.3, `css_parser` >= 1.17 and < 4, and Nokogiri >= 1.13.
8
+ The CI matrix covers `css_parser` 1.21, 2.x and 3.x on Ruby 3.3, 3.4 and 4.0.
9
+
10
+ ## Includes require opt-in
11
+
12
+ `mj-include` is now silently omitted by default, including in strict validation.
13
+ Enable it only for templates that need local files:
14
+
15
+ ```ruby
16
+ MjmlRb.mjml2html(source,
17
+ ignore_includes: false,
18
+ file_path: File.dirname(template_path),
19
+ actual_path: template_path,
20
+ include_path: ["../shared"]
21
+ )
22
+ ```
23
+
24
+ ```sh
25
+ mjml templates/email.mjml --config.allowIncludes -o email.html
26
+ mjml --stdin --filePath templates --config.allowIncludes < templates/email.mjml
27
+ ```
28
+
29
+ Paths resolve from `file_path`; CLI file inputs supply their own directory.
30
+ Nested includes resolve from the included file's directory. The original
31
+ canonical root and explicit `include_path` directories define the allowlist;
32
+ nested includes do not expand it. `include_path` accepts a string or an array,
33
+ and relative roots resolve against the original `file_path`.
34
+
35
+ Absolute paths, Windows drive paths, UNC paths, null bytes, and traversal or
36
+ symlink escapes are denied. Encoded paths are decoded before checking. Denied
37
+ includes produce `<!-- mj-include denied -->` and a structured `include-denied`
38
+ diagnostic: a warning in soft/skip mode, an error in strict mode. This is more
39
+ explicit than upstream 5.4.1, which omits a validation error for denied includes.
40
+ Missing allowed files still produce include diagnostics. Watch mode tracks
41
+ recursive enabled dependencies, including deletion, recreation and graph changes.
42
+
43
+ ## HTML and layout changes
44
+
45
+ - `mj-body` owns the HTML `<body>`. Its `id` and `css-class` are on that element;
46
+ update selectors that previously targeted the inner article div. The preview
47
+ is the first body child. ARIA, language and direction remain on the article.
48
+ - `mj-section gutter="20px"` or `gutter="4%"` adds responsive column spacing,
49
+ including edge-aware Outlook padding, RTL and non-stacking groups.
50
+ - An unspecified column gets `100 / sibling_count` percent. An explicit `30%`
51
+ column next to an unspecified column now produces `30%` and `50%`.
52
+ - `mj-social` and `mj-social-element` accept `border`; icons default to `0`.
53
+ - Border radii accept arbitrary CSS strings, including mixed units and `/`.
54
+ - `mj-hero` no longer accepts `container-background-color`. Its Outlook inner
55
+ table and child width account for outer horizontal padding; inner padding
56
+ appears in both Outlook and normal markup.
57
+ - Empty `mj-image` and `mj-font` no longer trigger Ruby-only required checks.
58
+
59
+ ### Backgrounds in browser previews
60
+
61
+ CSS applied through `mj-body`'s `css-class` now targets the actual HTML `<body>`.
62
+ Its background can paint the browser canvas below the content of a short email;
63
+ the inner article also receives the background colour set on `mj-body`.
64
+ Review both the body and any wrapper selectors when comparing v4 and v5 output.
65
+
66
+ In the maintainer's downstream Chrome comparisons, all 11 background-only
67
+ differences disappeared exactly when the browser harness set a white HTML-root
68
+ background. This was a diagnostic control applied only in the browser harness.
69
+ No footer markup change was needed. See the [validation record](PARITY_AUDIT.md#downstream-browser-validation)
70
+ for the full scope and results.
71
+
72
+ ## Formatting and source preservation
73
+
74
+ `keep_comments: false` removes source HTML comments while preserving Outlook
75
+ conditionals. Ending-tag content stays raw: bare ampersands, HTML void-tag
76
+ spelling and script operators are no longer rewritten for XML parsing.
77
+
78
+ `minify` takes precedence over `beautify`. The Ruby formatter conservatively
79
+ compacts markup and CSS while preserving raw content, preformatted elements,
80
+ script text, template tokens, VML and paired `htmlmin:ignore` regions. Ignore
81
+ markers are removed from output. Whitespace and CSS optimizations are not
82
+ byte-identical to upstream htmlnano/cssnano.
83
+
84
+ `minify_options: {minify_css: false}` disables CSS compaction. `minifyCss` and the
85
+ legacy `minifyCSS` spelling are accepted. Other htmlnano options are rejected.
86
+ Use `sanitize_styles: true` to validate template tokens in CSS. Default token
87
+ pairs are `{{ ... }}` and `[[ ... ]]`; `template_syntax` accepts an array of
88
+ `{prefix:, suffix:}` objects. Unbalanced tokens and mixed block/property/value
89
+ syntax report errors; `allow_mixed_syntax: true` enables mixed syntax.
90
+
91
+ ## Options, configuration and extensions
92
+
93
+ Snake_case, camelCase and dashed option keys are accepted. Conflicting aliases
94
+ in the same option layer raise `ArgumentError`; CLI errors return exit status 1.
95
+ Boolean options require Ruby booleans. JSON `.mjmlconfig` is preferred;
96
+ `.mjmlrc` remains a legacy alias. Config options apply only with
97
+ `use_mjml_config_options: true` (CLI `--config.useMjmlConfigOptions`). Explicit
98
+ constructor/call/CLI options win. `minify_options` merges by key.
99
+
100
+ `Compiler#validate` and CLI `--validate` parse and validate without rendering.
101
+ Valid CLI validation emits no HTML and does not write output files.
102
+ `Result#dependencies` exposes enabled include dependencies without changing the
103
+ existing `{html:, errors:, warnings:}` hash API.
104
+
105
+ Custom components use `MjmlRb.register_component`. Ending-tag metadata is shared
106
+ by parsing and validation, and later registrations are visible to existing
107
+ compilers. Registering a different class for an existing tag raises an error.
108
+
109
+ Non-empty `juice_options` and `juice_preserve_tags` are explicitly unsupported.
110
+ JavaScript `.mjmlconfig.js` and component packages, browser bundles, Node version
111
+ policy and removed `mjml-migrate` tooling are outside this Ruby port's scope.
112
+ See [the compatibility audit](PARITY_AUDIT.md) for other known differences.
@@ -0,0 +1,325 @@
1
+ # MJML v5.4 Support Plan
2
+
3
+ - Status: implemented for the 0.6.0 release; see bounded compatibility notes below
4
+ - Implementation reference: [`mjml@5.4.1`](https://github.com/mjmlio/mjml/releases/tag/v5.4.1)
5
+ - Original planning reference: `mjml@5.4.0`
6
+ - Original planning baseline: `mjml-rb` 0.5.2, documented against `mjml@4.18.0`
7
+ - Last reviewed: 2026-10-16
8
+
9
+ ## Implementation notes
10
+
11
+ The implementation advances the original 5.4.0 target to patch release 5.4.1.
12
+ The baseline and delivery sections below retain the original audit context;
13
+ for current behavior and explicit exceptions, use [the compatibility audit](PARITY_AUDIT.md)
14
+ and [migration guide](MIGRATING_TO_V5.md). CLI input-relative paths were already
15
+ fixed in 0.5.3 before this migration.
16
+
17
+ The development-only oracle is locked to 5.4.1. Ruby fixtures and focused live
18
+ reference contracts cover the v5 changes; they do not claim exhaustive email
19
+ client parity. Full htmlnano/cssnano behavior and Juice settings are deliberately
20
+ outside the implemented subset. Unsupported options raise explicitly.
21
+
22
+ Release preparation and the downstream validation summary are recorded in the
23
+ [0.6.0 release notes](releases/v0.6.0.md). The 5.4.0 targets and baseline defects
24
+ below are retained as historical planning material, not current release status.
25
+
26
+ ## Goal
27
+
28
+ Make the pure-Ruby compiler behaviorally compatible with the tagged MJML
29
+ v5.4.0 release without adding a Node.js runtime dependency.
30
+
31
+ Compatibility means:
32
+
33
+ - equivalent public compiler and CLI semantics;
34
+ - equivalent validation and include diagnostics;
35
+ - equivalent HTML structure and email-client behavior for critical markup;
36
+ - v5 security defaults, especially for `mj-include`; and
37
+ - a repeatable differential test suite against exactly `mjml@5.4.0`.
38
+
39
+ Exact byte-for-byte HTML is not a general requirement. Attribute order and
40
+ non-functional whitespace may differ, but Outlook conditionals, VML, CSS,
41
+ accessibility markup, widths, and option behavior must be equivalent.
42
+
43
+ ## Recommended release policy
44
+
45
+ - Keep `0.5.x` as the MJML v4 compatibility line.
46
+ - Ship v5 semantics in `0.6.0`, with the breaking defaults called out in the
47
+ changelog and migration guide.
48
+ - Do not add a long-lived v4/v5 runtime switch. It would duplicate parser,
49
+ validation, and rendering branches and make parity harder to prove.
50
+ - Pin `mjml@5.4.0` only in development/CI as the reference oracle. The gem
51
+ remains pure Ruby at runtime.
52
+
53
+ There are no new core component tags between v4.18.0 and v5.4.0. The migration
54
+ is primarily about security defaults, document structure, component attributes
55
+ and layout, configuration, and post-processing. The authoritative scope is the
56
+ [v4.18.0...v5.4.0 comparison](https://github.com/mjmlio/mjml/compare/v4.18.0...v5.4.0).
57
+
58
+ ## Baseline audit: fix before claiming v5 support
59
+
60
+ The current test suite passes, but differential probes against the official
61
+ compiler found the following correctness and consistency defects. Fix these
62
+ before changing golden fixtures to v5; otherwise the migration can hide
63
+ pre-existing regressions.
64
+
65
+ Baseline verification: `bundle exec rake test` completed with 589 runs, 2,284
66
+ assertions, 0 failures, 0 errors, and 1 skip.
67
+
68
+ ### Release blockers
69
+
70
+ | Priority | Location | Confirmed behavior | Required change |
71
+ |---|---|---|---|
72
+ | P1 | `lib/mjml-rb/compiler.rb:104-113` | `keep_comments: false` deletes every generated MSO/IE conditional and `OfficeDocumentSettings`, breaking Outlook layout. | Remove global comment stripping. Drop source comment nodes during parsing while always preserving generated conditionals. |
73
+ | P1 | `lib/mjml-rb/compiler.rb:8-17`, `lib/mjml-rb/parser.rb:391-403` | Includes are enabled by default and an absolute or traversing path can read any process-readable file. | Adopt the v5 disabled-by-default include policy and canonical allowlisted roots described below. |
74
+ | P1 | `lib/mjml-rb/cli.rb:189-197` | File compilation does not pass the input filename as `actual_path`/`file_path`, so a same-directory relative include is resolved against the process working directory and omitted. | Merge per-input path context into compiler options, including watch mode. |
75
+ | P1 | `lib/mjml-rb/parser.rb:84-168` | CSS includes are visited in reverse, collected, and appended to `mj-head`; this reverses include order and moves styles away from their source position. A later blue rule can incorrectly lose to an earlier red rule. | Replace each CSS include in place with its corresponding `mj-style` node and preserve source order. |
76
+ | P1 | `lib/mjml-rb/compiler.rb:115-123` | Regex minification collapses significant whitespace inside `<pre>` and raw content. | Replace it with token/HTML-aware processing that protects raw blocks, preformatted elements, template tokens, conditionals, and `htmlmin:ignore` regions. |
77
+ | P1 | `lib/mjml-rb/renderer.rb:223-248` | When one column has an explicit width, remaining width is divided among unspecified columns. Upstream gives every unspecified sibling `100 / sibling_count`; `[30%, unset]` must be `[30%, 50%]`, not `[30%, 70%]`. | Base every unspecified width on the total non-raw sibling count. Add percentage and pixel mixed-width fixtures. |
78
+ | P1 | `lib/mjml-rb/components/section.rb:511-548`, `lib/mjml-rb/components/section.rb:691-715`, `lib/mjml-rb/components/social.rb:183-233` | `mj-raw` siblings are silently dropped from sections, wrappers, and social blocks whenever a normal child exists. | Iterate original children. Apply layout wrappers only to non-raw children and render raw children directly. |
79
+ | P1 | `lib/mjml-rb/parser.rb:13-19`, `lib/mjml-rb/components/social.rb:328-354` | `mj-social-element` is not registered as an ending tag; nested HTML is validated as MJML and then escaped as text. | Centralize ending-tag metadata and render the element's raw inner HTML without escaping. |
80
+
81
+ ### Additional correctness debt
82
+
83
+ | Priority | Location | Confirmed behavior | Required change |
84
+ |---|---|---|---|
85
+ | P2 | `lib/mjml-rb/parser.rb:48-60` | With `ignore_includes: true`, the unexpanded `mj-include` remains in the AST and strict validation rejects it. Official v5 silently omits ignored includes. | Remove ignored include nodes during parsing before validation. |
86
+ | P2 | `lib/mjml-rb/components/image.rb:170-175` | An image with both explicit width and non-auto height emits `width="auto"`; upstream emits the numeric content width. | Remove the height-dependent special case and test Outlook-oriented width output. |
87
+ | P2 | `lib/mjml-rb/components/table.rb:115-129` | Comments inside `mj-table`, including MSO conditionals, are always discarded even when comments are kept. | Preserve serialized comments; source-comment policy belongs in the parser. |
88
+ | P2 | `lib/mjml-rb/components/accordion.rb:219-220`, `lib/mjml-rb/components/accordion.rb:253-254` | Accordion checkboxes and interactive icons are emitted unconditionally, while upstream hides them from Outlook with negated-MSO comments. | Restore the `<!--[if !mso | IE]><!-->` wrappers around unsupported interactive controls. |
89
+ | P2 | `lib/mjml-rb/components/accordion.rb:223-257` | `css-class` on `mj-accordion-title` is accepted but not rendered on the title cell. | Apply the class to the same element as the reference compiler. |
90
+ | P2 | `lib/mjml-rb/components/section.rb:79-83` | Padding width math recognizes only pixels even though section/wrapper schemas accept percentages. | Parse the numeric magnitude of every allowed unit and add percentage-padding width fixtures. |
91
+ | P2 | `lib/mjml-rb/validator.rb:107-110` | A known component with an empty allowed-attribute schema skips unknown-attribute validation, so strict mode accepts values such as `mj-title typo="x"`. | Distinguish an empty schema from an explicit allow-any schema. |
92
+ | P2 | `lib/mjml-rb/parser.rb:13-19`, `lib/mjml-rb/component_registry.rb:13-20` | Custom ending tags affect validation but not the parser's hard-coded CDATA wrapping. Raw text such as `A < B` then fails XML parsing. | Derive parser ending-tag handling from central registry metadata, retaining explicit structural exceptions such as `mj-table`. |
93
+ | P2 | `lib/mjml-rb/component_registry.rb:57-70`, `lib/mjml-rb/renderer.rb:628-659` | A warmed compiler does not see later component registrations, and validator/renderer precedence differs when a custom tag collides with a built-in. | Add a registry revision, invalidate renderer caches, and either reject duplicate tags or apply one documented precedence everywhere. |
94
+ | P2 | `lib/mjml-rb/parser.rb:296-303`, `lib/mjml-rb/parser.rb:355-359` | Bare-ampersand sanitization mutates ending-tag content; for example, raw script `a && b` becomes `a &amp;&amp; b`. | Sanitize only XML text/attributes, never bytes already protected as raw/CDATA content. |
95
+ | P2 | `lib/mjml-rb/compiler.rb:130-133`, `lib/mjml-rb/config_file.rb:31-34`, `lib/mjml-rb/cli.rb:51-61` | Official camelCase keys such as `keepComments` are rejected, including keys loaded from config, and the CLI does not consistently turn these failures into a controlled exit. | Add one option canonicalizer shared by library, CLI, and config loading; normalize errors to `ConfigError`/exit 1. |
96
+ | P2 | `lib/mjml-rb/cli.rb:189-220` | `mjml --validate` compiles and prints/writes HTML. The reference CLI only parses/validates and is silent on valid input. | Add a validate-only path and prohibit output generation in that mode. |
97
+ | P2 | `lib/mjml-rb/validator.rb:18-30`, `lib/mjml-rb/validator.rb:45-53` | Calling `Validator` directly with a missing include loses `Parser#include_errors` and reports a clean strict result. | Return a parse result containing AST and diagnostics, then make both `Compiler` and `Validator` consume it. |
98
+ | P2 | `lib/mjml-rb/parser.rb:111-115`, `lib/mjml-rb/compiler.rb:136-159` | Include diagnostics collect `file` and `tag_name`, then discard them while building the public result. Nokogiri parse line data is also lost. | Carry structured location fields through a parse result instead of mutable parser state. |
99
+
100
+ The claims in `docs/PARITY_AUDIT.md` for column width, social ending-tag
101
+ behavior, CSS include order, and `.mjmlrc` parity should be corrected as each
102
+ issue is resolved. The README's “all components are implemented and tested”
103
+ claim should also be reconciled with the load-time experimental warning.
104
+
105
+ ## V5 implementation scope
106
+
107
+ ### 1. Secure include model
108
+
109
+ This is the first implementation phase because it changes the default and
110
+ closes a local-file disclosure risk when untrusted MJML is compiled. The
111
+ [v5 parser](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-parser-xml/src/index.js)
112
+ is the behavioral reference.
113
+
114
+ - Change `ignore_includes`/`ignoreIncludes` default from `false` to `true`.
115
+ - Add `include_path`/`includePath`, accepting a string or array of roots.
116
+ - Add CLI `allowIncludes`; it maps to `ignore_includes: false`.
117
+ - When includes are enabled, make the template's canonical `file_path` root
118
+ and explicit canonical `include_path` roots the only readable locations.
119
+ - Resolve and compare `realpath` values so `..` and symlink escapes cannot
120
+ leave an allowed root.
121
+ - Reject absolute, UNC, drive-letter, null-byte, and repeatedly URL-encoded
122
+ traversal attempts when they escape the allowlist.
123
+ - Preserve relative resolution for nested includes by updating `actual_path`
124
+ to the included file and `file_path` to its directory.
125
+ - Preserve MJML, HTML, and CSS include types, inline CSS behavior, source
126
+ order, missing-file diagnostics, and circular-include detection.
127
+ - Omit ignored includes silently. For an enabled but denied include, emit
128
+ `<!-- mj-include denied -->` and a structured `include-denied` diagnostic.
129
+ - Watch enabled recursive include dependencies, not only the root input file.
130
+
131
+ ### 2. Move HTML `<body>` ownership to `mj-body`
132
+
133
+ Follow the tagged
134
+ [`mj-body`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-body/src/index.js)
135
+ and [skeleton](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-core/src/helpers/skeleton.js)
136
+ implementations rather than the initial v5 release prose where they differ.
137
+
138
+ - Add `id: string` to `mj-body`.
139
+ - Render the actual `<body>` from `Components::Body`.
140
+ - Put `id`, `css-class`, word spacing, and body background on `<body>`.
141
+ - Render preview as the first body child.
142
+ - Keep the accessibility wrapper inside the body with `aria-label` when a
143
+ title exists, `aria-roledescription="email"`, `role="article"`, `lang`,
144
+ `dir`, word spacing, and background styling.
145
+ - Remove body and preview generation from the global skeleton.
146
+ - Ensure `mj-html-attributes` selectors can target the real body without
147
+ creating a second body element.
148
+
149
+ ### 3. Component and validation changes
150
+
151
+ | Component/area | V5.4 behavior to implement |
152
+ |---|---|
153
+ | `mj-section` | Add `gutter: unit(px,%)`. Propagate gutter/direction and group context; reduce desktop column widths; apply horizontal edge-aware gutter padding in normal and Outlook output; convert regular mobile columns to vertical gaps; preserve horizontal `mj-group` layout; reverse edges for RTL; and normalize mixed-unit/odd-pixel output deterministically. Use the tagged [`mj-section`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-section/src/index.js) and [`mj-column`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-column/src/index.js) behavior. |
154
+ | `mj-social`, `mj-social-element` | Add `border: string`, default element border to `0`, cascade the parent value, allow a child override, and render it on the icon image. See [`mj-social`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-social/src/Social.js) and [`mj-social-element`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-social/src/SocialElement.js). |
155
+ | Border radius | Change every border-radius schema to `string`, including column and inner-column, image, carousel and thumbnail, social and social element, section, hero, and button. Accept mixed units and slash syntax. |
156
+ | `mj-hero` | Remove `container-background-color`; apply `inner-padding*` to Outlook and non-Outlook content; pass the padding-reduced width to children and the Outlook inner table. Match the tagged [`mj-hero`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml-hero/src/index.js). |
157
+ | `mj-accordion` | Replace legacy `input.mj-accordion-checkbox` selectors with `.mj-accordion-checkbox[type="checkbox"]` where v5 does so. |
158
+ | Required attributes | Remove Ruby-only required checks for `mj-image`, `mj-font`, and `mj-include` unless the tagged v5 validator produces the same diagnostic. V5.4 strict compilation accepts an empty `mj-image` and `mj-font`. |
159
+ | Component metadata | Use one registry/manifest for tag class, allowed attributes, ending-tag status, dependencies, head styles, and cache revision. Parser, validator, and renderer must not carry divergent copies. |
160
+
161
+ The media-query registry currently records only responsive width. Extend it to
162
+ record gutter padding and grouped/RTL variants without making every component
163
+ mutate a shared ad hoc hash.
164
+
165
+ ### 4. Post-processing and template safety
166
+
167
+ MJML v5 replaced its old minifier with htmlnano/cssnano and added template
168
+ syntax protection. Ruby does not need to use the same libraries, but must match
169
+ their observable contract.
170
+
171
+ - Make minify win when both `minify` and `beautify` are true.
172
+ - Preserve MSO/IE conditional comments regardless of `keep_comments`.
173
+ - Remove only source comments when `keep_comments` is false.
174
+ - Protect `<pre>`, `<textarea>`, raw/ending-tag content, script/style content,
175
+ VML, file-start raw content, and significant whitespace.
176
+ - Support paired `<!-- htmlmin:ignore -->` regions; preserve their content
177
+ verbatim during minification and remove only the markers during beautifying.
178
+ - Add `minify_options`/`minifyOptions`, including CSS-minification control and
179
+ the legacy `minifyCSS` alias expected by v5.
180
+ - Add `sanitize_styles`/`sanitizeStyles`,
181
+ `template_syntax`/`templateSyntax`, and
182
+ `allow_mixed_syntax`/`allowMixedSyntax`.
183
+ - Protect and restore template variables in CSS values, property names, and
184
+ blocks. Default delimiters are `{{ ... }}` and `[[ ... ]]`.
185
+ - Return an actionable error for unbalanced or disallowed mixed syntax; do not
186
+ silently mutate a template token.
187
+
188
+ Keep the Ruby API synchronous. The JavaScript reference becoming asynchronous
189
+ because of its minifier is an implementation detail, not a compatibility
190
+ requirement.
191
+
192
+ ### 5. Public API, config, and CLI
193
+
194
+ Create a single option schema and canonicalization function. Ruby snake_case is
195
+ the documented native form, while upstream camelCase is accepted as an alias.
196
+ If both spellings are supplied with different values, raise a clear conflict
197
+ error instead of depending on hash order.
198
+
199
+ | Ruby key | Upstream alias | V5.4 default |
200
+ |---|---|---|
201
+ | `keep_comments` | `keepComments` | `true` |
202
+ | `ignore_includes` | `ignoreIncludes` | `true` |
203
+ | `include_path` | `includePath` | none |
204
+ | `validation_level` | `validationLevel` | `soft` |
205
+ | `file_path` | `filePath` | `.` |
206
+ | `actual_path` | `actualPath` | `.` |
207
+ | `printer_support` | `printerSupport` | `false` |
208
+ | `minify_options` | `minifyOptions` | implementation defaults |
209
+ | `sanitize_styles` | `sanitizeStyles` | `false` |
210
+ | `template_syntax` | `templateSyntax` | `{{ }}` and `[[ ]]` |
211
+ | `allow_mixed_syntax` | `allowMixedSyntax` | `false` |
212
+ | `mjml_config_path` | `mjmlConfigPath` | none |
213
+ | `use_mjml_config_options` | `useMjmlConfigOptions` | `false` |
214
+ | `juice_options` | `juiceOptions` | `{}` |
215
+ | `juice_preserve_tags` | `juicePreserveTags` | none |
216
+
217
+ For Juice-specific options, either implement equivalent behavior in the custom
218
+ CSS inliner or return an explicit unsupported-option error. Silently accepting
219
+ and ignoring them would not be compatible.
220
+
221
+ Configuration and CLI work:
222
+
223
+ - Discover JSON `.mjmlconfig` and keep `.mjmlrc` as a documented legacy alias.
224
+ - Do not execute `.mjmlconfig.js` in Ruby. Return a clear unsupported-format
225
+ error and document Ruby custom-component registration as the alternative.
226
+ - Apply config options only when requested by `use_mjml_config_options`, while
227
+ explicit call/CLI options take precedence. Deep-merge `minify_options`.
228
+ - Preserve preprocessors ordering and reject non-object config roots cleanly.
229
+ - Give every file input its own `file_path` and `actual_path` context.
230
+ - Add stdin file-path support for include resolution.
231
+ - Make `--validate` parse and validate only; valid input produces no HTML.
232
+ - Make watch mode observe enabled recursive include dependencies and update
233
+ dependency watches when the include graph changes.
234
+
235
+ ### 6. Documentation and compatibility metadata
236
+
237
+ - Add a constant such as `MJML_COMPATIBILITY_VERSION = "5.4.0"` and expose it
238
+ in the library/CLI version output.
239
+ - Update `README.md`, `docs/USAGE.md`, `docs/ARCHITECTURE.md`, and
240
+ `docs/PARITY_AUDIT.md` from v4.18 to v5.4 semantics.
241
+ - Add a migration section covering the include default, `include_path`, body
242
+ structure/class placement, formatter changes, and looser border radii.
243
+ - State that `.mjmlconfig.js`, JavaScript component packages, the browser
244
+ bundle, Node-version policy, and removed `mjml-migrate` tooling are outside
245
+ the scope of this pure-Ruby port.
246
+ - Replace broad parity claims with the exact reference tag and the known
247
+ unsupported-option list.
248
+
249
+ ## Delivery phases
250
+
251
+ Each phase should land with its own differential fixtures. Do not update all
252
+ snapshots in one commit.
253
+
254
+ 1. **Lock the oracle and baseline**
255
+ - Pin `mjml@5.4.0` in a development-only differential harness.
256
+ - Convert the baseline defects above into failing tests.
257
+ - Record normalized v4 and v5 outputs for high-risk templates.
258
+ 2. **Fix current correctness defects**
259
+ - Fix Outlook comments, include file context/order, column widths, raw-child
260
+ loss, ending-tag metadata, raw preservation, and registry invalidation.
261
+ 3. **Implement v5 include security and option aliases**
262
+ - Change the default, add allowlisted roots, denial diagnostics, CLI
263
+ `allowIncludes`, and traversal/symlink coverage.
264
+ 4. **Move body ownership**
265
+ - Refactor body/skeleton responsibilities and add outer-HTML fixtures.
266
+ 5. **Port component deltas**
267
+ - Border-radius schemas, hero, accordion, social border, then section gutter.
268
+ 6. **Replace post-processing**
269
+ - Safe comment handling, formatter/minifier, template sanitization, and
270
+ `htmlmin:ignore`.
271
+ 7. **Align config and CLI**
272
+ - `.mjmlconfig`, per-file context, validate-only behavior, and include-aware
273
+ watch mode.
274
+ 8. **Document, benchmark, and release**
275
+ - Update compatibility docs, run the full oracle matrix, benchmark common
276
+ templates, and publish the v4-to-v5 migration notes.
277
+
278
+ ## Acceptance matrix
279
+
280
+ | Area | Required cases |
281
+ |---|---|
282
+ | Includes | Default ignored; explicitly enabled local include; string/array allowlist; MJML/HTML/CSS; nested relative includes; inline CSS order; missing and circular includes; absolute/UNC/drive/null/encoded traversal denied; symlink escape denied; structured diagnostics. |
283
+ | Body | Exactly one body; id/class on body; preview first; ARIA/lang/dir on the inner div; title omission; background placement; body-targeted HTML attributes. |
284
+ | Gutter | Two/three/four columns; px/% gutter; implicit and explicit px/% widths; mixed units; odd pixels; mobile stacking; grouped columns; RTL; borders/padding/full width; Outlook markup. |
285
+ | Social | Raw sibling; raw inner HTML; default border; parent cascade; child override; horizontal and vertical modes. |
286
+ | Radius | Multiple values, mixed units, and slash syntax across every radius-bearing component. |
287
+ | Hero | Shorthand/directional outer padding; inner padding in all clients; padding-reduced child/Outlook width; removed attribute validation. |
288
+ | Post-processing | Source comments versus generated conditionals; raw/pre whitespace; multiple ignore pairs; file-start content; template variables in CSS values/properties/blocks; multiline, mixed, and unbalanced delimiters; CSS minification disabled. |
289
+ | API/config/CLI | camelCase/snake_case equivalence and conflicts; config precedence; input-relative include; stdin context; validate emits no HTML; watch updates after include-graph changes. |
290
+ | Registry | Register before/after first compile; custom ending tags; duplicate-tag policy; repeated and concurrent compilation isolation. |
291
+ | Regression | Existing component fixtures; strict/soft/skip validation; Outlook/VML snapshots; malformed input; performance and memory bounds. |
292
+
293
+ Differential comparisons should use:
294
+
295
+ - exact fragments for critical Outlook, VML, body, and diagnostic contracts;
296
+ - normalized DOM comparisons for general markup;
297
+ - parsed CSS/media-query comparisons for responsive behavior; and
298
+ - exact result classification and CLI exit codes for validation/configuration.
299
+
300
+ ## Upstream tests to port
301
+
302
+ - [Include defaults](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/ignore-includes.test.js)
303
+ - [Include roots and types](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/include-path.test.js)
304
+ - [Include security](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/include-path-security.test.js)
305
+ - [Traversal handling](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/include-path-traversal.test.js)
306
+ - [Section gutter](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/section-gutter.test.js)
307
+ - [Social border](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/social-border.test.js)
308
+ - [Border-radius strings](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/border-radius-string.test.js)
309
+ - [Hero padding width](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/hero-padding-inner-width.test.js)
310
+ - [Template syntax sanitization](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/template-syntax-sanitization.test.js)
311
+ - [`htmlmin:ignore`](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/htmlmin-ignore.test.js)
312
+ - [Beautifier output](https://github.com/mjmlio/mjml/blob/v5.4.0/packages/mjml/test/beautify-output.test.js)
313
+
314
+ ## Definition of done
315
+
316
+ V5 support is complete only when all of the following are true:
317
+
318
+ - the baseline audit issues are fixed or explicitly documented as unsupported;
319
+ - every v5 option is implemented or rejected explicitly rather than ignored;
320
+ - the security suite proves includes cannot escape canonical allowed roots;
321
+ - the component and CLI acceptance matrices pass against `mjml@5.4.0`;
322
+ - generated Outlook conditionals and VML survive every option combination;
323
+ - the existing Ruby suite remains green;
324
+ - documentation names the exact compatibility tag and migration breakages; and
325
+ - no Node.js package is required at gem runtime.
@@ -0,0 +1,124 @@
1
+ # MJML v5 compatibility audit
2
+
3
+ Reference: [`mjml@5.4.1`](https://github.com/mjmlio/mjml/releases/tag/v5.4.1),
4
+ commit `1c434fcbfd042ddfcb9fa4e0496f7eedf1d90bc7`. Updated 2026-10-16.
5
+
6
+ The 0.6.0 implementation targets v5 semantics. This document replaces the old
7
+ v4.18 audit's broad “Match” claims with bounded, reproducible coverage. Passing
8
+ these checks does not establish identical rendering in every email client.
9
+
10
+ ## Verified contracts
11
+
12
+ | Area | Coverage |
13
+ |---|---|
14
+ | Includes | Disabled by default; explicit opt-in; canonical string/array allowlists; nested relative MJML; HTML and CSS; CSS cascade order; missing/circular files; encoded traversal, absolute/UNC/drive/null paths and symlink escapes; structured diagnostics |
15
+ | Body | One body; body id/class; preview first; article ARIA/lang/dir/background; body-targeted HTML attributes |
16
+ | Gutter | Pinned upstream fixtures for px/% widths and gutters, two/three/four columns, mixed units, fractional/odd pixels, mobile stacking, groups, RTL, borders/padding and full width |
17
+ | Components | Social borders, raw siblings and inner HTML; arbitrary border radii; hero padding and inner widths; accordion selectors/Outlook controls; numeric image width |
18
+ | Source/formatting | Raw operators and whitespace, source comments versus Outlook conditionals, minify precedence, paired ignore regions, CSS template values/properties/blocks, mixed/unbalanced syntax, CSS compaction switch |
19
+ | API and CLI | Shared option aliases and conflicts; opt-in config and explicit precedence; file/stdin context; validation without rendering; recursive include watching |
20
+ | Registry | Shared class/dependency/ending metadata; later registrations; duplicate-tag rejection; per-compilation parser and renderer state |
21
+
22
+ `test/reference/v5_cases.json` contains focused reference fragments and semantic
23
+ DOM projections. `test/test_v5_support.rb` and `test/test_v5_cli_config.rb` cover
24
+ security and Ruby-specific error/API contracts. The existing component golden
25
+ fragments continue to cover Outlook/VML and the broader rendering behavior.
26
+
27
+ ## Reproducing the reference checks
28
+
29
+ ```sh
30
+ npm ci --prefix test/reference --ignore-scripts
31
+ MJML_REFERENCE=1 bundle exec rake test
32
+ ```
33
+
34
+ `test/reference/package-lock.json` pins the development oracle and its dependency
35
+ graph. The gem includes Ruby runtime files, licenses and documentation; it does
36
+ not include the npm reference package or load Node/npm. Run these development
37
+ commands from a repository checkout; the gem does not ship the test suite.
38
+ Ordinary `bundle exec rake test` checks the committed reference expectations and
39
+ skips the live oracle. To deliberately refresh semantic projections from the
40
+ pinned reference, run `bundle exec ruby test/reference/update.rb`, then review
41
+ its diff. This command never generates expectations from the Ruby compiler.
42
+
43
+ The CI matrix covers `css_parser` 1.21, 2.x and 3.x on Ruby 3.3, 3.4 and 4.0,
44
+ plus a separate pinned MJML reference job. The supported dependency range is
45
+ `css_parser >= 1.17, < 4`; the CI floor for the 1.x line is 1.21.
46
+
47
+ ## Downstream browser validation
48
+
49
+ Maintainer-reported Halalbooking validation, supplied on 2026-10-16, tested
50
+ `mjml-rb` commit `90f646e35968fdca3afda58c1f2891a7f7a290d6` (gem 0.6.0)
51
+ with `css_parser` 3.3.0. Coverage comprised all eight booking-mailer actions
52
+ through eleven representative scenarios, all nine configured locales, and
53
+ desktop/mobile widths: 198 exact pixel comparisons, without tolerance or masks.
54
+
55
+ | Comparison | Result |
56
+ |---|---|
57
+ | Validated commit versus `313ad6a` (gem 0.6.0 / `css_parser` 2.2.0) | All 198 screenshots pixel-identical |
58
+ | Master controls versus the previous control run | All 198 screenshots pixel-identical |
59
+ | Validated commit versus downstream master's `mjml-rb` 0.5.2 | 185 pixel-identical; 11 background-only differences below short desktop emails; 2 comparisons restoring a missing space in the Turkish KYC email |
60
+
61
+ There were no rendering errors, missing images or unstable captures. All 198
62
+ generated HTML documents had balanced tags, and all 32 focused downstream
63
+ MJML/mailer specs passed.
64
+
65
+ The 11 background-only differences are explained by v5 body styling/class
66
+ ownership. A white HTML-root background applied only in the browser harness
67
+ eliminated all eleven differences exactly. No footer markup change was needed.
68
+ The two Turkish spacing changes were already present in the earlier v5 run;
69
+ switching to `css_parser` 3.3.0 introduced no additional visual changes.
70
+
71
+ This evidence covers representative Chrome renders, not certification across
72
+ email clients. The customer screenshots and source templates remain private;
73
+ public examples use synthetic content. These results are a maintainer-supplied
74
+ validation record, separate from the repository's reproducible reference suite.
75
+
76
+ ## Local performance check
77
+
78
+ `ruby -Ilib benchmark/compile.rb` measures warmed compilation and allocated Ruby
79
+ objects for one and thirty sections containing text and an image. On Ruby 4.0.1
80
+ on the development machine, comparing 0.5.3 with this change gave:
81
+
82
+ | Template | 0.5.3 ms/compile | 0.6.0 ms/compile | 0.5.3 objects | 0.6.0 objects |
83
+ |---|---:|---:|---:|---:|
84
+ | One section, 300 iterations | 0.47 | 0.65 | 1,554 | 2,007 |
85
+ | Thirty sections, 30 iterations | 8.35 | 8.53 | 32,124 | 33,157 |
86
+
87
+ These are local samples, not performance guarantees or peak-memory measurements.
88
+ Per-call parser/renderer state and option validation add overhead most visible
89
+ on small inputs; larger-template cost remained close to the baseline in this run.
90
+
91
+ ## Intentional differences and remaining limits
92
+
93
+ - The API is synchronous and returns structured `Result` errors instead of
94
+ JavaScript promises/exceptions. Soft issues are exposed as `warnings`.
95
+ - Denied includes produce explicit Ruby diagnostics. Upstream 5.4.1 renders the
96
+ denial comment but suppresses the validator error for that type. Ruby freezes
97
+ the initial include allowlist across recursion rather than widening it to
98
+ each included file's directory.
99
+ - CSS includes in `mj-head` retain their source position, preserving cascade
100
+ order. Upstream collects CSS includes; this port does not copy that reordering.
101
+ - The parser uses strict XML outside ending-tag content. Malformed structural
102
+ MJML accepted by upstream's HTML parser can fail in Ruby. `mj-table` retains
103
+ structural parsing for normalization rather than fully raw ending content.
104
+ - The formatter preserves raw regions conservatively and implements CSS
105
+ whitespace compaction, not the full htmlnano/cssnano optimization pipeline.
106
+ Only `minifyCss`/`minifyCSS`/`minify_css` is supported in `minify_options`;
107
+ other minifier settings are rejected explicitly.
108
+ - The custom CSS inliner implements common cascade and HTML-attribute syncing,
109
+ but is not Juice. Non-empty `juice_options` and `juice_preserve_tags` raise an
110
+ unsupported-option error. Template CSS intended for later evaluation should
111
+ use non-inline `mj-style` or raw styles; template-driven selector/block
112
+ evaluation in the CSS inliner is unsupported.
113
+ - Ruby components replace JavaScript package execution. `.mjmlconfig.js`, npm
114
+ component packages and the browser bundle are unsupported.
115
+ - Negative hero widths caused by padding wider than the container are clamped
116
+ to zero in Ruby; upstream currently emits negative widths.
117
+ - Watch mode polls known root files and enabled include dependencies; it does
118
+ not discover additional root inputs that later match a startup glob.
119
+ - Formatting, quote/entity spelling, attribute order and generated carousel
120
+ identifiers can differ. No automated email-client screenshot certification
121
+ is claimed.
122
+
123
+ Migration details are in [MIGRATING_TO_V5.md](MIGRATING_TO_V5.md). The original
124
+ baseline findings and acceptance goals remain in [the support plan](MJML_V5_SUPPORT_PLAN.md).
data/docs/RELEASING.md ADDED
@@ -0,0 +1,35 @@
1
+ # Publishing mjml-rb 0.6.0
2
+
3
+ The maintainer merges and publishes this release. Preparation does not create a
4
+ tag, publish to RubyGems, or create a GitHub release.
5
+
6
+ Before merging PR #24, confirm all ten CI jobs pass for its final head, inspect
7
+ the packaged gem and smoke-test its API and CLI from outside the checkout.
8
+ Runtime source under `lib/` and `bin/` must match the downstream-validated
9
+ commit `90f646e35968fdca3afda58c1f2891a7f7a290d6` unless a confirmed blocker is
10
+ fixed and revalidated. Release documentation and package contents may change.
11
+
12
+ After merging PR #24, run the following from a clean repository checkout.
13
+ The detached checkout selects the PR's exact merge commit even if `main` has
14
+ subsequently advanced. The release date in the changelog and prepared
15
+ [release notes](releases/v0.6.0.md) is 2026-10-16.
16
+
17
+ ```sh
18
+ set -e
19
+ test -z "$(git status --porcelain)"
20
+ test "$(gh pr view 24 --repo faraquet/mjml-rb --json state --jq .state)" = "MERGED"
21
+ git fetch origin main --tags
22
+ release_commit=$(gh pr view 24 --repo faraquet/mjml-rb --json mergeCommit --jq .mergeCommit.oid)
23
+ git switch --detach "$release_commit"
24
+ test "$(ruby -Ilib -rmjml-rb/version -e 'print MjmlRb::VERSION')" = "0.6.0"
25
+ gem build mjml-rb.gemspec
26
+ git tag -a v0.6.0 -m "mjml-rb 0.6.0"
27
+ git push origin refs/tags/v0.6.0
28
+ gem push mjml-rb-0.6.0.gem
29
+ gh release create v0.6.0 --repo faraquet/mjml-rb --verify-tag --title "mjml-rb 0.6.0" --notes-file docs/releases/v0.6.0.md
30
+ ```
31
+
32
+ `gem push` publishes the built artifact to RubyGems using the maintainer's
33
+ credentials, as described in the [RubyGems publishing guide](https://guides.rubygems.org/publishing/).
34
+ No version bump is needed: the gem version is already 0.6.0, and
35
+ `MjmlRb::MJML_COMPATIBILITY_VERSION` remains 5.4.1.