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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -2
- data/README.md +19 -20
- data/app/assets/stylesheets/{active_mail → activemail}/_settings.scss +1 -1
- data/app/assets/stylesheets/{active_mail/active_mail.scss → activemail/activemail.scss} +5 -5
- data/app/helpers/{active_mail → activemail}/styles_helper.rb +4 -4
- data/app/views/layouts/{active_mail → activemail}/_footer.html.inky-erb +1 -1
- data/app/views/layouts/{active_mail → activemail}/mailer.html.inky-erb +5 -5
- data/lib/{active_mail → activemail}/components/inky.rb +1 -1
- data/lib/{active_mail → activemail}/quality/configuration.rb +1 -1
- data/lib/{active_mail → activemail}/quality.rb +1 -1
- data/lib/{active_mail → activemail}/rails/engine.rb +9 -9
- data/lib/{active_mail → activemail}/version.rb +1 -1
- data/lib/activemail.rb +158 -2
- data/lib/generators/{active_mail → activemail}/component_generator.rb +2 -2
- data/lib/generators/{active_mail → activemail}/install_generator.rb +3 -3
- data/lib/generators/{active_mail → activemail}/styles_generator.rb +4 -4
- data/lib/generators/{active_mail → activemail}/templates/component.rb.tt +1 -1
- data/lib/generators/{active_mail → activemail}/templates/initializer.rb +1 -1
- data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-erb +3 -3
- data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-haml +3 -3
- data/lib/generators/{active_mail → activemail}/templates/mailer_layout.html.inky-slim +3 -3
- data/lib/generators/{active_mail → activemail}/views_generator.rb +2 -2
- data/lib/tasks/{active_mail.rake → activemail.rake} +4 -4
- metadata +64 -65
- data/lib/active_mail.rb +0 -161
- /data/app/assets/stylesheets/{active_mail/_active_mail_tokens.scss.erb → activemail/_activemail_tokens.scss.erb} +0 -0
- /data/app/assets/stylesheets/{active_mail → activemail}/_components.scss +0 -0
- /data/app/assets/stylesheets/{active_mail → activemail}/_dark.scss +0 -0
- /data/app/assets/stylesheets/{active_mail → activemail}/_grid.scss +0 -0
- /data/app/assets/stylesheets/{active_mail → activemail}/_utilities.scss +0 -0
- /data/app/views/layouts/{active_mail → activemail}/_head.html.inky-erb +0 -0
- /data/lib/{active_mail → activemail}/components/base.rb +0 -0
- /data/lib/{active_mail → activemail}/components/block_grid.rb +0 -0
- /data/lib/{active_mail → activemail}/components/button.rb +0 -0
- /data/lib/{active_mail → activemail}/components/callout.rb +0 -0
- /data/lib/{active_mail → activemail}/components/center.rb +0 -0
- /data/lib/{active_mail → activemail}/components/columns.rb +0 -0
- /data/lib/{active_mail → activemail}/components/container.rb +0 -0
- /data/lib/{active_mail → activemail}/components/cta.rb +0 -0
- /data/lib/{active_mail → activemail}/components/h_line.rb +0 -0
- /data/lib/{active_mail → activemail}/components/info_box.rb +0 -0
- /data/lib/{active_mail → activemail}/components/menu.rb +0 -0
- /data/lib/{active_mail → activemail}/components/menu_item.rb +0 -0
- /data/lib/{active_mail → activemail}/components/row.rb +0 -0
- /data/lib/{active_mail → activemail}/components/spacer.rb +0 -0
- /data/lib/{active_mail → activemail}/components/wrapper.rb +0 -0
- /data/lib/{active_mail → activemail}/configuration.rb +0 -0
- /data/lib/{active_mail → activemail}/inliner/base.rb +0 -0
- /data/lib/{active_mail → activemail}/inliner/interceptor.rb +0 -0
- /data/lib/{active_mail → activemail}/inliner/null.rb +0 -0
- /data/lib/{active_mail → activemail}/inliner/premailer.rb +0 -0
- /data/lib/{active_mail → activemail}/inliner/roadie.rb +0 -0
- /data/lib/{active_mail → activemail}/libxml.rb +0 -0
- /data/lib/{active_mail → activemail}/parse_error_reporter.rb +0 -0
- /data/lib/{active_mail → activemail}/quality/guard.rb +0 -0
- /data/lib/{active_mail → activemail}/quality/minitest.rb +0 -0
- /data/lib/{active_mail → activemail}/quality/preview_renderer.rb +0 -0
- /data/lib/{active_mail → activemail}/quality/render_all.rb +0 -0
- /data/lib/{active_mail → activemail}/quality/rspec.rb +0 -0
- /data/lib/{active_mail → activemail}/rails/compiled_stylesheet.rb +0 -0
- /data/lib/{active_mail → activemail}/rails/template_handler.rb +0 -0
- /data/lib/{active_mail → activemail}/tokens.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e81e755f1fae9c74a61005f5a87483c3a1cf561b79003081a134cb687f658004
|
|
4
|
+
data.tar.gz: 3f669eec79bd3b5df69c83fa3169fafcd7cabcaa014a43ce259399f43d435989
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
|
112
|
-
materialize a static `
|
|
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 `
|
|
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
|
|
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
|
|
126
|
-
(`app/views/layouts/
|
|
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
|
|
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
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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
|
|
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 "
|
|
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 "
|
|
5
|
-
@import "
|
|
6
|
-
@import "
|
|
7
|
-
@import "
|
|
8
|
-
@import "
|
|
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 = '
|
|
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
|
|
13
|
-
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
|
|
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("
|
|
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
|
-
<%=
|
|
20
|
-
<%# Fallback for when
|
|
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 "
|
|
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/
|
|
29
|
+
<%= render "layouts/activemail/head" %>
|
|
30
30
|
<%= yield %>
|
|
31
|
-
<%= render "layouts/
|
|
31
|
+
<%= render "layouts/activemail/footer" %>
|
|
32
32
|
</container>
|
|
33
33
|
</div>
|
|
34
34
|
</body>
|
|
@@ -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/
|
|
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 '
|
|
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 '
|
|
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('
|
|
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 "
|
|
14
|
-
initializer '
|
|
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[
|
|
17
|
+
assets.precompile += %w[activemail/activemail.css] if assets.respond_to?(:precompile)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
initializer '
|
|
20
|
+
initializer 'activemail.action_mailer' do
|
|
21
21
|
ActiveSupport.on_load(:action_mailer) do
|
|
22
|
-
require '
|
|
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
|
-
#
|
|
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/
|
|
32
|
+
load File.expand_path('../../tasks/activemail.rake', __dir__)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
data/lib/activemail.rb
CHANGED
|
@@ -1,5 +1,161 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
require '
|
|
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(' ', ' ')
|
|
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
|
|
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/
|
|
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', '
|
|
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/
|
|
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
|
|
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/
|
|
9
|
+
source_root File.expand_path('../../../app/assets/stylesheets/activemail', __dir__)
|
|
10
10
|
|
|
11
|
-
TARGET_DIR = File.join('app', 'assets', 'stylesheets', '
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
<%%=
|
|
19
|
-
<%%# Fallback for when
|
|
20
|
-
<%%= stylesheet_link_tag "
|
|
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
|
-
=
|
|
12
|
-
-# Fallback for when
|
|
13
|
-
= stylesheet_link_tag "
|
|
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
|
-
=
|
|
11
|
-
/ Fallback for when
|
|
12
|
-
= stylesheet_link_tag "
|
|
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/
|
|
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', '
|
|
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 '
|
|
3
|
+
require 'activemail'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
|
|
6
|
-
namespace :
|
|
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/
|
|
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 '
|
|
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.
|
|
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
|
|
56
|
-
|
|
57
|
-
dark mode
|
|
58
|
-
|
|
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/
|
|
68
|
-
- app/assets/stylesheets/
|
|
69
|
-
- app/assets/stylesheets/
|
|
70
|
-
- app/assets/stylesheets/
|
|
71
|
-
- app/assets/stylesheets/
|
|
72
|
-
- app/assets/stylesheets/
|
|
73
|
-
- app/assets/stylesheets/
|
|
74
|
-
- app/helpers/
|
|
75
|
-
- app/views/layouts/
|
|
76
|
-
- app/views/layouts/
|
|
77
|
-
- app/views/layouts/
|
|
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/
|
|
117
|
-
- lib/
|
|
118
|
-
- lib/
|
|
119
|
-
- lib/
|
|
120
|
-
- lib/
|
|
121
|
-
- lib/
|
|
122
|
-
- lib/
|
|
123
|
-
- lib/
|
|
124
|
-
- lib/
|
|
125
|
-
- lib/
|
|
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:
|
|
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(' ', ' ')
|
|
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
|
|
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
|
|
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
|
|
File without changes
|