8

Many email clients don't like linked CSS stylesheets, or even the embedded <style> tag, but rather want the CSS to appear inline as style attributes on all your markup.

  • BAD: <link rel=stylesheet type="text/css" href="/style.css">
  • BAD: <style type="text/css">...</style>
  • WORKS: <h1 style="margin: 0">...</h1>

However this inline style attribute approach is a right pain to manage.

I've found tools for Ruby and PHP that will take a CSS file and some separate markup as input and return you the merged result - a single file of markup with all the CSS converted to style attributes.

I'm looking for a Perl solution to this problem, but I've not found one on CPAN or by searching Google. Any pointers? Alternatively, are there CPAN modules one could combine to achieve the same result?

6
  • @mintywalker The code I posted worked on a bunch of files I had, produced valid HTML from valid HTML and seeming valid CSS from valid CSS. Have you tried it? It would be great to have some feedback. Commented Aug 13, 2009 at 16:10
  • @Sinan Ünür : yes - it certainly runs and gets pretty close, but I noted something it's not quite getting right by commenting on your answer - it's CSS::DOM problem, not your tho. And the lack of previewing on comments meants the comment got mangled, but I think you ought to be able to figure out the crux. Thank you very much for your help, I'm going to have a play with CSS::DOM but I fear the complexity may be beyond me here. Commented Aug 13, 2009 at 16:57
  • It might be easier to HTML::TreeBuilder and CSS. I am experimenting a little. Commented Aug 13, 2009 at 17:37
  • Why does it have to be Perl? Could you do this is a separate process using the Ruby stuff, for instance? Commented Aug 14, 2009 at 12:09
  • @brian d foy : In general, shelling out wouldn't be a huge issue, other than we're generating quite a lot of per-user emails, so the "cost" of shelling out is not trivial for us. Perhaps relative to the cost of doing the css convertion not hugely high, but still. Commented Aug 14, 2009 at 12:29

2 Answers 2

10

I do not know of a complete, pre-packaged solution.

CSS::DOM's compute_style is subject to pretty much the same caveats as emogrifier above. That module, in conjunction with HTML::TokeParser ought to be usable to cook up something.

Update: Here is a buggy mish-mash of things:

#!/usr/bin/perl

use strict;
use warnings;

use CSS::DOM;
use File::Slurp;
use HTML::DOM;
use HTML::TokeParser;

die "convert html_file css_file" unless @ARGV == 2;
my ($html_file, $css_file) = @ARGV;

my $html_parser = HTML::TokeParser->new($html_file)
    or die "Cannot open '$html_file': $!";

my $sheet = CSS::DOM::parse( scalar read_file $css_file );

while ( my $token = $html_parser->get_token ) {
    my $type = $token->[0];
    my $text = $type eq 'T' ? $token->[1] : $token->[-1];
    if ( $type eq 'S' ) {
        unless ( skip( $token->[1] ) ) {
            $text = insert_computed_style($sheet, $token);
        }
    }
    print $text;
}

sub insert_computed_style {
    my ($sheet, $token) = @_;
    my ($tag, $attr, $attrseq) = @$token[1 .. 3];
    my $doc = HTML::DOM->new;

    my $element = $doc->createElement($tag);

    for my $attr_name ( @$attrseq ) {
        $element->setAttribute($attr_name, $attr->{$attr_name});
    }

    my $style = CSS::DOM::compute_style(
        element => $element, user_sheet => $sheet
    );

    my @attrseq = (style => grep { lc $_ ne 'style' } @$attrseq );
    $attr->{style} = $style->cssText;

    my $text .= join(" ",
        "<$tag",
        map{ qq/$_='$attr->{$_}'/ } @attrseq );
    $text .= '>';

    return $text;
}

sub skip {
    my ($tag) = @_;
    $tag = lc $tag;
    return 1 if $tag =~ /^(?:h(?:ead|tml)|link|meta|script|title)$/;
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you - that is pretty amazing, although I think there is a bug/todo with CSS::Dom (which looks a little fragile still!) that skuppers things a little #foo { border-width: 2px } div { border: 1px dashed green } with the html &lt;div id=foo&gt; ... &lt;/div&gt; ought to get style='border: 1px dashed green; border-width: 2px;' but currently gets style='border-width: 2px; border: 1px dashed green'
@mintywalker That is a problem. I have looked at the source for compute_style and I am not sure I can fix that right now. A kludgy fix is to change the order in the CSS file: div { border: 1px dashed green } #foo { border-width: 8px } but that is not practical in the general case.
I'm going to have a play with CSS::DOM - for my purposes, spotting and throwing an exception would be better than emitting something that was wrong, so I'm wondering if I might be able to tweak compute_style in that direction at least. I'll report back here if I get anywhere (which is not guaranteed at all!) Again - many thanks, it's a very helpful start.
5

You can use CPAN Perl module CSS::Inliner https://metacpan.org/release/CSS-Inliner

1 Comment

I've recently run into this problem too. CSS::Inliner looks like exactly the tool for me. I want to keep my templates using CSS rules, but inline the styles right before I send the email. Thanks for the link!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.