mjml-rb 0.2.2 → 0.2.3

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: 8065e65f3045ffc642525cb247d79591a4c1c2cd3d57541ec768dc2d0fec6ef9
4
- data.tar.gz: 304dc0c8474a87438378344264bcc4e56e27052ea9e09d6003352a3c94f3dd4e
3
+ metadata.gz: 4c0d1fcf5bc3d3da63f1e883fbf9fc1c5d596f23e2d1845b52fac3a2795c15ec
4
+ data.tar.gz: 96945245aea91b2d4cb94336a2d29a58eb6c5cbf332ac76e2828f581f4e31a4f
5
5
  SHA512:
6
- metadata.gz: a9bc2dd17fe59a21d311aca96332ba32a5c09222641bf566518e9aa856f5a9f45121d72ed6b53f74217cf26b61a3378d2f800abe482a772585e0353eb8b5627b
7
- data.tar.gz: 90991a406d6ecc5ed614d0a12a6bba3094bb0c72535407fcf709e5e5d9bceab802d137e85e319ac47aa57ad3c5ab4bf58fddbd8524996468cdeb18ebb521d30e
6
+ metadata.gz: 2ab219c36a7dfb49bc89ce1f7b6da2dc8d5089e60bfb640b2f2606783d8f27ee5560133274935d0f6aee96ac076cdfac346c4d85775452a2933f2a47ff9d87e0
7
+ data.tar.gz: d3fea2c9c4cd07a967d62dc07ff16328c0b61cc7f20b108209f21c7d5f8eb061040eaf5085875c0bb09a81d56607e08e9efa3cfd83d147f296a5a8308687cca9
data/README.md CHANGED
@@ -60,9 +60,9 @@ The table below tracks current JS-to-Ruby migration status for MJML components i
60
60
  | `mj-navbar-link` | migrated | Implemented in `navbar.rb` as an ending-tag navbar child component. |
61
61
  | `mj-raw` | migrated | Implemented in `raw.rb`, including head insertion and top-level `position="file-start"` output before the doctype. |
62
62
  | `mj-head` | partial | Core tags such as `mj-title`, `mj-preview`, `mj-style`, `mj-font`, and `mj-attributes` are supported. |
63
- | `mj-attributes` | partial | `mj-all`, `mj-class`, and per-tag defaults are supported. |
63
+ | `mj-attributes` | migrated | Implemented in `attributes.rb`, including npm-style `mj-class` descendant defaults. |
64
64
  | `mj-all` | partial | Supported through `mj-attributes`. |
65
- | `mj-class` | partial | Supported through `mj-attributes`. |
65
+ | `mj-class` | migrated | Supported through `attributes.rb`, including nested per-tag descendant defaults. |
66
66
  | `mj-title` | partial | Supported through head context. |
67
67
  | `mj-preview` | partial | Supported through head context. |
68
68
  | `mj-style` | partial | Supported, including inline CSS application. |
@@ -94,16 +94,18 @@ module MjmlRb
94
94
  "border-bottom" => "none",
95
95
  "font-family" => accordion_attrs["font-family"]
96
96
  )
97
- inner = node.element_children.map do |child|
98
- case child.tag_name
99
- when "mj-accordion-element"
100
- render_accordion_element(child, context, accordion_attrs)
101
- when "mj-raw"
102
- raw_inner(child)
103
- else
104
- render_node(child, context, parent: "mj-accordion")
105
- end
106
- end.join
97
+ inner = with_inherited_mj_class(context, node) do
98
+ node.element_children.map do |child|
99
+ case child.tag_name
100
+ when "mj-accordion-element"
101
+ render_accordion_element(child, context, accordion_attrs)
102
+ when "mj-raw"
103
+ raw_inner(child)
104
+ else
105
+ render_node(child, context, parent: "mj-accordion")
106
+ end
107
+ end.join
108
+ end
107
109
 
108
110
  %(<tr><td style="#{outer_style}"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" class="mj-accordion" style="#{table_style}"><tbody>#{inner}</tbody></table></td></tr>)
109
111
  end
@@ -125,16 +127,18 @@ module MjmlRb
125
127
  content = []
126
128
  content << render_accordion_title(nil, attrs) unless has_title
127
129
 
128
- children.each do |child|
129
- case child.tag_name
130
- when "mj-accordion-title"
131
- child_attrs = attrs.merge(resolved_attributes(child, context))
132
- content << render_accordion_title(child, child_attrs)
133
- when "mj-accordion-text"
134
- child_attrs = attrs.merge(resolved_attributes(child, context))
135
- content << render_accordion_text(child, child_attrs)
136
- when "mj-raw"
137
- content << raw_inner(child)
130
+ with_inherited_mj_class(context, node) do
131
+ children.each do |child|
132
+ case child.tag_name
133
+ when "mj-accordion-title"
134
+ child_attrs = attrs.merge(resolved_attributes(child, context))
135
+ content << render_accordion_title(child, child_attrs)
136
+ when "mj-accordion-text"
137
+ child_attrs = attrs.merge(resolved_attributes(child, context))
138
+ content << render_accordion_text(child, child_attrs)
139
+ when "mj-raw"
140
+ content << raw_inner(child)
141
+ end
138
142
  end
139
143
  end
140
144
  content << render_accordion_text(nil, attrs) unless has_text
@@ -0,0 +1,53 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Attributes < Base
6
+ TAGS = %w[mj-attributes mj-all mj-class].freeze
7
+
8
+ class << self
9
+ def allowed_attributes_for(tag_name)
10
+ {}
11
+ end
12
+
13
+ def allowed_attributes
14
+ {}
15
+ end
16
+ end
17
+
18
+ def tags
19
+ TAGS
20
+ end
21
+
22
+ def render(tag_name:, node:, context:, attrs:, parent:)
23
+ ""
24
+ end
25
+
26
+ def handle_head(attributes_node, context)
27
+ attributes_node.element_children.each do |child|
28
+ case child.tag_name
29
+ when "mj-all"
30
+ context[:global_defaults].merge!(child.attributes)
31
+ when "mj-class"
32
+ name = child.attributes["name"]
33
+ next unless name
34
+
35
+ context[:classes][name] ||= {}
36
+ context[:classes][name].merge!(child.attributes.reject { |key, _| key == "name" })
37
+
38
+ defaults = child.element_children.each_with_object({}) do |class_child, memo|
39
+ memo[class_child.tag_name] = class_child.attributes
40
+ end
41
+ next if defaults.empty?
42
+
43
+ context[:classes_default][name] ||= {}
44
+ context[:classes_default][name].merge!(defaults)
45
+ else
46
+ context[:tag_defaults][child.tag_name] ||= {}
47
+ context[:tag_defaults][child.tag_name].merge!(child.attributes)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -45,6 +45,10 @@ module MjmlRb
45
45
  renderer.send(:resolved_attributes, node, context)
46
46
  end
47
47
 
48
+ def with_inherited_mj_class(context, node, &block)
49
+ renderer.send(:with_inherited_mj_class, context, node, &block)
50
+ end
51
+
48
52
  def raw_inner(node)
49
53
  renderer.send(:raw_inner, node)
50
54
  end
@@ -164,14 +164,16 @@ module MjmlRb
164
164
  end
165
165
 
166
166
  def render_navbar_children(node, context)
167
- node.element_children.map do |child|
168
- case child.tag_name
169
- when "mj-navbar-link", "mj-raw"
170
- render_node(child, context, parent: "mj-navbar")
171
- else
172
- ""
173
- end
174
- end.join
167
+ with_inherited_mj_class(context, node) do
168
+ node.element_children.map do |child|
169
+ case child.tag_name
170
+ when "mj-navbar-link", "mj-raw"
171
+ render_node(child, context, parent: "mj-navbar")
172
+ else
173
+ ""
174
+ end
175
+ end.join
176
+ end
175
177
  end
176
178
 
177
179
  def render_navbar_link(node, context, attrs, parent:)
@@ -224,22 +224,24 @@ module MjmlRb
224
224
  close_tr = %(<!--[if mso | IE]></tr><![endif]-->)
225
225
  close_table = %(<!--[if mso | IE]></table><![endif]-->)
226
226
 
227
- col_parts = columns.each_with_index.map do |col, i|
228
- col_attrs = resolved_attributes(col, context)
229
- v_align = col_attrs["vertical-align"] || "top"
230
- col_px = (box_width.to_f * widths[i] / 100.0).round
231
-
232
- td_open = %(<!--[if mso | IE]><td class="" style="vertical-align:#{v_align};width:#{col_px}px;" ><![endif]-->)
233
- td_close = %(<!--[if mso | IE]></td><![endif]-->)
234
-
235
- col_html = if col.tag_name == "mj-group"
236
- renderer.send(:render_group, col, context, widths[i])
237
- else
238
- context[:_column_width_pct] = widths[i]
239
- render_node(col, context, parent: "mj-section")
240
- end
241
-
242
- "#{td_open}\n#{col_html}\n#{td_close}"
227
+ col_parts = with_inherited_mj_class(context, node) do
228
+ columns.each_with_index.map do |col, i|
229
+ col_attrs = resolved_attributes(col, context)
230
+ v_align = col_attrs["vertical-align"] || "top"
231
+ col_px = (box_width.to_f * widths[i] / 100.0).round
232
+
233
+ td_open = %(<!--[if mso | IE]><td class="" style="vertical-align:#{v_align};width:#{col_px}px;" ><![endif]-->)
234
+ td_close = %(<!--[if mso | IE]></td><![endif]-->)
235
+
236
+ col_html = if col.tag_name == "mj-group"
237
+ renderer.send(:render_group, col, context, widths[i])
238
+ else
239
+ context[:_column_width_pct] = widths[i]
240
+ render_node(col, context, parent: "mj-section")
241
+ end
242
+
243
+ "#{td_open}\n#{col_html}\n#{td_close}"
244
+ end
243
245
  end
244
246
 
245
247
  ([open_table, open_tr] + col_parts + [close_tr, close_table]).join("\n")
@@ -317,11 +319,13 @@ module MjmlRb
317
319
  close_tr = %(<!--[if mso | IE]></tr><![endif]-->)
318
320
  close_table = %(<!--[if mso | IE]></table><![endif]-->)
319
321
 
320
- section_parts = children.map do |child|
321
- td_open = %(<!--[if mso | IE]><td class="" width="#{container_px}px" ><![endif]-->)
322
- td_close = %(<!--[if mso | IE]></td><![endif]-->)
323
- child_html = render_node(child, context, parent: "mj-wrapper")
324
- "#{td_open}\n#{child_html}\n#{td_close}"
322
+ section_parts = with_inherited_mj_class(context, node) do
323
+ children.map do |child|
324
+ td_open = %(<!--[if mso | IE]><td class="" width="#{container_px}px" ><![endif]-->)
325
+ td_close = %(<!--[if mso | IE]></td><![endif]-->)
326
+ child_html = render_node(child, context, parent: "mj-wrapper")
327
+ "#{td_open}\n#{child_html}\n#{td_close}"
328
+ end
325
329
  end
326
330
 
327
331
  ([open_table, open_tr] + section_parts + [close_tr, close_table]).join("\n")
@@ -139,14 +139,16 @@ module MjmlRb
139
139
  outlook_open = %(<!--[if mso | IE]><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr>)
140
140
  outlook_close = %(</tr></table><![endif]-->)
141
141
 
142
- children_html = elements.map.with_index do |child, idx|
143
- child_attrs = resolved_attributes(child, context)
144
- merged_attrs = ELEMENT_DEFAULTS.merge(inherited).merge(child_attrs)
145
- el_html = render_social_element(child, merged_attrs)
146
-
147
- outlook_td_open = idx == 0 ? "<td>" : "</td><td>"
148
- %(#{outlook_td_open}<![endif]--><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody>#{el_html}</tbody></table><!--[if mso | IE]>)
149
- end.join
142
+ children_html = with_inherited_mj_class(context, node) do
143
+ elements.map.with_index do |child, idx|
144
+ child_attrs = resolved_attributes(child, context)
145
+ merged_attrs = ELEMENT_DEFAULTS.merge(inherited).merge(child_attrs)
146
+ el_html = render_social_element(child, merged_attrs)
147
+
148
+ outlook_td_open = idx == 0 ? "<td>" : "</td><td>"
149
+ %(#{outlook_td_open}<![endif]--><table align="#{escape_attr(align)}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody>#{el_html}</tbody></table><!--[if mso | IE]>)
150
+ end.join
151
+ end
150
152
 
151
153
  %(#{outlook_open}#{children_html}</td>#{outlook_close})
152
154
  end
@@ -155,11 +157,13 @@ module MjmlRb
155
157
  inherited = inherited_attrs(social_attrs)
156
158
  elements = social_element_children(node)
157
159
 
158
- children_html = elements.map do |child|
159
- child_attrs = resolved_attributes(child, context)
160
- merged_attrs = ELEMENT_DEFAULTS.merge(inherited).merge(child_attrs)
161
- render_social_element(child, merged_attrs)
162
- end.join
160
+ children_html = with_inherited_mj_class(context, node) do
161
+ elements.map do |child|
162
+ child_attrs = resolved_attributes(child, context)
163
+ merged_attrs = ELEMENT_DEFAULTS.merge(inherited).merge(child_attrs)
164
+ render_social_element(child, merged_attrs)
165
+ end.join
166
+ end
163
167
 
164
168
  %(<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0px;"><tbody>#{children_html}</tbody></table>)
165
169
  end
@@ -20,6 +20,7 @@ module MjmlRb
20
20
  def parse(mjml, options = {})
21
21
  opts = normalize_options(options)
22
22
  xml = apply_preprocessors(mjml.to_s, opts[:preprocessors])
23
+ xml = wrap_raw_tags_in_cdata(xml)
23
24
  xml = normalize_html_void_tags(xml)
24
25
  xml = expand_includes(xml, opts) unless opts[:ignore_includes]
25
26
 
@@ -48,6 +49,7 @@ module MjmlRb
48
49
  end
49
50
 
50
51
  def expand_includes(xml, options)
52
+ xml = wrap_raw_tags_in_cdata(xml)
51
53
  xml = normalize_html_void_tags(xml)
52
54
  doc = Document.new(sanitize_bare_ampersands(xml))
53
55
  includes = XPath.match(doc, "//mj-include")
@@ -64,7 +66,7 @@ module MjmlRb
64
66
  replacement = if include_type == "html"
65
67
  %(<mj-raw><![CDATA[#{escape_cdata(include_content)}]]></mj-raw>)
66
68
  else
67
- normalize_html_void_tags(strip_xml_declaration(include_content))
69
+ wrap_raw_tags_in_cdata(normalize_html_void_tags(strip_xml_declaration(include_content)))
68
70
  end
69
71
 
70
72
  fragment = Document.new(sanitize_bare_ampersands("<include-root>#{replacement}</include-root>"))
@@ -101,6 +103,18 @@ module MjmlRb
101
103
  end
102
104
  end
103
105
 
106
+ def wrap_raw_tags_in_cdata(content)
107
+ content.gsub(/<mj-raw(\s[^<>]*?)?>(.*?)<\/mj-raw>/mi) do
108
+ attrs = Regexp.last_match(1).to_s
109
+ inner = Regexp.last_match(2).to_s
110
+ if inner.include?("<![CDATA[")
111
+ "<mj-raw#{attrs}>#{inner}</mj-raw>"
112
+ else
113
+ "<mj-raw#{attrs}><![CDATA[#{escape_cdata(inner)}]]></mj-raw>"
114
+ end
115
+ end
116
+ end
117
+
104
118
  def escape_cdata(content)
105
119
  content.to_s.gsub("]]>", "]]]]><![CDATA[>")
106
120
  end
@@ -1,6 +1,7 @@
1
1
  require "cgi"
2
2
  require "nokogiri"
3
3
  require_relative "components/accordion"
4
+ require_relative "components/attributes"
4
5
  require_relative "components/body"
5
6
  require_relative "components/breakpoint"
6
7
  require_relative "components/button"
@@ -62,7 +63,9 @@ module MjmlRb
62
63
  fonts: DEFAULT_FONTS.merge(hash_or_empty(options[:fonts])),
63
64
  global_defaults: {},
64
65
  tag_defaults: {},
65
- classes: {}
66
+ classes: {},
67
+ classes_default: {},
68
+ inherited_mj_class: ""
66
69
  }
67
70
 
68
71
  return context unless head
@@ -84,7 +87,11 @@ module MjmlRb
84
87
  width = node.attributes["width"].to_s.strip
85
88
  context[:breakpoint] = width unless width.empty?
86
89
  when "mj-attributes"
87
- absorb_attribute_node(node, context)
90
+ if (component = component_for(node.tag_name)) && component.respond_to?(:handle_head)
91
+ component.handle_head(node, context)
92
+ else
93
+ absorb_attribute_node(node, context)
94
+ end
88
95
  when "mj-html-attributes"
89
96
  absorb_html_attributes_node(node, context)
90
97
  when "mj-raw"
@@ -173,7 +180,9 @@ module MjmlRb
173
180
  end
174
181
 
175
182
  def render_children(node, context, parent:)
176
- node.children.map { |child| render_node(child, context, parent: parent) }.join("\n")
183
+ with_inherited_mj_class(context, node) do
184
+ node.children.map { |child| render_node(child, context, parent: parent) }.join("\n")
185
+ end
177
186
  end
178
187
 
179
188
  def render_node(node, context, parent:)
@@ -196,10 +205,12 @@ module MjmlRb
196
205
  def render_group(node, context, width_pct = 100)
197
206
  items = node.element_children.select { |e| e.tag_name == "mj-column" }
198
207
  widths = compute_column_widths(items, context)
199
- items.each_with_index.map do |item, i|
200
- context[:_column_width_pct] = widths[i]
201
- render_node(item, context, parent: "mj-group")
202
- end.join("\n")
208
+ with_inherited_mj_class(context, node) do
209
+ items.each_with_index.map do |item, i|
210
+ context[:_column_width_pct] = widths[i]
211
+ render_node(item, context, parent: "mj-group")
212
+ end.join("\n")
213
+ end
203
214
  end
204
215
 
205
216
  def compute_column_widths(columns, context)
@@ -391,6 +402,7 @@ module MjmlRb
391
402
  registry = {}
392
403
  # Register component classes here as they are implemented.
393
404
  register_component(registry, Components::Body.new(self))
405
+ register_component(registry, Components::Attributes.new(self))
394
406
  register_component(registry, Components::Breakpoint.new(self))
395
407
  register_component(registry, Components::Accordion.new(self))
396
408
  register_component(registry, Components::Button.new(self))
@@ -419,8 +431,18 @@ module MjmlRb
419
431
  attrs.merge!(context[:tag_defaults][node.tag_name] || {})
420
432
 
421
433
  node_classes = node.attributes["mj-class"].to_s.split(/\s+/).reject(&:empty?)
422
- node_classes.each do |klass|
423
- attrs.merge!(context[:classes][klass] || {})
434
+ class_attrs = node_classes.each_with_object({}) do |klass, memo|
435
+ mj_class_attrs = (context[:classes] || {})[klass] || {}
436
+ if memo["css-class"] && mj_class_attrs["css-class"]
437
+ memo["css-class"] = "#{memo["css-class"]} #{mj_class_attrs["css-class"]}"
438
+ end
439
+ memo.merge!(mj_class_attrs)
440
+ end
441
+ attrs.merge!(class_attrs)
442
+
443
+ inherited_classes = context[:inherited_mj_class].to_s.split(/\s+/).reject(&:empty?)
444
+ inherited_classes.each do |klass|
445
+ attrs.merge!(((context[:classes_default] || {})[klass] || {})[node.tag_name] || {})
424
446
  end
425
447
 
426
448
  attrs.merge!(node.attributes)
@@ -482,6 +504,15 @@ module MjmlRb
482
504
  node.element_children.find { |child| child.tag_name == tag_name }
483
505
  end
484
506
 
507
+ def with_inherited_mj_class(context, node)
508
+ previous = context[:inherited_mj_class]
509
+ current = node.attributes["mj-class"]
510
+ context[:inherited_mj_class] = (current && !current.empty?) ? current : previous
511
+ yield
512
+ ensure
513
+ context[:inherited_mj_class] = previous
514
+ end
515
+
485
516
  def root_file_start_raw(document)
486
517
  document.element_children.filter_map do |child|
487
518
  next unless child.tag_name == "mj-raw"
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.2".freeze
2
+ VERSION = "0.2.3".freeze
3
3
  end
data/mjml-rb.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = ">= 3.0"
13
13
 
14
- spec.homepage = "https://github.com/faraquet/mjml-rails"
14
+ spec.homepage = "https://github.com/faraquet/mjml-rb"
15
15
  spec.files = Dir.chdir(__dir__) do
16
16
  Dir.glob("{bin,lib}/**/*") + ["Gemfile", "LICENSE", "mjml-rb.gemspec", "README.md"]
17
17
  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.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk
@@ -54,6 +54,7 @@ files:
54
54
  - lib/mjml-rb/cli.rb
55
55
  - lib/mjml-rb/compiler.rb
56
56
  - lib/mjml-rb/components/accordion.rb
57
+ - lib/mjml-rb/components/attributes.rb
57
58
  - lib/mjml-rb/components/base.rb
58
59
  - lib/mjml-rb/components/body.rb
59
60
  - lib/mjml-rb/components/breakpoint.rb
@@ -78,7 +79,7 @@ files:
78
79
  - lib/mjml-rb/validator.rb
79
80
  - lib/mjml-rb/version.rb
80
81
  - mjml-rb.gemspec
81
- homepage: https://github.com/faraquet/mjml-rails
82
+ homepage: https://github.com/faraquet/mjml-rb
82
83
  licenses:
83
84
  - MIT
84
85
  metadata: {}