Dedent templates, autoindent interpolations, and more. With edge cases handled the right way. Primarily intended for code generation.
yarn add @anireat/d
import { d } from '@anireact/d';
let res = d`
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>Hello</title>
</head>
<body>
<p>Properly indented and trimmed 🎉</p>
</body>
`;
The dedent tag you want:
- Raw mode.
- If the first line is blank, it’s trimmed; the same for the last line.
- Other lines are dedented by the least common indent.
- Blank lines don’t affect the dedent width.
- Non-blank first line doesn’t affect the dedent width and isn’t dedented.
- Tab interpreted as a single space.
- Interpolation values are converted to strings.
- If the interpolation value is multiline, the lines (except the first one) are autoindented to match the indent of the line the interpolation is placed at. Empty lines of interpolation are kept empty.
- Completebly blank templates with no interpolations are kept untouched.
function d<T>(head: TemplateStringsArray, ...tail: T[]): string;
Examples:
// ␣ represents space,
// -> represents tab,
// ¶ indicates end of line.
- Basic usage:
The result:
d`␣␣¶ ␣␣␣␣␣␣␣␣↑ First line is trimmed.¶ ␣␣␣␣» This line has the least common indent -- 4 spaces. « ␣␣␣␣␣␣␣␣↓ Blank lines don’t affect the dedent width:¶ ¶ ␣␣¶ ␣␣␣␣¶ ¶ ␣␣␣␣␣␣␣␣if (check) {¶ ␣␣␣␣␣␣␣␣␣␣␣␣${'f1();\nf2();\n\nwhile (loop()) {\n f3();\n}'}¶ ␣␣␣␣␣␣␣␣␣␣␣␣// ↑ This interpolation is autoindented with 8 spaces.¶ ␣␣␣␣␣␣␣␣}¶ ¶ ␣␣␣␣␣␣␣␣↓ Last line is trimmed:¶ ␣␣`;
␣␣␣␣↑ First line is trimmed.¶ » This line has the least common indent -- 4 spaces. « ␣␣␣␣↓ Blank lines don’t affect the dedent width:¶ ¶ ¶ ¶ ¶ ␣␣␣␣if (check) {¶ ␣␣␣␣␣␣␣␣f1();¶ ␣␣␣␣␣␣␣␣f2();¶ ¶ ␣␣␣␣␣␣␣␣while (loop()) {¶ ␣␣␣␣␣␣␣␣␣␣␣␣f3();¶ ␣␣␣␣␣␣␣␣}¶ ␣␣␣␣␣␣␣␣// ↑ This interpolation is autoindented with 8 spaces.¶ ␣␣␣␣}¶ ¶ ␣␣␣␣↓ Last line is trimmed:
- Most common edge cases:
The result:
d`␣␣Non-blank first line -- ignore indent, don’t dedent.¶ ␣␣␣␣${''}¶ ␣␣␣␣␣␣␣␣↑ This interpolation forces least indent.¶ ->␣␣␣␣␣␣␣» Tab works as a single space. «¶ ␣␣␣␣␣␣␣␣» Non-blank last line isn’t trimmed. «`;
␣␣Non-blank first line -- ignore indent, don’t dedent.¶ ¶ ␣␣␣␣↑ This interpolation forces least indent.¶ ␣␣␣␣» Tab works as a single space. «¶ ␣␣␣␣» Non-blank last line isn’t trimmed. «
- Blank templates are kept unchanged:
The result:
d`␣␣¶ ␣␣␣␣¶ ␣␣␣␣␣␣¶ ␣␣␣␣¶ ␣␣`;
␣␣¶ ␣␣␣␣¶ ␣␣␣␣␣␣¶ ␣␣␣␣¶ ␣␣
- Not actually blank — dedented as usual:
The result:
d`␣␣␣␣␣␣¶ ␣␣${''}␣␣␣␣¶ ¶ ␣␣␣␣␣␣␣␣¶ ␣␣␣␣␣␣`;
␣␣␣␣¶ ¶ ␣␣␣␣␣␣
See the tests for more edge case examples.
Similar to the d
tag, but returns an array of Token<T>
objects, with no autoindent applied to interpolation tokens:
namespace d {
function tokenize<T>(head: TemplateStringsArray, ...tail: T[]): Token<T>[];
}
Examples:
deepEqual(
d.tokenize`¶
␣␣␣␣␣␣␣␣Literal¶
␣␣␣␣␣␣␣␣␣␣␣␣${'Interpolation'}¶
␣␣␣␣␣␣␣␣Another literal¶
␣␣␣␣`,
[
{ lit: true, value: 'Literal\n ' },
{ lit: false, value: 'Interpolation', pad: ' ' },
{ lit: true, value: '\nAnother literal' },
],
);
Custom dedent tag constructor:
function d<T, U>(params: Params<T, U>): Tag<T, U>;
See the types Tag<T,U>
and Params<T,U>
.
Examples:
// Similar to `d`, but uses cooked literals:
d({
raw: false,
impl: d,
});
// Similar to `d`, but doesn’t autoindent interpolations:
d({
impl: v => v.map(dstringify).join(''),
});
// Similar to `d`, but doesn’t concatenate substrings:
d({
impl: v => v.map(d),
});
// Similar to `d.tokenize`, but stringifies tokens:
d({
impl: v => v.map(d.stringify),
});
Template tag args type check. Does its best to detect actual template-tag calls and to filter out other similar signatures:
- The
x
array must be non-empty. - The head
x[0]
:- Must be
Array.isArray
. - Must be frozen.
- Must have own
raw
prop with non-enumerablevalue
descriptor.
- Must be
- The raws
x[0].raw
:- Must be
Array.isArray
. - Must be frozen.
- Must have the same length as head.
- Must be
- The tail
x.slice(1)
:- Must be one item shorter than head.
See the type Args<T>
.
function isTemplate(x: any[]): x is Args<any>;
Examples:
function f(...args: Args<any> | [x: string[]]) {
if (isTemplate(args)) {
// Called as a tag
} else {
let [x] = args;
// Called as a regular function
}
}
Just an identity function. Can be reused to save few bytes of your bundle, huh.
function id<T>(x: T): T;
Converts Token<T>
value using String()
:
namespace d {
function stringify(tok: Token<any>): string;
}
Examples:
d.stringify({
lit: true,
value: 'Literal',
}) === 'Literal';
d.stringify({
lit: false,
value: 2434,
pad: '',
}) === '2434';
d.stringify({
lit: false,
value: {
toString: () => 'x.toString',
},
pad: '',
}) === 'x.toString';
d.stringify({
lit: false,
value: {
[Symbol.toPrimitive]: () => 'x[Symbol.toPrimitive]',
},
pad: '',
}) === 'x[Symbol.toPrimitive]';
Converts token to string, autoindents interpolations:
function d(tok: Token<any>): string;
Examples:
let arr = [
{ lit: true, value: 'lit-1' },
{ lit: false, value: 'line-1\nline-2', pad: ' ' },
{ lit: true, value: 'lit-2' },
];
deepEqual(arr.map(d), [
'Literal: lit-1',
'Quasi: line-1\n line-2',
'Literal: lit-2',
]);
Main function of the default d
tag:
function d(arr: Token<any>[]): string;
- Stringifies tokens and autoindents interpolations.
- Then concatentates everything into a single string.
Template tag args array type:
export namespace d {
type Args<T> = [head: TemplateStringsArray, ...tail: T[]];
}
T
— interpolation type.
Template tag type:
export namespace d {
type Tag<T, U> = (...args: Args<T>) => U;
}
T
— interpolation type.U
— result type.
Better Dedent token type, either literal Lit
or interpolation
Quasi<T>
:
namespace d {
type Token<T> = Lit | Quasi<T>;
}
Literal token:
namespace d {
interface Lit {
readonly lit: true;
value: string;
}
}
lit
— type tag.value
— literal value.
Interpolation token:
namespace d {
interface Quasi<T> {
readonly lit: false;
value: T;
pad: string;
}
}
T
— interpolation type.lit
— type tag.value
— interpolation value.pad
— autoindent prefix.
Custom Better Dedent tag constructor params:
namespace d {
interface Params<T, U> {
raw?: boolean | undefined;
impl(arr: Token<T>[]): U;
}
}
T
— interpolation type.U
— result type.raw
— raw mode flag; defaults totrue
.impl
— tag implementation function.
If you have any issues, feel free to fork this repo, or contact me on Telegram https://t.me/miyaokamarina.
MIT © 2025 Yuri Zemskov