
Template Tag Common
Simplifies authoring JS string template tags. Tagged string templates
allow embedding a mini-language with JavaScript, and the example below
is syntactic sugar for a call to myMiniLang
.
myMiniLang`...`
This library makes it easier to write your own. See "Tagged template literals" for details about how template tag functions are called.
Contents
Example
The example code below defines a CSV (Comma-separated value file) formatter that takes into account whether an interpolation happens inside quotes.
// Import this library.const memoizedTagFunction trimCommonWhitespaceFromLines TypedString } = const Mintable = /** * A fragment of CSV. * Unlike simple strings, numbers, or Dates, * fragments may span multiple cells. */ {}Objectconst isCsvFragment = Mintable// Assumes module-keys/babel pluginconst mintCsvFragment = requiremoduleKeys /** * A template tag function that composes a CSV fragment * by ensuring that simple values are properly quoted. */const csv = // memoizeTagFunction caches the results of this// if csv`...` happens inside a loop, this only// happens once. { const raw = const contexts = let betweenQuotes = false raw if betweenQuotes const placeholder = '${...}' throw `Missing quote in CSV: \`\`` return raw contexts } // Called with the contexts computed above, the static chunks of text,// then the dynamic values to compute the actual result. { const len = valueslength let result = '' for let i = 0; i < len; ++i const alreadyQuoted = contextsi const value = valuesi let escaped = null if // Allow a CSV fragment to specify multiple cells escaped = alreadyQuoted ? `""` : valuecontent // TODO: maybe convert date to 2018-01-01T12:00:00Z format else escaped = JSON if alreadyQuoted escaped = escaped result += rawi result += escaped result += rawlen return } console// Logs something like// foo,1,bar,bar// "ab\"c",baz,"boo\n",far moduleexports = csv CsvFragment
API
calledAsTemplateTag(firstArgument, nArguments)
If defining a function that may be used as a template tag or called normally, then pass the first argument and the argument count and this will return true if the call was via a string template.
const calledAsTemplateTag = { if // Assume template tag calling convention const staticStrings ...dynamicValues = args ... else // Assume regular function calling convention ... }
This is true iff firstArgument
could be a result of
GetTemplateObject
and the number of dynamic arguments is consistent with a
template call.
It is possible, but unlikely, for this function to return true when
the caller is not a template literal. It is not likely that an
attacker could cause an untrusted input to specify static strings; no
firstArgument
deserialized via JSON.parse
will pass this function.
calledAsTemplateTagQuick(firstArgument, nArguments)
Like calledAsTemplateTag
but doesn't check that the
strings array contains only strings.
memoizedTagFunction(computeStaticHelper, computeResultHelper)
Memoizes operations on the static portions so the per-use cost of a tagged template literal is related to the complexity of handling the dynamic values.
computeStaticHelper
:{!function (Array.<string>): T}
called when there is no entry for the frozen static strings object, and cached weakly thereafter. Receives a string of arrays with a.raw
property that is a string array of the same length.computeResultHelper
:{!function (O, T, !Array.<string>, !Array.<*>): R}
a function that takes four parameters:- An options object. By default, an empty object.
- The result of computeStaticHelper above.
- The static chunks of text that surround the
${...}
- The dynamic values that result from evaluating the contents of
${...}
Returns {!function (!Array.<string>, ...*): R}
a template tag
function that calls computeStaticHelper
as needed on the static
portion and returns the result of applying computeResultHelper
.
By splitting tagged template processing into separate static analysis and dynamic value handling phases, we encourage granting privilege to the static portions which the developer specifies and treating with suspicion the dynamic values which may be controlled by an attacker.
options
object
Configuring tag handlers by passing an A computeResultHelper
's options
parameter bundles optional
configuration data together.
Configurations can be passed to a tag as a single argument before the template literal:
`Foo baz`
Configurations can be associated with a tag and then later used:
const myConfiguredTag = const tagResult = myConfiguredTag`foo baz`
Arrays cannot be valid options
objects because of the way we
distinguish a call to specify options from a use of the tag.
Life-cycle of a tag function
Execution of
const memoizedTagFunction = const myTag = const result = `string0 string1 string2\n`
is equivalent to
// The JavaScript engine does this under the hood.// It is hoisted to the top of the module.const staticStrings = 'string0 ' ' string1 ' ' string2\n' staticStringsraw = 'string0 ' ' string1 ' ' string2\\n' ObjectObject // This is the part that memoizedTagFunction does.const result =
but if this happened in a loop, the call to computeStaticHelper
would
probably only happen once.
trimCommonWhitespaceFromLines(strings, options)
Simplifies tripping common leading whitespace from a multiline template tag so that a template tag can be re-indented as a block.
This function takes the first argument to a tag handler.
A memoized tag handler's computeStaticHandler
function (see above)
can call this so that the cost is not incurred every time a particular
template is reached.
Using this in template tag handlers ensures that code blocks like the two below are treated the same even though the string templates' contents have been indented differently so as to flow nicely with the surrounding code.
{ if x return null return aTagHandler` { ... }` // Indent level 2}
{ if x return null else return aTagHandler` { ... }` // Indent level 3 }
The options
parameter is optional as are all its properties. Options include
option property name | meaning | default |
---|---|---|
trimEolAtStart |
trim starting line terminator from first chunk | false |
trimEolAtEnd |
trim ending line terminator from last chunk | false |
TypedString
A TypedString
is an object that represents a string that matches a known
contract. Each subclass
of TypedString
encapsulates such a contract.
Create a subclass of TypedString
when you want to treat some kinds of
strings specially.
This can make it very easy to write composable tag handlers -- tag handlers that can easily be split up or refactored into multiple steps.
The CSV example does not re-escape CSVFragment
s.
{}Object
Note that each concrete sub-class of TypedString
must have
a static property contractKey
. This allows using a minter and
verifier instead of error-prone instanceof
checks. That
module fetches them thus
const isCsvFragment = Mintableconst mintCsvFragment = requiremoduleKeys
Mintable.minterFor
returns a box that is openable when
there's a grant for the current module. The (x) => String(x))
allows it to degrade gracefully to returning a simple string.
Later that example checks whether a value has a particular content type before re-escaping
if
The output of csv`...`
is also a CSVFragment
return
which makes it easy to compose multiple uses of csv`...`
or split and refactor a single use.
const row0 = csv`...`const row1 = csv`...` // Combine two rows into onecsv``