activemail 1.2.2 → 2.0.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 +65 -2
- data/README.md +105 -47
- data/UPGRADING.md +112 -0
- data/app/assets/stylesheets/activemail/_components.scss +33 -42
- data/app/assets/stylesheets/activemail/_dark.scss +31 -20
- data/app/assets/stylesheets/activemail/_grid.scss +2 -0
- data/app/assets/stylesheets/activemail/_settings.scss +22 -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/mailer.html.inky-erb +0 -5
- 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 +65 -0
- data/lib/activemail/components/callout.rb +4 -5
- data/lib/activemail/components/center.rb +5 -7
- data/lib/activemail/components/columns.rb +26 -7
- data/lib/activemail/components/container.rb +11 -4
- 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/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_rules.rb +154 -0
- data/lib/activemail/quality/configuration.rb +83 -3
- data/lib/activemail/quality/guard.rb +45 -87
- data/lib/activemail/quality/minitest.rb +41 -25
- data/lib/activemail/quality/output_dir.rb +52 -0
- data/lib/activemail/quality/preview_renderer.rb +46 -33
- data/lib/activemail/quality/render_all.rb +96 -40
- data/lib/activemail/quality/rspec.rb +4 -8
- data/lib/activemail/quality/rule.rb +47 -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 +77 -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 +0 -2
- data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +0 -2
- data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +0 -2
- data/lib/tasks/activemail.rake +4 -5
- metadata +38 -7
- 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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
|
|
6
|
+
module ActiveMail
|
|
7
|
+
module Components
|
|
8
|
+
# Bulletproof button shared by <button> and <cta>: styles are inlined from the
|
|
9
|
+
# tokens (read at transform time) so it survives clients that strip <style>.
|
|
10
|
+
class ButtonRenderer < Base
|
|
11
|
+
extend T::Sig
|
|
12
|
+
extend T::Helpers
|
|
13
|
+
|
|
14
|
+
abstract!
|
|
15
|
+
|
|
16
|
+
CONTROL_PADDING = 'padding:12px 24px;'
|
|
17
|
+
|
|
18
|
+
sig { abstract.returns(String) }
|
|
19
|
+
def root_class; end
|
|
20
|
+
|
|
21
|
+
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
22
|
+
def transform(node, inner)
|
|
23
|
+
style = core.config.tokens.button_style(class?(node, 'secondary') ? :secondary : :primary)
|
|
24
|
+
expand = class?(node, 'expand')
|
|
25
|
+
width = expand ? ' style="width:100%;"' : ''
|
|
26
|
+
[
|
|
27
|
+
%(<table#{class_attribute(node, root_class)} #{TABLE_RESET}#{width}><tbody><tr><td>),
|
|
28
|
+
%(<table #{TABLE_RESET}#{width}><tbody><tr><td style="#{cell_style(style, expand)}">),
|
|
29
|
+
control(node, inner, style, expand),
|
|
30
|
+
'</td></tr></tbody></table></td></tr></tbody></table>'
|
|
31
|
+
].join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
sig { override.returns(T::Array[String]) }
|
|
37
|
+
def consumed_attributes
|
|
38
|
+
LINK_ATTRIBUTES
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Padding on the control makes the whole button a clickable target.
|
|
42
|
+
sig { params(node: Nokogiri::XML::Node, inner: String, style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
43
|
+
def control(node, inner, style, expand)
|
|
44
|
+
css = control_style(style, expand)
|
|
45
|
+
extra = passthrough_attributes(node)
|
|
46
|
+
return %(<span#{extra}#{style_attribute(node, css)}>#{inner}</span>) unless node.attr('href')
|
|
47
|
+
|
|
48
|
+
anchor(node, inner, before: extra, after: style_attribute(node, css))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The outline lives on the cell only: repeating it on the control would double the line.
|
|
52
|
+
sig { params(style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
53
|
+
def cell_style(style, expand)
|
|
54
|
+
border = style.border ? "border:1px solid #{style.border};" : ''
|
|
55
|
+
"background:#{style.background};border-radius:#{style.radius};#{border}#{'text-align:center;' if expand}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
59
|
+
def control_style(style, expand)
|
|
60
|
+
"display:#{expand ? 'block' : 'inline-block'};text-decoration:none;#{CONTROL_PADDING}" \
|
|
61
|
+
"background:#{style.background};color:#{style.color};font-weight:bold;border-radius:#{style.radius};"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Callout <
|
|
8
|
+
class Callout < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
13
|
classes = combine_classes(node, 'callout-inner')
|
|
14
|
-
attributes = pass_through_attributes(node)
|
|
15
14
|
[
|
|
16
|
-
%(<table
|
|
17
|
-
%(<th class="#{classes}">#{inner}</th><th class="expander"></th>),
|
|
15
|
+
%(<table class="callout"#{passthrough_attributes(node)}#{style_attribute(node, 'width:100%;')} #{TABLE_RESET}><tbody><tr>),
|
|
16
|
+
%(<th class="#{escape_attr(classes)}">#{inner}</th><th class="expander"></th>),
|
|
18
17
|
'</tr></tbody></table>'
|
|
19
18
|
].join
|
|
20
19
|
end
|
|
@@ -8,16 +8,14 @@ module ActiveMail
|
|
|
8
8
|
class Center < Base
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
|
-
sig { override.params(node: Nokogiri::XML::Node,
|
|
12
|
-
def transform(node,
|
|
13
|
-
elements
|
|
14
|
-
elements.each do |child|
|
|
11
|
+
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
|
+
def transform(node, inner)
|
|
13
|
+
node.elements.each do |child|
|
|
15
14
|
child['align'] = 'center'
|
|
16
15
|
child['class'] = combine_classes(child, 'float-center')
|
|
17
16
|
end
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
node.to_s
|
|
17
|
+
node.css('.menu-item').each { |item| item['class'] = combine_classes(item, 'float-center') }
|
|
18
|
+
%(<center#{root_attributes(node, classes: nil)}>#{inner}</center>)
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
end
|
|
@@ -22,26 +22,45 @@ module ActiveMail
|
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
+
sig { override.returns(T::Array[String]) }
|
|
26
|
+
def consumed_attributes
|
|
27
|
+
%w[small large]
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
sig { params(node: Nokogiri::XML::Node).returns([Integer, Integer]) }
|
|
26
31
|
def column_sizes(node)
|
|
27
32
|
small = positive_int(node.attr('small'))
|
|
28
33
|
large = positive_int(node.attr('large'))
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
[small || column_count, large || small || [column_count / column_siblings(node).size, 1].max]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Already transformed columns are interim <th class="columns"> elements; other siblings take no width.
|
|
38
|
+
sig { params(node: Nokogiri::XML::Node).returns(T::Array[Nokogiri::XML::Node]) }
|
|
39
|
+
def column_siblings(node)
|
|
40
|
+
return [node] unless node.parent
|
|
41
|
+
|
|
42
|
+
node.parent.elements.select { |sibling| sibling.name == node.name || column?(sibling) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(node: Nokogiri::XML::Node).returns(T::Boolean) }
|
|
46
|
+
def column?(node)
|
|
47
|
+
return false unless node.name == ::ActiveMail::Core::INTERIM_TH_TAG
|
|
48
|
+
|
|
49
|
+
class?(node, 'columns')
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
sig { params(node: Nokogiri::XML::Node, small: Integer, large: Integer).returns(String) }
|
|
34
53
|
def column_classes(node, small, large)
|
|
54
|
+
siblings = column_siblings(node)
|
|
35
55
|
classes = combine_classes(node, "small-#{small} large-#{large} columns")
|
|
36
|
-
classes += ' first'
|
|
37
|
-
classes += ' last'
|
|
56
|
+
classes += ' first' if siblings.first == node
|
|
57
|
+
classes += ' last' if siblings.last == node
|
|
38
58
|
classes
|
|
39
59
|
end
|
|
40
60
|
|
|
41
61
|
sig { params(node: Nokogiri::XML::Node, large: Integer).returns(String) }
|
|
42
62
|
def expander(node, large)
|
|
43
|
-
|
|
44
|
-
return '' unless large == column_count && subrows.empty?
|
|
63
|
+
return '' unless large == column_count && node.at_css('.row').nil?
|
|
45
64
|
|
|
46
65
|
%(<th class="expander"></th>)
|
|
47
66
|
end
|
|
@@ -61,7 +80,7 @@ module ActiveMail
|
|
|
61
80
|
# Neutralize the client default th rendering (bold, centered).
|
|
62
81
|
content_style = 'font-weight:normal;text-align:left;'
|
|
63
82
|
[
|
|
64
|
-
%(<#{::ActiveMail::Core::INTERIM_TH_TAG}
|
|
83
|
+
%(<#{::ActiveMail::Core::INTERIM_TH_TAG}#{root_attributes(node, classes: classes, layout_style: style)}>),
|
|
65
84
|
%(<table #{TABLE_RESET} style="width:100%;"><tbody><tr>),
|
|
66
85
|
%(<th style="#{content_style}">#{inner}</th>#{expander}),
|
|
67
86
|
%(</tr></tbody></table></#{::ActiveMail::Core::INTERIM_TH_TAG}>)
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Container <
|
|
8
|
+
class Container < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
|
-
attributes = combine_attributes(node, 'container')
|
|
14
13
|
width = container_width
|
|
14
|
+
attributes = root_attributes(node, classes: 'container', layout_style: "width:100%;max-width:#{width}px;margin:0 auto;")
|
|
15
15
|
# Outlook Word ignores max-width; the MSO ghost table pins the width.
|
|
16
16
|
[
|
|
17
17
|
%(<!--[if mso | IE]><table #{TABLE_RESET} align="center" width="#{width}"><tr><td><![endif]-->),
|
|
18
|
-
%(<table
|
|
18
|
+
%(<table#{attributes} #{TABLE_RESET} align="center">),
|
|
19
19
|
%(<tbody><tr><td>#{inner}</td></tr></tbody></table>),
|
|
20
20
|
'<!--[if mso | IE]></td></tr></table><![endif]-->'
|
|
21
21
|
].join
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
sig { override.returns(T::Array[String]) }
|
|
27
|
+
def consumed_attributes
|
|
28
|
+
[*super, 'align']
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
end
|
|
25
32
|
end
|
|
@@ -1,48 +1,24 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'button_renderer'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
|
|
9
|
-
# Styles are inlined so the button survives clients that strip <style> (Gmail mobile…).
|
|
10
|
-
class Cta < Base
|
|
8
|
+
class Cta < ButtonRenderer
|
|
11
9
|
extend T::Sig
|
|
12
10
|
|
|
13
|
-
sig { override.
|
|
11
|
+
sig { override.returns(String) }
|
|
12
|
+
def root_class
|
|
13
|
+
'cta'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
14
17
|
def transform(node, inner)
|
|
15
18
|
# A CTA without a link is an authoring bug — surface it at render time.
|
|
16
19
|
raise ArgumentError, '<cta> requires an href attribute' if node.attr('href').to_s.strip.empty?
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
anchor = %(<a href="#{escape_attr(node.attr('href'))}"#{link_attributes(node)} ) +
|
|
20
|
-
%(style="#{link_style(style)}">#{inner}</a>)
|
|
21
|
-
bulletproof_button_table(
|
|
22
|
-
outer_classes: combine_classes(node, 'cta'),
|
|
23
|
-
inner: anchor,
|
|
24
|
-
cell_style: cell_style(style)
|
|
25
|
-
)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
31
|
-
def cell_style(style)
|
|
32
|
-
"background:#{style.background};border-radius:#{style.radius};#{border_css(style)}"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# No border here: it lives on the cell only — a repeated <a> border doubles the line.
|
|
36
|
-
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
37
|
-
def link_style(style)
|
|
38
|
-
"display:inline-block;text-decoration:none;#{BUTTON_PADDING}" \
|
|
39
|
-
"background:#{style.background};color:#{style.color};font-weight:bold;" \
|
|
40
|
-
"border-radius:#{style.radius};"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
44
|
-
def border_css(style)
|
|
45
|
-
style.border ? "border:1px solid #{style.border};" : ''
|
|
21
|
+
super
|
|
46
22
|
end
|
|
47
23
|
end
|
|
48
24
|
end
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class HLine <
|
|
8
|
+
class HLine < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, _inner: String).returns(String) }
|
|
12
12
|
def transform(node, _inner)
|
|
13
|
-
|
|
14
|
-
attributes = pass_through_attributes(node)
|
|
15
|
-
%(<table #{attributes}class="#{classes}" #{TABLE_RESET}#{style_attribute(node, 'width:100%;')}><tbody><tr><th> </th></tr></tbody></table>)
|
|
13
|
+
%(<table#{root_attributes(node, classes: 'h-line', layout_style: 'width:100%;')} #{TABLE_RESET}><tbody><tr><th> </th></tr></tbody></table>)
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
end
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
8
|
# Colors read from tokens at transform time (runtime config), not load-time constants.
|
|
9
|
-
class InfoBox <
|
|
9
|
+
class InfoBox < TableComponent
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
12
|
sig { override.overridable.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
13
13
|
def transform(node, inner)
|
|
14
|
-
classes = combine_classes(node, 'info-box')
|
|
15
14
|
[
|
|
16
|
-
%(<table
|
|
15
|
+
%(<table#{root_attributes(node, classes: 'info-box', layout_style: 'width:100%;')} #{TABLE_RESET}><tbody><tr>),
|
|
17
16
|
%(<td style="#{cell_style}">#{inner}</td>),
|
|
18
17
|
'</tr></tbody></table>'
|
|
19
18
|
].join
|
|
@@ -21,16 +20,11 @@ module ActiveMail
|
|
|
21
20
|
|
|
22
21
|
private
|
|
23
22
|
|
|
24
|
-
# Box-scoped tokens (fall back to page-level ones) so a host can give the box
|
|
25
|
-
# a distinct surface without colliding with :background/:border/:text.
|
|
26
23
|
sig { returns(String) }
|
|
27
24
|
def cell_style
|
|
28
|
-
tokens =
|
|
29
|
-
background
|
|
30
|
-
|
|
31
|
-
text = tokens.color(:info_box_text) || tokens.color!(:text)
|
|
32
|
-
"background-color:#{background};border-left:5px solid #{border};border-radius:#{tokens.radius!(:box)};" \
|
|
33
|
-
"color:#{text};padding:#{tokens.spacing!(:md)};"
|
|
25
|
+
tokens = core.config.tokens
|
|
26
|
+
"background-color:#{tokens.color!(:info_box_background)};border-left:5px solid #{tokens.color!(:info_box_border)};" \
|
|
27
|
+
"border-radius:#{tokens.radius!(:box)};color:#{tokens.color!(:info_box_text)};padding:#{tokens.spacing!(:md)};"
|
|
34
28
|
end
|
|
35
29
|
end
|
|
36
30
|
end
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Menu <
|
|
8
|
+
class Menu < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
|
-
attributes = combine_attributes(node, 'menu')
|
|
14
13
|
[
|
|
15
|
-
%(<table
|
|
14
|
+
%(<table#{root_attributes(node, classes: 'menu')} #{TABLE_RESET}><tbody><tr><td>),
|
|
16
15
|
%(<table #{TABLE_RESET}><tbody><tr>#{inner}</tr></tbody></table>),
|
|
17
16
|
'</td></tr></tbody></table>'
|
|
18
17
|
].join
|
|
@@ -10,11 +10,18 @@ module ActiveMail
|
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
|
-
attributes =
|
|
13
|
+
attributes = root_attributes(node, classes: 'menu-item')
|
|
14
14
|
# No href → a non-link item, not a broken <a href="">; mirrors <button>.
|
|
15
|
-
content = node.attr('href') ?
|
|
15
|
+
content = node.attr('href') ? anchor(node, inner) : inner
|
|
16
16
|
th = ::ActiveMail::Core::INTERIM_TH_TAG
|
|
17
|
-
%(<#{th}
|
|
17
|
+
%(<#{th}#{attributes}>#{content}</#{th}>)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
sig { override.returns(T::Array[String]) }
|
|
23
|
+
def consumed_attributes
|
|
24
|
+
LINK_ATTRIBUTES
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Row <
|
|
8
|
+
class Row < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
|
-
|
|
14
|
-
%(<table #{attributes} #{TABLE_RESET}#{style_attribute(node, 'width:100%;')}><tbody><tr>#{inner}</tr></tbody></table>)
|
|
13
|
+
%(<table#{root_attributes(node, classes: 'row', layout_style: 'width:100%;')} #{TABLE_RESET}><tbody><tr>#{inner}</tr></tbody></table>)
|
|
15
14
|
end
|
|
16
15
|
end
|
|
17
16
|
end
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Spacer <
|
|
8
|
+
class Spacer < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, _inner: String).returns(String) }
|
|
12
12
|
def transform(node, _inner)
|
|
13
|
-
classes = combine_classes(node, 'spacer')
|
|
14
13
|
size_sm = node.attr('size-sm')
|
|
15
14
|
size_lg = node.attr('size-lg')
|
|
16
15
|
|
|
17
|
-
return build_table(node,
|
|
16
|
+
return build_table(node, 'spacer', size_for(node.attr('size')), skip: []) unless size_sm || size_lg
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
tables = []
|
|
19
|
+
tables << ['spacer hide-for-large', size_for(size_sm)] if size_sm
|
|
20
|
+
tables << ['spacer show-for-large', size_for(size_lg)] if size_lg
|
|
21
|
+
tables.each_with_index.map { |(classes, size), index| build_table(node, classes, size, skip: index.zero? ? [] : %w[id]) }.join
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
private
|
|
26
25
|
|
|
26
|
+
sig { override.returns(T::Array[String]) }
|
|
27
|
+
def consumed_attributes
|
|
28
|
+
[*super, 'size', 'size-sm', 'size-lg']
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
sig { params(value: T.untyped).returns(Integer) }
|
|
28
32
|
def size_for(value)
|
|
29
33
|
positive_int(value) || 16
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
# A repeated id would be invalid HTML: only the first table carries it.
|
|
37
|
+
sig { params(node: Nokogiri::XML::Node, classes: String, size: Integer, skip: T::Array[String]).returns(String) }
|
|
38
|
+
def build_table(node, classes, size, skip:)
|
|
35
39
|
# mso-line-height-rule:exactly keeps Outlook from inflating the spacer.
|
|
36
40
|
style = "font-size:#{size}px;line-height:#{size}px;mso-line-height-rule:exactly;"
|
|
37
41
|
[
|
|
38
|
-
%(<table
|
|
42
|
+
%(<table#{root_attributes(node, classes: classes, layout_style: 'width:100%;', skip: skip)} #{TABLE_RESET}><tbody><tr>),
|
|
39
43
|
%(<td height="#{size}" style="#{style}"> </td>),
|
|
40
44
|
'</tr></tbody></table>'
|
|
41
45
|
].join
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
|
|
6
|
+
module ActiveMail
|
|
7
|
+
module Components
|
|
8
|
+
# Root element is a layout table: its reset attributes are fixed, not author-overridable.
|
|
9
|
+
class TableComponent < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
extend T::Helpers
|
|
12
|
+
|
|
13
|
+
abstract!
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
sig { override.overridable.returns(T::Array[String]) }
|
|
18
|
+
def consumed_attributes
|
|
19
|
+
TABLE_RESET_ATTRIBUTES
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'table_component'
|
|
5
5
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
|
-
class Wrapper <
|
|
8
|
+
class Wrapper < TableComponent
|
|
9
9
|
extend T::Sig
|
|
10
10
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
|
-
attributes = combine_attributes(node, 'wrapper')
|
|
14
13
|
[
|
|
15
|
-
%(<table
|
|
14
|
+
%(<table#{root_attributes(node, classes: 'wrapper', layout_style: 'width:100%;')} #{TABLE_RESET} align="center"><tbody><tr>),
|
|
16
15
|
%(<td class="wrapper-inner">#{inner}</td></tr></tbody></table>)
|
|
17
16
|
].join
|
|
18
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
sig { override.returns(T::Array[String]) }
|
|
22
|
+
def consumed_attributes
|
|
23
|
+
[*super, 'align']
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|