Ask Ben: Getting CSS Class Names From My CSS Data
Last week, I was asked about getting CSS class names from a CSS file. This data was going to be used to create an XML file that populated the style menu of a rich text editor. I am not sure how to go from CSS class names to XML file (how do you automate the translation of CSS classes to "human-friendly" style options??). I can't figure that one out, but getting the CSS class names is relatively easy.
If you stop and think about how a CSS file is put together, you have your class names followed by rules. You can have multiple class names for the same rule such as "ul, ol { font-size: 11px ; }" where both UL and OL share the same rule. If we then think about the rule "{...}" as a single item, you can look as CSS data as dual-delimited list. The first delimiter is the rule construct - this separates the class names. Then, within each of those sub lists, each class name is delimited by a comma.
Using that, we can clean the CSS data and treat it like a simple list:
<!---
Store CSS for testing. This data could be built in
either by pulling it into a CFSaveContent or perhaps
a CFFile file read.
--->
<cfsavecontent variable="strCSSData">
/* This is my site header */
#header {
background-color: gold ;
}
/*
These are the general style for my site. These
will be applied everywhere and should have a
standard look and feel.
*/
ol,
ul,
p,
form,
h1, h2, h3, h4, h5 {
font-family: verdana ;
font-size: 11px ;
margin-bottom: 14px ;
margin-top: 0px ;
}
p {
line-height: 16px ;
}
ol,
ul {
line-height: 15px ;
}
ul li,
ol li {
margin-bottom: 3px ;
}
form {
margin: 0px 0px 0px 0px ;
}
form {
padding: 10px 10px 10px 10px ;
}
#footer {
font-size: 10px ;
}
#footer p {
margin-bottom: 0px ;
}
#footer p span.note,
#footer p span.date {
font-style: italic ;
}
</cfsavecontent>
<!---
Remove all line breaks. We are going to be doing some
regular expressions and stripping out line breaks will
make things slightly less complicated.
--->
<cfset strCSSData = strCSSData.ReplaceAll( "[\r\n]+", " " ) />
<!---
Strip out the CSS comments. These hold no value for us
when we are getting the classes.
--->
<cfset strCSSData = strCSSData.ReplaceAll( "/\*.*?\*/", " " ) />
<!---
Strip out anything between brackets and replace it with the
pipe. Remember, we don't care about what the rules are, we
just want to get the names of all the classes.
--->
<cfset strCSSData = strCSSData.ReplaceAll( "\{[^\}]*\}", "|" ) />
<!---
ASSERT: At this point, we have stripped out all the rule
data, comments, line breaks, and brackets. Right now, the
CSS data has become what is essentially a dual-delimited
list. The "|" separates rules. The "," sepparated
class names.
--->
<!--- Create an array to hold all of the class names. --->
<cfset arrClasses = ArrayNew( 1 ) />
<!---
Loop over the rules. Remember, each rule is now sepparated
by a pipe (when we stripped out the {..} stuff), so we
can loop over the rules ase a pipe-delimited list.
--->
<cfloop
index="strRule"
list="#strCSSData#"
delimiters="|">
<!---
Each rule may have one or more class sepparated by
commas such as (p, ul, ol). Each of these should be
its own class. Add each of these itesm to the master
class array.
--->
<!--- Trim the rule. --->
<cfset strRule = strRule.Trim() />
<!---
Check to see if we still have a length (name(s)) of
CSS classes.
--->
<cfif Len( strRule )>
<!---
Using the Array's underlying Java method, AddAll(),
we can add the entire array of class names
(created by splitting the comma delimited list) to
the master array.
--->
<cfset arrClasses.AddAll(
ListToArray( strRule, "," )
) />
</cfif>
</cfloop>
<!---
ASSERT: At this point, we now have an array of the CSS class
names. Now, due to the cascading nature of CSS, we might
have duplicate class names in the master class array. In
order to get the unique names for all classes, we can use
the indexing power of the ColdFusion struct to eliminate
the duplicates.
NOTE: Struct do NOT have any sense of ordering. If order of
classes names is important to you, you would have to do
a more involved loop.
--->
<!--- Create a struct to hold unique class names. --->
<cfset objUniqueClasses = StructNew() />
<!---
Loop over the master class array and add each class name to
the struct allowing like-names to overwrite each other.
--->
<cfloop
index="intClass"
from="1"
to="#ArrayLen( arrClasses )#"
step="1">
<!---
Add class name. Just set value to true - we will never
use this value again.
--->
<cfset objUniqueClasses[ arrClasses[ intClass ] ] = true />
</cfloop>
<!--- Output the results. --->
<cfoutput>
All classes:
<cfdump
var="#arrClasses#"
label="All CSS Classes"
/>
Unique classes:
<cfdump
var="#StructKeyArray( objUniqueClasses )#"
label="Unique CSS Classes"
/>
</cfoutput>
Running the above code gets us the two CFDumps for the master class array:
... and the unique class array:
That's all there is to it. Frankly, that was the easy step. How do you take that list and create a meaningful XML file? That I cannot answer.
Want to use code from this post? Check out the license.
Reader Comments
In your example, you technically do not have any class names. In CSS class names begin with a '.'
.header will style elements with an attribute of class equal to 'header'
#header will style elements with an attribute of id equal to 'header'
In your resulting array, there really were no classes shown. To use this list in a Rich Text Editor, you would probably want only the classes and not ids, or tags since the text editor would most likely use class="className" for an element.
What Scott Said ;)
What you are getting are the selectors basically what rules are in the CSS file.
The following code might do the trick as well:
<!--- Create an array to hold the CSS classes --->
<cfset aCSS = ArrayNew(1)>
<cfset start=1>
<!--- Find CSS class occurances (.something) --->
<cfloop condition="start lt len(strCSSData)">
<cfset temp = reFind("\.[-]?[_a-zA-Z][_a-zA-Z0-9-]*|[^\0-\177]*\\[0-9a-f]{1,6}(\r\n[ \n\r\t\f])?|\\[^\n\r\f0-9a-f]*",strCSSData,start,true)>
<cfif temp.pos[1]>
<cfset arrayappend(aCSS,mid(strCSSData,temp.pos[1],temp.len[1]))>
<cfset start = temp.pos[1]+temp.len[1]>
<cfelse>
<cfbreak>
</cfif>
</cfloop>
<cfdump var="#aCSS#">
Big post. Please make it available full after clicking more and leave several lines for preview
Oh, by the way .... I am deeply offended by this entry... it does not contain any semi-nude photos whatsoever !!!
reference:www.bennadel.com/blog/574-How-Do-I-Offend-You-Please-Let-Me-Know.htm
Yeah, I call them class names, but to be honest, I wasn't really thinking about what actually was a class name. I was thinking in my head "selector" but calling it class name.
If you think about it, class names are generally not enough for the ultimate goal here. Many "selectors" don't even have class names but should be totally valid markup (think about all the H tags).... this is part of why I would not be able to figure out the next step: translating this list to some sort of usable XML file.
Ben -
If you have a CSS file that defined styles for tag based selectors, you wouldn't want to use those selectors as style options in a drop down for a rich text editor. Rather, you'd have options in the RTE for h1,h2,h3 (or large header, medium header, small header), which the user could then use. The user is going to define the formatting for their content (whether they want lists, or paragraphs, or whatever). The fact that you have style definitions for these doesn't matter (IMHO).
However, if you have custom class definitions -- say "caption" (small, bold letters), or "important" (large red letters) -- these would make sense to have available in a drop down pulled from the CSS file. In this case, you could actually use a comment string to give a "user-friendly" name for the class which would be used to populate the drop down.
@Toby,
While I agree with in the most part, I think that letting the user choose H1/2/3/etc. might not be the best idea. As much as they are "standard", they are still left up to interpretation. So, your rich editor might have a style called "Page Title" or "Section Title" or "Item Title" or "Sub Title" or something along those lines. These might just put in the appropriate H tags. By letting the user arbitrarily choose what H tag they think is most appropriate, you might get screwy results. However, if you only allow them to choose "descriptive" titles (ie. Section Titlte), it might help to standardize (but of course, nothing is for sure).
Wow, lots of food for thought!!! I was the guy who asked Ben if he had any clue how to go about doing this and who was more than a little surprised to get a complete working app about 20 minutes later (thanks Ben!) :->
Goal is indeed to get a drop down list of unique styles for a WYSIWYG editor, and ideal would be something like "H1,H2,H3,p,MyStyle1,MyStyle2,Mystyle3" which I can then drop into the trivial FCK Editor XML file which doesn't know anything about the properties of each class, but just allows you to tell it what the class names are so people can select them from a list.
I have never used FCK editor, so I leave advice up to the rest of you :)
The problem with trying to allow your users to style their content in this way is that items get misapplied.
Its much better and easier for an end user not to have to worry about styles and simply apply structural HTML rather than CSS.
Not sure how you can do it with a WYSIWYG editor, but its a lot easier to have simple HTML for your users to create and leave the complexity of the styling to the style sheet itself.
One of my sessions at CFUNITED this year (CSS: Back to the Basics) is going to be covering Structural HTML and how to select it using advanced CSS Selectors - strangely enough ;)
Another potential issue of allowing a user to use any/all selectors in a CSS file is the fact that, depending on the editor, it may assign class="p" to an element when chosen from the list of styles, yet if the style is only p{CSS stuff;} then the style would not be applied.
Peter, if FCK applies the styles as class="className", and the class is chosen from the drop down, then you would need to make sure only cvalid class names appear in the drop down, and not every selector.
It might also be wise to have your CSS styles that will typically be applied by an end user in a separate CSS file, rather than in your layout CSS file(s) or others. You would then be running your regex on a much smaller file and you would have the added benefit of always knowing where to go to specify your user selectable styles too :)
Another thing to consider is that typically your css for a content area on a site might look like this:
#content p.extract {}
#content blockquote.feature {}
#content div.col1 {}
etc... or instead of using the id #content it might just be a class name .content. However, when you go to apply this to FCK editor the id/class will need to be stripped from the start of the selector - not terribly difficult, just make sure you can configure which id's/classes need to be stripped, so that you don't strip too much out of a rule, as in:
#content table.alt td.odd {}
You don't want to lose the "table.alt" part out of that selector, only the id (#content) part!
Depending on the project and the size of your CSS (this is gonna hurt), it's almost just easier to keep your CSS well organised and hand edit it. I realise that's not the point of this post, and Peter will kill me for even suggesting that (haha!), but sometimes (for simple projects) it's the truth :P
For a larger project though, or one where your users are competent enough and/or have the ability to edit the CSS themselves (for e.g. maybe in a FarCry site where CSS can be a content object, editable by users), a generated solution would be excellent.
Err, actually I should have said there is kinda 2 steps to applying styles to FCK editor. One is actually providing the CSS for the editor to apply when the user is editing the content, which is kinda what I was on about above (in regards to stripping the id/class from some selectors so that FCK editor can apply the styles - obviously you need to leave the stuff in the curly braces alone*), but the second part of the process is the XML, which I didn't realise (until now) was such a pain to edit... I am totally going to need a generated solution for this for use with a FarCry site of mine :P
Hey Ben,
I know that it has been a while since this thread was discussed, but I just stumbled upon it after looking for some sort of regex to parse css (I am terrible at regex, no practice). I created a UDF which might make life easier for some people. It takes the rules and returns structures. That way people can loop over the structs and create xml, or whatever.
Here is is-
<!---
Store CSS for testing. This data could be built in
either by pulling it into a CFSaveContent or perhaps
a CFFile file read.
--->
<cfsavecontent variable="strCSSData">
/* This is my site header */
#header {
background-color: gold ;
}
/*
These are the general style for my site. These
will be applied everywhere and should have a
standard look and feel.
*/
ol,
ul,
p,
form,
h1, h2, h3, h4, h5 {
font-family: verdana ;
font-size: 11px ;
margin-bottom: 14px ;
margin-top: 0px ;
}
p {
line-height: 16px ;
}
ol,
ul {
line-height: 15px ;
}
ul li,
ol li {
margin-bottom: 3px ;
}
form {
margin: 0px 0px 0px 0px ;
}
form {
padding: 10px 10px 10px 10px ;
}
#footer {
font-size: 10px ;
}
#footer p {
margin-bottom: 0px ;
}
#footer p span.note,
#footer p span.date {
font-style: italic ;
}
</cfsavecontent>
<cffunction name="cssToStruct" access="public" returntype="any">
<cfargument name="css_data" type="string" required="yes">
<!---Create the local scope--->
<cfset var local = {}>
<!---This struct will hold all of the rules--->
<cfset LOCAL.cssRules = {}>
<!---
Remove all line breaks. We are going to be doing some
regular expressions and stripping out line breaks will
make things slightly less complicated.
--->
<cfset LOCAL.strCSSData = reReplace(arguments.css_data,"[\r\n]+", " ","all") />
<!---
Strip out the CSS comments. These hold no value for us
when we are getting the classes.
--->
<cfset LOCAL.strCSSData = reReplace(LOCAL.strCSSData,"/\*.*?\*/", " ", "all" ) />
<!--- Create an array to hold all of the class names. --->
<cfset LOCAL.arrClasses = ArrayNew( 1 ) />
<!---
Loop over the rules. Remember, each rule is now sepparated
by a pipe (when we stripped out the {..} stuff), so we
can loop over the rules ase a pipe-delimited list.
--->
<cfloop
index="LOCAL.strRule"
list="#LOCAL.strCSSData#"
delimiters="}">
<!---
Check to see if we still have a length (name(s)) of
CSS classes.
--->
<cfif Len( trim(LOCAL.strRule) )>
<!---Add the item to the array--->
<cfset arrayAppend(LOCAL.arrClasses,LOCAL.strRule) />
</cfif>
</cfloop>
<cfloop array="#LOCAL.arrClasses#" index="LOCAL.each_class">
<cfset LOCAL.cssRules[listFirst(LOCAL.each_class,"{")] = {}>
<cfloop list="#listLast(LOCAL.each_class,"{")#" delimiters=";" index="LOCAL.each_rule">
<cfset LOCAL.cssRules[listFirst(LOCAL.each_class,"{")][trim(listFirst(LOCAL.each_rule,":"))] = trim(listLast(LOCAL.each_rule,":"))>
</cfloop>
</cfloop>
<cfreturn LOCAL.cssRules>
</cffunction>
<cfdump var="#cssToStruct(strCSSData)#">