mail_style 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,21 +5,29 @@ require 'css_parser'
5
5
  module MailStyle
6
6
  module InlineStyles
7
7
  DOCTYPE = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
8
-
8
+
9
9
  module InstanceMethods
10
10
  def create_mail_with_inline_styles
11
- write_inline_styles if @css.present?
11
+ write_inline_styles
12
12
  create_mail_without_inline_styles
13
13
  end
14
-
14
+
15
15
  protected
16
-
16
+
17
+ # Flatten nested parts
18
+ def collect_parts(parts)
19
+ nested = parts.present? ? parts.map { |p| collect_parts(p.parts) }.flatten : []
20
+ [parts, nested].flatten
21
+ end
22
+
17
23
  def write_inline_styles
24
+ parts = collect_parts(@parts)
25
+
18
26
  # Parse only text/html parts
19
27
  parsable_parts(@parts).each do |part|
20
28
  part.body = parse_html(part.body)
21
29
  end
22
-
30
+
23
31
  # Parse single part emails if the body is html
24
32
  real_content_type, ctype_attrs = parse_content_type
25
33
  self.body = parse_html(body) if body.is_a?(String) && real_content_type == 'text/html'
@@ -27,9 +35,9 @@ module MailStyle
27
35
 
28
36
  def parsable_parts(parts)
29
37
  selected = []
30
- parts.each do |p|
31
- selected << p if p.content_type == 'text/html'
32
- selected += parsable_parts(p.parts)
38
+ parts.each do |part|
39
+ selected << part if part.content_type == 'text/html'
40
+ selected += parsable_parts(part.parts)
33
41
  end
34
42
  selected
35
43
  end
@@ -41,17 +49,18 @@ module MailStyle
41
49
 
42
50
  # Write inline styles
43
51
  element_styles = {}
44
-
52
+
45
53
  css_parser.each_selector do |selector, declaration, specificity|
54
+ next if selector.include?(':')
46
55
  html_document.css(selector).each do |element|
47
56
  declaration.to_s.split(';').each do |style|
48
57
  # Split style in attribute and value
49
58
  attribute, value = style.split(':').map(&:strip)
50
-
59
+
51
60
  # Set element style defaults
52
61
  element_styles[element] ||= {}
53
62
  element_styles[element][attribute] ||= { :specificity => 0, :value => '' }
54
-
63
+
55
64
  # Update attribute value if specificity is higher than previous values
56
65
  if element_styles[element][attribute][:specificity] <= specificity
57
66
  element_styles[element][attribute] = { :specificity => specificity, :value => value }
@@ -59,12 +68,12 @@ module MailStyle
59
68
  end
60
69
  end
61
70
  end
62
-
71
+
63
72
  # Loop through element styles
64
73
  element_styles.each_pair do |element, attributes|
65
74
  # Elements current styles
66
75
  current_style = element['style'].to_s.split(';').sort
67
-
76
+
68
77
  # Elements new styles
69
78
  new_style = attributes.map{|attribute, style| "#{attribute}: #{update_image_urls(style[:value])}"}
70
79
 
@@ -74,94 +83,121 @@ module MailStyle
74
83
  # Set new styles
75
84
  element['style'] = style.join(';')
76
85
  end
77
-
86
+
78
87
  # Return HTML
79
88
  html_document.to_html
80
89
  end
81
-
90
+
82
91
  def absolutize_image_sources(document)
83
92
  document.css('img').each do |img|
84
93
  src = img['src']
85
94
  img['src'] = src.gsub(src, absolutize_url(src))
86
95
  end
87
-
96
+
88
97
  document
89
98
  end
90
-
99
+
91
100
  # Create Nokogiri html document from part contents and add/amend certain elements.
92
101
  # Reference: http://www.creativeglo.co.uk/email-design/html-email-design-and-coding-tips-part-2/
93
102
  def create_html_document(body)
94
103
  # Add doctype to html along with body
95
104
  document = Nokogiri::HTML.parse(DOCTYPE + body)
96
-
105
+
97
106
  # Set some meta stuff
98
107
  html = document.at_css('html')
99
108
  html['xmlns'] = 'http://www.w3.org/1999/xhtml'
100
-
109
+
101
110
  # Create <head> element if missing
102
111
  head = document.at_css('head')
103
-
112
+
104
113
  unless head.present?
105
114
  head = Nokogiri::XML::Node.new('head', document)
106
115
  document.at_css('body').add_previous_sibling(head)
107
116
  end
108
-
117
+
109
118
  # Add utf-8 content type meta tag
110
119
  meta = Nokogiri::XML::Node.new('meta', document)
111
120
  meta['http-equiv'] = 'Content-Type'
112
121
  meta['content'] = 'text/html; charset=utf-8'
113
122
  head.add_child(meta)
114
-
123
+
124
+ # Grab all the styles that are inside <style> elements already in the document
125
+ @inline_rules = ""
126
+ document.css("style").each do |style|
127
+ # Do not inline print media styles
128
+ next if style['media'] == 'print'
129
+
130
+ # <style data-immutable="true"> are kept in the document
131
+ next if style['data-immutable'] == 'true'
132
+
133
+ @inline_rules << style.content
134
+ style.remove
135
+ end
136
+
115
137
  # Return document
116
138
  document
117
139
  end
118
-
140
+
119
141
  # Update image urls
120
142
  def update_image_urls(style)
121
143
  if default_url_options[:host].present?
122
144
  # Replace urls in stylesheets
123
145
  style.gsub!($1, absolutize_url($1, 'stylesheets')) if style[/url\(['"]?(.*)['"]?\)/i]
124
146
  end
125
-
147
+
126
148
  style
127
149
  end
128
-
129
- # Absolutize URL (Absolutize? Seriously?)
150
+
151
+ # Absolutize URL (Absolutize? Seriously?)
130
152
  def absolutize_url(url, base_path = '')
131
153
  original_url = url
132
-
154
+
133
155
  unless original_url[URI::regexp(%w[http https])]
134
- # Calculate new path
156
+ protocol = default_url_options[:protocol]
157
+ protocol = "http://" if protocol.blank?
158
+ protocol+= "://" unless protocol.include?("://")
159
+
135
160
  host = default_url_options[:host]
136
- url = URI.join("http://#{host}/", File.join(base_path, original_url)).to_s
161
+
162
+ [host,protocol].each{|r| original_url.gsub!(r,"") }
163
+ host = protocol+host unless host[URI::regexp(%w[http https])]
164
+
165
+ url = URI.join host, base_path, original_url
137
166
  end
138
-
139
- url
167
+
168
+ url.to_s
169
+
140
170
  end
141
171
 
142
172
  # Css Parser
143
173
  def css_parser
144
174
  parser = CssParser::Parser.new
145
- parser.add_block!(css_rules)
175
+
176
+ parser.add_block!(css_rules) if @css.present?
177
+ parser.add_block!(@inline_rules)
146
178
  parser
147
179
  end
148
-
180
+
149
181
  # Css Rules
150
182
  def css_rules
151
- File.read(css_file)
183
+ if @css.is_a?(Array)
184
+ @css.collect{|r| File.read(css_file(r)) }.join("\n")
185
+ else
186
+ File.read css_file(@css)
187
+ end
152
188
  end
153
-
189
+
154
190
  # Find the css file
155
- def css_file
156
- if @css.present?
157
- css = @css.to_s
191
+ def css_file(name=nil)
192
+ if name.present?
193
+ css = name.to_s
158
194
  css = css[/\.css$/] ? css : "#{css}.css"
159
195
  path = File.join(RAILS_ROOT, 'public', 'stylesheets', css)
160
196
  File.exist?(path) ? path : raise(CSSFileNotFound)
161
197
  end
162
198
  end
163
199
  end
164
-
200
+
165
201
  def self.included(receiver)
166
202
  receiver.send :include, InstanceMethods
167
203
  receiver.class_eval do
@@ -2,14 +2,16 @@ module MailStyle
2
2
  module InlineStyles
3
3
  module InstanceMethods
4
4
  def css_file_with_sass
5
+ p "css_file_with_sass"
5
6
  if !Sass::Plugin.checked_for_updates || Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
6
7
  Sass::Plugin.update_stylesheets
7
8
  end
8
-
9
+
9
10
  css_file_without_sass
10
11
  end
11
-
12
- alias_method_chain :css_file, :sass
12
+
13
+ #alias_method_chain :css_file, :sass
13
14
  end
14
15
  end
15
16
  end
17
+
data/readme.textile CHANGED
@@ -11,9 +11,9 @@ h2. Install
11
11
  First install the dependencies:
12
12
 
13
13
  <pre><code>sudo gem install nokogiri css_parser</code></pre>
14
-
14
+
15
15
  Then install MailStyle to your rails app, either as a plugin:
16
-
16
+
17
17
  <pre><code>script/plugin install http://github.com/purify/mail_style</code></pre>
18
18
 
19
19
  Or you can install the gem:
@@ -36,16 +36,33 @@ Simply add the <code>css</code> method to your deliver actions:
36
36
  <pre><code>class Notifier < ActionMailer::Base
37
37
  def welcome_email
38
38
  css :email
39
-
39
+
40
40
  subject 'Welcome Aboard'
41
41
  recipients 'someone@example.com'
42
42
  from 'jimneath@googlemail.com'
43
43
  sent_on Time.now
44
44
  end
45
+
46
+ def newsletter_email
47
+ css [:email,:newsletter]
48
+
49
+ subject 'Newsletter'
50
+ recipients 'someone@example.com'
51
+ from 'jimneath@googlemail.com'
52
+ sent_on Time.now
53
+ end
45
54
  end</code></pre>
46
55
 
47
56
  This will look for a css file called _email.css_ in your _public/stylesheets_ folder. The <code>css</code> method can take either a string or a symbol. You can also pass the css file name with or without the .css extension.
48
57
 
58
+ h3. Styles in the email's HEAD
59
+
60
+ By default, &lt;style&gt; elements in the email document's &lt;head&gt; are processed and removed from the &lt;head&gt;.
61
+
62
+ Style elements with <code>media="print"</code> are ignored.
63
+
64
+ You can set a special <code>data-immutable="true"</code> attribute on &lt;style&gt; tags you do not want to be processed and removed from the document's &lt;head&gt;.
65
+
49
66
  h2. Image URL Correcting
50
67
 
51
68
  If you have _default_url_options[:host]_ set in your mailer, then MailStyle will do it's best to make the urls of images absolute.
@@ -84,4 +101,5 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
84
101
 
85
102
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
86
103
 
87
- 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.
104
+ 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.
105
+
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
- require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'spec_helper'
3
3
 
4
4
  RAILS_ROOT = File.join(File.dirname(__FILE__), '../../../../')
5
5
 
@@ -15,7 +15,23 @@ class TestMailer < ActionMailer::Base
15
15
  def test_multipart(css_file = nil)
16
16
  setup_email(css_file)
17
17
  content_type 'multipart/alternative'
18
- part :content_type => 'text/html', :body => '<p class="text">Hello World</p>'
18
+ part :content_type => 'text/html', :body => '<p class="text">Hello <a href="htt://example.com/">World</a></p>'
19
+ part :content_type => 'text/plain', :body => 'Hello World'
20
+ end
21
+
22
+ def test_nested_multipart_mixed(css_file = nil)
23
+ setup_email(css_file)
24
+ content_type "multipart/mixed"
25
+ part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
26
+ p.part :content_type => 'text/html', :body => '<p class="text">Hello World</p>'
27
+ p.part :content_type => 'text/plain', :body => 'Hello World'
28
+ end
29
+ end
30
+
31
+ def test_inline_rules(rules)
32
+ setup_email(nil)
33
+ content_type 'multipart/alternative'
34
+ part :content_type => 'text/html', :body => "#{rules}<p class=\"text\">Hello World</p>"
19
35
  part :content_type => 'text/plain', :body => 'Hello World'
20
36
  end
21
37
 
@@ -50,6 +66,47 @@ class TestMailer < ActionMailer::Base
50
66
  end
51
67
  end
52
68
 
69
+ shared_examples_for "inline styles" do
70
+ before(:each) do
71
+ css_rules <<-EOF
72
+ body { background: #000 }
73
+ .text { color: #0f0; font-size: 14px }
74
+ p { color: #f00; line-height: 1.5 }
75
+ EOF
76
+ end
77
+
78
+ it "should add the correct xml namespace" do
79
+ should match(/<html xmlns="http:\/\/www\.w3\.org\/1999\/xhtml">/)
80
+ end
81
+
82
+ it "should write the xhtml 1.0 doctype" do
83
+ should match(/<!DOCTYPE html PUBLIC "-\/\/W3C\/\/DTD XHTML 1\.0 Transitional\/\/EN" "http:\/\/www.w3.org\/TR\/xhtml1\/DTD\/xhtml1-transitional\.dtd">/mi)
84
+ end
85
+
86
+ it "should write utf-8 content type meta tag" do
87
+ should match(/<head>.*<meta http\-equiv="Content\-Type" content="text\/html; charset=utf\-8">.*<\/head>/mi)
88
+ end
89
+
90
+ it "should wrap with html and body tag if missing" do
91
+ should match(/<html.*>.*<body.*>.*<\/body>.*<\/html>/m)
92
+ end
93
+
94
+ it "should add style to body" do
95
+ should match(/<body style="background: #000">/)
96
+ end
97
+
98
+ it "should add both styles to paragraph" do
99
+ should match(/<p class="text" style="color: #0f0;font-size: 14px;line-height: 1.5">/)
100
+ end
101
+
102
+ it "should not crash on :pseudo-classes" do
103
+ css_rules("a:link { color: #f00 }")
104
+ expect do
105
+ subject
106
+ end.to_not raise_error(StandardError)
107
+ end
108
+ end
109
+
53
110
  describe 'Inline styles' do
54
111
  describe 'singlepart' do
55
112
  before(:each) do
@@ -95,41 +152,9 @@ describe 'Inline styles' do
95
152
  end
96
153
 
97
154
  describe 'rendering inline styles' do
98
- before(:each) do
99
- css_rules <<-EOF
100
- body { background: #000 }
101
- .text { color: #0f0; font-size: 14px }
102
- p { color: #f00; line-height: 1.5 }
103
- EOF
104
-
105
- # Generate email
106
- @email = TestMailer.deliver_test_multipart(:real)
107
- @html = html_part(@email)
108
- end
109
-
110
- it "should add the correct xml namespace" do
111
- @html.should match(/<html xmlns="http:\/\/www\.w3\.org\/1999\/xhtml">/)
112
- end
113
-
114
- it "should write the xhtml 1.0 doctype" do
115
- @html.should match(/<!DOCTYPE html PUBLIC "-\/\/W3C\/\/DTD XHTML 1\.0 Transitional\/\/EN" "http:\/\/www.w3.org\/TR\/xhtml1\/DTD\/xhtml1-transitional\.dtd">/mi)
116
- end
117
-
118
- it "should write utf-8 content type meta tag" do
119
- @html.should match(/<head>.*<meta http\-equiv="Content\-Type" content="text\/html; charset=utf\-8">.*<\/head>/mi)
120
- end
121
-
122
- it "should wrap with html and body tag if missing" do
123
- @html.should match(/<html.*>.*<body.*>.*<\/body>.*<\/html>/m)
124
- end
125
-
126
- it "should add style to body" do
127
- @html.should match(/<body style="background: #000">/)
128
- end
129
-
130
- it "should add both styles to paragraph" do
131
- @html.should match(/<p class="text" style="color: #0f0;font-size: 14px;line-height: 1.5">/)
132
- end
155
+ let(:email) { TestMailer.deliver_test_multipart(:real) }
156
+ subject { html_part(email) }
157
+ it_should_behave_like("inline styles")
133
158
  end
134
159
 
135
160
  describe 'combining styles' do
@@ -155,9 +180,13 @@ describe 'Inline styles' do
155
180
  end
156
181
 
157
182
  describe 'css file' do
158
- it "should do nothing if no css file is set" do
183
+ it "should not change the styles nothing if no css file is set" do
184
+ css_rules <<-EOF
185
+ .text { color: #0f0; }
186
+ p { color: #f00; }
187
+ EOF
159
188
  @email = TestMailer.deliver_test_multipart(nil)
160
- html_part(@email).should eql('<p class="text">Hello World</p>')
189
+ html_part(@email).should match(/<p class="text">/)
161
190
  end
162
191
 
163
192
  it "should raise MailStyle::CSSFileNotFound if css file does not exist" do
@@ -184,4 +213,49 @@ describe 'Inline styles' do
184
213
  @email.parts.length.should eql(2)
185
214
  end
186
215
  end
216
+
217
+ describe "multipart mixed" do
218
+ let(:email) { TestMailer.deliver_test_nested_multipart_mixed(:real) }
219
+ subject { html_part(email) }
220
+ it_should_behave_like("inline styles")
221
+ end
222
+
223
+ describe "inline rules" do
224
+ let(:email) { TestMailer.deliver_test_inline_rules("<style> .text { color: #f00; line-height: 1.5 } </style>") }
225
+ subject { html_part(email) }
226
+
227
+ it "should style the elements with rules inside the document" do
228
+ should match(/<p class="text" style="color:\s+#f00;\s*line-height:\s+1.5">/)
229
+ end
230
+
231
+ it "should remove the styles from the document" do
232
+ should_not match(/<style/)
233
+ end
234
+ end
235
+
236
+ describe "inline rules for print media" do
237
+ let(:email) { TestMailer.deliver_test_inline_rules('<style media="print"> .text { color: #f00; } </style>') }
238
+ subject { html_part(email) }
239
+
240
+ it "should not change element styles" do
241
+ should match(/<p class="text">/)
242
+ end
243
+
244
+ it "should not remove the styles from the document" do
245
+ should match(/<style media="print"/)
246
+ end
247
+ end
248
+
249
+ describe "inline immutable styles" do
250
+ let(:email) { TestMailer.deliver_test_inline_rules('<style data-immutable="true"> .text { color: #f00; } </style>') }
251
+ subject { html_part(email) }
252
+
253
+ it "should not change element styles" do
254
+ should match(/<p class="text">/)
255
+ end
256
+
257
+ it "should not remove the styles from the document" do
258
+ should match(/<style data-immutable="true"/)
259
+ end
260
+ end
187
261
  end
data/spec/spec_helper.rb CHANGED
@@ -5,9 +5,14 @@ require 'spec'
5
5
  require 'action_mailer'
6
6
  require 'mail_style'
7
7
 
8
+ def flatten_parts(parts)
9
+ nested = !parts.empty? ? parts.map { |p| flatten_parts(p.parts) }.flatten : []
10
+ [parts, nested].flatten
11
+ end
12
+
8
13
  # Extract HTML Part
9
14
  def html_part(email)
10
- email.parts.select{|part| part.content_type == 'text/html'}.first.body
15
+ flatten_parts(email.parts).select{|part| part.content_type == 'text/html'}.first.body
11
16
  end
12
17
 
13
18
  def css_rules(css)
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 4
9
- version: 0.1.4
8
+ - 5
9
+ version: 0.1.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jim Neath