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.
@@ -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