pax_global_header 0000666 0000000 0000000 00000000064 13732137630 0014517 g ustar 00root root 0000000 0000000 52 comment=be1634c542910f729d0070f5be2a36de97eb0d48
premailer-1.14.2/ 0000775 0000000 0000000 00000000000 13732137630 0013564 5 ustar 00root root 0000000 0000000 premailer-1.14.2/.editorconfig 0000664 0000000 0000000 00000000246 13732137630 0016243 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.14.2/.gitignore 0000664 0000000 0000000 00000000140 13732137630 0015547 0 ustar 00root root 0000000 0000000 .DS_Store
/*.gem
/bin/*.html
/html/
/vendor/
/doc/
/.yardoc/
*.sw?
/pkg/
/.bundle/
/*.sublime-*
premailer-1.14.2/.jrubyrc 0000664 0000000 0000000 00000000022 13732137630 0015237 0 ustar 00root root 0000000 0000000 cext.enabled=true
premailer-1.14.2/.travis.yml 0000664 0000000 0000000 00000000257 13732137630 0015701 0 ustar 00root root 0000000 0000000 cache: bundler
branches:
only: master
matrix:
fast_finish: true
language: ruby
before_install:
- gem update --system
- gem update bundler
rvm:
- 2.5
- 2.6
- 2.7
premailer-1.14.2/.yardopts 0000664 0000000 0000000 00000000227 13732137630 0015433 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.14.2/CHANGELOG.md 0000664 0000000 0000000 00000002045 13732137630 0015376 0 ustar 00root root 0000000 0000000 ## Premailer CHANGELOG
### Verion 1.14.1
* Fix to converting inline html 100px to 100
### Verion 1.14.0
* Convert inline html 100px to 100
### Verion 1.13.1
* Replace deprecated File.exists? with File.exist? (fixes Ruby 2.8 deprecation warning)
### Verion 1.13.0
* Fix URI.open deprecation warnings
### Version 1.12.1
* Fix greedy script regex.
### 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.14.2/Gemfile 0000664 0000000 0000000 00000000346 13732137630 0015062 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'
platforms :jruby do
gem 'jruby-openssl'
end
group :development, :test do
gem 'pry'
end
gemspec
premailer-1.14.2/Gemfile.lock 0000664 0000000 0000000 00000003010 13732137630 0016000 0 ustar 00root root 0000000 0000000 PATH
remote: .
specs:
premailer (1.14.2)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
GEM
remote: https://rubygems.org/
specs:
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
bump (0.9.0)
coderay (1.1.2)
coveralls (0.8.23)
json (>= 1.8, < 3)
simplecov (~> 0.16.1)
term-ansicolor (~> 1.3)
thor (>= 0.19.4, < 2.0)
tins (~> 1.6)
crack (0.4.3)
safe_yaml (~> 1.0.0)
css_parser (1.7.1)
addressable
docile (1.3.2)
hashdiff (1.0.0)
htmlentities (4.3.4)
json (2.2.0)
maxitest (3.3.0)
minitest (>= 5.0.0, < 5.12.0)
method_source (0.9.2)
mini_portile2 (2.3.0)
minitest (5.11.3)
nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (4.0.1)
rake (12.3.3)
redcarpet (3.5.0)
safe_yaml (1.0.5)
simplecov (0.16.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
term-ansicolor (1.7.1)
tins (~> 1.0)
thor (0.20.3)
tins (1.21.1)
webmock (3.7.5)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
PLATFORMS
ruby
DEPENDENCIES
bump
bundler (>= 1.3)
coveralls
jruby-openssl
maxitest
nokogiri (~> 1.8.2)
nokogumbo
premailer!
pry
rake (> 0.8, != 0.9.0)
redcarpet (~> 3.0)
webmock
BUNDLED WITH
2.1.4
premailer-1.14.2/LICENSE.md 0000664 0000000 0000000 00000002741 13732137630 0015174 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.14.2/README.md 0000664 0000000 0000000 00000011436 13732137630 0015050 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](https://premailer.github.io/premailer/HtmlToPlainText.html) 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
See .travis.yml for which ruby versions are tested. 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 |
| -premailer-align | 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
```
## Configuration options
The behavior of Premailer can be configured by passing options in the initializer.
For example, the following will accept HTML from a string and will exclude unmergeable css from being added to the `` of the output document.
```ruby
premailer = Premailer.new(html_string, with_html_string: true, drop_unmergeable_css_rules: true)
```
[See here for a full list of the available options](https://premailer.github.io/premailer/Premailer.html#initialize-instance_method).
## 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.14.2/Rakefile 0000664 0000000 0000000 00000002702 13732137630 0015232 0 ustar 00root root 0000000 0000000 require 'bundler/setup'
require 'rake/testtask'
require "bundler/gem_tasks"
require 'bump/tasks'
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
task :default => [:test]
premailer-1.14.2/bin/ 0000775 0000000 0000000 00000000000 13732137630 0014334 5 ustar 00root root 0000000 0000000 premailer-1.14.2/bin/premailer 0000775 0000000 0000000 00000000175 13732137630 0016245 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.14.2/lib/ 0000775 0000000 0000000 00000000000 13732137630 0014332 5 ustar 00root root 0000000 0000000 premailer-1.14.2/lib/premailer.rb 0000664 0000000 0000000 00000000375 13732137630 0016644 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.14.2/lib/premailer/ 0000775 0000000 0000000 00000000000 13732137630 0016312 5 ustar 00root root 0000000 0000000 premailer-1.14.2/lib/premailer/adapter.rb 0000664 0000000 0000000 00000003505 13732137630 0020262 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.14.2/lib/premailer/adapter/ 0000775 0000000 0000000 00000000000 13732137630 0017732 5 ustar 00root root 0000000 0000000 premailer-1.14.2/lib/premailer/adapter/nokogiri.rb 0000664 0000000 0000000 00000023332 13732137630 0022103 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
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_attr, html_attr|
if el[html_attr].nil? and not merged[css_attr].empty?
new_val = merged[css_attr].dup
# Remove url() function wrapper
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
# Remove !important, trailing semi-colon, and leading/trailing whitespace
new_val.gsub!(/;$|\s*!important/, '').strip!
# For width and height tags, remove px units
new_val.gsub!(/(\d+)px/, '\1') if %w[width height].include?(html_attr)
# For color-related tags, convert RGB to hex if specified by options
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
el[html_attr] = new_val
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_attr)
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) unless @options[:drop_unmergeable_css_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 = URI.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.14.2/lib/premailer/adapter/nokogiri_fast.rb 0000664 0000000 0000000 00000034560 13732137630 0023125 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_attr, html_attr|
if el[html_attr].nil? and not merged[css_attr].empty?
new_val = merged[css_attr].dup
# Remove url() function wrapper
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
# Remove !important, trailing semi-colon, and leading/trailing whitespace
new_val.gsub!(/;$|\s*!important/, '').strip!
# For width and height tags, remove px units
new_val.gsub!(/(\d+)px/, '\1') if %w[width height].include?(html_attr)
# For color-related tags, convert RGB to hex if specified by options
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
el[html_attr] = new_val
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_attr)
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) unless @options[:drop_unmergeable_css_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 = URI.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.14.2/lib/premailer/adapter/nokogumbo.rb 0000664 0000000 0000000 00000023035 13732137630 0022262 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
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_attr, html_attr|
if el[html_attr].nil? and not merged[css_attr].empty?
new_val = merged[css_attr].dup
# Remove url() function wrapper
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
# Remove !important, trailing semi-colon, and leading/trailing whitespace
new_val.gsub!(/;$|\s*!important/, '').strip!
# For width and height tags, remove px units
new_val.gsub!(/(\d+)px/, '\1') if %w[width height].include?(html_attr)
# For color-related tags, convert RGB to hex if specified by options
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
el[html_attr] = new_val
end
unless @options[:preserve_style_attribute]
merged.instance_variable_get("@declarations").tap do |declarations|
declarations.delete(css_attr)
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) unless @options[:drop_unmergeable_css_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 = URI.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.fragment(thing)
else
::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
premailer-1.14.2/lib/premailer/adapter/rgb_to_hex.rb 0000664 0000000 0000000 00000001473 13732137630 0022404 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.14.2/lib/premailer/executor.rb 0000664 0000000 0000000 00000005646 13732137630 0020510 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
END_HTML
premailer = Premailer.new(html, :with_html_string => true)
assert_empty 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
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_many_spaces
assert_plaintext "Long line\nnext line", "Long line next line", nil ,14
end
def test_img_alt_tags
# ensure html img tags that aren't self-closed are parsed,
# along with accepting both '' and "" as attribute quotes
#
assert_plaintext 'Example ( http://example.com/ )', ''
#
assert_plaintext 'Example ( http://example.com/ )', ''
#
assert_plaintext 'Example ( http://example.com/ )', ""
#
assert_plaintext 'Example ( http://example.com/ )', ""
# ensure that img alt text is handled properly for multiple
# img tags on the same line
# then
assert_plaintext "before then after", ' then '
# just
assert_plaintext "just after", ' just '
end
def test_links
# basic
assert_plaintext 'Link ( http://example.com/ )', 'Link'
# nested html
assert_plaintext 'Link ( http://example.com/ )', 'Link'
# nested html with new line
assert_plaintext 'Link ( http://example.com/ )', "\n\tLink\n\t"
# mailto
assert_plaintext 'Contact Us ( contact@example.org )', "Contact Us"
# complex link
assert_plaintext 'Link ( http://example.com:80/~user?aaa=bb&c=d,e,f#foo )', 'Link'
# attributes
assert_plaintext 'Link ( http://example.com/ )', 'Link'
# spacing
assert_plaintext 'Link ( http://example.com/ )', ' Link '
# multiple
assert_plaintext 'Link A ( http://example.com/a/ ) Link B ( http://example.com/b/ )', 'Link ALink B'
# merge links
assert_plaintext 'Link ( %%LINK%% )', 'Link'
assert_plaintext 'Link ( [LINK] )', 'Link'
assert_plaintext 'Link ( {LINK} )', 'Link'
# unsubscribe
assert_plaintext 'Link ( [[!unsubscribe]] )', 'Link'
# empty link gets dropped, and shouldn't run forever
assert_plaintext(("This is some more text\n\n" * 14 + "This is some more text"), "#{"\n
This is some more text
" * 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 LINK TEXT and then more text that does not wrap'
# same text and link
assert_plaintext 'http://example.com', 'http://example.com'
# link that includes a single quote
assert_plaintext "King's Gambit ( https://en.wikipedia.org/wiki/King's_Gambit )", "King's Gambit"
# link that includes double quotes
assert_plaintext '"Weird Al" Yankovic ( https://en.wikipedia.org/wiki/%22Weird_Al%22_Yankovic )', '"Weird Al" Yankovic', nil, 100
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.',
'
END_HTML
premailer = Premailer.new(html, :adapter => :nokogiri, :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('head style').inner_html
# red should remain in