mjml-rb 0.3.1 → 0.3.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 +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 +58 -4
- 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: 99c98b34eee9aea7822dc4f59250b3b8ca008b98a17cd7e5063eb04595460c31
|
|
4
|
+
data.tar.gz: 6541eb2234e61dd3399dce72b6f0f45f4e810601ecf1c7ff2ccc9a6405d74b84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ae18240675e7e9bf5572634a09cb2ae1079e93c874088b917181245a8b20947f59368b07d911fc781cd96676a7953e9f2aaa650bf69ed7eae9e4e5ff2962782
|
|
7
|
+
data.tar.gz: d6ae46c90053abdfeeb2445c87c7ebc07fbd9e36ac47b335577b03d2bfa0debb2dd5c6f9ce9208910c338ae7e7798d29b0c98ed4fc2b9a8b2aaf9eedd045a2f4
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "cgi"
|
|
2
2
|
require "nokogiri"
|
|
3
|
+
require "set"
|
|
3
4
|
require_relative "components/accordion"
|
|
4
5
|
require_relative "components/attributes"
|
|
5
6
|
require_relative "components/body"
|
|
@@ -539,6 +540,7 @@ module MjmlRb
|
|
|
539
540
|
existing[property] = merged
|
|
540
541
|
end
|
|
541
542
|
normalize_background_fallbacks!(node, existing)
|
|
543
|
+
sync_html_attributes!(node, existing)
|
|
542
544
|
node["style"] = serialize_css_declarations(existing)
|
|
543
545
|
end
|
|
544
546
|
|
|
@@ -552,12 +554,59 @@ module MjmlRb
|
|
|
552
554
|
important: declarations.fetch("background-color", {}).fetch(:important, false)
|
|
553
555
|
}
|
|
554
556
|
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Sync HTML attributes from inlined CSS declarations.
|
|
560
|
+
# Mirrors Juice's attribute syncing: width/height on TABLE/TD/TH/IMG,
|
|
561
|
+
# and style-to-attribute mappings (bgcolor, background, align, valign)
|
|
562
|
+
# on table-related elements.
|
|
563
|
+
# See: https://github.com/Automattic/juice/blob/master/lib/inline.js
|
|
564
|
+
WIDTH_HEIGHT_ELEMENTS = Set.new(%w[table td th img]).freeze
|
|
565
|
+
TABLE_ELEMENTS = Set.new(%w[table th tr td caption colgroup col thead tbody tfoot]).freeze
|
|
566
|
+
STYLE_TO_ATTRIBUTE = {
|
|
567
|
+
"background-color" => "bgcolor",
|
|
568
|
+
"background-image" => "background",
|
|
569
|
+
"text-align" => "align",
|
|
570
|
+
"vertical-align" => "valign"
|
|
571
|
+
}.freeze
|
|
572
|
+
|
|
573
|
+
def sync_html_attributes!(node, declarations)
|
|
574
|
+
tag = node.name.downcase
|
|
555
575
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
576
|
+
# Sync width/height on TABLE, TD, TH, IMG
|
|
577
|
+
if WIDTH_HEIGHT_ELEMENTS.include?(tag)
|
|
578
|
+
%w[width height].each do |prop|
|
|
579
|
+
css_value = declaration_value(declarations[prop])
|
|
580
|
+
next if css_value.nil? || css_value.empty?
|
|
581
|
+
|
|
582
|
+
# Convert CSS px values to plain numbers for HTML attributes;
|
|
583
|
+
# keep other values (auto, %) as-is.
|
|
584
|
+
html_value = css_value.sub(/px\z/i, "")
|
|
585
|
+
node[prop] = html_value
|
|
586
|
+
end
|
|
587
|
+
end
|
|
559
588
|
|
|
560
|
-
|
|
589
|
+
# Sync style-to-attribute mappings on table elements
|
|
590
|
+
if TABLE_ELEMENTS.include?(tag)
|
|
591
|
+
STYLE_TO_ATTRIBUTE.each do |css_prop, html_attr|
|
|
592
|
+
css_value = declaration_value(declarations[css_prop])
|
|
593
|
+
next if css_value.nil? || css_value.empty?
|
|
594
|
+
|
|
595
|
+
case html_attr
|
|
596
|
+
when "bgcolor"
|
|
597
|
+
next if %w[none transparent].include?(css_value.downcase)
|
|
598
|
+
node[html_attr] = css_value
|
|
599
|
+
when "background"
|
|
600
|
+
# Extract url(...) from background-image
|
|
601
|
+
url = css_value[/url\(['"]?([^'")]+)['"]?\)/i, 1]
|
|
602
|
+
node[html_attr] = url if url
|
|
603
|
+
when "align"
|
|
604
|
+
node[html_attr] = css_value
|
|
605
|
+
when "valign"
|
|
606
|
+
node[html_attr] = css_value
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
561
610
|
end
|
|
562
611
|
|
|
563
612
|
def syncable_background?(value)
|
|
@@ -648,6 +697,11 @@ module MjmlRb
|
|
|
648
697
|
register_component(registry, Components::Section.new(self))
|
|
649
698
|
register_component(registry, Components::Column.new(self))
|
|
650
699
|
register_component(registry, Components::Spacer.new(self))
|
|
700
|
+
|
|
701
|
+
MjmlRb.component_registry.custom_components.each do |klass|
|
|
702
|
+
register_component(registry, klass.new(self))
|
|
703
|
+
end
|
|
704
|
+
|
|
651
705
|
registry
|
|
652
706
|
end
|
|
653
707
|
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.3
|
|
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
|