From aa10ed0f13e45fdb0841c804d75792d92549491a Mon Sep 17 00:00:00 2001 From: robonen Date: Wed, 1 May 2024 01:22:05 +0700 Subject: [PATCH 01/25] feat(docs): update Vitepress config to include @robonen/renovate and @robonen/stdlib packages --- .vitepress/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index ec8bcc8..c590284 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -5,7 +5,7 @@ export default defineConfig({ title: "Tools", description: "A set of tools and utilities for web development", rewrites: { - 'packages/:pkg/README.md': 'packages/:pkg/index.md' + 'packages/:pkg/README.md': 'packages/:pkg/index.md', }, themeConfig: { sidebar: [ @@ -13,6 +13,8 @@ export default defineConfig({ text: 'Пакеты', items: [ { text: '@robonen/tsconfig', link: '/packages/tsconfig/' }, + { text: '@robonen/renovate', link: '/packages/renovate/' }, + { text: '@robonen/stdlib', link: '/packages/stdlib/' }, ], }, ], From 8a33f6945cbc17861b037229f3a51481463d1f1e Mon Sep 17 00:00:00 2001 From: robonen Date: Wed, 1 May 2024 01:23:07 +0700 Subject: [PATCH 02/25] chore(packages/renovate): mark package.json private to true --- packages/renovate/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/renovate/package.json b/packages/renovate/package.json index 03c194c..a73da85 100644 --- a/packages/renovate/package.json +++ b/packages/renovate/package.json @@ -1,6 +1,7 @@ { "name": "@robonen/renovate", "version": "0.0.1", + "private": true, "license": "UNLICENSED", "description": "Base configuration for renovate bot", "keywords": [ From 7091352be23c4eff3a1fc6894f0ef78c0dd08b85 Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 2 May 2024 06:53:46 +0700 Subject: [PATCH 03/25] chore(packages/stdlib): update import statement to use single quotes in mapRange function, lowercase in test desciptions --- packages/stdlib/src/index.ts | 4 +++- packages/stdlib/src/math/mapRange/index.ts | 2 +- packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts | 2 +- packages/stdlib/src/structs/stack/index.test.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/stdlib/src/index.ts b/packages/stdlib/src/index.ts index e076937..58694d8 100644 --- a/packages/stdlib/src/index.ts +++ b/packages/stdlib/src/index.ts @@ -2,4 +2,6 @@ export * from './text'; export * from './math'; export * from './patterns'; export * from './bits'; -export * from './structs'; \ No newline at end of file +export * from './structs'; +export * from './arrays'; +export * from './types'; diff --git a/packages/stdlib/src/math/mapRange/index.ts b/packages/stdlib/src/math/mapRange/index.ts index f82dd41..e345155 100644 --- a/packages/stdlib/src/math/mapRange/index.ts +++ b/packages/stdlib/src/math/mapRange/index.ts @@ -1,4 +1,4 @@ -import { clamp } from "../clamp"; +import { clamp } from '../clamp'; /** * Map a value from one range to another diff --git a/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts b/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts index 629e23c..8a67b57 100644 --- a/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts +++ b/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PubSub } from './index'; -describe('PubSub', () => { +describe('pubsub', () => { let eventBus: PubSub<{ event1: (arg: string) => void; event2: () => void diff --git a/packages/stdlib/src/structs/stack/index.test.ts b/packages/stdlib/src/structs/stack/index.test.ts index b66dab2..748cabe 100644 --- a/packages/stdlib/src/structs/stack/index.test.ts +++ b/packages/stdlib/src/structs/stack/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { Stack } from './index'; -describe('Stack', () => { +describe('stack', () => { describe('constructor', () => { it('create an empty stack if no initial values are provided', () => { const stack = new Stack(); From 6931fc6f1861dddaf3d9cb5806aa9f064766c9f8 Mon Sep 17 00:00:00 2001 From: robonen Date: Fri, 3 May 2024 06:57:46 +0700 Subject: [PATCH 04/25] chore(packages/stdlib): add ts and js utility types --- packages/stdlib/src/types/index.ts | 2 + packages/stdlib/src/types/js/casts.test.ts | 30 +++ packages/stdlib/src/types/js/casts.ts | 7 + packages/stdlib/src/types/js/complex.test.ts | 183 ++++++++++++++++++ packages/stdlib/src/types/js/complex.ts | 81 ++++++++ packages/stdlib/src/types/js/index.ts | 3 + .../stdlib/src/types/js/primitives.test.ts | 101 ++++++++++ packages/stdlib/src/types/js/primitives.ts | 65 +++++++ packages/stdlib/src/types/ts/index.ts | 1 + packages/stdlib/src/types/ts/string.test-d.ts | 43 ++++ packages/stdlib/src/types/ts/string.ts | 9 + 11 files changed, 525 insertions(+) create mode 100644 packages/stdlib/src/types/index.ts create mode 100644 packages/stdlib/src/types/js/casts.test.ts create mode 100644 packages/stdlib/src/types/js/casts.ts create mode 100644 packages/stdlib/src/types/js/complex.test.ts create mode 100644 packages/stdlib/src/types/js/complex.ts create mode 100644 packages/stdlib/src/types/js/index.ts create mode 100644 packages/stdlib/src/types/js/primitives.test.ts create mode 100644 packages/stdlib/src/types/js/primitives.ts create mode 100644 packages/stdlib/src/types/ts/index.ts create mode 100644 packages/stdlib/src/types/ts/string.test-d.ts create mode 100644 packages/stdlib/src/types/ts/string.ts diff --git a/packages/stdlib/src/types/index.ts b/packages/stdlib/src/types/index.ts new file mode 100644 index 0000000..25d7c6a --- /dev/null +++ b/packages/stdlib/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './js'; +export * from './ts'; \ No newline at end of file diff --git a/packages/stdlib/src/types/js/casts.test.ts b/packages/stdlib/src/types/js/casts.test.ts new file mode 100644 index 0000000..568a9c2 --- /dev/null +++ b/packages/stdlib/src/types/js/casts.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { toString } from './casts'; + +describe('casts', () => { + describe('toString', () => { + it('correct string representation of a value', () => { + // Primitives + expect(toString(true)).toBe('[object Boolean]'); + expect(toString(() => {})).toBe('[object Function]'); + expect(toString(5)).toBe('[object Number]'); + expect(toString(BigInt(5))).toBe('[object BigInt]'); + expect(toString('hello')).toBe('[object String]'); + expect(toString(Symbol('foo'))).toBe('[object Symbol]'); + + // Complex + expect(toString([])).toBe('[object Array]'); + expect(toString({})).toBe('[object Object]'); + expect(toString(undefined)).toBe('[object Undefined]'); + expect(toString(null)).toBe('[object Null]'); + expect(toString(/abc/)).toBe('[object RegExp]'); + expect(toString(new Date())).toBe('[object Date]'); + expect(toString(new Error())).toBe('[object Error]'); + expect(toString(new Promise(() => {}))).toBe('[object Promise]'); + expect(toString(new Map())).toBe('[object Map]'); + expect(toString(new Set())).toBe('[object Set]'); + expect(toString(new WeakMap())).toBe('[object WeakMap]'); + expect(toString(new WeakSet())).toBe('[object WeakSet]'); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/types/js/casts.ts b/packages/stdlib/src/types/js/casts.ts new file mode 100644 index 0000000..6190d84 --- /dev/null +++ b/packages/stdlib/src/types/js/casts.ts @@ -0,0 +1,7 @@ +/** + * To string any value. + * + * @param {any} value + * @returns {string} + */ +export const toString = (value: any): string => Object.prototype.toString.call(value); \ No newline at end of file diff --git a/packages/stdlib/src/types/js/complex.test.ts b/packages/stdlib/src/types/js/complex.test.ts new file mode 100644 index 0000000..ceff211 --- /dev/null +++ b/packages/stdlib/src/types/js/complex.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from 'vitest'; +import { isArray, isObject, isRegExp, isDate, isError, isPromise, isMap, isSet, isWeakMap, isWeakSet } from './complex'; + +describe('complex', () => { + describe('isArray', () => { + it('true if the value is an array', () => { + expect(isArray([])).toBe(true); + expect(isArray([1, 2, 3])).toBe(true); + }); + + it('false if the value is not an array', () => { + expect(isArray('')).toBe(false); + expect(isArray(123)).toBe(false); + expect(isArray(true)).toBe(false); + expect(isArray(null)).toBe(false); + expect(isArray(undefined)).toBe(false); + expect(isArray({})).toBe(false); + expect(isArray(new Map())).toBe(false); + expect(isArray(new Set())).toBe(false); + }); + }); + + describe('isObject', () => { + it('true if the value is an object', () => { + expect(isObject({})).toBe(true); + expect(isObject({ key: 'value' })).toBe(true); + }); + + it('false if the value is not an object', () => { + expect(isObject('')).toBe(false); + expect(isObject(123)).toBe(false); + expect(isObject(true)).toBe(false); + expect(isObject(null)).toBe(false); + expect(isObject(undefined)).toBe(false); + expect(isObject([])).toBe(false); + expect(isObject(new Map())).toBe(false); + expect(isObject(new Set())).toBe(false); + }); + }); + + describe('isRegExp', () => { + it('true if the value is a regexp', () => { + expect(isRegExp(/test/)).toBe(true); + expect(isRegExp(new RegExp('test'))).toBe(true); + }); + + it('false if the value is not a regexp', () => { + expect(isRegExp('')).toBe(false); + expect(isRegExp(123)).toBe(false); + expect(isRegExp(true)).toBe(false); + expect(isRegExp(null)).toBe(false); + expect(isRegExp(undefined)).toBe(false); + expect(isRegExp([])).toBe(false); + expect(isRegExp({})).toBe(false); + expect(isRegExp(new Map())).toBe(false); + expect(isRegExp(new Set())).toBe(false); + }); + }); + + describe('isDate', () => { + it('true if the value is a date', () => { + expect(isDate(new Date())).toBe(true); + }); + + it('false if the value is not a date', () => { + expect(isDate('')).toBe(false); + expect(isDate(123)).toBe(false); + expect(isDate(true)).toBe(false); + expect(isDate(null)).toBe(false); + expect(isDate(undefined)).toBe(false); + expect(isDate([])).toBe(false); + expect(isDate({})).toBe(false); + expect(isDate(new Map())).toBe(false); + expect(isDate(new Set())).toBe(false); + }); + }); + + describe('isError', () => { + it('true if the value is an error', () => { + expect(isError(new Error())).toBe(true); + }); + + it('false if the value is not an error', () => { + expect(isError('')).toBe(false); + expect(isError(123)).toBe(false); + expect(isError(true)).toBe(false); + expect(isError(null)).toBe(false); + expect(isError(undefined)).toBe(false); + expect(isError([])).toBe(false); + expect(isError({})).toBe(false); + expect(isError(new Map())).toBe(false); + expect(isError(new Set())).toBe(false); + }); + }); + + describe('isPromise', () => { + it('true if the value is a promise', () => { + expect(isPromise(new Promise(() => {}))).toBe(true); + }); + + it('false if the value is not a promise', () => { + expect(isPromise('')).toBe(false); + expect(isPromise(123)).toBe(false); + expect(isPromise(true)).toBe(false); + expect(isPromise(null)).toBe(false); + expect(isPromise(undefined)).toBe(false); + expect(isPromise([])).toBe(false); + expect(isPromise({})).toBe(false); + expect(isPromise(new Map())).toBe(false); + expect(isPromise(new Set())).toBe(false); + }); + }); + + describe('isMap', () => { + it('true if the value is a map', () => { + expect(isMap(new Map())).toBe(true); + }); + + it('false if the value is not a map', () => { + expect(isMap('')).toBe(false); + expect(isMap(123)).toBe(false); + expect(isMap(true)).toBe(false); + expect(isMap(null)).toBe(false); + expect(isMap(undefined)).toBe(false); + expect(isMap([])).toBe(false); + expect(isMap({})).toBe(false); + expect(isMap(new Set())).toBe(false); + }); + }); + + describe('isSet', () => { + it('true if the value is a set', () => { + expect(isSet(new Set())).toBe(true); + }); + + it('false if the value is not a set', () => { + expect(isSet('')).toBe(false); + expect(isSet(123)).toBe(false); + expect(isSet(true)).toBe(false); + expect(isSet(null)).toBe(false); + expect(isSet(undefined)).toBe(false); + expect(isSet([])).toBe(false); + expect(isSet({})).toBe(false); + expect(isSet(new Map())).toBe(false); + }); + }); + + describe('isWeakMap', () => { + it('true if the value is a weakmap', () => { + expect(isWeakMap(new WeakMap())).toBe(true); + }); + + it('false if the value is not a weakmap', () => { + expect(isWeakMap('')).toBe(false); + expect(isWeakMap(123)).toBe(false); + expect(isWeakMap(true)).toBe(false); + expect(isWeakMap(null)).toBe(false); + expect(isWeakMap(undefined)).toBe(false); + expect(isWeakMap([])).toBe(false); + expect(isWeakMap({})).toBe(false); + expect(isWeakMap(new Map())).toBe(false); + expect(isWeakMap(new Set())).toBe(false); + }); + }); + + describe('isWeakSet', () => { + it('true if the value is a weakset', () => { + expect(isWeakSet(new WeakSet())).toBe(true); + }); + + it('false if the value is not a weakset', () => { + expect(isWeakSet('')).toBe(false); + expect(isWeakSet(123)).toBe(false); + expect(isWeakSet(true)).toBe(false); + expect(isWeakSet(null)).toBe(false); + expect(isWeakSet(undefined)).toBe(false); + expect(isWeakSet([])).toBe(false); + expect(isWeakSet({})).toBe(false); + expect(isWeakSet(new Map())).toBe(false); + expect(isWeakSet(new Set())).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/types/js/complex.ts b/packages/stdlib/src/types/js/complex.ts new file mode 100644 index 0000000..94e4eb0 --- /dev/null +++ b/packages/stdlib/src/types/js/complex.ts @@ -0,0 +1,81 @@ +import { toString } from '.'; + +/** + * Check if a value is an array. + * + * @param {any} value + * @returns {value is any[]} + */ +export const isArray = (value: any): value is any[] => Array.isArray(value); + +/** + * Check if a value is an object. + * + * @param {any} value + * @returns {value is object} + */ +export const isObject = (value: any): value is object => toString(value) === '[object Object]'; + +/** + * Check if a value is a regexp. + * + * @param {any} value + * @returns {value is RegExp} + */ +export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]'; + +/** + * Check if a value is a date. + * + * @param {any} value + * @returns {value is Date} + */ +export const isDate = (value: any): value is Date => toString(value) === '[object Date]'; + +/** + * Check if a value is an error. + * + * @param {any} value + * @returns {value is Error} + */ +export const isError = (value: any): value is Error => toString(value) === '[object Error]'; + +/** + * Check if a value is a promise. + * + * @param {any} value + * @returns {value is Promise} + */ +export const isPromise = (value: any): value is Promise => toString(value) === '[object Promise]'; + +/** + * Check if a value is a map. + * + * @param {any} value + * @returns {value is Map} + */ +export const isMap = (value: any): value is Map => toString(value) === '[object Map]'; + +/** + * Check if a value is a set. + * + * @param {any} value + * @returns {value is Set} + */ +export const isSet = (value: any): value is Set => toString(value) === '[object Set]'; + +/** + * Check if a value is a weakmap. + * + * @param {any} value + * @returns {value is WeakMap} + */ +export const isWeakMap = (value: any): value is WeakMap => toString(value) === '[object WeakMap]'; + +/** + * Check if a value is a weakset. + * + * @param {any} value + * @returns {value is WeakSet} + */ +export const isWeakSet = (value: any): value is WeakSet => toString(value) === '[object WeakSet]'; diff --git a/packages/stdlib/src/types/js/index.ts b/packages/stdlib/src/types/js/index.ts new file mode 100644 index 0000000..70d8433 --- /dev/null +++ b/packages/stdlib/src/types/js/index.ts @@ -0,0 +1,3 @@ +export * from './casts'; +export * from './primitives'; +export * from './complex'; \ No newline at end of file diff --git a/packages/stdlib/src/types/js/primitives.test.ts b/packages/stdlib/src/types/js/primitives.test.ts new file mode 100644 index 0000000..799aa59 --- /dev/null +++ b/packages/stdlib/src/types/js/primitives.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { isBoolean, isFunction, isNumber, isBigInt, isString, isSymbol, isUndefined, isNull } from './primitives'; + +describe('primitives', () => { + describe('isBoolean', () => { + it('true if the value is a boolean', () => { + expect(isBoolean(true)).toBe(true); + expect(isBoolean(false)).toBe(true); + }); + + it('false if the value is not a boolean', () => { + expect(isBoolean(0)).toBe(false); + expect(isBoolean('true')).toBe(false); + expect(isBoolean(null)).toBe(false); + }); + }); + + describe('isFunction', () => { + it('true if the value is a function', () => { + expect(isFunction(() => {})).toBe(true); + expect(isFunction(function() {})).toBe(true); + }); + + it('false if the value is not a function', () => { + expect(isFunction(123)).toBe(false); + expect(isFunction('function')).toBe(false); + expect(isFunction(null)).toBe(false); + }); + }); + + describe('isNumber', () => { + it('true if the value is a number', () => { + expect(isNumber(123)).toBe(true); + expect(isNumber(3.14)).toBe(true); + }); + + it('false if the value is not a number', () => { + expect(isNumber('123')).toBe(false); + expect(isNumber(null)).toBe(false); + }); + }); + + describe('isBigInt', () => { + it('true if the value is a bigint', () => { + expect(isBigInt(BigInt(123))).toBe(true); + }); + + it('false if the value is not a bigint', () => { + expect(isBigInt(123)).toBe(false); + expect(isBigInt('123')).toBe(false); + expect(isBigInt(null)).toBe(false); + }); + }); + + describe('isString', () => { + it('true if the value is a string', () => { + expect(isString('hello')).toBe(true); + }); + + it('false if the value is not a string', () => { + expect(isString(123)).toBe(false); + expect(isString(null)).toBe(false); + }); + }); + + describe('isSymbol', () => { + it('true if the value is a symbol', () => { + expect(isSymbol(Symbol())).toBe(true); + }); + + it('false if the value is not a symbol', () => { + expect(isSymbol(123)).toBe(false); + expect(isSymbol('symbol')).toBe(false); + expect(isSymbol(null)).toBe(false); + }); + }); + + describe('isUndefined', () => { + it('true if the value is undefined', () => { + expect(isUndefined(undefined)).toBe(true); + }); + + it('false if the value is not undefined', () => { + expect(isUndefined(null)).toBe(false); + expect(isUndefined(123)).toBe(false); + expect(isUndefined('undefined')).toBe(false); + }); + }); + + describe('isNull', () => { + it('true if the value is null', () => { + expect(isNull(null)).toBe(true); + }); + + it('false if the value is not null', () => { + expect(isNull(undefined)).toBe(false); + expect(isNull(123)).toBe(false); + expect(isNull('null')).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/types/js/primitives.ts b/packages/stdlib/src/types/js/primitives.ts new file mode 100644 index 0000000..25958f2 --- /dev/null +++ b/packages/stdlib/src/types/js/primitives.ts @@ -0,0 +1,65 @@ +import { toString } from '.'; + +/** + * Check if a value is a boolean. + * + * @param {any} value + * @returns {value is boolean} + */ +export const isBoolean = (value: any): value is boolean => typeof value === 'boolean'; + +/** + * Check if a value is a function. + * + * @param {any} value + * @returns {value is Function} + */ +export const isFunction = (value: any): value is T => typeof value === 'function'; + +/** + * Check if a value is a number. + * + * @param {any} value + * @returns {value is number} + */ +export const isNumber = (value: any): value is number => typeof value === 'number'; + +/** + * Check if a value is a bigint. + * + * @param {any} value + * @returns {value is bigint} + */ +export const isBigInt = (value: any): value is bigint => typeof value === 'bigint'; + +/** + * Check if a value is a string. + * + * @param {any} value + * @returns {value is string} + */ +export const isString = (value: any): value is string => typeof value === 'string'; + +/** + * Check if a value is a symbol. + * + * @param {any} value + * @returns {value is symbol} + */ +export const isSymbol = (value: any): value is symbol => typeof value === 'symbol'; + +/** + * Check if a value is a undefined. + * + * @param {any} value + * @returns {value is undefined} + */ +export const isUndefined = (value: any): value is undefined => toString(value) === '[object Undefined]'; + +/** + * Check if a value is a null. + * + * @param {any} value + * @returns {value is null} + */ +export const isNull = (value: any): value is null => toString(value) === '[object Null]'; diff --git a/packages/stdlib/src/types/ts/index.ts b/packages/stdlib/src/types/ts/index.ts new file mode 100644 index 0000000..961e79e --- /dev/null +++ b/packages/stdlib/src/types/ts/index.ts @@ -0,0 +1 @@ +export * from './string'; \ No newline at end of file diff --git a/packages/stdlib/src/types/ts/string.test-d.ts b/packages/stdlib/src/types/ts/string.test-d.ts new file mode 100644 index 0000000..88d016d --- /dev/null +++ b/packages/stdlib/src/types/ts/string.test-d.ts @@ -0,0 +1,43 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import type { HasSpaces, Trim } from './string'; + +describe('string', () => { + describe('Trim', () => { + it('remove leading and trailing spaces from a string', () => { + type actual = Trim<' hello '>; + type expected = 'hello'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('remove only leading spaces from a string', () => { + type actual = Trim<' hello'>; + type expected = 'hello'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('remove only trailing spaces from a string', () => { + type actual = Trim<'hello '>; + type expected = 'hello'; + + expectTypeOf().toEqualTypeOf(); + }); + }); + + describe('HasSpaces', () => { + it('check if a string has spaces', () => { + type actual = HasSpaces<'hello world'>; + type expected = true; + + expectTypeOf().toEqualTypeOf(); + }); + + it('check if a string has no spaces', () => { + type actual = HasSpaces<'helloworld'>; + type expected = false; + + expectTypeOf().toEqualTypeOf(); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/types/ts/string.ts b/packages/stdlib/src/types/ts/string.ts new file mode 100644 index 0000000..0398e8e --- /dev/null +++ b/packages/stdlib/src/types/ts/string.ts @@ -0,0 +1,9 @@ +/** + * Trim leading and trailing whitespace from `S` + */ +export type Trim = S extends ` ${infer R}` ? Trim : S extends `${infer L} ` ? Trim : S; + +/** + * Check if `S` has any spaces + */ +export type HasSpaces = S extends `${string} ${string}` ? true : false; From dc48cbc44b32f2faa13debf4e9d38e731fc98293 Mon Sep 17 00:00:00 2001 From: robonen Date: Sat, 4 May 2024 06:59:35 +0700 Subject: [PATCH 05/25] feat(packages/stdlib): start getByPath and template tools --- packages/stdlib/src/arrays/getByPath/index.ts | 84 ++++++++++++++ packages/stdlib/src/arrays/index.ts | 1 + packages/stdlib/src/text/index.ts | 3 +- .../stdlib/src/text/template/index.test-d.ts | 105 ++++++++++++++++++ .../stdlib/src/text/template/index.test.ts | 42 +++++++ packages/stdlib/src/text/template/index.ts | 52 +++++++++ 6 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 packages/stdlib/src/arrays/getByPath/index.ts create mode 100644 packages/stdlib/src/arrays/index.ts create mode 100644 packages/stdlib/src/text/template/index.test-d.ts create mode 100644 packages/stdlib/src/text/template/index.test.ts create mode 100644 packages/stdlib/src/text/template/index.ts diff --git a/packages/stdlib/src/arrays/getByPath/index.ts b/packages/stdlib/src/arrays/getByPath/index.ts new file mode 100644 index 0000000..1a1ad7e --- /dev/null +++ b/packages/stdlib/src/arrays/getByPath/index.ts @@ -0,0 +1,84 @@ +type Exist = T extends undefined | null ? never : T; + +type ExtractFromObject, K> = + K extends keyof O + ? O[K] + : K extends keyof Exist + ? Exist[K] + : never; + +type ExtractFromArray = any[] extends A + ? A extends readonly (infer T)[] + ? T | undefined + : undefined + : K extends keyof A + ? A[K] + : undefined; + +type GetWithArray = K extends [] + ? O + : K extends [infer Key, ...infer Rest] + ? O extends Record + ? GetWithArray, Rest> + : O extends readonly any[] + ? GetWithArray, Rest> + : never + : never; + +type Path = T extends `${infer Key}.${infer Rest}` + ? [Key, ...Path] + : T extends `${infer Key}` + ? [Key] + : []; + +// Type that generate a type of a value by a path; +// e.g. ['a', 'b', 'c'] => { a: { b: { c: PropertyKey } } } +// e.g. ['a', 'b', 'c', 'd'] => { a: { b: { c: { d: PropertyKey } } } } +// e.g. ['a'] => { a: PropertyKey } +// e.g. ['a', '0'], => { a: [PropertyKey] } +// e.g. ['a', '0', 'b'] => { a: [{ b: PropertyKey }] } +// e.g. ['a', '0', 'b', '0'] => { a: [{ b: [PropertyKey] }] } +// e/g/ ['0', 'a'] => [{ a: PropertyKey }] +// +// Input: ['a', 'b', 'c'], constrain: PropertyKey +// Output: { a: { b: { c: PropertyKey } } } + +export type UnionToIntersection = ( + Union extends unknown + ? (distributedUnion: Union) => void + : never +) extends ((mergedIntersection: infer Intersection) => void) + ? Intersection & Union + : never; + + +type PathToType = T extends [infer Head, ...infer Rest] + ? Head extends string + ? Head extends `${number}` + ? Rest extends string[] + ? PathToType[] + : never + : Rest extends string[] + ? { [K in Head & string]: PathToType } + : never + : never + : unknown; + +export type Generate = UnionToIntersection>>; +type Get = GetWithArray>; + +export function getByPath(obj: O, path: K): Get; +export function getByPath(obj: Record, path: string): unknown { + const keys = path.split('.'); + let currentObj = obj; + + for (const key of keys) { + const value = currentObj[key]; + + if (value === undefined || value === null) return undefined; + + currentObj = value as Record; + } + + return currentObj; +} diff --git a/packages/stdlib/src/arrays/index.ts b/packages/stdlib/src/arrays/index.ts new file mode 100644 index 0000000..64eab40 --- /dev/null +++ b/packages/stdlib/src/arrays/index.ts @@ -0,0 +1 @@ +export * from './getByPath'; \ No newline at end of file diff --git a/packages/stdlib/src/text/index.ts b/packages/stdlib/src/text/index.ts index c740f6d..c4fea60 100644 --- a/packages/stdlib/src/text/index.ts +++ b/packages/stdlib/src/text/index.ts @@ -1,2 +1,3 @@ export * from './levenshtein-distance'; -export * from './trigram-distance'; \ No newline at end of file +export * from './trigram-distance'; +export * from './template'; \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.test-d.ts b/packages/stdlib/src/text/template/index.test-d.ts new file mode 100644 index 0000000..7842c4c --- /dev/null +++ b/packages/stdlib/src/text/template/index.test-d.ts @@ -0,0 +1,105 @@ +import { describe, expectTypeOf, it } from "vitest"; +import type { ClearPlaceholder, ExtractPlaceholders } from "./index"; + +describe('template', () => { + describe('ClearPlaceholder', () => { + it('ignores strings without braces', () => { + type actual = ClearPlaceholder<'name'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('removes all balanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}'>; + type actual2 = ClearPlaceholder<'{{name}}'>; + type actual3 = ClearPlaceholder<'{{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('removes all unbalanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}}'>; + type actual2 = ClearPlaceholder<'{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + }); + + describe('ExtractPlaceholders', () => { + it('string without placeholders', () => { + type actual = ExtractPlaceholders<'Hello name, how are?'>; + type expected = never; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one idexed placeholder', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you?'>; + type expected = '0'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two indexed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>; + type expected = '0' | '1'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one key placeholder', () => { + type actual = ExtractPlaceholders<'Hello {name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two key placeholders', () => { + type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>; + type expected = 'name' | 'managers.0.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with mixed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>; + type expected = '0' | '1.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.test.ts b/packages/stdlib/src/text/template/index.test.ts new file mode 100644 index 0000000..63f9797 --- /dev/null +++ b/packages/stdlib/src/text/template/index.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { templateObject } from './index'; + +describe('templateObject', () => { + // it('replace template placeholders with corresponding values from args', () => { + // const template = 'Hello, {names.0}!'; + // const args = { names: ['John'] }; + // const result = templateObject(template, args); + // expect(result).toBe('Hello, John!'); + // }); + + // it('replace template placeholders with corresponding values from args', () => { + // const template = 'Hello, {name}!'; + // const args = { name: 'John' }; + // const result = templateObject(template, args); + // expect(result).toBe('Hello, John!'); + // }); + + // it('replace template placeholders with fallback value if corresponding value is undefined', () => { + // const template = 'Hello, {name}!'; + // const args = { age: 25 }; + // const fallback = 'Guest'; + // const result = templateObject(template, args, fallback); + // expect(result).toBe('Hello, Guest!'); + // }); + + // it(' replace template placeholders with fallback value returned by fallback function if corresponding value is undefined', () => { + // const template = 'Hello, {name}!'; + // const args = { age: 25 }; + // const fallback = (key: string) => `Unknown ${key}`; + // const result = templateObject(template, args, fallback); + // expect(result).toBe('Hello, Unknown name!'); + // }); + + it('replace template placeholders with nested values from args', () => { + const template = 'Hello, {user.name}!'; + const args = { user: { name: 'John' } }; + const result = templateObject(template, args); + + expect(result).toBe('Hello, John!'); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.ts b/packages/stdlib/src/text/template/index.ts new file mode 100644 index 0000000..7c6cbee --- /dev/null +++ b/packages/stdlib/src/text/template/index.ts @@ -0,0 +1,52 @@ +import { getByPath, type Generate } from '../../arrays'; + +/** + * Type of a value that will be used to replace a placeholder in a template. + */ +type StringPrimitive = string | number | bigint | null | undefined; + +/** + * Type of a fallback value when a template key is not found. + */ +type TemplateFallback = string | ((key: string) => string); + +/** + * Type of an object that will be used to replace placeholders in a template. + */ +type TemplateArgsObject = StringPrimitive[] | { [key: string]: TemplateArgsObject | StringPrimitive }; + +/** + * Type of a template string with placeholders. + */ +const TEMPLATE_PLACEHOLDER = /{([^{}]+)}/gm; + +export type ClearPlaceholder = + T extends `${string}{${infer Template}` + ? ClearPlaceholder