mjml-rb 0.2.15 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 990083fd9e656cfe50b74937926c14f37ea359adf49530e5ffbacf17ef0ea00a
4
- data.tar.gz: a03cc468ddcdb25fdde896f3d53bbd8febaad32db0b3aae3ccad69725f11920c
3
+ metadata.gz: 3808a2e62a0c9a19970937ba5890b4257e8623442efc738e7725b20b11f412e0
4
+ data.tar.gz: 82f3f5e33d4f0b302cc0329dde2b4402713349850865cf591ade0762650f8579
5
5
  SHA512:
6
- metadata.gz: ee2bff6d7a36be6fe4f95b19eefd60e9ff295d62d681cd7e3131ff9e298859b66fea7f5d6887ae3f984d9f13fc6aeda4f3f58b8ee6e6cfd9b603d804cb0305a2
7
- data.tar.gz: fdd623a60697cb966591bfcad9fc973e71586ffc3776f6f3de5944fc63c2ea413324b3b6ca288303117b58fbfc12c8e2b13aca174b657a5e8c4a33eb73138e97
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" => "ltr",
31
- "padding" => "20px 0",
32
- "text-align" => "center"
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
- td_style = style_join(
163
- "border" => border_val,
164
- "border-top" => a["border-top"],
165
- "border-right" => a["border-right"],
166
- "border-bottom" => a["border-bottom"],
167
- "border-left" => a["border-left"],
168
- "border-radius" => border_radius,
169
- "background" => bg_color,
170
- "background-color" => bg_color,
171
- "direction" => a["direction"],
172
- "font-size" => "0px",
173
- "padding" => a["padding"],
174
- "padding-top" => a["padding-top"],
175
- "padding-right" => a["padding-right"],
176
- "padding-bottom" => a["padding-bottom"],
177
- "padding-left" => a["padding-left"],
178
- "text-align" => a["text-align"]
179
- )
180
-
181
- table_style = style_join(
182
- "background" => bg_color,
183
- "background-color" => bg_color,
184
- "border-radius" => border_radius,
185
- "width" => "100%"
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)}>#{inner}</td></tr></tbody></table></div>)
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
- "#{render_before}\n#{section_html}\n#{render_after}"
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.
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.15".freeze
2
+ VERSION = "0.2.16".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mjml-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.15
4
+ version: 0.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk