pax_global_header 0000666 0000000 0000000 00000000064 13202637005 0014510 g ustar 00root root 0000000 0000000 52 comment=aae3be75a543e7292beadc43619ce882348845fd
premailer-1.11.1/ 0000775 0000000 0000000 00000000000 13202637005 0013551 5 ustar 00root root 0000000 0000000 premailer-1.11.1/.editorconfig 0000664 0000000 0000000 00000000246 13202637005 0016230 0 ustar 00root root 0000000 0000000 # editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
premailer-1.11.1/.gitignore 0000664 0000000 0000000 00000000140 13202637005 0015534 0 ustar 00root root 0000000 0000000 .DS_Store
/*.gem
/bin/*.html
/html/
/vendor/
/doc/
/.yardoc/
*.sw?
/pkg/
/.bundle/
/*.sublime-*
premailer-1.11.1/.jrubyrc 0000664 0000000 0000000 00000000022 13202637005 0015224 0 ustar 00root root 0000000 0000000 cext.enabled=true
premailer-1.11.1/.travis.yml 0000664 0000000 0000000 00000000254 13202637005 0015663 0 ustar 00root root 0000000 0000000 cache: bundler
sudo: false
branches:
only: master
matrix:
fast_finish: true
before_install: rm Gemfile.lock
language: ruby
rvm:
- 2.1.9
- 2.2.6
- 2.3.3
- 2.4.0
premailer-1.11.1/.yardopts 0000664 0000000 0000000 00000000227 13202637005 0015420 0 ustar 00root root 0000000 0000000 --markup markdown
--markup-provider redcarpet
--charset utf-8
--no-private
--readme README.md
--title "Premailer Documentation"
-
README.md
LICENSE.md
premailer-1.11.1/CHANGELOG.md 0000664 0000000 0000000 00000001336 13202637005 0015365 0 ustar 00root root 0000000 0000000 ## Premailer CHANGELOG
### Version 1.11.1
* Fix input encoding in nokogiri adapters.
### Version 1.11.0
* Support for HTML fragments rendering (without enforcing of doctype, head, body). See :html_fragment option.
* Depends on css_parser 1.6.0.
### Version 1.10.4
* Exponential regexp in convert_to_text fixed.
### Version 1.10.3
* Keep consecutive whitespaces.
* Depends on css_parser 1.5.0.
### Version 1.10.2
* Fix LoadError addressable with Addressable 2.3.8
### Version 1.10.1
* Depends on css_parser 1.4.10.
* Drops wrong destructive sorting of attributes (`css_parser` already does it correctly)
* Replace obsolete `URI` calls with `Addressable::URI`.
* Drop last semicolon from attributes.
* Update tests.
premailer-1.11.1/Gemfile 0000664 0000000 0000000 00000000401 13202637005 0015037 0 ustar 00root root 0000000 0000000 # Keep Gemfile.lock from repo. Reason: https://grosser.it/2015/08/14/check-in-your-gemfile-lock/
source "https://rubygems.org"
gem 'css_parser', :git => 'https://github.com/premailer/css_parser.git'
platforms :jruby do
gem 'jruby-openssl'
end
gemspec
premailer-1.11.1/Gemfile.lock 0000664 0000000 0000000 00000002746 13202637005 0016004 0 ustar 00root root 0000000 0000000 GIT
remote: https://github.com/premailer/css_parser.git
revision: 6fc4fc316ed411082ae3733f96b796b4ab27528b
specs:
css_parser (1.6.0)
addressable
PATH
remote: .
specs:
premailer (1.11.1)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
GEM
remote: https://rubygems.org/
specs:
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
coveralls (0.8.21)
json (>= 1.8, < 3)
simplecov (~> 0.14.1)
term-ansicolor (~> 1.3)
thor (~> 0.19.4)
tins (~> 1.6)
crack (0.4.3)
safe_yaml (~> 1.0.0)
docile (1.1.5)
hashdiff (0.3.7)
htmlentities (4.3.4)
json (2.1.0)
maxitest (2.4.0)
minitest (>= 5.0.0, < 5.11.0)
mini_portile2 (2.3.0)
minitest (5.10.3)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13)
nokogiri
public_suffix (3.0.1)
rake (12.2.1)
redcarpet (3.4.0)
safe_yaml (1.0.4)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
term-ansicolor (1.6.0)
tins (~> 1.0)
thor (0.19.4)
tins (1.15.1)
webmock (3.1.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
yard (0.9.9)
PLATFORMS
ruby
DEPENDENCIES
bundler (>= 1.3)
coveralls
css_parser!
jruby-openssl
maxitest
nokogiri (~> 1.7)
nokogumbo
premailer!
rake (> 0.8, != 0.9.0)
redcarpet (~> 3.0)
webmock
yard
BUNDLED WITH
1.16.0
premailer-1.11.1/LICENSE.md 0000664 0000000 0000000 00000002741 13202637005 0015161 0 ustar 00root root 0000000 0000000 # Premailer License
Copyright (c) 2007-2017, 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.
premailer-1.11.1/README.md 0000664 0000000 0000000 00000010251 13202637005 0015027 0 ustar 00root root 0000000 0000000 # Premailer README [](https://travis-ci.org/premailer/premailer) [](https://badge.fury.io/rb/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)
## Installation
Install the Premailer gem from RubyGems.
```bash
gem install premailer
```
or add it to your `Gemfile` and run `bundle`.
## Example
```ruby
require 'premailer'
premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
# Write the plain-text output
# This must come before to_inline_css (https://github.com/premailer/premailer/issues/201)
File.open("output.txt", "w") do |fout|
fout.puts premailer.to_plain_text
end
# Write the HTML output
File.open("output.html", "w") do |fout|
fout.puts premailer.to_inline_css
end
# Output any CSS warnings
premailer.warnings.each do |w|
puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
end
```
## Adapters
Premailer's default adapter is nokogiri if both nokogiri and nokogumbo are included in the Gemfile list. However, if you want to use a different adapter, you can choose to.
There are three adapters in total (as of premailer 1.10.0)
1. nokogiri (default)
2. nokogiri_fast
3. nokogumbo
hpricot adapter removed due to its EOL, please use `~>1.9.0` version if You still need it..
`NokogiriFast` adapter improves the Algorithmic complexity of the running time by 20x with a slight compensation on memory. To switch to any of these adapters, add the following line. For example, if you want to include the `NokogiriFast` adapter,
```ruby
Premailer::Adapter.use = :nokogiri_fast
```
## Ruby Compatibility
Premailer is tested on Ruby 2.1 and above. JRuby support is close; contributors are welcome. Checkout the latest build status on the [Travis CI dashboard](https://travis-ci.org/#!/premailer/premailer).
## Premailer-specific CSS
Premailer looks for a few CSS attributes that make working with tables a bit easier.
| CSS Attribute | Availability |
| ------------- | ------------ |
| -premailer-width | Available on `table`, `th` and `td` elements |
| -premailer-height | Available on `table`, `tr`, `th` and `td` elements |
| -premailer-cellpadding | Available on `table` elements |
| -premailer-cellspacing | Available on `table` elements |
| data-premailer="ignore" | Available on `link` and `style` elements. Premailer will ignore these elements entirely. |
Each of these CSS declarations will be copied to appropriate element's attribute.
For example
```css
table { -premailer-cellspacing: 5; -premailer-width: 500; }
```
will result in
```html
```
## Contributions
Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC. Fork and patch to your heart's content. Please don't increment the version numbers, though.
A few areas that are particularly in need of love:
* Improved test coverage
* Move un-repeated background images defined in CSS for Outlook
## Credits and code
Thanks to [all the wonderful contributors](https://github.com/premailer/premailer/contributors) for their updates.
Thanks to [Greenhood + Company](http://www.greenhood.com/) for sponsoring some of the 1.5.6 updates,
and to [Campaign Monitor](https://www.campaignmonitor.com/) for supporting the web interface.
The source code can be found on [GitHub](https://github.com/premailer/premailer).
Copyright by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-2017. See [LICENSE.md](https://github.com/premailer/premailer/blob/master/LICENSE.md) for license details.
premailer-1.11.1/Rakefile 0000664 0000000 0000000 00000003062 13202637005 0015217 0 ustar 00root root 0000000 0000000 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]
premailer-1.11.1/bin/ 0000775 0000000 0000000 00000000000 13202637005 0014321 5 ustar 00root root 0000000 0000000 premailer-1.11.1/bin/premailer 0000775 0000000 0000000 00000000175 13202637005 0016232 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby
# This binary used in rubygems environment only as part of installed gem
require 'premailer/executor'
premailer-1.11.1/lib/ 0000775 0000000 0000000 00000000000 13202637005 0014317 5 ustar 00root root 0000000 0000000 premailer-1.11.1/lib/premailer.rb 0000664 0000000 0000000 00000000375 13202637005 0016631 0 ustar 00root root 0000000 0000000 require 'yaml'
require 'open-uri'
require 'digest/md5'
require 'cgi'
require 'addressable/uri'
require 'css_parser'
require 'premailer/adapter'
require 'premailer/adapter/rgb_to_hex'
require 'premailer/html_to_plain_text'
require 'premailer/premailer'
premailer-1.11.1/lib/premailer/ 0000775 0000000 0000000 00000000000 13202637005 0016277 5 ustar 00root root 0000000 0000000 premailer-1.11.1/lib/premailer/adapter.rb 0000664 0000000 0000000 00000003505 13202637005 0020247 0 ustar 00root root 0000000 0000000 class Premailer
# Manages the adapter classes. Currently supports:
#
# * nokogiri
# * nokogiri_fast
# * nokogumbo
module Adapter
autoload :Nokogiri, 'premailer/adapter/nokogiri'
autoload :NokogiriFast, 'premailer/adapter/nokogiri_fast'
autoload :Nokogumbo, 'premailer/adapter/nokogumbo'
# adapter to required file mapping.
REQUIREMENT_MAP = [
["nokogiri", :nokogiri],
["nokogiri", :nokogiri_fast],
["nokogumbo", :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 :nokogiri if defined?(::Nokogiri)
return :nokogiri_fast if defined?(::NokogiriFast)
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 nokogiri or nokogumbo")
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
premailer-1.11.1/lib/premailer/adapter/ 0000775 0000000 0000000 00000000000 13202637005 0017717 5 ustar 00root root 0000000 0000000 premailer-1.11.1/lib/premailer/adapter/nokogiri.rb 0000664 0000000 0000000 00000022375 13202637005 0022076 0 ustar 00root root 0000000 0000000 require 'nokogiri'
class Premailer
module Adapter
# Nokogiri adapter
module Nokogiri
include AdapterHelper::RgbToHex
# 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|
if el[html_att].nil? and not merged[css_att].empty?
new_html_att = merged[css_att].gsub(/url\(['"](.*)['"]\)/, '\1').gsub(/;$|\s*!important/, '').strip
el[html_att] = css_att.end_with?('color') && @options[:rgb_to_hex_attributes] ? ensure_hex(new_html_att) : new_html_att
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
end
# Collapse multiple rules into one as much as possible.
merged.create_shorthand! if @options[:create_shorthands]
# write the inline STYLE attribute
el['style'] = merged.declarations_to_s
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.traverse do |el|
if el.comment? and @options[:remove_comments]
el.remove
elsif el.element?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
@processed_doc = doc
if is_xhtml?
# we don't want to encode carriage returns
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
else
@processed_doc.to_html(:encoding => @options[:output_encoding])
end
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the head.
#
# doc is an Nokogiri document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Nokogiri::XML] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
if @options[:html_fragment]
style_tag = ::Nokogiri::XML::Node.new("style", doc)
style_tag.content = styles
doc.add_child(style_tag)
else
style_tag = doc.create_element "style", "#{styles}"
head = doc.at_css('head')
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element "head") if doc.root && doc.root.first_element_child
head ||= doc.add_child(doc.create_element "head")
head << style_tag
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] 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.
encoding = if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
thing = thing.force_encoding(@options[:input_encoding]).encode!
@options[:input_encoding]
else
@options[:input_encoding] || (RUBY_PLATFORM == 'java' ? nil : 'BINARY')
end
doc = if @options[:html_fragment]
::Nokogiri::HTML.fragment(thing, encoding)
else
::Nokogiri::HTML(thing, nil, 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
premailer-1.11.1/lib/premailer/adapter/nokogiri_fast.rb 0000664 0000000 0000000 00000033575 13202637005 0023117 0 ustar 00root root 0000000 0000000 require 'nokogiri'
class Premailer
module Adapter
# NokogiriFast adapter
module NokogiriFast
include AdapterHelper::RgbToHex
# 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
# Create an index for nodes by tag name/id/class
# Also precompute the map of nodes to descendants
index, all_nodes, descendants = make_index(doc)
# 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
# Try the new index based technique. If not supported, fall back to the old brute force one.
nodes = match_selector(index, all_nodes, descendants, selector) || doc.search(selector)
nodes.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
doc.search("script").remove if @options[:remove_scripts]
# 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|
if el[html_att].nil? and not merged[css_att].empty?
new_html_att = merged[css_att].gsub(/url\(['"](.*)['"]\)/, '\1').gsub(/;$|\s*!important/, '').strip
el[html_att] = css_att.end_with?('color') && @options[:rgb_to_hex_attributes] ? ensure_hex(new_html_att) : new_html_att
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
end
# Collapse multiple rules into one as much as possible.
merged.create_shorthand! if @options[:create_shorthands]
# write the inline STYLE attribute
el['style'] = merged.declarations_to_s
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.traverse do |el|
if el.comment? and @options[:remove_comments]
el.remove
elsif el.element?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
@processed_doc = doc
if is_xhtml?
# we don't want to encode carriage returns
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
else
@processed_doc.to_html(:encoding => @options[:output_encoding])
end
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the head.
#
# doc is an Nokogiri document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Nokogiri::XML] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
if @options[:html_fragment]
style_tag = ::Nokogiri::XML::Node.new("style", doc)
style_tag.content = styles
doc.add_child(style_tag)
else
style_tag = doc.create_element "style", styles
head = doc.at_css('head')
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element "head") if doc.root && doc.root.first_element_child
head ||= doc.add_child(doc.create_element "head")
head << style_tag
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] 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.
encoding = if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
thing = thing.force_encoding(@options[:input_encoding]).encode!
@options[:input_encoding]
else
@options[:input_encoding] || (RUBY_PLATFORM == 'java' ? nil : 'BINARY')
end
doc = if @options[:html_fragment]
::Nokogiri::HTML.fragment(thing, encoding)
else
::Nokogiri::HTML(thing, nil, 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
private
# For very large documents, it is useful to trade off some memory for performance.
# We can build an index of the nodes so we can quickly select by id/class/tagname
# instead of search the tree again and again.
#
# @param page The Nokogiri HTML document to index.
# @return [index, set_of_all_nodes, descendants] The index is a hash from key to set of nodes.
# The "descendants" is a hash mapping a node to the set of its descendant nodes.
def make_index(page)
index = {} # Contains a map of tag/class/id names to set of nodes.
all_nodes = [] # A plain array of all nodes in the doc. The superset.
descendants = {} # Maps node -> set of descendants
page.traverse do |node|
all_nodes.push(node)
if node != page then
index_ancestry(page, node, node.parent, descendants)
end
# Index the node by tag name. This is the least selective
# of the three index types empirically.
index[node.name] = (index[node.name] || Set.new).add(node)
# Index the node by all class attributes it possesses.
# Classes are modestly selective. Usually more than tag names
# but less selective than ids.
if node.has_attribute?("class") then
node.get_attribute("class").split(/\s+/).each do |c|
c = '.' + c
index[c] = (index[c] || Set.new).add(node)
end
end
# Index the node by its "id" attribute if it has one.
# This is usually the most selective of the three.
if node.has_attribute?("id") then
id = '#' + node.get_attribute("id")
index[id] = (index[id] || Set.new).add(node)
end
end
# If an index key isn't there, then we should treat it as an empty set.
# This makes the index total and we don't need to special case presence.
# Note that the default value will never be modified. So we don't need
# default_proc.
index.default = Set.new
descendants.default = Set.new
return index, Set.new(all_nodes), descendants
end
# @param doc The top level document
# @param elem The element whose ancestry is to be captured
# @param parent the current parent in the process of capturing. Should be set to elem.parent for starters.
# @param descendants The running hash map of node -> set of nodes that maps descendants of a node.
# @return The descendants argument after updating it.
def index_ancestry(doc, elem, parent, descendants)
if parent then
descendants[parent] = (descendants[parent] || Set.new).add(elem)
if doc != parent then
index_ancestry(doc, elem, parent.parent, descendants)
end
end
descendants
end
# @param index An index hash returned by make_index
# @param base The base set of nodes within which the given spec is to be matched.
# @param intersection_selector A CSS intersection selector string of the form
# "hello.world" or "#blue.diamond". This should not contain spaces.
# @return Set of nodes matching the given spec that are present in the base set.
def narrow_down_nodes(index, base, intersection_selector)
intersection_selector.split(/(?=[.#])/).reduce(base) do |acc, sel|
acc = index[sel].intersection(acc)
acc
end
end
# @param index An index returned by make_index
# @param allNodes The set of all nodes in the DOM to search
# @param selector A simple CSS tree matching selector of the form "div.container p.item span"
# @return Set of matching nodes
#
# Note that fancy CSS selector syntax is not supported. Anything
# not matching the regex /^[-a-zA-Z0-9\s_.#]*$/ should not be passed.
# It will return nil when such a selector is passed, so you can take
# action on the falsity of the return value.
def match_selector(index, all_nodes, descendants, selector)
if /[^-a-zA-Z0-9_\s.#]/.match(selector) then
return nil
end
take_children = false
selector.split(/\s+/).reduce(all_nodes) do |base, spec|
desc = base
if take_children then
desc = Set.new
base.each do |n|
desc.merge(descendants[n])
end
else
take_children = true
end
narrow_down_nodes(index, desc, spec)
end
end
end
end
end
premailer-1.11.1/lib/premailer/adapter/nokogumbo.rb 0000664 0000000 0000000 00000022100 13202637005 0022237 0 ustar 00root root 0000000 0000000 require 'nokogumbo'
class Premailer
module Adapter
# Nokogiri adapter
module Nokogumbo
include AdapterHelper::RgbToHex
# 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|
if el[html_att].nil? and not merged[css_att].empty?
new_html_att = merged[css_att].gsub(/url\(['"](.*)['"]\)/, '\1').gsub(/;$|\s*!important/, '').strip
el[html_att] = css_att.end_with?('color') && @options[:rgb_to_hex_attributes] ? ensure_hex(new_html_att) : new_html_att
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_att)
end
end
end
end
# Collapse multiple rules into one as much as possible.
merged.create_shorthand! if @options[:create_shorthands]
# write the inline STYLE attribute
el['style'] = merged.declarations_to_s
end
doc = write_unmergable_css_rules(doc, @unmergable_rules)
if @options[:remove_classes] or @options[:remove_comments]
doc.traverse do |el|
if el.comment? and @options[:remove_comments]
el.remove
elsif el.element?
el.remove_attribute('class') if @options[:remove_classes]
end
end
end
if @options[:remove_ids]
# find all anchor's targets and hash them
targets = []
doc.search("a[@href^='#']").each do |el|
target = el.get_attribute('href')[1..-1]
targets << target
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
end
# hash ids that are links target, delete others
doc.search("*[@id]").each do |el|
id = el.get_attribute('id')
if targets.include?(id)
el.set_attribute('id', Digest::MD5.hexdigest(id))
else
el.remove_attribute('id')
end
end
end
if @options[:reset_contenteditable]
doc.search('*[@contenteditable]').each do |el|
el.remove_attribute('contenteditable')
end
end
@processed_doc = doc
if is_xhtml?
# we don't want to encode carriage returns
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
else
@processed_doc.to_html(:encoding => @options[:output_encoding])
end
end
# Create a style element with un-mergable rules (e.g. :hover)
# and write it into the head.
#
# doc is an Nokogiri document and unmergable_css_rules is a Css::RuleSet.
#
# @return [::Nokogiri::XML] a document.
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
styles = unmergable_rules.to_s
unless styles.empty?
if @options[:html_fragment]
style_tag = ::Nokogiri::XML::Node.new("style", doc)
style_tag.content = styles
doc.add_child(style_tag)
else
style_tag = doc.create_element "style", styles
head = doc.at_css('head')
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element "head") if doc.root && doc.root.first_element_child
head ||= doc.add_child(doc.create_element "head")
head << style_tag
end
end
doc
end
# Converts the HTML document to a format suitable for plain-text e-mail.
#
# If present, uses the element as its base; otherwise uses the whole document.
#
# @return [String] 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!
end
doc = if @options[:html_fragment]
::Nokogiri::HTML5(thing)
else
::Nokogiri::HTML5.fragment(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
premailer-1.11.1/lib/premailer/adapter/rgb_to_hex.rb 0000664 0000000 0000000 00000001473 13202637005 0022371 0 ustar 00root root 0000000 0000000 # RGB helper for adapters, currently only nokogiri supported
module AdapterHelper
module RgbToHex
def to_hex(str)
str.to_i.to_s(16).rjust(2, '0').upcase
end
def is_rgb?(color)
pattern = %r{
rgb
\(\s* # literal open, with optional whitespace
(\d{1,3}) # capture 1-3 digits
\s*,\s* # comma, with optional whitespace
(\d{1,3}) # capture 1-3 digits
\s*,\s* # comma, with optional whitespace
(\d{1,3}) # capture 1-3 digits
\s*\) # literal close, with optional whitespace
}x
pattern.match(color)
end
def ensure_hex(color)
match_data = is_rgb?(color)
if match_data
"#{to_hex(match_data[1])}#{to_hex(match_data[2])}#{to_hex(match_data[3])}"
else
color
end
end
end
end
premailer-1.11.1/lib/premailer/executor.rb 0000664 0000000 0000000 00000005646 13202637005 0020475 0 ustar 00root root 0000000 0000000 require 'optparse'
require 'premailer'
# defaults
options = {
:base_url => nil,
:link_query_string => nil,
:remove_classes => false,
:verbose => false,
:line_length => 65,
:adapter => :nokogiri,
}
mode = :html
opts = OptionParser.new do |opts|
opts.banner = "Improve the rendering of HTML emails by making CSS inline among other things. Takes a path to a local file, a URL or a pipe as input.\n\n"
opts.define_head "Usage: premailer [options]"
opts.separator ""
opts.separator "Examples:"
opts.separator " premailer http://example.com/ > out.html"
opts.separator " premailer http://example.com/ --mode txt > out.txt"
opts.separator " cat input.html | premailer -q src=email > out.html"
opts.separator " premailer ./public/index.html"
opts.separator ""
opts.separator "Options:"
opts.on("--mode MODE", [:html, :txt], "Output: html or txt") do |v|
mode = v
end
opts.on("--adapter ADAPTER", [:nokogiri, :nokogiri_fast, :nokogumbo], "Adapter: nokogiri, nokogiri_fast or nokogumbo (default: #{options[:adapter]}") do |v|
options[:adapter] = 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
options[:remove_classes] = true
end
opts.on("-j", "--remove-scripts", "Remove
content
END_HTML
[:nokogiri, :nokogiri_fast, :nokogumbo].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, :nokogiri_fast, :nokogumbo].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :remove_scripts => false, :adapter => adapter)
premailer.to_inline_css
assert_equal 1, premailer.processed_doc.search('script').length
end
end
def test_strip_important_from_attributes
html = <
END_HTML
[:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter|
premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter)
assert_match 'bgcolor="#FF0000"', premailer.to_inline_css
end
end
def test_scripts_with_nokogiri
html = <
END_HTML
premailer = Premailer.new(html, :with_html_string => true, :remove_scripts => false, :adapter => :nokogiri)
premailer.to_inline_css
assert !premailer.processed_doc.css('script[type="application/ld+json"]').first.children.first.cdata?
end
def test_style_without_data_in_content
html = <