RELoop ColdFusion Custom Tag To Iterate Over Regular Expression Patterns

<!--- Kill extra output. --->
<cfsilent>
 
	<!---
		Check to see which execution mode the tag is running
		in. In the start mode, we will param the tag attributes
		and start the loop. In the end mode, we will finish
		executing the loop.
	--->
	<cfswitch expression="#THISTAG.ExecutionMode#">
 
		<cfcase value="Start">
 
			<!--- Param the tag attributes. --->
 
			<!---
				This is the CALLER-scoped variable into which we
				will store the contextual match. This may be a
				string or a struct depending on whether the user
				wants the Groups returned.
			--->
			<cfparam
				name="ATTRIBUTES.Index"
				type="variablename"
				/>
 
			<!---
				This is the text in which we will be searching
				for and iterating over the pattern matches.
			--->
			<cfparam
				name="ATTRIBUTES.Text"
				type="string"
				/>
 
			<!---
				This is the regular expression pattern that
				we will be matching. Since we using the Java
				regular expression engine, this goes by the
				java.util.regex.Pattern syntax, not
				necessarily by the ColdFusion regex syntax.
			--->
			<cfparam
				name="ATTRIBUTES.Pattern"
				type="string"
				/>
 
			<!---
				This is the CALLER-scoped variable into which
				the resulting string (after the patterns have
				been replaced) will be placed. If this value
				is not renamed, then no value will be passed
				back to the caller.
			--->
			<cfparam
				name="ATTRIBUTES.Variable"
				type="variablename"
				default="undefined"
				/>
 
			<!---
				This flags whether to return the single matched
				pattern or to return a structure with the set of
				groups returned.
			--->
			<cfparam
				name="ATTRIBUTES.ReturnGroups"
				type="boolean"
				default="false"
				/>
 
 
			<!---
				ASSERT: At this point, all of the tag attributes
				have been properly validated.
			--->
 
 
			<!---
				Set a short-hand flag to test for variable
				return (based on the current name).
			--->
			<cfset THISTAG.HasReturn = (ATTRIBUTES.Variable NEQ "undefined") />
 
 
			<!--- Create the compiled pattern object. --->
			<cfset THISTAG.Pattern = CreateObject(
				"java",
				"java.util.regex.Pattern"
				).Compile(
					JavaCast(
						"string",
						ATTRIBUTES.Pattern
						)
					)
				/>
 
 
			<!---
				Get the pattern matcher for our target text from
				the compiled pattern.
			--->
			<cfset THISTAG.Matcher = THISTAG.Pattern.Matcher(
				JavaCast(
					"string",
					ATTRIBUTES.Text
					)
				) />
 
 
			<!---
				Get the string buffer into which we will store
				the replaced text. However, we only care about
				this if the user wants the value back.
			--->
			<cfif THISTAG.HasReturn>
 
				<!--- Create string buffer. --->
				<cfset THISTAG.Buffer = CreateObject(
					"java",
					"java.lang.StringBuffer"
					).Init()
					/>
 
			</cfif>
 
 
			<!---
				Find the first pattern match. Since the
				Find() function returns a boolean flagging
				that a match was found, check to see if it
				returns true.
			--->
			<cfif THISTAG.Matcher.Find()>
 
				<!---
					Now that we found the first match, we need
					to check to see how the user wants the
					returned match - as a single string or as a
					group structure.
				--->
				<cfif ATTRIBUTES.ReturnGroups>
 
					<!---
						The user wants the group structure to be
						returned so create a structure to hold
						this data.
					--->
					<cfset THISTAG.Index = StructNew() />
 
					<!---
						Store the number of matching groups in
						the regular expression.
					--->
					<cfset THISTAG.Index.GroupCount = THISTAG.Matcher.GroupCount() />
 
					<!---
						Store the index of the match. Add one
						to get it to be a one-based index like
						ColdFusion.
					--->
					<cfset THISTAG.Index.Start = (THISTAG.Matcher.Start() + 1) />
 
					<!--- Store the entire match. --->
					<cfset THISTAG.Index[ "0" ] = THISTAG.Matcher.Group() />
 
					<!---
						Loop over the matching groups and store
						them via indexes into the Index object.
					--->
					<cfloop
						index="THISTAG.GroupIndex"
						from="1"
						to="#THISTAG.Index.GroupCount#"
						step="1">
 
						<cfset THISTAG.Index[ "#THISTAG.GroupIndex#" ] = THISTAG.Matcher.Group(
							JavaCast(
								"int",
								THISTAG.GroupIndex
								)
							) />
 
					</cfloop>
 
					<!---
						Store new index object into the CALLER-
						scoped index variable.
					--->
					<cfset "CALLER.#ATTRIBUTES.Index#" = THISTAG.Index />
 
				<cfelse>
 
					<!---
						The user just wants a string. Store
						the entire matching substring into the
						CALLER-scoped index variable.
					--->
					<cfset "CALLER.#ATTRIBUTES.Index#" = THISTAG.Matcher.Group() />
 
				</cfif>
 
			<cfelse>
 
				<!---
					No matching pattern was found. We need to
					full exit the tag without doing any looping.
					However, we may need to store the value back
					into the caller. Check to see if we have a
					return value.
				--->
				<cfif THISTAG.HasReturn>
 
					<!---
						Just store the submitted text back into
						the CALLER scope.
					--->
					<cfset "CALLER.#ATTRIBUTES.Variable#" = ATTRIBUTES.Text />
 
				</cfif>
 
				<!--- Exit out of tag execution. --->
				<cfexit method="exittag" />
 
			</cfif>
 
		</cfcase>
 
 
		<cfcase value="End">
 
			<!---
				Now that the user has had a chance to update
				the matching group values, let's see if they
				are looking for a return. If they are, then
				we need to replace the groups back into the
				string buffer.
			--->
			<cfif THISTAG.HasReturn>
 
				<!---
					Now, we have to see how the user was
					viewing the groups (as a single string
					or a group structure).
				--->
				<cfif ATTRIBUTES.ReturnGroups>
 
					<!---
						When we are dealing with the group
						structure, we only care about the entire
						matched string (the zero group). Store
						that back into the Index so that we can
						deal with the replacement uniformly.
					--->
					<cfset THISTAG.Index = CALLER[ ATTRIBUTES.Index ][ "0" ] />
 
				<cfelse>
 
					<!---
						Store the replacement string into the
						local Index so that we can deal with
						the replacement uniformly.
					--->
					<cfset THISTAG.Index = CALLER[ ATTRIBUTES.Index ] />
 
				</cfif>
 
 
				<!---
					Replace the string into the buffer. When
					appending the replacement, be sure to escape
					any special characters.
				--->
				<cfset THISTAG.Matcher.AppendReplacement(
					THISTAG.Buffer,
					JavaCast(
						"string",
						THISTAG.Index.ReplaceAll(
							JavaCast( "string", "([\\\$])" ),
							JavaCast( "string", "\$1" )
							)
						)
					) />
 
			</cfif>
 
 
			<!---
				ASSERT: At this point, we have dealt with all
				the changes that the user made to the matched
				patterns. Now, we can deal with the next
				match iteration.
			--->
 
 
			<!---
				Find the next pattern match. Since the
				Find() function returns a boolean flagging
				that a match was found, check to see if it
				returns true.
			--->
			<cfif THISTAG.Matcher.Find()>
 
				<!---
					Now that we found the next match, we need
					to check to see how the user wants the
					returned match - as a single string or as a
					group structure.
				--->
				<cfif ATTRIBUTES.ReturnGroups>
 
					<!---
						The user wants the group structure to be
						returned so create a structure to hold
						this data.
					--->
					<cfset THISTAG.Index = StructNew() />
 
					<!---
						Store the number of matching groups in
						the regular expression.
					--->
					<cfset THISTAG.Index.GroupCount = THISTAG.Matcher.GroupCount() />
 
					<!---
						Store the index of the match. Add one
						to get it to be a one-based index like
						ColdFusion.
					--->
					<cfset THISTAG.Index.Start = (THISTAG.Matcher.Start() + 1) />
 
					<!--- Store the entire match. --->
					<cfset THISTAG.Index[ "0" ] = THISTAG.Matcher.Group() />
 
					<!---
						Loop over the matching groups and store
						them via indexes into the Index object.
					--->
					<cfloop
						index="THISTAG.GroupIndex"
						from="1"
						to="#THISTAG.Index.GroupCount#"
						step="1">
 
						<cfset THISTAG.Index[ "#THISTAG.GroupIndex#" ] = THISTAG.Matcher.Group(
							JavaCast(
								"int",
								THISTAG.GroupIndex
								)
							) />
 
					</cfloop>
 
					<!---
						Store new index object into the CALLER-
						scoped index variable.
					--->
					<cfset "CALLER.#ATTRIBUTES.Index#" = THISTAG.Index />
 
				<cfelse>
 
					<!---
						The user just wants a string. Store
						the entire matching substring into the
						CALLER-scoped index variable.
					--->
					<cfset "CALLER.#ATTRIBUTES.Index#" = THISTAG.Matcher.Group() />
 
				</cfif>
 
 
				<!--- Exit the tag as a loop. --->
				<cfexit method="loop" />
 
			<cfelse>
 
				<!---
					No matching pattern was found. Therefore,
					we have exhausted all of the matches in
					this string. We need to full exit the tag;
					however, we may need to store the value back
					into the caller. Check to see if we have a
					return value.
				--->
				<cfif THISTAG.HasReturn>
 
					<!---
						Append the rest of the target text to
						the string buffer.
					--->
					<cfset THISTAG.Matcher.AppendTail(
						THISTAG.Buffer
						) />
 
					<!---
						Convert the string buffer into a
						single string and store it back into
						the CALLER-scoped variable.
					--->
					<cfset "CALLER.#ATTRIBUTES.Variable#" = THISTAG.Buffer.ToString() />
 
				</cfif>
 
				<!--- Exit out of tag execution. --->
				<cfexit method="exittag" />
 
			</cfif>
 
		</cfcase>
 
	</cfswitch>
 
</cfsilent>

For Cut-and-Paste