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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d1dc49d39635e3f4530f642a8699646f3bff8e545213387f91d95bb461ea67d
4
- data.tar.gz: 15139737ec96a78601bd7d0e0a992b8743060d4923664eea05462a76e70dc7d3
3
+ metadata.gz: 3808a2e62a0c9a19970937ba5890b4257e8623442efc738e7725b20b11f412e0
4
+ data.tar.gz: 82f3f5e33d4f0b302cc0329dde2b4402713349850865cf591ade0762650f8579
5
5
  SHA512:
6
- metadata.gz: 486e95fb106f03ebb1ae929ec548996f9d53868b360fcff23cfd0aa28d9e4240e3a3181f611b3270837c9012e523c32f3089c0ebe3a82e9bd6e067735a955bec
7
- data.tar.gz: 5bf0eb72f669aed9eb59dc1b8fbe16563082392bbc8d05e2f8d4ce4098a492606303cea65692757fdf5b8eed2c0fc2be7080daeac73f6f242b0a1dfc1c0d5551
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.
@@ -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
- memo[property] = value.sub(/\s*!important\s*\z/, "").strip
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.map { |property, value| "#{property}: #{value}" }.join("; ")
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"] = background_color
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)
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.14".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.14
4
+ version: 0.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk