mjml-rb 0.5.1 → 0.6.0

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +60 -22
  4. data/THIRD_PARTY_LICENSES.md +25 -0
  5. data/docs/ARCHITECTURE.md +363 -0
  6. data/docs/MIGRATING_TO_V5.md +112 -0
  7. data/docs/MJML_V5_SUPPORT_PLAN.md +325 -0
  8. data/docs/PARITY_AUDIT.md +124 -0
  9. data/docs/RELEASING.md +35 -0
  10. data/docs/USAGE.md +1258 -0
  11. data/docs/releases/v0.6.0.md +63 -0
  12. data/lib/mjml-rb/cli.rb +43 -31
  13. data/lib/mjml-rb/column_gutter.rb +81 -0
  14. data/lib/mjml-rb/compiler.rb +32 -122
  15. data/lib/mjml-rb/component_registry.rb +11 -1
  16. data/lib/mjml-rb/components/accordion.rb +11 -9
  17. data/lib/mjml-rb/components/attributes.rb +5 -5
  18. data/lib/mjml-rb/components/base.rb +5 -1
  19. data/lib/mjml-rb/components/body.rb +6 -3
  20. data/lib/mjml-rb/components/breakpoint.rb +1 -1
  21. data/lib/mjml-rb/components/carousel.rb +2 -2
  22. data/lib/mjml-rb/components/carousel_image.rb +2 -2
  23. data/lib/mjml-rb/components/column.rb +16 -11
  24. data/lib/mjml-rb/components/group.rb +13 -4
  25. data/lib/mjml-rb/components/head.rb +3 -3
  26. data/lib/mjml-rb/components/hero.rb +7 -2
  27. data/lib/mjml-rb/components/html_attributes.rb +3 -3
  28. data/lib/mjml-rb/components/image.rb +1 -3
  29. data/lib/mjml-rb/components/raw.rb +1 -1
  30. data/lib/mjml-rb/components/section.rb +23 -8
  31. data/lib/mjml-rb/components/social.rb +19 -28
  32. data/lib/mjml-rb/components/table.rb +2 -2
  33. data/lib/mjml-rb/config_file.rb +25 -15
  34. data/lib/mjml-rb/dependencies.rb +1 -0
  35. data/lib/mjml-rb/formatter.rb +113 -0
  36. data/lib/mjml-rb/node_compat.rb +19 -0
  37. data/lib/mjml-rb/options.rb +81 -0
  38. data/lib/mjml-rb/parser.rb +158 -131
  39. data/lib/mjml-rb/renderer.rb +84 -100
  40. data/lib/mjml-rb/result.rb +3 -2
  41. data/lib/mjml-rb/validator.rb +26 -34
  42. data/lib/mjml-rb/version.rb +2 -1
  43. data/lib/mjml-rb.rb +3 -3
  44. data/mjml-rb.gemspec +2 -2
  45. metadata +20 -2
  46. data/lib/mjml-rb/ast_node.rb +0 -37
@@ -64,15 +64,16 @@ module MjmlRb
64
64
  HTML
65
65
 
66
66
  def render(document, options = {})
67
+ @formatting = options[:minify] || options[:beautify]
67
68
  head = find_child(document, "mj-head")
68
69
  body = find_child(document, "mj-body")
69
70
  raise ArgumentError, "Missing <mj-body>" unless body
70
71
 
71
72
  context = build_context(head, options)
72
73
  context[:before_doctype] = root_file_start_raw(document)
73
- context[:lang] = options[:lang] || document.attributes["lang"] || "und"
74
- context[:dir] = options[:dir] || document.attributes["dir"] || "auto"
75
- context[:force_owa_desktop] = document.attributes["owa"] == "desktop"
74
+ context[:lang] = options[:lang] || document["lang"] || "und"
75
+ context[:dir] = options[:dir] || document["dir"] || "auto"
76
+ context[:force_owa_desktop] = document["owa"] == "desktop"
76
77
  context[:printer_support] = options[:printer_support] || options[:printerSupport]
77
78
  context[:column_widths] = {}
78
79
  append_component_head_styles(document, context)
@@ -84,6 +85,8 @@ module MjmlRb
84
85
 
85
86
  def build_context(head, options)
86
87
  context = {
88
+ sanitize_styles: options[:sanitize_styles],
89
+ template_syntax: options[:template_syntax] || Options::DEFAULTS[:template_syntax],
87
90
  title: "",
88
91
  preview: "",
89
92
  breakpoint: "480px",
@@ -115,7 +118,6 @@ module MjmlRb
115
118
  content = minify_outlook_conditionals(content)
116
119
  content = apply_html_attributes_to_content(content, context)
117
120
  title = context[:title].to_s
118
- preview = context[:preview]
119
121
  head_raw = Array(context[:head_raw]).join("\n")
120
122
  before_doctype = context[:before_doctype].to_s
121
123
  font_tags = build_font_tags(content, context[:inline_styles], context[:fonts])
@@ -127,7 +129,6 @@ module MjmlRb
127
129
  )
128
130
  component_styles_tag = build_style_tag(unique_strings(context[:component_head_styles]))
129
131
  user_styles_tag = build_style_tag(unique_strings(context[:user_styles]))
130
- 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>)
131
132
  html_attributes = {
132
133
  "lang" => context[:lang],
133
134
  "dir" => context[:dir],
@@ -135,10 +136,6 @@ module MjmlRb
135
136
  "xmlns:v" => "urn:schemas-microsoft-com:vml",
136
137
  "xmlns:o" => "urn:schemas-microsoft-com:office:office"
137
138
  }
138
- body_style = style_join(
139
- "word-spacing" => "normal",
140
- "background-color" => context[:background_color]
141
- )
142
139
 
143
140
  html = <<~HTML
144
141
  <!doctype html>
@@ -159,10 +156,7 @@ module MjmlRb
159
156
  #{user_styles_tag}
160
157
  #{head_raw}
161
158
  </head>
162
- <body style="#{body_style}">
163
- #{preview_block}
164
- #{content}
165
- </body>
159
+ #{content}
166
160
  </html>
167
161
  HTML
168
162
 
@@ -170,7 +164,8 @@ module MjmlRb
170
164
  html = preserve_raw_tag_spacing(html)
171
165
  html = strip_internal_raw_markers(html)
172
166
  html = merge_outlook_conditionals(html)
173
- before_doctype.empty? ? html : "#{before_doctype}\n#{html}"
167
+ html = before_doctype.empty? ? html : "#{before_doctype}\n#{html}"
168
+ @formatting ? html : html.gsub(/<!--mjml-rb:raw-(?:start|end)-->/, "")
174
169
  end
175
170
 
176
171
  def build_font_tags(content, inline_styles, fonts)
@@ -225,7 +220,7 @@ module MjmlRb
225
220
  return [100] if total == 0
226
221
 
227
222
  widths = columns.map do |col|
228
- w = col.attributes["width"]
223
+ w = resolved_attributes(col, context)["width"]
229
224
  if w && w.to_s =~ /(\d+(?:\.\d+)?)\s*%/
230
225
  $1.to_f
231
226
  elsif w && w.to_s =~ /(\d+(?:\.\d+)?)\s*px/
@@ -236,16 +231,7 @@ module MjmlRb
236
231
  end
237
232
  end
238
233
 
239
- specified = widths.compact.sum
240
- unset_count = widths.count(&:nil?)
241
-
242
- if unset_count > 0
243
- remaining = [100.0 - specified, 0.0].max
244
- each_unset = remaining / unset_count
245
- widths.map { |w| w || each_unset }
246
- else
247
- widths
248
- end
234
+ widths.map { |width| width || 100.0 / total }
249
235
  end
250
236
 
251
237
  def build_media_queries_tags(breakpoint, column_widths, force_owa_desktop: false, printer_support: false)
@@ -253,13 +239,13 @@ module MjmlRb
253
239
  return "" if widths.empty?
254
240
 
255
241
  base_rules = widths.map do |class_name, width_str|
256
- ".#{class_name} { width:#{width_str} !important; max-width: #{width_str}; }"
242
+ ".#{class_name} { #{media_query_declarations(width_str)} }"
257
243
  end
258
244
  moz_rules = widths.map do |class_name, width_str|
259
- ".moz-text-html .#{class_name} { width:#{width_str} !important; max-width: #{width_str}; }"
245
+ ".moz-text-html .#{class_name} { #{media_query_declarations(width_str)} }"
260
246
  end
261
247
  owa_rules = widths.map do |class_name, width_str|
262
- "[owa] .#{class_name} { width:#{width_str} !important; max-width: #{width_str}; }"
248
+ "[owa] .#{class_name} { #{media_query_declarations(width_str)} }"
263
249
  end
264
250
 
265
251
  bp = breakpoint.to_s.strip
@@ -283,29 +269,36 @@ module MjmlRb
283
269
  parts.join("\n")
284
270
  end
285
271
 
272
+ def media_query_declarations(value)
273
+ return "padding: #{value[:padding]} !important;" if value.is_a?(Hash)
274
+ "width:#{value} !important; max-width: #{value};"
275
+ end
276
+
286
277
  def build_style_tag(styles)
287
278
  return "" if styles.empty?
288
279
 
289
280
  "<style type=\"text/css\">#{styles.join("\n")}</style>"
290
281
  end
291
282
 
283
+ def outside_raw_regions(html)
284
+ html.split(/(<!--mjml-rb:raw-start-->.*?<!--mjml-rb:raw-end-->)/m).map do |part|
285
+ part.start_with?("<!--mjml-rb:raw-start-->") ? part : yield(part)
286
+ end.join
287
+ end
288
+
292
289
  def merge_outlook_conditionals(html)
293
- # MJML post-processes the HTML to merge adjacent Outlook conditional comments.
294
- # e.g. <![endif]-->\n<!--[if mso | IE]> become a single conditional block.
295
- html.gsub(/<!\[endif\]-->\s*<!--\[if mso \| IE\]>/m, "")
290
+ outside_raw_regions(html) do |part|
291
+ part.gsub(/<!\[endif\]-->\s*<!--\[if mso \| IE\]>/m, "")
292
+ end
296
293
  end
297
294
 
298
295
  def minify_outlook_conditionals(html)
299
- html.gsub(/(<!--\[if\s[^\]]+\]>)([\s\S]*?)(<!\[endif\]-->)/m) do
300
- prefix = Regexp.last_match(1)
301
- content = Regexp.last_match(2)
302
- suffix = Regexp.last_match(3)
303
-
304
- processed = content
305
- .gsub(/(^|>)(\s+)(<|$)/m, '\1\3')
306
- .gsub(/\s{2,}/m, " ")
307
-
308
- "#{prefix}#{processed}#{suffix}"
296
+ outside_raw_regions(html) do |part|
297
+ part.gsub(/(<!--\[if\s[^\]]+\]>)([\s\S]*?)(<!\[endif\]-->)/m) do
298
+ prefix, content, suffix = Regexp.last_match.captures
299
+ processed = content.gsub(/(^|>)(\s+)(<|$)/m, '\\1\\3').gsub(/\s{2,}/m, " ")
300
+ "#{prefix}#{processed}#{suffix}"
301
+ end
309
302
  end
310
303
  end
311
304
 
@@ -313,7 +306,7 @@ module MjmlRb
313
306
  rules = context[:html_attributes] || {}
314
307
  return content if rules.empty?
315
308
 
316
- root = html_attributes_fragment_root(content, context)
309
+ root = Nokogiri::HTML("<html>#{content}</html>")
317
310
 
318
311
  rules.each do |selector, attrs|
319
312
  next if selector.empty? || attrs.empty?
@@ -325,7 +318,7 @@ module MjmlRb
325
318
  end
326
319
  end
327
320
 
328
- root.inner_html
321
+ root.at_css("body").to_html
329
322
  end
330
323
 
331
324
  def html_attributes_fragment_root(content, context)
@@ -341,6 +334,9 @@ module MjmlRb
341
334
  def apply_inline_styles(html, context)
342
335
  css_blocks = unique_strings(context[:inline_styles]).reject { |css| css.nil? || css.strip.empty? }
343
336
  return html if css_blocks.empty?
337
+ if context[:sanitize_styles] && css_blocks.any? { |css| context[:template_syntax].any? { |syntax| css.include?(syntax[:prefix] || syntax["prefix"]) } }
338
+ raise ArgumentError, "Template syntax in inline MJML CSS is unsupported; use non-inline mj-style or raw styles"
339
+ end
344
340
 
345
341
  document = parse_html_document(html)
346
342
  rules, = parse_inline_css_rules(css_blocks.join("\n"))
@@ -626,41 +622,13 @@ module MjmlRb
626
622
  end
627
623
 
628
624
  def component_registry
629
- @component_registry ||= begin
630
- registry = {}
631
- # Register component classes here as they are implemented.
632
- register_component(registry, Components::Body.new(self))
633
- register_component(registry, Components::Head.new(self))
634
- register_component(registry, Components::Attributes.new(self))
635
- register_component(registry, Components::Breakpoint.new(self))
636
- register_component(registry, Components::Accordion.new(self))
637
- register_component(registry, Components::Button.new(self))
638
- register_component(registry, Components::Carousel.new(self))
639
- register_component(registry, Components::CarouselImage.new(self))
640
- register_component(registry, Components::Group.new(self))
641
- register_component(registry, Components::Hero.new(self))
642
- register_component(registry, Components::Image.new(self))
643
- register_component(registry, Components::Navbar.new(self))
644
- register_component(registry, Components::Raw.new(self))
645
- register_component(registry, Components::Text.new(self))
646
- register_component(registry, Components::Divider.new(self))
647
- register_component(registry, Components::HtmlAttributes.new(self))
648
- register_component(registry, Components::Table.new(self))
649
- register_component(registry, Components::Social.new(self))
650
- register_component(registry, Components::Section.new(self))
651
- register_component(registry, Components::Column.new(self))
652
- register_component(registry, Components::Spacer.new(self))
653
-
654
- MjmlRb.component_registry.custom_components.each do |klass|
655
- register_component(registry, klass.new(self))
656
- end
657
-
658
- registry
625
+ registry = MjmlRb.component_registry
626
+ if @registry_revision != registry.revision
627
+ instances = {}
628
+ @component_registry = registry.components.transform_values { |klass| instances[klass] ||= klass.new(self) }
629
+ @registry_revision = registry.revision
659
630
  end
660
- end
661
-
662
- def register_component(registry, component)
663
- component.tags.each { |tag| registry[tag] = component }
631
+ @component_registry
664
632
  end
665
633
 
666
634
  def resolved_attributes(node, context)
@@ -668,7 +636,7 @@ module MjmlRb
668
636
  attrs.merge!(context[:global_defaults] || {})
669
637
  attrs.merge!(context[:tag_defaults][node.tag_name] || {})
670
638
 
671
- node_classes = node.attributes["mj-class"].to_s.split(/\s+/).reject(&:empty?)
639
+ node_classes = node["mj-class"].to_s.split(/\s+/).reject(&:empty?)
672
640
  class_attrs = node_classes.each_with_object({}) do |klass, memo|
673
641
  mj_class_attrs = (context[:classes] || {})[klass] || {}
674
642
  if memo["css-class"] && mj_class_attrs["css-class"]
@@ -683,33 +651,35 @@ module MjmlRb
683
651
  attrs.merge!(((context[:classes_default] || {})[klass] || {})[node.tag_name] || {})
684
652
  end
685
653
 
686
- attrs.merge!(node.attributes)
654
+ attrs.merge!(node_string_attributes(node))
687
655
  attrs
688
656
  end
689
657
 
690
658
  def html_inner(node)
691
- if node.respond_to?(:inner_html)
692
- node.inner_html
693
- else
694
- escape_html(node.text_content)
695
- end
659
+ node.inner_html
696
660
  end
697
661
 
698
662
  def raw_inner(node)
699
- # For ending-tag nodes whose content was preserved as raw HTML by the parser
700
- return node.content if node.element? && node.content
701
-
702
- if node.respond_to?(:children)
703
- node.children.map do |child|
704
- if child.text?
705
- child.content.to_s
706
- else
707
- child.to_html
708
- end
709
- end.join
710
- else
711
- node.text_content
663
+ # For ending-tag nodes whose content was preserved as raw HTML by the parser.
664
+ # The parser marks these with data-mjml-ending-tag; their children are CDATA
665
+ # nodes containing the raw HTML content.
666
+ if node.element? && node["data-mjml-ending-tag"]
667
+ return node.children.select { |c| c.cdata? || c.text? }.map(&:content).join
712
668
  end
669
+
670
+ node.children.map do |child|
671
+ if child.text?
672
+ child.content.to_s
673
+ else
674
+ child.to_html
675
+ end
676
+ end.join
677
+ end
678
+
679
+ def protect_raw_html(content)
680
+ return content if content.empty? || (!@formatting && !content.include?("<!--[if"))
681
+ # Use separate markers so source htmlmin:ignore regions can be nested.
682
+ "<!--mjml-rb:raw-start-->#{content}<!--mjml-rb:raw-end-->"
713
683
  end
714
684
 
715
685
  def annotate_raw_html(content)
@@ -755,7 +725,7 @@ module MjmlRb
755
725
 
756
726
  def with_inherited_mj_class(context, node)
757
727
  previous = context[:inherited_mj_class]
758
- current = node.attributes["mj-class"]
728
+ current = node["mj-class"]
759
729
  context[:inherited_mj_class] = (current && !current.empty?) ? current : previous
760
730
  yield
761
731
  ensure
@@ -765,9 +735,9 @@ module MjmlRb
765
735
  def root_file_start_raw(document)
766
736
  document.element_children.filter_map do |child|
767
737
  next unless child.tag_name == "mj-raw"
768
- next unless child.attributes["position"] == "file-start"
738
+ next unless child["position"] == "file-start"
769
739
 
770
- raw_inner(child)
740
+ protect_raw_html(raw_inner(child))
771
741
  end.join("\n")
772
742
  end
773
743
 
@@ -779,6 +749,20 @@ module MjmlRb
779
749
  result
780
750
  end
781
751
 
752
+ # Internal-only attributes set by the parser for metadata tracking.
753
+ # These are excluded from the public attributes hash; data-mjml-raw
754
+ # is intentionally kept because it is used by the rendering pipeline.
755
+ INTERNAL_ATTRIBUTES = %w[data-mjml-file data-mjml-line data-mjml-ending-tag].freeze
756
+
757
+ def node_string_attributes(node)
758
+ result = {}
759
+ node.attributes.each do |name, attr|
760
+ next if INTERNAL_ATTRIBUTES.include?(name)
761
+ result[name] = attr.respond_to?(:value) ? attr.value : attr.to_s
762
+ end
763
+ result
764
+ end
765
+
782
766
  def escape_html(value)
783
767
  CGI.escapeHTML(value.to_s)
784
768
  end
@@ -1,11 +1,12 @@
1
1
  module MjmlRb
2
2
  class Result
3
- attr_reader :html, :errors, :warnings
3
+ attr_reader :html, :errors, :warnings, :dependencies
4
4
 
5
- def initialize(html: "", errors: [], warnings: [])
5
+ def initialize(html: "", errors: [], warnings: [], dependencies: [])
6
6
  @html = html.to_s
7
7
  @errors = Array(errors)
8
8
  @warnings = Array(warnings)
9
+ @dependencies = Array(dependencies).freeze
9
10
  end
10
11
 
11
12
  def success?
@@ -5,28 +5,31 @@ module MjmlRb
5
5
  class Validator
6
6
  GLOBAL_ALLOWED_ATTRIBUTES = %w[css-class mj-class].freeze
7
7
 
8
- REQUIRED_BY_TAG = {
9
- "mj-image" => %w[src],
10
- "mj-font" => %w[name href],
11
- "mj-include" => %w[path]
12
- }.freeze
13
-
14
8
  def initialize(parser: Parser.new)
15
9
  @parser = parser
16
10
  end
17
11
 
18
12
  def validate(mjml_or_ast, options = {})
19
- root = mjml_or_ast.is_a?(AstNode) ? mjml_or_ast : parse_ast(mjml_or_ast, options)
13
+ options = Options::DEFAULTS.merge(Options.normalize(options))
14
+ parsed = if mjml_or_ast.is_a?(Parser::ParseResult)
15
+ mjml_or_ast
16
+ elsif mjml_or_ast.is_a?(Nokogiri::XML::Node)
17
+ Parser::ParseResult.new(ast: mjml_or_ast, diagnostics: [])
18
+ else
19
+ @parser.parse_result(mjml_or_ast, options)
20
+ end
21
+ root = parsed.ast
20
22
  unless root&.tag_name == "mjml"
21
23
  return { errors: [error("Root element must be <mjml>", tag_name: root&.tag_name)], warnings: [] }
22
24
  end
23
25
 
24
- issues = []
26
+ issues = parsed.diagnostics.dup
27
+ return classify_issues(issues, options) if options[:validation_level].to_s == "skip"
25
28
  issues << error("Missing <mj-body>", tag_name: "mjml") unless root.element_children.any? { |c| c.tag_name == "mj-body" }
26
29
  walk(root, issues)
27
30
  classify_issues(issues, options)
28
31
  rescue Parser::ParseError => e
29
- { errors: [error(e.message, line: e.line)], warnings: [] }
32
+ { errors: [error(e.message, line: e.line, file: e.file)], warnings: [] }
30
33
  end
31
34
 
32
35
  private
@@ -42,23 +45,11 @@ module MjmlRb
42
45
  end
43
46
  end
44
47
 
45
- def parse_ast(mjml, options)
46
- @parser.parse(
47
- mjml,
48
- keep_comments: options.fetch(:keep_comments, false),
49
- preprocessors: options.fetch(:preprocessors, []),
50
- ignore_includes: options.fetch(:ignore_includes, false),
51
- file_path: options.fetch(:file_path, "."),
52
- actual_path: options.fetch(:actual_path, ".")
53
- )
54
- end
55
-
56
48
  def walk(node, errors)
57
49
  return unless node.element?
58
50
 
59
51
  validate_known_tag(node, errors)
60
52
  validate_allowed_children(node, errors)
61
- validate_required_attributes(node, errors)
62
53
  validate_supported_attributes(node, errors)
63
54
  validate_attribute_types(node, errors)
64
55
 
@@ -94,21 +85,11 @@ module MjmlRb
94
85
  end
95
86
  end
96
87
 
97
- def validate_required_attributes(node, errors)
98
- required = REQUIRED_BY_TAG[node.tag_name] || []
99
- required.each do |attr|
100
- next if node.attributes.key?(attr)
101
-
102
- errors << error("Attribute `#{attr}` is required for <#{node.tag_name}>",
103
- tag_name: node.tag_name, line: node.line, file: node.file)
104
- end
105
- end
106
-
107
88
  def validate_supported_attributes(node, errors)
108
89
  allowed_attributes = allowed_attributes_for(node.tag_name)
109
- return if allowed_attributes.empty?
90
+ return if node.tag_name == "mjml" || node.parent&.name == "mj-attributes"
110
91
 
111
- node.attributes.each_key do |attribute_name|
92
+ string_attrs(node).each_key do |attribute_name|
112
93
  next if allowed_attributes.key?(attribute_name)
113
94
  next if GLOBAL_ALLOWED_ATTRIBUTES.include?(attribute_name)
114
95
 
@@ -121,7 +102,7 @@ module MjmlRb
121
102
  allowed_attributes = allowed_attributes_for(node.tag_name)
122
103
  return if allowed_attributes.empty?
123
104
 
124
- node.attributes.each do |attribute_name, attribute_value|
105
+ string_attrs(node).each do |attribute_name, attribute_value|
125
106
  next if GLOBAL_ALLOWED_ATTRIBUTES.include?(attribute_name)
126
107
 
127
108
  expected_type = allowed_attributes[attribute_name]
@@ -218,6 +199,17 @@ module MjmlRb
218
199
  end
219
200
  end
220
201
 
202
+ INTERNAL_ATTRIBUTES = %w[data-mjml-file data-mjml-line data-mjml-ending-tag data-mjml-raw].freeze
203
+
204
+ def string_attrs(node)
205
+ result = {}
206
+ node.attributes.each do |name, attr|
207
+ next if INTERNAL_ATTRIBUTES.include?(name)
208
+ result[name] = attr.respond_to?(:value) ? attr.value : attr.to_s
209
+ end
210
+ result
211
+ end
212
+
221
213
  def error(message, line: nil, tag_name: nil, file: nil)
222
214
  location = [
223
215
  ("line #{line}" if line),
@@ -1,3 +1,4 @@
1
1
  module MjmlRb
2
- VERSION = "0.5.1".freeze
2
+ MJML_COMPATIBILITY_VERSION = "5.4.1".freeze
3
+ VERSION = "0.6.0".freeze
3
4
  end
data/lib/mjml-rb.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require_relative "mjml-rb/version"
2
2
  require_relative "mjml-rb/result"
3
- require_relative "mjml-rb/ast_node"
3
+ require_relative "mjml-rb/node_compat"
4
4
  require_relative "mjml-rb/dependencies"
5
5
  require_relative "mjml-rb/component_registry"
6
6
  require_relative "mjml-rb/config_file"
@@ -16,8 +16,8 @@ module MjmlRb
16
16
  warn <<~WARNING unless ENV["MJML_RB_NO_WARN"] == "1"
17
17
  WARNING: mjml-rb #{VERSION} is EXPERIMENTAL software.
18
18
  It is an unofficial Ruby port of MJML, not affiliated with or endorsed by
19
- the MJML team. Output may differ from the reference npm renderer. Not all
20
- components are fully implemented. Use in production at your own risk.
19
+ the MJML team. It targets MJML #{MJML_COMPATIBILITY_VERSION}; parser, formatter
20
+ and CSS inliner behavior may differ. Use in production at your own risk.
21
21
  Set MJML_RB_NO_WARN=1 to suppress this message.
22
22
  WARNING
23
23
 
data/mjml-rb.gemspec CHANGED
@@ -13,11 +13,11 @@ 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", "Rakefile", "mjml-rb.gemspec", "README.md"]
16
+ Dir.glob("{bin,lib}/**/*") + Dir.glob("docs/**/*.md") + ["Gemfile", "LICENSE", "THIRD_PARTY_LICENSES.md", "Rakefile", "mjml-rb.gemspec", "README.md", "CHANGELOG.md"]
17
17
  end
18
18
  spec.bindir = "bin"
19
19
  spec.executables = ["mjml"]
20
20
  spec.require_paths = ["lib"]
21
- spec.add_dependency "css_parser", ">= 1.17"
21
+ spec.add_dependency "css_parser", ">= 1.17", "< 4"
22
22
  spec.add_dependency "nokogiri", ">= 1.13"
23
23
  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.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '1.17'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '1.17'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: nokogiri
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -45,14 +51,23 @@ executables:
45
51
  extensions: []
46
52
  extra_rdoc_files: []
47
53
  files:
54
+ - CHANGELOG.md
48
55
  - Gemfile
49
56
  - LICENSE
50
57
  - README.md
51
58
  - Rakefile
59
+ - THIRD_PARTY_LICENSES.md
52
60
  - bin/mjml
61
+ - docs/ARCHITECTURE.md
62
+ - docs/MIGRATING_TO_V5.md
63
+ - docs/MJML_V5_SUPPORT_PLAN.md
64
+ - docs/PARITY_AUDIT.md
65
+ - docs/RELEASING.md
66
+ - docs/USAGE.md
67
+ - docs/releases/v0.6.0.md
53
68
  - lib/mjml-rb.rb
54
- - lib/mjml-rb/ast_node.rb
55
69
  - lib/mjml-rb/cli.rb
70
+ - lib/mjml-rb/column_gutter.rb
56
71
  - lib/mjml-rb/compiler.rb
57
72
  - lib/mjml-rb/component_registry.rb
58
73
  - lib/mjml-rb/components/accordion.rb
@@ -80,6 +95,9 @@ files:
80
95
  - lib/mjml-rb/components/text.rb
81
96
  - lib/mjml-rb/config_file.rb
82
97
  - lib/mjml-rb/dependencies.rb
98
+ - lib/mjml-rb/formatter.rb
99
+ - lib/mjml-rb/node_compat.rb
100
+ - lib/mjml-rb/options.rb
83
101
  - lib/mjml-rb/parser.rb
84
102
  - lib/mjml-rb/railtie.rb
85
103
  - lib/mjml-rb/renderer.rb
@@ -1,37 +0,0 @@
1
- module MjmlRb
2
- class AstNode
3
- attr_reader :tag_name, :attributes, :children, :content, :line, :file
4
-
5
- def initialize(tag_name:, attributes: {}, children: [], content: nil, line: nil, file: nil)
6
- @tag_name = tag_name.to_s
7
- @attributes = attributes.transform_keys(&:to_s)
8
- @children = Array(children)
9
- @content = content
10
- @line = line
11
- @file = file
12
- end
13
-
14
- def text?
15
- @tag_name == "#text"
16
- end
17
-
18
- def comment?
19
- @tag_name == "#comment"
20
- end
21
-
22
- def element?
23
- !text? && !comment?
24
- end
25
-
26
- def text_content
27
- return @content.to_s if text?
28
- result = +""
29
- @children.each { |child| result << child.text_content }
30
- result
31
- end
32
-
33
- def element_children
34
- @element_children ||= @children.select(&:element?)
35
- end
36
- end
37
- end