activemail 1.2.3 → 2.1.0
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/CHANGELOG.md +107 -2
- data/README.md +141 -56
- data/UPGRADING.md +185 -0
- data/app/assets/stylesheets/activemail/_components.scss +33 -42
- data/app/assets/stylesheets/activemail/_dark.scss +44 -24
- data/app/assets/stylesheets/activemail/_grid.scss +40 -14
- data/app/assets/stylesheets/activemail/_settings.scss +23 -10
- data/app/assets/stylesheets/activemail/_utilities.scss +3 -1
- data/app/assets/stylesheets/activemail/activemail.scss +6 -5
- data/app/helpers/active_mail/styles_helper.rb +14 -15
- data/app/views/layouts/activemail/_document_head.html.erb +18 -0
- data/app/views/layouts/activemail/mailer.html.inky-erb +1 -22
- data/lib/activemail/asset_pipeline/base.rb +47 -0
- data/lib/activemail/asset_pipeline/null.rb +19 -0
- data/lib/activemail/asset_pipeline/propshaft.rb +19 -0
- data/lib/activemail/asset_pipeline/sprockets.rb +39 -0
- data/lib/activemail/asset_pipeline.rb +35 -0
- data/lib/activemail/components/base.rb +35 -39
- data/lib/activemail/components/block_grid.rb +11 -4
- data/lib/activemail/components/button.rb +5 -28
- data/lib/activemail/components/button_renderer.rb +69 -0
- data/lib/activemail/components/callout.rb +4 -5
- data/lib/activemail/components/center.rb +10 -10
- data/lib/activemail/components/columns.rb +58 -15
- data/lib/activemail/components/container.rb +13 -6
- data/lib/activemail/components/cta.rb +9 -33
- data/lib/activemail/components/h_line.rb +3 -5
- data/lib/activemail/components/info_box.rb +6 -12
- data/lib/activemail/components/menu.rb +3 -4
- data/lib/activemail/components/menu_item.rb +10 -3
- data/lib/activemail/components/row.rb +3 -4
- data/lib/activemail/components/spacer.rb +16 -12
- data/lib/activemail/components/table_component.rb +23 -0
- data/lib/activemail/components/wrapper.rb +10 -4
- data/lib/activemail/configuration.rb +43 -57
- data/lib/activemail/errors.rb +15 -0
- data/lib/activemail/inliner/base.rb +2 -10
- data/lib/activemail/inliner/null.rb +0 -5
- data/lib/activemail/inliner/premailer/document.rb +100 -0
- data/lib/activemail/inliner/premailer.rb +3 -2
- data/lib/activemail/inliner/roadie.rb +1 -1
- data/lib/activemail/inliner.rb +33 -0
- data/lib/activemail/parse_error_reporter.rb +5 -4
- data/lib/activemail/pipeline.rb +26 -0
- data/lib/activemail/quality/builtin_message_rules.rb +58 -0
- data/lib/activemail/quality/builtin_rules.rb +165 -0
- data/lib/activemail/quality/configuration.rb +103 -8
- data/lib/activemail/quality/guard.rb +61 -83
- data/lib/activemail/quality/minitest.rb +56 -27
- data/lib/activemail/quality/output_dir.rb +52 -0
- data/lib/activemail/quality/output_filter.rb +24 -0
- data/lib/activemail/quality/preview_renderer.rb +46 -33
- data/lib/activemail/quality/render_all.rb +95 -39
- data/lib/activemail/quality/rspec.rb +14 -13
- data/lib/activemail/quality/rule.rb +47 -0
- data/lib/activemail/quality/rule_exemptions.rb +49 -0
- data/lib/activemail/rails/engine.rb +13 -13
- data/lib/activemail/rails/rendering.rb +51 -0
- data/lib/activemail/rails/template_handler.rb +3 -1
- data/lib/activemail/tokens/button_style.rb +1 -2
- data/lib/activemail/tokens/defaults.rb +78 -0
- data/lib/activemail/tokens/scss_serializer.rb +4 -4
- data/lib/activemail/tokens.rb +19 -58
- data/lib/activemail/version.rb +1 -1
- data/lib/activemail.rb +89 -61
- data/lib/generators/activemail/templates/component.rb.tt +2 -3
- data/lib/generators/activemail/templates/initializer.rb +0 -3
- data/lib/generators/activemail/templates/mailer_layout.html.inky-erb +3 -19
- data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +3 -11
- data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +3 -10
- data/lib/tasks/activemail.rake +4 -5
- metadata +22 -4
- data/lib/activemail/components/inky.rb +0 -18
- data/lib/activemail/inliner/interceptor.rb +0 -51
- data/lib/activemail/rails/compiled_stylesheet.rb +0 -62
data/lib/activemail/tokens.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'tokens/defaults'
|
|
5
6
|
require_relative 'tokens/button_style'
|
|
6
7
|
require_relative 'tokens/scss_serializer'
|
|
7
8
|
|
|
@@ -9,61 +10,6 @@ module ActiveMail
|
|
|
9
10
|
class Tokens
|
|
10
11
|
extend T::Sig
|
|
11
12
|
|
|
12
|
-
TokenMap = T.type_alias { T::Hash[Symbol, String] }
|
|
13
|
-
|
|
14
|
-
DEFAULT_COLORS = T.let(
|
|
15
|
-
{
|
|
16
|
-
primary: '#2a9d8f',
|
|
17
|
-
secondary: '#264653',
|
|
18
|
-
text: '#1a1a1a',
|
|
19
|
-
background: '#ffffff',
|
|
20
|
-
muted: '#6b7280',
|
|
21
|
-
border: '#e5e7eb',
|
|
22
|
-
button_text: '#ffffff'
|
|
23
|
-
}.freeze,
|
|
24
|
-
TokenMap
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
SYSTEM_FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
|
|
28
|
-
|
|
29
|
-
DEFAULT_FONTS = T.let(
|
|
30
|
-
{
|
|
31
|
-
body: SYSTEM_FONT_STACK,
|
|
32
|
-
heading: SYSTEM_FONT_STACK
|
|
33
|
-
}.freeze,
|
|
34
|
-
TokenMap
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
DEFAULT_SPACINGS = T.let(
|
|
38
|
-
{
|
|
39
|
-
xs: '4px',
|
|
40
|
-
sm: '8px',
|
|
41
|
-
md: '16px',
|
|
42
|
-
lg: '24px',
|
|
43
|
-
xl: '40px'
|
|
44
|
-
}.freeze,
|
|
45
|
-
TokenMap
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
DEFAULT_RADII = T.let(
|
|
49
|
-
{
|
|
50
|
-
button: '4px',
|
|
51
|
-
box: '4px'
|
|
52
|
-
}.freeze,
|
|
53
|
-
TokenMap
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Open/Closed: adding a group here is the only change needed to wire it everywhere.
|
|
57
|
-
GROUPS = T.let(
|
|
58
|
-
{
|
|
59
|
-
color: DEFAULT_COLORS,
|
|
60
|
-
font: DEFAULT_FONTS,
|
|
61
|
-
spacing: DEFAULT_SPACINGS,
|
|
62
|
-
radius: DEFAULT_RADII
|
|
63
|
-
}.freeze,
|
|
64
|
-
T::Hash[Symbol, TokenMap]
|
|
65
|
-
)
|
|
66
|
-
|
|
67
13
|
sig { void }
|
|
68
14
|
def initialize
|
|
69
15
|
@stores = T.let(GROUPS.transform_values(&:dup), T::Hash[Symbol, TokenMap])
|
|
@@ -123,6 +69,13 @@ module ActiveMail
|
|
|
123
69
|
end
|
|
124
70
|
end
|
|
125
71
|
|
|
72
|
+
# Every token with derived colors filled in; nil marks a derived token left unset.
|
|
73
|
+
sig { returns(T::Hash[Symbol, T::Hash[Symbol, T.nilable(String)]]) }
|
|
74
|
+
def resolved
|
|
75
|
+
colors = DERIVED_COLORS.keys.to_h { |name| [name, color(name)] }
|
|
76
|
+
@stores.merge(color: store_for(:color).merge(colors))
|
|
77
|
+
end
|
|
78
|
+
|
|
126
79
|
sig { params(variant: T.any(String, Symbol)).returns(ButtonStyle) }
|
|
127
80
|
def button_style(variant)
|
|
128
81
|
ButtonStyle.from(self, variant)
|
|
@@ -130,7 +83,7 @@ module ActiveMail
|
|
|
130
83
|
|
|
131
84
|
sig { returns(String) }
|
|
132
85
|
def to_scss
|
|
133
|
-
ScssSerializer.call(
|
|
86
|
+
ScssSerializer.call(resolved)
|
|
134
87
|
end
|
|
135
88
|
|
|
136
89
|
private
|
|
@@ -142,14 +95,22 @@ module ActiveMail
|
|
|
142
95
|
|
|
143
96
|
sig { params(group: Symbol, name: T.any(String, Symbol)).returns(String) }
|
|
144
97
|
def fetch!(group, name)
|
|
145
|
-
|
|
98
|
+
access(group, name, nil) || raise(KeyError, "unknown or unset #{group} token #{name.inspect}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(group: Symbol, key: Symbol).returns(T.nilable(String)) }
|
|
102
|
+
def derived(group, key)
|
|
103
|
+
return unless group == :color
|
|
104
|
+
|
|
105
|
+
source = DERIVED_COLORS[key]
|
|
106
|
+
source && store_for(:color)[source]
|
|
146
107
|
end
|
|
147
108
|
|
|
148
109
|
sig { params(group: Symbol, name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
149
110
|
def access(group, name, value)
|
|
150
111
|
store = store_for(group)
|
|
151
112
|
key = name.to_sym
|
|
152
|
-
return store
|
|
113
|
+
return store.fetch(key) { derived(group, key) } if value.nil?
|
|
153
114
|
# Emitted verbatim into SCSS — reject blanks that would yield broken CSS.
|
|
154
115
|
raise ArgumentError, "token #{key} value must not be blank" if value.strip.empty?
|
|
155
116
|
|
data/lib/activemail/version.rb
CHANGED
data/lib/activemail.rb
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'nokogiri'
|
|
5
|
+
require 'securerandom'
|
|
5
6
|
require 'sorbet-runtime'
|
|
6
7
|
|
|
7
8
|
require_relative 'activemail/version'
|
|
9
|
+
require_relative 'activemail/errors'
|
|
8
10
|
require_relative 'activemail/components/base'
|
|
11
|
+
require_relative 'activemail/components/button_renderer'
|
|
9
12
|
require_relative 'activemail/components/button'
|
|
10
13
|
require_relative 'activemail/components/row'
|
|
11
14
|
require_relative 'activemail/components/columns'
|
|
12
15
|
require_relative 'activemail/components/container'
|
|
13
|
-
require_relative 'activemail/components/inky'
|
|
14
16
|
require_relative 'activemail/components/block_grid'
|
|
15
17
|
require_relative 'activemail/components/menu'
|
|
16
18
|
require_relative 'activemail/components/menu_item'
|
|
@@ -22,19 +24,32 @@ require_relative 'activemail/components/wrapper'
|
|
|
22
24
|
require_relative 'activemail/components/cta'
|
|
23
25
|
require_relative 'activemail/components/info_box'
|
|
24
26
|
require_relative 'activemail/configuration'
|
|
25
|
-
require_relative 'activemail/
|
|
27
|
+
require_relative 'activemail/pipeline'
|
|
26
28
|
require_relative 'activemail/parse_error_reporter'
|
|
27
29
|
|
|
28
30
|
module ActiveMail
|
|
29
|
-
class ParseError < StandardError; end
|
|
30
|
-
|
|
31
31
|
class Core
|
|
32
32
|
extend T::Sig
|
|
33
33
|
|
|
34
34
|
# Nokogiri cannot parse a bare <th> outside a <tr>; components that emit
|
|
35
35
|
# <th> use this placeholder, swapped back at the end.
|
|
36
36
|
INTERIM_TH_TAG = 'activemail-interim-th'
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
# Comment placeholder handed to components as `inner`; the transformed children
|
|
39
|
+
# are moved there without being re-serialized.
|
|
40
|
+
SLOT_TEXT = 'activemail-slot'
|
|
41
|
+
SLOT = T.let("<!--#{SLOT_TEXT}-->".freeze, String)
|
|
42
|
+
|
|
43
|
+
# The component markup repeats the id of the node it replaces, which libxml2 still has registered.
|
|
44
|
+
LIBXML_ID_REDEFINED_CODE = 513
|
|
45
|
+
|
|
46
|
+
RAW_REGEX = T.let(%r{< *raw *>([\s\S]*?)</ *raw *>}i, Regexp)
|
|
47
|
+
|
|
48
|
+
# One pass over the serialized output; <style>/<script> bodies are matched first so they are left alone.
|
|
49
|
+
POSTPROCESS_REGEX = T.let(
|
|
50
|
+
%r{(?<verbatim><(?<tag>style|script)\b[\s\S]*?</\k<tag>\s*>)|(?<=<|</)#{Regexp.escape(INTERIM_TH_TAG)}(?![\w-])|\u00A0}i,
|
|
51
|
+
Regexp
|
|
52
|
+
)
|
|
38
53
|
|
|
39
54
|
DEFAULT_COMPONENTS = T.let(
|
|
40
55
|
{
|
|
@@ -44,7 +59,6 @@ module ActiveMail
|
|
|
44
59
|
'row' => ActiveMail::Components::Row,
|
|
45
60
|
'columns' => ActiveMail::Components::Columns,
|
|
46
61
|
'container' => ActiveMail::Components::Container,
|
|
47
|
-
'inky' => ActiveMail::Components::Inky,
|
|
48
62
|
'block-grid' => ActiveMail::Components::BlockGrid,
|
|
49
63
|
'menu' => ActiveMail::Components::Menu,
|
|
50
64
|
'item' => ActiveMail::Components::MenuItem,
|
|
@@ -60,85 +74,99 @@ module ActiveMail
|
|
|
60
74
|
sig { returns(Integer) }
|
|
61
75
|
attr_reader :column_count, :container_width
|
|
62
76
|
|
|
63
|
-
sig { returns(
|
|
64
|
-
attr_reader :
|
|
77
|
+
sig { returns(ActiveMail::Configuration) }
|
|
78
|
+
attr_reader :config
|
|
65
79
|
|
|
66
|
-
sig
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
sig do
|
|
81
|
+
params(
|
|
82
|
+
config: ActiveMail::Configuration,
|
|
83
|
+
components: T.untyped,
|
|
84
|
+
column_count: T.untyped,
|
|
85
|
+
container_width: T.untyped
|
|
86
|
+
).void
|
|
87
|
+
end
|
|
88
|
+
def initialize(config: ::ActiveMail.configuration, components: nil, column_count: nil, container_width: nil)
|
|
89
|
+
@config = T.let(config, ActiveMail::Configuration)
|
|
90
|
+
@component_instances = T.let(build_components(config, components), T::Hash[String, ActiveMail::Components::Base])
|
|
91
|
+
@column_count = T.let(ActiveMail.positive_integer!(:column_count, column_count || config.column_count), Integer)
|
|
92
|
+
@container_width = T.let(ActiveMail.positive_integer!(:container_width, container_width || config.container_width), Integer)
|
|
93
|
+
@error_reporter = T.let(ParseErrorReporter.new(@component_instances.keys + [INTERIM_TH_TAG], config), ParseErrorReporter)
|
|
72
94
|
end
|
|
73
95
|
|
|
74
96
|
# Object, not String: ActionView::OutputBuffer is no longer a String since Rails 7.1.
|
|
97
|
+
# Holds no per-call state: the instance is shared across threads.
|
|
75
98
|
sig { params(html_string: Object).returns(String) }
|
|
76
99
|
def transpile(html_string)
|
|
77
|
-
|
|
100
|
+
marker = "activemail-raw-#{SecureRandom.hex(8)}-"
|
|
101
|
+
raws = {}
|
|
102
|
+
str = extract_raws(normalize_input(html_string), marker, raws)
|
|
78
103
|
parse_cmd = ::ActiveMail.full_document?(str) ? :parse : :fragment
|
|
79
104
|
html = Nokogiri::HTML.public_send(parse_cmd, str)
|
|
80
|
-
|
|
105
|
+
@error_reporter.call(html.errors)
|
|
81
106
|
transform_doc(html)
|
|
82
|
-
|
|
83
|
-
string = string.gsub(INTERIM_TH_TAG_REGEX, 'th')
|
|
84
|
-
# Needle is a literal U+00A0 (Nokogiri decodes the nbsp entity to one); re-encode
|
|
85
|
-
# it to the entity for email clients that mishandle raw NBSP bytes.
|
|
86
|
-
string = string.gsub(' ', ' ')
|
|
87
|
-
re_inject_raws(string, raws)
|
|
107
|
+
re_inject_raws(postprocess(html.to_html), marker, raws)
|
|
88
108
|
end
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
def transform_doc(elem)
|
|
92
|
-
if elem.respond_to?(:children)
|
|
93
|
-
elem.children.each { |child| transform_doc(child) }
|
|
94
|
-
markup = component_factory(elem)
|
|
95
|
-
elem.replace(markup) if markup
|
|
96
|
-
end
|
|
97
|
-
elem
|
|
98
|
-
end
|
|
110
|
+
private
|
|
99
111
|
|
|
100
|
-
sig {
|
|
101
|
-
|
|
112
|
+
sig { returns(T::Hash[String, ActiveMail::Components::Base]) }
|
|
113
|
+
attr_reader :component_instances
|
|
114
|
+
|
|
115
|
+
sig { params(node: Nokogiri::XML::Node).void }
|
|
116
|
+
def transform_doc(node)
|
|
117
|
+
node.children.each { |child| transform_doc(child) }
|
|
102
118
|
component = component_instances[node.name]
|
|
103
|
-
|
|
119
|
+
replace_with_component(node, component, component.transform(node, SLOT)) if component
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Only the component's own markup is parsed; the node's children are moved into
|
|
123
|
+
# its slot, never re-serialized.
|
|
124
|
+
sig { params(node: Nokogiri::XML::Node, component: ActiveMail::Components::Base, markup: String).void }
|
|
125
|
+
def replace_with_component(node, component, markup)
|
|
126
|
+
fragment = Nokogiri::HTML::DocumentFragment.new(node.document, markup, node)
|
|
127
|
+
@error_reporter.call(fragment.errors.reject { |error| error.code == LIBXML_ID_REDEFINED_CODE })
|
|
128
|
+
slots = fragment.xpath('.//comment()').select { |comment| comment.content == SLOT_TEXT }
|
|
129
|
+
ensure_slot_usage!(node, component, slots.size)
|
|
130
|
+
slots.first&.replace(node.children)
|
|
131
|
+
node.replace(fragment)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(node: Nokogiri::XML::Node, component: ActiveMail::Components::Base, count: Integer).void }
|
|
135
|
+
def ensure_slot_usage!(node, component, count)
|
|
136
|
+
return if count == 1 || (count.zero? && node.children.all? { |child| child.text? && child.content.strip.empty? })
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
component.transform(node, inner)
|
|
138
|
+
raise ActiveMail::Error,
|
|
139
|
+
"<#{node.name}> (#{component.class}) must output `inner` exactly once, found #{count}: its children would be lost or duplicated"
|
|
108
140
|
end
|
|
109
141
|
|
|
110
|
-
sig { params(string: String
|
|
111
|
-
def extract_raws(string)
|
|
112
|
-
raws = []
|
|
113
|
-
i = 0
|
|
142
|
+
sig { params(string: String, marker: String, raws: T::Hash[String, String]).returns(String) }
|
|
143
|
+
def extract_raws(string, marker, raws)
|
|
114
144
|
# Only the tags + content, across lines: surrounding whitespace is left in
|
|
115
145
|
# place (true pass-through; eating adjacent newlines corrupts <pre>/inline text).
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
str = str.sub(regex, "###RAW#{i}###")
|
|
121
|
-
i += 1
|
|
146
|
+
string.gsub(RAW_REGEX) do
|
|
147
|
+
placeholder = "#{marker}#{raws.size}~"
|
|
148
|
+
raws[placeholder] = T.must(Regexp.last_match(1))
|
|
149
|
+
placeholder
|
|
122
150
|
end
|
|
123
|
-
[raws, str]
|
|
124
151
|
end
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
# U+00A0 is what Nokogiri decodes the nbsp entity to; re-encoded for email clients that mishandle raw NBSP bytes.
|
|
154
|
+
sig { params(string: String).returns(String) }
|
|
155
|
+
def postprocess(string)
|
|
156
|
+
string.gsub(POSTPROCESS_REGEX) do |match|
|
|
157
|
+
next match if Regexp.last_match(:verbatim)
|
|
158
|
+
|
|
159
|
+
match == "\u00A0" ? ' ' : 'th'
|
|
132
160
|
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
sig { params(string: String, marker: String, raws: T::Hash[String, String]).returns(String) }
|
|
164
|
+
def re_inject_raws(string, marker, raws)
|
|
165
|
+
str = raws.empty? ? string : string.gsub(/#{Regexp.escape(marker)}\d+~/) { |token| raws.fetch(token) }
|
|
133
166
|
str = str.html_safe if str.respond_to?(:html_safe)
|
|
134
167
|
str
|
|
135
168
|
end
|
|
136
169
|
|
|
137
|
-
private
|
|
138
|
-
|
|
139
|
-
# Internal transpilation details, not public surface (column_count/container_width stay public).
|
|
140
|
-
private :component_instances, :transform_doc, :component_factory, :extract_raws, :re_inject_raws
|
|
141
|
-
|
|
142
170
|
sig { params(config: ActiveMail::Configuration, overrides: T.untyped).returns(T::Hash[String, ActiveMail::Components::Base]) }
|
|
143
171
|
def build_components(config, overrides)
|
|
144
172
|
# Lookup is by node name (String); a Symbol key would never match.
|
|
@@ -151,8 +179,8 @@ module ActiveMail
|
|
|
151
179
|
def normalize_input(html_string)
|
|
152
180
|
html_string = html_string.to_s
|
|
153
181
|
html_string = html_string.dup.force_encoding(Encoding::UTF_8) if html_string.encoding == Encoding::BINARY
|
|
154
|
-
html_string = ::ActiveMail.scrub_invalid_bytes(html_string) unless html_string.valid_encoding?
|
|
155
|
-
html_string
|
|
182
|
+
html_string = ::ActiveMail.scrub_invalid_bytes(html_string, config.on_parse_error) unless html_string.valid_encoding?
|
|
183
|
+
html_string
|
|
156
184
|
end
|
|
157
185
|
end
|
|
158
186
|
end
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
module Components
|
|
5
5
|
# Register in config/initializers/activemail.rb:
|
|
6
6
|
# config.register_component "<%= tag_name %>", Components::<%= class_name %>
|
|
7
|
-
class <%= class_name %> < ActiveMail::Components::
|
|
7
|
+
class <%= class_name %> < ActiveMail::Components::TableComponent
|
|
8
8
|
def transform(node, inner)
|
|
9
|
-
|
|
10
|
-
%(<table class="#{classes}" #{TABLE_RESET}><tbody><tr><td>#{inner}</td></tr></tbody></table>)
|
|
9
|
+
%(<table#{root_attributes(node, classes: "<%= tag_name %>")} #{TABLE_RESET}><tbody><tr><td>#{inner}</td></tr></tbody></table>)
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
end
|
|
@@ -14,9 +14,6 @@ ActiveMail.configure do |config|
|
|
|
14
14
|
# ActiveMail::Inliner::Base subclass/instance.
|
|
15
15
|
# config.inliner = :premailer
|
|
16
16
|
|
|
17
|
-
# Set false if another inliner (e.g. premailer-rails) already runs on mailers.
|
|
18
|
-
# config.register_inline_interceptor = true
|
|
19
|
-
|
|
20
17
|
# How malformed markup is handled: :warn (default), :ignore, or :raise.
|
|
21
18
|
# config.on_parse_error = :warn
|
|
22
19
|
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="<%%= I18n.locale %>" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
3
|
-
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta name="x-apple-disable-message-reformatting">
|
|
7
|
-
<meta name="color-scheme" content="light dark">
|
|
8
|
-
<meta name="supported-color-schemes" content="light dark">
|
|
9
|
-
<!--[if mso]>
|
|
10
|
-
<noscript>
|
|
11
|
-
<xml>
|
|
12
|
-
<o:OfficeDocumentSettings>
|
|
13
|
-
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
14
|
-
</o:OfficeDocumentSettings>
|
|
15
|
-
</xml>
|
|
16
|
-
</noscript>
|
|
17
|
-
<![endif]-->
|
|
18
|
-
<%%= activemail_inline_styles %>
|
|
19
|
-
<%%# Fallback for when activemail_inline_styles cannot read the compiled asset. %>
|
|
20
|
-
<%%= stylesheet_link_tag "activemail/activemail", media: "all" %>
|
|
21
|
-
</head>
|
|
3
|
+
<%%= render "layouts/activemail/document_head" %>
|
|
22
4
|
|
|
23
5
|
<body style="margin:0;padding:0;word-spacing:normal;">
|
|
24
6
|
<div role="article" aria-roledescription="email" lang="<%%= I18n.locale %>" style="width:100%;">
|
|
25
7
|
<container>
|
|
8
|
+
<%%= render "layouts/activemail/head" %>
|
|
26
9
|
<%%= yield %>
|
|
10
|
+
<%%= render "layouts/activemail/footer" %>
|
|
27
11
|
</container>
|
|
28
12
|
</div>
|
|
29
13
|
</body>
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
!!!
|
|
2
2
|
%html{lang: I18n.locale, xmlns: "http://www.w3.org/1999/xhtml", "xmlns:v" => "urn:schemas-microsoft-com:vml", "xmlns:o" => "urn:schemas-microsoft-com:office:office"}
|
|
3
|
-
|
|
4
|
-
%meta{charset: "utf-8"}
|
|
5
|
-
%meta{name: "viewport", content: "width=device-width, initial-scale=1"}
|
|
6
|
-
%meta{name: "x-apple-disable-message-reformatting"}
|
|
7
|
-
%meta{name: "color-scheme", content: "light dark"}
|
|
8
|
-
%meta{name: "supported-color-schemes", content: "light dark"}
|
|
9
|
-
:plain
|
|
10
|
-
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
|
11
|
-
= activemail_inline_styles
|
|
12
|
-
-# Fallback for when activemail_inline_styles cannot read the compiled asset.
|
|
13
|
-
= stylesheet_link_tag "activemail/activemail", media: "all"
|
|
3
|
+
= render "layouts/activemail/document_head"
|
|
14
4
|
%body{style: "margin:0;padding:0;word-spacing:normal;"}
|
|
15
5
|
%div{role: "article", "aria-roledescription" => "email", lang: I18n.locale, style: "width:100%;"}
|
|
16
6
|
%container
|
|
7
|
+
= render "layouts/activemail/head"
|
|
17
8
|
= yield
|
|
9
|
+
= render "layouts/activemail/footer"
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
doctype html
|
|
2
2
|
html lang=I18n.locale xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
3
|
-
|
|
4
|
-
meta charset="utf-8"
|
|
5
|
-
meta name="viewport" content="width=device-width, initial-scale=1"
|
|
6
|
-
meta name="x-apple-disable-message-reformatting"
|
|
7
|
-
meta name="color-scheme" content="light dark"
|
|
8
|
-
meta name="supported-color-schemes" content="light dark"
|
|
9
|
-
| <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
|
10
|
-
= activemail_inline_styles
|
|
11
|
-
/ Fallback for when activemail_inline_styles cannot read the compiled asset.
|
|
12
|
-
= stylesheet_link_tag "activemail/activemail", media: "all"
|
|
3
|
+
= render "layouts/activemail/document_head"
|
|
13
4
|
body style="margin:0;padding:0;word-spacing:normal;"
|
|
14
5
|
div role="article" aria-roledescription="email" lang=I18n.locale style="width:100%;"
|
|
15
6
|
container
|
|
7
|
+
= render "layouts/activemail/head"
|
|
16
8
|
= yield
|
|
9
|
+
= render "layouts/activemail/footer"
|
data/lib/tasks/activemail.rake
CHANGED
|
@@ -20,15 +20,14 @@ namespace :activemail do
|
|
|
20
20
|
task render_all: :environment do
|
|
21
21
|
require 'activemail/quality/render_all'
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
puts "Rendered #{result.rendered} email(s) into #{output_root}"
|
|
23
|
+
root = defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root : Pathname.pwd
|
|
24
|
+
result = ActiveMail::Quality::RenderAll.new(root: root, config: ActiveMail::Quality.config).call
|
|
25
|
+
puts "Rendered #{result.rendered} email(s) into #{result.output_root}"
|
|
27
26
|
# A green run on zero previews would silently verify nothing — make it visible.
|
|
28
27
|
warn '[activemail] WARNING: no mailer previews were discovered — nothing was checked.' if result.discovered.zero?
|
|
29
28
|
result.render_failures.each { |key, error| puts " render failed: #{key}: #{error}" }
|
|
30
29
|
result.guard_failures.each do |key, violations|
|
|
31
|
-
puts " guard failed: #{key}", violations.map { |v| " -
|
|
30
|
+
puts " guard failed: #{key}", violations.map { |v| " - #{v}" }.join("\n")
|
|
32
31
|
end
|
|
33
32
|
abort "\n#{result.broken_required.size} required preview(s) failed: #{result.broken_required.join(', ')}" if result.broken_required.any?
|
|
34
33
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activemail
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Advitam
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- CHANGELOG.md
|
|
82
82
|
- LICENSE.txt
|
|
83
83
|
- README.md
|
|
84
|
+
- UPGRADING.md
|
|
84
85
|
- app/assets/stylesheets/activemail/_activemail_tokens.scss.erb
|
|
85
86
|
- app/assets/stylesheets/activemail/_components.scss
|
|
86
87
|
- app/assets/stylesheets/activemail/_dark.scss
|
|
@@ -89,13 +90,20 @@ files:
|
|
|
89
90
|
- app/assets/stylesheets/activemail/_utilities.scss
|
|
90
91
|
- app/assets/stylesheets/activemail/activemail.scss
|
|
91
92
|
- app/helpers/active_mail/styles_helper.rb
|
|
93
|
+
- app/views/layouts/activemail/_document_head.html.erb
|
|
92
94
|
- app/views/layouts/activemail/_footer.html.inky-erb
|
|
93
95
|
- app/views/layouts/activemail/_head.html.inky-erb
|
|
94
96
|
- app/views/layouts/activemail/mailer.html.inky-erb
|
|
95
97
|
- lib/activemail.rb
|
|
98
|
+
- lib/activemail/asset_pipeline.rb
|
|
99
|
+
- lib/activemail/asset_pipeline/base.rb
|
|
100
|
+
- lib/activemail/asset_pipeline/null.rb
|
|
101
|
+
- lib/activemail/asset_pipeline/propshaft.rb
|
|
102
|
+
- lib/activemail/asset_pipeline/sprockets.rb
|
|
96
103
|
- lib/activemail/components/base.rb
|
|
97
104
|
- lib/activemail/components/block_grid.rb
|
|
98
105
|
- lib/activemail/components/button.rb
|
|
106
|
+
- lib/activemail/components/button_renderer.rb
|
|
99
107
|
- lib/activemail/components/callout.rb
|
|
100
108
|
- lib/activemail/components/center.rb
|
|
101
109
|
- lib/activemail/components/columns.rb
|
|
@@ -103,32 +111,42 @@ files:
|
|
|
103
111
|
- lib/activemail/components/cta.rb
|
|
104
112
|
- lib/activemail/components/h_line.rb
|
|
105
113
|
- lib/activemail/components/info_box.rb
|
|
106
|
-
- lib/activemail/components/inky.rb
|
|
107
114
|
- lib/activemail/components/menu.rb
|
|
108
115
|
- lib/activemail/components/menu_item.rb
|
|
109
116
|
- lib/activemail/components/row.rb
|
|
110
117
|
- lib/activemail/components/spacer.rb
|
|
118
|
+
- lib/activemail/components/table_component.rb
|
|
111
119
|
- lib/activemail/components/wrapper.rb
|
|
112
120
|
- lib/activemail/configuration.rb
|
|
121
|
+
- lib/activemail/errors.rb
|
|
122
|
+
- lib/activemail/inliner.rb
|
|
113
123
|
- lib/activemail/inliner/base.rb
|
|
114
|
-
- lib/activemail/inliner/interceptor.rb
|
|
115
124
|
- lib/activemail/inliner/null.rb
|
|
116
125
|
- lib/activemail/inliner/premailer.rb
|
|
126
|
+
- lib/activemail/inliner/premailer/document.rb
|
|
117
127
|
- lib/activemail/inliner/roadie.rb
|
|
118
128
|
- lib/activemail/libxml.rb
|
|
119
129
|
- lib/activemail/parse_error_reporter.rb
|
|
130
|
+
- lib/activemail/pipeline.rb
|
|
120
131
|
- lib/activemail/quality.rb
|
|
132
|
+
- lib/activemail/quality/builtin_message_rules.rb
|
|
133
|
+
- lib/activemail/quality/builtin_rules.rb
|
|
121
134
|
- lib/activemail/quality/configuration.rb
|
|
122
135
|
- lib/activemail/quality/guard.rb
|
|
123
136
|
- lib/activemail/quality/minitest.rb
|
|
137
|
+
- lib/activemail/quality/output_dir.rb
|
|
138
|
+
- lib/activemail/quality/output_filter.rb
|
|
124
139
|
- lib/activemail/quality/preview_renderer.rb
|
|
125
140
|
- lib/activemail/quality/render_all.rb
|
|
126
141
|
- lib/activemail/quality/rspec.rb
|
|
127
|
-
- lib/activemail/
|
|
142
|
+
- lib/activemail/quality/rule.rb
|
|
143
|
+
- lib/activemail/quality/rule_exemptions.rb
|
|
128
144
|
- lib/activemail/rails/engine.rb
|
|
145
|
+
- lib/activemail/rails/rendering.rb
|
|
129
146
|
- lib/activemail/rails/template_handler.rb
|
|
130
147
|
- lib/activemail/tokens.rb
|
|
131
148
|
- lib/activemail/tokens/button_style.rb
|
|
149
|
+
- lib/activemail/tokens/defaults.rb
|
|
132
150
|
- lib/activemail/tokens/scss_serializer.rb
|
|
133
151
|
- lib/activemail/version.rb
|
|
134
152
|
- lib/generators/activemail/component_generator.rb
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require_relative 'base'
|
|
5
|
-
|
|
6
|
-
module ActiveMail
|
|
7
|
-
module Components
|
|
8
|
-
# Renders a bare <tr>, useful inside hand-written tables.
|
|
9
|
-
class Inky < Base
|
|
10
|
-
extend T::Sig
|
|
11
|
-
|
|
12
|
-
sig { override.params(_node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
13
|
-
def transform(_node, inner)
|
|
14
|
-
%(<tr>#{inner}</tr>)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'sorbet-runtime'
|
|
5
|
-
|
|
6
|
-
module ActiveMail
|
|
7
|
-
module Inliner
|
|
8
|
-
class Interceptor
|
|
9
|
-
class << self
|
|
10
|
-
extend T::Sig
|
|
11
|
-
|
|
12
|
-
sig { params(message: T.untyped).void }
|
|
13
|
-
def delivering_email(message)
|
|
14
|
-
config = ActiveMail.configuration
|
|
15
|
-
# Runtime check: an engine initializer runs before the host's, so a
|
|
16
|
-
# boot-time flag read would always see the default.
|
|
17
|
-
return unless config.register_inline_interceptor
|
|
18
|
-
|
|
19
|
-
inliner = config.resolved_inliner
|
|
20
|
-
return if inliner.noop?
|
|
21
|
-
|
|
22
|
-
html_parts(message).each do |part|
|
|
23
|
-
part.body = inliner.inline(part.body.to_s)
|
|
24
|
-
rescue StandardError => e
|
|
25
|
-
# Surface which inliner failed; do not swallow (a silently non-inlined
|
|
26
|
-
# mail is worse). Ruby sets e as #cause, preserving the original backtrace.
|
|
27
|
-
raise ActiveMail::Inliner::Error, "[#{inliner.class}] CSS inlining failed: #{e.message}"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
# The html part(s) to inline: a single text/html message, or every text/html
|
|
34
|
-
# part of a multipart message. Attachments are never inlined.
|
|
35
|
-
sig { params(message: T.untyped).returns(T::Array[T.untyped]) }
|
|
36
|
-
def html_parts(message)
|
|
37
|
-
return message.all_parts.select { |part| html_body_part?(part) } if message.multipart?
|
|
38
|
-
|
|
39
|
-
html_body_part?(message) ? [message] : []
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
sig { params(part: T.untyped).returns(T::Boolean) }
|
|
43
|
-
def html_body_part?(part)
|
|
44
|
-
return false unless part.respond_to?(:mime_type) && part.mime_type == 'text/html'
|
|
45
|
-
|
|
46
|
-
!(part.respond_to?(:attachment?) && part.attachment?)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|