activemail 1.0.0 → 1.0.1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -2
  3. data/README.md +19 -20
  4. data/app/assets/stylesheets/{active_mail → activemail}/_settings.scss +1 -1
  5. data/app/assets/stylesheets/{active_mail/active_mail.scss → activemail/activemail.scss} +5 -5
  6. data/app/helpers/{active_mail → activemail}/styles_helper.rb +4 -4
  7. data/app/views/layouts/{active_mail → activemail}/_footer.html.inky-erb +1 -1
  8. data/app/views/layouts/{active_mail → activemail}/mailer.html.inky-erb +5 -5
  9. data/lib/{active_mail → activemail}/components/inky.rb +1 -1
  10. data/lib/{active_mail → activemail}/quality/configuration.rb +1 -1
  11. data/lib/{active_mail → activemail}/quality.rb +1 -1
  12. data/lib/{active_mail → activemail}/rails/engine.rb +9 -9
  13. data/lib/{active_mail → activemail}/version.rb +1 -1
  14. data/lib/activemail.rb +158 -2
  15. data/lib/generators/{active_mail → activemail}/component_generator.rb +2 -2
  16. data/lib/generators/{active_mail → activemail}/install_generator.rb +3 -3
  17. data/lib/generators/{active_mail → activemail}/styles_generator.rb +4 -4
  18. data/lib/generators/{active_mail → activemail}/templates/component.rb.tt +1 -1
  19. data/lib/generators/{active_mail → activemail}/templates/initializer.rb +1 -1
  20. data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-erb +3 -3
  21. data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-haml +3 -3
  22. data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-slim +3 -3
  23. data/lib/generators/{active_mail → activemail}/views_generator.rb +2 -2
  24. data/lib/tasks/{active_mail.rake → activemail.rake} +4 -4
  25. metadata +64 -65
  26. data/lib/active_mail.rb +0 -161
  27. /data/app/assets/stylesheets/{active_mail/_active_mail_tokens.scss.erb → activemail/_activemail_tokens.scss.erb} +0 -0
  28. /data/app/assets/stylesheets/{active_mail → activemail}/_components.scss +0 -0
  29. /data/app/assets/stylesheets/{active_mail → activemail}/_dark.scss +0 -0
  30. /data/app/assets/stylesheets/{active_mail → activemail}/_grid.scss +0 -0
  31. /data/app/assets/stylesheets/{active_mail → activemail}/_utilities.scss +0 -0
  32. /data/app/views/layouts/{active_mail → activemail}/_head.html.inky-erb +0 -0
  33. /data/lib/{active_mail → activemail}/components/base.rb +0 -0
  34. /data/lib/{active_mail → activemail}/components/block_grid.rb +0 -0
  35. /data/lib/{active_mail → activemail}/components/button.rb +0 -0
  36. /data/lib/{active_mail → activemail}/components/callout.rb +0 -0
  37. /data/lib/{active_mail → activemail}/components/center.rb +0 -0
  38. /data/lib/{active_mail → activemail}/components/columns.rb +0 -0
  39. /data/lib/{active_mail → activemail}/components/container.rb +0 -0
  40. /data/lib/{active_mail → activemail}/components/cta.rb +0 -0
  41. /data/lib/{active_mail → activemail}/components/h_line.rb +0 -0
  42. /data/lib/{active_mail → activemail}/components/info_box.rb +0 -0
  43. /data/lib/{active_mail → activemail}/components/menu.rb +0 -0
  44. /data/lib/{active_mail → activemail}/components/menu_item.rb +0 -0
  45. /data/lib/{active_mail → activemail}/components/row.rb +0 -0
  46. /data/lib/{active_mail → activemail}/components/spacer.rb +0 -0
  47. /data/lib/{active_mail → activemail}/components/wrapper.rb +0 -0
  48. /data/lib/{active_mail → activemail}/configuration.rb +0 -0
  49. /data/lib/{active_mail → activemail}/inliner/base.rb +0 -0
  50. /data/lib/{active_mail → activemail}/inliner/interceptor.rb +0 -0
  51. /data/lib/{active_mail → activemail}/inliner/null.rb +0 -0
  52. /data/lib/{active_mail → activemail}/inliner/premailer.rb +0 -0
  53. /data/lib/{active_mail → activemail}/inliner/roadie.rb +0 -0
  54. /data/lib/{active_mail → activemail}/libxml.rb +0 -0
  55. /data/lib/{active_mail → activemail}/parse_error_reporter.rb +0 -0
  56. /data/lib/{active_mail → activemail}/quality/guard.rb +0 -0
  57. /data/lib/{active_mail → activemail}/quality/minitest.rb +0 -0
  58. /data/lib/{active_mail → activemail}/quality/preview_renderer.rb +0 -0
  59. /data/lib/{active_mail → activemail}/quality/render_all.rb +0 -0
  60. /data/lib/{active_mail → activemail}/quality/rspec.rb +0 -0
  61. /data/lib/{active_mail → activemail}/rails/compiled_stylesheet.rb +0 -0
  62. /data/lib/{active_mail → activemail}/rails/template_handler.rb +0 -0
  63. /data/lib/{active_mail → activemail}/tokens.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc582cd5c5cb4c6106a7621144e529eb4b52ba2e89abcea488698c7423f4bf5b
4
- data.tar.gz: ab394297c1480f88fa5c265f7865fd4ea84118715871e06a2d5b0b9205e68dfe
3
+ metadata.gz: e81e755f1fae9c74a61005f5a87483c3a1cf561b79003081a134cb687f658004
4
+ data.tar.gz: 3f669eec79bd3b5df69c83fa3169fafcd7cabcaa014a43ce259399f43d435989
5
5
  SHA512:
6
- metadata.gz: c0ff68ee11f5a0b880e5eaa5c9f07d41ae163a5863bd8d1fcd26be1a60ad79a438d7ddc0130122411426cb23083e3c46a07092eb94ef89798600b51046032209
7
- data.tar.gz: a5f8948b1410fbee84666b37f7b3dfa4358cc72b4cab87404fba8a9638e56e3534bb0492c0103eadde4a4158f6009bc383aba5b620216329e6cc20870254ddc2
6
+ metadata.gz: c5c47394192f0c8ae3da2c6b1832aa3e5083257d782cbd0593b7c59550c2607298404bde407f418980ec1de9a402e9162a189dcf56cda77cfac90df1fa5a2188
7
+ data.tar.gz: 1330453b77892f54770c4ba29d2c3a1e05243a5885785d41b2929cbea1ab7bd10ef595c0a442dcdffb83fe394497f3d479f6e7e0d355efc11aa513a86fee243f
data/CHANGELOG.md CHANGED
@@ -5,11 +5,19 @@ 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.0.1] - 2026-06-15
9
+
10
+ ### Changed
11
+
12
+ - Simplified the gem description and README; removed legacy branding.
13
+ - Unified the namespace to `ActiveMail` (gem name = require path = entry file =
14
+ namespace). Plain `require 'activemail'` now loads everything, so the
15
+ `require: 'active_mail'` workaround is no longer needed.
16
+
8
17
  ## [1.0.0] - 2026-06-13
9
18
 
10
19
  First release of **ActiveMail**, an opinionated, plug & play responsive email
11
- toolkit for Rails. The markup engine derives from `inky-rb` v2 (rebranded to the
12
- `ActiveMail` namespace); the batteries-included framework layer is new.
20
+ toolkit for Rails.
13
21
 
14
22
  ### Added
15
23
 
@@ -57,4 +65,5 @@ toolkit for Rails. The markup engine derives from `inky-rb` v2 (rebranded to the
57
65
  - Rails `>= 7.1` (tested up to 8.1).
58
66
  - Nokogiri `>= 1.16`.
59
67
 
68
+ [1.0.1]: https://github.com/AdVitam/activemail/compare/v1.0.0...v1.0.1
60
69
  [1.0.0]: https://github.com/AdVitam/activemail/releases/tag/v1.0.0
data/README.md CHANGED
@@ -7,8 +7,7 @@ tokens, automatic CSS inlining, generators, and test-time quality guards — so
7
7
  responsive, accessible email renders **out of the box**, with every default
8
8
  overridable.
9
9
 
10
- > Not affiliated with Rails core. The name echoes the `Active*` family by
11
- > convention only.
10
+ A fork and modernization of the now-unmaintained `inky-rb`. Not affiliated with Rails core.
12
11
 
13
12
  Write this:
14
13
 
@@ -43,12 +42,12 @@ renders consistently from Apple Mail to Outlook (Word engine) to Gmail mobile.
43
42
 
44
43
  ```ruby
45
44
  # Gemfile
46
- gem 'activemail'
45
+ gem 'activemail', '~> 1.0'
47
46
  ```
48
47
 
49
48
  ```bash
50
49
  bundle install
51
- bin/rails g active_mail:install
50
+ bin/rails g activemail:install
52
51
  ```
53
52
 
54
53
  The generator drops a commented initializer, a mailer layout, and wires the
@@ -61,7 +60,7 @@ render. CSS is inlined automatically before delivery.
61
60
  ## Configuration
62
61
 
63
62
  ```ruby
64
- # config/initializers/active_mail.rb
63
+ # config/initializers/activemail.rb
65
64
  ActiveMail.configure do |config|
66
65
  config.template_engine = :erb # underlying engine for `.html.inky` (default :erb)
67
66
  config.column_count = 12 # grid columns (default 12)
@@ -108,22 +107,22 @@ ActiveMail.tokens.color(:primary) # => "#2a9d8f"
108
107
 
109
108
  Defaults are neutral (a calm teal `primary`, near-black `text`, white
110
109
  `background`, …) and fully overridable. Under Sprockets the SCSS bridge is a
111
- preprocessed partial; under Propshaft run `rake active_mail:tokens:export` to
112
- materialize a static `_active_mail_tokens.scss`.
110
+ preprocessed partial; under Propshaft run `rake activemail:tokens:export` to
111
+ materialize a static `_activemail_tokens.scss`.
113
112
 
114
113
  ## Styling
115
114
 
116
- The framework stylesheet lives at `active_mail/active_mail` and is themed entirely
115
+ The framework stylesheet lives at `activemail/activemail` and is themed entirely
117
116
  by tokens — no hard-coded brand colors. It includes a fluid-hybrid grid, component
118
117
  hooks (`.button`, `.cta`, `.callout`, `.spacer`, …), utilities, and dark mode.
119
118
 
120
119
  Override at three levels, cheapest first:
121
120
 
122
121
  1. **Tokens** (Ruby) — covers most theming.
123
- 2. **`bin/rails g active_mail:styles`** — eject the SCSS partials into your app to
122
+ 2. **`bin/rails g activemail:styles`** — eject the SCSS partials into your app to
124
123
  edit them; your copies shadow the gem's.
125
- 3. **`bin/rails g active_mail:views`** — eject the default layout + partials
126
- (`app/views/layouts/active_mail/*`); a same-named file in your app wins by
124
+ 3. **`bin/rails g activemail:views`** — eject the default layout + partials
125
+ (`app/views/layouts/activemail/*`); a same-named file in your app wins by
127
126
  Rails path precedence. Put your logo/header/footer here — those are the app's
128
127
  identity, not the gem's.
129
128
 
@@ -265,7 +264,7 @@ the matched Nokogiri node (full DOM access) and the already-transformed inner
265
264
  HTML; return the replacement markup string. The generator scaffolds one:
266
265
 
267
266
  ```bash
268
- bin/rails g active_mail:component Divider
267
+ bin/rails g activemail:component Divider
269
268
  ```
270
269
 
271
270
  The generator namespaces the class as `Components::Divider` (under
@@ -297,19 +296,19 @@ ActiveMail::Core.new(components: { 'button' => MyButton }).release_the_kraken(so
297
296
 
298
297
  | Generator | Purpose |
299
298
  |---|---|
300
- | `active_mail:install` | Initializer + mailer layout + stylesheet wiring (works zero-config). `--haml` / `--slim` supported. |
301
- | `active_mail:views` | Eject the default layout + partials for customization. |
302
- | `active_mail:styles` | Eject the SCSS framework partials for customization. |
303
- | `active_mail:component NAME` | Scaffold a component class + print its register snippet. |
299
+ | `activemail:install` | Initializer + mailer layout + stylesheet wiring (works zero-config). `--haml` / `--slim` supported. |
300
+ | `activemail:views` | Eject the default layout + partials for customization. |
301
+ | `activemail:styles` | Eject the SCSS framework partials for customization. |
302
+ | `activemail:component NAME` | Scaffold a component class + print its register snippet. |
304
303
 
305
304
  ## Testing & quality
306
305
 
307
- An **opt-in** layer (never loaded by `require 'active_mail'`). Require it from your
306
+ An **opt-in** layer (never loaded by `require 'activemail'`). Require it from your
308
307
  test suite.
309
308
 
310
309
  ```ruby
311
310
  # Minitest — require the assertions module explicitly:
312
- require 'active_mail/quality/minitest'
311
+ require 'activemail/quality/minitest'
313
312
 
314
313
  class MailerTest < ActiveSupport::TestCase
315
314
  include ActiveMail::Quality::Minitest
@@ -322,7 +321,7 @@ end
322
321
 
323
322
  ```ruby
324
323
  # RSpec — require the matcher (registers be_a_valid_email when RSpec is loaded):
325
- require 'active_mail/quality/rspec'
324
+ require 'activemail/quality/rspec'
326
325
 
327
326
  expect(rendered_html).to be_a_valid_email
328
327
  ```
@@ -338,7 +337,7 @@ ActiveMail::Quality.configure do |c|
338
337
  end
339
338
  ```
340
339
 
341
- `rake active_mail:emails:render_all` renders every ActionMailer preview to disk
340
+ `rake activemail:emails:render_all` renders every ActionMailer preview to disk
342
341
  for visual diffing and fails on any guard violation among `required_previews`.
343
342
 
344
343
  ## Email-client compatibility policy
@@ -1,6 +1,6 @@
1
1
  // Semantic aliases mapped from the $am-* token vars. All !default so a host
2
2
  // app can pre-declare any value before importing the framework.
3
- @import "active_mail/active_mail_tokens";
3
+ @import "activemail/activemail_tokens";
4
4
 
5
5
  // Typography
6
6
  $am-font-family: $am-font-body !default;
@@ -1,11 +1,11 @@
1
1
  // ActiveMail email framework entry point. Critical styles are inlined by the
2
2
  // configured CSS inliner (Gmail mobile strips <style>); only media-query and
3
3
  // dark-mode enhancements stay in the <head>.
4
- @import "active_mail/settings";
5
- @import "active_mail/grid";
6
- @import "active_mail/components";
7
- @import "active_mail/utilities";
8
- @import "active_mail/dark";
4
+ @import "activemail/settings";
5
+ @import "activemail/grid";
6
+ @import "activemail/components";
7
+ @import "activemail/utilities";
8
+ @import "activemail/dark";
9
9
 
10
10
  body,
11
11
  table,
@@ -5,12 +5,12 @@ module ActiveMail
5
5
  # Embeds the compiled framework CSS as a <style> block so the Premailer adapter
6
6
  # (string-only) inlines it — it can't fetch the stylesheet_link_tag's asset URL.
7
7
  module StylesHelper
8
- FRAMEWORK_STYLESHEET = 'active_mail/active_mail.css'
8
+ FRAMEWORK_STYLESHEET = 'activemail/activemail.css'
9
9
 
10
10
  # '' (not raise) when the asset can't be read — degrades to the link fallback,
11
11
  # but warns, since the email then ships unstyled.
12
- def active_mail_inline_styles
13
- css = active_mail_compiled_css
12
+ def activemail_inline_styles
13
+ css = activemail_compiled_css
14
14
  if css.blank?
15
15
  ActiveMail.log_warning('[activemail] framework stylesheet could not be read from the asset pipeline; ' \
16
16
  'email ships without inlined framework CSS')
@@ -22,7 +22,7 @@ module ActiveMail
22
22
 
23
23
  private
24
24
 
25
- def active_mail_compiled_css
25
+ def activemail_compiled_css
26
26
  ActiveMail::CompiledStylesheet.read(FRAMEWORK_STYLESHEET)
27
27
  end
28
28
  end
@@ -4,7 +4,7 @@
4
4
  <spacer size="24"></spacer>
5
5
  <h-line></h-line>
6
6
  <p class="text-muted text-center">
7
- <%= I18n.t("active_mail.footer.sent_with", default: "Sent with ActiveMail") %>
7
+ <%= I18n.t("activemail.footer.sent_with", default: "Sent with ActiveMail") %>
8
8
  </p>
9
9
  </columns>
10
10
  </row>
@@ -16,19 +16,19 @@
16
16
  </noscript>
17
17
  <![endif]-->
18
18
  <%# Embedded so the Premailer adapter (HTML-string only) can inline the framework CSS. %>
19
- <%= active_mail_inline_styles %>
20
- <%# Fallback for when active_mail_inline_styles cannot read the compiled asset %>
19
+ <%= activemail_inline_styles %>
20
+ <%# Fallback for when activemail_inline_styles cannot read the compiled asset %>
21
21
  <%# (CDN-only host, compile=false without precompile); redundant otherwise, as the %>
22
22
  <%# embed's rules are idempotent once inlined. %>
23
- <%= stylesheet_link_tag "active_mail/active_mail", media: "all" %>
23
+ <%= stylesheet_link_tag "activemail/activemail", media: "all" %>
24
24
  </head>
25
25
 
26
26
  <body style="margin:0;padding:0;word-spacing:normal;">
27
27
  <div role="article" aria-roledescription="email" lang="<%= I18n.locale %>" style="width:100%;">
28
28
  <container>
29
- <%= render "layouts/active_mail/head" %>
29
+ <%= render "layouts/activemail/head" %>
30
30
  <%= yield %>
31
- <%= render "layouts/active_mail/footer" %>
31
+ <%= render "layouts/activemail/footer" %>
32
32
  </container>
33
33
  </div>
34
34
  </body>
@@ -5,7 +5,7 @@ require_relative 'base'
5
5
 
6
6
  module ActiveMail
7
7
  module Components
8
- # Renders a bare <tr> (mirrors inky.js).
8
+ # Renders a bare <tr>, useful inside hand-written tables.
9
9
  class Inky < Base
10
10
  extend T::Sig
11
11
 
@@ -48,7 +48,7 @@ module ActiveMail
48
48
  sig { void }
49
49
  def initialize
50
50
  @guard = T.let(Guard.new, Guard)
51
- @output_dir = T.let('tmp/active_mail_previews', String)
51
+ @output_dir = T.let('tmp/activemail_previews', String)
52
52
  @required_previews = T.let([], T::Array[String])
53
53
  end
54
54
  end
@@ -9,7 +9,7 @@ require_relative 'quality/preview_renderer'
9
9
 
10
10
  module ActiveMail
11
11
  # Opt-in email-quality layer. Host apps require this explicitly from their test
12
- # suite; `require 'active_mail'` must NOT pull it in.
12
+ # suite; `require 'activemail'` must NOT pull it in.
13
13
  module Quality
14
14
  extend T::Sig
15
15
 
@@ -2,34 +2,34 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'rails/engine'
5
- require 'active_mail/rails/compiled_stylesheet'
5
+ require 'activemail/rails/compiled_stylesheet'
6
6
 
7
7
  module ActiveMail
8
8
  module Rails
9
9
  class Engine < ::Rails::Engine
10
- config.annotations.register_extensions('active_mail') { |annotation| /<!--\s*(#{annotation}):?\s*(.*) -->/ } if config.respond_to?(:annotations)
10
+ config.annotations.register_extensions('activemail') { |annotation| /<!--\s*(#{annotation}):?\s*(.*) -->/ } if config.respond_to?(:annotations)
11
11
 
12
12
  # Sprockets only compiles whitelisted assets; the framework entry must be
13
- # reachable as `stylesheet_link_tag "active_mail/active_mail"` from a host.
14
- initializer 'active_mail.assets' do |app|
13
+ # reachable as `stylesheet_link_tag "activemail/activemail"` from a host.
14
+ initializer 'activemail.assets' do |app|
15
15
  # Propshaft exposes config.assets but no #precompile (Sprockets-only).
16
16
  assets = app.config.respond_to?(:assets) ? app.config.assets : nil
17
- assets.precompile += %w[active_mail/active_mail.css] if assets.respond_to?(:precompile)
17
+ assets.precompile += %w[activemail/activemail.css] if assets.respond_to?(:precompile)
18
18
  end
19
19
 
20
- initializer 'active_mail.action_mailer' do
20
+ initializer 'activemail.action_mailer' do
21
21
  ActiveSupport.on_load(:action_mailer) do
22
- require 'active_mail/inliner/interceptor'
22
+ require 'activemail/inliner/interceptor'
23
23
  # The interceptor honors config.register_inline_interceptor (and inliner =
24
24
  # :null) at delivery time — a boot-time check would precede host config.
25
25
  register_interceptor ActiveMail::Inliner::Interceptor
26
- # active_mail_inline_styles must be available to mailer layouts/views.
26
+ # activemail_inline_styles must be available to mailer layouts/views.
27
27
  helper ActiveMail::StylesHelper
28
28
  end
29
29
  end
30
30
 
31
31
  rake_tasks do
32
- load File.expand_path('../../tasks/active_mail.rake', __dir__)
32
+ load File.expand_path('../../tasks/activemail.rake', __dir__)
33
33
  end
34
34
  end
35
35
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ActiveMail
5
- VERSION = '1.0.0'
5
+ VERSION = '1.0.1'
6
6
  end
data/lib/activemail.rb CHANGED
@@ -1,5 +1,161 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- # Bundler auto-requires the gem name ('activemail'); the library lives in 'active_mail'.
5
- require 'active_mail'
4
+ require 'nokogiri'
5
+ require 'sorbet-runtime'
6
+
7
+ require_relative 'activemail/version'
8
+ require_relative 'activemail/components/base'
9
+ require_relative 'activemail/components/button'
10
+ require_relative 'activemail/components/row'
11
+ require_relative 'activemail/components/columns'
12
+ require_relative 'activemail/components/container'
13
+ require_relative 'activemail/components/inky'
14
+ require_relative 'activemail/components/block_grid'
15
+ require_relative 'activemail/components/menu'
16
+ require_relative 'activemail/components/menu_item'
17
+ require_relative 'activemail/components/center'
18
+ require_relative 'activemail/components/callout'
19
+ require_relative 'activemail/components/spacer'
20
+ require_relative 'activemail/components/h_line'
21
+ require_relative 'activemail/components/wrapper'
22
+ require_relative 'activemail/components/cta'
23
+ require_relative 'activemail/components/info_box'
24
+ require_relative 'activemail/configuration'
25
+ require_relative 'activemail/inliner/interceptor'
26
+ require_relative 'activemail/parse_error_reporter'
27
+
28
+ module ActiveMail
29
+ class ParseError < StandardError; end
30
+
31
+ class Core
32
+ extend T::Sig
33
+
34
+ # Nokogiri cannot parse a bare <th> outside a <tr>; components that emit
35
+ # <th> use this placeholder, swapped back at the end.
36
+ INTERIM_TH_TAG = 'activemail-interim-th'
37
+ INTERIM_TH_TAG_REGEX = T.let(%r{(?<=<|</)#{Regexp.escape(INTERIM_TH_TAG)}}, Regexp)
38
+
39
+ DEFAULT_COMPONENTS = T.let(
40
+ {
41
+ 'button' => ActiveMail::Components::Button,
42
+ 'row' => ActiveMail::Components::Row,
43
+ 'columns' => ActiveMail::Components::Columns,
44
+ 'container' => ActiveMail::Components::Container,
45
+ 'inky' => ActiveMail::Components::Inky,
46
+ 'block-grid' => ActiveMail::Components::BlockGrid,
47
+ 'menu' => ActiveMail::Components::Menu,
48
+ 'item' => ActiveMail::Components::MenuItem,
49
+ 'center' => ActiveMail::Components::Center,
50
+ 'callout' => ActiveMail::Components::Callout,
51
+ 'spacer' => ActiveMail::Components::Spacer,
52
+ 'h-line' => ActiveMail::Components::HLine,
53
+ 'wrapper' => ActiveMail::Components::Wrapper
54
+ }.freeze,
55
+ ActiveMail::ComponentMap
56
+ )
57
+
58
+ sig { returns(Integer) }
59
+ attr_reader :column_count, :container_width
60
+
61
+ sig { returns(T::Hash[String, ActiveMail::Components::Base]) }
62
+ attr_reader :component_instances
63
+
64
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
65
+ def initialize(options = {})
66
+ config = ::ActiveMail.configuration
67
+ @component_instances = T.let(build_components(config, options[:components]), T::Hash[String, ActiveMail::Components::Base])
68
+ @column_count = T.let(ActiveMail.assert_positive_dimension!(:column_count, options[:column_count] || config.column_count), Integer)
69
+ @container_width = T.let(ActiveMail.assert_positive_dimension!(:container_width, options[:container_width] || config.container_width), Integer)
70
+ end
71
+
72
+ # Object, not String: ActionView::OutputBuffer is no longer a String since Rails 7.1.
73
+ sig { params(html_string: Object).returns(String) }
74
+ def release_the_kraken(html_string)
75
+ raws, str = extract_raws(normalize_input(html_string))
76
+ parse_cmd = ::ActiveMail.full_document?(str) ? :parse : :fragment
77
+ html = Nokogiri::HTML.public_send(parse_cmd, str)
78
+ ParseErrorReporter.new(component_instances.keys).call(html.errors)
79
+ transform_doc(html)
80
+ string = html.to_html
81
+ string = string.gsub(INTERIM_TH_TAG_REGEX, 'th')
82
+ # Needle is a literal U+00A0 (Nokogiri decodes the nbsp entity to one); re-encode
83
+ # it to the entity for email clients that mishandle raw NBSP bytes.
84
+ string = string.gsub(' ', '&nbsp;')
85
+ re_inject_raws(string, raws)
86
+ end
87
+
88
+ sig { params(elem: Nokogiri::XML::Node).returns(Nokogiri::XML::Node) }
89
+ def transform_doc(elem)
90
+ if elem.respond_to?(:children)
91
+ elem.children.each { |child| transform_doc(child) }
92
+ markup = component_factory(elem)
93
+ elem.replace(markup) if markup
94
+ end
95
+ elem
96
+ end
97
+
98
+ sig { params(node: Nokogiri::XML::Node).returns(T.nilable(String)) }
99
+ def component_factory(node)
100
+ component = component_instances[node.name]
101
+ return unless component
102
+
103
+ # Nokogiri::NodeSet has no #join; map to String first.
104
+ inner = node.children.map(&:to_s).join # rubocop:disable Style/MapJoin
105
+ component.transform(node, inner)
106
+ end
107
+
108
+ sig { params(string: String).returns([T::Array[String], String]) }
109
+ def extract_raws(string)
110
+ raws = []
111
+ i = 0
112
+ # Only the tags + content, across lines: surrounding whitespace is left in
113
+ # place (true pass-through; eating adjacent newlines corrupts <pre>/inline text).
114
+ regex = %r{< *raw *>([\s\S]*?)</ *raw *>}i
115
+ str = string
116
+ while (raw = str.match(regex))
117
+ raws[i] = T.must(raw[1])
118
+ str = str.sub(regex, "###RAW#{i}###")
119
+ i += 1
120
+ end
121
+ [raws, str]
122
+ end
123
+
124
+ sig { params(string: String, raws: T::Array[String]).returns(String) }
125
+ def re_inject_raws(string, raws)
126
+ str = string
127
+ raws.each_with_index do |val, i|
128
+ # Block form: the 2-arg String#sub would expand \0/\1/\& in val.
129
+ str = str.sub("###RAW#{i}###") { val }
130
+ end
131
+ str = str.html_safe if str.respond_to?(:html_safe)
132
+ str
133
+ end
134
+
135
+ private
136
+
137
+ # Internal transpilation details, not public surface (column_count/container_width stay public).
138
+ private :component_instances, :transform_doc, :component_factory, :extract_raws, :re_inject_raws
139
+
140
+ sig { params(config: ActiveMail::Configuration, overrides: T.untyped).returns(T::Hash[String, ActiveMail::Components::Base]) }
141
+ def build_components(config, overrides)
142
+ # Lookup is by node name (String); a Symbol key would never match.
143
+ overrides = (overrides || {}).transform_keys(&:to_s)
144
+ overrides.each { |tag, klass| ActiveMail::Components.validate_component!(tag, klass) }
145
+ DEFAULT_COMPONENTS.merge(config.components).merge(overrides).transform_values { |klass| klass.new(self) }
146
+ end
147
+
148
+ sig { params(html_string: Object).returns(String) }
149
+ def normalize_input(html_string)
150
+ html_string = html_string.to_s
151
+ html_string = html_string.dup.force_encoding(Encoding::UTF_8) if html_string.encoding == Encoding::BINARY
152
+ html_string = ::ActiveMail.scrub_invalid_bytes(html_string) unless html_string.valid_encoding?
153
+ html_string.gsub(/doctype/i, 'DOCTYPE')
154
+ end
155
+ end
156
+ end
157
+
158
+ if defined?(Rails::Engine)
159
+ require 'activemail/rails/engine'
160
+ require 'activemail/rails/template_handler'
161
+ end
@@ -6,7 +6,7 @@ require 'rails/generators/named_base'
6
6
  module ActiveMail
7
7
  module Generators
8
8
  class ComponentGenerator < ::Rails::Generators::NamedBase
9
- desc 'Scaffold an ActiveMail component class (rails g active_mail:component Cta)'
9
+ desc 'Scaffold an ActiveMail component class (rails g activemail:component Cta)'
10
10
  source_root File.join(File.dirname(__FILE__), 'templates')
11
11
 
12
12
  def create_component
@@ -14,7 +14,7 @@ module ActiveMail
14
14
  end
15
15
 
16
16
  def show_register_snippet
17
- say "\nRegister the component in config/initializers/active_mail.rb:", :green
17
+ say "\nRegister the component in config/initializers/activemail.rb:", :green
18
18
  say %( config.register_component "#{tag_name}", Components::#{class_name})
19
19
  say '(top-level Components:: — rename the module if it collides in your app)', :yellow
20
20
  say "\nThen use <#{tag_name}>…</#{tag_name}> in your ActiveMail views.\n"
@@ -13,7 +13,7 @@ module ActiveMail
13
13
  class_option :slim, desc: 'Generate the layout in Slim', type: :boolean
14
14
 
15
15
  def create_initializer
16
- template 'initializer.rb', File.join('config', 'initializers', 'active_mail.rb')
16
+ template 'initializer.rb', File.join('config', 'initializers', 'activemail.rb')
17
17
  end
18
18
 
19
19
  # A plain mailer.html.erb would win over the generated inky layout; keep it.
@@ -31,12 +31,12 @@ module ActiveMail
31
31
 
32
32
  def show_readme
33
33
  say "\nActiveMail installed.", :green
34
- say ' • config/initializers/active_mail.rb — configure tokens, inliner, components.'
34
+ say ' • config/initializers/activemail.rb — configure tokens, inliner, components.'
35
35
  say " • app/views/layouts/#{layout_name.underscore}.html.inky-#{extension} — your mailer layout."
36
36
  say "\nPoint your mailers at the layout, e.g. `layout \"#{layout_name.underscore}\"`, and"
37
37
  say "name views *.html.inky-#{extension} to enable ActiveMail markup."
38
38
  say "\nCustomize styling via Ruby tokens in the initializer (config.tokens.color/font/spacing),"
39
- say 'or run `rails g active_mail:styles` to eject and edit the SCSS partials.'
39
+ say 'or run `rails g activemail:styles` to eject and edit the SCSS partials.'
40
40
  end
41
41
 
42
42
  private
@@ -6,12 +6,12 @@ module ActiveMail
6
6
  module Generators
7
7
  class StylesGenerator < ::Rails::Generators::Base
8
8
  desc 'Copy ActiveMail framework SCSS partials into the host app for customization'
9
- source_root File.expand_path('../../../app/assets/stylesheets/active_mail', __dir__)
9
+ source_root File.expand_path('../../../app/assets/stylesheets/activemail', __dir__)
10
10
 
11
- TARGET_DIR = File.join('app', 'assets', 'stylesheets', 'active_mail')
11
+ TARGET_DIR = File.join('app', 'assets', 'stylesheets', 'activemail')
12
12
 
13
13
  # The .scss.erb token bridge is deliberately not ejected: it needs ERB
14
- # preprocessing, and tokens come from Ruby config (rake active_mail:tokens:export).
14
+ # preprocessing, and tokens come from Ruby config (rake activemail:tokens:export).
15
15
  def copy_styles
16
16
  Dir.children(self.class.source_root).each do |name|
17
17
  next if name.end_with?('.erb')
@@ -23,7 +23,7 @@ module ActiveMail
23
23
  def show_readme
24
24
  say "\nEjected the ActiveMail SCSS partials to #{TARGET_DIR}.", :green
25
25
  say 'Token values come from Ruby (config.tokens.color/font/spacing). For a static'
26
- say 'SCSS partial of those values, run `rake active_mail:tokens:export`.'
26
+ say 'SCSS partial of those values, run `rake activemail:tokens:export`.'
27
27
  end
28
28
  end
29
29
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Components
5
- # Register in config/initializers/active_mail.rb:
5
+ # Register in config/initializers/activemail.rb:
6
6
  # config.register_component "<%= tag_name %>", Components::<%= class_name %>
7
7
  class <%= class_name %> < ActiveMail::Components::Base
8
8
  def transform(node, inner)
@@ -27,6 +27,6 @@ ActiveMail.configure do |config|
27
27
  # config.tokens.spacing :lg, "32px"
28
28
 
29
29
  # Register components (built-ins like ActiveMail::Components::Cta, or your own
30
- # Components::* from `rails g active_mail:component`).
30
+ # Components::* from `rails g activemail:component`).
31
31
  # config.register_component "cta", ActiveMail::Components::Cta
32
32
  end
@@ -15,9 +15,9 @@
15
15
  </xml>
16
16
  </noscript>
17
17
  <![endif]-->
18
- <%%= active_mail_inline_styles %>
19
- <%%# Fallback for when active_mail_inline_styles cannot read the compiled asset. %>
20
- <%%= stylesheet_link_tag "active_mail/active_mail", media: "all" %>
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
21
  </head>
22
22
 
23
23
  <body style="margin:0;padding:0;word-spacing:normal;">
@@ -8,9 +8,9 @@
8
8
  %meta{name: "supported-color-schemes", content: "light dark"}
9
9
  :plain
10
10
  <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
11
- = active_mail_inline_styles
12
- -# Fallback for when active_mail_inline_styles cannot read the compiled asset.
13
- = stylesheet_link_tag "active_mail/active_mail", media: "all"
11
+ = activemail_inline_styles
12
+ -# Fallback for when activemail_inline_styles cannot read the compiled asset.
13
+ = stylesheet_link_tag "activemail/activemail", media: "all"
14
14
  %body{style: "margin:0;padding:0;word-spacing:normal;"}
15
15
  %div{role: "article", "aria-roledescription" => "email", lang: I18n.locale, style: "width:100%;"}
16
16
  %container
@@ -7,9 +7,9 @@ html lang=I18n.locale xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-
7
7
  meta name="color-scheme" content="light dark"
8
8
  meta name="supported-color-schemes" content="light dark"
9
9
  | <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
10
- = active_mail_inline_styles
11
- / Fallback for when active_mail_inline_styles cannot read the compiled asset.
12
- = stylesheet_link_tag "active_mail/active_mail", media: "all"
10
+ = activemail_inline_styles
11
+ / Fallback for when activemail_inline_styles cannot read the compiled asset.
12
+ = stylesheet_link_tag "activemail/activemail", media: "all"
13
13
  body style="margin:0;padding:0;word-spacing:normal;"
14
14
  div role="article" aria-roledescription="email" lang=I18n.locale style="width:100%;"
15
15
  container
@@ -6,10 +6,10 @@ module ActiveMail
6
6
  module Generators
7
7
  class ViewsGenerator < ::Rails::Generators::Base
8
8
  desc 'Copy ActiveMail default layout views into the host app for customization'
9
- source_root File.expand_path('../../../app/views/layouts/active_mail', __dir__)
9
+ source_root File.expand_path('../../../app/views/layouts/activemail', __dir__)
10
10
 
11
11
  def copy_views
12
- directory '.', File.join('app', 'views', 'layouts', 'active_mail')
12
+ directory '.', File.join('app', 'views', 'layouts', 'activemail')
13
13
  end
14
14
  end
15
15
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_mail'
3
+ require 'activemail'
4
4
  require 'fileutils'
5
5
 
6
- namespace :active_mail do
6
+ namespace :activemail do
7
7
  namespace :tokens do
8
8
  desc 'Export design tokens to a static SCSS partial (for Propshaft apps that cannot preprocess .scss.erb)'
9
9
  # :environment so the host initializer's config.tokens overrides are loaded.
10
10
  task :export, [:path] => :environment do |_task, args|
11
- path = args[:path] || 'app/assets/stylesheets/active_mail/_active_mail_tokens.scss'
11
+ path = args[:path] || 'app/assets/stylesheets/activemail/_activemail_tokens.scss'
12
12
  FileUtils.mkdir_p(File.dirname(path))
13
13
  File.write(path, ActiveMail.scss_variables)
14
14
  puts "Wrote #{ActiveMail.tokens.colors.size + ActiveMail.tokens.fonts.size + ActiveMail.tokens.spacings.size} tokens to #{path}"
@@ -18,7 +18,7 @@ namespace :active_mail do
18
18
  namespace :emails do
19
19
  desc 'Render every host mailer preview to disk and run the quality guard on each'
20
20
  task render_all: :environment do
21
- require 'active_mail/quality/render_all'
21
+ require 'activemail/quality/render_all'
22
22
 
23
23
  config = ActiveMail::Quality.config
24
24
  output_root = defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join(config.output_dir) : Pathname(config.output_dir)
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.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Advitam
@@ -52,11 +52,10 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.5'
54
54
  description: |
55
- ActiveMail turns simple, semantic tags into the bulletproof, MSO-safe table markup
56
- email clients require, with a batteries-included Rails layer on top themeable SCSS,
57
- dark mode, design tokens, a pluggable CSS-inliner (premailer/roadie) and generators.
58
- email:
59
- - tech@advitam.fr
55
+ ActiveMail is a plug & play gem for sending beautiful emails, written entirely in Ruby.
56
+ It turns simple, semantic tags into responsive, bulletproof email markup and ships with
57
+ modern standards — dark mode and accessibility out of the box. Batteries included:
58
+ design tokens, components, automatic CSS inlining, and generators, all overridable.
60
59
  executables: []
61
60
  extensions: []
62
61
  extra_rdoc_files: []
@@ -64,65 +63,64 @@ files:
64
63
  - CHANGELOG.md
65
64
  - LICENSE.txt
66
65
  - README.md
67
- - app/assets/stylesheets/active_mail/_active_mail_tokens.scss.erb
68
- - app/assets/stylesheets/active_mail/_components.scss
69
- - app/assets/stylesheets/active_mail/_dark.scss
70
- - app/assets/stylesheets/active_mail/_grid.scss
71
- - app/assets/stylesheets/active_mail/_settings.scss
72
- - app/assets/stylesheets/active_mail/_utilities.scss
73
- - app/assets/stylesheets/active_mail/active_mail.scss
74
- - app/helpers/active_mail/styles_helper.rb
75
- - app/views/layouts/active_mail/_footer.html.inky-erb
76
- - app/views/layouts/active_mail/_head.html.inky-erb
77
- - app/views/layouts/active_mail/mailer.html.inky-erb
78
- - lib/active_mail.rb
79
- - lib/active_mail/components/base.rb
80
- - lib/active_mail/components/block_grid.rb
81
- - lib/active_mail/components/button.rb
82
- - lib/active_mail/components/callout.rb
83
- - lib/active_mail/components/center.rb
84
- - lib/active_mail/components/columns.rb
85
- - lib/active_mail/components/container.rb
86
- - lib/active_mail/components/cta.rb
87
- - lib/active_mail/components/h_line.rb
88
- - lib/active_mail/components/info_box.rb
89
- - lib/active_mail/components/inky.rb
90
- - lib/active_mail/components/menu.rb
91
- - lib/active_mail/components/menu_item.rb
92
- - lib/active_mail/components/row.rb
93
- - lib/active_mail/components/spacer.rb
94
- - lib/active_mail/components/wrapper.rb
95
- - lib/active_mail/configuration.rb
96
- - lib/active_mail/inliner/base.rb
97
- - lib/active_mail/inliner/interceptor.rb
98
- - lib/active_mail/inliner/null.rb
99
- - lib/active_mail/inliner/premailer.rb
100
- - lib/active_mail/inliner/roadie.rb
101
- - lib/active_mail/libxml.rb
102
- - lib/active_mail/parse_error_reporter.rb
103
- - lib/active_mail/quality.rb
104
- - lib/active_mail/quality/configuration.rb
105
- - lib/active_mail/quality/guard.rb
106
- - lib/active_mail/quality/minitest.rb
107
- - lib/active_mail/quality/preview_renderer.rb
108
- - lib/active_mail/quality/render_all.rb
109
- - lib/active_mail/quality/rspec.rb
110
- - lib/active_mail/rails/compiled_stylesheet.rb
111
- - lib/active_mail/rails/engine.rb
112
- - lib/active_mail/rails/template_handler.rb
113
- - lib/active_mail/tokens.rb
114
- - lib/active_mail/version.rb
66
+ - app/assets/stylesheets/activemail/_activemail_tokens.scss.erb
67
+ - app/assets/stylesheets/activemail/_components.scss
68
+ - app/assets/stylesheets/activemail/_dark.scss
69
+ - app/assets/stylesheets/activemail/_grid.scss
70
+ - app/assets/stylesheets/activemail/_settings.scss
71
+ - app/assets/stylesheets/activemail/_utilities.scss
72
+ - app/assets/stylesheets/activemail/activemail.scss
73
+ - app/helpers/activemail/styles_helper.rb
74
+ - app/views/layouts/activemail/_footer.html.inky-erb
75
+ - app/views/layouts/activemail/_head.html.inky-erb
76
+ - app/views/layouts/activemail/mailer.html.inky-erb
115
77
  - lib/activemail.rb
116
- - lib/generators/active_mail/component_generator.rb
117
- - lib/generators/active_mail/install_generator.rb
118
- - lib/generators/active_mail/styles_generator.rb
119
- - lib/generators/active_mail/templates/component.rb.tt
120
- - lib/generators/active_mail/templates/initializer.rb
121
- - lib/generators/active_mail/templates/mailer_layout.html.inky-erb
122
- - lib/generators/active_mail/templates/mailer_layout.html.inky-haml
123
- - lib/generators/active_mail/templates/mailer_layout.html.inky-slim
124
- - lib/generators/active_mail/views_generator.rb
125
- - lib/tasks/active_mail.rake
78
+ - lib/activemail/components/base.rb
79
+ - lib/activemail/components/block_grid.rb
80
+ - lib/activemail/components/button.rb
81
+ - lib/activemail/components/callout.rb
82
+ - lib/activemail/components/center.rb
83
+ - lib/activemail/components/columns.rb
84
+ - lib/activemail/components/container.rb
85
+ - lib/activemail/components/cta.rb
86
+ - lib/activemail/components/h_line.rb
87
+ - lib/activemail/components/info_box.rb
88
+ - lib/activemail/components/inky.rb
89
+ - lib/activemail/components/menu.rb
90
+ - lib/activemail/components/menu_item.rb
91
+ - lib/activemail/components/row.rb
92
+ - lib/activemail/components/spacer.rb
93
+ - lib/activemail/components/wrapper.rb
94
+ - lib/activemail/configuration.rb
95
+ - lib/activemail/inliner/base.rb
96
+ - lib/activemail/inliner/interceptor.rb
97
+ - lib/activemail/inliner/null.rb
98
+ - lib/activemail/inliner/premailer.rb
99
+ - lib/activemail/inliner/roadie.rb
100
+ - lib/activemail/libxml.rb
101
+ - lib/activemail/parse_error_reporter.rb
102
+ - lib/activemail/quality.rb
103
+ - lib/activemail/quality/configuration.rb
104
+ - lib/activemail/quality/guard.rb
105
+ - lib/activemail/quality/minitest.rb
106
+ - lib/activemail/quality/preview_renderer.rb
107
+ - lib/activemail/quality/render_all.rb
108
+ - lib/activemail/quality/rspec.rb
109
+ - lib/activemail/rails/compiled_stylesheet.rb
110
+ - lib/activemail/rails/engine.rb
111
+ - lib/activemail/rails/template_handler.rb
112
+ - lib/activemail/tokens.rb
113
+ - lib/activemail/version.rb
114
+ - lib/generators/activemail/component_generator.rb
115
+ - lib/generators/activemail/install_generator.rb
116
+ - lib/generators/activemail/styles_generator.rb
117
+ - lib/generators/activemail/templates/component.rb.tt
118
+ - lib/generators/activemail/templates/initializer.rb
119
+ - lib/generators/activemail/templates/mailer_layout.html.inky-erb
120
+ - lib/generators/activemail/templates/mailer_layout.html.inky-haml
121
+ - lib/generators/activemail/templates/mailer_layout.html.inky-slim
122
+ - lib/generators/activemail/views_generator.rb
123
+ - lib/tasks/activemail.rake
126
124
  homepage: https://github.com/AdVitam/activemail
127
125
  licenses:
128
126
  - MIT
@@ -147,5 +145,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
145
  requirements: []
148
146
  rubygems_version: 3.6.9
149
147
  specification_version: 4
150
- summary: Opinionated, plug & play responsive email toolkit for Rails.
148
+ summary: Plug & play gem to send modern emails, entirely in Ruby, with dark mode and
149
+ more out of the box.
151
150
  test_files: []
data/lib/active_mail.rb DELETED
@@ -1,161 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require 'nokogiri'
5
- require 'sorbet-runtime'
6
-
7
- require_relative 'active_mail/version'
8
- require_relative 'active_mail/components/base'
9
- require_relative 'active_mail/components/button'
10
- require_relative 'active_mail/components/row'
11
- require_relative 'active_mail/components/columns'
12
- require_relative 'active_mail/components/container'
13
- require_relative 'active_mail/components/inky'
14
- require_relative 'active_mail/components/block_grid'
15
- require_relative 'active_mail/components/menu'
16
- require_relative 'active_mail/components/menu_item'
17
- require_relative 'active_mail/components/center'
18
- require_relative 'active_mail/components/callout'
19
- require_relative 'active_mail/components/spacer'
20
- require_relative 'active_mail/components/h_line'
21
- require_relative 'active_mail/components/wrapper'
22
- require_relative 'active_mail/components/cta'
23
- require_relative 'active_mail/components/info_box'
24
- require_relative 'active_mail/configuration'
25
- require_relative 'active_mail/inliner/interceptor'
26
- require_relative 'active_mail/parse_error_reporter'
27
-
28
- module ActiveMail
29
- class ParseError < StandardError; end
30
-
31
- class Core
32
- extend T::Sig
33
-
34
- # Nokogiri cannot parse a bare <th> outside a <tr>; components that emit
35
- # <th> use this placeholder, swapped back at the end.
36
- INTERIM_TH_TAG = 'active-mail-interim-th'
37
- INTERIM_TH_TAG_REGEX = T.let(%r{(?<=<|</)#{Regexp.escape(INTERIM_TH_TAG)}}, Regexp)
38
-
39
- DEFAULT_COMPONENTS = T.let(
40
- {
41
- 'button' => ActiveMail::Components::Button,
42
- 'row' => ActiveMail::Components::Row,
43
- 'columns' => ActiveMail::Components::Columns,
44
- 'container' => ActiveMail::Components::Container,
45
- 'inky' => ActiveMail::Components::Inky,
46
- 'block-grid' => ActiveMail::Components::BlockGrid,
47
- 'menu' => ActiveMail::Components::Menu,
48
- 'item' => ActiveMail::Components::MenuItem,
49
- 'center' => ActiveMail::Components::Center,
50
- 'callout' => ActiveMail::Components::Callout,
51
- 'spacer' => ActiveMail::Components::Spacer,
52
- 'h-line' => ActiveMail::Components::HLine,
53
- 'wrapper' => ActiveMail::Components::Wrapper
54
- }.freeze,
55
- ActiveMail::ComponentMap
56
- )
57
-
58
- sig { returns(Integer) }
59
- attr_reader :column_count, :container_width
60
-
61
- sig { returns(T::Hash[String, ActiveMail::Components::Base]) }
62
- attr_reader :component_instances
63
-
64
- sig { params(options: T::Hash[Symbol, T.untyped]).void }
65
- def initialize(options = {})
66
- config = ::ActiveMail.configuration
67
- @component_instances = T.let(build_components(config, options[:components]), T::Hash[String, ActiveMail::Components::Base])
68
- @column_count = T.let(ActiveMail.assert_positive_dimension!(:column_count, options[:column_count] || config.column_count), Integer)
69
- @container_width = T.let(ActiveMail.assert_positive_dimension!(:container_width, options[:container_width] || config.container_width), Integer)
70
- end
71
-
72
- # Object, not String: ActionView::OutputBuffer is no longer a String since Rails 7.1.
73
- sig { params(html_string: Object).returns(String) }
74
- def release_the_kraken(html_string)
75
- raws, str = extract_raws(normalize_input(html_string))
76
- parse_cmd = ::ActiveMail.full_document?(str) ? :parse : :fragment
77
- html = Nokogiri::HTML.public_send(parse_cmd, str)
78
- ParseErrorReporter.new(component_instances.keys).call(html.errors)
79
- transform_doc(html)
80
- string = html.to_html
81
- string = string.gsub(INTERIM_TH_TAG_REGEX, 'th')
82
- # Needle is a literal U+00A0 (Nokogiri decodes the nbsp entity to one); re-encode
83
- # it to the entity for email clients that mishandle raw NBSP bytes.
84
- string = string.gsub(' ', '&nbsp;')
85
- re_inject_raws(string, raws)
86
- end
87
-
88
- sig { params(elem: Nokogiri::XML::Node).returns(Nokogiri::XML::Node) }
89
- def transform_doc(elem)
90
- if elem.respond_to?(:children)
91
- elem.children.each { |child| transform_doc(child) }
92
- markup = component_factory(elem)
93
- elem.replace(markup) if markup
94
- end
95
- elem
96
- end
97
-
98
- sig { params(node: Nokogiri::XML::Node).returns(T.nilable(String)) }
99
- def component_factory(node)
100
- component = component_instances[node.name]
101
- return unless component
102
-
103
- # Nokogiri::NodeSet has no #join; map to String first.
104
- inner = node.children.map(&:to_s).join # rubocop:disable Style/MapJoin
105
- component.transform(node, inner)
106
- end
107
-
108
- sig { params(string: String).returns([T::Array[String], String]) }
109
- def extract_raws(string)
110
- raws = []
111
- i = 0
112
- # Only the tags + content, across lines: surrounding whitespace is left in
113
- # place (true pass-through; eating adjacent newlines corrupts <pre>/inline text).
114
- regex = %r{< *raw *>([\s\S]*?)</ *raw *>}i
115
- str = string
116
- while (raw = str.match(regex))
117
- raws[i] = T.must(raw[1])
118
- str = str.sub(regex, "###RAW#{i}###")
119
- i += 1
120
- end
121
- [raws, str]
122
- end
123
-
124
- sig { params(string: String, raws: T::Array[String]).returns(String) }
125
- def re_inject_raws(string, raws)
126
- str = string
127
- raws.each_with_index do |val, i|
128
- # Block form: the 2-arg String#sub would expand \0/\1/\& in val.
129
- str = str.sub("###RAW#{i}###") { val }
130
- end
131
- str = str.html_safe if str.respond_to?(:html_safe)
132
- str
133
- end
134
-
135
- private
136
-
137
- # Internal transpilation details, not public surface (column_count/container_width stay public).
138
- private :component_instances, :transform_doc, :component_factory, :extract_raws, :re_inject_raws
139
-
140
- sig { params(config: ActiveMail::Configuration, overrides: T.untyped).returns(T::Hash[String, ActiveMail::Components::Base]) }
141
- def build_components(config, overrides)
142
- # Lookup is by node name (String); a Symbol key would never match.
143
- overrides = (overrides || {}).transform_keys(&:to_s)
144
- overrides.each { |tag, klass| ActiveMail::Components.validate_component!(tag, klass) }
145
- DEFAULT_COMPONENTS.merge(config.components).merge(overrides).transform_values { |klass| klass.new(self) }
146
- end
147
-
148
- sig { params(html_string: Object).returns(String) }
149
- def normalize_input(html_string)
150
- html_string = html_string.to_s
151
- html_string = html_string.dup.force_encoding(Encoding::UTF_8) if html_string.encoding == Encoding::BINARY
152
- html_string = ::ActiveMail.scrub_invalid_bytes(html_string) unless html_string.valid_encoding?
153
- html_string.gsub(/doctype/i, 'DOCTYPE')
154
- end
155
- end
156
- end
157
-
158
- if defined?(Rails::Engine)
159
- require 'active_mail/rails/engine'
160
- require 'active_mail/rails/template_handler'
161
- end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes