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.

11 comments:

  1. Now, that is neat! Cant wait to implement this on our documentation site and my blog! Kudos Boyan!

    ReplyDelete
  2. This is a very great brush! Thanks. Except, when I use it, the cfcomments seems not rendered properly.

    Eg.

    <[!]--- some comments --->

    Becomes

    <[!]--- some comments -->

    (1 ending dash is missing)

    And it is not wrapped with the cfcomments class (means not highlighted in yellow bg). However:

    <[!]--- some comments ---->

    is highlighted properly with 4 ending dashes.

    Did you encouter this before? Is it a bug?

    (I have to use [!] instead of ! to pass the HTML tag validation on this blog)

    Thanks
    Khoa

    ReplyDelete
  3. Great job! Thanks for all the hard work on this, I am finding it very useful.

    ReplyDelete
  4. I think Jayrod meant version 2.0 of SyntaxHighlighter, which seems to have undergone a major overhaul.

    ReplyDelete
  5. I took the original version (found it in the svn link) and modified it. I believe it works with 2.0 (or is really close). How can I send it to you?

    ReplyDelete
  6. You can send it to boyank at google mail and I can update it in SVN. Thanks!

    ReplyDelete
  7. I'd love to see a version for SyntaxHighlighter 2.0. Any news?

    ReplyDelete
  8. SyntaxHighlighter 2.0 (not on the Google Code project page anymore but on http://alexgorbatchev.com) still doesn't have ColdFusion support built-in, though it is planned, it seems, for a next version.

    On my SyntaxhHighlighter 2.0 Brushes Overview, you can find a ColdFusion brush for SH 2.0, but I'm not sure whether it is better or not then this one, if this one were modified for use with 2.0.

    I'd be happy to include this one as an alternative on the list once it is modified and tested with 2.0.

    ReplyDelete
  9. This one looks stunning and ver 2.x shBrushColdFusion is not.

    ReplyDelete

// //]]>