Search

Friday, October 05, 2007

Web Page Syntax Highlighting for ColdFusion - Brush for SyntaxHighlighter

I have been working on a implementing a solution to highlight ColdFusion code snippets when they are being posted on your web site/blog. Highlighting the code according to the appropriate language can make it much easier to read and comprehend (and make it pretty of course). To do this, I have created a ColdFusion syntax "brush" for the JavaScript highlighter SyntaxHighlighter (formally dp.SyntaxHighlighter). In this context, a "brush" is a set of language specific settings for highlighting/prettifying code. SyntaxHighlighter is a pretty cool piece of software and I had some fun and faced some challenges while trying to create ColdFusion specific highlighting brush. Here is a demo of the highlighter at work when given ColdFusion code snippet:
<cfcomponent displayname="applicationController" extends="ModelGlue.unity.controller.Controller" output="false">

<cfscript>
// Set component variables
variables.appConfig = 0;
variables.modelGlueConfig = 0;
variables.rolePermissions = structnew();
variables.userIDWithAdminRights = "";
</cfscript>

<!---
Function: name="changeUserSiteRole"
Created on: 09.25.2007
Updated on: 09.25.2007
Author: Boyan Kostadinov
Arguments: event(ModelGlue.Core.Event)
Return Value: none
Description:
--->
<cffunction name="changeUserSiteRole" access="public" returntype="void" output="false">
 <cfargument name="event" type="ModelGlue.Core.Event">
 <cfset var userID = arguments.event.getValue("userID", 0) />
 <cfset var siteID = arguments.event.getValue("siteID", 0) />
 <cfset var roleID = arguments.event.getValue("roleID", 0) />
 <cfset var showAllUsers = arguments.event.getValue("showAllUsers", false) />

<cfif userID neq 0 and siteID neq 0 and roleID neq 0> <!--- Call the changeUserSiteRole method in the UsersGateway to change the user's role in the specified siteID ---> <cfset variables.usersGateway.changeUserSiteRole(userID, siteID, roleID) /> </cfif>
<cfif showAllUsers> <cfset arguments.event.addResult("allUsers") /> <cfelse> <cfset arguments.event.addResult("singleUser") /> </cfif> </cffunction>
<cffunction name="onQueueComplete" access="public" returntype="void" output="false"> <cfargument name="event" type="any">
<cfset arguments.event.setValue("defaultEvent", getModelGlue().getBean("modelGlueConfiguration").getdefaultEvent()) /> <cfset arguments.event.setValue("appConfig", variables.appConfig) /> <cfset arguments.event.setValue("ApplicationVersion", variables.appConfig.GetConfigSetting("ApplicationVersion")) /> <cfset arguments.event.setValue("ApplicationLastRevisionDate", variables.appConfig.GetConfigSetting("ApplicationLastRevisionDate")) /> <cfset arguments.event.setValue("urlPrefix", variables.defaultTemplate & "?" & arguments.event.getValue("eventValue") & "=") /> <cfset arguments.event.setValue("CurrentPageUrl", getfilefrompath(cgi.script_name) & "?" & cgi.query_string) /> </cffunction>
<cffunction name="onRequestEnd" access="public" returntype="void" output="false"> <cfargument name="event" type="any"> </cffunction>
</cfcomponent>
To install the SyntaxHighlighter on your page:
  1. Get it http://code.google.com/p/syntaxhighlighter/
  2. Extract it somewhere accessable from the web
  3. Add the following code
    <!-- Include the SyntaxHighlighter stylesheet -->
    <style type="text/css" media="screen">@import url("pathToSyntaxHighlighterStyle/syntaxHighlighter.css");</style>
    <!-- Include the core SyntaxHighlighter library -->
    <script language="javascript" src="pathToSyntaxHighlighter/scripts/shCore.js"></script>
    <!-- Include the ColdFusion brush -->
    <script language="javascript" src="pathToSyntaxHighlighter/scripts/shBrushColdFusion.js"></script>
    <script language="javascript">
    window.onload = function () {
     // Set the path to the flash component to enable 'copy to clipboard' in firefox
     dp.SyntaxHighlighter.ClipboardSwf = 'pathToSyntaxHighlighter/scripts/clipboard.swf';
     // Enable blogger mode (if using Blogger)
     dp.SyntaxHighlighter.BloggerMode();
     // Highlight page elements with the name "code"
     // For configuration options see http://code.google.com/p/syntaxhighlighter/wiki/HighlightAll
     dp.SyntaxHighlighter.HighlightAll('code', false, true, false, 1, false);
    }
    </script>
    
  4. Modify the css for your preferences (Firebug is really handy here to figure out what classes you want to modify). Here are my changes:
    <style type="text/css">
    .dp-highlighter ol li, .dp-highlighter .columns div { padding: 0px 3px 0px 0px  !important; font-size: 12px !important; font-family: 'Lucida Console', 'Bitstream Vera Sans Mono', 'Courier New', Monaco, Courier, monospace; }
    .dp-highlighter .tools { padding-left: 0px; }
    .dp-highlighter .tools a { color: #000; text-decoration: underline; }
    .dp-highlighter .tools a:hover { margin-right: 10px; }
    </style>
    
  5. Add some more brushes. For supported languages are C++, C#, CSS, Delphi, Java, JavaScript, PHP, Pythod, Ruby, SQL, VB, XML, HTML, XLST and now ColdFusion :-). More information on the languages at http://code.google.com/p/syntaxhighlighter/wiki/Languages.Here are the brushes I am using:
    <script language="javascript" src="scripts/shBrushCSharp.js"></script>
    <script language="javascript" src="scripts/shBrushCss.js"></script>
    <script language="javascript" src="scripts/shBrushJScript.js"></script>
    <script language="javascript" src="scripts/shBrushSql.js"></script>
    <script language="javascript" src="scripts/shBrushVb.js"></script>
    <script language="javascript" src="scripts/shBrushXml.js"></script>
    
  6. Post your code enclosed in "<pre>" tags as follows:
    <!-- For xml/html/xslt code -->
    <pre class="xml" name="code">
    <!-- For css code -->
    <pre class="css" name="code">
    <!-- For coldfusion code -->
    <pre class="cf" name="code">
    <!-- For sql code -->
    <pre class="sql" name="code">
What the ColdFusion brush does:
  1. Highlights ColdFusion functions, tags, attributes, strings, numbers, comments, cfscript comments
  2. Highlights a very limited set of Model-Glue specific keywords (I got adventures)
  3. Uses the Dreamweaver color shema to apply the syntax highlighting
How to use it and/or modify it:
  1. To just use it (without modifications), just include the compressed version from the svn location at http://opensourceprojects.googlecode.com/svn/dpSyntaxHighlighterColdFusionBrush/trunk/compressed/shBrushColdFusion.js
  2. To use it but modify the css styles, get the uncompressed version from http://opensourceprojects.googlecode.com/svn/dpSyntaxHighlighterColdFusionBrush/trunk/uncompressed/shBrushColdFusion.js and modify the css definitions in "this.Style"
  3. Include it as a brush in your page:
    <script language="javascript" src="pathTo_shBrushColdFusion.js"></script>
Here is actual brush definiton file (highlighted with the JavaScript SyntaxHighlighter brush):
dp.sh.Brushes.ColdFusion = function()
{
 this.CssClass = 'dp-coldfusion';
 this.Style = '.dp-coldfusion { font: 13px "Courier New", Courier, monospace; }' +
 '.dp-coldfusion .tag, .dp-coldfusion .tag-name { color: #990033; }' +
 '.dp-coldfusion .attribute { color: #990033; }' +
 '.dp-coldfusion .attribute-value { color: #0000FF; }' +
 '.dp-coldfusion .cfcomments { background-color: #FFFF99; color: #000000; }' +
 '.dp-coldfusion .cfscriptcomments { color: #999999; }' +
 '.dp-coldfusion .keywords { color: #0000FF; }' +
 '.dp-coldfusion .mgkeywords { color: #CC9900; }' +
 '.dp-coldfusion .numbers { color: #ff0000; }' +
 '.dp-coldfusion .strings { color: green; }';

 this.mgKeywords = 'setvalue getvalue addresult viewcollection viewstate';

 this.keywords = 'var eq neq gt gte lt lte not and or true false ' +
 'abs acos addsoaprequestheader addsoapresponseheader ' +
 'arrayappend arrayavg arrayclear arraydeleteat arrayinsertat ' +
 'arrayisempty arraylen arraymax arraymin arraynew ' +
 'arrayprepend arrayresize arrayset arraysort arraysum ' +
 'arrayswap arraytolist asc asin atn binarydecode binaryencode ' +
 'bitand bitmaskclear bitmaskread bitmaskset bitnot bitor bitshln ' +
 'bitshrn bitxor ceiling charsetdecode charsetencode chr cjustify ' +
 'compare comparenocase cos createdate createdatetime createobject ' +
 'createobject createobject createobject createobject createodbcdate ' +
 'createodbcdatetime createodbctime createtime createtimespan ' +
 'createuuid dateadd datecompare dateconvert datediff dateformat ' +
 'datepart day dayofweek dayofweekasstring dayofyear daysinmonth ' +
 'daysinyear de decimalformat decrementvalue decrypt decryptbinary ' +
 'deleteclientvariable directoryexists dollarformat duplicate encrypt ' +
 'encryptbinary evaluate exp expandpath fileexists find findnocase ' +
 'findoneof firstdayofmonth fix formatbasen generatesecretkey ' +
 'getauthuser getbasetagdata getbasetaglist getbasetemplatepath ' +
 'getclientvariableslist getcontextroot getcurrenttemplatepath ' +
 'getdirectoryfrompath getencoding getexception getfilefrompath ' +
 'getfunctionlist getgatewayhelper gethttprequestdata gethttptimestring ' +
 'getk2serverdoccount getk2serverdoccountlimit getlocale ' +
 'getlocaledisplayname getlocalhostip getmetadata getmetricdata ' +
 'getpagecontext getprofilesections getprofilestring getsoaprequest ' +
 'getsoaprequestheader getsoapresponse getsoapresponseheader ' +
 'gettempdirectory gettempfile gettemplatepath gettickcount ' +
 'gettimezoneinfo gettoken hash hour htmlcodeformat htmleditformat ' +
 'iif incrementvalue inputbasen insert int isarray isbinary isboolean ' +
 'iscustomfunction isdate isdebugmode isdefined isk2serverabroker ' +
 'isk2serverdoccountexceeded isk2serveronline isleapyear islocalhost ' +
 'isnumeric isnumericdate isobject isquery issimplevalue issoaprequest ' +
 'isstruct isuserinrole isvalid isvalid isvalid iswddx isxml ' +
 'isxmlattribute isxmldoc isxmlelem isxmlnode isxmlroot javacast ' +
 'jsstringformat lcase left len listappend listchangedelims listcontains ' +
 'listcontainsnocase listdeleteat listfind listfindnocase listfirst ' +
 'listgetat listinsertat listlast listlen listprepend listqualify ' +
 'listrest listsetat listsort listtoarray listvaluecount ' +
 'listvaluecountnocase ljustify log log10 lscurrencyformat lsdateformat ' +
 'lseurocurrencyformat lsiscurrency lsisdate lsisnumeric lsnumberformat ' +
 'lsparsecurrency lsparsedatetime lsparseeurocurrency lsparsenumber ' +
 'lstimeformat ltrim max mid min minute month monthasstring now ' +
 'numberformat paragraphformat parameterexists parsedatetime pi ' +
 'preservesinglequotes quarter queryaddcolumn queryaddrow querynew ' +
 'querysetcell quotedvaluelist rand randomize randrange refind ' +
 'refindnocase releasecomobject removechars repeatstring replace ' +
 'replacelist replacenocase rereplace rereplacenocase reverse right ' +
 'rjustify round rtrim second sendgatewaymessage setencoding ' +
 'setlocale setprofilestring setvariable sgn sin spanexcluding ' +
 'spanincluding sqr stripcr structappend structclear structcopy ' +
 'structcount structdelete structfind structfindkey structfindvalue ' +
 'structget structinsert structisempty structkeyarray structkeyexists ' +
 'structkeylist structnew structsort structupdate tan timeformat ' +
 'tobase64 tobinary toscript tostring trim ucase urldecode urlencodedformat ' +
 'urlsessionformat val valuelist week wrap writeoutput xmlchildpos ' +
 'xmlelemnew xmlformat xmlgetnodetype xmlnew xmlparse xmlsearch xmltransform ' +
 'xmlvalidate year yesnoformat';

 // Array to hold the possible string matches
 this.stringMatches = new Array();
 this.attributeMatches = new Array();
}

dp.sh.Brushes.ColdFusion.prototype = new dp.sh.Highlighter();
dp.sh.Brushes.ColdFusion.Aliases = ['coldfusion', 'cf'];

dp.sh.Brushes.ColdFusion.prototype.ProcessRegexList = function()
{
 function push(array, value)
 {
  array[array.length] = value;
 }

 function find(array, element)
 {
  for(var i = 0; i < array.length; i++){
   if(array[i] == element){
    return i;
   }
  }

  return -1;
 }

 var match = null;
 var regex = null;

 // Match numbers
 // (\\d+)
 this.GetMatches(new RegExp('\\b(\\d+)', 'gm'), 'numbers');

 // Match mg keywords
 this.GetMatches(new RegExp(this.GetKeywords(this.mgKeywords), 'igm'), 'mgkeywords');

 // Match single line comments via the built in single line regex (for cfscript)
 this.GetMatches(dp.sh.RegexLib.SingleLineCComments, 'cfscriptcomments');

 // Match multi line comments via the built in multi line regex (for cfscript)
 this.GetMatches(dp.sh.RegexLib.MultiLineCComments, 'cfscriptcomments');

 // Match tag based comments (including multiline comments)
 // (\<|<)!---[\\s\\S]*?---(\>|>)
 this.GetMatches(new RegExp('(\<|<)!---[\\s\\S]*?---(\>|>)', 'gm'), 'cfcomments');

 // Match attributes and their values excluding cfset tags
 // (cfset\\s*)?([:\\w-\.]+)\\s*=\\s*(".*?"|\'.*?\')*
 regex = new RegExp('(cfset\\s*)?([:\\w-\.]+)\\s*=\\s*(".*?"|\'.*?\')*', 'gm');
 while((match = regex.exec(this.code)) != null)
 {
  // If there is match in element 1 (the tag is cfset), continute to the next match
  if (match[1] != undefined && match[1] != '')
  {
   continue;
  }

  // Add the atribute to the matches only if it has a matching value (dbtype="query")
  // and the match is not an empty string
  if (match[3] != undefined && match[3] != '' && match[3] != '""' && match[3] != "''")
  {
   push(this.matches, new dp.sh.Match(match[2], match.index, 'attribute'));
   push(this.matches, new dp.sh.Match(match[3], match.index + match[0].indexOf(match[3]), 'attribute-value'));
   // Add the attribute value to the array of string matches
   push(this.stringMatches, match[3]);

   // Add the attribute to the array of attribute matches
   push(this.attributeMatches, match[2]);
  }
 }

 // Match opening and closing tag brackets
 // (\<|<)/*\?*(?!\!)|/*\?*(\>|>)
 this.GetMatches(new RegExp('(\<|<)/*\\?*(?!\\!)|/*\\?*(\>|>)', 'gm'), 'tag');

 // Match tag names
 // (\<|<)/*\?*\s*(\w+)
 regex = new RegExp('(?:\<|<)/*\\?*\\s*([:\\w-\.]+)', 'gm');
 while((match = regex.exec(this.code)) != null)
 {
  push(this.matches, new dp.sh.Match(match[1], match.index + match[0].indexOf(match[1]), 'tag-name'));
 }

 // Match keywords
 regex = new RegExp(this.GetKeywords(this.keywords), 'igm');
 while((match = regex.exec(this.code)) != null)
 {
  // if a match exists (there is a value for the attribute)
  if (find(this.attributeMatches, match[0]) == -1)
  {
   push(this.matches, new dp.sh.Match(match[0], match.index, 'keywords'));
  }
 }

 // Match cfset tags and quoated attributes
 regex = new RegExp('cfset\\s*.*(".*?"|\'.*?\')', 'gm');
 while((match = regex.exec(this.code)) != null)
 {
  // if a match exists (there is a value for the attribute)
  if(match[1] != undefined && match[1] != '')
  {
   push(this.matches, new dp.sh.Match(match[1], match.index + match[0].indexOf(match[1]), 'strings'));

   // Add the attribute to the array of string matches
   push(this.stringMatches, match[1]);
  }
 }

 // Match string enclosed in double quoats
 while((match = dp.sh.RegexLib.DoubleQuotedString.exec(this.code)) != null)
 {
  //if (this.stringMatches.indexOf(match[0]) == -1)
  if (find(this.stringMatches, match[0]) == -1)
   push(this.matches, new dp.sh.Match(match[0], match.index, 'strings'));
 }

 // Match string enclosed in single quoats
 while((match = dp.sh.RegexLib.SingleQuotedString.exec(this.code)) != null)
 {
  //if (this.stringMatches.indexOf(match[0]) == -1)
  if (find(this.stringMatches, match[0]) == -1)
   push(this.matches, new dp.sh.Match(match[0], match.index, 'strings'));
 }
}
Questions/comments? You can leave a comment here or use my contact form to send me your thoughts.
// //]]>