mjml-rb 0.2.33 → 0.2.35
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/Gemfile +2 -0
- data/README.md +1 -1
- data/lib/mjml-rb/ast_node.rb +3 -2
- data/lib/mjml-rb/parser.rb +47 -5
- data/lib/mjml-rb/renderer.rb +10 -3
- data/lib/mjml-rb/validator.rb +14 -6
- 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: e9c0aec079dca1d68d66f5530f699398aa09703219b750945dde850c519f20de
|
|
4
|
+
data.tar.gz: ef9c027b625aa1e5f6000ec8f574472f8f38590e7013812be64d7b469f138e94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c41b2a470fad579dc02b752577625b8008d04f00ae24404242eb846f24b02fadd2bd8154474fbe1336d707819253b705326a59d97af7047d4b99d8af8d2b7b73
|
|
7
|
+
data.tar.gz: f2ee9e775a83778c2ee2712505956bc2515198f5bc886aea0c4137844d24b2cd3782b7a38a4c42826c34c86d214f4e8105dcdfc5a8fcdcae85d9d7cf42ce8a7d
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -46,4 +46,4 @@ bundle exec bin/mjml --migrate old.mjml -s
|
|
|
46
46
|
> MJML templates the same way you render ERB — no extra runtime, no
|
|
47
47
|
> `package.json`, no `node_modules`.
|
|
48
48
|
|
|
49
|
-
Remaining parity work is tracked in [
|
|
49
|
+
Remaining parity work is tracked in [npm ↔ Ruby Parity Audit](/docs/PARITY_AUDIT.md).
|
data/lib/mjml-rb/ast_node.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
module MjmlRb
|
|
2
2
|
class AstNode
|
|
3
|
-
attr_reader :tag_name, :attributes, :children, :content, :line
|
|
3
|
+
attr_reader :tag_name, :attributes, :children, :content, :line, :file
|
|
4
4
|
|
|
5
|
-
def initialize(tag_name:, attributes: {}, children: [], content: nil, line: nil)
|
|
5
|
+
def initialize(tag_name:, attributes: {}, children: [], content: nil, line: nil, file: nil)
|
|
6
6
|
@tag_name = tag_name.to_s
|
|
7
7
|
@attributes = attributes.transform_keys(&:to_s)
|
|
8
8
|
@children = Array(children)
|
|
9
9
|
@content = content
|
|
10
10
|
@line = line
|
|
11
|
+
@file = file
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def text?
|
data/lib/mjml-rb/parser.rb
CHANGED
|
@@ -33,7 +33,8 @@ module MjmlRb
|
|
|
33
33
|
xml = normalize_html_void_tags(xml)
|
|
34
34
|
xml = expand_includes(xml, opts) unless opts[:ignore_includes]
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
xml = annotate_line_numbers(sanitize_bare_ampersands(xml))
|
|
37
|
+
doc = Document.new(xml)
|
|
37
38
|
element_to_ast(doc.root, keep_comments: opts[:keep_comments])
|
|
38
39
|
rescue ParseException => e
|
|
39
40
|
raise ParseError.new("XML parse error: #{e.message}")
|
|
@@ -119,6 +120,7 @@ module MjmlRb
|
|
|
119
120
|
fragment = Document.new(sanitize_bare_ampersands("<include-root>#{replacement}</include-root>"))
|
|
120
121
|
insert_before = include_node
|
|
121
122
|
fragment.root.children.each do |child|
|
|
123
|
+
annotate_include_source(child, resolved_path) if child.is_a?(Element)
|
|
122
124
|
parent.insert_before(insert_before, deep_clone(child))
|
|
123
125
|
end
|
|
124
126
|
parent.delete(include_node)
|
|
@@ -211,6 +213,36 @@ module MjmlRb
|
|
|
211
213
|
content.gsub(/&(?!(?:#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);)/, "&")
|
|
212
214
|
end
|
|
213
215
|
|
|
216
|
+
# Adds data-mjml-line attributes to MJML tags so line numbers survive
|
|
217
|
+
# REXML parsing (which doesn't expose source positions).
|
|
218
|
+
# Skips content inside CDATA sections to avoid modifying raw HTML.
|
|
219
|
+
def annotate_line_numbers(xml)
|
|
220
|
+
line = 1
|
|
221
|
+
xml.gsub(/(\n)|(<!\[CDATA\[.*?\]\]>)|(<(?:mj-[\w-]+|mjml)(?=[\s\/>]))/m) do
|
|
222
|
+
if Regexp.last_match(1) # newline
|
|
223
|
+
line += 1
|
|
224
|
+
"\n"
|
|
225
|
+
elsif Regexp.last_match(2) # CDATA section — count newlines, pass through
|
|
226
|
+
line += Regexp.last_match(2).count("\n")
|
|
227
|
+
Regexp.last_match(2)
|
|
228
|
+
else # opening MJML tag
|
|
229
|
+
"#{Regexp.last_match(3)} data-mjml-line=\"#{line}\""
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Recursively marks REXML elements from included files with data-mjml-file.
|
|
235
|
+
# Only sets the attribute on elements that don't already have it (preserving
|
|
236
|
+
# deeper include annotations from recursive expansion).
|
|
237
|
+
def annotate_include_source(element, file_path)
|
|
238
|
+
return unless element.is_a?(Element)
|
|
239
|
+
|
|
240
|
+
if (element.name.start_with?("mj-") || element.name == "mjml") && !element.attributes["data-mjml-file"]
|
|
241
|
+
element.add_attribute("data-mjml-file", file_path)
|
|
242
|
+
end
|
|
243
|
+
element.each_element { |child| annotate_include_source(child, file_path) }
|
|
244
|
+
end
|
|
245
|
+
|
|
214
246
|
def resolve_include_path(include_path, actual_path, file_path)
|
|
215
247
|
include_path = include_path.to_s
|
|
216
248
|
return include_path if File.absolute_path(include_path) == include_path && File.file?(include_path)
|
|
@@ -245,16 +277,25 @@ module MjmlRb
|
|
|
245
277
|
def element_to_ast(element, keep_comments:)
|
|
246
278
|
raise ParseError, "Missing XML root element" unless element
|
|
247
279
|
|
|
280
|
+
# Extract metadata annotations (added by annotate_line_numbers / annotate_include_source)
|
|
281
|
+
# and strip them from the public attributes hash.
|
|
282
|
+
meta_line = element.attributes["data-mjml-line"]&.to_i
|
|
283
|
+
meta_file = element.attributes["data-mjml-file"]
|
|
284
|
+
attrs = element.attributes.each_with_object({}) do |(name, val), h|
|
|
285
|
+
h[name] = val unless name.start_with?("data-mjml-")
|
|
286
|
+
end
|
|
287
|
+
|
|
248
288
|
# For ending-tag elements whose content was wrapped in CDATA, store
|
|
249
289
|
# the raw HTML directly as content instead of parsing structurally.
|
|
250
290
|
if ENDING_TAGS_FOR_CDATA.include?(element.name)
|
|
251
291
|
raw_content = element.children.select { |c| c.is_a?(Text) }.map(&:value).join
|
|
252
292
|
return AstNode.new(
|
|
253
293
|
tag_name: element.name,
|
|
254
|
-
attributes:
|
|
294
|
+
attributes: attrs,
|
|
255
295
|
children: [],
|
|
256
296
|
content: raw_content.empty? ? nil : raw_content,
|
|
257
|
-
line:
|
|
297
|
+
line: meta_line,
|
|
298
|
+
file: meta_file
|
|
258
299
|
)
|
|
259
300
|
end
|
|
260
301
|
|
|
@@ -272,9 +313,10 @@ module MjmlRb
|
|
|
272
313
|
|
|
273
314
|
AstNode.new(
|
|
274
315
|
tag_name: element.name,
|
|
275
|
-
attributes:
|
|
316
|
+
attributes: attrs,
|
|
276
317
|
children: children,
|
|
277
|
-
line:
|
|
318
|
+
line: meta_line,
|
|
319
|
+
file: meta_file
|
|
278
320
|
)
|
|
279
321
|
end
|
|
280
322
|
end
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -27,7 +27,11 @@ module MjmlRb
|
|
|
27
27
|
HTML_VOID_TAGS = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
|
28
28
|
|
|
29
29
|
DEFAULT_FONTS = {
|
|
30
|
-
"
|
|
30
|
+
"Open Sans" => "https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700",
|
|
31
|
+
"Droid Sans" => "https://fonts.googleapis.com/css?family=Droid+Sans:300,400,500,700",
|
|
32
|
+
"Lato" => "https://fonts.googleapis.com/css?family=Lato:300,400,500,700",
|
|
33
|
+
"Roboto" => "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700",
|
|
34
|
+
"Ubuntu" => "https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700"
|
|
31
35
|
}.freeze
|
|
32
36
|
|
|
33
37
|
DOCUMENT_RESET_CSS = <<~CSS.freeze
|
|
@@ -114,7 +118,7 @@ module MjmlRb
|
|
|
114
118
|
media_queries_tags = build_media_queries_tags(context[:breakpoint], context[:column_widths])
|
|
115
119
|
component_styles_tag = build_style_tag(unique_strings(context[:component_head_styles]))
|
|
116
120
|
user_styles_tag = build_style_tag(unique_strings(context[:user_styles]))
|
|
117
|
-
preview_block = preview.empty? ? "" : %(<div style="display:none;max-height:
|
|
121
|
+
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>)
|
|
118
122
|
html_attributes = {
|
|
119
123
|
"lang" => context[:lang],
|
|
120
124
|
"dir" => context[:dir],
|
|
@@ -122,7 +126,10 @@ module MjmlRb
|
|
|
122
126
|
"xmlns:v" => "urn:schemas-microsoft-com:vml",
|
|
123
127
|
"xmlns:o" => "urn:schemas-microsoft-com:office:office"
|
|
124
128
|
}
|
|
125
|
-
body_style = style_join(
|
|
129
|
+
body_style = style_join(
|
|
130
|
+
"word-spacing" => "normal",
|
|
131
|
+
"background-color" => context[:background_color]
|
|
132
|
+
)
|
|
126
133
|
|
|
127
134
|
html = <<~HTML
|
|
128
135
|
<!doctype html>
|
data/lib/mjml-rb/validator.rb
CHANGED
|
@@ -64,7 +64,7 @@ module MjmlRb
|
|
|
64
64
|
|
|
65
65
|
errors << error(
|
|
66
66
|
"Element <#{child.tag_name}> is not allowed inside <#{node.tag_name}>",
|
|
67
|
-
tag_name: child.tag_name
|
|
67
|
+
tag_name: child.tag_name, line: child.line, file: child.file
|
|
68
68
|
)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -74,7 +74,8 @@ module MjmlRb
|
|
|
74
74
|
required.each do |attr|
|
|
75
75
|
next if node.attributes.key?(attr)
|
|
76
76
|
|
|
77
|
-
errors << error("Attribute `#{attr}` is required for <#{node.tag_name}>",
|
|
77
|
+
errors << error("Attribute `#{attr}` is required for <#{node.tag_name}>",
|
|
78
|
+
tag_name: node.tag_name, line: node.line, file: node.file)
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
|
|
@@ -86,7 +87,8 @@ module MjmlRb
|
|
|
86
87
|
next if allowed_attributes.key?(attribute_name)
|
|
87
88
|
next if GLOBAL_ALLOWED_ATTRIBUTES.include?(attribute_name)
|
|
88
89
|
|
|
89
|
-
errors << error("Attribute `#{attribute_name}` is not allowed for <#{node.tag_name}>",
|
|
90
|
+
errors << error("Attribute `#{attribute_name}` is not allowed for <#{node.tag_name}>",
|
|
91
|
+
tag_name: node.tag_name, line: node.line, file: node.file)
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
@@ -103,7 +105,7 @@ module MjmlRb
|
|
|
103
105
|
|
|
104
106
|
errors << error(
|
|
105
107
|
"Attribute `#{attribute_name}` on <#{node.tag_name}> has invalid value `#{attribute_value}` for type `#{expected_type}`",
|
|
106
|
-
tag_name: node.tag_name
|
|
108
|
+
tag_name: node.tag_name, line: node.line, file: node.file
|
|
107
109
|
)
|
|
108
110
|
end
|
|
109
111
|
end
|
|
@@ -183,12 +185,18 @@ module MjmlRb
|
|
|
183
185
|
end
|
|
184
186
|
end
|
|
185
187
|
|
|
186
|
-
def error(message, line: nil, tag_name: nil)
|
|
188
|
+
def error(message, line: nil, tag_name: nil, file: nil)
|
|
189
|
+
location = [
|
|
190
|
+
("line #{line}" if line),
|
|
191
|
+
("in #{file}" if file)
|
|
192
|
+
].compact.join(", ")
|
|
193
|
+
|
|
187
194
|
{
|
|
188
195
|
line: line,
|
|
196
|
+
file: file,
|
|
189
197
|
message: message,
|
|
190
198
|
tag_name: tag_name,
|
|
191
|
-
formatted_message: message
|
|
199
|
+
formatted_message: location.empty? ? message : "#{message} (#{location})"
|
|
192
200
|
}
|
|
193
201
|
end
|
|
194
202
|
end
|
data/lib/mjml-rb/version.rb
CHANGED