md-roadie 2.4.2.md.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.autotest +10 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +22 -0
  4. data/.yardopts +1 -0
  5. data/Appraisals +15 -0
  6. data/Changelog.md +185 -0
  7. data/Gemfile +11 -0
  8. data/Guardfile +8 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +310 -0
  11. data/Rakefile +30 -0
  12. data/autotest/discover.rb +1 -0
  13. data/gemfiles/rails_3.0.gemfile +7 -0
  14. data/gemfiles/rails_3.0.gemfile.lock +123 -0
  15. data/gemfiles/rails_3.1.gemfile +7 -0
  16. data/gemfiles/rails_3.1.gemfile.lock +126 -0
  17. data/gemfiles/rails_3.2.gemfile +7 -0
  18. data/gemfiles/rails_3.2.gemfile.lock +124 -0
  19. data/gemfiles/rails_4.0.gemfile +7 -0
  20. data/gemfiles/rails_4.0.gemfile.lock +119 -0
  21. data/lib/roadie.rb +79 -0
  22. data/lib/roadie/action_mailer_extensions.rb +95 -0
  23. data/lib/roadie/asset_pipeline_provider.rb +28 -0
  24. data/lib/roadie/asset_provider.rb +62 -0
  25. data/lib/roadie/css_file_not_found.rb +22 -0
  26. data/lib/roadie/filesystem_provider.rb +74 -0
  27. data/lib/roadie/inliner.rb +251 -0
  28. data/lib/roadie/railtie.rb +39 -0
  29. data/lib/roadie/selector.rb +50 -0
  30. data/lib/roadie/style_declaration.rb +42 -0
  31. data/lib/roadie/version.rb +3 -0
  32. data/md-roadie.gemspec +36 -0
  33. data/spec/fixtures/app/assets/stylesheets/integration.css +10 -0
  34. data/spec/fixtures/public/stylesheets/integration.css +10 -0
  35. data/spec/fixtures/views/integration_mailer/marketing.html.erb +2 -0
  36. data/spec/fixtures/views/integration_mailer/notification.html.erb +8 -0
  37. data/spec/fixtures/views/integration_mailer/notification.text.erb +6 -0
  38. data/spec/integration_spec.rb +110 -0
  39. data/spec/lib/roadie/action_mailer_extensions_spec.rb +227 -0
  40. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +65 -0
  41. data/spec/lib/roadie/css_file_not_found_spec.rb +29 -0
  42. data/spec/lib/roadie/filesystem_provider_spec.rb +94 -0
  43. data/spec/lib/roadie/inliner_spec.rb +591 -0
  44. data/spec/lib/roadie/selector_spec.rb +55 -0
  45. data/spec/lib/roadie/style_declaration_spec.rb +49 -0
  46. data/spec/lib/roadie_spec.rb +101 -0
  47. data/spec/shared_examples/asset_provider_examples.rb +11 -0
  48. data/spec/spec_helper.rb +69 -0
  49. data/spec/support/anonymous_mailer.rb +21 -0
  50. data/spec/support/change_url_options.rb +5 -0
  51. data/spec/support/have_attribute_matcher.rb +28 -0
  52. data/spec/support/have_node_matcher.rb +19 -0
  53. data/spec/support/have_selector_matcher.rb +6 -0
  54. data/spec/support/have_styling_matcher.rb +25 -0
  55. data/spec/support/parse_styling.rb +25 -0
  56. metadata +318 -0
@@ -0,0 +1,39 @@
1
+ require 'action_mailer'
2
+ require 'roadie'
3
+ require 'roadie/action_mailer_extensions'
4
+
5
+ module Roadie
6
+ # {Roadie::Railtie} registers {Roadie} with the current Rails application
7
+ # It adds configuration options:
8
+ #
9
+ # config.roadie.enabled = true
10
+ # Set this to false to disable Roadie completely. This could be useful if
11
+ # you don't want Roadie in certain environments.
12
+ #
13
+ # config.roadie.provider = nil
14
+ # You can use this to set a provider yourself. See {Roadie::AssetProvider}.
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.
24
+ #
25
+ # @see Roadie
26
+ # @see AssetProvider
27
+ class Railtie < Rails::Railtie
28
+ config.roadie = ActiveSupport::OrderedOptions.new
29
+ config.roadie.enabled = true
30
+ config.roadie.provider = nil
31
+ config.roadie.after_inlining = nil
32
+
33
+ initializer "roadie.extend_action_mailer" do
34
+ ActiveSupport.on_load(:action_mailer) do
35
+ include Roadie::ActionMailerExtensions
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
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
35
+ :-ms-input-placeholder :-moz-placeholder
36
+ :before :after].freeze
37
+
38
+ def pseudo_element?
39
+ selector.include? '::'
40
+ end
41
+
42
+ def at_rule?
43
+ selector[0, 1] == '@'
44
+ end
45
+
46
+ def pseudo_function?
47
+ BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,42 @@
1
+ module Roadie
2
+ class StyleDeclaration
3
+ include Comparable
4
+ attr_reader :property, :value, :important, :specificity
5
+
6
+ def initialize(property, value, important, specificity)
7
+ @property = property
8
+ @value = value
9
+ @important = important
10
+ @specificity = specificity
11
+ end
12
+
13
+ def important?
14
+ @important
15
+ end
16
+
17
+ def <=>(other)
18
+ if important == other.important
19
+ specificity <=> other.specificity
20
+ else
21
+ important ? 1 : -1
22
+ end
23
+ end
24
+
25
+ def to_s
26
+ [property, value_with_important].join(':')
27
+ end
28
+
29
+ def inspect
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
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Roadie
2
+ VERSION = '2.4.2.md.1'
3
+ end
@@ -0,0 +1,36 @@
1
+ # roadie.gemspec
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ $:.push File.expand_path("../lib", __FILE__)
5
+ require 'roadie/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'md-roadie'
9
+ s.version = Roadie::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ['Magnus Bergmark', 'BJ Neilsen']
12
+ s.email = ['magnus.bergmark@gmail.com', 'bj.neilsen@gmail.com']
13
+ s.homepage = 'http://github.com/Mange/roadie'
14
+ s.summary = %q{Making HTML emails comfortable for the Rails rockstars}
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
+
17
+ s.add_dependency 'nokogiri', RUBY_VERSION < '1.9.3' ? ['> 1.5.0', '< 1.6.0'] : '> 1.5.0'
18
+ s.add_dependency 'css_parser', '~> 1.3.4'
19
+ s.add_dependency 'actionmailer', '> 3.0.0', '< 5.0.0'
20
+ s.add_dependency 'sprockets'
21
+
22
+ s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'rails'
24
+ s.add_development_dependency 'rspec'
25
+ s.add_development_dependency 'rspec-rails'
26
+ s.add_development_dependency 'special_delivery'
27
+
28
+ s.add_development_dependency 'appraisal'
29
+
30
+ s.extra_rdoc_files = %w[README.md Changelog.md]
31
+ s.require_paths = %w[lib]
32
+
33
+ s.files = `git ls-files`.split("\n")
34
+ s.test_files = `git ls-files -- spec/*`.split("\n")
35
+ end
36
+
@@ -0,0 +1,10 @@
1
+ body { background: url(../images/dots.png) repeat-x; }
2
+ #message { background-color: #fff; margin: 0 auto; width: 75%; }
3
+
4
+ h1 { color: #eee; }
5
+ strong {
6
+ -moz-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
7
+ -webkit-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
8
+ -o-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
9
+ box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
10
+ }
@@ -0,0 +1,10 @@
1
+ body { background: url(../images/dots.png) repeat-x; }
2
+ #message { background-color: #fff; margin: 0 auto; width: 75%; }
3
+
4
+ h1 { color: #eee; }
5
+ strong {
6
+ -moz-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
7
+ -webkit-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
8
+ -o-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
9
+ box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
10
+ }
@@ -0,0 +1,2 @@
1
+ Contact us to buy stuff!
2
+ <a href="http://www.example.com/cheap-marketing">SPAM MASTERS</a>
@@ -0,0 +1,8 @@
1
+ <div id="message">
2
+ <h1>Dear person</1>
3
+
4
+ <p>I have to inform you that <strong><%= @reason %></strong>.</p>
5
+
6
+ <p>Thank you,<br />
7
+ The app</p>
8
+ </div>
@@ -0,0 +1,6 @@
1
+ = Dear person =
2
+
3
+ I have to inform you that <%= @reason %>.
4
+
5
+ Thank you,
6
+ The app
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ module Roadie
4
+ shared_examples "roadie integration" do
5
+ mailer = Class.new(AnonymousMailer) do
6
+ default :css => 'integration', :from => 'john@example.com'
7
+ append_view_path FIXTURES_PATH.join('views')
8
+
9
+ # Needed for correct path lookup
10
+ self.mailer_name = "integration_mailer"
11
+
12
+ def notification(to, reason)
13
+ @reason = reason
14
+ mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
15
+ end
16
+
17
+ def marketing(to)
18
+ headers('X-Spam' => 'No way! Trust us!')
19
+ mail(:subject => 'Buy cheap v1agra', :to => to)
20
+ end
21
+
22
+ def url_options
23
+ # This allows apps to calculate any options on a per-email basis
24
+ super.merge(:protocol => 'https')
25
+ end
26
+ end
27
+
28
+ def parse_html_in_email(mail)
29
+ Nokogiri::HTML.parse mail.html_part.body.decoded
30
+ end
31
+
32
+ before(:each) do
33
+ change_default_url_options(:host => 'example.app.org')
34
+ mailer.delivery_method = :test
35
+ end
36
+
37
+ it "inlines styles for an email" do
38
+ email = mailer.notification('doe@example.com', 'your quota limit has been reached')
39
+
40
+ email.to.should == ['doe@example.com']
41
+ email.from.should == ['john@example.com']
42
+ email.should have(2).parts
43
+
44
+ email.text_part.body.decoded.should_not match(/<.*>/)
45
+
46
+ html = email.html_part.body.decoded
47
+ html.should include '<!DOCTYPE'
48
+ html.should include '<head'
49
+
50
+ document = parse_html_in_email(email)
51
+ document.should have_selector('body #message h1')
52
+ document.should have_styling('background' => 'url(https://example.app.org/images/dots.png) repeat-x').at_selector('body')
53
+ document.should have_selector('strong[contains("quota")]')
54
+
55
+ # If we deliver mails we can catch weird problems with headers being invalid
56
+ email.deliver
57
+ end
58
+
59
+ it "does not add headers for the roadie options" do
60
+ email = mailer.notification('doe@example.com', 'no berries left in chest')
61
+ email.header.fields.map(&:name).should_not include('css')
62
+ end
63
+
64
+ it "keeps custom headers in place" do
65
+ email = mailer.marketing('everyone@inter.net')
66
+ email.header['X-Spam'].should be_present
67
+ end
68
+
69
+ it "applies CSS3 styles" do
70
+ email = mailer.notification('doe@example.com', 'your quota limit has been reached')
71
+ document = parse_html_in_email(email)
72
+ strong_node = document.css('strong').first
73
+ stylings = SpecHelpers.styling_of_node(strong_node)
74
+ stylings.should include(['box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
75
+ stylings.should include(['-o-box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
76
+ end
77
+
78
+ it "only removes the css option when disabled" do
79
+ Rails.application.config.roadie.enabled = false
80
+
81
+ email = mailer.notification('doe@example.com', 'your quota limit has been reached')
82
+
83
+ email.header.fields.map(&:name).should_not include('css')
84
+
85
+ email.to.should == ['doe@example.com']
86
+ email.from.should == ['john@example.com']
87
+ email.should have(2).parts
88
+
89
+ html = email.html_part.body.decoded
90
+ html.should_not include '<!DOCTYPE'
91
+ html.should_not include '<head'
92
+
93
+ document = parse_html_in_email(email)
94
+ document.should_not have_styling('color' => '#eee').at_selector('h1')
95
+ document.should_not have_styling('background' => 'url(https://example.app.org/images/dots.png) repeat-x').at_selector('body')
96
+ end
97
+ end
98
+
99
+ describe "filesystem integration" do
100
+ it_behaves_like "roadie integration" do
101
+ before(:each) { Rails.application.config.assets.enabled = false }
102
+ end
103
+ end
104
+
105
+ describe "asset pipeline integration" do
106
+ it_behaves_like "roadie integration" do
107
+ before(:each) { Rails.application.config.assets.enabled = true }
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,227 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ module Roadie
5
+ describe ActionMailerExtensions, "CSS selection" do
6
+ mailer = Class.new(AnonymousMailer) do
7
+ default :css => 'default'
8
+
9
+ def default_css
10
+ mail(:subject => "Default CSS") do |format|
11
+ format.html { render :text => '' }
12
+ end
13
+ end
14
+
15
+ def override_css(css)
16
+ mail(:subject => "Default CSS", :css => css) do |format|
17
+ format.html { render :text => '' }
18
+ end
19
+ end
20
+ end
21
+
22
+ def expect_global_css(files)
23
+ Roadie.should_receive(:inline_css).with(provider, files, anything, anything, anything).and_return('')
24
+ end
25
+
26
+ let(:provider) { double("asset provider", :all => '') }
27
+
28
+ before(:each) do
29
+ Roadie.stub(:inline_css => 'unexpected value passed to inline_css')
30
+ Roadie.stub(:current_provider => provider)
31
+ end
32
+
33
+ it "uses the default CSS when :css is not specified" do
34
+ expect_global_css ['default']
35
+ mailer.default_css
36
+ end
37
+
38
+ it "uses the specified CSS instead of the default" do
39
+ expect_global_css ['some', 'other/files']
40
+ mailer.override_css([:some, 'other/files'])
41
+ end
42
+
43
+ it "allows procs defining the CSS files to use" do
44
+ proc = lambda { 'from proc' }
45
+
46
+ expect_global_css ['from proc']
47
+ mailer.override_css([proc])
48
+ end
49
+
50
+ it "runs procs in the context of the instance" do
51
+ new_mailer = Class.new(mailer) do
52
+ private
53
+ def a_private_method
54
+ 'from private method'
55
+ end
56
+ end
57
+ proc = lambda { a_private_method }
58
+
59
+ expect_global_css ['from private method']
60
+ new_mailer.override_css([proc])
61
+ end
62
+
63
+ it "uses no global CSS when :css is set to nil" do
64
+ expect_global_css []
65
+ mailer.override_css(nil)
66
+ end
67
+
68
+ it "uses no global CSS when :css is set to false" do
69
+ expect_global_css []
70
+ mailer.override_css(false)
71
+ end
72
+
73
+ it "uses no global CSS when :css is set to a proc returning nil" do
74
+ expect_global_css []
75
+ mailer.override_css(lambda { nil })
76
+ end
77
+ end
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
+
155
+ describe ActionMailerExtensions, "using HTML" do
156
+ mailer = Class.new(AnonymousMailer) do
157
+ default :css => 'simple'
158
+
159
+ def multipart
160
+ mail(:subject => "Multipart email") do |format|
161
+ format.html { render :text => 'Hello HTML' }
162
+ format.text { render :text => 'Hello Text' }
163
+ end
164
+ end
165
+
166
+ def singlepart_html
167
+ mail(:subject => "HTML email") do |format|
168
+ format.html { render :text => 'Hello HTML' }
169
+ end
170
+ end
171
+
172
+ def singlepart_plain
173
+ mail(:subject => "Text email") do |format|
174
+ format.text { render :text => 'Hello Text' }
175
+ end
176
+ end
177
+ end
178
+
179
+ let(:provider) { double("asset provider", :all => '') }
180
+
181
+ before(:each) do
182
+ Roadie.stub(:inline_css => 'unexpected value passed to inline_css')
183
+ Roadie.stub(:current_provider => provider)
184
+ end
185
+
186
+ describe "for singlepart text/plain" do
187
+ it "does not touch the email body" do
188
+ Roadie.should_not_receive(:inline_css)
189
+ mailer.singlepart_plain
190
+ end
191
+ end
192
+
193
+ describe "for singlepart text/html" do
194
+ it "inlines css to the email body" do
195
+ Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything, anything).and_return('html')
196
+ mailer.singlepart_html.body.decoded.should == 'html'
197
+ end
198
+
199
+ it "does not inline css when Roadie is disabled" do
200
+ Roadie.stub :enabled? => false
201
+ Roadie.should_not_receive(:inline_css)
202
+ mailer.singlepart_html.body.decoded.should == 'Hello HTML'
203
+ end
204
+ end
205
+
206
+ describe "for multipart" do
207
+ it "keeps both parts" do
208
+ mailer.multipart.should have(2).parts
209
+ end
210
+
211
+ it "inlines css to the email's html part" do
212
+ Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything, anything).and_return('html')
213
+ email = mailer.multipart
214
+ email.html_part.body.decoded.should == 'html'
215
+ email.text_part.body.decoded.should == 'Hello Text'
216
+ end
217
+
218
+ it "does not inline css when Roadie is disabled" do
219
+ Roadie.stub :enabled? => false
220
+ Roadie.should_not_receive(:inline_css)
221
+ email = mailer.multipart
222
+ email.html_part.body.decoded.should == 'Hello HTML'
223
+ email.text_part.body.decoded.should == 'Hello Text'
224
+ end
225
+ end
226
+ end
227
+ end