inline-style-umanni 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +55 -0
- data/README.rdoc +153 -0
- data/Rakefile +9 -0
- data/example.rb +5 -0
- data/inline-style.gemspec +29 -0
- data/lib/inline-style.rb +98 -0
- data/lib/inline-style/css_parser_wrapper.rb +15 -0
- data/lib/inline-style/csspool_wrapper.rb +12 -0
- data/lib/inline-style/mail-interceptor.rb +36 -0
- data/lib/inline-style/rack-middleware.rb +33 -0
- data/lib/inline-style/rule.rb +18 -0
- data/lib/inline-style/version.rb +3 -0
- data/spec/css_inlining_spec.rb +90 -0
- data/spec/css_parsing_spec.rb +146 -0
- data/spec/fixtures/all.css +3 -0
- data/spec/fixtures/boletin.html +261 -0
- data/spec/fixtures/box-model.html +37 -0
- data/spec/fixtures/inline.html +107 -0
- data/spec/fixtures/none.css +3 -0
- data/spec/fixtures/print.css +3 -0
- data/spec/fixtures/selectors.html +27 -0
- data/spec/fixtures/style.css +3 -0
- data/spec/interceptor_spec.rb +64 -0
- data/spec/rack_middleware_spec.rb +60 -0
- data/spec/spec_helper.rb +62 -0
- metadata +153 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
inline-style (0.4.10)
|
5
|
+
css_parser
|
6
|
+
facets
|
7
|
+
maca-fork-csspool
|
8
|
+
nokogiri
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
activesupport (3.0.4)
|
14
|
+
css_parser (1.1.5)
|
15
|
+
diff-lcs (1.1.2)
|
16
|
+
facets (2.9.1)
|
17
|
+
ffi (1.0.7)
|
18
|
+
rake (>= 0.8.7)
|
19
|
+
i18n (0.5.0)
|
20
|
+
maca-fork-csspool (2.0.2)
|
21
|
+
ffi
|
22
|
+
mail (2.2.15)
|
23
|
+
activesupport (>= 2.3.6)
|
24
|
+
i18n (>= 0.4.0)
|
25
|
+
mime-types (~> 1.16)
|
26
|
+
treetop (~> 1.4.8)
|
27
|
+
mime-types (1.16)
|
28
|
+
nokogiri (1.4.4)
|
29
|
+
polyglot (0.3.1)
|
30
|
+
rack (1.2.1)
|
31
|
+
rake (0.8.7)
|
32
|
+
rspec (2.4.0)
|
33
|
+
rspec-core (~> 2.4.0)
|
34
|
+
rspec-expectations (~> 2.4.0)
|
35
|
+
rspec-mocks (~> 2.4.0)
|
36
|
+
rspec-core (2.4.0)
|
37
|
+
rspec-expectations (2.4.0)
|
38
|
+
diff-lcs (~> 1.1.2)
|
39
|
+
rspec-mocks (2.4.0)
|
40
|
+
treetop (1.4.9)
|
41
|
+
polyglot (>= 0.3.1)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
css_parser
|
48
|
+
facets
|
49
|
+
inline-style!
|
50
|
+
maca-fork-csspool
|
51
|
+
mail
|
52
|
+
nokogiri
|
53
|
+
rack
|
54
|
+
rspec
|
55
|
+
rspec-core
|
data/README.rdoc
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
= inline-style
|
2
|
+
|
3
|
+
http://github.com/umanni/inline-style
|
4
|
+
|
5
|
+
|
6
|
+
== Description
|
7
|
+
|
8
|
+
Will take all css in a page (either from linked stylesheet or from style tag) and will embed it in the style attribute for
|
9
|
+
each refered element taking selector specificity and declarator order.
|
10
|
+
|
11
|
+
Useful for html email: some clients (gmail, et all) won't render non inline styles.
|
12
|
+
|
13
|
+
* Includes a Rack middleware for using with Rails, Sinatra, etc...
|
14
|
+
* Includes a interceptor for the mail gem which allows automatic
|
15
|
+
inline processing for both mail as well as ActionMailer.
|
16
|
+
* It takes into account selector specificity.
|
17
|
+
|
18
|
+
|
19
|
+
== Usage
|
20
|
+
|
21
|
+
|
22
|
+
gem 'maca-fork-csspool' # this is optional if csspool is required inline-style will resort to it for css parsing, otherwise it will use the pure ruby css_parser
|
23
|
+
require 'csspool' # csspool should be faster because it uses librcroco written in C wich can also be an issue, use css_parser if you deploy to heroku
|
24
|
+
|
25
|
+
require 'inline-style'
|
26
|
+
|
27
|
+
html = File.read("./index.html")
|
28
|
+
puts InlineStyle.process(html, :stylesheets_paths => "./styles")
|
29
|
+
|
30
|
+
index.html contains:
|
31
|
+
|
32
|
+
<style type="text/css" media="screen">
|
33
|
+
* {
|
34
|
+
font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
|
35
|
+
margin: 4px 3px 2px 1px;
|
36
|
+
padding: 0;
|
37
|
+
}
|
38
|
+
|
39
|
+
#list {
|
40
|
+
margin: 10;
|
41
|
+
}
|
42
|
+
|
43
|
+
#list li {
|
44
|
+
font-family: Arial;
|
45
|
+
}
|
46
|
+
|
47
|
+
.element {
|
48
|
+
padding: 10;
|
49
|
+
}
|
50
|
+
|
51
|
+
.odd {
|
52
|
+
background-color: black;
|
53
|
+
}
|
54
|
+
|
55
|
+
.pair {
|
56
|
+
background-color: red;
|
57
|
+
}
|
58
|
+
|
59
|
+
</style>
|
60
|
+
|
61
|
+
|
62
|
+
<ul id='number' class='listing inlined' style='background-color: yellow'>
|
63
|
+
<li class='list-element odd'>
|
64
|
+
<span>1</span>
|
65
|
+
</li>
|
66
|
+
<li class='list-element pair'>
|
67
|
+
<span>2</span>
|
68
|
+
</li>
|
69
|
+
<li class='list-element odd'>
|
70
|
+
<span>3</span>
|
71
|
+
</li>
|
72
|
+
<li class='list-element pair'>
|
73
|
+
<span>4</span>
|
74
|
+
</li>
|
75
|
+
</ul>
|
76
|
+
|
77
|
+
Will become:
|
78
|
+
|
79
|
+
<ul id="number" class="listing inlined" style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;background-color: yellow;'>
|
80
|
+
<li class="list-element odd" style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;background-color: black;'>
|
81
|
+
<span style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;'>1</span>
|
82
|
+
</li>
|
83
|
+
<li class="list-element pair" style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;background-color: red;'>
|
84
|
+
<span style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;'>2</span>
|
85
|
+
</li>
|
86
|
+
<li class="list-element odd" style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;background-color: black;'>
|
87
|
+
<span style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;'>3</span>
|
88
|
+
</li>
|
89
|
+
<li class="list-element pair" style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;background-color: red;'>
|
90
|
+
<span style='font-family: "Lucida Grande", Lucida, Verdana, sans-serif;margin: 4.0px 3.0px 2.0px 1.0px;padding: 0.0;'>4</span>
|
91
|
+
</li>
|
92
|
+
</ul>
|
93
|
+
|
94
|
+
|
95
|
+
== Rack Middleware:
|
96
|
+
|
97
|
+
# Process all routes:
|
98
|
+
use InlineStyle::Rack::Middleware
|
99
|
+
|
100
|
+
# Restrict processing to single route:
|
101
|
+
use InlineStyle::Rack::Middleware, :paths => %r(/mails/.*)
|
102
|
+
|
103
|
+
# Restrict processing to some routes:
|
104
|
+
use InlineStyle::Rack::Middleware, :paths => [%r(/mails/.*), "/somepath"]
|
105
|
+
|
106
|
+
|
107
|
+
== Mail Interceptor
|
108
|
+
|
109
|
+
If using the mail library the following code will work:
|
110
|
+
|
111
|
+
Mail.register_interceptor \
|
112
|
+
InlineStyle::Mail::Interceptor.new(:stylesheets_path => 'public')
|
113
|
+
|
114
|
+
If using ActionMailer (which wraps mail):
|
115
|
+
|
116
|
+
ActionMailer::Base.register_interceptor \
|
117
|
+
InlineStyle::Mail::Interceptor.new(:stylesheets_path => 'public')
|
118
|
+
|
119
|
+
|
120
|
+
== Requirements:
|
121
|
+
|
122
|
+
nokogiri && (css_parser || maca-fork-csspool)
|
123
|
+
|
124
|
+
|
125
|
+
== Install:
|
126
|
+
|
127
|
+
sudo gem install inline-style
|
128
|
+
|
129
|
+
|
130
|
+
== License:
|
131
|
+
|
132
|
+
(The MIT License)
|
133
|
+
|
134
|
+
Copyright (c) 2009 Macario Ortega
|
135
|
+
|
136
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
137
|
+
a copy of this software and associated documentation files (the
|
138
|
+
'Software'), to deal in the Software without restriction, including
|
139
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
140
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
141
|
+
permit persons to whom the Software is furnished to do so, subject to
|
142
|
+
the following conditions:
|
143
|
+
|
144
|
+
The above copyright notice and this permission notice shall be
|
145
|
+
included in all copies or substantial portions of the Software.
|
146
|
+
|
147
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
148
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
149
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
150
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
151
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
152
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
153
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "inline-style/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "inline-style-umanni"
|
7
|
+
s.version = InlineStyle::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Umanni"]
|
10
|
+
s.email = ["ygor@umanni.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Inlines CSS for html email delivery}
|
13
|
+
s.description = %q{Inlines CSS for html email delivery}
|
14
|
+
s.post_install_message = %{Please read documentation for changes on the default css parser gem, specifically if you use csspool}
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency 'rspec'
|
22
|
+
s.add_development_dependency 'rack'
|
23
|
+
s.add_development_dependency 'rspec-core'
|
24
|
+
s.add_development_dependency 'mail'
|
25
|
+
|
26
|
+
s.add_dependency 'nokogiri'
|
27
|
+
s.add_dependency 'css_parser'
|
28
|
+
s.add_dependency 'maca-fork-csspool'
|
29
|
+
end
|
data/lib/inline-style.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
5
|
+
|
6
|
+
require "inline-style/rule"
|
7
|
+
require "inline-style/rack-middleware" # This two may be should be required by user if she needs it
|
8
|
+
require "inline-style/mail-interceptor"
|
9
|
+
|
10
|
+
class InlineStyle
|
11
|
+
CSSParser =
|
12
|
+
if const_defined? :CSSPool
|
13
|
+
require 'inline-style/csspool_wrapper'
|
14
|
+
CSSPoolWrapper
|
15
|
+
else
|
16
|
+
require 'inline-style/css_parser_wrapper'
|
17
|
+
CssParserWrapper
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [String, Nokogiri::HTML::Document] html Html or Nokogiri html to be inlined
|
21
|
+
# @param [Hash] opts Processing options
|
22
|
+
#
|
23
|
+
# @option opts [String] :stylesheets_path (ENV['DOCUMENT_ROOT'])
|
24
|
+
# Stylesheets root path or app's public directory where the stylesheets are to be found
|
25
|
+
def self.process html, opts = {}
|
26
|
+
new(html, opts).process
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize html, opts = {}
|
30
|
+
@stylesheets_path = opts[:stylesheets_path] || ENV['DOCUMENT_ROOT'] || '.'
|
31
|
+
@html = html
|
32
|
+
@dom = String === html ? Nokogiri.HTML(html) : html
|
33
|
+
end
|
34
|
+
|
35
|
+
def process
|
36
|
+
nodes_with_rules.each_pair do |node, rules|
|
37
|
+
rules = rules.sort_by{ |sel| "#{sel.specificity}%04d" % rules.index(sel) }
|
38
|
+
|
39
|
+
styles = []
|
40
|
+
rules.each do |rule|
|
41
|
+
next if rule.dynamic_pseudo_class
|
42
|
+
rule.declarations.each do |declaration|
|
43
|
+
if defined = styles.assoc(declaration.first)
|
44
|
+
styles[styles.index(defined)] = declaration # overrides defined declaration
|
45
|
+
else
|
46
|
+
styles << declaration
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
style = styles.map{ |declaration| declaration.join(': ') }.join('; ')
|
52
|
+
node['style'] = "#{style};" unless style.empty?
|
53
|
+
end
|
54
|
+
pre_parsed? ? @dom : @dom.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def nodes_with_rules
|
59
|
+
nodes, body = {}, @dom.css('body')
|
60
|
+
|
61
|
+
parse_css.rules.each do |rule|
|
62
|
+
body.css(rule.selector).each do |node|
|
63
|
+
nodes[node] ||= []
|
64
|
+
nodes[node].push rule
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
body.css('[style]').each do |node|
|
69
|
+
nodes[node] ||= []
|
70
|
+
nodes[node].push Rule.new ':inline', node['style'], '1000' # :inline is not really a pseudoclass
|
71
|
+
end
|
72
|
+
|
73
|
+
nodes
|
74
|
+
end
|
75
|
+
|
76
|
+
def pre_parsed?
|
77
|
+
@html == @dom
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns parsed CSS
|
81
|
+
def extract_css
|
82
|
+
@dom.css('style, link[rel=stylesheet]').collect do |node|
|
83
|
+
next unless /^$|screen|all/ === node['media'].to_s
|
84
|
+
node.remove
|
85
|
+
|
86
|
+
if node.name == 'style'
|
87
|
+
node.content
|
88
|
+
else
|
89
|
+
uri = %r{^https?://} === node['href'] ? node['href'] : File.join(@stylesheets_path, node['href'].sub(/\?.+$/,''))
|
90
|
+
open(uri).read
|
91
|
+
end
|
92
|
+
end.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_css
|
96
|
+
CSSParser.new extract_css
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'css_parser'
|
2
|
+
|
3
|
+
class InlineStyle
|
4
|
+
class CssParserWrapper
|
5
|
+
attr_accessor :rules
|
6
|
+
|
7
|
+
def initialize(css_code)
|
8
|
+
parser, @rules = CssParser::Parser.new, []
|
9
|
+
parser.add_block! css_code
|
10
|
+
parser.each_rule_set do |rule_set|
|
11
|
+
rule_set.each_selector { |sel, dec, spec| @rules << Rule.new(sel, dec, '%04d' % spec.to_i) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class InlineStyle
|
2
|
+
class CSSPoolWrapper
|
3
|
+
attr_accessor :rules
|
4
|
+
|
5
|
+
def initialize css_code
|
6
|
+
parser = CSSPool.CSS css_code
|
7
|
+
@rules = parser.rule_sets.map do |rule_set|
|
8
|
+
rule_set.selectors.map { |sel| Rule.new(sel.to_s, sel.declarations.join, "0#{sel.specificity.join}") }
|
9
|
+
end.flatten
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# A interceptor for +mail+ (https://github.com/mikel/mail) to
|
2
|
+
# automatically inline the styles of outgoing e-mails. To use:
|
3
|
+
#
|
4
|
+
# Mail.register_interceptor \
|
5
|
+
# InlineStyle::Mail::Interceptor.new(:stylesheets_path => 'public')
|
6
|
+
#
|
7
|
+
# Rails 3's ActionMailer wraps around the +mail+ and also supports
|
8
|
+
# interceptors. Example usage:
|
9
|
+
#
|
10
|
+
# ActionMailer::Base.register_interceptor \
|
11
|
+
# InlineStyle::Mail::Interceptor.new(:stylesheets_path => 'public')
|
12
|
+
#
|
13
|
+
module InlineStyle::Mail
|
14
|
+
class Interceptor
|
15
|
+
# The mime types we should inline. Basically HTML and XHTML.
|
16
|
+
# If you have something else you can just push it onto the list
|
17
|
+
INLINE_MIME_TYPES = %w(text/html application/xhtml+xml)
|
18
|
+
|
19
|
+
# Save the options to later pass to InlineStyle.process
|
20
|
+
def initialize(options={})
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
# Mail callback where we actually inline the styles
|
25
|
+
def delivering_email(part)
|
26
|
+
if part.multipart?
|
27
|
+
for part in part.parts
|
28
|
+
delivering_email part
|
29
|
+
end
|
30
|
+
elsif INLINE_MIME_TYPES.any? {|m| part.content_type.starts_with? m}
|
31
|
+
part.body = InlineStyle.process(part.body.to_s, @options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module InlineStyle::Rack
|
2
|
+
class Middleware
|
3
|
+
# @param [Hash] opts Middlewar options
|
4
|
+
#
|
5
|
+
# @option opts [String] :stylesheets_path (env['DOCUMENT_ROOT'])
|
6
|
+
# Stylesheets root path or app's public directory where the stylesheets are to be found
|
7
|
+
# @option opts [Regexp, Array, String] :paths
|
8
|
+
# Limit processing to the passed absolute paths
|
9
|
+
# Can be an array of strings or regular expressions, a single string or regular expression
|
10
|
+
# If not passed will process output for every path.
|
11
|
+
# Regexps and strings must comence with '/'
|
12
|
+
# @option opts [Boolean] :pseudo (false)
|
13
|
+
# If set to true will inline style for pseudo classes according to the W3C specification:
|
14
|
+
# http://www.w3.org/TR/css-style-attr.
|
15
|
+
# Should probably be left as false because browsers don't seem to comply with the specification for pseudo class style in the style attribute.
|
16
|
+
def initialize app, opts = {}
|
17
|
+
@app = app
|
18
|
+
@opts = opts
|
19
|
+
@paths = /^(?:#{ [*opts[:paths]].join('|') })/
|
20
|
+
end
|
21
|
+
|
22
|
+
def call env
|
23
|
+
response = @app.call env
|
24
|
+
return response unless @paths === env['PATH_INFO']
|
25
|
+
|
26
|
+
status, headers, body = response
|
27
|
+
|
28
|
+
body = InlineStyle.process(body.first, {:stylesheets_path => env['DOCUMENT_ROOT']}.merge(@opts))
|
29
|
+
[status, headers, [body]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|