emjay 0.2.0 → 0.2.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/emjay/components/body/mj_accordion.rb +0 -3
  3. data/lib/emjay/components/body/mj_accordion_element.rb +0 -3
  4. data/lib/emjay/components/body/mj_accordion_text.rb +0 -3
  5. data/lib/emjay/components/body/mj_accordion_title.rb +0 -3
  6. data/lib/emjay/components/body/mj_body.rb +0 -3
  7. data/lib/emjay/components/body/mj_button.rb +0 -3
  8. data/lib/emjay/components/body/mj_carousel.rb +0 -3
  9. data/lib/emjay/components/body/mj_carousel_image.rb +0 -3
  10. data/lib/emjay/components/body/mj_column.rb +1 -4
  11. data/lib/emjay/components/body/mj_divider.rb +0 -3
  12. data/lib/emjay/components/body/mj_group.rb +0 -3
  13. data/lib/emjay/components/body/mj_hero.rb +0 -3
  14. data/lib/emjay/components/body/mj_image.rb +0 -3
  15. data/lib/emjay/components/body/mj_navbar.rb +0 -3
  16. data/lib/emjay/components/body/mj_navbar_link.rb +0 -3
  17. data/lib/emjay/components/body/mj_raw.rb +0 -3
  18. data/lib/emjay/components/body/mj_section.rb +0 -3
  19. data/lib/emjay/components/body/mj_social.rb +0 -3
  20. data/lib/emjay/components/body/mj_social_element.rb +0 -3
  21. data/lib/emjay/components/body/mj_spacer.rb +0 -3
  22. data/lib/emjay/components/body/mj_table.rb +0 -3
  23. data/lib/emjay/components/body/mj_text.rb +0 -3
  24. data/lib/emjay/components/body/mj_wrapper.rb +0 -2
  25. data/lib/emjay/components/head/mj_attributes.rb +0 -3
  26. data/lib/emjay/components/head/mj_breakpoint.rb +0 -3
  27. data/lib/emjay/components/head/mj_font.rb +0 -3
  28. data/lib/emjay/components/head/mj_head.rb +0 -3
  29. data/lib/emjay/components/head/mj_html_attributes.rb +0 -3
  30. data/lib/emjay/components/head/mj_preview.rb +0 -3
  31. data/lib/emjay/components/head/mj_style.rb +0 -3
  32. data/lib/emjay/components/head/mj_title.rb +0 -3
  33. data/lib/emjay/rails/mail_interceptor.rb +8 -2
  34. data/lib/emjay/rails/template_handler.rb +6 -2
  35. data/lib/emjay/railtie.rb +12 -2
  36. data/lib/emjay/registry.rb +70 -6
  37. data/lib/emjay/renderer.rb +70 -49
  38. data/lib/emjay/skeleton.rb +15 -6
  39. data/lib/emjay/version.rb +1 -1
  40. data/lib/emjay.rb +3 -34
  41. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7e892dd10debedb98375075acbc595f229f28a1cf7976a90444748f123236b8
4
- data.tar.gz: adfb7062c1f9ca6d7a7926f9add058896e1d71078a795a64f0638cc036ad387e
3
+ metadata.gz: 12fae46a93b64e1980adc9085d568a1d337f13d9d8562d94937b7f24f861f0cb
4
+ data.tar.gz: f0db6163d0a00288d62c2b2b2820185f0bbfb7853ffc4b8c726c117d37e3d7e5
5
5
  SHA512:
6
- metadata.gz: b337e7f6f0ebeece8666fed2347bd0f00a5e1b814312b25d9ec81c31021749f7e81d576e4c318f38cc2c81fcc8560bb9914a9a146cf77d5a8729ada51510f046
7
- data.tar.gz: 5b3b358d75521a337ef0ee78af3b1d7929dd8cdcec194d2762332619f067558e9a04289686364d8627e50cf4963ec6bd192de72c1d78bd17c371388b972e56cd
6
+ metadata.gz: 754f37c1091076bba65cff5de501bce2e8f7b138f872e870c3cca2d4819c45e657acd0b4fe0c449503b7a1ed60927303665ed1a6316db37c57e4c8ed4ba2cff7
7
+ data.tar.gz: fda91c57c46af356ee1184c8408f32c1c4caffe1db3fd2f617490ec721929dab2b9921c16f510db32ce75ac68ca61efe497114b9ab28984c8421e3d99a577a7d
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -94,6 +93,4 @@ module Emjay
94
93
  end
95
94
  end
96
95
  end
97
-
98
- Registry.register(Components::MjAccordion)
99
96
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
 
7
6
  module Emjay
@@ -122,6 +121,4 @@ module Emjay
122
121
  end
123
122
  end
124
123
  end
125
-
126
- Registry.register(Components::MjAccordionElement)
127
124
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -118,6 +117,4 @@ module Emjay
118
117
  end
119
118
  end
120
119
  end
121
-
122
- Registry.register(Components::MjAccordionText)
123
120
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
 
7
6
  module Emjay
@@ -166,6 +165,4 @@ module Emjay
166
165
  end
167
166
  end
168
167
  end
169
-
170
- Registry.register(Components::MjAccordionTitle)
171
168
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -65,6 +64,4 @@ module Emjay
65
64
  end
66
65
  end
67
66
  end
68
-
69
- Registry.register(Components::MjBody)
70
67
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -193,6 +192,4 @@ module Emjay
193
192
  end
194
193
  end
195
194
  end
196
-
197
- Registry.register(Components::MjButton)
198
195
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
  require_relative "../../helpers/gen_random_hex_string"
7
6
 
@@ -405,6 +404,4 @@ module Emjay
405
404
  end
406
405
  end
407
406
  end
408
-
409
- Registry.register(Components::MjCarousel)
410
407
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/suffix_css_classes"
6
5
 
7
6
  module Emjay
@@ -183,6 +182,4 @@ module Emjay
183
182
  end
184
183
  end
185
184
  end
186
-
187
- Registry.register(Components::MjCarouselImage)
188
185
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -250,7 +249,7 @@ module Emjay
250
249
  if unit == "px"
251
250
  floor_width = reduced.floor
252
251
  fractional = reduced - floor_width
253
- extra = [0, [sibling, (sibling * fractional).round].min].max
252
+ extra = (sibling * fractional).round.clamp(0, sibling)
254
253
  {parsed_width: floor_width + ((index < extra) ? 1 : 0), unit: unit}
255
254
  else
256
255
  {parsed_width: reduced, unit: unit}
@@ -425,6 +424,4 @@ module Emjay
425
424
  end
426
425
  end
427
426
  end
428
-
429
- Registry.register(Components::MjColumn)
430
427
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -115,6 +114,4 @@ module Emjay
115
114
  end
116
115
  end
117
116
  end
118
-
119
- Registry.register(Components::MjDivider)
120
117
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -191,6 +190,4 @@ module Emjay
191
190
  end
192
191
  end
193
192
  end
194
-
195
- Registry.register(Components::MjGroup)
196
193
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -377,6 +376,4 @@ module Emjay
377
376
  end
378
377
  end
379
378
  end
380
-
381
- Registry.register(Components::MjHero)
382
379
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
  require_relative "../../helpers/make_lower_breakpoint"
7
6
 
@@ -183,6 +182,4 @@ module Emjay
183
182
  end
184
183
  end
185
184
  end
186
-
187
- Registry.register(Components::MjImage)
188
185
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
  require_relative "../../helpers/make_lower_breakpoint"
7
6
  require_relative "../../helpers/gen_random_hex_string"
@@ -182,6 +181,4 @@ module Emjay
182
181
  end
183
182
  end
184
183
  end
185
-
186
- Registry.register(Components::MjNavbar)
187
184
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
  require_relative "../../helpers/suffix_css_classes"
7
6
 
@@ -124,6 +123,4 @@ module Emjay
124
123
  end
125
124
  end
126
125
  end
127
-
128
- Registry.register(Components::MjNavbarLink)
129
126
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -29,6 +28,4 @@ module Emjay
29
28
  end
30
29
  end
31
30
  end
32
-
33
- Registry.register(Components::MjRaw)
34
31
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/suffix_css_classes"
6
5
 
7
6
  module Emjay
@@ -440,6 +439,4 @@ module Emjay
440
439
  end
441
440
  end
442
441
  end
443
-
444
- Registry.register(Components::MjSection)
445
442
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -169,6 +168,4 @@ module Emjay
169
168
  end
170
169
  end
171
170
  end
172
-
173
- Registry.register(Components::MjSocial)
174
171
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -270,6 +269,4 @@ module Emjay
270
269
  end
271
270
  end
272
271
  end
273
-
274
- Registry.register(Components::MjSocialElement)
275
272
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -52,6 +51,4 @@ module Emjay
52
51
  end
53
52
  end
54
53
  end
55
-
56
- Registry.register(Components::MjSpacer)
57
54
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/width_parser"
6
5
 
7
6
  module Emjay
@@ -108,6 +107,4 @@ module Emjay
108
107
  end
109
108
  end
110
109
  end
111
-
112
- Registry.register(Components::MjTable)
113
110
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../body_component"
4
- require_relative "../../registry"
5
4
  require_relative "../../helpers/conditional_tag"
6
5
 
7
6
  module Emjay
@@ -95,6 +94,4 @@ module Emjay
95
94
  end
96
95
  end
97
96
  end
98
-
99
- Registry.register(Components::MjText)
100
97
  end
@@ -51,6 +51,4 @@ module Emjay
51
51
  end
52
52
  end
53
53
  end
54
-
55
- Registry.register(Components::MjWrapper)
56
54
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -33,6 +32,4 @@ module Emjay
33
32
  end
34
33
  end
35
34
  end
36
-
37
- Registry.register(Components::MjAttributes)
38
35
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -23,6 +22,4 @@ module Emjay
23
22
  end
24
23
  end
25
24
  end
26
-
27
- Registry.register(Components::MjBreakpoint)
28
25
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -19,6 +18,4 @@ module Emjay
19
18
  end
20
19
  end
21
20
  end
22
-
23
- Registry.register(Components::MjFont)
24
21
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -15,6 +14,4 @@ module Emjay
15
14
  end
16
15
  end
17
16
  end
18
-
19
- Registry.register(Components::MjHead)
20
17
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -28,6 +27,4 @@ module Emjay
28
27
  end
29
28
  end
30
29
  end
31
-
32
- Registry.register(Components::MjHtmlAttributes)
33
30
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -19,6 +18,4 @@ module Emjay
19
18
  end
20
19
  end
21
20
  end
22
-
23
- Registry.register(Components::MjPreview)
24
21
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -29,6 +28,4 @@ module Emjay
29
28
  end
30
29
  end
31
30
  end
32
-
33
- Registry.register(Components::MjStyle)
34
31
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../head_component"
4
- require_relative "../../registry"
5
4
 
6
5
  module Emjay
7
6
  module Components
@@ -19,6 +18,4 @@ module Emjay
19
18
  end
20
19
  end
21
20
  end
22
-
23
- Registry.register(Components::MjTitle)
24
21
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
4
+
3
5
  module Emjay
4
6
  module Rails
5
7
  class MailInterceptor
@@ -14,6 +16,12 @@ module Emjay
14
16
  end
15
17
 
16
18
  def compile_mjml!(message)
19
+ Measurometer.instrument("emjay.mail_interceptor.compile") { compile_message!(message) }
20
+ end
21
+
22
+ private
23
+
24
+ def compile_message!(message)
17
25
  if message.multipart?
18
26
  message.parts.each { |part| compile_part!(part) }
19
27
  else
@@ -21,8 +29,6 @@ module Emjay
21
29
  end
22
30
  end
23
31
 
24
- private
25
-
26
32
  def compile_part!(part)
27
33
  if part.multipart?
28
34
  part.parts.each { |p| compile_part!(p) }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
4
+
3
5
  module Emjay
4
6
  module Rails
5
7
  module TemplateHandler
@@ -8,8 +10,10 @@ module Emjay
8
10
  # template extension with ERB support. MJML → HTML compilation
9
11
  # happens later via Emjay::Rails::MailInterceptor, after Rails
10
12
  # has assembled the full render (template + layout).
11
- erb_handler = ActionView::Template.registered_template_handler(:erb)
12
- erb_handler.call(template, source)
13
+ Measurometer.instrument("emjay.template_handler.compile") do
14
+ erb_handler = ActionView::Template.registered_template_handler(:erb)
15
+ erb_handler.call(template, source)
16
+ end
13
17
  end
14
18
  end
15
19
  end
data/lib/emjay/railtie.rb CHANGED
@@ -2,11 +2,21 @@
2
2
 
3
3
  module Emjay
4
4
  class Railtie < Rails::Railtie
5
- initializer "emjay.register_template_handler_and_interceptor" do
6
- ActiveSupport.on_load(:action_mailer) do
5
+ # Register the template handler on :action_view load, not :action_mailer.
6
+ # ActionView's FileSystemResolver builds its PathParser regex from the
7
+ # registered extensions on first template lookup; if ActionView loads
8
+ # before ActionMailer (e.g. a controller renders a view first), the
9
+ # regex is cached without :mjml and .html.mjml templates stay invisible
10
+ # for the rest of the process.
11
+ initializer "emjay.register_template_handler" do
12
+ ActiveSupport.on_load(:action_view) do
7
13
  require "emjay/rails/template_handler"
8
14
  ActionView::Template.register_template_handler(:mjml, Emjay::Rails::TemplateHandler)
15
+ end
16
+ end
9
17
 
18
+ initializer "emjay.register_mail_interceptor" do
19
+ ActiveSupport.on_load(:action_mailer) do
10
20
  require "emjay/rails/mail_interceptor"
11
21
  interceptor = Emjay::Rails::MailInterceptor
12
22
  ActionMailer::Base.register_interceptor(interceptor)
@@ -1,19 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
4
+
3
5
  module Emjay
6
+ # Lazy autoloader for MJML components. Scans `lib/emjay/components/**/*.rb`
7
+ # once at load time, then derives both the MJML tag name and the Ruby class
8
+ # from each filename:
9
+ #
10
+ # components/body/mj_button.rb → tag "mj-button" → Emjay::Components::MjButton
11
+ #
12
+ # Files are `require`d and constants resolved on first `find`.
4
13
  module Registry
5
- @components = {}
14
+ class UnknownComponent < StandardError; end
6
15
 
7
- def self.register(component_class)
8
- @components[component_class.component_name] = component_class
9
- end
16
+ COMPONENTS_ROOT = File.expand_path("components", __dir__)
17
+
18
+ PATHS = Dir.glob("{head,body}/*.rb", base: COMPONENTS_ROOT).sort.each_with_object({}) do |rel, h|
19
+ basename = File.basename(rel, ".rb") # e.g. "mj_html_attributes"
20
+ tag = basename.tr("_", "-") # → "mj-html-attributes"
21
+ h[tag] = File.join(COMPONENTS_ROOT, rel)
22
+ end.freeze
23
+
24
+ @loaded = {}
25
+ @mutex = Mutex.new
10
26
 
11
27
  def self.find(tag_name)
12
- @components[tag_name]
28
+ @loaded[tag_name] ||= load_component(tag_name)
13
29
  end
14
30
 
15
31
  def self.components
16
- @components
32
+ @components ||= LazyComponents.new
33
+ end
34
+
35
+ # Kept for backwards compatibility with callers that eagerly register.
36
+ def self.register(component_class)
37
+ @loaded[component_class.component_name] = component_class
38
+ end
39
+
40
+ def self.load_component(tag_name)
41
+ path = PATHS[tag_name] or return nil
42
+ @mutex.synchronize do
43
+ return @loaded[tag_name] if @loaded[tag_name]
44
+ Measurometer.instrument("emjay.registry.load_component.#{tag_name}") do
45
+ require path
46
+ const_name = File.basename(path, ".rb").split("_").map(&:capitalize).join
47
+ Emjay::Components.const_get(const_name)
48
+ end
49
+ end
50
+ end
51
+
52
+ # The renderer treats `Registry.components` as a hash keyed by tag name.
53
+ # This proxy resolves each lookup through `Registry.find` so components
54
+ # only load when actually referenced.
55
+ class LazyComponents
56
+ include Enumerable
57
+
58
+ def [](tag_name)
59
+ component = Registry.find(tag_name)
60
+ # We only reach here on render paths (never during parsing, which uses
61
+ # Registry.find directly and tolerates nil). If a tag looks like MJML
62
+ # but resolves to nothing, it's almost certainly a typo like
63
+ # `<mj-nonesuch>` — surface it instead of silently rendering blank.
64
+ if component.nil? && tag_name.to_s.start_with?("mj-")
65
+ raise UnknownComponent, "Unknown MJML component #{tag_name.inspect}"
66
+ end
67
+ component
68
+ end
69
+
70
+ def each
71
+ PATHS.each_key { |tag| yield tag, Registry.find(tag) }
72
+ end
73
+
74
+ def keys
75
+ PATHS.keys
76
+ end
77
+
78
+ def values
79
+ PATHS.each_key.map { |tag| Registry.find(tag) }
80
+ end
17
81
  end
18
82
  end
19
83
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri"
4
+ require "measurometer"
4
5
  require_relative "global_data"
5
6
  require_relative "registry"
6
7
  require_relative "skeleton"
@@ -13,15 +14,21 @@ module Emjay
13
14
  VOID_ELEMENTS_RE = /(<(?:br|hr|img|input|meta|link|area|base|col|embed|param|source|track|wbr)(?:\s[^>]*)?)>/i
14
15
 
15
16
  def self.call(mjml_string, options = {})
17
+ Measurometer.instrument("emjay.render") { render(mjml_string, options) }
18
+ end
19
+
20
+ def self.render(mjml_string, options = {})
16
21
  # 1. Pre-process and parse MJML string with Nokogiri::XML.
17
22
  # Two fixups are needed to bridge MJML (which embeds HTML content) and XML:
18
23
  # a) Convert HTML void elements to self-closing (e.g. <br> → <br/>)
19
24
  # so XML parsing doesn't treat them as unclosed tags
20
25
  # b) Escape bare < characters from template syntax in mj-raw
21
26
  # (e.g. { if item < 5 }) so they don't break XML parsing
22
- preprocessed = mjml_string.gsub(VOID_ELEMENTS_RE, '\1/>')
23
- preprocessed = preprocessed.gsub(/<(?![a-zA-Z\/!?])/, RAW_LT_PLACEHOLDER)
24
- doc = Nokogiri::XML(preprocessed)
27
+ doc = Measurometer.instrument("emjay.parse") do
28
+ preprocessed = mjml_string.gsub(VOID_ELEMENTS_RE, '\1/>')
29
+ preprocessed = preprocessed.gsub(/<(?![a-zA-Z\/!?])/, RAW_LT_PLACEHOLDER)
30
+ Nokogiri::XML(preprocessed)
31
+ end
25
32
  mjml_root = doc.at_xpath("//mjml") || doc.root
26
33
 
27
34
  # 2. Build GlobalData
@@ -79,9 +86,11 @@ module Emjay
79
86
 
80
87
  # Process head
81
88
  if mj_head_el
82
- head_node = nokogiri_to_hash(mj_head_el)
83
- head_result = processing.call(head_node, head_helpers)
84
- global_data.head_raw = head_result if head_result.is_a?(Array)
89
+ Measurometer.instrument("emjay.process_head") do
90
+ head_node = Measurometer.instrument("emjay.to_hash.head") { nokogiri_to_hash(mj_head_el) }
91
+ head_result = processing.call(head_node, head_helpers)
92
+ global_data.head_raw = head_result if head_result.is_a?(Array)
93
+ end
85
94
  end
86
95
 
87
96
  # Body helpers context
@@ -113,15 +122,17 @@ module Emjay
113
122
  # Process body
114
123
  content = nil
115
124
  if mj_body_el
116
- body_node = nokogiri_to_hash(mj_body_el)
117
- body_node = apply_attributes.call(body_node)
118
- content = processing.call(body_node, body_helpers)
125
+ content = Measurometer.instrument("emjay.process_body") do
126
+ body_node = Measurometer.instrument("emjay.to_hash.body") { nokogiri_to_hash(mj_body_el) }
127
+ body_node = Measurometer.instrument("emjay.apply_attributes") { apply_attributes.call(body_node) }
128
+ processing.call(body_node, body_helpers)
129
+ end
119
130
  end
120
131
 
121
132
  raise "Malformed MJML. Check that your structure is correct and enclosed in <mjml> tags." unless content
122
133
 
123
134
  # Minify outlook conditionals
124
- content = MinifyOutlookConditionals.call(content)
135
+ content = Measurometer.instrument("emjay.minify_outlook_conditionals") { MinifyOutlookConditionals.call(content) }
125
136
 
126
137
  # Handle mj-raw outside body (before-doctype)
127
138
  mjml_root&.xpath("mj-raw")&.each do |raw_el|
@@ -136,44 +147,50 @@ module Emjay
136
147
  # syntax (mj-raw) with placeholders so XML parsing succeeds. Use custom
137
148
  # serializer so text nodes (including > in templates) pass through raw.
138
149
  unless global_data.html_attributes.empty?
139
- escaped = content.gsub(/<(?![a-zA-Z\/!?])/, RAW_LT_PLACEHOLDER)
140
- content_doc = Nokogiri::XML.fragment(escaped)
141
- global_data.html_attributes.each do |selector, data|
142
- content_doc.css(selector).each do |node|
143
- data.each do |attr_name, value|
144
- node[attr_name] = value || ""
150
+ content = Measurometer.instrument("emjay.html_attributes") do
151
+ escaped = content.gsub(/<(?![a-zA-Z\/!?])/, RAW_LT_PLACEHOLDER)
152
+ content_doc = Nokogiri::XML.fragment(escaped)
153
+ global_data.html_attributes.each do |selector, data|
154
+ content_doc.css(selector).each do |node|
155
+ data.each do |attr_name, value|
156
+ node[attr_name] = value || ""
157
+ end
145
158
  end
146
159
  end
160
+ serialize_fragment(content_doc).gsub(RAW_LT_PLACEHOLDER, "<")
147
161
  end
148
- content = serialize_fragment(content_doc).gsub(RAW_LT_PLACEHOLDER, "<")
149
162
  end
150
163
 
151
164
  # Wrap in skeleton
152
- content = Skeleton.call(
153
- before_doctype: global_data.before_doctype,
154
- breakpoint: global_data.breakpoint,
155
- content: content,
156
- fonts: global_data.fonts,
157
- media_queries: global_data.media_queries,
158
- head_style: global_data.head_style,
159
- components_head_style: global_data.components_head_style,
160
- head_raw: global_data.head_raw.is_a?(Array) ? global_data.head_raw : [],
161
- title: global_data.title,
162
- style: global_data.style,
163
- force_owa_desktop: global_data.force_owa_desktop,
164
- printer_support: options[:printer_support] || false,
165
- inline_style: global_data.inline_style,
166
- lang: global_data.lang,
167
- dir: global_data.dir
168
- )
165
+ content = Measurometer.instrument("emjay.skeleton") do
166
+ Skeleton.call(
167
+ before_doctype: global_data.before_doctype,
168
+ breakpoint: global_data.breakpoint,
169
+ content: content,
170
+ fonts: global_data.fonts,
171
+ media_queries: global_data.media_queries,
172
+ head_style: global_data.head_style,
173
+ components_head_style: global_data.components_head_style,
174
+ head_raw: global_data.head_raw.is_a?(Array) ? global_data.head_raw : [],
175
+ title: global_data.title,
176
+ style: global_data.style,
177
+ force_owa_desktop: global_data.force_owa_desktop,
178
+ printer_support: options[:printer_support] || false,
179
+ inline_style: global_data.inline_style,
180
+ lang: global_data.lang,
181
+ dir: global_data.dir
182
+ )
183
+ end
169
184
 
170
185
  # CSS inlining for <mj-style inline="inline">
171
186
  if global_data.inline_style.any?
172
- content = inline_css(content, global_data.inline_style.join("\n"))
187
+ content = Measurometer.instrument("emjay.inline_css") do
188
+ inline_css(content, global_data.inline_style.join("\n"))
189
+ end
173
190
  end
174
191
 
175
192
  # Merge outlook conditionals
176
- MergeOutlookConditionals.call(content)
193
+ Measurometer.instrument("emjay.merge_outlook_conditionals") { MergeOutlookConditionals.call(content) }
177
194
  end
178
195
 
179
196
  # Converts a Nokogiri element to a hash matching the JS parsed format
@@ -185,9 +202,11 @@ module Emjay
185
202
 
186
203
  children = element.children.select(&:element?).map { |child| nokogiri_to_hash(child) }
187
204
 
188
- # For ending-tag components, content is the inner HTML
205
+ # For ending-tag components, content is the inner HTML. Only MJML tags
206
+ # can be components — plain HTML (e.g. <a>, <b> nested in mj-text) never
207
+ # is, so don't waste a Registry lookup on it.
189
208
  tag_name = element.name
190
- component_class = Registry.find(tag_name)
209
+ component_class = tag_name.start_with?("mj-") ? Registry.find(tag_name) : nil
191
210
  content = if component_class&.ending_tag?
192
211
  inner = element.inner_html.strip
193
212
  # Restore escaped bare < characters (from template syntax in mj-raw etc.)
@@ -257,16 +276,18 @@ module Emjay
257
276
  def self.inline_css(html, extra_css)
258
277
  require "premailer"
259
278
 
260
- premailer = Premailer.new(
261
- html,
262
- with_html_string: true,
263
- css_string: extra_css,
264
- include_style_tags: false,
265
- include_link_tags: false,
266
- preserve_styles: true,
267
- adapter: :nokogiri
268
- )
269
- premailer.to_inline_css
279
+ premailer = Measurometer.instrument("emjay.inline_css.premailer_parse") do
280
+ Premailer.new(
281
+ html,
282
+ with_html_string: true,
283
+ css_string: extra_css,
284
+ include_style_tags: false,
285
+ include_link_tags: false,
286
+ preserve_styles: true,
287
+ adapter: :nokogiri
288
+ )
289
+ end
290
+ Measurometer.instrument("emjay.inline_css.to_inline_css") { premailer.to_inline_css }
270
291
  end
271
292
 
272
293
  RAW_LT_PLACEHOLDER = "___MJML_RAW_LT___"
@@ -299,7 +320,7 @@ module Emjay
299
320
  end
300
321
  end
301
322
 
302
- private_class_method :nokogiri_to_hash, :apply_attributes_fn, :inline_css,
323
+ private_class_method :render, :nokogiri_to_hash, :apply_attributes_fn, :inline_css,
303
324
  :serialize_fragment, :serialize_node
304
325
  end
305
326
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
3
4
  require_relative "helpers/fonts"
4
5
  require_relative "helpers/media_queries"
5
6
  require_relative "helpers/styles"
@@ -26,12 +27,20 @@ module Emjay
26
27
 
27
28
  before_doctype_str = before_doctype.empty? ? "" : "#{before_doctype}\n"
28
29
 
29
- fonts_tags = Fonts.build_tags(content, inline_style, fonts)
30
- media_query_tags = MediaQueries.build_tags(breakpoint, media_queries,
31
- force_owa_desktop: force_owa_desktop,
32
- printer_support: printer_support)
33
- component_styles = Styles.build_from_components(breakpoint, components_head_style, head_style)
34
- tag_styles = Styles.build_from_tags(breakpoint, style)
30
+ fonts_tags = Measurometer.instrument("emjay.skeleton.fonts") do
31
+ Fonts.build_tags(content, inline_style, fonts)
32
+ end
33
+ media_query_tags = Measurometer.instrument("emjay.skeleton.media_queries") do
34
+ MediaQueries.build_tags(breakpoint, media_queries,
35
+ force_owa_desktop: force_owa_desktop,
36
+ printer_support: printer_support)
37
+ end
38
+ component_styles = Measurometer.instrument("emjay.skeleton.component_styles") do
39
+ Styles.build_from_components(breakpoint, components_head_style, head_style)
40
+ end
41
+ tag_styles = Measurometer.instrument("emjay.skeleton.tag_styles") do
42
+ Styles.build_from_tags(breakpoint, style)
43
+ end
35
44
  raw = head_raw.compact.join("\n")
36
45
 
37
46
  <<~HTML
data/lib/emjay/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emjay
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
data/lib/emjay.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
4
+
3
5
  require_relative "emjay/version"
4
6
  require_relative "emjay/registry"
5
7
  require_relative "emjay/component"
@@ -22,40 +24,7 @@ require_relative "emjay/helpers/styles"
22
24
  require_relative "emjay/helpers/make_lower_breakpoint"
23
25
  require_relative "emjay/helpers/gen_random_hex_string"
24
26
 
25
- # Head components
26
- require_relative "emjay/components/head/mj_head"
27
- require_relative "emjay/components/head/mj_attributes"
28
- require_relative "emjay/components/head/mj_style"
29
- require_relative "emjay/components/head/mj_font"
30
- require_relative "emjay/components/head/mj_title"
31
- require_relative "emjay/components/head/mj_preview"
32
- require_relative "emjay/components/head/mj_breakpoint"
33
- require_relative "emjay/components/head/mj_html_attributes"
34
-
35
- # Body components
36
- require_relative "emjay/components/body/mj_body"
37
- require_relative "emjay/components/body/mj_section"
38
- require_relative "emjay/components/body/mj_column"
39
- require_relative "emjay/components/body/mj_text"
40
- require_relative "emjay/components/body/mj_wrapper"
41
- require_relative "emjay/components/body/mj_group"
42
- require_relative "emjay/components/body/mj_image"
43
- require_relative "emjay/components/body/mj_button"
44
- require_relative "emjay/components/body/mj_divider"
45
- require_relative "emjay/components/body/mj_spacer"
46
- require_relative "emjay/components/body/mj_table"
47
- require_relative "emjay/components/body/mj_raw"
48
- require_relative "emjay/components/body/mj_hero"
49
- require_relative "emjay/components/body/mj_social"
50
- require_relative "emjay/components/body/mj_social_element"
51
- require_relative "emjay/components/body/mj_navbar"
52
- require_relative "emjay/components/body/mj_navbar_link"
53
- require_relative "emjay/components/body/mj_accordion"
54
- require_relative "emjay/components/body/mj_accordion_element"
55
- require_relative "emjay/components/body/mj_accordion_title"
56
- require_relative "emjay/components/body/mj_accordion_text"
57
- require_relative "emjay/components/body/mj_carousel"
58
- require_relative "emjay/components/body/mj_carousel_image"
27
+ # Components are autoloaded on demand via Emjay::Registry — see registry.rb.
59
28
 
60
29
  module Emjay
61
30
  def self.to_html(mjml_string, options = {})
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emjay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-08-19 00:00:00.000000000 Z
10
+ date: 2026-09-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: nokogiri
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: measurometer
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
40
54
  description: Converts MJML email markup to responsive HTML — no Node.js, no native
41
55
  extensions, no shelling out.
42
56
  email: