emjay 0.1.0

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +158 -0
  4. data/lib/emjay/body_component.rb +142 -0
  5. data/lib/emjay/component.rb +61 -0
  6. data/lib/emjay/components/body/mj_accordion.rb +99 -0
  7. data/lib/emjay/components/body/mj_accordion_element.rb +127 -0
  8. data/lib/emjay/components/body/mj_accordion_text.rb +123 -0
  9. data/lib/emjay/components/body/mj_accordion_title.rb +171 -0
  10. data/lib/emjay/components/body/mj_body.rb +70 -0
  11. data/lib/emjay/components/body/mj_button.rb +198 -0
  12. data/lib/emjay/components/body/mj_carousel.rb +410 -0
  13. data/lib/emjay/components/body/mj_carousel_image.rb +188 -0
  14. data/lib/emjay/components/body/mj_column.rb +287 -0
  15. data/lib/emjay/components/body/mj_divider.rb +120 -0
  16. data/lib/emjay/components/body/mj_group.rb +196 -0
  17. data/lib/emjay/components/body/mj_hero.rb +382 -0
  18. data/lib/emjay/components/body/mj_image.rb +188 -0
  19. data/lib/emjay/components/body/mj_navbar.rb +187 -0
  20. data/lib/emjay/components/body/mj_navbar_link.rb +129 -0
  21. data/lib/emjay/components/body/mj_raw.rb +34 -0
  22. data/lib/emjay/components/body/mj_section.rb +442 -0
  23. data/lib/emjay/components/body/mj_social.rb +174 -0
  24. data/lib/emjay/components/body/mj_social_element.rb +272 -0
  25. data/lib/emjay/components/body/mj_spacer.rb +57 -0
  26. data/lib/emjay/components/body/mj_table.rb +113 -0
  27. data/lib/emjay/components/body/mj_text.rb +100 -0
  28. data/lib/emjay/components/body/mj_wrapper.rb +56 -0
  29. data/lib/emjay/components/head/mj_attributes.rb +38 -0
  30. data/lib/emjay/components/head/mj_breakpoint.rb +28 -0
  31. data/lib/emjay/components/head/mj_font.rb +24 -0
  32. data/lib/emjay/components/head/mj_head.rb +20 -0
  33. data/lib/emjay/components/head/mj_html_attributes.rb +33 -0
  34. data/lib/emjay/components/head/mj_preview.rb +24 -0
  35. data/lib/emjay/components/head/mj_style.rb +34 -0
  36. data/lib/emjay/components/head/mj_title.rb +24 -0
  37. data/lib/emjay/global_data.rb +64 -0
  38. data/lib/emjay/head_component.rb +37 -0
  39. data/lib/emjay/helpers/conditional_tag.rb +24 -0
  40. data/lib/emjay/helpers/fonts.rb +34 -0
  41. data/lib/emjay/helpers/gen_random_hex_string.rb +9 -0
  42. data/lib/emjay/helpers/make_lower_breakpoint.rb +17 -0
  43. data/lib/emjay/helpers/media_queries.rb +47 -0
  44. data/lib/emjay/helpers/merge_outlook_conditionals.rb +11 -0
  45. data/lib/emjay/helpers/minify_outlook_conditionals.rb +18 -0
  46. data/lib/emjay/helpers/shorthand_parser.rb +33 -0
  47. data/lib/emjay/helpers/styles.rb +34 -0
  48. data/lib/emjay/helpers/suffix_css_classes.rb +12 -0
  49. data/lib/emjay/helpers/width_parser.rb +26 -0
  50. data/lib/emjay/rails/mail_interceptor.rb +37 -0
  51. data/lib/emjay/rails/template_handler.rb +16 -0
  52. data/lib/emjay/railtie.rb +21 -0
  53. data/lib/emjay/registry.rb +19 -0
  54. data/lib/emjay/renderer.rb +302 -0
  55. data/lib/emjay/skeleton.rb +80 -0
  56. data/lib/emjay/version.rb +5 -0
  57. data/lib/emjay.rb +66 -0
  58. data/llms.txt +130 -0
  59. metadata +129 -0
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/width_parser"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjColumn < BodyComponent
10
+ def self.component_name
11
+ "mj-column"
12
+ end
13
+
14
+ def self.default_attributes
15
+ {
16
+ "direction" => "ltr",
17
+ "vertical-align" => "top"
18
+ }
19
+ end
20
+
21
+ def self.allowed_attributes
22
+ {
23
+ "background-color" => "color",
24
+ "border" => "string",
25
+ "border-bottom" => "string",
26
+ "border-left" => "string",
27
+ "border-radius" => "string",
28
+ "border-right" => "string",
29
+ "border-top" => "string",
30
+ "direction" => "enum(ltr,rtl)",
31
+ "inner-background-color" => "color",
32
+ "padding-bottom" => "unit(px,%)",
33
+ "padding-left" => "unit(px,%)",
34
+ "padding-right" => "unit(px,%)",
35
+ "padding-top" => "unit(px,%)",
36
+ "inner-border" => "string",
37
+ "inner-border-bottom" => "string",
38
+ "inner-border-left" => "string",
39
+ "inner-border-radius" => "string",
40
+ "inner-border-right" => "string",
41
+ "inner-border-top" => "string",
42
+ "padding" => "unit(px,%){1,4}",
43
+ "vertical-align" => "enum(top,bottom,middle)",
44
+ "width" => "unit(px,%)"
45
+ }
46
+ end
47
+
48
+ def get_child_context
49
+ parent_width = @context[:container_width]
50
+ non_raw_siblings = @props[:non_raw_siblings] || 1
51
+ borders = get_shorthand_border_value("right") + get_shorthand_border_value("left")
52
+ paddings = get_shorthand_attr_value("padding", "right") + get_shorthand_attr_value("padding", "left")
53
+ inner_borders = get_shorthand_border_value("left", "inner-border") +
54
+ get_shorthand_border_value("right", "inner-border")
55
+
56
+ all_paddings = paddings + borders + inner_borders
57
+
58
+ container_width = get_attribute("width") || "#{parent_width.to_f / non_raw_siblings}px"
59
+
60
+ parsed = WidthParser.call(container_width, parse_float_to_int: false)
61
+
62
+ container_width = if parsed[:unit] == "%"
63
+ "#{(parent_width.to_f * parsed[:parsed_width]) / 100 - all_paddings}px"
64
+ else
65
+ "#{parsed[:parsed_width] - all_paddings}px"
66
+ end
67
+
68
+ @context.merge(container_width: container_width)
69
+ end
70
+
71
+ def get_styles
72
+ has_br = has_border_radius?
73
+ has_ibr = has_inner_border_radius?
74
+
75
+ table_style = {
76
+ "background-color" => get_attribute("background-color"),
77
+ "border" => get_attribute("border"),
78
+ "border-bottom" => get_attribute("border-bottom"),
79
+ "border-left" => get_attribute("border-left"),
80
+ "border-radius" => get_attribute("border-radius"),
81
+ "border-right" => get_attribute("border-right"),
82
+ "border-top" => get_attribute("border-top"),
83
+ "vertical-align" => get_attribute("vertical-align"),
84
+ **(has_br ? {"border-collapse" => "separate"} : {})
85
+ }
86
+
87
+ {
88
+ div: {
89
+ "font-size" => "0px",
90
+ "text-align" => "left",
91
+ "direction" => get_attribute("direction"),
92
+ "display" => "inline-block",
93
+ "vertical-align" => get_attribute("vertical-align"),
94
+ "width" => get_mobile_width
95
+ },
96
+ table: {
97
+ **(has_gutter? ? {
98
+ "background-color" => get_attribute("inner-background-color"),
99
+ "border" => get_attribute("inner-border"),
100
+ "border-bottom" => get_attribute("inner-border-bottom"),
101
+ "border-left" => get_attribute("inner-border-left"),
102
+ "border-radius" => get_attribute("inner-border-radius"),
103
+ "border-right" => get_attribute("inner-border-right"),
104
+ "border-top" => get_attribute("inner-border-top")
105
+ } : table_style),
106
+ **(has_ibr ? {"border-collapse" => "separate"} : {})
107
+ },
108
+ tdOutlook: {
109
+ "vertical-align" => get_attribute("vertical-align"),
110
+ "width" => get_width_as_pixel
111
+ },
112
+ gutter: {
113
+ **table_style,
114
+ "padding" => get_attribute("padding"),
115
+ "padding-top" => get_attribute("padding-top"),
116
+ "padding-right" => get_attribute("padding-right"),
117
+ "padding-bottom" => get_attribute("padding-bottom"),
118
+ "padding-left" => get_attribute("padding-left")
119
+ }
120
+ }
121
+ end
122
+
123
+ def render
124
+ classes_name = "#{get_column_class} mj-outlook-group-fix"
125
+ css_class = get_attribute("css-class")
126
+ classes_name += " #{css_class}" if css_class
127
+
128
+ <<~HTML
129
+ <div#{html_attributes(class: classes_name, style: :div)}>
130
+ #{has_gutter? ? render_gutter : render_column}
131
+ </div>
132
+ HTML
133
+ end
134
+
135
+ private
136
+
137
+ def get_mobile_width
138
+ container_width = @context[:container_width]
139
+ non_raw_siblings = @props[:non_raw_siblings] || 1
140
+ width = get_attribute("width")
141
+ mobile_width = get_attribute("mobileWidth")
142
+
143
+ return "100%" if mobile_width != "mobileWidth"
144
+
145
+ return "#{(100 / non_raw_siblings).to_i}%" unless width
146
+
147
+ parsed = WidthParser.call(width, parse_float_to_int: false)
148
+
149
+ case parsed[:unit]
150
+ when "%" then width
151
+ else
152
+ "#{(parsed[:parsed_width] / container_width.to_f) * 100}%"
153
+ end
154
+ end
155
+
156
+ def get_width_as_pixel
157
+ container_width = @context[:container_width]
158
+ parsed = WidthParser.call(get_parsed_width(true), parse_float_to_int: false)
159
+
160
+ if parsed[:unit] == "%"
161
+ "#{format_float((container_width.to_f * parsed[:parsed_width]) / 100)}px"
162
+ else
163
+ "#{format_float(parsed[:parsed_width])}px"
164
+ end
165
+ end
166
+
167
+ def get_parsed_width(to_string = false)
168
+ non_raw_siblings = @props[:non_raw_siblings] || 1
169
+ width = get_attribute("width") || "#{100.0 / non_raw_siblings}%"
170
+
171
+ parsed = WidthParser.call(width, parse_float_to_int: false)
172
+
173
+ if to_string
174
+ "#{format_float(parsed[:parsed_width])}#{parsed[:unit]}"
175
+ else
176
+ parsed
177
+ end
178
+ end
179
+
180
+ def get_column_class
181
+ add_media_query = @context[:add_media_query]
182
+
183
+ parsed = get_parsed_width
184
+ formatted = format_float(parsed[:parsed_width]).to_s.tr(".", "-")
185
+
186
+ class_name = case parsed[:unit]
187
+ when "%" then "mj-column-per-#{formatted}"
188
+ else "mj-column-px-#{formatted}"
189
+ end
190
+
191
+ add_media_query&.call(class_name, parsed)
192
+
193
+ class_name
194
+ end
195
+
196
+ # Formats a float to match JS toString() — strips trailing .0
197
+ def format_float(value)
198
+ (value == value.to_i) ? value.to_i : value
199
+ end
200
+
201
+ def has_border_radius?
202
+ br = get_attribute("border-radius")
203
+ br && !br.empty?
204
+ end
205
+
206
+ def has_inner_border_radius?
207
+ ibr = get_attribute("inner-border-radius")
208
+ ibr && !ibr.empty?
209
+ end
210
+
211
+ def has_gutter?
212
+ %w[padding padding-bottom padding-left padding-right padding-top].any? { |attr|
213
+ !get_attribute(attr).nil?
214
+ }
215
+ end
216
+
217
+ def render_gutter
218
+ has_br = has_border_radius?
219
+
220
+ <<~HTML
221
+ <table#{html_attributes(
222
+ border: "0",
223
+ cellpadding: "0",
224
+ cellspacing: "0",
225
+ role: "presentation",
226
+ width: "100%",
227
+ **(has_br ? {style: {"border-collapse" => "separate"}} : {})
228
+ )}>
229
+ <tbody>
230
+ <tr>
231
+ <td#{html_attributes(style: :gutter)}>
232
+ #{render_column}
233
+ </td>
234
+ </tr>
235
+ </tbody>
236
+ </table>
237
+ HTML
238
+ end
239
+
240
+ def render_column
241
+ children = @props[:children] || []
242
+
243
+ <<~HTML
244
+ <table#{html_attributes(
245
+ border: "0",
246
+ cellpadding: "0",
247
+ cellspacing: "0",
248
+ role: "presentation",
249
+ style: :table,
250
+ width: "100%"
251
+ )}>
252
+ <tbody>
253
+ #{render_children(children, renderer: ->(component) {
254
+ if component.class.raw_element?
255
+ component.render
256
+ else
257
+ <<~CELL
258
+ <tr>
259
+ <td#{component.html_attributes(
260
+ align: component.get_attribute("align"),
261
+ class: component.get_attribute("css-class"),
262
+ style: {
263
+ "background" => component.get_attribute("container-background-color"),
264
+ "font-size" => "0px",
265
+ "padding" => component.get_attribute("padding"),
266
+ "padding-top" => component.get_attribute("padding-top"),
267
+ "padding-right" => component.get_attribute("padding-right"),
268
+ "padding-bottom" => component.get_attribute("padding-bottom"),
269
+ "padding-left" => component.get_attribute("padding-left"),
270
+ "word-break" => "break-word"
271
+ }
272
+ )}>
273
+ #{component.render}
274
+ </td>
275
+ </tr>
276
+ CELL
277
+ end
278
+ })}
279
+ </tbody>
280
+ </table>
281
+ HTML
282
+ end
283
+ end
284
+ end
285
+
286
+ Registry.register(Components::MjColumn)
287
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/width_parser"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjDivider < BodyComponent
10
+ def self.component_name
11
+ "mj-divider"
12
+ end
13
+
14
+ def self.default_attributes
15
+ {
16
+ "border-color" => "#000000",
17
+ "border-style" => "solid",
18
+ "border-width" => "4px",
19
+ "padding" => "10px 25px",
20
+ "width" => "100%",
21
+ "align" => "center"
22
+ }
23
+ end
24
+
25
+ def self.allowed_attributes
26
+ {
27
+ "border-color" => "color",
28
+ "border-style" => "string",
29
+ "border-width" => "unit(px)",
30
+ "container-background-color" => "color",
31
+ "padding" => "unit(px,%){1,4}",
32
+ "padding-bottom" => "unit(px,%)",
33
+ "padding-left" => "unit(px,%)",
34
+ "padding-right" => "unit(px,%)",
35
+ "padding-top" => "unit(px,%)",
36
+ "width" => "unit(px,%)",
37
+ "align" => "enum(left,center,right)"
38
+ }
39
+ end
40
+
41
+ def get_styles
42
+ compute_align = case get_attribute("align")
43
+ when "left"
44
+ "0px"
45
+ when "right"
46
+ "0px 0px 0px auto"
47
+ else
48
+ "0px auto"
49
+ end
50
+
51
+ border_top = ["style", "width", "color"].map { |attr|
52
+ get_attribute("border-#{attr}")
53
+ }.join(" ")
54
+
55
+ p_styles = {
56
+ "border-top" => border_top,
57
+ "font-size" => "1px",
58
+ "margin" => compute_align,
59
+ "width" => get_attribute("width")
60
+ }
61
+
62
+ {
63
+ p: p_styles,
64
+ outlook: p_styles.merge("width" => get_outlook_width)
65
+ }
66
+ end
67
+
68
+ def render
69
+ p_attrs = html_attributes(style: :p)
70
+ <<~HTML
71
+ <p
72
+ #{p_attrs}
73
+ >
74
+ </p>
75
+ #{render_after}
76
+ HTML
77
+ end
78
+
79
+ private
80
+
81
+ def get_outlook_width
82
+ container_width = @context[:container_width]
83
+ padding_size = get_shorthand_attr_value("padding", "left") +
84
+ get_shorthand_attr_value("padding", "right")
85
+
86
+ width = get_attribute("width")
87
+ parsed = WidthParser.call(width)
88
+
89
+ case parsed[:unit]
90
+ when "%"
91
+ effective_width = container_width.to_i - padding_size
92
+ percent_multiplier = parsed[:parsed_width].to_i / 100.0
93
+ "#{(effective_width * percent_multiplier).to_i}px"
94
+ when "px"
95
+ width
96
+ else
97
+ "#{container_width.to_i - padding_size}px"
98
+ end
99
+ end
100
+
101
+ def render_after
102
+ outlook_attrs = html_attributes(
103
+ align: get_attribute("align"),
104
+ border: "0",
105
+ cellpadding: "0",
106
+ cellspacing: "0",
107
+ style: :outlook,
108
+ role: "presentation",
109
+ width: get_outlook_width
110
+ )
111
+ <<~HTML
112
+ <!--[if mso | IE]><table#{outlook_attrs} ><tr><td style="height:0;line-height:0;"> &nbsp;
113
+ </td></tr></table><![endif]-->
114
+ HTML
115
+ end
116
+ end
117
+ end
118
+
119
+ Registry.register(Components::MjDivider)
120
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/width_parser"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjGroup < BodyComponent
10
+ def self.component_name
11
+ "mj-group"
12
+ end
13
+
14
+ def self.allowed_attributes
15
+ {
16
+ "background-color" => "color",
17
+ "direction" => "enum(ltr,rtl)",
18
+ "vertical-align" => "enum(top,bottom,middle)",
19
+ "width" => "unit(px,%)"
20
+ }
21
+ end
22
+
23
+ def self.default_attributes
24
+ {"direction" => "ltr"}
25
+ end
26
+
27
+ def get_child_context
28
+ parent_width = @context[:container_width]
29
+ non_raw_siblings = @props[:non_raw_siblings] || 1
30
+ padding_size = get_shorthand_attr_value("padding", "left") +
31
+ get_shorthand_attr_value("padding", "right")
32
+
33
+ container_width = get_attribute("width") ||
34
+ "#{parent_width.to_f / non_raw_siblings}px"
35
+
36
+ parsed = WidthParser.call(container_width, parse_float_to_int: false)
37
+
38
+ container_width = if parsed[:unit] == "%"
39
+ "#{(parent_width.to_f * parsed[:parsed_width]) / 100 - padding_size}px"
40
+ else
41
+ "#{parsed[:parsed_width] - padding_size}px"
42
+ end
43
+
44
+ children = @props[:children] || []
45
+
46
+ @context.merge(
47
+ container_width: container_width,
48
+ non_raw_siblings: children.length
49
+ )
50
+ end
51
+
52
+ def get_styles
53
+ {
54
+ div: {
55
+ "font-size" => "0",
56
+ "line-height" => "0",
57
+ "text-align" => "left",
58
+ "display" => "inline-block",
59
+ "width" => "100%",
60
+ "direction" => get_attribute("direction"),
61
+ "vertical-align" => get_attribute("vertical-align"),
62
+ "background-color" => get_attribute("background-color")
63
+ },
64
+ tdOutlook: {
65
+ "vertical-align" => get_attribute("vertical-align"),
66
+ "width" => get_width_as_pixel
67
+ }
68
+ }
69
+ end
70
+
71
+ def render
72
+ children = @props[:children] || []
73
+ non_raw_siblings = @props[:non_raw_siblings] || 1
74
+
75
+ group_context = get_child_context
76
+ group_width = group_context[:container_width]
77
+ container_width = @context[:container_width]
78
+
79
+ get_element_width = ->(width) {
80
+ unless width
81
+ return "#{container_width.to_i / non_raw_siblings.to_i}px"
82
+ end
83
+
84
+ parsed = WidthParser.call(width, parse_float_to_int: false)
85
+
86
+ if parsed[:unit] == "%"
87
+ "#{(100 * parsed[:parsed_width]) / group_width.to_f}px"
88
+ else
89
+ "#{parsed[:parsed_width]}#{parsed[:unit]}"
90
+ end
91
+ }
92
+
93
+ classes_name = "#{get_column_class} mj-outlook-group-fix"
94
+ css_class = get_attribute("css-class")
95
+ classes_name += " #{css_class}" if css_class
96
+
97
+ <<~HTML
98
+ <div#{html_attributes(class: classes_name, style: :div)}>
99
+ <!--[if mso | IE]>
100
+ <table#{html_attributes(
101
+ bgcolor: ((get_attribute("background-color") == "none") ? nil : get_attribute("background-color")),
102
+ border: "0",
103
+ cellpadding: "0",
104
+ cellspacing: "0",
105
+ role: "presentation"
106
+ )}>
107
+ <tr>
108
+ <![endif]-->
109
+ #{render_children(children, attributes: {"mobileWidth" => "mobileWidth"}, renderer: ->(component) {
110
+ if component.class.raw_element?
111
+ component.render
112
+ else
113
+ comp_width = if component.respond_to?(:get_width_as_pixel, true)
114
+ begin
115
+ component.send(:get_width_as_pixel)
116
+ rescue
117
+ component.get_attribute("width")
118
+ end
119
+ else
120
+ component.get_attribute("width")
121
+ end
122
+
123
+ <<~CELL
124
+ <!--[if mso | IE]>
125
+ <td#{component.html_attributes(
126
+ style: {
127
+ "align" => component.get_attribute("align"),
128
+ "vertical-align" => component.get_attribute("vertical-align"),
129
+ "width" => get_element_width.call(comp_width)
130
+ }
131
+ )}>
132
+ <![endif]-->
133
+ #{component.render}
134
+ <!--[if mso | IE]>
135
+ </td>
136
+ <![endif]-->
137
+ CELL
138
+ end
139
+ })}
140
+ <!--[if mso | IE]>
141
+ </tr>
142
+ </table>
143
+ <![endif]-->
144
+ </div>
145
+ HTML
146
+ end
147
+
148
+ private
149
+
150
+ def get_parsed_width(to_string = false)
151
+ non_raw_siblings = @props[:non_raw_siblings] || 1
152
+ width = get_attribute("width") || "#{100.0 / non_raw_siblings}%"
153
+
154
+ parsed = WidthParser.call(width, parse_float_to_int: false)
155
+
156
+ if to_string
157
+ "#{format_float(parsed[:parsed_width])}#{parsed[:unit]}"
158
+ else
159
+ parsed
160
+ end
161
+ end
162
+
163
+ def get_width_as_pixel
164
+ container_width = @context[:container_width]
165
+ parsed = WidthParser.call(get_parsed_width(true), parse_float_to_int: false)
166
+
167
+ if parsed[:unit] == "%"
168
+ "#{format_float((container_width.to_f * parsed[:parsed_width]) / 100)}px"
169
+ else
170
+ "#{format_float(parsed[:parsed_width])}px"
171
+ end
172
+ end
173
+
174
+ def get_column_class
175
+ add_media_query = @context[:add_media_query]
176
+ parsed = get_parsed_width
177
+
178
+ class_name = case parsed[:unit]
179
+ when "%"
180
+ "mj-column-per-#{parsed[:parsed_width].to_i}"
181
+ else
182
+ "mj-column-px-#{parsed[:parsed_width].to_i}"
183
+ end
184
+
185
+ add_media_query&.call(class_name, parsed)
186
+ class_name
187
+ end
188
+
189
+ def format_float(value)
190
+ (value == value.to_i) ? value.to_i : value
191
+ end
192
+ end
193
+ end
194
+
195
+ Registry.register(Components::MjGroup)
196
+ end