roadie 2.0.0 → 2.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ require 'pathname'
2
+
3
+ module Roadie
4
+ # A provider that looks for files on the filesystem
5
+ #
6
+ # Usage:
7
+ # config.roadie.provider = FilesystemProvider.new("prefix", "path/to/stylesheets")
8
+ #
9
+ # Path specification follows certain rules thatare detailed in {#initialize}.
10
+ #
11
+ # @see #initialize
12
+ class FilesystemProvider < AssetProvider
13
+ # @return [Pathname] Pathname representing the directory of the assets
14
+ attr_reader :path
15
+
16
+ # Initializes a new instance of FilesystemProvider.
17
+ #
18
+ # The passed path can come in some variants:
19
+ # * +Pathname+ - will be used as-is
20
+ # * +String+ - If pointing to an absolute path, uses that path. If a relative path, relative from the +Rails.root+
21
+ # * +nil+ - Use the default path (equal to "public/stylesheets")
22
+ #
23
+ # @example Pointing to a directory in the project
24
+ # FilesystemProvider.new(Rails.root.join("public", "assets"))
25
+ # FilesystemProvider.new("public/assets")
26
+ #
27
+ # @example Pointing to external resource
28
+ # FilesystemProvider.new("/home/app/stuff")
29
+ #
30
+ # @param [String] prefix The prefix (see {#prefix})
31
+ # @param [String, Pathname, nil] path The path to use
32
+ def initialize(prefix = "/stylesheets", path = nil)
33
+ super(prefix)
34
+ if path
35
+ @path = resolve_path(path)
36
+ else
37
+ @path = default_path
38
+ end
39
+ end
40
+
41
+ # Looks for the file in the tree. If the file cannot be found, and it does not end with ".css", the lookup
42
+ # will be retried with ".css" appended to the filename.
43
+ #
44
+ # @return [String] contents of the file
45
+ def find(name)
46
+ base = remove_prefix(name)
47
+ file = path.join(base)
48
+ if file.exist?
49
+ file.read.strip
50
+ else
51
+ return find("#{base}.css") if base.to_s !~ /\.css$/
52
+ raise CSSFileNotFound.new(name, base.to_s)
53
+ end
54
+ end
55
+
56
+ private
57
+ def default_path
58
+ resolve_path("public/stylesheets")
59
+ end
60
+
61
+ def resolve_path(path)
62
+ if path.kind_of?(Pathname)
63
+ @path = path
64
+ else
65
+ pathname = Pathname.new(path)
66
+ if pathname.absolute?
67
+ @path = pathname
68
+ else
69
+ @path = Roadie.app.root.join(path)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -21,17 +21,24 @@ module Roadie
21
21
  \)
22
22
  }x
23
23
 
24
- # Initialize a new Inliner with the given CSS, HTML and url_options.
24
+ # Initialize a new Inliner with the given Provider, CSS targets, HTML, and `url_options`.
25
25
  #
26
- # @param [String] css
26
+ # @param [AssetProvider] assets
27
+ # @param [Array] targets List of CSS files to load via the provider
27
28
  # @param [String] html
28
- # @param [Hash] url_options Supported keys: +:host+, +:port+, +:protocol+ and +:asset_path_prefix:+
29
- def initialize(css, html, url_options)
30
- @css = css
31
- @inline_css = []
29
+ # @param [Hash] url_options Supported keys: +:host+, +:port+, +:protocol+
30
+ def initialize(assets, targets, html, url_options)
31
+ @assets = assets
32
+ @css = assets.all(targets)
32
33
  @html = html
34
+ @inline_css = []
33
35
  @url_options = url_options
34
- @asset_path_prefix = (url_options && url_options[:asset_path_prefix] || "/assets/")
36
+
37
+ if url_options and url_options[:asset_path_prefix]
38
+ raise ArgumentError, "The asset_path_prefix URL option is not working anymore. You need to add the following configuration to your application.rb:\n" +
39
+ " config.roadie.provider = AssetPipelineProvider.new(#{url_options[:asset_path_prefix].inspect})\n" +
40
+ "Note that the prefix \"/assets\" is the default one, so you do not need to configure anything in that case."
41
+ end
35
42
  end
36
43
 
37
44
  # Start the inlining and return the final HTML output
@@ -50,7 +57,7 @@ module Roadie
50
57
  end
51
58
 
52
59
  private
53
- attr_reader :css, :html, :url_options, :document
60
+ attr_reader :css, :html, :assets, :url_options, :document
54
61
 
55
62
  def inline_css
56
63
  @inline_css.join("\n")
@@ -63,13 +70,6 @@ module Roadie
63
70
  end
64
71
  end
65
72
 
66
- def find_asset_from_url(url)
67
- asset_filename = url.path.sub(/^#{Regexp.quote(@asset_path_prefix)}/, '').gsub(%r{^/|//+}, '')
68
- Roadie.assets[asset_filename].tap do |asset|
69
- raise CSSFileNotFound.new(asset_filename, url) unless asset
70
- end
71
- end
72
-
73
73
  def adjust_html
74
74
  Nokogiri::HTML.parse(html).tap do |document|
75
75
  yield document
@@ -99,7 +99,7 @@ module Roadie
99
99
 
100
100
  def extract_link_elements
101
101
  all_link_elements_to_be_inlined_with_url.each do |link, url|
102
- asset = find_asset_from_url(url)
102
+ asset = assets.find(url.path)
103
103
  @inline_css << asset.to_s
104
104
  link.remove
105
105
  end
@@ -0,0 +1,22 @@
1
+ require 'action_mailer'
2
+ require 'roadie/action_mailer_extensions'
3
+
4
+ module Roadie
5
+ # {Roadie::Railtie} registers {Roadie} with the current Rails application
6
+ # It adds another configuration option:
7
+ #
8
+ # config.roadie.provider = nil
9
+ #
10
+ # You can use this to set a provider yourself.
11
+ #
12
+ # @see Roadie
13
+ # @see AssetProvider
14
+ class Railtie < Rails::Railtie
15
+ config.roadie = ActiveSupport::OrderedOptions.new
16
+ config.roadie.provider = nil
17
+
18
+ initializer "roadie.extend_action_mailer" do
19
+ ActionMailer::Base.send :include, Roadie::ActionMailerExtensions
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module Roadie
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.0.pre1'
3
3
  end
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.add_dependency 'actionmailer', '~> 3.1.0'
20
20
  s.add_dependency 'sprockets'
21
21
 
22
+ s.add_development_dependency 'rails'
22
23
  s.add_development_dependency 'rspec-rails', '>= 2.0.0'
23
24
 
24
25
  s.extra_rdoc_files = %w[README.md Changelog.md]
@@ -0,0 +1,10 @@
1
+ body { background: url(../images/dots.png) repeat-x; }
2
+ #message { background-color: #fff; margin: 0 auto; width: 75%; }
3
+
4
+ h1 { color: #eee; }
5
+ strong {
6
+ -moz-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
7
+ -webkit-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
8
+ -o-box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
9
+ box-shadow: #62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0;
10
+ }
@@ -1,68 +1,83 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "roadie integration" do
4
- class IntegrationMailer < ActionMailer::Base
5
- default :css => :integration, :from => 'john@example.com'
6
- append_view_path FIXTURES_PATH.join('views')
7
-
8
- def notification(to, reason)
9
- @reason = reason
10
- mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
3
+ module Roadie
4
+ shared_examples "roadie integration" do
5
+ mailer = Class.new(ActionMailer::Base) do
6
+ def self.name() "IntegrationMailer" end
7
+
8
+ default :css => :integration, :from => 'john@example.com'
9
+ append_view_path FIXTURES_PATH.join('views')
10
+
11
+ def notification(to, reason)
12
+ @reason = reason
13
+ mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
14
+ end
15
+
16
+ def marketing(to)
17
+ headers('X-Spam' => 'No way! Trust us!')
18
+ mail(:subject => 'Buy cheap v1agra', :to => to)
19
+ end
11
20
  end
12
21
 
13
- def marketing(to)
14
- headers('X-Spam' => 'No way! Trust us!')
15
- mail(:subject => 'Buy cheap v1agra', :to => to)
22
+ before(:each) do
23
+ Rails.application.config.action_mailer.default_url_options = {:host => "example.app.org"}
24
+
25
+ Rails.stub(:root => FIXTURES_PATH)
26
+ mailer.delivery_method = :test
16
27
  end
17
- end
18
28
 
19
- before(:each) do
20
- config = double(:action_mailer => double(:default_url_options => {:host => "example.app.org"}))
21
- Rails.application.stub(:config => config)
29
+ it "inlines styles for an email" do
30
+ email = mailer.notification('doe@example.com', 'your quota limit has been reached')
22
31
 
23
- Rails.stub(:root => FIXTURES_PATH)
24
- IntegrationMailer.delivery_method = :test
25
- end
32
+ email.to.should == ['doe@example.com']
33
+ email.from.should == ['john@example.com']
34
+ email.should have(2).parts
26
35
 
27
- it "inlines styles for an email" do
28
- email = IntegrationMailer.notification('doe@example.com', 'your quota limit has been reached')
36
+ email.parts.find { |part| part.mime_type == 'text/html' }.tap do |html_part|
37
+ document = Nokogiri::HTML.parse(html_part.body.decoded)
38
+ document.should have_selector('html > head + body')
39
+ document.should have_selector('body #message h1')
40
+ document.should have_styling('background' => 'url(http://example.app.org/images/dots.png) repeat-x').at_selector('body')
41
+ document.should have_selector('strong[contains("quota")]')
42
+ end
29
43
 
30
- email.to.should == ['doe@example.com']
31
- email.from.should == ['john@example.com']
32
- email.should have(2).parts
44
+ email.parts.find { |part| part.mime_type == 'text/plain' }.tap do |plain_part|
45
+ plain_part.body.decoded.should_not match(/<.*>/)
46
+ end
33
47
 
34
- email.parts.find { |part| part.mime_type == 'text/html' }.tap do |html_part|
35
- document = Nokogiri::HTML.parse(html_part.body.decoded)
36
- document.should have_selector('html > head + body')
37
- document.should have_selector('body #message h1')
38
- document.should have_styling('background' => 'url(http://example.app.org/images/dots.png) repeat-x').at_selector('body')
39
- document.should have_selector('strong[contains("quota")]')
48
+ # If we deliver mails we can catch weird problems with headers being invalid
49
+ email.deliver
40
50
  end
41
51
 
42
- email.parts.find { |part| part.mime_type == 'text/plain' }.tap do |plain_part|
43
- plain_part.body.decoded.should_not match(/<.*>/)
52
+ it "does not add headers for the roadie options" do
53
+ email = mailer.notification('doe@example.com', 'no berries left in chest')
54
+ email.header.fields.map(&:name).should_not include('css')
44
55
  end
45
56
 
46
- # If we deliver mails we can catch weird problems with headers being invalid
47
- email.deliver
48
- end
57
+ it "keeps custom headers in place" do
58
+ email = mailer.marketing('everyone@inter.net')
59
+ email.header['X-Spam'].should be_present
60
+ end
49
61
 
50
- it "does not add headers for the roadie options" do
51
- email = IntegrationMailer.notification('doe@example.com', 'no berries left in chest')
52
- email.header.fields.map(&:name).should_not include('css')
62
+ it "applies CSS3 styles" do
63
+ email = mailer.notification('doe@example.com', 'your quota limit has been reached')
64
+ document = Nokogiri::HTML.parse(email.html_part.body.decoded)
65
+ strong_node = document.css('strong').first
66
+ stylings = SpecHelpers.styling_of_node(strong_node)
67
+ stylings.should include(['box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
68
+ stylings.should include(['-o-box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
69
+ end
53
70
  end
54
71
 
55
- it "keeps custom headers in place" do
56
- email = IntegrationMailer.marketing('everyone@inter.net')
57
- email.header['X-Spam'].should be_present
72
+ describe "filesystem integration" do
73
+ it_behaves_like "roadie integration" do
74
+ before(:each) { Rails.application.config.assets.enabled = false }
75
+ end
58
76
  end
59
77
 
60
- it "applies CSS3 styles" do
61
- email = IntegrationMailer.notification('doe@example.com', 'your quota limit has been reached')
62
- document = Nokogiri::HTML.parse(email.html_part.body.decoded)
63
- strong_node = document.css('strong').first
64
- stylings = SpecHelpers.styling_of_node(strong_node)
65
- stylings.should include(['box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
66
- stylings.should include(['-o-box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
78
+ describe "asset pipeline integration" do
79
+ it_behaves_like "roadie integration" do
80
+ before(:each) { Rails.application.config.assets.enabled = true }
81
+ end
67
82
  end
68
83
  end
@@ -1,106 +1,122 @@
1
1
  # coding: utf-8
2
2
  require 'spec_helper'
3
3
 
4
- describe Roadie::ActionMailerExtensions, "inlining styles" do
5
- class InliningMailer < ActionMailer::Base
6
- default :css => :simple
7
-
8
- def multipart
9
- mail(:subject => "Multipart email") do |format|
10
- format.html { render :text => 'Hello HTML' }
11
- format.text { render :text => 'Hello Text' }
4
+ module Roadie
5
+ describe ActionMailerExtensions, "CSS selection" do
6
+ mailer = Class.new(ActionMailer::Base) do
7
+ default :css => :default
8
+
9
+ # Just to make ActionMailer::Base not puke
10
+ def self.name
11
+ "SomeMailer"
12
12
  end
13
- end
14
13
 
15
- def singlepart_html
16
- mail(:subject => "HTML email") do |format|
17
- format.html { render :text => 'Hello HTML' }
14
+ def default_css
15
+ mail(:subject => "Default CSS") do |format|
16
+ format.html { render :text => '' }
17
+ end
18
18
  end
19
- end
20
19
 
21
- def singlepart_plain
22
- mail(:subject => "Text email") do |format|
23
- format.text { render :text => 'Hello Text' }
20
+ def override_css(css)
21
+ mail(:subject => "Default CSS", :css => css) do |format|
22
+ format.html { render :text => '' }
23
+ end
24
24
  end
25
25
  end
26
- end
27
-
28
- before(:each) do
29
- Roadie.stub!(:load_css => 'loaded css')
30
- Roadie.stub!(:inline_css => 'unexpected value passed to inline_css')
31
- end
32
26
 
33
- describe "for singlepart text/plain" do
34
- it "does not touch the email body" do
35
- Roadie.should_not_receive(:inline_css)
36
- InliningMailer.singlepart_plain
27
+ def expect_global_css(files)
28
+ Roadie.should_receive(:inline_css).with(provider, files, anything, anything).and_return('')
37
29
  end
38
- end
39
30
 
40
- describe "for singlepart text/html" do
41
- it "inlines css to the email body" do
42
- Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
43
- InliningMailer.singlepart_html.body.decoded.should == 'html'
31
+ let(:provider) { double("asset provider", :all => '') }
32
+
33
+ before(:each) do
34
+ Roadie.stub(:inline_css => 'unexpected value passed to inline_css')
35
+ Roadie.stub(:current_provider => provider)
44
36
  end
45
- end
46
37
 
47
- describe "for multipart" do
48
- it "keeps both parts" do
49
- InliningMailer.multipart.should have(2).parts
38
+ it "uses no global CSS when :css is set to nil" do
39
+ expect_global_css []
40
+ mailer.override_css(nil)
50
41
  end
51
42
 
52
- it "inlines css to the email's html part" do
53
- Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
54
- email = InliningMailer.multipart
55
- email.html_part.body.decoded.should == 'html'
56
- email.text_part.body.decoded.should == 'Hello Text'
43
+ it "uses no global CSS when :css is set to false" do
44
+ expect_global_css []
45
+ mailer.override_css(false)
57
46
  end
58
- end
59
- end
60
47
 
61
- describe Roadie::ActionMailerExtensions, "loading css files" do
62
- class CssLoadingMailer < ActionMailer::Base
63
- default :css => :default_value
64
- def use_default
65
- mail &with_empty_html_response
48
+ it "uses the default CSS when :css is not specified" do
49
+ expect_global_css ['default']
50
+ mailer.default_css
66
51
  end
67
52
 
68
- def override(target)
69
- mail :css => target, &with_empty_html_response
53
+ it "uses the specified CSS instead of the default" do
54
+ expect_global_css ['some', 'other/files']
55
+ mailer.override_css([:some, 'other/files'])
70
56
  end
57
+ end
71
58
 
72
- protected
73
- def with_empty_html_response
74
- Proc.new { |format| format.html { render :text => '' } }
59
+ describe ActionMailerExtensions, "using HTML" do
60
+ mailer = Class.new(ActionMailer::Base) do
61
+ default :css => :simple
62
+
63
+ # Just to make ActionMailer::Base not puke
64
+ def self.name
65
+ "SomeMailer"
75
66
  end
76
- end
77
67
 
78
- before(:each) do
79
- Roadie.stub!(:inline_css => 'html')
80
- end
68
+ def multipart
69
+ mail(:subject => "Multipart email") do |format|
70
+ format.html { render :text => 'Hello HTML' }
71
+ format.text { render :text => 'Hello Text' }
72
+ end
73
+ end
81
74
 
82
- it "loads css from Roadie" do
83
- Roadie.should_receive(:load_css).with(anything).and_return('')
84
- CssLoadingMailer.use_default
85
- end
75
+ def singlepart_html
76
+ mail(:subject => "HTML email") do |format|
77
+ format.html { render :text => 'Hello HTML' }
78
+ end
79
+ end
86
80
 
87
- it "loads the css specified in the default mailer settings" do
88
- Roadie.should_receive(:load_css).with(['default_value']).and_return('')
89
- CssLoadingMailer.use_default
90
- end
81
+ def singlepart_plain
82
+ mail(:subject => "Text email") do |format|
83
+ format.text { render :text => 'Hello Text' }
84
+ end
85
+ end
86
+ end
91
87
 
92
- it "loads the css specified in the specific mailer action instead of the default choice" do
93
- Roadie.should_receive(:load_css).with(['specific']).and_return('')
94
- CssLoadingMailer.override(:specific)
95
- end
88
+ let(:provider) { double("asset provider", :all => '') }
96
89
 
97
- it "loads no css when specifying false in the mailer action" do
98
- Roadie.should_not_receive(:load_css)
99
- CssLoadingMailer.override(false)
100
- end
90
+ before(:each) do
91
+ Roadie.stub(:inline_css => 'unexpected value passed to inline_css')
92
+ Roadie.stub(:current_provider => provider)
93
+ end
94
+
95
+ describe "for singlepart text/plain" do
96
+ it "does not touch the email body" do
97
+ Roadie.should_not_receive(:inline_css)
98
+ mailer.singlepart_plain
99
+ end
100
+ end
101
+
102
+ describe "for singlepart text/html" do
103
+ it "inlines css to the email body" do
104
+ Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything).and_return('html')
105
+ mailer.singlepart_html.body.decoded.should == 'html'
106
+ end
107
+ end
101
108
 
102
- it "loads multiple css files when given an array" do
103
- Roadie.should_receive(:load_css).with(['specific', 'other']).and_return('')
104
- CssLoadingMailer.override([:specific, :other])
109
+ describe "for multipart" do
110
+ it "keeps both parts" do
111
+ mailer.multipart.should have(2).parts
112
+ end
113
+
114
+ it "inlines css to the email's html part" do
115
+ Roadie.should_receive(:inline_css).with(provider, ['simple'], 'Hello HTML', anything).and_return('html')
116
+ email = mailer.multipart
117
+ email.html_part.body.decoded.should == 'html'
118
+ email.text_part.body.decoded.should == 'Hello Text'
119
+ end
120
+ end
105
121
  end
106
122
  end