mjml-rb 0.2.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 +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +79 -0
- data/bin/mjml +6 -0
- data/lib/mjml-rb/ast_node.rb +34 -0
- data/lib/mjml-rb/cli.rb +305 -0
- data/lib/mjml-rb/compiler.rb +109 -0
- data/lib/mjml-rb/components/accordion.rb +210 -0
- data/lib/mjml-rb/components/base.rb +76 -0
- data/lib/mjml-rb/components/body.rb +46 -0
- data/lib/mjml-rb/components/breakpoint.rb +25 -0
- data/lib/mjml-rb/components/button.rb +157 -0
- data/lib/mjml-rb/components/column.rb +241 -0
- data/lib/mjml-rb/components/divider.rb +120 -0
- data/lib/mjml-rb/components/hero.rb +285 -0
- data/lib/mjml-rb/components/html_attributes.rb +32 -0
- data/lib/mjml-rb/components/image.rb +183 -0
- data/lib/mjml-rb/components/navbar.rb +279 -0
- data/lib/mjml-rb/components/section.rb +331 -0
- data/lib/mjml-rb/components/social.rb +303 -0
- data/lib/mjml-rb/components/spacer.rb +63 -0
- data/lib/mjml-rb/components/table.rb +162 -0
- data/lib/mjml-rb/components/text.rb +71 -0
- data/lib/mjml-rb/dependencies.rb +67 -0
- data/lib/mjml-rb/migrator.rb +18 -0
- data/lib/mjml-rb/parser.rb +169 -0
- data/lib/mjml-rb/renderer.rb +513 -0
- data/lib/mjml-rb/result.rb +23 -0
- data/lib/mjml-rb/validator.rb +187 -0
- data/lib/mjml-rb/version.rb +3 -0
- data/lib/mjml-rb.rb +30 -0
- data/mjml-rb.gemspec +23 -0
- metadata +101 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require_relative "dependencies"
|
|
2
|
+
require_relative "parser"
|
|
3
|
+
|
|
4
|
+
module MjmlRb
|
|
5
|
+
class Validator
|
|
6
|
+
GLOBAL_ALLOWED_ATTRIBUTES = %w[css-class mj-class].freeze
|
|
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
|
+
def initialize(parser: Parser.new)
|
|
15
|
+
@parser = parser
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate(mjml_or_ast, options = {})
|
|
19
|
+
root = mjml_or_ast.is_a?(AstNode) ? mjml_or_ast : parse_ast(mjml_or_ast, options)
|
|
20
|
+
return [error("Root element must be <mjml>", tag_name: root&.tag_name)] unless root&.tag_name == "mjml"
|
|
21
|
+
|
|
22
|
+
errors = []
|
|
23
|
+
errors << error("Missing <mj-body>", tag_name: "mjml") unless root.element_children.any? { |c| c.tag_name == "mj-body" }
|
|
24
|
+
walk(root, errors)
|
|
25
|
+
errors
|
|
26
|
+
rescue Parser::ParseError => e
|
|
27
|
+
[error(e.message, line: e.line)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_ast(mjml, options)
|
|
33
|
+
@parser.parse(
|
|
34
|
+
mjml,
|
|
35
|
+
keep_comments: options.fetch(:keep_comments, false),
|
|
36
|
+
preprocessors: options.fetch(:preprocessors, []),
|
|
37
|
+
ignore_includes: options.fetch(:ignore_includes, false),
|
|
38
|
+
file_path: options.fetch(:file_path, "."),
|
|
39
|
+
actual_path: options.fetch(:actual_path, ".")
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def walk(node, errors)
|
|
44
|
+
return unless node.element?
|
|
45
|
+
|
|
46
|
+
validate_allowed_children(node, errors)
|
|
47
|
+
validate_required_attributes(node, errors)
|
|
48
|
+
validate_supported_attributes(node, errors)
|
|
49
|
+
validate_attribute_types(node, errors)
|
|
50
|
+
|
|
51
|
+
node.element_children.each { |child| walk(child, errors) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_allowed_children(node, errors)
|
|
55
|
+
allowed = Dependencies::RULES[node.tag_name]
|
|
56
|
+
return unless allowed
|
|
57
|
+
|
|
58
|
+
node.element_children.each do |child|
|
|
59
|
+
next if allowed_child?(child.tag_name, allowed)
|
|
60
|
+
|
|
61
|
+
errors << error(
|
|
62
|
+
"Element <#{child.tag_name}> is not allowed inside <#{node.tag_name}>",
|
|
63
|
+
tag_name: child.tag_name
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_required_attributes(node, errors)
|
|
69
|
+
required = REQUIRED_BY_TAG[node.tag_name] || []
|
|
70
|
+
required.each do |attr|
|
|
71
|
+
next if node.attributes.key?(attr)
|
|
72
|
+
|
|
73
|
+
errors << error("Attribute `#{attr}` is required for <#{node.tag_name}>", tag_name: node.tag_name)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_supported_attributes(node, errors)
|
|
78
|
+
allowed_attributes = allowed_attributes_for(node.tag_name)
|
|
79
|
+
return if allowed_attributes.empty?
|
|
80
|
+
|
|
81
|
+
node.attributes.each_key do |attribute_name|
|
|
82
|
+
next if allowed_attributes.key?(attribute_name)
|
|
83
|
+
next if GLOBAL_ALLOWED_ATTRIBUTES.include?(attribute_name)
|
|
84
|
+
|
|
85
|
+
errors << error("Attribute `#{attribute_name}` is not allowed for <#{node.tag_name}>", tag_name: node.tag_name)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_attribute_types(node, errors)
|
|
90
|
+
allowed_attributes = allowed_attributes_for(node.tag_name)
|
|
91
|
+
return if allowed_attributes.empty?
|
|
92
|
+
|
|
93
|
+
node.attributes.each do |attribute_name, attribute_value|
|
|
94
|
+
next if GLOBAL_ALLOWED_ATTRIBUTES.include?(attribute_name)
|
|
95
|
+
|
|
96
|
+
expected_type = allowed_attributes[attribute_name]
|
|
97
|
+
next unless expected_type
|
|
98
|
+
next if valid_attribute_value?(attribute_value, expected_type)
|
|
99
|
+
|
|
100
|
+
errors << error(
|
|
101
|
+
"Attribute `#{attribute_name}` on <#{node.tag_name}> has invalid value `#{attribute_value}` for type `#{expected_type}`",
|
|
102
|
+
tag_name: node.tag_name
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def allowed_attributes_for(tag_name)
|
|
108
|
+
component_class = component_class_for_tag(tag_name)
|
|
109
|
+
return {} unless component_class
|
|
110
|
+
|
|
111
|
+
if component_class.respond_to?(:allowed_attributes_for)
|
|
112
|
+
component_class.allowed_attributes_for(tag_name)
|
|
113
|
+
else
|
|
114
|
+
component_class.allowed_attributes
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def component_class_for_tag(tag_name)
|
|
119
|
+
MjmlRb::Components.constants.filter_map do |name|
|
|
120
|
+
value = MjmlRb::Components.const_get(name)
|
|
121
|
+
value if value.is_a?(Class) && value < MjmlRb::Components::Base
|
|
122
|
+
rescue NameError
|
|
123
|
+
nil
|
|
124
|
+
end.find { |klass| klass.tags.include?(tag_name) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def valid_attribute_value?(value, expected_type)
|
|
128
|
+
return true if value.nil?
|
|
129
|
+
|
|
130
|
+
case expected_type
|
|
131
|
+
when "string"
|
|
132
|
+
true
|
|
133
|
+
when "color"
|
|
134
|
+
color?(value)
|
|
135
|
+
when /\Aenum\((.+)\)\z/
|
|
136
|
+
Regexp.last_match(1).split(",").map(&:strip).include?(value)
|
|
137
|
+
when /\Aunit\((.+)\)(?:\{(\d+),(\d+)\})?\z/
|
|
138
|
+
units = Regexp.last_match(1).split(",").map(&:strip)
|
|
139
|
+
min_count = Regexp.last_match(2)&.to_i || 1
|
|
140
|
+
max_count = Regexp.last_match(3)&.to_i || 1
|
|
141
|
+
unit_values?(value, units, min_count: min_count, max_count: max_count)
|
|
142
|
+
else
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def color?(value)
|
|
148
|
+
value.match?(/\A#[0-9a-fA-F]{3,8}\z/) ||
|
|
149
|
+
value.match?(/\Argb[a]?\([^)]+\)\z/i) ||
|
|
150
|
+
value.match?(/\Ahsl[a]?\([^)]+\)\z/i) ||
|
|
151
|
+
value.match?(/\A[a-z]+\z/i)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def unit_values?(value, units, min_count:, max_count:)
|
|
155
|
+
parts = value.to_s.strip.split(/\s+/)
|
|
156
|
+
return false if parts.empty? || parts.size < min_count || parts.size > max_count
|
|
157
|
+
|
|
158
|
+
parts.all? { |part| unit_value?(part, units) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def unit_value?(value, units)
|
|
162
|
+
return true if value.match?(/\A0(?:\.0+)?\z/)
|
|
163
|
+
|
|
164
|
+
units.any? do |unit|
|
|
165
|
+
value.match?(/\A-?\d+(?:\.\d+)?#{Regexp.escape(unit)}\z/)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def allowed_child?(tag_name, allowed_patterns)
|
|
170
|
+
allowed_patterns.any? do |pattern|
|
|
171
|
+
case pattern
|
|
172
|
+
when Regexp then pattern.match?(tag_name)
|
|
173
|
+
else pattern == tag_name
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def error(message, line: nil, tag_name: nil)
|
|
179
|
+
{
|
|
180
|
+
line: line,
|
|
181
|
+
message: message,
|
|
182
|
+
tag_name: tag_name,
|
|
183
|
+
formatted_message: message
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/mjml-rb.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "mjml-rb/version"
|
|
2
|
+
require_relative "mjml-rb/result"
|
|
3
|
+
require_relative "mjml-rb/ast_node"
|
|
4
|
+
require_relative "mjml-rb/dependencies"
|
|
5
|
+
require_relative "mjml-rb/parser"
|
|
6
|
+
require_relative "mjml-rb/renderer"
|
|
7
|
+
require_relative "mjml-rb/compiler"
|
|
8
|
+
require_relative "mjml-rb/validator"
|
|
9
|
+
require_relative "mjml-rb/migrator"
|
|
10
|
+
require_relative "mjml-rb/cli"
|
|
11
|
+
|
|
12
|
+
module MjmlRb
|
|
13
|
+
# Print an experimental warning once at load time unless suppressed.
|
|
14
|
+
# Suppress by setting the environment variable MJML_RB_NO_WARN=1.
|
|
15
|
+
warn <<~WARNING unless ENV["MJML_RB_NO_WARN"] == "1"
|
|
16
|
+
WARNING: mjml-rb #{VERSION} is EXPERIMENTAL software.
|
|
17
|
+
It is an unofficial Ruby port of MJML, not affiliated with or endorsed by
|
|
18
|
+
the MJML team. Output may differ from the reference npm renderer. Not all
|
|
19
|
+
components are fully implemented. Use in production at your own risk.
|
|
20
|
+
Set MJML_RB_NO_WARN=1 to suppress this message.
|
|
21
|
+
WARNING
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def mjml2html(mjml, options = {})
|
|
25
|
+
Compiler.new(options).compile(mjml).to_h
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias to_html mjml2html
|
|
29
|
+
end
|
|
30
|
+
end
|
data/mjml-rb.gemspec
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative "lib/mjml-rb/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "mjml-rb"
|
|
5
|
+
spec.version = MjmlRb::VERSION
|
|
6
|
+
spec.authors = ["Andrei Andriichuk"]
|
|
7
|
+
spec.email = ["andreiandriichuk@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Ruby implementation of the MJML toolchain"
|
|
10
|
+
spec.description = "Ruby-first MJML compiler API and CLI with compatibility-focused behavior."
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
spec.required_ruby_version = ">= 3.0"
|
|
13
|
+
|
|
14
|
+
spec.homepage = "https://github.com/faraquet/mjml-rails"
|
|
15
|
+
spec.files = Dir.chdir(__dir__) do
|
|
16
|
+
Dir.glob("{bin,lib}/**/*") + ["Gemfile", "LICENSE", "mjml-rb.gemspec", "README.md"]
|
|
17
|
+
end
|
|
18
|
+
spec.bindir = "bin"
|
|
19
|
+
spec.executables = ["mjml"]
|
|
20
|
+
spec.require_paths = ["lib"]
|
|
21
|
+
spec.add_dependency "nokogiri"
|
|
22
|
+
spec.add_dependency "rexml"
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mjml-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrei Andriichuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: nokogiri
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rexml
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
description: Ruby-first MJML compiler API and CLI with compatibility-focused behavior.
|
|
41
|
+
email:
|
|
42
|
+
- andreiandriichuk@gmail.com
|
|
43
|
+
executables:
|
|
44
|
+
- mjml
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- Gemfile
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- bin/mjml
|
|
52
|
+
- lib/mjml-rb.rb
|
|
53
|
+
- lib/mjml-rb/ast_node.rb
|
|
54
|
+
- lib/mjml-rb/cli.rb
|
|
55
|
+
- lib/mjml-rb/compiler.rb
|
|
56
|
+
- lib/mjml-rb/components/accordion.rb
|
|
57
|
+
- lib/mjml-rb/components/base.rb
|
|
58
|
+
- lib/mjml-rb/components/body.rb
|
|
59
|
+
- lib/mjml-rb/components/breakpoint.rb
|
|
60
|
+
- lib/mjml-rb/components/button.rb
|
|
61
|
+
- lib/mjml-rb/components/column.rb
|
|
62
|
+
- lib/mjml-rb/components/divider.rb
|
|
63
|
+
- lib/mjml-rb/components/hero.rb
|
|
64
|
+
- lib/mjml-rb/components/html_attributes.rb
|
|
65
|
+
- lib/mjml-rb/components/image.rb
|
|
66
|
+
- lib/mjml-rb/components/navbar.rb
|
|
67
|
+
- lib/mjml-rb/components/section.rb
|
|
68
|
+
- lib/mjml-rb/components/social.rb
|
|
69
|
+
- lib/mjml-rb/components/spacer.rb
|
|
70
|
+
- lib/mjml-rb/components/table.rb
|
|
71
|
+
- lib/mjml-rb/components/text.rb
|
|
72
|
+
- lib/mjml-rb/dependencies.rb
|
|
73
|
+
- lib/mjml-rb/migrator.rb
|
|
74
|
+
- lib/mjml-rb/parser.rb
|
|
75
|
+
- lib/mjml-rb/renderer.rb
|
|
76
|
+
- lib/mjml-rb/result.rb
|
|
77
|
+
- lib/mjml-rb/validator.rb
|
|
78
|
+
- lib/mjml-rb/version.rb
|
|
79
|
+
- mjml-rb.gemspec
|
|
80
|
+
homepage: https://github.com/faraquet/mjml-rails
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata: {}
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.0'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 4.0.3
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Ruby implementation of the MJML toolchain
|
|
101
|
+
test_files: []
|