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
<table cellspacing='5' width='500'>
```
## 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 <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.
#
# @return [::Hpricot] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\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 <body> 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 <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 Nokogiri document and <tt>unmergable_css_rules</tt> 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 = "<style type=\"text/css\">\n#{styles}</style>"
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 <body> 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 <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 Nokogiri document and <tt>unmergable_css_rules</tt> 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 = "<style type=\"text/css\">\n#{styles}</style>"
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 <body> 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 <optional uri|optional path> [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 <script> elements") do |v|
options[:remove_classes] = v
end
opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length].to_s})") do |v|
options[:line_length] = v
end
opts.on("-e", "--entities", "Output HTML entities instead of UTF-8 when using Nokogiri") do |v|
options[:output_encoding] = "US-ASCII"
end
opts.on("-d", "--io-exceptions", "Abort on I/O errors") do |v|
options[:io_exceptions] = v
end
opts.on("-v", "--verbose", "Print additional information at runtime") do |v|
options[:verbose] = v
end
opts.on_tail("-?", "--help", "Show this message") do
puts opts
exit
end
opts.on_tail("-V", "--version", "Show version") do
puts "Premailer #{Premailer::VERSION} (c) 2008-2010 Alex Dunae"
exit
end
end
opts.parse!
$stderr.puts "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
premailer = nil
input = nil
if ARGV.size > 0
# Executed via command line or shell script
input = ARGV.shift
else
# Called in piped command
input = $stdin.read
options[:with_html_string] = true
end
if input
premailer = Premailer.new(input, options)
else
puts opts
exit 1
end
if mode == :txt
print premailer.to_plain_text
else
print premailer.to_inline_css
end
exit
================================================
FILE: lib/premailer/html_to_plain_text.rb
================================================
# coding: utf-8
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 = 65, from_charset = 'UTF-8')
txt = html
# strip text ignored html. Useful for removing
# headers and footers that aren't needed in the
# text version
txt.gsub!(/<!-- start text\/html -->.*?<!-- end text\/html -->/m, '')
# replace images with their alt attributes
# for img tags with "" for attribute quotes
# with or without closing tag
# eg. the following formats:
# <img alt="" />
# <img alt="">
txt.gsub!(/<img.+?alt=\"([^\"]*)\"[^>]*\>/i, '\1')
# for img tags with '' for attribute quotes
# with or without closing tag
# eg. the following formats:
# <img alt='' />
# <img alt=''>
txt.gsub!(/<img.+?alt=\'([^\']*)\'[^>]*\>/i, '\1')
# links
txt.gsub!(/<a\s.*?href=["'](mailto:)?([^"']*)["'][^>]*>((.|\s)*?)<\/a>/i) do |s|
if $3.empty?
''
else
$3.strip + ' ( ' + $2.strip + ' )'
end
end
# handle headings (H1-H6)
txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |s|
hlevel = $1.to_i
htext = $2
htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
htext.gsub!(/<\/?[^>]*>/i, '') # strip tags
# determine maximum line length
hlength = 0
htext.each_line { |l| llength = l.strip.length; hlength = llength if llength > hlength }
hlength = line_length if hlength > line_length
case hlevel
when 1 # H1, asterisks above and below
htext = ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
when 2 # H1, dashes above and below
htext = ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
else # H3-H6, dashes below
htext = htext + "\n" + ('-' * hlength)
end
"\n\n" + htext + "\n\n"
end
# wrap spans
txt.gsub!(/(<\/span>)[\s]+(<span)/mi, '\1 \2')
# lists -- TODO: should handle ordered lists
txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, '* ')
# list not followed by a newline
txt.gsub!(/<\/li>[\s]*(?![\n])/i, "\n")
# paragraphs and line breaks
txt.gsub!(/<\/p>/i, "\n\n")
txt.gsub!(/<br[\/ ]*>/i, "\n")
# strip remaining tags
txt.gsub!(/<\/?[^>]*>/, '')
# decode HTML entities
he = HTMLEntities.new
txt = he.decode(txt)
# no more than two consecutive spaces
txt.gsub!(/ {2,}/, " ")
txt = word_wrap(txt, line_length)
# remove linefeeds (\r\n and \r -> \n)
txt.gsub!(/\r\n?/, "\n")
# strip extra spaces
txt.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
txt.gsub!(/\n[ \t]+/, "\n") # space at start of lines
txt.gsub!(/[ \t]+\n/, "\n") # space at end of lines
# no more than two consecutive newlines
txt.gsub!(/[\n]{3,}/, "\n\n")
# the word messes up the parens
txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |s|
($1 == "\n" ? $1 : '' ) + '( ' + $2 + ' )' + ($3 == "\n" ? $1 : '' )
end
txt.strip
end
# Taken from Rails' word_wrap helper (http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap)
def word_wrap(txt, line_length)
txt.split("\n").collect do |line|
line.length > line_length ? line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip : line
end * "\n"
end
end
================================================
FILE: lib/premailer/premailer.rb
================================================
# 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 of use
#
# ```ruby
# 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
# ```
#
require 'premailer/version'
class Premailer
include HtmlToPlainText
include CssParser
CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
# Unmergable selectors regexp.
RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
# Reset selectors regexp.
RE_RESET_SELECTORS = /^(\:\#outlook|body.*|\.ReadMsgBody|\.ExternalClass|img|\#backgroundTable)$/
# list of HTMLEntities to fix
# source: http://stackoverflow.com/questions/2812781/how-to-convert-webpage-apostrophe-8217-to-ascii-39-in-ruby-1-
HTML_ENTITIES = {
"’" => "'",
"…" => "...",
"‘" => "'",
"‚" => ',',
"‛" => "'",
"“" => '"',
"”" => '"',
"‐" => '-',
"–" => '-',
"—" => '--',
"―" => '--'
}
# list of CSS attributes that can be rendered as HTML attributes
#
# @todo too much repetition
# @todo background=""
RELATED_ATTRIBUTES = {
'h1' => {'text-align' => 'align'},
'h2' => {'text-align' => 'align'},
'h3' => {'text-align' => 'align'},
'h4' => {'text-align' => 'align'},
'h5' => {'text-align' => 'align'},
'h6' => {'text-align' => 'align'},
'p' => {'text-align' => 'align'},
'div' => {'text-align' => 'align'},
'blockquote' => {'text-align' => 'align'},
'body' => {'background-color' => 'bgcolor'},
'table' => {
'-premailer-align' => 'align',
'background-color' => 'bgcolor',
'background-image' => 'background',
'-premailer-width' => 'width',
'-premailer-height' => 'height',
'-premailer-cellpadding' => 'cellpadding',
'-premailer-cellspacing' => 'cellspacing'
},
'tr' => {
'text-align' => 'align',
'background-color' => 'bgcolor',
'-premailer-height' => 'height'
},
'th' => {
'text-align' => 'align',
'background-color' => 'bgcolor',
'vertical-align' => 'valign',
'-premailer-width' => 'width',
'-premailer-height' => 'height'
},
'td' => {
'text-align' => 'align',
'background-color' => 'bgcolor',
'vertical-align' => 'valign',
'-premailer-width' => 'width',
'-premailer-height' => 'height'
},
'img' => {
'float' => 'align',
'-premailer-width' => 'width',
'-premailer-height' => 'height'
}
}
# URI of the HTML file used
attr_reader :html_file
# base URL used to resolve links
attr_reader :base_url
# base directory used to resolve links for local files
# @return [String] base directory
attr_reader :base_dir
# unmergeable CSS rules to be preserved in the head (CssParser)
attr_reader :unmergable_rules
# processed HTML document (Hpricot/Nokogiri)
attr_reader :processed_doc
# source HTML document (Hpricot/Nokogiri)
attr_reader :doc
# Warning levels
module Warnings
# No warnings
NONE = 0
# Safe
SAFE = 1
# Poor
POOR = 2
# Risky
RISKY = 3
end
include Warnings
# Waning level names
WARN_LABEL = %w(NONE SAFE POOR RISKY)
# Create a new Premailer object.
#
# @param html is the HTML data to process. It can be either an IO object, the URL of a
# remote file, a local path or a raw HTML string. If passing an HTML string you
# must set the with_html_string option to true.
#
# @param [Hash] options the options to handle html with.
# @option options [Fixnum] :line_length Line length used by to_plain_text. Default is 65.
# @option options [Fixnum] :warn_level What level of CSS compatibility warnings to show (see {Premailer::Warnings}).
# @option options [String] :link_query_string A string to append to every <tt>a href=""</tt> link. Do not include the initial <tt>?</tt>.
# @option options [String] :base_url Used to calculate absolute URLs for local files.
# @option options [Array(String)] :css Manually specify CSS stylesheets.
# @option options [Boolean] :css_to_attributes Copy related CSS attributes into HTML attributes (e.g. background-color to bgcolor)
# @option options [String] :css_string Pass CSS as a string
# @option options [Boolean] :remove_ids Remove ID attributes whenever possible and convert IDs used as anchors to hashed to avoid collisions in webmail programs. Default is false.
# @option options [Boolean] :remove_classes Remove class attributes. Default is false.
# @option options [Boolean] :remove_comments Remove html comments. Default is false.
# @option options [Boolean] :remove_scripts Remove <tt>script</tt> elements. Default is true.
# @option options [Boolean] :reset_contenteditable Remove <tt>contenteditable</tt> attributes. Default is true.
# @option options [Boolean] :preserve_styles Whether to preserve any <tt>link rel=stylesheet</tt> and <tt>style</tt> elements. Default is false.
# @option options [Boolean] :preserve_reset Whether to preserve styles associated with the MailChimp reset code. Default is true.
# @option options [Boolean] :with_html_string Whether the html param should be treated as a raw string. Default is false.
# @option options [Boolean] :verbose Whether to print errors and warnings to <tt>$stderr</tt>. Default is false.
# @option options [Boolean] :include_link_tags Whether to include css from <tt>link rel=stylesheet</tt> tags. Default is true.
# @option options [Boolean] :include_style_tags Whether to include css from <tt>style</tt> tags. Default is true.
# @option options [String] :input_encoding Manually specify the source documents encoding. This is a good idea. Default is ASCII-8BIT.
# @option options [Boolean] :replace_html_entities Convert HTML entities to actual characters. Default is false.
# @option options [Boolean] :escape_url_attributes URL Escapes href, src, and background attributes on elements. Default is true.
# @option options [Symbol] :adapter Which HTML parser to use, either <tt>:nokogiri</tt> or <tt>:hpricot</tt>. Default is <tt>:hpricot</tt>.
# @option options [String] :output_encoding Output encoding option for Nokogiri adapter. Should be set to "US-ASCII" to output HTML entities instead of Unicode characters.
# @option options [Boolean] :create_shorthands Combine several properties into a shorthand one, e.g. font: style weight size. Default is true.
def initialize(html, options = {})
@options = {:warn_level => Warnings::SAFE,
:line_length => 65,
:link_query_string => nil,
:base_url => nil,
:remove_classes => false,
:remove_ids => false,
:remove_comments => false,
:remove_scripts => true,
:reset_contenteditable => true,
:css => [],
:css_to_attributes => true,
:with_html_string => false,
:css_string => nil,
:preserve_styles => false,
:preserve_reset => true,
:verbose => false,
:debug => false,
:io_exceptions => false,
:include_link_tags => true,
:include_style_tags => true,
:input_encoding => 'ASCII-8BIT',
:output_encoding => nil,
:replace_html_entities => false,
:escape_url_attributes => true,
:unescaped_ampersand => false,
:create_shorthands => true,
:adapter => Adapter.use,
}.merge(options)
@html_file = html
@is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
@css_files = [@options[:css]].flatten
@css_warnings = []
@base_url = nil
@base_dir = nil
@unmergable_rules = nil
if @options[:base_url]
@base_url = URI.parse(@options.delete(:base_url))
elsif not @is_local_file
@base_url = URI.parse(@html_file)
end
@css_parser = CssParser::Parser.new({
:absolute_paths => true,
:import => true,
:io_exceptions => @options[:io_exceptions]
})
@adapter_class = Adapter.find @options[:adapter]
self.class.send(:include, @adapter_class)
@doc = load_html(@html_file)
@processed_doc = @doc
@processed_doc = convert_inline_links(@processed_doc, @base_url) if @base_url
if options[:link_query_string]
@processed_doc = append_query_string(@processed_doc, options[:link_query_string])
end
load_css_from_options!
load_css_from_html!
end
# CSS warnings.
# @return [Array(Hash)] Array of warnings.
def warnings
return [] if @options[:warn_level] == Warnings::NONE
@css_warnings = check_client_support if @css_warnings.empty?
@css_warnings
end
protected
def load_css_from_local_file!(path)
css_block = ''
path.gsub!(/\Afile:/, '')
begin
File.open(path, "r") do |file|
while line = file.gets
css_block << line
end
end
load_css_from_string(css_block)
rescue; end
end
def load_css_from_string(css_string)
@css_parser.add_block!(css_string, {:base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld]})
end
# @private
def load_css_from_options! # :nodoc:
load_css_from_string(@options[:css_string]) if @options[:css_string]
@css_files.each do |css_file|
if Premailer.local_data?(css_file)
load_css_from_local_file!(css_file)
else
@css_parser.load_uri!(css_file)
end
end
end
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
def load_css_from_html! # :nodoc:
tags = @doc.search("link[@rel='stylesheet']:not([@data-premailer='ignore']), style:not([@data-premailer='ignore'])")
if tags
tags.each do |tag|
if tag.to_s.strip =~ /^\<link/i && tag.attributes['href'] && media_type_ok?(tag.attributes['media']) && @options[:include_link_tags]
# A user might want to <link /> to a local css file that is also mirrored on the site
# but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
if tag.attributes['href'].to_s.include? @base_url.to_s and @html_file.kind_of?(String)
if @options[:with_html_string]
link_uri = tag.attributes['href'].to_s.sub(@base_url.to_s, '')
else
link_uri = File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
# if the file does not exist locally, try to grab the remote reference
unless File.exists?(link_uri)
link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
end
end
else
link_uri = tag.attributes['href'].to_s
end
if Premailer.local_data?(link_uri)
$stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
load_css_from_local_file!(link_uri)
else
$stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
@css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
end
elsif tag.to_s.strip =~ /^\<style/i && @options[:include_style_tags]
@css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
end
end
tags.remove unless @options[:preserve_styles]
end
end
# here be deprecated methods
public
# @private
# @deprecated
def local_uri?(uri) # :nodoc:
warn "[DEPRECATION] `local_uri?` is deprecated. Please use `Premailer.local_data?` instead."
Premailer.local_data?(uri)
end
# here be instance methods
# @private
def media_type_ok?(media_types)
media_types = media_types.to_s
return true if media_types.nil? or media_types.empty?
media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
end
def append_query_string(doc, qs)
return doc if qs.nil?
qs.to_s.gsub!(/^[\?]*/, '').strip!
return doc if qs.empty?
begin
current_host = @base_url.host
rescue
current_host = nil
end
$stderr.puts "Attempting to append_query_string: #{qs}" if @options[:verbose]
doc.search('a').each do|el|
href = el.attributes['href'].to_s.strip
next if href.nil? or href.empty?
next if href[0,1] =~ /[\#\{\[\<\%]/ # don't bother with anchors or special-looking links
begin
href = URI.parse(href)
if current_host and href.host != nil and href.host != current_host
$stderr.puts "Skipping append_query_string for: #{href.to_s} because host is no good" if @options[:verbose]
next
end
if href.scheme and href.scheme != 'http' and href.scheme != 'https'
puts "Skipping append_query_string for: #{href.to_s} because scheme is no good" if @options[:verbose]
next
end
if href.query and not href.query.empty?
amp = @options[:unescaped_ampersand] ? '&' : '&'
href.query = href.query + amp + qs
else
href.query = qs
end
el['href'] = href.to_s
rescue URI::Error => e
$stderr.puts "Skipping append_query_string for: #{href.to_s} (#{e.message})" if @options[:verbose]
next
end
end
doc
end
# Check for an XHTML doctype
def is_xhtml?
intro = @doc.to_html.strip.split("\n")[0..2].join(' ')
is_xhtml = !!(intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
$stderr.puts "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
is_xhtml
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] || ''
escape_attrs = @options[:escape_url_attributes]
['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].to_s =~ /^([\%\<\{\#\[]|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:|cid:)/i
next
end
if tag.attributes[attribute].to_s =~ /^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
next unless escape_attrs
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)
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
# @private
def self.is_media_query?(media_types)
media_types && media_types.any?{|mt| mt.to_s.count('()') >= 2 }
end
# @private
def self.escape_string(str) # :nodoc:
str.gsub(/"/ , "'")
end
# @private
def self.resolve_link(path, base_path) # :nodoc:
path.strip!
resolved = nil
if path =~ /\A(?:(https?|ftp|file):)\/\//i
resolved = path
Premailer.canonicalize(resolved)
elsif base_path.kind_of?(URI)
resolved = base_path.merge(path)
Premailer.canonicalize(resolved)
elsif base_path.kind_of?(String) and base_path =~ /\A(?:(?:https?|ftp|file):)\/\//i
resolved = URI.parse(base_path)
resolved = resolved.merge(path)
Premailer.canonicalize(resolved)
else
File.expand_path(path, File.dirname(base_path))
end
end
# Test the passed variable to see if we are in local or remote mode.
#
# IO objects return true, as do strings that look like URLs.
def self.local_data?(data)
return true if data.is_a?(IO) || data.is_a?(StringIO)
return true if data =~ /\Afile:\/\//i
return false if data =~ /\A(?:(https?|ftp):)\/\//i
true
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 ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
warnings = []
properties = []
# Get a list off CSS properties
@processed_doc.search("*[@style]").each do |el|
style_url = el.attributes['style'].to_s.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[data['support']],
:clients => data['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[data['support']],
:clients => data['unsupported_in'].join(', ')})
end
end
warnings
end
end
================================================
FILE: lib/premailer/version.rb
================================================
class Premailer
# Premailer version.
VERSION = '1.8.6'.freeze
end
================================================
FILE: lib/premailer.rb
================================================
require 'yaml'
require 'open-uri'
require 'digest/md5'
require 'cgi'
require 'css_parser'
require 'premailer/adapter'
require 'premailer/html_to_plain_text'
require 'premailer/premailer'
================================================
FILE: misc/client_support.yaml
================================================
# 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
================================================
FILE: premailer.gemspec
================================================
require './lib/premailer/version'
Gem::Specification.new "premailer", Premailer::VERSION do |s|
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.files = `git ls-files lib misc LICENSE.md README.md`.split("\n")
s.executables = ['premailer']
s.required_ruby_version = '>= 2.0.0'
s.add_dependency('css_parser', '>= 1.3.7')
s.add_dependency('htmlentities', ['>= 4.0.0'])
s.add_development_dependency "bundler", "~> 1.3"
s.add_development_dependency('rake', ['~> 0.8', '!= 0.9.0'])
s.add_development_dependency('hpricot', '>= 0.8.3')
s.add_development_dependency('nokogiri', '>= 1.4.4')
s.add_development_dependency('yard', '~> 0.8.7.6')
s.add_development_dependency('redcarpet', '~> 3.0')
s.add_development_dependency('maxitest')
s.add_development_dependency('coveralls')
s.add_development_dependency('webmock')
s.add_development_dependency('nokogumbo')
end
================================================
FILE: test/files/base.html
================================================
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!--
You can read this newsletter online at
[webversion]
-->
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Premailer Test</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<style type="text/css">
@import "import.css" screen, handheld;
</style>
<style type="text/css">
@import "noimport.css" print;
</style>
<style type="text/css">
#iphone { display: block; }
</style>
</head>
<body>
<div id="wrapper">
<p class="hide" id="hide01">This line should be hidden.</p>
<p class="hide" id="iphone">This is an iPhone style.</p>
<table width="646" class="container" cellspacing="0" cellpadding="0">
<tr><td id="webversion" colspan="6">Having trouble reading this newsletter? <webversion>Click here to see it in your browser</webversion></td></tr>
<tr><td height="13" colspan="6" class="frame"> </td></tr>
<table width="646" class="container" cellspacing="0" cellpadding="0">
<tr>
<td class="frame" width="13"> </td>
<td class="gutter" width="60"> </td>
<td class="content" colspan="2" width="500">
<h1><span>Premailer Test</span></h1>
<table width="500" cellpadding="0" cellspacing="0">
<tr>
<td width="20"> </td>
<td colspan="2" width="460">
<h2>Lorem ipsum dolor</h2>
<h3>Suspendisse id velit vitae ligula volutpat condimentum</h3>
<p class="dt">Morbi commodo, ipsum sed</p>
<p class="unaligned"><img src="2009-placeholder.png" alt="Image" align="right" class="right">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis. Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. <a href="http://premailer.dialect.ca/">Nulla facilisi</a>. Nulla libero.</p>
<p attr="another quote">Here’s a quote. Here’s a quote. “Here’s a quote in quotes”.</p>
<p>Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. Nulla facilisi.</p>
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis. Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. Nulla facilisi. Nulla libero.</p>
<blockquote><p>“Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis.”</p></blockquote>
<p>Aliquam erat volutpat. Sed quis velit. Nulla facilisi. Nulla libero.</p>
<h3>Link tests</h3>
<ul>
<li><a id="l01" href="/">Relative path to root</a></li>
<li><a id="l02" href="http://premailer.dialect.ca/">Absolute path to root</a></li>
<li><a id="l03" href="http://example.com/">Different domain</a></li>
<li><a id="l04" href="images/">Relative path to sub-directory</a></li>
<li><a id="l05" href="#relative">Link is not converted</a></li>
<li><a id="l06" href="http://example.com/test.html?cn=tf&c=20&ord=%%RANDOM%%">Funky ASP URL</a></li>
<li><a id="l07" href="?query=string">Appends tracking query string</a></li>
<li><a id="l08" href="{DONOTCONVERT}">Link is not converted</a></li>
<li><a id="l09" href="[DONOTCONVERT]">Link is not converted</a></li>
<li><a id="l10" href="<DONOTCONVERT>">Link is not converted</a></li>
<li><a id="l11" href="mailto:premailer@example.com">mailto link</a></li>
<li><a id="l12" href="ftp://example.com">FTP link</a></li>
<li><a id="l13" href="gopher://gopher.floodgap.com/1/fun/twitpher">Gopher link</a></li>
</ul>
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis.</p>
<p> </p>
</td>
<td width="20"> </td>
</tr>
</table>
<p class="section"><img src="dots_end.png" alt="---" width="499" height="75"></p>
</td><!-- /#content -->
<td class="gutter" width="60"> </td>
<td class="frame" width="13"> </td>
</tr>
<tr>
<td class="frame" width="13"> </td>
<td colspan="4" width="620">
<table summary="Contact information" cellspacing="0" cellpadding="0" width="620">
<tr><td height="4" class="hairline"> </td></tr>
<tr><td height="34"class="contact"> </td></tr>
<tr>
<td align="center" class="contact" id="contact_info">
<p id="address">Premailer Test<br>
<a href="http://dialect.ca/?utm_source=Premailer&utm_medium=Test+Suite&utm_campaign=Premailer">by Dialect</a><br>
Vancouver Island, British Columbia<br>
250 555.2222</p>
</td>
</tr>
<tr><td height="34"class="contact"> </td></tr>
<tr><td height="4" class="hairline"> </td></tr>
</table>
</td>
<td class="frame" width="13"> </td>
</tr>
<tr>
<td class="frame" width="13"> </td>
<td colspan="4" class="content" height="60"> </td>
<td class="frame" width="13"> </td>
</tr>
<tr><td height="13" colspan="6" class="frame"> </td></tr>
<tr><td height="22" colspan="6" > </td></tr>
<tr><td id="credit" colspan="6">Newsletter communications by<br><a href="http://dialect.ca/dialogue/?utm_source=Dialogue&utm_medium=Credit&utm_campaign=South+Hollow"><img src="inc/dialect.png" alt="Dialect" width="60" height="35" border="0"></a><br><unsubscribe>Click here to unsubscribe</unsubscribe></td></tr>
</table>
</div>
</body>
</html>
================================================
FILE: test/files/chars.html
================================================
<!DOCTYPE html>
<html>
<body>
<p>cédille cé & garçon garçon à à & ©</p>
</body>
</html>
================================================
FILE: test/files/html4.html
================================================
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Title</title>
</head>
<body>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
<br>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</body>
</html>
================================================
FILE: test/files/html_with_uri.html
================================================
<html>
<link rel="stylesheet" type="text/css" href="styles.css">
<body>
<p>
The following line should not make this html be identified as a URI
http://foobar.com
</p>
</body>
</html>
================================================
FILE: test/files/ignore.css
================================================
body {
color: orange;
}
================================================
FILE: test/files/ignore.html
================================================
<!doctype html>
<html>
<head>
<title>Should ignore link and style elements with data attribute</title>
<link rel="stylesheet" type="text/css" href="ignore.css" data-premailer="ignore" />
<style type="text/css" data-premailer="ignore">
h1 {
color: red;
}
</style>
</head>
<body>
<h1>Content</h1>
</body>
</html>
================================================
FILE: test/files/import.css
================================================
/*
* Premailer styles - should import
*
* $Package: Premailer $
* $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb 2009) $WCDATE$ $
* $Rev: 95 $WCREV$ $
*/
.hide {
display: none;
}
================================================
FILE: test/files/iso-8859-2.html
================================================
<body>In Hungary we use some special accented characters: .</body>
================================================
FILE: test/files/iso-8859-5.html
================================================
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-5">
</head>
<body>
<p> </p>
</body>
</html>
================================================
FILE: test/files/no_css.html
================================================
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Premailer No CSS Test</title>
</style>
</head>
<body>
<p class="hide" id="hide01">This line should be hidden.</p>
</body>
</html>
================================================
FILE: test/files/noimport.css
================================================
/*
* Premailer styles - should not import
*
* $Package: Premailer $
* $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb 2009) $WCDATE$ $
* $Rev: 95 $WCREV$ $
*/
body {
background: none red !important;
}
================================================
FILE: test/files/styles.css
================================================
/*
* Premailer styles
*
* $Package: Premailer $
* $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb 2009) $WCDATE$ $
* $Rev: 95 $WCREV$ $
*/
@import "noimport.css" print;
/*** structure and table cells ***/
body{font:13px/1.231 "Arial",sans-serif;color: #fff;background-color: #9EBF00;}
#wrapper { width: 100%; margin: 2em 0; background-color: #9EBF00; color: #fff; }
.container { margin: 0 auto; line-height: 130%; color: #4d4d4d; text-align: left; }
.frame { background-color: #b6d93f; font-size: 1px; line-height: 1px; }
.hairline { background-color: #9ebf00; font-size: 1px; line-height: 1px; }
.masthead, .gutter { color: #999; background-color: #fff; }
.content { line-height: 158%; color: #999; background-color: #fff; }
#webversion, #footer { margin: 0 auto; text-align: center; font-size: 85%; }
/*** general styles ***/
h1, h1 a, h2, h2 a { color: #9ebf00; }
h1 span { padding: 0 .5em; background: #fff; }
h1 {
margin: 32px 0 19px;
font: bold 85% "Verdana", sans-serif;
text-transform: uppercase;
text-align: center;
letter-spacing: .1em;
background: transparent url("dots_h.gif") repeat-x 0 55%;
}
h2 { margin: 0 0 .4em; font: normal 205%/109% "Arial", sans-serif; }
h3, h3 a { color: #808080; }
h3 { margin: 0 0 .1em; font: normal 165%/109% "Arial", sans-serif; }
table{ border-collapse:collapse;border-spacing:0; border: 0; }
caption,th,td {text-align:left;font-weight:normal; margin: 0; padding: 0; }
pre,code,kbd,samp,tt{font-family:monospace;line-height:100%;}
th, td { vertical-align: top; }
a { text-decoration: none; }
blockquote { margin: 0; padding: 5px 30px; text-align: center; }
blockquote, blockquote p { font: italic 16px/145% "Georgia", serif; }
p, blockquote { color: #999; }
p { margin: 0 0 1em; font: normal 100%/158% "Arial", sans-serif; vertical-align: top; }
.hide { text-align: center; color: red; font-size: 150%; }
/*** specific elements ***/
h2 + h3 { font-style: italic;}
p[attr~=quote] { font-style: italic;}
ul li:first-of-type { font-style: italic;}
.content p a, .content li a { color: #8AAD09; text-decoration: underline; }
.content p.dt { margin-bottom: .8em; font: italic 95%/135% "Arial", sans-serif; }
img.right { float: right; margin: 0 0 30px 20px; }
.contact { text-align: center; background: #9EC03B url("contact_bg.png") repeat 0 0; }
.contact, .contact p, .contact a { color: #fff; text-decoration: none; }
.contact p { margin-bottom: 0; line-height: 140%; }
.contact a:hover { text-decoration: underline; }
#webversion, #webversion a {
font: bold 12px/28px "Trebuchet", "Trebuchet MS", serif;
color: #fff;
background: #9ebf00;
}
#credit { padding-bottom: 20px; }
#credit, #credit a {
color: #fff;
font: normal 10px/13px "Verdana", sans-serif;
text-align: center;
}
#contact_info { padding: 5px; }
================================================
FILE: test/files/xhtml.html
================================================
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Title</title>
</head>
<body>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
<br/>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</body>
</html>
================================================
FILE: test/future_tests.rb
================================================
# encoding: UTF-8
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestPremailer < Premailer::TestCase
def test_related_attributes
flunk 'Not implemented'
local_setup
# h1 { text-align: center; }
assert_equal 'center', @doc.at('h1')['align']
# td { vertical-align: top; }
assert_equal 'top', @doc.at('td')['valign']
# p { vertical-align: top; } -- not allowed
assert_nil @doc.at('p')['valign']
# no align attr is specified for <p> elements, so it should not appear
assert_nil @doc.at('p.unaligned')['align']
# .contact { background: #9EC03B url("contact_bg.png") repeat 0 0; }
assert_equal '#9EC03B', @doc.at('td.contact')['bgcolor']
# body { background-color: #9EBF00; }
assert_equal '#9EBF00', @doc.at('body')['bgcolor']
end
def test_merging_cellpadding
flunk 'Not implemented'
local_setup('cellpadding.html', {:prefer_cellpadding => true})
assert_equal '0', @doc.at('#t1')['cellpadding']
assert_match /padding\:/i, @doc.at('#t1 td')['style']
assert_equal '5', @doc.at('#t2')['cellpadding']
refute_match /padding\:/i, @doc.at('#t2 td')['style']
assert_nil @doc.at('#t3')['cellpadding']
assert_match /padding\:/i, @doc.at('#t3 td')['style']
assert_nil @doc.at('#t4')['cellpadding']
assert_match /padding\:/i, @doc.at('#t4a')['style']
assert_match /padding\:/i, @doc.at('#t4b')['style']
end
def test_preserving_media_queries
flunk 'Not implemented'
local_setup
assert_match /display\: none/i, @doc.at('#iphone')['style']
end
end
================================================
FILE: test/helper.rb
================================================
require 'bundler/setup'
require 'maxitest/autorun'
require 'webmock/minitest'
require 'premailer'
class Premailer::TestCase < Minitest::Test
BASE_URI = 'http://premailer.dev/'
BASE_PATH = File.expand_path(File.dirname(__FILE__)) + '/files'
def setup
stub_request(:any, /premailer\.dev\/*/).to_return do |request|
file_path = BASE_PATH + URI.parse(request.uri).path
if File.exists?(file_path)
{ :status => 200, :body => File.open(file_path) }
else
{ :status => 404, :body => "#{file_path} not found" }
end
end
stub_request(:get, /my\.example\.com\:8080\/*/).to_return(:status => 200, :body => "", :headers => {})
end
def default_test; end
protected
def local_setup(f = 'base.html', opts = {})
base_file = BASE_PATH + '/' + f
premailer = Premailer.new(base_file, opts)
premailer.to_inline_css
@doc = premailer.processed_doc
end
def remote_setup(f = 'base.html', opts = {})
@premailer = Premailer.new(BASE_URI + "#{f}", opts)
@premailer.to_inline_css
@doc = @premailer.processed_doc
end
end
================================================
FILE: test/test_adapter.rb
================================================
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestAdapter < Premailer::TestCase
def test_default_to_best_available
require 'hpricot'
assert_equal 'Premailer::Adapter::Hpricot', Premailer::Adapter.use.name
end
def test_settable_via_symbol
Premailer::Adapter.use = :hpricot
assert_equal 'Premailer::Adapter::Hpricot', Premailer::Adapter.use.name
end
def test_adapters_are_findable_by_symbol
assert_equal 'Premailer::Adapter::Hpricot', Premailer::Adapter.find(:hpricot).name
end
def test_adapters_are_findable_by_class
assert_equal 'Premailer::Adapter::Hpricot', Premailer::Adapter.find(Premailer::Adapter::Hpricot).name
end
def test_raises_argument_error
assert_raises(ArgumentError, "Invalid adapter: unknown") {
Premailer::Adapter.find(:unknown)
}
end
end
================================================
FILE: test/test_html_to_plain_text.rb
================================================
# encoding: utf-8
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestHtmlToPlainText < Premailer::TestCase
include HtmlToPlainText
def test_to_plain_text_with_fragment
premailer = Premailer.new('<p>Test</p>', :with_html_string => true)
assert_match /Test/, premailer.to_plain_text
end
def test_to_plain_text_with_body
html = <<END_HTML
<html>
<title>Ignore me</title>
<body>
<p>Test</p>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
assert_match /Test/, premailer.to_plain_text
end
def test_to_plain_text_with_malformed_body
html = <<END_HTML
<html>
<title>Ignore me</title>
<body>
<p>Test
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
assert_match /Test/, premailer.to_plain_text
end
def test_specialchars
assert_plaintext 'cédille garçon & à ñ', 'cédille garçon & à ñ'
end
def test_stripping_whitespace
assert_plaintext "text\ntext", " \ttext\ntext\n"
assert_plaintext "a\na", " \na \n a \t"
assert_plaintext "a\n\na", " \na \n\t \n \n a \t"
assert_plaintext "test text", "test text "
assert_plaintext "test text", "test text"
end
def test_wrapping_spans
html = <<END_HTML
<html>
<body>
<p><span>Test</span>
<span>line 2</span>
</p>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
assert_match /Test line 2/, premailer.to_plain_text
end
def test_line_breaks
assert_plaintext "Test text\nTest text", "Test text\r\nTest text"
assert_plaintext "Test text\nTest text", "Test text\rTest text"
end
def test_lists
assert_plaintext "* item 1\n* item 2", "<li class='123'>item 1</li> <li>item 2</li>\n"
assert_plaintext "* item 1\n* item 2\n* item 3", "<li>item 1</li> \t\n <li>item 2</li> <li> item 3</li>\n"
end
def test_stripping_html
assert_plaintext 'test text', "<p class=\"123'45 , att\" att=tester>test <span class='te\"st'>text</span>\n"
end
def test_stripping_ignored_blocks
html = <<END_HTML
<p>test</p>
<!-- start text/html -->
<img src="logo.png" alt="logo">
<!-- end text/html -->
<p>text</p>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
assert_match /test\n\ntext/, premailer.to_plain_text
end
def test_paragraphs_and_breaks
assert_plaintext "Test text\n\nTest text", "<p>Test text</p><p>Test text</p>"
assert_plaintext "Test text\n\nTest text", "\n<p>Test text</p>\n\n\n\t<p>Test text</p>\n"
assert_plaintext "Test text\nTest text", "\n<p>Test text<br/>Test text</p>\n"
assert_plaintext "Test text\nTest text", "\n<p>Test text<br> \tTest text<br></p>\n"
assert_plaintext "Test text\n\nTest text", "Test text<br><BR />Test text"
end
def test_headings
assert_plaintext "****\nTest\n****", "<h1>Test</h1>"
assert_plaintext "****\nTest\n****", "\t<h1>\nTest</h1> "
assert_plaintext "***********\nTest line 1\nTest 2\n***********", "\t<h1>\nTest line 1<br>Test 2</h1> "
assert_plaintext "****\nTest\n****\n\n****\nTest\n****", "<h1>Test</h1> <h1>Test</h1>"
assert_plaintext "----\nTest\n----", "<h2>Test</h2>"
assert_plaintext "Test\n----", "<h3> <span class='a'>Test </span></h3>"
end
def test_wrapping_lines
raw = ''
100.times { raw += 'test ' }
txt = convert_to_text(raw, 20)
lens = []
txt.each_line { |l| lens << l.length }
assert lens.max <= 20
end
def test_wrapping_lines_with_spaces
assert_plaintext "Long line\nnew line", 'Long line new line', nil ,10
end
def test_img_alt_tags
# ensure html imag tags that aren't self-closed are parsed,
# along with accepting both '' and "" as attribute quotes
# <img alt="" />
assert_plaintext 'Example ( http://example.com/ )', '<a href="http://example.com/"><img src="http://example.ru/hello.jpg" alt="Example"/></a>'
# <img alt="">
assert_plaintext 'Example ( http://example.com/ )', '<a href="http://example.com/"><img src="http://example.ru/hello.jpg" alt="Example"></a>'
# <img alt='' />
assert_plaintext 'Example ( http://example.com/ )', "<a href='http://example.com/'><img src='http://example.ru/hello.jpg' alt='Example'/></a>"
# <img alt=''>
assert_plaintext 'Example ( http://example.com/ )', "<a href='http://example.com/'><img src='http://example.ru/hello.jpg' alt='Example'></a>"
end
def test_links
# basic
assert_plaintext 'Link ( http://example.com/ )', '<a href="http://example.com/">Link</a>'
# nested html
assert_plaintext 'Link ( http://example.com/ )', '<a href="http://example.com/"><span class="a">Link</span></a>'
# nested html with new line
assert_plaintext 'Link ( http://example.com/ )', "<a href='http://example.com/'>\n\t<span class='a'>Link</span>\n\t</a>"
# mailto
assert_plaintext 'Contact Us ( contact@example.org )', "<a href='mailto:contact@example.org'>Contact Us</a>"
# complex link
assert_plaintext 'Link ( http://example.com:80/~user?aaa=bb&c=d,e,f#foo )', '<a href="http://example.com:80/~user?aaa=bb&c=d,e,f#foo">Link</a>'
# attributes
assert_plaintext 'Link ( http://example.com/ )', '<a title=\'title\' href="http://example.com/">Link</a>'
# spacing
assert_plaintext 'Link ( http://example.com/ )', '<a href=" http://example.com/ "> Link </a>'
# multiple
assert_plaintext 'Link A ( http://example.com/a/ ) Link B ( http://example.com/b/ )', '<a href="http://example.com/a/">Link A</a> <a href="http://example.com/b/">Link B</a>'
# merge links
assert_plaintext 'Link ( %%LINK%% )', '<a href="%%LINK%%">Link</a>'
assert_plaintext 'Link ( [LINK] )', '<a href="[LINK]">Link</a>'
assert_plaintext 'Link ( {LINK} )', '<a href="{LINK}">Link</a>'
# unsubscribe
assert_plaintext 'Link ( [[!unsubscribe]] )', '<a href="[[!unsubscribe]]">Link</a>'
# empty link gets dropped, and shouldn't run forever
assert_plaintext(("This is some more text\n\n" * 14 + "This is some more text"), "<a href=\"test\"></a>#{"\n<p>This is some more text</p>" * 15}")
# links that go outside of line should wrap nicely
assert_plaintext "Long text before the actual link and then LINK TEXT \n( http://www.long.link ) and then more text that does not wrap", 'Long text before the actual link and then <a href="http://www.long.link"/>LINK TEXT</a> and then more text that does not wrap'
end
# see https://github.com/alexdunae/premailer/issues/72
def test_multiple_links_per_line
assert_plaintext 'This is link1 ( http://www.google.com ) and link2 ( http://www.google.com ) is next.',
'<p>This is <a href="http://www.google.com" >link1</a> and <a href="http://www.google.com" >link2 </a> is next.</p>',
nil, 10000
end
# see https://github.com/alexdunae/premailer/issues/72
def test_links_within_headings
assert_plaintext "****************************\nTest ( http://example.com/ )\n****************************",
"<h1><a href='http://example.com/'>Test</a></h1>"
end
def assert_plaintext(out, raw, msg = nil, line_length = 65)
assert_equal out, convert_to_text(raw, line_length), msg
end
end
================================================
FILE: test/test_links.rb
================================================
# encoding: UTF-8
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestLinks < Premailer::TestCase
def test_empty_query_string
premailer = Premailer.new('<p>Test</p>', :with_html_string => true, :link_query_string => ' ')
premailer.to_inline_css
end
def test_appending_link_query_string
qs = 'utm_source=1234&tracking=good&doublescape'
opts = {:base_url => 'http://example.com/', :link_query_string => qs, :with_html_string => true, :adapter => :hpricot}
appendable = [
'/',
opts[:base_url],
'https://example.com/tester',
'images/',
"#{opts[:base_url]}test.html?cn=tf&c=20&ord=random",
'?query=string'
]
not_appendable = [
'%DONOTCONVERT%',
'{DONOTCONVERT}',
'[DONOTCONVERT]',
'<DONOTCONVERT>',
'{@msg-txturl}',
'[[!unsubscribe]]',
'#relative',
'tel:5555551212',
'http://example.net/',
'mailto:premailer@example.com',
'ftp://example.com',
'gopher://gopher.floodgap.com/1/fun/twitpher'
]
html = appendable.collect {|url| "<a href='#{url}'>Link</a>" }
premailer = Premailer.new(html.to_s, opts)
premailer.to_inline_css
premailer.processed_doc.search('a').each do |el|
href = el.attributes['href'].to_s
next if href.nil? or href.empty?
uri = URI.parse(href)
assert_match qs, uri.query, "missing query string for #{el.to_s}"
end
html = not_appendable.collect {|url| "<a href='#{url}'>Link</a>" }
premailer = Premailer.new(html.to_s, opts)
premailer.to_inline_css
premailer.processed_doc.search('a').each do |el|
href = el['href']
next if href.nil? or href.empty?
assert not_appendable.include?(href), "link #{href} should not be converted: see #{not_appendable.to_s}"
end
end
def test_stripping_extra_question_marks_from_query_string
qs = '??utm_source=1234'
premailer = Premailer.new("<a href='/test/?'>Link</a> <a href='/test/'>Link</a>", :link_query_string => qs, :with_html_string => true)
premailer.to_inline_css
premailer.processed_doc.search('a').each do |a|
assert_equal '/test/?utm_source=1234', a['href'].to_s
end
premailer = Premailer.new("<a href='/test/?123&456'>Link</a>", :link_query_string => qs, :with_html_string => true)
premailer.to_inline_css
assert_equal '/test/?123&456&utm_source=1234', premailer.processed_doc.at('a')['href']
end
def test_unescape_ampersand
qs = 'utm_source=1234'
premailer = Premailer.new("<a href='/test/?q=query'>Link</a>", :link_query_string => qs, :with_html_string => true, :unescaped_ampersand => true)
premailer.to_inline_css
premailer.processed_doc.search('a').each do |a|
assert_equal '/test/?q=query&utm_source=1234', a['href'].to_s
end
end
def test_preserving_links
html = "<a href='http://example.com/index.php?pram1=one&pram2=two'>Link</a>"
premailer = Premailer.new(html.to_s, :link_query_string => '', :with_html_string => true)
premailer.to_inline_css
assert_equal 'http://example.com/index.php?pram1=one&pram2=two', premailer.processed_doc.at('a')['href']
html = "<a href='http://example.com/index.php?pram1=one&pram2=two'>Link</a>"
premailer = Premailer.new(html.to_s, :link_query_string => 'qs', :with_html_string => true)
premailer.to_inline_css
assert_equal 'http://example.com/index.php?pram1=one&pram2=two&qs', premailer.processed_doc.at('a')['href']
end
def test_resolving_urls_from_string
['test.html', '/test.html', './test.html',
'test/../test.html', 'test/../test/../test.html'].each do |q|
assert_equal 'http://example.com/test.html', Premailer.resolve_link(q, 'http://example.com/'), q
end
assert_equal 'https://example.net:80/~basedir/test.html?var=1#anchor', Premailer.resolve_link('test/../test/../test.html?var=1#anchor', 'https://example.net:80/~basedir/')
end
def test_resolving_urls_from_uri
base_uri = URI.parse('http://example.com/')
['test.html', '/test.html', './test.html',
'test/../test.html', 'test/../test/../test.html'].each do |q|
assert_equal 'http://example.com/test.html', Premailer.resolve_link(q, base_uri), q
end
base_uri = URI.parse('https://example.net:80/~basedir/')
assert_equal 'https://example.net:80/~basedir/test.html?var=1#anchor', Premailer.resolve_link('test/../test/../test.html?var=1#anchor', base_uri)
# base URI with a query string
base_uri = URI.parse('http://example.com/dir/index.cfm?newsletterID=16')
assert_equal 'http://example.com/dir/index.cfm?link=15', Premailer.resolve_link('?link=15', base_uri)
# URI preceded by a space
base_uri = URI.parse('http://example.com/')
assert_equal 'http://example.com/path', Premailer.resolve_link(' path', base_uri)
end
def test_resolving_urls_from_html_string
# The inner URI is on its own line to ensure that the impl doesn't match
# URIs based on start of line.
base_uri = "<html><head></head><body>\nhttp://example.com/\n</body>"
['test.html', '/test.html', './test.html',
'test/../test.html', 'test/../test/../test.html'].each do |q|
Premailer.resolve_link(q, base_uri)
end
end
def test_resolving_urls_in_doc
# force Nokogiri since this consistenly segfaults with Hpricot
base_file = File.dirname(__FILE__) + '/files/base.html'
base_url = 'https://my.example.com:8080/test-path.html'
premailer = Premailer.new(base_file, :base_url => base_url, :adapter => :nokogiri)
premailer.to_inline_css
pdoc = premailer.processed_doc
doc = premailer.doc
# unchanged links
['#l02', '#l03', '#l05', '#l06', '#l07', '#l08',
'#l09', '#l10', '#l11', '#l12', '#l13'].each do |link_id|
assert_equal doc.at(link_id).attributes['href'], pdoc.at(link_id).attributes['href'], link_id
end
assert_equal 'https://my.example.com:8080/', pdoc.at('#l01').attributes['href'].to_s
assert_equal 'https://my.example.com:8080/images/', pdoc.at('#l04').attributes['href'].to_s
end
def test_convertable_inline_links
convertable = [
'my/path/to',
'other/path',
'/'
]
html = convertable.collect {|url| "<a href='#{url}'>Link</a>" }
premailer = Premailer.new(html.to_s, :base_url => "http://example.com", :with_html_string => true)
premailer.processed_doc.search('a').each do |el|
href = el.attributes['href'].to_s
assert(href =~ /http:\/\/example.com/, "link #{href} is not absolute")
end
end
def test_non_convertable_inline_links
not_convertable = [
'%DONOTCONVERT%',
'{DONOTCONVERT}',
'[DONOTCONVERT]',
'<DONOTCONVERT>',
'{@msg-txturl}',
'[[!unsubscribe]]',
'#relative',
'tel:5555551212',
'mailto:premailer@example.com',
'ftp://example.com',
'gopher://gopher.floodgap.com/1/fun/twitpher',
'cid:13443452066.10392logo.jpeg@inline_attachment'
]
html = not_convertable.collect {|url| "<a href='#{url}'>Link</a>" }
premailer = Premailer.new(html.to_s, :base_url => "example.com", :with_html_string => true)
premailer.to_inline_css
premailer.processed_doc.search('a').each do |el|
href = el.attributes['href'].to_s
assert not_convertable.include?(href), "link #{href} should not be converted: see #{not_convertable.inspect}"
end
end
end
================================================
FILE: test/test_misc.rb
================================================
# encoding: UTF-8
require File.expand_path(File.dirname(__FILE__)) + '/helper'
# Random tests for specific issues.
#
# The test suite will be cleaned up at some point soon.
class TestMisc < Premailer::TestCase
# in response to http://github.com/alexdunae/premailer/issues#issue/4
#
# NB: 2010-11-16 -- after reverting to Hpricot this test can no longer pass.
# It's too much of an edge case to get any dev time.
def test_parsing_extra_quotes
io = StringIO.new('<p></p>
<h3 "id="WAR"><a name="WAR"></a>Writes and Resources</h3>
<table></table>')
premailer = Premailer.new(io, :adapter => :nokogiri)
assert_match /<h3>[\s]*<a name="WAR">[\s]*<\/a>[\s]*Writes and Resources[\s]*<\/h3>/i, premailer.to_inline_css
end
def test_styles_in_the_body
html = <<END_HTML
<html>
<body>
<style type="text/css"> p { color: red; } </style>
<p>Test</p>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /color\: red/i, premailer.processed_doc.at('p')['style']
end
def test_commented_out_styles_in_the_body
html = <<END_HTML
<html>
<body>
<style type="text/css"> <!-- p { color: red; } --> </style>
<p>Test</p>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /color\: red/i, premailer.processed_doc.at('p')['style']
end
def test_not_applying_styles_to_the_head
html = <<END_HTML
<html>
<head>
<title>Title</title>
<style type="text/css"> * { color: red; } </style>
</head>
<body>
<p><a>Test</a></p>
</body>
</html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
premailer.to_inline_css
h = premailer.processed_doc.at('head')
assert_nil h['style']
t = premailer.processed_doc.at('title')
assert_nil t['style']
end
end
def test_multiple_identical_ids
html = <<-END_HTML
<html>
<head>
<style type="text/css"> #the_id { color: red; } </style>
</head>
<body>
<p id="the_id">Test</p>
<p id="the_id">Test</p>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
premailer.processed_doc.search('p').each do |el|
assert_match /red/i, el['style']
end
end
def test_preserving_styles
html = <<END_HTML
<html>
<head>
<link rel="stylesheet" href="#"/>
<style type="text/css"> a:hover { color: red; } </style>
</head>
<body>
<p><a>Test</a></p>
</body>
</html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :preserve_styles => true, :adapter => adapter)
premailer.to_inline_css
assert_equal 1, premailer.processed_doc.search('head link').length
assert_equal 1, premailer.processed_doc.search('head style').length
premailer = Premailer.new(html, :with_html_string => true, :preserve_styles => false, :adapter => adapter)
premailer.to_inline_css
assert_nil premailer.processed_doc.at('body link')
# should be preserved as unmergeable
assert_match /color: red/i, premailer.processed_doc.at('body style').inner_html
assert_match /a:hover/i, premailer.processed_doc.at('style').inner_html
end
end
def test_unmergable_rules
html = <<END_HTML
<html> <head> <style type="text/css"> a { color:blue; } a:hover { color: red; } </style> </head>
<p><a>Test</a></p>
</body> </html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true, :verbose => true)
premailer.to_inline_css
# blue should be inlined
refute_match /a\:hover[\s]*\{[\s]*color\:[\s]*blue[\s]*;[\s]*\}/i, premailer.processed_doc.at('body style').inner_html
# red should remain in <style> block
assert_match /a\:hover[\s]*\{[\s]*color\:[\s]*red;[\s]*\}/i, premailer.processed_doc.at('body style').inner_html
end
def test_unmergable_media_queries
html = <<END_HTML
<html> <head>
<style type="text/css">
a { color: blue; }
@media (min-width:500px) {
a { color: red; }
}
@media screen and (orientation: portrait) {
a { color: green; }
}
</style>
</head>
<body>
<p><a>Test</a></p>
</body> </html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
premailer.to_inline_css
style_tag = premailer.processed_doc.at('body style')
assert style_tag, "#{adapter} failed to add a body style tag"
style_tag_contents = style_tag.inner_html
assert_equal "color: blue;", premailer.processed_doc.at('a').attributes['style'].to_s,
"#{adapter}: Failed to inline the default style"
assert_match /@media \(min-width:500px\) \{.*?a \{.*?color: red;.*?\}.*?\}/m, style_tag_contents,
"#{adapter}: Failed to add media query with no type to style"
assert_match /@media screen and \(orientation: portrait\) \{.*?a \{.*?color: green;.*?\}.*?\}/m, style_tag_contents,
"#{adapter}: Failed to add media query with type to style"
end
end
def test_unmergable_rules_with_no_body
html = <<END_HTML
<html>
<style type="text/css"> a:hover { color: red; } </style>
<p><a>Test</a></p>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /a\:hover[\s]*\{[\s]*color\:[\s]*red;[\s]*\}/i, premailer.processed_doc.at('style').inner_html
end
# in response to https://github.com/alexdunae/premailer/issues#issue/7
def test_ignoring_link_pseudo_selectors
html = <<END_HTML
<html>
<style type="text/css"> td a:link.top_links { color: red; } </style>
<body>
<td><a class="top_links">Test</a></td>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /color: red/, premailer.processed_doc.at('a').attributes['style'].to_s
end
# in response to https://github.com/alexdunae/premailer/issues#issue/7
#
# fails sometimes in JRuby, see https://github.com/alexdunae/premailer/issues/79
def test_parsing_bad_markup_around_tables
html = <<END_HTML
<html>
<style type="text/css">
.style3 { font-size: xx-large; }
.style5 { background-color: #000080; }
</style>
<tr>
<td valign="top" class="style3">
<!-- MSCellType="ContentHead" -->
<strong>PROMOCION CURSOS PRESENCIALES</strong></td>
<strong>
<td valign="top" style="height: 125px" class="style5">
<!-- MSCellType="DecArea" -->
<img alt="" src="../../images/CertisegGold.GIF" width="608" height="87" /></td>
</tr>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /font-size: xx-large/, premailer.processed_doc.search('.style3').first.attributes['style'].to_s
refute_match /background: #000080/, premailer.processed_doc.search('.style5').first.attributes['style'].to_s
assert_match /#000080/, premailer.processed_doc.search('.style5').first.attributes['bgcolor'].to_s
end
# in response to https://github.com/alexdunae/premailer/issues/56
def test_inline_important
html = <<END_HTML
<html>
<style type="text/css">
p { color: red !important; }
</style>
<body>
<p style='color: green !important;'>test</p></div>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true, :adapter => :nokogiri)
premailer.to_inline_css
assert_equal 'color: green !important;', premailer.processed_doc.search('p').first.attributes['style'].to_s
end
# in response to https://github.com/alexdunae/premailer/issues/28
def test_handling_shorthand_auto_properties
html = <<END_HTML
<html>
<style type="text/css">
#page { margin: 0; margin-left: auto; margin-right: auto; }
p { border: 1px solid black; border-right: none; }
</style>
<body>
<div id='page'><p>test</p></div>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_match /margin: 0 auto/, premailer.processed_doc.search('#page').first.attributes['style'].to_s
assert_match /border-style: solid none solid solid;/, premailer.processed_doc.search('p').first.attributes['style'].to_s
end
def test_sorting_style_attributes
html = <<END_HTML
<html>
<style type="text/css">
#page { right: 10px; left: 5px }
</style>
<body>
<div id='page'>test</div>
</body>
</html>
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
premailer.to_inline_css
assert_equal "left: 5px; right: 10px;", premailer.processed_doc.search('#page').first.attributes['style'].to_s
end
def test_removing_scripts
html = <<END_HTML
<html>
<head>
<script>script to be removed</script>
</head>
<body>
content
</body>
</html>
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
<html>
<head>
<style>td { background-color: #FF0000 !important; }</style>
</head>
<body>
<table><tr><td>red</td></tr></table>
</body>
</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
<html>
<body>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Person",
"name": "John Doe",
"jobTitle": "Graduate research assistant",
"affiliation": "University of Dreams",
"additionalName": "Johnny",
"url": "http://www.example.com",
"address": {
"@type": "PostalAddress",
"streetAddress": "1234 Peach Drive",
"addressLocality": "Wonderland",
"addressRegion": "Georgia"
}
}
</script
</body>
</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 = <<END_HTML
<html>
<head>
<style>#logo {content:url(good.png)};}</style>
</head>
<body>
<image id="logo"/>
</body>
</html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
assert_match 'content: url(good.png)', premailer.to_inline_css
end
end
def test_style_with_data_in_content
html = <<END_HTML
<html>
<head>
<style>#logo {content: url(data:image/png;base64,LOTSOFSTUFF)};}</style>
</head>
<body>
<image id="logo"/>
</body>
</html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
assert_match 'content: url(data:image/png;base64,LOTSOFSTUFF)', premailer.to_inline_css
end
end
end
================================================
FILE: test/test_premailer.rb
================================================
# -*- encoding: UTF-8 -*-
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestPremailer < Premailer::TestCase
def test_special_characters_nokogiri
html = '<p>cédille cé & garçon garçon à à & ©</p>'
premailer = Premailer.new(html, :with_html_string => true, :adapter => :nokogiri)
premailer.to_inline_css
assert_equal 'cédille cé & garçon garçon à à & ©', premailer.processed_doc.at('p').inner_html
end
def test_special_characters_nokogiri_remote
remote_setup('chars.html', :adapter => :nokogiri)
@premailer.to_inline_css
assert_equal 'cédille cé & garçon garçon à à & ©', @premailer.processed_doc.at('p').inner_html
end
#def test_cyrillic_nokogiri_remote
# if RUBY_VERSION =~ /1.9/
# remote_setup('iso-8859-5.html', :adapter => :nokogiri) #, :encoding => 'iso-8859-5')
# @premailer.to_inline_css
# assert_equal Encoding.find('ISO-8859-5'), @premailer.processed_doc.at('p').inner_html.encoding
# end
#end
# TODO: this passes when run from rake but not when run from:
# ruby -Itest test/test_premailer.rb -n test_special_characters_hpricot
def test_special_characters_hpricot
html = '<p>cédille cé & garçon garçon à à &</p>'
premailer = Premailer.new(html, :with_html_string => true, :adapter => :hpricot)
premailer.to_inline_css
assert_equal 'cédille cé & garçon garçon à à &', premailer.processed_doc.at('p').inner_html
end
def test_detecting_html
[:nokogiri, :hpricot].each do |adapter|
remote_setup('base.html', :adapter => adapter)
assert !@premailer.is_xhtml?
end
end
def test_detecting_xhtml
[:nokogiri, :hpricot].each do |adapter|
remote_setup('xhtml.html', :adapter => adapter)
assert @premailer.is_xhtml?
end
end
def test_self_closing_xhtml_tags
[:nokogiri, :hpricot].each do |adapter|
remote_setup('xhtml.html', :adapter => adapter)
assert_match /<br[\s]*\/>/, @premailer.to_s
assert_match /<br[\s]*\/>/, @premailer.to_inline_css
end
end
def test_non_self_closing_html_tags
[:nokogiri, :hpricot].each do |adapter|
remote_setup('html4.html', :adapter => adapter)
assert_match /<br>/, @premailer.to_s
assert_match /<br>/, @premailer.to_inline_css
end
end
def test_mailtos_with_query_strings
html = <<END_HTML
<html>
<a href="mailto:info@example.com?subject=Programmübersicht&body=Lorem ipsum dolor sit amet.">Test</a>
</html>
END_HTML
qs = 'testing=123'
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :link_query_string => qs, :adapter => adapter)
premailer.to_inline_css
refute_match /testing=123/, premailer.processed_doc.search('a').first.attributes['href'].to_s
end
end
def test_escaping_strings
local_setup
str = %q{url("/images/test.png");}
assert_equal("url(\'/images/test.png\');", Premailer.escape_string(str))
end
def test_preserving_ignored_style_elements
[:nokogiri, :hpricot].each do |adapter|
local_setup('ignore.html', :adapter => adapter)
assert_nil @doc.at('h1')['style']
end
end
def test_preserving_ignored_link_elements
[:nokogiri, :hpricot].each do |adapter|
local_setup('ignore.html', :adapter => adapter)
assert_nil @doc.at('body')['style']
end
end
def test_importing_local_css
# , :hpricot
[:nokogiri].each do |adapter|
local_setup('base.html', :adapter => adapter)
# noimport.css (print stylesheet) sets body { background } to red
refute_match /red/, @doc.at('body').attributes['style'].to_s
# import.css sets .hide to { display: none }
assert_match /display: none/, @doc.at('#hide01').attributes['style'].to_s
end
end
def test_css_to_attributes
[:nokogiri, :hpricot].each do |adapter|
html = '<td style="background-color: #FFF;"></td>'
premailer = Premailer.new(html, {:with_html_string => true, :adapter => adapter, :css_to_attributes => true})
premailer.to_inline_css
assert_equal ';', premailer.processed_doc.search('td').first.attributes['style'].to_s
assert_equal '#FFF', premailer.processed_doc.search('td').first.attributes['bgcolor'].to_s
end
end
def test_avoid_changing_css_to_attributes
[:nokogiri, :hpricot].each do |adapter|
html = '<td style="background-color: #FFF;"></td>'
premailer = Premailer.new(html, {:with_html_string => true, :adapter => adapter, :css_to_attributes => false})
premailer.to_inline_css
assert_match /background: #FFF/, premailer.processed_doc.search('td').first.attributes['style'].to_s
end
end
def test_importing_remote_css
[:nokogiri, :hpricot].each do |adapter|
remote_setup('base.html', :adapter => adapter)
# noimport.css (print stylesheet) sets body { background } to red
refute_match /red/, @doc.at('body')['style']
# import.css sets .hide to { display: none }
assert_match /display: none/, @doc.at('#hide01')['style']
end
end
def test_importing_css_as_string
files_base = File.expand_path(File.dirname(__FILE__)) + '/files/'
css_string = IO.read(File.join(files_base, 'import.css'))
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new(File.join(files_base, 'no_css.html'), {:css_string => css_string, :adapter => adapter})
premailer.to_inline_css
@doc = premailer.processed_doc
# import.css sets .hide to { display: none }
assert_match /display: none/, @doc.at('#hide01')['style']
end
end
def test_local_remote_check
assert Premailer.local_data?( StringIO.new('a') )
assert Premailer.local_data?( '/path/' )
assert !Premailer.local_data?( 'http://example.com/path/' )
# the old way is deprecated but should still work
premailer = Premailer.new( StringIO.new('a') )
silence_stderr do
assert premailer.local_uri?( '/path/' )
end
end
def test_initialize_can_accept_io_object
[:nokogiri, :hpricot].each do |adapter|
io = StringIO.new('hi mom')
premailer = Premailer.new(io, :adapter => adapter)
assert_match /hi mom/, premailer.to_inline_css
end
end
def test_initialize_can_accept_html_string
[:nokogiri, :hpricot].each do |adapter|
premailer = Premailer.new('<p>test</p>', :with_html_string => true, :adapter => adapter)
assert_match /test/, premailer.to_inline_css
end
end
def test_initialize_no_escape_attributes_option
html = <<END_HTML
<html> <body>
<a id="google" href="http://google.com">Google</a>
<a id="noescape" href="{{link_url}}">Link</a>
</body> </html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
pm = Premailer.new(html, :with_html_string => true, :adapter => adapter, :escape_url_attributes => false)
pm.to_inline_css
doc = pm.processed_doc
assert_equal doc.at('#google')['href'], 'http://google.com'
assert_equal doc.at('#noescape')['href'], '{{link_url}}'
end
end
def test_remove_ids
html = <<END_HTML
<html> <head> <style type="text/css"> #remove { color:blue; } </style> </head>
<body>
<p id="remove"><a href="#keep">Test</a></p>
<p id="keep">Test</p>
</body> </html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
pm = Premailer.new(html, :with_html_string => true, :remove_ids => true, :adapter => adapter)
pm.to_inline_css
doc = pm.processed_doc
assert_nil doc.at('#remove')
assert_nil doc.at('#keep')
hashed_id = doc.at('a')['href'][1..-1]
refute_nil doc.at("\##{hashed_id}")
end
end
def test_reset_contenteditable
html = <<-___
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html> <head> <style type="text/css"> #remove { color:blue; } </style> </head>
<body>
<div contenteditable="true" id="editable"> Test </div>
</body> </html>
___
[:nokogiri, :hpricot].each do |adapter|
pm = Premailer.new(html, :with_html_string => true, :reset_contenteditable => true, :adapter => adapter)
pm.to_inline_css
doc = pm.processed_doc
assert_nil doc.at('#editable')['contenteditable'],
"#{adapter}: contenteditable attribute not removed"
end
end
def test_carriage_returns_as_entities
html = <<-html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<body>\n\r<p>test</p>\n\r<p>test</p>
</body></html>
html
[:nokogiri, :hpricot].each do |adapter|
pm = Premailer.new(html, :with_html_string => true, :adapter => adapter)
assert_match /\r/, pm.to_inline_css
end
end
def test_advanced_selectors
remote_setup('base.html', :adapter => :nokogiri)
assert_match /italic/, @doc.at('h2 + h3')['style']
assert_match /italic/, @doc.at('p[attr~=quote]')['style']
assert_match /italic/, @doc.at('ul li:first-of-type')['style']
remote_setup('base.html', :adapter => :hpricot)
assert_match /italic/, @doc.at('p[@attr~="quote"]')['style']
assert_match /italic/, @doc.at('ul li:first-of-type')['style']
end
def test_premailer_related_attributes
html = <<END_HTML
<html> <head> <style>table { -premailer-width: 500; } td { -premailer-height: 20}; </style>
<body>
<table> <tr> <td> Test </td> </tr> </table>
</body> </html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
pm = Premailer.new(html, :with_html_string => true, :adapter => adapter)
pm.to_inline_css
doc = pm.processed_doc
assert_equal '500', doc.at('table')['width']
assert_equal '20', doc.at('td')['height']
end
end
def test_include_link_tags_option
local_setup('base.html', :adapter => :nokogiri, :include_link_tags => true)
assert_match /1\.231/, @doc.at('body').attributes['style'].to_s
assert_match /display: none/, @doc.at('.hide').attributes['style'].to_s
local_setup('base.html', :adapter => :nokogiri, :include_link_tags => false)
refute_match /1\.231/, @doc.at('body').attributes['style'].to_s
assert_match /display: none/, @doc.at('.hide').attributes['style'].to_s
end
def test_include_style_tags_option
local_setup('base.html', :adapter => :nokogiri, :include_style_tags => true)
assert_match /1\.231/, @doc.at('body').attributes['style'].to_s
assert_match /display: block/, @doc.at('#iphone').attributes['style'].to_s
local_setup('base.html', :adapter => :nokogiri, :include_style_tags => false)
assert_match /1\.231/, @doc.at('body').attributes['style'].to_s
refute_match /display: block/, @doc.at('#iphone').attributes['style'].to_s
end
def test_input_encoding
html_special_characters = "Ää, Öö, Üü"
expected_html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<html><body><p>" + html_special_characters + "</p></body></html>\n"
pm = Premailer.new(html_special_characters, :with_html_string => true, :adapter => :nokogiri, :input_encoding => "UTF-8")
assert_equal expected_html, pm.to_inline_css
end
# output_encoding option should return HTML Entities when set to US-ASCII
def test_output_encoding
html_special_characters = "©"
html_entities_characters = "©"
expected_html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<html><body><p>" + html_entities_characters + "</p></body></html>\n"
pm = Premailer.new(html_special_characters, :output_encoding => "US-ASCII", :with_html_string => true, :adapter => :nokogiri, :input_encoding => "UTF-8");
assert_equal expected_html, pm.to_inline_css
end
def test_meta_encoding_downcase
meta_encoding = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">'
expected_html = Regexp.new(Regexp.escape('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'), Regexp::IGNORECASE)
pm = Premailer.new(meta_encoding, :with_html_string => true, :adapter => :nokogiri, :input_encoding => "utf-8")
assert_match expected_html, pm.to_inline_css
end
def test_meta_encoding_upcase
meta_encoding = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
expected_html = Regexp.new(Regexp.escape('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">'), Regexp::IGNORECASE)
pm = Premailer.new(meta_encoding, :with_html_string => true, :adapter => :nokogiri, :input_encoding => "UTF-8")
assert_match expected_html, pm.to_inline_css
end
def test_htmlentities
html_entities = "’"
expected_html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n<html><body><p>'</p></body></html>\n"
pm = Premailer.new(html_entities, :with_html_string => true, :adapter => :nokogiri, :replace_html_entities => true)
assert_equal expected_html, pm.to_inline_css
end
# If a line other than the first line in the html string begins with a URI
# Premailer should not identify the html string as a URI. Otherwise the following
# exception would be raised: ActionView::Template::Error: bad URI(is not URI?)
def test_line_starting_with_uri_in_html_with_linked_css
files_base = File.expand_path(File.dirname(__FILE__)) + '/files/'
html_string = IO.read(File.join(files_base, 'html_with_uri.html'))
premailer = Premailer.new(html_string, :with_html_string => true)
premailer.to_inline_css
end
def test_empty_html_nokogiri
html = ""
css = "a:hover {color:red;}"
pm = Premailer.new(html, :with_html_string => true, :css_string => css, :adapter => :nokogiri)
pm.to_inline_css
end
def silence_stderr(&block)
orig_stderr = $stderr
$stderr = File.open(File::NULL, 'w')
block.call
ensure
$stderr = orig_stderr
end
end
================================================
FILE: test/test_warnings.rb
================================================
# encoding: UTF-8
require File.expand_path(File.dirname(__FILE__)) + '/helper'
class TestWarnings < Premailer::TestCase
def test_element_warnings
html = <<END_HTML
<!DOCTYPE html>
<html>
<head><link rel="alternate" href="http://example.com/"></head>
<body>
<form method="post"> Test </form>
</body>
</html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter)
assert_equal 2, warnings.length
assert warnings.any? { |w| w[:message] == 'form HTML element'}
assert warnings.any? { |w| w[:message] == 'link HTML element'}
end
end
def test_css_warnings
html = <<END_HTML
<!DOCTYPE html>
<html><body>
<div style="margin: 5px; height: 100px;">Test</div>
</body></html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter)
assert_equal 2, warnings.length
assert warnings.any? { |w| w[:message] == 'height CSS property'}
assert warnings.any? { |w| w[:message] == 'margin CSS property'}
end
end
def test_css_aliased_warnings
html = <<END_HTML
<!DOCTYPE html>
<html><body>
<div style="margin-top: 5px;">Test</div>
</body></html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter)
assert_equal 1, warnings.length
assert warnings.any? { |w| w[:message] == 'margin-top CSS property'}
end
end
def test_attribute_warnings
html = <<END_HTML
<!DOCTYPE html>
<html><body>
<img src="#" ismap>
</body></html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter)
assert_equal 1, warnings.length
assert warnings.any? { |w| w[:message] == 'ismap HTML attribute'}
end
end
def test_warn_level
html = <<END_HTML
<!DOCTYPE html>
<html><body>
<div style="color: red; font-family: sans-serif;">Test</div>
</body></html>
END_HTML
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter, Premailer::Warnings::SAFE)
assert_equal 2, warnings.length
end
[:nokogiri, :hpricot].each do |adapter|
warnings = get_warnings(html, adapter, Premailer::Warnings::POOR)
assert_equal 1, warnings.length
end
end
protected
def get_warnings(html, adapter = :nokogiri, warn_level = Premailer::Warnings::SAFE)
pm = Premailer.new(html, {:adpater => adapter, :with_html_string => true, :warn_level => warn_level})
pm.to_inline_css
pm.check_client_support
end
end
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
SYMBOL INDEX (163 symbols across 15 files)
FILE: lib/premailer/adapter.rb
class Premailer (line 3) | class Premailer
type Adapter (line 8) | module Adapter
function use (line 22) | def self.use
function default (line 32) | def self.default
function use= (line 51) | def self.use=(new_adapter)
function find (line 57) | def self.find(adapter)
FILE: lib/premailer/adapter/hpricot.rb
class Premailer (line 3) | class Premailer
type Adapter (line 4) | module Adapter
type Hpricot (line 6) | module Hpricot
function to_inline_css (line 10) | def to_inline_css
function write_unmergable_css_rules (line 144) | def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
function to_plain_text (line 166) | def to_plain_text
function to_s (line 179) | def to_s
function load_html (line 186) | def load_html(input) # :nodoc:
FILE: lib/premailer/adapter/nokogiri.rb
class Premailer (line 3) | class Premailer
type Adapter (line 4) | module Adapter
type Nokogiri (line 6) | module Nokogiri
function to_inline_css (line 11) | def to_inline_css
function write_unmergable_css_rules (line 146) | def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
function to_plain_text (line 170) | def to_plain_text
function to_s (line 183) | def to_s
function load_html (line 194) | def load_html(input) # :nodoc:
FILE: lib/premailer/adapter/nokogumbo.rb
class Premailer (line 3) | class Premailer
type Adapter (line 4) | module Adapter
type Nokogumbo (line 6) | module Nokogumbo
function to_inline_css (line 11) | def to_inline_css
function write_unmergable_css_rules (line 145) | def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
function to_plain_text (line 169) | def to_plain_text
function to_s (line 182) | def to_s
function load_html (line 193) | def load_html(input) # :nodoc:
FILE: lib/premailer/html_to_plain_text.rb
type HtmlToPlainText (line 5) | module HtmlToPlainText
function convert_to_text (line 10) | def convert_to_text(html, line_length = 65, from_charset = 'UTF-8')
function word_wrap (line 112) | def word_wrap(txt, line_length)
FILE: lib/premailer/premailer.rb
class Premailer (line 34) | class Premailer
type Warnings (line 131) | module Warnings
method initialize (line 177) | def initialize(html, options = {})
method warnings (line 247) | def warnings
method load_css_from_local_file! (line 254) | def load_css_from_local_file!(path)
method load_css_from_string (line 268) | def load_css_from_string(css_string)
method load_css_from_options! (line 273) | def load_css_from_options! # :nodoc:
method load_css_from_html! (line 286) | def load_css_from_html! # :nodoc:
method local_uri? (line 330) | def local_uri?(uri) # :nodoc:
method media_type_ok? (line 338) | def media_type_ok?(media_types)
method append_query_string (line 344) | def append_query_string(doc, qs)
method is_xhtml? (line 395) | def is_xhtml?
method convert_inline_links (line 410) | def convert_inline_links(doc, base_uri) # :nodoc:
method is_media_query? (line 456) | def self.is_media_query?(media_types)
method escape_string (line 461) | def self.escape_string(str) # :nodoc:
method resolve_link (line 466) | def self.resolve_link(path, base_path) # :nodoc:
method local_data? (line 487) | def self.local_data?(data)
method canonicalize (line 495) | def self.canonicalize(uri) # :nodoc:
method check_client_support (line 508) | def check_client_support # :nodoc:
FILE: lib/premailer/version.rb
class Premailer (line 1) | class Premailer
FILE: test/future_tests.rb
class TestPremailer (line 4) | class TestPremailer < Premailer::TestCase
method test_related_attributes (line 5) | def test_related_attributes
method test_merging_cellpadding (line 28) | def test_merging_cellpadding
method test_preserving_media_queries (line 45) | def test_preserving_media_queries
FILE: test/helper.rb
class Premailer::TestCase (line 6) | class Premailer::TestCase < Minitest::Test
method setup (line 10) | def setup
method default_test (line 23) | def default_test; end
method local_setup (line 26) | def local_setup(f = 'base.html', opts = {})
method remote_setup (line 33) | def remote_setup(f = 'base.html', opts = {})
FILE: test/test_adapter.rb
class TestAdapter (line 3) | class TestAdapter < Premailer::TestCase
method test_default_to_best_available (line 5) | def test_default_to_best_available
method test_settable_via_symbol (line 10) | def test_settable_via_symbol
method test_adapters_are_findable_by_symbol (line 15) | def test_adapters_are_findable_by_symbol
method test_adapters_are_findable_by_class (line 19) | def test_adapters_are_findable_by_class
method test_raises_argument_error (line 23) | def test_raises_argument_error
FILE: test/test_html_to_plain_text.rb
class TestHtmlToPlainText (line 4) | class TestHtmlToPlainText < Premailer::TestCase
method test_to_plain_text_with_fragment (line 7) | def test_to_plain_text_with_fragment
method test_to_plain_text_with_body (line 12) | def test_to_plain_text_with_body
method test_to_plain_text_with_malformed_body (line 26) | def test_to_plain_text_with_malformed_body
method test_specialchars (line 38) | def test_specialchars
method test_stripping_whitespace (line 42) | def test_stripping_whitespace
method test_wrapping_spans (line 50) | def test_wrapping_spans
method test_line_breaks (line 63) | def test_line_breaks
method test_lists (line 68) | def test_lists
method test_stripping_html (line 73) | def test_stripping_html
method test_stripping_ignored_blocks (line 77) | def test_stripping_ignored_blocks
method test_paragraphs_and_breaks (line 89) | def test_paragraphs_and_breaks
method test_headings (line 97) | def test_headings
method test_wrapping_lines (line 106) | def test_wrapping_lines
method test_wrapping_lines_with_spaces (line 117) | def test_wrapping_lines_with_spaces
method test_img_alt_tags (line 121) | def test_img_alt_tags
method test_links (line 135) | def test_links
method test_multiple_links_per_line (line 176) | def test_multiple_links_per_line
method test_links_within_headings (line 183) | def test_links_within_headings
method assert_plaintext (line 188) | def assert_plaintext(out, raw, msg = nil, line_length = 65)
FILE: test/test_links.rb
class TestLinks (line 4) | class TestLinks < Premailer::TestCase
method test_empty_query_string (line 5) | def test_empty_query_string
method test_appending_link_query_string (line 10) | def test_appending_link_query_string
method test_stripping_extra_question_marks_from_query_string (line 62) | def test_stripping_extra_question_marks_from_query_string
method test_unescape_ampersand (line 78) | def test_unescape_ampersand
method test_preserving_links (line 89) | def test_preserving_links
method test_resolving_urls_from_string (line 104) | def test_resolving_urls_from_string
method test_resolving_urls_from_uri (line 113) | def test_resolving_urls_from_uri
method test_resolving_urls_from_html_string (line 132) | def test_resolving_urls_from_html_string
method test_resolving_urls_in_doc (line 142) | def test_resolving_urls_in_doc
method test_convertable_inline_links (line 161) | def test_convertable_inline_links
method test_non_convertable_inline_links (line 177) | def test_non_convertable_inline_links
FILE: test/test_misc.rb
class TestMisc (line 7) | class TestMisc < Premailer::TestCase
method test_parsing_extra_quotes (line 13) | def test_parsing_extra_quotes
method test_styles_in_the_body (line 21) | def test_styles_in_the_body
method test_commented_out_styles_in_the_body (line 37) | def test_commented_out_styles_in_the_body
method test_not_applying_styles_to_the_head (line 53) | def test_not_applying_styles_to_the_head
method test_multiple_identical_ids (line 78) | def test_multiple_identical_ids
method test_preserving_styles (line 98) | def test_preserving_styles
method test_unmergable_rules (line 129) | def test_unmergable_rules
method test_unmergable_media_queries (line 145) | def test_unmergable_media_queries
method test_unmergable_rules_with_no_body (line 182) | def test_unmergable_rules_with_no_body
method test_ignoring_link_pseudo_selectors (line 196) | def test_ignoring_link_pseudo_selectors
method test_parsing_bad_markup_around_tables (line 214) | def test_parsing_bad_markup_around_tables
method test_inline_important (line 240) | def test_inline_important
method test_handling_shorthand_auto_properties (line 258) | def test_handling_shorthand_auto_properties
method test_sorting_style_attributes (line 279) | def test_sorting_style_attributes
method test_removing_scripts (line 296) | def test_removing_scripts
method test_strip_important_from_attributes (line 321) | def test_strip_important_from_attributes
method test_scripts_with_nokogiri (line 339) | def test_scripts_with_nokogiri
method test_style_without_data_in_content (line 370) | def test_style_without_data_in_content
method test_style_with_data_in_content (line 387) | def test_style_with_data_in_content
FILE: test/test_premailer.rb
class TestPremailer (line 5) | class TestPremailer < Premailer::TestCase
method test_special_characters_nokogiri (line 6) | def test_special_characters_nokogiri
method test_special_characters_nokogiri_remote (line 13) | def test_special_characters_nokogiri_remote
method test_special_characters_hpricot (line 29) | def test_special_characters_hpricot
method test_detecting_html (line 36) | def test_detecting_html
method test_detecting_xhtml (line 43) | def test_detecting_xhtml
method test_self_closing_xhtml_tags (line 50) | def test_self_closing_xhtml_tags
method test_non_self_closing_html_tags (line 58) | def test_non_self_closing_html_tags
method test_mailtos_with_query_strings (line 66) | def test_mailtos_with_query_strings
method test_escaping_strings (line 82) | def test_escaping_strings
method test_preserving_ignored_style_elements (line 89) | def test_preserving_ignored_style_elements
method test_preserving_ignored_link_elements (line 97) | def test_preserving_ignored_link_elements
method test_importing_local_css (line 105) | def test_importing_local_css
method test_css_to_attributes (line 118) | def test_css_to_attributes
method test_avoid_changing_css_to_attributes (line 128) | def test_avoid_changing_css_to_attributes
method test_importing_remote_css (line 137) | def test_importing_remote_css
method test_importing_css_as_string (line 149) | def test_importing_css_as_string
method test_local_remote_check (line 164) | def test_local_remote_check
method test_initialize_can_accept_io_object (line 176) | def test_initialize_can_accept_io_object
method test_initialize_can_accept_html_string (line 184) | def test_initialize_can_accept_html_string
method test_initialize_no_escape_attributes_option (line 191) | def test_initialize_no_escape_attributes_option
method test_remove_ids (line 208) | def test_remove_ids
method test_reset_contenteditable (line 228) | def test_reset_contenteditable
method test_carriage_returns_as_entities (line 245) | def test_carriage_returns_as_entities
method test_advanced_selectors (line 260) | def test_advanced_selectors
method test_premailer_related_attributes (line 271) | def test_premailer_related_attributes
method test_include_link_tags_option (line 288) | def test_include_link_tags_option
method test_include_style_tags_option (line 298) | def test_include_style_tags_option
method test_input_encoding (line 308) | def test_input_encoding
method test_output_encoding (line 316) | def test_output_encoding
method test_meta_encoding_downcase (line 324) | def test_meta_encoding_downcase
method test_meta_encoding_upcase (line 331) | def test_meta_encoding_upcase
method test_htmlentities (line 338) | def test_htmlentities
method test_line_starting_with_uri_in_html_with_linked_css (line 348) | def test_line_starting_with_uri_in_html_with_linked_css
method test_empty_html_nokogiri (line 356) | def test_empty_html_nokogiri
method silence_stderr (line 364) | def silence_stderr(&block)
FILE: test/test_warnings.rb
class TestWarnings (line 4) | class TestWarnings < Premailer::TestCase
method test_element_warnings (line 5) | def test_element_warnings
method test_css_warnings (line 24) | def test_css_warnings
method test_css_aliased_warnings (line 40) | def test_css_aliased_warnings
method test_attribute_warnings (line 55) | def test_attribute_warnings
method test_warn_level (line 70) | def test_warn_level
method get_warnings (line 90) | def get_warnings(html, adapter = :nokogiri, warn_level = Premailer::Wa...
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (136K chars).
[
{
"path": ".gitignore",
"chars": 87,
"preview": ".DS_Store\n*.gem\nbin/*.html\nhtml/\nvendor/\ndoc/\n.yardoc/\n*.sw?\npkg/\n.bundle/\n*.sublime-*\n"
},
{
"path": ".jrubyrc",
"chars": 18,
"preview": "cext.enabled=true\n"
},
{
"path": ".travis.yml",
"chars": 157,
"preview": "cache: bundler\nsudo: false\nbranches:\n only: master\nmatrix:\n fast_finish: true\nbefore_install: rm Gemfile.lock\nrvm:\n -"
},
{
"path": ".yardopts",
"chars": 151,
"preview": "--markup markdown\n--markup-provider redcarpet\n--charset utf-8\n--no-private\n--readme README.md\n--title \"Premailer Documen"
},
{
"path": "Gemfile",
"chars": 157,
"preview": "source \"https://rubygems.org\"\n\ngem 'css_parser', :git => 'git://github.com/premailer/css_parser.git'\n\nplatforms :jruby d"
},
{
"path": "LICENSE.md",
"chars": 1505,
"preview": "# Premailer License\n\nCopyright (c) 2007-2012, Alex Dunae. All rights reserved.\n\nRedistribution and use in source and bi"
},
{
"path": "README.md",
"chars": 3821,
"preview": "# Premailer README [](https://travis-ci.org/"
},
{
"path": "Rakefile",
"chars": 1586,
"preview": "require 'bundler/setup'\nrequire 'rake/testtask'\nrequire \"bundler/gem_tasks\"\nrequire 'yard'\n\nGEM_ROOT = File.dirname(__FI"
},
{
"path": "bin/premailer",
"chars": 125,
"preview": "#!/usr/bin/env ruby\n\n# This binary used in rubygems environment only as part of installed gem\n\nrequire 'premailer/execut"
},
{
"path": "lib/premailer/adapter/hpricot.rb",
"chars": 7436,
"preview": "require 'hpricot'\n\nclass Premailer\n module Adapter\n # Hpricot adapter\n module Hpricot\n\n # Merge CSS into the"
},
{
"path": "lib/premailer/adapter/nokogiri.rb",
"chars": 9374,
"preview": "require 'nokogiri'\n\nclass Premailer\n module Adapter\n # Nokogiri adapter\n module Nokogiri\n\n # Merge CSS into "
},
{
"path": "lib/premailer/adapter/nokogumbo.rb",
"chars": 9193,
"preview": "require 'nokogumbo'\n\nclass Premailer\n module Adapter\n # Nokogiri adapter\n module Nokogumbo\n\n # Merge CSS int"
},
{
"path": "lib/premailer/adapter.rb",
"chars": 1812,
"preview": "\n\nclass Premailer\n # Manages the adapter classes. Currently supports:\n #\n # * nokogiri\n # * hpricot\n module Adapter"
},
{
"path": "lib/premailer/executor.rb",
"chars": 2730,
"preview": "require 'optparse'\nrequire 'premailer'\n\n# defaults\noptions = {\n :base_url => nil,\n :link_query_string => nil,\n :remov"
},
{
"path": "lib/premailer/html_to_plain_text.rb",
"chars": 3562,
"preview": "# coding: utf-8\nrequire 'htmlentities'\n\n# Support functions for Premailer\nmodule HtmlToPlainText\n\n # Returns the text i"
},
{
"path": "lib/premailer/premailer.rb",
"chars": 19754,
"preview": "# Premailer processes HTML and CSS to improve e-mail deliverability.\n#\n# Premailer's main function is to render all CSS "
},
{
"path": "lib/premailer/version.rb",
"chars": 70,
"preview": "class Premailer\n # Premailer version.\n VERSION = '1.8.6'.freeze\nend\n"
},
{
"path": "lib/premailer.rb",
"chars": 188,
"preview": "require 'yaml'\nrequire 'open-uri'\nrequire 'digest/md5'\nrequire 'cgi'\nrequire 'css_parser'\n\nrequire 'premailer/adapter'\nr"
},
{
"path": "misc/client_support.yaml",
"chars": 6957,
"preview": "# Capabilities of e-mail clients\n#\n# Sources\n# * http://campaignmonitor.com/css/\n# * http://www.campaignmonitor.com/blog"
},
{
"path": "premailer.gemspec",
"chars": 1157,
"preview": "require './lib/premailer/version'\n\nGem::Specification.new \"premailer\", Premailer::VERSION do |s|\n s.summary = \"Preflig"
},
{
"path": "test/files/base.html",
"chars": 5676,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n<!--\n\n You can read this new"
},
{
"path": "test/files/chars.html",
"chars": 122,
"preview": "<!DOCTYPE html>\n<html>\n<body>\n<p>cédille cé & garçon garçon à à & ©</p>\n</body>\n</htm"
},
{
"path": "test/files/html4.html",
"chars": 625,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n \"http://www.w3.org/TR/html4/strict.dtd\">\n<html>\n<head>\n <title>Tit"
},
{
"path": "test/files/html_with_uri.html",
"chars": 202,
"preview": "<html>\n <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\">\n <body>\n <p>\n The following line should not "
},
{
"path": "test/files/ignore.css",
"chars": 26,
"preview": "body {\n color: orange;\n}\n"
},
{
"path": "test/files/ignore.html",
"chars": 359,
"preview": "<!doctype html>\n<html>\n <head>\n <title>Should ignore link and style elements with data attribute</title>\n <link r"
},
{
"path": "test/files/import.css",
"chars": 186,
"preview": "/*\n * Premailer styles - should import\n *\n * $Package: Premailer $\n * $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb 2009"
},
{
"path": "test/files/iso-8859-2.html",
"chars": 70,
"preview": "<body>In Hungary we use some special accented characters: .</body>\n"
},
{
"path": "test/files/iso-8859-5.html",
"chars": 126,
"preview": "<html>\n<head>\n<meta http-equiv=\"Content-type\" content=\"text/html; charset=iso-8859-5\">\n</head>\n<body>\n<p> </p>\n</body>\n<"
},
{
"path": "test/files/no_css.html",
"chars": 313,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n<html>\n<head>\n\t<meta http-equ"
},
{
"path": "test/files/noimport.css",
"chars": 207,
"preview": "/*\n * Premailer styles - should not import\n *\n * $Package: Premailer $\n * $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb "
},
{
"path": "test/files/styles.css",
"chars": 2809,
"preview": "/*\n * Premailer styles\n *\n * $Package: Premailer $\n * $Date: 2009-02-09 17:15:56 -0800 (Mon, 09 Feb 2009) $WCDATE$ $\n * "
},
{
"path": "test/files/xhtml.html",
"chars": 654,
"preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\""
},
{
"path": "test/future_tests.rb",
"chars": 1583,
"preview": "# encoding: UTF-8\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestPremailer < Premailer::TestCas"
},
{
"path": "test/helper.rb",
"chars": 1097,
"preview": "require 'bundler/setup'\nrequire 'maxitest/autorun'\nrequire 'webmock/minitest'\nrequire 'premailer'\n\nclass Premailer::Test"
},
{
"path": "test/test_adapter.rb",
"chars": 842,
"preview": "require File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestAdapter < Premailer::TestCase\n\n def test_defaul"
},
{
"path": "test/test_html_to_plain_text.rb",
"chars": 7304,
"preview": "# encoding: utf-8\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestHtmlToPlainText < Premailer::T"
},
{
"path": "test/test_links.rb",
"chars": 7491,
"preview": "# encoding: UTF-8\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestLinks < Premailer::TestCase\n "
},
{
"path": "test/test_misc.rb",
"chars": 12068,
"preview": "# encoding: UTF-8\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\n# Random tests for specific issues.\n#\n# "
},
{
"path": "test/test_premailer.rb",
"chars": 14267,
"preview": "# -*- encoding: UTF-8 -*-\n\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestPremailer < Premailer"
},
{
"path": "test/test_warnings.rb",
"chars": 2596,
"preview": "# encoding: UTF-8\nrequire File.expand_path(File.dirname(__FILE__)) + '/helper'\n\nclass TestWarnings < Premailer::TestCase"
}
]
About this extraction
This page contains the full source code of the alexdunae/premailer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (125.5 KB), approximately 36.9k tokens, and a symbol index with 163 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.