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.
- data/.gitignore +1 -0
- data/Changelog.md +12 -1
- data/Gemfile.lock +49 -30
- data/README.md +39 -9
- data/lib/roadie.rb +45 -26
- data/lib/roadie/action_mailer_extensions.rb +13 -8
- data/lib/roadie/asset_pipeline_provider.rb +28 -0
- data/lib/roadie/asset_provider.rb +62 -0
- data/lib/roadie/filesystem_provider.rb +74 -0
- data/lib/roadie/inliner.rb +16 -16
- data/lib/roadie/railtie.rb +22 -0
- data/lib/roadie/version.rb +1 -1
- data/roadie.gemspec +1 -0
- data/spec/fixtures/public/stylesheets/integration.css +10 -0
- data/spec/integration_spec.rb +62 -47
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +91 -75
- data/spec/lib/roadie/asset_pipeline_provider_spec.rb +65 -0
- data/spec/lib/roadie/filesystem_provider_spec.rb +94 -0
- data/spec/lib/roadie/inliner_spec.rb +30 -5
- data/spec/lib/roadie_spec.rb +31 -16
- data/spec/shared_examples/asset_provider_examples.rb +11 -0
- data/spec/spec_helper.rb +11 -7
- metadata +30 -19
- data/spec/fixtures/app/assets/stylesheets/bar.css +0 -1
- data/spec/fixtures/app/assets/stylesheets/foo.css +0 -1
- data/spec/fixtures/app/assets/stylesheets/green_paragraphs.css +0 -1
- data/spec/fixtures/app/assets/stylesheets/large_purple_paragraphs.css +0 -1
- data/spec/fixtures/app/assets/stylesheets/subdirectory/findme.css +0 -1
- data/spec/fixtures/app/assets/stylesheets/subdirectory/red_paragraphs.css +0 -1
@@ -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
|
data/lib/roadie/inliner.rb
CHANGED
@@ -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 [
|
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+
|
29
|
-
def initialize(
|
30
|
-
@
|
31
|
-
@
|
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
|
-
|
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 =
|
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
|
data/lib/roadie/version.rb
CHANGED
data/roadie.gemspec
CHANGED
@@ -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
|
+
}
|
data/spec/integration_spec.rb
CHANGED
@@ -1,68 +1,83 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
32
|
+
email.to.should == ['doe@example.com']
|
33
|
+
email.from.should == ['john@example.com']
|
34
|
+
email.should have(2).parts
|
26
35
|
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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 "
|
53
|
-
|
54
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|