mjml-rb 0.5.1 → 0.6.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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +60 -22
- data/THIRD_PARTY_LICENSES.md +25 -0
- data/docs/ARCHITECTURE.md +363 -0
- data/docs/MIGRATING_TO_V5.md +112 -0
- data/docs/MJML_V5_SUPPORT_PLAN.md +325 -0
- data/docs/PARITY_AUDIT.md +124 -0
- data/docs/RELEASING.md +35 -0
- data/docs/USAGE.md +1258 -0
- data/docs/releases/v0.6.0.md +63 -0
- data/lib/mjml-rb/cli.rb +43 -31
- data/lib/mjml-rb/column_gutter.rb +81 -0
- data/lib/mjml-rb/compiler.rb +32 -122
- data/lib/mjml-rb/component_registry.rb +11 -1
- data/lib/mjml-rb/components/accordion.rb +11 -9
- data/lib/mjml-rb/components/attributes.rb +5 -5
- data/lib/mjml-rb/components/base.rb +5 -1
- data/lib/mjml-rb/components/body.rb +6 -3
- data/lib/mjml-rb/components/breakpoint.rb +1 -1
- data/lib/mjml-rb/components/carousel.rb +2 -2
- data/lib/mjml-rb/components/carousel_image.rb +2 -2
- data/lib/mjml-rb/components/column.rb +16 -11
- data/lib/mjml-rb/components/group.rb +13 -4
- data/lib/mjml-rb/components/head.rb +3 -3
- data/lib/mjml-rb/components/hero.rb +7 -2
- data/lib/mjml-rb/components/html_attributes.rb +3 -3
- data/lib/mjml-rb/components/image.rb +1 -3
- data/lib/mjml-rb/components/raw.rb +1 -1
- data/lib/mjml-rb/components/section.rb +23 -8
- data/lib/mjml-rb/components/social.rb +19 -28
- data/lib/mjml-rb/components/table.rb +2 -2
- data/lib/mjml-rb/config_file.rb +25 -15
- data/lib/mjml-rb/dependencies.rb +1 -0
- data/lib/mjml-rb/formatter.rb +113 -0
- data/lib/mjml-rb/node_compat.rb +19 -0
- data/lib/mjml-rb/options.rb +81 -0
- data/lib/mjml-rb/parser.rb +158 -131
- data/lib/mjml-rb/renderer.rb +84 -100
- data/lib/mjml-rb/result.rb +3 -2
- data/lib/mjml-rb/validator.rb +26 -34
- data/lib/mjml-rb/version.rb +2 -1
- data/lib/mjml-rb.rb +3 -3
- data/mjml-rb.gemspec +2 -2
- metadata +20 -2
- data/lib/mjml-rb/ast_node.rb +0 -37
|
@@ -9,7 +9,7 @@ module MjmlRb
|
|
|
9
9
|
|
|
10
10
|
ALLOWED_ATTRIBUTES = {
|
|
11
11
|
"align" => "enum(left,center,right)",
|
|
12
|
-
"border-radius" => "
|
|
12
|
+
"border-radius" => "string",
|
|
13
13
|
"container-background-color" => "color",
|
|
14
14
|
"icon-width" => "unit(px,%)",
|
|
15
15
|
"left-icon" => "string",
|
|
@@ -21,7 +21,7 @@ module MjmlRb
|
|
|
21
21
|
"right-icon" => "string",
|
|
22
22
|
"thumbnails" => "enum(visible,hidden,supported)",
|
|
23
23
|
"tb-border" => "string",
|
|
24
|
-
"tb-border-radius" => "
|
|
24
|
+
"tb-border-radius" => "string",
|
|
25
25
|
"tb-hover-border-color" => "color",
|
|
26
26
|
"tb-selected-border-color" => "color",
|
|
27
27
|
"tb-width" => "unit(px,%)"
|
|
@@ -13,9 +13,9 @@ module MjmlRb
|
|
|
13
13
|
"title" => "string",
|
|
14
14
|
"src" => "string",
|
|
15
15
|
"thumbnails-src" => "string",
|
|
16
|
-
"border-radius" => "
|
|
16
|
+
"border-radius" => "string",
|
|
17
17
|
"tb-border" => "string",
|
|
18
|
-
"tb-border-radius" => "
|
|
18
|
+
"tb-border-radius" => "string"
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
21
|
DEFAULT_ATTRIBUTES = {
|
|
@@ -10,7 +10,7 @@ module MjmlRb
|
|
|
10
10
|
"border" => "string",
|
|
11
11
|
"border-bottom" => "string",
|
|
12
12
|
"border-left" => "string",
|
|
13
|
-
"border-radius" => "
|
|
13
|
+
"border-radius" => "string",
|
|
14
14
|
"border-right" => "string",
|
|
15
15
|
"border-top" => "string",
|
|
16
16
|
"direction" => "enum(ltr,rtl)",
|
|
@@ -18,7 +18,7 @@ module MjmlRb
|
|
|
18
18
|
"inner-border" => "string",
|
|
19
19
|
"inner-border-bottom" => "string",
|
|
20
20
|
"inner-border-left" => "string",
|
|
21
|
-
"inner-border-radius" => "
|
|
21
|
+
"inner-border-radius" => "string",
|
|
22
22
|
"inner-border-right" => "string",
|
|
23
23
|
"inner-border-top" => "string",
|
|
24
24
|
"padding" => "unit(px,%){1,4}",
|
|
@@ -40,21 +40,25 @@ module MjmlRb
|
|
|
40
40
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
41
41
|
width_pct = context.delete(:_column_width_pct) || 100.0
|
|
42
42
|
mobile_width = context.delete(:_column_mobile_width)
|
|
43
|
+
column_gutter = context.delete(:_column_gutter)
|
|
43
44
|
css_class = attrs["css-class"]
|
|
44
45
|
a = self.class.default_attributes.merge(attrs)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
if column_gutter
|
|
48
|
+
col_class_name = column_gutter.width_class
|
|
49
|
+
responsive_width = column_gutter.width
|
|
50
|
+
context[:column_widths][column_gutter.padding_class] = {padding: column_gutter.desktop_padding} unless column_gutter.grouped?
|
|
51
|
+
elsif a["width"]&.strip&.end_with?("px")
|
|
52
|
+
px_val = a["width"].to_f.round.to_s
|
|
53
|
+
col_class_name = "mj-column-px-#{px_val}"
|
|
54
|
+
responsive_width = "#{px_val}px"
|
|
51
55
|
else
|
|
52
56
|
pct_str = width_pct.to_f.to_s.sub(/\.?0+$/, "")
|
|
53
57
|
col_class_name = "mj-column-per-#{pct_str.gsub('.', '-')}"
|
|
54
|
-
|
|
58
|
+
responsive_width = "#{pct_str}%"
|
|
55
59
|
end
|
|
56
|
-
|
|
57
|
-
col_class =
|
|
60
|
+
context[:column_widths][col_class_name] = responsive_width if context[:column_widths]
|
|
61
|
+
col_class = [col_class_name, column_gutter&.padding_class, "mj-outlook-group-fix"].compact.join(" ")
|
|
58
62
|
col_class = "#{col_class} #{css_class}" if css_class && !css_class.empty?
|
|
59
63
|
|
|
60
64
|
vertical_align = a["vertical-align"]
|
|
@@ -64,7 +68,8 @@ module MjmlRb
|
|
|
64
68
|
"direction" => a["direction"],
|
|
65
69
|
"display" => "inline-block",
|
|
66
70
|
"vertical-align" => vertical_align,
|
|
67
|
-
"width" => (mobile_width ? mobile_width_value(a, width_pct, context[:container_width]) : "100%")
|
|
71
|
+
"width" => (column_gutter ? column_gutter.mobile_width : mobile_width ? mobile_width_value(a, width_pct, context[:container_width]) : "100%"),
|
|
72
|
+
"padding" => column_gutter&.mobile_padding
|
|
68
73
|
)
|
|
69
74
|
|
|
70
75
|
column_markup =
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "base"
|
|
2
|
+
require_relative "../column_gutter"
|
|
2
3
|
|
|
3
4
|
module MjmlRb
|
|
4
5
|
module Components
|
|
@@ -21,9 +22,11 @@ module MjmlRb
|
|
|
21
22
|
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
22
23
|
css_class = a["css-class"]
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
unit = a["width"]&.end_with?("px") ? "px" : "%"
|
|
26
|
+
value = unit == "px" ? a["width"].to_f : width_pct
|
|
27
|
+
width_string = value.to_s.sub(/\.?0+$/, "")
|
|
28
|
+
col_class_name = "mj-column-#{unit == '%' ? 'per' : 'px'}-#{width_string.tr('.', '-')}"
|
|
29
|
+
context[:column_widths][col_class_name] = "#{width_string}#{unit}" if context[:column_widths]
|
|
27
30
|
|
|
28
31
|
group_class = "#{col_class_name} mj-outlook-group-fix"
|
|
29
32
|
group_class = "#{group_class} #{css_class}" if css_class && !css_class.empty?
|
|
@@ -71,12 +74,18 @@ module MjmlRb
|
|
|
71
74
|
case child.tag_name
|
|
72
75
|
when "mj-column"
|
|
73
76
|
width_pct = widths[column_index] || 100.0
|
|
77
|
+
gutter = if context[:gutter]
|
|
78
|
+
ColumnGutter.new(width: resolved_attributes(child, context)["width"] || "#{width_pct}%", container_width: group_width,
|
|
79
|
+
gutter: context[:gutter], count: columns.size, index: column_index, direction: attrs["direction"], grouped: true)
|
|
80
|
+
end
|
|
81
|
+
context[:_column_gutter] = gutter
|
|
74
82
|
column_index += 1
|
|
75
83
|
context[:_column_width_pct] = width_pct
|
|
76
84
|
context[:_column_mobile_width] = true
|
|
77
85
|
td_style = style_join(
|
|
78
86
|
"vertical-align" => resolved_attributes(child, context)["vertical-align"] || "top",
|
|
79
|
-
"width" => "#{(group_width_px * width_pct / 100.0).round}px"
|
|
87
|
+
"width" => "#{(group_width_px * width_pct / 100.0).round}px",
|
|
88
|
+
"padding" => gutter&.desktop_padding("px")
|
|
80
89
|
)
|
|
81
90
|
td_open = %(<!--[if mso | IE]><td#{html_attrs("style" => td_style)}><![endif]-->)
|
|
82
91
|
td_close = %(<!--[if mso | IE]></td><![endif]-->)
|
|
@@ -42,14 +42,14 @@ module MjmlRb
|
|
|
42
42
|
context[:preview] = raw_inner(node).strip
|
|
43
43
|
when "mj-style"
|
|
44
44
|
css = raw_inner(node)
|
|
45
|
-
if node
|
|
45
|
+
if node["inline"] == "inline"
|
|
46
46
|
context[:inline_styles] << css
|
|
47
47
|
else
|
|
48
48
|
context[:user_styles] << css
|
|
49
49
|
end
|
|
50
50
|
when "mj-font"
|
|
51
|
-
name = node
|
|
52
|
-
href = node
|
|
51
|
+
name = node["name"]
|
|
52
|
+
href = node["href"]
|
|
53
53
|
context[:fonts][name] = href if name && href
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -13,7 +13,6 @@ module MjmlRb
|
|
|
13
13
|
"background-height" => "unit(px,%)",
|
|
14
14
|
"background-position" => "string",
|
|
15
15
|
"border-radius" => "string",
|
|
16
|
-
"container-background-color" => "color",
|
|
17
16
|
"inner-background-color" => "color",
|
|
18
17
|
"inner-padding" => "unit(px,%){1,4}",
|
|
19
18
|
"inner-padding-top" => "unit(px,%)",
|
|
@@ -131,6 +130,7 @@ module MjmlRb
|
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
def render_content(attrs, content, container_width)
|
|
133
|
+
container_width = hero_container_width(attrs, container_width)
|
|
134
134
|
outlook_attrs = {
|
|
135
135
|
"align" => "center",
|
|
136
136
|
"border" => "0",
|
|
@@ -152,7 +152,12 @@ module MjmlRb
|
|
|
152
152
|
"class" => "mj-hero-content",
|
|
153
153
|
"style" => style_join(
|
|
154
154
|
"background-color" => attrs["inner-background-color"],
|
|
155
|
-
"margin" => "0px auto"
|
|
155
|
+
"margin" => "0px auto",
|
|
156
|
+
"padding" => attrs["inner-padding"],
|
|
157
|
+
"padding-top" => attrs["inner-padding-top"],
|
|
158
|
+
"padding-right" => attrs["inner-padding-right"],
|
|
159
|
+
"padding-bottom" => attrs["inner-padding-bottom"],
|
|
160
|
+
"padding-left" => attrs["inner-padding-left"]
|
|
156
161
|
)
|
|
157
162
|
}
|
|
158
163
|
inner_table_attrs = {
|
|
@@ -32,16 +32,16 @@ module MjmlRb
|
|
|
32
32
|
node.element_children.each do |selector|
|
|
33
33
|
next unless selector.tag_name == "mj-selector"
|
|
34
34
|
|
|
35
|
-
path = selector
|
|
35
|
+
path = selector["path"].to_s.strip
|
|
36
36
|
next if path.empty?
|
|
37
37
|
|
|
38
38
|
custom_attrs = selector.element_children.each_with_object({}) do |child, memo|
|
|
39
39
|
next unless child.tag_name == "mj-html-attribute"
|
|
40
40
|
|
|
41
|
-
name = child
|
|
41
|
+
name = child["name"].to_s.strip
|
|
42
42
|
next if name.empty?
|
|
43
43
|
|
|
44
|
-
memo[name] = child.
|
|
44
|
+
memo[name] = child.content
|
|
45
45
|
end
|
|
46
46
|
next if custom_attrs.empty?
|
|
47
47
|
|
|
@@ -20,7 +20,7 @@ module MjmlRb
|
|
|
20
20
|
"border-left" => "string",
|
|
21
21
|
"border-right" => "string",
|
|
22
22
|
"border-top" => "string",
|
|
23
|
-
"border-radius" => "
|
|
23
|
+
"border-radius" => "string",
|
|
24
24
|
"container-background-color" => "color",
|
|
25
25
|
"fluid-on-mobile" => "boolean",
|
|
26
26
|
"full-width" => "enum(full-width)",
|
|
@@ -168,8 +168,6 @@ module MjmlRb
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def img_width_attribute(attrs, content_width)
|
|
171
|
-
return "auto" if attrs["width"] && attrs["height"] && attrs["height"] != "auto"
|
|
172
|
-
return attrs["width"].to_i.to_s if attrs["width"] && attrs["width"] =~ /^(\d+(?:\.\d+)?)\s*px$/
|
|
173
171
|
|
|
174
172
|
content_width.to_i.to_s
|
|
175
173
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "base"
|
|
2
|
+
require_relative "../column_gutter"
|
|
2
3
|
|
|
3
4
|
module MjmlRb
|
|
4
5
|
module Components
|
|
@@ -20,6 +21,7 @@ module MjmlRb
|
|
|
20
21
|
"border-right" => "string",
|
|
21
22
|
"border-top" => "string",
|
|
22
23
|
"direction" => "enum(ltr,rtl)",
|
|
24
|
+
"gutter" => "unit(px,%)",
|
|
23
25
|
"full-width" => "enum(full-width,false,)",
|
|
24
26
|
"padding" => "unit(px,%){1,4}",
|
|
25
27
|
"padding-bottom" => "unit(px,%)",
|
|
@@ -30,7 +32,7 @@ module MjmlRb
|
|
|
30
32
|
"text-padding" => "unit(px,%){1,4}"
|
|
31
33
|
}.freeze
|
|
32
34
|
|
|
33
|
-
WRAPPER_ALLOWED_ATTRIBUTES = SECTION_ALLOWED_ATTRIBUTES.merge(
|
|
35
|
+
WRAPPER_ALLOWED_ATTRIBUTES = SECTION_ALLOWED_ATTRIBUTES.reject { |key, _| key == "gutter" }.merge(
|
|
34
36
|
"gap" => "unit(px)",
|
|
35
37
|
).freeze
|
|
36
38
|
|
|
@@ -79,7 +81,7 @@ module MjmlRb
|
|
|
79
81
|
def parse_padding_value(str)
|
|
80
82
|
return 0 if str.nil? || str.to_s.strip.empty?
|
|
81
83
|
|
|
82
|
-
str
|
|
84
|
+
str.to_f
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
def parse_padding_side(attrs, side)
|
|
@@ -122,7 +124,7 @@ module MjmlRb
|
|
|
122
124
|
# Merge adjacent Outlook conditional comments. Applied locally within
|
|
123
125
|
# each section/wrapper to avoid incorrectly merging across sibling sections.
|
|
124
126
|
def merge_outlook_conditionals(html)
|
|
125
|
-
|
|
127
|
+
renderer.send(:merge_outlook_conditionals, html)
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
# Build an attribute string for Outlook conditional comment tags.
|
|
@@ -439,7 +441,6 @@ module MjmlRb
|
|
|
439
441
|
)
|
|
440
442
|
table_style = style_join(
|
|
441
443
|
{
|
|
442
|
-
"border-radius" => border_radius,
|
|
443
444
|
"border-collapse" => (has_border_radius ? "separate" : nil),
|
|
444
445
|
"width" => "100%"
|
|
445
446
|
}.merge(full_width ? {} : background_styles)
|
|
@@ -510,6 +511,9 @@ module MjmlRb
|
|
|
510
511
|
# the section's box width so that column children compute correct widths.
|
|
511
512
|
def render_section_columns(node, context, box_width)
|
|
512
513
|
previous_container_width = context[:container_width]
|
|
514
|
+
previous_gutter, previous_direction = context.values_at(:gutter, :section_direction)
|
|
515
|
+
attrs = self.class.default_attributes.merge(resolved_attributes(node, context))
|
|
516
|
+
context[:gutter], context[:section_direction] = attrs.values_at("gutter", "direction")
|
|
513
517
|
context[:container_width] = "#{box_width}px"
|
|
514
518
|
|
|
515
519
|
columns = node.element_children.select { |e| %w[mj-column mj-group].include?(e.tag_name) }
|
|
@@ -523,7 +527,9 @@ module MjmlRb
|
|
|
523
527
|
close_table = %(<!--[if mso | IE]></table><![endif]-->)
|
|
524
528
|
|
|
525
529
|
col_parts = with_inherited_mj_class(context, node) do
|
|
526
|
-
|
|
530
|
+
node.children.map do |col|
|
|
531
|
+
i = columns.index(col)
|
|
532
|
+
next render_node(col, context, parent: "mj-section") unless i
|
|
527
533
|
col_attrs = resolved_attributes(col, context)
|
|
528
534
|
v_align = col_attrs["vertical-align"] || "top"
|
|
529
535
|
col_css = col_attrs["css-class"]
|
|
@@ -535,10 +541,16 @@ module MjmlRb
|
|
|
535
541
|
(box_width.to_f * widths[i] / 100.0).round
|
|
536
542
|
end
|
|
537
543
|
|
|
538
|
-
|
|
544
|
+
gutter = if context[:gutter] && col.tag_name == "mj-column"
|
|
545
|
+
ColumnGutter.new(width: col_width_attr || "#{widths[i]}%", container_width: box_width,
|
|
546
|
+
gutter: context[:gutter], count: columns.size, index: i, direction: context[:section_direction])
|
|
547
|
+
end
|
|
548
|
+
padding = gutter ? "padding:#{gutter.desktop_padding('px')};" : ""
|
|
549
|
+
td_open = %(<!--[if mso | IE]><td class="#{escape_attr(outlook_class)}" style="vertical-align:#{v_align};width:#{col_px}px;#{padding}" ><![endif]-->)
|
|
539
550
|
td_close = %(<!--[if mso | IE]></td><![endif]-->)
|
|
540
551
|
|
|
541
552
|
context[:_column_width_pct] = widths[i]
|
|
553
|
+
context[:_column_gutter] = gutter
|
|
542
554
|
col_html = render_node(col, context, parent: "mj-section")
|
|
543
555
|
|
|
544
556
|
"#{td_open}\n#{col_html}\n#{td_close}"
|
|
@@ -548,6 +560,7 @@ module MjmlRb
|
|
|
548
560
|
([open_table, open_tr] + col_parts + [close_tr, close_table]).join("\n")
|
|
549
561
|
ensure
|
|
550
562
|
context[:container_width] = previous_container_width
|
|
563
|
+
context[:gutter], context[:section_direction] = previous_gutter, previous_direction
|
|
551
564
|
end
|
|
552
565
|
|
|
553
566
|
# ── mj-wrapper ─────────────────────────────────────────────────────────
|
|
@@ -692,14 +705,16 @@ module MjmlRb
|
|
|
692
705
|
# npm wrapper renders each child in its own row, unlike section which
|
|
693
706
|
# places all columns in a single row.
|
|
694
707
|
def render_wrapped_children_wrapper(node, context, container_px, gap)
|
|
695
|
-
children = node.element_children.
|
|
708
|
+
children = node.element_children.reject { |e| e.tag_name == "mj-raw" }
|
|
696
709
|
return render_children(node, context, parent: "mj-wrapper") if children.empty?
|
|
697
710
|
|
|
698
711
|
open_table = %(<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><![endif]-->)
|
|
699
712
|
close_table = %(<!--[if mso | IE]></table><![endif]-->)
|
|
700
713
|
|
|
701
714
|
section_parts = with_inherited_mj_class(context, node) do
|
|
702
|
-
children.
|
|
715
|
+
node.children.map do |child|
|
|
716
|
+
index = children.index(child)
|
|
717
|
+
next render_node(child, context, parent: "mj-wrapper") unless index
|
|
703
718
|
child_attrs = resolved_attributes(child, context)
|
|
704
719
|
child_css = child_attrs["css-class"]
|
|
705
720
|
outlook_class = suffix_css_classes(child_css)
|
|
@@ -34,7 +34,8 @@ module MjmlRb
|
|
|
34
34
|
|
|
35
35
|
SOCIAL_ALLOWED_ATTRIBUTES = {
|
|
36
36
|
"align" => "enum(left,right,center)",
|
|
37
|
-
"border
|
|
37
|
+
"border" => "string",
|
|
38
|
+
"border-radius" => "string",
|
|
38
39
|
"container-background-color" => "color",
|
|
39
40
|
"color" => "color",
|
|
40
41
|
"font-family" => "string",
|
|
@@ -63,7 +64,8 @@ module MjmlRb
|
|
|
63
64
|
"icon-position" => "enum(left,right)",
|
|
64
65
|
"background-color" => "color",
|
|
65
66
|
"color" => "color",
|
|
66
|
-
"border
|
|
67
|
+
"border" => "string",
|
|
68
|
+
"border-radius" => "string",
|
|
67
69
|
"font-family" => "string",
|
|
68
70
|
"font-size" => "unit(px)",
|
|
69
71
|
"font-style" => "string",
|
|
@@ -103,7 +105,7 @@ module MjmlRb
|
|
|
103
105
|
|
|
104
106
|
# Attributes Social parent passes down to its mj-social-element children
|
|
105
107
|
INHERITED_ATTRS = %w[
|
|
106
|
-
border-radius color font-family font-size font-weight font-style
|
|
108
|
+
border border-radius color font-family font-size font-weight font-style
|
|
107
109
|
icon-size icon-height icon-padding text-padding line-height text-decoration
|
|
108
110
|
].freeze
|
|
109
111
|
|
|
@@ -121,6 +123,7 @@ module MjmlRb
|
|
|
121
123
|
}.freeze
|
|
122
124
|
|
|
123
125
|
ELEMENT_DEFAULT_ATTRIBUTES = {
|
|
126
|
+
"border" => "0",
|
|
124
127
|
"alt" => "",
|
|
125
128
|
"align" => "left",
|
|
126
129
|
"icon-position" => "left",
|
|
@@ -198,38 +201,25 @@ module MjmlRb
|
|
|
198
201
|
def render_horizontal(node, context, social_attrs)
|
|
199
202
|
align = social_attrs["align"]
|
|
200
203
|
inherited = inherited_attrs(social_attrs)
|
|
201
|
-
elements = social_element_children(node)
|
|
202
|
-
return "" if elements.empty?
|
|
203
|
-
|
|
204
|
-
outlook_open = %(<!--[if mso | IE]><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr>)
|
|
205
|
-
outlook_close = %(</tr></table><![endif]-->)
|
|
206
|
-
|
|
207
204
|
children_html = with_inherited_mj_class(context, node) do
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
el_html
|
|
212
|
-
|
|
213
|
-
outlook_td_open = idx == 0 ? "<td>" : "</td><td>"
|
|
214
|
-
%(#{outlook_td_open}<![endif]--><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody>#{el_html}</tbody></table><!--[if mso | IE]>)
|
|
205
|
+
node.children.map do |child|
|
|
206
|
+
next render_node(child, context, parent: "mj-social") unless child.tag_name == "mj-social-element"
|
|
207
|
+
child_attrs = ELEMENT_DEFAULT_ATTRIBUTES.merge(inherited).merge(resolved_attributes(child, context))
|
|
208
|
+
el_html = render_social_element(child, child_attrs)
|
|
209
|
+
%(<!--[if mso | IE]><td><![endif]--><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody>#{el_html}</tbody></table><!--[if mso | IE]></td><![endif]-->)
|
|
215
210
|
end.join
|
|
216
211
|
end
|
|
217
|
-
|
|
218
|
-
%(#{outlook_open}#{children_html}</td>#{outlook_close})
|
|
212
|
+
%(<!--[if mso | IE]><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation"><tr><![endif]-->#{children_html}<!--[if mso | IE]></tr></table><![endif]-->)
|
|
219
213
|
end
|
|
220
214
|
|
|
221
215
|
def render_vertical(node, context, social_attrs)
|
|
222
216
|
inherited = inherited_attrs(social_attrs)
|
|
223
|
-
elements = social_element_children(node)
|
|
224
|
-
|
|
225
217
|
children_html = with_inherited_mj_class(context, node) do
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
render_social_element(child, merged_attrs)
|
|
218
|
+
node.children.map do |child|
|
|
219
|
+
next render_node(child, context, parent: "mj-social") unless child.tag_name == "mj-social-element"
|
|
220
|
+
render_social_element(child, ELEMENT_DEFAULT_ATTRIBUTES.merge(inherited).merge(resolved_attributes(child, context)))
|
|
230
221
|
end.join
|
|
231
222
|
end
|
|
232
|
-
|
|
233
223
|
%(<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0px;"><tbody>#{children_html}</tbody></table>)
|
|
234
224
|
end
|
|
235
225
|
|
|
@@ -237,7 +227,7 @@ module MjmlRb
|
|
|
237
227
|
|
|
238
228
|
def render_social_element(node, attrs)
|
|
239
229
|
a = attrs # already merged with defaults by caller
|
|
240
|
-
net_name = node
|
|
230
|
+
net_name = node["name"]
|
|
241
231
|
network = SOCIAL_NETWORKS[net_name] || {}
|
|
242
232
|
|
|
243
233
|
# Resolve href: if network has a share-url, substitute [[URL]] with the raw href
|
|
@@ -296,6 +286,7 @@ module MjmlRb
|
|
|
296
286
|
)
|
|
297
287
|
|
|
298
288
|
img_style = style_join(
|
|
289
|
+
"border" => a["border"],
|
|
299
290
|
"border-radius" => border_radius,
|
|
300
291
|
"display" => "block"
|
|
301
292
|
)
|
|
@@ -325,7 +316,7 @@ module MjmlRb
|
|
|
325
316
|
HTML
|
|
326
317
|
|
|
327
318
|
# Content cell (text)
|
|
328
|
-
content = node.
|
|
319
|
+
content = raw_inner_for_body(node).strip
|
|
329
320
|
content_cell = if content.empty?
|
|
330
321
|
""
|
|
331
322
|
else
|
|
@@ -344,7 +335,7 @@ module MjmlRb
|
|
|
344
335
|
"text-decoration" => a["text-decoration"]
|
|
345
336
|
)
|
|
346
337
|
|
|
347
|
-
inner_text = " #{
|
|
338
|
+
inner_text = " #{content} "
|
|
348
339
|
text_elem = if has_link
|
|
349
340
|
link_attrs_str = html_attrs({ "href" => final_href, "style" => text_style, "rel" => rel_val, "target" => target_val })
|
|
350
341
|
%(<a#{link_attrs_str}>#{inner_text}</a>)
|
|
@@ -118,7 +118,7 @@ module MjmlRb
|
|
|
118
118
|
|
|
119
119
|
def serialize_table_node(node)
|
|
120
120
|
return node.content.to_s if node.text?
|
|
121
|
-
return "" if node.comment?
|
|
121
|
+
return "<!--#{node.content}-->" if node.comment?
|
|
122
122
|
|
|
123
123
|
attrs = normalize_table_node_attributes(node)
|
|
124
124
|
attr_html = attrs.map { |key, value| %( #{key}="#{escape_attr(value)}") }.join
|
|
@@ -130,7 +130,7 @@ module MjmlRb
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def normalize_table_node_attributes(node)
|
|
133
|
-
attrs = node.
|
|
133
|
+
attrs = node_string_attributes(node).dup
|
|
134
134
|
style_map = parse_style_map(attrs["style"])
|
|
135
135
|
|
|
136
136
|
if %w[table td th a].include?(node.tag_name)
|
data/lib/mjml-rb/config_file.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require_relative "options"
|
|
2
3
|
|
|
3
4
|
module MjmlRb
|
|
4
5
|
class ConfigFile
|
|
5
|
-
DEFAULT_NAME = ".
|
|
6
|
+
DEFAULT_NAME = ".mjmlconfig"
|
|
7
|
+
NAMES = [DEFAULT_NAME, ".mjmlrc", ".mjmlconfig.js"].freeze
|
|
6
8
|
|
|
7
9
|
class ConfigError < StandardError
|
|
8
10
|
attr_reader :path
|
|
@@ -13,30 +15,38 @@ module MjmlRb
|
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
def self.load(
|
|
17
|
-
path = File.
|
|
18
|
-
|
|
18
|
+
def self.load(location = Dir.pwd)
|
|
19
|
+
path = if File.directory?(location)
|
|
20
|
+
NAMES.map { |name| File.join(location, name) }.find { |candidate| File.file?(candidate) }
|
|
21
|
+
else
|
|
22
|
+
location
|
|
23
|
+
end
|
|
24
|
+
return {} unless path
|
|
25
|
+
if path.end_with?(".js")
|
|
26
|
+
raise ConfigError.new("JavaScript MJML configuration is unsupported; use JSON and register Ruby components", path: path)
|
|
27
|
+
end
|
|
19
28
|
|
|
20
29
|
raw = JSON.parse(File.read(path))
|
|
30
|
+
raise ArgumentError, "Configuration root must be an object" unless raw.is_a?(Hash)
|
|
21
31
|
config = {}
|
|
22
|
-
|
|
32
|
+
if raw.key?("options") && !raw["options"].is_a?(Hash)
|
|
33
|
+
raise ArgumentError, "Configuration options must be an object"
|
|
34
|
+
end
|
|
35
|
+
config[:options] = Options.normalize(raw["options"]) if raw["options"]
|
|
23
36
|
if raw["packages"].is_a?(Array)
|
|
24
|
-
raw["packages"].each do |
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
raw["packages"].each do |package|
|
|
38
|
+
unless package.is_a?(String) && package.end_with?(".rb")
|
|
39
|
+
raise ArgumentError, "JavaScript component packages are unsupported; use a Ruby .rb component file"
|
|
40
|
+
end
|
|
41
|
+
require File.expand_path(package, File.dirname(path))
|
|
27
42
|
end
|
|
28
43
|
config[:packages_loaded] = raw["packages"]
|
|
29
44
|
end
|
|
30
|
-
|
|
31
|
-
if raw["options"].is_a?(Hash)
|
|
32
|
-
config[:options] = raw["options"].each_with_object({}) do |(k, v), memo|
|
|
33
|
-
memo[k.to_s.tr("-", "_").to_sym] = v
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
45
|
config
|
|
38
46
|
rescue JSON::ParserError => e
|
|
39
47
|
raise ConfigError.new("Failed to parse #{path}: #{e.message}", path: path)
|
|
48
|
+
rescue ArgumentError, SystemCallError, LoadError => e
|
|
49
|
+
raise ConfigError.new("Failed to load #{path}: #{e.message}", path: path)
|
|
40
50
|
end
|
|
41
51
|
end
|
|
42
52
|
end
|