Repository: alexdunae/premailer
Branch: master
Commit: be30faeeec0a
Files: 41
Total size: 125.5 KB
Directory structure:
gitextract_7t_qp4dq/
├── .gitignore
├── .jrubyrc
├── .travis.yml
├── .yardopts
├── Gemfile
├── LICENSE.md
├── README.md
├── Rakefile
├── bin/
│ └── premailer
├── lib/
│ ├── premailer/
│ │ ├── adapter/
│ │ │ ├── hpricot.rb
│ │ │ ├── nokogiri.rb
│ │ │ └── nokogumbo.rb
│ │ ├── adapter.rb
│ │ ├── executor.rb
│ │ ├── html_to_plain_text.rb
│ │ ├── premailer.rb
│ │ └── version.rb
│ └── premailer.rb
├── misc/
│ └── client_support.yaml
├── premailer.gemspec
└── test/
├── files/
│ ├── base.html
│ ├── chars.html
│ ├── html4.html
│ ├── html_with_uri.html
│ ├── ignore.css
│ ├── ignore.html
│ ├── import.css
│ ├── iso-8859-2.html
│ ├── iso-8859-5.html
│ ├── no_css.html
│ ├── noimport.css
│ ├── styles.css
│ └── xhtml.html
├── future_tests.rb
├── helper.rb
├── test_adapter.rb
├── test_html_to_plain_text.rb
├── test_links.rb
├── test_misc.rb
├── test_premailer.rb
└── test_warnings.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
*.gem
bin/*.html
html/
vendor/
doc/
.yardoc/
*.sw?
pkg/
.bundle/
*.sublime-*
================================================
FILE: .jrubyrc
================================================
cext.enabled=true
================================================
FILE: .travis.yml
================================================
cache: bundler
sudo: false
branches:
only: master
matrix:
fast_finish: true
before_install: rm Gemfile.lock
rvm:
- 2.0.0
- 2.1.0
- 2.2.0
- 2.3.0
================================================
FILE: .yardopts
================================================
--markup markdown
--markup-provider redcarpet
--charset utf-8
--no-private
--readme README.md
--title "Premailer Documentation"
-
README.md
LICENSE.md
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
gem 'css_parser', :git => 'git://github.com/premailer/css_parser.git'
platforms :jruby do
gem 'jruby-openssl'
end
gemspec
================================================
FILE: LICENSE.md
================================================
# Premailer License
Copyright (c) 2007-2012, Alex Dunae. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* 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.
* 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.
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.
================================================
FILE: README.md
================================================
# Premailer README [](https://travis-ci.org/premailer/premailer)
## 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)
## Premailer 2.0 is coming
I'm looking for input on a version 2.0 update to Premailer. Please visit the [Premailer 2.0 Planning Page](https://github.com/premailer/premailer/wiki/New-Premailer-2.0-Planning) and give me your feedback.
## Installation
Install the Premailer gem from RubyGems.
```bash
gem install premailer
```
or add it to your `Gemfile` and run `bundle`.
## Example
```ruby
require 'rubygems' # optional for Ruby 1.9 or above.
require 'premailer'
premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
# Write the HTML output
File.open("output.html", "w") do |fout|
fout.puts premailer.to_inline_css
end
# Write the plain-text output
File.open("output.txt", "w") do |fout|
fout.puts premailer.to_plain_text
end
# Output any CSS warnings
premailer.warnings.each do |w|
puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
end
```
## Ruby Compatibility
Premailer is tested on Ruby 1.8.7, Ruby 1.9.2, Ruby 1.9.3, and Ruby 2.x.0 . It also works on REE. JRuby support is close; contributors are welcome. Checkout the latest build status on the [Travis CI dashboard](https://travis-ci.org/#!/premailer/premailer).
## Premailer-specific CSS
Premailer looks for a few CSS attributes that make working with tables a bit easier.
| CSS Attribute | Availability |
| ------------- | ------------ |
| -premailer-width | Available on `table`, `th` and `td` elements |
| -premailer-height | Available on `table`, `tr`, `th` and `td` elements |
| -premailer-cellpadding | Available on `table` elements |
| -premailer-cellspacing | Available on `table` elements |
| data-premailer="ignore" | Available on `link` and `style` elements. Premailer will ignore these elements entirely. |
Each of these CSS declarations will be copied to appropriate element's attribute.
For example
```css
table { -premailer-cellspacing: 5; -premailer-width: 500; }
```
will result in
```html
```
## Contributions
Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC. Fork and patch to your heart's content. Please don't increment the version numbers, though.
A few areas that are particularly in need of love:
* Improved test coverage
* Move un-repeated background images defined in CSS for Outlook
## Credits and code
Thanks to [all the wonderful contributors](https://github.com/premailer/premailer/contributors) for their updates.
Thanks to [Greenhood + Company](http://www.greenhood.com/) for sponsoring some of the 1.5.6 updates,
and to [Campaign Monitor](https://www.campaignmonitor.com/) for supporting the web interface.
The web interface can be found at [premailer.dialect.ca](http://premailer.dialect.ca).
The source code can be found on [GitHub](https://github.com/alexdunae/premailer).
Copyright by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-2012. See [LICENSE.md](https://github.com/alexdunae/premailer/blob/master/LICENSE.md) for license details.
================================================
FILE: Rakefile
================================================
require 'bundler/setup'
require 'rake/testtask'
require "bundler/gem_tasks"
require 'yard'
GEM_ROOT = File.dirname(__FILE__).freeze unless defined?(GEM_ROOT)
lib_path = File.expand_path('lib', GEM_ROOT)
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include? lib_path
require 'premailer/version'
desc 'Parse a URL and write out the output.'
task :inline do
require 'premailer'
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, :verbose => true, :adapter => :nokogiri)
File.open(output, "w") do |fout|
fout.puts premailer.to_inline_css
end
puts "Succesfully parsed '#{url}' into '#{output}'"
puts premailer.warnings.length.to_s + ' CSS warnings were found'
end
task :text do
require 'premailer'
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)
File.open(output, "w") do |fout|
fout.puts premailer.to_plain_text
end
puts "Succesfully parsed '#{url}' into '#{output}'"
end
Rake::TestTask.new do |t|
t.test_files = FileList['test/test_*.rb']
t.verbose = false
t.warning = false
end
YARD::Rake::YardocTask.new do |yard|
yard.options << "--title='Premailer #{Premailer::VERSION} Documentation'"
end
task :default => [:test]
================================================
FILE: bin/premailer
================================================
#!/usr/bin/env ruby
# This binary used in rubygems environment only as part of installed gem
require 'premailer/executor'
================================================
FILE: lib/premailer/adapter/hpricot.rb
================================================
require 'hpricot'
class Premailer
module Adapter
# Hpricot adapter
module Hpricot
# Merge CSS into the HTML document.
# @return [String] HTML.
def to_inline_css
doc = @processed_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, media_types|
# Save un-mergable rules separately
selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
# Convert element names to lower case
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
else
begin
if selector =~ Premailer::RE_RESET_SELECTORS
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
# however, this doesn't mean for testing pur
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
end
# Change single ID CSS selectors into xpath so that we can match more
# than one element. Added to work around dodgy generated code.
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
# convert attribute selectors to hpricot's format
selector.gsub!(/\[([\w]+)\]/, '[@\1]')
selector.gsub!(/\[([\w]+)([\=\~\^\$\*]+)([\w\s]+)\]/, '[@\1\2\'\3\']')
doc.search(selector).each do |el|
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
# Add a style attribute or append to the existing one
block = "[SPEC=#{specificity}[#{declaration}]]"
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
end
end
rescue ::Hpricot::Error, RuntimeError, ArgumentError
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
next
end
end
end
# Remove script tags
if @options[:remove_scripts]
doc.search("script").remove
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 = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
declarations << rs
end
# Perform style folding
merged = CssParser.merge(declarations)
merged.expand_shorthand!
merged.create_shorthand! if @options[:create_shorthands]
# Duplicate CSS attributes as HTML attributes
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
el[html_att] = merged[css_att].gsub(/url\('(.*)'\)/,'\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
# write the inline STYLE attribute
el['style'] = Premailer.escape_string(merged.declarations_to_s)
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.search('*').each do |el|
if el.comment? and @options[:remove_comments]
lst = el.parent.children
el.parent = nil
lst.delete(el)
elsif el.elem?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
@processed_doc = doc
@processed_doc.to_original_html
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the body.
#
# doc is an Hpricot document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Hpricot] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
style_tag = "\n\n"
if head = doc.search('head')
head.append(style_tag)
elsif body = doc.search('body')
body.append(style_tag)
else
doc.inner_html= doc.inner_html << style_tag
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] Plain text.
def to_plain_text
html_src = ''
begin
html_src = @doc.search("body").inner_html
rescue; end
html_src = @doc.to_html unless html_src and not html_src.empty?
convert_to_text(html_src, @options[:line_length], @html_encoding)
end
# Gets the original HTML as a string.
# @return [String] HTML.
def to_s
@doc.to_original_html
end
# Load the HTML file and convert it into an Hpricot document.
#
# @return [::Hpricot] a document.
def load_html(input) # :nodoc:
thing = nil
# TODO: duplicate options
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
thing = input
elsif @is_local_file
@base_dir = File.dirname(input)
thing = File.open(input, 'r')
else
thing = open(input)
end
# TODO: deal with Hpricot seg faults on empty input
thing ? Hpricot(thing) : nil
end
end
end
end
================================================
FILE: lib/premailer/adapter/nokogiri.rb
================================================
require 'nokogiri'
class Premailer
module Adapter
# Nokogiri adapter
module Nokogiri
# Merge CSS into the HTML document.
#
# @return [String] an HTML.
def to_inline_css
doc = @processed_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, media_types|
# Save un-mergable rules separately
selector.gsub!(/:link([\s]*)+/i) { |m| $1 }
# Convert element names to lower case
selector.gsub!(/([\s]|^)([\w]+)/) { |m| $1.to_s + $2.to_s.downcase }
if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
else
begin
if selector =~ Premailer::RE_RESET_SELECTORS
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
# however, this doesn't mean for testing pur
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
end
# Change single ID CSS selectors into xpath so that we can match more
# than one element. Added to work around dodgy generated code.
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
doc.search(selector).each do |el|
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
# Add a style attribute or append to the existing one
block = "[SPEC=#{specificity}[#{declaration}]]"
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
end
end
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
next
end
end
end
# Remove script tags
if @options[:remove_scripts]
doc.search("script").remove
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 = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
declarations << rs
end
# Perform style folding
merged = CssParser.merge(declarations)
merged.expand_shorthand!
# Duplicate CSS attributes as HTML attributes
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
el[html_att] = merged[css_att].gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
# Collapse multiple rules into one as much as possible.
merged.create_shorthand! if @options[:create_shorthands]
# write the inline STYLE attribute
# split by ';' but ignore those in brackets
attributes = Premailer.escape_string(merged.declarations_to_s).split(/;(?![^(]*\))/).map(&:strip)
attributes = attributes.map { |attr| [attr.split(':').first, attr] }.sort_by { |pair| pair.first }.map { |pair| pair[1] }
el['style'] = attributes.join('; ') + ";"
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.traverse do |el|
if el.comment? and @options[:remove_comments]
el.remove
elsif el.element?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
@processed_doc = doc
if is_xhtml?
# we don't want to encode carriage returns
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
else
@processed_doc.to_html(:encoding => @options[:output_encoding])
end
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the body.
#
# doc is an Nokogiri document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Nokogiri::XML] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
style_tag = ""
unless (body = doc.search('body')).empty?
if doc.at_css('body').children && !doc.at_css('body').children.empty?
doc.at_css('body').children.before(::Nokogiri::XML.fragment(style_tag))
else
doc.at_css('body').add_child(::Nokogiri::XML.fragment(style_tag))
end
else
doc.inner_html = style_tag += doc.inner_html
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] a plain text.
def to_plain_text
html_src = ''
begin
html_src = @doc.at("body").inner_html
rescue;
end
html_src = @doc.to_html unless html_src and not html_src.empty?
convert_to_text(html_src, @options[:line_length], @html_encoding)
end
# Gets the original HTML as a string.
# @return [String] HTML.
def to_s
if is_xhtml?
@doc.to_xhtml(:encoding => nil)
else
@doc.to_html(:encoding => nil)
end
end
# Load the HTML file and convert it into an Nokogiri document.
#
# @return [::Nokogiri::XML] a document.
def load_html(input) # :nodoc:
thing = nil
# TODO: duplicate options
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
thing = input
elsif @is_local_file
@base_dir = File.dirname(input)
thing = File.open(input, 'r')
else
thing = open(input)
end
if thing.respond_to?(:read)
thing = thing.read
end
return nil unless thing
doc = nil
# Handle HTML entities
if @options[:replace_html_entities] == true and thing.is_a?(String)
HTML_ENTITIES.map do |entity, replacement|
thing.gsub! entity, replacement
end
end
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
# However, we really don't want to hardcode this. ASCII-8BIT should be the default, but not the only option.
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
thing = thing.force_encoding(@options[:input_encoding]).encode!
doc = ::Nokogiri::HTML(thing, nil, @options[:input_encoding]) { |c| c.recover }
else
default_encoding = RUBY_PLATFORM == 'java' ? nil : 'BINARY'
doc = ::Nokogiri::HTML(thing, nil, @options[:input_encoding] || default_encoding) { |c| c.recover }
end
# Fix for removing any CDATA tags from both style and script tags inserted per
# https://github.com/sparklemotion/nokogiri/issues/311 and
# https://github.com/premailer/premailer/issues/199
%w(style script).each do |tag|
doc.search(tag).children.each do |child|
child.swap(child.text()) if child.cdata?
end
end
doc
end
end
end
end
================================================
FILE: lib/premailer/adapter/nokogumbo.rb
================================================
require 'nokogumbo'
class Premailer
module Adapter
# Nokogiri adapter
module Nokogumbo
# Merge CSS into the HTML document.
#
# @return [String] an HTML.
def to_inline_css
doc = @processed_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, media_types|
# Save un-mergable rules separately
selector.gsub!(/:link([\s]*)+/i) { |m| $1 }
# Convert element names to lower case
selector.gsub!(/([\s]|^)([\w]+)/) { |m| $1.to_s + $2.to_s.downcase }
if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
else
begin
if selector =~ Premailer::RE_RESET_SELECTORS
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
# however, this doesn't mean for testing pur
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
end
# Change single ID CSS selectors into xpath so that we can match more
# than one element. Added to work around dodgy generated code.
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
doc.search(selector).each do |el|
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
# Add a style attribute or append to the existing one
block = "[SPEC=#{specificity}[#{declaration}]]"
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
end
end
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
next
end
end
end
# Remove script tags
if @options[:remove_scripts]
doc.search("script").remove
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 = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
declarations << rs
end
# Perform style folding
merged = CssParser.merge(declarations)
merged.expand_shorthand!
# Duplicate CSS attributes as HTML attributes
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
el[html_att] = merged[css_att].gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
# Collapse multiple rules into one as much as possible.
merged.create_shorthand! if @options[:create_shorthands]
# write the inline STYLE attribute
attributes = Premailer.escape_string(merged.declarations_to_s).split(';').map(&:strip)
attributes = attributes.map { |attr| [attr.split(':').first, attr] }.sort_by { |pair| pair.first }.map { |pair| pair[1] }
el['style'] = attributes.join('; ') + ";"
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.traverse do |el|
if el.comment? and @options[:remove_comments]
el.remove
elsif el.element?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
@processed_doc = doc
if is_xhtml?
# we don't want to encode carriage returns
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
else
@processed_doc.to_html(:encoding => @options[:output_encoding])
end
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the body.
#
# doc is an Nokogiri document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Nokogiri::XML] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
style_tag = ""
unless (body = doc.search('body')).empty?
if doc.at_css('body').children && !doc.at_css('body').children.empty?
doc.at_css('body').children.before(::Nokogiri::XML.fragment(style_tag))
else
doc.at_css('body').add_child(::Nokogiri::XML.fragment(style_tag))
end
else
doc.inner_html = style_tag += doc.inner_html
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] a plain text.
def to_plain_text
html_src = ''
begin
html_src = @doc.at("body").inner_html
rescue;
end
html_src = @doc.to_html unless html_src and not html_src.empty?
convert_to_text(html_src, @options[:line_length], @html_encoding)
end
# Gets the original HTML as a string.
# @return [String] HTML.
def to_s
if is_xhtml?
@doc.to_xhtml(:encoding => nil)
else
@doc.to_html(:encoding => nil)
end
end
# Load the HTML file and convert it into an Nokogiri document.
#
# @return [::Nokogiri::XML] a document.
def load_html(input) # :nodoc:
thing = nil
# TODO: duplicate options
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
thing = input
elsif @is_local_file
@base_dir = File.dirname(input)
thing = File.open(input, 'r')
else
thing = open(input)
end
if thing.respond_to?(:read)
thing = thing.read
end
return nil unless thing
doc = nil
# Handle HTML entities
if @options[:replace_html_entities] == true and thing.is_a?(String)
HTML_ENTITIES.map do |entity, replacement|
thing.gsub! entity, replacement
end
end
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
# However, we really don't want to hardcode this. ASCII-8BIT should be the default, but not the only option.
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
thing = thing.force_encoding(@options[:input_encoding]).encode!
doc = ::Nokogiri::HTML5(thing)
else
default_encoding = RUBY_PLATFORM == 'java' ? nil : 'BINARY'
doc = ::Nokogiri::HTML5(thing)
end
# Fix for removing any CDATA tags from both style and script tags inserted per
# https://github.com/sparklemotion/nokogiri/issues/311 and
# https://github.com/premailer/premailer/issues/199
%w(style script).each do |tag|
doc.search(tag).children.each do |child|
child.swap(child.text()) if child.cdata?
end
end
doc
end
end
end
end
================================================
FILE: lib/premailer/adapter.rb
================================================
class Premailer
# Manages the adapter classes. Currently supports:
#
# * nokogiri
# * hpricot
module Adapter
autoload :Hpricot, 'premailer/adapter/hpricot'
autoload :Nokogiri, 'premailer/adapter/nokogiri'
autoload :Nokogumbo, 'premailer/adapter/nokogumbo'
# adapter to required file mapping.
REQUIREMENT_MAP = [
["hpricot", :hpricot],
["nokogiri", :nokogiri],
["nokogumbi", :nokogumbo],
]
# Returns the adapter to use.
def self.use
return @use if @use
self.use = self.default
@use
end
# The default adapter based on what you currently have loaded and
# installed. First checks to see if any adapters are already loaded,
# then checks to see which are installed if none are loaded.
# @raise [RuntimeError] unless suitable adapter found.
def self.default
return :hpricot if defined?(::Hpricot)
return :nokogiri if defined?(::Nokogiri)
return :nokogumbo if defined?(::Nokogumbo)
REQUIREMENT_MAP.each do |(library, adapter)|
begin
require library
return adapter
rescue LoadError
next
end
end
raise RuntimeError.new("No suitable adapter for Premailer was found, please install hpricot or nokogiri")
end
# Sets the adapter to use.
# @raise [ArgumentError] unless the adapter exists.
def self.use=(new_adapter)
@use = find(new_adapter)
end
# Returns an adapter.
# @raise [ArgumentError] unless the adapter exists.
def self.find(adapter)
return adapter if adapter.is_a?(Module)
Premailer::Adapter.const_get("#{adapter.to_s.split('_').map{|s| s.capitalize}.join('')}")
rescue NameError
raise ArgumentError, "Invalid adapter: #{adapter}"
end
end
end
================================================
FILE: lib/premailer/executor.rb
================================================
require 'optparse'
require 'premailer'
# defaults
options = {
:base_url => nil,
:link_query_string => nil,
:remove_classes => false,
:verbose => false,
:line_length => 65
}
mode = :html
opts = OptionParser.new do |opts|
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"
opts.define_head "Usage: premailer [options]"
opts.separator ""
opts.separator "Examples:"
opts.separator " premailer http://example.com/ > out.html"
opts.separator " premailer http://example.com/ --mode txt > out.txt"
opts.separator " cat input.html | premailer -q src=email > out.html"
opts.separator " premailer ./public/index.html"
opts.separator ""
opts.separator "Options:"
opts.on("--mode MODE", [:html, :txt], "Output: html or txt") do |v|
mode = v
end
opts.on("-b", "--base-url STRING", String, "Base URL, useful for local files") do |v|
options[:base_url] = v
end
opts.on("-q", "--query-string STRING", String, "Query string to append to links") do |v|
options[:link_query_string] = v
end
opts.on("--css FILE,FILE", Array, "Additional CSS stylesheets") do |v|
options[:css] = v
end
opts.on("-r", "--remove-classes", "Remove HTML classes") do |v|
options[:remove_classes] = v
end
opts.on("-j", "--remove-scripts", "Remove
content
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :remove_scripts => true, :adapter => adapter)
premailer.to_inline_css
assert_equal 0, premailer.processed_doc.search('script').length
end
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :remove_scripts => false, :adapter => adapter)
premailer.to_inline_css
assert_equal 1, premailer.processed_doc.search('script').length
end
end
def test_strip_important_from_attributes
html = <
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
assert_match 'bgcolor="#FF0000"', premailer.to_inline_css
end
end
def test_scripts_with_nokogiri
html = <
END_HTML
premailer = Premailer.new(html, :with_html_string => true, :remove_scripts => false, :adapter => :nokogiri)
premailer.to_inline_css
assert !premailer.processed_doc.css('script[type="application/ld+json"]').first.children.first.cdata?
end
def test_style_without_data_in_content
html = <