Triple-backtick template literal with indentation support

This is somewhat similar to python or java's ability to use triple-quotes to represent literal strings without lots of escape characters, something I think could be useful is the following extension to backticked template literals:

Allow specifying triple- (or maybe quintuple-, or septuple-, or any-odd-number-uple-) backticked literals, which behave almost the same as a regular backticked template literal, with a few key differences:

  • The string is automatically "dedented", along the lines of what the dedent library does. A simple algorithm would be:
    • ignore the first line
    • calculate the "indent" using the whitespace at the beginning of the first line
    • remove that indent from every line
  • The string would be automatically trimmed
  • (maybe) backticks inside the string don't need to be escaped

it would look something like this:

console.log(```
  yaml:
    is:
      supported: nicely
```)

This would calculate that the "indent" is two spaces (from the first line, "​ yaml:...") and would print out:

yaml:
  is:
    supported: nicely

To achieve the same thing with an existing template literal makes code quite hard to read, especially when the literal is defined somewhere indented:

class MyClass {
  printYaml() {
     console.log(`
yaml:
  is:
    supported: in a very ugly way
`)
  }
}

vs in the new world:

class MyClass {
  printYaml() {
    console.log(```
      yaml:
        is:
          supported: nicely
    ```)
  }
}

There are a variety of inconsistent ways to deal with this right now:

  • use dedent or similar. This makes what could be pure data have a dependency and reduces its portability. You can no longer copy-paste snippets, you have to copy-paste snippets, install an npm package, maybe install the types too, import it and then use it. This solution also cannot be used in ecmascript-compatible data formats like json5
  • do this automatically at runtime. This is what jest inline snapshots do - they ignore indents when comparing objects to snapshots. This works ok, but looks wrong if the call sites are moved around.

The above approaches also require teaching formatters like prettier to detect when they can safely adjust the indent to make code look better. e.g.

gql`...`
where whitespace is insignificant; or
dedent`...`
or
.toMatchInlineSnapshot(`...`)
where it'll be "fixed" at runtime. This isn't easy to maintain, since the formatter needs to be tightly coupled to the code, and will never be able to fully understand the context.

With this proposal, formatters would be able to confidently sync template indentation with the code the literal is found in without worrying about the runtime behaviour of dedent, graphql-tag, jest, etc.


This could be implemented without any syntax changes, since it's already valid syntax:

var s = ```
  abc
```

is equivalent to:

var s = (``)(`
  abc
`)(``)

i.e. when run, this code will try to use the empty string

(``)
as an es string tag, passing in '\n abc\n', the return value of which is then used as another es string tag which receives the empty string. Obviously, none of that will currently work at runtime, because an empty string is not a function. But without any syntax/parser changes, strings could be overloaded as functions to allow this behaviour, with whatever cleverness is required internally to make it work as documented.

The alternative would be to make a breaking syntax change (in all likelihood, breaking no functional code), which would allow using backticks unescaped inside a triple-quoted literal. This would be very useful for things like js code examples within code.

Either approach should be easy enough to shim in preprocessors like typescript or babel for forwards-compatibility.

I'd be interested to read people's thoughts on this, and start a discussion about prior art, implementations in other languages, backwards-compatible runtime changes vs breaking syntax changes, etc.

3 Likes

I really like this idea. Indentation is the most consistent annoyance for me with templates.

Any thoughts on the feasibility of tweaking the syntax to avoid needing to escape within the template? e.g.

const printBashCommand = () => {
  console.log(```
    ./some-bash-script.sh `ls`
  ```);
};

You could even use more backticks to allow triple-backticks inside templates without escaping:

const getMarkdown = () => {
  return `````
    # blah blah

    ```json
    { "foo": "bar" }
    ```

    some _more_ *markdown*
  `````;
};

I've wanted to do this multiple times, in fact I had a need just last week.

It is possible to do this at runtime using a tagged template, but it's not super ergonomic (and incurs a not-insignificant runtime cost):

const raw = dedent`
  yaml:
    is:
      supported: nicely
`;

YAML.parse(raw);

Doing this also hurts feeding the dedented string into a second tagged template literal. It's actually impossible, you have to call the overall tag like gql as a function and not a tag. If your dedent isn't doing proper caching of the indented -> dedented strings array, this can seriously hurt performance (reprocessing the same "template literal" multiple times).

const raw = dedent` … `;

gql`${raw}`; // <- this doesn't work right.

// Have to call it:
gql(raw); // <- this only works if the tag can support both tag and call forms
1 Like

@jridgewell agreed - I should have included an explicit dedent example in the OP. IMO using tagged templates like that is a bit of a hack, which is partly why I want to propose this. Similar to your example, it doesn't really allow for usage with tagged template literals with customised behaviour, like slonik which escapes variables to protect against sql injection:

const query = sql`
  select *
  from mytable
  where id = ${foo}
`

Effectively, every tagged template literal function needs to build in dedent behaviour, or be ok with ugly code, or ugly outputs.

Is it worth creating a strawman proposal repo already, or should I keep this open for a while longer to collect more feedback? I haven't made a proposal before so I'm a bit unsure of the process.

It would be possible to write a dedent function that can optionally take another tag function to call, to be used like

const query = dedent(sql)`
  select *
  from mytable
  where id = ${foo}
`;

but I agree it's kinda ugly and will prevent syntax highlighting or linting the template literal contents or whatever relies on the tag name.

Yah, if you'd like to write up a proposal repo for this, I'm interested. Basically you need to hit "Use this template" on GitHub - tc39/template-for-proposals: A template for ECMAScript proposals, and fill out the readme with details.

Yah, that's what was landed on in the #tc39 IRC channel. But it's not free (some runtime processing and WeakMap lookup), and will conflict with the "templateness" in proposals like GitHub - tc39/proposal-array-is-template-object: TC39 proposal to identify template strings. I'm not super excited with this approach, but it's a stop-gap.

@jridgewell I made a starter repo: https://github.com/mmkal/proposal-multi-backtick-templates

Let me know here or via the issues anything you'd like me to add. I can add you as a collaborator too if you'd like (guessing your github username is jridgewell too?). Also... would you be interested in being a "champion"? :upside_down_face:

Yes, please add me as a collaborator. I am @jridgewell.

Yes.

As a co-champion Presented this in the TC39 meeting and it is now on Stage1!

3 Likes

That's awesome! This is a convenient thing for templates!

Why not learn from (or combine with) the idea of C# 11's raw string literals, which solves almost all problems:

  • It solves the indentation problem in this thread
  • Can contain any arbitrary text without escape sequences in the content
  • Can support interpolation and defining custom interpolation delimiters so that the "normal delimiters" can be in the content without being escaped as well

I have put the details in another idea/proposal:

1 Like

https://github.com/tc39/proposal-string-dedent/issues/40

Since triple backticks (```) are not backward compatible, I have updated my proposal at