pax_global_header00006660000000000000000000000064132026370050014510gustar00rootroot0000000000000052 comment=aae3be75a543e7292beadc43619ce882348845fd premailer-1.11.1/000077500000000000000000000000001320263700500135515ustar00rootroot00000000000000premailer-1.11.1/.editorconfig000066400000000000000000000002461320263700500162300ustar00rootroot00000000000000# 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/.gitignore000066400000000000000000000001401320263700500155340ustar00rootroot00000000000000.DS_Store /*.gem /bin/*.html /html/ /vendor/ /doc/ /.yardoc/ *.sw? /pkg/ /.bundle/ /*.sublime-* premailer-1.11.1/.jrubyrc000066400000000000000000000000221320263700500152240ustar00rootroot00000000000000cext.enabled=true premailer-1.11.1/.travis.yml000066400000000000000000000002541320263700500156630ustar00rootroot00000000000000cache: 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/.yardopts000066400000000000000000000002271320263700500154200ustar00rootroot00000000000000--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.md000066400000000000000000000013361320263700500153650ustar00rootroot00000000000000## 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/Gemfile000066400000000000000000000004011320263700500150370ustar00rootroot00000000000000# 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.lock000066400000000000000000000027461320263700500160040ustar00rootroot00000000000000GIT 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.md000066400000000000000000000027411320263700500151610ustar00rootroot00000000000000# 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.md000066400000000000000000000102511320263700500150270ustar00rootroot00000000000000# Premailer README [![Build Status](https://travis-ci.org/premailer/premailer.png?branch=master)](https://travis-ci.org/premailer/premailer) [![Gem Version](https://badge.fury.io/rb/premailer.svg)](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/Rakefile000066400000000000000000000030621320263700500152170ustar00rootroot00000000000000require '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/000077500000000000000000000000001320263700500143215ustar00rootroot00000000000000premailer-1.11.1/bin/premailer000077500000000000000000000001751320263700500162320ustar00rootroot00000000000000#!/usr/bin/env ruby # This binary used in rubygems environment only as part of installed gem require 'premailer/executor' premailer-1.11.1/lib/000077500000000000000000000000001320263700500143175ustar00rootroot00000000000000premailer-1.11.1/lib/premailer.rb000066400000000000000000000003751320263700500166310ustar00rootroot00000000000000require '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/000077500000000000000000000000001320263700500162775ustar00rootroot00000000000000premailer-1.11.1/lib/premailer/adapter.rb000066400000000000000000000035051320263700500202470ustar00rootroot00000000000000class 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/000077500000000000000000000000001320263700500177175ustar00rootroot00000000000000premailer-1.11.1/lib/premailer/adapter/nokogiri.rb000066400000000000000000000223751320263700500220760ustar00rootroot00000000000000require '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.rb000066400000000000000000000335751320263700500231170ustar00rootroot00000000000000require '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.rb000066400000000000000000000221001320263700500222370ustar00rootroot00000000000000require '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.rb000066400000000000000000000014731320263700500223710ustar00rootroot00000000000000# 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.rb000066400000000000000000000056461320263700500204750ustar00rootroot00000000000000require '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 = <
red
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 = < END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| premailer = Premailer.new(html, :with_html_string => true, :adapter => adapter) assert_match /content:\s*url\(data:image\/png;base64,LOTSOFSTUFF\)/, premailer.to_inline_css end end end premailer-1.11.1/test/test_premailer.rb000066400000000000000000000344471320263700500201100ustar00rootroot00000000000000# -*- encoding: UTF-8 -*- require File.expand_path(File.dirname(__FILE__)) + '/helper' class TestPremailer < Premailer::TestCase def test_special_characters_nokogiri html = '

cédille cé & garçon garçon à à   & ©

' 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 def test_detecting_html [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| remote_setup('base.html', :adapter => adapter) assert !@premailer.is_xhtml? end end def test_detecting_xhtml [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| remote_setup('xhtml.html', :adapter => adapter) assert @premailer.is_xhtml? end end def test_self_closing_xhtml_tags [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| remote_setup('xhtml.html', :adapter => adapter) assert_match //, @premailer.to_s assert_match //, @premailer.to_inline_css end end def test_non_self_closing_html_tags [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| remote_setup('html4.html', :adapter => adapter) assert_match /
/, @premailer.to_s assert_match /
/, @premailer.to_inline_css end end def test_mailtos_with_query_strings html = < Test END_HTML qs = 'testing=123' [:nokogiri, :nokogiri_fast, :nokogumbo].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_preserving_ignored_style_elements [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| local_setup('ignore.html', :adapter => adapter) assert_nil @doc.at('h1')['style'] end end def test_preserving_ignored_link_elements [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| local_setup('ignore.html', :adapter => adapter) assert_nil @doc.at('body')['style'] end end def test_importing_local_css [:nokogiri, :nokogiri_fast, :nokogumbo].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, :nokogiri_fast, :nokogumbo].each do |adapter| html = '' 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, :nokogiri_fast, :nokogumbo].each do |adapter| html = '' premailer = Premailer.new(html, {:with_html_string => true, :adapter => adapter, :css_to_attributes => false}) premailer.to_inline_css assert_match /background-color: #FFF/, premailer.processed_doc.at_css('td').attributes['style'].to_s end end def test_importing_remote_css [:nokogiri, :nokogiri_fast, :nokogumbo].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, :nokogiri_fast, :nokogumbo].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, :nokogiri_fast, :nokogumbo].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, :nokogiri_fast, :nokogumbo].each do |adapter| premailer = Premailer.new('

test

', :with_html_string => true, :adapter => adapter) assert_match /test/, premailer.to_inline_css end end def test_initialize_no_escape_attributes_option html = < Google Link END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 = <

Test

Test

END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 = <<-___
Test
___ [:nokogiri, :nokogiri_fast, :nokogumbo].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_css('#editable')['contenteditable'], "#{adapter}: contenteditable attribute not removed" end end def test_carriage_returns_as_entities html = <<-html \n\r

test

\n\r

test

html [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| pm = Premailer.new(html, :with_html_string => true, :adapter => adapter) assert_match /\n/, 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'] end def test_premailer_related_attributes html = <
Test
END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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_empty_css_att html = <<-END_HTML
Test
END_HTML pm = Premailer.new(html, :with_html_string => true, :rgb_to_hex_attributes => true, :remove_scripts => true, :adapter => :nokogiri) pm.to_inline_css doc = pm.processed_doc assert_match /]+550px.+bgcolor="FAFAFA"/, doc.at('table').to_s end def test_rgb_color html = <<-END_HTML
Test
END_HTML pm = Premailer.new(html, :with_html_string => true, :rgb_to_hex_attributes => true, :remove_scripts => true, :adapter => :nokogiri) pm.to_inline_css doc = pm.processed_doc assert_equal 'FAFAFA', doc.at('table')['bgcolor'] end def test_non_rgb_color html = <<-END_HTML
Test
END_HTML pm = Premailer.new(html, :with_html_string => true, :rgb_to_hex_attributes => true, :adapter => :nokogiri) pm.to_inline_css doc = pm.processed_doc assert_equal 'red', doc.at('table')['bgcolor'] 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 = "Ää, Öö, Üü" pm = Premailer.new(html_special_characters, :with_html_string => true, :adapter => :nokogiri, :input_encoding => "UTF-8") assert_match /#{html_special_characters}/, 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 = /#{html_entities_characters}/ pm = Premailer.new(html_special_characters, :output_encoding => "US-ASCII", :with_html_string => true, :adapter => :nokogiri, :input_encoding => "UTF-8"); assert_match expected_html, pm.to_inline_css end def test_meta_encoding_downcase meta_encoding = '' expected_html = Regexp.new(Regexp.escape(''), 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 = '' expected_html = Regexp.new(Regexp.escape(''), 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 = "’" pm = Premailer.new(html_entities, :with_html_string => true, :adapter => :nokogiri, :replace_html_entities => true) assert_match /'/, 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, input_encoding: 'UTF-8') 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 premailer-1.11.1/test/test_warnings.rb000066400000000000000000000052101320263700500177420ustar00rootroot00000000000000# encoding: UTF-8 require File.expand_path(File.dirname(__FILE__)) + '/helper' class TestWarnings < Premailer::TestCase def test_element_warnings html = <
Test
END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 = <
Test
END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 = <
Test
END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].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 [:nokogiri, :nokogiri_fast, :nokogumbo].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 = <
Test
END_HTML [:nokogiri, :nokogiri_fast, :nokogumbo].each do |adapter| warnings = get_warnings(html, adapter, Premailer::Warnings::SAFE) assert_equal 2, warnings.length end [:nokogiri, :nokogiri_fast, :nokogumbo].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