Simple tagged template literals (strings)
Installing
Install from npm:
npm install @civicnet/strop
Install and save as a dependency:
npm install --save @civicnet/strop
Basic use
const StrOP = require('@civicnet/strop');
const sample = new StrOP('Sample');
let person = { name: 'Alice', job: 'programmer', options: [ 'yes', 'no' ] };
let greeting = sample`
Hi, ${person.name}!
Do you like being a ${person.job}?
Options:
${person.options.map((o) => `* ${o}`).join('\n')}
`;
console.log(`${greeting}`);
This will produce the following output:
Hi, Alice!
Do you like being a programmer?
Options:
* yes
* no
What it does
StrOP
is a factory for template literal tags. Their main use is to adjust indentation, so that multi-line template literals can be used intuitively anywhere in your code.
A tag's default behaviour:
- trims leading and/or trailing lines that are empty (or contain only indentation characters);
- removes common indentation from all remaining non-empty lines;
- interpolates placeholders exactly like untagged template literals and indents them accordingly;
- produces an object which can be converted to a string.
If the first line is not empty and doesn't contain only indentation characters, it will not have its indentation removed, nor will it be taken into account when computing the common indentation.
Single-line templates do not have their indentation changed.
Constructing tags
A single parameter is passed to the constructor: the tag's name
. It doesn't do anything, but can be useful when debugging.
Tag results
By default, a tag produces an object which contains all the information needed to be lazily converted to a string. This can be done in several ways:
console.log(`${greeting}`);
console.log(greeting.toString());
console.log(String.raw(...greeting));
The same string is produced; either variation might be suitable for different situations.
The object is also an instance of the tag:
console.log(greeting instanceof sample); // true
Loading from files
Templates can also be loaded from files:
let greet = sample.file('./examples/greeting.in');
console.log(`${greet(person)}`);
Loading a file returns a function which should be called with object parameters; their keys are used by the template during interpolation.
The parameters are searched in the order they are passed. If the template references a key that doesn't exist in any of the passed objects, an error will be thrown.
The tag that loaded the file is used to produce the result.
Customizing tags
A tag factory is used because each individual tag can be customized.
The indentation characters that are removed can be changed, interpolation can be customized using rules and types, and the overall behaviour can be altered by overwriting tags' methods.
Indentation characters
The indentation characters that a tag may remove are located in its indent
property string; the default value of '\t '
allows tags to remove tabs and spaces.
An example:
const custom = new StrOP('Custom indentation');
custom.indent = '\t >';
let todo = custom` TODO:
> Write code
> Test code
`;
console.log(`${todo}`);
Its output:
TODO:
Write code
Test code
Reminder: the first line will not have its indentation removed if it is not empty and doesn't contain only indentation characters.
In the odd case that a mix of different indentation characters is used in the template, only identical runs at the beginning of every eligible line are considered "common" (and removed).
Rules and types
Rules and types can be used to customize interpolations.
Rules match interpolated primitive values and replace them with other values:
sample.rule(3, 'three');
sample.rule(4, 'four');
let count = sample`${1}, ${2}, ${3}, ${4}`;
console.log(`${count}`); // 1, 2, three, four
They work as expected with undefined
, null
, NaN
and other "exotic" values (similar to ===
).
Types match using the interpolated values' constructors and replace them by calling the corresponding handlers:
class Percentage extends Number { }
sample.type(Percentage, (p) => `${(p * 100).toFixed(2)}%`);
let score = sample`Your score is: ${new Percentage(28 / 30)}`;
console.log(`${score}`); // Your score is: 93.33%
Handlers are called with the interpolated value, in the context (this
) of the tag instance.
Note: Every interpolated value's entire prototype (inheritance) chain is searched; if the value matches multiple types, only the most specialized one's handler will be called.
Rules and types take precedence over the interpolated values' string conversion methods.
Methods
A tag's behaviour can be customized by overwriting its methods.
file(path)
This method is not called internally.
The default implementation loads the template file indicated by path
and returns a function which can be called with objects, the keys of which are searched (in the order they are passed) during interpolation.
Custom implementations could be used to:
- locate template files;
- provide default values;
- alter the way the returned function is called.
Note: The result of the function call should not be altered, as it is implied to be similar to that of tagging template literals. Both cases can be customized by overwriting the pass
method instead.
Custom implementations should (but are not required to) call the default implementation.
pass({ raw }, ...values)
This method is the actual tag function; it is called internally to prepare the result of a tag operation, after indendation is removed from the raw
strings, and with the original values
. It is used both for template literals and the default file
method's returned functions.
The default implementation returns an array-like object which wraps the parameters; when converted to a string, it calls the render
method for for every interpolated value. Proper indentation will be added to the rendered results if they span multiple lines.
Custom implementations could be used to:
- emplace DSLs;
- make external calls;
- alter the returned value.
Note: The returned value is further processed internally to ensure it is an instance of the tag; this does not work for primitives and "breaks" most typed objects.
If built-in Boolean
, Date
, Number
or String
objects are returned, the default unwrap
method will (correctly) convert them to primitives.
Custom implementations should (but are not required to) call the default implementation.
render(value)
This method is called internally to produce a string representation for an interpolated value
; it is called for each interpolated value by the object returned by the default pass
method when it is converted to a string.
The default implementation searches rules and types for substitution and converts the value (or substitute) to a string.
Custom implementations could be used to:
- change the substitution logic;
- quote, decorate and/or escape values;
- cache conversions.
Custom implementations must call the original implementation to apply rules and types, as there are no other means to achieve this.
rule(value, as)
This method is not called internally.
The default implementation instructs the tag to replace every interpolated value
with as
.
There are no discernible cases that would require overwriting this method.
Custom implementations must call the original implementation to add effective rules, as there are no other means to achieve this.
type(factory, handler)
This method is not called internally.
The default implementation instructs the tag to replace every interpolated value that is an instace of the factory
function by calling the handler
function with the value, in the context (this
) of the tag.
Every interpolated value's entire prototype (inheritance) chain is searched; if the value matches multiple types, only the most specialized one's handler will be called.
There are no discernible cases that would require overwriting this method.
Custom implementations must call the original implementation to add effective types, as there are no other means to achieve this.
unindent(...strings)
This method is called internally with the template's raw strings
to remove indentation characters.
The default implementation trims leading and/or trailing lines that are empty (or contain only indentation characters) and removes common indentation from all remaining non-empty lines.
If the first line is not empty and doesn't contain only indentation characters, it will not have its indentation removed, nor will it be taken into account when computing the common indentation.
Intermediate empty lines are preserved but are not taken into account when computing the common indentation. This is done to mitigate some text editor behaviours which may automatically remove trailing whitespace and/or trim empty-looking lines.
Note: Lines that are neither completely empty nor at the very beginning or end of the template will be taken into account when computing the common indentation.
Single-line templates do not have their indentation changed.
There are no discernible cases that would require overwriting this method.
Custom implementations should (but are not required to) call the default implementation and must always return an array of the same length as strings
.
unwrap(value, hint = 'default')
This method is not called internally by default. It is provided as a helper for objects returned by the pass
method which didn't provide their own primitive conversion method.
The default implementation checks if value
is (or was) a built-in Boolean
, Date
, Number
or String
object and returns its primitive conversion; other objects are converted to strings directly and primitives are returned unchanged.
Custom implementations could be used to:
- alter the way built-in objects are converted;
- convert additional types of objects.
Custom implementations should (but are not required to) call the default implementation.
Tests
Tests can be run using:
npm test