mjml-rb 0.2.35 → 0.2.36
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/lib/mjml-rb/compiler.rb +1 -0
- data/lib/mjml-rb/parser.rb +61 -20
- data/lib/mjml-rb/renderer.rb +32 -4
- data/lib/mjml-rb/validator.rb +16 -0
- data/lib/mjml-rb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2075cd423603c4070dae0acdbecf052f474931e96c079d6369eabbefb505306c
|
|
4
|
+
data.tar.gz: 1d5fd4664d222e546cbf61c51f1e8fbe6e76880d9e1e0eb858cce266cec58989
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e82ec6fe3d8af7361b3fe5691eed18449724f3b4fc5c2318077ccfe5f406b20bed0f8aaa724a059d2298a53d8a4a04a470ee2484ab259f007d5a67fa4da8e08
|
|
7
|
+
data.tar.gz: a09686d9b4cffab082e5c3bb0a7b3049c2702e857eaec5234aa3185c1cb8b47949e772711470ea29b9d0884932c7b5a9bc62016bcb76f8a96fd348d2688eee18
|
data/lib/mjml-rb/compiler.rb
CHANGED
data/lib/mjml-rb/parser.rb
CHANGED
|
@@ -66,6 +66,7 @@ module MjmlRb
|
|
|
66
66
|
return xml if includes.empty?
|
|
67
67
|
|
|
68
68
|
css_includes = []
|
|
69
|
+
head_includes = []
|
|
69
70
|
|
|
70
71
|
includes.reverse_each do |include_node|
|
|
71
72
|
path_attr = include_node.attributes["path"]
|
|
@@ -109,23 +110,34 @@ module MjmlRb
|
|
|
109
110
|
replacement = if include_type == "html"
|
|
110
111
|
%(<mj-raw><![CDATA[#{escape_cdata(include_content)}]]></mj-raw>)
|
|
111
112
|
else
|
|
112
|
-
prepared =
|
|
113
|
-
|
|
114
|
-
expand_includes(prepared, options.merge(
|
|
113
|
+
prepared = prepare_mjml_include_document(include_content)
|
|
114
|
+
prepared = wrap_ending_tags_in_cdata(normalize_html_void_tags(prepared))
|
|
115
|
+
expanded = expand_includes(prepared, options.merge(
|
|
115
116
|
actual_path: resolved_path,
|
|
116
117
|
file_path: File.dirname(resolved_path)
|
|
117
118
|
), included_in + [resolved_path])
|
|
119
|
+
body_children, include_head_children = extract_mjml_include_children(expanded)
|
|
120
|
+
head_includes.unshift(*include_head_children)
|
|
121
|
+
body_children
|
|
118
122
|
end
|
|
119
123
|
|
|
120
|
-
fragment = Document.new(sanitize_bare_ampersands("<include-root>#{replacement}</include-root>"))
|
|
121
124
|
insert_before = include_node
|
|
122
|
-
|
|
125
|
+
replacement_nodes = if replacement.is_a?(Array)
|
|
126
|
+
replacement
|
|
127
|
+
else
|
|
128
|
+
fragment = Document.new(sanitize_bare_ampersands("<include-root>#{replacement}</include-root>"))
|
|
129
|
+
fragment.root.children.map { |child| deep_clone(child) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
replacement_nodes.each do |child|
|
|
123
133
|
annotate_include_source(child, resolved_path) if child.is_a?(Element)
|
|
124
|
-
parent.insert_before(insert_before,
|
|
134
|
+
parent.insert_before(insert_before, child)
|
|
125
135
|
end
|
|
126
136
|
parent.delete(include_node)
|
|
127
137
|
end
|
|
128
138
|
|
|
139
|
+
inject_head_includes(doc, head_includes) unless head_includes.empty?
|
|
140
|
+
|
|
129
141
|
# Inject CSS includes into mj-head
|
|
130
142
|
unless css_includes.empty?
|
|
131
143
|
inject_css_includes(doc, css_includes)
|
|
@@ -138,22 +150,34 @@ module MjmlRb
|
|
|
138
150
|
raise ParseError, "Failed to parse included content: #{e.message}"
|
|
139
151
|
end
|
|
140
152
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
return
|
|
153
|
+
def prepare_mjml_include_document(content)
|
|
154
|
+
stripped = strip_xml_declaration(content)
|
|
155
|
+
return stripped if stripped.match?(/<mjml(?=[\s>])/i)
|
|
156
|
+
|
|
157
|
+
"<mjml><mj-body>#{stripped}</mj-body></mjml>"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_mjml_include_children(xml)
|
|
161
|
+
include_doc = Document.new(sanitize_bare_ampersands(xml))
|
|
162
|
+
mjml_root = include_doc.root
|
|
163
|
+
return [[], []] unless mjml_root&.name == "mjml"
|
|
144
164
|
|
|
145
|
-
|
|
165
|
+
body = XPath.first(mjml_root, "mj-body")
|
|
146
166
|
head = XPath.first(mjml_root, "mj-head")
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
|
|
168
|
+
[
|
|
169
|
+
body ? body.children.map { |child| deep_clone(child) } : [],
|
|
170
|
+
head ? head.children.map { |child| deep_clone(child) } : []
|
|
171
|
+
]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inject_head_includes(doc, head_includes)
|
|
175
|
+
head = ensure_head(doc)
|
|
176
|
+
head_includes.each { |child| head.add(child) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def inject_css_includes(doc, css_includes)
|
|
180
|
+
head = ensure_head(doc)
|
|
157
181
|
|
|
158
182
|
# Add each CSS include as an mj-style element
|
|
159
183
|
css_includes.each do |css_include|
|
|
@@ -164,6 +188,23 @@ module MjmlRb
|
|
|
164
188
|
end
|
|
165
189
|
end
|
|
166
190
|
|
|
191
|
+
def ensure_head(doc)
|
|
192
|
+
mjml_root = doc.root
|
|
193
|
+
return unless mjml_root
|
|
194
|
+
|
|
195
|
+
head = XPath.first(mjml_root, "mj-head")
|
|
196
|
+
return head if head
|
|
197
|
+
|
|
198
|
+
head = Element.new("mj-head")
|
|
199
|
+
body = XPath.first(mjml_root, "mj-body")
|
|
200
|
+
if body
|
|
201
|
+
mjml_root.insert_before(body, head)
|
|
202
|
+
else
|
|
203
|
+
mjml_root.add(head)
|
|
204
|
+
end
|
|
205
|
+
head
|
|
206
|
+
end
|
|
207
|
+
|
|
167
208
|
def strip_xml_declaration(content)
|
|
168
209
|
content.sub(/\A<\?xml[^>]*\?>\s*/m, "")
|
|
169
210
|
end
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -72,6 +72,8 @@ module MjmlRb
|
|
|
72
72
|
context[:before_doctype] = root_file_start_raw(document)
|
|
73
73
|
context[:lang] = options[:lang] || document.attributes["lang"] || "und"
|
|
74
74
|
context[:dir] = options[:dir] || document.attributes["dir"] || "auto"
|
|
75
|
+
context[:force_owa_desktop] = document.attributes["owa"] == "desktop"
|
|
76
|
+
context[:printer_support] = options[:printer_support] || options[:printerSupport]
|
|
75
77
|
context[:column_widths] = {}
|
|
76
78
|
append_component_head_styles(document, context)
|
|
77
79
|
content = render_node(body, context, parent: "mjml")
|
|
@@ -110,12 +112,18 @@ module MjmlRb
|
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
def build_html_document(content, context)
|
|
115
|
+
content = minify_outlook_conditionals(content)
|
|
113
116
|
title = context[:title].to_s
|
|
114
117
|
preview = context[:preview]
|
|
115
118
|
head_raw = Array(context[:head_raw]).join("\n")
|
|
116
119
|
before_doctype = context[:before_doctype].to_s
|
|
117
120
|
font_tags = build_font_tags(content, context[:inline_styles], context[:fonts])
|
|
118
|
-
media_queries_tags = build_media_queries_tags(
|
|
121
|
+
media_queries_tags = build_media_queries_tags(
|
|
122
|
+
context[:breakpoint],
|
|
123
|
+
context[:column_widths],
|
|
124
|
+
force_owa_desktop: context[:force_owa_desktop],
|
|
125
|
+
printer_support: context[:printer_support]
|
|
126
|
+
)
|
|
119
127
|
component_styles_tag = build_style_tag(unique_strings(context[:component_head_styles]))
|
|
120
128
|
user_styles_tag = build_style_tag(unique_strings(context[:user_styles]))
|
|
121
129
|
preview_block = preview.empty? ? "" : %(<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">#{escape_html(preview)}</div>)
|
|
@@ -140,7 +148,6 @@ module MjmlRb
|
|
|
140
148
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
141
149
|
<!--<![endif]-->
|
|
142
150
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
143
|
-
<meta charset="utf-8">
|
|
144
151
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
145
152
|
<style type="text/css">#{DOCUMENT_RESET_CSS}</style>
|
|
146
153
|
#{OUTLOOK_DOCUMENT_SETTINGS}
|
|
@@ -160,6 +167,7 @@ module MjmlRb
|
|
|
160
167
|
|
|
161
168
|
html = apply_html_attributes(html, context)
|
|
162
169
|
html = apply_inline_styles(html, context)
|
|
170
|
+
html = merge_outlook_conditionals(html)
|
|
163
171
|
before_doctype.empty? ? html : "#{before_doctype}\n#{html}"
|
|
164
172
|
end
|
|
165
173
|
|
|
@@ -238,7 +246,7 @@ module MjmlRb
|
|
|
238
246
|
end
|
|
239
247
|
end
|
|
240
248
|
|
|
241
|
-
def build_media_queries_tags(breakpoint, column_widths)
|
|
249
|
+
def build_media_queries_tags(breakpoint, column_widths, force_owa_desktop: false, printer_support: false)
|
|
242
250
|
widths = column_widths || {}
|
|
243
251
|
return "" if widths.empty?
|
|
244
252
|
|
|
@@ -263,7 +271,13 @@ module MjmlRb
|
|
|
263
271
|
parts << "<style media=\"screen and (min-width:#{bp})\">\n#{moz_rules.join("\n")}\n</style>"
|
|
264
272
|
end
|
|
265
273
|
|
|
266
|
-
|
|
274
|
+
if printer_support
|
|
275
|
+
parts << "<style type=\"text/css\">\n@media only print {\n#{base_rules.join("\n")}\n}\n</style>"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
if force_owa_desktop
|
|
279
|
+
parts << "<style type=\"text/css\">\n#{owa_rules.join("\n")}\n</style>"
|
|
280
|
+
end
|
|
267
281
|
parts.join("\n")
|
|
268
282
|
end
|
|
269
283
|
|
|
@@ -279,6 +293,20 @@ module MjmlRb
|
|
|
279
293
|
html.gsub(/<!\[endif\]-->\s*<!--\[if mso \| IE\]>/m, "")
|
|
280
294
|
end
|
|
281
295
|
|
|
296
|
+
def minify_outlook_conditionals(html)
|
|
297
|
+
html.gsub(/(<!--\[if\s[^\]]+\]>)([\s\S]*?)(<!\[endif\]-->)/m) do
|
|
298
|
+
prefix = Regexp.last_match(1)
|
|
299
|
+
content = Regexp.last_match(2)
|
|
300
|
+
suffix = Regexp.last_match(3)
|
|
301
|
+
|
|
302
|
+
processed = content
|
|
303
|
+
.gsub(/(^|>)(\s+)(<|$)/m, '\1\3')
|
|
304
|
+
.gsub(/\s{2,}/m, " ")
|
|
305
|
+
|
|
306
|
+
"#{prefix}#{processed}#{suffix}"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
282
310
|
def apply_html_attributes(html, context)
|
|
283
311
|
rules = context[:html_attributes] || {}
|
|
284
312
|
return html if rules.empty?
|
data/lib/mjml-rb/validator.rb
CHANGED
|
@@ -43,14 +43,26 @@ module MjmlRb
|
|
|
43
43
|
def walk(node, errors)
|
|
44
44
|
return unless node.element?
|
|
45
45
|
|
|
46
|
+
validate_known_tag(node, errors)
|
|
46
47
|
validate_allowed_children(node, errors)
|
|
47
48
|
validate_required_attributes(node, errors)
|
|
48
49
|
validate_supported_attributes(node, errors)
|
|
49
50
|
validate_attribute_types(node, errors)
|
|
50
51
|
|
|
52
|
+
return if Dependencies::ENDING_TAGS.include?(node.tag_name)
|
|
53
|
+
|
|
51
54
|
node.element_children.each { |child| walk(child, errors) }
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def validate_known_tag(node, errors)
|
|
58
|
+
return if known_tag?(node.tag_name)
|
|
59
|
+
|
|
60
|
+
errors << error(
|
|
61
|
+
"Element <#{node.tag_name}> doesn't exist or is not registered",
|
|
62
|
+
tag_name: node.tag_name, line: node.line, file: node.file
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
54
66
|
def validate_allowed_children(node, errors)
|
|
55
67
|
# Ending-tag components treat content as raw HTML; REXML still parses
|
|
56
68
|
# children structurally, so skip child validation for those tags.
|
|
@@ -130,6 +142,10 @@ module MjmlRb
|
|
|
130
142
|
end.find { |klass| klass.tags.include?(tag_name) }
|
|
131
143
|
end
|
|
132
144
|
|
|
145
|
+
def known_tag?(tag_name)
|
|
146
|
+
tag_name == "mjml" || !component_class_for_tag(tag_name).nil?
|
|
147
|
+
end
|
|
148
|
+
|
|
133
149
|
def valid_attribute_value?(value, expected_type)
|
|
134
150
|
return true if value.nil?
|
|
135
151
|
|
data/lib/mjml-rb/version.rb
CHANGED