From c7bbe2d06242d8ec026b3e209e6e5d2fc5efe3be Mon Sep 17 00:00:00 2001 From: JD Solanki Date: Fri, 30 Jun 2023 16:31:11 +0530 Subject: [PATCH] feat(template): support object style templates (#34) Co-authored-by: Anthony Fu --- src/string.test.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++ src/string.ts | 40 +++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/string.test.ts b/src/string.test.ts index 6b5cc64..fdc89de 100644 --- a/src/string.test.ts +++ b/src/string.test.ts @@ -15,6 +15,7 @@ it('template', () => { '{0} + {1} = {2}{3}', 1, '1', + // @ts-expect-error disallow non-literal on type { v: 2 }, [2, 3], ), @@ -34,6 +35,67 @@ it('template', () => { ).toEqual('Hi') }) +it('namedTemplate', () => { + expect( + template( + '{greet}! My name is {name}.', + { greet: 'Hello', name: 'Anthony' }, + ), + ).toEqual('Hello! My name is Anthony.') + + expect( + template( + '{a} + {b} = {result}', + { a: 1, b: 2, result: 3 }, + ), + ).toEqual('1 + 2 = 3') + + expect( + template( + '{1} + {b} = 3', + { 1: 'a', b: 2 }, + ), + ).toEqual('a + 2 = 3') + + // Without fallback return the variable name + expect( + template( + '{10}', + {}, + ), + ).toEqual('10') + + expect( + template( + '{11}', + null, + ), + ).toEqual('undefined') + + expect( + template( + '{11}', + undefined, + ), + ).toEqual('undefined') + + expect( + template( + '{10}', + {}, + 'unknown', + ), + ).toEqual('unknown') + + expect( + template( + '{1} {2} {3} {4}', + { 4: 'known key' }, + k => String(+k * 2), + ), + ).toEqual('2 4 6 known key') +}) + it('slash', () => { expect(slash('\\123')).toEqual('/123') expect(slash('\\\\')).toEqual('//') diff --git a/src/string.ts b/src/string.ts index 971a513..d8274be 100644 --- a/src/string.ts +++ b/src/string.ts @@ -1,3 +1,5 @@ +import { isObject } from './is' + /** * Replace backslash to slash * @@ -31,6 +33,8 @@ export function ensureSuffix(suffix: string, str: string) { /** * Dead simple template engine, just like Python's `.format()` + * Support passing variables as either in index based or object/name based approach + * While using object/name based approach, you can pass a fallback value as the third argument * * @category String * @example @@ -41,14 +45,38 @@ export function ensureSuffix(suffix: string, str: string) { * 'Anthony' * ) // Hello Inès! My name is Anthony. * ``` + * +* ``` + * const result = namedTemplate( + * '{greet}! My name is {name}.', + * { greet: 'Hello', name: 'Anthony' } + * ) // Hello! My name is Anthony. + * ``` + * + * * const result = namedTemplate( + * '{greet}! My name is {name}.', + * { greet: 'Hello' }, // name isn't passed hence fallback will be used for name + * 'placeholder' + * ) // Hello! My name is placeholder. + * ``` */ +export function template(str: string, object: Record, fallback?: string | ((key: string) => string)): string +export function template(str: string, ...args: (string | number | BigInt | undefined | null)[]): string export function template(str: string, ...args: any[]): string { - return str.replace(/{(\d+)}/g, (match, key) => { - const index = Number(key) - if (Number.isNaN(index)) - return match - return args[index] - }) + const [firstArg, fallback] = args + + if (isObject(firstArg)) { + const vars = firstArg as Record + return str.replace(/{([\w\d]+)}/g, (_, key) => vars[key] || ((typeof fallback === 'function' ? fallback(key) : fallback) ?? key)) + } + else { + return str.replace(/{(\d+)}/g, (_, key) => { + const index = Number(key) + if (Number.isNaN(index)) + return key + return args[index] + }) + } } // port from nanoid