Tagscript

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?);
OptionDefaultWhat it does
messagerequiredThe template to render. Passed positionally.
seedVariables{}Variables the template can read, as name to transformer.
charLimitnullMaximum characters the render may produce. Going over rejects. null means no limit.
tagLimit2000Maximum characters read from inside one {...}. The rest of that tag body is dropped.
parenTypeParenType.BothWhich 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.

PropertyTypeWhat it holds
bodystring | nullThe rendered output, trimmed.
rawstringThe template exactly as passed in.
actionsIActionsSide effects the template requested. Your code decides what to do.
variablesRecord<string, ITransformer>Seeded variables plus anything the template defined during the render.
keyValuesIKeyValuesWhatever you passed in. The interpreter never touches it.
errorsTagScriptError[]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 raisesThe body getsresponse.errors gets
TemplateErrorthe error's message, as writtenthe TemplateError
anything elsea generic messagea ParserError, with the real error on cause
StopSignalthe render so far, then its messagenothing, 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 accepted

Both is the default. Pick one form when your templates sit inside something else that already gives . or ( a meaning.

How a render works

  1. buildNodeTree scans 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.
  2. For each node, a Lexer splits the text into declaration, parameter and payload.
  3. willAccept runs on every parser, and the ones that accept are tried in registration order.
  4. 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.
  5. 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

Interpreter, Response, Context, Lexer

Last updated on

On this page

Edit on Github