premailer 1.7.1 → 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ *.gem
3
+ Gemfile.lock
4
+ bin/*.html
5
+ html/
6
+ vendor/
7
+ rdoc/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,11 @@
1
+ = Premailer License
2
+
3
+ Copyright (c) 2007-2011, Alex Dunae. All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+ * Neither the name of Premailer, Alex Dunae nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -42,6 +42,26 @@ Download the Premailer gem from RubyGems.
42
42
  premailer.warnings.each do |w|
43
43
  puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
44
44
  end
45
+
46
+ === Premailer-specific CSS
47
+
48
+ Premailer looks for a few CSS attributes that make working with tables a bit easier.
49
+
50
+ +-premailer-width+:: Available on <tt>table</tt>, <tt>th</tt> and <tt>td</tt> elements
51
+ +-premailer-height+:: Available on <tt>table</tt>, <tt>tr</tt>, <tt>th</tt> and <tt>td</tt> elements
52
+ +-premailer-cellpadding+:: Available on <tt>table</tt> elements
53
+ +-premailer-cellspacing+:: Available on <tt>table</tt> elements
54
+
55
+ Each of these CSS declarations will be copied to appropriate element's attribute.
56
+
57
+ For example
58
+
59
+ table { -premailer-cellspacing: 5; -premailer-width: 500;}
60
+
61
+ will result in
62
+
63
+ <table cellspacing='5' width='500'>
64
+
45
65
 
46
66
  === Contributions
47
67
 
@@ -1,99 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'optparse'
4
- require 'rubygems'
5
- require File.expand_path(File.join(__FILE__) + '/../../lib/premailer.rb')
6
-
7
- # defaults
8
- options = {
9
- :base_url => nil,
10
- :link_query_string => nil,
11
- :remove_classes => false,
12
- :verbose => false,
13
- :line_length => 65
14
- }
15
-
16
- mode = :html
17
-
18
- opts = OptionParser.new do |opts|
19
- opts.banner = "Improve the rendering of HTML emails by making CSS inline among other things. Takes a path to a local file, a URL or a pipe as input.\n\n"
20
- opts.define_head "Usage: premailer <optional uri|optional path> [options]"
21
- opts.separator ""
22
- opts.separator "Examples:"
23
- opts.separator " premailer http://example.com/ > out.html"
24
- opts.separator " premailer http://example.com/ --mode txt > out.txt"
25
- opts.separator " cat input.html | premailer -q src=email > out.html"
26
- opts.separator " premailer ./public/index.html"
27
- opts.separator ""
28
- opts.separator "Options:"
29
-
30
- opts.on("--mode MODE", [:html, :txt], "Output: html or txt") do |v|
31
- mode = v
32
- end
33
-
34
- opts.on("-b", "--base-url STRING", String, "Base URL, useful for local files") do |v|
35
- options[:base_url] = v
36
- end
37
-
38
- opts.on("-q", "--query-string STRING", String, "Query string to append to links") do |v|
39
- options[:link_query_string] = v
40
- end
41
-
42
- opts.on("--css FILE,FILE", Array, "Additional CSS stylesheets") do |v|
43
- options[:css] = v
44
- end
45
-
46
- opts.on("-r", "--remove-classes", "Remove HTML classes") do |v|
47
- options[:remove_classes] = v
48
- end
49
-
50
- opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length].to_s})") do |v|
51
- options[:line_length] = v
52
- end
3
+ # This binary used in rubygems environment only as part of installed gem
53
4
 
54
- opts.on("-d", "--io-exceptions", "Abort on I/O errors") do |v|
55
- options[:io_exceptions] = v
56
- end
57
-
58
- opts.on("-v", "--verbose", "Print additional information at runtime") do |v|
59
- options[:verbose] = v
60
- end
61
-
62
- opts.on_tail("-?", "--help", "Show this message") do
63
- puts opts
64
- exit
65
- end
66
-
67
- opts.on_tail("-V", "--version", "Show version") do
68
- puts "Premailer #{Premailer::VERSION} (c) 2008-2010 Alex Dunae"
69
- exit
70
- end
71
- end
72
- opts.parse!
73
-
74
- $stderr.puts "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
75
-
76
- premailer = nil
77
- input = nil
78
-
79
- if $stdin.tty?
80
- input = ARGV.shift
81
- else
82
- input = $stdin
83
- options[:with_html_string] = true
84
- end
85
-
86
- if input
87
- premailer = Premailer.new(input, options)
88
- else
89
- puts opts
90
- exit 1
91
- end
92
-
93
- if mode == :txt
94
- print premailer.to_plain_text
95
- else
96
- print premailer.to_inline_css
97
- end
5
+ require 'rubygems'
6
+ require 'premailer/executor'
98
7
 
99
- exit
@@ -3,6 +3,8 @@ require 'open-uri'
3
3
  require 'digest/md5'
4
4
  require 'cgi'
5
5
  require 'css_parser'
6
- require File.expand_path(File.dirname(__FILE__) + '/premailer/adapter')
7
- require File.expand_path(File.dirname(__FILE__) + '/premailer/html_to_plain_text')
8
- require File.expand_path(File.dirname(__FILE__) + '/premailer/premailer')
6
+
7
+ require 'premailer/adapter'
8
+ require 'premailer/html_to_plain_text'
9
+ require 'premailer/premailer'
10
+
@@ -29,9 +29,19 @@ class Premailer
29
29
  @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
30
30
  else
31
31
  begin
32
+ if selector =~ Premailer::RE_RESET_SELECTORS
33
+ # this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
34
+ # however, this doesn't mean for testing pur
35
+ @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
36
+ end
37
+
32
38
  # Change single ID CSS selectors into xpath so that we can match more
33
39
  # than one element. Added to work around dodgy generated code.
34
40
  selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
41
+
42
+ # convert attribute selectors to hpricot's format
43
+ selector.gsub!(/\[([\w]+)\]/, '[@\1]')
44
+ selector.gsub!(/\[([\w]+)([\=\~\^\$\*]+)([\w\s]+)\]/, '[@\1\2\'\3\']')
35
45
 
36
46
  doc.search(selector).each do |el|
37
47
  if el.elem? and (el.name != 'head' and el.parent.name != 'head')
@@ -57,7 +67,6 @@ class Premailer
57
67
  rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
58
68
  declarations << rs
59
69
  end
60
-
61
70
  # Perform style folding
62
71
  merged = CssParser.merge(declarations)
63
72
  merged.expand_shorthand!
@@ -70,6 +79,7 @@ class Premailer
70
79
  end
71
80
 
72
81
  merged.create_dimensions_shorthand!
82
+ merged.create_border_shorthand!
73
83
 
74
84
  # write the inline STYLE attribute
75
85
  el['style'] = Premailer.escape_string(merged.declarations_to_s)
@@ -120,18 +130,18 @@ class Premailer
120
130
  #
121
131
  # Returns an Hpricot document.
122
132
  def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
123
- if head = doc.search('head')
124
- styles = ''
125
- unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
126
- styles += "#{selector} { #{declarations} }\n"
127
- end
133
+ styles = ''
134
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
135
+ styles += "#{selector} { #{declarations} }\n"
136
+ end
128
137
 
129
- unless styles.empty?
130
- style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
131
- head.html.empty? ? head.inner_html(style_tag) : head.append(style_tag)
138
+ unless styles.empty?
139
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
140
+ if body = doc.search('body')
141
+ body.append(style_tag)
142
+ else
143
+ doc.inner_html= doc.inner_html << style_tag
132
144
  end
133
- else
134
- $stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
135
145
  end
136
146
  doc
137
147
  end
@@ -52,7 +52,6 @@ class Premailer
52
52
  style = el.attributes['style'].to_s
53
53
 
54
54
  declarations = []
55
-
56
55
  style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
57
56
  rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
58
57
  declarations << rs
@@ -70,6 +69,7 @@ class Premailer
70
69
  end
71
70
 
72
71
  merged.create_dimensions_shorthand!
72
+ merged.create_border_shorthand!
73
73
 
74
74
  # write the inline STYLE attribute
75
75
  el['style'] = Premailer.escape_string(merged.declarations_to_s)
@@ -108,7 +108,8 @@ class Premailer
108
108
 
109
109
  @processed_doc = doc
110
110
  if is_xhtml?
111
- @processed_doc.to_xhtml
111
+ # we don't want to encode carriage returns
112
+ @processed_doc.to_xhtml(:encoding => nil).gsub(/&\#xD;/i, "\r")
112
113
  else
113
114
  @processed_doc.to_html
114
115
  end
@@ -121,20 +122,20 @@ class Premailer
121
122
  #
122
123
  # Returns an Nokogiri document.
123
124
  def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
124
- if head = doc.at('head')
125
- styles = ''
126
- unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
127
- styles += "#{selector} { #{declarations} }\n"
128
- end
129
-
130
- unless styles.empty?
131
- style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
125
+ styles = ''
126
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
127
+ styles += "#{selector} { #{declarations} }\n"
128
+ end
132
129
 
133
- head.add_child(style_tag)
130
+ unless styles.empty?
131
+ style_tag = "<style type=\"text/css\">\n#{styles}></style>"
132
+ if body = doc.search('body')
133
+ doc.at_css('body').children.first.before(style_tag)
134
+ else
135
+ doc.inner_html = style_tag += doc.inner_html
134
136
  end
135
- else
136
- $stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
137
137
  end
138
+
138
139
  doc
139
140
  end
140
141
 
@@ -157,9 +158,9 @@ class Premailer
157
158
  # Returns the original HTML as a string.
158
159
  def to_s
159
160
  if is_xhtml?
160
- @doc.to_xhtml
161
+ @doc.to_xhtml(:encoding => nil)
161
162
  else
162
- @doc.to_html
163
+ @doc.to_html(:encoding => nil)
163
164
  end
164
165
  end
165
166
 
@@ -190,9 +191,9 @@ class Premailer
190
191
  # Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
191
192
  if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
192
193
  thing = thing.force_encoding('ASCII-8BIT').encode!
193
- doc = ::Nokogiri::HTML(thing) {|c| c.noent.recover }
194
+ doc = ::Nokogiri::HTML(thing) {|c| c.recover }
194
195
  else
195
- doc = ::Nokogiri::HTML(thing, nil, 'ASCII-8BIT') {|c| c.noent.recover }
196
+ doc = ::Nokogiri::HTML(thing, nil, @options[:inputencoding] || 'BINARY') {|c| c.recover }
196
197
  end
197
198
 
198
199
  return doc
@@ -0,0 +1,96 @@
1
+ require 'optparse'
2
+ require 'premailer'
3
+
4
+ # defaults
5
+ options = {
6
+ :base_url => nil,
7
+ :link_query_string => nil,
8
+ :remove_classes => false,
9
+ :verbose => false,
10
+ :line_length => 65
11
+ }
12
+
13
+ mode = :html
14
+
15
+ opts = OptionParser.new do |opts|
16
+ opts.banner = "Improve the rendering of HTML emails by making CSS inline among other things. Takes a path to a local file, a URL or a pipe as input.\n\n"
17
+ opts.define_head "Usage: premailer <optional uri|optional path> [options]"
18
+ opts.separator ""
19
+ opts.separator "Examples:"
20
+ opts.separator " premailer http://example.com/ > out.html"
21
+ opts.separator " premailer http://example.com/ --mode txt > out.txt"
22
+ opts.separator " cat input.html | premailer -q src=email > out.html"
23
+ opts.separator " premailer ./public/index.html"
24
+ opts.separator ""
25
+ opts.separator "Options:"
26
+
27
+ opts.on("--mode MODE", [:html, :txt], "Output: html or txt") do |v|
28
+ mode = v
29
+ end
30
+
31
+ opts.on("-b", "--base-url STRING", String, "Base URL, useful for local files") do |v|
32
+ options[:base_url] = v
33
+ end
34
+
35
+ opts.on("-q", "--query-string STRING", String, "Query string to append to links") do |v|
36
+ options[:link_query_string] = v
37
+ end
38
+
39
+ opts.on("--css FILE,FILE", Array, "Additional CSS stylesheets") do |v|
40
+ options[:css] = v
41
+ end
42
+
43
+ opts.on("-r", "--remove-classes", "Remove HTML classes") do |v|
44
+ options[:remove_classes] = v
45
+ end
46
+
47
+ opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length].to_s})") do |v|
48
+ options[:line_length] = v
49
+ end
50
+
51
+ opts.on("-d", "--io-exceptions", "Abort on I/O errors") do |v|
52
+ options[:io_exceptions] = v
53
+ end
54
+
55
+ opts.on("-v", "--verbose", "Print additional information at runtime") do |v|
56
+ options[:verbose] = v
57
+ end
58
+
59
+ opts.on_tail("-?", "--help", "Show this message") do
60
+ puts opts
61
+ exit
62
+ end
63
+
64
+ opts.on_tail("-V", "--version", "Show version") do
65
+ puts "Premailer #{Premailer::VERSION} (c) 2008-2010 Alex Dunae"
66
+ exit
67
+ end
68
+ end
69
+ opts.parse!
70
+
71
+ $stderr.puts "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
72
+
73
+ premailer = nil
74
+ input = nil
75
+
76
+ if $stdin.tty? or STDIN.fcntl(Fcntl::F_GETFL, 0) == 0
77
+ input = ARGV.shift
78
+ else
79
+ input = $stdin
80
+ options[:with_html_string] = true
81
+ end
82
+
83
+ if input
84
+ premailer = Premailer.new(input, options)
85
+ else
86
+ puts opts
87
+ exit 1
88
+ end
89
+
90
+ if mode == :txt
91
+ print premailer.to_plain_text
92
+ else
93
+ print premailer.to_inline_css
94
+ end
95
+
96
+ exit
@@ -13,6 +13,23 @@ module HtmlToPlainText
13
13
  # decode HTML entities
14
14
  he = HTMLEntities.new
15
15
  txt = he.decode(txt)
16
+
17
+ # replace image by their alt attribute
18
+ txt.gsub!(/<img.+?alt=\"([^\"]*)\"[^>]*\/>/i, '\1')
19
+
20
+ # replace image by their alt attribute
21
+ txt.gsub!(/<img.+?alt=\"([^\"]*)\"[^>]*\/>/i, '\1')
22
+ txt.gsub!(/<img.+?alt='([^\']*)\'[^>]*\/>/i, '\1')
23
+
24
+ # links
25
+ txt.gsub!(/<a.+?href=\"([^\"]*)\"[^>]*>(.+?)<\/a>/i) do |s|
26
+ $2.strip + ' ( ' + $1.strip + ' )'
27
+ end
28
+
29
+ txt.gsub!(/<a.+?href='([^\']*)\'[^>]*>(.+?)<\/a>/i) do |s|
30
+ $2.strip + ' ( ' + $1.strip + ' )'
31
+ end
32
+
16
33
 
17
34
  # handle headings (H1-H6)
18
35
  txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
@@ -43,11 +60,6 @@ module HtmlToPlainText
43
60
  # wrap spans
44
61
  txt.gsub!(/(<\/span>)[\s]+(<span)/mi, '\1 \2')
45
62
 
46
- # links
47
- txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s|
48
- $2.strip + ' ( ' + $1.strip + ' )'
49
- end
50
-
51
63
  # lists -- TODO: should handle ordered lists
52
64
  txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, '* ')
53
65
  # list not followed by a newline
@@ -61,7 +73,7 @@ module HtmlToPlainText
61
73
  txt.gsub!(/<\/?[^>]*>/, '')
62
74
 
63
75
  txt = word_wrap(txt, line_length)
64
-
76
+
65
77
  # remove linefeeds (\r\n and \r -> \n)
66
78
  txt.gsub!(/\r\n?/, "\n")
67
79
 
@@ -73,6 +85,14 @@ module HtmlToPlainText
73
85
  # no more than two consecutive newlines
74
86
  txt.gsub!(/[\n]{3,}/, "\n\n")
75
87
 
88
+ # no more than two consecutive spaces
89
+ txt.gsub!(/ {2,}/, " ")
90
+
91
+ # the word messes up the parens
92
+ txt.gsub!(/\([ \n](http[^)]+)[\n ]\)/) do |s|
93
+ "( " + $1 + " )"
94
+ end
95
+
76
96
  txt.strip
77
97
  end
78
98