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 +4 -4
- data/lib/emjay/rails/mail_interceptor.rb +8 -2
- data/lib/emjay/rails/template_handler.rb +6 -2
- data/lib/emjay/registry.rb +7 -3
- data/lib/emjay/renderer.rb +66 -47
- data/lib/emjay/skeleton.rb +15 -6
- data/lib/emjay/version.rb +1 -1
- data/lib/emjay.rb +2 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12fae46a93b64e1980adc9085d568a1d337f13d9d8562d94937b7f24f861f0cb
|
|
4
|
+
data.tar.gz: f0db6163d0a00288d62c2b2b2820185f0bbfb7853ffc4b8c726c117d37e3d7e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
12
|
-
|
|
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/registry.rb
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
data/lib/emjay/renderer.rb
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 =
|
|
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 =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
data/lib/emjay/skeleton.rb
CHANGED
|
@@ -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 =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
data/lib/emjay.rb
CHANGED
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.
|
|
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-
|
|
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:
|