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,382 @@
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 MjHero < BodyComponent
10
+ def self.component_name
11
+ "mj-hero"
12
+ end
13
+
14
+ def self.default_attributes
15
+ {
16
+ "mode" => "fixed-height",
17
+ "height" => "0px",
18
+ "background-url" => nil,
19
+ "background-position" => "center center",
20
+ "padding" => "0px",
21
+ "padding-bottom" => nil,
22
+ "padding-left" => nil,
23
+ "padding-right" => nil,
24
+ "padding-top" => nil,
25
+ "background-color" => "#ffffff",
26
+ "vertical-align" => "top"
27
+ }
28
+ end
29
+
30
+ def self.allowed_attributes
31
+ {
32
+ "mode" => "string",
33
+ "height" => "unit(px,%)",
34
+ "background-url" => "string",
35
+ "background-width" => "unit(px,%)",
36
+ "background-height" => "unit(px,%)",
37
+ "background-position" => "string",
38
+ "border-radius" => "string",
39
+ "inner-background-color" => "color",
40
+ "inner-padding" => "unit(px,%){1,4}",
41
+ "inner-padding-top" => "unit(px,%)",
42
+ "inner-padding-left" => "unit(px,%)",
43
+ "inner-padding-right" => "unit(px,%)",
44
+ "inner-padding-bottom" => "unit(px,%)",
45
+ "padding" => "unit(px,%){1,4}",
46
+ "padding-bottom" => "unit(px,%)",
47
+ "padding-left" => "unit(px,%)",
48
+ "padding-right" => "unit(px,%)",
49
+ "padding-top" => "unit(px,%)",
50
+ "background-color" => "color",
51
+ "vertical-align" => "enum(top,bottom,middle)"
52
+ }
53
+ end
54
+
55
+ def get_child_context
56
+ container_width = @context[:container_width]
57
+ padding_size = get_shorthand_attr_value("padding", "left") +
58
+ get_shorthand_attr_value("padding", "right")
59
+
60
+ current = "#{container_width.to_f}px"
61
+ parsed = WidthParser.call(current, parse_float_to_int: false)
62
+
63
+ current = if parsed[:unit] == "%"
64
+ "#{(container_width.to_f * parsed[:parsed_width]) / 100 - padding_size}px"
65
+ else
66
+ "#{parsed[:parsed_width] - padding_size}px"
67
+ end
68
+
69
+ @context.merge(container_width: current)
70
+ end
71
+
72
+ def get_styles
73
+ container_width = @context[:container_width]
74
+ current_container_width = get_child_context[:container_width]
75
+
76
+ bg_height_raw = get_attribute("background-height")
77
+ bg_width_raw = get_attribute("background-width")
78
+ bg_height = bg_height_raw.to_i
79
+ bg_width_val = bg_width_raw.to_i
80
+ background_ratio = if bg_height_raw.nil? || bg_width_raw.nil? || bg_width_val == 0
81
+ "NaN"
82
+ else
83
+ (bg_height.to_f / bg_width_val * 100).round
84
+ end
85
+
86
+ width = get_attribute("background-width") || container_width
87
+
88
+ {
89
+ div: {
90
+ "margin" => "0 auto",
91
+ "max-width" => container_width
92
+ },
93
+ table: {
94
+ "width" => "100%"
95
+ },
96
+ tr: {
97
+ "vertical-align" => "top"
98
+ },
99
+ "td-fluid": {
100
+ "width" => "0.01%",
101
+ "padding-bottom" => "#{background_ratio}%",
102
+ "mso-padding-bottom-alt" => "0"
103
+ },
104
+ "outlook-table": {
105
+ "width" => container_width
106
+ },
107
+ "outlook-td": {
108
+ "line-height" => "0",
109
+ "font-size" => "0",
110
+ "mso-line-height-rule" => "exactly"
111
+ },
112
+ "outlook-inner-table": {
113
+ "width" => current_container_width
114
+ },
115
+ "outlook-image": {
116
+ "border" => "0",
117
+ "height" => get_attribute("background-height"),
118
+ "mso-position-horizontal" => "center",
119
+ "position" => "absolute",
120
+ "top" => "0",
121
+ "width" => width,
122
+ "z-index" => "-3"
123
+ },
124
+ "outlook-inner-td": {
125
+ "background-color" => get_attribute("inner-background-color"),
126
+ "padding" => get_attribute("inner-padding"),
127
+ "padding-top" => get_attribute("inner-padding-top"),
128
+ "padding-left" => get_attribute("inner-padding-left"),
129
+ "padding-right" => get_attribute("inner-padding-right"),
130
+ "padding-bottom" => get_attribute("inner-padding-bottom")
131
+ },
132
+ "inner-div": {
133
+ "background-color" => get_attribute("inner-background-color"),
134
+ "float" => get_attribute("align"),
135
+ "margin" => "0px auto",
136
+ "width" => get_attribute("width"),
137
+ "padding" => get_attribute("inner-padding"),
138
+ "padding-top" => get_attribute("inner-padding-top"),
139
+ "padding-left" => get_attribute("inner-padding-left"),
140
+ "padding-right" => get_attribute("inner-padding-right"),
141
+ "padding-bottom" => get_attribute("inner-padding-bottom")
142
+ },
143
+ "inner-table": {
144
+ "width" => "100%",
145
+ "margin" => "0px"
146
+ }
147
+ }
148
+ end
149
+
150
+ def render
151
+ container_width = @context[:container_width]
152
+
153
+ outlook_table_attrs = html_attributes(
154
+ align: "center",
155
+ border: "0",
156
+ cellpadding: "0",
157
+ cellspacing: "0",
158
+ role: "presentation",
159
+ style: "outlook-table",
160
+ width: container_width.to_i
161
+ )
162
+
163
+ outlook_td_attrs = html_attributes(style: "outlook-td")
164
+ outlook_image_attrs = html_attributes(
165
+ style: "outlook-image",
166
+ src: get_attribute("background-url"),
167
+ "xmlns:v": "urn:schemas-microsoft-com:vml"
168
+ )
169
+
170
+ div_attrs = html_attributes(
171
+ align: get_attribute("align"),
172
+ class: get_attribute("css-class"),
173
+ style: :div
174
+ )
175
+
176
+ table_attrs = html_attributes(
177
+ border: "0",
178
+ cellpadding: "0",
179
+ cellspacing: "0",
180
+ role: "presentation",
181
+ style: :table
182
+ )
183
+
184
+ tr_attrs = html_attributes(style: :tr)
185
+
186
+ <<~HTML
187
+ <!--[if mso | IE]>
188
+ <table
189
+ #{outlook_table_attrs}
190
+ >
191
+ <tr>
192
+ <td#{outlook_td_attrs}>
193
+ <v:image
194
+ #{outlook_image_attrs}
195
+ />
196
+ <![endif]-->
197
+ <div
198
+ #{div_attrs}
199
+ >
200
+ <table
201
+ #{table_attrs}
202
+ >
203
+ <tbody>
204
+ <tr
205
+ #{tr_attrs}
206
+ >
207
+ #{render_mode}
208
+ </tr>
209
+ </tbody>
210
+ </table>
211
+ </div>
212
+ <!--[if mso | IE]>
213
+ </td>
214
+ </tr>
215
+ </table>
216
+ <![endif]-->
217
+ HTML
218
+ end
219
+
220
+ private
221
+
222
+ def get_background
223
+ parts = [get_attribute("background-color")]
224
+ if get_attribute("background-url")
225
+ parts << "url('#{get_attribute("background-url")}')"
226
+ parts << "no-repeat"
227
+ parts << "#{get_attribute("background-position")} / cover"
228
+ end
229
+ parts.compact.reject(&:empty?).join(" ")
230
+ end
231
+
232
+ def render_content
233
+ current_container_width = get_child_context[:container_width]
234
+
235
+ outlook_inner_table_attrs = html_attributes(
236
+ align: get_attribute("align"),
237
+ border: "0",
238
+ cellpadding: "0",
239
+ cellspacing: "0",
240
+ style: "outlook-inner-table",
241
+ width: current_container_width.to_i
242
+ )
243
+
244
+ outlook_inner_td_attrs = html_attributes(style: "outlook-inner-td")
245
+
246
+ inner_div_attrs = html_attributes(
247
+ align: get_attribute("align"),
248
+ class: "mj-hero-content",
249
+ style: "inner-div"
250
+ )
251
+
252
+ inner_table_attrs = html_attributes(
253
+ border: "0",
254
+ cellpadding: "0",
255
+ cellspacing: "0",
256
+ role: "presentation",
257
+ style: "inner-table"
258
+ )
259
+
260
+ children = @props[:children] || []
261
+
262
+ children_html = render_children(children,
263
+ renderer: ->(component) {
264
+ if component.class.raw_element?
265
+ component.render
266
+ else
267
+ td_attrs = component.html_attributes(
268
+ align: component.get_attribute("align"),
269
+ background: component.get_attribute("container-background-color"),
270
+ class: component.get_attribute("css-class"),
271
+ style: {
272
+ "background" => component.get_attribute("container-background-color"),
273
+ "font-size" => "0px",
274
+ "padding" => component.get_attribute("padding"),
275
+ "padding-top" => component.get_attribute("padding-top"),
276
+ "padding-right" => component.get_attribute("padding-right"),
277
+ "padding-bottom" => component.get_attribute("padding-bottom"),
278
+ "padding-left" => component.get_attribute("padding-left"),
279
+ "word-break" => "break-word"
280
+ }
281
+ )
282
+ <<~CHILD
283
+ <tr>
284
+ <td
285
+ #{td_attrs}
286
+ >
287
+ #{component.render}
288
+ </td>
289
+ </tr>
290
+ CHILD
291
+ end
292
+ })
293
+
294
+ <<~HTML
295
+ <!--[if mso | IE]>
296
+ <table
297
+ #{outlook_inner_table_attrs}
298
+ >
299
+ <tr>
300
+ <td#{outlook_inner_td_attrs}>
301
+ <![endif]-->
302
+ <div
303
+ #{inner_div_attrs}
304
+ >
305
+ <table
306
+ #{inner_table_attrs}
307
+ >
308
+ <tbody>
309
+ <tr>
310
+ <td#{html_attributes(style: "inner-td")} >
311
+ <table
312
+ #{inner_table_attrs}
313
+ >
314
+ <tbody>
315
+ #{children_html}
316
+ </tbody>
317
+ </table>
318
+ </td>
319
+ </tr>
320
+ </tbody>
321
+ </table>
322
+ </div>
323
+ <!--[if mso | IE]>
324
+ </td>
325
+ </tr>
326
+ </table>
327
+ <![endif]-->
328
+ HTML
329
+ end
330
+
331
+ def render_mode
332
+ common_style = {
333
+ "background" => get_background,
334
+ "background-position" => get_attribute("background-position"),
335
+ "background-repeat" => "no-repeat",
336
+ "border-radius" => get_attribute("border-radius"),
337
+ "padding" => get_attribute("padding"),
338
+ "padding-top" => get_attribute("padding-top"),
339
+ "padding-left" => get_attribute("padding-left"),
340
+ "padding-right" => get_attribute("padding-right"),
341
+ "padding-bottom" => get_attribute("padding-bottom"),
342
+ "vertical-align" => get_attribute("vertical-align")
343
+ }
344
+
345
+ case get_attribute("mode")
346
+ when "fluid-height"
347
+ magic_td_attrs = html_attributes(style: "td-fluid")
348
+ td_attrs = html_attributes(
349
+ background: get_attribute("background-url"),
350
+ style: common_style
351
+ )
352
+ <<~HTML
353
+ <td#{magic_td_attrs} />
354
+ <td#{td_attrs}>
355
+ #{render_content}
356
+ </td>
357
+ <td#{magic_td_attrs} />
358
+ HTML
359
+ else # fixed-height
360
+ height = get_attribute("height").to_i -
361
+ get_shorthand_attr_value("padding", "top") -
362
+ get_shorthand_attr_value("padding", "bottom")
363
+
364
+ td_attrs = html_attributes(
365
+ background: get_attribute("background-url"),
366
+ style: common_style.merge("height" => "#{height}px"),
367
+ height: height
368
+ )
369
+ <<~HTML
370
+ <td
371
+ #{td_attrs}
372
+ >
373
+ #{render_content}
374
+ </td>
375
+ HTML
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ Registry.register(Components::MjHero)
382
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/width_parser"
6
+ require_relative "../../helpers/make_lower_breakpoint"
7
+
8
+ module Emjay
9
+ module Components
10
+ class MjImage < BodyComponent
11
+ def self.component_name
12
+ "mj-image"
13
+ end
14
+
15
+ def self.default_attributes
16
+ {
17
+ "alt" => "",
18
+ "align" => "center",
19
+ "border" => "0",
20
+ "height" => "auto",
21
+ "padding" => "10px 25px",
22
+ "target" => "_blank",
23
+ "font-size" => "13px"
24
+ }
25
+ end
26
+
27
+ def self.allowed_attributes
28
+ {
29
+ "alt" => "string",
30
+ "href" => "string",
31
+ "name" => "string",
32
+ "src" => "string",
33
+ "srcset" => "string",
34
+ "sizes" => "string",
35
+ "title" => "string",
36
+ "rel" => "string",
37
+ "align" => "enum(left,center,right)",
38
+ "border" => "string",
39
+ "border-bottom" => "string",
40
+ "border-left" => "string",
41
+ "border-right" => "string",
42
+ "border-top" => "string",
43
+ "border-radius" => "string",
44
+ "container-background-color" => "color",
45
+ "fluid-on-mobile" => "boolean",
46
+ "padding" => "unit(px,%){1,4}",
47
+ "padding-bottom" => "unit(px,%)",
48
+ "padding-left" => "unit(px,%)",
49
+ "padding-right" => "unit(px,%)",
50
+ "padding-top" => "unit(px,%)",
51
+ "target" => "string",
52
+ "width" => "unit(px)",
53
+ "height" => "unit(px,auto)",
54
+ "max-height" => "unit(px,%)",
55
+ "font-size" => "unit(px)",
56
+ "usemap" => "string"
57
+ }
58
+ end
59
+
60
+ def get_styles
61
+ width = get_content_width
62
+ full_width = get_attribute("full-width") == "full-width"
63
+ parsed = WidthParser.call(width)
64
+
65
+ {
66
+ img: {
67
+ "border" => get_attribute("border"),
68
+ "border-left" => get_attribute("border-left"),
69
+ "border-right" => get_attribute("border-right"),
70
+ "border-top" => get_attribute("border-top"),
71
+ "border-bottom" => get_attribute("border-bottom"),
72
+ "border-radius" => get_attribute("border-radius"),
73
+ "display" => "block",
74
+ "outline" => "none",
75
+ "text-decoration" => "none",
76
+ "height" => get_attribute("height"),
77
+ "max-height" => get_attribute("max-height"),
78
+ "min-width" => full_width ? "100%" : nil,
79
+ "width" => "100%",
80
+ "max-width" => full_width ? "100%" : nil,
81
+ "font-size" => get_attribute("font-size")
82
+ },
83
+ td: {
84
+ "width" => full_width ? nil : "#{parsed[:parsed_width]}#{parsed[:unit]}"
85
+ },
86
+ table: {
87
+ "min-width" => full_width ? "100%" : nil,
88
+ "max-width" => full_width ? "100%" : nil,
89
+ "width" => full_width ? "#{parsed[:parsed_width]}#{parsed[:unit]}" : nil,
90
+ "border-collapse" => "collapse",
91
+ "border-spacing" => "0px"
92
+ }
93
+ }
94
+ end
95
+
96
+ def head_style(breakpoint)
97
+ "\n @media only screen and (max-width:#{MakeLowerBreakpoint.call(breakpoint)}) {\n table.mj-full-width-mobile { width: 100% !important; }\n td.mj-full-width-mobile { width: auto !important; }\n }\n "
98
+ end
99
+
100
+ def render
101
+ fluid_class = get_attribute("fluid-on-mobile") ? "mj-full-width-mobile" : nil
102
+
103
+ table_attrs = html_attributes(
104
+ border: "0",
105
+ cellpadding: "0",
106
+ cellspacing: "0",
107
+ role: "presentation",
108
+ style: :table,
109
+ class: fluid_class
110
+ )
111
+
112
+ td_attrs = html_attributes(
113
+ style: :td,
114
+ class: fluid_class
115
+ )
116
+
117
+ <<~HTML
118
+ <table
119
+ #{table_attrs}
120
+ >
121
+ <tbody>
122
+ <tr>
123
+ <td#{td_attrs}>
124
+ #{render_image}
125
+ </td>
126
+ </tr>
127
+ </tbody>
128
+ </table>
129
+ HTML
130
+ end
131
+
132
+ private
133
+
134
+ def get_content_width
135
+ width = get_attribute("width")
136
+ explicit_width = width ? width.to_i : Float::INFINITY
137
+ box = get_box_widths[:box]
138
+ [box, explicit_width].min
139
+ end
140
+
141
+ def render_image
142
+ height = get_attribute("height")
143
+
144
+ img_attrs = {
145
+ alt: get_attribute("alt"),
146
+ src: get_attribute("src"),
147
+ srcset: get_attribute("srcset"),
148
+ sizes: get_attribute("sizes"),
149
+ style: :img,
150
+ title: get_attribute("title"),
151
+ width: get_content_width,
152
+ usemap: get_attribute("usemap")
153
+ }
154
+
155
+ if height
156
+ img_attrs[:height] = (height == "auto") ? height : height.to_i
157
+ end
158
+
159
+ img = <<~HTML.chomp
160
+ <img
161
+ #{html_attributes(img_attrs)}
162
+ />
163
+ HTML
164
+
165
+ if get_attribute("href")
166
+ a_attrs = html_attributes(
167
+ href: get_attribute("href"),
168
+ target: get_attribute("target"),
169
+ rel: get_attribute("rel"),
170
+ name: get_attribute("name"),
171
+ title: get_attribute("title")
172
+ )
173
+ <<~HTML.chomp
174
+ <a
175
+ #{a_attrs}
176
+ >
177
+ #{img}
178
+ </a>
179
+ HTML
180
+ else
181
+ img
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ Registry.register(Components::MjImage)
188
+ end