Class: Premailer

Inherits:
Object
  • Object
show all
Includes:
CssParser, Warnings
Defined in:
lib/premailer/premailer.rb

Overview

Premailer by Alex Dunae (dunae.ca, e-mail ‘code’ at the same domain), 2008-09

Premailer processes HTML and CSS to improve e-mail deliverability.

Premailer’s main function is to render all CSS as inline style attributes. It also converts relative links to absolute links and checks the ‘safety’ of CSS properties against a CSS support chart.

Example

premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)

# Write the HTML output
fout = File.open("output.html", "w")
fout.puts premailer.to_inline_css
fout.close

# Write the plain-text output
fout = File.open("ouput.txt", "w")
fout.puts premailer.to_plain_text
fout.close

# List any CSS warnings
puts premailer.warnings.length.to_s + ' warnings found'
premailer.warnings.each do |w|
  puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
end

premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
puts premailer.to_inline_css

Defined Under Namespace

Modules: Warnings

Constant Summary collapse

VERSION =
'1.5.4'
CLIENT_SUPPORT_FILE =
File.dirname(__FILE__) + '/../../misc/client_support.yaml'
RE_UNMERGABLE_SELECTORS =
/(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
WARN_LABEL =
%w(NONE SAFE POOR RISKY)

Constants included from Warnings

Warnings::NONE, Warnings::POOR, Warnings::RISKY, Warnings::SAFE

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, options = {}) ⇒ Premailer

Create a new Premailer object.

path is the path to the HTML file to process. Can be either the URL of a remote file or a local path.

Options

line_length

Line length used by to_plain_text. Boolean, default is 65.

warn_level

What level of CSS compatibility warnings to show (see Warnings).

link_query_string

A string to append to every <a href=“”> link. Do not include the initial ?.

base_url

Used to calculate absolute URLs for local files.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/premailer/premailer.rb', line 73

def initialize(path, options = {})
  @options = {:warn_level => Warnings::SAFE, 
              :line_length => 65, 
              :link_query_string => nil, 
              :base_url => nil,
              :local_file => true,
              :remove_classes => false}.merge(options)
  @html_file = path
  
  if options[:local_file] == false
    @is_local_file = false
  elsif path =~ /^(http|https|ftp)\:\/\//i
    @is_local_file = false
  else
    @is_local_file = true
  end

  @css_warnings = []

  @css_parser = CssParser::Parser.new({:absolute_paths => true,
                                       :import => true,
                                       :io_exceptions => false
                                      })
  
  @doc, @html_charset = load_html(@html_file)
  @processed_doc = @doc
  
  if @is_local_file and @options[:base_url]
    @processed_doc = convert_inline_links(@processed_doc, @options[:base_url])
  elsif not @is_local_file
    @processed_doc = convert_inline_links(@processed_doc, @html_file)
  end
  load_css_from_html!
end

Instance Attribute Details

#docObject (readonly)

source HTML document (Hpricot)



51
52
53
# File 'lib/premailer/premailer.rb', line 51

def doc
  @doc
end

#html_fileObject (readonly)

URI of the HTML file used



45
46
47
# File 'lib/premailer/premailer.rb', line 45

def html_file
  @html_file
end

#processed_docObject (readonly)

processed HTML document (Hpricot)



48
49
50
# File 'lib/premailer/premailer.rb', line 48

def processed_doc
  @processed_doc
end

Instance Method Details

#to_inline_cssObject

Merge CSS into the HTML document.

Returns a string.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/premailer/premailer.rb', line 136

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|
    # Save un-mergable rules separately
    selector.gsub!(/:link([\s]|$)+/i, '')

    # Convert element names to lower case
    selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }

    if selector =~ RE_UNMERGABLE_SELECTORS
      unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
    else
      
      doc.search(selector) do |el|
        if el.elem?
          # Add a style attribute or append to the existing one  
          block = "[SPEC=#{specificity}[#{declaration}]]"
          el['style'] = (el.attributes['style'] ||= '') + ' ' + block
        end
      end
    end
  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 = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
      declarations << rs
    end

    # Perform style folding and save
    merged = CssParser.merge(declarations)

    el['style'] = Premailer.escape_string(merged.declarations_to_s)
  end

  doc = write_unmergable_css_rules(doc, unmergable_rules)

  doc.search('*').remove_class if @options[:remove_classes]  

  @processed_doc = doc

  doc.to_html
end

#to_plain_textObject

Converts the HTML document to a format suitable for plain-text e-mail.

Returns a string.



123
124
125
126
127
128
129
130
131
# File 'lib/premailer/premailer.rb', line 123

def to_plain_text
  html_src = ''
  begin
    html_src = @doc.search("body").innerHTML
  rescue
    html_src = @doc.to_html
  end
  convert_to_text(html_src, @options[:line_length], @html_charset)
end

#to_sObject

Returns the original HTML as a string.



116
117
118
# File 'lib/premailer/premailer.rb', line 116

def to_s
  @doc.to_html
end

#warningsObject

Array containing a hash of CSS warnings.



109
110
111
112
113
# File 'lib/premailer/premailer.rb', line 109

def warnings
  return [] if @options[:warn_level] == Warnings::NONE
  @css_warnings = check_client_support if @css_warnings.empty?
  @css_warnings
end