diff --git a/packages/stdlib/build.config.ts b/packages/stdlib/build.config.ts index f86264a..9195cb6 100644 --- a/packages/stdlib/build.config.ts +++ b/packages/stdlib/build.config.ts @@ -2,9 +2,8 @@ import { defineBuildConfig } from 'unbuild'; export default defineBuildConfig({ rollup: { - emitCJS: true, esbuild: { - minify: true, + // minify: true, }, }, }); \ No newline at end of file diff --git a/packages/stdlib/src/arrays/getByPath/index.ts b/packages/stdlib/src/arrays/getByPath/index.ts new file mode 100644 index 0000000..6ceea36 --- /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 + : string; + +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/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/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(); 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..b896be1 --- /dev/null +++ b/packages/stdlib/src/text/template/index.test.ts @@ -0,0 +1,48 @@ +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 result = templateObject('Hello {{user.name}, your address {user.addresses.0.street}', { + user: { + name: 'John Doe', + addresses: [ + { street: '123 Main St', city: 'Springfield'}, + { street: '456 Elm St', city: 'Shelbyville'} + ] + } + }); + + expect(result).toBe('Hello {John Doe, your address 123 Main St'); + }); +}); \ 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..6418480 --- /dev/null +++ b/packages/stdlib/src/text/template/index.ts @@ -0,0 +1,66 @@ +import { getByPath, type Generate } from '../../arrays'; +import { isFunction } from '../../types'; + +/** + * 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; + +/** + * Removes the placeholder syntax from a template string. + * + * @example + * type Base = ClearPlaceholder<'{user.name}'>; // 'user.name' + * type Unbalanced = ClearPlaceholder<'{user.name'>; // 'user.name' + */ +export type ClearPlaceholder = + T extends `${string}{${infer Template}` + ? ClearPlaceholder