mjml-rb 0.4.0 → 0.4.2
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 +4 -1
- data/README.md +2 -0
- data/Rakefile +9 -0
- data/lib/mjml-rb/cli.rb +16 -1
- data/lib/mjml-rb/compiler.rb +62 -10
- data/lib/mjml-rb/components/accordion.rb +9 -13
- data/lib/mjml-rb/components/attributes.rb +0 -4
- data/lib/mjml-rb/components/base.rb +4 -7
- data/lib/mjml-rb/components/body.rb +0 -4
- data/lib/mjml-rb/components/breakpoint.rb +0 -4
- data/lib/mjml-rb/components/button.rb +2 -21
- data/lib/mjml-rb/components/carousel.rb +0 -4
- data/lib/mjml-rb/components/carousel_image.rb +0 -4
- data/lib/mjml-rb/components/css_helpers.rb +33 -0
- data/lib/mjml-rb/components/divider.rb +2 -15
- data/lib/mjml-rb/components/group.rb +0 -4
- data/lib/mjml-rb/components/head.rb +0 -4
- data/lib/mjml-rb/components/hero.rb +0 -4
- data/lib/mjml-rb/components/image.rb +2 -15
- data/lib/mjml-rb/components/navbar.rb +0 -4
- data/lib/mjml-rb/components/raw.rb +0 -4
- data/lib/mjml-rb/components/section.rb +0 -10
- data/lib/mjml-rb/components/social.rb +6 -10
- data/lib/mjml-rb/components/spacer.rb +0 -4
- data/lib/mjml-rb/components/table.rb +2 -6
- data/lib/mjml-rb/components/text.rb +2 -6
- data/lib/mjml-rb/config_file.rb +10 -2
- data/lib/mjml-rb/parser.rb +10 -5
- data/lib/mjml-rb/validator.rb +19 -6
- data/lib/mjml-rb/version.rb +1 -1
- data/mjml-rb.gemspec +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0062fce070623c7a358e1af4811bb928a46d7f0cd4e1320a71e16276ccca1d74
|
|
4
|
+
data.tar.gz: a4e81e450419963570486009b46f44962d93c25b41cdbe4df721346db6e79fc8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 880bfd1a06dfe3fb9e8fe2169fe660554cb4e9ca96ee5d6eff1d2b41f6df748af14b753895d7b58a3106e33335516f6f4caadd2fd01717c597846b6104330fbf
|
|
7
|
+
data.tar.gz: 94f84fa6a66210d9177db704c16efa6fce09d983eb138bc5d13fdbf9bf967ac553b867907cfce9190a2bd7e14a6e6108c4ff74eb776d16221cbf77689f59ea85
|
data/Gemfile
CHANGED
data/README.md
CHANGED
data/Rakefile
ADDED
data/lib/mjml-rb/cli.rb
CHANGED
|
@@ -52,6 +52,10 @@ module MjmlRb
|
|
|
52
52
|
@stderr.puts("\nCommand line error:")
|
|
53
53
|
@stderr.puts(e.message)
|
|
54
54
|
1
|
|
55
|
+
rescue ConfigFile::ConfigError => e
|
|
56
|
+
@stderr.puts("\nConfiguration error:")
|
|
57
|
+
@stderr.puts(e.message)
|
|
58
|
+
1
|
|
55
59
|
rescue OptionParser::ParseError => e
|
|
56
60
|
@stderr.puts(e.message)
|
|
57
61
|
1
|
|
@@ -194,7 +198,10 @@ module MjmlRb
|
|
|
194
198
|
end
|
|
195
199
|
|
|
196
200
|
def emit_results(processed, output_mode, options)
|
|
197
|
-
processed.each
|
|
201
|
+
processed.each do |item|
|
|
202
|
+
emit_warnings(item[:compiled], item[:file])
|
|
203
|
+
emit_errors(item[:compiled], item[:file])
|
|
204
|
+
end
|
|
198
205
|
|
|
199
206
|
invalid = processed.any? { |item| !item[:compiled].errors.empty? }
|
|
200
207
|
if options[:validate].any? && invalid
|
|
@@ -213,6 +220,14 @@ module MjmlRb
|
|
|
213
220
|
invalid ? 2 : 0
|
|
214
221
|
end
|
|
215
222
|
|
|
223
|
+
def emit_warnings(result, file)
|
|
224
|
+
return if result.warnings.empty?
|
|
225
|
+
result.warnings.each do |warning|
|
|
226
|
+
prefix = file ? "File: #{file}\n" : ""
|
|
227
|
+
@stderr.puts("WARNING: #{prefix}#{warning[:formatted_message] || warning[:message]}")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
216
231
|
def emit_errors(result, file)
|
|
217
232
|
return if result.errors.empty?
|
|
218
233
|
result.errors.each do |error|
|
data/lib/mjml-rb/compiler.rb
CHANGED
|
@@ -17,8 +17,17 @@ module MjmlRb
|
|
|
17
17
|
actual_path: "."
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
+
# Additional option keys accepted by the renderer but not in DEFAULT_OPTIONS.
|
|
21
|
+
EXTRA_VALID_KEYS = %i[lang dir fonts printerSupport].freeze
|
|
22
|
+
|
|
23
|
+
VALID_OPTION_KEYS = (DEFAULT_OPTIONS.keys + EXTRA_VALID_KEYS).freeze
|
|
24
|
+
|
|
25
|
+
VALID_VALIDATION_LEVELS = %w[soft strict skip].freeze
|
|
26
|
+
|
|
20
27
|
def initialize(options = {})
|
|
21
|
-
|
|
28
|
+
normalized = symbolize_keys(options)
|
|
29
|
+
validate_options!(normalized)
|
|
30
|
+
@options = DEFAULT_OPTIONS.merge(normalized)
|
|
22
31
|
@parser = Parser.new
|
|
23
32
|
@validator = Validator.new(parser: @parser)
|
|
24
33
|
@renderer = Renderer.new
|
|
@@ -29,7 +38,16 @@ module MjmlRb
|
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def compile(mjml, options = {})
|
|
32
|
-
|
|
41
|
+
normalized = symbolize_keys(options)
|
|
42
|
+
validate_options!(normalized)
|
|
43
|
+
merged = @options.merge(normalized)
|
|
44
|
+
|
|
45
|
+
do_compile(mjml, merged)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def do_compile(mjml, merged)
|
|
33
51
|
ast = @parser.parse(
|
|
34
52
|
mjml,
|
|
35
53
|
keep_comments: merged[:keep_comments],
|
|
@@ -39,26 +57,43 @@ module MjmlRb
|
|
|
39
57
|
actual_path: merged[:actual_path]
|
|
40
58
|
)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
include_issues = format_include_errors(@parser.include_errors)
|
|
61
|
+
validation = validate_if_needed(ast, merged)
|
|
62
|
+
include_validation = classify_include_issues(include_issues, merged)
|
|
63
|
+
|
|
64
|
+
all_errors = validation[:errors] + include_validation[:errors]
|
|
65
|
+
all_warnings = validation[:warnings] + include_validation[:warnings]
|
|
66
|
+
|
|
67
|
+
return Result.new(errors: all_errors, warnings: all_warnings) if strict_validation_failed?(merged, all_errors)
|
|
44
68
|
|
|
45
69
|
html = @renderer.render(ast, merged)
|
|
46
|
-
|
|
70
|
+
Result.new(
|
|
47
71
|
html: post_process(html, merged),
|
|
48
|
-
errors:
|
|
49
|
-
warnings:
|
|
72
|
+
errors: all_errors,
|
|
73
|
+
warnings: all_warnings
|
|
50
74
|
)
|
|
51
|
-
result
|
|
52
75
|
rescue Parser::ParseError => e
|
|
53
76
|
Result.new(errors: [format_error(e.message, line: e.line)])
|
|
54
77
|
rescue StandardError => e
|
|
55
78
|
Result.new(errors: [format_error(e.message)])
|
|
56
79
|
end
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
def validate_options!(options)
|
|
82
|
+
unknown = options.keys - VALID_OPTION_KEYS
|
|
83
|
+
unless unknown.empty?
|
|
84
|
+
raise ArgumentError, "Unknown option(s): #{unknown.join(', ')}. Valid options are: #{VALID_OPTION_KEYS.join(', ')}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if options.key?(:validation_level)
|
|
88
|
+
level = options[:validation_level].to_s
|
|
89
|
+
unless VALID_VALIDATION_LEVELS.include?(level)
|
|
90
|
+
raise ArgumentError, "Invalid validation_level: #{level.inspect}. Must be one of: #{VALID_VALIDATION_LEVELS.join(', ')}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
59
94
|
|
|
60
95
|
def validate_if_needed(ast, options)
|
|
61
|
-
return [] if options[:validation_level].to_s == "skip"
|
|
96
|
+
return { errors: [], warnings: [] } if options[:validation_level].to_s == "skip"
|
|
62
97
|
@validator.validate(ast, options)
|
|
63
98
|
end
|
|
64
99
|
|
|
@@ -98,6 +133,23 @@ module MjmlRb
|
|
|
98
133
|
end
|
|
99
134
|
end
|
|
100
135
|
|
|
136
|
+
def format_include_errors(include_errors)
|
|
137
|
+
Array(include_errors).map do |ie|
|
|
138
|
+
format_error(ie[:message], line: ie[:line])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def classify_include_issues(issues, options)
|
|
143
|
+
return { errors: [], warnings: [] } if issues.empty?
|
|
144
|
+
|
|
145
|
+
level = options[:validation_level].to_s
|
|
146
|
+
if level == "strict"
|
|
147
|
+
{ errors: issues, warnings: [] }
|
|
148
|
+
else
|
|
149
|
+
{ errors: [], warnings: issues }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
101
153
|
def format_error(message, line: nil)
|
|
102
154
|
{
|
|
103
155
|
line: line,
|
|
@@ -104,7 +104,7 @@ module MjmlRb
|
|
|
104
104
|
.moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display:none; }
|
|
105
105
|
CSS
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
DEFAULT_ATTRIBUTES = {
|
|
108
108
|
"border" => "2px solid black",
|
|
109
109
|
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
110
110
|
"icon-align" => "middle",
|
|
@@ -118,21 +118,17 @@ module MjmlRb
|
|
|
118
118
|
"padding" => "10px 25px"
|
|
119
119
|
}.freeze
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
TITLE_DEFAULT_ATTRIBUTES = {
|
|
122
122
|
"font-size" => "13px",
|
|
123
123
|
"padding" => "16px"
|
|
124
124
|
}.freeze
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
TEXT_DEFAULT_ATTRIBUTES = {
|
|
127
127
|
"font-size" => "13px",
|
|
128
128
|
"line-height" => "1",
|
|
129
129
|
"padding" => "16px"
|
|
130
130
|
}.freeze
|
|
131
131
|
|
|
132
|
-
def tags
|
|
133
|
-
TAGS
|
|
134
|
-
end
|
|
135
|
-
|
|
136
132
|
def head_style
|
|
137
133
|
HEAD_STYLE
|
|
138
134
|
end
|
|
@@ -146,11 +142,11 @@ module MjmlRb
|
|
|
146
142
|
when "mj-accordion"
|
|
147
143
|
render_accordion(node, context, attrs)
|
|
148
144
|
when "mj-accordion-element"
|
|
149
|
-
render_accordion_element(node, context,
|
|
145
|
+
render_accordion_element(node, context, DEFAULT_ATTRIBUTES.merge(attrs))
|
|
150
146
|
when "mj-accordion-title"
|
|
151
|
-
render_accordion_title(node,
|
|
147
|
+
render_accordion_title(node, DEFAULT_ATTRIBUTES.merge(attrs))
|
|
152
148
|
when "mj-accordion-text"
|
|
153
|
-
render_accordion_text(node,
|
|
149
|
+
render_accordion_text(node, DEFAULT_ATTRIBUTES.merge(attrs))
|
|
154
150
|
else
|
|
155
151
|
render_children(node, context, parent: parent)
|
|
156
152
|
end
|
|
@@ -159,7 +155,7 @@ module MjmlRb
|
|
|
159
155
|
private
|
|
160
156
|
|
|
161
157
|
def render_accordion(node, context, attrs)
|
|
162
|
-
accordion_attrs =
|
|
158
|
+
accordion_attrs = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
163
159
|
outer_style = style_join(
|
|
164
160
|
"padding" => accordion_attrs["padding"],
|
|
165
161
|
"background-color" => accordion_attrs["container-background-color"]
|
|
@@ -225,7 +221,7 @@ module MjmlRb
|
|
|
225
221
|
end
|
|
226
222
|
|
|
227
223
|
def render_accordion_title(node, attrs)
|
|
228
|
-
title_attrs =
|
|
224
|
+
title_attrs = TITLE_DEFAULT_ATTRIBUTES.merge(attrs)
|
|
229
225
|
td_style = style_join(
|
|
230
226
|
"width" => "100%",
|
|
231
227
|
"background-color" => title_attrs["background-color"],
|
|
@@ -262,7 +258,7 @@ module MjmlRb
|
|
|
262
258
|
end
|
|
263
259
|
|
|
264
260
|
def render_accordion_text(node, attrs)
|
|
265
|
-
text_attrs =
|
|
261
|
+
text_attrs = TEXT_DEFAULT_ATTRIBUTES.merge(attrs)
|
|
266
262
|
td_style = style_join(
|
|
267
263
|
"background" => text_attrs["background-color"],
|
|
268
264
|
"font-size" => text_attrs["font-size"],
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
require_relative "css_helpers"
|
|
2
|
+
|
|
1
3
|
module MjmlRb
|
|
2
4
|
module Components
|
|
3
5
|
class Base
|
|
6
|
+
include CssHelpers
|
|
4
7
|
class << self
|
|
5
8
|
def tags
|
|
6
9
|
const_defined?(:TAGS) ? const_get(:TAGS) : []
|
|
@@ -11,13 +14,7 @@ module MjmlRb
|
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def default_attributes
|
|
14
|
-
|
|
15
|
-
const_get(:DEFAULT_ATTRIBUTES)
|
|
16
|
-
elsif const_defined?(:DEFAULTS)
|
|
17
|
-
const_get(:DEFAULTS)
|
|
18
|
-
else
|
|
19
|
-
{}
|
|
20
|
-
end
|
|
17
|
+
const_defined?(:DEFAULT_ATTRIBUTES) ? const_get(:DEFAULT_ATTRIBUTES) : {}
|
|
21
18
|
end
|
|
22
19
|
end
|
|
23
20
|
|
|
@@ -41,7 +41,7 @@ module MjmlRb
|
|
|
41
41
|
"width" => "unit(px,%)"
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
DEFAULT_ATTRIBUTES = {
|
|
45
45
|
"align" => "center",
|
|
46
46
|
"background-color" => "#414141",
|
|
47
47
|
"border" => "none",
|
|
@@ -59,12 +59,8 @@ module MjmlRb
|
|
|
59
59
|
"vertical-align" => "middle"
|
|
60
60
|
}.freeze
|
|
61
61
|
|
|
62
|
-
def tags
|
|
63
|
-
TAGS
|
|
64
|
-
end
|
|
65
|
-
|
|
66
62
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
67
|
-
a =
|
|
63
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
68
64
|
|
|
69
65
|
bg_color = a["background-color"]
|
|
70
66
|
inner_padding = a["inner-padding"]
|
|
@@ -173,21 +169,6 @@ module MjmlRb
|
|
|
173
169
|
"#{result.to_i}px"
|
|
174
170
|
end
|
|
175
171
|
|
|
176
|
-
def shorthand_value(parts, side)
|
|
177
|
-
case parts.length
|
|
178
|
-
when 1 then parts[0]
|
|
179
|
-
when 2, 3 then parts[1]
|
|
180
|
-
when 4 then side == :left ? parts[3] : parts[1]
|
|
181
|
-
else "0"
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def parse_border_width(border_str)
|
|
186
|
-
return 0 if border_str.nil? || border_str.strip == "none"
|
|
187
|
-
|
|
188
|
-
m = border_str.match(/(\d+(?:\.\d+)?)\s*px/)
|
|
189
|
-
m ? m[1].to_f : 0
|
|
190
|
-
end
|
|
191
172
|
end
|
|
192
173
|
end
|
|
193
174
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module MjmlRb
|
|
2
|
+
module Components
|
|
3
|
+
# Shared CSS utility methods used by multiple components.
|
|
4
|
+
# Extracted from Button, Image, Divider, and Section to eliminate
|
|
5
|
+
# duplicated logic.
|
|
6
|
+
module CssHelpers
|
|
7
|
+
# Extracts a value from a CSS shorthand property (padding, margin).
|
|
8
|
+
# Follows CSS shorthand rules:
|
|
9
|
+
# 1 value → all sides
|
|
10
|
+
# 2 values → vertical | horizontal
|
|
11
|
+
# 3 values → top | horizontal | bottom
|
|
12
|
+
# 4 values → top | right | bottom | left
|
|
13
|
+
def shorthand_value(parts, side)
|
|
14
|
+
case parts.length
|
|
15
|
+
when 1 then parts[0]
|
|
16
|
+
when 2, 3 then parts[1]
|
|
17
|
+
when 4 then side == :left ? parts[3] : parts[1]
|
|
18
|
+
else "0"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Extracts the numeric border width (in px) from a CSS border shorthand
|
|
23
|
+
# string like "2px solid #000". Returns a Float for sub-pixel values,
|
|
24
|
+
# or 0 when the border is nil, empty, "none", or has no px unit.
|
|
25
|
+
def parse_border_width(border_str)
|
|
26
|
+
return 0 if border_str.nil? || border_str.to_s.strip.empty? || border_str.to_s.strip == "none"
|
|
27
|
+
|
|
28
|
+
match = border_str.to_s.match(/(\d+(?:\.\d+)?)\s*px/)
|
|
29
|
+
match ? match[1].to_f : 0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -19,7 +19,7 @@ module MjmlRb
|
|
|
19
19
|
"align" => "enum(left,center,right)"
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
DEFAULT_ATTRIBUTES = {
|
|
23
23
|
"align" => "center",
|
|
24
24
|
"border-color" => "#000000",
|
|
25
25
|
"border-style" => "solid",
|
|
@@ -28,12 +28,8 @@ module MjmlRb
|
|
|
28
28
|
"width" => "100%"
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
-
def tags
|
|
32
|
-
TAGS
|
|
33
|
-
end
|
|
34
|
-
|
|
35
31
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
36
|
-
a =
|
|
32
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
37
33
|
|
|
38
34
|
outer_td_style = style_join(
|
|
39
35
|
"background" => a["container-background-color"],
|
|
@@ -112,15 +108,6 @@ module MjmlRb
|
|
|
112
108
|
end
|
|
113
109
|
end
|
|
114
110
|
|
|
115
|
-
def shorthand_value(parts, side)
|
|
116
|
-
case parts.length
|
|
117
|
-
when 1 then parts[0]
|
|
118
|
-
when 2, 3 then parts[1]
|
|
119
|
-
when 4 then side == :left ? parts[3] : parts[1]
|
|
120
|
-
else "0"
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
111
|
def outlook_block(align, style, width)
|
|
125
112
|
# Strip trailing px for the HTML width attribute
|
|
126
113
|
width_int = width.to_i.to_s
|
|
@@ -43,10 +43,6 @@ module MjmlRb
|
|
|
43
43
|
"vertical-align" => "top"
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
|
-
def tags
|
|
47
|
-
TAGS
|
|
48
|
-
end
|
|
49
|
-
|
|
50
46
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
51
47
|
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
52
48
|
container_width = normalize_container_width(context[:container_width] || "600px")
|
|
@@ -37,7 +37,7 @@ module MjmlRb
|
|
|
37
37
|
"usemap" => "string"
|
|
38
38
|
}.freeze
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
DEFAULT_ATTRIBUTES = {
|
|
41
41
|
"alt" => "",
|
|
42
42
|
"align" => "center",
|
|
43
43
|
"border" => "0",
|
|
@@ -47,10 +47,6 @@ module MjmlRb
|
|
|
47
47
|
"font-size" => "13px"
|
|
48
48
|
}.freeze
|
|
49
49
|
|
|
50
|
-
def tags
|
|
51
|
-
TAGS
|
|
52
|
-
end
|
|
53
|
-
|
|
54
50
|
def head_style(breakpoint)
|
|
55
51
|
lower_breakpoint = make_lower_breakpoint(breakpoint)
|
|
56
52
|
|
|
@@ -67,7 +63,7 @@ module MjmlRb
|
|
|
67
63
|
end
|
|
68
64
|
|
|
69
65
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
70
|
-
a =
|
|
66
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
71
67
|
|
|
72
68
|
fluid = a["fluid-on-mobile"] == "true"
|
|
73
69
|
full_width = a["full-width"] == "full-width"
|
|
@@ -202,15 +198,6 @@ module MjmlRb
|
|
|
202
198
|
end
|
|
203
199
|
end
|
|
204
200
|
|
|
205
|
-
def shorthand_value(parts, side)
|
|
206
|
-
case parts.length
|
|
207
|
-
when 1 then parts[0]
|
|
208
|
-
when 2, 3 then parts[1]
|
|
209
|
-
when 4 then side == :left ? parts[3] : parts[1]
|
|
210
|
-
else "0"
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
201
|
def make_lower_breakpoint(breakpoint)
|
|
215
202
|
matched = breakpoint.to_s.match(/[0-9]+/)
|
|
216
203
|
return breakpoint if matched.nil?
|
|
@@ -54,10 +54,6 @@ module MjmlRb
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def tags
|
|
58
|
-
TAGS
|
|
59
|
-
end
|
|
60
|
-
|
|
61
57
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
62
58
|
case tag_name
|
|
63
59
|
when "mj-wrapper"
|
|
@@ -80,12 +76,6 @@ module MjmlRb
|
|
|
80
76
|
value.to_s.to_i
|
|
81
77
|
end
|
|
82
78
|
|
|
83
|
-
def parse_border_width(border_str)
|
|
84
|
-
return 0 if border_str.nil? || border_str.to_s.strip.empty? || border_str.to_s.strip == "none"
|
|
85
|
-
|
|
86
|
-
border_str =~ /(\d+(?:\.\d+)?)\s*px/ ? $1.to_i : 0
|
|
87
|
-
end
|
|
88
|
-
|
|
89
79
|
def parse_padding_value(str)
|
|
90
80
|
return 0 if str.nil? || str.to_s.strip.empty?
|
|
91
81
|
|
|
@@ -107,7 +107,7 @@ module MjmlRb
|
|
|
107
107
|
icon-size icon-height icon-padding text-padding line-height text-decoration
|
|
108
108
|
].freeze
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
SOCIAL_DEFAULT_ATTRIBUTES = {
|
|
111
111
|
"align" => "center",
|
|
112
112
|
"border-radius" => "3px",
|
|
113
113
|
"color" => "#333333",
|
|
@@ -120,7 +120,7 @@ module MjmlRb
|
|
|
120
120
|
"text-decoration" => "none"
|
|
121
121
|
}.freeze
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
ELEMENT_DEFAULT_ATTRIBUTES = {
|
|
124
124
|
"alt" => "",
|
|
125
125
|
"align" => "left",
|
|
126
126
|
"icon-position" => "left",
|
|
@@ -136,17 +136,13 @@ module MjmlRb
|
|
|
136
136
|
"vertical-align" => "middle"
|
|
137
137
|
}.freeze
|
|
138
138
|
|
|
139
|
-
def tags
|
|
140
|
-
TAGS
|
|
141
|
-
end
|
|
142
|
-
|
|
143
139
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
144
140
|
case tag_name
|
|
145
141
|
when "mj-social"
|
|
146
142
|
render_social(node, context, attrs)
|
|
147
143
|
when "mj-social-element"
|
|
148
144
|
# Direct dispatch (no parent attrs merging) — fallback for standalone use
|
|
149
|
-
render_social_element(node,
|
|
145
|
+
render_social_element(node, ELEMENT_DEFAULT_ATTRIBUTES.merge(attrs))
|
|
150
146
|
end
|
|
151
147
|
end
|
|
152
148
|
|
|
@@ -155,7 +151,7 @@ module MjmlRb
|
|
|
155
151
|
# ── mj-social ──────────────────────────────────────────────────────────
|
|
156
152
|
|
|
157
153
|
def render_social(node, context, attrs)
|
|
158
|
-
a =
|
|
154
|
+
a = SOCIAL_DEFAULT_ATTRIBUTES.merge(attrs)
|
|
159
155
|
|
|
160
156
|
outer_td_style = style_join(
|
|
161
157
|
"background" => a["container-background-color"],
|
|
@@ -211,7 +207,7 @@ module MjmlRb
|
|
|
211
207
|
children_html = with_inherited_mj_class(context, node) do
|
|
212
208
|
elements.map.with_index do |child, idx|
|
|
213
209
|
child_attrs = resolved_attributes(child, context)
|
|
214
|
-
merged_attrs =
|
|
210
|
+
merged_attrs = ELEMENT_DEFAULT_ATTRIBUTES.merge(inherited).merge(child_attrs)
|
|
215
211
|
el_html = render_social_element(child, merged_attrs)
|
|
216
212
|
|
|
217
213
|
outlook_td_open = idx == 0 ? "<td>" : "</td><td>"
|
|
@@ -229,7 +225,7 @@ module MjmlRb
|
|
|
229
225
|
children_html = with_inherited_mj_class(context, node) do
|
|
230
226
|
elements.map do |child|
|
|
231
227
|
child_attrs = resolved_attributes(child, context)
|
|
232
|
-
merged_attrs =
|
|
228
|
+
merged_attrs = ELEMENT_DEFAULT_ATTRIBUTES.merge(inherited).merge(child_attrs)
|
|
233
229
|
render_social_element(child, merged_attrs)
|
|
234
230
|
end.join
|
|
235
231
|
end
|
|
@@ -27,7 +27,7 @@ module MjmlRb
|
|
|
27
27
|
"width" => "unit(px,%,auto)"
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
DEFAULT_ATTRIBUTES = {
|
|
31
31
|
"align" => "left",
|
|
32
32
|
"border" => "none",
|
|
33
33
|
"cellpadding" => "0",
|
|
@@ -41,12 +41,8 @@ module MjmlRb
|
|
|
41
41
|
"width" => "100%"
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
|
-
def tags
|
|
45
|
-
TAGS
|
|
46
|
-
end
|
|
47
|
-
|
|
48
44
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
49
|
-
a =
|
|
45
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
50
46
|
|
|
51
47
|
outer_td_style = style_join(
|
|
52
48
|
"background" => a["container-background-color"],
|
|
@@ -27,7 +27,7 @@ module MjmlRb
|
|
|
27
27
|
"vertical-align" => "enum(top,bottom,middle)"
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
DEFAULT_ATTRIBUTES = {
|
|
31
31
|
"align" => "left",
|
|
32
32
|
"color" => "#000000",
|
|
33
33
|
"font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
|
|
@@ -36,12 +36,8 @@ module MjmlRb
|
|
|
36
36
|
"padding" => "10px 25px"
|
|
37
37
|
}.freeze
|
|
38
38
|
|
|
39
|
-
def tags
|
|
40
|
-
TAGS
|
|
41
|
-
end
|
|
42
|
-
|
|
43
39
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
44
|
-
a =
|
|
40
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
45
41
|
height = a["height"]
|
|
46
42
|
|
|
47
43
|
outer_td_style = style_join(
|
data/lib/mjml-rb/config_file.rb
CHANGED
|
@@ -4,6 +4,15 @@ module MjmlRb
|
|
|
4
4
|
class ConfigFile
|
|
5
5
|
DEFAULT_NAME = ".mjmlrc"
|
|
6
6
|
|
|
7
|
+
class ConfigError < StandardError
|
|
8
|
+
attr_reader :path
|
|
9
|
+
|
|
10
|
+
def initialize(message, path:)
|
|
11
|
+
@path = path
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
7
16
|
def self.load(dir = Dir.pwd)
|
|
8
17
|
path = File.join(dir, DEFAULT_NAME)
|
|
9
18
|
return {} unless File.exist?(path)
|
|
@@ -27,8 +36,7 @@ module MjmlRb
|
|
|
27
36
|
|
|
28
37
|
config
|
|
29
38
|
rescue JSON::ParserError => e
|
|
30
|
-
|
|
31
|
-
{}
|
|
39
|
+
raise ConfigError.new("Failed to parse #{path}: #{e.message}", path: path)
|
|
32
40
|
end
|
|
33
41
|
end
|
|
34
42
|
end
|
data/lib/mjml-rb/parser.rb
CHANGED
|
@@ -35,7 +35,12 @@ module MjmlRb
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Errors collected during include expansion (missing files, etc.)
|
|
39
|
+
# Read after calling #parse to retrieve non-fatal include issues.
|
|
40
|
+
attr_reader :include_errors
|
|
41
|
+
|
|
38
42
|
def parse(mjml, options = {})
|
|
43
|
+
@include_errors = []
|
|
39
44
|
opts = normalize_options(options)
|
|
40
45
|
xml = apply_preprocessors(mjml.to_s, opts[:preprocessors])
|
|
41
46
|
xml = wrap_ending_tags_in_cdata(xml)
|
|
@@ -93,12 +98,12 @@ module MjmlRb
|
|
|
93
98
|
include_content = resolved_path ? File.read(resolved_path) : nil
|
|
94
99
|
|
|
95
100
|
if include_content.nil?
|
|
96
|
-
# Collect error as an mj-raw comment node instead of raising
|
|
97
101
|
display_path = resolved_path || File.expand_path(path_attr, options[:file_path].to_s)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
@include_errors << {
|
|
103
|
+
message: "mj-include fails to read file : #{path_attr} at #{display_path}",
|
|
104
|
+
tag_name: "mj-include",
|
|
105
|
+
file: display_path
|
|
106
|
+
}
|
|
102
107
|
parent.delete(include_node)
|
|
103
108
|
next
|
|
104
109
|
end
|
data/lib/mjml-rb/validator.rb
CHANGED
|
@@ -17,18 +17,31 @@ module MjmlRb
|
|
|
17
17
|
|
|
18
18
|
def validate(mjml_or_ast, options = {})
|
|
19
19
|
root = mjml_or_ast.is_a?(AstNode) ? mjml_or_ast : parse_ast(mjml_or_ast, options)
|
|
20
|
-
|
|
20
|
+
unless root&.tag_name == "mjml"
|
|
21
|
+
return { errors: [error("Root element must be <mjml>", tag_name: root&.tag_name)], warnings: [] }
|
|
22
|
+
end
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
walk(root,
|
|
25
|
-
|
|
24
|
+
issues = []
|
|
25
|
+
issues << error("Missing <mj-body>", tag_name: "mjml") unless root.element_children.any? { |c| c.tag_name == "mj-body" }
|
|
26
|
+
walk(root, issues)
|
|
27
|
+
classify_issues(issues, options)
|
|
26
28
|
rescue Parser::ParseError => e
|
|
27
|
-
[error(e.message, line: e.line)]
|
|
29
|
+
{ errors: [error(e.message, line: e.line)], warnings: [] }
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
# In "soft" mode validation issues are non-blocking warnings;
|
|
35
|
+
# in "strict" mode they are blocking errors.
|
|
36
|
+
def classify_issues(issues, options)
|
|
37
|
+
level = options.fetch(:validation_level, "soft").to_s
|
|
38
|
+
if level == "strict"
|
|
39
|
+
{ errors: issues, warnings: [] }
|
|
40
|
+
else
|
|
41
|
+
{ errors: [], warnings: issues }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
32
45
|
def parse_ast(mjml, options)
|
|
33
46
|
@parser.parse(
|
|
34
47
|
mjml,
|
data/lib/mjml-rb/version.rb
CHANGED
data/mjml-rb.gemspec
CHANGED
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
|
|
14
14
|
spec.homepage = "https://github.com/faraquet/mjml-rb"
|
|
15
15
|
spec.files = Dir.chdir(__dir__) do
|
|
16
|
-
Dir.glob("{bin,lib}/**/*") + ["Gemfile", "LICENSE", "mjml-rb.gemspec", "README.md"]
|
|
16
|
+
Dir.glob("{bin,lib}/**/*") + ["Gemfile", "LICENSE", "Rakefile", "mjml-rb.gemspec", "README.md"]
|
|
17
17
|
end
|
|
18
18
|
spec.bindir = "bin"
|
|
19
19
|
spec.executables = ["mjml"]
|
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.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Andriichuk
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- Gemfile
|
|
49
49
|
- LICENSE
|
|
50
50
|
- README.md
|
|
51
|
+
- Rakefile
|
|
51
52
|
- bin/mjml
|
|
52
53
|
- lib/mjml-rb.rb
|
|
53
54
|
- lib/mjml-rb/ast_node.rb
|
|
@@ -63,6 +64,7 @@ files:
|
|
|
63
64
|
- lib/mjml-rb/components/carousel.rb
|
|
64
65
|
- lib/mjml-rb/components/carousel_image.rb
|
|
65
66
|
- lib/mjml-rb/components/column.rb
|
|
67
|
+
- lib/mjml-rb/components/css_helpers.rb
|
|
66
68
|
- lib/mjml-rb/components/divider.rb
|
|
67
69
|
- lib/mjml-rb/components/group.rb
|
|
68
70
|
- lib/mjml-rb/components/head.rb
|