Exploring Ruby CSS parsers: TamTam and CSSPool

In response to yesterday’s post about inlining CSS for HTML emails, I got a couple of comments suggesting alternatives to my CSS parser class. Not wanting to have to maintain code unless I have to, I decided to give them both a try and see how they worked out.

TamTam

First up is TamTam, suggested by batnight. I’d actually spotted TamTam and link blogged it a few weeks ago, which shows how transient attention can be. TamTam is a complete solution for inlining CSS, so I should be able to replace all my code with:

require 'rubygems'
require 'hpricot'
require 'tamtam'

inlined = TamTam.inline(
  :css => File.read('/path/to/my.css'),
  :body => File.read('/path/to/my.html')
)

puts inlined

That looks ideal, but unfortunately as soon as I tried passing my code into it an error emerged.

/Library/Ruby/Gems/gems/tamtam-0.0.2/lib/tamtam.rb:77:in `apply_to': Trouble on style td#email-header-message on element  (Exception): can't convert nil into String	from /Library/Ruby/Gems/gems/tamtam-0.0.2/lib/tamtam.rb:24:in `inline'

Digging into the library it seems that its parsing fails if there’s a CSS rule that doesn’t match any elements in the document. That’s not a problem if your CSS file is targetted to a specific block of HTML, but if the idea is to build a set of rules that can be applied across a selection of page/emails that may be a problem.

CSSPool

CSSPool, suggested by Dan Kubb, is a more generic CSS parser which is designed to work with hpricot to map between CSS and HTML. Initially I just used something very much like the examples they offer to get a sense of the format of the objects they give access to:

require 'rubygems'
require 'hpricot'
require 'csspool'

sac = CSS::SAC::Parser.new
css_doc = sac.parse(File.read('/path/to/my.css'))
html_doc = Hpricot.parse(File.read('/path/to/my.css'))
css_doc.find_all_rules_matching(html_doc).each do |rule|
  puts rule
end

Unfortunately this too failed on me with:

NoMethodError: undefined method `accept' for nil:NilClass

From a quick look at the source code it seemed that this error was occurring when a CSS selector didn’t match the supplied document, just as in TamTam. It turned out to be pretty easy to patch, though, and I’ve submitted a report in their tracker.

That done I was able to iterate over it and access the selectors easily enough, but what I’ve not yet been able to find is a way to get the parser to give me the actual CSS declarations appropriately formatted for including in the HTML. When I have a rule I can get the selector with:

rule.selector.to_css

But to find out what styles the selector applies isn’t so straightforward. If anyone has an easy way to do that, I’d love to hear about it in the comments!

Update (later that day): Version 0.2.3 of CSSPool is now out and includes my fix.

Tags: , , , , , ,

3 comments

  1. James, you may also want to check out Premailer (http://code.dunae.ca/premailer.web/) which does exactly what you want I think. Its written in ruby using Hpricot, and the source code is available off a link at the bottom of the page.

  2. rule.properties is a Set of property Arrays for the selector. Each array has a property name, and a value object, and the important property of the property (whatever that is).

  3. Here are a couple of migrations if you want to parse your css into a database. There’s probably a better OO way to get some of the info I’m after (please share), but since I’m comfortable with group by queries, this worked for me.

    class CreateCss < ActiveRecord::Migration
    def self.up
    create_table :selector do |t|
    t.column :name, :string
    end

    create_table :value do |t|
    t.column :selector_id, :integer
    t.column :property_id, :integer
    t.column :value, :string
    end

    create_table :property do |t|
    t.column :name, :string
    end
    end

    def self.down
    drop_table :property
    drop_table :value
    drop_table :selector
    end
    end

    require ‘app/models/selector’
    require ‘app/models/property’
    require ‘app/models/value’

    require ‘csspool’

    class ParseCss prop.id, :selector_id => sel.id, :value => property[1].to_s)
    end
    end
    end

    def self.down
    Value.delete_all
    Property.delete_all
    Selector.delete_all
    end
    end