Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Cyril Hanquez and Hugo Sombreireiro and Reto Aeberli and Steven Peeters and Guust Nieuwenhuis and Aurélien Deleusière

Applying Flattened CSS To XHTML For Use In Remote Environments

By Ben Nadel on
Tags: ColdFusion

Styling content with CSS (Cascading Style Sheets) is awesome and has become the de facto way to separate content from formatting. Doing this creates slimmer, more maintainable code. Of course, I'm not here to evangelize CSS - I think we all know that it rocks. I'm here to talk about merging CSS and XHTML for use in remote environments. See, keeping CSS separate from content is great in your local, controlled application; but, if you have to create content for use in another environment such as an email or a Javascript widget, you can't always depend on the remote system to properly handle (or even allow) CSS blocks. And, even if it does allow CSS, you can't be sure that it won't have its own CSS rules that conflicts with or even override your CSS.

Because of this limitation, I have been experimenting with ways to take CSS and merge it with the generated content so that all CSS definitions are transformed into inline STYLE attributes of the rendered DOM elements. While I don't have a great solution yet, I think I am on the right track, using XML and XPath to transform the given XHTML into styled XHTML. To see what I am going on about, let's look at a sample XHTML email receipt that one might get for making an online purchase:

  • <!--- Set up the email body. --->
  • <cfsavecontent variable="strEmailBody">
  •  
  • <div id="emailbody">
  •  
  • <h1>
  • Dear customer,
  • </h1>
  •  
  • <p>
  • Thank you for you order from HotFemaleMuscle.com. We
  • pride ourselves on producing the hottest female muscle
  • videos that the internet has to offer.
  • </p>
  •  
  • <p>
  • Due to the huge popularity of our products, please
  • allow 2-4 business days for your order to ship.
  • </p>
  •  
  •  
  • <h2>
  • Order Summary
  • </h2>
  •  
  • <p>
  • The following serves as an invoice to your order.
  • </p>
  •  
  • <table cellspacing="0" cellpadding="0" border="0" id="order-summary">
  • <tr>
  • <td class="field-label">
  • Order Number:
  • </td>
  • <td class="field-value">
  • HFM00143054
  • </td>
  • </tr>
  • <tr>
  • <td class="field-label">
  • Date:
  • </td>
  • <td class="field-value">
  • March 9, 2009
  • </td>
  • </tr>
  • </table>
  •  
  •  
  • <h3>
  • Products
  • </h3>
  •  
  • <table cellspacing="0" cellpadding="0" border="0" id="order-items">
  • <thead>
  • <tr>
  • <th>
  • Video
  • </th>
  • <th>
  • Quantity
  • </th>
  • <th>
  • Price
  • </th>
  • </tr>
  • </thead>
  • <tbody>
  • <tr>
  • <td>
  • Women's Muscle Power 13 - Muscle Beauties
  • in Brazil
  • </td>
  • <td>
  • 1
  • </td>
  • <td>
  • $39.95
  • </td>
  • </tr>
  • </tbody>
  • </table>
  •  
  • <p align="right">
  • <strong>Total: $39.95</strong>
  • </p>
  •  
  •  
  • <div id="footer">
  •  
  • <p>
  • If you have any questions about your order,
  • please contact us at 1-800-555-FEMM. Please
  • have your order number ready for our customer
  • service agents.
  • </p>
  •  
  • </div>
  •  
  • </div>
  •  
  • </cfsavecontent>

There's nothing special about the XHTML used in this email, other than the fact that it is XHTML compliant (meaning, that it can be parsed as an XML document). What you'll notice about this, though, is that there's no styling. In fact, rendering this code in an email looks downright sloppy:

 
 
 
 
 
 
XHTML Email Rendered Without Any Style. 
 
 
 

If we want to style it, we could throw a STYLE tag at the top and define the CSS. However, as I cautioned before, I am not sure how dependable this technique is in the various email clients. The safer approach is to use STYLE attributes in our XHTML markup. Of course, this makes our markup very dirty and repetitive. So, I've experimented with using XML and XPath to get a "best of both worlds" approach. In the following code, I am defining my style rules in XML and then using XPath to locate target elements and apply the given rules in a cascading manner. In this way, we keep a pure separation of content and formatting but leverage the dependability of inline styles:

  • <!---
  • Define the CSS for this email. The CSS here will be applied
  • in top-down manner.
  • --->
  • <cfxml variable="xmlCSS">
  •  
  • <styles>
  • <style xpath="//*">
  • font-family: verdana, arial ;
  • font-size: 12px ;
  • line-height: 17px ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • </style>
  • <style xpath="//h1 | //h2 | //h3 | //p | //table">
  • margin-bottom: 16px ;
  • </style>
  • <style xpath="//h1">
  • font-size: 16px ;
  • </style>
  • <style xpath="//h2">
  • border-bottom: 2px solid #E0E0E0 ;
  • margin-bottom: 12px ;
  • margin-top: 23px ;
  • padding-bottom: 4px ;
  • </style>
  • <style xpath="//h3">
  • margin-bottom: 12px ;
  • </style>
  • <style xpath="//td[ @class = 'field-label' ]">
  • font-weight: bold ;
  • padding: 0px 10px 5px 0px ;
  • text-align: right ;
  • </style>
  • <style xpath="//td[ @class = 'field-value' ]">
  • padding: 0px 0px 5px 0px ;
  • </style>
  • <style xpath="//table[ @id = 'order-items' ]">
  • border: 1px solid #666666 ;
  • width: 100% ;
  • </style>
  • <style xpath="//table[ @id = 'order-items' ]//th">
  • background-color: #E0E0E0 ;
  • border-bottom: 1px solid #999999 ;
  • padding: 3px 5px 3px 5px ;
  • text-align: left ;
  • </style>
  • <style xpath="//table[ @id = 'order-items' ]//td">
  • padding: 3px 5px 3px 5px ;
  • </style>
  • <style xpath="//div[ @id = 'footer' ]">
  • border-top: 2px solid #E0E0E0 ;
  • padding-top: 7px ;
  • </style>
  • <style xpath="//div[ @id = 'footer' ]/p">
  • color: #666666 ;
  • font-size: 10px ;
  • line-height: 14px ;
  • </style>
  • </styles>
  •  
  • </cfxml>
  •  
  •  
  •  
  • <!---
  • Because we are going to be using the email body as XHTML,
  • let's parse it into a ColdFusion XML document so we can
  • easily search it and update it's properties.
  • --->
  • <cfset xmlEmail = XmlParse( Trim( strEmailBody ) ) />
  •  
  •  
  • <!---
  • Loop over all the CSS styles and apply them to the nodes
  • at the resulting XPATH search path.
  • --->
  • <cfloop
  • index="xmlStyle"
  • array="#XmlSearch( xmlCSS, '//style' )#">
  •  
  • <!--- Get the xpath. --->
  • <cfset strXPath = xmlStyle.XmlAttributes.Xpath />
  •  
  • <!---
  • Get the CSS (clear out extra spaces when we get it -
  • this will allow us to add it to the email content without
  • breaking attributes).
  • --->
  • <cfset strStyle = Trim(
  • REReplace(
  • xmlStyle.XmlText,
  • "\s+",
  • " ",
  • "all"
  • )
  • ) />
  •  
  •  
  • <!---
  • Now that we have our CSS and XPath, let's gather the target
  • nodes and append the CSS rules.
  • --->
  • <cfloop
  • index="xmlDOM"
  • array="#XmlSearch( xmlEmail, strXPath )#">
  •  
  • <!---
  • Param the style attribute of this DOM element. This
  • way, if this is the first style to be applied, we
  • can append without error.
  • --->
  • <cfparam
  • name="xmlDOM.XmlAttributes.style"
  • type="string"
  • default=""
  • />
  •  
  • <!--- Append the current rules. --->
  • <cfset xmlDOM.XmlAttributes.style &= strStyle />
  •  
  • </cfloop>
  •  
  • </cfloop>

As you can see, each style node in our CSS XML document has an XPath attribute to define the target nodes in our content XHTML. Because the styles get applied in a top-down fashion, we can still leverage all of the cascading properties of CSS as the CSS is being "applied" to the XHTML. And, because XPath and CSS rules are quite similar, the XPath is fairly readable and maintainable.

Now, when we render the exact same XHTML as above, we get the following formatted output:

 
 
 
 
 
 
XHTML Email With Integrated CSS. 
 
 
 

While there's a lot more we could do with the formatting, I think we can agree that this is much nice than the plain XHTML email. I like where this technique is going.




Reader Comments

Didn't you already have some CSS parsing code for your work with spreadsheets?

I'm totally talking out of my rear here, but ...

I have to wonder how hard it would be to write a really basic CSS specifier parser so that you wouldn't have to use that bastardized XPath syntax, and could thus use any normal CSS-editing software. I can't imagine it would be too difficult to parse out selectors, then translate them into XPath expressions, then apply them as you are doing here.

Even something ridiculously simple like "([^{]+)[{]([^}])+[}]" would suffice for 99% of the cases, allowing you to progressively enhance the parser as you need the additional functionality.

Obviously, it wouldn't hold up well to CSS hacks, but for normal stuff like you are doing here it would probably be fine.

Reply to this Comment

@Rick,

I think that's where the next evolution of this algorithm should go. I don't think there would be too much effort to parse *simplified* CSS into something that can handle the XPath behind the scenes.

Reply to this Comment

Great idea for a utility!

I have to agree with Rick O that the real wow factor comes with parsing CSS. The killer app would be processing a bunch of external CSS files to update an HTML file in the manner you show above.

It would really help an organization reuse existing styleseets. You could theretically take a modern, standards-compliant page from your site and retrofit it to work in Outlook. Madness!

Reply to this Comment

@David,

Yeah, I like the idea of going in that direction. I don't think it would be too hard. We'll see what I can come up with.

Reply to this Comment

Looks like a really nice idea Ben, looking forward to where this goes... :)

Also I wouldn't worry to much with handling CSS hacks or anything like that as they are not going to work inline in an email client anyway.

Generally the browser email clients like gmail hotmail will be quite good at allowing CSS as long as the styles are inline as they remove <style> tags. Outlook Express and outlook 03 seem pretty good at rendering any CSS and will allow style, but because of Outlook 07 I generally keep the emails to CSS 1 syntax as that barely understands CSS at all :(
It seems that this is becasue they dropped the IE rendering engine and decided to use the word 07 one instead >.<

Reply to this Comment

@Sebastiaan, @Shuns,

I think people will still need to be aware of the limitations of the different email clients - this is not meant to remove that layer of understanding. I think the beauty here lies in the ability to apply a distinct set of styles to unstyled XHTML content; the beauty is in the ease of integration.

That said, I've been to email-standards before and yes, it is very depressing :)

@Paolo,

It's the same code as the XHTML above, only every element (just about) has a STYLE attribute. It looks really messy.

Reply to this Comment

I know Ben I wasn't saying that you would need to remove that understanding layer, merely saying that css parsing functionality could probably just throw an error if the user tried to use a css hack or something - it's not like those would work anyway :p

Reply to this Comment

@Shuns,

I think what will end up happening is that the person using this technique will have to assume some responsibility in how it will be used. But, we'll see....

Reply to this Comment

@Tony,

That is some very cool stuff! I really like. It seems like it might make some contextual controls even easier than in regular CSS. Thanks for passing that along.

Reply to this Comment

It is a nice concept.. but it fails way to easily since HTML standards to not follow XML standard. Just adding a NOWRAP to the table or a BR tag causes the entire thing to die.

There is a really nice PHP utility called emogrifier(http://www.pelagodesign.com/sidecar/emogrifier/) that will take an entire page, strip out the styles and apply them inline.

Anyone knoe of something that takes your concept here and takes it to the next level like emogrifier?

Reply to this Comment

@Brien,

That is where I am trying to go with this. I will try to upgrade soon. As far as HTML vs. XHTML, I think when the person uses this technique, they have to make sure to use XHTML - this is a very specific use case.

Reply to this Comment

@Brien,

Taking a look at the PHP source code, it looks like they are taking a similar approach - using XPATH on the HTML document to apply the styles.

Reply to this Comment

Hey Ben,

Yeah it is a similar, looks like the main difference between the XML in CF and PHP is that in PHP they can turn the strict Error Checking off to handle the HTML better than CF.

Instead of using the ColdFusion XML parser, have you thought about using an HTML parser? I tried using Jericho HTML Parser (http://www.bennadel.com/index.cfm?dax=blog:1521.view) to convert all CSS to inline styles to make our emails GMail compliant, but haven't gotten too far on it yet.

Reply to this Comment

I've been using the following code for a while to inline styles for html emails.

It uses these open source libraries :

* CSS4J: a CSS API implementation for the Java™ platform
http://www.informatica.info/projects/css/

* JTidy : a DOM parser for real-world HTML
http://jtidy.sourceforge.net/

here's a simple example :

<style>.main{ text-align:center } </style>
<div class='main'>main</div>

gets converted into

<html>
<body>
<div class="main" style="text-align: center; ">main</div>
</body>
</html>

Feel free to use the following code how you see fit :)

public String inlineStyles(String html) {

try {

Tidy tidy = new Tidy(); // obtain a new Tidy instance
tidy.setXHTML(true);
tidy.setQuiet(true);
tidy.setShowWarnings(false);

Document doc = tidy.parseDOM(new ByteArrayInputStream(html.getBytes("UTF-8")), null);

XHTMLDocumentFactory factory = (XHTMLDocumentFactory)XHTMLDocumentFactory.getInstance();
factory.setStyleDatabase(new HeadlessStyleDatabase());
factory.setStyleCache(false);

factory.setDefaultStyleSheet((DOM4JCSSStyleSheet)factory.getCSSStyleSheetFactory().createStyleSheet());

DOMReader reader = new DOMReader(factory);
XHTMLDocument document = (XHTMLDocument)reader.read(doc);

computeStyles(document.getRootElement());

for (Node node : (List<Node>)document.selectNodes("//style")) {
node.getParent().remove(node);
}

// OutputFormat format = OutputFormat.createCompactFormat();
// format.setTrimText(false);

OutputFormat format = new OutputFormat();
format.setIndentSize(2);
format.setNewlines(true);
format.setTrimText(false);
format.setPadText(false);

StringWriter sw = new StringWriter();
sw.append("<html>");
HTMLWriter writer = new HTMLWriter(sw, format);
writer.write(document.selectSingleNode("//body"));
writer.flush();
sw.append("</html>");

return sw.toString();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}

private static void computeStyles(Element element) throws Exception {
if (element instanceof CSSStylableElement) {
CSSStyleDeclaration computedStyle = ((CSSStylableElement)element).getComputedStyle();
String cssText = computedStyle.getCssText();
if (cssText.length() > 0) {
element.addAttribute("style", cssText);
}
}
for (Object n : element.content()) {
if (n instanceof Element) {
Element e = (Element)n;
computeStyles(e);
}
}
}

Reply to this Comment

@Aaron,

I don't think I ever really did much more on this, sorry. It's probably time, though, as I am working on an application that will be sending out multiple branded emails. Something like this would definitely make it much easier to code/maintain.

Reply to this Comment

@Ben - yeah, actually I jumped the gun a little (didn't test well enough, cuz I hadn't done much HTML cfmail stuff before). Then, people complained it didn't look good in GMail and Outlook. So, I researched and saw this, http://www.campaignmonitor.com/css/, which shows that a lot of CSS isn't supported in many email clients. Ugh!

I was re-using the CSS from the default Refynr Stream. Obviously, that didn't work out. As a temp workaround, I just simplified the returned HTML & removed images, but the emails look pretty boring now. I'd love something more interesting that worked in the most popular email clients.

BTW - if you have a minute, give Refynr.com a try. It uses jQuery Mobile Alpha 2 (so looks good on Mac/Win FF, Chrome, & Safari + iPad, iPhone, and at least some Android devices). It also uses some functions from your KinkyTwits (http://www.bennadel.com/projects/kinky-twits.htm) and some of your fancy RegEx stuff for turning auto-hyperlinking URLs in Tweets, etc. I'd love your honest feedback. I have a thick skin, no worries ;-)

Reply to this Comment

@Aaron,

Yeah, CSS in emails is quirky. I use a lot of DIVs and padding. Speaking of Campaign Monitor, they have a pretty badass service where you can test your email in dozens of email clients at the same time. It takes screen shots of all the renderings. I think it costs $5 per test, which is kind of pricey; but, the payoff of having compatible, bitchy looking emails might be well worth it.

I'll definitely take a look :)

Reply to this Comment

hi,

I am using dom4j-1.6.1.jar and css4j-0.12.1.jar to process some urls.

For a few urls the code seems to be hang. Even no exception is thrown. For a particular stylableelement or style code doesnot return any value and hangs.

For a few websites code hang in CSSStyleDeclaration.getPropertyValue(style);

and for a few websites code hang in
CSSStylableElement.getComputedStyle();

Could you please update me, is there any known issue or fix for this.

thanks in advance.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.