emjay 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/lib/emjay/components/body/mj_column.rb +149 -6
- data/lib/emjay/components/body/mj_section.rb +4 -1
- data/lib/emjay/components/body/mj_social_element.rb +3 -0
- data/lib/emjay/renderer.rb +6 -3
- data/lib/emjay/version.rb +1 -1
- 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: b7e892dd10debedb98375075acbc595f229f28a1cf7976a90444748f123236b8
|
|
4
|
+
data.tar.gz: adfb7062c1f9ca6d7a7926f9add058896e1d71078a795a64f0638cc036ad387e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b337e7f6f0ebeece8666fed2347bd0f00a5e1b814312b25d9ec81c31021749f7e81d576e4c318f38cc2c81fcc8560bb9914a9a146cf77d5a8729ada51510f046
|
|
7
|
+
data.tar.gz: 5b3b358d75521a337ef0ee78af3b1d7929dd8cdcec194d2762332619f067558e9a04289686364d8627e50cf4963ec6bd192de72c1d78bd17c371388b972e56cd
|
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.
|
|
@@ -71,6 +71,8 @@ module Emjay
|
|
|
71
71
|
def get_styles
|
|
72
72
|
has_br = has_border_radius?
|
|
73
73
|
has_ibr = has_inner_border_radius?
|
|
74
|
+
outlook_gutter = get_outlook_gutter_styles
|
|
75
|
+
mobile_gutter = get_mobile_gutter_styles
|
|
74
76
|
|
|
75
77
|
table_style = {
|
|
76
78
|
"background-color" => get_attribute("background-color"),
|
|
@@ -91,7 +93,8 @@ module Emjay
|
|
|
91
93
|
"direction" => get_attribute("direction"),
|
|
92
94
|
"display" => "inline-block",
|
|
93
95
|
"vertical-align" => get_attribute("vertical-align"),
|
|
94
|
-
"width" => get_mobile_width
|
|
96
|
+
"width" => get_mobile_width,
|
|
97
|
+
**mobile_gutter
|
|
95
98
|
},
|
|
96
99
|
table: {
|
|
97
100
|
**(has_gutter? ? {
|
|
@@ -107,7 +110,8 @@ module Emjay
|
|
|
107
110
|
},
|
|
108
111
|
tdOutlook: {
|
|
109
112
|
"vertical-align" => get_attribute("vertical-align"),
|
|
110
|
-
"width" => get_width_as_pixel
|
|
113
|
+
"width" => get_width_as_pixel,
|
|
114
|
+
**outlook_gutter
|
|
111
115
|
},
|
|
112
116
|
gutter: {
|
|
113
117
|
**table_style,
|
|
@@ -121,7 +125,9 @@ module Emjay
|
|
|
121
125
|
end
|
|
122
126
|
|
|
123
127
|
def render
|
|
124
|
-
classes_name =
|
|
128
|
+
classes_name = get_column_class.dup
|
|
129
|
+
classes_name += " #{get_desktop_gutter_class_name}" if has_column_gutter?
|
|
130
|
+
classes_name += " mj-outlook-group-fix"
|
|
125
131
|
css_class = get_attribute("css-class")
|
|
126
132
|
classes_name += " #{css_class}" if css_class
|
|
127
133
|
|
|
@@ -180,15 +186,20 @@ module Emjay
|
|
|
180
186
|
def get_column_class
|
|
181
187
|
add_media_query = @context[:add_media_query]
|
|
182
188
|
|
|
183
|
-
parsed = get_parsed_width
|
|
184
|
-
|
|
189
|
+
parsed = has_column_gutter? ? get_desktop_width : get_parsed_width
|
|
190
|
+
normalized = (parsed[:unit] == "px") ? normalize_px_value(parsed[:parsed_width]) : parsed[:parsed_width]
|
|
191
|
+
formatted = format_float(normalized).to_s.tr(".", "-")
|
|
185
192
|
|
|
186
193
|
class_name = case parsed[:unit]
|
|
187
194
|
when "%" then "mj-column-per-#{formatted}"
|
|
188
195
|
else "mj-column-px-#{formatted}"
|
|
189
196
|
end
|
|
190
197
|
|
|
191
|
-
add_media_query&.call(class_name, parsed)
|
|
198
|
+
add_media_query&.call(class_name, {unit: parsed[:unit], parsed_width: normalized})
|
|
199
|
+
|
|
200
|
+
if has_column_gutter? && !@context[:is_in_group]
|
|
201
|
+
add_media_query&.call(get_desktop_gutter_class_name, {padding: get_desktop_padding})
|
|
202
|
+
end
|
|
192
203
|
|
|
193
204
|
class_name
|
|
194
205
|
end
|
|
@@ -198,6 +209,138 @@ module Emjay
|
|
|
198
209
|
(value == value.to_i) ? value.to_i : value
|
|
199
210
|
end
|
|
200
211
|
|
|
212
|
+
def has_column_gutter?
|
|
213
|
+
gutter = @context[:gutter]
|
|
214
|
+
!gutter.nil? && gutter != ""
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def get_normalized_gutter_value(target_unit)
|
|
218
|
+
gutter = @context[:gutter]
|
|
219
|
+
return 0 if gutter.nil? || gutter == ""
|
|
220
|
+
|
|
221
|
+
parsed = WidthParser.call(gutter, parse_float_to_int: false)
|
|
222
|
+
parent_width = @context[:container_width]
|
|
223
|
+
|
|
224
|
+
return parsed[:parsed_width] if parsed[:unit] == target_unit
|
|
225
|
+
|
|
226
|
+
if target_unit == "%" && parsed[:unit] == "px"
|
|
227
|
+
(parsed[:parsed_width] / parent_width.to_f) * 100
|
|
228
|
+
elsif target_unit == "px" && parsed[:unit] == "%"
|
|
229
|
+
(parent_width.to_f * parsed[:parsed_width]) / 100
|
|
230
|
+
else
|
|
231
|
+
parsed[:parsed_width]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def get_desktop_unit
|
|
236
|
+
get_parsed_width[:unit]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def get_desktop_width
|
|
240
|
+
parsed = get_parsed_width
|
|
241
|
+
return parsed unless has_column_gutter?
|
|
242
|
+
|
|
243
|
+
sibling = @props[:sibling] || 1
|
|
244
|
+
index = @props[:index] || 0
|
|
245
|
+
unit = parsed[:unit]
|
|
246
|
+
gutter = get_normalized_gutter_value(unit)
|
|
247
|
+
reduction = (gutter * (sibling - 1)) / sibling.to_f
|
|
248
|
+
reduced = [0, normalize_unit_value(parsed[:parsed_width] - reduction)].max
|
|
249
|
+
|
|
250
|
+
if unit == "px"
|
|
251
|
+
floor_width = reduced.floor
|
|
252
|
+
fractional = reduced - floor_width
|
|
253
|
+
extra = [0, [sibling, (sibling * fractional).round].min].max
|
|
254
|
+
{parsed_width: floor_width + ((index < extra) ? 1 : 0), unit: unit}
|
|
255
|
+
else
|
|
256
|
+
{parsed_width: reduced, unit: unit}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def get_desktop_gutter_class_name
|
|
261
|
+
unit = get_desktop_unit
|
|
262
|
+
gutter = get_normalized_gutter_value(unit)
|
|
263
|
+
unit_token = (unit == "%") ? "per" : unit
|
|
264
|
+
direction_token = (@context[:direction] == "rtl") ? "-rtl" : ""
|
|
265
|
+
normalized = (unit == "px") ? normalize_px_value(gutter) : gutter
|
|
266
|
+
gutter_token = normalize_unit_value(normalized).to_s.tr(".", "-")
|
|
267
|
+
sibling = @props[:sibling] || 1
|
|
268
|
+
index = @props[:index] || 0
|
|
269
|
+
"mj-column-gutter-#{sibling}-#{index + 1}-#{unit_token}-#{gutter_token}#{direction_token}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def get_desktop_padding_values(unit)
|
|
273
|
+
first = @props[:first]
|
|
274
|
+
last = @props[:last]
|
|
275
|
+
sibling = @props[:sibling] || 1
|
|
276
|
+
direction = @context[:direction]
|
|
277
|
+
gutter = get_normalized_gutter_value(unit)
|
|
278
|
+
normalized = (unit == "px") ? normalize_px_value(gutter) : gutter
|
|
279
|
+
is_px = unit == "px"
|
|
280
|
+
half_leading = is_px ? (normalized / 2.0).ceil : normalized / 2.0
|
|
281
|
+
half_trailing = is_px ? (normalized / 2.0).floor : normalized / 2.0
|
|
282
|
+
is_rtl = direction == "rtl"
|
|
283
|
+
|
|
284
|
+
return {top: 0, right: 0, bottom: 0, left: 0} if sibling == 1
|
|
285
|
+
|
|
286
|
+
if is_rtl
|
|
287
|
+
{top: 0, right: first ? 0 : half_trailing, bottom: 0, left: last ? 0 : half_leading}
|
|
288
|
+
else
|
|
289
|
+
{top: 0, right: last ? 0 : half_leading, bottom: 0, left: first ? 0 : half_trailing}
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def get_mobile_padding_values
|
|
294
|
+
first = @props[:first]
|
|
295
|
+
last = @props[:last]
|
|
296
|
+
gutter = get_normalized_gutter_value("%")
|
|
297
|
+
half = gutter / 2.0
|
|
298
|
+
{top: first ? 0 : half, right: 0, bottom: last ? 0 : half, left: 0}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def format_padding(top, right, bottom, left, unit)
|
|
302
|
+
if unit == "px"
|
|
303
|
+
"#{normalize_px_value(top)}px #{normalize_px_value(right)}px #{normalize_px_value(bottom)}px #{normalize_px_value(left)}px"
|
|
304
|
+
else
|
|
305
|
+
"#{normalize_unit_value(top)}#{unit} #{normalize_unit_value(right)}#{unit} #{normalize_unit_value(bottom)}#{unit} #{normalize_unit_value(left)}#{unit}"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def get_desktop_padding
|
|
310
|
+
unit = get_desktop_unit
|
|
311
|
+
v = get_desktop_padding_values(unit)
|
|
312
|
+
format_padding(v[:top], v[:right], v[:bottom], v[:left], unit)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def get_mobile_padding
|
|
316
|
+
v = get_mobile_padding_values
|
|
317
|
+
format_padding(v[:top], v[:right], v[:bottom], v[:left], "%")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def get_outlook_gutter_styles
|
|
321
|
+
return {} unless has_column_gutter?
|
|
322
|
+
{"padding" => get_desktop_padding_values("px").then { |v| format_padding(v[:top], v[:right], v[:bottom], v[:left], "px") }}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def get_mobile_gutter_styles
|
|
326
|
+
return {} unless has_column_gutter?
|
|
327
|
+
|
|
328
|
+
if @context[:is_in_group]
|
|
329
|
+
{"padding" => get_desktop_padding}
|
|
330
|
+
else
|
|
331
|
+
{"padding" => get_mobile_padding}
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def normalize_unit_value(value)
|
|
336
|
+
rounded = value.to_f.round(6)
|
|
337
|
+
(rounded == rounded.to_i) ? rounded.to_i : rounded
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def normalize_px_value(value)
|
|
341
|
+
value.to_f.round
|
|
342
|
+
end
|
|
343
|
+
|
|
201
344
|
def has_border_radius?
|
|
202
345
|
br = get_attribute("border-radius")
|
|
203
346
|
br && !br.empty?
|
|
@@ -28,6 +28,7 @@ module Emjay
|
|
|
28
28
|
"border-top" => "string",
|
|
29
29
|
"direction" => "enum(ltr,rtl)",
|
|
30
30
|
"full-width" => "enum(full-width,false,)",
|
|
31
|
+
"gutter" => "unit(px,%)",
|
|
31
32
|
"padding" => "unit(px,%){1,4}",
|
|
32
33
|
"padding-top" => "unit(px,%)",
|
|
33
34
|
"padding-bottom" => "unit(px,%)",
|
|
@@ -54,7 +55,9 @@ module Emjay
|
|
|
54
55
|
widths = get_box_widths
|
|
55
56
|
@context.merge(
|
|
56
57
|
container_width: "#{widths[:box]}px",
|
|
57
|
-
gap: get_attribute("gap")
|
|
58
|
+
gap: get_attribute("gap"),
|
|
59
|
+
gutter: get_attribute("gutter"),
|
|
60
|
+
direction: get_attribute("direction")
|
|
58
61
|
)
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -48,6 +48,7 @@ module Emjay
|
|
|
48
48
|
"align" => "left",
|
|
49
49
|
"icon-position" => "left",
|
|
50
50
|
"color" => "#000",
|
|
51
|
+
"border" => "0",
|
|
51
52
|
"border-radius" => "3px",
|
|
52
53
|
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
53
54
|
"font-size" => "13px",
|
|
@@ -66,6 +67,7 @@ module Emjay
|
|
|
66
67
|
"icon-position" => "enum(left,right)",
|
|
67
68
|
"background-color" => "color",
|
|
68
69
|
"color" => "color",
|
|
70
|
+
"border" => "string",
|
|
69
71
|
"border-radius" => "string",
|
|
70
72
|
"font-family" => "string",
|
|
71
73
|
"font-size" => "unit(px)",
|
|
@@ -123,6 +125,7 @@ module Emjay
|
|
|
123
125
|
"width" => icon_size
|
|
124
126
|
},
|
|
125
127
|
img: {
|
|
128
|
+
"border" => get_attribute("border"),
|
|
126
129
|
"border-radius" => get_attribute("border-radius"),
|
|
127
130
|
"display" => "block"
|
|
128
131
|
},
|
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
|
data/lib/emjay/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.0
|
|
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-19 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
|