4

My client has a database table of email bodies that get sent at certain times to customers. The text for the emails contains ColdFusion expressions like Dear #firstName# and so on. These emails are HTML - they also contain all sorts of HTML mark-up. What I'd like to do is read that text from the database into a string and then have ColdFusion Evaluate() that string to resolve the variables. When I do that, Evaluate() throws an exception because it doesn't like the HTML markup in there (I also tried filtering the string through HTMLEditFormat() as an intermediate step for grins but it didn't like the entities in there).

My predecessor solved this problem by writing the email text out to a file and then cfincluding that. It works. It's seems really hacky though. Is there a more elegant way to handle this using something like Evaluate that I'm not seeing?

DaveBurns
  • 2,036
  • 2
  • 27
  • 37
  • I would like to point out that writing this to the FS or using Evaluate() allows for ANY and ALL code to be executed. This could lead to security issues if not handled with the utmost care. I'd go with @kevink's per-variable replacement. – Ben Doom Dec 09 '09 at 20:48
  • Agreed with Ben. Evaluate works fine, until you are getting so many per second requests that CF gives errors. Replace is the best way to go so far that I've found. – Jas Panesar Dec 12 '09 at 21:45

4 Answers4

9

Not sure you need rereplace, you could brute force it with a simple replace if you don't have too many fields to merge

How about something like this (not tested)

<cfset var BaseTemplate = "... lots of html with embedded tokens">

<cfloop (on whatever)>

   <cfset LoopTemplate = replace(BaseTemplate, "#firstName#", myvarforFirstName, "All">
   <cfset LoopTemplate = replace(LoopTemplate, "#lastName#",  myvarforLastName, "All">
   <cfset LoopTemplate = replace(LoopTemplate, "#address#",   myvarforAddress, "All">

</cfloop>

Just treat the html block as a simple string.

kevink
  • 1,958
  • 3
  • 14
  • 14
  • 1
    Yup. I typically populate email templates with dynamic variables this way, just keywords enclosed in other %characters%. – Sergey Galashyn Dec 09 '09 at 08:27
  • I've done similar, but avoided hash marks to delineate variables specifically because it plays havoc when reading the file in. I generally like to enclose my variables with [[brackets]]. – ale Dec 09 '09 at 13:06
  • Sounded like he was processing legacy code with the #fieldname# syntax already embedded in his "templates". If you were designing it from scratch I agree the "#" character would be a terrible choice. – kevink Dec 09 '09 at 13:41
  • Yes, the previous programmer used #'s on purpose. He'd read in the customer's name from the db into a variable then cfinclude the email template and CF would resolve those variables for him. The person creating the email templates needed to know what the names of the CF variables were. I appreciate the simplicity and compactness of the code but it creates a system that is very tightly bound and inflexible. Searching and replacing variables/keywords myself is more code but I like the idea since it makes it a bit more self-documenting. – DaveBurns Dec 09 '09 at 14:07
  • BTW, while I agree doing the replacing myself is probably better design, the best quick-fix from my customer's limited budget PoV is still some way to trick Evaluate() into replacing the variables for me while smoothly ignoring the HTML. I guess from the responses that there's no way to do that? – DaveBurns Dec 09 '09 at 14:11
  • I like the solution. Using the code in this answer above, you'd probably run a query or lookup function above to get the mailing list right? ... – Dan Sorensen Dec 09 '09 at 17:56
  • Have you tried global replacing > with something like |gt| and < with something like |lt| then running evaluate? If it works then you just swap them back as the last pass – kevink Dec 09 '09 at 17:56
  • @kevink - Evaluate wants the entire thing I pass it to be an expression so it will choke. That's the difference with the cfinclude approach: it allows for CFML to be in there. Hmm, I wonder what would happen though if I put double-quotes at the beginning and end of the string before calling evaluate? I'd also have to escape any double-quotes inside it. Then after the eval, change the escaped quotes back. – DaveBurns Dec 09 '09 at 18:51
  • I use replace extensively to send out emails where the template is stored in the email with dynamic content in them. Works great. Use a different character than # and you're laughing. – Jas Panesar Dec 12 '09 at 21:44
9

What other languages often do that seems to work very well is just have some kind of token within your template that can be easily replaced by a regular expression. So you might have a template like:

Dear {{name}}, Thanks for trying {{product_name}}.  Etc...

And then you can simply:

<cfset str = ReplaceNoCase(str, "{{name}}", name, "ALL") />

And when you want to get fancier you could just write a method to wrap this:

<cffunction name="fillInTemplate" access="public" returntype="string" output="false">
    <cfargument name="map" type="struct" required="true" />
    <cfargument name="template" type="string" required="true" />

    <cfset var str = arguments.template />
    <cfset var k = "" />

    <cfloop list="#StructKeyList(arguments.map)#" index="k">
        <cfset str = ReplaceNoCase(str, "{{#k#}}", arguments.map[k], "ALL") />
    </cfloop>

    <cfreturn str />
</cffunction>

And use it like so:

<cfset map = { name : "John", product : "SpecialWidget" } />
<cfset filledInTemplate = fillInTemplate(map, someTemplate) />
Bialecki
  • 30,061
  • 36
  • 87
  • 109
  • 1
    This gets the nod for complete sample code although instead of Replace, I had to use REReplaceNoCase since Replace is case-sensitive and CF converted all the struct member keys to upper-case. – DaveBurns Dec 29 '09 at 19:25
  • 1
    Makes sense. I updated to use ReplaceNoCase instead of REReplaceNoCase because you shouldn't need regular expressions, but I suppose you could use them if you wanted. – Bialecki Dec 30 '09 at 16:20
  • Interesting. I'm using CF7 and I looked for ReplaceNoCase but it's not listed in my CF7 WACK book and it doesn't have an online help entry at Adobe unless you look at CF8 and above. I assumed it was added in CF8 but I do see it used in sample code for CF6. So, not sure what to make of that. – DaveBurns Jan 31 '10 at 05:24
  • 1
    See this help page for ColdFusion MX 7: http://www.adobe.com/livedocs/coldfusion/7/htmldocs/wwhelp/wwhimpl/common/html/wwhelp.htm?context=ColdFusion_Documentation&file=00000613.htm. It does exist and I have to say, documentation is not one of the CF's strong points. – Bialecki Feb 01 '10 at 16:12
2

CF 7+: You may use regular expression, REReplace()?

CF 9: use Virtual File System

Henry
  • 32,689
  • 19
  • 120
  • 221
  • Henry - Thanks, I forgot to mention that I'm on CF7. I don't see how I'd use REReplace to do variable substitution. Is there something I don't see in the doc? – DaveBurns Dec 09 '09 at 03:09
  • you can do almost anything with regular expression! :) Well, just fish out the variable name inside #varName#, and then call evalulate(varName), and replace the "#varName#" with evaluated result? – Henry Dec 09 '09 at 03:17
  • or... loop through every character in the string, like an input stream, when u encounter the # sign, stop outputting to the output stream, until u hit another # sign, evaluate the recorded string, and add that to your output stream. :) – Henry Dec 09 '09 at 03:20
  • I think ReReplace is a good solution. You could be more pragmatic if there are just three variables to replace and use ReplaceNoCase(). – SWD Dec 09 '09 at 13:49
  • @Henry: +1, that's also what I would do. Unwritten SO rule, proven again: Telling the concept is not very fruitful, telling the concept and adding a code sample = up-votes. ;) – Tomalak Dec 09 '09 at 19:22
-2

If the variable is in a structure from, something like a form post, then you can use "StructFind". It does exactly as you request. I ran into this issue when processing a form with dynamic inputs.

Ex.

StructFind(FORM, 'WhatYouNeed')
McGarnagle
  • 101,349
  • 31
  • 229
  • 260