mjml-rb 0.2.13 → 0.2.15

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: 9c54cb0ddf9071b81422853dfcca7e3748ed404b7ea1afba2a99ce9a42229d01
4
- data.tar.gz: 7d2469230fa62b2dd52df43e3e8858f1a38aeb5964469af5ec8669182664e9f8
3
+ metadata.gz: 990083fd9e656cfe50b74937926c14f37ea359adf49530e5ffbacf17ef0ea00a
4
+ data.tar.gz: a03cc468ddcdb25fdde896f3d53bbd8febaad32db0b3aae3ccad69725f11920c
5
5
  SHA512:
6
- metadata.gz: 8e349fdbec211923111e8ce2f7a593f46f18308aef3461c531dc3fa6ccfdf97d024c8b181e0289c0b8819db272d49fd3c45c2130298373a8867f6f8f19f14916
7
- data.tar.gz: 9603224d7e49fc263441767a0d3a289bd5bbba4cdf93afebc07b1dd7dd0416f8cd5e0161843d50d31e2f21b6390be8a946569027c52f19ab4d6605442544507b
6
+ metadata.gz: ee2bff6d7a36be6fe4f95b19eefd60e9ff295d62d681cd7e3131ff9e298859b66fea7f5d6887ae3f984d9f13fc6aeda4f3f58b8ee6e6cfd9b603d804cb0305a2
7
+ data.tar.gz: fdd623a60697cb966591bfcad9fc973e71586ffc3776f6f3de5944fc63c2ea413324b3b6ca288303117b58fbfc12c8e2b13aca174b657a5e8c4a33eb73138e97
data/README.md CHANGED
@@ -44,7 +44,7 @@ The table below tracks current JS-to-Ruby migration status for MJML components i
44
44
  | `mj-section` | migrated | Implemented in `section.rb`. |
45
45
  | `mj-wrapper` | migrated | Implemented via `section.rb`. |
46
46
  | `mj-column` | migrated | Implemented in `column.rb`. |
47
- | `mj-group` | migrated | Rendered directly by the renderer. |
47
+ | `mj-group` | migrated | Implemented in `group.rb`, including width-aware child rendering and Outlook table wrappers. |
48
48
  | `mj-text` | migrated | Implemented in `text.rb`. |
49
49
  | `mj-image` | migrated | Implemented in `image.rb`. |
50
50
  | `mj-button` | migrated | Implemented in `button.rb`. |
@@ -0,0 +1,134 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Group < Base
6
+ TAGS = ["mj-group"].freeze
7
+
8
+ ALLOWED_ATTRIBUTES = {
9
+ "background-color" => "color",
10
+ "direction" => "enum(ltr,rtl)",
11
+ "vertical-align" => "enum(top,bottom,middle)",
12
+ "width" => "unit(px,%)"
13
+ }.freeze
14
+
15
+ DEFAULT_ATTRIBUTES = {
16
+ "direction" => "ltr"
17
+ }.freeze
18
+
19
+ def tags
20
+ TAGS
21
+ end
22
+
23
+ def render(tag_name:, node:, context:, attrs:, parent:)
24
+ width_pct = context.delete(:_column_width_pct) || 100.0
25
+ a = DEFAULT_ATTRIBUTES.merge(attrs)
26
+ css_class = a["css-class"]
27
+
28
+ pct_str = width_pct.to_f.to_s.sub(/\.?0+$/, "")
29
+ class_suffix = pct_str.gsub(".", "-")
30
+ context[:column_widths][class_suffix] = pct_str if context[:column_widths]
31
+
32
+ group_class = "mj-column-per-#{class_suffix} mj-outlook-group-fix"
33
+ group_class = "#{group_class} #{css_class}" if css_class && !css_class.empty?
34
+
35
+ group_width = group_container_width(context, a, width_pct)
36
+ div_style = style_join(
37
+ "font-size" => "0",
38
+ "line-height" => "0",
39
+ "text-align" => "left",
40
+ "display" => "inline-block",
41
+ "width" => "100%",
42
+ "direction" => a["direction"],
43
+ "vertical-align" => a["vertical-align"],
44
+ "background-color" => a["background-color"]
45
+ )
46
+
47
+ inner = with_group_container_width(context, group_width) do
48
+ render_group_children(node, context, a, group_width)
49
+ end
50
+
51
+ %(<div class="#{escape_attr(group_class)}" style="#{div_style}">#{inner}</div>)
52
+ end
53
+
54
+ private
55
+
56
+ def render_group_children(node, context, attrs, group_width)
57
+ columns = node.element_children.select { |child| child.tag_name == "mj-column" }
58
+ widths = renderer.send(:compute_column_widths, columns, context)
59
+ group_width_px = parse_pixel_value(group_width)
60
+ group_bg = attrs["background-color"]
61
+ table_attrs = {
62
+ "bgcolor" => (group_bg == "none" ? nil : group_bg),
63
+ "border" => "0",
64
+ "cellpadding" => "0",
65
+ "cellspacing" => "0",
66
+ "role" => "presentation"
67
+ }
68
+
69
+ open_table = %(<!--[if mso | IE]><table#{html_attrs(table_attrs)}><tr><![endif]-->)
70
+ close_table = %(<!--[if mso | IE]></tr></table><![endif]-->)
71
+ column_index = 0
72
+
73
+ body = with_inherited_mj_class(context, node) do
74
+ node.children.map do |child|
75
+ case child.tag_name
76
+ when "mj-column"
77
+ width_pct = widths[column_index] || 100.0
78
+ column_index += 1
79
+ context[:_column_width_pct] = width_pct
80
+ td_style = style_join(
81
+ "vertical-align" => resolved_attributes(child, context)["vertical-align"] || "top",
82
+ "width" => "#{(group_width_px * width_pct / 100.0).round}px"
83
+ )
84
+ td_open = %(<!--[if mso | IE]><td#{html_attrs("style" => td_style)}><![endif]-->)
85
+ td_close = %(<!--[if mso | IE]></td><![endif]-->)
86
+ "#{td_open}#{render_node(child, context, parent: "mj-group")}#{td_close}"
87
+ when "mj-raw"
88
+ render_node(child, context, parent: "mj-group")
89
+ else
90
+ render_node(child, context, parent: "mj-group")
91
+ end
92
+ end.join("\n")
93
+ end
94
+
95
+ "#{open_table}#{body}#{close_table}"
96
+ end
97
+
98
+ def with_group_container_width(context, width)
99
+ previous = context[:container_width]
100
+ context[:container_width] = width
101
+ yield
102
+ ensure
103
+ context[:container_width] = previous
104
+ end
105
+
106
+ def group_container_width(context, attrs, width_pct)
107
+ parent_width = parse_pixel_value(context[:container_width] || "600px")
108
+ width = attrs["width"]
109
+
110
+ raw_width =
111
+ if present_attr?(width) && width.end_with?("%")
112
+ parent_width * parse_pixel_value(width) / 100.0
113
+ elsif present_attr?(width) && width.end_with?("px")
114
+ parse_pixel_value(width)
115
+ else
116
+ parent_width * width_pct / 100.0
117
+ end
118
+
119
+ "#{[raw_width, 0].max}px"
120
+ end
121
+
122
+ def present_attr?(value)
123
+ value && !value.empty?
124
+ end
125
+
126
+ def parse_pixel_value(value)
127
+ return 0.0 unless present_attr?(value)
128
+
129
+ matched = value.to_s.match(/(-?\d+(?:\.\d+)?)/)
130
+ matched ? matched[1].to_f : 0.0
131
+ end
132
+ end
133
+ end
134
+ end
@@ -233,12 +233,8 @@ module MjmlRb
233
233
  td_open = %(<!--[if mso | IE]><td class="" style="vertical-align:#{v_align};width:#{col_px}px;" ><![endif]-->)
234
234
  td_close = %(<!--[if mso | IE]></td><![endif]-->)
235
235
 
236
- col_html = if col.tag_name == "mj-group"
237
- renderer.send(:render_group, col, context, widths[i])
238
- else
239
- context[:_column_width_pct] = widths[i]
240
- render_node(col, context, parent: "mj-section")
241
- end
236
+ context[:_column_width_pct] = widths[i]
237
+ col_html = render_node(col, context, parent: "mj-section")
242
238
 
243
239
  "#{td_open}\n#{col_html}\n#{td_close}"
244
240
  end
@@ -7,6 +7,7 @@ require_relative "components/breakpoint"
7
7
  require_relative "components/button"
8
8
  require_relative "components/carousel"
9
9
  require_relative "components/carousel_image"
10
+ require_relative "components/group"
10
11
  require_relative "components/head"
11
12
  require_relative "components/hero"
12
13
  require_relative "components/image"
@@ -163,24 +164,7 @@ module MjmlRb
163
164
  if (component = component_for(node.tag_name))
164
165
  return component.render(tag_name: node.tag_name, node: node, context: context, attrs: attrs, parent: parent)
165
166
  end
166
-
167
- case node.tag_name
168
- when "mj-group"
169
- render_group(node, context)
170
- else
171
- render_children(node, context, parent: node.tag_name)
172
- end
173
- end
174
-
175
- def render_group(node, context, width_pct = 100)
176
- items = node.element_children.select { |e| e.tag_name == "mj-column" }
177
- widths = compute_column_widths(items, context)
178
- with_inherited_mj_class(context, node) do
179
- items.each_with_index.map do |item, i|
180
- context[:_column_width_pct] = widths[i]
181
- render_node(item, context, parent: "mj-group")
182
- end.join("\n")
183
- end
167
+ render_children(node, context, parent: node.tag_name)
184
168
  end
185
169
 
186
170
  def compute_column_widths(columns, context)
@@ -360,25 +344,32 @@ module MjmlRb
360
344
  property, value = entry.split(":", 2).map { |part| part&.strip }
361
345
  next if property.nil? || property.empty? || value.nil? || value.empty?
362
346
 
363
- 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
+ }
364
352
  end
365
353
  end
366
354
 
367
355
  def merge_inline_style!(node, declarations)
368
356
  existing = parse_css_declarations(node["style"].to_s)
369
357
  declarations.each do |property, value|
370
- existing[property] = value
358
+ existing[property] = merge_css_declaration(existing[property], value)
371
359
  end
372
360
  normalize_background_fallbacks!(node, existing)
373
- node["style"] = existing.map { |property, value| "#{property}: #{value}" }.join("; ")
361
+ node["style"] = serialize_css_declarations(existing)
374
362
  end
375
363
 
376
364
  def normalize_background_fallbacks!(node, declarations)
377
- background_color = declarations["background-color"]
365
+ background_color = declaration_value(declarations["background-color"])
378
366
  return if background_color.nil? || background_color.empty?
379
367
 
380
- if syncable_background?(declarations["background"])
381
- 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
+ }
382
373
  end
383
374
 
384
375
  return unless node.name == "td"
@@ -406,6 +397,25 @@ module MjmlRb
406
397
  !normalized.include?(" right")
407
398
  end
408
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
+
409
419
  def append_component_head_styles(document, context)
410
420
  component_registry.each_value.uniq.each do |component|
411
421
  next unless component.respond_to?(:head_style)
@@ -444,6 +454,7 @@ module MjmlRb
444
454
  register_component(registry, Components::Button.new(self))
445
455
  register_component(registry, Components::Carousel.new(self))
446
456
  register_component(registry, Components::CarouselImage.new(self))
457
+ register_component(registry, Components::Group.new(self))
447
458
  register_component(registry, Components::Hero.new(self))
448
459
  register_component(registry, Components::Image.new(self))
449
460
  register_component(registry, Components::Navbar.new(self))
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.13".freeze
2
+ VERSION = "0.2.15".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.13
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk
@@ -63,6 +63,7 @@ files:
63
63
  - lib/mjml-rb/components/carousel_image.rb
64
64
  - lib/mjml-rb/components/column.rb
65
65
  - lib/mjml-rb/components/divider.rb
66
+ - lib/mjml-rb/components/group.rb
66
67
  - lib/mjml-rb/components/head.rb
67
68
  - lib/mjml-rb/components/hero.rb
68
69
  - lib/mjml-rb/components/html_attributes.rb