mjml-rb 0.2.3 → 0.2.5

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: 4c0d1fcf5bc3d3da63f1e883fbf9fc1c5d596f23e2d1845b52fac3a2795c15ec
4
- data.tar.gz: 96945245aea91b2d4cb94336a2d29a58eb6c5cbf332ac76e2828f581f4e31a4f
3
+ metadata.gz: 288347d54c01e4a6c60ed76c71ed988cb74f8a88eed9de7d7fb06cf740bd79e5
4
+ data.tar.gz: 369dc21e8dd16555318abfd6bc4cfa28649e4bbcce10a8a9b9b6f0613c87fb90
5
5
  SHA512:
6
- metadata.gz: 2ab219c36a7dfb49bc89ce1f7b6da2dc8d5089e60bfb640b2f2606783d8f27ee5560133274935d0f6aee96ac076cdfac346c4d85775452a2933f2a47ff9d87e0
7
- data.tar.gz: d3fea2c9c4cd07a967d62dc07ff16328c0b61cc7f20b108209f21c7d5f8eb061040eaf5085875c0bb09a81d56607e08e9efa3cfd83d147f296a5a8308687cca9
6
+ metadata.gz: 3bf2de79918958b9d4cd9b66191e6b2b52c5494d97d961a129d2d9db7193690a8616063c1168245ced32c736a24caca07aeda5945747d44a972568894b79fd01
7
+ data.tar.gz: e4e56ca10989e917fb5b4d14b4d090099b1003c97599d0dd57330a35c09981dee2424568e00a1b144050d5c564da86e5bf713cbf098065585a985b78a48a2039
data/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
  > and not all components or attributes are fully implemented yet.
9
9
  > **Do not use in production without thorough testing of every template against
10
10
  > the official npm renderer.** API and output format may change without notice.
11
+ > This is a **fully open source project**, and help is welcome:
12
+ > feedback, bug reports, test cases, optimizations, proposals, and pull requests.
11
13
  > No warranty of any kind is provided.
12
14
 
13
15
  This directory contains a Ruby-first implementation of the main MJML user-facing tooling:
@@ -59,14 +61,14 @@ The table below tracks current JS-to-Ruby migration status for MJML components i
59
61
  | `mj-navbar` | migrated | Implemented in `navbar.rb`, including `base-url` propagation and breakpoint-aware hamburger CSS. |
60
62
  | `mj-navbar-link` | migrated | Implemented in `navbar.rb` as an ending-tag navbar child component. |
61
63
  | `mj-raw` | migrated | Implemented in `raw.rb`, including head insertion and top-level `position="file-start"` output before the doctype. |
62
- | `mj-head` | partial | Core tags such as `mj-title`, `mj-preview`, `mj-style`, `mj-font`, and `mj-attributes` are supported. |
64
+ | `mj-head` | migrated | Implemented in `head.rb` and dispatches supported head children through component handlers. |
63
65
  | `mj-attributes` | migrated | Implemented in `attributes.rb`, including npm-style `mj-class` descendant defaults. |
64
66
  | `mj-all` | partial | Supported through `mj-attributes`. |
65
67
  | `mj-class` | migrated | Supported through `attributes.rb`, including nested per-tag descendant defaults. |
66
- | `mj-title` | partial | Supported through head context. |
67
- | `mj-preview` | partial | Supported through head context. |
68
- | `mj-style` | partial | Supported, including inline CSS application. |
69
- | `mj-font` | partial | Supported for font link injection. |
68
+ | `mj-title` | migrated | Implemented in `head.rb`. |
69
+ | `mj-preview` | migrated | Implemented in `head.rb`. |
70
+ | `mj-style` | migrated | Implemented in `head.rb`, including inline-style registration. |
71
+ | `mj-font` | migrated | Implemented in `head.rb`. |
70
72
  | `mj-carousel` | not migrated | Declared in dependency rules but no renderer implementation yet. |
71
73
  | `mj-carousel-image` | not migrated | Declared in dependency rules but no renderer implementation yet. |
72
74
  | `mj-breakpoint` | migrated | Supported in `mj-head` and used to control desktop column media-query widths. |
@@ -20,6 +20,11 @@ module MjmlRb
20
20
  def render(tag_name:, node:, context:, attrs:, parent:)
21
21
  ""
22
22
  end
23
+
24
+ def handle_head(node, context)
25
+ width = node.attributes["width"].to_s.strip
26
+ context[:breakpoint] = width unless width.empty?
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -0,0 +1,59 @@
1
+ require_relative "base"
2
+
3
+ module MjmlRb
4
+ module Components
5
+ class Head < Base
6
+ TAGS = %w[mj-head mj-title mj-preview mj-style mj-font].freeze
7
+
8
+ ALLOWED_ATTRIBUTES = {
9
+ "mj-style" => {
10
+ "inline" => "string"
11
+ },
12
+ "mj-font" => {
13
+ "name" => "string",
14
+ "href" => "string"
15
+ }
16
+ }.freeze
17
+
18
+ class << self
19
+ def allowed_attributes_for(tag_name)
20
+ ALLOWED_ATTRIBUTES[tag_name] || {}
21
+ end
22
+
23
+ def allowed_attributes
24
+ {}
25
+ end
26
+ end
27
+
28
+ def tags
29
+ TAGS
30
+ end
31
+
32
+ def render(tag_name:, node:, context:, attrs:, parent:)
33
+ ""
34
+ end
35
+
36
+ def handle_head(node, context)
37
+ case node.tag_name
38
+ when "mj-head"
39
+ node.element_children.each do |child|
40
+ component = renderer.send(:component_for, child.tag_name)
41
+ component.handle_head(child, context) if component&.respond_to?(:handle_head)
42
+ end
43
+ when "mj-title"
44
+ context[:title] = raw_inner(node).strip
45
+ when "mj-preview"
46
+ context[:preview] = raw_inner(node).strip
47
+ when "mj-style"
48
+ css = raw_inner(node)
49
+ context[:head_styles] << css
50
+ context[:inline_styles] << css if node.attributes["inline"] == "inline"
51
+ when "mj-font"
52
+ name = node.attributes["name"]
53
+ href = node.attributes["href"]
54
+ context[:fonts][name] = href if name && href
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -3,7 +3,7 @@ require_relative "base"
3
3
  module MjmlRb
4
4
  module Components
5
5
  class HtmlAttributes < Base
6
- TAGS = %w[mj-selector mj-html-attribute].freeze
6
+ TAGS = %w[mj-html-attributes mj-selector mj-html-attribute].freeze
7
7
 
8
8
  ALLOWED_ATTRIBUTES = {
9
9
  "mj-selector" => {
@@ -27,6 +27,28 @@ module MjmlRb
27
27
  def render(tag_name:, node:, context:, attrs:, parent:)
28
28
  render_children(node, context, parent: parent)
29
29
  end
30
+
31
+ def handle_head(node, context)
32
+ node.element_children.each do |selector|
33
+ next unless selector.tag_name == "mj-selector"
34
+
35
+ path = selector.attributes["path"].to_s.strip
36
+ next if path.empty?
37
+
38
+ custom_attrs = selector.element_children.each_with_object({}) do |child, memo|
39
+ next unless child.tag_name == "mj-html-attribute"
40
+
41
+ name = child.attributes["name"].to_s.strip
42
+ next if name.empty?
43
+
44
+ memo[name] = child.text_content
45
+ end
46
+ next if custom_attrs.empty?
47
+
48
+ context[:html_attributes][path] ||= {}
49
+ context[:html_attributes][path].merge!(custom_attrs)
50
+ end
51
+ end
30
52
  end
31
53
  end
32
54
  end
@@ -16,6 +16,10 @@ module MjmlRb
16
16
  def render(tag_name:, node:, context:, attrs:, parent:)
17
17
  raw_inner(node)
18
18
  end
19
+
20
+ def handle_head(node, context)
21
+ context[:head_raw] << raw_inner(node)
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -5,6 +5,7 @@ require_relative "components/attributes"
5
5
  require_relative "components/body"
6
6
  require_relative "components/breakpoint"
7
7
  require_relative "components/button"
8
+ require_relative "components/head"
8
9
  require_relative "components/hero"
9
10
  require_relative "components/image"
10
11
  require_relative "components/navbar"
@@ -71,76 +72,13 @@ module MjmlRb
71
72
  return context unless head
72
73
 
73
74
  head.element_children.each do |node|
74
- case node.tag_name
75
- when "mj-title"
76
- context[:title] = node.text_content.strip
77
- when "mj-preview"
78
- context[:preview] = node.text_content.strip
79
- when "mj-style"
80
- context[:head_styles] << node.text_content
81
- context[:inline_styles] << node.text_content if node.attributes["inline"] == "inline"
82
- when "mj-font"
83
- name = node.attributes["name"]
84
- href = node.attributes["href"]
85
- context[:fonts][name] = href if name && href
86
- when "mj-breakpoint"
87
- width = node.attributes["width"].to_s.strip
88
- context[:breakpoint] = width unless width.empty?
89
- when "mj-attributes"
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
95
- when "mj-html-attributes"
96
- absorb_html_attributes_node(node, context)
97
- when "mj-raw"
98
- context[:head_raw] << raw_inner(node)
99
- end
75
+ component = component_for(node.tag_name)
76
+ component.handle_head(node, context) if component&.respond_to?(:handle_head)
100
77
  end
101
78
 
102
79
  context
103
80
  end
104
81
 
105
- def absorb_attribute_node(attributes_node, context)
106
- attributes_node.element_children.each do |child|
107
- case child.tag_name
108
- when "mj-all"
109
- context[:global_defaults].merge!(child.attributes)
110
- when "mj-class"
111
- name = child.attributes["name"]
112
- next unless name
113
-
114
- context[:classes][name] = child.attributes.reject { |k, _| k == "name" }
115
- else
116
- context[:tag_defaults][child.tag_name] ||= {}
117
- context[:tag_defaults][child.tag_name].merge!(child.attributes)
118
- end
119
- end
120
- end
121
-
122
- def absorb_html_attributes_node(html_attributes_node, context)
123
- html_attributes_node.element_children.each do |selector|
124
- next unless selector.tag_name == "mj-selector"
125
-
126
- path = selector.attributes["path"].to_s.strip
127
- next if path.empty?
128
-
129
- custom_attrs = selector.element_children.each_with_object({}) do |child, memo|
130
- next unless child.tag_name == "mj-html-attribute"
131
-
132
- name = child.attributes["name"].to_s.strip
133
- next if name.empty?
134
-
135
- memo[name] = child.text_content
136
- end
137
- next if custom_attrs.empty?
138
-
139
- context[:html_attributes][path] ||= {}
140
- context[:html_attributes][path].merge!(custom_attrs)
141
- end
142
- end
143
-
144
82
  def build_html_document(content, context)
145
83
  title = context[:title].to_s
146
84
  preview = context[:preview]
@@ -271,14 +209,10 @@ module MjmlRb
271
209
  rules.each do |selector, attrs|
272
210
  next if selector.empty? || attrs.empty?
273
211
 
274
- begin
275
- document.css(selector).each do |node|
276
- attrs.each do |name, value|
277
- node[name] = value.to_s
278
- end
212
+ select_nodes(document, selector).each do |node|
213
+ attrs.each do |name, value|
214
+ node[name] = value.to_s
279
215
  end
280
- rescue Nokogiri::CSS::SyntaxError
281
- next
282
216
  end
283
217
  end
284
218
 
@@ -293,18 +227,53 @@ module MjmlRb
293
227
  parse_inline_css_rules(css_blocks.join("\n")).each do |selector, declarations|
294
228
  next if selector.empty? || declarations.empty?
295
229
 
296
- begin
297
- document.css(selector).each do |node|
298
- merge_inline_style!(node, declarations)
299
- end
300
- rescue Nokogiri::CSS::SyntaxError
301
- next
230
+ select_nodes(document, selector).each do |node|
231
+ merge_inline_style!(node, declarations)
302
232
  end
303
233
  end
304
234
 
305
235
  document.to_html
306
236
  end
307
237
 
238
+ def select_nodes(document, selector)
239
+ document.css(selector)
240
+ rescue Nokogiri::CSS::SyntaxError, Nokogiri::XML::XPath::SyntaxError
241
+ fallback_select_nodes(document, selector)
242
+ end
243
+
244
+ def fallback_select_nodes(document, selector)
245
+ return [] unless selector.include?(":lang(")
246
+
247
+ lang_values = selector.scan(/:lang\(([^)]+)\)/).flatten.map do |value|
248
+ value.to_s.strip.gsub(/\A['"]|['"]\z/, "").downcase
249
+ end.reject(&:empty?)
250
+ return [] if lang_values.empty?
251
+
252
+ base_selector = selector.gsub(/:lang\(([^)]+)\)/, "").strip
253
+ base_selector = "*" if base_selector.empty?
254
+
255
+ document.css(base_selector).select do |node|
256
+ lang_values.all? { |lang| lang_matches?(node, lang) }
257
+ end
258
+ rescue Nokogiri::CSS::SyntaxError, Nokogiri::XML::XPath::SyntaxError
259
+ []
260
+ end
261
+
262
+ def lang_matches?(node, lang)
263
+ current = node
264
+
265
+ while current
266
+ value = current["lang"]
267
+ if value && !value.empty?
268
+ normalized = value.downcase
269
+ return normalized == lang || normalized.start_with?("#{lang}-")
270
+ end
271
+ current = current.parent
272
+ end
273
+
274
+ false
275
+ end
276
+
308
277
  def parse_inline_css_rules(css)
309
278
  stripped_css = strip_css_comments(css.to_s)
310
279
  plain_css = strip_css_at_rules(stripped_css)
@@ -402,6 +371,7 @@ module MjmlRb
402
371
  registry = {}
403
372
  # Register component classes here as they are implemented.
404
373
  register_component(registry, Components::Body.new(self))
374
+ register_component(registry, Components::Head.new(self))
405
375
  register_component(registry, Components::Attributes.new(self))
406
376
  register_component(registry, Components::Breakpoint.new(self))
407
377
  register_component(registry, Components::Accordion.new(self))
@@ -412,6 +382,7 @@ module MjmlRb
412
382
  register_component(registry, Components::Raw.new(self))
413
383
  register_component(registry, Components::Text.new(self))
414
384
  register_component(registry, Components::Divider.new(self))
385
+ register_component(registry, Components::HtmlAttributes.new(self))
415
386
  register_component(registry, Components::Table.new(self))
416
387
  register_component(registry, Components::Social.new(self))
417
388
  register_component(registry, Components::Section.new(self))
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.2.3".freeze
2
+ VERSION = "0.2.5".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.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk
@@ -61,6 +61,7 @@ files:
61
61
  - lib/mjml-rb/components/button.rb
62
62
  - lib/mjml-rb/components/column.rb
63
63
  - lib/mjml-rb/components/divider.rb
64
+ - lib/mjml-rb/components/head.rb
64
65
  - lib/mjml-rb/components/hero.rb
65
66
  - lib/mjml-rb/components/html_attributes.rb
66
67
  - lib/mjml-rb/components/image.rb