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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +158 -0
- data/lib/emjay/body_component.rb +142 -0
- data/lib/emjay/component.rb +61 -0
- data/lib/emjay/components/body/mj_accordion.rb +99 -0
- data/lib/emjay/components/body/mj_accordion_element.rb +127 -0
- data/lib/emjay/components/body/mj_accordion_text.rb +123 -0
- data/lib/emjay/components/body/mj_accordion_title.rb +171 -0
- data/lib/emjay/components/body/mj_body.rb +70 -0
- data/lib/emjay/components/body/mj_button.rb +198 -0
- data/lib/emjay/components/body/mj_carousel.rb +410 -0
- data/lib/emjay/components/body/mj_carousel_image.rb +188 -0
- data/lib/emjay/components/body/mj_column.rb +287 -0
- data/lib/emjay/components/body/mj_divider.rb +120 -0
- data/lib/emjay/components/body/mj_group.rb +196 -0
- data/lib/emjay/components/body/mj_hero.rb +382 -0
- data/lib/emjay/components/body/mj_image.rb +188 -0
- data/lib/emjay/components/body/mj_navbar.rb +187 -0
- data/lib/emjay/components/body/mj_navbar_link.rb +129 -0
- data/lib/emjay/components/body/mj_raw.rb +34 -0
- data/lib/emjay/components/body/mj_section.rb +442 -0
- data/lib/emjay/components/body/mj_social.rb +174 -0
- data/lib/emjay/components/body/mj_social_element.rb +272 -0
- data/lib/emjay/components/body/mj_spacer.rb +57 -0
- data/lib/emjay/components/body/mj_table.rb +113 -0
- data/lib/emjay/components/body/mj_text.rb +100 -0
- data/lib/emjay/components/body/mj_wrapper.rb +56 -0
- data/lib/emjay/components/head/mj_attributes.rb +38 -0
- data/lib/emjay/components/head/mj_breakpoint.rb +28 -0
- data/lib/emjay/components/head/mj_font.rb +24 -0
- data/lib/emjay/components/head/mj_head.rb +20 -0
- data/lib/emjay/components/head/mj_html_attributes.rb +33 -0
- data/lib/emjay/components/head/mj_preview.rb +24 -0
- data/lib/emjay/components/head/mj_style.rb +34 -0
- data/lib/emjay/components/head/mj_title.rb +24 -0
- data/lib/emjay/global_data.rb +64 -0
- data/lib/emjay/head_component.rb +37 -0
- data/lib/emjay/helpers/conditional_tag.rb +24 -0
- data/lib/emjay/helpers/fonts.rb +34 -0
- data/lib/emjay/helpers/gen_random_hex_string.rb +9 -0
- data/lib/emjay/helpers/make_lower_breakpoint.rb +17 -0
- data/lib/emjay/helpers/media_queries.rb +47 -0
- data/lib/emjay/helpers/merge_outlook_conditionals.rb +11 -0
- data/lib/emjay/helpers/minify_outlook_conditionals.rb +18 -0
- data/lib/emjay/helpers/shorthand_parser.rb +33 -0
- data/lib/emjay/helpers/styles.rb +34 -0
- data/lib/emjay/helpers/suffix_css_classes.rb +12 -0
- data/lib/emjay/helpers/width_parser.rb +26 -0
- data/lib/emjay/rails/mail_interceptor.rb +37 -0
- data/lib/emjay/rails/template_handler.rb +16 -0
- data/lib/emjay/railtie.rb +21 -0
- data/lib/emjay/registry.rb +19 -0
- data/lib/emjay/renderer.rb +302 -0
- data/lib/emjay/skeleton.rb +80 -0
- data/lib/emjay/version.rb +5 -0
- data/lib/emjay.rb +66 -0
- data/llms.txt +130 -0
- metadata +129 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../head_component"
|
|
4
|
+
require_relative "../../registry"
|
|
5
|
+
|
|
6
|
+
module Emjay
|
|
7
|
+
module Components
|
|
8
|
+
class MjHead < HeadComponent
|
|
9
|
+
def self.component_name
|
|
10
|
+
"mj-head"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handler
|
|
14
|
+
handler_children
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Registry.register(Components::MjHead)
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../head_component"
|
|
4
|
+
require_relative "../../registry"
|
|
5
|
+
|
|
6
|
+
module Emjay
|
|
7
|
+
module Components
|
|
8
|
+
class MjHtmlAttributes < HeadComponent
|
|
9
|
+
def self.component_name
|
|
10
|
+
"mj-html-attributes"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handler
|
|
14
|
+
add = @context[:add]
|
|
15
|
+
children = @props[:children] || []
|
|
16
|
+
|
|
17
|
+
children.select { |c| c[:tag_name] == "mj-selector" }.each do |selector|
|
|
18
|
+
path = (selector[:attributes] || {})["path"]
|
|
19
|
+
|
|
20
|
+
custom = (selector[:children] || [])
|
|
21
|
+
.select { |c| c[:tag_name] == "mj-html-attribute" && (c[:attributes] || {})["name"] }
|
|
22
|
+
.each_with_object({}) { |c, acc|
|
|
23
|
+
acc[c[:attributes]["name"]] = c[:content] || ""
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
add.call(:html_attributes, path, custom)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Registry.register(Components::MjHtmlAttributes)
|
|
33
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../head_component"
|
|
4
|
+
require_relative "../../registry"
|
|
5
|
+
|
|
6
|
+
module Emjay
|
|
7
|
+
module Components
|
|
8
|
+
class MjPreview < HeadComponent
|
|
9
|
+
def self.component_name
|
|
10
|
+
"mj-preview"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.ending_tag?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handler
|
|
18
|
+
@context[:add].call(:preview, get_content)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Registry.register(Components::MjPreview)
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../head_component"
|
|
4
|
+
require_relative "../../registry"
|
|
5
|
+
|
|
6
|
+
module Emjay
|
|
7
|
+
module Components
|
|
8
|
+
class MjStyle < HeadComponent
|
|
9
|
+
def self.component_name
|
|
10
|
+
"mj-style"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.ending_tag?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.allowed_attributes
|
|
18
|
+
{"inline" => "string"}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def handler
|
|
22
|
+
add = @context[:add]
|
|
23
|
+
key = if get_attribute("inline") == "inline"
|
|
24
|
+
:inline_style
|
|
25
|
+
else
|
|
26
|
+
:style
|
|
27
|
+
end
|
|
28
|
+
add.call(key, get_content)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Registry.register(Components::MjStyle)
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../head_component"
|
|
4
|
+
require_relative "../../registry"
|
|
5
|
+
|
|
6
|
+
module Emjay
|
|
7
|
+
module Components
|
|
8
|
+
class MjTitle < HeadComponent
|
|
9
|
+
def self.component_name
|
|
10
|
+
"mj-title"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.ending_tag?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handler
|
|
18
|
+
@context[:add].call(:title, get_content)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Registry.register(Components::MjTitle)
|
|
24
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
class GlobalData
|
|
5
|
+
ARRAY_FIELDS = %i[inline_style components_head_style head_raw style].freeze
|
|
6
|
+
HASH_FIELDS = %i[classes classes_default default_attributes html_attributes fonts head_style media_queries].freeze
|
|
7
|
+
SCALAR_FIELDS = %i[before_doctype breakpoint preview title force_owa_desktop lang dir].freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor(*ARRAY_FIELDS, *HASH_FIELDS, *SCALAR_FIELDS)
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@before_doctype = ""
|
|
13
|
+
@breakpoint = options[:breakpoint] || "480px"
|
|
14
|
+
@classes = {}
|
|
15
|
+
@classes_default = {}
|
|
16
|
+
@default_attributes = {}
|
|
17
|
+
@html_attributes = {}
|
|
18
|
+
@fonts = options.fetch(:fonts, default_fonts)
|
|
19
|
+
@inline_style = []
|
|
20
|
+
@head_style = {}
|
|
21
|
+
@components_head_style = []
|
|
22
|
+
@head_raw = []
|
|
23
|
+
@media_queries = {}
|
|
24
|
+
@preview = ""
|
|
25
|
+
@style = []
|
|
26
|
+
@title = ""
|
|
27
|
+
@force_owa_desktop = false
|
|
28
|
+
@lang = "und"
|
|
29
|
+
@dir = "auto"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Mirrors JS headHelpers.add() semantics
|
|
33
|
+
def add(key, *params)
|
|
34
|
+
val = send(key)
|
|
35
|
+
if val.is_a?(Array)
|
|
36
|
+
val.push(*params)
|
|
37
|
+
elsif val.is_a?(Hash)
|
|
38
|
+
if params.length > 1
|
|
39
|
+
val[params[0]] = if val[params[0]].is_a?(Hash)
|
|
40
|
+
val[params[0]].merge(params[1])
|
|
41
|
+
else
|
|
42
|
+
params[1]
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
send(:"#{key}=", params[0])
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
send(:"#{key}=", params[0])
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def default_fonts
|
|
55
|
+
{
|
|
56
|
+
"Open Sans" => "https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700",
|
|
57
|
+
"Droid Sans" => "https://fonts.googleapis.com/css?family=Droid+Sans:300,400,500,700",
|
|
58
|
+
"Lato" => "https://fonts.googleapis.com/css?family=Lato:300,400,500,700",
|
|
59
|
+
"Roboto" => "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700",
|
|
60
|
+
"Ubuntu" => "https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700"
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "component"
|
|
4
|
+
|
|
5
|
+
module Emjay
|
|
6
|
+
# Base class for head components. Port of JS HeadComponent from createComponent.js.
|
|
7
|
+
class HeadComponent < Component
|
|
8
|
+
def handler
|
|
9
|
+
# Subclasses override
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def handler_children
|
|
13
|
+
children = @props[:children] || []
|
|
14
|
+
components = @context[:components] || {}
|
|
15
|
+
|
|
16
|
+
children.filter_map do |child|
|
|
17
|
+
component_class = components[child[:tag_name]]
|
|
18
|
+
|
|
19
|
+
unless component_class
|
|
20
|
+
warn "No matching component for tag: #{child[:tag_name]}"
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
component = component_class.new(
|
|
25
|
+
attributes: child[:attributes] || {},
|
|
26
|
+
children: child[:children] || [],
|
|
27
|
+
content: child[:content] || "",
|
|
28
|
+
context: get_child_context
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
component.handler if component.respond_to?(:handler)
|
|
32
|
+
|
|
33
|
+
component.render if component.respond_to?(:render)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module ConditionalTag
|
|
5
|
+
START_CONDITIONAL_TAG = "<!--[if mso | IE]>"
|
|
6
|
+
START_MSO_CONDITIONAL_TAG = "<!--[if mso]>"
|
|
7
|
+
END_CONDITIONAL_TAG = "<![endif]-->"
|
|
8
|
+
START_NEGATION_CONDITIONAL_TAG = "<!--[if !mso | IE]><!-->"
|
|
9
|
+
START_MSO_NEGATION_CONDITIONAL_TAG = "<!--[if !mso]><!-->"
|
|
10
|
+
END_NEGATION_CONDITIONAL_TAG = "<!--<![endif]-->"
|
|
11
|
+
|
|
12
|
+
def self.conditional_tag(content, negation: false)
|
|
13
|
+
start_tag = negation ? START_NEGATION_CONDITIONAL_TAG : START_CONDITIONAL_TAG
|
|
14
|
+
end_tag = negation ? END_NEGATION_CONDITIONAL_TAG : END_CONDITIONAL_TAG
|
|
15
|
+
"\n #{start_tag}\n #{content}\n #{end_tag}\n "
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.mso_conditional_tag(content, negation: false)
|
|
19
|
+
start_tag = negation ? START_MSO_NEGATION_CONDITIONAL_TAG : START_MSO_CONDITIONAL_TAG
|
|
20
|
+
end_tag = negation ? END_NEGATION_CONDITIONAL_TAG : END_CONDITIONAL_TAG
|
|
21
|
+
"\n #{start_tag}\n #{content}\n #{end_tag}\n "
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module Fonts
|
|
5
|
+
# Builds font import tags (<link> and @import) for fonts used in the content.
|
|
6
|
+
# Port of fonts.js buildFontsTags
|
|
7
|
+
def self.build_tags(content, inline_style, fonts = {})
|
|
8
|
+
to_import = []
|
|
9
|
+
|
|
10
|
+
fonts.each do |name, url|
|
|
11
|
+
regex = /"[^"]*font-family:[^"]*#{Regexp.escape(name)}[^"]*"/mi
|
|
12
|
+
inline_regex = /font-family:[^;}]*#{Regexp.escape(name)}/mi
|
|
13
|
+
|
|
14
|
+
if content.match?(regex) || inline_style.any? { |s| s.match?(inline_regex) }
|
|
15
|
+
to_import << url
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return "" if to_import.empty?
|
|
20
|
+
|
|
21
|
+
links = to_import.map { |url| %(<link href="#{url}" rel="stylesheet" type="text/css">) }.join("\n")
|
|
22
|
+
imports = to_import.map { |url| "@import url(#{url});" }.join("\n")
|
|
23
|
+
|
|
24
|
+
<<~HTML
|
|
25
|
+
<!--[if !mso]><!-->
|
|
26
|
+
#{links}
|
|
27
|
+
<style type="text/css">
|
|
28
|
+
#{imports}
|
|
29
|
+
</style>
|
|
30
|
+
<!--<![endif]-->
|
|
31
|
+
HTML
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module MakeLowerBreakpoint
|
|
5
|
+
# Given a breakpoint string like "600px", returns "599px".
|
|
6
|
+
# Port of mjml-core/src/helpers/makeLowerBreakpoint.js
|
|
7
|
+
def self.call(breakpoint)
|
|
8
|
+
match = breakpoint.to_s.match(/[0-9]+/)
|
|
9
|
+
return breakpoint unless match
|
|
10
|
+
|
|
11
|
+
pixels = match[0].to_i
|
|
12
|
+
"#{pixels - 1}px"
|
|
13
|
+
rescue
|
|
14
|
+
breakpoint
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module MediaQueries
|
|
5
|
+
# Builds <style> tags for responsive media queries.
|
|
6
|
+
# Port of mediaQueries.js
|
|
7
|
+
def self.build_tags(breakpoint, media_queries = {}, force_owa_desktop: false, printer_support: false)
|
|
8
|
+
return "" if media_queries.empty?
|
|
9
|
+
|
|
10
|
+
base = media_queries.map { |class_name, mq| ".#{class_name} #{mq}" }
|
|
11
|
+
thunderbird = media_queries.map { |class_name, mq| ".moz-text-html .#{class_name} #{mq}" }
|
|
12
|
+
owa = base.map { |mq| "[owa] #{mq}" }
|
|
13
|
+
|
|
14
|
+
result = +""
|
|
15
|
+
result << <<~HTML
|
|
16
|
+
<style type="text/css">
|
|
17
|
+
@media only screen and (min-width:#{breakpoint}) {
|
|
18
|
+
#{base.join("\n")}
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
21
|
+
<style media="screen and (min-width:#{breakpoint})">
|
|
22
|
+
#{thunderbird.join("\n")}
|
|
23
|
+
</style>
|
|
24
|
+
HTML
|
|
25
|
+
|
|
26
|
+
if printer_support
|
|
27
|
+
result << <<~HTML
|
|
28
|
+
<style type="text/css">
|
|
29
|
+
@media only print {
|
|
30
|
+
#{base.join("\n")}
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
HTML
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if force_owa_desktop
|
|
37
|
+
result << <<~HTML
|
|
38
|
+
<style type="text/css">
|
|
39
|
+
#{owa.join("\n")}
|
|
40
|
+
</style>
|
|
41
|
+
HTML
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module MergeOutlookConditionals
|
|
5
|
+
# Removes adjacent `<![endif]-->...<!--[if mso | IE]>` pairs.
|
|
6
|
+
# Port of mergeOutlookConditionnals.js
|
|
7
|
+
def self.call(content)
|
|
8
|
+
content.gsub(/<!\[endif\]-->\s*?<!--\[if mso \| IE\]>/m, "")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module MinifyOutlookConditionals
|
|
5
|
+
# Collapses whitespace inside Outlook conditional blocks.
|
|
6
|
+
# Port of minifyOutlookConditionnals.js
|
|
7
|
+
def self.call(content)
|
|
8
|
+
content.gsub(/(<!--\[if\s[^\]]+\]>)([\s\S]*?)(<!\[endif\]-->)/m) do
|
|
9
|
+
prefix = $1
|
|
10
|
+
inner = $2
|
|
11
|
+
suffix = $3
|
|
12
|
+
processed = inner.gsub(/(^|>)(\s+)(<|$)/m) { "#{$1}#{$3}" }
|
|
13
|
+
.gsub(/\s{2,}/m, " ")
|
|
14
|
+
"#{prefix}#{processed}#{suffix}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module ShorthandParser
|
|
5
|
+
# Parses CSS shorthand values (like padding/margin) into per-direction integers.
|
|
6
|
+
# Port of shorthandParser.js
|
|
7
|
+
def self.call(css_value, direction)
|
|
8
|
+
parts = css_value.to_s.strip.gsub(/\s+/, " ").split(" ", 4)
|
|
9
|
+
|
|
10
|
+
directions = case parts.length
|
|
11
|
+
when 2
|
|
12
|
+
{top: 0, bottom: 0, left: 1, right: 1}
|
|
13
|
+
when 3
|
|
14
|
+
{top: 0, left: 1, right: 1, bottom: 2}
|
|
15
|
+
when 4
|
|
16
|
+
{top: 0, right: 1, bottom: 2, left: 3}
|
|
17
|
+
else
|
|
18
|
+
return css_value.to_i
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
(parts[directions[direction.to_sym]] || "0").to_i
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module BorderParser
|
|
26
|
+
# Extracts the first number from a border string (e.g. "1px solid black" -> 1).
|
|
27
|
+
# Port of borderParser in shorthandParser.js
|
|
28
|
+
def self.call(border)
|
|
29
|
+
match = border.to_s.match(/(?:(?:^| )(\d+))/)
|
|
30
|
+
match ? match[1].to_i : 0
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module Styles
|
|
5
|
+
# Builds <style> tag from component head styles and head style functions.
|
|
6
|
+
# Port of styles.js buildStyleFromComponents
|
|
7
|
+
def self.build_from_components(breakpoint, components_head_style, head_style)
|
|
8
|
+
head_styles = head_style.values
|
|
9
|
+
all = components_head_style + head_styles
|
|
10
|
+
|
|
11
|
+
return "" if all.empty?
|
|
12
|
+
|
|
13
|
+
css = all.map { |style_fn| style_fn.call(breakpoint) }.join("\n")
|
|
14
|
+
|
|
15
|
+
<<~HTML.chomp
|
|
16
|
+
<style type="text/css">#{css}
|
|
17
|
+
</style>
|
|
18
|
+
HTML
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Builds <style> tag from user-provided style strings/procs.
|
|
22
|
+
# Port of styles.js buildStyleFromTags
|
|
23
|
+
def self.build_from_tags(breakpoint, styles)
|
|
24
|
+
return "" if styles.empty?
|
|
25
|
+
|
|
26
|
+
css = styles.map { |style| style.respond_to?(:call) ? style.call(breakpoint) : style }.join("\n")
|
|
27
|
+
|
|
28
|
+
<<~HTML.chomp
|
|
29
|
+
<style type="text/css">#{css}
|
|
30
|
+
</style>
|
|
31
|
+
HTML
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module SuffixCssClasses
|
|
5
|
+
# Appends a suffix to each CSS class in a space-separated string.
|
|
6
|
+
# Port of suffixCssClasses.js
|
|
7
|
+
def self.call(classes, suffix)
|
|
8
|
+
return "" unless classes && !classes.empty?
|
|
9
|
+
classes.split(" ").map { |c| "#{c}-#{suffix}" }.join(" ")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module WidthParser
|
|
5
|
+
UNIT_REGEX = /[\d.,]*(\D*)$/
|
|
6
|
+
|
|
7
|
+
# Parses a width string like "600px" or "50%" into { parsed_width:, unit: }.
|
|
8
|
+
# Port of widthParser.js
|
|
9
|
+
def self.call(width, parse_float_to_int: true)
|
|
10
|
+
width_str = width.to_s
|
|
11
|
+
unit_match = UNIT_REGEX.match(width_str)
|
|
12
|
+
width_unit = unit_match ? unit_match[1] : ""
|
|
13
|
+
|
|
14
|
+
parsed_width = if width_unit == "%" && !parse_float_to_int
|
|
15
|
+
width_str.to_f
|
|
16
|
+
else
|
|
17
|
+
width_str.to_i
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
parsed_width: parsed_width,
|
|
22
|
+
unit: width_unit.empty? ? "px" : width_unit
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module Rails
|
|
5
|
+
class MailInterceptor
|
|
6
|
+
MJML_TAG_PATTERN = /<mjml[\s>]/i
|
|
7
|
+
|
|
8
|
+
def self.delivering_email(message)
|
|
9
|
+
new.compile_mjml!(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.previewing_email(message)
|
|
13
|
+
new.compile_mjml!(message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compile_mjml!(message)
|
|
17
|
+
if message.multipart?
|
|
18
|
+
message.parts.each { |part| compile_part!(part) }
|
|
19
|
+
else
|
|
20
|
+
compile_part!(message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def compile_part!(part)
|
|
27
|
+
return if part.multipart?
|
|
28
|
+
return unless part.content_type&.include?("text/html") || part.content_type.nil?
|
|
29
|
+
|
|
30
|
+
body = part.body.decoded
|
|
31
|
+
return unless MJML_TAG_PATTERN.match?(body)
|
|
32
|
+
|
|
33
|
+
part.body = Emjay.to_html(body)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module Rails
|
|
5
|
+
module TemplateHandler
|
|
6
|
+
def self.call(template, source)
|
|
7
|
+
# Pure ERB passthrough — exists only to register .mjml as a valid
|
|
8
|
+
# template extension with ERB support. MJML → HTML compilation
|
|
9
|
+
# happens later via Emjay::Rails::MailInterceptor, after Rails
|
|
10
|
+
# has assembled the full render (template + layout).
|
|
11
|
+
erb_handler = ActionView::Template.registered_template_handler(:erb)
|
|
12
|
+
erb_handler.call(template, source)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer "emjay.register_template_handler" do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
require "emjay/rails/template_handler"
|
|
8
|
+
ActionView::Template.register_template_handler(:mjml, Emjay::Rails::TemplateHandler)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "emjay.register_mail_interceptor" do
|
|
13
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
14
|
+
require "emjay/rails/mail_interceptor"
|
|
15
|
+
interceptor = Emjay::Rails::MailInterceptor
|
|
16
|
+
ActionMailer::Base.register_interceptor(interceptor)
|
|
17
|
+
ActionMailer::Base.register_preview_interceptor(interceptor)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emjay
|
|
4
|
+
module Registry
|
|
5
|
+
@components = {}
|
|
6
|
+
|
|
7
|
+
def self.register(component_class)
|
|
8
|
+
@components[component_class.component_name] = component_class
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.find(tag_name)
|
|
12
|
+
@components[tag_name]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.components
|
|
16
|
+
@components
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|