Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Hello, GitHub!

  • Loading branch information...
commit 9c3c44407de4225e276bd12900a9356851f562e3 0 parents
@alexdunae alexdunae authored
49 CHANGELOG.rdoc
@@ -0,0 +1,49 @@
+= Premailer CHANGELOG
+
+== Version 1.5.2
+ * released to GitHub
+ * fixed handling of mailto links
+ * various minor updates
+
+== Version 1.5.1
+ * bugfix (http://code.google.com/p/premailer/issues/detail?id=1 and http://code.google.com/p/premailer/issues/detail?id=2) thanks to Russell Norris
+ * bugfix (http://code.google.com/p/premailer/issues/detail?id=4) thanks to Dave Holmes
+
+== Version 1.5.0
+ * preview release of Ruby gem
+
+== Version 1.4
+ * incremental parsing improvements
+ * respect <tt>@media</tt> rule (http://www.w3.org/TR/CSS21/media.html#at-media-rule)
+ * better quote escaping
+
+== Version 1.3
+ * separate CSS parser into its own library
+ * handle <tt>background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC);</tt>
+ * preserve <tt>:hover</tt> etc... in head styles
+
+== Version 1.2
+ * respect <tt>LINK</tt> media types
+ * better style folding
+ * incremental parsing improvements
+
+== Version 1.1
+ * proper calculation of selector specificity per CSS 2.1 spec
+ * support for <tt>@import</tt>
+ * preliminary support for shorthand CSS properties (<tt>margin</tt>, <tt>padding</tt>)
+ * preliminary separation of CSS parser
+
+== Version 1.0
+ * ported web interface to eRuby
+ * incremental parsing improvements
+
+== Version 0.9
+ * initial proof-of-concept
+ * PHP web version
+
+== TODO: Future
+ * complete shorthand properties support (<tt>border-width</tt>, <tt>font</tt>, <tt>background</tt>)
+ * UTF-8 and other charsets (test page: http://kianga.kcore.de/2004/09/21/utf8_test)
+ * make warnings for <tt>border</tt> match <tt>border-left</tt>, etc...
+ * Integrate CSS validator
+ * Remove unused classes and IDs
42 LICENSE.rdoc
@@ -0,0 +1,42 @@
+= Premailer License
+
+Copyright (c) 2007-09 Alex Dunae
+
+Premailer is copyrighted free software by Alex Dunae (http://dunae.ca/).
+You can redistribute it and/or modify it under the conditions below:
+
+ 1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+ 2. You may modify your copy of the software in any way, provided that
+ you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise
+ make them Freely Available, such as by posting said
+ modifications to the internet or an equivalent medium, or by
+ allowing the author to include your modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) rename any non-standard executables so the names do not conflict
+ with standard executables, which must also be provided.
+
+ d) make other distribution arrangements with the author.
+
+ 3. You may modify and include the part of the software into any other
+ software (possibly commercial) as long as clear acknowledgement and
+ a link back to the original software (http://code.dunae.ca/premailer.web/)
+ is provided.
+
+ 5. The scripts and library files supplied as input to or produced as
+ output from the software do not automatically fall under the
+ copyright of the software, but belong to whomever generated them,
+ and may be sold commercially, and may be aggregated with this
+ software.
+
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE.
66 README.rdoc
@@ -0,0 +1,66 @@
+= Premailer README
+
+=== What is this?
+
+For the best HTML e-mail delivery results, CSS should be inline. This is a
+huge pain and a simple newsletter becomes un-managable very quickly. This
+script is my solution.
+
+* CSS styles are converted to inline style attributes
+ Checks style and link[rel=stylesheet] tags and preserves existing inline attributes
+* Relative paths are converted to absolute paths
+ Checks links in href, src and CSS url('')
+* CSS properties are checked against e-mail client capabilities
+ Based on the Email Standards Project’s guides
+* A plain text version is created
+ Optional
+
+
+=== Installation
+
+Download the Premailer gem from GitHub.
+
+ gem sources -a http://gems.github.com
+ sudo gem install alexdunae-premailer
+
+=== Example
+ premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
+
+ # Write the HTML output
+ fout = File.open("output.html", "w")
+ fout.puts premailer.to_inline_css
+ fout.close
+
+ # Write the plain-text output
+ fout = File.open("ouput.txt", "w")
+ fout.puts premailer.to_plain_text
+ fout.close
+
+ # Output any CSS warnings
+ premailer.warnings.each do |w|
+ puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
+ end
+
+=== Contributions
+
+Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC. Pull and patch to your heart's content.
+
+A few areas that are particularly in need of love:
+* Testing suite
+ There were unit tests but they were so funky that it was better to just strip them out.
+* Test running Premailer on local files
+* Create a binary file for easing command line use, allowing the output to be piped in *nix systems
+* Ruby 1.9 testing
+* Test with Rails
+* Move un-repeated background images defined in CSS to +<td background="">+ for Outlook
+* Correctly parse http://www.webstandards.org/files/acid2/test.html
+
+=== Credits and code
+
+Premailer is written in Ruby.
+
+The web interface can be found at http://premailer.dialect.ca/ .
+
+The source code can be found at http://github.com/alexdunae/premailer .
+
+Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-2009.
1  init.rb
@@ -0,0 +1 @@
+require 'premailer'
9 lib/premailer.rb
@@ -0,0 +1,9 @@
+# Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
+
+require 'yaml'
+require 'open-uri'
+require 'hpricot'
+require 'css_parser'
+
+require File.dirname(__FILE__) + "/premailer/html_to_plain_text"
+require File.dirname(__FILE__) + "/premailer/premailer"
58 lib/premailer/html_to_plain_text.rb
@@ -0,0 +1,58 @@
+require 'text/reform'
+require 'htmlentities'
+
+# Support functions for Premailer
+module HtmlToPlainText
+
+ # Returns the text in UTF-8 format with all HTML tags removed
+ #
+ # TODO:
+ # - add support for DL, OL
+ def convert_to_text(html, line_length, from_charset = 'UTF-8')
+ r = Text::Reform.new(:trim => true,
+ :squeeze => false,
+ :break => Text::Reform.break_wrap)
+
+ txt = html
+
+ he = HTMLEntities.new # decode HTML entities
+
+ txt = he.decode(txt)
+
+ txt.gsub!(/<h([0-9]+)[^>]*>(.*)<\/h[0-9]+>/i) do |s| # handle headings
+ hlevel = $1.to_i
+ htext = $2.gsub(/<\/?[^>]*>/i, '') # remove tags inside headings
+ hlength = (htext.length > line_length ?
+ line_length :
+ htext.length)
+
+ case hlevel
+ when 1 # H1
+ ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength) + "\n"
+ when 2 # H2
+ ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength) + "\n"
+ else # H3-H6 are styled the same
+ htext + "\n" + ('-' * htext.length) + "\n"
+ end
+ end
+
+ txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s| # links
+ $2 + ' [' + $1 + ']'
+ end
+
+ txt.gsub!(/(<li[\s]+[^>]*>|<li>)/i, ' * ') # unordered LIsts
+ txt.gsub!(/<\/p>/i, "\n\n") # paragraphs
+
+ txt.gsub!(/<\/?[^>]*>/, '') # strip remaining tags
+ txt.gsub!(/\A[\s]+|[\s]+\Z|^[ \t]+/m, '') # strip extra spaces
+ txt.gsub!(/[\n]{3,}/m, "\n\n") # tighten line breaks
+
+ txt = r.format(('[' * line_length), txt) # wrap text
+ txt.gsub!(/^[\*][\s]/m, ' * ') # add spaces back to lists
+
+ txt.gsub!(/^\s+$/, "\n") # \r\n and \r -> \n
+ txt.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
+ txt.gsub!(/[\n]{3,}/, "\n")
+ txt
+ end
+end
393 lib/premailer/premailer.rb
@@ -0,0 +1,393 @@
+#!/usr/bin/ruby
+#
+# Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
+#
+# Premailer processes HTML and CSS to improve e-mail deliverability.
+#
+# Premailer's main function is to render all CSS as inline <tt>style</tt>
+# attributes. It also converts relative links to absolute links and checks
+# the 'safety' of CSS properties against a CSS support chart.
+#
+# = Example
+# premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
+#
+# # Write the HTML output
+# fout = File.open("output.html", "w")
+# fout.puts premailer.to_inline_css
+# fout.close
+#
+# # Write the plain-text output
+# fout = File.open("ouput.txt", "w")
+# fout.puts premailer.to_plain_text
+# fout.close
+#
+# # List any CSS warnings
+# puts premailer.warnings.length.to_s + ' warnings found'
+# premailer.warnings.each do |w|
+# puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
+# end
+#
+# premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
+# puts premailer.to_inline_css
+class Premailer
+ include HtmlToPlainText
+ include CssParser
+
+ VERSION = '1.5.2'
+
+ CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
+
+ RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
+
+ # should also exclude :first-letter, etc...
+
+ # URI of the HTML file used
+ attr_reader :html_file
+
+ module Warnings
+ NONE = 0
+ SAFE = 1
+ POOR = 2
+ RISKY = 3
+ end
+ include Warnings
+
+ WARN_LABEL = %w(NONE SAFE POOR RISKY)
+
+ # Create a new Premailer object.
+ #
+ # +path+ is the path to the HTML file to process. Can be either the URL of a
+ # remote file or a local path.
+ #
+ # ==== Options
+ # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
+ # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
+ # [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
+ # [+base_url+] Used to calculate absolute URLs for local files.
+ def initialize(path, options = {})
+ @options = {:warn_level => Warnings::SAFE,
+ :line_length => 65,
+ :link_query_string => nil,
+ :base_url => nil,
+ :remove_classes => false}.merge(options)
+ @html_file = path
+
+ @is_local_file = true
+ if path =~ /^(http|https|ftp)\:\/\//i
+ @is_local_file = false
+ end
+
+ @css_warnings = []
+
+ @css_parser = CssParser::Parser.new({:absolute_paths => true,
+ :import => true,
+ :io_exceptions => false
+ })
+
+ @doc, @html_charset = load_html(@html_file)
+
+ if @is_local_file and @options[:base_url]
+ @doc = convert_inline_links(@doc, @options[:base_url])
+ elsif not @is_local_file
+ @doc = convert_inline_links(@doc, @html_file)
+ end
+ load_css_from_html!
+ end
+
+ # Array containing a hash of CSS warnings.
+ def warnings
+ return [] if @options[:warn_level] == Warnings::NONE
+ @css_warnings = check_client_support if @css_warnings.empty?
+ @css_warnings
+ end
+
+ # Returns the original HTML as a string.
+ def to_s
+ @doc.to_html
+ end
+
+ # Converts the HTML document to a format suitable for plain-text e-mail.
+ #
+ # Returns a string.
+ def to_plain_text
+ html_src = ''
+ begin
+ html_src = @doc.search("body").innerHTML
+ rescue
+ html_src = @doc.to_html
+ end
+ convert_to_text(html_src, @options[:line_length], @html_charset)
+ end
+
+ # Merge CSS into the HTML document.
+ #
+ # Returns a string.
+ def to_inline_css
+ doc = @doc
+ unmergable_rules = CssParser::Parser.new
+
+ # Give all styles already in style attributes a specificity of 1000
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
+ doc.search("*[@style]").each do |el|
+ el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
+ end
+
+ # Iterate through the rules and merge them into the HTML
+ @css_parser.each_selector(:all) do |selector, declaration, specificity|
+ # Save un-mergable rules separately
+ selector.gsub!(/:link([\s]|$)+/i, '')
+
+ # Convert element names to lower case
+ selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
+
+ if selector =~ RE_UNMERGABLE_SELECTORS
+ unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
+ else
+
+ doc.search(selector) do |el|
+ if el.elem?
+ # Add a style attribute or append to the existing one
+ block = "[SPEC=#{specificity}[#{declaration}]]"
+ el['style'] = (el.attributes['style'] ||= '') + ' ' + block
+ end
+ end
+ end
+ end
+
+ # Read <style> attributes and perform folding
+ doc.search("*[@style]").each do |el|
+ style = el.attributes['style'].to_s
+
+ declarations = []
+
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
+ rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
+ declarations << rs
+ end
+
+ # Perform style folding and save
+ merged = CssParser.merge(declarations)
+
+ el['style'] = Premailer.escape_string(merged.declarations_to_s)
+ end
+
+ doc = write_unmergable_css_rules(doc, unmergable_rules)
+
+ doc.search('*').remove_class if @options[:remove_classes]
+
+ doc.to_html
+ end
+
+
+protected
+ # Load the HTML file and convert it into an Hpricot document.
+ #
+ # Returns an Hpricot document and a string with the HTML file's character set.
+ def load_html(path) # :nodoc:
+ if @is_local_file
+ Hpricot(File.open(path, "r") {|f| f.read })
+ else
+ Hpricot(open(path))
+ end
+ end
+
+ # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
+ def load_css_from_html! # :nodoc:
+ if tags = @doc.search("link[@rel='stylesheet'], style")
+ tags.each do |tag|
+
+ if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
+
+ link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
+ if @is_local_file
+ css_block = ''
+ begin
+ File.open(link_uri, "r") do |file|
+ while line = file.gets
+ css_block << line
+ end
+ end
+ @css_parser.add_block!(css_block, {:base_uri => @html_file})
+ rescue; end
+ else
+ @css_parser.load_uri!(link_uri)
+ end
+
+ elsif tag.to_s.strip =~ /^\<style/i
+ @css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
+ end
+ end
+ tags.remove
+ end
+ end
+
+ def media_type_ok?(media_types) # :nodoc:
+ return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
+ rescue
+ return true
+ end
+
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
+ # and write it into the <tt>body</tt>.
+ #
+ # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
+ #
+ # Returns an Hpricot document.
+ def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
+ styles = ''
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
+ styles += "#{selector} { #{declarations} }\n"
+ end
+
+ unless styles.empty?
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
+ doc.search("head").append(style_tag)
+ end
+ doc
+ end
+
+ # Convert relative links to absolute links.
+ #
+ # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
+ # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
+ #
+ # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
+ #
+ # Returns an Hpricot document.
+ def convert_inline_links(doc, base_uri) # :nodoc:
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
+
+ append_qs = @options[:link_query_string] ||= ''
+
+ ['href', 'src', 'background'].each do |attribute|
+ tags = doc.search("*[@#{attribute}]")
+
+ next if tags.empty?
+
+ tags.each do |tag|
+
+ # skip links that look like they have merge tags
+ # and mailto, ftp, etc...
+ if tag.attributes[attribute] =~ /^(\{|\[|<|\#|mailto:|ftp:|gopher:)/i
+ next
+ end
+
+ if tag.attributes[attribute] =~ /^http/i
+ begin
+ merged = URI.parse(tag.attributes[attribute])
+ rescue; next; end
+ else
+ begin
+ merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
+ rescue
+ begin
+ merged = Premailer.resolve_link(URI.escape(tag.attributes[attribute].to_s), base_uri)
+ rescue; end
+ end
+ end
+
+ # make sure 'merged' is a URI
+ merged = URI.parse(merged.to_s) unless merged.kind_of?(URI)
+
+ # only append a querystring to <a> tags
+ if tag.name =~ /^a$/i and not append_qs.empty?
+ if merged.query
+ merged.query = merged.query + '&' + append_qs
+ else
+ merged.query = append_qs
+ end
+ end
+ tag[attribute] = merged.to_s
+
+ end # end of each tag
+ end # end of each attrs
+
+ doc.search("*[@style]").each do |el|
+ el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
+ end
+ doc
+ end
+
+ def self.escape_string(str) # :nodoc:
+ str.gsub(/"/, "'")
+ end
+
+ def self.resolve_link(path, base_path) # :nodoc:
+ resolved = nil
+ if base_path.kind_of?(URI)
+ resolved = base_path.merge(path)
+ return Premailer.canonicalize(resolved)
+ elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
+ resolved = URI.parse(base_path)
+ resolved = resolved.merge(path)
+ return Premailer.canonicalize(resolved)
+ else
+
+ return File.expand_path(path, File.dirname(base_path))
+ end
+ end
+
+ # from http://www.ruby-forum.com/topic/140101
+ def self.canonicalize(uri) # :nodoc:
+ u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
+ u.normalize!
+ newpath = u.path
+ while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
+ $1 == '..' ? match : ''
+ } do end
+ newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
+ u.path = newpath
+ u.to_s
+ end
+
+ # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
+ def check_client_support # :nodoc:
+ @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
+
+ warnings = []
+ properties = []
+
+ # Get a list off CSS properties
+ @doc.search("*[@style]").each do |el|
+ style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
+ properties.push($1)
+ end
+ end
+
+ properties.uniq!
+
+ property_support = @client_support['css_properties']
+ properties.each do |prop|
+ if property_support.include?(prop) and
+ property_support[prop].include?('support') and
+ property_support[prop]['support'] >= @options[:warn_level]
+ warnings.push({:message => "#{prop} CSS property",
+ :level => WARN_LABEL[property_support[prop]['support']],
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
+ end
+ end
+
+ @client_support['attributes'].each do |attribute, data|
+ next unless data['support'] >= @options[:warn_level]
+ if @doc.search("*[@#{attribute}]").length > 0
+ warnings.push({:message => "#{attribute} HTML attribute",
+ :level => WARN_LABEL[property_support[prop]['support']],
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
+ end
+ end
+
+ @client_support['elements'].each do |element, data|
+ next unless data['support'] >= @options[:warn_level]
+ if @doc.search("element").length > 0
+ warnings.push({:message => "#{element} HTML element",
+ :level => WARN_LABEL[property_support[prop]['support']],
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
+ end
+ end
+
+ return warnings
+ end
+end
+
+
+
230 misc/client_support.yaml
@@ -0,0 +1,230 @@
+# Capabilities of e-mail clients
+#
+# Sources
+# * http://campaignmonitor.com/css/
+# * http://www.campaignmonitor.com/blog/archives/2007/04/a_guide_to_css_support_in_emai_2.html
+# * http://www.campaignmonitor.com/blog/archives/2007/11/do_image_maps_work_in_html_ema.html
+# * http://www.campaignmonitor.com/blog/archives/2007/11/how_forms_perform_in_html_emai.html
+# * http://www.xavierfrenette.com/articles/css-support-in-webmail/
+# * http://www.email-standards.org/
+# Updated 2008-08-26
+#
+# Support: 1 = SAFE, 2 = POOR, 3 = RISKY
+elements:
+ map:
+ support: 2
+ unsupported_in: [GMail]
+ area:
+ support: 2
+ unsupported_in: [GMail]
+ form:
+ support: 3
+ unsupported_in: [Mobile Me, Old Yahoo, AOL, Live Mail, Outlook 07, Outlook 03]
+ link:
+ support: 2
+ unsupported_in: [GMail, Hotmail, Old Yahoo]
+attributes:
+ ismap:
+ support: 2
+ unsupported_in: [GMail]
+css_properties:
+ color:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ font-size:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ font-style:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ font-weight:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ text-align:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ text-decoration:
+ unsupported_in: [Eudora]
+ support_level: 92%
+ support: 1
+ background-color:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ border: &border_shorthand
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ border-bottom: *border_shorthand
+ border-left: *border_shorthand
+ border-right: *border_shorthand
+ border-top: *border_shorthand
+ display:
+ unsupported_in: [Outlook 07, Eudora]
+ support_level: 85%
+ support: 2
+ font-family:
+ unsupported_in: [Eudora, Old GMail, New GMail]
+ support_level: 92%
+ support: 2
+ font-variant:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ letter-spacing:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ line-height:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ padding: &padding_shorthand
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ padding-bottom: *padding_shorthand
+ padding-left: *padding_shorthand
+ padding-right: *padding_shorthand
+ padding-top: *padding_shorthand
+ table-layout:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ text-indent:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ text-transform:
+ unsupported_in: [Notes 6, Eudora]
+ support_level: 85%
+ support: 2
+ border-collapse:
+ unsupported_in: [Entourage 2004, Notes 6, Eudora]
+ support_level: 77%
+ support: 3
+ clear:
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
+ support_level: 77%
+ support: 3
+ direction:
+ unsupported_in: [Outlook 07, Entourage 2004, Eudora, New GMail]
+ support_level: 77%
+ support: 3
+ float:
+ unsupported_in: [Outlook 07, Eudora, Old GMail]
+ support_level: 85%
+ support: 3
+ vertical-align:
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
+ support_level: 77%
+ support: 3
+ width:
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
+ support_level: 77%
+ support: 3
+ word-spacing:
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
+ support_level: 77%
+ support: 3
+ height:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail]
+ support_level: 77%
+ support: 3
+ list-style-type:
+ unsupported_in: [Outlook 07, Eudora, Hotmail]
+ support_level: 85%
+ support: 3
+ overflow:
+ unsupported_in: [Outlook 07, Entourage 2004, Notes 6, Eudora]
+ support_level: 69%
+ support: 3
+ visibility:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, aolWeb]
+ support_level: 77%
+ support: 3
+ white-space:
+ unsupported_in: [Outlook 03, Windows Mail, AOL 9, AOL 10, Notes 6, Eudora, Mobile Me]
+ support_level: 54%
+ support: 3
+ background-image:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ background-repeat:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ clip:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, New GMail, Live Mail, Mobile Me]
+ support_level: 77%
+ support: 3
+ cursor:
+ unsupported_in: [Outlook 07, Entourage 2004, Notes 6, Eudora, Old GMail, New GMail]
+ support_level: 69%
+ support: 3
+ list-style-image:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ list-style-position:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, Hotmail]
+ support_level: 77%
+ support: 3
+ margin: &margin_shorthand
+ unsupported_in: [AOL 9, Notes 6, Eudora, Live Mail, Hotmail]
+ support_level: 77%
+ support: 3
+ margin-bottom: *margin_shorthand
+ margin-left: *margin_shorthand
+ margin-right: *margin_shorthand
+ margin-top: *margin_shorthand
+ z-index:
+ unsupported_in: [Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
+ support_level: 85%
+ support: 3
+ left:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ right:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ top:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
+ support_level: 77%
+ support: 3
+ background-position:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, Old GMail, New GMail, Live Mail, Hotmail]
+ support_level: 77%
+ support: 3
+ border-spacing:
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, AOL 10, Notes 6, Eudora, Live Mail, Hotmail]
+ support_level: 46%
+ support: 3
+ bottom:
+ unsupported_in: [Outlook 07, AOL 9, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
+ support_level: 69%
+ support: 3
+ empty-cells:
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, AOL 9, AOL 10, Notes 6, Eudora, Hotmail]
+ support_level: 38%
+ support: 3
+ position:
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, New Yahoo, Old GMail, New GMail, Live Mail, Hotmail, Mobile Me]
+ support_level: 77%
+ support: 3
+ caption-side:
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Mac Mail, Entourage 2004, Entourage 2008, AOL 9, AOL 10, AOL Desktop for Mac, Notes 6, Eudora, New Yahoo, Hotmail]
+ support_level: 15%
+ support: 3
+ opacity:
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail, Hotmail]
+ support_level: 54%
+ support: 3
16 premailer.gemspec
@@ -0,0 +1,16 @@
+Gem::Specification.new do |s|
+ s.name = "premailer"
+ s.version = "1.5.2"
+ s.date = "2009-11-27"
+ s.summary = "Preflight for HTML e-mail."
+ s.email = "code@dunae.ca"
+ s.homepage = "http://premailer.dialect.ca/"
+ s.description = "Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code."
+ s.has_rdoc = true
+ s.author = "Alex Dunae"
+ s.rdoc_options << '--all' << '--inline-source' << '--line-numbers' << '--charset' << 'utf-8'
+ s.files = ['README.rdoc', 'CHANGELOG.rdoc', 'LICENSE.rdoc', 'lib/premailer.rb', 'lib/premailer/premailer.rb', 'lib/premailer/html_to_plain_text.rb', 'misc/client_support.yaml']
+ s.add_dependency('hpricot', '>= 0.6')
+ s.add_dependency('css_parser', '>= 0.9.0')
+ s.add_dependency('text-reform', '>= 0.2.0')
+end
58 rakefile.rb
@@ -0,0 +1,58 @@
+require 'rake'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+require 'fileutils'
+require 'lib/premailer'
+
+desc 'Default: parse a URL.'
+task :default => [:inline]
+
+desc 'Parse a URL and write out the output.'
+task :inline do
+ url = ENV['url']
+ output = ENV['output']
+
+ if !url or url.empty? or !output or output.empty?
+ puts 'Usage: rake inline url=http://example.com/ output=output.html'
+ exit
+ end
+
+ premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
+ fout = File.open(output, "w")
+ fout.puts premailer.to_inline_css
+ fout.close
+
+ puts "Succesfully parsed '#{url}' into '#{output}'"
+ puts premailer.warnings.length.to_s + ' CSS warnings were found'
+end
+
+task :text do
+ url = ENV['url']
+ output = ENV['output']
+
+ if !url or url.empty? or !output or output.empty?
+ puts 'Usage: rake text url=http://example.com/ output=output.txt'
+ exit
+ end
+
+ premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
+ fout = File.open(output, "w")
+ fout.puts premailer.to_plain_text
+ fout.close
+
+ puts "Succesfully parsed '#{url}' into '#{output}'"
+end
+
+desc 'Generate documentation for Premailer.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = 'Premailer'
+ rdoc.options << '--all'
+ rdoc.options << '--inline-source'
+ rdoc.options << '--line-numbers'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('LICENSE.rdoc')
+ rdoc.rdoc_files.include('CHANGELOG.rdoc')
+ rdoc.rdoc_files.include('lib/*.rb')
+end
+
Please sign in to comment.
Something went wrong with that request. Please try again.