mjml-rb 0.3.1 → 0.3.2
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 +53 -0
- data/lib/mjml-rb/cli.rb +5 -0
- data/lib/mjml-rb/component_registry.rb +65 -0
- data/lib/mjml-rb/config_file.rb +34 -0
- data/lib/mjml-rb/renderer.rb +5 -0
- data/lib/mjml-rb/validator.rb +4 -9
- data/lib/mjml-rb/version.rb +1 -1
- data/lib/mjml-rb.rb +10 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a961ff4aa96b2bc79520f261eb233c0e8b77f363afa3d7e0c670fa622359ab7
|
|
4
|
+
data.tar.gz: 295a1289237c408dbcd2752c26bc45c9b16dd80edfe00933bcec864bf250bebf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e544b134c2db7296c58cd9a5c6235b5780448a3673618ae5392ca6eeada84cafa67d88ad7cf1e8aab99aa41ab92feabcc3255ec7eb30ccdbd386b52cc73515ba
|
|
7
|
+
data.tar.gz: 3751e0e61fcfd95378c03d1024caa4dcc4ccc0e7dd1f92a08108afe98c5858cff3d2c31e8a551185a145c44e36e96d7d934fcfc4cd86168fad8ec74650192661
|
data/README.md
CHANGED
|
@@ -95,6 +95,59 @@ compiler options in your application config:
|
|
|
95
95
|
config.mjml_rb.compiler_options = { validation_level: "soft" }
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
## Custom components
|
|
99
|
+
|
|
100
|
+
You can register custom MJML components written in Ruby:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class MjRating < MjmlRb::Components::Base
|
|
104
|
+
TAGS = ["mj-rating"].freeze
|
|
105
|
+
ALLOWED_ATTRIBUTES = { "stars" => "integer", "color" => "color" }.freeze
|
|
106
|
+
DEFAULT_ATTRIBUTES = { "stars" => "5", "color" => "#f4b400" }.freeze
|
|
107
|
+
|
|
108
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
109
|
+
stars = (attrs["stars"] || "5").to_i
|
|
110
|
+
color = attrs["color"] || "#f4b400"
|
|
111
|
+
%(<div style="color:#{escape_attr(color)}">#{"\u2605" * stars}</div>)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
MjmlRb.register_component(MjRating,
|
|
116
|
+
dependencies: { "mj-column" => ["mj-rating"] },
|
|
117
|
+
ending_tags: ["mj-rating"]
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `dependencies` hash declares which parent tags accept the new component as a child. The `ending_tags` list tells the parser to treat content as raw HTML (like `mj-text`). Both are optional.
|
|
122
|
+
|
|
123
|
+
Once registered, the component works in MJML markup and is validated like any built-in component.
|
|
124
|
+
|
|
125
|
+
## `.mjmlrc` config file
|
|
126
|
+
|
|
127
|
+
Place a `.mjmlrc` file (JSON) in your project root to auto-register custom components and set default compiler options:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"packages": [
|
|
132
|
+
"./lib/mjml_components/mj_rating.rb"
|
|
133
|
+
],
|
|
134
|
+
"options": {
|
|
135
|
+
"beautify": true,
|
|
136
|
+
"validation-level": "soft"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **`packages`** — Ruby files to `require`. Each file should call `MjmlRb.register_component` to register its components.
|
|
142
|
+
- **`options`** — Default compiler options. CLI flags and programmatic options override these.
|
|
143
|
+
|
|
144
|
+
The CLI loads `.mjmlrc` automatically from the working directory. For the library API, load it explicitly:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
MjmlRb::ConfigFile.load("/path/to/project")
|
|
148
|
+
result = MjmlRb.mjml2html(mjml_string)
|
|
149
|
+
```
|
|
150
|
+
|
|
98
151
|
## Architecture
|
|
99
152
|
|
|
100
153
|
The compile pipeline is intentionally simple and fully Ruby-based:
|
data/lib/mjml-rb/cli.rb
CHANGED
|
@@ -24,6 +24,11 @@ module MjmlRb
|
|
|
24
24
|
parser.parse!(argv)
|
|
25
25
|
options[:positional] = argv
|
|
26
26
|
|
|
27
|
+
rc_config = ConfigFile.load
|
|
28
|
+
if rc_config[:options]
|
|
29
|
+
options[:config] = rc_config[:options].merge(options[:config])
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
input_mode, input_values = resolve_input(options)
|
|
28
33
|
output_mode = resolve_output(options)
|
|
29
34
|
config = options[:config]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
class ComponentRegistry
|
|
5
|
+
attr_reader :custom_components, :custom_dependencies, :custom_ending_tags
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@custom_components = []
|
|
9
|
+
@custom_dependencies = {}
|
|
10
|
+
@custom_ending_tags = Set.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(klass, dependencies: {}, ending_tags: [])
|
|
14
|
+
validate_component!(klass)
|
|
15
|
+
@custom_components << klass unless @custom_components.include?(klass)
|
|
16
|
+
dependencies.each do |parent, children|
|
|
17
|
+
@custom_dependencies[parent] = ((@custom_dependencies[parent] || []) + Array(children)).uniq
|
|
18
|
+
end
|
|
19
|
+
@custom_ending_tags.merge(Array(ending_tags))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def component_class_for_tag(tag_name)
|
|
23
|
+
all_component_classes.find { |klass| klass.tags.include?(tag_name) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dependency_rules
|
|
27
|
+
merged = {}
|
|
28
|
+
Dependencies::RULES.each { |k, v| merged[k] = v.dup }
|
|
29
|
+
@custom_dependencies.each do |parent, children|
|
|
30
|
+
merged[parent] = ((merged[parent] || []) + Array(children)).uniq
|
|
31
|
+
end
|
|
32
|
+
merged
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ending_tags
|
|
36
|
+
Dependencies::ENDING_TAGS | @custom_ending_tags
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@custom_components.clear
|
|
41
|
+
@custom_dependencies.clear
|
|
42
|
+
@custom_ending_tags.clear
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def all_component_classes
|
|
48
|
+
builtin = MjmlRb::Components.constants.filter_map do |name|
|
|
49
|
+
value = MjmlRb::Components.const_get(name)
|
|
50
|
+
value if value.is_a?(Class) && value < MjmlRb::Components::Base
|
|
51
|
+
rescue NameError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
(builtin + @custom_components).uniq
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_component!(klass)
|
|
58
|
+
raise ArgumentError, "Expected a Class, got #{klass.class}" unless klass.is_a?(Class)
|
|
59
|
+
unless klass.respond_to?(:tags) && klass.respond_to?(:allowed_attributes)
|
|
60
|
+
raise ArgumentError, "Component class must respond to .tags and .allowed_attributes (inherit from MjmlRb::Components::Base)"
|
|
61
|
+
end
|
|
62
|
+
raise ArgumentError, "Component must define at least one tag via TAGS" if klass.tags.empty?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
class ConfigFile
|
|
5
|
+
DEFAULT_NAME = ".mjmlrc"
|
|
6
|
+
|
|
7
|
+
def self.load(dir = Dir.pwd)
|
|
8
|
+
path = File.join(dir, DEFAULT_NAME)
|
|
9
|
+
return {} unless File.exist?(path)
|
|
10
|
+
|
|
11
|
+
raw = JSON.parse(File.read(path))
|
|
12
|
+
config = {}
|
|
13
|
+
|
|
14
|
+
if raw["packages"].is_a?(Array)
|
|
15
|
+
raw["packages"].each do |pkg_path|
|
|
16
|
+
resolved = File.expand_path(pkg_path, dir)
|
|
17
|
+
require resolved
|
|
18
|
+
end
|
|
19
|
+
config[:packages_loaded] = raw["packages"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if raw["options"].is_a?(Hash)
|
|
23
|
+
config[:options] = raw["options"].each_with_object({}) do |(k, v), memo|
|
|
24
|
+
memo[k.to_s.tr("-", "_").to_sym] = v
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
config
|
|
29
|
+
rescue JSON::ParserError => e
|
|
30
|
+
warn "WARNING: Failed to parse #{path}: #{e.message}"
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -648,6 +648,11 @@ module MjmlRb
|
|
|
648
648
|
register_component(registry, Components::Section.new(self))
|
|
649
649
|
register_component(registry, Components::Column.new(self))
|
|
650
650
|
register_component(registry, Components::Spacer.new(self))
|
|
651
|
+
|
|
652
|
+
MjmlRb.component_registry.custom_components.each do |klass|
|
|
653
|
+
register_component(registry, klass.new(self))
|
|
654
|
+
end
|
|
655
|
+
|
|
651
656
|
registry
|
|
652
657
|
end
|
|
653
658
|
end
|
data/lib/mjml-rb/validator.rb
CHANGED
|
@@ -49,7 +49,7 @@ module MjmlRb
|
|
|
49
49
|
validate_supported_attributes(node, errors)
|
|
50
50
|
validate_attribute_types(node, errors)
|
|
51
51
|
|
|
52
|
-
return if
|
|
52
|
+
return if MjmlRb.component_registry.ending_tags.include?(node.tag_name)
|
|
53
53
|
|
|
54
54
|
node.element_children.each { |child| walk(child, errors) }
|
|
55
55
|
end
|
|
@@ -66,9 +66,9 @@ module MjmlRb
|
|
|
66
66
|
def validate_allowed_children(node, errors)
|
|
67
67
|
# Ending-tag components treat content as raw HTML; REXML still parses
|
|
68
68
|
# children structurally, so skip child validation for those tags.
|
|
69
|
-
return if
|
|
69
|
+
return if MjmlRb.component_registry.ending_tags.include?(node.tag_name)
|
|
70
70
|
|
|
71
|
-
allowed =
|
|
71
|
+
allowed = MjmlRb.component_registry.dependency_rules[node.tag_name]
|
|
72
72
|
return unless allowed
|
|
73
73
|
|
|
74
74
|
node.element_children.each do |child|
|
|
@@ -134,12 +134,7 @@ module MjmlRb
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
def component_class_for_tag(tag_name)
|
|
137
|
-
MjmlRb
|
|
138
|
-
value = MjmlRb::Components.const_get(name)
|
|
139
|
-
value if value.is_a?(Class) && value < MjmlRb::Components::Base
|
|
140
|
-
rescue NameError
|
|
141
|
-
nil
|
|
142
|
-
end.find { |klass| klass.tags.include?(tag_name) }
|
|
137
|
+
MjmlRb.component_registry.component_class_for_tag(tag_name)
|
|
143
138
|
end
|
|
144
139
|
|
|
145
140
|
def known_tag?(tag_name)
|
data/lib/mjml-rb/version.rb
CHANGED
data/lib/mjml-rb.rb
CHANGED
|
@@ -2,6 +2,8 @@ require_relative "mjml-rb/version"
|
|
|
2
2
|
require_relative "mjml-rb/result"
|
|
3
3
|
require_relative "mjml-rb/ast_node"
|
|
4
4
|
require_relative "mjml-rb/dependencies"
|
|
5
|
+
require_relative "mjml-rb/component_registry"
|
|
6
|
+
require_relative "mjml-rb/config_file"
|
|
5
7
|
require_relative "mjml-rb/parser"
|
|
6
8
|
require_relative "mjml-rb/renderer"
|
|
7
9
|
require_relative "mjml-rb/compiler"
|
|
@@ -43,6 +45,14 @@ module MjmlRb
|
|
|
43
45
|
ActionView::Template.register_template_handler(:mjml, TemplateHandler.new)
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def register_component(klass, dependencies: {}, ending_tags: [])
|
|
49
|
+
component_registry.register(klass, dependencies: dependencies, ending_tags: ending_tags)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def component_registry
|
|
53
|
+
@component_registry ||= ComponentRegistry.new
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
def mjml2html(mjml, options = {})
|
|
47
57
|
Compiler.new(options).compile(mjml).to_h
|
|
48
58
|
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.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Andriichuk
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/mjml-rb/ast_node.rb
|
|
54
54
|
- lib/mjml-rb/cli.rb
|
|
55
55
|
- lib/mjml-rb/compiler.rb
|
|
56
|
+
- lib/mjml-rb/component_registry.rb
|
|
56
57
|
- lib/mjml-rb/components/accordion.rb
|
|
57
58
|
- lib/mjml-rb/components/attributes.rb
|
|
58
59
|
- lib/mjml-rb/components/base.rb
|
|
@@ -75,6 +76,7 @@ files:
|
|
|
75
76
|
- lib/mjml-rb/components/spacer.rb
|
|
76
77
|
- lib/mjml-rb/components/table.rb
|
|
77
78
|
- lib/mjml-rb/components/text.rb
|
|
79
|
+
- lib/mjml-rb/config_file.rb
|
|
78
80
|
- lib/mjml-rb/dependencies.rb
|
|
79
81
|
- lib/mjml-rb/parser.rb
|
|
80
82
|
- lib/mjml-rb/railtie.rb
|