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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5afc17046611848190458b50e1b7d0670011e8f1e32a6723b63c7e7ff76029c
4
- data.tar.gz: ff832c4861cd1c28d76f47e0f66f257e8fa1c956dd6e0836826df83be3e4782d
3
+ metadata.gz: 2a961ff4aa96b2bc79520f261eb233c0e8b77f363afa3d7e0c670fa622359ab7
4
+ data.tar.gz: 295a1289237c408dbcd2752c26bc45c9b16dd80edfe00933bcec864bf250bebf
5
5
  SHA512:
6
- metadata.gz: e43bae28df235a80ad028bc43e37401c45c59c8e0d486f09e8d22faa786c1e18e62e696bde4cce668fcee4ea1c7d3d037cec3ec93d890b0e8e33404f98b776fe
7
- data.tar.gz: c35f403ee71148fa10f6e9d794a61695f35f74201e561fba1afecb4686c71388c01f13bf69c19b799214102ad142a6f86281d922b3ed5eed2ed1e348b446f737
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
@@ -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
@@ -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 Dependencies::ENDING_TAGS.include?(node.tag_name)
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 Dependencies::ENDING_TAGS.include?(node.tag_name)
69
+ return if MjmlRb.component_registry.ending_tags.include?(node.tag_name)
70
70
 
71
- allowed = Dependencies::RULES[node.tag_name]
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::Components.constants.filter_map do |name|
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)
@@ -1,3 +1,3 @@
1
1
  module MjmlRb
2
- VERSION = "0.3.1".freeze
2
+ VERSION = "0.3.2".freeze
3
3
  end
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.1
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