roadie 2.3.4 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/.travis.yml +15 -4
- data/Appraisals +5 -1
- data/Changelog.md +24 -4
- data/Gemfile +1 -1
- data/Guardfile +1 -1
- data/README.md +87 -15
- data/gemfiles/{rails-3.0.gemfile → rails_3.0.gemfile} +1 -1
- data/{Gemfile.lock → gemfiles/rails_3.0.gemfile.lock} +30 -56
- data/gemfiles/{rails-3.1.gemfile → rails_3.1.gemfile} +1 -1
- data/gemfiles/{rails-3.1.gemfile.lock → rails_3.1.gemfile.lock} +33 -31
- data/gemfiles/rails_3.2.gemfile +7 -0
- data/gemfiles/{rails-3.2.gemfile.lock → rails_3.2.gemfile.lock} +35 -33
- data/gemfiles/rails_4.0.gemfile +7 -0
- data/gemfiles/rails_4.0.gemfile.lock +119 -0
- data/lib/roadie.rb +15 -1
- data/lib/roadie/action_mailer_extensions.rb +34 -5
- data/lib/roadie/asset_provider.rb +1 -1
- data/lib/roadie/inliner.rb +29 -9
- data/lib/roadie/railtie.rb +9 -0
- data/lib/roadie/selector.rb +48 -0
- data/lib/roadie/style_declaration.rb +11 -3
- data/lib/roadie/version.rb +1 -1
- data/roadie.gemspec +5 -3
- data/spec/integration_spec.rb +1 -1
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +81 -5
- data/spec/lib/roadie/inliner_spec.rb +94 -6
- data/spec/lib/roadie/selector_spec.rb +51 -0
- data/spec/lib/roadie/style_declaration_spec.rb +4 -0
- data/spec/lib/roadie_spec.rb +20 -0
- data/spec/spec_helper.rb +5 -2
- metadata +57 -23
- data/gemfiles/rails-3.0.gemfile.lock +0 -121
- data/gemfiles/rails-3.0.x.Gemfile.lock +0 -76
- data/gemfiles/rails-3.1.x.Gemfile.lock +0 -73
- data/gemfiles/rails-3.2.gemfile +0 -7
data/lib/roadie/railtie.rb
CHANGED
@@ -13,6 +13,14 @@ module Roadie
|
|
13
13
|
# config.roadie.provider = nil
|
14
14
|
# You can use this to set a provider yourself. See {Roadie::AssetProvider}.
|
15
15
|
#
|
16
|
+
# config.roadie.after_inlining = lambda do |doc|
|
17
|
+
# doc.css('#products p.desc a[href^="/"]').each do |link|
|
18
|
+
# link['href'] = "http://www.foo.com" + link['href']
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# You can use this to set a custom inliner. A custom inliner transforms an outgoing HTML email using application specific rules.
|
22
|
+
# The custom inliner is invoked after the default inliner.A custom inliner can be created using a `lambda` that accepts one parameter
|
23
|
+
# or an object that responds to the `call` method with one parameter.
|
16
24
|
#
|
17
25
|
# @see Roadie
|
18
26
|
# @see AssetProvider
|
@@ -20,6 +28,7 @@ module Roadie
|
|
20
28
|
config.roadie = ActiveSupport::OrderedOptions.new
|
21
29
|
config.roadie.enabled = true
|
22
30
|
config.roadie.provider = nil
|
31
|
+
config.roadie.after_inlining = nil
|
23
32
|
|
24
33
|
initializer "roadie.extend_action_mailer" do
|
25
34
|
ActiveSupport.on_load(:action_mailer) do
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Roadie
|
2
|
+
class Selector
|
3
|
+
def initialize(selector)
|
4
|
+
@selector = selector.to_s.strip
|
5
|
+
end
|
6
|
+
|
7
|
+
def specificity
|
8
|
+
@specificity ||= CssParser.calculate_specificity selector
|
9
|
+
end
|
10
|
+
|
11
|
+
def inlinable?
|
12
|
+
!(pseudo_element? || at_rule? || pseudo_function?)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
selector
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_str() to_s end
|
20
|
+
def inspect() selector.inspect end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
if other.is_a?(self.class)
|
24
|
+
other.selector == selector
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
attr_reader :selector
|
32
|
+
|
33
|
+
private
|
34
|
+
BAD_PSEUDO_FUNCTIONS = %w[:active :focus :hover :link :target :visited].freeze
|
35
|
+
|
36
|
+
def pseudo_element?
|
37
|
+
selector.include? '::'
|
38
|
+
end
|
39
|
+
|
40
|
+
def at_rule?
|
41
|
+
selector[0, 1] == '@'
|
42
|
+
end
|
43
|
+
|
44
|
+
def pseudo_function?
|
45
|
+
BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -23,12 +23,20 @@ module Roadie
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def to_s
|
26
|
-
[property,
|
26
|
+
[property, value_with_important].join(':')
|
27
27
|
end
|
28
28
|
|
29
29
|
def inspect
|
30
|
-
|
31
|
-
|
30
|
+
"#{to_s} (#{specificity})"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def value_with_important
|
35
|
+
if important
|
36
|
+
"#{value} !important"
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
32
40
|
end
|
33
41
|
end
|
34
42
|
end
|
data/lib/roadie/version.rb
CHANGED
data/roadie.gemspec
CHANGED
@@ -14,12 +14,14 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.summary = %q{Making HTML emails comfortable for the Rails rockstars}
|
15
15
|
s.description = %q{Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.}
|
16
16
|
|
17
|
-
s.add_dependency 'nokogiri', '
|
18
|
-
s.add_dependency 'css_parser'
|
19
|
-
s.add_dependency 'actionmailer', '> 3.0.0', '<
|
17
|
+
s.add_dependency 'nokogiri', '~> 1.6.0'
|
18
|
+
s.add_dependency 'css_parser', '~> 1.3.4'
|
19
|
+
s.add_dependency 'actionmailer', '> 3.0.0', '< 5.0.0'
|
20
20
|
s.add_dependency 'sprockets'
|
21
21
|
|
22
|
+
s.add_development_dependency 'rake'
|
22
23
|
s.add_development_dependency 'rails'
|
24
|
+
s.add_development_dependency 'rspec'
|
23
25
|
s.add_development_dependency 'rspec-rails'
|
24
26
|
|
25
27
|
s.add_development_dependency 'appraisal'
|
data/spec/integration_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module Roadie
|
4
4
|
shared_examples "roadie integration" do
|
5
5
|
mailer = Class.new(AnonymousMailer) do
|
6
|
-
default :css =>
|
6
|
+
default :css => 'integration', :from => 'john@example.com'
|
7
7
|
append_view_path FIXTURES_PATH.join('views')
|
8
8
|
|
9
9
|
# Needed for correct path lookup
|
@@ -4,7 +4,7 @@ require 'spec_helper'
|
|
4
4
|
module Roadie
|
5
5
|
describe ActionMailerExtensions, "CSS selection" do
|
6
6
|
mailer = Class.new(AnonymousMailer) do
|
7
|
-
default :css =>
|
7
|
+
default :css => 'default'
|
8
8
|
|
9
9
|
def default_css
|
10
10
|
mail(:subject => "Default CSS") do |format|
|
@@ -20,7 +20,7 @@ module Roadie
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def expect_global_css(files)
|
23
|
-
Roadie.should_receive(:inline_css).with(provider, files, anything, anything).and_return('')
|
23
|
+
Roadie.should_receive(:inline_css).with(provider, files, anything, anything, anything).and_return('')
|
24
24
|
end
|
25
25
|
|
26
26
|
let(:provider) { double("asset provider", :all => '') }
|
@@ -76,9 +76,85 @@ module Roadie
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
+
describe ActionMailerExtensions, "after_initialize handler" do
|
80
|
+
let(:global_after_inlining_handler) { double("global after inlining handler") }
|
81
|
+
let(:per_mailer_after_inlining_handler) { double("per mailer after inlining handler") }
|
82
|
+
let(:per_mail_after_inlining_handler) { double("per mail after inlining handler") }
|
83
|
+
let(:provider) { double("asset provider", :all => '') }
|
84
|
+
|
85
|
+
before(:each) do
|
86
|
+
Roadie.stub(:current_provider => provider)
|
87
|
+
Roadie.stub(:after_inlining_handler => global_after_inlining_handler)
|
88
|
+
end
|
89
|
+
|
90
|
+
def expect_inlining_handler(handler)
|
91
|
+
Roadie.should_receive(:inline_css).with(provider, anything, anything, anything, handler)
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "global" do
|
95
|
+
let(:mailer) do
|
96
|
+
Class.new(AnonymousMailer) do
|
97
|
+
def nil_handler
|
98
|
+
mail(:subject => "Nil handler") do |format|
|
99
|
+
format.html { render :text => '' }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def global_handler
|
104
|
+
mail(:subject => "Global handler") do |format|
|
105
|
+
format.html { render :text => '' }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
it "is set to the provided global handler when mailer/per mail handler are not specified" do
|
112
|
+
expect_inlining_handler(global_after_inlining_handler)
|
113
|
+
mailer.global_handler
|
114
|
+
end
|
115
|
+
|
116
|
+
it "is not used when not set" do
|
117
|
+
Roadie.stub(:after_inlining_handler => nil)
|
118
|
+
expect_inlining_handler(nil)
|
119
|
+
mailer.nil_handler
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "overridden" do
|
124
|
+
let(:mailer) do
|
125
|
+
handler = per_mailer_after_inlining_handler
|
126
|
+
Class.new(AnonymousMailer) do
|
127
|
+
default :after_inlining => handler
|
128
|
+
|
129
|
+
def per_mailer_handler
|
130
|
+
mail(:subject => "Mailer handler") do |format|
|
131
|
+
format.html { render :text => '' }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def per_mail_handler(handler)
|
136
|
+
mail(:subject => "Per Mail handler", :after_inlining => handler) do |format|
|
137
|
+
format.html { render :text => '' }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it "is set to the provided mailer handler" do
|
144
|
+
expect_inlining_handler(per_mailer_after_inlining_handler)
|
145
|
+
mailer.per_mailer_handler
|
146
|
+
end
|
147
|
+
|
148
|
+
it "is set to the provided per mail handler" do
|
149
|
+
expect_inlining_handler(per_mail_after_inlining_handler)
|
150
|
+
mailer.per_mail_handler(per_mail_after_inlining_handler)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
79
155
|
describe ActionMailerExtensions, "using HTML" do
|
80
156
|
mailer = Class.new(AnonymousMailer) do
|
81
|
-
default :css =>
|
157
|
+
default :css => 'simple'
|
82
158
|
|
83
159
|
def multipart
|
84
160
|
mail(:subject => "Multipart email") do |format|
|
@@ -116,7 +192,7 @@ module Roadie
|
|
116
192
|
|
117
193
|
describe "for singlepart text/html" do
|
118
194
|
it "inlines css to the email body" do
|
119
|
-
Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything).and_return('html')
|
195
|
+
Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything, anything).and_return('html')
|
120
196
|
mailer.singlepart_html.body.decoded.should == 'html'
|
121
197
|
end
|
122
198
|
|
@@ -133,7 +209,7 @@ module Roadie
|
|
133
209
|
end
|
134
210
|
|
135
211
|
it "inlines css to the email's html part" do
|
136
|
-
Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything).and_return('html')
|
212
|
+
Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything, anything).and_return('html')
|
137
213
|
email = mailer.multipart
|
138
214
|
email.html_part.body.decoded.should == 'html'
|
139
215
|
email.text_part.body.decoded.should == 'Hello Text'
|
@@ -10,7 +10,8 @@ describe Roadie::Inliner do
|
|
10
10
|
|
11
11
|
def rendering(html, options = {})
|
12
12
|
url_options = options.fetch(:url_options, {:host => 'example.com'})
|
13
|
-
|
13
|
+
after_inlining_handler = options[:after_inlining_handler]
|
14
|
+
Nokogiri::HTML.parse Roadie::Inliner.new(provider, ['global.css'], html, url_options, after_inlining_handler).execute
|
14
15
|
end
|
15
16
|
|
16
17
|
describe "initialization" do
|
@@ -82,10 +83,10 @@ describe Roadie::Inliner do
|
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
|
-
it "
|
86
|
+
it "keeps !important properties" do
|
86
87
|
use_css "a { text-decoration: underline !important; }
|
87
88
|
a.hard-to-spot { text-decoration: none; }"
|
88
|
-
rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline')
|
89
|
+
rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline !important')
|
89
90
|
end
|
90
91
|
|
91
92
|
it "combines with already present inline styles" do
|
@@ -98,9 +99,37 @@ describe Roadie::Inliner do
|
|
98
99
|
rendering('<p style="color: green"></p>').should have_styling([['color', 'red'], ['color', 'green']])
|
99
100
|
end
|
100
101
|
|
101
|
-
it "
|
102
|
-
use_css
|
103
|
-
|
102
|
+
it "does not apply link and dynamic pseudo selectors" do
|
103
|
+
use_css "
|
104
|
+
p:active { color: red }
|
105
|
+
p:focus { color: red }
|
106
|
+
p:hover { color: red }
|
107
|
+
p:link { color: red }
|
108
|
+
p:target { color: red }
|
109
|
+
p:visited { color: red }
|
110
|
+
|
111
|
+
p.active { width: 100%; }
|
112
|
+
"
|
113
|
+
rendering('<p class="active"></p>').should have_styling('width' => '100%')
|
114
|
+
end
|
115
|
+
|
116
|
+
it "does not crash on any pseudo element selectors" do
|
117
|
+
use_css "
|
118
|
+
p.some-element { width: 100%; }
|
119
|
+
p::some-element { color: red; }
|
120
|
+
"
|
121
|
+
rendering('<p class="some-element"></p>').should have_styling('width' => '100%')
|
122
|
+
end
|
123
|
+
|
124
|
+
it "works with nth-child" do
|
125
|
+
use_css "
|
126
|
+
p { color: red; }
|
127
|
+
p:nth-child(2n) { color: green; }
|
128
|
+
"
|
129
|
+
rendering("
|
130
|
+
<p class='one'></p>
|
131
|
+
<p class='two'></p>
|
132
|
+
").should have_styling('color' => 'green').at_selector('.two')
|
104
133
|
end
|
105
134
|
|
106
135
|
it "ignores selectors with @" do
|
@@ -135,6 +164,17 @@ describe Roadie::Inliner do
|
|
135
164
|
expect { rendering '<p></p>' }.not_to raise_error
|
136
165
|
end
|
137
166
|
|
167
|
+
it "does not pick up scripts generating styles" do
|
168
|
+
expect {
|
169
|
+
rendering <<-HTML
|
170
|
+
<script>
|
171
|
+
var color = "red";
|
172
|
+
document.write("<style type='text/css'>p { color: " + color + "; }</style>");
|
173
|
+
</script>
|
174
|
+
HTML
|
175
|
+
}.not_to raise_error
|
176
|
+
end
|
177
|
+
|
138
178
|
describe "inline <style> element" do
|
139
179
|
it "is used for inlined styles" do
|
140
180
|
rendering(<<-HTML).should have_styling([['color', 'green'], ['font-size', '1.1em']])
|
@@ -190,6 +230,17 @@ describe Roadie::Inliner do
|
|
190
230
|
<p>Hello World</p>
|
191
231
|
HTML
|
192
232
|
end
|
233
|
+
|
234
|
+
it "is not touched when inside a SVG element" do
|
235
|
+
expect {
|
236
|
+
rendering <<-HTML
|
237
|
+
<p>Hello World</p>
|
238
|
+
<svg>
|
239
|
+
<style>This is not parseable by the CSS parser!</style>
|
240
|
+
</svg>
|
241
|
+
HTML
|
242
|
+
}.to_not raise_error
|
243
|
+
end
|
193
244
|
end
|
194
245
|
end
|
195
246
|
|
@@ -433,12 +484,49 @@ describe Roadie::Inliner do
|
|
433
484
|
end
|
434
485
|
end
|
435
486
|
|
487
|
+
# This case was happening for some users when emails were rendered as part
|
488
|
+
# of the request cycle. I do not know it we *really* should accept these
|
489
|
+
# values, but it looks like Rails do accept it so we might as well do it
|
490
|
+
# too.
|
491
|
+
it "supports protocol settings with additional tokens" do
|
492
|
+
use_css "img { background: url(/a.jpg); }"
|
493
|
+
rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https://'}).tap do |document|
|
494
|
+
document.should have_attribute('src' => 'https://example.com/b.jpg').at_selector('img')
|
495
|
+
document.should have_styling('background' => 'url(https://example.com/a.jpg)')
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
436
499
|
it "does not touch data: URIs" do
|
437
500
|
use_css "div { background: url(data:abcdef); }"
|
438
501
|
rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)')
|
439
502
|
end
|
440
503
|
end
|
441
504
|
|
505
|
+
describe "custom converter" do
|
506
|
+
let(:html) { '<div id="foo"></div>' }
|
507
|
+
|
508
|
+
it "is invoked" do
|
509
|
+
after_inlining_handler = double("converter")
|
510
|
+
after_inlining_handler.should_receive(:call).with(anything)
|
511
|
+
rendering(html, :after_inlining_handler => after_inlining_handler)
|
512
|
+
end
|
513
|
+
|
514
|
+
it "modifies the document using lambda" do
|
515
|
+
after_inlining_handler = lambda {|d| d.css("#foo").first["class"] = "bar"}
|
516
|
+
rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar"
|
517
|
+
end
|
518
|
+
|
519
|
+
it "modifies the document using object" do
|
520
|
+
klass = Class.new do
|
521
|
+
def call(d)
|
522
|
+
d.css("#foo").first["class"] = "bar"
|
523
|
+
end
|
524
|
+
end
|
525
|
+
after_inlining_handler = klass.new
|
526
|
+
rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
442
530
|
describe "inserting tags" do
|
443
531
|
it "inserts a doctype if not present" do
|
444
532
|
rendering('<html><body></body></html>').to_xml.should include('<!DOCTYPE ')
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
module Roadie
|
5
|
+
describe Selector do
|
6
|
+
it "can be coerced into String" do
|
7
|
+
("I love " + Selector.new("html")).should == "I love html"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can be inlined when simple" do
|
11
|
+
Selector.new("html body #main p.class").should be_inlinable
|
12
|
+
end
|
13
|
+
|
14
|
+
it "cannot be inlined when containing pseudo functions" do
|
15
|
+
%w[
|
16
|
+
p:active
|
17
|
+
p:focus
|
18
|
+
p:hover
|
19
|
+
p:link
|
20
|
+
p:target
|
21
|
+
p:visited
|
22
|
+
].each do |bad_selector|
|
23
|
+
Selector.new(bad_selector).should_not be_inlinable
|
24
|
+
end
|
25
|
+
|
26
|
+
Selector.new('p.active').should be_inlinable
|
27
|
+
end
|
28
|
+
|
29
|
+
it "cannot be inlined when containing pseudo elements" do
|
30
|
+
Selector.new('p::some-element').should_not be_inlinable
|
31
|
+
end
|
32
|
+
|
33
|
+
it "cannot be inlined when selector is an at-rule" do
|
34
|
+
Selector.new('@keyframes progress-bar-stripes').should_not be_inlinable
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has a calculated specificity" do
|
38
|
+
selector = "html p.active.nice #main.deep-selector"
|
39
|
+
Selector.new(selector).specificity.should == CssParser.calculate_specificity(selector)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "is equal to other selectors when they match the same things" do
|
43
|
+
Selector.new("foo").should == Selector.new("foo ")
|
44
|
+
Selector.new("foo").should_not == "foo"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "strips the given selector" do
|
48
|
+
Selector.new(" foo \n").to_s.should == Selector.new("foo").to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|