Parsing And Keeping A CSS Model Using ColdFusion

Posted January 20, 2007 at 4:26 PM by Ben Nadel

Tags: ColdFusion, HTML / CSS

I am working on adding very basic CSS support to my POIUtility.cfc ColdFusion component. Right now, it can easily convert ColdFusion query objects to Microsoft Excel documents but it has no formatting options. And, while there is no real CSS outside of the web, I find that it is such a nice model that I want to provide it for Excel documents as well (if only on a very limited basis).

CSS, as a concept, is very simple to use and to read, but from a programmatic standpoint, CSS is a bit of beast to handle. Part of the difficulty is the cascading nature of various CSS definitions and part of it stems from the fact that multiple styles may or MAY NOT be defined by a single style line item. For instance, this CSS:

font: bold 12px verdana ;

... is equal to:

font: 12px verdana ;
font-weight: bold ;

... which is equal to:

font-famliy: verdana ;
font-size: 12px ;
font-weight: bold ;

... which is equal to:

font: 800 12px verdana ;

As you can see, CSS is awesome and easy to use and very flexibile, but hard to script, interpret, and parse.

In order to deal with this, I was hoping to leverage an existing solution. I came across the Java Swing library for CSS parsing, javax.swing.text.html.StyleSheet. This package clearly states in the documentation that its CSS support is less than stellar, and from what I tested, it was a quite less than even that.

So, I decided that I would have to hack something together for myself. I decided that the easiest thing for me to deal with was single-value style definitions. That way, once I had parsed it all out, all I would have to worry about was one style at a time. To that end, I would take things like this:

font: bold 12px verdana ;

... and break them up into a struct where each individual style definition would be it's own value:

font-weight: bold
font-size: 12px
font-family: verdana

This makes the application of the CSS to the final product easier because not only are you only working about one style at a time, it makes applying the cascading affect very easy to control. Every time a "font-weight" is parsed from incoming CSS stream, it simply overrides the existing ColdFusion struct value at the key "font-weight".

Ok, so the concept of parsing is not too bad, but the implementation is not all that pretty and not super flexible. To start off on a sane foot, I decided that I would only support certain CSS values (the ones needed for the POI project). Then, I basically use a chained set of regular expression replaces to replace compound values with multiple single values:

  • <cffunction
  • name="ParseRawCSS"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="This takes raw HTML-style CSS and returns a default CSS structure with overwritten parsed values.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CSS"
  • type="string"
  • required="false"
  • default=""
  • hint="This is a list of standard CSS definitions like 'font: 12px verdana ;'"
  • />
  •  
  • <cfscript>
  •  
  • // Define the local scope.
  • var LOCAL = StructNew();
  •  
  • // Create a new CSS structure.
  • LOCAL.CSS = StructNew();
  •  
  • // Set default values. This sets up not only default values,
  • // but it also defines the CSS definitions that we are going
  • // to care about. Going forward, we will not allow new CSS
  • // definitions to be add, only to be overridden.
  • LOCAL.CSS[ "background-color" ] = "";
  • LOCAL.CSS[ "background-style" ] = "";
  • LOCAL.CSS[ "border-bottom-color" ] = "";
  • LOCAL.CSS[ "border-bottom-style" ] = "";
  • LOCAL.CSS[ "border-bottom-width" ] = "";
  • LOCAL.CSS[ "border-left-color" ] = "";
  • LOCAL.CSS[ "border-left-style" ] = "";
  • LOCAL.CSS[ "border-left-width" ] = "";
  • LOCAL.CSS[ "border-right-color" ] = "";
  • LOCAL.CSS[ "border-right-style" ] = "";
  • LOCAL.CSS[ "border-right-width" ] = "";
  • LOCAL.CSS[ "border-top-color" ] = "";
  • LOCAL.CSS[ "border-top-style" ] = "";
  • LOCAL.CSS[ "border-top-width" ] = "";
  • LOCAL.CSS[ "color" ] = "";
  • LOCAL.CSS[ "font-family" ] = "";
  • LOCAL.CSS[ "font-size" ] = "";
  • LOCAL.CSS[ "font-style" ] = "";
  • LOCAL.CSS[ "font-weight" ] = "";
  • LOCAL.CSS[ "text-align" ] = "";
  • LOCAL.CSS[ "vertical-align" ] = "";
  •  
  •  
  • // Clean up the raw CSS values. We don't want to deal with complex CSS
  • // declarations like font: bold 12px verdana. We want each style to be
  • // defined individually. Keep attacking the raw css and replacing in the
  • // single-values. Clean the initial white space first.
  • LOCAL.CleanCSS = ARGUMENTS.CSS.Trim().ToLowerCase().ReplaceAll(
  •  
  • "\s+", " "
  •  
  • // Make sure that all colons are right to the right of their types followed
  • // by a single space to rhe right.
  • ).ReplaceAll(
  •  
  • "\s*:\s*", ": "
  •  
  • // Break out the full font declaration into parts.
  • ).ReplaceAll(
  •  
  • "font: bold (\d+\w{2}) (\w+)",
  • "font-size: $1 ; font-family: $2 ; font-weight: bold ;"
  •  
  • // Break out the full font declaration into parts.
  • ).ReplaceAll(
  •  
  • "font: italic (\d+\w{2}) (\w+)",
  • "font-size: $1 ; font-family: $2 ; font-style: italic ;"
  •  
  • // Break out the partial font declaration into parts.
  • ).ReplaceAll(
  •  
  • "font: (\d+\w{2}) (\w+)",
  • "font-size: $1 ; font-family: $2 ;"
  •  
  • // Break out a font family name.
  • ).ReplaceAll(
  •  
  • "font: (\w+)",
  • "font-family: $1 ;"
  •  
  • // Break out the full border definition into single values for each of the
  • // four possible borders.
  • ).ReplaceAll(
  •  
  • "border: (\d+\w{2}) (solid|dotted|dashed|double) (\w+)",
  • "border-top-width: $1 ; border-top-style: $2 ; border-top-color: $3 ; border-right-width: $1 ; border-right-style: $2 ; border-right-color: $3 ; border-bottom-width: $1 ; border-bottom-style: $2 ; border-bottom-color: $3 ; border-left-width: $1 ; border-left-style: $2 ; border-left-color: $3 ;"
  •  
  • // Break out a partial border definition into values for each of the four
  • // possible borders. Set default color to black.
  • ).ReplaceAll(
  •  
  • "border: (\d+\w{2}) (solid|dotted|dashed|double)",
  • "border-top-width: $1 ; border-top-style: $2 ; border-top-color: black ; border-right-width: $1 ; border-right-style: $2 ; border-right-color: black ; border-bottom-width: $1 ; border-bottom-style: $2 ; border-bottom-color: black ; border-left-width: $1 ; border-left-style: $2 ; border-left-color: black ;"
  •  
  • // Break out a partial border definition into values for each of the four
  • // possible borders. Set default color to black and width to 2px.
  • ).ReplaceAll(
  •  
  • "border: (solid|dotted|dashed|double)",
  • "border-top-width: 2px ; border-top-style: $2 ; border-top-color: black ; border-right-width: 2px ; border-right-style: $2 ; border-right-color: black ; border-bottom-width: 2px ; border-bottom-style: $2 ; border-bottom-color: black ; border-left-width: 2px ; border-left-style: $2 ; border-left-color: black ;"
  •  
  • // Break out full, single-border definitions into single values.
  • ).ReplaceAll(
  •  
  • "(border-(top|right|bottom|left)): (\d+\w{2}) (solid|dotted|dashed|double) (\w+)",
  • "$1-width: $3 ; $1-style: $4 ; $1-color: $5 ;"
  •  
  • // Break out partial border to single values. Set default color to black.
  • ).ReplaceAll(
  •  
  • "(border-(top|right|bottom|left)): (\d+\w{2}) (solid|dotted|dashed|double)",
  • "$1-width: $3 ; $1-style: $4 ; $1-color: black ;"
  •  
  • // Break out partial border to single values. Set default color to black and
  • // default width to 2px.
  • ).ReplaceAll(
  •  
  • "(border-(top|right|bottom|left)): (solid|dotted|dashed|double)",
  • "$1-width: 2px ; $1-style: $3 ; $1-color: black ;"
  •  
  • // Break 4 part width definition into single width definitions to each of
  • // the four possible borders.
  • ).ReplaceAll(
  •  
  • "border-width: (\d\w{2}) (\d\w{2}) (\d\w{2}) (\d\w{2})",
  • "border-top-width: $1 ; border-right-width: $2 ; border-bottom-width: $3 ; border-left-width: $4 ;"
  •  
  • // Break out full background in single values.
  • ).ReplaceAll(
  •  
  • "background: (solid|dots|vertical|horizontal) (\w+)",
  • "background-style: $1 ; background-color: $2 ;"
  •  
  • // Break out the partial background style into a single value style.
  • ).ReplaceAll(
  •  
  • "background: (solid|dots|vertical|horizontal)",
  • "background-style: $1 ;"
  •  
  • // Break out the partial background color into a single value style.
  • ).ReplaceAll(
  •  
  • "background: (\w+)",
  • "background-color: $1 ;"
  •  
  • // Clear out extra semi colons.
  • ).ReplaceAll(
  •  
  • "(\s*;\s*)+",
  • " ; "
  •  
  • );
  •  
  •  
  • // ASSERT: At this point, we have taken in the raw CSS string that might
  • // have contained many compound CSS style definitions and replaced it with
  • // a string that has ONLY single-value CSS definitions.
  •  
  •  
  • // Break the clean CSS into name-value pairs. This will create an array
  • // of strings, each of which contains a single name-value CSS definition.
  • LOCAL.Pairs = ListToArray( LOCAL.CleanCSS, ";" );
  •  
  • // Loop over each CSS pair using the array's item iterator.
  • for (
  • LOCAL.PairIterator = LOCAL.Pairs.Iterator() ;
  • LOCAL.PairIterator.HasNext() ;
  • ){
  •  
  • // Break out the name value pair. To make sure we have at least
  • // two items in the resulting list, we are appending " : " to the
  • // item values. Since we trim each value, this will not corrupt
  • // our reading, just ensure that the reading of data itself does
  • // not fail.
  • LOCAL.Pair = ToString(LOCAL.PairIterator.Next().Trim() & " : ").Split( ":" );
  •  
  • // Get the name and value values.
  • LOCAL.Name = LOCAL.Pair[ 1 ].Trim();
  • LOCAL.Value = LOCAL.Pair[ 2 ].Trim();
  •  
  • // Check to see if the name exists in the CSS struct. Remember, we only
  • // want to allow values that we KNOW how to handle. If we come across
  • // a CSS definition that is not already in the Struct, just ignore it.
  • if (StructKeyExists( LOCAL.CSS, LOCAL.Name )){
  •  
  • // This is cool, overwrite it. At this point, however, we might
  • // not have exactly proper values. Not sure if I want to deal with
  • // that here or during the CSS application.
  • LOCAL.CSS[ LOCAL.Name ] = LOCAL.Value;
  •  
  • }
  •  
  • }
  •  
  •  
  • // Return the default CSS object.
  • return( LOCAL.CSS );
  •  
  • </cfscript>
  • </cffunction>

If you noticed anything non-standard (like "dots" for background-style), it's because it's POI / Excel specific. Using the above ColdFusion user defined function (UDF), I can make this call:

  • <!--- Parse Raw CSS into a standardized struct. --->
  • <cfset objCSS = ParseRawCSS(
  • "font: 12px verdana ;" &
  • "font-weight: bold ;" &
  • "border: 1px solid red ;" &
  • "border-bottom-width: 3px ;" &
  • "color: lime ;"
  • ) />

In this example, I am breaking up the string into separate values so that you can read it easier. Notice, however, that I am including the ";" between each CSS definition. I am doing my best to keep this as standard as possible (and easy to parse). This results in following struct:


 
 
 

 
Parsing CSS Using ColdFusion  
 
 
 

You will notice that the border definition was successfully copied over each single border value. Then, the bottom border width successfully over wrote the original border width.

There is a lot that is wrong with this approach I am sure. But, for my purposes, it has created a very simple and yet effective way to keep a resulting CSS definition object. Next, I will show you how I have integrated this with my ColduFion POI utility component, but that will have to wait for another post (yeah, I am a dirty tease).




Reader Comments

Mar 28, 2007 at 2:41 PM // reply »
2 Comments

I just read your post about "Parsing And Keeping A CSS Model Using ColdFusion." I've got a question regarding using CSS w/ CFMX. I've got a problem involving a table with categories on a calendar. The categories all need to be colour coded.. All of the calendar data is being called from a database, and I've been told that I need to use a CFSWITCH command. Though my cfmx knowledge is faily limited, can you help me out?


Mar 28, 2007 at 6:10 PM // reply »
10,640 Comments

Yeah, no problem. What defines the coloring? What are you coloring? I just need some more info before I can help.


Apr 2, 2007 at 10:32 AM // reply »
2 Comments

The colouring is defined by a css style called ".eventDayHeader" this is a universal style that applies to every event. What I'm colouring is a header on an event calendar, but I'm trying to get the actual event header to be colour coded according to what kind of event it is. So an emergency event would be bolded, bright red etc... While a social event would be pastel and such.

The event data is pulled from a MySQL table like so:

(cfquery name="queryDetails" datasource="#request.dsn#")

SELECT
*, tblKalendar.*, tblKalendarCategories.CategoryName

FROM

tblKalendar RIGHT OUTER JOIN tblKalendarCategories ON tblKalendar.CategoryID = tblKalendarCategories.CategoryID

WHERE
EventID = (cfqueryparam cfsqltype="cf_sql_varchar" value="#EventID#" /)

(/cfquery)

So what I'm trying to do is have the Javascript look at the event information as it's being put into the table and have the event category reference information on the stylesheet. I've come up with a skeleton

(link rel="stylesheet" href="http://***/kalendar.css")(/link)
(script)

function doSomething( in )
{
...
return out;
}

var gsCSS = "";
var sTheme = doSomething( document.location.href );

switch( sTheme )
{
case "birthday":
gsCSS = "birthday.css";
break;

}
(/script)

Any help??


Apr 2, 2007 at 3:33 PM // reply »
10,640 Comments

@Kevin,

I am not 100% sure what you mean... my guess though is that this is going to be much easier to do in ColdFusion rather than in Javascript. Since ColdFusion is getting the events out of the database (and I assume writing the table HTML), you might as well set the CSS as you draw the table in CF?


Aug 30, 2010 at 2:56 PM // reply »
2 Comments

Hi Ben,

I am trying to export simple data to excel using <cfcontent type="application/vnd.ms-excel">. What is the best way to link an external stylesheet so the excel format can recognize the styles. For some reason, I am not able to do it..


Sep 5, 2010 at 1:25 PM // reply »
10,640 Comments

@Naveen,

When kluding Excel data together, I will typically try to export a valid XLS file as XML or MHT or some other type of text-file. Then, look at it and try to reverse engineer how it is working and linking.

It might be that you have to read in the external file and simply output it with the Excel data when you stream it back.


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »
Feb 9, 2012 at 10:29 PM
Learning ColdFusion 9: Application-Specific Data Sources
@Ben, No offence, but if people were really wanting advanced features they would be using a platform like ASP.NET MVC. CFML is so structurally compromised as a tag-based scripting language that ... read »
Feb 9, 2012 at 10:03 PM
Subversion - Cleanup Failed To Process The Following Paths
@Leviaguirre, do you still have problems with this? ... read »