Last active
September 21, 2020 07:40
-
-
Save disco0/447ba916128d5fad0f1e43676b48f8de to your computer and use it in GitHub Desktop.
typescript-template-string-reduce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"readmeBehavior": "previewFooter", | |
"scriptType": "module", | |
"showConsole": true, | |
"scripts": [ | |
], | |
"layout": "preview" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///<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, "&") | |
.replace(/</g, "<") | |
.replace(/>/g, ">"); | |
const xmlEscapeMap = <S extends string>(value: S, index: number, arr: Array<S>) => | |
value.replace(/&/g, "&") | |
.replace(/</g, "<") | |
.replace(/>/g, ">"); | |
export const templateXML = (base: TemplateStringsArray, ...values: any[]): string => | |
reduceTemplateString( | |
templateStringArrayMap(base, xmlEscape), values, xmlEscape) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///<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