Burrows Code Blog

February 19, 2011

Emailify – Internal Stylesheets to Inline Styles

Filed under: Web Development — Tags: , , , , , , , , , — burrowscode @ 8:32 pm

(Note, see the git repository for bleeding edge code)

I haven’t blogged in what seems like eons, but with outpour of emails begging me to produce content, I’ve decided to appease the masses.

This project will make more sense with some context. So context. At work I’ve been charged with implementing our HTML email alert system. As of right now all of our alerts are produced in plain text, and plain text is fucking hard to read (no matter how much eye-candy ascii art we add). One of the challenges with getting the system up and running was producing a html/css spec that would render properly in most email clients. So when our designer presented me the final spec, I shuddered when I realized that all of the styles were inline because email clients don’t really support internal stylesheets (and clearly not external stylesheets).

After reading about two lines of this madeness: <tr styles="font-weight: 400; font-size: 12px; width: 100px; height: 20px;">

I realized, SOMETHING MUST BE DONE.

Thus I looked around to see if anyone was providing a service to convert internal stylesheets to inline styles. And I found stuff like this Internal to Inline Generator and HTML Composition Thing. Those projects seemed to be a good start, so they inspired me to put together a piece of ruby code that automate the process in a way that would be useful when one’s code is using something like ActionMailer.

And the result of said inspiration and about 3 hours of work (with intermittent Reddit usage, proggit ftw) is Emailify. The code is still far from complete (the bug hunt has just begun, in particular I expect there to be issues with the way that I am handling scope, still need to read the CSS specification so that can be handled correctly) but it seems to work on simple code.

The code itself is pretty straightforward. First it parses the html document with REXML and then pulls out the relevant css section and hands those off to CssParser. We then iterate through the html document (while keeping track of our current scope, probably broke) and then applying relevant styles at each step along the way. Once the code is a bit more robust I think that I’ll throw it into a gem and release it for the greater good of web developers errrrywhere.

Most of the implementation is a chainfest, BUT THAT’S JUST HOW I ROLL.

Here’s the code for those of you too lazy to click the github link.

class Emailify
  require 'css_parser'
  require 'rexml/document'
  require 'rexml/parsers/treeparser'
  include CssParser
  include REXML

  def initialize(html_data)
    @html_data = html_data
  end

  def get_cleaned_data
    convert_internal_styles_to_inline_styles
  end

  private
  def convert_internal_styles_to_inline_styles
    html = Document.new(@html_data)
    css_parser = CssParser::Parser.new
    get_script_tags(html.elements, 'css').each {|s| css_parser.add_block!(s.text)}
    go_from_internal_to_inline!(html.elements.dup, css_parser)
  end

  def get_script_tags(data, type)
    data.map {|e| handle_script_tags(e, type)}.flatten
  end

  def handle_script_tags(e, type)
    return e if check_type(e, type) 
    return get_script_tags(e.elements, type)
  end

  def check_type(e, type)
    e.attributes['type'] == "text/#{type}"
  end

  def go_from_internal_to_inline!(data, css_parser, scope=[])
    # No map!
    data = data.map {|e| handle_all_tags!(e, css_parser, scope)}
    # puts data
    data
  end

  def handle_all_tags!(e, css_parser, scope=[])
    scope.push(get_scope_info(e))
    apply_inline_styles!(e, get_relevant_styles(css_parser, scope))

    go_from_internal_to_inline!(e.elements, css_parser, scope)
    scope.pop
    e
  end
  
  def get_scope_info(e)
    {:id => "#{e.name}##{e.attributes['id']}" || nil, :class => "#{e.name}.#{e.attributes['class']}" || nil, :tag => e.name}
  end

  def apply_inline_styles!(e, styles)
    # OUT FORMAT: [{:style => x, :value => y}, ...]
    fixed_styles = styles.inject([]) do |arr, s| 
      arr + s[:declarations].split(';').inject([]) do |sub_arr, dec| 
        sub_arr.push({:style => $1.strip, :value => $2.strip}) if dec =~ /(.+):(.+)/m
      end
    end
    
    fixed_styles.each {|s| e.attributes[s[:style]] = s[:value]}
  end

  def get_relevant_styles(css_parser, scope)
    ret = []
    css_parser.each_selector {|sel, decs, spec| ret.push({:selector => sel, :declarations => decs, :specificity => spec}) if in_scope?(sel, scope)}
    ret
  end

  def in_scope?(sel, scope)
    added, dup_scope, sels = 0, scope.dup, sel.split(' ')
    sels.reverse_each.select do |sctor| 
      if index = flatten_get_tags(dup_scope).index(sctor)
        dup_scope = dup_scope.take((index) / 3)
        added += 1
        true
      end
    end

    added == sels.length
  end

  def flatten_get_tags(scope)
    scope.map(&:to_a).flatten.reject {|s| s.is_a?(Symbol)}
  end
end

html_data = %q{
  <html>
    <head>
      <script type="text/css">
        body.test-class {color: #222}
        h1#test-id {font-size: 20px}
        body p.p-class {font-size: 25px; font-weight: 400;}
      </script>
    </head>
    <title>WHAT IT BE YO</title>
    <body class="test-class" color="#111">
      <h1 id="test-id">HEY THERE</h1>
      <p class="p-class">
        This is a bunch of text. Text is cool.
      </p>
    </body>
    <script type="text/css">
      .another-class {font-weight: 400}
    </script>
    <script type="text/javascript">
      PLACEHOLDINGNSHIT
    </script>
  </html>
}
e = Emailify.new(html_data)
puts e.get_cleaned_data

Also mad props to Alex Dunae for writing CssParser as I really didn’t want to write a css parser.

Until later.

Theme: WordPress Classic. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.