(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.



