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,183 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Image < Base
6
+ TAGS = ["mj-image"].freeze
7
+
8
+ DEFAULTS = {
9
+ "alt" => "",
10
+ "align" => "center",
11
+ "border" => "0",
12
+ "height" => "auto",
13
+ "padding" => "10px 25px",
14
+ "target" => "_blank",
15
+ "font-size" => "13px"
16
+ }.freeze
17
+
18
+ HEAD_STYLE = <<~CSS.freeze
19
+ @media only screen and (max-width:480px) {
20
+ table.mj-full-width-mobile { width: 100% !important; }
21
+ td.mj-full-width-mobile { width: auto !important; }
22
+ }
23
+ CSS
24
+
25
+ def tags
26
+ TAGS
27
+ end
28
+
29
+ def head_style
30
+ HEAD_STYLE
31
+ end
32
+
33
+ def head_style_tags
34
+ ["mj-image"]
35
+ end
36
+
37
+ def render(tag_name:, node:, context:, attrs:, parent:)
38
+ a = DEFAULTS.merge(attrs)
39
+
40
+ fluid = a["fluid-on-mobile"] == "true"
41
+ full_width = a["full-width"] == "full-width"
42
+ content_width = get_content_width(a, context)
43
+
44
+ outer_td_style = style_join(
45
+ "background" => a["container-background-color"],
46
+ "font-size" => "0px",
47
+ "padding" => a["padding"],
48
+ "padding-top" => a["padding-top"],
49
+ "padding-right" => a["padding-right"],
50
+ "padding-bottom" => a["padding-bottom"],
51
+ "padding-left" => a["padding-left"],
52
+ "word-break" => "break-word"
53
+ )
54
+ outer_td_attrs = {
55
+ "align" => a["align"],
56
+ "class" => a["css-class"],
57
+ "style" => outer_td_style
58
+ }
59
+
60
+ table_style = style_join(
61
+ "min-width" => full_width ? "100%" : nil,
62
+ "max-width" => full_width ? "100%" : nil,
63
+ "width" => full_width ? "#{content_width}px" : nil,
64
+ "border-collapse" => "collapse",
65
+ "border-spacing" => "0px"
66
+ )
67
+ table_attrs = {
68
+ "border" => "0",
69
+ "cellpadding" => "0",
70
+ "cellspacing" => "0",
71
+ "role" => "presentation",
72
+ "style" => table_style,
73
+ "class" => fluid ? "mj-full-width-mobile" : nil
74
+ }
75
+
76
+ td_attrs = {
77
+ "style" => style_join("width" => full_width ? nil : "#{content_width}px"),
78
+ "class" => fluid ? "mj-full-width-mobile" : nil,
79
+ "width" => full_width ? nil : content_width.to_i.to_s,
80
+ "height" => height_attribute(a["height"]),
81
+ "align" => a["align"]
82
+ }
83
+
84
+ img_style = style_join(
85
+ "border" => a["border"],
86
+ "border-left" => a["border-left"],
87
+ "border-right" => a["border-right"],
88
+ "border-top" => a["border-top"],
89
+ "border-bottom" => a["border-bottom"],
90
+ "border-radius" => a["border-radius"],
91
+ "display" => "block",
92
+ "outline" => "none",
93
+ "text-decoration" => "none",
94
+ "height" => a["height"],
95
+ "max-height" => a["max-height"],
96
+ "min-width" => full_width ? "100%" : nil,
97
+ "width" => "100%",
98
+ "max-width" => full_width ? "100%" : nil,
99
+ "font-size" => a["font-size"]
100
+ )
101
+
102
+ img_attrs = {
103
+ "alt" => a["alt"],
104
+ "src" => a["src"],
105
+ "srcset" => a["srcset"],
106
+ "sizes" => a["sizes"],
107
+ "style" => img_style,
108
+ "title" => a["title"],
109
+ "width" => img_width_attribute(a, content_width),
110
+ "usemap" => a["usemap"],
111
+ "height" => height_attribute(a["height"])
112
+ }
113
+
114
+ img_tag = "<img#{html_attrs(img_attrs)} />"
115
+
116
+ inner = if a["href"]
117
+ link_attrs = {
118
+ "href" => a["href"],
119
+ "target" => a["target"],
120
+ "rel" => a["rel"],
121
+ "name" => a["name"],
122
+ "title" => a["title"]
123
+ }
124
+ "<a#{html_attrs(link_attrs)}>#{img_tag}</a>"
125
+ else
126
+ img_tag
127
+ end
128
+
129
+ table = %(<table#{html_attrs(table_attrs)}><tbody><tr><td#{html_attrs(td_attrs)}>#{inner}</td></tr></tbody></table>)
130
+
131
+ %(<tr><td#{html_attrs(outer_td_attrs)}>#{table}</td></tr>)
132
+ end
133
+
134
+ private
135
+
136
+ def height_attribute(height)
137
+ return if height.nil? || height.empty?
138
+
139
+ height == "auto" ? "auto" : height.to_i.to_s
140
+ end
141
+
142
+ def img_width_attribute(attrs, content_width)
143
+ return "auto" if attrs["width"] && attrs["height"] && attrs["height"] != "auto"
144
+ return attrs["width"].to_i.to_s if attrs["width"] && attrs["width"] =~ /^(\d+(?:\.\d+)?)\s*px$/
145
+
146
+ content_width.to_i.to_s
147
+ end
148
+
149
+ def get_content_width(attrs, context)
150
+ container_width = (context[:container_width] || "600px").to_f
151
+
152
+ padding = attrs["padding"] || "10px 25px"
153
+ parts = padding.split(/\s+/)
154
+ pad_left = shorthand_value(parts, :left).to_f
155
+ pad_right = shorthand_value(parts, :right).to_f
156
+
157
+ if attrs["padding-left"]
158
+ pad_left = attrs["padding-left"].to_f
159
+ end
160
+ if attrs["padding-right"]
161
+ pad_right = attrs["padding-right"].to_f
162
+ end
163
+
164
+ box = container_width - pad_left - pad_right
165
+
166
+ if attrs["width"] && attrs["width"] =~ /^(\d+(?:\.\d+)?)\s*px$/
167
+ [box, ::Regexp.last_match(1).to_f].min.to_i
168
+ else
169
+ box.to_i
170
+ end
171
+ end
172
+
173
+ def shorthand_value(parts, side)
174
+ case parts.length
175
+ when 1 then parts[0]
176
+ when 2, 3 then parts[1]
177
+ when 4 then side == :left ? parts[3] : parts[1]
178
+ else "0"
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,279 @@
1
+ require "securerandom"
2
+
3
+ require_relative "base"
4
+
5
+ module MjmlRb
6
+ module Components
7
+ class Navbar < Base
8
+ TAGS = %w[mj-navbar mj-navbar-link].freeze
9
+
10
+ NAVBAR_ALLOWED_ATTRIBUTES = {
11
+ "align" => "enum(left,center,right)",
12
+ "base-url" => "string",
13
+ "hamburger" => "string",
14
+ "ico-align" => "enum(left,center,right)",
15
+ "ico-open" => "string",
16
+ "ico-close" => "string",
17
+ "ico-color" => "color",
18
+ "ico-font-size" => "unit(px,%)",
19
+ "ico-font-family" => "string",
20
+ "ico-text-transform" => "string",
21
+ "ico-padding" => "unit(px,%){1,4}",
22
+ "ico-padding-left" => "unit(px,%)",
23
+ "ico-padding-top" => "unit(px,%)",
24
+ "ico-padding-right" => "unit(px,%)",
25
+ "ico-padding-bottom" => "unit(px,%)",
26
+ "padding" => "unit(px,%){1,4}",
27
+ "padding-left" => "unit(px,%)",
28
+ "padding-top" => "unit(px,%)",
29
+ "padding-right" => "unit(px,%)",
30
+ "padding-bottom" => "unit(px,%)",
31
+ "ico-text-decoration" => "string",
32
+ "ico-line-height" => "unit(px,%)"
33
+ }.freeze
34
+
35
+ NAVBAR_LINK_ALLOWED_ATTRIBUTES = {
36
+ "color" => "color",
37
+ "font-family" => "string",
38
+ "font-size" => "unit(px)",
39
+ "font-style" => "string",
40
+ "font-weight" => "string",
41
+ "href" => "string",
42
+ "name" => "string",
43
+ "target" => "string",
44
+ "rel" => "string",
45
+ "letter-spacing" => "string",
46
+ "line-height" => "unit(px,%)",
47
+ "padding-bottom" => "unit(px,%)",
48
+ "padding-left" => "unit(px,%)",
49
+ "padding-right" => "unit(px,%)",
50
+ "padding-top" => "unit(px,%)",
51
+ "padding" => "unit(px,%){1,4}",
52
+ "text-decoration" => "string",
53
+ "text-transform" => "string"
54
+ }.freeze
55
+
56
+ NAVBAR_DEFAULT_ATTRIBUTES = {
57
+ "align" => "center",
58
+ "base-url" => nil,
59
+ "hamburger" => nil,
60
+ "ico-align" => "center",
61
+ "ico-open" => "&#9776;",
62
+ "ico-close" => "&#8855;",
63
+ "ico-color" => "#000000",
64
+ "ico-font-size" => "30px",
65
+ "ico-font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
66
+ "ico-text-transform" => "uppercase",
67
+ "ico-padding" => "10px",
68
+ "ico-text-decoration" => "none",
69
+ "ico-line-height" => "30px"
70
+ }.freeze
71
+
72
+ NAVBAR_LINK_DEFAULT_ATTRIBUTES = {
73
+ "color" => "#000000",
74
+ "font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
75
+ "font-size" => "13px",
76
+ "font-weight" => "normal",
77
+ "line-height" => "22px",
78
+ "padding" => "15px 10px",
79
+ "target" => "_blank",
80
+ "text-decoration" => "none",
81
+ "text-transform" => "uppercase"
82
+ }.freeze
83
+
84
+ class << self
85
+ def allowed_attributes_for(tag_name)
86
+ tag_name == "mj-navbar-link" ? NAVBAR_LINK_ALLOWED_ATTRIBUTES : NAVBAR_ALLOWED_ATTRIBUTES
87
+ end
88
+
89
+ def allowed_attributes
90
+ NAVBAR_ALLOWED_ATTRIBUTES
91
+ end
92
+ end
93
+
94
+ def tags
95
+ TAGS
96
+ end
97
+
98
+ def head_style(breakpoint)
99
+ lower_breakpoint = make_lower_breakpoint(breakpoint)
100
+
101
+ <<~CSS
102
+ noinput.mj-menu-checkbox { display:block!important; max-height:none!important; visibility:visible!important; }
103
+
104
+ @media only screen and (max-width:#{lower_breakpoint}) {
105
+ .mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links { display:none!important; }
106
+ .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-inline-links,
107
+ .mj-menu-checkbox[type="checkbox"] ~ .mj-menu-trigger { display:block!important; max-width:none!important; max-height:none!important; font-size:inherit!important; }
108
+ .mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links > a { display:block!important; }
109
+ .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-menu-trigger .mj-menu-icon-close { display:block!important; }
110
+ .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-menu-trigger .mj-menu-icon-open { display:none!important; }
111
+ }
112
+ CSS
113
+ end
114
+
115
+ def head_style_tags
116
+ ["mj-navbar"]
117
+ end
118
+
119
+ def render(tag_name:, node:, context:, attrs:, parent:)
120
+ case tag_name
121
+ when "mj-navbar"
122
+ render_navbar(node, context, attrs)
123
+ when "mj-navbar-link"
124
+ render_navbar_link(node, context, attrs, parent: parent)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def render_navbar(node, context, attrs)
131
+ a = NAVBAR_DEFAULT_ATTRIBUTES.merge(attrs)
132
+ align = a["align"]
133
+
134
+ outer_td_style = style_join(
135
+ "font-size" => "0px",
136
+ "padding" => a["padding"],
137
+ "padding-top" => a["padding-top"],
138
+ "padding-right" => a["padding-right"],
139
+ "padding-bottom" => a["padding-bottom"],
140
+ "padding-left" => a["padding-left"],
141
+ "text-align" => align,
142
+ "word-break" => "break-word"
143
+ )
144
+ outer_td_attrs = {
145
+ "align" => align,
146
+ "class" => a["css-class"],
147
+ "style" => outer_td_style
148
+ }
149
+
150
+ previous_base_url = context[:navbar_base_url]
151
+ context[:navbar_base_url] = a["base-url"]
152
+
153
+ inner = +""
154
+ inner << render_hamburger(a) if a["hamburger"] == "hamburger"
155
+ inner << %(<div class="mj-inline-links" style="width:100%;text-align:#{escape_attr(align)};">)
156
+ inner << %(<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0" align="#{escape_attr(align)}"><tr><![endif]-->)
157
+ inner << render_navbar_children(node, context)
158
+ inner << %(<!--[if mso | IE]></tr></table><![endif]-->)
159
+ inner << %(</div>)
160
+
161
+ %(<tr><td#{html_attrs(outer_td_attrs)}>#{inner}</td></tr>)
162
+ ensure
163
+ context[:navbar_base_url] = previous_base_url
164
+ end
165
+
166
+ def render_navbar_children(node, context)
167
+ node.element_children.map do |child|
168
+ case child.tag_name
169
+ when "mj-navbar-link", "mj-raw"
170
+ render_node(child, context, parent: "mj-navbar")
171
+ else
172
+ ""
173
+ end
174
+ end.join
175
+ end
176
+
177
+ def render_navbar_link(node, context, attrs, parent:)
178
+ a = NAVBAR_LINK_DEFAULT_ATTRIBUTES.merge(attrs)
179
+ href = joined_href(a["href"], context[:navbar_base_url])
180
+ css_class = a["css-class"]
181
+ anchor_class = ["mj-link", css_class].compact.join(" ")
182
+ td_class = suffix_css_classes(css_class, "outlook")
183
+
184
+ anchor_style = style_join(
185
+ "display" => "inline-block",
186
+ "color" => a["color"],
187
+ "font-family" => a["font-family"],
188
+ "font-size" => a["font-size"],
189
+ "font-style" => a["font-style"],
190
+ "font-weight" => a["font-weight"],
191
+ "letter-spacing" => a["letter-spacing"],
192
+ "line-height" => a["line-height"],
193
+ "text-decoration" => a["text-decoration"],
194
+ "text-transform" => a["text-transform"],
195
+ "padding" => a["padding"],
196
+ "padding-top" => a["padding-top"],
197
+ "padding-left" => a["padding-left"],
198
+ "padding-right" => a["padding-right"],
199
+ "padding-bottom" => a["padding-bottom"]
200
+ )
201
+ link_attrs = {
202
+ "class" => anchor_class,
203
+ "href" => href,
204
+ "rel" => a["rel"],
205
+ "target" => a["target"],
206
+ "name" => a["name"],
207
+ "style" => anchor_style
208
+ }
209
+
210
+ content = raw_inner(node)
211
+ link = %(<a#{html_attrs(link_attrs)}>#{content}</a>)
212
+ return link unless parent == "mj-navbar"
213
+
214
+ td_style = style_join(
215
+ "padding" => a["padding"],
216
+ "padding-top" => a["padding-top"],
217
+ "padding-left" => a["padding-left"],
218
+ "padding-right" => a["padding-right"],
219
+ "padding-bottom" => a["padding-bottom"]
220
+ )
221
+
222
+ %(<!--[if mso | IE]><td#{html_attrs("class" => td_class, "style" => td_style)}><![endif]-->#{link}<!--[if mso | IE]></td><![endif]-->)
223
+ end
224
+
225
+ def render_hamburger(attrs)
226
+ label_key = SecureRandom.hex(8)
227
+ trigger_style = style_join(
228
+ "display" => "none",
229
+ "max-height" => "0px",
230
+ "max-width" => "0px",
231
+ "font-size" => "0px",
232
+ "overflow" => "hidden"
233
+ )
234
+ label_style = style_join(
235
+ "display" => "block",
236
+ "cursor" => "pointer",
237
+ "mso-hide" => "all",
238
+ "-moz-user-select" => "none",
239
+ "user-select" => "none",
240
+ "color" => attrs["ico-color"],
241
+ "font-size" => attrs["ico-font-size"],
242
+ "font-family" => attrs["ico-font-family"],
243
+ "text-transform" => attrs["ico-text-transform"],
244
+ "text-decoration" => attrs["ico-text-decoration"],
245
+ "line-height" => attrs["ico-line-height"],
246
+ "padding" => attrs["ico-padding"],
247
+ "padding-top" => attrs["ico-padding-top"],
248
+ "padding-right" => attrs["ico-padding-right"],
249
+ "padding-bottom" => attrs["ico-padding-bottom"],
250
+ "padding-left" => attrs["ico-padding-left"]
251
+ )
252
+
253
+ checkbox = %(<!--[if !mso]><!--><input type="checkbox" id="#{escape_attr(label_key)}" class="mj-menu-checkbox" style="display:none !important; max-height:0; visibility:hidden;" /><!--<![endif]-->)
254
+ trigger = %(<div class="mj-menu-trigger" style="#{trigger_style}"><label for="#{escape_attr(label_key)}" class="mj-menu-label" align="#{escape_attr(attrs["ico-align"])}" style="#{label_style}"><span class="mj-menu-icon-open" style="mso-hide:all;">#{attrs["ico-open"]}</span><span class="mj-menu-icon-close" style="display:none;mso-hide:all;">#{attrs["ico-close"]}</span></label></div>)
255
+
256
+ "#{checkbox}#{trigger}"
257
+ end
258
+
259
+ def joined_href(href, base_url)
260
+ return href if href.nil? || href.empty? || base_url.nil? || base_url.empty?
261
+
262
+ "#{base_url}#{href}"
263
+ end
264
+
265
+ def suffix_css_classes(classes, suffix)
266
+ return nil if classes.nil? || classes.empty?
267
+
268
+ classes.split(/\s+/).map { |klass| "#{klass}-#{suffix}" }.join(" ")
269
+ end
270
+
271
+ def make_lower_breakpoint(breakpoint)
272
+ pixels = breakpoint.to_s[/\d+/]
273
+ return breakpoint if pixels.nil?
274
+
275
+ "#{pixels.to_i - 1}px"
276
+ end
277
+ end
278
+ end
279
+ end