mjml-rb 0.2.1
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/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +79 -0
- data/bin/mjml +6 -0
- data/lib/mjml-rb/ast_node.rb +34 -0
- data/lib/mjml-rb/cli.rb +305 -0
- data/lib/mjml-rb/compiler.rb +109 -0
- data/lib/mjml-rb/components/accordion.rb +210 -0
- data/lib/mjml-rb/components/base.rb +76 -0
- data/lib/mjml-rb/components/body.rb +46 -0
- data/lib/mjml-rb/components/breakpoint.rb +25 -0
- data/lib/mjml-rb/components/button.rb +157 -0
- data/lib/mjml-rb/components/column.rb +241 -0
- data/lib/mjml-rb/components/divider.rb +120 -0
- data/lib/mjml-rb/components/hero.rb +285 -0
- data/lib/mjml-rb/components/html_attributes.rb +32 -0
- data/lib/mjml-rb/components/image.rb +183 -0
- data/lib/mjml-rb/components/navbar.rb +279 -0
- data/lib/mjml-rb/components/section.rb +331 -0
- data/lib/mjml-rb/components/social.rb +303 -0
- data/lib/mjml-rb/components/spacer.rb +63 -0
- data/lib/mjml-rb/components/table.rb +162 -0
- data/lib/mjml-rb/components/text.rb +71 -0
- data/lib/mjml-rb/dependencies.rb +67 -0
- data/lib/mjml-rb/migrator.rb +18 -0
- data/lib/mjml-rb/parser.rb +169 -0
- data/lib/mjml-rb/renderer.rb +513 -0
- data/lib/mjml-rb/result.rb +23 -0
- data/lib/mjml-rb/validator.rb +187 -0
- data/lib/mjml-rb/version.rb +3 -0
- data/lib/mjml-rb.rb +30 -0
- data/mjml-rb.gemspec +23 -0
- metadata +101 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
module Components
|
|
5
|
+
class Accordion < Base
|
|
6
|
+
TAGS = %w[
|
|
7
|
+
mj-accordion
|
|
8
|
+
mj-accordion-element
|
|
9
|
+
mj-accordion-title
|
|
10
|
+
mj-accordion-text
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
HEAD_STYLE = <<~CSS.freeze
|
|
14
|
+
noinput.mj-accordion-checkbox { display:block!important; }
|
|
15
|
+
@media yahoo, only screen and (min-width:0) {
|
|
16
|
+
.mj-accordion-element { display:block; }
|
|
17
|
+
input.mj-accordion-checkbox, .mj-accordion-less { display:none!important; }
|
|
18
|
+
input.mj-accordion-checkbox + * .mj-accordion-title { cursor:pointer; touch-action:manipulation; -webkit-user-select:none; -moz-user-select:none; user-select:none; }
|
|
19
|
+
input.mj-accordion-checkbox + * .mj-accordion-content { overflow:hidden; display:none; }
|
|
20
|
+
input.mj-accordion-checkbox + * .mj-accordion-more { display:block!important; }
|
|
21
|
+
input.mj-accordion-checkbox:checked + * .mj-accordion-content { display:block; }
|
|
22
|
+
input.mj-accordion-checkbox:checked + * .mj-accordion-more { display:none!important; }
|
|
23
|
+
input.mj-accordion-checkbox:checked + * .mj-accordion-less { display:block!important; }
|
|
24
|
+
}
|
|
25
|
+
.moz-text-html input.mj-accordion-checkbox + * .mj-accordion-title { cursor:auto; touch-action:auto; -webkit-user-select:auto; -moz-user-select:auto; user-select:auto; }
|
|
26
|
+
.moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow:hidden; display:block; }
|
|
27
|
+
.moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display:none; }
|
|
28
|
+
CSS
|
|
29
|
+
|
|
30
|
+
DEFAULTS = {
|
|
31
|
+
"border" => "2px solid black",
|
|
32
|
+
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
33
|
+
"icon-align" => "middle",
|
|
34
|
+
"icon-wrapped-url" => "https://i.imgur.com/bIXv1bk.png",
|
|
35
|
+
"icon-wrapped-alt" => "+",
|
|
36
|
+
"icon-unwrapped-url" => "https://i.imgur.com/w4uTygT.png",
|
|
37
|
+
"icon-unwrapped-alt" => "-",
|
|
38
|
+
"icon-position" => "right",
|
|
39
|
+
"icon-height" => "32px",
|
|
40
|
+
"icon-width" => "32px",
|
|
41
|
+
"padding" => "10px 25px"
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
TITLE_DEFAULTS = {
|
|
45
|
+
"font-size" => "13px",
|
|
46
|
+
"padding" => "16px"
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
TEXT_DEFAULTS = {
|
|
50
|
+
"font-size" => "13px",
|
|
51
|
+
"line-height" => "1",
|
|
52
|
+
"padding" => "16px"
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
def tags
|
|
56
|
+
TAGS
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def head_style
|
|
60
|
+
HEAD_STYLE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def head_style_tags
|
|
64
|
+
["mj-accordion"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
68
|
+
case tag_name
|
|
69
|
+
when "mj-accordion"
|
|
70
|
+
render_accordion(node, context, attrs)
|
|
71
|
+
when "mj-accordion-element"
|
|
72
|
+
render_accordion_element(node, context, DEFAULTS.merge(attrs))
|
|
73
|
+
when "mj-accordion-title"
|
|
74
|
+
render_accordion_title(node, DEFAULTS.merge(attrs))
|
|
75
|
+
when "mj-accordion-text"
|
|
76
|
+
render_accordion_text(node, DEFAULTS.merge(attrs))
|
|
77
|
+
else
|
|
78
|
+
render_children(node, context, parent: parent)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def render_accordion(node, context, attrs)
|
|
85
|
+
accordion_attrs = DEFAULTS.merge(attrs)
|
|
86
|
+
outer_style = style_join(
|
|
87
|
+
"padding" => accordion_attrs["padding"],
|
|
88
|
+
"background-color" => accordion_attrs["container-background-color"]
|
|
89
|
+
)
|
|
90
|
+
table_style = style_join(
|
|
91
|
+
"width" => "100%",
|
|
92
|
+
"border-collapse" => "collapse",
|
|
93
|
+
"border" => accordion_attrs["border"],
|
|
94
|
+
"border-bottom" => "none",
|
|
95
|
+
"font-family" => accordion_attrs["font-family"]
|
|
96
|
+
)
|
|
97
|
+
inner = node.element_children.map do |child|
|
|
98
|
+
case child.tag_name
|
|
99
|
+
when "mj-accordion-element"
|
|
100
|
+
render_accordion_element(child, context, accordion_attrs)
|
|
101
|
+
when "mj-raw"
|
|
102
|
+
raw_inner(child)
|
|
103
|
+
else
|
|
104
|
+
render_node(child, context, parent: "mj-accordion")
|
|
105
|
+
end
|
|
106
|
+
end.join
|
|
107
|
+
|
|
108
|
+
%(<tr><td style="#{outer_style}"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" class="mj-accordion" style="#{table_style}"><tbody>#{inner}</tbody></table></td></tr>)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_accordion_element(node, context, parent_attrs)
|
|
112
|
+
attrs = parent_attrs.merge(resolved_attributes(node, context))
|
|
113
|
+
td_style = style_join(
|
|
114
|
+
"padding" => "0px",
|
|
115
|
+
"background-color" => attrs["background-color"]
|
|
116
|
+
)
|
|
117
|
+
label_style = style_join(
|
|
118
|
+
"font-size" => "13px",
|
|
119
|
+
"font-family" => attrs["font-family"] || parent_attrs["font-family"]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
children = node.element_children
|
|
123
|
+
has_title = children.any? { |child| child.tag_name == "mj-accordion-title" }
|
|
124
|
+
has_text = children.any? { |child| child.tag_name == "mj-accordion-text" }
|
|
125
|
+
content = []
|
|
126
|
+
content << render_accordion_title(nil, attrs) unless has_title
|
|
127
|
+
|
|
128
|
+
children.each do |child|
|
|
129
|
+
case child.tag_name
|
|
130
|
+
when "mj-accordion-title"
|
|
131
|
+
child_attrs = attrs.merge(resolved_attributes(child, context))
|
|
132
|
+
content << render_accordion_title(child, child_attrs)
|
|
133
|
+
when "mj-accordion-text"
|
|
134
|
+
child_attrs = attrs.merge(resolved_attributes(child, context))
|
|
135
|
+
content << render_accordion_text(child, child_attrs)
|
|
136
|
+
when "mj-raw"
|
|
137
|
+
content << raw_inner(child)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
content << render_accordion_text(nil, attrs) unless has_text
|
|
141
|
+
|
|
142
|
+
css_class = attrs["css-class"] ? %( class="#{escape_attr(attrs["css-class"])}") : ""
|
|
143
|
+
%(<tr#{css_class}><td style="#{td_style}"><label class="mj-accordion-element" style="#{label_style}"><input class="mj-accordion-checkbox" type="checkbox" style="display:none;" /><div>#{content.join("\n")}</div></label></td></tr>)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render_accordion_title(node, attrs)
|
|
147
|
+
title_attrs = TITLE_DEFAULTS.merge(attrs)
|
|
148
|
+
td_style = style_join(
|
|
149
|
+
"width" => "100%",
|
|
150
|
+
"background-color" => title_attrs["background-color"],
|
|
151
|
+
"color" => title_attrs["color"],
|
|
152
|
+
"font-size" => title_attrs["font-size"],
|
|
153
|
+
"font-family" => title_attrs["font-family"] || DEFAULTS["font-family"],
|
|
154
|
+
"font-weight" => title_attrs["font-weight"],
|
|
155
|
+
"padding" => title_attrs["padding"],
|
|
156
|
+
"padding-bottom" => title_attrs["padding-bottom"],
|
|
157
|
+
"padding-left" => title_attrs["padding-left"],
|
|
158
|
+
"padding-right" => title_attrs["padding-right"],
|
|
159
|
+
"padding-top" => title_attrs["padding-top"]
|
|
160
|
+
)
|
|
161
|
+
td2_style = style_join(
|
|
162
|
+
"padding" => "16px",
|
|
163
|
+
"background" => title_attrs["background-color"],
|
|
164
|
+
"vertical-align" => title_attrs["icon-align"] || "middle"
|
|
165
|
+
)
|
|
166
|
+
icon_style = style_join(
|
|
167
|
+
"display" => "none",
|
|
168
|
+
"width" => title_attrs["icon-width"] || DEFAULTS["icon-width"],
|
|
169
|
+
"height" => title_attrs["icon-height"] || DEFAULTS["icon-height"]
|
|
170
|
+
)
|
|
171
|
+
table_style = style_join(
|
|
172
|
+
"width" => "100%",
|
|
173
|
+
"border-bottom" => title_attrs["border"] || DEFAULTS["border"]
|
|
174
|
+
)
|
|
175
|
+
title_content = node ? raw_inner(node) : ""
|
|
176
|
+
title_cell = %(<td style="#{td_style}">#{title_content}</td>)
|
|
177
|
+
icon_cell = %(<td class="mj-accordion-ico" style="#{td2_style}"><img src="#{escape_attr(title_attrs["icon-wrapped-url"] || DEFAULTS["icon-wrapped-url"])}" alt="#{escape_attr(title_attrs["icon-wrapped-alt"] || DEFAULTS["icon-wrapped-alt"])}" class="mj-accordion-more" style="#{icon_style}" /><img src="#{escape_attr(title_attrs["icon-unwrapped-url"] || DEFAULTS["icon-unwrapped-url"])}" alt="#{escape_attr(title_attrs["icon-unwrapped-alt"] || DEFAULTS["icon-unwrapped-alt"])}" class="mj-accordion-less" style="#{icon_style}" /></td>)
|
|
178
|
+
cells = title_attrs["icon-position"] == "left" ? "#{icon_cell}#{title_cell}" : "#{title_cell}#{icon_cell}"
|
|
179
|
+
|
|
180
|
+
%(<div class="mj-accordion-title"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="#{table_style}"><tbody><tr>#{cells}</tr></tbody></table></div>)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render_accordion_text(node, attrs)
|
|
184
|
+
text_attrs = TEXT_DEFAULTS.merge(attrs)
|
|
185
|
+
td_style = style_join(
|
|
186
|
+
"background" => text_attrs["background-color"],
|
|
187
|
+
"font-size" => text_attrs["font-size"],
|
|
188
|
+
"font-family" => text_attrs["font-family"] || DEFAULTS["font-family"],
|
|
189
|
+
"font-weight" => text_attrs["font-weight"],
|
|
190
|
+
"letter-spacing" => text_attrs["letter-spacing"],
|
|
191
|
+
"line-height" => text_attrs["line-height"],
|
|
192
|
+
"color" => text_attrs["color"],
|
|
193
|
+
"padding" => text_attrs["padding"],
|
|
194
|
+
"padding-bottom" => text_attrs["padding-bottom"],
|
|
195
|
+
"padding-left" => text_attrs["padding-left"],
|
|
196
|
+
"padding-right" => text_attrs["padding-right"],
|
|
197
|
+
"padding-top" => text_attrs["padding-top"]
|
|
198
|
+
)
|
|
199
|
+
table_style = style_join(
|
|
200
|
+
"width" => "100%",
|
|
201
|
+
"border-bottom" => text_attrs["border"] || DEFAULTS["border"]
|
|
202
|
+
)
|
|
203
|
+
content = node ? raw_inner(node) : ""
|
|
204
|
+
css_class = text_attrs["css-class"] ? %( class="#{escape_attr(text_attrs["css-class"])}") : ""
|
|
205
|
+
|
|
206
|
+
%(<div class="mj-accordion-content"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="#{table_style}"><tbody><tr><td#{css_class} style="#{td_style}">#{content}</td></tr></tbody></table></div>)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module MjmlRb
|
|
2
|
+
module Components
|
|
3
|
+
class Base
|
|
4
|
+
class << self
|
|
5
|
+
def tags
|
|
6
|
+
const_defined?(:TAGS) ? const_get(:TAGS) : []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def allowed_attributes
|
|
10
|
+
const_defined?(:ALLOWED_ATTRIBUTES) ? const_get(:ALLOWED_ATTRIBUTES) : {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def default_attributes
|
|
14
|
+
if const_defined?(:DEFAULT_ATTRIBUTES)
|
|
15
|
+
const_get(:DEFAULT_ATTRIBUTES)
|
|
16
|
+
elsif const_defined?(:DEFAULTS)
|
|
17
|
+
const_get(:DEFAULTS)
|
|
18
|
+
else
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(renderer)
|
|
25
|
+
@renderer = renderer
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tags
|
|
29
|
+
self.class.tags
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :renderer
|
|
35
|
+
|
|
36
|
+
def render_node(node, context, parent:)
|
|
37
|
+
renderer.send(:render_node, node, context, parent: parent)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_children(node, context, parent:)
|
|
41
|
+
renderer.send(:render_children, node, context, parent: parent)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolved_attributes(node, context)
|
|
45
|
+
renderer.send(:resolved_attributes, node, context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def raw_inner(node)
|
|
49
|
+
renderer.send(:raw_inner, node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Like raw_inner but HTML-escapes text nodes. Use for components such as
|
|
53
|
+
# mj-text where the inner content is treated as HTML but bare text must
|
|
54
|
+
# be properly encoded (e.g. & -> &).
|
|
55
|
+
def html_inner(node)
|
|
56
|
+
renderer.send(:html_inner, node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def escape_html(value)
|
|
60
|
+
renderer.send(:escape_html, value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def style_join(hash)
|
|
64
|
+
renderer.send(:style_join, hash)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def escape_attr(value)
|
|
68
|
+
renderer.send(:escape_attr, value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def html_attrs(hash)
|
|
72
|
+
renderer.send(:html_attrs, hash)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
module Components
|
|
5
|
+
class Body < Base
|
|
6
|
+
TAGS = ["mj-body"].freeze
|
|
7
|
+
|
|
8
|
+
ALLOWED_ATTRIBUTES = {
|
|
9
|
+
"width" => "unit(px)",
|
|
10
|
+
"background-color" => "color"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
DEFAULT_ATTRIBUTES = {
|
|
14
|
+
"width" => "600px"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def tags
|
|
18
|
+
TAGS
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
22
|
+
return render_children(node, context, parent: parent) unless tag_name == "mj-body"
|
|
23
|
+
|
|
24
|
+
body_attrs = self.class.default_attributes.merge(attrs)
|
|
25
|
+
background_color = body_attrs["background-color"]
|
|
26
|
+
container_width = body_attrs["width"]
|
|
27
|
+
|
|
28
|
+
context[:background_color] = background_color
|
|
29
|
+
context[:container_width] = container_width
|
|
30
|
+
|
|
31
|
+
div_attributes = {
|
|
32
|
+
"aria-label" => context[:title].to_s.empty? ? nil : context[:title],
|
|
33
|
+
"aria-roledescription" => "email",
|
|
34
|
+
"class" => body_attrs["css-class"],
|
|
35
|
+
"role" => "article",
|
|
36
|
+
"lang" => context[:lang],
|
|
37
|
+
"dir" => context[:dir],
|
|
38
|
+
"style" => style_join("background-color" => background_color)
|
|
39
|
+
}
|
|
40
|
+
children = render_children(node, context, parent: "mj-body")
|
|
41
|
+
|
|
42
|
+
%(<div#{html_attrs(div_attributes)}>#{children}</div>)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
module Components
|
|
5
|
+
class Breakpoint < Base
|
|
6
|
+
TAGS = ["mj-breakpoint"].freeze
|
|
7
|
+
|
|
8
|
+
ALLOWED_ATTRIBUTES = {
|
|
9
|
+
"width" => "unit(px)"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
DEFAULT_ATTRIBUTES = {
|
|
13
|
+
"width" => "480px"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def tags
|
|
17
|
+
TAGS
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
21
|
+
""
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
module Components
|
|
5
|
+
class Button < Base
|
|
6
|
+
TAGS = ["mj-button"].freeze
|
|
7
|
+
|
|
8
|
+
DEFAULTS = {
|
|
9
|
+
"align" => "center",
|
|
10
|
+
"background-color" => "#414141",
|
|
11
|
+
"border" => "none",
|
|
12
|
+
"border-radius" => "3px",
|
|
13
|
+
"color" => "#ffffff",
|
|
14
|
+
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
15
|
+
"font-size" => "13px",
|
|
16
|
+
"font-weight" => "normal",
|
|
17
|
+
"inner-padding" => "10px 25px",
|
|
18
|
+
"line-height" => "120%",
|
|
19
|
+
"padding" => "10px 25px",
|
|
20
|
+
"target" => "_blank",
|
|
21
|
+
"text-decoration" => "none",
|
|
22
|
+
"text-transform" => "none",
|
|
23
|
+
"vertical-align" => "middle"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def tags
|
|
27
|
+
TAGS
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
31
|
+
a = DEFAULTS.merge(attrs)
|
|
32
|
+
|
|
33
|
+
bg_color = a["background-color"]
|
|
34
|
+
inner_padding = a["inner-padding"]
|
|
35
|
+
vertical_align = a["vertical-align"]
|
|
36
|
+
href = a["href"]
|
|
37
|
+
tag = href ? "a" : "p"
|
|
38
|
+
|
|
39
|
+
outer_td_style = style_join(
|
|
40
|
+
"background" => a["container-background-color"],
|
|
41
|
+
"font-size" => "0px",
|
|
42
|
+
"padding" => a["padding"],
|
|
43
|
+
"padding-top" => a["padding-top"],
|
|
44
|
+
"padding-right" => a["padding-right"],
|
|
45
|
+
"padding-bottom" => a["padding-bottom"],
|
|
46
|
+
"padding-left" => a["padding-left"],
|
|
47
|
+
"word-break" => "break-word"
|
|
48
|
+
)
|
|
49
|
+
outer_td_attrs = {
|
|
50
|
+
"align" => a["align"],
|
|
51
|
+
"vertical-align" => vertical_align,
|
|
52
|
+
"class" => a["css-class"],
|
|
53
|
+
"style" => outer_td_style
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
table_style = style_join(
|
|
57
|
+
"border-collapse" => "separate",
|
|
58
|
+
"width" => a["width"],
|
|
59
|
+
"line-height" => "100%"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
td_style = style_join(
|
|
63
|
+
"border" => a["border"],
|
|
64
|
+
"border-bottom" => a["border-bottom"],
|
|
65
|
+
"border-left" => a["border-left"],
|
|
66
|
+
"border-radius" => a["border-radius"],
|
|
67
|
+
"border-right" => a["border-right"],
|
|
68
|
+
"border-top" => a["border-top"],
|
|
69
|
+
"cursor" => "auto",
|
|
70
|
+
"font-style" => a["font-style"],
|
|
71
|
+
"height" => a["height"],
|
|
72
|
+
"mso-padding-alt" => inner_padding,
|
|
73
|
+
"text-align" => a["text-align"],
|
|
74
|
+
"background" => bg_color == "none" ? nil : bg_color
|
|
75
|
+
)
|
|
76
|
+
td_attrs = {
|
|
77
|
+
"align" => "center",
|
|
78
|
+
"bgcolor" => bg_color == "none" ? nil : bg_color,
|
|
79
|
+
"role" => "presentation",
|
|
80
|
+
"style" => td_style,
|
|
81
|
+
"valign" => vertical_align
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
link_style = style_join(
|
|
85
|
+
"display" => "inline-block",
|
|
86
|
+
"width" => calculate_a_width(a),
|
|
87
|
+
"background" => bg_color == "none" ? nil : bg_color,
|
|
88
|
+
"color" => a["color"],
|
|
89
|
+
"font-family" => a["font-family"],
|
|
90
|
+
"font-size" => a["font-size"],
|
|
91
|
+
"font-style" => a["font-style"],
|
|
92
|
+
"font-weight" => a["font-weight"],
|
|
93
|
+
"line-height" => a["line-height"],
|
|
94
|
+
"letter-spacing" => a["letter-spacing"],
|
|
95
|
+
"margin" => "0",
|
|
96
|
+
"text-decoration" => a["text-decoration"],
|
|
97
|
+
"text-transform" => a["text-transform"],
|
|
98
|
+
"padding" => inner_padding,
|
|
99
|
+
"mso-padding-alt" => "0px",
|
|
100
|
+
"border-radius" => a["border-radius"]
|
|
101
|
+
)
|
|
102
|
+
link_attrs = { "style" => link_style }
|
|
103
|
+
if tag == "a"
|
|
104
|
+
link_attrs["href"] = escape_attr(href)
|
|
105
|
+
link_attrs["name"] = a["name"]
|
|
106
|
+
link_attrs["rel"] = a["rel"]
|
|
107
|
+
link_attrs["title"] = a["title"]
|
|
108
|
+
link_attrs["target"] = a["target"]
|
|
109
|
+
else
|
|
110
|
+
link_attrs["name"] = a["name"]
|
|
111
|
+
link_attrs["title"] = a["title"]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
content = raw_inner(node)
|
|
115
|
+
inner_tag = %(<#{tag}#{html_attrs(link_attrs)}>#{content}</#{tag}>)
|
|
116
|
+
table = %(<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="#{table_style}"><tbody><tr><td#{html_attrs(td_attrs)}>#{inner_tag}</td></tr></tbody></table>)
|
|
117
|
+
|
|
118
|
+
%(<tr><td#{html_attrs(outer_td_attrs)}>#{table}</td></tr>)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def calculate_a_width(attrs)
|
|
124
|
+
width = attrs["width"]
|
|
125
|
+
return nil unless width
|
|
126
|
+
return nil unless width =~ /^(\d+(?:\.\d+)?)\s*px$/
|
|
127
|
+
|
|
128
|
+
parsed_width = ::Regexp.last_match(1).to_f
|
|
129
|
+
inner_padding = attrs["inner-padding"] || "10px 25px"
|
|
130
|
+
parts = inner_padding.split(/\s+/)
|
|
131
|
+
left_pad = shorthand_value(parts, :left).to_f
|
|
132
|
+
right_pad = shorthand_value(parts, :right).to_f
|
|
133
|
+
border_left = parse_border_width(attrs["border-left"] || attrs["border"] || "none")
|
|
134
|
+
border_right = parse_border_width(attrs["border-right"] || attrs["border"] || "none")
|
|
135
|
+
|
|
136
|
+
result = parsed_width - left_pad - right_pad - border_left - border_right
|
|
137
|
+
"#{result.to_i}px"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def shorthand_value(parts, side)
|
|
141
|
+
case parts.length
|
|
142
|
+
when 1 then parts[0]
|
|
143
|
+
when 2, 3 then parts[1]
|
|
144
|
+
when 4 then side == :left ? parts[3] : parts[1]
|
|
145
|
+
else "0"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_border_width(border_str)
|
|
150
|
+
return 0 if border_str.nil? || border_str.strip == "none"
|
|
151
|
+
|
|
152
|
+
m = border_str.match(/(\d+(?:\.\d+)?)\s*px/)
|
|
153
|
+
m ? m[1].to_f : 0
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|