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 ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ *.swp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in inline-style.gemspec
4
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => [:spec]
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ Bundler::GemHelper.install_tasks
data/example.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require "#{ dir = File.dirname(__FILE__) }/lib/inline-style"
3
+
4
+ html = File.read("#{ fixtures = dir + '/spec/fixtures' }/boletin.html")
5
+ puts InlineStyle.process(html)
@@ -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
@@ -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
+