roadie 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +108 -0
- data/Rakefile +60 -0
- data/lib/roadie.rb +28 -0
- data/lib/roadie/action_mailer_extensions.rb +49 -0
- data/lib/roadie/inliner.rb +145 -0
- data/spec/fixtures/public/stylesheets/bar.css +1 -0
- data/spec/fixtures/public/stylesheets/foo.css +1 -0
- data/spec/fixtures/public/stylesheets/integration.css +4 -0
- data/spec/fixtures/views/integration_mailer/notification.html.erb +8 -0
- data/spec/fixtures/views/integration_mailer/notification.text.erb +6 -0
- data/spec/integration_spec.rb +46 -0
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +112 -0
- data/spec/lib/roadie/inliner_spec.rb +221 -0
- data/spec/lib/roadie_spec.rb +27 -0
- data/spec/spec_helper.rb +101 -0
- metadata +178 -0
data/README.textile
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
h1. Roadie
|
2
|
+
|
3
|
+
Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.
|
4
|
+
|
5
|
+
If you want to have this in Rails 2, please see "MailStyle":https://www.github.com/purify/mail_style.
|
6
|
+
|
7
|
+
h2. How does it work?
|
8
|
+
|
9
|
+
Email clients have bad support for stylesheets, and some of them blocks stylesheets from downloading. The easiest way to handle this is to work with all styles inline, but that is error prone and hard to work with as you cannot use classes and/or reuse styling.
|
10
|
+
|
11
|
+
This gem helps making this easier by automatically inlining stylesheet rules into the document before sending it. You just give it a list of stylesheets and it will go though all of the selectors assigning the styles to the maching elements. Careful attention has been put into rules being applied in the correct order, so it should behave just like in the browser[1].
|
12
|
+
|
13
|
+
Roadie also rewrites all relative URLs in the email to a absolute counterpart, making images you insert and those referenced in your stylesheets work. No more headaches about how to write the stylesheets while still having them work with emails from your acceptance environments.
|
14
|
+
|
15
|
+
fn1. Of course, rules like @:hover@ will not work by definition. Only static styles can be added.
|
16
|
+
|
17
|
+
h2. Features
|
18
|
+
|
19
|
+
* Writes CSS styles inline
|
20
|
+
** Respects @!important@ styles
|
21
|
+
** Does not overwrite styles already present in the @style@ attribute of tags
|
22
|
+
** Supports the same CSS selectors as "Nokogiri":http://nokogiri.org/ (use CSS3 selectors in your emails!)
|
23
|
+
* Makes image urls absolute
|
24
|
+
** Hostname and port configurable on a per-environment basis
|
25
|
+
* Makes link <code>href</code>s absolute
|
26
|
+
* Automatically adds proper html skeleton when missing (you don't have to create a layout for emails)[2]
|
27
|
+
|
28
|
+
fn2. This might be removed in a future version, though. You really ought to create a good layout and not let Roadie guess how you want to have it structured
|
29
|
+
|
30
|
+
h3. What about Sass / Less?
|
31
|
+
|
32
|
+
Sass is supported "by accident" as long as the stylesheets are generated and stored in the stylesheets directory. This is the default behavior from Sass. You are recommended to add a deploy task that generates the stylesheets to make sure that they are present at all times.
|
33
|
+
|
34
|
+
h2. Install
|
35
|
+
|
36
|
+
Add the gem to Rails' Gemfile
|
37
|
+
<pre><code>gem 'roadie'</code></pre>
|
38
|
+
|
39
|
+
h2. Usage
|
40
|
+
|
41
|
+
Simply specify the <code>:css</code> option to mailer:
|
42
|
+
|
43
|
+
<pre><code>class Notifier < ActionMailer::Base
|
44
|
+
default :css => :email, :from => 'support@mycompany.com'
|
45
|
+
def registration_mail
|
46
|
+
mail(:subject => 'Welcome Aboard', :to => 'someone@example.com')
|
47
|
+
end
|
48
|
+
|
49
|
+
def newsletter
|
50
|
+
mail(:subject => 'Newsletter', :to => 'someone@example.com', :css => [:email, :newsletter])
|
51
|
+
end
|
52
|
+
end</code></pre>
|
53
|
+
(you could also use the @defaults@ method in ActionMailer)
|
54
|
+
|
55
|
+
This will look for a css file called @email.css@ in your @public/stylesheets@ folder. The @css@ method can take either a string, a symbol or an array of both. You should pass the CSS filename without the ".css" extension.
|
56
|
+
|
57
|
+
h3. Image URL Correcting
|
58
|
+
|
59
|
+
If you have @default_url_options[:host]@ set in your mailer, then Roadie will do it's best to make the urls of images and in stylesheets absolute.
|
60
|
+
|
61
|
+
In @application.rb@:
|
62
|
+
<pre><code>class Application
|
63
|
+
config.action_mailer.default_url_options = {:host => 'example.com'}
|
64
|
+
end</code></pre>
|
65
|
+
|
66
|
+
If you want to to be different depending on your environment, just set it in your environment's configuration instead.
|
67
|
+
|
68
|
+
h3. Ignoring stylesheets
|
69
|
+
|
70
|
+
By default, @style@ elements in the email document's @head@ are processed along with the stylesheets and removed from the @head@.
|
71
|
+
|
72
|
+
You can set a special <code>data-immutable="true"</code> attribute on @style@ tags you do not want to be processed and removed from the document's @head@. This is the place to put things like @:hover@ selectors that you want to have for email clients allowing them.
|
73
|
+
|
74
|
+
Style elements with <code>media="print"</code> are always ignored.
|
75
|
+
|
76
|
+
h2. Bugs / TODO
|
77
|
+
|
78
|
+
* Improve overall performance
|
79
|
+
* Clean up stylesheet assignment code
|
80
|
+
|
81
|
+
h2. History and contributors
|
82
|
+
|
83
|
+
This gem was originally developed for Rails 2 use on "Purify":http://purifyapp.com under the name "MailStyle":https://www.github.com/purify/mail_style. However, the author stopped maintaining it and a fork took place to make it Rails 3 compatible.
|
84
|
+
|
85
|
+
The following people have contributed to the orignal gem:
|
86
|
+
|
87
|
+
* "Jim Neath":http://jimneath.org (Original author)
|
88
|
+
* "Lars Klevans":http://tastybyte.blogspot.com/
|
89
|
+
* "Jonas Grimfelt":http://github.com/grimen
|
90
|
+
* "Ben Johnson":http://www.binarylogic.com
|
91
|
+
* "Istvan Hoka":http://istvanhoka.com/
|
92
|
+
* "Voraz":http://blog.voraz.com.br
|
93
|
+
|
94
|
+
h2. License
|
95
|
+
|
96
|
+
(The MIT License)
|
97
|
+
|
98
|
+
Copyright © 2009-2011
|
99
|
+
|
100
|
+
* "Jim Neath":http://jimneath.org
|
101
|
+
* Magnus Bergmark <magnus.bergmark@gmail.com>
|
102
|
+
|
103
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
104
|
+
|
105
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
106
|
+
|
107
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
108
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'rake'
|
14
|
+
require 'jeweler'
|
15
|
+
|
16
|
+
require 'rake/rdoctask'
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
|
19
|
+
NAME = "roadie"
|
20
|
+
EXTRA_RDOC_FILES = %w[README.textile]
|
21
|
+
Jeweler::Tasks.new do |t|
|
22
|
+
t.name = NAME
|
23
|
+
t.summary = %{Making HTML emails comfortable for the Rails rockstars}
|
24
|
+
t.email = "magnus.bergmark@gmail.com"
|
25
|
+
t.homepage = "http://github.com/Mange/roadie"
|
26
|
+
t.description = %{Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.}
|
27
|
+
t.author = "Magnus Bergmark"
|
28
|
+
|
29
|
+
t.require_path = 'lib'
|
30
|
+
t.files = %w(Rakefile) + EXTRA_RDOC_FILES + Dir.glob(File.join(*%w[{lib,spec} ** *]).to_s)
|
31
|
+
t.extra_rdoc_files = EXTRA_RDOC_FILES
|
32
|
+
|
33
|
+
# dependencies defined in Gemfile
|
34
|
+
end
|
35
|
+
|
36
|
+
Jeweler::RubygemsDotOrgTasks.new
|
37
|
+
|
38
|
+
desc "Generate documentation for the #{NAME} plugin."
|
39
|
+
Rake::RDocTask.new(:rdoc) do |t|
|
40
|
+
t.rdoc_dir = 'rdoc'
|
41
|
+
t.title = NAME
|
42
|
+
t.options << '--line-numbers' << '--inline-source'
|
43
|
+
t.rdoc_files.include(EXTRA_RDOC_FILES)
|
44
|
+
t.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Run plugin specs for #{NAME}."
|
48
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
49
|
+
t.pattern = 'spec/**/*_spec.rb'
|
50
|
+
t.rspec_opts = ["-c"]
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Run plugin specs for #{NAME} with specdoc formatting and colors"
|
54
|
+
RSpec::Core::RakeTask.new('specdoc') do |t|
|
55
|
+
t.pattern = 'spec/**/*_spec.rb'
|
56
|
+
t.rspec_opts = ["--format specdoc", "-c"]
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Default: Run specs."
|
60
|
+
task :default => :spec
|
data/lib/roadie.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Roadie
|
2
|
+
class CSSFileNotFound < StandardError; end
|
3
|
+
|
4
|
+
def self.inline_css(*args);
|
5
|
+
Roadie::Inliner.new(*args).execute
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load_css(root, targets)
|
9
|
+
loaded_css = []
|
10
|
+
stylesheets = root.join('public', 'stylesheets')
|
11
|
+
|
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
|
18
|
+
end
|
19
|
+
loaded_css.join("\n")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'roadie/inliner'
|
24
|
+
|
25
|
+
require 'action_mailer'
|
26
|
+
require 'roadie/action_mailer_extensions'
|
27
|
+
|
28
|
+
ActionMailer::Base.send :include, Roadie::ActionMailerExtensions
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'css_parser'
|
4
|
+
|
5
|
+
module Roadie
|
6
|
+
module ActionMailerExtensions
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method_chain :collect_responses_and_parts_order, :inline_styles
|
10
|
+
alias_method_chain :mail, :inline_styles
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
def mail_with_inline_styles(headers = {}, &block)
|
16
|
+
@inline_style_css_targets = headers[:css]
|
17
|
+
mail_without_inline_styles(headers.except(:css), &block).tap do |email|
|
18
|
+
email[:css] = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect_responses_and_parts_order_with_inline_styles(headers, &block)
|
23
|
+
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]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def inline_style_response(response)
|
33
|
+
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)
|
35
|
+
else
|
36
|
+
response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def css_targets
|
41
|
+
return nil if @inline_style_css_targets == false
|
42
|
+
Array(@inline_style_css_targets || self.class.default[:css] || []).map { |target| target.to_s }
|
43
|
+
end
|
44
|
+
|
45
|
+
def css_rules
|
46
|
+
@css_rules ||= Roadie.load_css(Rails.root, css_targets) if css_targets.present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Roadie
|
2
|
+
class Inliner
|
3
|
+
CSS_URL_REGEXP = %r{
|
4
|
+
url\(
|
5
|
+
(["']?)
|
6
|
+
(
|
7
|
+
[^(]* # Text leading up to before opening parens
|
8
|
+
(?:\([^)]*\))* # Texts containing parens pairs
|
9
|
+
[^(]+ # Texts without parens - required
|
10
|
+
)
|
11
|
+
\1
|
12
|
+
\)
|
13
|
+
}x
|
14
|
+
|
15
|
+
attr_reader :css, :html, :url_options
|
16
|
+
|
17
|
+
def initialize(css, html, url_options)
|
18
|
+
@css = css
|
19
|
+
@inline_css = []
|
20
|
+
@html = html
|
21
|
+
@url_options = url_options
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
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)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def inline_css
|
36
|
+
@inline_css.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def parsed_css
|
40
|
+
CssParser::Parser.new.tap do |parser|
|
41
|
+
parser.add_block!(css) if css
|
42
|
+
parser.add_block!(inline_css)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def adjust_html
|
47
|
+
Nokogiri::HTML.parse(html).tap do |document|
|
48
|
+
yield document
|
49
|
+
end.to_html
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_missing_structure(document)
|
53
|
+
html_node = document.at_css('html')
|
54
|
+
html_node['xmlns'] ||= 'http://www.w3.org/1999/xhtml'
|
55
|
+
|
56
|
+
if document.at_css('html > head').present?
|
57
|
+
head = document.at_css('html > head')
|
58
|
+
else
|
59
|
+
head = Nokogiri::XML::Node.new('head', document)
|
60
|
+
document.at_css('html').children.before(head)
|
61
|
+
end
|
62
|
+
|
63
|
+
unless document.at_css('html > head > meta[http-equiv=Content-Type]')
|
64
|
+
meta = Nokogiri::XML::Node.new('meta', document)
|
65
|
+
meta['http-equiv'] = 'Content-Type'
|
66
|
+
meta['content'] = 'text/html; charset=utf-8'
|
67
|
+
head.add_child(meta)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_inline_style_elements(document)
|
72
|
+
document.css("style").each do |style|
|
73
|
+
next if style['media'] == 'print' or style['data-immutable']
|
74
|
+
@inline_css << style.content
|
75
|
+
style.remove
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def inline_css_rules(document)
|
80
|
+
matched_elements = {}
|
81
|
+
assign_rules_to_elements(document, matched_elements)
|
82
|
+
|
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('; ')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
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)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
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
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def make_image_urls_absolute(document)
|
112
|
+
document.css('img').each do |img|
|
113
|
+
img['src'] = ensure_absolute_url(img['src']) if img['src']
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def make_style_urls_absolute(document)
|
118
|
+
document.css('*[style]').each do |element|
|
119
|
+
styling = element['style']
|
120
|
+
element['style'] = styling.gsub(CSS_URL_REGEXP) { "url(#{$1}#{ensure_absolute_url($2, '/stylesheets')}#{$1})" }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def ensure_absolute_url(url, base_path = nil)
|
125
|
+
base, uri = absolute_url_base(base_path), URI.parse(url)
|
126
|
+
if uri.relative? and base
|
127
|
+
base.merge(uri).to_s
|
128
|
+
else
|
129
|
+
uri.to_s
|
130
|
+
end
|
131
|
+
rescue URI::InvalidURIError
|
132
|
+
return url
|
133
|
+
end
|
134
|
+
|
135
|
+
def absolute_url_base(base_path)
|
136
|
+
return nil unless url_options
|
137
|
+
URI::Generic.build({
|
138
|
+
:scheme => url_options[:protocol] || 'http',
|
139
|
+
:host => url_options[:host],
|
140
|
+
:port => url_options[:port],
|
141
|
+
:path => base_path
|
142
|
+
})
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
contents of bar
|
@@ -0,0 +1 @@
|
|
1
|
+
contents of foo
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "roadie integration" do
|
4
|
+
class TestApplication
|
5
|
+
def config
|
6
|
+
OpenStruct.new(:action_mailer => OpenStruct.new(:default_url_options => {:host => "example.app.org"}))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class IntegrationMailer < ActionMailer::Base
|
11
|
+
default :css => :integration, :from => 'john@example.com'
|
12
|
+
append_view_path Pathname.new(__FILE__).dirname.join('fixtures').join('views')
|
13
|
+
|
14
|
+
def notification(to, reason)
|
15
|
+
@reason = reason
|
16
|
+
mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
before(:each) do
|
21
|
+
Rails.stub!(:root => Pathname.new(__FILE__).dirname.join('fixtures'), :application => TestApplication.new)
|
22
|
+
IntegrationMailer.delivery_method = :test
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should inline styles for an email" do
|
26
|
+
email = IntegrationMailer.notification('doe@example.com', 'your quota limit has been reached')
|
27
|
+
|
28
|
+
email.to.should == ['doe@example.com']
|
29
|
+
email.from.should == ['john@example.com']
|
30
|
+
email.should have(2).parts
|
31
|
+
|
32
|
+
email.parts.find { |part| part.mime_type == 'text/html' }.tap do |html_part|
|
33
|
+
document = Nokogiri::HTML.parse(html_part.body.decoded)
|
34
|
+
document.should have_selector('html > head + body')
|
35
|
+
document.should have_selector('body #message h1')
|
36
|
+
document.should have_styling('background' => 'url(http://example.app.org/images/dots.png) repeat-x').at_selector('body')
|
37
|
+
document.should have_selector('strong[contains("quota")]')
|
38
|
+
end
|
39
|
+
|
40
|
+
email.parts.find { |part| part.mime_type == 'text/plain' }.tap do |plain_part|
|
41
|
+
plain_part.body.decoded.should_not match(/<.*>/)
|
42
|
+
end
|
43
|
+
|
44
|
+
email.deliver
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'spec_helper'
|
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' }
|
12
|
+
end
|
13
|
+
end
|
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
|
+
def singlepart_html
|
27
|
+
mail(:subject => "HTML email") do |format|
|
28
|
+
format.html { render :text => 'Hello HTML' }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def singlepart_plain
|
33
|
+
mail(:subject => "Text email") do |format|
|
34
|
+
format.text { render :text => 'Hello Text' }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
before(:each) do
|
40
|
+
Roadie.stub!(:load_css => 'loaded css')
|
41
|
+
Roadie.stub!(:inline_css => 'unexpected value') # Make sure a implementation problem doesn't hurt these examples
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "for singlepart text/plain" do
|
45
|
+
it "should not touch the email body" do
|
46
|
+
Roadie.should_not_receive(:inline_css)
|
47
|
+
InliningMailer.singlepart_plain
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "for singlepart text/html" do
|
52
|
+
it "should inline css to the email body" do
|
53
|
+
Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
|
54
|
+
InliningMailer.singlepart_html.body.decoded.should == 'html'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "for multipart" do
|
59
|
+
it "should keep both parts" do
|
60
|
+
InliningMailer.multipart.should have(2).parts
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should inline css to the email's html part" do
|
64
|
+
Roadie.should_receive(:inline_css).with(anything, 'Hello HTML', anything).and_return('html')
|
65
|
+
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'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Roadie::ActionMailerExtensions, "loading css files" do
|
73
|
+
class CssLoadingMailer < ActionMailer::Base
|
74
|
+
default :css => :default_value
|
75
|
+
def use_default
|
76
|
+
mail &with_empty_html_response
|
77
|
+
end
|
78
|
+
|
79
|
+
def override(target)
|
80
|
+
mail :css => target, &with_empty_html_response
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
def with_empty_html_response
|
85
|
+
Proc.new { |format| format.html { render :text => '' } }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
before(:each) do
|
90
|
+
Roadie.stub!(:inline_css => 'html')
|
91
|
+
end
|
92
|
+
|
93
|
+
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('')
|
95
|
+
CssLoadingMailer.use_default
|
96
|
+
end
|
97
|
+
|
98
|
+
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('')
|
100
|
+
CssLoadingMailer.override(:specific)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should load no css when specifying false in the mailer action" do
|
104
|
+
Roadie.should_not_receive(:load_css)
|
105
|
+
CssLoadingMailer.override(false)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should load multiple css files when given an array" do
|
109
|
+
Roadie.should_receive(:load_css).with(Rails.root, ['specific', 'other']).and_return('')
|
110
|
+
CssLoadingMailer.override([:specific, :other])
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Roadie::Inliner do
|
4
|
+
def use_css(css); @css = css; end
|
5
|
+
def rendering(html, options = {})
|
6
|
+
Nokogiri::HTML.parse Roadie::Inliner.new(@css, html, options.fetch(:url_options, {:host => 'example.com'})).execute
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "inlining styles" do
|
10
|
+
before(:each) do
|
11
|
+
# Make sure to have some css even when we don't specify any
|
12
|
+
# We have specific tests for when this is nil
|
13
|
+
use_css ''
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should inline simple attributes" do
|
17
|
+
use_css 'p { color: green }'
|
18
|
+
rendering('<p></p>').should have_styling('color' => 'green').at_selector('p')
|
19
|
+
end
|
20
|
+
|
21
|
+
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')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should use the ones attributes with the highest specificality when conflicts arises" do
|
28
|
+
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')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should support multiple selectors for the same rules" do
|
34
|
+
use_css 'p, a { color: green; }'
|
35
|
+
rendering('<p></p><a></a>').tap do |document|
|
36
|
+
document.should have_styling('color' => 'green').at_selector('p')
|
37
|
+
document.should have_styling('color' => 'green').at_selector('a')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should respect !important properties" do
|
42
|
+
use_css "a { text-decoration: underline !important; }
|
43
|
+
a.hard-to-spot { text-decoration: none; }"
|
44
|
+
rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline').at_selector('a')
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should combine with already present inline styles" do
|
48
|
+
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')
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not overwrite already present inline styles" do
|
53
|
+
use_css "p { color: red }"
|
54
|
+
rendering('<p style="color: green"></p>').should have_styling('color' => 'green').at_selector('p')
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should ignore selectors with :psuedo-classes" do
|
58
|
+
use_css 'p:hover { color: red }'
|
59
|
+
rendering('<p></p>').should_not have_styling('color' => 'red').at_selector('p')
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "inline <style> elements" do
|
63
|
+
it "should be used for inlined styles" do
|
64
|
+
rendering(<<-HTML).should have_styling('color' => 'green', 'font-size' => '1.1em').at_selector('p')
|
65
|
+
<html>
|
66
|
+
<head>
|
67
|
+
<style type="text/css">p { color: green; }</style>
|
68
|
+
</head>
|
69
|
+
<body>
|
70
|
+
<p>Hello World</p>
|
71
|
+
<style type="text/css">p { font-size: 1.1em; }</style>
|
72
|
+
</body>
|
73
|
+
</html>
|
74
|
+
HTML
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should be removed" do
|
78
|
+
rendering(<<-HTML).should_not have_selector('style')
|
79
|
+
<html>
|
80
|
+
<head>
|
81
|
+
<style type="text/css">p { color: green; }</style>
|
82
|
+
</head>
|
83
|
+
<body>
|
84
|
+
<style type="text/css">p { font-size: 1.1em; }</style>
|
85
|
+
</body>
|
86
|
+
</html>
|
87
|
+
HTML
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should not be touched when data-immutable=true" do
|
91
|
+
document = rendering <<-HTML
|
92
|
+
<style type="text/css" data-immutable="true">p { color: red; }</style>
|
93
|
+
<p></p>
|
94
|
+
HTML
|
95
|
+
document.should have_selector('style[data-immutable=true]')
|
96
|
+
document.should_not have_styling('color' => 'red').at_selector('p')
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not be touched when media=print" do
|
100
|
+
document = rendering <<-HTML
|
101
|
+
<style type="text/css" media="print">p { color: red; }</style>
|
102
|
+
<p></p>
|
103
|
+
HTML
|
104
|
+
document.should have_selector('style[media=print]')
|
105
|
+
document.should_not have_styling('color' => 'red').at_selector('p')
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should still be inlined when no external css rules are defined" do
|
109
|
+
use_css nil
|
110
|
+
rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p')
|
111
|
+
<style type="text/css">p { color: green; }</style>
|
112
|
+
<p>Hello World</p>
|
113
|
+
HTML
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "making urls absolute" do
|
119
|
+
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')
|
123
|
+
end
|
124
|
+
|
125
|
+
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')
|
127
|
+
end
|
128
|
+
|
129
|
+
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("/paper.png")"></p>').should have_styling('background' => 'url("http://example.com/paper.png")').at_selector('p')
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should work on external style declarations" do
|
135
|
+
use_css "p { background-image: url(/paper.png); }
|
136
|
+
table { background-image: url('/paper.png'); }
|
137
|
+
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')
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should not touch style urls that are already absolute" do
|
144
|
+
external_url = 'url(http://other.example.org/paper.png)'
|
145
|
+
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')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should not touch the urls when no url options are defined" do
|
151
|
+
use_css "img { background: url(/a.jpg); }"
|
152
|
+
rendering('<img src="/b.jpg" />', :url_options => nil).tap do |document|
|
153
|
+
document.should have_attribute('src' => '/b.jpg').at_selector('img')
|
154
|
+
document.should have_styling('background' => 'url(/a.jpg)').at_selector('img')
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should support port and protocol settings" do
|
159
|
+
use_css "img { background: url(/a.jpg); }"
|
160
|
+
rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https', :port => '8080'}).tap do |document|
|
161
|
+
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')
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should not touch data: URIs" do
|
167
|
+
use_css "div { background: url(data:abcdef); }"
|
168
|
+
rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)').at_selector('div')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "inserting tags" do
|
173
|
+
it "should insert a doctype if not present" do
|
174
|
+
rendering('<html><body></body></html>').to_xml.should include('<!DOCTYPE ')
|
175
|
+
rendering('<!DOCTYPE html><html><body></body></html>').to_xml.should_not match(/(DOCTYPE.*?){2}/)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should set xmlns of <html> to that of XHTML" do
|
179
|
+
rendering('<html><body></body></html>').should have_selector('html[xmlns="http://www.w3.org/1999/xhtml"]')
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should insert basic html structure if not present" do
|
183
|
+
rendering('<h1>Hey!</h1>').should have_selector('html > head + body > h1')
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should insert <head> if not present" do
|
187
|
+
rendering('<html><body></body></html>').should have_selector('html > head + body')
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should insert meta tag describing content-type" do
|
191
|
+
rendering('<html><head></head><body></body></html>').should have_selector('head meta[http-equiv="Content-Type"][content="text/html; charset=utf-8"]')
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should not insert duplicate meta tags describing content-type" do
|
195
|
+
rendering(<<-HTML).to_html.scan('meta').should have(1).item
|
196
|
+
<html>
|
197
|
+
<head>
|
198
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
199
|
+
</head>
|
200
|
+
</html>
|
201
|
+
HTML
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "css url regex" do
|
206
|
+
it "should parse css urls" do
|
207
|
+
{
|
208
|
+
'url(/foo.jpg)' => '/foo.jpg',
|
209
|
+
'url("/foo.jpg")' => '/foo.jpg',
|
210
|
+
"url('/foo.jpg')" => '/foo.jpg',
|
211
|
+
'url(http://localhost/foo.jpg)' => 'http://localhost/foo.jpg',
|
212
|
+
'url("http://localhost/foo.jpg")' => 'http://localhost/foo.jpg',
|
213
|
+
"url('http://localhost/foo.jpg')" => 'http://localhost/foo.jpg',
|
214
|
+
'url(/andromeda_(galaxy).jpg)' => '/andromeda_(galaxy).jpg',
|
215
|
+
}.each do |raw, expected|
|
216
|
+
raw =~ Roadie::Inliner::CSS_URL_REGEXP
|
217
|
+
$2.should == expected
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Roadie do
|
4
|
+
describe ".inline_css" do
|
5
|
+
it "should create an instance of Roadie::Inliner and execute it" do
|
6
|
+
Roadie::Inliner.should_receive(:new).with('attri', 'butes').and_return(double('inliner', :execute => 'html'))
|
7
|
+
Roadie.inline_css('attri', 'butes').should == 'html'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe ".load_css(root, targets)" do
|
12
|
+
let(:fixtures_root) { Pathname.new(__FILE__).dirname.join('..', 'fixtures') }
|
13
|
+
|
14
|
+
it "should load files matching the target names under root/public/stylesheets" do
|
15
|
+
Roadie.load_css(fixtures_root, ['foo']).should == 'contents of foo'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should load files in order and join them with a newline" do
|
19
|
+
Roadie.load_css(fixtures_root, %w[foo bar]).should == "contents of foo\ncontents of bar"
|
20
|
+
Roadie.load_css(fixtures_root, %w[bar foo]).should == "contents of bar\ncontents of foo"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should raise a Roadie::CSSFileNotFound error when a css file could not be found" do
|
24
|
+
expect { Roadie.load_css(fixtures_root, ['not_here']) }.to raise_error(Roadie::CSSFileNotFound, /not_here/)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler'
|
6
|
+
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'rspec'
|
16
|
+
require 'action_mailer'
|
17
|
+
require 'roadie'
|
18
|
+
|
19
|
+
class TestApplication
|
20
|
+
def config
|
21
|
+
OpenStruct.new(:action_mailer => OpenStruct.new(:default_url_options => {:host => "example.com"}))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if defined?(Rails)
|
26
|
+
Rails.stub!(:root => Pathname.new('/path/to'), :application => TestApplication.new)
|
27
|
+
else
|
28
|
+
class Rails
|
29
|
+
def self.root; Pathname.new('/path/to'); end
|
30
|
+
def self.application; TestApplication.new; end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
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
|
+
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: roadie
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 270495472
|
5
|
+
prerelease: true
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
- pre1
|
11
|
+
version: 1.0.0.pre1
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- Magnus Bergmark
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-01-06 00:00:00 +01:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
name: nokogiri
|
34
|
+
requirement: *id001
|
35
|
+
type: :runtime
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
name: css_parser
|
48
|
+
requirement: *id002
|
49
|
+
type: :runtime
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 7
|
58
|
+
segments:
|
59
|
+
- 3
|
60
|
+
- 0
|
61
|
+
- 0
|
62
|
+
version: 3.0.0
|
63
|
+
name: actionmailer
|
64
|
+
requirement: *id003
|
65
|
+
type: :runtime
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
name: rake
|
78
|
+
requirement: *id004
|
79
|
+
type: :development
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
prerelease: false
|
82
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 270495428
|
88
|
+
segments:
|
89
|
+
- 1
|
90
|
+
- 5
|
91
|
+
- 0
|
92
|
+
- pre5
|
93
|
+
version: 1.5.0.pre5
|
94
|
+
name: jeweler
|
95
|
+
requirement: *id005
|
96
|
+
type: :development
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
hash: 15
|
105
|
+
segments:
|
106
|
+
- 2
|
107
|
+
- 0
|
108
|
+
- 0
|
109
|
+
version: 2.0.0
|
110
|
+
name: rspec-rails
|
111
|
+
requirement: *id006
|
112
|
+
type: :development
|
113
|
+
description: Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.
|
114
|
+
email: magnus.bergmark@gmail.com
|
115
|
+
executables: []
|
116
|
+
|
117
|
+
extensions: []
|
118
|
+
|
119
|
+
extra_rdoc_files:
|
120
|
+
- README.textile
|
121
|
+
files:
|
122
|
+
- README.textile
|
123
|
+
- Rakefile
|
124
|
+
- lib/roadie.rb
|
125
|
+
- lib/roadie/action_mailer_extensions.rb
|
126
|
+
- lib/roadie/inliner.rb
|
127
|
+
- spec/fixtures/public/stylesheets/bar.css
|
128
|
+
- spec/fixtures/public/stylesheets/foo.css
|
129
|
+
- spec/fixtures/public/stylesheets/integration.css
|
130
|
+
- spec/fixtures/views/integration_mailer/notification.html.erb
|
131
|
+
- spec/fixtures/views/integration_mailer/notification.text.erb
|
132
|
+
- spec/integration_spec.rb
|
133
|
+
- spec/lib/roadie/action_mailer_extensions_spec.rb
|
134
|
+
- spec/lib/roadie/inliner_spec.rb
|
135
|
+
- spec/lib/roadie_spec.rb
|
136
|
+
- spec/spec_helper.rb
|
137
|
+
has_rdoc: true
|
138
|
+
homepage: http://github.com/Mange/roadie
|
139
|
+
licenses: []
|
140
|
+
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
hash: 3
|
152
|
+
segments:
|
153
|
+
- 0
|
154
|
+
version: "0"
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ">"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
hash: 25
|
161
|
+
segments:
|
162
|
+
- 1
|
163
|
+
- 3
|
164
|
+
- 1
|
165
|
+
version: 1.3.1
|
166
|
+
requirements: []
|
167
|
+
|
168
|
+
rubyforge_project:
|
169
|
+
rubygems_version: 1.3.7
|
170
|
+
signing_key:
|
171
|
+
specification_version: 3
|
172
|
+
summary: Making HTML emails comfortable for the Rails rockstars
|
173
|
+
test_files:
|
174
|
+
- spec/integration_spec.rb
|
175
|
+
- spec/lib/roadie/action_mailer_extensions_spec.rb
|
176
|
+
- spec/lib/roadie/inliner_spec.rb
|
177
|
+
- spec/lib/roadie_spec.rb
|
178
|
+
- spec/spec_helper.rb
|