Running templates
The run method, its options, and the Response object it returns.
Interpreter holds a list of parsers and renders templates with them.
import { IfStatementParser, Interpreter, RandomParser } from 'tagscript';
const ts = new Interpreter(new RandomParser(), new IfStatementParser());One interpreter can render any number of templates, and it holds no per-render state, so build it once at startup rather than per request. Change the parser list later with ts.addParsers(...) and ts.setParsers(...).
run
ts.run(message, options?);| Option | Default | What it does |
|---|---|---|
message | required | The template to render. Passed positionally. |
seedVariables | {} | Variables the template can read, as name to transformer. |
charLimit | null | Maximum characters the render may produce. Going over rejects. null means no limit. |
tagLimit | 2000 | Maximum characters read from inside one {...}. The rest of that tag body is dropped. |
parenType | ParenType.Both | Which parameter syntaxes are allowed: Both, Parenthesis or Dot. |
keyValues | {} | Arbitrary data for your own parsers, readable at ctx.response.keyValues. |
const response = await ts.run(template, {
seedVariables: { args: new StringTransformer('hello') },
charLimit: 2_000,
});The old positional form, run(message, seedVariables, charLimit, tagLimit, parenType, keyValues), still
works and is deprecated. It will be removed in the next major.
Response
run resolves to a Response, not a string.
| Property | Type | What it holds |
|---|---|---|
body | string | null | The rendered output, trimmed. |
raw | string | The template exactly as passed in. |
actions | IActions | Side effects the template requested. Your code decides what to do. |
variables | Record<string, ITransformer> | Seeded variables plus anything the template defined during the render. |
keyValues | IKeyValues | Whatever you passed in. The interpreter never touches it. |
errors | TagScriptError[] | Every error a parser raised. Empty when the render was clean. |
Limits
charLimit is the one that matters for untrusted templates. Without it, a template that expands cheaply into a very large string is the remaining way a template author can cause trouble.
await ts.run(template, { seedVariables: vars, charLimit: 2_000 });Going over the limit rejects with a WorkloadExceededError, so wrap the call:
try {
const response = await ts.run(template, { seedVariables: vars, charLimit: 2_000 });
return response.body;
} catch (error) {
if (error instanceof WorkloadExceededError) return 'That tag produced too much output.';
throw error;
}Errors
A parser failing does not reject and does not end the render. The interpreter replaces just that tag
and keeps going, and records what happened on response.errors.
What lands in the body depends on whose mistake it was.
| The parser raises | The body gets | response.errors gets |
|---|---|---|
TemplateError | the error's message, as written | the TemplateError |
| anything else | a generic message | a ParserError, with the real error on cause |
StopSignal | the render so far, then its message | nothing, this is control flow rather than a failure |
The split exists because the person who wrote the template is usually not the person running the
process. They have no console and no stack trace, so a mistake they can fix is worth putting in
front of them. Malformed JSON in an {embed}, or a parameter that is not a number. Raise a
TemplateError for those:
public parse(ctx: Context) {
const seconds = Number.parseInt(ctx.tag.parameter!, 10);
if (Number.isNaN(seconds)) throw new TemplateError('cooldown needs a number of seconds', ctx.tag.declaration);
return '';
}Anything else your parser throws is a bug in your parser, not in the template. Telling a template
author Cannot read properties of undefined helps nobody and can leak internals into a public
channel, so the body gets GENERIC_PARSER_ERROR_MESSAGE and the real error is kept for you:
const response = await ts.run(template);
for (const error of response.errors) {
if (error instanceof ParserError) logger.error(error.cause);
}Before 3.0, any throw from a parser ended the render and put the raw error message in the body, which
is how stop worked. Ending the render early is now StopSignal. If you
wrote a parser that throws a plain Error to halt, switch it to throw new StopSignal(message).
tagLimit caps how much of a single tag's body the lexer reads, at 2000 characters by default. It truncates rather than throwing, which can turn a long tag into a different, shorter tag, so lower it only when you have a reason.
Restricting the parameter syntax
ParenType decides which of the two parameter forms are legal for a render.
import { Interpreter, ParenType, StrictVarsParser } from 'tagscript';
const ts = new Interpreter(new StrictVarsParser());
await ts.run('{args.2}', vars, null, 2_000, ParenType.Parenthesis);
// '{args.2}', the dot form was not acceptedBoth is the default. Pick one form when your templates sit inside something else that already gives . or ( a meaning.
How a render works
buildNodeTreescans the template for brace pairs, ignoring any escaped with a backslash. Nodes come out ordered by closing brace, which is why inner tags render before outer ones.- For each node, a
Lexersplits the text into declaration, parameter and payload. willAcceptruns on every parser, and the ones that accept are tried in registration order.- The first parser to return a non-null value wins, and its string replaces the tag. Every later node's coordinates shift by the length difference.
- If no parser returns a value, the tag is left in place.
Registration order decides which parser wins a tag two parsers both accept, so register the more specific one first.
API reference
Last updated on