Skip to content

Instantly share code, notes, and snippets.

@disco0
Last active September 21, 2020 07:40
Show Gist options
  • Save disco0/447ba916128d5fad0f1e43676b48f8de to your computer and use it in GitHub Desktop.
Save disco0/447ba916128d5fad0f1e43676b48f8de to your computer and use it in GitHub Desktop.
typescript-template-string-reduce
{
"readmeBehavior": "previewFooter",
"scriptType": "module",
"showConsole": true,
"scripts": [
],
"layout": "preview"
}
///<reference lib="esnext"/>
//#region util
type Identity = typeof Identity;
const Identity = <V>(value: V): V => value;
type ToString = typeof asString;
const asString = (value: any): string => String(value);
/**
* Merge values of two arrays with additional transform function, if supplied
*/
function zipArray<T1, T2, R1 extends (value: T1) => any, R2 extends (value: T2) => any>(
arr1: Array<T1>,
arr2: Array<T2>,
fn1?: R1,
fn2?: R2
) {
fn1 ??= (value: T1) => value;
fn2 ??= (value: T2) => value;
return arr1.flatMap((item: T1, index: number) => [...[fn1(item), fn2(arr2[index])]]);
}
//#endregion util
//#region Types
type TA = TemplateStringsArray;
export type StringOrTemplateStringParams =
[String: TemplateStringsArray, ...Values: Array<any>]
| [String: string];
export interface StringOrTemplateStringFunction
{
(...args: StringOrTemplateStringParams): string
}
type Transformer = ToString
interface TransformerObject
{
text?: Transformer;
value?: Transformer;
}
function isTransformObject(obj: any): obj is TransformerObject
{
// obj is TransformerObject if:
// - type Object,
// - Any keys defined match 'text' | 'value', their values are functions
return (typeof obj === 'object')
? (Object.keys(obj).filter(key =>
( !['text', 'value'].includes(key)) || typeof obj[key] !== 'function').length !== 0)
: false
}
type TransformerTuple =
[
text: Transformer,
value: Transformer
]
type TransformerArg = Transformer | TransformerObject | TransformerTuple
//#endregion Types
/**
* Takes arguments from a template string function and returns the concatenated
* form. Default return type is string, but can be manually specified.
* @param base
* First argument of TemplateStringsArray from template string function.
* @param values
* Values received in template string function after `base`, passed as a
* single array of remaining parameters from template string function
* arguments, like so:
* ``` ts
* let exampleTemplate = (base, ...values: any[]) => reduceTemplateString(base, values)
* ```
* @param valueTransformer
* A function that receives each element from `values` with any required
* additional processing.
*/
function reduceTemplateString<V>(base: TA, values: V[], valueTransformers?: Transformer): string;
function reduceTemplateString<V>(base: TA, values: V[], valueTransformers: TransformerTuple | TransformerObject): string;
function reduceTemplateString<V>(base: TA, values: V[], tsArg1?: TransformerArg, tsArg2?: Transformer): string
{
// If no values
const isBasic: boolean = (base.length === 1 && values.length === 0);
let stringTransform: Transformer, valueTransform: Transformer;
//#region Overload parse
/**
* Any failures to match will be caught by ??= assignments below
*/
// If no args
if(!tsArg1)
{
/**
* Catch second transform in edge cases like this:
*``` ts
* const undefinedStringTransform = undefined; // But more subtle
* reduceTemplateString(
* base, values, undefinedStringTransform, (value) => value.trim());
*```
*/
if(tsArg2)
valueTransform = tsArg2;
}
// Check if object passed (e.g. {text: fn, value: fn})
else if (isTransformObject(tsArg1))
{
if(tsArg1.text) stringTransform = tsArg1.text;
if(tsArg1.value) valueTransform = tsArg1.value;
}
// Treat as partial tuple
else
{
// Possible to be more restrictive?
if(typeof tsArg1 === 'function')
stringTransform = tsArg1;
// Possible to be more restrictive?
if(typeof tsArg2 === 'function')
valueTransform = tsArg2;
}
stringTransform ??= asString;
valueTransform ??= asString;
let target = base[0];
return [
base[0],
...zipArray<string, string>(base.slice(1), values, stringTransform, valueTransform)
].join('')
// values.forEach((value, index) => {
// target += valueTransformer(value)
// target += base[index];
// });
return target;
}
/**
*
* @param values
* @param mapper
* Function passed into {@link Array#map | map method}
*/
function templateStringArrayMap(values: TemplateStringsArray, mapper: ToString)
{
const newValues = values.map(mapper);
return Object.assign(newValues, {raw: values.raw})
}
const xmlEscape = (value: string) =>
value.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const xmlEscapeMap = <S extends string>(value: S, index: number, arr: Array<S>) =>
value.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
export const templateXML = (base: TemplateStringsArray, ...values: any[]): string =>
reduceTemplateString(
templateStringArrayMap(base, xmlEscape), values, xmlEscape)
///<reference lib="es2019.array"/>
//#region Util
const Identity = <V>(value: V): V => value;
type Identity = typeof Identity;
const asString = (value: any): string => String(value);
type ToString = (value: any) => string;
type ZipFunction<V = any, R = any> = (value: V) => R
/**
* Merge values of two arrays with additional transform function, if supplied
*/
function zipArray<T1, T2>(
arr1: Array<T1>,
arr2: Array<T2>,
fn1: ZipFunction<T1> = (value: T1) => value,
fn2: ZipFunction<T2> = (value: T2) => value
) {
return arr1.flatMap((item: T1, index: number) => [...[fn1!(item), fn2!(arr2[index])]]);
}
//#endregion Util
//#region Types
type TA = TemplateStringsArray;
export type StringOrTemplateStringParams =
[String: TemplateStringsArray, ...Values: Array<any>]
| [String: string];
export interface StringOrTemplateStringFunction
{
(...args: StringOrTemplateStringParams): string
}
type Transformer = (value: any) => string
interface TransformerObject
{
strings?: Transformer;
values?: Transformer;
}
type TransformerTuple =
[
strings: Transformer,
values?: Transformer
] | [ ]
type TransformerArg = Transformer | TransformerObject | TransformerTuple
//#endregion Types
/**
* Takes arguments from a template string function and returns the concatenated
* form. Default return type is string, but can be manually specified.
* @param base
* First argument of TemplateStringsArray from template string function.
* @param values
* Values received in template string function after `base`, passed as a
* single array of remaining parameters from template string function
* arguments, like so:
* ``` ts
* let exampleTemplate = (base, ...values: any[]) => reduceTemplateString(base, values)
* ```
* @param valueTransformer
* A function that receives each element from `values` with any required
* additional processing.
*/
export function reduceTemplateString<V>(strings: TA, values: V[], valueTransformer?: TransformerArg): string;
export function reduceTemplateString<V>(strings: TA, values: V[], valueTransformer?: TransformerTuple): string;
export function reduceTemplateString<V>(strings: TA, values: V[], valueTransformer?: TransformerObject): string;
export function reduceTemplateString<V>(strings: TA, values: V[], ...valueTransformers: [TransformerArg] | [...TransformerTuple] | [] | any[]): string
{
const transformers: {
strings: Transformer;
values: Transformer;
} = { strings: asString, values: asString };
// Collapse overloads
if(valueTransformers.length === 0) { }
else if(valueTransformers.length === 1)
{
// Shift context into first rest param only
const arg = valueTransformers[0];
// Overload 1: valueTransformer?: Transformer
if(typeof arg === 'function')
{
// Single transformer passed? Use as value transformer
transformers.values = arg;
}
// Overload 2: valueTransformer?: TransformerTuple
else if(Array.isArray(arg))
{
const [strings, values] = arg;
// TODO: Maybe dont allow holes?
if(typeof strings === 'function')
transformers.strings = strings;
if(typeof values === 'function')
transformers.values = values;
}
// Overload 3: valueTransformer?: TransformerObject
else if(typeof arg === 'object')
{
Object.entries(arg).forEach(([key, value]: ['strings' | 'values' | string, any]) => {
if(!(
(key === 'strings' || key === 'values') && typeof value === 'function'
)) return
transformers[key] = value;
})
}
}
// Overload 4: Spread TransformerTuple
else if(valueTransformers.length === 2)
{
const [strings, values] = valueTransformers.slice(0, 2);
// TODO: Maybe dont allow holes?
if(typeof strings === 'function')
transformers.strings = strings;
if(typeof values === 'function')
transformers.values = values;
}
let target = transformers.strings(strings[0]);
values.forEach((value, index) => {
target += transformers.values(value)
target += transformers.strings(strings[index]);
});
return target;
}
/**
* Applies map function to template string values, and redefines `raw` property
* to avoid disturbing the typechecker
*
* @param values
* Values from `TemplateStringsArray` typed (and first) argument received
* in a template string function.
*
* @param mapper
* Function passed into `Array#map`
*/
function templateStringArrayMap(values: TemplateStringsArray, mapper: ToString)
{
const newValues = values.map(mapper);
return Object.assign(newValues, {raw: values.raw})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment