Note: this post remains for posterity but rather than using the template tags described here I'd suggest taking a look at common-tags.
In some recent code at work we're using fairly large strings for error descriptions.
JavaScript has always made it hard to deal with long strings, especially when you need to format them across multiple lines. The solutions have always been that you need to concatenate multiple strings like so:
var foo = "So here is us, on the " +
"raggedy edge. Don't push me, " +
"and I won't push you.";
Or use line-continuations:
var foo = "So here is us, on the \
raggedy edge. Don't push me, \
and I won't push you.";
Neither of these are pretty. The latter was originally strictly illegal in ECMAScript (hattip Joseanpg):
A 'LineTerminator' character cannot appear in a string literal, even if preceded by a backslash \. The correct way to cause a line terminator character to be part of the string value of a string literal is to use an escape sequence such as \n or \u000A.
There's a rather wonderful bug on bugzilla discussing the ramifications of changing behaviour to strictly enforce the spec back in 2001. In case you're wondering it was WONTFIXED.
In ECMAScript 5th edition this part was revised to the following:
A line terminator character cannot appear in a string literal, except as part of a LineContinuation to produce the empty character sequence. The correct way to cause a line terminator character to be part of the String value of a string literal is to use an escape sequence such as \n or \u000A.
Anyway, enough with the history lesson, we now have features in ES6 on the horizon and if you're already starting to use ES6 via the likes of babel or as features become available then it provides some interesting possibilities.
Diving in with ES6 template strings
Having seen template strings I jumped right in only to find this happening.
var raggedy = 'raggedy';
console.log(`So here is us, on the
${raggedy} edge. Don't push
me, and I won't push you.`);
So that looks neat right? But hang-on what does it output?
So here is us, on the raggedy edge. Don't push me, and I won't push you.
Dammit - so by default template strings preserve leading whitespace like heredoc in PHP (which is kind of a memory I'd buried).
Ok so reading more on the subject of es6 template strings, processing template strings differently is possible with tag functions. Tags are functions that allow you to process the template string how you want.
Making a template tag
With that in mind I thought OK, I'm going to make a tag to make template strings have no leading whitespace on multiple lines but still handle var interpolation as by default.
Here's the code (es6) (note a simpler version is linked to at the end of this post if you're not using full-fat es6 via babel):
export function singleLineString(strings, ...values) {
// Interweave the strings with the
// substitution vars first.
let output = '';
for (let i = 0; i < values.length; i++) {
output += strings[i] + values[i];
}
output += strings[values.length];
// Split on newlines.
let lines = output.split(/(?:\r\n|\n|\r)/);
// Rip out the leading whitespace.
return lines.map((line) => {
return line.replace(/^\s+/gm, '');
}).join(' ').trim();
}
The way tags work is they're passed an array of strings and the rest of the args are the vars to be substituted. Note: as we're using ES6 in this example we've made values into an array by using the spread operator. If the original source string was setup as follows:
var two = 'two';
var four = 'four';
var templateString = `one ${two} three ${four}`;
Then strings is: ["one ", " three ", ""]
and values is ["two", "four"]
The loop up front is just dealing with interleaving the two lists to do the var substitution in the correct position.
The next step is to split on newlines to get a list of lines. Then the leading whitespace is removed from each line. Lastly the list is joined with a space between each line and the result is trimmed to remove any trailing space.
The net effect is that you get an easy way to have a long multiline string formatted nicely with no unexpected whitespace.
var raggedy = 'raggedy';
console.log(singleLineString`So here is us, on the
${raggedy} edge. Don't push
me, and I won't push you.`);
Outputs:
So here is us, on the raggedy edge. Don't push me, and I won't push you.
Trying this out in the browser
As template strings are becoming more and more available - here's a version without the fancy ES6 syntax which should work in anything that supports template strings:
See http://jsbin.com/yiliwu/4/edit?js,console
All in all it's not too bad. Hopefully someone else will find this useful. Let us know if you have suggestions or alternative solutions.
Libraries
🔥 common-tags has been my team's goto solution for solving these kind of problems for a while now. It's in use in a lot of projects and therefore provides a good set of battle-tested solutions to common problems with template strings. I'd definitely recommend checking it out.