HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Text substitution templating function

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
textfunctionsubstitutiontemplating

Problem

In a recent interview I was asked to solve the below problem.

Problem:


Given a string with variables in them, e.g.

"I am a string with {{ variable }} in them"


Create an output where the {{ variable}} is replaced with their
actual values.


So if given the input:

var template = {
  "sentence": "The {{ object }} {{ verb }} ",
  "object": "{{ adjective }} {{ noun }}",
  "verb": "flew",
  "adjective": "colorful",
  "noun": "bird"
};




the function call

fillInTheBlanks(template, 'sentence')




must produce the output:

The colorful bird flew


I came up with the following solution (which works):

var sentence = fillInTheBlanks(template, 'sentence');

//Accomplish however you'd like, additional utility functions are fine
function fillInTheBlanks(template, outputKey) {

  var val = template[outputKey];

  while (val.search(/{{/i) >=0) {
      val = splitAndMerge(val);
      val = val.replace(/,/g, " ");
  }

 return val;
}

function splitAndMerge(val) {
   var splittedSentence = val.split(' ');
    _.forEach(splittedSentence, function(item, idx) {
    if (item === '{{') {
      splittedSentence[idx+1] = resolveString(splittedSentence[idx+1]);
      splittedSentence.splice(idx+2, 1);
      splittedSentence.splice(idx, 1);
    }
  });

  return  splittedSentence.toString();

}

function resolveString(item) {
  return template[item];
}


PLNKR: http://plnkr.co/edit/4ciGzVewZRDi4RcSDsfI?p=preview

However, this solution was not considered elegant. what would have been a better way to solve this ?

Solution

Generally, using split and treating a string as an array - only to turn it back into a string - isn't very elegant for find-and-replace task like this. If the input is a string, and the output is a string, making the intermediate step an array seems odd given that JS has some OK string manipulation features.

Your code is also buggy. Since you use toString instead of a plain join, you introduce commas in the string. And you then have to remove those again, but you do that with a global replace. Meaning the string can't have commas of its own, or they'll get eaten too.

Instead of all that, you could simply do join(" "), and get the same result as you would with toString() & replace(/,/g, " ") but without stomping on existing commas.

You're also replying on spaces in the string. But really, the only delimiters you should care about are {{ and }}. I should be able to write a template string like "{{ foo }}{{ bar }}" (no space in between) to get a string like StackOverflow. Or I might use hyphens or something else (e.g. the aforementioned commas) and I wouldn't expect the code to eat them. I would also expect to be able to use two spaces inside the braces - or no spaces at all. Neither thing is handled properly by your code, as it's totally dependent on the whitespace being just so.

Lastly, your code would fail on malformed template strings like "here are two braces {{ and here two more {{". That's not a proper template, and should just be returned unaltered. But your code only looks for the opening braces and assumes there to be two close braces later on. So given the input above, I get "here are two braces two more " as output, because it aggressively splices away some words.

A couple of minor notes:

-
/{{/i - the i flag (case-insensitive matching) doesn't matter here. There is no upper- or lowercase for curly braces.

-
"Split" as an adjective is also "split". So "splitted sentence" isn't quite grammatically kosher.

-
You could combine the splice calls, since splice both removes and inserts elements in an array. I.e.

splittedSentence.splice(idx, 3, resolveString(splittedSentence[idx+1]));


-
The resolveString() function is problematic. For one, it's a bit much to wrap property access in a function. In fact, calling the function is longer than just writing template[item]. But more importantly the function relies on the template object being accessible in its scope. Note that fillInTheBlanks, the point of the task, does not make such an assumption; there you must pass in the object. Which means you could call fillInTheBlanks("x", { x: "hi!" }) and it should work. But in your case it would fail, because the template object won't be in resolveString()'s scope.

-
And as @Jonah pointed out, and which I'd missed, you're using underscore (or lodash) just to do a forEach, when forEach is built in. Sure, some really old browsers lack it, but if that's a concern there's always the plain old for-loop.

Anyway, I said you don't need to split the string as there are string manipulation functions you can use instead. Specifically, the String.replace method accepts a function as its 2nd argument. That function will be passed the matched part(s) of the string, and its return value will be used as the replacement. Add in a regular expression, and you've got a pretty neat solution.

So I imagine that your interviewers were looking for something like this perhaps:



var template = {
"sentence": "The {{ object }} {{ verb }} ",
"object": "{{ adjective }} {{ noun }}",
"verb": "flew",
"adjective": "colorful",
"noun": "bird"
};

function interpolate(string, values) {
return string.replace(/\{\{\s(.?)\s*\}\}/g, function (_, key) {
return interpolate(values[key] || "", values);
});
}

function fillInTheBlanks(template, key) {
return interpolate(template[key], template);
}

// output (just for the stack snippet)
document.write( fillInTheBlanks(template, 'sentence') );




In this case, fillInTheBlanks is just there to kick things off. The actual work is done in interpolate. That function looks for instances of {{...}} in its input string, and then replaces it with the value for whatever key is between in the braces (ignoring spaces and such). And it calls itself on the resulting string, in order to achieve the recursive replacement behavior.

The name interpolate comes from "string interpolation", the general name for "filling in the blanks" in a template string.

The regular expression can be explained like so:

\{\{          // look for two opening braces "{{"
\s*           // followed by zero or more whitespace characters
(             // start a capture group
  .*?         // match anything that's matched by the following (ungreedy matching)
)             // end capture group
\s*           // match zero or more whitespace characters
\}\}          // match the close braces "}}"


You could skip the \s* parts, and ins

Code Snippets

splittedSentence.splice(idx, 3, resolveString(splittedSentence[idx+1]));
\{\{          // look for two opening braces "{{"
\s*           // followed by zero or more whitespace characters
(             // start a capture group
  .*?         // match anything that's matched by the following (ungreedy matching)
)             // end capture group
\s*           // match zero or more whitespace characters
\}\}          // match the close braces "}}"
var values = {
  foo: "{{ bar }}",
  bar: "{{ foo }}"
};

interpolate("{{ foo }}", values);

Context

StackExchange Code Review Q#110460, answer score: 4

Revisions (0)

No revisions yet.