roadie 2.0.0 → 2.1.0.pre1
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 +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
|