@anireact/d
Version:
Dedent templates, autoindent interpolations, and more.
292 lines (260 loc) • 7.88 kB
text/typescript
import { getRaw, scan } from './private.mjs';
/**
* Just an identity function. Can be reused to save few bytes of your bundle, huh.
*/
export const id = <t extends unknown>(x: t): t => x;
/**
* Template tag args type check. Does its best to detect actual template-tag
* calls and to filter out other similar signatures:
*
* - The {@linkcode args} array must be non-empty.
* - The head `args[0]`:
* - Must be `Array.isArray`.
* - Must be frozen.
* - Must have own `raw` prop with non-enumerable `value` descriptor.
* - The raws `args[0].raw`:
* - Must be `Array.isArray`.
* - Must be frozen.
* - Must have the same length as head.
* - The tail `args.slice(1)`:
* - Must be one item shorter than head.
*
* ```typescript
* function f(...args: BDD.Args<any> | [x: string[]]) {
* if (isTemplate(args)) {
* // Called as a tag
* } else {
* let [x] = args;
* // Called as a regular function
* }
* }
* ```
*/
export const isTemplate = <t extends d.Args<any>>(args: any[]): args is t => {
if (!args.length) return false;
let head = args[0];
if (!Array.isArray(head)) return false;
if (!('raw' in head)) return false;
let prop = Object.getOwnPropertyDescriptor(head, 'raw');
let raws = head.raw;
if (!prop) return false;
if (!('value' in prop)) return false;
if (prop.enumerable) return false;
if (!Array.isArray(raws)) return false;
if (!Object.isFrozen(head)) return false;
if (!Object.isFrozen(raws)) return false;
if (head.length !== raws.length) return false;
if (head.length !== args.length) return false;
return true;
};
/**
* The dedent tag you want:
*
* - Raw mode.
* - If the first line is blank, it’s trimmed.
* - If the last line is blank, it’s trimmed.
* - 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 are autoindented to
* match the indent of the line the interpolation is placed at. Blank lines of
* interpolation are not autoindented.
* - Completebly blank templates with no interpolations are kept untouched.
*
* _See README or tests for additional examples._
*/
export function d(...args: d.Args<any>): string;
/**
* Custom dedent tag constructor.
*
* _See the types {@linkcode d.Tag|Tag} and {@linkcode d.Params|Params}._
*
* ```typescript
* // 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(d.stringify).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),
* });
* ```
*
* @param params Tag constructor params.
* @returns Constructed dedent tag.
*/
export function d<q, z>(params: d.Params<q, z>): d.Tag<q, z>;
/**
* The default {@linkcode d} tag {@linkcode d.Params.impl|impl} function:
*
* 1. Stringifies tokens and autoindents interpolations.
* 2. Then concatentates everything into a single string.
*/
export function d(v: d.Token<any>[]): string;
/**
* Converts {@linkcode d.Token|Token} to string, autoindents interpolations:
*
* ```typescript
* let arr = [
* { lit: true, value: 'lit-1' },
* { lit: false, value: 'line-1\nline-2', pad: ' ' },
* { lit: true, value: 'lit-2' },
* ];
*
* deepEqual(arr.map(d), [
* 'lit-1',
* 'line-1\n line-2',
* 'lit-2',
* ]);
* ```
*
* @param t A token to autoindent.
* @returns Stringified and autoindented token value.
*/
export function d(t: d.Token<any>): string;
export function d(...args: d.Args<any> | [d.Params<any, any> | d.Token<any>[] | d.Token<any>]) {
// Tag call:
if (isTemplate(args)) {
return scan(args[0].raw, args.slice(1)).map(d).join('');
}
let head = args[0];
// function Dedent(v: Dedent.Token<any>[]): string;
if (Array.isArray(head)) {
return head.map(d).join('');
}
// function Dedent<q, z>(params: Dedent.Params<q, z>): Dedent.Tag<q, z>;
else if ('impl' in head) {
let { raw = true, impl } = head;
let getLit = raw ? getRaw : id;
return (...args: d.Args<any>) => impl(scan(getLit(args[0]), args.slice(1)));
}
// function Dedent(t: Dedent.Token<any>): string;
else {
let str = String(head.value);
if (head.lit) return str;
let buf = str.split('\n');
let pad = head.pad;
for (let i = 1; i < buf.length; i++) {
let line = buf[i]!;
if (line) buf[i] = pad + line;
}
return buf.join('\n');
}
}
export namespace d {
/**
* Template tag args array type.
*
* @template q Quasi type.
*/
export type Args<q> = [head: TemplateStringsArray, ...tail: q[]];
/**
* Template tag type.
*
* @template q Quasi type.
* @template z Result type.
*/
export type Tag<q, z> = (...args: Args<q>) => z;
/**
* Better Dedent token type, either literal {@linkcode Lit} or interpolation
* {@linkcode Quasi}.
*/
export type Token<q> = Lit | Quasi<q>;
/**
* Literal token.
*/
export interface Lit {
/** Type tag. */ readonly lit: true;
/** Literal value. */ value: string;
}
/**
* Quasi token.
*
* @template q Quasi type.
*/
export interface Quasi<q> {
/** Type tag. */ readonly lit: false;
/** Quasi value. */ value: q;
/** Autoindent prefix. */ pad: string;
}
/**
* Custom Better Dedent tag {@link d|constructor} params.
*
* @template q Quasi type.
* @template z Result type.
*/
export interface Params<q, z> {
/** Raw mode flag; defaults to `true`. */ raw?: boolean | undefined;
/** Tag implementation function. */ impl(v: Token<q>[]): z;
}
/**
* Converts {@linkcode Token} value using `String()`.
*
* ```typescript
* 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]';
* ```
*/
export const stringify = (t: d.Token<any>): string => String(t.value);
/**
* Similar to the {@linkcode d} tag, but returns an array of
* {@linkcode Token} objects, with no autoindent applied to interpolation
* tokens:
*
* ```typescript
* deepEqual(
* d.tokenize`¶
* ␣␣␣␣␣␣␣␣Literal¶
* ␣␣␣␣␣␣␣␣␣␣␣␣${'Interpolation'}¶
* ␣␣␣␣␣␣␣␣Another literal¶
* ␣␣␣␣`,
* [
* { lit: true, value: 'Literal\n ' },
* { lit: false, value: 'Interpolation', pad: ' ' },
* { lit: true, value: '\nAnother literal' },
* ],
* );
* ```
*/
export const tokenize = <q extends unknown>(head: TemplateStringsArray, ...tail: q[]): Token<q>[] => {
return scan(head.raw, tail);
};
}