Tagged Template Literals

Hi,
We’re considering Haxe as an alternative to Typescript on our project. We’re heavily relying on ES6 tagged template literals for things like html and sql templating, i.e.:

let Comp1 = ({url,title}) => html`
<div>
    <a href="${url}">${title}</a>
    ${Comp2({url})}
</div>
`

or:

sql`SELECT * FROM mytable WHERE col1=${val}`

They’re very fast (in a simplest form, it’s just a string concatenation + escaping for dynamic parts) and easily composable. They’ve become very popular in Javascript land and a lot of libs are using them (including new Google’s Polymer).

Is there an easy/convenient way to create and use them from Haxe?

1 Like

You can use macros to accomplish about anything in Haxe.

For example, you could do something like this:

import Implementation.html;

class Usage {
  static function main() {
    var Comp1 = data -> html('
      <div>
        <a href="${data.url}">${data.title}</a>
      </div>
    ');
    trace(Comp1({ 
      url: 'http://example.com/?foo=2&bar=blargh', 
      title: "Let's rock!" 
    }));
  }
}
Implementation
import haxe.macro.*;

class Implementation {
  #if macro
  static function interpolate(e:Expr, escape:Expr->Expr)
    return
      switch e {
        case macro @:markup $e:

          interpolate(e, escape);

        case { expr: EConst(CString(s)) }:

          var parts = {
            var cur:Expr = MacroStringTools.formatString(s, e.pos),
                ret = [];

            while (true)
              switch cur {
                case macro $lh + $rh:
                  ret.push(rh);
                  cur = lh;
                default:
                  ret.push(cur);
                  break;
              }

            ret.reverse();
            ret;
          }

          var ret = parts.shift();

          for (i in 0...parts.length) {
            var next = parts[i];
            if (i % 2 == 0)
              next = escape(next);
            ret = macro @:pos(next.pos) $ret + $next;
          }
          ret;
        default:
          Context.error('string literal expected', e.pos);
      }
  #end
  static public macro function html(e)
    return interpolate(e, v -> macro @:pos(v.pos) StringTools.htmlEscape($v, true));
}

That would generate the following JavaScript:

class Usage {
  static main() {
    var Comp1 = function(data) {
      return "\r\n      <div>\r\n        <a href=\"" + StringTools.htmlEscape(data.url,true) + "\">" + StringTools.htmlEscape(data.title,true) + "</a>\r\n      </div>\r\n    ";
    };
    console.log("src/Usage.hx:10:",Comp1({ url : "http://example.com/?foo=2&bar=blargh", title : "Let's rock!"}));
  }
}

You could also quite easily do things like trimming whitespace in the macro, which will thus happen at compile time and result in (slightly) less generated JS.

4 Likes

Thank you, that seems pretty cool. I haven’t played yet with macros in Haxe (it seems they are much harder to grasp than in lisp, due to Haxe’s much more complex AST).

That seems like a good base for compile-time only solution in Haxe, it just needs some kind of prevention of double-escaping (to enable composition of multiple “components”).

Do you have maybe some example of implementation which would be compatible with existing JS tagged template functions? i.e. when you call:

html`<div>
    <a href="${'http://example.com/?foo=2&bar=blargh'}">${"Let's rock!"}</a>
</div>
`

html function gets this params:

  [ '<div>\n    <a href="', '">', '</a>\n</div>\n' ],
  'http://example.com/?foo=2&bar=blargh',
  "Let's rock!"

(first param is array of strings and the rest are results of expressions)

There is a “Tagged templates” section with more info on this page: Template literals (Template strings) - JavaScript | MDN

Yes, Haxe macros are quite a bit more complex, although the AST itself is relatively straight forward. The explicitness actually takes a bit of problems away, e.g. an if is an EIf while in a lisp macro you may well encounter (if x y z wtf).

The additional complexity comes from the fact that there are many different kinds of macros (expression macros, build macros, genericBuild macros, init macros, static extension macros, implicit cast macros … insert Forrest Gump shrimps meme here). And the fact that you may interact with the compiler (in particular the typer), even defining hooks such as:

  • onTypeNotFound: allows you to provide a type if none is found
  • onGenerate: receives all types and allows you to do additional static analysis
  • onAfterGenerate: runs after all code is generated, so you may post-process the output, e.g. pass it to a minifier or whatever

Yeah, that’s easily accomplished in many different ways, but with so little context I can’t tell you which way to go.

You can use a build macro to make it so that @meta'string' patterns are all converted into such calls:

import Dummy.html;

@:build(TemplateLiterals.use())
class Usage {
  static function main() {
    var Comp1 = data -> @html'
      <div>
        <a href="${data.url}">${data.title}</a>
      </div>
    ';
    trace(Comp1({
      url: 'http://example.com/?foo=2&bar=blargh',
      title: "Let's rock!"
    }));
  }
}

Note that variadic functions are not really a thing in Haxe. You can use haxe.extern.Rest to define the signatures. Implementing these yourself is a bit more awkward - normally you’ll be taking this from a JavaScript lib instead:

class Dummy {
  static public final html:(literals:Array<String>, values:haxe.extern.Rest<Any>)->String
    = Reflect.makeVarArgs(function (a:Array<Dynamic>) {
      var literals:Array<String> = a.shift();
      var parts:Array<Any> = cast a;

      return [for (i in 0...literals.length)
        literals[i] + switch parts[i] {
          case null: '';
          case v: StringTools.htmlEscape(Std.string(v));
        }
      ].join('');
    });
}

As for the build macro itself, it is reasonably small:

#if macro
import haxe.macro.*;
using haxe.macro.Tools;
#end
class TemplateLiterals {
  #if macro
  static function crawl(e:Expr)
    return switch e.map(crawl) {
      case { expr: EMeta(m, { pos: pos, expr: EConst(CString(s)) }) }:

        var literals = [];
        var args = [macro $a{literals}];

        var parts = {
          var cur:Expr = MacroStringTools.formatString(s, e.pos),
              ret = [];

          while (true)
            switch cur {
              case macro $lh + $rh:
                ret.push(rh);
                cur = lh;
              default:
                ret.push(cur);
                break;
            }

          ret.reverse();
          ret;
        }

        for (i in 0...parts.length)
          (
            if (i % 2 == 0) literals
            else args
          ).push(parts[i]);

        macro @:pos(m.pos) $i{m.name}($a{args});
      case v: v;
    }
  static function use() {
    var ret = Context.getBuildFields();

    for (f in ret)
      f.kind = switch f.kind {
        case FVar(t, e):
          FVar(t, crawl(e));
        case FProp(get, set, t, e):
          FProp(get, set, t, crawl(e));
        case FFun(f):
          FFun({
            args: f.args,
            ret: f.ret,
            expr: crawl(f.expr)
          });
      }

    return ret;
  }
  #end
}
2 Likes

OK, thanks, that gives me enough info to get started. I’ll also take a closer look to some of your libs (hxx, tink_template), which may be even better solution in the long term.

1 Like