mjml-rb 0.2.39 → 0.3.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 +4 -4
- data/README.md +9 -6
- data/lib/mjml-rb/components/column.rb +18 -1
- data/lib/mjml-rb/components/group.rb +1 -0
- data/lib/mjml-rb/components/hero.rb +13 -2
- data/lib/mjml-rb/components/section.rb +14 -4
- data/lib/mjml-rb/railtie.rb +13 -11
- data/lib/mjml-rb/renderer.rb +16 -6
- data/lib/mjml-rb/template_handler.rb +46 -95
- data/lib/mjml-rb/version.rb +1 -1
- data/lib/mjml-rb.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: a5afc17046611848190458b50e1b7d0670011e8f1e32a6723b63c7e7ff76029c
|
|
4
|
+
data.tar.gz: ff832c4861cd1c28d76f47e0f66f257e8fa1c956dd6e0836826df83be3e4782d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e43bae28df235a80ad028bc43e37401c45c59c8e0d486f09e8d22faa786c1e18e62e696bde4cce668fcee4ea1c7d3d037cec3ec93d890b0e8e33404f98b776fe
|
|
7
|
+
data.tar.gz: c35f403ee71148fa10f6e9d794a61695f35f74201e561fba1afecb4686c71388c01f13bf69c19b799214102ad142a6f86281d922b3ed5eed2ed1e348b446f737
|
data/README.md
CHANGED
|
@@ -51,15 +51,18 @@ By default, `.mjml` files are treated as raw MJML/XML source.
|
|
|
51
51
|
If you want Slim-backed MJML templates, configure it explicitly:
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
|
-
config.mjml_rb.
|
|
54
|
+
config.mjml_rb.rails_template_language = :slim
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Supported values are `:
|
|
57
|
+
Supported values are `:slim` and `:haml`.
|
|
58
58
|
|
|
59
|
-
With a configured `
|
|
60
|
-
that template engine first, so partials and embedded Ruby can assemble
|
|
61
|
-
before the outer template is compiled to HTML. Without that setting,
|
|
62
|
-
MJML source is rejected instead of being guessed.
|
|
59
|
+
With a configured `rails_template_language`, `.mjml` templates are rendered
|
|
60
|
+
through that template engine first, so partials and embedded Ruby can assemble
|
|
61
|
+
MJML before the outer template is compiled to HTML. Without that setting,
|
|
62
|
+
non-XML MJML source is rejected instead of being guessed.
|
|
63
|
+
|
|
64
|
+
For `:slim` or `:haml`, the matching Rails template handler must already be
|
|
65
|
+
registered in `ActionView` by the corresponding gem or integration layer.
|
|
63
66
|
|
|
64
67
|
Create a view such as `app/views/user_mailer/welcome.html.mjml`:
|
|
65
68
|
|
|
@@ -39,6 +39,7 @@ module MjmlRb
|
|
|
39
39
|
|
|
40
40
|
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
41
41
|
width_pct = context.delete(:_column_width_pct) || 100.0
|
|
42
|
+
mobile_width = context.delete(:_column_mobile_width)
|
|
42
43
|
css_class = attrs["css-class"]
|
|
43
44
|
a = self.class.default_attributes.merge(attrs)
|
|
44
45
|
|
|
@@ -63,7 +64,7 @@ module MjmlRb
|
|
|
63
64
|
"direction" => a["direction"],
|
|
64
65
|
"display" => "inline-block",
|
|
65
66
|
"vertical-align" => vertical_align,
|
|
66
|
-
"width" => "100%"
|
|
67
|
+
"width" => (mobile_width ? mobile_width_value(a, width_pct, context[:container_width]) : "100%")
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
column_markup =
|
|
@@ -168,6 +169,22 @@ module MjmlRb
|
|
|
168
169
|
value && !value.empty?
|
|
169
170
|
end
|
|
170
171
|
|
|
172
|
+
def mobile_width_value(attrs, width_pct, parent_width)
|
|
173
|
+
width = attrs["width"]
|
|
174
|
+
|
|
175
|
+
if present_attr?(width) && width.end_with?("%")
|
|
176
|
+
width
|
|
177
|
+
elsif present_attr?(width) && width.end_with?("px")
|
|
178
|
+
parent_width_px = parse_pixel_value(parent_width || "600px")
|
|
179
|
+
return "100%" if parent_width_px.zero?
|
|
180
|
+
|
|
181
|
+
percentage = (parse_pixel_value(width) / parent_width_px) * 100
|
|
182
|
+
"#{percentage.to_s.sub(/\.?0+$/, "")}%"
|
|
183
|
+
else
|
|
184
|
+
"#{width_pct.to_f.to_s.sub(/\.?0+$/, "")}%"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
171
188
|
def with_child_container_width(context, attrs, width_pct)
|
|
172
189
|
previous_container_width = context[:container_width]
|
|
173
190
|
context[:container_width] = child_container_width(context, attrs, width_pct)
|
|
@@ -77,6 +77,7 @@ module MjmlRb
|
|
|
77
77
|
width_pct = widths[column_index] || 100.0
|
|
78
78
|
column_index += 1
|
|
79
79
|
context[:_column_width_pct] = width_pct
|
|
80
|
+
context[:_column_mobile_width] = true
|
|
80
81
|
td_style = style_join(
|
|
81
82
|
"vertical-align" => resolved_attributes(child, context)["vertical-align"] || "top",
|
|
82
83
|
"width" => "#{(group_width_px * width_pct / 100.0).round}px"
|
|
@@ -238,11 +238,11 @@ module MjmlRb
|
|
|
238
238
|
def hero_container_width(attrs, container_width)
|
|
239
239
|
width = parse_unit_value(container_width)
|
|
240
240
|
content_width = width - padding_side(attrs, "left") - padding_side(attrs, "right")
|
|
241
|
-
|
|
241
|
+
px_length([content_width, 0].max)
|
|
242
242
|
end
|
|
243
243
|
|
|
244
244
|
def normalize_container_width(value)
|
|
245
|
-
|
|
245
|
+
px_length(parse_unit_value(value))
|
|
246
246
|
end
|
|
247
247
|
|
|
248
248
|
def padding_side(attrs, side)
|
|
@@ -280,6 +280,17 @@ module MjmlRb
|
|
|
280
280
|
def blank?(value)
|
|
281
281
|
value.nil? || value.to_s.empty?
|
|
282
282
|
end
|
|
283
|
+
|
|
284
|
+
def px_length(value)
|
|
285
|
+
"#{format_number(value)}px"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def format_number(value)
|
|
289
|
+
number = value.to_f
|
|
290
|
+
return number.to_i.to_s if number == number.to_i
|
|
291
|
+
|
|
292
|
+
number.to_s.sub(/\.?0+$/, "")
|
|
293
|
+
end
|
|
283
294
|
end
|
|
284
295
|
end
|
|
285
296
|
end
|
|
@@ -64,7 +64,7 @@ module MjmlRb
|
|
|
64
64
|
render_wrapper(node, context, attrs)
|
|
65
65
|
else
|
|
66
66
|
a = self.class.default_attributes.merge(attrs)
|
|
67
|
-
if a["full-width"] == "full-width"
|
|
67
|
+
if a["full-width"] == "full-width" && !context[:_inside_full_width_wrapper]
|
|
68
68
|
render_full_width_section(node, context, a)
|
|
69
69
|
else
|
|
70
70
|
render_section(node, context, a)
|
|
@@ -254,8 +254,8 @@ module MjmlRb
|
|
|
254
254
|
|
|
255
255
|
# Build v:fill attributes
|
|
256
256
|
fill_pairs = [
|
|
257
|
-
["origin", "#{v_origin_x}, #{v_origin_y}"],
|
|
258
|
-
["position", "#{v_pos_x}, #{v_pos_y}"],
|
|
257
|
+
["origin", "#{format_vml_number(v_origin_x)}, #{format_vml_number(v_origin_y)}"],
|
|
258
|
+
["position", "#{format_vml_number(v_pos_x)}, #{format_vml_number(v_pos_y)}"],
|
|
259
259
|
["src", bg_url],
|
|
260
260
|
["color", bg_color],
|
|
261
261
|
["type", vml_type]
|
|
@@ -287,6 +287,13 @@ module MjmlRb
|
|
|
287
287
|
end
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
def format_vml_number(value)
|
|
291
|
+
number = value.to_f
|
|
292
|
+
return number.to_i.to_s if number == number.to_i
|
|
293
|
+
|
|
294
|
+
number.to_s.sub(/\.?0+$/, "")
|
|
295
|
+
end
|
|
296
|
+
|
|
290
297
|
def vml_size_attributes(bg_size)
|
|
291
298
|
case bg_size
|
|
292
299
|
when "cover"
|
|
@@ -639,7 +646,9 @@ module MjmlRb
|
|
|
639
646
|
box_width = container_px - pad_left - pad_right - border_left - border_right
|
|
640
647
|
|
|
641
648
|
previous_container_width = context[:container_width]
|
|
649
|
+
previous_full_width_wrapper = context[:_inside_full_width_wrapper]
|
|
642
650
|
context[:container_width] = "#{box_width}px"
|
|
651
|
+
context[:_inside_full_width_wrapper] = full_width
|
|
643
652
|
|
|
644
653
|
div_attrs = {"class" => (full_width ? nil : css_class), "style" => div_style}
|
|
645
654
|
inner = merge_outlook_conditionals(render_wrapped_children_wrapper(node, context, container_px, a["gap"]))
|
|
@@ -686,6 +695,7 @@ module MjmlRb
|
|
|
686
695
|
end
|
|
687
696
|
ensure
|
|
688
697
|
context[:container_width] = previous_container_width if previous_container_width
|
|
698
|
+
context[:_inside_full_width_wrapper] = previous_full_width_wrapper
|
|
689
699
|
end
|
|
690
700
|
|
|
691
701
|
# Wrap each child mj-section/mj-wrapper in an Outlook conditional <tr><td>.
|
|
@@ -703,7 +713,7 @@ module MjmlRb
|
|
|
703
713
|
child_attrs = resolved_attributes(child, context)
|
|
704
714
|
child_css = child_attrs["css-class"]
|
|
705
715
|
outlook_class = suffix_css_classes(child_css)
|
|
706
|
-
td_open = %(<!--[if mso | IE]><tr><td class="#{escape_attr(outlook_class)}" width="#{container_px}
|
|
716
|
+
td_open = %(<!--[if mso | IE]><tr><td class="#{escape_attr(outlook_class)}" width="#{container_px}" ><![endif]-->)
|
|
707
717
|
td_close = %(<!--[if mso | IE]></td></tr><![endif]-->)
|
|
708
718
|
child_html = with_wrapper_child_gap(context, index.zero? ? nil : gap) do
|
|
709
719
|
render_node(child, context, parent: "mj-wrapper")
|
data/lib/mjml-rb/railtie.rb
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
if defined?(Rails::Railtie)
|
|
2
|
+
require_relative "template_handler"
|
|
2
3
|
|
|
3
|
-
module MjmlRb
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
module MjmlRb
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
config.mjml_rb = ActiveSupport::OrderedOptions.new
|
|
7
|
+
config.mjml_rb.compiler_options = {validation_level: "strict"}
|
|
8
|
+
config.mjml_rb.rails_template_language = nil
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
initializer "mjml_rb.action_view" do |app|
|
|
11
|
+
MjmlRb.rails_compiler_options = app.config.mjml_rb.compiler_options.to_h
|
|
12
|
+
MjmlRb.rails_template_language = app.config.mjml_rb.rails_template_language
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
ActiveSupport.on_load(:action_view) do
|
|
15
|
+
MjmlRb.register_action_view_template_handler!
|
|
16
|
+
end
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -113,6 +113,7 @@ module MjmlRb
|
|
|
113
113
|
|
|
114
114
|
def build_html_document(content, context)
|
|
115
115
|
content = minify_outlook_conditionals(content)
|
|
116
|
+
content = apply_html_attributes_to_content(content, context)
|
|
116
117
|
title = context[:title].to_s
|
|
117
118
|
preview = context[:preview]
|
|
118
119
|
head_raw = Array(context[:head_raw]).join("\n")
|
|
@@ -165,7 +166,6 @@ module MjmlRb
|
|
|
165
166
|
</html>
|
|
166
167
|
HTML
|
|
167
168
|
|
|
168
|
-
html = apply_html_attributes(html, context)
|
|
169
169
|
html = apply_inline_styles(html, context)
|
|
170
170
|
html = merge_outlook_conditionals(html)
|
|
171
171
|
before_doctype.empty? ? html : "#{before_doctype}\n#{html}"
|
|
@@ -307,23 +307,33 @@ module MjmlRb
|
|
|
307
307
|
end
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
-
def
|
|
310
|
+
def apply_html_attributes_to_content(content, context)
|
|
311
311
|
rules = context[:html_attributes] || {}
|
|
312
|
-
return
|
|
312
|
+
return content if rules.empty?
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
root = html_attributes_fragment_root(content, context)
|
|
315
315
|
|
|
316
316
|
rules.each do |selector, attrs|
|
|
317
317
|
next if selector.empty? || attrs.empty?
|
|
318
318
|
|
|
319
|
-
select_nodes(
|
|
319
|
+
select_nodes(root, selector).each do |node|
|
|
320
320
|
attrs.each do |name, value|
|
|
321
321
|
node[name] = value.to_s
|
|
322
322
|
end
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
root.inner_html
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def html_attributes_fragment_root(content, context)
|
|
330
|
+
wrapper_attrs = {
|
|
331
|
+
"data-mjml-body-root" => "true",
|
|
332
|
+
"lang" => context[:lang],
|
|
333
|
+
"dir" => context[:dir]
|
|
334
|
+
}
|
|
335
|
+
fragment = Nokogiri::HTML::DocumentFragment.parse("<div#{html_attrs(wrapper_attrs)}>#{content}</div>")
|
|
336
|
+
fragment.at_css("div[data-mjml-body-root='true']")
|
|
327
337
|
end
|
|
328
338
|
|
|
329
339
|
def apply_inline_styles(html, context)
|
|
@@ -1,114 +1,65 @@
|
|
|
1
|
+
require "action_view"
|
|
2
|
+
require "action_view/template"
|
|
3
|
+
|
|
1
4
|
module MjmlRb
|
|
2
5
|
class TemplateHandler
|
|
3
|
-
|
|
4
|
-
TEMPLATE_ENGINES = {
|
|
5
|
-
slim: {
|
|
6
|
-
require: "slim",
|
|
7
|
-
gem_name: "slim"
|
|
8
|
-
},
|
|
9
|
-
haml: {
|
|
10
|
-
require: "haml",
|
|
11
|
-
gem_name: "haml"
|
|
12
|
-
}
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
def call(template, source = nil)
|
|
17
|
-
<<~RUBY
|
|
18
|
-
::MjmlRb::TemplateHandler.render(self, #{template.source.inspect}, #{template.identifier.inspect}, local_assigns)
|
|
19
|
-
RUBY
|
|
20
|
-
end
|
|
6
|
+
SUPPORTED_TEMPLATE_LANGUAGES = %w[:slim :haml].freeze
|
|
21
7
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
end
|
|
8
|
+
def call(template, source = nil)
|
|
9
|
+
compiled_source = compile_source(template, source)
|
|
10
|
+
template_path = template.respond_to?(:virtual_path) ? template.virtual_path : template.identifier
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
if /<mjml.*?>/i.match?(compiled_source)
|
|
13
|
+
"::MjmlRb::TemplateHandler.render_compiled_source(begin;#{compiled_source};end, #{template_path.inspect})"
|
|
14
|
+
else
|
|
15
|
+
"::MjmlRb::TemplateHandler.render_partial_source(begin;#{compiled_source};end)"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
19
|
+
def self.render_compiled_source(mjml_source, template_path)
|
|
20
|
+
mjml_result = ::MjmlRb::Compiler.new(::MjmlRb.rails_compiler_options || {}).compile(mjml_source.to_s)
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
if mjml_result.errors.any?
|
|
23
|
+
raise "MJML compilation failed for #{template_path}: #{mjml_result.errors.map { |error| error[:formatted_message] || error[:message] }.join(', ')}"
|
|
37
24
|
end
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
language = ::MjmlRb.rails_template_language
|
|
43
|
-
if language.nil?
|
|
44
|
-
stripped_source = source.to_s.lstrip
|
|
45
|
-
return source.to_s if stripped_source.start_with?("<")
|
|
46
|
-
|
|
47
|
-
raise "MJML Rails template_language is not configured for non-XML templates. Supported values: nil, :erb, :slim, :haml"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
case language
|
|
51
|
-
when :erb
|
|
52
|
-
render_erb_source(view_context, source.to_s, local_assigns)
|
|
53
|
-
when :slim, :haml
|
|
54
|
-
render_template_language_source(view_context, source.to_s, local_assigns, language)
|
|
55
|
-
else
|
|
56
|
-
raise "Unsupported MJML Rails template_language `#{language}`. Supported values: nil, :erb, :slim, :haml"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
26
|
+
html = mjml_result.html.to_s
|
|
27
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
28
|
+
end
|
|
59
29
|
|
|
60
|
-
|
|
61
|
-
|
|
30
|
+
def self.render_partial_source(compiled_source)
|
|
31
|
+
output = compiled_source.to_s
|
|
32
|
+
output.respond_to?(:html_safe) ? output.html_safe : output
|
|
33
|
+
end
|
|
62
34
|
|
|
63
|
-
|
|
64
|
-
define_local_reader(view_context, name, value)
|
|
65
|
-
end
|
|
35
|
+
private
|
|
66
36
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
37
|
+
def template_handler
|
|
38
|
+
language = MjmlRb.rails_template_language
|
|
39
|
+
raise missing_rails_template_language_error if language.nil?
|
|
70
40
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
require engine[:require]
|
|
74
|
-
|
|
75
|
-
case language
|
|
76
|
-
when :slim
|
|
77
|
-
::Slim::Template.new { source }.render(view_context, local_assigns.transform_keys(&:to_sym))
|
|
78
|
-
when :haml
|
|
79
|
-
if defined?(::Haml::Template)
|
|
80
|
-
::Haml::Template.new { source }.render(view_context, local_assigns.transform_keys(&:to_sym))
|
|
81
|
-
else
|
|
82
|
-
raise "MJML Rails template_language is set to :haml, but this Haml version does not expose Haml::Template"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
rescue LoadError
|
|
86
|
-
raise "MJML Rails template_language is set to :#{language}, but the `#{engine[:gem_name]}` gem is not available"
|
|
87
|
-
end
|
|
41
|
+
handler = ActionView::Template.registered_template_handler(language)
|
|
42
|
+
return handler if handler
|
|
88
43
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
singleton_class.send(:define_method, name) do
|
|
92
|
-
value
|
|
93
|
-
end
|
|
94
|
-
end
|
|
44
|
+
raise "MJML Rails rails_template_language `#{language}` is not registered with ActionView. Make sure the matching Rails template handler is loaded."
|
|
45
|
+
end
|
|
95
46
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
47
|
+
def compile_source(template, source)
|
|
48
|
+
return template.source.inspect if xml_source?(template.source)
|
|
99
49
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
yield
|
|
104
|
-
ensure
|
|
105
|
-
next_depth = view_context.instance_variable_get(MJML_CAPTURE_DEPTH_IVAR).to_i - 1
|
|
106
|
-
view_context.instance_variable_set(MJML_CAPTURE_DEPTH_IVAR, [next_depth, 0].max)
|
|
107
|
-
end
|
|
50
|
+
template_handler.call(template, source)
|
|
51
|
+
rescue RuntimeError => error
|
|
52
|
+
raise error unless error.message == missing_rails_template_language_error
|
|
108
53
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
54
|
+
%(raise #{missing_rails_template_language_error.inspect})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def xml_source?(source)
|
|
58
|
+
source.to_s.lstrip.start_with?("<")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def missing_rails_template_language_error
|
|
62
|
+
"MJML Rails rails_template_language is not configured for non-XML templates. Configure it with one of: #{SUPPORTED_TEMPLATE_LANGUAGES.join(', ')}. Otherwise use raw XML MJML."
|
|
112
63
|
end
|
|
113
64
|
end
|
|
114
65
|
end
|
data/lib/mjml-rb/version.rb
CHANGED
data/lib/mjml-rb.rb
CHANGED
|
@@ -40,7 +40,7 @@ module MjmlRb
|
|
|
40
40
|
return unless defined?(ActionView::Template)
|
|
41
41
|
|
|
42
42
|
require_relative "mjml-rb/template_handler"
|
|
43
|
-
ActionView::Template.register_template_handler(:mjml, TemplateHandler)
|
|
43
|
+
ActionView::Template.register_template_handler(:mjml, TemplateHandler.new)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def mjml2html(mjml, options = {})
|