mjml-rb 0.2.1

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.
@@ -0,0 +1,241 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Column < Base
6
+ TAGS = %w[mj-column].freeze
7
+
8
+ ALLOWED_ATTRIBUTES = {
9
+ "background-color" => "color",
10
+ "border" => "string",
11
+ "border-bottom" => "string",
12
+ "border-left" => "string",
13
+ "border-radius" => "unit(px,%){1,4}",
14
+ "border-right" => "string",
15
+ "border-top" => "string",
16
+ "direction" => "enum(ltr,rtl)",
17
+ "inner-background-color" => "color",
18
+ "inner-border" => "string",
19
+ "inner-border-bottom" => "string",
20
+ "inner-border-left" => "string",
21
+ "inner-border-radius" => "unit(px,%){1,4}",
22
+ "inner-border-right" => "string",
23
+ "inner-border-top" => "string",
24
+ "padding" => "unit(px,%){1,4}",
25
+ "padding-bottom" => "unit(px,%)",
26
+ "padding-left" => "unit(px,%)",
27
+ "padding-right" => "unit(px,%)",
28
+ "padding-top" => "unit(px,%)",
29
+ "vertical-align" => "enum(top,bottom,middle)",
30
+ "width" => "unit(px,%)"
31
+ }.freeze
32
+
33
+ DEFAULT_ATTRIBUTES = {
34
+ "direction" => "ltr",
35
+ "vertical-align" => "top"
36
+ }.freeze
37
+
38
+ GUTTER_ATTRIBUTES = %w[padding padding-top padding-right padding-bottom padding-left].freeze
39
+
40
+ def render(tag_name:, node:, context:, attrs:, parent:)
41
+ width_pct = context.delete(:_column_width_pct) || 100.0
42
+ css_class = attrs["css-class"]
43
+ a = self.class.default_attributes.merge(attrs)
44
+
45
+ pct_str = width_pct.to_f.to_s.sub(/\.?0+$/, "")
46
+ col_class_suffix = pct_str.gsub(".", "-")
47
+ context[:column_widths][col_class_suffix] = pct_str if context[:column_widths]
48
+
49
+ col_class = "mj-column-per-#{col_class_suffix} mj-outlook-group-fix"
50
+ col_class = "#{col_class} #{css_class}" if css_class && !css_class.empty?
51
+
52
+ vertical_align = a["vertical-align"]
53
+ col_style = style_join(
54
+ "font-size" => "0px",
55
+ "text-align" => "left",
56
+ "direction" => a["direction"],
57
+ "display" => "inline-block",
58
+ "vertical-align" => vertical_align,
59
+ "width" => "100%"
60
+ )
61
+
62
+ column_markup =
63
+ if gutter?(a)
64
+ render_gutter(node, context, a, vertical_align, width_pct)
65
+ else
66
+ render_column(node, context, a, vertical_align, width_pct, inside_gutter: false)
67
+ end
68
+
69
+ %(<div class="#{escape_attr(col_class)}" style="#{col_style}">#{column_markup}</div>)
70
+ end
71
+
72
+ private
73
+
74
+ def gutter?(attrs)
75
+ GUTTER_ATTRIBUTES.any? { |name| attrs[name] && !attrs[name].empty? }
76
+ end
77
+
78
+ def render_gutter(node, context, attrs, vertical_align, width_pct)
79
+ table_attrs = {
80
+ "border" => "0",
81
+ "cellpadding" => "0",
82
+ "cellspacing" => "0",
83
+ "role" => "presentation",
84
+ "width" => "100%",
85
+ "style" => (has_border_radius?(attrs) ? "border-collapse:separate" : nil)
86
+ }
87
+ td_attrs = {
88
+ "style" => style_join(table_style(attrs, vertical_align).merge(gutter_style(attrs, vertical_align)))
89
+ }
90
+
91
+ %(<table#{html_attrs(table_attrs)}><tbody><tr><td#{html_attrs(td_attrs)}>#{render_column(node, context, attrs, vertical_align, width_pct, inside_gutter: true)}</td></tr></tbody></table>)
92
+ end
93
+
94
+ def render_column(node, context, attrs, vertical_align, width_pct, inside_gutter:)
95
+ table_attrs = {
96
+ "border" => "0",
97
+ "cellpadding" => "0",
98
+ "cellspacing" => "0",
99
+ "role" => "presentation",
100
+ "width" => "100%"
101
+ }
102
+
103
+ styles = inside_gutter ? inner_table_style(attrs) : table_style(attrs, vertical_align)
104
+ table_attrs["style"] = style_join(styles) if styles.any?
105
+
106
+ children = with_child_container_width(context, attrs, width_pct) do
107
+ render_children(node, context, parent: "mj-column")
108
+ end
109
+ %(<table#{html_attrs(table_attrs)}><tbody>#{children}</tbody></table>)
110
+ end
111
+
112
+ def table_style(attrs, vertical_align)
113
+ style = {
114
+ "background-color" => attrs["background-color"],
115
+ "border" => attrs["border"],
116
+ "border-bottom" => attrs["border-bottom"],
117
+ "border-left" => attrs["border-left"],
118
+ "border-radius" => attrs["border-radius"],
119
+ "border-right" => attrs["border-right"],
120
+ "border-top" => attrs["border-top"],
121
+ "vertical-align" => vertical_align
122
+ }
123
+ style["border-collapse"] = "separate" if has_border_radius?(attrs)
124
+ style
125
+ end
126
+
127
+ def inner_table_style(attrs)
128
+ style = {
129
+ "background-color" => attrs["inner-background-color"],
130
+ "border" => attrs["inner-border"],
131
+ "border-bottom" => attrs["inner-border-bottom"],
132
+ "border-left" => attrs["inner-border-left"],
133
+ "border-radius" => attrs["inner-border-radius"],
134
+ "border-right" => attrs["inner-border-right"],
135
+ "border-top" => attrs["inner-border-top"]
136
+ }
137
+ style["border-collapse"] = "separate" if has_inner_border_radius?(attrs)
138
+ style
139
+ end
140
+
141
+ def gutter_style(attrs, vertical_align)
142
+ {
143
+ "padding" => attrs["padding"],
144
+ "padding-top" => attrs["padding-top"],
145
+ "padding-right" => attrs["padding-right"],
146
+ "padding-bottom" => attrs["padding-bottom"],
147
+ "padding-left" => attrs["padding-left"],
148
+ "vertical-align" => vertical_align
149
+ }
150
+ end
151
+
152
+ def has_border_radius?(attrs)
153
+ present_attr?(attrs["border-radius"])
154
+ end
155
+
156
+ def has_inner_border_radius?(attrs)
157
+ present_attr?(attrs["inner-border-radius"])
158
+ end
159
+
160
+ def present_attr?(value)
161
+ value && !value.empty?
162
+ end
163
+
164
+ def with_child_container_width(context, attrs, width_pct)
165
+ previous_container_width = context[:container_width]
166
+ context[:container_width] = child_container_width(context, attrs, width_pct)
167
+ yield
168
+ ensure
169
+ context[:container_width] = previous_container_width
170
+ end
171
+
172
+ def child_container_width(context, attrs, width_pct)
173
+ parent_width = parse_pixel_value(context[:container_width] || "600px")
174
+ width = attrs["width"]
175
+ raw_width =
176
+ if present_attr?(width) && width.end_with?("%")
177
+ parent_width * parse_pixel_value(width) / 100.0
178
+ elsif present_attr?(width) && width.end_with?("px")
179
+ parse_pixel_value(width)
180
+ else
181
+ parent_width * width_pct / 100.0
182
+ end
183
+
184
+ all_paddings = horizontal_padding_width(attrs) +
185
+ horizontal_border_width(attrs, "border") +
186
+ horizontal_border_width(attrs, "inner-border")
187
+
188
+ "#{[raw_width - all_paddings, 0].max}px"
189
+ end
190
+
191
+ def horizontal_padding_width(attrs)
192
+ padding_value(attrs, "left") + padding_value(attrs, "right")
193
+ end
194
+
195
+ def padding_value(attrs, side)
196
+ specific_padding = attrs["padding-#{side}"]
197
+ return parse_pixel_value(specific_padding) if present_attr?(specific_padding)
198
+
199
+ shorthand_padding_value(attrs["padding"], side)
200
+ end
201
+
202
+ def shorthand_padding_value(value, side)
203
+ return 0.0 unless present_attr?(value)
204
+
205
+ parts = value.split(/\s+/)
206
+ case parts.length
207
+ when 1
208
+ parse_pixel_value(parts[0])
209
+ when 2
210
+ side == "left" || side == "right" ? parse_pixel_value(parts[1]) : parse_pixel_value(parts[0])
211
+ when 3
212
+ side == "left" || side == "right" ? parse_pixel_value(parts[1]) : parse_pixel_value(side == "top" ? parts[0] : parts[2])
213
+ when 4
214
+ parse_pixel_value(parts[side == "left" ? 3 : 1])
215
+ else
216
+ 0.0
217
+ end
218
+ end
219
+
220
+ def horizontal_border_width(attrs, attribute_prefix)
221
+ border_width(attrs["#{attribute_prefix}-left"] || attrs[attribute_prefix]) +
222
+ border_width(attrs["#{attribute_prefix}-right"] || attrs[attribute_prefix])
223
+ end
224
+
225
+ def border_width(value)
226
+ return 0.0 unless present_attr?(value)
227
+ return 0.0 if value.strip == "none"
228
+
229
+ matched = value.match(/(-?\d+(?:\.\d+)?)px/)
230
+ matched ? matched[1].to_f : 0.0
231
+ end
232
+
233
+ def parse_pixel_value(value)
234
+ return 0.0 unless present_attr?(value)
235
+
236
+ matched = value.to_s.match(/(-?\d+(?:\.\d+)?)/)
237
+ matched ? matched[1].to_f : 0.0
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,120 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Divider < Base
6
+ TAGS = ["mj-divider"].freeze
7
+
8
+ DEFAULTS = {
9
+ "align" => "center",
10
+ "border-color" => "#000000",
11
+ "border-style" => "solid",
12
+ "border-width" => "4px",
13
+ "padding" => "10px 25px",
14
+ "width" => "100%"
15
+ }.freeze
16
+
17
+ def tags
18
+ TAGS
19
+ end
20
+
21
+ def render(tag_name:, node:, context:, attrs:, parent:)
22
+ a = DEFAULTS.merge(attrs)
23
+
24
+ outer_td_style = style_join(
25
+ "background" => a["container-background-color"],
26
+ "font-size" => "0px",
27
+ "padding" => a["padding"],
28
+ "padding-top" => a["padding-top"],
29
+ "padding-right" => a["padding-right"],
30
+ "padding-bottom" => a["padding-bottom"],
31
+ "padding-left" => a["padding-left"],
32
+ "word-break" => "break-word"
33
+ )
34
+ outer_td_attrs = {
35
+ "align" => a["align"],
36
+ "class" => a["css-class"],
37
+ "style" => outer_td_style
38
+ }
39
+
40
+ border_top = "#{a["border-style"]} #{a["border-width"]} #{a["border-color"]}"
41
+ margin = compute_margin(a["align"])
42
+
43
+ p_style = style_join(
44
+ "border-top" => border_top,
45
+ "font-size" => "1px",
46
+ "margin" => margin,
47
+ "width" => a["width"]
48
+ )
49
+
50
+ outlook_width = get_outlook_width(a, context)
51
+ outlook_style = style_join(
52
+ "border-top" => border_top,
53
+ "font-size" => "1px",
54
+ "margin" => margin,
55
+ "width" => outlook_width
56
+ )
57
+
58
+ p_tag = %(<p style="#{p_style}"></p>)
59
+ outlook = outlook_block(a["align"], outlook_style, outlook_width)
60
+
61
+ %(<tr><td#{html_attrs(outer_td_attrs)}>#{p_tag}\n#{outlook}</td></tr>)
62
+ end
63
+
64
+ private
65
+
66
+ def compute_margin(align)
67
+ case align
68
+ when "left" then "0px"
69
+ when "right" then "0px 0px 0px auto"
70
+ else "0px auto"
71
+ end
72
+ end
73
+
74
+ def get_outlook_width(attrs, context)
75
+ container_width = (context[:container_width] || "600px").to_f
76
+ padding = attrs["padding"] || "10px 25px"
77
+ parts = padding.split(/\s+/)
78
+ pad_left = shorthand_value(parts, :left).to_f
79
+ pad_right = shorthand_value(parts, :right).to_f
80
+
81
+ if attrs["padding-left"]
82
+ pad_left = attrs["padding-left"].to_f
83
+ end
84
+ if attrs["padding-right"]
85
+ pad_right = attrs["padding-right"].to_f
86
+ end
87
+
88
+ width = attrs["width"] || "100%"
89
+
90
+ if width =~ /^(\d+(?:\.\d+)?)\s*%$/
91
+ pct = ::Regexp.last_match(1).to_f / 100.0
92
+ effective = container_width - pad_left - pad_right
93
+ "#{(effective * pct).to_i}px"
94
+ elsif width =~ /^(\d+(?:\.\d+)?)\s*px$/
95
+ width
96
+ else
97
+ "#{(container_width - pad_left - pad_right).to_i}px"
98
+ end
99
+ end
100
+
101
+ def shorthand_value(parts, side)
102
+ case parts.length
103
+ when 1 then parts[0]
104
+ when 2, 3 then parts[1]
105
+ when 4 then side == :left ? parts[3] : parts[1]
106
+ else "0"
107
+ end
108
+ end
109
+
110
+ def outlook_block(align, style, width)
111
+ # Strip trailing px for the HTML width attribute
112
+ width_int = width.to_i.to_s
113
+ <<~HTML.strip
114
+ <!--[if mso | IE]><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" style="#{style}" role="presentation" width="#{width_int}" ><tr><td style="height:0;line-height:0;"> &nbsp;
115
+ </td></tr></table><![endif]-->
116
+ HTML
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,285 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Hero < Base
6
+ TAGS = ["mj-hero"].freeze
7
+
8
+ ALLOWED_ATTRIBUTES = {
9
+ "mode" => "string",
10
+ "height" => "unit(px,%)",
11
+ "background-url" => "string",
12
+ "background-width" => "unit(px,%)",
13
+ "background-height" => "unit(px,%)",
14
+ "background-position" => "string",
15
+ "border-radius" => "string",
16
+ "container-background-color" => "color",
17
+ "inner-background-color" => "color",
18
+ "inner-padding" => "unit(px,%){1,4}",
19
+ "inner-padding-top" => "unit(px,%)",
20
+ "inner-padding-left" => "unit(px,%)",
21
+ "inner-padding-right" => "unit(px,%)",
22
+ "inner-padding-bottom" => "unit(px,%)",
23
+ "padding" => "unit(px,%){1,4}",
24
+ "padding-bottom" => "unit(px,%)",
25
+ "padding-left" => "unit(px,%)",
26
+ "padding-right" => "unit(px,%)",
27
+ "padding-top" => "unit(px,%)",
28
+ "background-color" => "color",
29
+ "vertical-align" => "enum(top,bottom,middle)"
30
+ }.freeze
31
+
32
+ DEFAULT_ATTRIBUTES = {
33
+ "mode" => "fixed-height",
34
+ "height" => "0px",
35
+ "background-url" => nil,
36
+ "background-position" => "center center",
37
+ "padding" => "0px",
38
+ "padding-bottom" => nil,
39
+ "padding-left" => nil,
40
+ "padding-right" => nil,
41
+ "padding-top" => nil,
42
+ "background-color" => "#ffffff",
43
+ "vertical-align" => "top"
44
+ }.freeze
45
+
46
+ def tags
47
+ TAGS
48
+ end
49
+
50
+ def render(tag_name:, node:, context:, attrs:, parent:)
51
+ a = DEFAULT_ATTRIBUTES.merge(attrs)
52
+ container_width = normalize_container_width(context[:container_width] || "600px")
53
+ content_width = hero_container_width(a, container_width)
54
+
55
+ content = with_container_width(context, content_width) do
56
+ render_children(node, context, parent: "mj-hero")
57
+ end
58
+
59
+ div_attrs = {
60
+ "class" => a["css-class"],
61
+ "style" => style_join(
62
+ "margin" => "0 auto",
63
+ "max-width" => container_width
64
+ )
65
+ }
66
+
67
+ table_attrs = {
68
+ "border" => "0",
69
+ "cellpadding" => "0",
70
+ "cellspacing" => "0",
71
+ "role" => "presentation",
72
+ "style" => "width:100%;",
73
+ "width" => "100%"
74
+ }
75
+
76
+ wrapper = %(<div#{html_attrs(div_attrs)}><table#{html_attrs(table_attrs)}><tbody><tr style="vertical-align:top;">#{render_mode(a, content, container_width)}</tr></tbody></table></div>)
77
+
78
+ "#{outlook_before(a, container_width)}#{wrapper}#{outlook_after}"
79
+ end
80
+
81
+ private
82
+
83
+ def render_mode(attrs, content, container_width)
84
+ common_attrs = {
85
+ "background" => attrs["background-url"],
86
+ "style" => style_join(
87
+ "background" => background_style(attrs),
88
+ "background-position" => attrs["background-position"],
89
+ "background-repeat" => "no-repeat",
90
+ "border-radius" => attrs["border-radius"],
91
+ "padding" => attrs["padding"],
92
+ "padding-top" => attrs["padding-top"],
93
+ "padding-left" => attrs["padding-left"],
94
+ "padding-right" => attrs["padding-right"],
95
+ "padding-bottom" => attrs["padding-bottom"],
96
+ "vertical-align" => attrs["vertical-align"]
97
+ )
98
+ }
99
+
100
+ if attrs["mode"] == "fluid-height"
101
+ ratio = background_ratio(attrs, container_width)
102
+ fluid_td_attrs = {
103
+ "style" => style_join(
104
+ "width" => "0.01%",
105
+ "padding-bottom" => "#{ratio}%",
106
+ "mso-padding-bottom-alt" => "0"
107
+ )
108
+ }
109
+
110
+ [
111
+ %(<td#{html_attrs(fluid_td_attrs)}></td>),
112
+ %(<td#{html_attrs(common_attrs)}>#{render_content(attrs, content, container_width)}</td>),
113
+ %(<td#{html_attrs(fluid_td_attrs)}></td>)
114
+ ].join
115
+ else
116
+ height = [parse_unit_value(attrs["height"]) - padding_side(attrs, "top") - padding_side(attrs, "bottom"), 0].max
117
+ fixed_attrs = common_attrs.merge(
118
+ "height" => height.to_i.to_s,
119
+ "style" => style_join(
120
+ "background" => background_style(attrs),
121
+ "background-position" => attrs["background-position"],
122
+ "background-repeat" => "no-repeat",
123
+ "border-radius" => attrs["border-radius"],
124
+ "padding" => attrs["padding"],
125
+ "padding-top" => attrs["padding-top"],
126
+ "padding-left" => attrs["padding-left"],
127
+ "padding-right" => attrs["padding-right"],
128
+ "padding-bottom" => attrs["padding-bottom"],
129
+ "vertical-align" => attrs["vertical-align"],
130
+ "height" => "#{height.to_i}px"
131
+ )
132
+ )
133
+ %(<td#{html_attrs(fixed_attrs)}>#{render_content(attrs, content, container_width)}</td>)
134
+ end
135
+ end
136
+
137
+ def render_content(attrs, content, container_width)
138
+ outlook_attrs = {
139
+ "align" => "center",
140
+ "border" => "0",
141
+ "cellpadding" => "0",
142
+ "cellspacing" => "0",
143
+ "role" => "presentation",
144
+ "width" => container_width.delete_suffix("px"),
145
+ "style" => "width:#{container_width};"
146
+ }
147
+ outlook_td_style = style_join(
148
+ "background-color" => attrs["inner-background-color"],
149
+ "padding" => attrs["inner-padding"],
150
+ "padding-top" => attrs["inner-padding-top"],
151
+ "padding-left" => attrs["inner-padding-left"],
152
+ "padding-right" => attrs["inner-padding-right"],
153
+ "padding-bottom" => attrs["inner-padding-bottom"]
154
+ )
155
+ inner_div_attrs = {
156
+ "class" => "mj-hero-content",
157
+ "style" => style_join(
158
+ "background-color" => attrs["inner-background-color"],
159
+ "margin" => "0px auto"
160
+ )
161
+ }
162
+ inner_table_attrs = {
163
+ "border" => "0",
164
+ "cellpadding" => "0",
165
+ "cellspacing" => "0",
166
+ "role" => "presentation",
167
+ "style" => "width:100%;margin:0px;",
168
+ "width" => "100%"
169
+ }
170
+
171
+ [
172
+ %(<!--[if mso | IE]><table#{html_attrs(outlook_attrs)}><tr><td style="#{outlook_td_style}"><![endif]-->),
173
+ %(<div#{html_attrs(inner_div_attrs)}><table#{html_attrs(inner_table_attrs)}><tbody>#{content}</tbody></table></div>),
174
+ %(<!--[if mso | IE]></td></tr></table><![endif]-->)
175
+ ].join
176
+ end
177
+
178
+ def outlook_before(attrs, container_width)
179
+ table_attrs = {
180
+ "align" => "center",
181
+ "border" => "0",
182
+ "cellpadding" => "0",
183
+ "cellspacing" => "0",
184
+ "role" => "presentation",
185
+ "style" => "width:#{container_width};",
186
+ "width" => container_width.delete_suffix("px")
187
+ }
188
+ image_attrs = {
189
+ "style" => style_join(
190
+ "border" => "0",
191
+ "height" => attrs["background-height"],
192
+ "mso-position-horizontal" => "center",
193
+ "position" => "absolute",
194
+ "top" => "0",
195
+ "width" => attrs["background-width"] || container_width,
196
+ "z-index" => "-3"
197
+ ),
198
+ "src" => attrs["background-url"],
199
+ "xmlns:v" => "urn:schemas-microsoft-com:vml"
200
+ }
201
+
202
+ opening = %(<!--[if mso | IE]><table#{html_attrs(table_attrs)}><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">)
203
+ return "#{opening}<![endif]-->" if attrs["background-url"].nil? || attrs["background-url"].empty?
204
+
205
+ "#{opening}<v:image#{html_attrs(image_attrs)} /><![endif]-->"
206
+ end
207
+
208
+ def outlook_after
209
+ %(<!--[if mso | IE]></td></tr></table><![endif]-->)
210
+ end
211
+
212
+ def background_style(attrs)
213
+ parts = [attrs["background-color"]]
214
+ if attrs["background-url"] && !attrs["background-url"].empty?
215
+ parts << %(url('#{attrs["background-url"]}'))
216
+ parts << "no-repeat"
217
+ parts << "#{attrs["background-position"]} / cover"
218
+ end
219
+ parts.compact.join(" ")
220
+ end
221
+
222
+ def background_ratio(attrs, container_width)
223
+ background_height = parse_unit_value(attrs["background-height"])
224
+ background_width = parse_unit_value(attrs["background-width"] || container_width)
225
+ return 0 if background_width.zero?
226
+
227
+ ((background_height / background_width) * 100).round
228
+ end
229
+
230
+ def with_container_width(context, width)
231
+ previous = context[:container_width]
232
+ context[:container_width] = width
233
+ yield
234
+ ensure
235
+ context[:container_width] = previous
236
+ end
237
+
238
+ def hero_container_width(attrs, container_width)
239
+ width = parse_unit_value(container_width)
240
+ content_width = width - padding_side(attrs, "left") - padding_side(attrs, "right")
241
+ "#{[content_width, 0].max}px"
242
+ end
243
+
244
+ def normalize_container_width(value)
245
+ "#{parse_unit_value(value)}px"
246
+ end
247
+
248
+ def padding_side(attrs, side)
249
+ specific = attrs["padding-#{side}"]
250
+ return parse_unit_value(specific) unless blank?(specific)
251
+
252
+ padding_shorthand_value(attrs["padding"], side)
253
+ end
254
+
255
+ def padding_shorthand_value(value, side)
256
+ return 0 if blank?(value)
257
+
258
+ parts = value.to_s.strip.split(/\s+/)
259
+ case parts.length
260
+ when 1
261
+ parse_unit_value(parts[0])
262
+ when 2
263
+ %w[left right].include?(side) ? parse_unit_value(parts[1]) : parse_unit_value(parts[0])
264
+ when 3
265
+ %w[left right].include?(side) ? parse_unit_value(parts[1]) : parse_unit_value(side == "top" ? parts[0] : parts[2])
266
+ when 4
267
+ parse_unit_value(parts[side == "left" ? 3 : 1])
268
+ else
269
+ 0
270
+ end
271
+ end
272
+
273
+ def parse_unit_value(value)
274
+ return 0 if blank?(value)
275
+
276
+ match = value.to_s.match(/-?\d+(?:\.\d+)?/)
277
+ match ? match[0].to_f : 0
278
+ end
279
+
280
+ def blank?(value)
281
+ value.nil? || value.to_s.empty?
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class HtmlAttributes < Base
6
+ TAGS = %w[mj-selector mj-html-attribute].freeze
7
+
8
+ ALLOWED_ATTRIBUTES = {
9
+ "mj-selector" => {
10
+ "path" => "string"
11
+ },
12
+ "mj-html-attribute" => {
13
+ "name" => "string"
14
+ }
15
+ }.freeze
16
+
17
+ class << self
18
+ def allowed_attributes_for(tag_name)
19
+ ALLOWED_ATTRIBUTES[tag_name] || {}
20
+ end
21
+ end
22
+
23
+ def self.allowed_attributes
24
+ {}
25
+ end
26
+
27
+ def render(tag_name:, node:, context:, attrs:, parent:)
28
+ render_children(node, context, parent: parent)
29
+ end
30
+ end
31
+ end
32
+ end