activemail 1.1.1 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7cc246dce0922db6f211036614c9c09e61f07ec31be8a8f080ec5367b639162
4
- data.tar.gz: f73779825c12264751b039b6fdac9bce76db075d26f058f432d9b85f79529f6a
3
+ metadata.gz: 45cf8d77d0e4e10e7669e5cfeaa843b174a495b900d6bc5ffc83b2af18bfb11d
4
+ data.tar.gz: 4b750b051e3354ffc5eeac01e71df54f9bc724b6d3c00ab2d160845a70396db7
5
5
  SHA512:
6
- metadata.gz: 6edee3036ed2af1fa135508c8b936a61a9045a48f8b44ada6e762dead35510f6a8fd0b8c623f82132d72b6726c4cb3a7adfa96c58f93eebb0eb239340cd69118
7
- data.tar.gz: 1d49cbdcafea296842256ec928e626513d3fefe6f0d81d6ce1a2ea9229b9173735269c80123e94c832ba2bd691d8ce08468fba33cb65839cceb162de50af6c99
6
+ metadata.gz: aebffed9184e53716057ec140ea66c71ebca81cac9d15ca3cd60b20da4f7fdfc0da8f5fa599e3aa18a08b7d75400295be8b56c1a776cc36465ac412646d515f0
7
+ data.tar.gz: a9975ff0585326d018de39b7d651515d788f792465ac49f8d147034df5e7abc6e9fbcd1a5e4fb717612e59969efac36a5799bc8d1e35e698146cee7afb59f287
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-07-01
9
+
10
+ ### Added
11
+
12
+ - `config.blank_link_rel` (default `"noopener"`): a `rel` is now emitted automatically on
13
+ `target="_blank"` anchors. Set `nil` to disable; an explicit `rel="…"` still wins.
14
+
8
15
  ## [1.1.1] - 2026-06-16
9
16
 
10
17
  ### Added
@@ -29,7 +29,7 @@ module ActiveMail
29
29
  abstract!
30
30
 
31
31
  IGNORED_ON_PASSTHROUGH = T.let(
32
- %w[class id href size large no-expander small target up size-sm size-lg style].freeze,
32
+ %w[class id href size large no-expander small target rel up size-sm size-lg style].freeze,
33
33
  T::Array[String]
34
34
  )
35
35
 
@@ -103,8 +103,20 @@ module ActiveMail
103
103
  end
104
104
 
105
105
  sig { params(node: Nokogiri::XML::Node).returns(String) }
106
- def target_attribute(node)
107
- node.attributes['target'] ? %( target="#{escape_attr(node.attributes['target'])}") : ''
106
+ def link_attributes(node)
107
+ target = node.attributes['target']&.value
108
+ rel = resolve_rel(node, target)
109
+ [
110
+ target ? %( target="#{escape_attr(target)}") : '',
111
+ rel ? %( rel="#{escape_attr(rel)}") : ''
112
+ ].join
113
+ end
114
+
115
+ sig { params(node: Nokogiri::XML::Node, target: T.nilable(String)).returns(T.nilable(String)) }
116
+ def resolve_rel(node, target)
117
+ rel = node.attributes['rel']&.value
118
+ rel = nil if rel&.strip&.empty?
119
+ rel || (ActiveMail.configuration.blank_link_rel if target == '_blank')
108
120
  end
109
121
 
110
122
  # Outlook-safe nested-table structure kept in one place for <button> and <cta>.
@@ -28,11 +28,11 @@ module ActiveMail
28
28
 
29
29
  sig { params(node: Nokogiri::XML::Node, inner: String, expand: T::Boolean).returns(String) }
30
30
  def anchor(node, inner, expand)
31
- target = target_attribute(node)
31
+ links = link_attributes(node)
32
32
  extra = expand ? ' align="center" class="float-center"' : ''
33
33
  # Padding on the <a> makes the whole button a clickable target.
34
34
  link_style = "display:inline-block;text-decoration:none;#{BUTTON_PADDING}"
35
- attrs = %(#{pass_through_attributes(node)}href="#{escape_attr(node.attr('href'))}"#{target}#{extra})
35
+ attrs = %(#{pass_through_attributes(node)}href="#{escape_attr(node.attr('href'))}"#{links}#{extra})
36
36
  %(<a #{attrs}#{style_attribute(node, link_style)}>#{inner}</a>)
37
37
  end
38
38
  end
@@ -16,7 +16,7 @@ module ActiveMail
16
16
  raise ArgumentError, '<cta> requires an href attribute' if node.attr('href').to_s.strip.empty?
17
17
 
18
18
  style = ActiveMail.tokens.button_style(class?(node, 'secondary') ? :secondary : :primary)
19
- anchor = %(<a href="#{escape_attr(node.attr('href'))}"#{target_attribute(node)} ) +
19
+ anchor = %(<a href="#{escape_attr(node.attr('href'))}"#{link_attributes(node)} ) +
20
20
  %(style="#{link_style(style)}">#{inner}</a>)
21
21
  bulletproof_button_table(
22
22
  outer_classes: combine_classes(node, 'cta'),
@@ -12,7 +12,7 @@ module ActiveMail
12
12
  def transform(node, inner)
13
13
  attributes = combine_attributes(node, 'menu-item')
14
14
  # No href → a non-link item, not a broken <a href="">; mirrors <button>.
15
- content = node.attr('href') ? %(<a href="#{escape_attr(node.attr('href'))}"#{target_attribute(node)}>#{inner}</a>) : inner
15
+ content = node.attr('href') ? %(<a href="#{escape_attr(node.attr('href'))}"#{link_attributes(node)}>#{inner}</a>) : inner
16
16
  th = ::ActiveMail::Core::INTERIM_TH_TAG
17
17
  %(<#{th} #{attributes}#{style_attribute(node)}>#{content}</#{th}>)
18
18
  end
@@ -86,6 +86,13 @@ module ActiveMail
86
86
  value
87
87
  end
88
88
 
89
+ sig { params(name: Symbol, value: T.untyped).returns(Integer) }
90
+ def self.positive_integer!(name, value)
91
+ raise TypeError, "#{name} must be an Integer, got #{value.inspect} (#{value.class})" unless value.is_a?(Integer)
92
+
93
+ assert_positive_dimension!(name, value)
94
+ end
95
+
89
96
  class Configuration
90
97
  extend T::Sig
91
98
 
@@ -113,16 +120,13 @@ module ActiveMail
113
120
  end
114
121
 
115
122
  sig { returns(Symbol) }
116
- attr_reader :template_engine
117
-
118
- sig { returns(Symbol) }
119
- attr_reader :on_parse_error
123
+ attr_reader :template_engine, :on_parse_error
120
124
 
121
125
  sig { returns(Integer) }
122
- attr_reader :column_count
126
+ attr_reader :column_count, :container_width
123
127
 
124
- sig { returns(Integer) }
125
- attr_reader :container_width
128
+ sig { returns(T.nilable(String)) }
129
+ attr_reader :blank_link_rel
126
130
 
127
131
  # Mutating the returned hash would bypass validate_component!.
128
132
  sig { returns(ActiveMail::ComponentMap) }
@@ -146,6 +150,7 @@ module ActiveMail
146
150
  @inliner = T.let(:premailer, InlinerSetting)
147
151
  @resolved_inliner = T.let(nil, T.nilable(ActiveMail::Inliner::Base))
148
152
  @register_inline_interceptor = T.let(true, T::Boolean)
153
+ @blank_link_rel = T.let('noopener', T.nilable(String))
149
154
  end
150
155
 
151
156
  # Validates eagerly (like sibling setters): a typo fails at boot, not silently mid-delivery.
@@ -180,12 +185,20 @@ module ActiveMail
180
185
 
181
186
  sig { params(value: T.untyped).returns(Integer) }
182
187
  def column_count=(value)
183
- @column_count = positive_integer!(:column_count, value)
188
+ @column_count = ActiveMail.positive_integer!(:column_count, value)
184
189
  end
185
190
 
186
191
  sig { params(value: T.untyped).returns(Integer) }
187
192
  def container_width=(value)
188
- @container_width = positive_integer!(:container_width, value)
193
+ @container_width = ActiveMail.positive_integer!(:container_width, value)
194
+ end
195
+
196
+ sig { params(value: T.untyped).returns(T.nilable(String)) }
197
+ def blank_link_rel=(value)
198
+ raise TypeError, "blank_link_rel must be a String or nil, got #{value.inspect} (#{value.class})" unless value.nil? || value.is_a?(String)
199
+
200
+ normalized = value&.strip
201
+ @blank_link_rel = normalized&.empty? ? nil : normalized
189
202
  end
190
203
 
191
204
  sig { params(value: T.untyped).returns(ActiveMail::ComponentMap) }
@@ -216,14 +229,5 @@ module ActiveMail
216
229
 
217
230
  raise ArgumentError, "unknown inliner #{value.inspect}, expected one of #{INLINERS.keys.inspect}, an Inliner::Base subclass, or an instance"
218
231
  end
219
-
220
- # Integer-only (no to_int): a Float would otherwise be silently truncated
221
- # (12.9 -> 12), contradicting assert_positive_dimension!'s invariant.
222
- sig { params(name: Symbol, value: T.untyped).returns(Integer) }
223
- def positive_integer!(name, value)
224
- raise TypeError, "#{name} must be an Integer, got #{value.inspect} (#{value.class})" unless value.is_a?(Integer)
225
-
226
- ActiveMail.assert_positive_dimension!(name, value)
227
- end
228
232
  end
229
233
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ActiveMail
5
- VERSION = '1.1.1'
5
+ VERSION = '1.2.0'
6
6
  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.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Advitam