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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9c0aec079dca1d68d66f5530f699398aa09703219b750945dde850c519f20de
4
- data.tar.gz: ef9c027b625aa1e5f6000ec8f574472f8f38590e7013812be64d7b469f138e94
3
+ metadata.gz: 2075cd423603c4070dae0acdbecf052f474931e96c079d6369eabbefb505306c
4
+ data.tar.gz: 1d5fd4664d222e546cbf61c51f1e8fbe6e76880d9e1e0eb858cce266cec58989
5
5
  SHA512:
6
- metadata.gz: c41b2a470fad579dc02b752577625b8008d04f00ae24404242eb846f24b02fadd2bd8154474fbe1336d707819253b705326a59d97af7047d4b99d8af8d2b7b73
7
- data.tar.gz: f2ee9e775a83778c2ee2712505956bc2515198f5bc886aea0c4137844d24b2cd3782b7a38a4c42826c34c86d214f4e8105dcdfc5a8fcdcae85d9d7cf42ce8a7d
6
+ metadata.gz: 3e82ec6fe3d8af7361b3fe5691eed18449724f3b4fc5c2318077ccfe5f406b20bed0f8aaa724a059d2298a53d8a4a04a470ee2484ab259f007d5a67fa4da8e08
7
+ data.tar.gz: a09686d9b4cffab082e5c3bb0a7b3049c2702e857eaec5234aa3185c1cb8b47949e772711470ea29b9d0884932c7b5a9bc62016bcb76f8a96fd348d2688eee18
@@ -10,6 +10,7 @@ module MjmlRb
10
10
  minify: false,
11
11
  keep_comments: true,
12
12
  ignore_includes: false,
13
+ printer_support: false,
13
14
  preprocessors: [],
14
15
  validation_level: "soft",
15
16
  file_path: ".",
@@ -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 = wrap_ending_tags_in_cdata(normalize_html_void_tags(strip_xml_declaration(include_content)))
113
- # Recursively expand includes in the included content
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
- fragment.root.children.each do |child|
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, deep_clone(child))
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 inject_css_includes(doc, css_includes)
142
- mjml_root = doc.root
143
- return unless mjml_root
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
- # Find or create mj-head
165
+ body = XPath.first(mjml_root, "mj-body")
146
166
  head = XPath.first(mjml_root, "mj-head")
147
- unless head
148
- head = Element.new("mj-head")
149
- # Insert mj-head before mj-body if possible
150
- body = XPath.first(mjml_root, "mj-body")
151
- if body
152
- mjml_root.insert_before(body, head)
153
- else
154
- mjml_root.add(head)
155
- end
156
- end
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
@@ -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(context[:breakpoint], context[:column_widths])
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
- parts << "<style type=\"text/css\">\n#{owa_rules.join("\n")}\n</style>"
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?
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.35".freeze
2
+ VERSION = "0.2.36".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mjml-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.35
4
+ version: 0.2.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk