mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 02:44:45 +00:00
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -26,4 +26,4 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm run all:test
|
||||
run: pnpm run all:build && pnpm run all:test
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ node_modules
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
cache
|
||||
out
|
||||
build
|
||||
dist
|
||||
|
||||
@@ -2,8 +2,8 @@ import { defineConfig } from 'vitepress';
|
||||
|
||||
export default defineConfig({
|
||||
lang: 'ru-RU',
|
||||
title: "Tools",
|
||||
description: "A set of tools and utilities for web development",
|
||||
title: "Toolkit",
|
||||
description: "A collection of typescript and javascript development tools",
|
||||
rewrites: {
|
||||
'packages/:pkg/README.md': 'packages/:pkg/index.md',
|
||||
},
|
||||
14
docs/index.md
Normal file
14
docs/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: Toolkit
|
||||
tagline: A collection of typescript and javascript development tools
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /
|
||||
- theme: alt
|
||||
text: View on GitHub
|
||||
link: /
|
||||
@@ -29,6 +29,7 @@
|
||||
"vitepress": "^1.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
"all:build": "pnpm -r build",
|
||||
"all:test": "pnpm -r test",
|
||||
"create": "jiti ./cli.ts",
|
||||
"docs:dev": "vitepress dev .",
|
||||
|
||||
1
packages/platform/README.md
Normal file
1
packages/platform/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# @robonen/platform
|
||||
5
packages/platform/jsr.json
Normal file
5
packages/platform/jsr.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@robonen/platform",
|
||||
"version": "0.0.0",
|
||||
"exports": "./src/index.ts"
|
||||
}
|
||||
36
packages/platform/package.json
Normal file
36
packages/platform/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@robonen/platform",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "./packages/platform"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0",
|
||||
"engines": {
|
||||
"node": ">=20.13.1"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.umd.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.umd.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
47
packages/platform/src/multi/debounce/index.ts
Normal file
47
packages/platform/src/multi/debounce/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface DebounceOptions {
|
||||
/**
|
||||
* Call the function on the leading edge of the timeout, instead of waiting for the trailing edge
|
||||
*/
|
||||
readonly immediate?: boolean;
|
||||
|
||||
/**
|
||||
* Call the function on the trailing edge with the last used arguments.
|
||||
* Result of call is from previous call
|
||||
*/
|
||||
readonly trailing?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_DEBOUNCE_OPTIONS: DebounceOptions = {
|
||||
trailing: true,
|
||||
}
|
||||
|
||||
export function debounce<FnArguments extends unknown[], FnReturn>(
|
||||
fn: (...args: FnArguments) => PromiseLike<FnReturn> | FnReturn,
|
||||
timeout: number = 20,
|
||||
options: DebounceOptions = {},
|
||||
) {
|
||||
options = {
|
||||
...DEFAULT_DEBOUNCE_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (!Number.isFinite(timeout) || timeout <= 0)
|
||||
throw new TypeError('Debounce timeout must be a positive number');
|
||||
|
||||
// Last result for leading edge
|
||||
let leadingValue: PromiseLike<FnReturn> | FnReturn;
|
||||
|
||||
// Debounce timeout id
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
// Promises to be resolved when debounce is finished
|
||||
let resolveList: Array<(value: unknown) => void> = [];
|
||||
|
||||
// State of currently resolving promise
|
||||
let currentResolve: Promise<FnReturn>;
|
||||
|
||||
// Trailing call information
|
||||
let trailingArgs: unknown[];
|
||||
|
||||
|
||||
}
|
||||
19
packages/platform/src/multi/global/index.ts
Normal file
19
packages/platform/src/multi/global/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// TODO: tests
|
||||
|
||||
/**
|
||||
* @name _global
|
||||
* @category Multi
|
||||
* @description Global object that works in any environment
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const _global =
|
||||
typeof globalThis !== 'undefined'
|
||||
? globalThis
|
||||
: typeof window !== 'undefined'
|
||||
? window
|
||||
: typeof global !== 'undefined'
|
||||
? global
|
||||
: typeof self !== 'undefined'
|
||||
? self
|
||||
: undefined;
|
||||
2
packages/platform/src/multi/index.ts
Normal file
2
packages/platform/src/multi/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './global';
|
||||
// export * from './debounce';
|
||||
3
packages/platform/tsconfig.json
Normal file
3
packages/platform/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||
}
|
||||
@@ -27,6 +27,6 @@
|
||||
"test": "renovate-config-validator ./default.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"renovate": "^38.100.0"
|
||||
"renovate": "^38.101.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
rollup: {
|
||||
emitCJS: true,
|
||||
esbuild: {
|
||||
minify: true,
|
||||
// minify: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -43,9 +43,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@vitest/coverage-v8": "^2.1.1",
|
||||
"pathe": "^1.1.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.1.1"
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"pathe": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
84
packages/stdlib/src/arrays/getByPath/index.ts
Normal file
84
packages/stdlib/src/arrays/getByPath/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
type Exist<T> = T extends undefined | null ? never : T;
|
||||
|
||||
type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
|
||||
K extends keyof O
|
||||
? O[K]
|
||||
: K extends keyof Exist<O>
|
||||
? Exist<O>[K]
|
||||
: never;
|
||||
|
||||
type ExtractFromArray<A extends readonly any[], K> = any[] extends A
|
||||
? A extends readonly (infer T)[]
|
||||
? T | undefined
|
||||
: undefined
|
||||
: K extends keyof A
|
||||
? A[K]
|
||||
: undefined;
|
||||
|
||||
type GetWithArray<O, K> = K extends []
|
||||
? O
|
||||
: K extends [infer Key, ...infer Rest]
|
||||
? O extends Record<PropertyKey, unknown>
|
||||
? GetWithArray<ExtractFromObject<O, Key>, Rest>
|
||||
: O extends readonly any[]
|
||||
? GetWithArray<ExtractFromArray<O, Key>, Rest>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Path<T> = T extends `${infer Key}.${infer Rest}`
|
||||
? [Key, ...Path<Rest>]
|
||||
: 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> = (
|
||||
Union extends unknown
|
||||
? (distributedUnion: Union) => void
|
||||
: never
|
||||
) extends ((mergedIntersection: infer Intersection) => void)
|
||||
? Intersection & Union
|
||||
: never;
|
||||
|
||||
|
||||
type PathToType<T extends string[]> = T extends [infer Head, ...infer Rest]
|
||||
? Head extends string
|
||||
? Head extends `${number}`
|
||||
? Rest extends string[]
|
||||
? PathToType<Rest>[]
|
||||
: never
|
||||
: Rest extends string[]
|
||||
? { [K in Head & string]: PathToType<Rest> }
|
||||
: never
|
||||
: never
|
||||
: string;
|
||||
|
||||
export type Generate<T extends string> = UnionToIntersection<PathToType<Path<T>>>;
|
||||
type Get<O, K> = GetWithArray<O, Path<K>>;
|
||||
|
||||
export function getByPath<O, K extends string>(obj: O, path: K): Get<O, K>;
|
||||
export function getByPath(obj: Record<string, unknown>, 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<string, unknown>;
|
||||
}
|
||||
|
||||
return currentObj;
|
||||
}
|
||||
1
packages/stdlib/src/arrays/index.ts
Normal file
1
packages/stdlib/src/arrays/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './getByPath';
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* @returns {Function} A function that generates unique flags
|
||||
* @throws {RangeError} If more than 31 flags are created
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function flagsGenerator() {
|
||||
let lastFlag = 0;
|
||||
@@ -22,6 +24,8 @@ export function flagsGenerator() {
|
||||
*
|
||||
* @param {number[]} flags - The flags to combine
|
||||
* @returns {number} The combined flags
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function and(...flags: number[]) {
|
||||
return flags.reduce((acc, flag) => acc & flag, -1);
|
||||
@@ -32,6 +36,8 @@ export function and(...flags: number[]) {
|
||||
*
|
||||
* @param {number[]} flags - The flags to combine
|
||||
* @returns {number} The combined flags
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function or(...flags: number[]) {
|
||||
return flags.reduce((acc, flag) => acc | flag, 0);
|
||||
@@ -42,6 +48,8 @@ export function or(...flags: number[]) {
|
||||
*
|
||||
* @param {number} flag - The flag to apply the NOT operator to
|
||||
* @returns {number} The result of the NOT operator
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function not(flag: number) {
|
||||
return ~flag;
|
||||
@@ -51,7 +59,10 @@ export function not(flag: number) {
|
||||
* Function to make sure a flag has a specific bit set
|
||||
*
|
||||
* @param {number} flag - The flag to check
|
||||
* @param {number} other - Flag to check
|
||||
* @returns {boolean} Whether the flag has the bit set
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function has(flag: number, other: number) {
|
||||
return (flag & other) === other;
|
||||
@@ -62,6 +73,8 @@ export function has(flag: number, other: number) {
|
||||
*
|
||||
* @param {number} flag - The flag to check
|
||||
* @returns {boolean} Whether the flag is set
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function is(flag: number) {
|
||||
return flag !== 0;
|
||||
@@ -73,6 +86,8 @@ export function is(flag: number) {
|
||||
* @param {number} flag - Source flag
|
||||
* @param {number} other - Flag to unset
|
||||
* @returns {number} The new flag
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function unset(flag: number, other: number) {
|
||||
return flag & ~other;
|
||||
@@ -84,6 +99,8 @@ export function unset(flag: number, other: number) {
|
||||
* @param {number} flag - Source flag
|
||||
* @param {number} other - Flag to toggle
|
||||
* @returns {number} The new flag
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function toggle(flag: number, other: number) {
|
||||
return flag ^ other;
|
||||
|
||||
@@ -3,3 +3,5 @@ export * from './math';
|
||||
export * from './patterns';
|
||||
export * from './bits';
|
||||
export * from './structs';
|
||||
export * from './arrays';
|
||||
export * from './types';
|
||||
|
||||
13
packages/stdlib/src/math/basic/clamp/index.ts
Normal file
13
packages/stdlib/src/math/basic/clamp/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Clamps a number between a minimum and maximum value
|
||||
*
|
||||
* @param {number} value The number to clamp
|
||||
* @param {number} min Minimum value
|
||||
* @param {number} max Maximum value
|
||||
* @returns {number} The clamped number
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
61
packages/stdlib/src/math/basic/lerp/index.test.ts
Normal file
61
packages/stdlib/src/math/basic/lerp/index.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerp, lerp} from './index';
|
||||
|
||||
describe('lerp', () => {
|
||||
it('interpolates between two values', () => {
|
||||
const result = lerp(0, 10, 0.5);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
it('returns start value when t is 0', () => {
|
||||
const result = lerp(0, 10, 0);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns end value when t is 1', () => {
|
||||
const result = lerp(0, 10, 1);
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
it('handles negative interpolation values', () => {
|
||||
const result = lerp(0, 10, -0.5);
|
||||
expect(result).toBe(-5);
|
||||
});
|
||||
|
||||
it('handles interpolation values greater than 1', () => {
|
||||
const result = lerp(0, 10, 1.5);
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerp', () => {
|
||||
it('returns 0 when value is start', () => {
|
||||
const result = inverseLerp(0, 10, 0);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 1 when value is end', () => {
|
||||
const result = inverseLerp(0, 10, 10);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('interpolates correctly between two values', () => {
|
||||
const result = inverseLerp(0, 10, 5);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
|
||||
it('handles values less than start', () => {
|
||||
const result = inverseLerp(0, 10, -5);
|
||||
expect(result).toBe(-0.5);
|
||||
});
|
||||
|
||||
it('handles values greater than end', () => {
|
||||
const result = inverseLerp(0, 10, 15);
|
||||
expect(result).toBe(1.5);
|
||||
});
|
||||
|
||||
it('handles same start and end values', () => {
|
||||
const result = inverseLerp(10, 10, 10);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
27
packages/stdlib/src/math/basic/lerp/index.ts
Normal file
27
packages/stdlib/src/math/basic/lerp/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Linearly interpolates between two values
|
||||
*
|
||||
* @param {number} start The start value
|
||||
* @param {number} end The end value
|
||||
* @param {number} t The interpolation value
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function lerp(start: number, end: number, t: number) {
|
||||
return start + t * (end - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse linear interpolation between two values
|
||||
*
|
||||
* @param {number} start The start value
|
||||
* @param {number} end The end value
|
||||
* @param {number} value The value to interpolate
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function inverseLerp(start: number, end: number, value: number) {
|
||||
return start === end ? 0 : (value - start) / (end - start);
|
||||
}
|
||||
46
packages/stdlib/src/math/basic/remap/index.test.ts
Normal file
46
packages/stdlib/src/math/basic/remap/index.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remap} from './index';
|
||||
|
||||
describe('remap', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(remap(5, 0, 10, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(remap(0, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(remap(10, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value outside range (below)
|
||||
expect(remap(-5, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value outside range (above)
|
||||
expect(remap(15, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remap(75, 50, 100, -50, 50)).toBe(0);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remap(-25, -50, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value
|
||||
expect(remap(3.5, 0, 10, 0, 100)).toBe(35);
|
||||
|
||||
// positive floating-point ranges
|
||||
expect(remap(1.25, 0, 2.5, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point value
|
||||
expect(remap(-2.5, -5, 0, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point ranges
|
||||
expect(remap(-1.25, -2.5, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remap(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { clamp } from "../clamp";
|
||||
import { clamp } from '../clamp';
|
||||
import {inverseLerp, lerp} from '../lerp';
|
||||
|
||||
/**
|
||||
* Map a value from one range to another
|
||||
@@ -9,15 +10,14 @@ import { clamp } from "../clamp";
|
||||
* @param {number} out_min The minimum value of the output range
|
||||
* @param {number} out_max The maximum value of the output range
|
||||
* @returns {number} The mapped value
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function mapRange(value: number, in_min: number, in_max: number, out_min: number, out_max: number): number {
|
||||
// Zero input range means invalid input, so return lowest output range value
|
||||
export function remap(value: number, in_min: number, in_max: number, out_min: number, out_max: number) {
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
// To ensure the value is within the input range, clamp it
|
||||
const clampedValue = clamp(value, in_min, in_max);
|
||||
|
||||
// Finally, map the value from the input range to the output range
|
||||
return (clampedValue - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
|
||||
}
|
||||
35
packages/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
35
packages/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {clampBigInt} from './index';
|
||||
|
||||
describe('clampBigInt', () => {
|
||||
it('clamp a value within the given range', () => {
|
||||
// value < min
|
||||
expect(clampBigInt(-10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value > max
|
||||
expect(clampBigInt(200n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value within range
|
||||
expect(clampBigInt(50n, 0n, 100n)).toBe(50n);
|
||||
|
||||
// value at min
|
||||
expect(clampBigInt(0n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value at max
|
||||
expect(clampBigInt(100n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value at midpoint
|
||||
expect(clampBigInt(50n, 100n, 100n)).toBe(100n);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// all values are the same
|
||||
expect(clampBigInt(5n, 5n, 5n)).toBe(5n);
|
||||
|
||||
// min > max
|
||||
expect(clampBigInt(10n, 100n, 50n)).toBe(50n);
|
||||
|
||||
// negative range and value
|
||||
expect(clampBigInt(-10n, -100n, -5n)).toBe(-10n);
|
||||
});
|
||||
});
|
||||
16
packages/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
16
packages/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {minBigInt} from '../minBigInt';
|
||||
import {maxBigInt} from '../maxBigInt';
|
||||
|
||||
/**
|
||||
* Clamps a bigint between a minimum and maximum value
|
||||
*
|
||||
* @param {bigint} value The number to clamp
|
||||
* @param {bigint} min Minimum value
|
||||
* @param {bigint} max Maximum value
|
||||
* @returns {bigint} The clamped number
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function clampBigInt(value: bigint, min: bigint, max: bigint) {
|
||||
return minBigInt(maxBigInt(value, min), max);
|
||||
}
|
||||
83
packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
83
packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerpBigInt, lerpBigInt} from './index';
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
describe('lerpBigInt', () => {
|
||||
it('interpolates between two bigint values', () => {
|
||||
const result = lerpBigInt(0n, 10n, 0.5);
|
||||
expect(result).toBe(5n);
|
||||
});
|
||||
|
||||
it('returns start value when t is 0', () => {
|
||||
const result = lerpBigInt(0n, 10n, 0);
|
||||
expect(result).toBe(0n);
|
||||
});
|
||||
|
||||
it('returns end value when t is 1', () => {
|
||||
const result = lerpBigInt(0n, 10n, 1);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles negative interpolation values', () => {
|
||||
const result = lerpBigInt(0n, 10n, -0.5);
|
||||
expect(result).toBe(-5n);
|
||||
});
|
||||
|
||||
it('handles interpolation values greater than 1', () => {
|
||||
const result = lerpBigInt(0n, 10n, 1.5);
|
||||
expect(result).toBe(15n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerpBigInt', () => {
|
||||
it('returns 0 when value is start', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 0n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 1 when value is end', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 10n);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('interpolates correctly between two bigint values', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 5n);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
|
||||
it('handles values less than start', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, -5n);
|
||||
expect(result).toBe(-0.5);
|
||||
});
|
||||
|
||||
it('handles values greater than end', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 15n);
|
||||
expect(result).toBe(1.5);
|
||||
});
|
||||
|
||||
it('handles same start and end values', () => {
|
||||
const result = inverseLerpBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('handles the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('handles values just below the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
});
|
||||
27
packages/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
27
packages/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Linearly interpolates between bigint values
|
||||
*
|
||||
* @param {bigint} start The start value
|
||||
* @param {bigint} end The end value
|
||||
* @param {number} t The interpolation value
|
||||
* @returns {bigint} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
||||
return start + ((end - start) * BigInt(t * 10000)) / 10000n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse linear interpolation between two bigint values
|
||||
*
|
||||
* @param {bigint} start The start value
|
||||
* @param {bigint} end The end value
|
||||
* @param {bigint} value The value to interpolate
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
|
||||
return start === end ? 0 : Number((value - start) * 10000n / (end - start)) / 10000;
|
||||
}
|
||||
39
packages/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
39
packages/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { maxBigInt } from './index';
|
||||
|
||||
describe('maxBigInt', () => {
|
||||
it('returns -Infinity when no values are provided', () => {
|
||||
expect(() => maxBigInt()).toThrow(new TypeError('maxBigInt requires at least one argument'));
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of positive bigints', () => {
|
||||
const result = maxBigInt(10n, 20n, 5n, 15n);
|
||||
expect(result).toBe(20n);
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of negative bigints', () => {
|
||||
const result = maxBigInt(-10n, -20n, -5n, -15n);
|
||||
expect(result).toBe(-5n);
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of mixed positive and negative bigints', () => {
|
||||
const result = maxBigInt(10n, -20n, 5n, -15n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the value itself when only one bigint is provided', () => {
|
||||
const result = maxBigInt(10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the largest value when all values are the same', () => {
|
||||
const result = maxBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles a large number of bigints', () => {
|
||||
const values = Array.from({ length: 1000 }, (_, i) => BigInt(i));
|
||||
const result = maxBigInt(...values);
|
||||
expect(result).toBe(999n);
|
||||
});
|
||||
});
|
||||
15
packages/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
15
packages/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Like `Math.max` but for BigInts
|
||||
*
|
||||
* @param {...bigint} values The values to compare
|
||||
* @returns {bigint} The largest value
|
||||
* @throws {TypeError} If no arguments are provided
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function maxBigInt(...values: bigint[]) {
|
||||
if (!values.length)
|
||||
throw new TypeError('maxBigInt requires at least one argument');
|
||||
|
||||
return values.reduce((acc, val) => val > acc ? val : acc);
|
||||
}
|
||||
39
packages/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
39
packages/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {minBigInt} from './index';
|
||||
|
||||
describe('minBigInt', () => {
|
||||
it('returns Infinity when no values are provided', () => {
|
||||
expect(() => minBigInt()).toThrow(new TypeError('minBigInt requires at least one argument'));
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of positive bigints', () => {
|
||||
const result = minBigInt(10n, 20n, 5n, 15n);
|
||||
expect(result).toBe(5n);
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of negative bigints', () => {
|
||||
const result = minBigInt(-10n, -20n, -5n, -15n);
|
||||
expect(result).toBe(-20n);
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of mixed positive and negative bigints', () => {
|
||||
const result = minBigInt(10n, -20n, 5n, -15n);
|
||||
expect(result).toBe(-20n);
|
||||
});
|
||||
|
||||
it('returns the value itself when only one bigint is provided', () => {
|
||||
const result = minBigInt(10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the smallest value when all values are the same', () => {
|
||||
const result = minBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles a large number of bigints', () => {
|
||||
const values = Array.from({length: 1000}, (_, i) => BigInt(i));
|
||||
const result = minBigInt(...values);
|
||||
expect(result).toBe(0n);
|
||||
});
|
||||
});
|
||||
15
packages/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
15
packages/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Like `Math.min` but for BigInts
|
||||
*
|
||||
* @param {...bigint} values The values to compare
|
||||
* @returns {bigint} The smallest value
|
||||
* @throws {TypeError} If no arguments are provided
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function minBigInt(...values: bigint[]) {
|
||||
if (!values.length)
|
||||
throw new TypeError('minBigInt requires at least one argument');
|
||||
|
||||
return values.reduce((acc, val) => val < acc ? val : acc);
|
||||
}
|
||||
32
packages/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
32
packages/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remapBigInt} from './index';
|
||||
|
||||
describe('remapBigInt', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(remapBigInt(5n, 0n, 10n, 0n, 100n)).toBe(50n);
|
||||
|
||||
// value at min
|
||||
expect(remapBigInt(0n, 0n, 10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value at max
|
||||
expect(remapBigInt(10n, 0n, 10n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value outside range (below)
|
||||
expect(remapBigInt(-5n, 0n, 10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value outside range (above)
|
||||
expect(remapBigInt(15n, 0n, 10n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remapBigInt(75n, 50n, 100n, -50n, 50n)).toBe(0n);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remapBigInt(-25n, -50n, 0n, 0n, 100n)).toBe(50n);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
|
||||
});
|
||||
});
|
||||
23
packages/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
23
packages/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { clampBigInt } from '../clampBigInt';
|
||||
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
|
||||
|
||||
/**
|
||||
* Map a bigint value from one range to another
|
||||
*
|
||||
* @param {bigint} value The value to map
|
||||
* @param {bigint} in_min The minimum value of the input range
|
||||
* @param {bigint} in_max The maximum value of the input range
|
||||
* @param {bigint} out_min The minimum value of the output range
|
||||
* @param {bigint} out_max The maximum value of the output range
|
||||
* @returns {bigint} The mapped value
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_min: bigint, out_max: bigint) {
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
const clampedValue = clampBigInt(value, in_min, in_max);
|
||||
|
||||
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Clamps a number between a minimum and maximum value
|
||||
*
|
||||
* @param {number} value The number to clamp
|
||||
* @param {number} min Minimum value
|
||||
* @param {number} max Maximum value
|
||||
* @returns {number} The clamped number
|
||||
*/
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
// The clamp function takes a value, a minimum, and a maximum as parameters.
|
||||
// It ensures that the value falls within the range defined by the minimum and maximum values.
|
||||
// If the value is less than the minimum, it returns the minimum value.
|
||||
// If the value is greater than the maximum, it returns the maximum value.
|
||||
// Otherwise, it returns the original value.
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
@@ -1,2 +1,9 @@
|
||||
export * from './clamp';
|
||||
export * from './mapRange';
|
||||
export * from './basic/clamp';
|
||||
export * from './basic/lerp';
|
||||
export * from './basic/remap';
|
||||
|
||||
export * from './bigint/clampBigInt';
|
||||
export * from './bigint/lerpBigInt';
|
||||
export * from './bigint/maxBigInt';
|
||||
export * from './bigint/minBigInt';
|
||||
export * from './bigint/remapBigInt';
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mapRange } from './index';
|
||||
|
||||
describe('mapRange', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(mapRange(5, 0, 10, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(mapRange(0, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(mapRange(10, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value outside range (below)
|
||||
expect(mapRange(-5, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value outside range (above)
|
||||
expect(mapRange(15, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(mapRange(75, 50, 100, -50, 50)).toBe(0);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(mapRange(-25, -50, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value
|
||||
expect(mapRange(3.5, 0, 10, 0, 100)).toBe(35);
|
||||
|
||||
// positive floating-point ranges
|
||||
expect(mapRange(1.25, 0, 2.5, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point value
|
||||
expect(mapRange(-2.5, -5, 0, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point ranges
|
||||
expect(mapRange(-1.25, -2.5, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(mapRange(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,8 @@ export type EventsRecord = Record<string, Subscriber>;
|
||||
/**
|
||||
* Simple PubSub implementation
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
* @template {EventsRecord} Events
|
||||
*/
|
||||
export class PubSub<Events extends EventsRecord> {
|
||||
@@ -85,7 +87,7 @@ export class PubSub<Events extends EventsRecord> {
|
||||
* @param {...Parameters<Events[K]>} args Arguments for the listener
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): boolean {
|
||||
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>) {
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
if (!listeners)
|
||||
|
||||
@@ -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<number>();
|
||||
|
||||
@@ -4,6 +4,9 @@ export type StackOptions = {
|
||||
|
||||
/**
|
||||
* Represents a stack data structure
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
* @template T The type of elements stored in the stack
|
||||
*/
|
||||
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
@@ -13,7 +16,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private maxSize: number;
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
* The stack data structure
|
||||
@@ -21,7 +24,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* @private
|
||||
* @type {T[]}
|
||||
*/
|
||||
private stack: T[];
|
||||
private readonly stack: T[];
|
||||
|
||||
/**
|
||||
* Creates an instance of Stack
|
||||
@@ -39,7 +42,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* Gets the number of elements in the stack
|
||||
* @returns {number} The number of elements in the stack
|
||||
*/
|
||||
public get length(): number {
|
||||
public get length() {
|
||||
return this.stack.length;
|
||||
}
|
||||
|
||||
@@ -47,7 +50,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* Checks if the stack is empty
|
||||
* @returns {boolean} `true` if the stack is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
public get isEmpty() {
|
||||
return this.stack.length === 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* Checks if the stack is full
|
||||
* @returns {boolean} `true` if the stack is full, `false` otherwise
|
||||
*/
|
||||
public get isFull(): boolean {
|
||||
public get isFull() {
|
||||
return this.stack.length === this.maxSize;
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* Pops an element from the stack
|
||||
* @returns {T} The element popped from the stack
|
||||
*/
|
||||
public pop(): T | undefined {
|
||||
public pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
@@ -86,7 +89,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
* Peeks at the top element of the stack
|
||||
* @returns {T} The top element of the stack
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
public peek() {
|
||||
if (this.isEmpty)
|
||||
throw new RangeError('Stack is empty');
|
||||
|
||||
@@ -109,7 +112,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
public toArray() {
|
||||
return this.stack.toReversed();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './levenshtein-distance';
|
||||
export * from './trigram-distance';
|
||||
// TODO: Template is not implemented yet
|
||||
// export * from './template';
|
||||
@@ -4,12 +4,12 @@
|
||||
* @param {string} left First string
|
||||
* @param {string} right Second string
|
||||
* @returns {number} The Levenshtein distance between the two strings
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function levenshteinDistance(left: string, right: string): number {
|
||||
// If the strings are equal, the distance is 0
|
||||
if (left === right) return 0;
|
||||
|
||||
// If either string is empty, the distance is the length of the other string
|
||||
if (left.length === 0) return right.length;
|
||||
if (right.length === 0) return left.length;
|
||||
|
||||
|
||||
105
packages/stdlib/src/text/template/index.test-d.ts
Normal file
105
packages/stdlib/src/text/template/index.test-d.ts
Normal file
@@ -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<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('removes all balanced braces from placeholders', () => {
|
||||
type actual1 = ClearPlaceholder<'{name}'>;
|
||||
type actual2 = ClearPlaceholder<'{{name}}'>;
|
||||
type actual3 = ClearPlaceholder<'{{{name}}}'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual3>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('removes all unbalanced braces from placeholders', () => {
|
||||
type actual1 = ClearPlaceholder<'{name}}'>;
|
||||
type actual2 = ClearPlaceholder<'{{name}}}'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExtractPlaceholders', () => {
|
||||
it('string without placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello name, how are?'>;
|
||||
type expected = never;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with one idexed placeholder', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, how are you?'>;
|
||||
type expected = '0';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with two indexed placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>;
|
||||
type expected = '0' | '1';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with one key placeholder', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {name}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with two key placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>;
|
||||
type expected = 'name' | 'managers.0.name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with mixed placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>;
|
||||
type expected = '0' | '1.name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholder and balanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholder and unbalanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholders and balanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>;
|
||||
type expected = 'name' | 'positions';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholders and unbalanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>;
|
||||
type expected = 'name' | 'positions';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
48
packages/stdlib/src/text/template/index.test.ts
Normal file
48
packages/stdlib/src/text/template/index.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
66
packages/stdlib/src/text/template/index.ts
Normal file
66
packages/stdlib/src/text/template/index.ts
Normal file
@@ -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> =
|
||||
T extends `${string}{${infer Template}`
|
||||
? ClearPlaceholder<Template>
|
||||
: T extends `${infer Template}}${string}`
|
||||
? ClearPlaceholder<Template>
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Extracts all placeholders from a template string.
|
||||
*
|
||||
* @example
|
||||
* type Base = ExtractPlaceholders<'Hello {user.name}, {user.addresses.0.street}'>; // 'user.name' | 'user.addresses.0.street'
|
||||
*/
|
||||
export type ExtractPlaceholders<T extends string> =
|
||||
T extends `${infer Before}}${infer After}`
|
||||
? Before extends `${string}{${infer Placeholder}`
|
||||
? ClearPlaceholder<Placeholder> | ExtractPlaceholders<After>
|
||||
: ExtractPlaceholders<After>
|
||||
: never;
|
||||
|
||||
export function templateObject<T extends string, A extends Generate<ExtractPlaceholders<T>>>(template: T, args: A, fallback?: TemplateFallback): string {
|
||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||
const value = getByPath(args, key) as string;
|
||||
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
|
||||
});
|
||||
}
|
||||
|
||||
// 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'},
|
||||
// ],
|
||||
// },
|
||||
// });
|
||||
@@ -5,6 +5,8 @@ export type Trigrams = Map<string, number>;
|
||||
*
|
||||
* @param {string} text The text to extract trigrams
|
||||
* @returns {Trigrams} A map of trigram to count
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function trigramProfile(text: string): Trigrams {
|
||||
text = '\n\n' + text + '\n\n';
|
||||
@@ -26,6 +28,8 @@ export function trigramProfile(text: string): Trigrams {
|
||||
* @param {Trigrams} left First text trigram profile
|
||||
* @param {Trigrams} right Second text trigram profile
|
||||
* @returns {number} The trigram distance between the two strings
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function trigramDistance(left: Trigrams, right: Trigrams): number {
|
||||
let distance = -4;
|
||||
|
||||
2
packages/stdlib/src/types/index.ts
Normal file
2
packages/stdlib/src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './js';
|
||||
export * from './ts';
|
||||
30
packages/stdlib/src/types/js/casts.test.ts
Normal file
30
packages/stdlib/src/types/js/casts.test.ts
Normal file
@@ -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]');
|
||||
});
|
||||
});
|
||||
});
|
||||
7
packages/stdlib/src/types/js/casts.ts
Normal file
7
packages/stdlib/src/types/js/casts.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* To string any value.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export const toString = (value: any): string => Object.prototype.toString.call(value);
|
||||
183
packages/stdlib/src/types/js/complex.test.ts
Normal file
183
packages/stdlib/src/types/js/complex.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
81
packages/stdlib/src/types/js/complex.ts
Normal file
81
packages/stdlib/src/types/js/complex.ts
Normal file
@@ -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<any>}
|
||||
*/
|
||||
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
|
||||
|
||||
/**
|
||||
* Check if a value is a map.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Map<any, any>}
|
||||
*/
|
||||
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
|
||||
|
||||
/**
|
||||
* Check if a value is a set.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Set<any>}
|
||||
*/
|
||||
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
|
||||
|
||||
/**
|
||||
* Check if a value is a weakmap.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is WeakMap<object, any>}
|
||||
*/
|
||||
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
|
||||
|
||||
/**
|
||||
* Check if a value is a weakset.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is WeakSet<object>}
|
||||
*/
|
||||
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';
|
||||
3
packages/stdlib/src/types/js/index.ts
Normal file
3
packages/stdlib/src/types/js/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './casts';
|
||||
export * from './primitives';
|
||||
export * from './complex';
|
||||
101
packages/stdlib/src/types/js/primitives.test.ts
Normal file
101
packages/stdlib/src/types/js/primitives.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
packages/stdlib/src/types/js/primitives.ts
Normal file
65
packages/stdlib/src/types/js/primitives.ts
Normal file
@@ -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 = <T extends Function>(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]';
|
||||
1
packages/stdlib/src/types/ts/index.ts
Normal file
1
packages/stdlib/src/types/ts/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './string';
|
||||
43
packages/stdlib/src/types/ts/string.test-d.ts
Normal file
43
packages/stdlib/src/types/ts/string.test-d.ts
Normal file
@@ -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<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('remove only leading spaces from a string', () => {
|
||||
type actual = Trim<' hello'>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('remove only trailing spaces from a string', () => {
|
||||
type actual = Trim<'hello '>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HasSpaces', () => {
|
||||
it('check if a string has spaces', () => {
|
||||
type actual = HasSpaces<'hello world'>;
|
||||
type expected = true;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('check if a string has no spaces', () => {
|
||||
type actual = HasSpaces<'helloworld'>;
|
||||
type expected = false;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
9
packages/stdlib/src/types/ts/string.ts
Normal file
9
packages/stdlib/src/types/ts/string.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Trim leading and trailing whitespace from `S`
|
||||
*/
|
||||
export type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
|
||||
|
||||
/**
|
||||
* Check if `S` has any spaces
|
||||
*/
|
||||
export type HasSpaces<S extends string> = S extends `${string} ${string}` ? true : false;
|
||||
@@ -31,7 +31,7 @@ pnpm install -D @robonen/tsconfig
|
||||
"resolveJsonModule": true, // разрешить импортировать файлы JSON
|
||||
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
|
||||
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
|
||||
"removeComments": true, // удалять комментарии из исходного кода
|
||||
"removeComments": false, // удалять комментарии из исходного кода
|
||||
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
|
||||
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
|
||||
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"removeComments": true,
|
||||
"removeComments": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
|
||||
1
packages/vue/README.md
Normal file
1
packages/vue/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# @robonen/vue
|
||||
5
packages/vue/jsr.json
Normal file
5
packages/vue/jsr.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@robonen/vue",
|
||||
"version": "0.0.0",
|
||||
"exports": "./src/index.ts"
|
||||
}
|
||||
46
packages/vue/package.json
Normal file
46
packages/vue/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@robonen/vue",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "./packages/vue"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0",
|
||||
"engines": {
|
||||
"node": ">=20.13.1"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.umd.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.umd.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"dev": "vitest dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"jsdom": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@robonen/stdlib": "workspace:*",
|
||||
"vue": "catalog:"
|
||||
}
|
||||
}
|
||||
5
packages/vue/src/composables/index.ts
Normal file
5
packages/vue/src/composables/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './useCached';
|
||||
export * from './useCounter';
|
||||
export * from './useRenderCount';
|
||||
export * from './useSyncRefs';
|
||||
export * from './useToggle';
|
||||
44
packages/vue/src/composables/useCached/index.test.ts
Normal file
44
packages/vue/src/composables/useCached/index.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { useCached } from '.';
|
||||
|
||||
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
|
||||
describe('useCached', () => {
|
||||
it('default comparator', async () => {
|
||||
const externalValue = ref(0);
|
||||
const cachedValue = useCached(externalValue);
|
||||
|
||||
expect(cachedValue.value).toBe(0);
|
||||
|
||||
externalValue.value = 1;
|
||||
await nextTick();
|
||||
expect(cachedValue.value).toBe(1);
|
||||
|
||||
externalValue.value = 10;
|
||||
await nextTick();
|
||||
expect(cachedValue.value).toBe(10);
|
||||
});
|
||||
|
||||
it('custom array comparator', async () => {
|
||||
const externalValue = ref([1]);
|
||||
const initialValue = externalValue.value;
|
||||
|
||||
const cachedValue = useCached(externalValue, arrayEquals);
|
||||
|
||||
expect(cachedValue.value).toEqual(initialValue);
|
||||
|
||||
externalValue.value = initialValue;
|
||||
await nextTick();
|
||||
expect(cachedValue.value).toEqual(initialValue);
|
||||
|
||||
externalValue.value = [1];
|
||||
await nextTick();
|
||||
expect(cachedValue.value).toEqual(initialValue);
|
||||
|
||||
externalValue.value = [2];
|
||||
await nextTick();
|
||||
expect(cachedValue.value).not.toEqual(initialValue);
|
||||
expect(cachedValue.value).toEqual([2]);
|
||||
});
|
||||
});
|
||||
36
packages/vue/src/composables/useCached/index.ts
Normal file
36
packages/vue/src/composables/useCached/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ref, watch, type Ref, type WatchOptions } from 'vue';
|
||||
|
||||
export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
||||
|
||||
/**
|
||||
* @name useCached
|
||||
* @category Reactivity
|
||||
* @description Caches the value of an external ref and updates it only when the value changes
|
||||
*
|
||||
* @param {Ref<T>} externalValue Ref to cache
|
||||
* @param {Comparator<T>} comparator Comparator function to compare the values
|
||||
* @param {WatchOptions} watchOptions Watch options
|
||||
* @returns {Ref<T>} Cached ref
|
||||
*
|
||||
* @example
|
||||
* const externalValue = ref(0);
|
||||
* const cachedValue = useCached(externalValue);
|
||||
*
|
||||
* @example
|
||||
* const externalValue = ref(0);
|
||||
* const cachedValue = useCached(externalValue, (a, b) => a === b, { immediate: true });
|
||||
*/
|
||||
export function useCached<Value = unknown>(
|
||||
externalValue: Ref<Value>,
|
||||
comparator: Comparator<Value> = (a, b) => a === b,
|
||||
watchOptions?: WatchOptions,
|
||||
): Ref<Value> {
|
||||
const cached = ref(externalValue.value) as Ref<Value>;
|
||||
|
||||
watch(() => externalValue.value, (value) => {
|
||||
if (!comparator(value, cached.value))
|
||||
cached.value = value;
|
||||
}, watchOptions);
|
||||
|
||||
return cached;
|
||||
}
|
||||
8
packages/vue/src/composables/useCounter/demo.vue
Normal file
8
packages/vue/src/composables/useCounter/demo.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
76
packages/vue/src/composables/useCounter/index.test.ts
Normal file
76
packages/vue/src/composables/useCounter/index.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { it, expect, describe } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { useCounter } from '.';
|
||||
|
||||
describe('useCounter', () => {
|
||||
it('initialize count with the provided initial value', () => {
|
||||
const { count } = useCounter(5);
|
||||
expect(count.value).toBe(5);
|
||||
});
|
||||
|
||||
it('initialize count with the provided initial value from a ref', () => {
|
||||
const { count } = useCounter(ref(5));
|
||||
expect(count.value).toBe(5);
|
||||
});
|
||||
|
||||
it('increment count by 1 by default', () => {
|
||||
const { count, increment } = useCounter(0);
|
||||
increment();
|
||||
expect(count.value).toBe(1);
|
||||
});
|
||||
|
||||
it('increment count by the specified delta', () => {
|
||||
const { count, increment } = useCounter(0);
|
||||
increment(5);
|
||||
expect(count.value).toBe(5);
|
||||
});
|
||||
|
||||
it('decrement count by 1 by default', () => {
|
||||
const { count, decrement } = useCounter(5);
|
||||
decrement();
|
||||
expect(count.value).toBe(4);
|
||||
});
|
||||
|
||||
it('decrement count by the specified delta', () => {
|
||||
const { count, decrement } = useCounter(10);
|
||||
decrement(5);
|
||||
expect(count.value).toBe(5);
|
||||
});
|
||||
|
||||
it('set count to the specified value', () => {
|
||||
const { count, set } = useCounter(0);
|
||||
set(10);
|
||||
expect(count.value).toBe(10);
|
||||
});
|
||||
|
||||
it('get the current count value', () => {
|
||||
const { get } = useCounter(5);
|
||||
expect(get()).toBe(5);
|
||||
});
|
||||
|
||||
it('reset count to the initial value', () => {
|
||||
const { count, reset } = useCounter(10);
|
||||
count.value = 5;
|
||||
reset();
|
||||
expect(count.value).toBe(10);
|
||||
});
|
||||
|
||||
it('reset count to the specified value', () => {
|
||||
const { count, reset } = useCounter(10);
|
||||
count.value = 5;
|
||||
reset(20);
|
||||
expect(count.value).toBe(20);
|
||||
});
|
||||
|
||||
it('clamp count to the minimum value', () => {
|
||||
const { count, decrement } = useCounter(Number.MIN_SAFE_INTEGER);
|
||||
decrement();
|
||||
expect(count.value).toBe(Number.MIN_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('clamp count to the maximum value', () => {
|
||||
const { count, increment } = useCounter(Number.MAX_SAFE_INTEGER);
|
||||
increment();
|
||||
expect(count.value).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
});
|
||||
71
packages/vue/src/composables/useCounter/index.ts
Normal file
71
packages/vue/src/composables/useCounter/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ref, unref, type MaybeRef, type Ref } from 'vue';
|
||||
import { clamp } from '@robonen/stdlib';
|
||||
|
||||
export interface UseCounterOptions {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export interface UseConterReturn {
|
||||
count: Ref<number>;
|
||||
increment: (delta?: number) => void;
|
||||
decrement: (delta?: number) => void;
|
||||
set: (value: number) => void;
|
||||
get: () => number;
|
||||
reset: (value?: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name useCounter
|
||||
* @category Utilities
|
||||
* @description A composable that provides a counter with increment, decrement, set, get, and reset functions
|
||||
*
|
||||
* @param {MaybeRef<number>} [initialValue=0] The initial value of the counter
|
||||
* @param {UseCounterOptions} [options={}] The options for the counter
|
||||
* @param {number} [options.min=Number.MIN_SAFE_INTEGER] The minimum value of the counter
|
||||
* @param {number} [options.max=Number.MAX_SAFE_INTEGER] The maximum value of the counter
|
||||
* @returns {UseConterReturn} The counter object
|
||||
*
|
||||
* @example
|
||||
* const { count, increment } = useCounter(0);
|
||||
*
|
||||
* @example
|
||||
* const { count, increment, decrement, set, get, reset } = useCounter(0, { min: 0, max: 10 });
|
||||
*/
|
||||
export function useCounter(
|
||||
initialValue: MaybeRef<number> = 0,
|
||||
options: UseCounterOptions = {},
|
||||
): UseConterReturn {
|
||||
let _initialValue = unref(initialValue);
|
||||
const count = ref(initialValue);
|
||||
|
||||
const {
|
||||
min = Number.MIN_SAFE_INTEGER,
|
||||
max = Number.MAX_SAFE_INTEGER,
|
||||
} = options;
|
||||
|
||||
const increment = (delta = 1) =>
|
||||
count.value = clamp(count.value + delta, min, max);
|
||||
|
||||
const decrement = (delta = 1) =>
|
||||
count.value = clamp(count.value - delta, min, max);
|
||||
|
||||
const set = (value: number) =>
|
||||
count.value = clamp(value, min, max);
|
||||
|
||||
const get = () => count.value;
|
||||
|
||||
const reset = (value = _initialValue) => {
|
||||
_initialValue = value;
|
||||
return set(value);
|
||||
};
|
||||
|
||||
return {
|
||||
count,
|
||||
increment,
|
||||
decrement,
|
||||
set,
|
||||
get,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
75
packages/vue/src/composables/useRenderCount/index.test.ts
Normal file
75
packages/vue/src/composables/useRenderCount/index.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { defineComponent, nextTick, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { useRenderCount } from '.';
|
||||
|
||||
const ComponentStub = defineComponent({
|
||||
setup() {
|
||||
const count = useRenderCount();
|
||||
const visibleCount = ref(0);
|
||||
const hiddenCount = ref(0);
|
||||
|
||||
return { count, visibleCount, hiddenCount };
|
||||
},
|
||||
template: `<div>{{ visibleCount }}</div>`,
|
||||
});
|
||||
|
||||
describe('useRenderCount', () => {
|
||||
it('return the number of times the component has been rendered', async () => {
|
||||
const component = mount(ComponentStub);
|
||||
|
||||
// Initial render
|
||||
expect(component.vm.count).toBe(0);
|
||||
|
||||
component.vm.hiddenCount = 1;
|
||||
await nextTick();
|
||||
|
||||
// Will not trigger a render
|
||||
expect(component.vm.count).toBe(0);
|
||||
expect(component.text()).toBe('0');
|
||||
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a render
|
||||
expect(component.vm.count).toBe(1);
|
||||
expect(component.text()).toBe('1');
|
||||
|
||||
component.vm.visibleCount++;
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a single render for both updates
|
||||
expect(component.vm.count).toBe(2);
|
||||
expect(component.text()).toBe('3');
|
||||
});
|
||||
|
||||
it('can be used with a specific component instance', async () => {
|
||||
const component = mount(ComponentStub);
|
||||
const instance = component.vm.$;
|
||||
|
||||
const count = useRenderCount(instance);
|
||||
|
||||
// Initial render
|
||||
expect(count.value).toBe(0);
|
||||
|
||||
component.vm.hiddenCount = 1;
|
||||
await nextTick();
|
||||
|
||||
// Will not trigger a render
|
||||
expect(count.value).toBe(0);
|
||||
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a render
|
||||
expect(count.value).toBe(1);
|
||||
|
||||
component.vm.visibleCount++;
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a single render for both updates
|
||||
expect(count.value).toBe(2);
|
||||
});
|
||||
});
|
||||
25
packages/vue/src/composables/useRenderCount/index.ts
Normal file
25
packages/vue/src/composables/useRenderCount/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { onUpdated, readonly, type ComponentInternalInstance } from 'vue';
|
||||
import { useCounter } from '../useCounter';
|
||||
import { getLifeCycleTarger } from '../../utils';
|
||||
|
||||
/**
|
||||
* @name useRenderCount
|
||||
* @category Components
|
||||
* @description Returns the number of times the component has been rendered into the DOM
|
||||
*
|
||||
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||
* @returns {Readonly<Ref<number>>} The number of times the component has been rendered
|
||||
*
|
||||
* @example
|
||||
* const count = useRenderCount();
|
||||
*
|
||||
* @example
|
||||
* const count = useRenderCount(getCurrentInstance());
|
||||
*/
|
||||
export function useRenderCount(instance?: ComponentInternalInstance) {
|
||||
const { count, increment } = useCounter(0);
|
||||
|
||||
onUpdated(increment, getLifeCycleTarger(instance));
|
||||
|
||||
return readonly(count);
|
||||
}
|
||||
39
packages/vue/src/composables/useSyncRefs/index.test.ts
Normal file
39
packages/vue/src/composables/useSyncRefs/index.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { useSyncRefs } from '.';
|
||||
|
||||
describe('useSyncRefs', () => {
|
||||
it('sync the value of a source ref with multiple target refs', () => {
|
||||
const source = ref(0);
|
||||
const target1 = ref(0);
|
||||
const target2 = ref(0);
|
||||
useSyncRefs(source, [target1, target2]);
|
||||
|
||||
source.value = 10;
|
||||
|
||||
expect(target1.value).toBe(10);
|
||||
expect(target2.value).toBe(10);
|
||||
});
|
||||
|
||||
it('sync the value of a source ref with a single target ref', () => {
|
||||
const source = ref(0);
|
||||
const target = ref(0);
|
||||
useSyncRefs(source, target);
|
||||
|
||||
source.value = 20;
|
||||
|
||||
expect(target.value).toBe(20);
|
||||
});
|
||||
|
||||
it('stop watching when the stop handle is called', () => {
|
||||
const source = ref(0);
|
||||
const target = ref(0);
|
||||
const stop = useSyncRefs(source, target);
|
||||
|
||||
source.value = 30;
|
||||
stop();
|
||||
source.value = 40;
|
||||
|
||||
expect(target.value).toBe(30);
|
||||
});
|
||||
});
|
||||
44
packages/vue/src/composables/useSyncRefs/index.ts
Normal file
44
packages/vue/src/composables/useSyncRefs/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { watch, type Ref, type WatchOptions, type WatchSource } from 'vue';
|
||||
import { isArray } from '@robonen/stdlib';
|
||||
|
||||
/**
|
||||
* @name useSyncRefs
|
||||
* @category Reactivity
|
||||
* @description Syncs the value of a source ref with multiple target refs
|
||||
*
|
||||
* @param {WatchSource<T>} source Source ref to sync
|
||||
* @param {Ref<T> | Ref<T>[]} targets Target refs to sync
|
||||
* @param {WatchOptions} watchOptions Watch options
|
||||
* @returns {WatchStopHandle} Watch stop handle
|
||||
*
|
||||
* @example
|
||||
* const source = ref(0);
|
||||
* const target1 = ref(0);
|
||||
* const target2 = ref(0);
|
||||
* useSyncRefs(source, [target1, target2]);
|
||||
*
|
||||
* @example
|
||||
* const source = ref(0);
|
||||
* const target1 = ref(0);
|
||||
* useSyncRefs(source, target1, { immediate: true });
|
||||
*/
|
||||
export function useSyncRefs<T = unknown>(
|
||||
source: WatchSource<T>,
|
||||
targets: Ref<T> | Ref<T>[],
|
||||
watchOptions: WatchOptions = {},
|
||||
) {
|
||||
const {
|
||||
flush = 'sync',
|
||||
deep = false,
|
||||
immediate = true,
|
||||
} = watchOptions;
|
||||
|
||||
if (!isArray(targets))
|
||||
targets = [targets];
|
||||
|
||||
return watch(
|
||||
source,
|
||||
(value) => targets.forEach((target) => target.value = value),
|
||||
{ flush, deep, immediate },
|
||||
);
|
||||
}
|
||||
49
packages/vue/src/composables/useToggle/index.ts
Normal file
49
packages/vue/src/composables/useToggle/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { isRef, ref, toValue, type MaybeRefOrGetter, type MaybeRef, type Ref } from 'vue';
|
||||
|
||||
export interface UseToggleOptions<Enabled, Disabled> {
|
||||
enabledValue?: MaybeRefOrGetter<Enabled>,
|
||||
disabledValue?: MaybeRefOrGetter<Disabled>,
|
||||
}
|
||||
|
||||
// two overloads
|
||||
// 1. const [state, toggle] = useToggle(nonRefValue, options)
|
||||
// 2. const toggle = useToggle(refValue, options)
|
||||
// 3. const [state, toggle] = useToggle() // true, false by default
|
||||
|
||||
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
|
||||
initialValue: Ref<V>,
|
||||
options?: UseToggleOptions<Enabled, Disabled>,
|
||||
): (value?: V) => V;
|
||||
|
||||
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
|
||||
initialValue?: V,
|
||||
options?: UseToggleOptions<Enabled, Disabled>,
|
||||
): [Ref<V>, (value?: V) => V];
|
||||
|
||||
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
|
||||
initialValue: MaybeRef<V> = false,
|
||||
options: UseToggleOptions<Enabled, Disabled> = {},
|
||||
) {
|
||||
const {
|
||||
enabledValue = false,
|
||||
disabledValue = true,
|
||||
} = options;
|
||||
|
||||
const state = ref(initialValue) as Ref<V>;
|
||||
|
||||
const toggle = (value?: V) => {
|
||||
if (arguments.length) {
|
||||
state.value = value!;
|
||||
return state.value;
|
||||
}
|
||||
|
||||
const enabled = toValue(enabledValue);
|
||||
const disabled = toValue(disabledValue);
|
||||
|
||||
state.value = state.value === enabled ? disabled : enabled;
|
||||
|
||||
return state.value;
|
||||
};
|
||||
|
||||
return isRef(initialValue) ? toggle : [state, toggle];
|
||||
}
|
||||
2
packages/vue/src/index.ts
Normal file
2
packages/vue/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './composables';
|
||||
export * from './utils';
|
||||
5
packages/vue/src/utils/components.ts
Normal file
5
packages/vue/src/utils/components.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||
|
||||
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
|
||||
return target || getCurrentInstance();
|
||||
}
|
||||
1
packages/vue/src/utils/index.ts
Normal file
1
packages/vue/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './components';
|
||||
3
packages/vue/tsconfig.json
Normal file
3
packages/vue/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||
}
|
||||
7
packages/vue/vitest.config.ts
Normal file
7
packages/vue/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
});
|
||||
2439
pnpm-lock.yaml
generated
2439
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,10 @@
|
||||
packages:
|
||||
- packages/*
|
||||
catalog:
|
||||
'@vitest/coverage-v8': ^2.1.1
|
||||
'@vue/test-utils': ^2.4.6
|
||||
jsdom: ^25.0.1
|
||||
pathe: ^1.1.2
|
||||
unbuild: 3.0.0-rc.8
|
||||
vitest: ^2.1.1
|
||||
vue: ^3.5.10
|
||||
|
||||
Reference in New Issue
Block a user