roadie 1.0.0.pre2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,8 @@
1
1
  .DS_Store
2
2
  pkg
3
- .*~
3
+ .*~
4
+ .yardoc
5
+
6
+ .rspec
7
+ .idea/*
8
+ .rvmrc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/Gemfile.lock CHANGED
@@ -10,57 +10,66 @@ GEM
10
10
  remote: http://rubygems.org/
11
11
  specs:
12
12
  abstract (1.0.0)
13
- actionmailer (3.0.1)
14
- actionpack (= 3.0.1)
15
- mail (~> 2.2.5)
16
- actionpack (3.0.1)
17
- activemodel (= 3.0.1)
18
- activesupport (= 3.0.1)
13
+ actionmailer (3.0.3)
14
+ actionpack (= 3.0.3)
15
+ mail (~> 2.2.9)
16
+ actionpack (3.0.3)
17
+ activemodel (= 3.0.3)
18
+ activesupport (= 3.0.3)
19
19
  builder (~> 2.1.2)
20
20
  erubis (~> 2.6.6)
21
- i18n (~> 0.4.1)
21
+ i18n (~> 0.4)
22
22
  rack (~> 1.2.1)
23
- rack-mount (~> 0.6.12)
24
- rack-test (~> 0.5.4)
23
+ rack-mount (~> 0.6.13)
24
+ rack-test (~> 0.5.6)
25
25
  tzinfo (~> 0.3.23)
26
- activemodel (3.0.1)
27
- activesupport (= 3.0.1)
26
+ activemodel (3.0.3)
27
+ activesupport (= 3.0.3)
28
28
  builder (~> 2.1.2)
29
- i18n (~> 0.4.1)
30
- activesupport (3.0.1)
29
+ i18n (~> 0.4)
30
+ activesupport (3.0.3)
31
31
  builder (2.1.2)
32
- css_parser (1.0.1)
32
+ css_parser (1.1.5)
33
33
  diff-lcs (1.1.2)
34
34
  erubis (2.6.6)
35
35
  abstract (>= 1.0.0)
36
- i18n (0.4.1)
37
- mail (2.2.7)
36
+ i18n (0.5.0)
37
+ mail (2.2.15)
38
38
  activesupport (>= 2.3.6)
39
- mime-types
40
- treetop (>= 1.4.5)
39
+ i18n (>= 0.4.0)
40
+ mime-types (~> 1.16)
41
+ treetop (~> 1.4.8)
41
42
  mime-types (1.16)
42
- nokogiri (1.4.3.1)
43
+ nokogiri (1.4.4)
43
44
  polyglot (0.3.1)
44
45
  rack (1.2.1)
45
46
  rack-mount (0.6.13)
46
47
  rack (>= 1.0.0)
47
- rack-test (0.5.6)
48
+ rack-test (0.5.7)
48
49
  rack (>= 1.0)
49
- rspec (2.0.0)
50
- rspec-core (= 2.0.0)
51
- rspec-expectations (= 2.0.0)
52
- rspec-mocks (= 2.0.0)
53
- rspec-core (2.0.0)
54
- rspec-expectations (2.0.0)
55
- diff-lcs (>= 1.1.2)
56
- rspec-mocks (2.0.0)
57
- rspec-core (= 2.0.0)
58
- rspec-expectations (= 2.0.0)
59
- rspec-rails (2.0.1)
60
- rspec (~> 2.0.0)
61
- treetop (1.4.8)
50
+ railties (3.0.3)
51
+ actionpack (= 3.0.3)
52
+ activesupport (= 3.0.3)
53
+ rake (>= 0.8.7)
54
+ thor (~> 0.14.4)
55
+ rake (0.8.7)
56
+ rspec (2.5.0)
57
+ rspec-core (~> 2.5.0)
58
+ rspec-expectations (~> 2.5.0)
59
+ rspec-mocks (~> 2.5.0)
60
+ rspec-core (2.5.1)
61
+ rspec-expectations (2.5.0)
62
+ diff-lcs (~> 1.1.2)
63
+ rspec-mocks (2.5.0)
64
+ rspec-rails (2.5.0)
65
+ actionpack (~> 3.0)
66
+ activesupport (~> 3.0)
67
+ railties (~> 3.0)
68
+ rspec (~> 2.5.0)
69
+ thor (0.14.6)
70
+ treetop (1.4.9)
62
71
  polyglot (>= 0.3.1)
63
- tzinfo (0.3.23)
72
+ tzinfo (0.3.24)
64
73
 
65
74
  PLATFORMS
66
75
  ruby
data/README.textile CHANGED
@@ -73,6 +73,10 @@ You can set a special <code>data-immutable="true"</code> attribute on @style@ ta
73
73
 
74
74
  Style elements with <code>media="print"</code> are always ignored.
75
75
 
76
+ h2. Documentation
77
+
78
+ * "Online documentation for master":http://rubydoc.info/github/Mange/roadie/master/frames
79
+
76
80
  h2. Bugs / TODO
77
81
 
78
82
  * Improve overall performance
data/lib/roadie.rb CHANGED
@@ -1,26 +1,35 @@
1
1
  module Roadie
2
- class CSSFileNotFound < StandardError; end
3
-
4
- def self.inline_css(*args);
2
+ # Shortcut for inlining CSS using {Inliner}
3
+ # @see Inliner
4
+ def self.inline_css(*args)
5
5
  Roadie::Inliner.new(*args).execute
6
6
  end
7
7
 
8
+ # Tries to load the CSS "names" specified in the +targets+ parameter inside the +root+ path.
9
+ #
10
+ # @example
11
+ # Roadie.load_css(Rails.root, %w[application newsletter])
12
+ #
13
+ # @param [Pathname] root The root path of your stylesheets
14
+ # @param [Array<String, Symbol>] targets Stylesheet names - <b>without extensions</b>
15
+ # @return [String] The combined contents of the CSS files
16
+ # @raise [CSSFileNotFound] When a target cannot be found under +[root]/[target].css+
8
17
  def self.load_css(root, targets)
9
- loaded_css = []
10
- stylesheets = root.join('public', 'stylesheets')
18
+ css_files_from_targets(root, targets).map do |file|
19
+ raise CSSFileNotFound, file unless file.exist?
20
+ file.read
21
+ end.join("\n")
22
+ end
11
23
 
12
- targets.map { |target| stylesheets.join("#{target}.css") }.each do |target_file|
13
- if target_file.exist?
14
- loaded_css << target_file.read
15
- else
16
- raise CSSFileNotFound, "Could not find #{target_file}"
17
- end
24
+ private
25
+ def self.css_files_from_targets(root, targets)
26
+ targets.map { |target| root.join("#{target}.css") }
18
27
  end
19
- loaded_css.join("\n")
20
- end
21
28
  end
22
29
 
23
30
  require 'roadie/version'
31
+ require 'roadie/css_file_not_found'
32
+ require 'roadie/style_declaration'
24
33
  require 'roadie/inliner'
25
34
 
26
35
  require 'action_mailer'
@@ -3,6 +3,9 @@ require 'nokogiri'
3
3
  require 'css_parser'
4
4
 
5
5
  module Roadie
6
+ # This module adds the Roadie functionality to ActionMailer 3 when included in ActionMailer::Base.
7
+ #
8
+ # If you want to add Roadie to any other mail framework, take a look at how this module is implemented.
6
9
  module ActionMailerExtensions
7
10
  def self.included(base)
8
11
  base.class_eval do
@@ -14,24 +17,28 @@ module Roadie
14
17
  protected
15
18
  def mail_with_inline_styles(headers = {}, &block)
16
19
  @inline_style_css_targets = headers[:css]
17
- mail_without_inline_styles(headers.except(:css), &block).tap do |email|
18
- email[:css] = nil
20
+ mail_without_inline_styles(headers, &block).tap do |email|
21
+ email.header.fields.delete_if { |field| field.name == 'css' }
19
22
  end
20
23
  end
21
24
 
22
25
  def collect_responses_and_parts_order_with_inline_styles(headers, &block)
23
26
  responses, order = collect_responses_and_parts_order_without_inline_styles(headers, &block)
24
- new_responses = []
25
- responses.each do |response|
26
- new_responses << inline_style_response(response)
27
- end
28
- [new_responses, order]
27
+ [responses.map { |response| inline_style_response(response) }, order]
29
28
  end
30
29
 
31
30
  private
31
+ def url_options
32
+ Rails.application.config.action_mailer.default_url_options
33
+ end
34
+
35
+ def stylesheet_root
36
+ Rails.root.join('public', 'stylesheets')
37
+ end
38
+
32
39
  def inline_style_response(response)
33
40
  if response[:content_type] == 'text/html'
34
- response.merge :body => Roadie.inline_css(css_rules, response[:body], Rails.application.config.action_mailer.default_url_options)
41
+ response.merge :body => Roadie.inline_css(css_rules, response[:body], url_options)
35
42
  else
36
43
  response
37
44
  end
@@ -43,7 +50,7 @@ module Roadie
43
50
  end
44
51
 
45
52
  def css_rules
46
- @css_rules ||= Roadie.load_css(Rails.root, css_targets) if css_targets.present?
53
+ @css_rules ||= Roadie.load_css(stylesheet_root, css_targets) if css_targets.present?
47
54
  end
48
55
  end
49
56
  end
@@ -0,0 +1,12 @@
1
+ module Roadie
2
+ # Raised when a stylesheet specified for inlining is not present.
3
+ # You can access the target filename via #filename.
4
+ class CSSFileNotFound < StandardError
5
+ attr_reader :filename
6
+
7
+ def initialize(filename)
8
+ @filename = filename
9
+ super("Could not find #{filename}")
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,14 @@
1
+ require 'set'
2
+
1
3
  module Roadie
4
+ # This class is the core of Roadie as it does all the actual work. You just give it
5
+ # the CSS rules, the HTML and the url_options for rewriting URLs and let it go on
6
+ # doing all the heavy lifting and building.
2
7
  class Inliner
8
+ # Regexp matching all the url() declarations in CSS
9
+ #
10
+ # It matches without any quotes and with both single and double quotes
11
+ # inside the parenthesis. There's much room for improvement, of course.
3
12
  CSS_URL_REGEXP = %r{
4
13
  url\(
5
14
  (["']?)
@@ -8,12 +17,15 @@ module Roadie
8
17
  (?:\([^)]*\))* # Texts containing parens pairs
9
18
  [^(]+ # Texts without parens - required
10
19
  )
11
- \1
20
+ \1 # Closing quote
12
21
  \)
13
22
  }x
14
23
 
15
- attr_reader :css, :html, :url_options
16
-
24
+ # Initialize a new Inliner with the given CSS, HTML and url_options.
25
+ #
26
+ # @param [String] css
27
+ # @param [String] html
28
+ # @param [Hash] url_options Supported keys: +:host+, +:port+ and +:protocol+
17
29
  def initialize(css, html, url_options)
18
30
  @css = css
19
31
  @inline_css = []
@@ -21,17 +33,23 @@ module Roadie
21
33
  @url_options = url_options
22
34
  end
23
35
 
36
+ # Start the inlining and return the final HTML output
37
+ # @return [String]
24
38
  def execute
25
39
  adjust_html do |document|
26
- add_missing_structure(document)
27
- extract_inline_style_elements(document)
28
- inline_css_rules(document)
29
- make_image_urls_absolute(document)
30
- make_style_urls_absolute(document)
40
+ @document = document
41
+ add_missing_structure
42
+ extract_inline_style_elements
43
+ inline_css_rules
44
+ make_image_urls_absolute
45
+ make_style_urls_absolute
46
+ @document = nil
31
47
  end
32
48
  end
33
49
 
34
50
  private
51
+ attr_reader :css, :html, :url_options, :document
52
+
35
53
  def inline_css
36
54
  @inline_css.join("\n")
37
55
  end
@@ -49,7 +67,7 @@ module Roadie
49
67
  end.to_html
50
68
  end
51
69
 
52
- def add_missing_structure(document)
70
+ def add_missing_structure
53
71
  html_node = document.at_css('html')
54
72
  html_node['xmlns'] ||= 'http://www.w3.org/1999/xhtml'
55
73
 
@@ -68,7 +86,7 @@ module Roadie
68
86
  end
69
87
  end
70
88
 
71
- def extract_inline_style_elements(document)
89
+ def extract_inline_style_elements
72
90
  document.css("style").each do |style|
73
91
  next if style['media'] == 'print' or style['data-immutable']
74
92
  @inline_css << style.content
@@ -76,45 +94,60 @@ module Roadie
76
94
  end
77
95
  end
78
96
 
79
- def inline_css_rules(document)
80
- matched_elements = {}
81
- assign_rules_to_elements(document, matched_elements)
97
+ def inline_css_rules
98
+ elements_with_declarations.each do |element, declarations|
99
+ ordered_declarations = []
100
+ seen_properties = Set.new
101
+ declarations.sort.reverse_each do |declaration|
102
+ next if seen_properties.include?(declaration.property)
103
+ ordered_declarations.unshift(declaration)
104
+ seen_properties << declaration.property
105
+ end
82
106
 
83
- matched_elements.each do |element, rules|
84
- rules_string = rules.map { |property, rule| [property, rule[:value]].join(':') }.join('; ')
85
- element['style'] = [rules_string, element['style']].compact.join('; ')
107
+ rules_string = ordered_declarations.map { |declaration| declaration.to_s }.join(';')
108
+ element['style'] = [rules_string, element['style']].compact.join(';')
86
109
  end
87
110
  end
88
111
 
89
- def assign_rules_to_elements(document, matched_elements)
90
- parsed_css.each_rule_set do |rules|
91
- rules.selectors.reject { |selector| selector.include?(':') }.each do |selector|
92
- document.css(selector.strip).each do |element|
93
- register_rules_for_element(matched_elements, element, selector, rules)
112
+ def elements_with_declarations
113
+ Hash.new { |hash, key| hash[key] = [] }.tap do |element_declarations|
114
+ parsed_css.each_rule_set do |rule_set|
115
+ each_selector_without_psuedo(rule_set) do |selector, specificity|
116
+ each_element_in_selector(selector) do |element|
117
+ style_declarations_in_rule_set(specificity, rule_set) do |declaration|
118
+ element_declarations[element] << declaration
119
+ end
120
+ end
94
121
  end
95
122
  end
96
123
  end
97
124
  end
98
125
 
99
- def register_rules_for_element(store, element, selector, rules)
100
- specificity = CssParser.calculate_specificity(selector)
101
- element_rules = (store[element] ||= {})
102
- rules.each_declaration do |property, value, important|
103
- stored = (element_rules[property] ||= {:specificity => -1})
104
- more_specific = (stored[:specificity] <= specificity)
105
- if (important and not stored[:important]) or (important and stored[:important] and more_specific) or (more_specific and not stored[:important])
106
- stored.merge!(:value => value, :specificity => specificity, :important => important)
107
- end
126
+ def each_selector_without_psuedo(rules)
127
+ rules.selectors.reject { |selector| selector.include?(':') }.each do |selector|
128
+ yield selector, CssParser.calculate_specificity(selector)
129
+ end
130
+ end
131
+
132
+ def each_element_in_selector(selector)
133
+ document.css(selector.strip).each do |element|
134
+ yield element
135
+ end
136
+ end
137
+
138
+ def style_declarations_in_rule_set(specificity, rule_set)
139
+ rule_set.each_declaration do |property, value, important|
140
+ yield StyleDeclaration.new(property, value, important, specificity)
108
141
  end
109
142
  end
110
143
 
111
- def make_image_urls_absolute(document)
144
+ def make_image_urls_absolute
112
145
  document.css('img').each do |img|
113
146
  img['src'] = ensure_absolute_url(img['src']) if img['src']
114
147
  end
115
148
  end
116
149
 
117
- def make_style_urls_absolute(document)
150
+ def make_style_urls_absolute
118
151
  document.css('*[style]').each do |element|
119
152
  styling = element['style']
120
153
  element['style'] = styling.gsub(CSS_URL_REGEXP) { "url(#{$1}#{ensure_absolute_url($2, '/stylesheets')}#{$1})" }
@@ -0,0 +1,34 @@
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].join(':')
27
+ end
28
+
29
+ def inspect
30
+ extra = [important ? '!important' : nil, specificity].compact
31
+ "#{to_s} (#{extra.join(' , ')})"
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Roadie
2
- VERSION = '1.0.0.pre2'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -0,0 +1,2 @@
1
+ Contact us to buy stuff!
2
+ <a href="http://www.example.com/cheap-marketing">SPAM MASTERS</a>
@@ -15,6 +15,11 @@ describe "roadie integration" do
15
15
  @reason = reason
16
16
  mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
17
17
  end
18
+
19
+ def marketing(to)
20
+ headers('X-Spam' => 'No way! Trust us!')
21
+ mail(:subject => 'Buy cheap v1agra', :to => to)
22
+ end
18
23
  end
19
24
 
20
25
  before(:each) do
@@ -41,6 +46,17 @@ describe "roadie integration" do
41
46
  plain_part.body.decoded.should_not match(/<.*>/)
42
47
  end
43
48
 
49
+ # If we deliver mails we can catch weird problems with headers being invalid
44
50
  email.deliver
45
51
  end
52
+
53
+ it "should not add headers for the roadie options" do
54
+ email = IntegrationMailer.notification('doe@example.com', 'no berries left in chest')
55
+ email.header.fields.map(&:name).should_not include('css')
56
+ end
57
+
58
+ it "should keep custom headers in place" do
59
+ email = IntegrationMailer.marketing('everyone@inter.net')
60
+ email.header['X-Spam'].should be_present
61
+ end
46
62
  end
@@ -12,17 +12,6 @@ describe Roadie::ActionMailerExtensions, "inlining styles" do
12
12
  end
13
13
  end
14
14
 
15
- # Not sure how to implement this one.
16
- # TODO: Either remove or implement
17
- def nested_multipart_mixed(css_file = nil)
18
- raise "Nested multipart mixed is not implemented"
19
- content_type "multipart/mixed"
20
- part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
21
- p.part :content_type => 'text/html', :body => 'Hello HTML'
22
- p.part :content_type => 'text/plain', :body => 'Hello Text'
23
- end
24
- end
25
-
26
15
  def singlepart_html
27
16
  mail(:subject => "HTML email") do |format|
28
17
  format.html { render :text => 'Hello HTML' }
@@ -38,7 +27,7 @@ describe Roadie::ActionMailerExtensions, "inlining styles" do
38
27
 
39
28
  before(:each) do
40
29
  Roadie.stub!(:load_css => 'loaded css')
41
- Roadie.stub!(:inline_css => 'unexpected value') # Make sure a implementation problem doesn't hurt these examples
30
+ Roadie.stub!(:inline_css => 'unexpected value passed to inline_css')
42
31
  end
43
32
 
44
33
  describe "for singlepart text/plain" do
@@ -63,8 +52,8 @@ describe Roadie::ActionMailerExtensions, "inlining styles" do
63
52
  it "should inline css to the email's html part" do
64
53
  Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
65
54
  email = InliningMailer.multipart
66
- email.parts.find { |part| part.mime_type == 'text/html' }.body.decoded.should == 'html'
67
- email.parts.find { |part| part.mime_type == 'text/plain' }.body.decoded.should == 'Hello Text'
55
+ email.html_part.body.decoded.should == 'html'
56
+ email.text_part.body.decoded.should == 'Hello Text'
68
57
  end
69
58
  end
70
59
  end
@@ -90,13 +79,18 @@ describe Roadie::ActionMailerExtensions, "loading css files" do
90
79
  Roadie.stub!(:inline_css => 'html')
91
80
  end
92
81
 
82
+ it "should load css from Rails' stylesheet root" do
83
+ Roadie.should_receive(:load_css).with(Rails.root.join('public', 'stylesheets'), anything).and_return('')
84
+ CssLoadingMailer.use_default
85
+ end
86
+
93
87
  it "should load the css specified in the default mailer settings" do
94
- Roadie.should_receive(:load_css).with(Rails.root, ['default_value']).and_return('')
88
+ Roadie.should_receive(:load_css).with(anything, ['default_value']).and_return('')
95
89
  CssLoadingMailer.use_default
96
90
  end
97
91
 
98
92
  it "should load the css specified in the specific mailer action instead of the default choice" do
99
- Roadie.should_receive(:load_css).with(Rails.root, ['specific']).and_return('')
93
+ Roadie.should_receive(:load_css).with(anything, ['specific']).and_return('')
100
94
  CssLoadingMailer.override(:specific)
101
95
  end
102
96
 
@@ -106,7 +100,7 @@ describe Roadie::ActionMailerExtensions, "loading css files" do
106
100
  end
107
101
 
108
102
  it "should load multiple css files when given an array" do
109
- Roadie.should_receive(:load_css).with(Rails.root, ['specific', 'other']).and_return('')
103
+ Roadie.should_receive(:load_css).with(anything, ['specific', 'other']).and_return('')
110
104
  CssLoadingMailer.override([:specific, :other])
111
105
  end
112
106
  end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ module Roadie
4
+ describe CSSFileNotFound do
5
+ it "is initialized with a filename" do
6
+ CSSFileNotFound.new('file.css').filename.should == 'file.css'
7
+ end
8
+
9
+ it "has a message" do
10
+ CSSFileNotFound.new('style.css').message.should == 'Could not find style.css'
11
+ end
12
+ end
13
+ end
@@ -15,19 +15,38 @@ describe Roadie::Inliner do
15
15
 
16
16
  it "should inline simple attributes" do
17
17
  use_css 'p { color: green }'
18
- rendering('<p></p>').should have_styling('color' => 'green').at_selector('p')
18
+ rendering('<p></p>').should have_styling('color' => 'green')
19
+ end
20
+
21
+ it "should keep the order of the styles that was inlined" do
22
+ use_css 'h1 { padding: 2px; margin: 5px; }'
23
+ rendering('<h1></h1>').should have_styling([['padding', '2px'], ['margin', '5px']])
19
24
  end
20
25
 
21
26
  it "should combine multiple selectors into one" do
22
- use_css "p { color: green; }
23
- .tip { float: right; }"
24
- rendering('<p class="tip"></p>').should have_styling('color' => 'green', 'float' => 'right').at_selector('p')
27
+ use_css 'p { color: green; }
28
+ .tip { float: right; }'
29
+ rendering('<p class="tip"></p>').should have_styling('color' => 'green', 'float' => 'right')
25
30
  end
26
31
 
27
32
  it "should use the ones attributes with the highest specificality when conflicts arises" do
28
33
  use_css "p { color: red; }
29
- .safe { color: green; border: 1px solid black; }"
30
- rendering('<p class="safe"></p>').should have_styling('color' => 'green', 'border' => '1px solid black').at_selector('p')
34
+ .safe { color: green; }"
35
+ rendering('<p class="safe"></p>').should have_styling('color' => 'green')
36
+ end
37
+
38
+ it "should sort styles by specificity order" do
39
+ use_css 'p { margin: 2px; }
40
+ #big { margin: 10px; }
41
+ .down { margin-bottom: 5px; }'
42
+
43
+ rendering('<p class="down"></p>').should have_styling([
44
+ ['margin', '2px'], ['margin-bottom', '5px']
45
+ ])
46
+
47
+ rendering('<p class="down" id="big"></p>').should have_styling([
48
+ ['margin-bottom', '5px'], ['margin', '10px']
49
+ ])
31
50
  end
32
51
 
33
52
  it "should support multiple selectors for the same rules" do
@@ -41,27 +60,27 @@ describe Roadie::Inliner do
41
60
  it "should respect !important properties" do
42
61
  use_css "a { text-decoration: underline !important; }
43
62
  a.hard-to-spot { text-decoration: none; }"
44
- rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline').at_selector('a')
63
+ rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline')
45
64
  end
46
65
 
47
66
  it "should combine with already present inline styles" do
48
67
  use_css "p { color: green }"
49
- rendering('<p style="font-size: 1.1em"></p>').should have_styling('color' => 'green', 'font-size' => '1.1em').at_selector('p')
68
+ rendering('<p style="font-size: 1.1em"></p>').should have_styling([['color', 'green'], ['font-size', '1.1em']])
50
69
  end
51
70
 
52
- it "should not overwrite already present inline styles" do
71
+ it "should not touch already present inline styles" do
53
72
  use_css "p { color: red }"
54
- rendering('<p style="color: green"></p>').should have_styling('color' => 'green').at_selector('p')
73
+ rendering('<p style="color: green"></p>').should have_styling([['color', 'red'], ['color', 'green']])
55
74
  end
56
75
 
57
76
  it "should ignore selectors with :psuedo-classes" do
58
77
  use_css 'p:hover { color: red }'
59
- rendering('<p></p>').should_not have_styling('color' => 'red').at_selector('p')
78
+ rendering('<p></p>').should_not have_styling('color' => 'red')
60
79
  end
61
80
 
62
81
  describe "inline <style> elements" do
63
82
  it "should be used for inlined styles" do
64
- rendering(<<-HTML).should have_styling('color' => 'green', 'font-size' => '1.1em').at_selector('p')
83
+ rendering(<<-HTML).should have_styling([['color', 'green'], ['font-size', '1.1em']])
65
84
  <html>
66
85
  <head>
67
86
  <style type="text/css">p { color: green; }</style>
@@ -93,7 +112,7 @@ describe Roadie::Inliner do
93
112
  <p></p>
94
113
  HTML
95
114
  document.should have_selector('style[data-immutable=true]')
96
- document.should_not have_styling('color' => 'red').at_selector('p')
115
+ document.should_not have_styling('color' => 'red')
97
116
  end
98
117
 
99
118
  it "should not be touched when media=print" do
@@ -117,41 +136,41 @@ describe Roadie::Inliner do
117
136
 
118
137
  describe "making urls absolute" do
119
138
  it "should work on image sources" do
120
- rendering('<img src="/images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg').at_selector('img')
121
- rendering('<img src="../images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg').at_selector('img')
122
- rendering('<img src="foo.jpg" />').should have_attribute('src' => 'http://example.com/foo.jpg').at_selector('img')
139
+ rendering('<img src="/images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
140
+ rendering('<img src="../images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
141
+ rendering('<img src="foo.jpg" />').should have_attribute('src' => 'http://example.com/foo.jpg')
123
142
  end
124
143
 
125
144
  it "should not touch image sources that are already absolute" do
126
- rendering('<img src="http://other.example.org/images/foo.jpg" />').should have_attribute('src' => 'http://other.example.org/images/foo.jpg').at_selector('img')
145
+ rendering('<img src="http://other.example.org/images/foo.jpg" />').should have_attribute('src' => 'http://other.example.org/images/foo.jpg')
127
146
  end
128
147
 
129
148
  it "should work on inlined style attributes" do
130
- rendering('<p style="background: url(/paper.png)"></p>').should have_styling('background' => 'url(http://example.com/paper.png)').at_selector('p')
131
- rendering('<p style="background: url(&quot;/paper.png&quot;)"></p>').should have_styling('background' => 'url("http://example.com/paper.png")').at_selector('p')
149
+ rendering('<p style="background: url(/paper.png)"></p>').should have_styling('background' => 'url(http://example.com/paper.png)')
150
+ rendering('<p style="background: url(&quot;/paper.png&quot;)"></p>').should have_styling('background' => 'url("http://example.com/paper.png")')
132
151
  end
133
152
 
134
153
  it "should work on external style declarations" do
135
154
  use_css "p { background-image: url(/paper.png); }
136
155
  table { background-image: url('/paper.png'); }
137
156
  div { background-image: url(\"/paper.png\"); }"
138
- rendering('<p></p>').should have_styling('background-image' => 'url(http://example.com/paper.png)').at_selector('p')
139
- rendering('<table></table>').should have_styling('background-image' => "url('http://example.com/paper.png')").at_selector('table')
140
- rendering('<div></div>').should have_styling('background-image' => 'url("http://example.com/paper.png")').at_selector('div')
157
+ rendering('<p></p>').should have_styling('background-image' => 'url(http://example.com/paper.png)')
158
+ rendering('<table></table>').should have_styling('background-image' => "url('http://example.com/paper.png')")
159
+ rendering('<div></div>').should have_styling('background-image' => 'url("http://example.com/paper.png")')
141
160
  end
142
161
 
143
162
  it "should not touch style urls that are already absolute" do
144
163
  external_url = 'url(http://other.example.org/paper.png)'
145
164
  use_css "p { background-image: #{external_url}; }"
146
- rendering('<p></p>').should have_styling('background-image' => external_url).at_selector('p')
147
- rendering(%(<div style="background-image: #{external_url}"></div>)).should have_styling('background-image' => external_url).at_selector('div')
165
+ rendering('<p></p>').should have_styling('background-image' => external_url)
166
+ rendering(%(<div style="background-image: #{external_url}"></div>)).should have_styling('background-image' => external_url)
148
167
  end
149
168
 
150
169
  it "should not touch the urls when no url options are defined" do
151
170
  use_css "img { background: url(/a.jpg); }"
152
171
  rendering('<img src="/b.jpg" />', :url_options => nil).tap do |document|
153
172
  document.should have_attribute('src' => '/b.jpg').at_selector('img')
154
- document.should have_styling('background' => 'url(/a.jpg)').at_selector('img')
173
+ document.should have_styling('background' => 'url(/a.jpg)')
155
174
  end
156
175
  end
157
176
 
@@ -159,13 +178,13 @@ describe Roadie::Inliner do
159
178
  use_css "img { background: url(/a.jpg); }"
160
179
  rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https', :port => '8080'}).tap do |document|
161
180
  document.should have_attribute('src' => 'https://example.com:8080/b.jpg').at_selector('img')
162
- document.should have_styling('background' => 'url(https://example.com:8080/a.jpg)').at_selector('img')
181
+ document.should have_styling('background' => 'url(https://example.com:8080/a.jpg)')
163
182
  end
164
183
  end
165
184
 
166
185
  it "should not touch data: URIs" do
167
186
  use_css "div { background: url(data:abcdef); }"
168
- rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)').at_selector('div')
187
+ rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)')
169
188
  end
170
189
  end
171
190
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module Roadie
4
+ describe StyleDeclaration do
5
+ it "should be initialized with a property, value, if it is marked as important, and the specificity" do
6
+ StyleDeclaration.new('color', 'green', true, 45).tap do |declaration|
7
+ declaration.property.should == 'color'
8
+ declaration.value.should == 'green'
9
+ declaration.should be_important
10
+ declaration.specificity.should == 45
11
+ end
12
+ end
13
+
14
+ describe "string representation" do
15
+ it "should be the property and the value joined with a colon" do
16
+ StyleDeclaration.new('color', 'green', false, 1).to_s.should == 'color:green'
17
+ StyleDeclaration.new('font-size', '1.1em', false, 1).to_s.should == 'font-size:1.1em'
18
+ end
19
+ end
20
+
21
+ describe "comparing" do
22
+ def declaration(specificity, important = false)
23
+ StyleDeclaration.new('color', 'green', important, specificity)
24
+ end
25
+
26
+ it "should compare on specificity" do
27
+ declaration(5).should be == declaration(5)
28
+ declaration(4).should be < declaration(5)
29
+ declaration(6).should be > declaration(5)
30
+ end
31
+
32
+ context "with an important declaration" do
33
+ it "should be less than the important declaration regardless of the specificity" do
34
+ declaration(99, false).should be < declaration(1, true)
35
+ end
36
+
37
+ it "should compare like normal when both declarations are important" do
38
+ declaration(5, true).should be == declaration(5, true)
39
+ declaration(4, true).should be < declaration(5, true)
40
+ declaration(6, true).should be > declaration(5, true)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -9,7 +9,7 @@ describe Roadie do
9
9
  end
10
10
 
11
11
  describe ".load_css(root, targets)" do
12
- let(:fixtures_root) { Pathname.new(__FILE__).dirname.join('..', 'fixtures') }
12
+ let(:fixtures_root) { Pathname.new(__FILE__).dirname.join('..', 'fixtures', 'public', 'stylesheets') }
13
13
 
14
14
  it "should load files matching the target names under root/public/stylesheets" do
15
15
  Roadie.load_css(fixtures_root, ['foo']).should == 'contents of foo'
data/spec/spec_helper.rb CHANGED
@@ -13,6 +13,9 @@ rescue Bundler::BundlerError => e
13
13
  end
14
14
 
15
15
  require 'rspec'
16
+
17
+ Dir['./spec/support/**/*'].each { |file| require file }
18
+
16
19
  require 'action_mailer'
17
20
  require 'roadie'
18
21
 
@@ -31,71 +34,3 @@ else
31
34
  end
32
35
  end
33
36
 
34
- RSpec::Matchers.define :have_styling do |rules|
35
- chain :at_selector do |selector|
36
- @selector = selector
37
- end
38
-
39
- match do |document|
40
- styles = parsed_styles(document)
41
- if rules.nil?
42
- styles.blank?
43
- else
44
- rules.stringify_keys.should == parsed_styles(document)
45
- end
46
- end
47
-
48
- describe { "have styles #{rules.inspect} at selector #{@selector.inspect}" }
49
- failure_message_for_should { |document| "expected styles at #{@selector.inspect} to be #{rules.inspect} but was #{parsed_styles(document).inspect}" }
50
- failure_message_for_should_not { "expected styles at #{@selector.inspect} to not be #{rules.inspect}" }
51
-
52
- def element_styles(document)
53
- node = document.css(@selector).first
54
- node && node['style']
55
- end
56
-
57
- def parsed_styles(document)
58
- return @parsed_styles if defined?(@parsed_styles)
59
- if (styles = element_styles(document)).present?
60
- @parsed_styles = styles.split(';').inject({}) do |styles, item|
61
- attribute, value = item.split(':', 2)
62
- styles.merge!(attribute.strip => value.strip)
63
- end
64
- else
65
- @parsed_styles = nil
66
- end
67
- end
68
- end
69
-
70
- RSpec::Matchers.define :have_attribute do |attribute|
71
- chain :at_selector do |selector|
72
- @selector = selector
73
- end
74
-
75
- match do |document|
76
- name, expected = attribute.first
77
- expected == attribute(document, name)
78
- end
79
-
80
- describe { "have attribute #{attribute.inspect} at selector #{@selector.inspect}" }
81
- failure_message_for_should do |document|
82
- name, expected = attribute.first
83
- "expected #{name} attribute at #{@selector.inspect} to be #{expected.inspect} but was #{attribute(document, name).inspect}"
84
- end
85
- failure_message_for_should_not do |document|
86
- name, expected = attribute.first
87
- "expected #{name} attribute at #{@selector.inspect} to not be #{expected.inspect}"
88
- end
89
-
90
- def attribute(document, attribute_name)
91
- node = document.css(@selector).first
92
- node && node[attribute_name]
93
- end
94
- end
95
-
96
- RSpec::Matchers.define :have_selector do |selector|
97
- match { |document| document.css(selector).present? }
98
- failure_message_for_should { "expected document to #{name_to_sentence}#{expected_to_sentence}"}
99
- failure_message_for_should_not { "expected document to not #{name_to_sentence}#{expected_to_sentence}"}
100
- end
101
-
@@ -0,0 +1,28 @@
1
+ RSpec::Matchers.define :have_attribute do |attribute|
2
+ @selector = 'body > *:first'
3
+
4
+ chain :at_selector do |selector|
5
+ @selector = selector
6
+ end
7
+
8
+ match do |document|
9
+ name, expected = attribute.first
10
+ expected == attribute(document, name)
11
+ end
12
+
13
+ describe { "have attribute #{attribute.inspect} at selector #{@selector.inspect}" }
14
+ failure_message_for_should do |document|
15
+ name, expected = attribute.first
16
+ "expected #{name} attribute at #{@selector.inspect} to be #{expected.inspect} but was #{attribute(document, name).inspect}"
17
+ end
18
+ failure_message_for_should_not do |document|
19
+ name, expected = attribute.first
20
+ "expected #{name} attribute at #{@selector.inspect} to not be #{expected.inspect}"
21
+ end
22
+
23
+ def attribute(document, attribute_name)
24
+ node = document.css(@selector).first
25
+ node && node[attribute_name]
26
+ end
27
+ end
28
+
@@ -0,0 +1,6 @@
1
+ RSpec::Matchers.define :have_selector do |selector|
2
+ match { |document| document.css(selector).present? }
3
+ failure_message_for_should { "expected document to #{name_to_sentence}#{expected_to_sentence}"}
4
+ failure_message_for_should_not { "expected document to not #{name_to_sentence}#{expected_to_sentence}"}
5
+ end
6
+
@@ -0,0 +1,36 @@
1
+ RSpec::Matchers.define :have_styling do |rules|
2
+ @selector = 'body > *:first'
3
+
4
+ chain :at_selector do |selector|
5
+ @selector = selector
6
+ end
7
+
8
+ match do |document|
9
+ if rules.nil?
10
+ parsed_styles(document).blank?
11
+ else
12
+ rules.to_a.should == parsed_styles(document)
13
+ end
14
+ end
15
+
16
+ describe { "have styles #{rules.inspect} at selector #{@selector.inspect}" }
17
+ failure_message_for_should { |document| "expected styles at #{@selector.inspect} to be #{rules.inspect} but was #{parsed_styles(document).inspect}" }
18
+ failure_message_for_should_not { "expected styles at #{@selector.inspect} to not be #{rules.inspect}" }
19
+
20
+ def parsed_styles(document)
21
+ parse_styles(element_style(document))
22
+ end
23
+
24
+ def element_style(document)
25
+ node = document.css(@selector).first
26
+ node && node['style']
27
+ end
28
+
29
+ def parse_styles(styles)
30
+ return [] if styles.blank?
31
+ styles.split(';').inject([]) do |array, item|
32
+ array << item.split(':', 2).map(&:strip)
33
+ end
34
+ end
35
+ end
36
+
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roadie
3
3
  version: !ruby/object:Gem::Version
4
- hash: 270495473
5
- prerelease: true
4
+ hash: 23
5
+ prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
9
  - 0
10
- - pre2
11
- version: 1.0.0.pre2
10
+ version: 1.0.0
12
11
  platform: ruby
13
12
  authors:
14
13
  - Magnus Bergmark
@@ -16,7 +15,7 @@ autorequire:
16
15
  bindir: bin
17
16
  cert_chain: []
18
17
 
19
- date: 2011-01-06 00:00:00 +01:00
18
+ date: 2011-03-09 00:00:00 +01:00
20
19
  default_executable:
21
20
  dependencies:
22
21
  - !ruby/object:Gem::Dependency
@@ -91,29 +90,36 @@ extra_rdoc_files:
91
90
  files:
92
91
  - .autotest
93
92
  - .gitignore
94
- - .rspec
93
+ - .yardopts
95
94
  - Gemfile
96
95
  - Gemfile.lock
97
96
  - MIT-LICENSE
98
97
  - README.textile
99
98
  - Rakefile
100
- - VERSION
101
99
  - autotest/discover.rb
102
100
  - lib/roadie.rb
103
101
  - lib/roadie/action_mailer_extensions.rb
102
+ - lib/roadie/css_file_not_found.rb
104
103
  - lib/roadie/inliner.rb
104
+ - lib/roadie/style_declaration.rb
105
105
  - lib/roadie/version.rb
106
106
  - roadie.gemspec
107
107
  - spec/fixtures/public/stylesheets/bar.css
108
108
  - spec/fixtures/public/stylesheets/foo.css
109
109
  - spec/fixtures/public/stylesheets/integration.css
110
+ - spec/fixtures/views/integration_mailer/marketing.html.erb
110
111
  - spec/fixtures/views/integration_mailer/notification.html.erb
111
112
  - spec/fixtures/views/integration_mailer/notification.text.erb
112
113
  - spec/integration_spec.rb
113
114
  - spec/lib/roadie/action_mailer_extensions_spec.rb
115
+ - spec/lib/roadie/css_file_not_found_spec.rb
114
116
  - spec/lib/roadie/inliner_spec.rb
117
+ - spec/lib/roadie/style_declaration_spec.rb
115
118
  - spec/lib/roadie_spec.rb
116
119
  - spec/spec_helper.rb
120
+ - spec/support/have_attribute_matcher.rb
121
+ - spec/support/have_selector_matcher.rb
122
+ - spec/support/have_styling_matcher.rb
117
123
  has_rdoc: true
118
124
  homepage: http://github.com/Mange/roadie
119
125
  licenses: []
@@ -135,14 +141,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
141
  required_rubygems_version: !ruby/object:Gem::Requirement
136
142
  none: false
137
143
  requirements:
138
- - - ">"
144
+ - - ">="
139
145
  - !ruby/object:Gem::Version
140
- hash: 25
146
+ hash: 3
141
147
  segments:
142
- - 1
143
- - 3
144
- - 1
145
- version: 1.3.1
148
+ - 0
149
+ version: "0"
146
150
  requirements: []
147
151
 
148
152
  rubyforge_project:
@@ -154,10 +158,16 @@ test_files:
154
158
  - spec/fixtures/public/stylesheets/bar.css
155
159
  - spec/fixtures/public/stylesheets/foo.css
156
160
  - spec/fixtures/public/stylesheets/integration.css
161
+ - spec/fixtures/views/integration_mailer/marketing.html.erb
157
162
  - spec/fixtures/views/integration_mailer/notification.html.erb
158
163
  - spec/fixtures/views/integration_mailer/notification.text.erb
159
164
  - spec/integration_spec.rb
160
165
  - spec/lib/roadie/action_mailer_extensions_spec.rb
166
+ - spec/lib/roadie/css_file_not_found_spec.rb
161
167
  - spec/lib/roadie/inliner_spec.rb
168
+ - spec/lib/roadie/style_declaration_spec.rb
162
169
  - spec/lib/roadie_spec.rb
163
170
  - spec/spec_helper.rb
171
+ - spec/support/have_attribute_matcher.rb
172
+ - spec/support/have_selector_matcher.rb
173
+ - spec/support/have_styling_matcher.rb
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --colour
2
- --format progress
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.0.0.pre1