Proof of Concept CFBuilder Extension: convertToCFSCRIPT

This post is more than 2 years old.

I'm working on a project now that involves quite a bit of cut and paste from an old site into a new one. While doing so, I'm also moving some code from tag based CFCs into script based ones. Obviously you can't just paste tag based code into a script based tag. I wondered if it would be possible to build something to help me with some of the grunt work conversion. Specifically, I wanted something that would change:

<cfset x = 1>

Into:

x = 1;

Turns out it wasn't difficult at all - once I figured out how to work with CFBuilder's selection/editor support for extensions.

First - before going any further, if you haven't read about how to build extensions with CFBuilder, I'd suggest taking a look at the docs first. I'd also check out Simon Free's article on DevNet. I'm not going to cover the entire process, just the portions that handle working with text selection.

So the first thing we need is our ide_config.xml file. I've pasted it below:

<application> <name>convertToCFScript</name> <author>Raymond Camden</author> <version>1</version> <email>ray@camdenfamily.com</email> <description>intro.html</description> <license>license.html</license>
&lt;menucontributions&gt;
	&lt;contribution target="editor"&gt;
		&lt;menu name="Convert to CFSCRIPT"&gt;
			&lt;action name="Do It" handlerid="convert" showResponse="false"&gt;&lt;/action&gt;
		&lt;/menu&gt;
	&lt;/contribution&gt;
&lt;/menucontributions&gt;        

&lt;handlers&gt;              
&lt;handler id="convert" type="CFM" filename="convert.cfm" /&gt;
&lt;/handlers&gt;

</application>

My extension will have one menu contribution, and note that it has a target of editor. This tells CFB that my extension works within an opened file, and not the Navigator or RDS section. In this case, I'll have a new menu, Convert to CFScript, with one item, Do It. (FYI, why "Do It"? Currently you can't add an action to the root menu. You can also add a 'folder' with an action underneath it. Obviously thats a bit silly here. I've filed an ER to make this unnecessary.) The action runs my convert handler. Let's look at that now:

<cfsetting showdebugoutput="false"> <cfset ideData=form.ideEventInfo> <cfset data = xmlParse(ideData)>

<cfset fileName = data.event.ide.editor.file.xmlAttributes.location> <cfset selectionStartLine = data.event.ide.editor.selection.xmlAttributes.startline> <cfset selectionStartCol = data.event.ide.editor.selection.xmlAttributes.startcolumn> <cfset selectionEndLine = data.event.ide.editor.selection.xmlAttributes.endline> <cfset selectionEndCol = data.event.ide.editor.selection.xmlAttributes.startline> <cfset selectionText = data.event.ide.editor.selection.text.xmlText>

<!--- handle cfsets ---> <cfset newText = rereplaceNoCase(selectionText, "<cfset[[:space:]]+(.?)[[:space:]]>", "\1;", "all")>

<cfsavecontent variable="test"> <cfoutput> <response> <ide> <commands> <command type="inserttext"> <params> <param key="text"><![CDATA[#newText#]]></param> </params> </command> </commands> </ide> </response> </cfoutput> </cfsavecontent>

<cfheader name="Content-Type" value="text/xml"><cfoutput>#test#</cfoutput>

So first off - when I began development on this little utility, I wasn't sure what the editor was going to send me. I did a bunch of cflogging and after looking at the XML, I saw that I was given:

  • The filename (this includes the full path).
  • The starting line and column of the selection. I've filed an ER to also include the starting numeric character position.
  • The ending line and column of the selection.
  • The actual selected text.

I didn't actually need all of these values, but I've kept them in the code for purely educational purposes.

The actual meat of my logic is one regex. As you can see, I look for cfsets and simply replace them with a script equivalent. If I decide to keep working on this extension, I could look at supporting loops and other items, but for now I wanted to keep things simple. I'll also recommend doing what I did - testing my regex in another file. That kept CFBuilder out of the picture and let me fine tune my regex until it works like I wanted.

Finally - we need to tell the editor to replace the selection with my new text. I was stuck here for a while until I got help from Evelin Varghese of Adobe. Turns out there is a bug in the documentation. The docs say to usebut what you really want is <command type="inserttext">. Once I got the XML set right, everything worked perfectly. Here is a small video example:

Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Kevin Penny posted on 1/11/2010 at 8:53 PM

Awesome Ray! I was going to do this exact same thing! I also wanted an easy way to convert tag to script - with every tag/language component (including the loops etc). Video shows a great start - I'd love to contribute to this over time - keep it up

Comment 2 by Raymond Camden posted on 1/11/2010 at 8:55 PM

To add a bit more support, cfabort/cfinclude:

<!--- aborts, first with no showerror --->
<cfset buffer = rereplaceNoCase(buffer, "<cfabort>", "abort;", "all")>

<!--- aborts, now w/ showerror --->
<cfset buffer = rereplaceNoCase(buffer, "<cfabort[[:space:]]+showerror[[:space:]]*=[[:space:]]*(.*?)[[:space:]]*>", "abort(showerror=\1);", "all")>

<!--- cfinclude --->
<cfset buffer = rereplaceNoCase(buffer, "<cfinclude[[:space:]]+template[[:space:]]*=[[:space:]]*""(.*?)""[[:space:]]*>", "include template=""\1"";", "all")>

Oh - those lines use "buffer" which come from my test script, not the extension. I'll add these in later today and update the zip if folks think it is useful.

Comment 3 by Sam Farmer posted on 1/11/2010 at 8:58 PM

As Kevin said, Awesome! This will prove to be very useful.

Comment 4 by Gary Funk posted on 1/11/2010 at 9:36 PM

@Ray: This is the greatest thing you have done this week.This is totally WOW!. If you can convert a full document, you can clean out your wishlist.

Comment 5 by Robert Gatti posted on 1/11/2010 at 11:24 PM

I know I'm going to be odd guy out but I fail to understand the reasoning behind transforming working code from one language to another. I'm trying to think from the point of someone else on my team sync'ing and all of a sudden seeing totally different looking code, not to mention that I should really go and test the cfc to make sure nothing broke (but I know I wouldn't be diligent at).

Comment 6 by Raymond Camden posted on 1/11/2010 at 11:31 PM

If I had a working tag based CFC, I would not change it. This is code in older, non-MVC format, so it's a lot of business logic mixed in with views.

Comment 7 by Akbarsait posted on 1/11/2010 at 11:32 PM

This is really awesome! And will be really helpful to head start on creating our CFB Extensions. I have added this to CFBuilder Tutorials page of my site here - http://ur.ly/3UVG

Comment 8 by John C. Bland II posted on 1/12/2010 at 10:39 AM

Ray, I'm a fiend for xhtml compliant cf code (lol) and it doesn't work for that.

BEFORE:
<cfset arguments.targetUrl = variables.issuesUrl & arguments.targetUrl />
<cfset arguments.targetUrl = rereplace(arguments.targetUrl, "{verb}", arguments.verb, "all") />
<cfreturn arguments.targetUrl />

AFTER:
arguments.targetUrl = variables.issuesUrl & arguments.targetUrl /;
arguments.targetUrl = rereplace(arguments.targetUrl, "{verb}", arguments.verb, "all") /;
<cfreturn arguments.targetUrl />

Notice the /; at the end and no cfreturn converted?

Also, it'd be sweet if you added a second option to wrap in cfscript or in the default option force a wrap. Something there would be nice.

Great idea and work though. I was wondering if someone was going to do this the other day. :-)

Comment 9 by Peter Boughton posted on 1/12/2010 at 5:19 PM

John, whilst I am also very much in favour of self-closing tags like cfset (for the improved readability it provides), it is entirely incorrect to call it "xhtml compliant cf code", because it is complies with neither HTML nor XML standards.

However, as you have demonstrated, CFML does share many of the complexities of HTML (and indeed has many more besides), and - unless you're able to restrict yourself to a very strict subset - attempting to parse CFML solely with RegEx will lead to madness. There are just too many valid constructs that will trip you up.

Something as simple as this will trip it up:
<cfset HtmlCode = '<input type="text"/>' />

(And I can provide plenty more real-world examples)

Which isn't to say this might not be useful - for anyone insane enough to actually want CFScript - but it's important to make clear that a converter like this is at best an 80-90% job, and is likely to need human attention afterwards.

Comment 10 by Raymond Camden posted on 1/12/2010 at 5:21 PM

Woah, "for anyone insane enough to actually want CFSCript" - I guess you aren't a fan then? ;)

Comment 11 by Peter Boughton posted on 1/12/2010 at 5:56 PM

Not a fan of brace-based languages, no.

Which isn't to say I'm entirely opposed to CFScript, but using it primarily is insane.

Comment 12 by Raymond Camden posted on 1/12/2010 at 5:57 PM

We will have to agree to disagree. ;) I love it - and use it 100% of the time in my CFCs. In my views, not as much of course. It doesn't make sense for HTML.

Comment 13 by Marko Simic posted on 1/12/2010 at 6:40 PM

Ray,
Good luck with converter. Just thinking of it I foreseen dozens of problems. Afraid to think what will pop up during development :) But, as such it's quite a challenge.

PS
cfscript ftw :)

Comment 14 by Kevin Penny posted on 1/12/2010 at 8:05 PM

@Ray -
Maybe something you've thought of before, but I've created and used other cfml parsing engines that attempt to either reformat or generate cfml etc - What I've found seems to work pretty well at times is to treat a tag as a valid xml node - the perfect example might be the cfloop tag -
For example - if you put an ending /> at the end of a cfloop beginning block, you can surround the initial tag and simply parse it
i.e.
<cfloop from="1" to="#arraylen(x)#" index="i">
becomes
<cfloop from="1" to="#arraylen(x)#" index="i"/>
then surrounding becomes:
<tag>
<cfloop from="1" to="#arraylen(x)#" index="i"/>
</tag>
And voila, you have an easy way to parse 'most' cfloops in this fashion, ad can be as dynamic as you want.

Just some gas for the engine -

Comment 15 by Raymond Camden posted on 1/13/2010 at 3:24 AM

I've updated the download to look for / at the end of cfset. It also looks for cfabort and cfinclude.