roadie 2.3.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, value].join(':')
26
+ [property, value_with_important].join(':')
27
27
  end
28
28
 
29
29
  def inspect
30
- extra = [important ? '!important' : nil, specificity].compact
31
- "#{to_s} (#{extra.join(' , ')})"
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
@@ -1,3 +1,3 @@
1
1
  module Roadie
2
- VERSION = '2.3.4'
2
+ VERSION = '2.4.0'
3
3
  end
@@ -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', '>= 1.4.4'
18
- s.add_dependency 'css_parser'
19
- s.add_dependency 'actionmailer', '> 3.0.0', '< 3.3.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'
@@ -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 => :integration, :from => 'john@example.com'
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 => :default
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 => :simple
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
- Nokogiri::HTML.parse Roadie::Inliner.new(provider, ['global.css'], html, url_options).execute
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 "respects !important properties" do
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 "ignores selectors with :psuedo-classes" do
102
- use_css 'p:hover { color: red }'
103
- rendering('<p></p>').should_not have_styling('color' => 'red')
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