@civicnet/strop

Simple tagged template literals (strings)

Usage no npm install needed!

<script type="module">
  import civicnetStrop from 'https://cdn.skypack.dev/@civicnet/strop';
</script>

README

StrOP    npm Build Coverage

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

License

MIT

CivicNet