mjml-rb 0.2.14 → 0.2.16
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/lib/mjml-rb/components/section.rb +252 -38
- data/lib/mjml-rb/renderer.rb +32 -6
- data/lib/mjml-rb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3808a2e62a0c9a19970937ba5890b4257e8623442efc738e7725b20b11f412e0
|
|
4
|
+
data.tar.gz: 82f3f5e33d4f0b302cc0329dde2b4402713349850865cf591ade0762650f8579
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47b7c807c43e56df28c60a7c0fae60002c8a95782bc733e293aff5f3b41e06952fd94bedd421fb669fe3002ce261708146d628bce64a79a929706530f6069028
|
|
7
|
+
data.tar.gz: 740e73174150f6130b39f8be696e584a75f56a5daa5387bfd45458f1a44706f84d2c6d25e000229f9ba3a5b28036236de7fa16deb3c1158979d3a1d55e9afd6c
|
|
@@ -7,6 +7,12 @@ module MjmlRb
|
|
|
7
7
|
|
|
8
8
|
SECTION_ALLOWED_ATTRIBUTES = {
|
|
9
9
|
"background-color" => "color",
|
|
10
|
+
"background-url" => "string",
|
|
11
|
+
"background-repeat" => "enum(repeat,no-repeat)",
|
|
12
|
+
"background-size" => "string",
|
|
13
|
+
"background-position" => "string",
|
|
14
|
+
"background-position-x" => "string",
|
|
15
|
+
"background-position-y" => "string",
|
|
10
16
|
"border" => "string",
|
|
11
17
|
"border-bottom" => "string",
|
|
12
18
|
"border-left" => "string",
|
|
@@ -27,9 +33,12 @@ module MjmlRb
|
|
|
27
33
|
).freeze
|
|
28
34
|
|
|
29
35
|
DEFAULT_ATTRIBUTES = {
|
|
30
|
-
"direction"
|
|
31
|
-
"padding"
|
|
32
|
-
"text-align"
|
|
36
|
+
"direction" => "ltr",
|
|
37
|
+
"padding" => "20px 0",
|
|
38
|
+
"text-align" => "center",
|
|
39
|
+
"background-repeat" => "repeat",
|
|
40
|
+
"background-size" => "auto",
|
|
41
|
+
"background-position" => "top center"
|
|
33
42
|
}.freeze
|
|
34
43
|
|
|
35
44
|
class << self
|
|
@@ -116,6 +125,166 @@ module MjmlRb
|
|
|
116
125
|
" #{parts.join(' ')} "
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
# ── Background helpers ───────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
VERTICAL_KEYWORDS = %w[top bottom].freeze
|
|
131
|
+
HORIZONTAL_KEYWORDS = %w[left right].freeze
|
|
132
|
+
|
|
133
|
+
def has_background?(a)
|
|
134
|
+
url = a["background-url"]
|
|
135
|
+
url && !url.to_s.strip.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parse_background_position(position_str)
|
|
139
|
+
tokens = position_str.to_s.strip.split(/\s+/)
|
|
140
|
+
|
|
141
|
+
case tokens.size
|
|
142
|
+
when 0
|
|
143
|
+
{x: "center", y: "top"}
|
|
144
|
+
when 1
|
|
145
|
+
if VERTICAL_KEYWORDS.include?(tokens[0])
|
|
146
|
+
{x: "center", y: tokens[0]}
|
|
147
|
+
else
|
|
148
|
+
{x: tokens[0], y: "center"}
|
|
149
|
+
end
|
|
150
|
+
when 2
|
|
151
|
+
first, second = tokens
|
|
152
|
+
if VERTICAL_KEYWORDS.include?(first) ||
|
|
153
|
+
(first == "center" && HORIZONTAL_KEYWORDS.include?(second))
|
|
154
|
+
{x: second, y: first}
|
|
155
|
+
else
|
|
156
|
+
{x: first, y: second}
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
{x: "center", y: "top"}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def get_background_position(a)
|
|
164
|
+
base = parse_background_position(a["background-position"] || "top center")
|
|
165
|
+
pos_x = a["background-position-x"]
|
|
166
|
+
pos_y = a["background-position-y"]
|
|
167
|
+
x = (pos_x && !pos_x.to_s.empty?) ? pos_x : base[:x]
|
|
168
|
+
y = (pos_y && !pos_y.to_s.empty?) ? pos_y : base[:y]
|
|
169
|
+
{x: x, y: y}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_background_string(a)
|
|
173
|
+
pos = get_background_position(a)
|
|
174
|
+
"#{pos[:x]} #{pos[:y]}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def get_background(a)
|
|
178
|
+
bg_url = a["background-url"]
|
|
179
|
+
bg_color = a["background-color"]
|
|
180
|
+
bg_size = a["background-size"]
|
|
181
|
+
bg_repeat = a["background-repeat"]
|
|
182
|
+
|
|
183
|
+
if has_background?(a)
|
|
184
|
+
pos_str = get_background_string(a)
|
|
185
|
+
parts = []
|
|
186
|
+
parts << bg_color if bg_color && !bg_color.to_s.empty?
|
|
187
|
+
parts << "url('#{bg_url}')"
|
|
188
|
+
parts << pos_str
|
|
189
|
+
parts << "/ #{bg_size}"
|
|
190
|
+
parts << bg_repeat
|
|
191
|
+
parts.join(" ")
|
|
192
|
+
else
|
|
193
|
+
bg_color
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ── VML background for Outlook ───────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
PERCENTAGE_RE = /\A\d+(\.\d+)?%\z/
|
|
200
|
+
|
|
201
|
+
VML_KEYWORD_TO_PERCENT = {
|
|
202
|
+
"left" => "0%", "top" => "0%",
|
|
203
|
+
"center" => "50%",
|
|
204
|
+
"right" => "100%", "bottom" => "100%"
|
|
205
|
+
}.freeze
|
|
206
|
+
|
|
207
|
+
def render_with_background(section_html, a, container_px)
|
|
208
|
+
bg_url = a["background-url"]
|
|
209
|
+
bg_color = a["background-color"]
|
|
210
|
+
bg_repeat = a["background-repeat"] || "repeat"
|
|
211
|
+
bg_size = a["background-size"] || "auto"
|
|
212
|
+
is_repeat = bg_repeat == "repeat"
|
|
213
|
+
|
|
214
|
+
pos = get_background_position(a)
|
|
215
|
+
|
|
216
|
+
# Normalize keywords to percentages
|
|
217
|
+
bg_pos_x = VML_KEYWORD_TO_PERCENT.fetch(pos[:x], nil) || (pos[:x] =~ PERCENTAGE_RE ? pos[:x] : "50%")
|
|
218
|
+
bg_pos_y = VML_KEYWORD_TO_PERCENT.fetch(pos[:y], nil) || (pos[:y] =~ PERCENTAGE_RE ? pos[:y] : "0%")
|
|
219
|
+
|
|
220
|
+
# Compute VML origin/position per axis
|
|
221
|
+
v_origin_x, v_pos_x = vml_axis_values(bg_pos_x, is_repeat, true)
|
|
222
|
+
v_origin_y, v_pos_y = vml_axis_values(bg_pos_y, is_repeat, false)
|
|
223
|
+
|
|
224
|
+
# VML size attributes
|
|
225
|
+
v_size_attrs = vml_size_attributes(bg_size)
|
|
226
|
+
|
|
227
|
+
# VML type
|
|
228
|
+
is_auto = bg_size == "auto"
|
|
229
|
+
vml_type = (!is_repeat && !is_auto) ? "frame" : "tile"
|
|
230
|
+
|
|
231
|
+
# Auto special case: force tile, reset position
|
|
232
|
+
if is_auto
|
|
233
|
+
v_origin_x = 0.5; v_pos_x = 0.5
|
|
234
|
+
v_origin_y = 0; v_pos_y = 0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Build v:fill attributes
|
|
238
|
+
fill_pairs = [
|
|
239
|
+
["origin", "#{v_origin_x}, #{v_origin_y}"],
|
|
240
|
+
["position", "#{v_pos_x}, #{v_pos_y}"],
|
|
241
|
+
["src", bg_url],
|
|
242
|
+
["color", bg_color],
|
|
243
|
+
["type", vml_type]
|
|
244
|
+
]
|
|
245
|
+
fill_pairs << ["size", v_size_attrs[:size]] if v_size_attrs[:size]
|
|
246
|
+
fill_pairs << ["aspect", v_size_attrs[:aspect]] if v_size_attrs[:aspect]
|
|
247
|
+
fill_str = fill_pairs.map { |(k, v)| %(#{k}="#{escape_attr(v.to_s)}") }.join(" ")
|
|
248
|
+
|
|
249
|
+
%(<!--[if mso | IE]><v:rect style="mso-width-percent:1000;" xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false"><v:fill #{fill_str} /><v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0"><![endif]-->) +
|
|
250
|
+
section_html +
|
|
251
|
+
%(<!--[if mso | IE]></v:textbox></v:rect><![endif]-->)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def vml_axis_values(pct_str, is_repeat, is_x)
|
|
255
|
+
if pct_str =~ PERCENTAGE_RE
|
|
256
|
+
decimal = pct_str.to_f / 100.0
|
|
257
|
+
if is_repeat
|
|
258
|
+
[decimal, decimal]
|
|
259
|
+
else
|
|
260
|
+
val = (-50 + decimal * 100) / 100.0
|
|
261
|
+
[val, val]
|
|
262
|
+
end
|
|
263
|
+
elsif is_repeat
|
|
264
|
+
[is_x ? 0.5 : 0, is_x ? 0.5 : 0]
|
|
265
|
+
else
|
|
266
|
+
[is_x ? 0 : -0.5, is_x ? 0 : -0.5]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def vml_size_attributes(bg_size)
|
|
271
|
+
case bg_size
|
|
272
|
+
when "cover"
|
|
273
|
+
{size: "1,1", aspect: "atleast"}
|
|
274
|
+
when "contain"
|
|
275
|
+
{size: "1,1", aspect: "atmost"}
|
|
276
|
+
when "auto"
|
|
277
|
+
{}
|
|
278
|
+
else
|
|
279
|
+
parts = bg_size.to_s.strip.split(/\s+/)
|
|
280
|
+
if parts.size == 1
|
|
281
|
+
{size: bg_size, aspect: "atmost"}
|
|
282
|
+
else
|
|
283
|
+
{size: parts.join(",")}
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
119
288
|
# ── mj-section ─────────────────────────────────────────────────────────
|
|
120
289
|
|
|
121
290
|
def render_section(node, context, attrs)
|
|
@@ -124,6 +293,7 @@ module MjmlRb
|
|
|
124
293
|
css_class = a["css-class"]
|
|
125
294
|
bg_color = a["background-color"]
|
|
126
295
|
border_radius = a["border-radius"]
|
|
296
|
+
bg_has = has_background?(a)
|
|
127
297
|
|
|
128
298
|
# Box width: container minus horizontal padding and borders
|
|
129
299
|
border_left = parse_border_width(a["border-left"] || a["border"])
|
|
@@ -148,46 +318,85 @@ module MjmlRb
|
|
|
148
318
|
|
|
149
319
|
render_before = %(<!--[if mso | IE]><table#{outlook_attrs(before_pairs)}><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->)
|
|
150
320
|
|
|
151
|
-
# Section div, table, td
|
|
152
|
-
div_style = style_join(
|
|
153
|
-
"background" => bg_color,
|
|
154
|
-
"background-color" => bg_color,
|
|
155
|
-
"margin" => "0px auto",
|
|
156
|
-
"max-width" => "#{container_px}px"
|
|
157
|
-
)
|
|
158
|
-
|
|
321
|
+
# Section div, table, td — styles differ based on background-url presence
|
|
159
322
|
border_val = a["border"]
|
|
160
323
|
border_val = nil if border_val.nil? || border_val.to_s.strip.empty? || border_val.to_s.strip == "none"
|
|
161
324
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
325
|
+
if bg_has
|
|
326
|
+
bg_value = get_background(a)
|
|
327
|
+
bg_string = get_background_string(a)
|
|
328
|
+
bg_repeat = a["background-repeat"]
|
|
329
|
+
bg_size = a["background-size"]
|
|
330
|
+
|
|
331
|
+
div_style = style_join(
|
|
332
|
+
"background" => bg_value,
|
|
333
|
+
"background-position" => bg_string,
|
|
334
|
+
"background-repeat" => bg_repeat,
|
|
335
|
+
"background-size" => bg_size,
|
|
336
|
+
"margin" => "0px auto",
|
|
337
|
+
"max-width" => "#{container_px}px"
|
|
338
|
+
)
|
|
339
|
+
table_style = style_join(
|
|
340
|
+
"background" => bg_value,
|
|
341
|
+
"background-position" => bg_string,
|
|
342
|
+
"background-repeat" => bg_repeat,
|
|
343
|
+
"background-size" => bg_size,
|
|
344
|
+
"border-radius" => border_radius,
|
|
345
|
+
"width" => "100%"
|
|
346
|
+
)
|
|
347
|
+
td_style = style_join(
|
|
348
|
+
"border" => border_val,
|
|
349
|
+
"border-top" => a["border-top"],
|
|
350
|
+
"border-right" => a["border-right"],
|
|
351
|
+
"border-bottom" => a["border-bottom"],
|
|
352
|
+
"border-left" => a["border-left"],
|
|
353
|
+
"border-radius" => border_radius,
|
|
354
|
+
"direction" => a["direction"],
|
|
355
|
+
"font-size" => "0px",
|
|
356
|
+
"padding" => a["padding"],
|
|
357
|
+
"padding-top" => a["padding-top"],
|
|
358
|
+
"padding-right" => a["padding-right"],
|
|
359
|
+
"padding-bottom" => a["padding-bottom"],
|
|
360
|
+
"padding-left" => a["padding-left"],
|
|
361
|
+
"text-align" => a["text-align"]
|
|
362
|
+
)
|
|
363
|
+
else
|
|
364
|
+
div_style = style_join(
|
|
365
|
+
"background" => bg_color,
|
|
366
|
+
"background-color" => bg_color,
|
|
367
|
+
"margin" => "0px auto",
|
|
368
|
+
"max-width" => "#{container_px}px"
|
|
369
|
+
)
|
|
370
|
+
table_style = style_join(
|
|
371
|
+
"background" => bg_color,
|
|
372
|
+
"background-color" => bg_color,
|
|
373
|
+
"border-radius" => border_radius,
|
|
374
|
+
"width" => "100%"
|
|
375
|
+
)
|
|
376
|
+
td_style = style_join(
|
|
377
|
+
"border" => border_val,
|
|
378
|
+
"border-top" => a["border-top"],
|
|
379
|
+
"border-right" => a["border-right"],
|
|
380
|
+
"border-bottom" => a["border-bottom"],
|
|
381
|
+
"border-left" => a["border-left"],
|
|
382
|
+
"border-radius" => border_radius,
|
|
383
|
+
"background" => bg_color,
|
|
384
|
+
"background-color" => bg_color,
|
|
385
|
+
"direction" => a["direction"],
|
|
386
|
+
"font-size" => "0px",
|
|
387
|
+
"padding" => a["padding"],
|
|
388
|
+
"padding-top" => a["padding-top"],
|
|
389
|
+
"padding-right" => a["padding-right"],
|
|
390
|
+
"padding-bottom" => a["padding-bottom"],
|
|
391
|
+
"padding-left" => a["padding-left"],
|
|
392
|
+
"text-align" => a["text-align"]
|
|
393
|
+
)
|
|
394
|
+
end
|
|
187
395
|
|
|
188
396
|
div_attrs = {"class" => css_class, "style" => div_style}
|
|
189
397
|
table_attrs = {
|
|
190
398
|
"align" => "center",
|
|
399
|
+
"background" => bg_has ? a["background-url"] : nil,
|
|
191
400
|
"border" => "0",
|
|
192
401
|
"cellpadding" => "0",
|
|
193
402
|
"cellspacing" => "0",
|
|
@@ -202,14 +411,19 @@ module MjmlRb
|
|
|
202
411
|
}
|
|
203
412
|
inner = merge_outlook_conditionals(render_section_columns(node, context, box_width))
|
|
204
413
|
|
|
414
|
+
# Wrap in innerDiv when background image is present (prevents Yahoo whitespace gaps)
|
|
415
|
+
inner_content = bg_has ? %(<div style="line-height:0;font-size:0">#{inner}</div>) : inner
|
|
416
|
+
|
|
205
417
|
section_html =
|
|
206
418
|
%(<div#{html_attrs(div_attrs)}>) +
|
|
207
419
|
%(<table#{html_attrs(table_attrs)}>) +
|
|
208
|
-
%(<tbody><tr><td#{html_attrs(td_attrs)}>#{
|
|
420
|
+
%(<tbody><tr><td#{html_attrs(td_attrs)}>#{inner_content}</td></tr></tbody></table></div>)
|
|
209
421
|
|
|
210
422
|
render_after = %(<!--[if mso | IE]></td></tr></table><![endif]-->)
|
|
211
423
|
|
|
212
|
-
|
|
424
|
+
body = bg_has ? render_with_background(section_html, a, container_px) : section_html
|
|
425
|
+
|
|
426
|
+
"#{render_before}\n#{body}\n#{render_after}"
|
|
213
427
|
end
|
|
214
428
|
|
|
215
429
|
# Generate Outlook IE conditional wrappers around each column/group.
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -344,25 +344,32 @@ module MjmlRb
|
|
|
344
344
|
property, value = entry.split(":", 2).map { |part| part&.strip }
|
|
345
345
|
next if property.nil? || property.empty? || value.nil? || value.empty?
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
important = value.match?(/\s*!important\s*\z/)
|
|
348
|
+
memo[property] = {
|
|
349
|
+
value: value.sub(/\s*!important\s*\z/, "").strip,
|
|
350
|
+
important: important
|
|
351
|
+
}
|
|
348
352
|
end
|
|
349
353
|
end
|
|
350
354
|
|
|
351
355
|
def merge_inline_style!(node, declarations)
|
|
352
356
|
existing = parse_css_declarations(node["style"].to_s)
|
|
353
357
|
declarations.each do |property, value|
|
|
354
|
-
existing[property] = value
|
|
358
|
+
existing[property] = merge_css_declaration(existing[property], value)
|
|
355
359
|
end
|
|
356
360
|
normalize_background_fallbacks!(node, existing)
|
|
357
|
-
node["style"] = existing
|
|
361
|
+
node["style"] = serialize_css_declarations(existing)
|
|
358
362
|
end
|
|
359
363
|
|
|
360
364
|
def normalize_background_fallbacks!(node, declarations)
|
|
361
|
-
background_color = declarations["background-color"]
|
|
365
|
+
background_color = declaration_value(declarations["background-color"])
|
|
362
366
|
return if background_color.nil? || background_color.empty?
|
|
363
367
|
|
|
364
|
-
if syncable_background?(declarations["background"])
|
|
365
|
-
declarations["background"] =
|
|
368
|
+
if syncable_background?(declaration_value(declarations["background"]))
|
|
369
|
+
declarations["background"] = {
|
|
370
|
+
value: background_color,
|
|
371
|
+
important: declarations.fetch("background-color", {}).fetch(:important, false)
|
|
372
|
+
}
|
|
366
373
|
end
|
|
367
374
|
|
|
368
375
|
return unless node.name == "td"
|
|
@@ -390,6 +397,25 @@ module MjmlRb
|
|
|
390
397
|
!normalized.include?(" right")
|
|
391
398
|
end
|
|
392
399
|
|
|
400
|
+
def merge_css_declaration(existing, incoming)
|
|
401
|
+
return incoming if existing.nil?
|
|
402
|
+
return existing if existing[:important] && !incoming[:important]
|
|
403
|
+
|
|
404
|
+
incoming
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def declaration_value(declaration)
|
|
408
|
+
declaration && declaration[:value]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def serialize_css_declarations(declarations)
|
|
412
|
+
declarations.map do |property, declaration|
|
|
413
|
+
value = declaration[:value]
|
|
414
|
+
value = "#{value} !important" if declaration[:important]
|
|
415
|
+
"#{property}: #{value}"
|
|
416
|
+
end.join("; ")
|
|
417
|
+
end
|
|
418
|
+
|
|
393
419
|
def append_component_head_styles(document, context)
|
|
394
420
|
component_registry.each_value.uniq.each do |component|
|
|
395
421
|
next unless component.respond_to?(:head_style)
|
data/lib/mjml-rb/version.rb
CHANGED