Skip to content

anireact/d

Repository files navigation

Better Dedent

Dedent templates, autoindent interpolations, and more. With edge cases handled the right way. Primarily intended for code generation.

Usage

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>
`;

API

d

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:
    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:¶
    ␣␣`;
    The result:
    ␣␣␣␣↑ 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:
    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. «`;
    The result:
    ␣␣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:
    d`␣␣¶
    ␣␣␣␣¶
    ␣␣␣␣␣␣¶
    ␣␣␣␣¶
    ␣␣`;
    The result:
    ␣␣¶
    ␣␣␣␣¶
    ␣␣␣␣␣␣¶
    ␣␣␣␣¶
    ␣␣
    
  • Not actually blank — dedented as usual:
    d`␣␣␣␣␣␣¶
    ␣␣${''}␣␣␣␣¶
    
    ␣␣␣␣␣␣␣␣¶
    ␣␣␣␣␣␣`;
    The result:
    ␣␣␣␣¶
    ¶
    ␣␣␣␣␣␣
    

See the tests for more edge case examples.

d.tokenize

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' },
    ],
);

d(params)

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),
});

isTemplate()

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-enumerable value descriptor.
  • The raws x[0].raw:
    • Must be Array.isArray.
    • Must be frozen.
    • Must have the same length as head.
  • 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
    }
}

id()

Just an identity function. Can be reused to save few bytes of your bundle, huh.

function id<T>(x: T): T;

d.stringify()

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]';

d(tok)

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',
]);

d(arr)

Main function of the default d tag:

function d(arr: Token<any>[]): string;
  1. Stringifies tokens and autoindents interpolations.
  2. Then concatentates everything into a single string.

Types

d.Args<t>

Template tag args array type:

export namespace d {
    type Args<T> = [head: TemplateStringsArray, ...tail: T[]];
}
  • T — interpolation type.

d.Tag<T,U>

Template tag type:

export namespace d {
    type Tag<T, U> = (...args: Args<T>) => U;
}
  • T — interpolation type.
  • U — result type.

d.Token<T>

Better Dedent token type, either literal Lit or interpolation Quasi<T>:

namespace d {
    type Token<T> = Lit | Quasi<T>;
}

d.Lit

Literal token:

namespace d {
    interface Lit {
        readonly lit: true;
        value: string;
    }
}
  • lit — type tag.
  • value — literal value.

d.Quasi<T>

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.

d.Params<T,U>

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 to true.
  • impl — tag implementation function.

Support

If you have any issues, feel free to fork this repo, or contact me on Telegram https://t.me/miyaokamarina.

License

MIT © 2025 Yuri Zemskov

About

Dedent templates, autoindent interpolations, and more.

Resources

License

Stars

Watchers

Forks

Packages

No packages published