roadie 2.3.4 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|