UNPKG

@anireact/d

Version:

Dedent templates, autoindent interpolations, and more.

292 lines (260 loc) 7.88 kB
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); }; }