Elegant way to generate string from tagged template?

# Glen Huang (10 years ago)

When a template is passed to a function, what’s the quickest way to return a string with additional arguments being interpolated into the template?

For example,

minify`
	<ul>
		<li>${content}</li>
	</ul>
`

In this caseminify will return the minified html, but what’s the quickest way to get the unminified string interpolated from the template? What I can think of is to loop over the additional arguments, and interspersedly join the elements in template object with the arguments, and then append the dangling element in template object. Sounds very tedious. Does es6 have an array method to ease this transformation?

What such method does basically, should be the following:

Given an array of [1,2,3] and other array of [“a”,”b”], it should return [1,”a”,2,“b”,3]. (probably a lot of edge cases to cover, but just ignore them for now).

But maybe I overthink this? Is there already an easy way to do this?

# Gary Guo (10 years ago)

From: curvedmark at gmail.com Date: Tue, 23 Dec 2014 14:55:57 +0800

Given an array of [1,2,3] and other array of [“a”,”b”], it should return [1,”a”,2,“b”,3]. (probably a lot of edge cases to cover, but just ignore them for now).

It is not a hard task. I don't think we need to add it into the spec. If you really want, use this:

Array.blend=(a,b)=>{
    let ret="";
    for(let i=0;ret+=a[i],i<b.length;ret+=b[i],i++);
    return ret;
};
# Gary Guo (10 years ago)

From: curvedmark at gmail.com Date: Tue, 23 Dec 2014 14:55:57 +0800

Given an array of [1,2,3] and other array of [“a”,”b”], it should return [1,”a”,2,“b”,3]. (probably a lot of edge cases to cover, but just ignore them for now).

I came up with an idea. Symmetric to String.raw, we can have a function that represents the default procedure.

String.default=(a,...b)=>{
    let ret="";
    for(let i=0;ret+=a[i],i<b.length;ret+=b[i],i++);
    return ret;
};

So in semantics, String.default `Template` === `Template`. By introducing this, we can define a common procedure between tagged and untagged templates: untagged templates are equivalent to the tagged ones with tag String.default.

When implementing our own template functions, we can use:

function custom(template, ...substitutions){
    // Perform actions on substitutions or template
    return String.default(template, ...substitutions);
}
# Glen Huang (10 years ago)

Thanks for the suggestion.

You inspired me to realize that the basic idea shouldn’t be to find a way to intersperse two arrays, but to find a way to undo the tagging of template.

so `template` === tag`template`, where tag = (templateObj, ...args) => untag(templateObj, ...args)

If you intend to put the templateObj back together into a string eventually (which i guess most tagging functions do), this kind of action should be very common.

Since the target (templateObj) is an array, I think the method should belong to Array, and it should behave like a join, but instead of joining array items with a string, it joins with many strings. Maybe something like Array.prototype.interspersedJoin (much like your String.default):

[1,2,3].interspersedJoin(“a”, “b”) = “1a2b3”
[1,2,3].interspersedJoin(“a”) = “1a23”
[1,2,3].interspersedJoin(“a”,”b”,”c”) = “1a2b3c”

And for the tag function:

function tag(templateObj, …args) {
	return templateObj.interspersedJoin(...args);
}
# Peter Seliger (10 years ago)

Since the target (templateObj) is an array, I think the method should belong to Array, and it should behave like a join, but instead of joining array items with a string, it joins with many strings. Maybe something like Array.prototype.interspersedJoin (much like your String.default):

As for prototypal array approaches, one might rather think of [interlock] for it more precisely names, what you are going to achieve, and it also is not as specialized as your suggestion for an [interspersedJoin] already needs to be - how about this approach?

(function (Array) {


  var
    array_prototype = Array.prototype,


    isFunction = (function (TYPEOF_FUNCTION_TYPE) {
      return function (type) {

        return (
             (typeof type == TYPEOF_FUNCTION_TYPE)
          && (typeof type.call == TYPEOF_FUNCTION_TYPE)
          && (typeof type.apply == TYPEOF_FUNCTION_TYPE)
        );
      };
    }("function")),

    array_from = (isFunction(Array.from) && Array.from) || (function
(array_prototype_slice) {
      return function (listType) {

        return array_prototype_slice.call(listType);
      };
    }(array_prototype.slice))
  ;


  array_prototype.interlock = function (listType) {
    var
      arr_A = array_from(this),
      arr_B = array_from(listType),

      len_A = arr_A.length,
      len_B = arr_B.length,
      len = (len_A + len_B),

      idx_A,
      idx_B,
      idx = idx_A = idx_B = -1,

      arr = []
    ;
    arr.length = len;

    while ((idx + 1) < len) {
//console.log("within while :: 1st part :: idx, len : ", idx, len);

      if (++idx_A < len_A) {
        ++idx;
        (idx_A in arr_A) && (arr[idx] = arr_A[idx_A]);
//console.log("within A condition :: idx_A, len_A, arr : ", idx_A, len_A, arr);
      }

//console.log("within while :: 2nd part :: idx, len : ", idx, len);

      if (++idx_B < len_B) {
        ++idx;
        (idx_B in arr_B) && (arr[idx] = arr_B[idx_B]);
//console.log("within B condition :: idx_B, len_B, arr : ", idx_B, len_B, arr);
      }
    }
    return arr;
  };


}(Array));


console.log('[1,2,3].interlock("a");', [1,2,3].interlock("a"));
console.log('[1,2,3].interlock(["a"]);', [1,2,3].interlock(["a"]));
console.log('[1,2,3].interlock("a").join("");',
[1,2,3].interlock("a").join(""));
console.log('[1,2,3].interlock(["a"]).join("");',
[1,2,3].interlock(["a"]).join(""));

console.log('[1,2,3].interlock("ab");', [1,2,3].interlock("ab"));
console.log('[1,2,3].interlock(["a", "b"]);', [1,2,3].interlock(["a", "b"]));
console.log('[1,2,3].interlock("ab").join("");',
[1,2,3].interlock("ab").join(""));
console.log('[1,2,3].interlock(["a", "b"]).join("");',
[1,2,3].interlock(["a", "b"]).join(""));

console.log('[1,2,3].interlock("abc");', [1,2,3].interlock("abc"));
console.log('[1,2,3].interlock(["a", "b", "c"]);',
[1,2,3].interlock(["a", "b", "c"]));
console.log('[1,2,3].interlock("abc").join("");',
[1,2,3].interlock("abc").join(""));
console.log('[1,2,3].interlock(["a", "b", "c"]).join("");',
[1,2,3].interlock(["a", "b", "c"]).join(""));


/*

console.log("[1,3,5,7,9,11,13].interlock([2,4,6]);",
""+[1,3,5,7,9,11,13].interlock([2,4,6]));
console.log("[2,4,6].interlock([1,3,5,7,9,11,13]);",
""+[2,4,6].interlock([1,3,5,7,9,11,13]));

console.log("[2,,,4,,,6].interlock([1,3,5,7,9,11,13,]);",
""+[2,,,4,,,6].interlock([1,3,5,7,9,11,13,]));
console.log("[2,,,4,,,6].interlock([1,3,5,7,9,11,13,,]);",
""+[2,,,4,,,6].interlock([1,3,5,7,9,11,13,,]));
console.log("[2,,,4,,,6].interlock([1,3,5,7,9,11,13,,,]);",
""+[2,,,4,,,6].interlock([1,3,5,7,9,11,13,,,]));
console.log("[2,,,4,,,6].interlock([1,3,,5,7,,9,,11,13,,,]);",
""+[2,,,4,,,6].interlock([1,3,,5,7,,9,,11,13,,,]));
console.log("[2,,,4,,,6].interlock([1,,3,,5,,7,,,9,,,11,13,,,]);",
""+[2,,,4,,,6].interlock([1,,3,,5,,7,,,9,,,11,13,,,]));

*/
# Brendan Eich (10 years ago)

Perhaps the most elegant way is functional composition on arrays, not string hacking just-so imperative coding: zip composed with flatten and join. You could even use underscore.js:

but JS's OO standard library style prefers method chaining. Array lacks standard zip and flatten, although concat does one level of flattening:

js> Array.prototype.flatten1 = function () { return [].concat.apply([], this); };
(function () { return [].concat.apply([], this); })
js> Array.prototype.zip = function (b) { var r = []; var n = Math.min(this.length, b.length) ; for (var i = 0; i < n; i++) r.push([this[i], b[i]]); return r; }
(function (b) { var r = []; var n = Math.min(this.length, b.length) ; 
for (var i = 0; i < n; i++) r.push([this[i], b[i]]); return r; })
js> ['a', 'b', 'c'].zip([1, 2, 3]).flatten1().join('');
"a1b2c3"

This is just a quick sketch. A real standard library zip would be a bit more involved, ditto any flatten or flatten1 ;-).

# Ron Buckton (10 years ago)

For ES6 you could use rbuckton/QueryJS like this:

var a = [1,2,3];
var b = ["a", "b", "c"]
var c = Query
  .from(a)
  .zip(b, (a, b) => [a, b])
  .flatMap(a => a)
  .toArray()
  .join("");
# Glen Huang (10 years ago)

On second thought, make it could be just as simple as this?

function tag(templateObj, ..args) {
	return String.raw({raw: templateObj}, …args);
}
# Brendan Eich (10 years ago)

Clever -- treat the cooked values as raw. Should work -- test in Traceur?

# Glen Huang (10 years ago)

This returns true in traceur:

function tag(templateObj, ...args) {
	return String.raw({raw: templateObj}, ...args);
}

let content = "world";
console.log(tag`hello\n${content}` === `hello\n${content}`);

Looks like problem solved. :)

# Glen Huang (10 years ago)

I think I had been doing the whole thing wrong.

Instead of doing:

minify`
	<ul>
		<li>${content}</li>
	</ul>
`

I should just do:

minify(`
	<ul>
		<li>${content}</li>
	</ul>
`)

And forget about putting the template object back into a string altogether.

# Brendan Eich (10 years ago)

Glen Huang wrote:

I think I had been doing the whole thing wrong.

That is unclear, but it's clear to me that template strings in ES6 (including String.raw) are expressive enough to do what you need. Confirm?

# Glen Huang (10 years ago)

That is unclear

I just want to minify the passed html string, but I got this incorrect mental model that tagged templates are just passing a string to a function. (And my incorrect mental model somehow gets corrected when considering params of that function. Yep, i’m inconsistent by nature :). I guess this comes from my write-as-little-as-possible OCD. :)

but it's clear to me that template strings in ES6 (including String.raw) are expressive enough to do what you need. Confirm?

You question got me thinking: String.raw({raw: templateObj}, …args) looks like a hack. if this is the solution es6 is offering. I’m a bit worried: the expression itself doesn’t convey developer’s intent very well.

I also start to think about use cases for tagged templates and whether developers are really likely to generate final strings directly from template objects (and use this hack a lot).

From what I read:

  1. Get unescaped strings

This what String.raw is for. "new RegExp(String.raw\s+${dynamic})” looks very appealing. So manual generation not needed here.

  1. I18n

People probably need to generate a sprintf-like format string first: “Hello, %s” from the template object, and then get “Bonjour, %s” from a translation table, then replace the placeholders with args.

In this case, the final string is not generated directly from the template object. So hacking String.raw might not be needed very often, probably not a terrible idea.

However, this is does raise the question that is it a good idea to add a sprintf-like function to es6?