emjay 0.2.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44b1f5f2d6b785a000c044f8910beb812ab3b851fa5aa9a9950bdafc14842cd3
4
- data.tar.gz: ff1959f81bd0468820a03c87a64396a63a14fcc4b90cc11465021519e704bd70
3
+ metadata.gz: 12fae46a93b64e1980adc9085d568a1d337f13d9d8562d94937b7f24f861f0cb
4
+ data.tar.gz: f0db6163d0a00288d62c2b2b2820185f0bbfb7853ffc4b8c726c117d37e3d7e5
5
5
  SHA512:
6
- metadata.gz: 2b137f248b18a2d4128e58626158b70e607d74e600f2096df7c0d920cfd9b4411c04912dc1e5a8e244aec4e201718edc62396c6ecb65bf53185c28e8e56204d6
7
- data.tar.gz: ba4fceec3711cb71600ecc8eaee52865a873a34aabbf952ac40487903024bb266062689b95cf12b285fab4072eb6b52dcc4fc38156b7eb12b60ebec154353922
6
+ metadata.gz: 754f37c1091076bba65cff5de501bce2e8f7b138f872e870c3cca2d4819c45e657acd0b4fe0c449503b7a1ed60927303665ed1a6316db37c57e4c8ed4ba2cff7
7
+ data.tar.gz: fda91c57c46af356ee1184c8408f32c1c4caffe1db3fd2f617490ec721929dab2b9921c16f510db32ce75ac68ca61efe497114b9ab28984c8421e3d99a577a7d
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "measurometer"
4
+
3
5
  module Emjay
4
6
  # Lazy autoloader for MJML components. Scans `lib/emjay/components/**/*.rb`
5
7
  # once at load time, then derives both the MJML tag name and the Ruby class
@@ -39,9 +41,11 @@ module Emjay
39
41
  path = PATHS[tag_name] or return nil
40
42
  @mutex.synchronize do
41
43
  return @loaded[tag_name] if @loaded[tag_name]
42
- require path
43
- const_name = File.basename(path, ".rb").split("_").map(&:capitalize).join
44
- Emjay::Components.const_get(const_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
45
49
  end
46
50
  end
47
51
 
@@ -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
@@ -259,16 +276,18 @@ module Emjay
259
276
  def self.inline_css(html, extra_css)
260
277
  require "premailer"
261
278
 
262
- premailer = Premailer.new(
263
- html,
264
- with_html_string: true,
265
- css_string: extra_css,
266
- include_style_tags: false,
267
- include_link_tags: false,
268
- preserve_styles: true,
269
- adapter: :nokogiri
270
- )
271
- 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 }
272
291
  end
273
292
 
274
293
  RAW_LT_PLACEHOLDER = "___MJML_RAW_LT___"
@@ -301,7 +320,7 @@ module Emjay
301
320
  end
302
321
  end
303
322
 
304
- private_class_method :nokogiri_to_hash, :apply_attributes_fn, :inline_css,
323
+ private_class_method :render, :nokogiri_to_hash, :apply_attributes_fn, :inline_css,
305
324
  :serialize_fragment, :serialize_node
306
325
  end
307
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.1"
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"
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.1
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-27 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: