Select Page

CF_SaveFile Custom Tag In ColdFusion

Ben Nadel
Published: April 20, 2024

A couple of years ago, on the Lucee Dev forum, I proposed a file attribute for the CFSaveContent tag in ColdFusion. This comes out of a pattern that I employ quite often in which I use the CFSaveContent tag to generate the contents of a static file; and then, use the fileWrite() function to save said content disk. So, why not combine these two steps into a single step using a ColdFusion custom tag.

The CFSaveContent tag is little more than a custom tag that the ColdFusion server has made globally available. There’s no magic to it—it simply grabs the generatedContent value, stores it into the provided variable name, and then resets the output. We can do this ourselves in our own custom tag. Only, instead of assigning the content to a variable, we can write the content directly to a file.

Yesterday, I wrote about dedenting text in ColdFusion. That post was a precursor to this post. When you consider the aesthetics of using the CFSaveContent tag, especially when used in conjunction with the CFOutput tag, there is always some unnecessary whitespace on the left side of the content:

<cfsavecontent variable="data">
	<cfoutput>
		<!--- ... CONTENT ... --->
	</cfoutput>>
</cfsavecontent>

In order to keep the content properly nested, it requires at least one level of indentation below the CFSaveContent tag; and, possibly two levels if there’s an intermediary CFOutput tag. But, we don’t need—or want—this indentation in the resultant file. As such, before we save the content, we need to dedent it by one or two tabs.

In Lucee CFML’s version of the CFSaveContent tag, they introduced the trim and append attributes. We can include those attributes as well in our custom tag; and, provide new dedent (Boolean) and indentation ("tab" or "space") attributes to control the dedenting behavior.

First, let’s see how this might be used (with sane defaults). In the following ColdFusion code, I’m generating a static .html file.

<cfscript>
	author = "Ben Nadel";
	createdAt = now();
</cfscript>
<cf_SaveFile filepath="#expandPath( './static.html' )#">
	<cfoutput>
		<!doctype html>
		<html lang="en">
		<head>
			<meta charset="utf-8" />
			<meta nane="author" content="#encodeForHtmlAttribute( author )#" />
			<meta nane="generated" content="#createdAt.dateFormat( "yyyy-mm-dd" )#" />
		</head>
		<body>
			<h1>
				SaveFile ColdFusion Custom Tag Demo
			</h1>
			<figure>
				<blockquote>
					Be excellent to each other!
				</blockquote>
				<figcaption>
					&mdash; Ted "Theodore" Logan
				</figcaption>
			</figure>
		</body>
		</html>
	</cfoutput>
</cf_SaveFile>

As you can see, we’re invoking our ColdFusion custom tag using the CF_SaveFile syntax. The contents of the HTML are indented using a base indentation of 2-tabs (as discussed above). However, when we open the resultant static.html file, we can see that all of the unnecessary tabs have been stripped-out:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta nane="author" content="Ben&#x20;Nadel" />
	<meta nane="generated" content="2024-04-20" />
</head>
<body>
	<h1>
		SaveFile ColdFusion Custom Tag Demo
	</h1>
	<figure>
		<blockquote>
			Be excellent to each other!
		</blockquote>
		<figcaption>
			&mdash; Ted "Theodore" Logan
		</figcaption>
	</figure>
</body>
</html>

Note that the left-most parts of the content are all now flush with the left-edge of the HTML file. That’s the magic of dedenting.

Other than the dedenting functionality, the rest of the logic in our SaveFile.cfm custom tag is rather straightforward:

<cfscript>
	// Define tag attributes and defaults.
	param name="attributes.filePath" type="string";
	param name="attributes.dedent" type="string" default=true;
	param name="attributes.indentation" type="string" default="tab";
	param name="attributes.trim" type="boolean" default=true;
	param name="attributes.append" type="boolean" default=false;
	param name="attributes.charset" type="string" default="utf-8";
	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //
	// Since this ColdFusion custom tag deals with generated output, we only care about
	// the tag in its "end" mode once we have generated content to consume.
	if ( thistag.executionMode == "start" ) {
		exit
			method = "exitTemplate"
		;
	}
	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //
	fileContent = thistag.generatedContent;
	if ( attributes.dedent ) {
		fileContent = dedentContent( fileContent, attributes.indentation );
	}
	if ( attributes.trim ) {
		fileContent = trim( fileContent );
	}
	targetFile = ( attributes.append )
		? fileOpen( attributes.filePath, "append", attributes.charset )
		: fileOpen( attributes.filePath, "write", attributes.charset )
	;
	try {
		fileWrite( targetFile, fileContent, attributes.charset )
	} finally {
		fileClose( targetFile );
	}
	// We don't want this tag to generate content - all content was written to the file.
	thistag.generatedContent = "";
	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //
	/**
	* I remove indentation from the given input but leave the input such that the relative
	* indentation across lines remains constant.
	*/
	private string function dedentContent(
		required string input,
		required string indentation
		) {
		var indentationCharacter = ( indentation == "tab" )
			? chr( 9 )  // Tab.
			: chr( 32 ) // Space.
		;
		// In order to figure out how much we can dedent the text, we must first locate
		// the smallest amount of indentation that currently exists across all lines of
		// text. However, we only care about lines of text that have non-indentation
		// characters on them (ie, we want to skip over empty lines). As such, we're going
		// to create a pattern that must end on non-indentation (or line-break) character.
		var minWidth = input
			.reMatch( "(?m)^#indentationCharacter#*[^#indentationCharacter#\r\n]" )
			.map(
				( linePrefix ) => {
					return ( linePrefix.len() - 1 );
				}
			)
			// NOTE: ArrayMin() returns zero if array is empty.
			.min()
		;
		if ( ! minWidth ) {
			return input;
		}
		// Now that we've found the smallest amount of indentation across all lines of
		// text, we can remove exactly that amount of indentation from each line of text.
		var result = input
			.reReplace( "(?m)^#indentationCharacter#{#minWidth#}", "", "all" )
		;
		return result;
	}
</cfscript>

Ultimately, the whole dedenting concept is a nice-to-have feature of the SaveContent.cfm custom tag. But, the general workflow would have worked just as well without it; and would have greatly reduced the logic within our implementation. Only, we would have had some superfluous tabs in our generated file—not the end of the world.

Which leads me back to my original proposition: that it would be nice for the native CFSaveContent tag to include a file attribute. Perhaps even one that could represent either a file path or an instantiated file object.

Man, I love ColdFusion! Having both a tag-based model and a script-based model makes the language so darned flexible!

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


https://bennadel.com/go/4638

Source: www.bennadel.com