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,442 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/suffix_css_classes"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjSection < BodyComponent
10
+ def self.component_name
11
+ "mj-section"
12
+ end
13
+
14
+ def self.allowed_attributes
15
+ {
16
+ "background-color" => "color",
17
+ "background-url" => "string",
18
+ "background-repeat" => "enum(repeat,no-repeat)",
19
+ "background-size" => "string",
20
+ "background-position" => "string",
21
+ "background-position-x" => "string",
22
+ "background-position-y" => "string",
23
+ "border" => "string",
24
+ "border-bottom" => "string",
25
+ "border-left" => "string",
26
+ "border-radius" => "string",
27
+ "border-right" => "string",
28
+ "border-top" => "string",
29
+ "direction" => "enum(ltr,rtl)",
30
+ "full-width" => "enum(full-width,false,)",
31
+ "padding" => "unit(px,%){1,4}",
32
+ "padding-top" => "unit(px,%)",
33
+ "padding-bottom" => "unit(px,%)",
34
+ "padding-left" => "unit(px,%)",
35
+ "padding-right" => "unit(px,%)",
36
+ "text-align" => "enum(left,center,right)",
37
+ "text-padding" => "unit(px,%){1,4}"
38
+ }
39
+ end
40
+
41
+ def self.default_attributes
42
+ {
43
+ "background-repeat" => "repeat",
44
+ "background-size" => "auto",
45
+ "background-position" => "top center",
46
+ "direction" => "ltr",
47
+ "padding" => "20px 0",
48
+ "text-align" => "center",
49
+ "text-padding" => "4px 4px 4px 0"
50
+ }
51
+ end
52
+
53
+ def get_child_context
54
+ widths = get_box_widths
55
+ @context.merge(
56
+ container_width: "#{widths[:box]}px",
57
+ gap: get_attribute("gap")
58
+ )
59
+ end
60
+
61
+ def get_styles
62
+ container_width = @context[:container_width]
63
+ full_width = full_width?
64
+ has_border_radius = has_border_radius?
65
+ is_first = @props[:index] == 0
66
+
67
+ background = if has_background?
68
+ {
69
+ "background" => get_background,
70
+ "background-position" => get_background_string,
71
+ "background-repeat" => get_attribute("background-repeat"),
72
+ "background-size" => get_attribute("background-size")
73
+ }
74
+ else
75
+ {
76
+ "background" => get_attribute("background-color"),
77
+ "background-color" => get_attribute("background-color")
78
+ }
79
+ end
80
+
81
+ {
82
+ tableFullwidth: {
83
+ **(full_width ? background : {}),
84
+ "width" => "100%"
85
+ },
86
+ table: {
87
+ **(full_width ? {} : background),
88
+ "width" => "100%",
89
+ **(has_border_radius ? {"border-collapse" => "separate"} : {})
90
+ },
91
+ td: {
92
+ "border" => get_attribute("border"),
93
+ "border-bottom" => get_attribute("border-bottom"),
94
+ "border-left" => get_attribute("border-left"),
95
+ "border-right" => get_attribute("border-right"),
96
+ "border-top" => get_attribute("border-top"),
97
+ "border-radius" => get_attribute("border-radius"),
98
+ "direction" => get_attribute("direction"),
99
+ "font-size" => "0px",
100
+ "padding" => get_attribute("padding"),
101
+ "padding-bottom" => get_attribute("padding-bottom"),
102
+ "padding-left" => get_attribute("padding-left"),
103
+ "padding-right" => get_attribute("padding-right"),
104
+ "padding-top" => get_attribute("padding-top"),
105
+ "text-align" => get_attribute("text-align")
106
+ },
107
+ div: {
108
+ **(full_width ? {} : background),
109
+ "margin" => "0px auto",
110
+ "max-width" => container_width,
111
+ "border-radius" => get_attribute("border-radius"),
112
+ **(has_border_radius ? {"overflow" => "hidden"} : {}),
113
+ "margin-top" => ((!is_first) ? @context[:gap] : nil)
114
+ },
115
+ innerDiv: {
116
+ "line-height" => "0",
117
+ "font-size" => "0"
118
+ }
119
+ }
120
+ end
121
+
122
+ def render
123
+ full_width? ? render_full_width : render_simple
124
+ end
125
+
126
+ private
127
+
128
+ def full_width?
129
+ get_attribute("full-width") == "full-width"
130
+ end
131
+
132
+ def has_background?
133
+ !get_attribute("background-url").nil?
134
+ end
135
+
136
+ def has_border_radius?
137
+ br = get_attribute("border-radius")
138
+ br && !br.empty?
139
+ end
140
+
141
+ def get_background
142
+ parts = [get_attribute("background-color")]
143
+ if has_background?
144
+ parts << "url('#{get_attribute("background-url")}')"
145
+ parts << get_background_string
146
+ parts << "/ #{get_attribute("background-size")}"
147
+ parts << get_attribute("background-repeat")
148
+ end
149
+ parts.compact.reject(&:empty?).join(" ")
150
+ end
151
+
152
+ def get_background_string
153
+ pos = get_background_position
154
+ "#{pos[:pos_x]} #{pos[:pos_y]}"
155
+ end
156
+
157
+ def get_background_position
158
+ x, y = parse_background_position
159
+ {
160
+ pos_x: get_attribute("background-position-x") || x,
161
+ pos_y: get_attribute("background-position-y") || y
162
+ }
163
+ end
164
+
165
+ def parse_background_position
166
+ parts = (get_attribute("background-position") || "top center").split(" ")
167
+ if parts.length == 1
168
+ val = parts[0]
169
+ if %w[top bottom].include?(val)
170
+ return ["center", val]
171
+ end
172
+ return [val, "center"]
173
+ end
174
+ if parts.length == 2
175
+ val1, val2 = parts
176
+ if %w[top bottom].include?(val1) || (val1 == "center" && %w[left right].include?(val2))
177
+ return [val2, val1]
178
+ end
179
+ return [val1, val2]
180
+ end
181
+ ["center", "top"]
182
+ end
183
+
184
+ def render_before
185
+ container_width = @context[:container_width]
186
+ bgcolor_attr = get_attribute("background-color") ? {bgcolor: get_attribute("background-color")} : {}
187
+ is_first = @props[:index] == 0
188
+
189
+ <<~HTML
190
+ <!--[if mso | IE]>
191
+ <table#{html_attributes(
192
+ align: "center",
193
+ border: "0",
194
+ cellpadding: "0",
195
+ cellspacing: "0",
196
+ class: SuffixCssClasses.call(get_attribute("css-class"), "outlook"),
197
+ role: "presentation",
198
+ style: {
199
+ "width" => container_width.to_s,
200
+ "padding-top" => ((!is_first) ? @context[:gap] : nil)
201
+ },
202
+ width: container_width.to_i,
203
+ **bgcolor_attr
204
+ )}>
205
+ <tr>
206
+ <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
207
+ <![endif]-->
208
+ HTML
209
+ end
210
+
211
+ def render_after
212
+ <<~HTML
213
+ <!--[if mso | IE]>
214
+ </td>
215
+ </tr>
216
+ </table>
217
+ <![endif]-->
218
+ HTML
219
+ end
220
+
221
+ def render_wrapped_children
222
+ children = @props[:children] || []
223
+
224
+ rendered = render_children(children, renderer: ->(component) {
225
+ if component.class.raw_element?
226
+ component.render
227
+ else
228
+ <<~HTML
229
+ <!--[if mso | IE]>
230
+ <td#{component.html_attributes(
231
+ align: component.get_attribute("align"),
232
+ class: SuffixCssClasses.call(component.get_attribute("css-class"), "outlook"),
233
+ style: :tdOutlook
234
+ )}>
235
+ <![endif]-->
236
+ #{component.render}
237
+ <!--[if mso | IE]>
238
+ </td>
239
+ <![endif]-->
240
+ HTML
241
+ end
242
+ })
243
+
244
+ <<~HTML
245
+ <!--[if mso | IE]>
246
+ <tr>
247
+ <![endif]-->
248
+ #{rendered}
249
+ <!--[if mso | IE]>
250
+ </tr>
251
+ <![endif]-->
252
+ HTML
253
+ end
254
+
255
+ def render_section
256
+ has_bg = has_background?
257
+
258
+ <<~HTML
259
+ <div#{html_attributes(
260
+ class: (full_width? ? nil : get_attribute("css-class")),
261
+ style: :div
262
+ )}>
263
+ #{"<div#{html_attributes(style: :innerDiv)}>" if has_bg}
264
+ <table#{html_attributes(
265
+ align: "center",
266
+ background: (full_width? ? nil : get_attribute("background-url")),
267
+ border: "0",
268
+ cellpadding: "0",
269
+ cellspacing: "0",
270
+ role: "presentation",
271
+ style: :table
272
+ )}>
273
+ <tbody>
274
+ <tr>
275
+ <td#{html_attributes(style: :td)}>
276
+ <!--[if mso | IE]>
277
+ <table role="presentation" border="0" cellpadding="0" cellspacing="0">
278
+ <![endif]-->
279
+ #{render_wrapped_children}
280
+ <!--[if mso | IE]>
281
+ </table>
282
+ <![endif]-->
283
+ </td>
284
+ </tr>
285
+ </tbody>
286
+ </table>
287
+ #{"</div>" if has_bg}
288
+ </div>
289
+ HTML
290
+ end
291
+
292
+ def render_full_width
293
+ content = if has_background?
294
+ render_with_background(<<~HTML
295
+ #{render_before}
296
+ #{render_section}
297
+ #{render_after}
298
+ HTML
299
+ )
300
+ else
301
+ <<~HTML
302
+ #{render_before}
303
+ #{render_section}
304
+ #{render_after}
305
+ HTML
306
+ end
307
+
308
+ <<~HTML
309
+ <table#{html_attributes(
310
+ align: "center",
311
+ class: get_attribute("css-class"),
312
+ background: get_attribute("background-url"),
313
+ border: "0",
314
+ cellpadding: "0",
315
+ cellspacing: "0",
316
+ role: "presentation",
317
+ style: :tableFullwidth
318
+ )}>
319
+ <tbody>
320
+ <tr>
321
+ <td>
322
+ #{content}
323
+ </td>
324
+ </tr>
325
+ </tbody>
326
+ </table>
327
+ HTML
328
+ end
329
+
330
+ def render_simple
331
+ section = render_section
332
+
333
+ <<~HTML
334
+ #{render_before}
335
+ #{has_background? ? render_with_background(section) : section}
336
+ #{render_after}
337
+ HTML
338
+ end
339
+
340
+ def render_with_background(content)
341
+ full_width = full_width?
342
+ container_width = @context[:container_width]
343
+
344
+ bg_pos = get_background_position
345
+ bg_pos_x = bg_pos[:pos_x]
346
+ bg_pos_y = bg_pos[:pos_y]
347
+
348
+ # Convert named positions to percentages
349
+ bg_pos_x = case bg_pos_x
350
+ when "left" then "0%"
351
+ when "center" then "50%"
352
+ when "right" then "100%"
353
+ else
354
+ /^\d+(\.\d+)?%$/.match?(bg_pos_x) ? bg_pos_x : "50%"
355
+ end
356
+
357
+ bg_pos_y = case bg_pos_y
358
+ when "top" then "0%"
359
+ when "center" then "50%"
360
+ when "bottom" then "100%"
361
+ else
362
+ /^\d+(\.\d+)?%$/.match?(bg_pos_y) ? bg_pos_y : "0%"
363
+ end
364
+
365
+ bg_repeat = get_attribute("background-repeat") == "repeat"
366
+
367
+ v_origin_x, v_pos_x = compute_vml_position(bg_pos_x, bg_repeat, true)
368
+ v_origin_y, v_pos_y = compute_vml_position(bg_pos_y, bg_repeat, false)
369
+
370
+ v_size_attributes = {}
371
+ bg_size = get_attribute("background-size")
372
+ if bg_size == "cover" || bg_size == "contain"
373
+ v_size_attributes = {
374
+ size: "1,1",
375
+ aspect: ((bg_size == "cover") ? "atleast" : "atmost")
376
+ }
377
+ elsif bg_size != "auto"
378
+ parts = bg_size.split(" ")
379
+ v_size_attributes = if parts.length == 1
380
+ {size: bg_size, aspect: "atmost"}
381
+ else
382
+ {size: parts.join(",")}
383
+ end
384
+ end
385
+
386
+ vml_type = (get_attribute("background-repeat") == "no-repeat") ? "frame" : "tile"
387
+ if bg_size == "auto"
388
+ vml_type = "tile"
389
+ v_origin_x = 0.5
390
+ v_pos_x = 0.5
391
+ v_origin_y = 0
392
+ v_pos_y = 0
393
+ end
394
+
395
+ <<~HTML
396
+ <!--[if mso | IE]>
397
+ <v:rect#{html_attributes(
398
+ :style => full_width ? {"mso-width-percent" => "1000"} : {"width" => container_width},
399
+ "xmlns:v" => "urn:schemas-microsoft-com:vml",
400
+ :fill => "true",
401
+ :stroke => "false"
402
+ )}>
403
+ <v:fill#{html_attributes(
404
+ origin: "#{v_origin_x}, #{v_origin_y}",
405
+ position: "#{v_pos_x}, #{v_pos_y}",
406
+ src: get_attribute("background-url"),
407
+ color: get_attribute("background-color"),
408
+ type: vml_type,
409
+ **v_size_attributes
410
+ )} />
411
+ <v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0">
412
+ <![endif]-->
413
+ #{content}
414
+ <!--[if mso | IE]>
415
+ </v:textbox>
416
+ </v:rect>
417
+ <![endif]-->
418
+ HTML
419
+ end
420
+
421
+ def compute_vml_position(pos_str, bg_repeat, is_x)
422
+ if pos_str =~ /^(\d+(\.\d+)?)%$/
423
+ decimal = $1.to_i / 100.0
424
+ if bg_repeat
425
+ [decimal, decimal]
426
+ else
427
+ val = (-50 + decimal * 100) / 100.0
428
+ [val, val]
429
+ end
430
+ elsif bg_repeat
431
+ default = is_x ? 0.5 : 0
432
+ [default, default]
433
+ else
434
+ default = is_x ? 0 : -0.5
435
+ [default, default]
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ Registry.register(Components::MjSection)
442
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+
6
+ module Emjay
7
+ module Components
8
+ class MjSocial < BodyComponent
9
+ def self.component_name
10
+ "mj-social"
11
+ end
12
+
13
+ def self.default_attributes
14
+ {
15
+ "align" => "center",
16
+ "border-radius" => "3px",
17
+ "color" => "#333333",
18
+ "font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
19
+ "font-size" => "13px",
20
+ "icon-size" => "20px",
21
+ "inner-padding" => nil,
22
+ "line-height" => "22px",
23
+ "mode" => "horizontal",
24
+ "padding" => "10px 25px",
25
+ "text-decoration" => "none"
26
+ }
27
+ end
28
+
29
+ def self.allowed_attributes
30
+ {
31
+ "align" => "enum(left,right,center)",
32
+ "border-radius" => "string",
33
+ "container-background-color" => "color",
34
+ "color" => "color",
35
+ "font-family" => "string",
36
+ "font-size" => "unit(px)",
37
+ "font-style" => "string",
38
+ "font-weight" => "string",
39
+ "icon-size" => "unit(px,%)",
40
+ "icon-height" => "unit(px,%)",
41
+ "icon-padding" => "unit(px,%){1,4}",
42
+ "inner-padding" => "unit(px,%){1,4}",
43
+ "line-height" => "unit(px,%,)",
44
+ "mode" => "enum(horizontal,vertical)",
45
+ "padding-bottom" => "unit(px,%)",
46
+ "padding-left" => "unit(px,%)",
47
+ "padding-right" => "unit(px,%)",
48
+ "padding-top" => "unit(px,%)",
49
+ "padding" => "unit(px,%){1,4}",
50
+ "table-layout" => "enum(auto,fixed)",
51
+ "text-padding" => "unit(px,%){1,4}",
52
+ "text-decoration" => "string",
53
+ "vertical-align" => "enum(top,bottom,middle)"
54
+ }
55
+ end
56
+
57
+ def get_styles
58
+ {
59
+ tableVertical: {
60
+ "margin" => "0px"
61
+ }
62
+ }
63
+ end
64
+
65
+ def render
66
+ if get_attribute("mode") == "horizontal"
67
+ render_horizontal
68
+ else
69
+ render_vertical
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def get_social_element_attributes
76
+ base = {}
77
+ if get_attribute("inner-padding")
78
+ base["padding"] = get_attribute("inner-padding")
79
+ end
80
+
81
+ %w[border-radius color font-family font-size font-weight font-style
82
+ icon-size icon-height icon-padding text-padding line-height text-decoration].each_with_object(base) do |attr, result|
83
+ val = get_attribute(attr)
84
+ result[attr] = val unless val.nil?
85
+ end
86
+ end
87
+
88
+ def render_horizontal
89
+ children = @props[:children] || []
90
+
91
+ align_attr = html_attributes(
92
+ align: get_attribute("align"),
93
+ border: "0",
94
+ cellpadding: "0",
95
+ cellspacing: "0",
96
+ role: "presentation"
97
+ )
98
+
99
+ children_html = render_children(children,
100
+ attributes: get_social_element_attributes,
101
+ renderer: ->(component) {
102
+ if component.class.raw_element?
103
+ component.render
104
+ else
105
+ table_attrs = component.html_attributes(
106
+ align: get_attribute("align"),
107
+ border: "0",
108
+ cellpadding: "0",
109
+ cellspacing: "0",
110
+ role: "presentation",
111
+ style: {
112
+ "float" => "none",
113
+ "display" => "inline-table"
114
+ }
115
+ )
116
+ <<~CHILD
117
+ <!--[if mso | IE]>
118
+ <td>
119
+ <![endif]-->
120
+ <table
121
+ #{table_attrs}
122
+ >
123
+ <tbody>
124
+ #{component.render}
125
+ </tbody>
126
+ </table>
127
+ <!--[if mso | IE]>
128
+ </td>
129
+ <![endif]-->
130
+ CHILD
131
+ end
132
+ })
133
+
134
+ <<~HTML
135
+ <!--[if mso | IE]>
136
+ <table
137
+ #{align_attr}
138
+ >
139
+ <tr>
140
+ <![endif]-->
141
+ #{children_html}
142
+ <!--[if mso | IE]>
143
+ </tr>
144
+ </table>
145
+ <![endif]-->
146
+ HTML
147
+ end
148
+
149
+ def render_vertical
150
+ children = @props[:children] || []
151
+
152
+ table_attrs = html_attributes(
153
+ border: "0",
154
+ cellpadding: "0",
155
+ cellspacing: "0",
156
+ role: "presentation",
157
+ style: :tableVertical
158
+ )
159
+
160
+ <<~HTML
161
+ <table
162
+ #{table_attrs}
163
+ >
164
+ <tbody>
165
+ #{render_children(children, attributes: get_social_element_attributes)}
166
+ </tbody>
167
+ </table>
168
+ HTML
169
+ end
170
+ end
171
+ end
172
+
173
+ Registry.register(Components::MjSocial)
174
+ end