emjay 0.1.2 → 0.2.1
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/README.md +5 -1
- data/lib/emjay/components/body/mj_accordion.rb +0 -3
- data/lib/emjay/components/body/mj_accordion_element.rb +0 -3
- data/lib/emjay/components/body/mj_accordion_text.rb +0 -3
- data/lib/emjay/components/body/mj_accordion_title.rb +0 -3
- data/lib/emjay/components/body/mj_body.rb +0 -3
- data/lib/emjay/components/body/mj_button.rb +0 -3
- data/lib/emjay/components/body/mj_carousel.rb +0 -3
- data/lib/emjay/components/body/mj_carousel_image.rb +0 -3
- data/lib/emjay/components/body/mj_column.rb +149 -9
- data/lib/emjay/components/body/mj_divider.rb +0 -3
- data/lib/emjay/components/body/mj_group.rb +0 -3
- data/lib/emjay/components/body/mj_hero.rb +0 -3
- data/lib/emjay/components/body/mj_image.rb +0 -3
- data/lib/emjay/components/body/mj_navbar.rb +0 -3
- data/lib/emjay/components/body/mj_navbar_link.rb +0 -3
- data/lib/emjay/components/body/mj_raw.rb +0 -3
- data/lib/emjay/components/body/mj_section.rb +4 -4
- data/lib/emjay/components/body/mj_social.rb +0 -3
- data/lib/emjay/components/body/mj_social_element.rb +3 -3
- data/lib/emjay/components/body/mj_spacer.rb +0 -3
- data/lib/emjay/components/body/mj_table.rb +0 -3
- data/lib/emjay/components/body/mj_text.rb +0 -3
- data/lib/emjay/components/body/mj_wrapper.rb +0 -2
- data/lib/emjay/components/head/mj_attributes.rb +0 -3
- data/lib/emjay/components/head/mj_breakpoint.rb +0 -3
- data/lib/emjay/components/head/mj_font.rb +0 -3
- data/lib/emjay/components/head/mj_head.rb +0 -3
- data/lib/emjay/components/head/mj_html_attributes.rb +0 -3
- data/lib/emjay/components/head/mj_preview.rb +0 -3
- data/lib/emjay/components/head/mj_style.rb +0 -3
- data/lib/emjay/components/head/mj_title.rb +0 -3
- data/lib/emjay/railtie.rb +12 -2
- data/lib/emjay/registry.rb +66 -6
- data/lib/emjay/renderer.rb +10 -5
- data/lib/emjay/version.rb +1 -1
- data/lib/emjay.rb +1 -34
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44b1f5f2d6b785a000c044f8910beb812ab3b851fa5aa9a9950bdafc14842cd3
|
|
4
|
+
data.tar.gz: ff1959f81bd0468820a03c87a64396a63a14fcc4b90cc11465021519e704bd70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b137f248b18a2d4128e58626158b70e607d74e600f2096df7c0d920cfd9b4411c04912dc1e5a8e244aec4e201718edc62396c6ecb65bf53185c28e8e56204d6
|
|
7
|
+
data.tar.gz: ba4fceec3711cb71600ecc8eaee52865a873a34aabbf952ac40487903024bb266062689b95cf12b285fab4072eb6b52dcc4fc38156b7eb12b60ebec154353922
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ emjay implements the full set of standard MJML components:
|
|
|
12
12
|
|
|
13
13
|
**Features:** CSS inlining via `<mj-style inline="inline">`, global default attributes, `mj-class`, `mj-html-attributes` with CSS selectors, custom fonts, responsive breakpoints, Outlook conditionals, `lang`/`dir` attributes.
|
|
14
14
|
|
|
15
|
-
All components and features from the [MJML documentation](https://documentation.mjml.io/) should work as described. The [MJML templates and examples](https://mjml.io/templates) are a good starting point for building your own emails. Output is tested against the upstream [MJML 4 JavaScript implementation](https://github.com/mjmlio/mjml) using fixture-based comparison and backported behavioral tests. If you find a case where emjay produces different output from the reference MJML implementation, please [open an issue](https://github.com/julik/emjay/issues).
|
|
15
|
+
All components and features from the [MJML documentation](https://documentation.mjml.io/) should work as described. The [MJML templates and examples](https://mjml.io/templates) are a good starting point for building your own emails. Output is tested against the upstream [MJML 5.4.0 JavaScript implementation](https://github.com/mjmlio/mjml/tree/5.4.0) using fixture-based comparison and backported behavioral tests. If you find a case where emjay produces different output from the reference MJML implementation, please [open an issue](https://github.com/julik/emjay/issues).
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -141,6 +141,10 @@ MJML partials work with the standard `render` helper. Use them to share common s
|
|
|
141
141
|
</mj-section>
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### mj-include is not supported
|
|
145
|
+
|
|
146
|
+
emjay does not implement MJML's `<mj-include />` file-inclusion directive. In a Rails app, use standard ERB partials via `<%= render "shared/header" %>` (as shown above) — partials also accept locals, which `mj-include` does not.
|
|
147
|
+
|
|
144
148
|
### How it works
|
|
145
149
|
|
|
146
150
|
The `.mjml` template handler is an ERB passthrough — it does not compile MJML itself. Instead, an ActionMailer interceptor (`Emjay::Rails::MailInterceptor`) compiles the assembled MJML to HTML after Rails has applied layouts and partials. This means `yield` in a layout receives MJML (not HTML), and the full document is compiled in one pass.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -94,6 +93,4 @@ module Emjay
|
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
95
|
end
|
|
97
|
-
|
|
98
|
-
Registry.register(Components::MjAccordion)
|
|
99
96
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -122,6 +121,4 @@ module Emjay
|
|
|
122
121
|
end
|
|
123
122
|
end
|
|
124
123
|
end
|
|
125
|
-
|
|
126
|
-
Registry.register(Components::MjAccordionElement)
|
|
127
124
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -118,6 +117,4 @@ module Emjay
|
|
|
118
117
|
end
|
|
119
118
|
end
|
|
120
119
|
end
|
|
121
|
-
|
|
122
|
-
Registry.register(Components::MjAccordionText)
|
|
123
120
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -166,6 +165,4 @@ module Emjay
|
|
|
166
165
|
end
|
|
167
166
|
end
|
|
168
167
|
end
|
|
169
|
-
|
|
170
|
-
Registry.register(Components::MjAccordionTitle)
|
|
171
168
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -193,6 +192,4 @@ module Emjay
|
|
|
193
192
|
end
|
|
194
193
|
end
|
|
195
194
|
end
|
|
196
|
-
|
|
197
|
-
Registry.register(Components::MjButton)
|
|
198
195
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
require_relative "../../helpers/gen_random_hex_string"
|
|
7
6
|
|
|
@@ -405,6 +404,4 @@ module Emjay
|
|
|
405
404
|
end
|
|
406
405
|
end
|
|
407
406
|
end
|
|
408
|
-
|
|
409
|
-
Registry.register(Components::MjCarousel)
|
|
410
407
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/suffix_css_classes"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -183,6 +182,4 @@ module Emjay
|
|
|
183
182
|
end
|
|
184
183
|
end
|
|
185
184
|
end
|
|
186
|
-
|
|
187
|
-
Registry.register(Components::MjCarouselImage)
|
|
188
185
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -71,6 +70,8 @@ module Emjay
|
|
|
71
70
|
def get_styles
|
|
72
71
|
has_br = has_border_radius?
|
|
73
72
|
has_ibr = has_inner_border_radius?
|
|
73
|
+
outlook_gutter = get_outlook_gutter_styles
|
|
74
|
+
mobile_gutter = get_mobile_gutter_styles
|
|
74
75
|
|
|
75
76
|
table_style = {
|
|
76
77
|
"background-color" => get_attribute("background-color"),
|
|
@@ -91,7 +92,8 @@ module Emjay
|
|
|
91
92
|
"direction" => get_attribute("direction"),
|
|
92
93
|
"display" => "inline-block",
|
|
93
94
|
"vertical-align" => get_attribute("vertical-align"),
|
|
94
|
-
"width" => get_mobile_width
|
|
95
|
+
"width" => get_mobile_width,
|
|
96
|
+
**mobile_gutter
|
|
95
97
|
},
|
|
96
98
|
table: {
|
|
97
99
|
**(has_gutter? ? {
|
|
@@ -107,7 +109,8 @@ module Emjay
|
|
|
107
109
|
},
|
|
108
110
|
tdOutlook: {
|
|
109
111
|
"vertical-align" => get_attribute("vertical-align"),
|
|
110
|
-
"width" => get_width_as_pixel
|
|
112
|
+
"width" => get_width_as_pixel,
|
|
113
|
+
**outlook_gutter
|
|
111
114
|
},
|
|
112
115
|
gutter: {
|
|
113
116
|
**table_style,
|
|
@@ -121,7 +124,9 @@ module Emjay
|
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
def render
|
|
124
|
-
classes_name =
|
|
127
|
+
classes_name = get_column_class.dup
|
|
128
|
+
classes_name += " #{get_desktop_gutter_class_name}" if has_column_gutter?
|
|
129
|
+
classes_name += " mj-outlook-group-fix"
|
|
125
130
|
css_class = get_attribute("css-class")
|
|
126
131
|
classes_name += " #{css_class}" if css_class
|
|
127
132
|
|
|
@@ -180,15 +185,20 @@ module Emjay
|
|
|
180
185
|
def get_column_class
|
|
181
186
|
add_media_query = @context[:add_media_query]
|
|
182
187
|
|
|
183
|
-
parsed = get_parsed_width
|
|
184
|
-
|
|
188
|
+
parsed = has_column_gutter? ? get_desktop_width : get_parsed_width
|
|
189
|
+
normalized = (parsed[:unit] == "px") ? normalize_px_value(parsed[:parsed_width]) : parsed[:parsed_width]
|
|
190
|
+
formatted = format_float(normalized).to_s.tr(".", "-")
|
|
185
191
|
|
|
186
192
|
class_name = case parsed[:unit]
|
|
187
193
|
when "%" then "mj-column-per-#{formatted}"
|
|
188
194
|
else "mj-column-px-#{formatted}"
|
|
189
195
|
end
|
|
190
196
|
|
|
191
|
-
add_media_query&.call(class_name, parsed)
|
|
197
|
+
add_media_query&.call(class_name, {unit: parsed[:unit], parsed_width: normalized})
|
|
198
|
+
|
|
199
|
+
if has_column_gutter? && !@context[:is_in_group]
|
|
200
|
+
add_media_query&.call(get_desktop_gutter_class_name, {padding: get_desktop_padding})
|
|
201
|
+
end
|
|
192
202
|
|
|
193
203
|
class_name
|
|
194
204
|
end
|
|
@@ -198,6 +208,138 @@ module Emjay
|
|
|
198
208
|
(value == value.to_i) ? value.to_i : value
|
|
199
209
|
end
|
|
200
210
|
|
|
211
|
+
def has_column_gutter?
|
|
212
|
+
gutter = @context[:gutter]
|
|
213
|
+
!gutter.nil? && gutter != ""
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def get_normalized_gutter_value(target_unit)
|
|
217
|
+
gutter = @context[:gutter]
|
|
218
|
+
return 0 if gutter.nil? || gutter == ""
|
|
219
|
+
|
|
220
|
+
parsed = WidthParser.call(gutter, parse_float_to_int: false)
|
|
221
|
+
parent_width = @context[:container_width]
|
|
222
|
+
|
|
223
|
+
return parsed[:parsed_width] if parsed[:unit] == target_unit
|
|
224
|
+
|
|
225
|
+
if target_unit == "%" && parsed[:unit] == "px"
|
|
226
|
+
(parsed[:parsed_width] / parent_width.to_f) * 100
|
|
227
|
+
elsif target_unit == "px" && parsed[:unit] == "%"
|
|
228
|
+
(parent_width.to_f * parsed[:parsed_width]) / 100
|
|
229
|
+
else
|
|
230
|
+
parsed[:parsed_width]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def get_desktop_unit
|
|
235
|
+
get_parsed_width[:unit]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def get_desktop_width
|
|
239
|
+
parsed = get_parsed_width
|
|
240
|
+
return parsed unless has_column_gutter?
|
|
241
|
+
|
|
242
|
+
sibling = @props[:sibling] || 1
|
|
243
|
+
index = @props[:index] || 0
|
|
244
|
+
unit = parsed[:unit]
|
|
245
|
+
gutter = get_normalized_gutter_value(unit)
|
|
246
|
+
reduction = (gutter * (sibling - 1)) / sibling.to_f
|
|
247
|
+
reduced = [0, normalize_unit_value(parsed[:parsed_width] - reduction)].max
|
|
248
|
+
|
|
249
|
+
if unit == "px"
|
|
250
|
+
floor_width = reduced.floor
|
|
251
|
+
fractional = reduced - floor_width
|
|
252
|
+
extra = (sibling * fractional).round.clamp(0, sibling)
|
|
253
|
+
{parsed_width: floor_width + ((index < extra) ? 1 : 0), unit: unit}
|
|
254
|
+
else
|
|
255
|
+
{parsed_width: reduced, unit: unit}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def get_desktop_gutter_class_name
|
|
260
|
+
unit = get_desktop_unit
|
|
261
|
+
gutter = get_normalized_gutter_value(unit)
|
|
262
|
+
unit_token = (unit == "%") ? "per" : unit
|
|
263
|
+
direction_token = (@context[:direction] == "rtl") ? "-rtl" : ""
|
|
264
|
+
normalized = (unit == "px") ? normalize_px_value(gutter) : gutter
|
|
265
|
+
gutter_token = normalize_unit_value(normalized).to_s.tr(".", "-")
|
|
266
|
+
sibling = @props[:sibling] || 1
|
|
267
|
+
index = @props[:index] || 0
|
|
268
|
+
"mj-column-gutter-#{sibling}-#{index + 1}-#{unit_token}-#{gutter_token}#{direction_token}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def get_desktop_padding_values(unit)
|
|
272
|
+
first = @props[:first]
|
|
273
|
+
last = @props[:last]
|
|
274
|
+
sibling = @props[:sibling] || 1
|
|
275
|
+
direction = @context[:direction]
|
|
276
|
+
gutter = get_normalized_gutter_value(unit)
|
|
277
|
+
normalized = (unit == "px") ? normalize_px_value(gutter) : gutter
|
|
278
|
+
is_px = unit == "px"
|
|
279
|
+
half_leading = is_px ? (normalized / 2.0).ceil : normalized / 2.0
|
|
280
|
+
half_trailing = is_px ? (normalized / 2.0).floor : normalized / 2.0
|
|
281
|
+
is_rtl = direction == "rtl"
|
|
282
|
+
|
|
283
|
+
return {top: 0, right: 0, bottom: 0, left: 0} if sibling == 1
|
|
284
|
+
|
|
285
|
+
if is_rtl
|
|
286
|
+
{top: 0, right: first ? 0 : half_trailing, bottom: 0, left: last ? 0 : half_leading}
|
|
287
|
+
else
|
|
288
|
+
{top: 0, right: last ? 0 : half_leading, bottom: 0, left: first ? 0 : half_trailing}
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def get_mobile_padding_values
|
|
293
|
+
first = @props[:first]
|
|
294
|
+
last = @props[:last]
|
|
295
|
+
gutter = get_normalized_gutter_value("%")
|
|
296
|
+
half = gutter / 2.0
|
|
297
|
+
{top: first ? 0 : half, right: 0, bottom: last ? 0 : half, left: 0}
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def format_padding(top, right, bottom, left, unit)
|
|
301
|
+
if unit == "px"
|
|
302
|
+
"#{normalize_px_value(top)}px #{normalize_px_value(right)}px #{normalize_px_value(bottom)}px #{normalize_px_value(left)}px"
|
|
303
|
+
else
|
|
304
|
+
"#{normalize_unit_value(top)}#{unit} #{normalize_unit_value(right)}#{unit} #{normalize_unit_value(bottom)}#{unit} #{normalize_unit_value(left)}#{unit}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def get_desktop_padding
|
|
309
|
+
unit = get_desktop_unit
|
|
310
|
+
v = get_desktop_padding_values(unit)
|
|
311
|
+
format_padding(v[:top], v[:right], v[:bottom], v[:left], unit)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def get_mobile_padding
|
|
315
|
+
v = get_mobile_padding_values
|
|
316
|
+
format_padding(v[:top], v[:right], v[:bottom], v[:left], "%")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def get_outlook_gutter_styles
|
|
320
|
+
return {} unless has_column_gutter?
|
|
321
|
+
{"padding" => get_desktop_padding_values("px").then { |v| format_padding(v[:top], v[:right], v[:bottom], v[:left], "px") }}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def get_mobile_gutter_styles
|
|
325
|
+
return {} unless has_column_gutter?
|
|
326
|
+
|
|
327
|
+
if @context[:is_in_group]
|
|
328
|
+
{"padding" => get_desktop_padding}
|
|
329
|
+
else
|
|
330
|
+
{"padding" => get_mobile_padding}
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def normalize_unit_value(value)
|
|
335
|
+
rounded = value.to_f.round(6)
|
|
336
|
+
(rounded == rounded.to_i) ? rounded.to_i : rounded
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def normalize_px_value(value)
|
|
340
|
+
value.to_f.round
|
|
341
|
+
end
|
|
342
|
+
|
|
201
343
|
def has_border_radius?
|
|
202
344
|
br = get_attribute("border-radius")
|
|
203
345
|
br && !br.empty?
|
|
@@ -282,6 +424,4 @@ module Emjay
|
|
|
282
424
|
end
|
|
283
425
|
end
|
|
284
426
|
end
|
|
285
|
-
|
|
286
|
-
Registry.register(Components::MjColumn)
|
|
287
427
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -115,6 +114,4 @@ module Emjay
|
|
|
115
114
|
end
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
|
-
|
|
119
|
-
Registry.register(Components::MjDivider)
|
|
120
117
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -191,6 +190,4 @@ module Emjay
|
|
|
191
190
|
end
|
|
192
191
|
end
|
|
193
192
|
end
|
|
194
|
-
|
|
195
|
-
Registry.register(Components::MjGroup)
|
|
196
193
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -377,6 +376,4 @@ module Emjay
|
|
|
377
376
|
end
|
|
378
377
|
end
|
|
379
378
|
end
|
|
380
|
-
|
|
381
|
-
Registry.register(Components::MjHero)
|
|
382
379
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
require_relative "../../helpers/make_lower_breakpoint"
|
|
7
6
|
|
|
@@ -183,6 +182,4 @@ module Emjay
|
|
|
183
182
|
end
|
|
184
183
|
end
|
|
185
184
|
end
|
|
186
|
-
|
|
187
|
-
Registry.register(Components::MjImage)
|
|
188
185
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
require_relative "../../helpers/make_lower_breakpoint"
|
|
7
6
|
require_relative "../../helpers/gen_random_hex_string"
|
|
@@ -182,6 +181,4 @@ module Emjay
|
|
|
182
181
|
end
|
|
183
182
|
end
|
|
184
183
|
end
|
|
185
|
-
|
|
186
|
-
Registry.register(Components::MjNavbar)
|
|
187
184
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
require_relative "../../helpers/suffix_css_classes"
|
|
7
6
|
|
|
@@ -124,6 +123,4 @@ module Emjay
|
|
|
124
123
|
end
|
|
125
124
|
end
|
|
126
125
|
end
|
|
127
|
-
|
|
128
|
-
Registry.register(Components::MjNavbarLink)
|
|
129
126
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/suffix_css_classes"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -28,6 +27,7 @@ module Emjay
|
|
|
28
27
|
"border-top" => "string",
|
|
29
28
|
"direction" => "enum(ltr,rtl)",
|
|
30
29
|
"full-width" => "enum(full-width,false,)",
|
|
30
|
+
"gutter" => "unit(px,%)",
|
|
31
31
|
"padding" => "unit(px,%){1,4}",
|
|
32
32
|
"padding-top" => "unit(px,%)",
|
|
33
33
|
"padding-bottom" => "unit(px,%)",
|
|
@@ -54,7 +54,9 @@ module Emjay
|
|
|
54
54
|
widths = get_box_widths
|
|
55
55
|
@context.merge(
|
|
56
56
|
container_width: "#{widths[:box]}px",
|
|
57
|
-
gap: get_attribute("gap")
|
|
57
|
+
gap: get_attribute("gap"),
|
|
58
|
+
gutter: get_attribute("gutter"),
|
|
59
|
+
direction: get_attribute("direction")
|
|
58
60
|
)
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -437,6 +439,4 @@ module Emjay
|
|
|
437
439
|
end
|
|
438
440
|
end
|
|
439
441
|
end
|
|
440
|
-
|
|
441
|
-
Registry.register(Components::MjSection)
|
|
442
442
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -169,6 +168,4 @@ module Emjay
|
|
|
169
168
|
end
|
|
170
169
|
end
|
|
171
170
|
end
|
|
172
|
-
|
|
173
|
-
Registry.register(Components::MjSocial)
|
|
174
171
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -48,6 +47,7 @@ module Emjay
|
|
|
48
47
|
"align" => "left",
|
|
49
48
|
"icon-position" => "left",
|
|
50
49
|
"color" => "#000",
|
|
50
|
+
"border" => "0",
|
|
51
51
|
"border-radius" => "3px",
|
|
52
52
|
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
53
53
|
"font-size" => "13px",
|
|
@@ -66,6 +66,7 @@ module Emjay
|
|
|
66
66
|
"icon-position" => "enum(left,right)",
|
|
67
67
|
"background-color" => "color",
|
|
68
68
|
"color" => "color",
|
|
69
|
+
"border" => "string",
|
|
69
70
|
"border-radius" => "string",
|
|
70
71
|
"font-family" => "string",
|
|
71
72
|
"font-size" => "unit(px)",
|
|
@@ -123,6 +124,7 @@ module Emjay
|
|
|
123
124
|
"width" => icon_size
|
|
124
125
|
},
|
|
125
126
|
img: {
|
|
127
|
+
"border" => get_attribute("border"),
|
|
126
128
|
"border-radius" => get_attribute("border-radius"),
|
|
127
129
|
"display" => "block"
|
|
128
130
|
},
|
|
@@ -267,6 +269,4 @@ module Emjay
|
|
|
267
269
|
end
|
|
268
270
|
end
|
|
269
271
|
end
|
|
270
|
-
|
|
271
|
-
Registry.register(Components::MjSocialElement)
|
|
272
272
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/width_parser"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -108,6 +107,4 @@ module Emjay
|
|
|
108
107
|
end
|
|
109
108
|
end
|
|
110
109
|
end
|
|
111
|
-
|
|
112
|
-
Registry.register(Components::MjTable)
|
|
113
110
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../body_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
require_relative "../../helpers/conditional_tag"
|
|
6
5
|
|
|
7
6
|
module Emjay
|
|
@@ -95,6 +94,4 @@ module Emjay
|
|
|
95
94
|
end
|
|
96
95
|
end
|
|
97
96
|
end
|
|
98
|
-
|
|
99
|
-
Registry.register(Components::MjText)
|
|
100
97
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../head_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -33,6 +32,4 @@ module Emjay
|
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
end
|
|
36
|
-
|
|
37
|
-
Registry.register(Components::MjAttributes)
|
|
38
35
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../head_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -23,6 +22,4 @@ module Emjay
|
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
end
|
|
26
|
-
|
|
27
|
-
Registry.register(Components::MjBreakpoint)
|
|
28
25
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../head_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -28,6 +27,4 @@ module Emjay
|
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
29
|
end
|
|
31
|
-
|
|
32
|
-
Registry.register(Components::MjHtmlAttributes)
|
|
33
30
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../head_component"
|
|
4
|
-
require_relative "../../registry"
|
|
5
4
|
|
|
6
5
|
module Emjay
|
|
7
6
|
module Components
|
|
@@ -19,6 +18,4 @@ module Emjay
|
|
|
19
18
|
end
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
|
-
|
|
23
|
-
Registry.register(Components::MjPreview)
|
|
24
21
|
end
|
data/lib/emjay/railtie.rb
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module Emjay
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Register the template handler on :action_view load, not :action_mailer.
|
|
6
|
+
# ActionView's FileSystemResolver builds its PathParser regex from the
|
|
7
|
+
# registered extensions on first template lookup; if ActionView loads
|
|
8
|
+
# before ActionMailer (e.g. a controller renders a view first), the
|
|
9
|
+
# regex is cached without :mjml and .html.mjml templates stay invisible
|
|
10
|
+
# for the rest of the process.
|
|
11
|
+
initializer "emjay.register_template_handler" do
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
13
|
require "emjay/rails/template_handler"
|
|
8
14
|
ActionView::Template.register_template_handler(:mjml, Emjay::Rails::TemplateHandler)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
9
17
|
|
|
18
|
+
initializer "emjay.register_mail_interceptor" do
|
|
19
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
10
20
|
require "emjay/rails/mail_interceptor"
|
|
11
21
|
interceptor = Emjay::Rails::MailInterceptor
|
|
12
22
|
ActionMailer::Base.register_interceptor(interceptor)
|
data/lib/emjay/registry.rb
CHANGED
|
@@ -1,19 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Emjay
|
|
4
|
+
# Lazy autoloader for MJML components. Scans `lib/emjay/components/**/*.rb`
|
|
5
|
+
# once at load time, then derives both the MJML tag name and the Ruby class
|
|
6
|
+
# from each filename:
|
|
7
|
+
#
|
|
8
|
+
# components/body/mj_button.rb → tag "mj-button" → Emjay::Components::MjButton
|
|
9
|
+
#
|
|
10
|
+
# Files are `require`d and constants resolved on first `find`.
|
|
4
11
|
module Registry
|
|
5
|
-
|
|
12
|
+
class UnknownComponent < StandardError; end
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
COMPONENTS_ROOT = File.expand_path("components", __dir__)
|
|
15
|
+
|
|
16
|
+
PATHS = Dir.glob("{head,body}/*.rb", base: COMPONENTS_ROOT).sort.each_with_object({}) do |rel, h|
|
|
17
|
+
basename = File.basename(rel, ".rb") # e.g. "mj_html_attributes"
|
|
18
|
+
tag = basename.tr("_", "-") # → "mj-html-attributes"
|
|
19
|
+
h[tag] = File.join(COMPONENTS_ROOT, rel)
|
|
20
|
+
end.freeze
|
|
21
|
+
|
|
22
|
+
@loaded = {}
|
|
23
|
+
@mutex = Mutex.new
|
|
10
24
|
|
|
11
25
|
def self.find(tag_name)
|
|
12
|
-
@
|
|
26
|
+
@loaded[tag_name] ||= load_component(tag_name)
|
|
13
27
|
end
|
|
14
28
|
|
|
15
29
|
def self.components
|
|
16
|
-
@components
|
|
30
|
+
@components ||= LazyComponents.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Kept for backwards compatibility with callers that eagerly register.
|
|
34
|
+
def self.register(component_class)
|
|
35
|
+
@loaded[component_class.component_name] = component_class
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.load_component(tag_name)
|
|
39
|
+
path = PATHS[tag_name] or return nil
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
return @loaded[tag_name] if @loaded[tag_name]
|
|
42
|
+
require path
|
|
43
|
+
const_name = File.basename(path, ".rb").split("_").map(&:capitalize).join
|
|
44
|
+
Emjay::Components.const_get(const_name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The renderer treats `Registry.components` as a hash keyed by tag name.
|
|
49
|
+
# This proxy resolves each lookup through `Registry.find` so components
|
|
50
|
+
# only load when actually referenced.
|
|
51
|
+
class LazyComponents
|
|
52
|
+
include Enumerable
|
|
53
|
+
|
|
54
|
+
def [](tag_name)
|
|
55
|
+
component = Registry.find(tag_name)
|
|
56
|
+
# We only reach here on render paths (never during parsing, which uses
|
|
57
|
+
# Registry.find directly and tolerates nil). If a tag looks like MJML
|
|
58
|
+
# but resolves to nothing, it's almost certainly a typo like
|
|
59
|
+
# `<mj-nonesuch>` — surface it instead of silently rendering blank.
|
|
60
|
+
if component.nil? && tag_name.to_s.start_with?("mj-")
|
|
61
|
+
raise UnknownComponent, "Unknown MJML component #{tag_name.inspect}"
|
|
62
|
+
end
|
|
63
|
+
component
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def each
|
|
67
|
+
PATHS.each_key { |tag| yield tag, Registry.find(tag) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def keys
|
|
71
|
+
PATHS.keys
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def values
|
|
75
|
+
PATHS.each_key.map { |tag| Registry.find(tag) }
|
|
76
|
+
end
|
|
17
77
|
end
|
|
18
78
|
end
|
|
19
79
|
end
|
data/lib/emjay/renderer.rb
CHANGED
|
@@ -90,10 +90,13 @@ module Emjay
|
|
|
90
90
|
global_data: global_data,
|
|
91
91
|
container_width: "600px",
|
|
92
92
|
add_media_query: ->(class_name, data) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
global_data.media_queries[class_name] = if data.key?(:padding)
|
|
94
|
+
"{ padding:#{data[:padding]} !important; }"
|
|
95
|
+
else
|
|
96
|
+
pw = data[:parsed_width]
|
|
97
|
+
pw = pw.to_i if pw == pw.to_i
|
|
96
98
|
"{ width:#{pw}#{data[:unit]} !important; max-width: #{pw}#{data[:unit]}; }"
|
|
99
|
+
end
|
|
97
100
|
},
|
|
98
101
|
add_head_style: ->(identifier, head_style_fn) {
|
|
99
102
|
global_data.head_style[identifier] = head_style_fn
|
|
@@ -182,9 +185,11 @@ module Emjay
|
|
|
182
185
|
|
|
183
186
|
children = element.children.select(&:element?).map { |child| nokogiri_to_hash(child) }
|
|
184
187
|
|
|
185
|
-
# For ending-tag components, content is the inner HTML
|
|
188
|
+
# For ending-tag components, content is the inner HTML. Only MJML tags
|
|
189
|
+
# can be components — plain HTML (e.g. <a>, <b> nested in mj-text) never
|
|
190
|
+
# is, so don't waste a Registry lookup on it.
|
|
186
191
|
tag_name = element.name
|
|
187
|
-
component_class = Registry.find(tag_name)
|
|
192
|
+
component_class = tag_name.start_with?("mj-") ? Registry.find(tag_name) : nil
|
|
188
193
|
content = if component_class&.ending_tag?
|
|
189
194
|
inner = element.inner_html.strip
|
|
190
195
|
# Restore escaped bare < characters (from template syntax in mj-raw etc.)
|
data/lib/emjay/version.rb
CHANGED
data/lib/emjay.rb
CHANGED
|
@@ -22,40 +22,7 @@ require_relative "emjay/helpers/styles"
|
|
|
22
22
|
require_relative "emjay/helpers/make_lower_breakpoint"
|
|
23
23
|
require_relative "emjay/helpers/gen_random_hex_string"
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
require_relative "emjay/components/head/mj_head"
|
|
27
|
-
require_relative "emjay/components/head/mj_attributes"
|
|
28
|
-
require_relative "emjay/components/head/mj_style"
|
|
29
|
-
require_relative "emjay/components/head/mj_font"
|
|
30
|
-
require_relative "emjay/components/head/mj_title"
|
|
31
|
-
require_relative "emjay/components/head/mj_preview"
|
|
32
|
-
require_relative "emjay/components/head/mj_breakpoint"
|
|
33
|
-
require_relative "emjay/components/head/mj_html_attributes"
|
|
34
|
-
|
|
35
|
-
# Body components
|
|
36
|
-
require_relative "emjay/components/body/mj_body"
|
|
37
|
-
require_relative "emjay/components/body/mj_section"
|
|
38
|
-
require_relative "emjay/components/body/mj_column"
|
|
39
|
-
require_relative "emjay/components/body/mj_text"
|
|
40
|
-
require_relative "emjay/components/body/mj_wrapper"
|
|
41
|
-
require_relative "emjay/components/body/mj_group"
|
|
42
|
-
require_relative "emjay/components/body/mj_image"
|
|
43
|
-
require_relative "emjay/components/body/mj_button"
|
|
44
|
-
require_relative "emjay/components/body/mj_divider"
|
|
45
|
-
require_relative "emjay/components/body/mj_spacer"
|
|
46
|
-
require_relative "emjay/components/body/mj_table"
|
|
47
|
-
require_relative "emjay/components/body/mj_raw"
|
|
48
|
-
require_relative "emjay/components/body/mj_hero"
|
|
49
|
-
require_relative "emjay/components/body/mj_social"
|
|
50
|
-
require_relative "emjay/components/body/mj_social_element"
|
|
51
|
-
require_relative "emjay/components/body/mj_navbar"
|
|
52
|
-
require_relative "emjay/components/body/mj_navbar_link"
|
|
53
|
-
require_relative "emjay/components/body/mj_accordion"
|
|
54
|
-
require_relative "emjay/components/body/mj_accordion_element"
|
|
55
|
-
require_relative "emjay/components/body/mj_accordion_title"
|
|
56
|
-
require_relative "emjay/components/body/mj_accordion_text"
|
|
57
|
-
require_relative "emjay/components/body/mj_carousel"
|
|
58
|
-
require_relative "emjay/components/body/mj_carousel_image"
|
|
25
|
+
# Components are autoloaded on demand via Emjay::Registry — see registry.rb.
|
|
59
26
|
|
|
60
27
|
module Emjay
|
|
61
28
|
def self.to_html(mjml_string, options = {})
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: emjay
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: nokogiri
|
|
@@ -108,7 +108,7 @@ licenses:
|
|
|
108
108
|
metadata:
|
|
109
109
|
homepage_uri: https://github.com/julik/emjay
|
|
110
110
|
source_code_uri: https://github.com/julik/emjay
|
|
111
|
-
changelog_uri: https://github.com/julik/emjay/
|
|
111
|
+
changelog_uri: https://github.com/julik/emjay/releases
|
|
112
112
|
rdoc_options: []
|
|
113
113
|
require_paths:
|
|
114
114
|
- lib
|