Skip to main content
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Sean Corfield
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Sean Corfield ( @seancorfield )

How To UN-Unformat Your Code (Like A Pro)

By on
Tags:

After my previous post on how to quickly and easily take my style of formatting and strip it out of ColdFusion code, some people suggested that I make a function that goes back the other way (adding my style of formatting into ColdFusion code). While I love this concept, the problem is actually quite a bear! Removing formatting doesn't really require much understanding of the code - you simply have to match patterns; adding formatting back in, however, now that requires really understanding what the code is doing at a detailed level. As such, there is no way that I would be able to do that, at least not with the attention span I was willing to give it. But, I did come up with a quick little something that might at least get you in the right direction.

Now, the Obsessive.cfc ColdFusion component has two methods:

  • Obsessive::format( code )
  • Obsessive::unformat( code )

I have only briefly tested this, so this was more for fun than anything else (don't take it as finished). It seems to work OK - it tries to put in appropriate tag-based line breaks and spacing. There's so much more that would need to be done; for example, normalizing all of the tag attributes (to use double quotes). Anyway, if you are curious, here is what I have:

Obsessive.cfc

<cfcomponent
	output="false"
	hint="I am a component written by Ben Nadel to help people remove Ben's own meticulous formatting from his code - some kids just aren't cool enough ;)">


	<cffunction
		name="init"
		access="public"
		returntype="any"
		output="false"
		hint="I initialize this component.">

		<!--- I am the code that being unformatted. --->
		<cfset this.setCode( "" ) />

		<!--- Set up some constants. --->
		<cfset this.tab = chr( 9 ) />
		<cfset this.newLine = (chr( 13 ) & chr( 10 )) />
		<cfset this.newLine2 = (this.newLine & this.newLine) />

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="createMatcher"
		access="public"
		returntype="any"
		output="false"
		hint="I create a pattern matcher based on the given regular expression and store it as the internal matcher.">

		<!--- Define arguments. --->
		<cfargument
			name="pattern"
			type="string"
			required="true"
			hint="I am the regular expression pattern."
			/>

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Compile the regular expression for the tag. --->
		<cfset local.pattern = createObject(
			"java",
			"java.util.regex.Pattern"
			).compile(
				javaCast( "string", arguments.pattern )
			) />

		<!---
			Create a pattern matcher for the given pattern and
			the current code.
		--->
		<cfset local.matcher = local.pattern.matcher(
			this.getCode()
			) />

		<!--- Store the current matcher. --->
		<cfset this.setMatcher( local.matcher ) />

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="findMatch"
		access="public"
		returntype="boolean"
		output="false"
		hint="I call find on the current matcher.">

		<!--- Check to see if the next match was found. --->
		<cfif this.getMatcher().find()>

			<!--- The next match was found. --->
			<cfreturn true />

		<cfelse>

			<!---
				The next match was not found. At this point, we
				want to append any existing code into the buffer
				and then comit the buffer back into the code.
			--->

			<!--- Append the tail. --->
			<cfset this.getMatcher().appendTail( this.getBuffer() ) />

			<!--- Commit the buffer into the code. --->
			<cfset this.setCode( this.getBuffer().toString() ) />

			<!--- Return false. --->
			<cfreturn false />

		</cfif>
	</cffunction>


	<cffunction
		name="format"
		access="public"
		returntype="string"
		output="false"
		hint="I format the given code (adding Ben's code meticulous code formatting - as much as possible).">

		<!--- Define arguments. --->
		<cfargument
			name="code"
			type="string"
			required="true"
			hint="I am the code being formatted."
			/>

		<!--- Set the code, format it, and return it. --->
		<cfreturn this
			.setCode( arguments.code )
			.formatCFTag( "CFArgument", true )
			.formatCFTag( "CFCookie", true )
			.formatCFTag( "CFComponent" )
			.formatCFTag( "CFContent", true )
			.formatCFTag( "CFDirectory", true )
			.formatCFTag( "CFFunction" )
			.formatCFTag( "CFHeader", true )
			.formatCFTag( "CFHttp" )
			.formatCFTag( "CFHttpParam", true )
			.formatCFTag( "CFImage", true )
			.formatCFTag( "CFLocation", true )
			.formatCFTag( "CFLock" )
			.formatCFTag( "CFLoop" )
			.formatCFTag( "CFMail" )
			.formatCFTag( "CFMailParam", true )
			.formatCFTag( "CFParam", true )
			.formatCFTag( "CFSetting", true )
			.formatCFTag( "CFZip" )
			.formatCFTag( "CFZipParam", true )
			.formatSpacing()
			.formatComments()
			.getCode()
			/>
	</cffunction>


	<cffunction
		name="formatCFTag"
		access="public"
		returntype="any"
		output="false"
		hint="I format the given ColdFusoin tag with the given name.">

		<!--- Define arguments. --->
		<cfargument
			name="tagName"
			type="string"
			required="true"
			hint="I am the name of the tag being formatted."
			/>

		<cfargument
			name="isSelfClosing"
			type="boolean"
			required="false"
			default="false"
			hint="I flag whether or not this type of flag should be self-closing."
			/>

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Create the pattern for the tag. --->
		<cfsavecontent variable="local.regex">(?ixm)
			<cfoutput>

				^(\p{Blank}*)<#arguments.tagName#
				(
					[^>"']
					|
					"([^"]|"")*"
					|
					'([^']|'')*'
				)*
				>

			</cfoutput>
		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Get the tag code. --->
			<cfset local.tagCode = trim( this.getMatch() ) />

			<!--- Parse the tag code into a data set. --->
			<cfset local.tagData = this.parseTag(
				trim( this.getMatch() )
				) />

			<!--- Get the leading space. --->
			<cfset local.leadingSpace = this.getMatch( 1 ) />

			<!---
				Normalize the spaces (replacing 4 spaces with a
				tab). There's simply no hope for code that was
				using two-space indents.
			--->
			<cfset local.leadingSpace = reReplace(
				local.leadingSpace,
				" {4}",
				this.tab,
				"all"
				) />

			<!---
				Now that we have replaced-in tabs, remove any left
				over spaces from the leading space.
			--->
			<cfset local.leadingSpace = reReplace(
				local.leadingSpace,
				" +",
				"",
				"all"
				) />


			<!---
				Create a constant for the indent of the sub-lines.
				This will be the leading space plus an additional
				tab character.
			--->
			<cfset local.subIndent = (local.leadingSpace & this.tab) />


			<!---
				Now that have the tag data parsed and the leading
				space (indent), we can now construct the new tag
				output.
			--->
			<cfset local.newCode = (
				local.leadingSpace &
				"<" &
				lCase( local.tagData.name )
				) />

			<!--- Loop over each attribute to append to code. --->
			<cfloop
				index="local.attribute"
				array="#local.tagData.attributes#">

				<!--- Add the attribute on its own line. --->
				<cfset local.newCode &= (
					this.newLine &
					local.subIndent &
					local.attribute.name &
					"=" &
					local.attribute.value
					) />

			</cfloop>

			<!---
				When closing the tag, check to see if this tag is
				self closing.
			--->
			<cfif arguments.isSelfClosing>

				<!--- Add the self-closing cap on the next line. --->
				<cfset local.newCode &= (
					this.newLine &
					local.subIndent &
					"/>"
					) />

			<cfelse>

				<!--- Add the cap on the same line. --->
				<cfset local.newCode &= ">" />

			</cfif>


			<!---
				If there was only ONE attribute in the tag,
				override the spacing so that it is all on one line.
			--->
			<cfif (arrayLen( local.tagData.attributes ) eq 1)>

				<!--- Remove line breaks. --->
				<cfset local.newCode = reReplace(
					local.newCode,
					"[ \t]*[\r\n]+[ \t]*",
					" ",
					"all"
					) />

			</cfif>


			<!--- Replace with the new tag code. --->
			<cfset this.replaceMatch( local.newCode ) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="formatComments"
		access="public"
		returntype="any"
		output="false"
		hint="I format the comments.">

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Get all multiline comments. --->
		<cfset this.createMatcher(
			"(?ms)^([ \t]*)<!---(((?!--->).)+)--->"
			) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Get the leading space. --->
			<cfset local.leadingSpace = this.getMatch( 1 ) />

			<!---
				Normalize the spaces (replacing 4 spaces with a
				tab). There's simply no hope for code that was
				using two-space indents.
			--->
			<cfset local.leadingSpace = reReplace(
				local.leadingSpace,
				" {4}",
				this.tab,
				"all"
				) />

			<!---
				Now that we have replaced-in tabs, remove any left
				over spaces from the leading space.
			--->
			<cfset local.leadingSpace = reReplace(
				local.leadingSpace,
				" +",
				"",
				"all"
				) />


			<!---
				Create a constant for the indent of the sub-lines.
				This will be the leading space plus an additional
				tab character.
			--->
			<cfset local.subIndent = (local.leadingSpace & this.tab) />


			<!--- Get the comment itself. --->
			<cfset local.comment = trim( this.getMatch( 2 ) ) />

			<!--- Remove any line breaks from the comment. --->
			<cfset local.comment = reReplace(
				local.comment,
				"\s+",
				" ",
				"all"
				) />


			<!---
				Check to see how long the comment is. If it is
				longer than 70 characters, we will break it up
				into several lines. Otherwise, we will leave it
				as a single line.
			--->
			<cfif (len( local.comment ) gt 70)>

				<!--- Build up the new comment. --->
				<cfset local.newComment = (
					local.leadingSpace &
					"<!---" &
					this.newLine
					) />

				<!---
					Break the comment into words of length no
					greater than 70.
				--->
				<cfset local.lines = reMatch(
					".{0,70}\b(?!\s)",
					local.comment
					) />

				<!--- Add the lines to the new comment. --->
				<cfset local.newComment &= (
					local.subIndent &
					arrayToList(
						local.lines,
						(this.newLine & local.subIndent)
						) &
					this.newLine
					) />

				<!--- Close the comment. --->
				<cfset local.newComment &= (
					local.leadingSpace &
					"--->"
					) />

				<!--- Repalce in the new comment. --->
				<cfset this.replaceMatch( local.newComment ) />

			<cfelse>

				<!--- Leave as a single-line comment. --->
				<cfset this.replaceMatch(
					local.leadingSpace &
					"<!--- " &
					local.comment &
					" --->"
					) />

			</cfif>

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the tag. --->
		<cfsavecontent variable="local.regex">(?ixm)
			<cfoutput>

				(
					<(cffunction)
					([^>"]|"([^"]|"")*")*
					>.*
					(\r\n?|\n)

					(^\p{Blank}*$(\r\n?|\n))*
				)

				^(\p{Blank}*)<(cfargument)

			</cfoutput>
		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Add the pre-argument comment. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.getMatch( 8 ) &
				"<!--- Define arguments. --->" &
				this.newLine &
				this.getMatch( 8 ) &
				"<" &
				this.getMatch( 9 )
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the local scope. --->
		<cfsavecontent variable="local.regex">(?ixm)

			^(\p{Blank}*)<(cfset)\s+var\s+local

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Add the local scope comment. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				"<!--- Define the local scope. --->" &
				this.newLine &
				this.getMatch( 1 ) &
				"<cfset var local"
				) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="formatSpacing"
		access="public"
		returntype="any"
		output="false"
		hint="I add certain spacing to the code.">

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				<(cfcomponent)
				([^>"]|"([^"]|"")*")*
				>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))*

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.newLine2
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				<(cf(function|argument))
				([^>"]|"([^"]|"")*")*
				>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))*

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.newLine
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				</(cffunction)>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))*

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.newLine2
				) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="getBuffer"
		access="public"
		returntype="any"
		output="false"
		hint="I return the current string buffer being used in conjunction with the current regular expression pattern matcher.">

		<!--- Return the buffer. --->
		<cfreturn variables.buffer />
	</cffunction>


	<cffunction
		name="getCode"
		access="public"
		returntype="string"
		output="false"
		hint="I return the code.">

		<!--- Return this current code state. --->
		<cfreturn variables.code />
	</cffunction>


	<cffunction
		name="getMatch"
		access="public"
		returntype="any"
		output="false"
		hint="I get the current match.">

		<!--- Define arguments. --->
		<cfargument
			name="group"
			type="string"
			required="false"
			default="0"
			hint="I am the group being returned - default to zero, which is the entire match."
			/>

		<!---
			Return the current match from the active
			pattern matcher.
		--->
		<cfreturn this.getMatcher().group(
			javaCast( "int", arguments.group )
			) />
	</cffunction>


	<cffunction
		name="getMatcher"
		access="public"
		returntype="any"
		output="false"
		hint="I return the current regular expression pattern matcher.">

		<!--- Return the current matcher. --->
		<cfreturn variables.matcher />
	</cffunction>


	<cffunction
		name="initBuffer"
		access="public"
		returntype="any"
		output="false"
		hint="I create a new string buffer to be used with the current regular expression pattern matcher.">

		<!--- Store a new buffer. --->
		<cfset variables.buffer = createObject(
			"java",
			"java.lang.StringBuffer"
			).init()
			/>

		<!--- Return this component reference. --->
		<cfreturn />
	</cffunction>


	<cffunction
		name="parseTag"
		access="public"
		returntype="struct"
		output="false"
		hint="I parse the given tag code.">

		<!--- Define arguments. --->
		<cfargument
			name="code"
			type="string"
			required="true"
			hint="I am the flat tag code being parsed."
			/>

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Create the tag structure. --->
		<cfset local.tag = {
			name = "",
			attributes = []
			} />

		<!---
			Create a new pattern for matching the tokens of
			the tag, starting with the tag name and then the
			attribute pairs.
		--->
		<cfsavecontent variable="local.regex">(?x)

			^<[^\s]+

			|

			\s+
			[\w:]+
			\s*=\s*
			(
				"[^"]*(""[^"]*)*"
				|
				'[^']*(''[^']*)*'
				|
				[^\s]+
			)

		</cfsavecontent>

		<!--- Extract the tag tokens. --->
		<cfset local.tokens = reMatch(
			local.regex,
			arguments.code
			) />

		<!--- Set the name of the tag as the first token. --->
		<cfset local.tag.name = reReplace(
			lCase( local.tokens[ 1 ] ),
			"^<",
			"",
			"one"
			) />

		<!---
			Delete the tag name token so we don't process it as
			one of the tag attributs.
		--->
		<cfset arrayDeleteAt( local.tokens, 1 ) />

		<!---
			Now, let's look over the rest of the tokens and parse
			the name-value pairs.
		--->
		<cfloop
			index="local.token"
			array="#local.tokens#">

			<!--- Create a new attribute as dicated by the equals. --->
			<cfset local.attribute = {
				name = lcase( trim( listFirst( local.token, "=" ) ) ),
				value = trim( listRest( local.token, "=" ) )
				} />

			<!---
				Further clean the value to make sure that value is
				only double quoted.
			--->
			<cfset local.attribute.value = (
				"""" &
				reReplace(
					reReplace(
						local.attribute.value,
						"^[""']|[""']$",
						"",
						"all"
						),
					"""""|""(?!"")",
					"""""",
					"all"
					) &
				""""
				) />

			<!--- Add the parsed attribute to the collection. --->
			<cfset arrayAppend(
				local.tag.attributes,
				local.attribute
				) />

		</cfloop>

		<!--- Return the parsed tag data. --->
		<cfreturn local.tag />
	</cffunction>


	<cffunction
		name="removeMatches"
		access="public"
		returntype="any"
		output="false"
		hint="I remove all of the matches.">

		<!--- Loop over all the matches. --->
		<cfloop condition="this.findMatch()">

			<!---
				Replace the current match with the empty string.
				This will remove it from the code.
			--->
			<cfset this.replaceMatch( "" ) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="replaceMatch"
		access="public"
		returntype="any"
		output="false"
		hint="I replace the given string in leu of the current match.">

		<!--- Define arguments. --->
		<cfargument
			name="value"
			type="string"
			required="true"
			hint="I am the value being replaced in."
			/>

		<!--- Replace the current string. --->
		<cfset this.getMatcher().appendReplacement(
			this.getBuffer(),
			reReplace(
				arguments.value,
				"([\\\$])",
				"\\\1",
				"all"
				)
			)/>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="setCode"
		access="public"
		returntype="any"
		output="false"
		hint="I store the given code as part of the object state.">

		<!--- Define arguments. --->
		<cfargument
			name="code"
			type="string"
			required="true"
			hint="I am the code being stored."
			/>

		<!--- Store the code. --->
		<cfset variables.code = arguments.code />

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="setMatcher"
		access="public"
		returntype="any"
		output="false"
		hint="I store the given matcher as the internal matcher.">

		<!--- Define arguments. --->
		<cfargument
			name="matcher"
			type="any"
			required="true"
			hint="I am the matcher being stored."
			/>

		<!--- Store the matcher. --->
		<cfset variables.matcher = arguments.matcher />

		<!---
			Whenever we store a new matcher, we want to re-init
			the buffer since this matcher will be used with a
			replacement mechanism.
		--->
		<cfset this.initBuffer() />

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="unformat"
		access="public"
		returntype="string"
		output="false"
		hint="I unformat the given code (removing Ben's code meticulous code formatting).">

		<!--- Define arguments. --->
		<cfargument
			name="code"
			type="string"
			required="true"
			hint="I am the code being unformatted."
			/>

		<!--- Set the code, unformat it, and return it. --->
		<cfreturn this
			.setCode( arguments.code )
			.unformatCFTag( "CFArgument" )
			.unformatCFTag( "CFCookie" )
			.unformatCFTag( "CFComponent" )
			.unformatCFTag( "CFContent" )
			.unformatCFTag( "CFDirectory" )
			.unformatCFTag( "CFFunction" )
			.unformatCFTag( "CFHeader" )
			.unformatCFTag( "CFHttp" )
			.unformatCFTag( "CFHttpParam" )
			.unformatCFTag( "CFIF" )
			.unformatCFTag( "CFImage" )
			.unformatCFTag( "CFLocation" )
			.unformatCFTag( "CFLock" )
			.unformatCFTag( "CFLoop" )
			.unformatCFTag( "CFMail" )
			.unformatCFTag( "CFMailParam" )
			.unformatCFTag( "CFParam" )
			.unformatCFTag( "CFReturn" )
			.unformatCFTag( "CFSet" )
			.unformatCFTag( "CFSetting" )
			.unformatCFTag( "CFZip" )
			.unformatCFTag( "CFZipParam" )
			.unformatComments()
			.unformatSpacing()
			.getCode()
			/>
	</cffunction>


	<cffunction
		name="unformatCFTag"
		access="public"
		returntype="any"
		output="false"
		hint="I unformat the ColdFusion tag with the given name.">

		<!--- Define arguments. --->
		<cfargument
			name="tagName"
			type="string"
			required="true"
			hint="I am the name of the tag being unformatted."
			/>

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Create the pattern for the tag. --->
		<cfsavecontent variable="local.regex">(?ix)
			<cfoutput>

				<#arguments.tagName#
				([^>"]|"([^"]|"")*")*
				>

			</cfoutput>
		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Remove the line breaks. --->
			<cfset local.replace = reReplace(
				this.getMatch(),
				"[\s\t]*[\r\n]+[\s\t]*",
				" ",
				"all"
				) />

			<!--- Remove any spaces before periods. --->
			<cfset local.replace = reReplace(
				local.replace,
				" \.",
				".",
				"all"
				) />

			<!--- Replace match.. --->
			<cfset this.replaceMatch( local.replace ) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="unformatComments"
		access="public"
		returntype="any"
		output="false"
		hint="I unformat the comments.">

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Get all multiline comments. --->
		<cfset this.createMatcher(
			"(?ms)^([ \t]*)<!---(((?!--->).)+)--->"
			) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Unwrap the comments. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				"<!---" &
				reReplace(
					this.getMatch( 2 ),
					"[ \t]*(\r\n?|\n)[ \t]*",
					" ",
					"all"
					) &
				"--->"
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- We want to remove any "Define" comments. --->
		<cfset this.createMatcher(
			"(?ms)^[ \t]*(<!---) Define(((?!--->).)+)(--->)"
			) />

		<!--- Remove matches. --->
		<cfset this.removeMatches() />

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>


	<cffunction
		name="unformatSpacing"
		access="public"
		returntype="any"
		output="false"
		hint="I remove certain spacing from the code.">

		<!--- Define the local scope. --->
		<cfset var local = {} />

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				<(cfcomponent)
				([^>"]|"([^"]|"")*")*
				>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))+

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.newLine
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				<(cf(function|argument))
				([^>"]|"([^"]|"")*")*
				>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))+

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 )
				) />

		</cfloop>

		<!--- --------------------------------------------- --->

		<!--- Create the pattern for the spacing. --->
		<cfsavecontent variable="local.regex">(?ixm)

			(
				</(cffunction)>
				.*
				(\r\n?|\n)
			)

			(^(\p{Blank})*$(\r\n?|\n))+

		</cfsavecontent>

		<!--- Create a new matcher. --->
		<cfset this.createMatcher( local.regex ) />

		<!--- Loop over each of the match. --->
		<cfloop condition="this.findMatch()">

			<!--- Replace the post-tag empty lines. --->
			<cfset this.replaceMatch(
				this.getMatch( 1 ) &
				this.newLine
				) />

		</cfloop>

		<!--- Return this component reference. --->
		<cfreturn this />
	</cffunction>

</cfcomponent>

I would show a demo of the code, but I can't get it to re-format code enough to present it nicely in a blog post.

Want to use code from this post? Check out the license.

Reader Comments

32 Comments

Looking over your cfc I notice you set all function to access publicly. Since only two(Plus Init) of them should ever get called from outside of the cfc, why don't you make the rest private?

15,688 Comments

@Daniel,

Three reasons for that:

1. Pre-ColdFusion 9, you can't execute variables-scoped methods using named arguments, only ordered arguments. As such, if I were to make my methods private, I could only call them as such:

variables.method( arg, arg )

... and NOT:

variables.method( name = arg, name = arg )

Now, I'm not saying I call all my methods with named arguments, but I like the option, especially if I ever need to have variable length argument lists.

In CF9, however, this bug will be removed.

2. Private methods simply don't add value to the way I program.

3. I am pretty sure, although now I am beginning to doubt myself (and this related back to #1), but I think that the THIS scope of a CFC only contains the public methods, even when called internally. And, I happen to prefer writing THIS.method() rather than using the variables scope. This is a shallow, aesthetic reason, but one that I think about.

32 Comments

@Ben,

I am glad they fixed that in CF9 as that doesn't make sense why you wouldn't be able to if one was private and one wasn't.

In general I can see that they don't add value to the way most people program. Though if CFBuilder continues to get better I hope their insight would be able to detect public/private methods and only show it if it is able to be used in that situation. Also in my opinion it does help make code more readable so you know which functions are suppose to be used outside of the cfc.

I guess from a java standpoint I am already use to using variableName.method()

For me in general I prefer providing access to the function only where it is meant to be more for logic and design reasons. Overall I don't know if there is anything better about either way.

26 Comments

"2. Private methods simply don't add value to the way I program. "

No offense but this has a smell to it.

That aside, thank you so much for this one. It reminds me of awhile ago looking for a good code beautifier? Does anyone know of one out there? A couple years ago I didn't find anything that struck me as easy to use. I really think it's needed for modern programming though.

Just as we allow each developer to set their preferences in their IDE for how big a tab is or what color to show javascript functions, we should allow developers to use their own syntax for their code. Let them double space functions but don't indent them in a component if they want and yet when some other developer pops that cfc open, they'll see it the way they want, functions are indented and, even better, all in cfscript.

Seems like pie in the sky but it's nearly 2010, surely someone's done something to address this issue by now.

4 Comments

@Allen,

I know this isn't a Microsoft blog, sorry Ben, but they have done some really slick things in Visual Studio as far as formatting goes. They offer a lot of options for your particular style of programming and the neat thing is all you have to do is go to the end of the code page, remove the closing } and re-enter it,(C# anyway), and the whole page gets formatted the way you have it configured. The beauty of it is, each programmer can follow those steps and have the code formatted to their liking in seconds. Opening { on the same like or separate, space between arguments, tab spacing, etc.... Being CFBuilder is in the hands of Adobe, a company with $$$ to spend on developing a slick IDE, it would be nice if they could build something like that into the product.

15,688 Comments

@Daniel,

If ColdFusion was updated to allow both public and private methods to be accessed internally via the "THIS" scope, I would probably start using more intentful access settings.

@Allen,

It may have a smell, but to me, it honestly doesn't add value. What's the worst case scenario:

Someone blindly grabs a CFC and starts using methods without any understanding of what they do... they set something that they shouldn't... something blows up (either immediately, or eventually).

... OK. I guess I should have read up on how this CFC should be used ;)

... and that's the "worst" case scenario. The best case scenario is that I can now easily refer to all of my methods via the THIS scope and use named-parameters without ColdFusion throwing an error.

1 Comments

@Ben: The THIS scope by definition shows only public variables and functions (and in this is different from Java and other languages!) - so if you really want to prefix all your function calls with THIS then indeed you have to declare them as public. But why do you want to do that?

15,688 Comments

@Wouter,

With CF9 fixing the named-arguments-scope-invocation issue, it might not matter any more. Going forward, I should be able to use named args on either public or private scopes:

this.do( action = "something" );

.... and:

variables.do( action = "something" );

As such, I can now use named-args on private methods.

The question then becomes, do I *want* to type out "varaibles"? Personally, I just like the way "this" looks and feels (and is much short to type).

4 Comments

@Ben,

You should know I'm still working in MX 6.1 (and trying to get a two year-old licence for CF8 activated!) ;-) If I find a bit of time, I'll try some CF9 programming at home.

But I'm not concerned about what I have to type, I'm probably thinking too much like a Java-developer: I care about what parts (methods, variables) I make public when I build components. A variable that is internal to the component shouldn't be visible when you do a CFDUMP. I don't want to give other code (possibly not my own, since I work in a large company) acces to parts of my components that must remain internal.

And since discovering that even CF components can actually implement inheritance I've stopped putting THIS in front of methods - you can get some weird results if you don't pay attention (like declaring a method "private" and having a THIS.methodcall() fail...

15,688 Comments

@Wouter,

"you can get some weird results if you don't pay attention"

... I suppose this would be true in all aspects of programming :)

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel