roadie 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.md ADDED
@@ -0,0 +1,37 @@
1
+ ### dev
2
+
3
+ [full changelog](https://github.com/Mange/roadie/compare/v1.1.0...master)
4
+
5
+ * Nothing yet
6
+
7
+ ### 1.1.0
8
+
9
+ [full changelog](https://github.com/Mange/roadie/compare/v1.0.1...v1.1.0)
10
+
11
+ * Enhancements:
12
+ * Support for inlining `<link>` elements (thanks to [aliix](https://github.com/aliix))
13
+
14
+ ### 1.0.1
15
+
16
+ [full changelog](https://github.com/Mange/roadie/compare/v1.0.0...v1.0.1)
17
+
18
+ * Enhancements:
19
+ * Full, official support for Ruby 1.9.2 (in addition to 1.8.7)
20
+ * Dependencies:
21
+ * Explicilty depend on nokogiri >= 1.4.4
22
+
23
+ ### 1.0.0
24
+
25
+ [full changelog](https://github.com/Mange/roadie/compare/legacy...v1.0.0)
26
+
27
+ Roadie fork!
28
+
29
+ * Enhancements:
30
+ * Support for Rails 3.0
31
+ * Code cleanup
32
+ * Support `!important`
33
+ * Tests
34
+ * + some other enhancements
35
+ * Deprecations:
36
+ * Removed support for Rails 2.x
37
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- roadie (1.0.0)
4
+ roadie (1.0.1)
5
5
  actionmailer (~> 3.0.0)
6
6
  css_parser
7
7
  nokogiri (>= 1.4.4)
@@ -40,7 +40,7 @@ GEM
40
40
  mime-types (~> 1.16)
41
41
  treetop (~> 1.4.8)
42
42
  mime-types (1.16)
43
- nokogiri (1.4.4)
43
+ nokogiri (1.4.5)
44
44
  polyglot (0.3.1)
45
45
  rack (1.2.1)
46
46
  rack-mount (0.6.13)
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ Roadie
2
+ ======
3
+
4
+ > Making HTML emails comfortable for the Rails rockstars
5
+
6
+ Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.
7
+
8
+ If you want to have this in Rails 2, please see [MailStyle](https://www.github.com/purify/mail_style).
9
+
10
+ How does it work?
11
+ -----------------
12
+
13
+ Email clients have bad support for stylesheets, and some of them blocks stylesheets from downloading. The easiest way to handle this is to work with all styles inline, but that is error prone and hard to work with as you cannot use classes and/or reuse styling.
14
+
15
+ This gem helps making this easier by automatically inlining stylesheet rules into the document before sending it. You just give it a list of stylesheets and it will go though all of the selectors assigning the styles to the maching elements. Careful attention has been put into rules being applied in the correct order, so it should behave just like in the browser¹.
16
+
17
+ Roadie also rewrites all relative URLs in the email to a absolute counterpart, making images you insert and those referenced in your stylesheets work. No more headaches about how to write the stylesheets while still having them work with emails from your acceptance environments.
18
+
19
+ ¹: Of course, rules like `:hover` will not work by definition. Only static styles can be added.
20
+
21
+ Features
22
+ --------
23
+
24
+ * Writes CSS styles inline
25
+ * Respects `!important` styles
26
+ * Does not overwrite styles already present in the `style` attribute of tags
27
+ * Supports the same CSS selectors as [Nokogiri](http://nokogiri.org/) (use CSS3 selectors in your emails!)
28
+ * Makes image urls absolute
29
+ * Hostname and port configurable on a per-environment basis
30
+ * Makes link `href`s absolute
31
+ * Automatically adds proper html skeleton when missing (you don't have to create a layout for emails)²
32
+
33
+ ²: This might be removed in a future version, though. You really ought to create a good layout and not let Roadie guess how you want to have it structured
34
+
35
+ ### What about Sass / Less? ###
36
+
37
+ Sass is supported "by accident" as long as the stylesheets are generated and stored in the stylesheets directory. This is the default behavior from Sass. You are recommended to add a deploy task that generates the stylesheets to make sure that they are present at all times.
38
+
39
+ Install
40
+ -------
41
+
42
+ Roadie is officially supported in both Ruby 1.8.7 and 1.9.2.
43
+
44
+ Add the gem to Rails' Gemfile
45
+
46
+ ```ruby
47
+ gem 'roadie'
48
+ ```
49
+
50
+ Usage
51
+ -----
52
+
53
+ Simply specify the `:css` option to mailer:
54
+
55
+ ```ruby
56
+ class Notifier < ActionMailer::Base
57
+ default :css => :email, :from => 'support@mycompany.com'
58
+
59
+ def registration_mail
60
+ mail(:subject => 'Welcome Aboard', :to => 'someone@example.com')
61
+ end
62
+
63
+ def newsletter
64
+ mail(:subject => 'Newsletter', :to => 'someone@example.com', :css => [:email, :newsletter])
65
+ end
66
+ end
67
+ ```
68
+
69
+ This will look for a css file called `email.css` in your `public/stylesheets` folder. The `css` method can take either a string, a symbol or an array of both. You should pass the CSS filename without the ".css" extension.
70
+
71
+ ### Image URL rewriting ###
72
+
73
+ If you have `default_url_options[:host]` set in your mailer, then Roadie will do it's best to make the URLs of images and in stylesheets absolute.
74
+
75
+ In `application.rb`:
76
+
77
+ ```ruby
78
+ class Application
79
+ config.action_mailer.default_url_options = {:host => 'example.com'}
80
+ end
81
+ ```
82
+
83
+ If you want to to be different depending on your environment, just set it in your environment's configuration instead.
84
+
85
+ ### Ignoring stylesheets ###
86
+
87
+ By default, `style` and `link` elements in the email document's `head` are processed along with the stylesheets and removed from the `head`.
88
+
89
+ You can set a special `data-immutable="true"` attribute on `style` and `link` tags you do not want to be processed and removed from the document's `head`. This is the place to put things like `:hover` selectors that you want to have for email clients allowing them.
90
+
91
+ Style and link elements with `media="print"` are always ignored.
92
+
93
+ ### Inlining link tags ###
94
+
95
+ Any `link` element that is part of your email will be linked in. You can exclude them by setting `data-immutable` as you would on normal `style` elements. Linked stylesheets for print media is also ignored as you would expect.
96
+
97
+ If the `link` tag uses an absolute URL to the stylesheet, it will not be inlined. Use a relative path instead:
98
+
99
+ ```html
100
+ <head>
101
+ <link rel="stylesheet" type="text/css" href="/stylesheets/emails/rock.css"> <!-- Will be inlined -->
102
+ <link rel="stylesheet" type="text/css" href="http://www.metal.org/metal.css"> <!-- Will NOT be inlined -->
103
+ <link rel="stylesheet" type="text/css" href="/stylesheets/jazz.css" media="print"> <!-- Will NOT be inlined -->
104
+ <link rel="stylesheet" type="text/css" href="/ambient.css" data-immutable> <!-- Will NOT be inlined -->
105
+ </head>
106
+ ```
107
+
108
+ Bugs / TODO
109
+ -----------
110
+
111
+ * Improve overall performance
112
+ * Clean up stylesheet assignment code
113
+
114
+ Documentation
115
+ -------------
116
+
117
+ * [Online documentation for 1.0.1](http://rubydoc.info/gems/roadie/1.0.1/frames)
118
+ * [Online documentation for 1.0.0](http://rubydoc.info/gems/roadie/1.0.0/frames)
119
+ * [Online documentation for master](http://rubydoc.info/github/Mange/roadie/master/frames)
120
+ * [Changelog](https://github.com/Mange/roadie/blob/master/Changelog.md)
121
+
122
+ History and contributors
123
+ ------------------------
124
+
125
+ This gem was originally developed for Rails 2 use on [Purify](http://purifyapp.com) under the name [MailStyle](https://www.github.com/purify/mail_style). However, the author stopped maintaining it and a fork took place to make it Rails 3 compatible.
126
+
127
+ The following people have contributed to the orignal gem:
128
+
129
+ * [Jim Neath](http://jimneath.org) (Original author)
130
+ * [Lars Klevans](http://tastybyte.blogspot.com/)
131
+ * [Jonas Grimfelt](http://github.com/grimen)
132
+ * [Ben Johnson](http://www.binarylogic.com)
133
+ * [Istvan Hoka](http://istvanhoka.com/)
134
+ * [Voraz](http://blog.voraz.com.br)
135
+
136
+ License
137
+ -------
138
+
139
+ (The MIT License)
140
+
141
+ Copyright (c) 2009-2011
142
+
143
+ * [Jim Neath](http://jimneath.org)
144
+ * Magnus Bergmark <magnus.bergmark@gmail.com>
145
+
146
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
147
+
148
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
149
+
150
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
151
+
@@ -2,11 +2,21 @@ module Roadie
2
2
  # Raised when a stylesheet specified for inlining is not present.
3
3
  # You can access the target filename via #filename.
4
4
  class CSSFileNotFound < StandardError
5
- attr_reader :filename
5
+ attr_reader :filename, :guess
6
6
 
7
- def initialize(filename)
7
+ def initialize(filename, guess = nil)
8
8
  @filename = filename
9
- super("Could not find #{filename}")
9
+ @guess = guess
10
+ super(build_message)
10
11
  end
12
+
13
+ private
14
+ def build_message
15
+ if guess
16
+ "Could not find #{filename} (guessed from #{guess.inspect})"
17
+ else
18
+ "Could not find #{filename}"
19
+ end
20
+ end
11
21
  end
12
22
  end
@@ -39,6 +39,7 @@ module Roadie
39
39
  adjust_html do |document|
40
40
  @document = document
41
41
  add_missing_structure
42
+ extract_link_elements
42
43
  extract_inline_style_elements
43
44
  inline_css_rules
44
45
  make_image_urls_absolute
@@ -88,6 +89,18 @@ module Roadie
88
89
  end
89
90
  end
90
91
 
92
+ def extract_link_elements
93
+ all_link_elements_to_be_inlined_with_url.each do |link, url|
94
+ # Joining on an "absolute" path ignores everything before the absoluted path
95
+ # so we have to remove the starting slash
96
+ url_path = url.path.sub(%r{^/}, '')
97
+ file_path = Rails.root.join('public', url_path)
98
+ raise CSSFileNotFound.new(file_path, link['href']) unless file_path.file?
99
+ @inline_css << file_path.read
100
+ link.remove
101
+ end
102
+ end
103
+
91
104
  def extract_inline_style_elements
92
105
  document.css("style").each do |style|
93
106
  next if style['media'] == 'print' or style['data-immutable']
@@ -177,5 +190,18 @@ module Roadie
177
190
  :path => base_path
178
191
  })
179
192
  end
193
+
194
+ def all_link_elements_with_url
195
+ document.css("link[rel=stylesheet]").map { |link| [link, URI.parse(link['href'])] }
196
+ end
197
+
198
+ def all_link_elements_to_be_inlined_with_url
199
+ all_link_elements_with_url.reject do |link, url|
200
+ absolute_path_url = (url.host or url.path.nil?)
201
+ blacklisted_element = (link['media'] == 'print' or link['data-immutable'])
202
+
203
+ absolute_path_url or blacklisted_element
204
+ end
205
+ end
180
206
  end
181
207
  end
@@ -1,3 +1,3 @@
1
1
  module Roadie
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.0'
3
3
  end
data/roadie.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_development_dependency 'rspec-rails', '>= 2.0.0'
22
22
 
23
- s.extra_rdoc_files = %w[README.textile]
23
+ s.extra_rdoc_files = %w[README.md Changelog.md]
24
24
  s.require_paths = %w[lib]
25
25
 
26
26
  s.files = `git ls-files`.split("\n")
@@ -0,0 +1 @@
1
+ p { color: green; }
@@ -0,0 +1 @@
1
+ p { color: purple; font-size: 18px; }
@@ -27,7 +27,7 @@ describe "roadie integration" do
27
27
  IntegrationMailer.delivery_method = :test
28
28
  end
29
29
 
30
- it "should inline styles for an email" do
30
+ it "inlines styles for an email" do
31
31
  email = IntegrationMailer.notification('doe@example.com', 'your quota limit has been reached')
32
32
 
33
33
  email.to.should == ['doe@example.com']
@@ -50,12 +50,12 @@ describe "roadie integration" do
50
50
  email.deliver
51
51
  end
52
52
 
53
- it "should not add headers for the roadie options" do
53
+ it "does not add headers for the roadie options" do
54
54
  email = IntegrationMailer.notification('doe@example.com', 'no berries left in chest')
55
55
  email.header.fields.map(&:name).should_not include('css')
56
56
  end
57
57
 
58
- it "should keep custom headers in place" do
58
+ it "keeps custom headers in place" do
59
59
  email = IntegrationMailer.marketing('everyone@inter.net')
60
60
  email.header['X-Spam'].should be_present
61
61
  end
@@ -31,25 +31,25 @@ describe Roadie::ActionMailerExtensions, "inlining styles" do
31
31
  end
32
32
 
33
33
  describe "for singlepart text/plain" do
34
- it "should not touch the email body" do
34
+ it "does not touch the email body" do
35
35
  Roadie.should_not_receive(:inline_css)
36
36
  InliningMailer.singlepart_plain
37
37
  end
38
38
  end
39
39
 
40
40
  describe "for singlepart text/html" do
41
- it "should inline css to the email body" do
41
+ it "inlines css to the email body" do
42
42
  Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
43
43
  InliningMailer.singlepart_html.body.decoded.should == 'html'
44
44
  end
45
45
  end
46
46
 
47
47
  describe "for multipart" do
48
- it "should keep both parts" do
48
+ it "keeps both parts" do
49
49
  InliningMailer.multipart.should have(2).parts
50
50
  end
51
51
 
52
- it "should inline css to the email's html part" do
52
+ it "inlines css to the email's html part" do
53
53
  Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
54
54
  email = InliningMailer.multipart
55
55
  email.html_part.body.decoded.should == 'html'
@@ -79,27 +79,27 @@ describe Roadie::ActionMailerExtensions, "loading css files" do
79
79
  Roadie.stub!(:inline_css => 'html')
80
80
  end
81
81
 
82
- it "should load css from Rails' stylesheet root" do
82
+ it "loads css from Rails' stylesheet root" do
83
83
  Roadie.should_receive(:load_css).with(Rails.root.join('public', 'stylesheets'), anything).and_return('')
84
84
  CssLoadingMailer.use_default
85
85
  end
86
86
 
87
- it "should load the css specified in the default mailer settings" do
87
+ it "loads the css specified in the default mailer settings" do
88
88
  Roadie.should_receive(:load_css).with(anything, ['default_value']).and_return('')
89
89
  CssLoadingMailer.use_default
90
90
  end
91
91
 
92
- it "should load the css specified in the specific mailer action instead of the default choice" do
92
+ it "loads the css specified in the specific mailer action instead of the default choice" do
93
93
  Roadie.should_receive(:load_css).with(anything, ['specific']).and_return('')
94
94
  CssLoadingMailer.override(:specific)
95
95
  end
96
96
 
97
- it "should load no css when specifying false in the mailer action" do
97
+ it "loads no css when specifying false in the mailer action" do
98
98
  Roadie.should_not_receive(:load_css)
99
99
  CssLoadingMailer.override(false)
100
100
  end
101
101
 
102
- it "should load multiple css files when given an array" do
102
+ it "loads multiple css files when given an array" do
103
103
  Roadie.should_receive(:load_css).with(anything, ['specific', 'other']).and_return('')
104
104
  CssLoadingMailer.override([:specific, :other])
105
105
  end
@@ -6,8 +6,24 @@ module Roadie
6
6
  CSSFileNotFound.new('file.css').filename.should == 'file.css'
7
7
  end
8
8
 
9
- it "has a message" do
10
- CSSFileNotFound.new('style.css').message.should == 'Could not find style.css'
9
+ it "can be initialized with the guess the filename was based on" do
10
+ CSSFileNotFound.new('file.css', :file).guess.should == :file
11
+ end
12
+
13
+ it "has a nil guess when no guess was specified" do
14
+ CSSFileNotFound.new('').guess.should be_nil
15
+ end
16
+
17
+ context "without a guess" do
18
+ it "has a message with the wanted filename" do
19
+ CSSFileNotFound.new('style.css').message.should == 'Could not find style.css'
20
+ end
21
+ end
22
+
23
+ context "with a guess" do
24
+ it "has a message with the wanted filename and the guess" do
25
+ CSSFileNotFound.new('style.css', :style).message.should == 'Could not find style.css (guessed from :style)'
26
+ end
11
27
  end
12
28
  end
13
29
  end
@@ -15,29 +15,29 @@ describe Roadie::Inliner do
15
15
  use_css ''
16
16
  end
17
17
 
18
- it "should inline simple attributes" do
18
+ it "inlines simple attributes" do
19
19
  use_css 'p { color: green }'
20
20
  rendering('<p></p>').should have_styling('color' => 'green')
21
21
  end
22
22
 
23
- it "should keep the order of the styles that was inlined" do
23
+ it "keeps the order of the styles that are inlined" do
24
24
  use_css 'h1 { padding: 2px; margin: 5px; }'
25
25
  rendering('<h1></h1>').should have_styling([['padding', '2px'], ['margin', '5px']])
26
26
  end
27
27
 
28
- it "should combine multiple selectors into one" do
28
+ it "combines multiple selectors into one" do
29
29
  use_css 'p { color: green; }
30
30
  .tip { float: right; }'
31
31
  rendering('<p class="tip"></p>').should have_styling('color' => 'green', 'float' => 'right')
32
32
  end
33
33
 
34
- it "should use the ones attributes with the highest specificality when conflicts arises" do
34
+ it "uses the attributes with the highest specificity when conflicts arises" do
35
35
  use_css "p { color: red; }
36
36
  .safe { color: green; }"
37
37
  rendering('<p class="safe"></p>').should have_styling('color' => 'green')
38
38
  end
39
39
 
40
- it "should sort styles by specificity order" do
40
+ it "sorts styles by specificity order" do
41
41
  use_css 'p { margin: 2px; }
42
42
  #big { margin: 10px; }
43
43
  .down { margin-bottom: 5px; }'
@@ -51,7 +51,7 @@ describe Roadie::Inliner do
51
51
  ])
52
52
  end
53
53
 
54
- it "should support multiple selectors for the same rules" do
54
+ it "supports multiple selectors for the same rules" do
55
55
  use_css 'p, a { color: green; }'
56
56
  rendering('<p></p><a></a>').tap do |document|
57
57
  document.should have_styling('color' => 'green').at_selector('p')
@@ -59,29 +59,29 @@ describe Roadie::Inliner do
59
59
  end
60
60
  end
61
61
 
62
- it "should respect !important properties" do
62
+ it "respects !important properties" do
63
63
  use_css "a { text-decoration: underline !important; }
64
64
  a.hard-to-spot { text-decoration: none; }"
65
65
  rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline')
66
66
  end
67
67
 
68
- it "should combine with already present inline styles" do
68
+ it "combines with already present inline styles" do
69
69
  use_css "p { color: green }"
70
70
  rendering('<p style="font-size: 1.1em"></p>').should have_styling([['color', 'green'], ['font-size', '1.1em']])
71
71
  end
72
72
 
73
- it "should not touch already present inline styles" do
73
+ it "does not touch already present inline styles" do
74
74
  use_css "p { color: red }"
75
75
  rendering('<p style="color: green"></p>').should have_styling([['color', 'red'], ['color', 'green']])
76
76
  end
77
77
 
78
- it "should ignore selectors with :psuedo-classes" do
78
+ it "ignores selectors with :psuedo-classes" do
79
79
  use_css 'p:hover { color: red }'
80
80
  rendering('<p></p>').should_not have_styling('color' => 'red')
81
81
  end
82
82
 
83
- describe "inline <style> elements" do
84
- it "should be used for inlined styles" do
83
+ describe "inline <style> element" do
84
+ it "is used for inlined styles" do
85
85
  rendering(<<-HTML).should have_styling([['color', 'green'], ['font-size', '1.1em']])
86
86
  <html>
87
87
  <head>
@@ -95,7 +95,7 @@ describe Roadie::Inliner do
95
95
  HTML
96
96
  end
97
97
 
98
- it "should be removed" do
98
+ it "is removed" do
99
99
  rendering(<<-HTML).should_not have_selector('style')
100
100
  <html>
101
101
  <head>
@@ -108,16 +108,16 @@ describe Roadie::Inliner do
108
108
  HTML
109
109
  end
110
110
 
111
- it "should not be touched when data-immutable=true" do
111
+ it "is not touched when data-immutable is set" do
112
112
  document = rendering <<-HTML
113
- <style type="text/css" data-immutable="true">p { color: red; }</style>
113
+ <style type="text/css" data-immutable>p { color: red; }</style>
114
114
  <p></p>
115
115
  HTML
116
- document.should have_selector('style[data-immutable=true]')
116
+ document.should have_selector('style[data-immutable]')
117
117
  document.should_not have_styling('color' => 'red')
118
118
  end
119
119
 
120
- it "should not be touched when media=print" do
120
+ it "is not touched when for print media" do
121
121
  document = rendering <<-HTML
122
122
  <style type="text/css" media="print">p { color: red; }</style>
123
123
  <p></p>
@@ -126,7 +126,9 @@ describe Roadie::Inliner do
126
126
  document.should_not have_styling('color' => 'red').at_selector('p')
127
127
  end
128
128
 
129
- it "should still be inlined when no external css rules are defined" do
129
+ it "is still inlined when no external css rules are defined" do
130
+ # This is just testing that the main code paths are still active even
131
+ # when css is set to nil
130
132
  use_css nil
131
133
  rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p')
132
134
  <style type="text/css">p { color: green; }</style>
@@ -136,23 +138,194 @@ describe Roadie::Inliner do
136
138
  end
137
139
  end
138
140
 
141
+ describe "linked stylesheets" do
142
+ before(:each) do
143
+ Rails.stub(:root => FixturesPath)
144
+ end
145
+
146
+ it "inlines styles from the linked stylesheet" do
147
+ rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p')
148
+ <html>
149
+ <head>
150
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css">
151
+ </head>
152
+ <body>
153
+ <p></p>
154
+ </body>
155
+ </html>
156
+ HTML
157
+ end
158
+
159
+ it "inlines styles from more than one linked stylesheet" do
160
+ html = <<-HTML
161
+ <html>
162
+ <head>
163
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css">
164
+ <link rel="stylesheet" href="/stylesheets/large_purple_paragraphs.css">
165
+ </head>
166
+ <body>
167
+ <p></p>
168
+ </body>
169
+ </html>
170
+ HTML
171
+
172
+ rendering(html).should have_styling([
173
+ ['color', 'purple'],
174
+ ['font-size', '18px'],
175
+ ]).at_selector('p')
176
+ end
177
+
178
+ it "removes the stylesheet links from the DOM" do
179
+ rendering(<<-HTML).should_not have_selector('link')
180
+ <html>
181
+ <head>
182
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css">
183
+ <link rel="stylesheet" href="/stylesheets/large_purple_paragraphs.css">
184
+ </head>
185
+ <body>
186
+ </body>
187
+ </html>
188
+ HTML
189
+ end
190
+
191
+ context "when stylesheet is for print media" do
192
+ it "does not inline the stylesheet" do
193
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
194
+ <html>
195
+ <head>
196
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css" media="print">
197
+ </head>
198
+ <body>
199
+ <p></p>
200
+ </body>
201
+ </html>
202
+ HTML
203
+ end
204
+
205
+ it "does not remove the links" do
206
+ rendering(<<-HTML).should have_selector('link')
207
+ <html>
208
+ <head>
209
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css" media="print">
210
+ </head>
211
+ <body>
212
+ </body>
213
+ </html>
214
+ HTML
215
+ end
216
+ end
217
+
218
+ context "when stylesheet is marked as immutable" do
219
+ it "does not inline the stylesheet" do
220
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
221
+ <html>
222
+ <head>
223
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css" data-immutable="true">
224
+ </head>
225
+ <body>
226
+ <p></p>
227
+ </body>
228
+ </html>
229
+ HTML
230
+ end
231
+
232
+ it "does not remove link" do
233
+ rendering(<<-HTML).should have_selector('link')
234
+ <html>
235
+ <head>
236
+ <link rel="stylesheet" href="/stylesheets/green_paragraphs.css" data-immutable="true">
237
+ </head>
238
+ <body>
239
+ </body>
240
+ </html>
241
+ HTML
242
+ end
243
+ end
244
+
245
+ context "when stylesheet link uses an absolute URL" do
246
+ it "does not inline the stylesheet" do
247
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
248
+ <html>
249
+ <head>
250
+ <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css">
251
+ </head>
252
+ <body>
253
+ <p></p>
254
+ </body>
255
+ </html>
256
+ HTML
257
+ end
258
+
259
+ it "does not remove link" do
260
+ rendering(<<-HTML).should have_selector('link')
261
+ <html>
262
+ <head>
263
+ <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css">
264
+ </head>
265
+ <body>
266
+ </body>
267
+ </html>
268
+ HTML
269
+ end
270
+ end
271
+
272
+ context "stylesheet cannot be found on disk" do
273
+ it "raises an error" do
274
+ html = <<-HTML
275
+ <html>
276
+ <head>
277
+ <link rel="stylesheet" href="/stylesheets/not_found.css">
278
+ </head>
279
+ <body>
280
+ </body>
281
+ </html>
282
+ HTML
283
+
284
+ expect { rendering(html) }.to raise_error do |error|
285
+ error.should be_a(Roadie::CSSFileNotFound)
286
+ error.filename.should == Rails.root.join('public', 'stylesheets', 'not_found.css')
287
+ error.guess.should == '/stylesheets/not_found.css'
288
+ end
289
+ end
290
+ end
291
+
292
+ context "link element is not for a stylesheet" do
293
+ it "is ignored" do
294
+ html = <<-HTML
295
+ <html>
296
+ <head>
297
+ <link rel="not_stylesheet" href="/stylesheets/green_paragraphs.css">
298
+ </head>
299
+ <body>
300
+ <p></p>
301
+ </body>
302
+ </html>
303
+ HTML
304
+ rendering(html).tap do |document|
305
+ document.should_not have_styling('color' => 'green').at_selector('p')
306
+ document.should have_selector('link')
307
+ end
308
+ end
309
+ end
310
+ end
311
+
139
312
  describe "making urls absolute" do
140
- it "should work on image sources" do
313
+ it "works on image sources" do
141
314
  rendering('<img src="/images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
142
315
  rendering('<img src="../images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
143
316
  rendering('<img src="foo.jpg" />').should have_attribute('src' => 'http://example.com/foo.jpg')
144
317
  end
145
318
 
146
- it "should not touch image sources that are already absolute" do
319
+ it "does not touch image sources that are already absolute" do
147
320
  rendering('<img src="http://other.example.org/images/foo.jpg" />').should have_attribute('src' => 'http://other.example.org/images/foo.jpg')
148
321
  end
149
322
 
150
- it "should work on inlined style attributes" do
323
+ it "works on inlined style attributes" do
151
324
  rendering('<p style="background: url(/paper.png)"></p>').should have_styling('background' => 'url(http://example.com/paper.png)')
152
325
  rendering('<p style="background: url(&quot;/paper.png&quot;)"></p>').should have_styling('background' => 'url("http://example.com/paper.png")')
153
326
  end
154
327
 
155
- it "should work on external style declarations" do
328
+ it "works on external style declarations" do
156
329
  use_css "p { background-image: url(/paper.png); }
157
330
  table { background-image: url('/paper.png'); }
158
331
  div { background-image: url(\"/paper.png\"); }"
@@ -161,14 +334,14 @@ describe Roadie::Inliner do
161
334
  rendering('<div></div>').should have_styling('background-image' => 'url("http://example.com/paper.png")')
162
335
  end
163
336
 
164
- it "should not touch style urls that are already absolute" do
337
+ it "does not touch style urls that are already absolute" do
165
338
  external_url = 'url(http://other.example.org/paper.png)'
166
339
  use_css "p { background-image: #{external_url}; }"
167
340
  rendering('<p></p>').should have_styling('background-image' => external_url)
168
341
  rendering(%(<div style="background-image: #{external_url}"></div>)).should have_styling('background-image' => external_url)
169
342
  end
170
343
 
171
- it "should not touch the urls when no url options are defined" do
344
+ it "does not touch the urls when no url options are defined" do
172
345
  use_css "img { background: url(/a.jpg); }"
173
346
  rendering('<img src="/b.jpg" />', :url_options => nil).tap do |document|
174
347
  document.should have_attribute('src' => '/b.jpg').at_selector('img')
@@ -176,7 +349,7 @@ describe Roadie::Inliner do
176
349
  end
177
350
  end
178
351
 
179
- it "should support port and protocol settings" do
352
+ it "supports port and protocol settings" do
180
353
  use_css "img { background: url(/a.jpg); }"
181
354
  rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https', :port => '8080'}).tap do |document|
182
355
  document.should have_attribute('src' => 'https://example.com:8080/b.jpg').at_selector('img')
@@ -184,38 +357,38 @@ describe Roadie::Inliner do
184
357
  end
185
358
  end
186
359
 
187
- it "should not touch data: URIs" do
360
+ it "does not touch data: URIs" do
188
361
  use_css "div { background: url(data:abcdef); }"
189
362
  rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)')
190
363
  end
191
364
  end
192
365
 
193
366
  describe "inserting tags" do
194
- it "should insert a doctype if not present" do
367
+ it "inserts a doctype if not present" do
195
368
  rendering('<html><body></body></html>').to_xml.should include('<!DOCTYPE ')
196
369
  rendering('<!DOCTYPE html><html><body></body></html>').to_xml.should_not match(/(DOCTYPE.*?){2}/)
197
370
  end
198
371
 
199
- it "should set xmlns of <html> to that of XHTML" do
372
+ it "sets xmlns of <html> to that of XHTML" do
200
373
  rendering('<html><body></body></html>').should have_selector('html[xmlns="http://www.w3.org/1999/xhtml"]')
201
374
  end
202
375
 
203
- it "should insert basic html structure if not present" do
376
+ it "inserts basic html structure if not present" do
204
377
  rendering('<h1>Hey!</h1>').should have_selector('html > head + body > h1')
205
378
  end
206
379
 
207
- it "should insert <head> if not present" do
380
+ it "inserts <head> if not present" do
208
381
  rendering('<html><body></body></html>').should have_selector('html > head + body')
209
382
  end
210
383
 
211
- it "should insert meta tag describing content-type" do
384
+ it "inserts meta tag describing content-type" do
212
385
  rendering('<html><head></head><body></body></html>').tap do |document|
213
386
  document.should have_selector('head meta[http-equiv="Content-Type"]')
214
387
  document.css('head meta[http-equiv="Content-Type"]').first['content'].should == 'text/html; charset=UTF-8'
215
388
  end
216
389
  end
217
390
 
218
- it "should not insert duplicate meta tags describing content-type" do
391
+ it "does not insert duplicate meta tags describing content-type" do
219
392
  rendering(<<-HTML).to_html.scan('meta').should have(1).item
220
393
  <html>
221
394
  <head>
@@ -227,15 +400,15 @@ describe Roadie::Inliner do
227
400
  end
228
401
 
229
402
  describe "css url regex" do
230
- it "should parse css urls" do
403
+ it "parses css urls" do
231
404
  {
232
- 'url(/foo.jpg)' => '/foo.jpg',
233
- 'url("/foo.jpg")' => '/foo.jpg',
234
- "url('/foo.jpg')" => '/foo.jpg',
235
- 'url(http://localhost/foo.jpg)' => 'http://localhost/foo.jpg',
236
- 'url("http://localhost/foo.jpg")' => 'http://localhost/foo.jpg',
237
- "url('http://localhost/foo.jpg')" => 'http://localhost/foo.jpg',
238
- 'url(/andromeda_(galaxy).jpg)' => '/andromeda_(galaxy).jpg',
405
+ %{url(/foo.jpg)} => '/foo.jpg',
406
+ %{url("/foo.jpg")} => '/foo.jpg',
407
+ %{url('/foo.jpg')} => '/foo.jpg',
408
+ %{url(http://localhost/foo.jpg)} => 'http://localhost/foo.jpg',
409
+ %{url("http://localhost/foo.jpg")} => 'http://localhost/foo.jpg',
410
+ %{url('http://localhost/foo.jpg')} => 'http://localhost/foo.jpg',
411
+ %{url(/andromeda_(galaxy).jpg)} => '/andromeda_(galaxy).jpg',
239
412
  }.each do |raw, expected|
240
413
  raw =~ Roadie::Inliner::CSS_URL_REGEXP
241
414
  $2.should == expected
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  module Roadie
4
4
  describe StyleDeclaration do
5
- it "should be initialized with a property, value, if it is marked as important, and the specificity" do
5
+ it "is initialized with a property, value, if it is marked as important, and the specificity" do
6
6
  StyleDeclaration.new('color', 'green', true, 45).tap do |declaration|
7
7
  declaration.property.should == 'color'
8
8
  declaration.value.should == 'green'
@@ -12,7 +12,7 @@ module Roadie
12
12
  end
13
13
 
14
14
  describe "string representation" do
15
- it "should be the property and the value joined with a colon" do
15
+ it "is the property and the value joined with a colon" do
16
16
  StyleDeclaration.new('color', 'green', false, 1).to_s.should == 'color:green'
17
17
  StyleDeclaration.new('font-size', '1.1em', false, 1).to_s.should == 'font-size:1.1em'
18
18
  end
@@ -23,18 +23,18 @@ module Roadie
23
23
  StyleDeclaration.new('color', 'green', important, specificity)
24
24
  end
25
25
 
26
- it "should compare on specificity" do
26
+ it "compares on specificity" do
27
27
  declaration(5).should be == declaration(5)
28
28
  declaration(4).should be < declaration(5)
29
29
  declaration(6).should be > declaration(5)
30
30
  end
31
31
 
32
32
  context "with an important declaration" do
33
- it "should be less than the important declaration regardless of the specificity" do
33
+ it "is less than the important declaration regardless of the specificity" do
34
34
  declaration(99, false).should be < declaration(1, true)
35
35
  end
36
36
 
37
- it "should compare like normal when both declarations are important" do
37
+ it "compares like normal when both declarations are important" do
38
38
  declaration(5, true).should be == declaration(5, true)
39
39
  declaration(4, true).should be < declaration(5, true)
40
40
  declaration(6, true).should be > declaration(5, true)
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Roadie do
4
4
  describe ".inline_css" do
5
- it "should create an instance of Roadie::Inliner and execute it" do
5
+ it "creates an instance of Roadie::Inliner and execute it" do
6
6
  Roadie::Inliner.should_receive(:new).with('attri', 'butes').and_return(double('inliner', :execute => 'html'))
7
7
  Roadie.inline_css('attri', 'butes').should == 'html'
8
8
  end
@@ -11,16 +11,16 @@ describe Roadie do
11
11
  describe ".load_css(root, targets)" do
12
12
  let(:fixtures_root) { Pathname.new(__FILE__).dirname.join('..', 'fixtures', 'public', 'stylesheets') }
13
13
 
14
- it "should load files matching the target names under root/public/stylesheets" do
14
+ it "loads files matching the target names under root/public/stylesheets" do
15
15
  Roadie.load_css(fixtures_root, ['foo']).should == 'contents of foo'
16
16
  end
17
17
 
18
- it "should load files in order and join them with a newline" do
18
+ it "loads files in order and join them with a newline" do
19
19
  Roadie.load_css(fixtures_root, %w[foo bar]).should == "contents of foo\ncontents of bar"
20
20
  Roadie.load_css(fixtures_root, %w[bar foo]).should == "contents of bar\ncontents of foo"
21
21
  end
22
22
 
23
- it "should raise a Roadie::CSSFileNotFound error when a css file could not be found" do
23
+ it "raises a Roadie::CSSFileNotFound error when a css file could not be found" do
24
24
  expect { Roadie.load_css(fixtures_root, ['not_here']) }.to raise_error(Roadie::CSSFileNotFound, /not_here/)
25
25
  end
26
26
  end
data/spec/spec_helper.rb CHANGED
@@ -34,3 +34,5 @@ else
34
34
  end
35
35
  end
36
36
 
37
+ FixturesPath = Pathname.new(File.dirname(__FILE__)).join('fixtures')
38
+
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: roadie
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.0.1
5
+ version: 1.1.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Magnus Bergmark
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-14 00:00:00 +02:00
13
+ date: 2011-06-24 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -65,15 +65,17 @@ executables: []
65
65
  extensions: []
66
66
 
67
67
  extra_rdoc_files:
68
- - README.textile
68
+ - README.md
69
+ - Changelog.md
69
70
  files:
70
71
  - .autotest
71
72
  - .gitignore
72
73
  - .yardopts
74
+ - Changelog.md
73
75
  - Gemfile
74
76
  - Gemfile.lock
75
77
  - MIT-LICENSE
76
- - README.textile
78
+ - README.md
77
79
  - Rakefile
78
80
  - autotest/discover.rb
79
81
  - lib/roadie.rb
@@ -85,7 +87,9 @@ files:
85
87
  - roadie.gemspec
86
88
  - spec/fixtures/public/stylesheets/bar.css
87
89
  - spec/fixtures/public/stylesheets/foo.css
90
+ - spec/fixtures/public/stylesheets/green_paragraphs.css
88
91
  - spec/fixtures/public/stylesheets/integration.css
92
+ - spec/fixtures/public/stylesheets/large_purple_paragraphs.css
89
93
  - spec/fixtures/views/integration_mailer/marketing.html.erb
90
94
  - spec/fixtures/views/integration_mailer/notification.html.erb
91
95
  - spec/fixtures/views/integration_mailer/notification.text.erb
@@ -130,7 +134,9 @@ summary: Making HTML emails comfortable for the Rails rockstars
130
134
  test_files:
131
135
  - spec/fixtures/public/stylesheets/bar.css
132
136
  - spec/fixtures/public/stylesheets/foo.css
137
+ - spec/fixtures/public/stylesheets/green_paragraphs.css
133
138
  - spec/fixtures/public/stylesheets/integration.css
139
+ - spec/fixtures/public/stylesheets/large_purple_paragraphs.css
134
140
  - spec/fixtures/views/integration_mailer/marketing.html.erb
135
141
  - spec/fixtures/views/integration_mailer/notification.html.erb
136
142
  - spec/fixtures/views/integration_mailer/notification.text.erb
data/README.textile DELETED
@@ -1,114 +0,0 @@
1
- h1. Roadie
2
-
3
- Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.
4
-
5
- If you want to have this in Rails 2, please see "MailStyle":https://www.github.com/purify/mail_style.
6
-
7
- h2. How does it work?
8
-
9
- Email clients have bad support for stylesheets, and some of them blocks stylesheets from downloading. The easiest way to handle this is to work with all styles inline, but that is error prone and hard to work with as you cannot use classes and/or reuse styling.
10
-
11
- This gem helps making this easier by automatically inlining stylesheet rules into the document before sending it. You just give it a list of stylesheets and it will go though all of the selectors assigning the styles to the maching elements. Careful attention has been put into rules being applied in the correct order, so it should behave just like in the browser[1].
12
-
13
- Roadie also rewrites all relative URLs in the email to a absolute counterpart, making images you insert and those referenced in your stylesheets work. No more headaches about how to write the stylesheets while still having them work with emails from your acceptance environments.
14
-
15
- fn1. Of course, rules like @:hover@ will not work by definition. Only static styles can be added.
16
-
17
- h2. Features
18
-
19
- * Writes CSS styles inline
20
- ** Respects @!important@ styles
21
- ** Does not overwrite styles already present in the @style@ attribute of tags
22
- ** Supports the same CSS selectors as "Nokogiri":http://nokogiri.org/ (use CSS3 selectors in your emails!)
23
- * Makes image urls absolute
24
- ** Hostname and port configurable on a per-environment basis
25
- * Makes link <code>href</code>s absolute
26
- * Automatically adds proper html skeleton when missing (you don't have to create a layout for emails)[2]
27
-
28
- fn2. This might be removed in a future version, though. You really ought to create a good layout and not let Roadie guess how you want to have it structured
29
-
30
- h3. What about Sass / Less?
31
-
32
- Sass is supported "by accident" as long as the stylesheets are generated and stored in the stylesheets directory. This is the default behavior from Sass. You are recommended to add a deploy task that generates the stylesheets to make sure that they are present at all times.
33
-
34
- h2. Install
35
-
36
- Roadie is officially supported in both Ruby 1.8.7 and 1.9.2.
37
-
38
- Add the gem to Rails' Gemfile
39
- <pre><code>gem 'roadie'</code></pre>
40
-
41
- h2. Usage
42
-
43
- Simply specify the <code>:css</code> option to mailer:
44
-
45
- <pre><code>class Notifier < ActionMailer::Base
46
- default :css => :email, :from => 'support@mycompany.com'
47
- def registration_mail
48
- mail(:subject => 'Welcome Aboard', :to => 'someone@example.com')
49
- end
50
-
51
- def newsletter
52
- mail(:subject => 'Newsletter', :to => 'someone@example.com', :css => [:email, :newsletter])
53
- end
54
- end</code></pre>
55
- (you could also use the @defaults@ method in ActionMailer)
56
-
57
- This will look for a css file called @email.css@ in your @public/stylesheets@ folder. The @css@ method can take either a string, a symbol or an array of both. You should pass the CSS filename without the ".css" extension.
58
-
59
- h3. Image URL Correcting
60
-
61
- If you have @default_url_options[:host]@ set in your mailer, then Roadie will do it's best to make the urls of images and in stylesheets absolute.
62
-
63
- In @application.rb@:
64
- <pre><code>class Application
65
- config.action_mailer.default_url_options = {:host => 'example.com'}
66
- end</code></pre>
67
-
68
- If you want to to be different depending on your environment, just set it in your environment's configuration instead.
69
-
70
- h3. Ignoring stylesheets
71
-
72
- By default, @style@ elements in the email document's @head@ are processed along with the stylesheets and removed from the @head@.
73
-
74
- You can set a special <code>data-immutable="true"</code> attribute on @style@ tags you do not want to be processed and removed from the document's @head@. This is the place to put things like @:hover@ selectors that you want to have for email clients allowing them.
75
-
76
- Style elements with <code>media="print"</code> are always ignored.
77
-
78
- h2. Documentation
79
-
80
- * "Online documentation for master":http://rubydoc.info/github/Mange/roadie/master/frames
81
-
82
- h2. Bugs / TODO
83
-
84
- * Improve overall performance
85
- * Clean up stylesheet assignment code
86
-
87
- h2. History and contributors
88
-
89
- This gem was originally developed for Rails 2 use on "Purify":http://purifyapp.com under the name "MailStyle":https://www.github.com/purify/mail_style. However, the author stopped maintaining it and a fork took place to make it Rails 3 compatible.
90
-
91
- The following people have contributed to the orignal gem:
92
-
93
- * "Jim Neath":http://jimneath.org (Original author)
94
- * "Lars Klevans":http://tastybyte.blogspot.com/
95
- * "Jonas Grimfelt":http://github.com/grimen
96
- * "Ben Johnson":http://www.binarylogic.com
97
- * "Istvan Hoka":http://istvanhoka.com/
98
- * "Voraz":http://blog.voraz.com.br
99
-
100
- h2. License
101
-
102
- (The MIT License)
103
-
104
- Copyright &copy; 2009-2011
105
-
106
- * "Jim Neath":http://jimneath.org
107
- * Magnus Bergmark <magnus.bergmark@gmail.com>
108
-
109
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
110
-
111
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
112
-
113
- THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
114
-