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.

Advertisement

5 Comments »

  1. What? A designer that writes crappy code? Say it isn’t so…

    Comment by John — February 19, 2011 @ 9:15 pm

    • I mean really it’s more that he didn’t have a choice. It’s the lack of support for internal styles that leads to the bad code. This is one time that the designer isn’t to blame haha.

      Comment by burrowscode — February 19, 2011 @ 11:06 pm

  2. Read it, love it, fuck it. I’m out. Keep it fresh. Support dairy farmers. Eat squirrel.

    Comment by Alexandar the Great — February 20, 2011 @ 5:21 am

  3. hi!

    next time give a try to premailer it’s do the same :)

    https://github.com/alexdunae/premailer/

    a

    Comment by szaboat — March 11, 2011 @ 5:53 pm

    • Thanks for sharing. This would have been nice to know about when I initially ran into the issue.

      Comment by burrowscode — March 13, 2011 @ 9:59 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Theme: WordPress Classic. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.