Parsing And Keeping A CSS Model Using ColdFusion
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:
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).
Want to use code from this post? Check out the license.
Reader 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?
Yeah, no problem. What defines the coloring? What are you coloring? I just need some more info before I can help.
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??
@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?
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..
@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.