roadie 1.0.0.pre2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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