mirror of
https://github.com/Warky-Devs/artemis-kit.git
synced 2025-05-18 19:37:28 +00:00
Initial commit
This commit is contained in:
parent
986458dd4b
commit
aba68a3c0a
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
37
README.md
37
README.md
@ -1,2 +1,37 @@
|
||||
# artemis-kit
|
||||
Artemis Kit is a libary for various utilities and functions in javascript/typescript.
|
||||
|
||||
A comprehensive TypeScript/JavaScript utility library focused on precision and efficiency.
|
||||
|
||||
## Features
|
||||
|
||||
### String Tools
|
||||
- Advanced string manipulation
|
||||
- Pattern matching and validation
|
||||
- Text transformation utilities
|
||||
- Unicode handling
|
||||
|
||||
### Blob Tools
|
||||
- Blob creation and modification
|
||||
- Binary data handling
|
||||
- Stream processing
|
||||
- MIME type management
|
||||
|
||||
### File Conversion Tools
|
||||
- Format conversions
|
||||
- File type transformations
|
||||
- Encoding/decoding utilities
|
||||
- Batch processing capabilities
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm install @warkypublic/artemis-kit
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
20
eslint.config.js
Normal file
20
eslint.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{ files: ["**/*.{js,mjs,cjs,ts}"] },
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
eqeqeq: "warn",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
},
|
||||
},
|
||||
];
|
67
package.json
Normal file
67
package.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@warkypublic/artemis-kit",
|
||||
"version": "1.0.0",
|
||||
"description": "A comprehensive TypeScript/JavaScript utility library focused on precision and efficiency.",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"clean": "rm -rf dist",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"keywords": [
|
||||
"string",
|
||||
"blob",
|
||||
"dependencies",
|
||||
"workspace",
|
||||
"package",
|
||||
"cli",
|
||||
"tools",
|
||||
"npm",
|
||||
"yarn",
|
||||
"pnpm"
|
||||
],
|
||||
"author": "Hein (Warkanum) Puth",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"eslint": "^9.16.0",
|
||||
"globals": "^15.13.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.2",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Warky-Devs/artemis-kit"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Warky-Devs/artemis-kit/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Warky-Devs/artemis-kit#readme",
|
||||
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
|
||||
}
|
3203
pnpm-lock.yaml
Normal file
3203
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
16
src/base64/Base64ToBlob.ts
Normal file
16
src/base64/Base64ToBlob.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Converts a base64 string to a Blob object.
|
||||
* @param base64 - The base64 encoded string to convert
|
||||
* @returns A Blob containing the decoded binary data
|
||||
*/
|
||||
export function base64ToBlob(base64: string): Blob {
|
||||
const byteString = atob(base64)
|
||||
const arrayBuffer = new ArrayBuffer(byteString.length)
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
uint8Array[i] = byteString.charCodeAt(i)
|
||||
}
|
||||
|
||||
return new Blob([arrayBuffer])
|
||||
}
|
19
src/base64/BlobToBase64.ts
Normal file
19
src/base64/BlobToBase64.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Converts a Blob object to a base64 encoded string.
|
||||
* @param blob - The Blob object to convert
|
||||
* @returns Promise that resolves with the base64 encoded string
|
||||
*/
|
||||
function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const dataUrl = (reader.result ?? '') as string
|
||||
const base64 = dataUrl?.split?.(',')?.[1]
|
||||
resolve(base64)
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
export { blobToBase64 }
|
14
src/base64/BlobToString.ts
Normal file
14
src/base64/BlobToString.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Converts a Blob object to a string. Defaults to a blank string if the Blob is null.
|
||||
* @param blob - The Blob object to convert
|
||||
* @returns Promise that resolves with the text
|
||||
*/
|
||||
async function blobToString(blob: Blob | string): Promise<string> {
|
||||
if (!blob) return ''
|
||||
if (typeof blob === 'string') {
|
||||
return blob
|
||||
}
|
||||
return await blob.text()
|
||||
}
|
||||
|
||||
export { blobToString }
|
18
src/base64/FileToBase64.ts
Normal file
18
src/base64/FileToBase64.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Converts a File object to a base64 encoded string.
|
||||
* @param file - The File object to convert
|
||||
* @returns Promise that resolves with the base64 encoded string
|
||||
*/
|
||||
function FileToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const d = reader.result?.toString()
|
||||
resolve(btoa(d ?? ''))
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
export { FileToBase64 }
|
17
src/base64/FileToBlob.ts
Normal file
17
src/base64/FileToBlob.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Converts a File object to a Blob object.
|
||||
* @param file - The File object to convert
|
||||
* @returns Promise that resolves with the Blob
|
||||
*/
|
||||
function FileToBlob(file: File): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve(new Blob([reader.result as ArrayBuffer]))
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
export { FileToBlob }
|
20
src/base64/base64-decode-unicode.ts
Normal file
20
src/base64/base64-decode-unicode.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Decodes a base64 string that contains Unicode characters.
|
||||
* This function handles Unicode characters correctly by:
|
||||
* 1. Converting base64 to binary
|
||||
* 2. Converting each byte to a hex representation
|
||||
* 3. Converting hex-encoded UTF-8 sequences back to Unicode characters
|
||||
*
|
||||
* @param str - The base64 encoded string to decode
|
||||
* @returns The decoded Unicode string
|
||||
*/
|
||||
export function b64DecodeUnicode(str: string): any {
|
||||
return decodeURIComponent(
|
||||
Array.prototype.map
|
||||
.call(
|
||||
atob(str),
|
||||
(c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`
|
||||
)
|
||||
.join('')
|
||||
)
|
||||
}
|
17
src/base64/base64-encode-unicode.ts
Normal file
17
src/base64/base64-encode-unicode.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Encodes a Unicode string to base64.
|
||||
* This function handles Unicode characters correctly by:
|
||||
* 1. Converting Unicode to UTF-8 percent-encoding
|
||||
* 2. Converting percent-encoded bytes to binary
|
||||
* 3. Converting binary to base64
|
||||
*
|
||||
* @param str - The Unicode string to encode
|
||||
* @returns The base64 encoded string
|
||||
*/
|
||||
export function b64EncodeUnicode(str: any): string {
|
||||
return btoa(
|
||||
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_match, p1) =>
|
||||
String.fromCharCode(Number.parseInt(p1, 16))
|
||||
)
|
||||
)
|
||||
}
|
6
src/base64/index.ts
Normal file
6
src/base64/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { base64ToBlob } from './Base64ToBlob'
|
||||
export { blobToBase64 } from './BlobToBase64'
|
||||
export { FileToBase64 } from './FileToBase64'
|
||||
export { FileToBlob } from './FileToBlob'
|
||||
export { b64DecodeUnicode } from './base64-decode-unicode'
|
||||
export { b64EncodeUnicode } from './base64-encode-unicode'
|
164
src/dataqueue/buffer.ts
Normal file
164
src/dataqueue/buffer.ts
Normal file
@ -0,0 +1,164 @@
|
||||
// buffer.ts
|
||||
import { ActiveRecord, BufferOptions } from './types';
|
||||
import { NestedQueue } from './queue';
|
||||
|
||||
export class ActiveRecordBuffer<T extends object> {
|
||||
private buffer: Map<string | number, ActiveRecord<T>>;
|
||||
private queue: NestedQueue<T>;
|
||||
private autoSaveTimer?: any;
|
||||
private options: Required<BufferOptions>;
|
||||
|
||||
constructor(queue: NestedQueue<T>, options: BufferOptions = {}) {
|
||||
this.queue = queue;
|
||||
this.buffer = new Map();
|
||||
this.options = {
|
||||
autoSave: options.autoSave ?? true,
|
||||
bufferSize: options.bufferSize ?? 100,
|
||||
flushInterval: options.flushInterval ?? 5000
|
||||
};
|
||||
|
||||
if (this.options.autoSave) {
|
||||
this.startAutoSave();
|
||||
}
|
||||
}
|
||||
|
||||
private startAutoSave() {
|
||||
this.autoSaveTimer = setInterval(() => {
|
||||
this.flush();
|
||||
}, this.options.flushInterval);
|
||||
}
|
||||
|
||||
private stopAutoSave() {
|
||||
if (this.autoSaveTimer) {
|
||||
clearInterval(this.autoSaveTimer);
|
||||
}
|
||||
}
|
||||
|
||||
private getRecordId(record: T): string | number {
|
||||
if ('id' in record) return record['id'] as string | number;
|
||||
if ('key' in record) return record['key'] as string;
|
||||
throw new Error('Record must have an id or key property');
|
||||
}
|
||||
|
||||
async load(record: T): Promise<ActiveRecord<T>> {
|
||||
const id = this.getRecordId(record);
|
||||
|
||||
if (this.buffer.has(id)) {
|
||||
return this.buffer.get(id)!;
|
||||
}
|
||||
|
||||
const activeRecord: ActiveRecord<T> = {
|
||||
data: { ...record },
|
||||
id,
|
||||
isDirty: false,
|
||||
isNew: false,
|
||||
changes: {},
|
||||
originalData: { ...record }
|
||||
};
|
||||
|
||||
this.buffer.set(id, activeRecord);
|
||||
|
||||
if (this.buffer.size > this.options.bufferSize) {
|
||||
await this.flush();
|
||||
}
|
||||
|
||||
return activeRecord;
|
||||
}
|
||||
|
||||
create(data: T): ActiveRecord<T> {
|
||||
const id = this.getRecordId(data);
|
||||
|
||||
const activeRecord: ActiveRecord<T> = {
|
||||
data: { ...data } as any,
|
||||
id,
|
||||
isDirty: true,
|
||||
isNew: true,
|
||||
changes: { ...data } as any,
|
||||
originalData: { }
|
||||
} as ActiveRecord<T>;
|
||||
|
||||
this.buffer.set(id, activeRecord);
|
||||
return activeRecord;
|
||||
}
|
||||
|
||||
update(id: string | number, changes: Partial<T>): ActiveRecord<T> {
|
||||
const record = this.buffer.get(id);
|
||||
if (!record) {
|
||||
throw new Error(`Record with id ${id} not found in buffer`);
|
||||
}
|
||||
|
||||
record.isDirty = true;
|
||||
record.data = { ...record.data, ...changes };
|
||||
record.changes = { ...record.changes, ...changes };
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id: string | number) {
|
||||
const record = this.buffer.get(id);
|
||||
if (record) {
|
||||
await this.queue.remove(String(id));
|
||||
this.buffer.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
get(id: string | number): ActiveRecord<T> | undefined {
|
||||
return this.buffer.get(id);
|
||||
}
|
||||
|
||||
hasChanges(id: string | number): boolean {
|
||||
const record = this.buffer.get(id);
|
||||
return record ? record.isDirty : false;
|
||||
}
|
||||
|
||||
getDirtyRecords(): ActiveRecord<T>[] {
|
||||
return Array.from(this.buffer.values()).filter(record => record.isDirty);
|
||||
}
|
||||
|
||||
getAll(): ActiveRecord<T>[] {
|
||||
return Array.from(this.buffer.values());
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
const dirtyRecords = this.getDirtyRecords();
|
||||
|
||||
for (const record of dirtyRecords) {
|
||||
if (record.isNew) {
|
||||
await this.queue.add(record.data);
|
||||
} else {
|
||||
await this.queue.update(String(record.id), record.changes);
|
||||
}
|
||||
|
||||
record.isDirty = false;
|
||||
record.isNew = false;
|
||||
record.changes = {};
|
||||
record.originalData = { ...record.data };
|
||||
}
|
||||
|
||||
this.buffer.clear();
|
||||
}
|
||||
|
||||
rollback(id: string | number) {
|
||||
const record = this.buffer.get(id);
|
||||
if (record) {
|
||||
record.data = { ...record.originalData };
|
||||
record.isDirty = false;
|
||||
record.changes = {};
|
||||
}
|
||||
}
|
||||
|
||||
rollbackAll() {
|
||||
for (const [id] of this.buffer) {
|
||||
this.rollback(id);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.buffer.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.stopAutoSave();
|
||||
this.clear();
|
||||
}
|
||||
}
|
150
src/dataqueue/examples.ts
Normal file
150
src/dataqueue/examples.ts
Normal file
@ -0,0 +1,150 @@
|
||||
// // examples.ts
|
||||
// import { EnhancedNestedQueue } from './queue';
|
||||
// import { LocalStoragePersistence } from './persistence';
|
||||
// import { useQueue, useActiveRecord } from './hooks';
|
||||
|
||||
// // Example 1: Basic Queue Usage
|
||||
// interface TodoItem {
|
||||
// id: number;
|
||||
// title: string;
|
||||
// completed: boolean;
|
||||
// subTasks?: TodoItem[];
|
||||
// }
|
||||
|
||||
// const todoQueue = new EnhancedNestedQueue<TodoItem>([], {
|
||||
// persistence: new LocalStoragePersistence('todos'),
|
||||
// middleware: [
|
||||
// {
|
||||
// beforeAction: (action) => {
|
||||
// console.log('Action:', action);
|
||||
// return action;
|
||||
// },
|
||||
// afterAction: (action, state) => {
|
||||
// console.log('New State:', state);
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// });
|
||||
|
||||
// // Example 2: React Component with Queue
|
||||
// function TodoList() {
|
||||
// const { data, queue, buffer } = useQueue<TodoItem>([], {
|
||||
// persistence: new LocalStoragePersistence('todos')
|
||||
// });
|
||||
|
||||
// const addTodo = (title: string) => {
|
||||
// const newTodo = buffer.create({
|
||||
// id: Date.now(),
|
||||
// title,
|
||||
// completed: false
|
||||
// });
|
||||
// };
|
||||
|
||||
// const toggleTodo = (id: number) => {
|
||||
// const todo = data.find(t => t.id === id);
|
||||
// if (todo) {
|
||||
// buffer.update(id, { completed: !todo.completed });
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <button onClick={() => addTodo('New Task')}>Add Task</button>
|
||||
// {data.map(todo => (
|
||||
// <div key={todo.id}>
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={todo.completed}
|
||||
// onChange={() => toggleTodo(todo.id)}
|
||||
// />
|
||||
// {todo.title}
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Example 3: Active Record Pattern
|
||||
// function TodoEditor({ todoId }: { todoId: number }) {
|
||||
// const { data, queue } = useQueue<TodoItem>([]);
|
||||
// const todo = data.find(t => t.id === todoId);
|
||||
|
||||
// const {
|
||||
// record,
|
||||
// update,
|
||||
// save,
|
||||
// rollback,
|
||||
// isDirty
|
||||
// } = useActiveRecord(queue, todo!);
|
||||
|
||||
// if (!record) return null;
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <input
|
||||
// value={record.data.title}
|
||||
// onChange={e => update({ title: e.target.value })}
|
||||
// />
|
||||
// <button onClick={save} disabled={!isDirty}>
|
||||
// Save
|
||||
// </button>
|
||||
// <button onClick={rollback} disabled={!isDirty}>
|
||||
// Cancel
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Example 4: Complex Nested Data
|
||||
// interface Department {
|
||||
// id: number;
|
||||
// name: string;
|
||||
// employees: Employee[];
|
||||
// }
|
||||
|
||||
// interface Employee {
|
||||
// id: number;
|
||||
// name: string;
|
||||
// roles: string[];
|
||||
// }
|
||||
|
||||
// const departmentQueue = new EnhancedNestedQueue<Department>();
|
||||
|
||||
// // Adding nested data
|
||||
// departmentQueue.add({
|
||||
// id: 1,
|
||||
// name: 'Engineering',
|
||||
// employees: [
|
||||
// { id: 1, name: 'John', roles: ['dev'] }
|
||||
// ]
|
||||
// }, '');
|
||||
|
||||
// // Updating nested employee
|
||||
// departmentQueue.update('0.employees.0', {
|
||||
// roles: ['dev', 'lead']
|
||||
// });
|
||||
|
||||
// // Sorting employees by name
|
||||
// departmentQueue.sort('name', {
|
||||
// path: '0.employees',
|
||||
// direction: 'asc'
|
||||
// });
|
||||
|
||||
// // Example 5: Batch Operations
|
||||
// async function batchUpdate() {
|
||||
// const buffer = departmentQueue.getBuffer();
|
||||
|
||||
// // Load multiple records
|
||||
// const records = await Promise.all([
|
||||
// buffer.load({ id: 1, name: 'Dept 1', employees: [] }),
|
||||
// buffer.load({ id: 2, name: 'Dept 2', employees: [] })
|
||||
// ]);
|
||||
|
||||
// // Update multiple records
|
||||
// records.forEach(record => {
|
||||
// buffer.update(record.id, { name: `Updated ${record.data.name}` });
|
||||
// });
|
||||
|
||||
// // Flush all changes at once
|
||||
// await buffer.flush();
|
||||
// }
|
84
src/dataqueue/hooks.ts
Normal file
84
src/dataqueue/hooks.ts
Normal file
@ -0,0 +1,84 @@
|
||||
// // hooks.ts
|
||||
// import { useState, useEffect, useRef } from 'react';
|
||||
// import { EnhancedNestedQueue } from './queue';
|
||||
// import { QueueOptions, BufferOptions, ActiveRecord } from './types';
|
||||
|
||||
// export function useQueue<T extends object>(
|
||||
// initialData: T[],
|
||||
// options: QueueOptions<T> = {},
|
||||
// bufferOptions: BufferOptions = {}
|
||||
// ) {
|
||||
// const [data, setData] = useState<T[]>(initialData);
|
||||
// const queueRef = useRef<EnhancedNestedQueue<T>>();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!queueRef.current) {
|
||||
// queueRef.current = new EnhancedNestedQueue<T>(
|
||||
// initialData,
|
||||
// options,
|
||||
// bufferOptions
|
||||
// );
|
||||
// }
|
||||
|
||||
// const unsubscribe = queueRef.current.subscribe((newData) => {
|
||||
// setData(newData);
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// unsubscribe();
|
||||
// queueRef.current?.getBuffer().dispose();
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
// return {
|
||||
// data,
|
||||
// queue: queueRef.current!,
|
||||
// buffer: queueRef.current?.getBuffer()
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function useActiveRecord<T extends object>(
|
||||
// queue: EnhancedNestedQueue<T>,
|
||||
// record: T
|
||||
// ) {
|
||||
// const [activeRecord, setActiveRecord] = useState<ActiveRecord<T>>();
|
||||
// const buffer = queue.getBuffer();
|
||||
|
||||
// useEffect(() => {
|
||||
// buffer.load(record).then(setActiveRecord);
|
||||
|
||||
// return () => {
|
||||
// if (activeRecord?.isDirty) {
|
||||
// buffer.flush();
|
||||
// }
|
||||
// };
|
||||
// }, [record]);
|
||||
|
||||
// const updateRecord = (changes: Partial<T>) => {
|
||||
// if (activeRecord) {
|
||||
// const updated = buffer.update(activeRecord.id, changes);
|
||||
// setActiveRecord(updated);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const saveRecord = async () => {
|
||||
// if (activeRecord?.isDirty) {
|
||||
// await buffer.flush();
|
||||
// }
|
||||
// };
|
||||
|
||||
// const rollbackRecord = () => {
|
||||
// if (activeRecord) {
|
||||
// buffer.rollback(activeRecord.id);
|
||||
// setActiveRecord(buffer.get(activeRecord.id));
|
||||
// }
|
||||
// };
|
||||
|
||||
// return {
|
||||
// record: activeRecord,
|
||||
// update: updateRecord,
|
||||
// save: saveRecord,
|
||||
// rollback: rollbackRecord,
|
||||
// isDirty: activeRecord?.isDirty ?? false
|
||||
// };
|
||||
// }
|
9
src/dataqueue/index.ts
Normal file
9
src/dataqueue/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export {NestedQueue,EnhancedNestedQueue} from './queue'
|
||||
|
||||
import {InMemoryPersistence,IndexedDBPersistence,LocalStoragePersistence} from './persistence'
|
||||
const NestedQueuePersistence = {
|
||||
InMemoryPersistence,
|
||||
IndexedDBPersistence,
|
||||
LocalStoragePersistence
|
||||
}
|
||||
export {NestedQueuePersistence}
|
99
src/dataqueue/persistence.ts
Normal file
99
src/dataqueue/persistence.ts
Normal file
@ -0,0 +1,99 @@
|
||||
|
||||
import type { PersistenceAdapter, QueueData } from './types';
|
||||
|
||||
export class LocalStoragePersistence<T> implements PersistenceAdapter<T> {
|
||||
constructor(private key: string) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
async save(data: QueueData<T>): Promise<void> {
|
||||
localStorage.setItem(this.key, JSON.stringify(data));
|
||||
}
|
||||
|
||||
async load(): Promise<QueueData<T> | null> {
|
||||
const data = localStorage.getItem(this.key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
localStorage.removeItem(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexedDBPersistence<T> implements PersistenceAdapter<T> {
|
||||
private dbName: string;
|
||||
private storeName: string;
|
||||
|
||||
constructor(dbName: string, storeName: string) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
}
|
||||
|
||||
private openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async save(data: QueueData<T>): Promise<void> {
|
||||
const db = await this.openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(data, 'queueData');
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async load(): Promise<QueueData<T> | null> {
|
||||
const db = await this.openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get('queueData');
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
const db = await this.openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryPersistence<T> implements PersistenceAdapter<T> {
|
||||
private data: QueueData<T> | null = null;
|
||||
|
||||
async save(data: QueueData<T>): Promise<void> {
|
||||
this.data = [...data];
|
||||
}
|
||||
|
||||
async load(): Promise<QueueData<T> | null> {
|
||||
return this.data ? [...this.data] : null;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.data = null;
|
||||
}
|
||||
}
|
285
src/dataqueue/queue.ts
Normal file
285
src/dataqueue/queue.ts
Normal file
@ -0,0 +1,285 @@
|
||||
// queue.ts
|
||||
import {
|
||||
QueueData,
|
||||
QueueOptions,
|
||||
QueueAction,
|
||||
|
||||
SortOptions,
|
||||
BufferOptions,
|
||||
Middleware,
|
||||
PersistenceAdapter,
|
||||
} from "./types";
|
||||
import { ActiveRecordBuffer } from "./buffer";
|
||||
import { QueueUtils } from "./utils";
|
||||
|
||||
export class NestedQueue<T extends object> {
|
||||
protected data: QueueData<T>;
|
||||
private listeners: Set<(data: QueueData<T>) => void>;
|
||||
private middleware: Middleware<T>[];
|
||||
private persistence?: PersistenceAdapter<T>;
|
||||
private initialized: Promise<void>;
|
||||
|
||||
constructor(initialData: QueueData<T> = [], options: QueueOptions<T> = {}) {
|
||||
this.data = [...initialData];
|
||||
this.listeners = new Set();
|
||||
this.middleware = options.middleware || [];
|
||||
this.persistence = options.persistence;
|
||||
|
||||
this.initialized =
|
||||
options.autoload && this.persistence
|
||||
? this.loadFromPersistence()
|
||||
: Promise.resolve();
|
||||
}
|
||||
|
||||
subscribe(callback: () => void) {
|
||||
this.listeners.add(callback);
|
||||
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
private notify() {
|
||||
this.listeners.forEach((callback) => callback(this.data));
|
||||
}
|
||||
|
||||
private getNestedValue(obj: any, path: string | string[]) {
|
||||
const keys = Array.isArray(path) ? path : path.split(".");
|
||||
return keys.reduce((current, key) => current?.[key], obj);
|
||||
}
|
||||
|
||||
private setNestedValue(obj: any, path: string, value: any) {
|
||||
const keys = path.split(".");
|
||||
const lastKey = keys.pop()!;
|
||||
const target = keys.reduce((current, key) => {
|
||||
current[key] = current[key] || {};
|
||||
return current[key];
|
||||
}, obj);
|
||||
target[lastKey] = value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private async executeAction(action: QueueAction<T>): Promise<void> {
|
||||
// Run through beforeAction middleware
|
||||
let currentAction = action;
|
||||
for (const m of this.middleware) {
|
||||
if (m.beforeAction) {
|
||||
currentAction = m.beforeAction(currentAction) || currentAction;
|
||||
if (!currentAction) return; // Action cancelled by middleware
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the action
|
||||
switch (currentAction.type) {
|
||||
case "add":
|
||||
this.executeAdd(currentAction.payload, currentAction.path);
|
||||
break;
|
||||
case "remove":
|
||||
this.executeRemove(currentAction.path!);
|
||||
break;
|
||||
case "update":
|
||||
this.executeUpdate(currentAction.path!, currentAction.payload);
|
||||
break;
|
||||
case "sort":
|
||||
this.executeSort(
|
||||
currentAction.payload.key,
|
||||
currentAction.payload.options
|
||||
);
|
||||
break;
|
||||
case "clear":
|
||||
this.executeClear();
|
||||
break;
|
||||
}
|
||||
|
||||
// Run through afterAction middleware
|
||||
for (const m of this.middleware) {
|
||||
if (m.afterAction) {
|
||||
m.afterAction(currentAction, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist changes if enabled
|
||||
if (this.persistence) {
|
||||
await this.persistence.save(this.data);
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private executeAdd(item: T, path: string = "") {
|
||||
this.data = [...this.data];
|
||||
|
||||
if (!path) {
|
||||
this.data.push(item);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.getNestedValue(this.data, path);
|
||||
if (Array.isArray(target)) {
|
||||
target.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
private executeRemove(path: string) {
|
||||
const pathParts = path.split(".");
|
||||
const index = pathParts.pop()!;
|
||||
const parentPath = pathParts.join(".");
|
||||
|
||||
this.data = [...this.data];
|
||||
const target = parentPath
|
||||
? this.getNestedValue(this.data, parentPath)
|
||||
: this.data;
|
||||
|
||||
if (Array.isArray(target)) {
|
||||
target.splice(Number(index), 1);
|
||||
}
|
||||
}
|
||||
|
||||
private executeUpdate(path: string, value: Partial<T>) {
|
||||
this.data = [...this.data];
|
||||
const existing = this.getNestedValue(this.data, path);
|
||||
|
||||
if (existing) {
|
||||
this.setNestedValue(this.data, path, { ...existing, ...value });
|
||||
}
|
||||
}
|
||||
|
||||
private executeSort(key: keyof T | string[], options: SortOptions<T> = {}) {
|
||||
const {
|
||||
deep = true,
|
||||
maxDepth = Infinity,
|
||||
path = "",
|
||||
direction = "asc",
|
||||
sortFn,
|
||||
} = options;
|
||||
|
||||
const sortArray = (array: any[], depth: number = 0): void => {
|
||||
if (sortFn) {
|
||||
array.sort((a, b) => sortFn(a, b));
|
||||
} else {
|
||||
array.sort((a, b) => {
|
||||
const aValue = Array.isArray(key)
|
||||
? this.getNestedValue(a, key)
|
||||
: a[key];
|
||||
const bValue = Array.isArray(key)
|
||||
? this.getNestedValue(b, key)
|
||||
: b[key];
|
||||
|
||||
return QueueUtils.compareValues(aValue, bValue, direction);
|
||||
});
|
||||
}
|
||||
|
||||
if (deep && depth < maxDepth) {
|
||||
array.forEach((item) => {
|
||||
Object.values(item).forEach((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
sortArray(value, depth + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.data = [...this.data];
|
||||
|
||||
if (path) {
|
||||
const target = this.getNestedValue(this.data, path);
|
||||
if (Array.isArray(target)) {
|
||||
sortArray(target);
|
||||
}
|
||||
} else {
|
||||
sortArray(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
private executeClear() {
|
||||
this.data = [];
|
||||
}
|
||||
|
||||
// ... Core action execution methods ...
|
||||
// (Previously defined methods: executeAction, executeAdd, executeRemove, etc.)
|
||||
|
||||
// Public API methods
|
||||
async add(item: T, path: string = "") {
|
||||
await this.initialized;
|
||||
return this.executeAction({
|
||||
type: "add",
|
||||
payload: item,
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
async remove(path: string) {
|
||||
await this.initialized;
|
||||
return this.executeAction({
|
||||
type: "remove",
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
async update(path: string, value: Partial<T>) {
|
||||
await this.initialized;
|
||||
return this.executeAction({
|
||||
type: "update",
|
||||
payload: value,
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
get(path: string) {
|
||||
return this.getNestedValue(this.data, path);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
// ... Search and filter methods ...
|
||||
// (Previously defined methods: search, filter, findOne, sort)
|
||||
|
||||
async clear() {
|
||||
await this.initialized;
|
||||
return this.executeAction({
|
||||
type: "clear",
|
||||
});
|
||||
}
|
||||
|
||||
private async loadFromPersistence(): Promise<void> {
|
||||
if (!this.persistence) return;
|
||||
|
||||
try {
|
||||
const persistedData = await this.persistence.load();
|
||||
if (persistedData) {
|
||||
this.data = persistedData;
|
||||
this.notify();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load persisted data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async clearPersistence(): Promise<void> {
|
||||
if (this.persistence) {
|
||||
await this.persistence.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced queue with buffer support
|
||||
export class EnhancedNestedQueue<T extends object> extends NestedQueue<T> {
|
||||
private activeRecordBuffer: ActiveRecordBuffer<T>;
|
||||
|
||||
constructor(
|
||||
initialData: T[] = [],
|
||||
options: QueueOptions<T> = {},
|
||||
bufferOptions: BufferOptions = {}
|
||||
) {
|
||||
super(initialData, options);
|
||||
this.activeRecordBuffer = new ActiveRecordBuffer<T>(this, bufferOptions);
|
||||
}
|
||||
|
||||
getBuffer(): ActiveRecordBuffer<T> {
|
||||
return this.activeRecordBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
export default EnhancedNestedQueue;
|
73
src/dataqueue/types.ts
Normal file
73
src/dataqueue/types.ts
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
export type QueueData<T> = T[];
|
||||
|
||||
export interface SearchOptions {
|
||||
exact?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
deep?: boolean;
|
||||
paths?: string[];
|
||||
}
|
||||
|
||||
export interface FilterOptions {
|
||||
deep?: boolean;
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
export interface SortOptions<T> {
|
||||
deep?: boolean;
|
||||
maxDepth?: number;
|
||||
path?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
sortFn?: (a: T, b: T) => number;
|
||||
}
|
||||
|
||||
export type QueueAction<T> = {
|
||||
type: 'add' | 'remove' | 'update' | 'sort' | 'clear';
|
||||
payload?: T | any;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export interface Middleware<T> {
|
||||
beforeAction?: (action: QueueAction<T>) => QueueAction<T> | null;
|
||||
afterAction?: (action: QueueAction<T>, state: QueueData<T>) => void;
|
||||
}
|
||||
|
||||
export interface PersistenceAdapter<T> {
|
||||
save: (data: QueueData<T>) => Promise<void>;
|
||||
load: () => Promise<QueueData<T> | null>;
|
||||
clear: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface QueueOptions<T> {
|
||||
persistence?: PersistenceAdapter<T>;
|
||||
middleware?: Middleware<T>[];
|
||||
autoload?: boolean;
|
||||
}
|
||||
|
||||
export interface BufferOptions {
|
||||
autoSave?: boolean;
|
||||
bufferSize?: number;
|
||||
flushInterval?: number;
|
||||
}
|
||||
|
||||
export interface ActiveRecord<T> {
|
||||
data: T;
|
||||
id: string | number;
|
||||
isDirty: boolean;
|
||||
isNew: boolean;
|
||||
changes: Partial<T>;
|
||||
originalData: T;
|
||||
}
|
||||
|
||||
export interface QueueOperations<T> {
|
||||
add: (item: T, path?: string) => Promise<void>;
|
||||
remove: (path: string) => Promise<void>;
|
||||
update: (path: string, value: Partial<T>) => Promise<void>;
|
||||
get: (path: string) => any;
|
||||
getAll: () => T[];
|
||||
search: (query: string | object, options?: SearchOptions) => T[];
|
||||
filter: (predicate: (item: T) => boolean, options?: FilterOptions) => T[];
|
||||
findOne: (predicate: (item: T) => boolean) => T | undefined;
|
||||
sort: (key: keyof T | string[], options?: SortOptions<T>) => Promise<void>;
|
||||
clear: () => Promise<void>;
|
||||
}
|
117
src/dataqueue/utils.ts
Normal file
117
src/dataqueue/utils.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// utils.ts
|
||||
export class QueueUtils {
|
||||
static deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => QueueUtils.deepClone(item)) as unknown as T;
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
acc[key] = QueueUtils.deepClone(obj[key]);
|
||||
return acc;
|
||||
}, {} as T);
|
||||
}
|
||||
|
||||
static createPath(parts: (string | number)[]): string {
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
static parsePath(path: string): string[] {
|
||||
return path.split('.');
|
||||
}
|
||||
|
||||
static getValueAtPath<T>(obj: T, path: string): any {
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||
}
|
||||
|
||||
static setValueAtPath<T>(obj: T, path: string, value: any): T {
|
||||
const clone = QueueUtils.deepClone(obj);
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop()!;
|
||||
const target = keys.reduce((current:any, key) => {
|
||||
if (!(key in current)) {
|
||||
current[key] = {};
|
||||
}
|
||||
return current[key];
|
||||
}, clone);
|
||||
target[lastKey] = value;
|
||||
return clone;
|
||||
}
|
||||
|
||||
static compareValues(a: any, b: any, direction: 'asc' | 'desc' = 'asc'): number {
|
||||
if (a === b) return 0;
|
||||
if (a === null) return direction === 'asc' ? -1 : 1;
|
||||
if (b === null) return direction === 'asc' ? 1 : -1;
|
||||
|
||||
const typeA = typeof a;
|
||||
const typeB = typeof b;
|
||||
|
||||
if (typeA !== typeB) {
|
||||
a = String(a);
|
||||
b = String(b);
|
||||
}
|
||||
|
||||
if (typeA === 'string') {
|
||||
return direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
|
||||
}
|
||||
|
||||
return direction === 'asc' ? a - b : b - a;
|
||||
}
|
||||
|
||||
static debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: any;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
static throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle: boolean;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static batch(callback: () => void): void {
|
||||
Promise.resolve().then(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Decorators for method enhancement
|
||||
export function debounce(wait: number) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = QueueUtils.debounce(original, wait);
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle(limit: number) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = QueueUtils.throttle(original, limit);
|
||||
return descriptor;
|
||||
};
|
||||
}
|
24
src/i18n/examples.ts
Normal file
24
src/i18n/examples.ts
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
/**
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* import { i18n, t, tt } from './i18n';
|
||||
*
|
||||
* // Configure once at app startup
|
||||
* i18n.configure({
|
||||
* apiUrl: 'https://api.example.com/translations'
|
||||
* });
|
||||
*
|
||||
* // Sync usage
|
||||
* const message = t('welcome_message', 'Welcome');
|
||||
*
|
||||
* // Async usage
|
||||
* const asyncMessage = await tt('welcome_message', 'Welcome');
|
||||
*
|
||||
* // Listen for updates
|
||||
* window.addEventListener('i18n-updated', (event) => {
|
||||
* const { id, value } = event.detail;
|
||||
* // Update UI with new translation
|
||||
* });
|
||||
* ```
|
||||
*/
|
356
src/i18n/index.ts
Normal file
356
src/i18n/index.ts
Normal file
@ -0,0 +1,356 @@
|
||||
/**
|
||||
* @fileoverview Internationalization module with IndexedDB caching and server synchronization
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration options for the I18n manager
|
||||
*/
|
||||
type I18nConfig = {
|
||||
/** Base URL for the translations API */
|
||||
apiUrl?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translation entry structure as stored in the cache
|
||||
*/
|
||||
type CacheEntry = {
|
||||
/** The translated string value */
|
||||
value: string;
|
||||
/** Version number of the translation */
|
||||
version: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translation string registration format
|
||||
*/
|
||||
type TranslationString = {
|
||||
/** Unique identifier for the translation */
|
||||
id: string;
|
||||
/** The translated text */
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Server response format for translation requests
|
||||
*/
|
||||
type TranslationResponse = {
|
||||
/** The translated text */
|
||||
value: string;
|
||||
/** Version number of the translation */
|
||||
version: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Event payload for translation updates
|
||||
*/
|
||||
type I18nUpdateEvent = CustomEvent<{
|
||||
/** Identifier of the updated translation */
|
||||
id: string;
|
||||
/** New translated value */
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Core I18n manager interface
|
||||
*/
|
||||
interface I18nManager {
|
||||
configure(options?: I18nConfig): void;
|
||||
registerStrings(strings: TranslationString[], version: number): Promise<void>;
|
||||
getString(componentId: string, defaultValue?: string): Promise<string>;
|
||||
getStringSync(componentId: string, defaultValue?: string): string;
|
||||
clearCache(): Promise<void>;
|
||||
getApiUrl(): string;
|
||||
}
|
||||
|
||||
const createI18nManager = (): I18nManager => {
|
||||
/** Database name for IndexedDB storage */
|
||||
const DB_NAME = 'i18n-cache';
|
||||
/** Store name for translations within IndexedDB */
|
||||
const STORE_NAME = 'translations';
|
||||
/** Current version of the translations schema */
|
||||
const CURRENT_VERSION = 1;
|
||||
/** Default API endpoint if none provided */
|
||||
const DEFAULT_API_URL = '/api/translations';
|
||||
|
||||
let db: IDBDatabase | null = null;
|
||||
let serverUrl: string = DEFAULT_API_URL;
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const pendingUpdates = new Set<string>();
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let isInitialized = false;
|
||||
|
||||
/**
|
||||
* Initializes the IndexedDB database and sets up the object store
|
||||
* @returns Promise that resolves when the database is ready
|
||||
*/
|
||||
const initDatabase = async (): Promise<void> => {
|
||||
if (isInitialized) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, CURRENT_VERSION);
|
||||
|
||||
request.onerror = () => reject(new Error('Failed to open database'));
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const database = (event.target as IDBOpenDBRequest).result;
|
||||
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
||||
const store = database.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
store.createIndex('version', 'version', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = async (event) => {
|
||||
db = (event.target as IDBOpenDBRequest).result;
|
||||
await loadCacheFromDb();
|
||||
isInitialized = true;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads all translations from IndexedDB into memory cache
|
||||
*/
|
||||
const loadCacheFromDb = async (): Promise<void> => {
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const entries = (event.target as IDBRequest).result;
|
||||
entries.forEach(entry => {
|
||||
cache.set(entry.id, {
|
||||
value: entry.value,
|
||||
version: entry.version
|
||||
});
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => reject(new Error('Failed to load cache'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers new translations in both IndexedDB and memory cache
|
||||
* @param strings - Array of translation strings to register
|
||||
* @param version - Version number for these translations
|
||||
*/
|
||||
const registerStrings = async (strings: TranslationString[], version: number): Promise<void> => {
|
||||
if (!initPromise) configure();
|
||||
await initPromise;
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
strings.forEach(string => {
|
||||
const entry = {
|
||||
id: string.id,
|
||||
value: string.value,
|
||||
version: version
|
||||
};
|
||||
store.put(entry);
|
||||
cache.set(string.id, {
|
||||
value: string.value,
|
||||
version: version
|
||||
});
|
||||
});
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(new Error('Failed to register strings'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a translation from the server
|
||||
* @param componentId - Identifier for the translation to fetch
|
||||
* @returns The translated string or null if fetch fails
|
||||
*/
|
||||
const fetchFromServer = async (componentId: string): Promise<string | null> => {
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/${componentId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch translation');
|
||||
}
|
||||
const data: TranslationResponse = await response.json();
|
||||
|
||||
await registerStrings([{
|
||||
id: componentId,
|
||||
value: data.value
|
||||
}], data.version);
|
||||
|
||||
return data.value;
|
||||
} catch (error) {
|
||||
console.error('Error fetching translation:', error);
|
||||
return null;
|
||||
} finally {
|
||||
pendingUpdates.delete(componentId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits a custom event when translations are updated
|
||||
* @param id - Identifier of the updated translation
|
||||
* @param value - New translated value
|
||||
*/
|
||||
const emitUpdateEvent = (id: string, value: string): void => {
|
||||
const event = new CustomEvent('i18n-updated', {
|
||||
detail: { id, value }
|
||||
}) as I18nUpdateEvent;
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronously retrieves a translation with background update check
|
||||
* @param componentId - Identifier for the translation
|
||||
* @param defaultValue - Fallback value if translation not found
|
||||
* @returns The translated string or default value
|
||||
*/
|
||||
const getStringSync = (componentId: string, defaultValue = ''): string => {
|
||||
if (!isInitialized) {
|
||||
console.warn('I18nManager not initialized. Call configure() first or await initialization.');
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const cached = cache.get(componentId);
|
||||
if (cached) {
|
||||
if (cached.version !== CURRENT_VERSION && !pendingUpdates.has(componentId)) {
|
||||
pendingUpdates.add(componentId);
|
||||
fetchFromServer(componentId).then(newValue => {
|
||||
if (newValue) {
|
||||
emitUpdateEvent(componentId, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
if (!pendingUpdates.has(componentId)) {
|
||||
pendingUpdates.add(componentId);
|
||||
fetchFromServer(componentId).then(newValue => {
|
||||
if (newValue) {
|
||||
emitUpdateEvent(componentId, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves a translation
|
||||
* @param componentId - Identifier for the translation
|
||||
* @param defaultValue - Fallback value if translation not found
|
||||
* @returns Promise resolving to the translated string
|
||||
*/
|
||||
const getString = async (componentId: string, defaultValue = ''): Promise<string> => {
|
||||
if (!initPromise) configure();
|
||||
await initPromise;
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise( (resolve) => {
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get(componentId);
|
||||
|
||||
request.onsuccess = async (event) => {
|
||||
const result = (event.target as IDBRequest).result;
|
||||
|
||||
if (result && result.version === CURRENT_VERSION) {
|
||||
resolve(result.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverValue = await fetchFromServer(componentId);
|
||||
resolve(serverValue || defaultValue);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('Error reading from cache');
|
||||
resolve(defaultValue);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears all cached translations
|
||||
*/
|
||||
const clearCache = async (): Promise<void> => {
|
||||
if (!initPromise) configure();
|
||||
await initPromise;
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
cache.clear();
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(new Error('Failed to clear cache'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the I18n manager
|
||||
* @param options - Configuration options
|
||||
*/
|
||||
const configure = (options: I18nConfig = {}): void => {
|
||||
if (!isInitialized) {
|
||||
serverUrl = options.apiUrl || DEFAULT_API_URL;
|
||||
initPromise = initDatabase();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.apiUrl) {
|
||||
serverUrl = options.apiUrl;
|
||||
pendingUpdates.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current API URL
|
||||
*/
|
||||
const getApiUrl = (): string => serverUrl;
|
||||
|
||||
return {
|
||||
configure,
|
||||
registerStrings,
|
||||
getString,
|
||||
getStringSync,
|
||||
clearCache,
|
||||
getApiUrl
|
||||
};
|
||||
};
|
||||
|
||||
// Create the singleton instance
|
||||
const i18nManager = createI18nManager();
|
||||
|
||||
// Export the main manager
|
||||
export const i18n = i18nManager;
|
||||
|
||||
// Export shortcut functions
|
||||
export const _t = i18nManager.getStringSync;
|
||||
export const _tt = i18nManager.getString;
|
||||
|
||||
// Export everything as a namespace
|
||||
export default {
|
||||
...i18nManager,
|
||||
_t,
|
||||
_tt
|
||||
};
|
||||
|
||||
// Type declarations for the shortcut functions
|
||||
export type GetStringSync = typeof i18nManager.getStringSync;
|
||||
export type GetString = typeof i18nManager.getString;
|
8
src/index.ts
Normal file
8
src/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export * from './base64'
|
||||
export * from './strings'
|
||||
export * from './mime'
|
||||
export * from './promise'
|
||||
export * from './i18n'
|
||||
export * from './dataqueue'
|
||||
//export * from './logger'
|
43
src/logger/examples.ts
Normal file
43
src/logger/examples.ts
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
// // Example server reporting plugin
|
||||
// const createServerReportingPlugin = (endpoint: string): LoggerPlugin => ({
|
||||
// name: 'ServerReportingPlugin',
|
||||
// onLog: async (entry: LogEntry): Promise<void> => {
|
||||
// // Only send errors and warnings to the server
|
||||
// if (entry.level === 'ERROR' || entry.level === 'WARN') {
|
||||
// try {
|
||||
// const response = await fetch(endpoint, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify(entry)
|
||||
// });
|
||||
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to send log to server:', error);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Example local storage plugin
|
||||
// const createLocalStoragePlugin = (maxEntries: number = 100): LoggerPlugin => {
|
||||
// const STORAGE_KEY = 'app_logs';
|
||||
|
||||
// return {
|
||||
// name: 'LocalStoragePlugin',
|
||||
// onLog: async (entry: LogEntry): Promise<void> => {
|
||||
// try {
|
||||
// const storedLogs = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
||||
// const updatedLogs = [entry, ...storedLogs].slice(0, maxEntries);
|
||||
// localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedLogs));
|
||||
// } catch (error) {
|
||||
// console.error('Failed to store log in localStorage:', error);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// };
|
102
src/logger/index.ts
Normal file
102
src/logger/index.ts
Normal file
@ -0,0 +1,102 @@
|
||||
// Types
|
||||
type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
|
||||
|
||||
type LogEntry = {
|
||||
timestamp: Date;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
type LoggerPlugin = {
|
||||
name: string;
|
||||
onLog: (entry: LogEntry) => Promise<void>;
|
||||
};
|
||||
|
||||
type Logger = {
|
||||
error: (message: string, data?: unknown) => Promise<void>;
|
||||
warn: (message: string, data?: unknown) => Promise<void>;
|
||||
info: (message: string, data?: unknown) => Promise<void>;
|
||||
debug: (message: string, data?: unknown) => Promise<void>;
|
||||
registerPlugin: (plugin: LoggerPlugin) => void;
|
||||
};
|
||||
|
||||
// Create logger instance
|
||||
const createLogger = (): Logger => {
|
||||
const plugins: LoggerPlugin[] = [];
|
||||
|
||||
const getLogLevelEmoji = (level: LogLevel): string => {
|
||||
switch (level) {
|
||||
case 'ERROR':
|
||||
return '🔥'; // Fire for errors
|
||||
case 'WARN':
|
||||
return '⚠️'; // Warning sign for warnings
|
||||
case 'INFO':
|
||||
return '💡'; // Light bulb for info
|
||||
case 'DEBUG':
|
||||
return '🔍'; // Magnifying glass for debug
|
||||
}
|
||||
};
|
||||
|
||||
const formatMessage = (entry: LogEntry): string => {
|
||||
const timestamp = entry.timestamp.toISOString();
|
||||
const emoji = getLogLevelEmoji(entry.level);
|
||||
let message = `${emoji} [${timestamp}] ${entry.level}: ${entry.message}`;
|
||||
if (entry.data) {
|
||||
message += `\nData: ${JSON.stringify(entry.data, null, 2)}`;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
const notifyPlugins = async (entry: LogEntry): Promise<void> => {
|
||||
const pluginPromises = plugins.map(plugin =>
|
||||
plugin.onLog(entry).catch(error => {
|
||||
console.error(`Plugin "${plugin.name}" failed:`, error);
|
||||
})
|
||||
);
|
||||
await Promise.all(pluginPromises);
|
||||
};
|
||||
|
||||
const log = async (level: LogLevel, message: string, data?: unknown): Promise<void> => {
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date(),
|
||||
level,
|
||||
message,
|
||||
data
|
||||
};
|
||||
|
||||
const formattedMessage = formatMessage(entry);
|
||||
|
||||
// Console output with color coding
|
||||
switch (level) {
|
||||
case 'ERROR':
|
||||
console.error(formattedMessage);
|
||||
break;
|
||||
case 'WARN':
|
||||
console.warn(formattedMessage);
|
||||
break;
|
||||
case 'INFO':
|
||||
console.info(formattedMessage);
|
||||
break;
|
||||
case 'DEBUG':
|
||||
console.debug(formattedMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
await notifyPlugins(entry);
|
||||
};
|
||||
|
||||
return {
|
||||
error: (message: string, data?: unknown) => log('ERROR', message, data),
|
||||
warn: (message: string, data?: unknown) => log('WARN', message, data),
|
||||
info: (message: string, data?: unknown) => log('INFO', message, data),
|
||||
debug: (message: string, data?: unknown) => log('DEBUG', message, data),
|
||||
registerPlugin: (plugin: LoggerPlugin) => {
|
||||
plugins.push(plugin);
|
||||
console.log(`🔌 Plugin "${plugin.name}" registered`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export {createLogger}
|
||||
export type {LogEntry,LogLevel,LoggerPlugin,Logger}
|
80
src/mime/index.ts
Normal file
80
src/mime/index.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { MimeTypeList } from "./mime";
|
||||
|
||||
/**
|
||||
* Gets file extension from MIME type, with substring matching
|
||||
* @param pMime - MIME type string (e.g. "image/jpeg")
|
||||
* @returns First matching extension or empty string if not found
|
||||
*/
|
||||
export const getExtFromMime = (pMime?: string): string => {
|
||||
if (!pMime) return "";
|
||||
const mime = pMime.toLowerCase().trim();
|
||||
|
||||
// Try exact match first
|
||||
const exactMatch = MimeTypeList[mime]?.extensions?.[0];
|
||||
if (exactMatch) return exactMatch;
|
||||
|
||||
// Try substring match
|
||||
const keys = Object.keys(MimeTypeList);
|
||||
for (const key of keys) {
|
||||
if (key.includes(mime) || mime.includes(key)) {
|
||||
return MimeTypeList[key]?.extensions?.[0] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the file extension from a filename
|
||||
* @param pFilename - Name of file including extension
|
||||
* @returns Extension without dot or empty string if no extension
|
||||
*/
|
||||
export const getExtFromFilename = (pFilename: string): string => {
|
||||
if (!pFilename) return "";
|
||||
const parts = pFilename.trim().split(".");
|
||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the MIME type for a file extension
|
||||
* @param pExt - File extension with or without dot
|
||||
* @returns MIME type string or empty string if not found
|
||||
*/
|
||||
export const getMimeFromExt = (pExt: string): string => {
|
||||
if (!pExt) return "";
|
||||
pExt = pExt.replace(".", "");
|
||||
const keys = Object.keys(MimeTypeList);
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
const mime = MimeTypeList[keys[i]]?.extensions?.find(
|
||||
(v: string) => v.toLowerCase() === pExt.toLowerCase()
|
||||
);
|
||||
if (mime && mime !== "") {
|
||||
return keys[i];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a file extension is valid for a given MIME type
|
||||
* @param pMime - MIME type string
|
||||
* @param pExt - File extension to check
|
||||
* @returns boolean indicating if extension matches MIME type
|
||||
*/
|
||||
export const isValidExtForMime = (pMime: string, pExt: string): boolean => {
|
||||
if (!pMime || !pExt) return false;
|
||||
const mime = pMime.toLowerCase().trim();
|
||||
const ext = pExt.replace(".", "").toLowerCase().trim();
|
||||
return MimeTypeList[mime]?.extensions?.includes(ext) ?? false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all valid extensions for a MIME type
|
||||
* @param pMime - MIME type string
|
||||
* @returns Array of valid extensions or empty array if none found
|
||||
*/
|
||||
export const getAllExtensionsForMime = (pMime: string): string[] => {
|
||||
if (!pMime) return [];
|
||||
const mime = pMime.toLowerCase().trim();
|
||||
return MimeTypeList[mime]?.extensions ?? [];
|
||||
};
|
8878
src/mime/mime.ts
Normal file
8878
src/mime/mime.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
src/object/compare.ts
Normal file
38
src/object/compare.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Performs object comparison with optional deep comparison
|
||||
* @param obj First object to compare
|
||||
* @param objToCompare Second object to compare
|
||||
* @param deep Enable deep comparison for nested objects/arrays
|
||||
*/
|
||||
export function objectCompare<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
objToCompare: T,
|
||||
deep = false
|
||||
): boolean {
|
||||
if (!obj || !objToCompare) return false;
|
||||
|
||||
return Object.keys(obj).length === Object.keys(objToCompare).length &&
|
||||
Object.keys(obj).every((key) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(objToCompare, key)) return false;
|
||||
|
||||
const val1 = obj[key];
|
||||
const val2 = objToCompare[key];
|
||||
|
||||
if (!deep) return val1 === val2;
|
||||
|
||||
if (Array.isArray(val1) && Array.isArray(val2)) {
|
||||
return val1.length === val2.length &&
|
||||
val1.every((item, i) =>
|
||||
typeof item === 'object' && item !== null
|
||||
? objectCompare(item as Record<string, unknown>, val2[i] as Record<string, unknown>, true)
|
||||
: item === val2[i]
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) {
|
||||
return objectCompare(val1 as Record<string, unknown>, val2 as Record<string, unknown>, true);
|
||||
}
|
||||
|
||||
return val1 === val2;
|
||||
});
|
||||
}
|
3
src/object/index.ts
Normal file
3
src/object/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export {getNestedValue,setNestedValue} from './nested'
|
||||
export {objectCompare} from './compare'
|
||||
export {createSelectOptions} from './util'
|
46
src/object/nested.ts
Normal file
46
src/object/nested.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Gets a nested value from an object using a path string
|
||||
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
|
||||
* @param obj - Source object to extract value from
|
||||
* @returns Value at path or undefined if path invalid
|
||||
*/
|
||||
export function getNestedValue(path: string, obj: Record<string, any>): any {
|
||||
return path
|
||||
.replace(/\[(\w+)\]/g, ".$1") // Convert brackets to dot notation
|
||||
.split(".") // Split path into parts
|
||||
.reduce((prev, curr) => prev?.[curr], obj); // Traverse object
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sets a nested value in an object using a path string
|
||||
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
|
||||
* @param value - Value to set at path
|
||||
* @param obj - Target object to modify
|
||||
* @returns Modified object
|
||||
*/
|
||||
/**
|
||||
* Sets a nested value, creating objects and arrays if needed
|
||||
*/
|
||||
export function setNestedValue(path: string, value: any, obj: Record<string, any>): Record<string, any> {
|
||||
const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".");
|
||||
const lastKey = parts.pop()!;
|
||||
|
||||
const target = parts.reduce((prev, curr) => {
|
||||
// Handle array indices
|
||||
if (/^\d+$/.test(curr)) {
|
||||
if (!Array.isArray(prev[curr])) {
|
||||
prev[curr] = [];
|
||||
}
|
||||
}
|
||||
// Create missing objects
|
||||
else if (!prev[curr]) {
|
||||
prev[curr] = {};
|
||||
}
|
||||
return prev[curr];
|
||||
}, obj);
|
||||
|
||||
target[lastKey] = value;
|
||||
return obj;
|
||||
}
|
49
src/object/util.ts
Normal file
49
src/object/util.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Converts an object or array into an array of key-value pairs with customizable property names.
|
||||
* Commonly used for transforming data into a format suitable for select/dropdown components.
|
||||
*
|
||||
* @param obj - The input object or array to be transformed
|
||||
* @param options - Configuration options for the transformation
|
||||
* @param options.labelKey - The key name to use for the label property (default: 'label')
|
||||
* @param options.valueKey - The key name to use for the value property (default: 'value')
|
||||
* @returns An array of objects with the specified label and value properties, or an empty array if input is invalid
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* createSelectOptions({ key1: "Label 1", key2: "Label 2" })
|
||||
* // Returns: [{ label: "Label 1", value: "key1" }, { label: "Label 2", value: "key2" }]
|
||||
*
|
||||
* // Custom key names
|
||||
* createSelectOptions(
|
||||
* { key1: "Label 1", key2: "Label 2" },
|
||||
* { labelKey: 'text', valueKey: 'id' }
|
||||
* )
|
||||
* // Returns: [{ text: "Label 1", id: "key1" }, { text: "Label 2", id: "key2" }]
|
||||
*/
|
||||
export function createSelectOptions<T = string>(
|
||||
obj: Record<string, T> | T[] | null | undefined,
|
||||
options: {
|
||||
labelKey?: string;
|
||||
valueKey?: string;
|
||||
} = {}
|
||||
): Array<Record<string, T | string>> {
|
||||
const {
|
||||
labelKey = 'label',
|
||||
valueKey = 'value'
|
||||
} = options;
|
||||
|
||||
// Return empty array for null or undefined input
|
||||
if (!obj) return [];
|
||||
|
||||
// Return the original array if input is already an array
|
||||
if (Array.isArray(obj)) return obj as Array<Record<string, T | string>>;
|
||||
|
||||
// Return empty array for non-object inputs (like numbers or strings)
|
||||
if (typeof obj !== 'object') return [];
|
||||
|
||||
// Transform object into array of key-value pairs with custom property names
|
||||
return Object.keys(obj).map((key) => ({
|
||||
[labelKey]: obj[key],
|
||||
[valueKey]: key
|
||||
}));
|
||||
}
|
84
src/promise/index.ts
Normal file
84
src/promise/index.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Configuration options for the WaitUntil function
|
||||
*/
|
||||
interface WaitUntilOptions {
|
||||
/** Maximum time to wait in milliseconds before timing out (default: 5000ms) */
|
||||
timeout?: number;
|
||||
/** Interval between condition checks in milliseconds (default: 100ms) */
|
||||
interval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously waits until a condition is met or times out
|
||||
* @param condition - Function that returns true when the wait condition is satisfied
|
||||
* @param options - Configuration options for timeout and check interval
|
||||
* @throws {Error} When the timeout period is exceeded
|
||||
* @returns Promise that resolves when the condition is met
|
||||
* @example
|
||||
* await WaitUntil(() => someElement.isVisible(), { timeout: 10000 });
|
||||
*/
|
||||
export const WaitUntil = async (
|
||||
condition: () => boolean,
|
||||
options?: WaitUntilOptions
|
||||
): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (!condition()) {
|
||||
return;
|
||||
}
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, options?.interval ?? 100);
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject(new Error("Wait Timeout"));
|
||||
}, options?.timeout ?? 5000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a debounced version of a function that delays its execution
|
||||
* until after the specified wait time has elapsed since the last call
|
||||
*/
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a throttled version of a function that executes at most once
|
||||
* during the specified interval
|
||||
*/
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let inThrottle = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Measures the execution time of an async function
|
||||
*/
|
||||
export const measureTime = async <T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<{ result: T; duration: number }> => {
|
||||
const start = performance.now();
|
||||
const result = await fn();
|
||||
const duration = performance.now() - start;
|
||||
return { result, duration };
|
||||
};
|
83
src/strings/caseConversion.ts
Normal file
83
src/strings/caseConversion.ts
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
/**
|
||||
* Capitalizes the first letter of each word in a sentence
|
||||
* @param sentence - Input string to transform
|
||||
* @returns Transformed string with initial capitals
|
||||
*/
|
||||
export const initCaps = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
return sentence.replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts text to title case, following standard title capitalization rules
|
||||
* Keeps articles, conjunctions, and prepositions in lowercase unless they start the title
|
||||
* @param sentence - Input string to transform
|
||||
* @returns Transformed string in title case
|
||||
*/
|
||||
export const titleCase = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
const smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i;
|
||||
|
||||
return sentence.toLowerCase().replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, (word, index, title) => {
|
||||
if (index > 0 && index + word.length !== title.length &&
|
||||
word.search(smallWords) > -1 && title.charAt(index - 2) !== ":" &&
|
||||
(title.charAt(index + word.length) !== '-' || title.charAt(index - 1) === '-') &&
|
||||
title.charAt(index - 1).search(/[^\s-]/) < 0) {
|
||||
return word.toLowerCase();
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.substr(1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts text to camelCase format
|
||||
* @param sentence - Input string to transform
|
||||
* @returns Transformed string in camelCase
|
||||
*/
|
||||
export const camelCase = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
return sentence
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) =>
|
||||
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
|
||||
.replace(/\s+/g, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts text to snake_case format
|
||||
* @param sentence - Input string to transform
|
||||
* @returns Transformed string in snake_case
|
||||
*/
|
||||
export const snakeCase = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
return sentence
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
||||
.replace(/^_/, '')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts snake_case to camelCase
|
||||
* @param sentence - Input string in snake_case
|
||||
* @returns Transformed string in camelCase
|
||||
*/
|
||||
export const reverseSnakeCase = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
return sentence
|
||||
.toLowerCase()
|
||||
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits camelCase text into space-separated words
|
||||
* @param sentence - Input string in camelCase
|
||||
* @returns Space-separated string
|
||||
*/
|
||||
export const splitCamelCase = (sentence: string): string => {
|
||||
if (!sentence) return sentence;
|
||||
return sentence
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
||||
.trim();
|
||||
};
|
36
src/strings/fileSize.ts
Normal file
36
src/strings/fileSize.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Converts bytes to human readable file size string with units
|
||||
* @param bytes - Number of bytes to format
|
||||
* @param si - Use SI units (1000) instead of binary units (1024)
|
||||
* @param dp - Decimal places to display
|
||||
* @returns Formatted string with appropriate unit suffix
|
||||
*/
|
||||
export function humanFileSize(bytes: number, si: boolean = false, dp: number = 1): string {
|
||||
// Define base unit (1000 for SI, 1024 for binary)
|
||||
const thresh: number = si ? 1000 : 1024;
|
||||
|
||||
// Return bytes if below threshold
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
// Define unit suffixes based on SI/binary preference
|
||||
const units: string[] = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
|
||||
let u: number = -1;
|
||||
const r: number = 10 ** dp;
|
||||
|
||||
// Divide by threshold until result is less than threshold or we run out of units
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (
|
||||
Math.round(Math.abs(bytes) * r) / r >= thresh &&
|
||||
u < units.length - 1
|
||||
);
|
||||
|
||||
// Format with specified decimal places and appropriate unit
|
||||
return `${bytes.toFixed(dp)} ${units[u]}`;
|
||||
}
|
6
src/strings/index.ts
Normal file
6
src/strings/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './trim'
|
||||
export * from './replace'
|
||||
export * from './caseConversion'
|
||||
export * from './locale'
|
||||
export * from './fileSize'
|
||||
export * from './legacy'
|
278
src/strings/legacy.ts
Normal file
278
src/strings/legacy.ts
Normal file
@ -0,0 +1,278 @@
|
||||
|
||||
/**
|
||||
* Checks if a value exists in an array or matches a single value
|
||||
* @param args - First argument is the source (array/value), followed by values to check
|
||||
* @returns True if any subsequent argument matches source or exists in source array
|
||||
*/
|
||||
export function inop(...args: unknown[]): boolean {
|
||||
if (args.length < 2) return false;
|
||||
|
||||
const [source, ...searchValues] = args;
|
||||
|
||||
// Handle array-like objects
|
||||
if (source !== null && 'length' in (source as { length?: number })) {
|
||||
const arr = Array.from(source as ArrayLike<unknown>);
|
||||
return searchValues.some(val => arr.includes(val));
|
||||
}
|
||||
|
||||
// Handle single value comparison
|
||||
return searchValues.some(val => source === val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive version of inop() for string comparisons
|
||||
* @param args - First argument is source (string/array), followed by values to check
|
||||
* @returns True if any subsequent argument matches source with case-insensitive comparison
|
||||
*/
|
||||
export function iinop(...args: unknown[]): boolean {
|
||||
if (args.length < 2) return false;
|
||||
|
||||
const [source, ...searchValues] = args;
|
||||
|
||||
// Handle array-like objects
|
||||
if (source !== null && 'length' in (source as { length?: number })) {
|
||||
const arr = Array.from(source as ArrayLike<unknown>);
|
||||
return searchValues.some(val => {
|
||||
return arr.some(item => {
|
||||
if (typeof item === 'string' && typeof val === 'string') {
|
||||
return item.toLowerCase() === val.toLowerCase();
|
||||
}
|
||||
return item === val;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle single value comparison
|
||||
if (typeof source === 'string') {
|
||||
return searchValues.some(val => {
|
||||
if (typeof val === 'string') {
|
||||
return source.toLowerCase() === val.toLowerCase();
|
||||
}
|
||||
return source === val;
|
||||
});
|
||||
}
|
||||
|
||||
return searchValues.some(val => source === val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base date for Clarion date calculations (December 28, 1800)
|
||||
*/
|
||||
const CLARION_EPOCH = new Date(1800, 11, 28);
|
||||
|
||||
/**
|
||||
* Converts a Clarion integer time value to a formatted time string
|
||||
* @param val - Clarion time value (HHMMSS.CC format where CC is centiseconds)
|
||||
* @param detail - If true, includes centiseconds in output
|
||||
* @returns Formatted time string (HH:MM:SS or HH:MM:SS.CC)
|
||||
*/
|
||||
export function clarionIntToTime(val: number, detail?: boolean): string {
|
||||
// Ensure non-negative value
|
||||
if (val < 0) {
|
||||
val = 0;
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
const sec_num = val / 100;
|
||||
|
||||
// Extract time components
|
||||
const hours: number = Math.floor(val / 360000);
|
||||
const minutes: number = Math.floor((sec_num - hours * 3600) / 60);
|
||||
const seconds: number = Math.floor(
|
||||
sec_num - hours * 3600 - minutes * 60
|
||||
);
|
||||
const ms: number = Math.floor(
|
||||
val - hours * 360000 - minutes * 6000 - seconds * 100
|
||||
);
|
||||
|
||||
// Format time components with leading zeros
|
||||
const paddedHours = hours.toString().padStart(2, '0');
|
||||
const paddedMinutes = minutes.toString().padStart(2, '0');
|
||||
const paddedSeconds = seconds.toString().padStart(2, '0');
|
||||
|
||||
// Handle centiseconds formatting
|
||||
let msString: string;
|
||||
if (ms < 10) {
|
||||
msString = ms.toString() + '00';
|
||||
} else if (ms < 100) {
|
||||
msString = ms.toString() + '0';
|
||||
} else {
|
||||
msString = ms.toString();
|
||||
}
|
||||
|
||||
// Return formatted time string
|
||||
return detail
|
||||
? `${paddedHours}:${paddedMinutes}:${paddedSeconds}.${msString}`
|
||||
: `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a time string to a Clarion integer time value
|
||||
* @param timeStr - Time string in format "HH:MM:SS" or "HH:MM:SS.CC"
|
||||
* @returns Clarion time value (HHMMSS.CC format where CC is centiseconds)
|
||||
* @throws Error if the time string format is invalid
|
||||
*/
|
||||
export function clarionTimeToInt(timeStr: string): number {
|
||||
// Regular expressions to match both formats
|
||||
const basicTimeRegex = /^(\d{2}):(\d{2}):(\d{2})$/;
|
||||
const detailedTimeRegex = /^(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/;
|
||||
|
||||
let hours: number;
|
||||
let minutes: number;
|
||||
let seconds: number;
|
||||
let centiseconds: number = 0;
|
||||
|
||||
// Try matching both formats
|
||||
const basicMatch = timeStr.match(basicTimeRegex);
|
||||
const detailedMatch = timeStr.match(detailedTimeRegex);
|
||||
|
||||
if (detailedMatch) {
|
||||
// Parse detailed time format (HH:MM:SS.CC)
|
||||
[, hours, minutes, seconds, centiseconds] = detailedMatch.map(Number);
|
||||
|
||||
// Handle different centisecond precision
|
||||
if (centiseconds < 10) {
|
||||
centiseconds *= 100;
|
||||
} else if (centiseconds < 100) {
|
||||
centiseconds *= 10;
|
||||
}
|
||||
} else if (basicMatch) {
|
||||
// Parse basic time format (HH:MM:SS)
|
||||
[, hours, minutes, seconds] = basicMatch.map(Number);
|
||||
} else {
|
||||
throw new Error('Invalid time format. Expected HH:MM:SS or HH:MM:SS.CC');
|
||||
}
|
||||
|
||||
// Validate time components
|
||||
if (hours >= 24 || minutes >= 60 || seconds >= 60 || centiseconds >= 1000) {
|
||||
throw new Error('Invalid time values');
|
||||
}
|
||||
|
||||
// Convert to Clarion integer format
|
||||
return hours * 360000 + minutes * 6000 + seconds * 100 + centiseconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current Clarion clock value (centiseconds since midnight)
|
||||
* @returns Number of centiseconds since midnight
|
||||
*/
|
||||
export function clarionClock(): number {
|
||||
// Get current date and midnight
|
||||
const today = new Date();
|
||||
const midnight = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate milliseconds since midnight
|
||||
const millisecondsPassed = today.getTime() - midnight.getTime();
|
||||
|
||||
// Convert to centiseconds and add 1 (Clarion offset)
|
||||
// Division by 10 converts milliseconds to centiseconds
|
||||
return Math.floor(millisecondsPassed / 10 + 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a JavaScript Date object to a Clarion date integer
|
||||
* @param date - JavaScript Date object
|
||||
* @returns Number of days since December 28, 1800 (Clarion date format)
|
||||
*/
|
||||
export function clarionDateToInt(date: Date): number {
|
||||
// Clone the input date to avoid modifying it
|
||||
const inputDate = new Date(date);
|
||||
|
||||
// Set time to noon to avoid daylight saving time issues
|
||||
inputDate.setHours(12, 0, 0, 0);
|
||||
CLARION_EPOCH.setHours(12, 0, 0, 0);
|
||||
|
||||
// Calculate days difference
|
||||
const diffTime = inputDate.getTime() - CLARION_EPOCH.getTime();
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Clarion date integer to a JavaScript Date object
|
||||
* @param days - Number of days since December 28, 1800 (Clarion date format)
|
||||
* @returns JavaScript Date object
|
||||
*/
|
||||
export function clarionIntToDate(days: number): Date {
|
||||
// Create new date to avoid modifying the epoch constant
|
||||
const resultDate = new Date(CLARION_EPOCH);
|
||||
|
||||
// Set to noon to avoid daylight saving time issues
|
||||
resultDate.setHours(12, 0, 0, 0);
|
||||
|
||||
// Add the days
|
||||
resultDate.setDate(CLARION_EPOCH.getDate() + days);
|
||||
|
||||
return resultDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date string to a Clarion date integer
|
||||
* @param dateStr - Date string in format "YYYY-MM-DD" or "MM/DD/YYYY"
|
||||
* @returns Number of days since December 28, 1800 (Clarion date format)
|
||||
* @throws Error if the date string format is invalid
|
||||
*/
|
||||
export function clarionDateStringToInt(dateStr: string): number {
|
||||
// Regular expressions for supported date formats
|
||||
const isoFormatRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // YYYY-MM-DD
|
||||
const usFormatRegex = /^(\d{2})\/(\d{2})\/(\d{4})$/; // MM/DD/YYYY
|
||||
|
||||
let year: number;
|
||||
let month: number;
|
||||
let day: number;
|
||||
|
||||
const isoMatch = dateStr.match(isoFormatRegex);
|
||||
const usMatch = dateStr.match(usFormatRegex);
|
||||
|
||||
if (isoMatch) {
|
||||
[, year, month, day] = isoMatch.map(Number);
|
||||
month--; // JavaScript months are 0-based
|
||||
} else if (usMatch) {
|
||||
[, month, day, year] = usMatch.map(Number);
|
||||
month--; // JavaScript months are 0-based
|
||||
} else {
|
||||
throw new Error('Invalid date format. Expected YYYY-MM-DD or MM/DD/YYYY');
|
||||
}
|
||||
|
||||
// Validate date components
|
||||
if (month < 0 || month > 11 || day < 1 || day > 31 || year < 1800) {
|
||||
throw new Error('Invalid date values');
|
||||
}
|
||||
|
||||
const date = new Date(year, month, day);
|
||||
|
||||
// Check if the date is valid
|
||||
if (date.getMonth() !== month || date.getDate() !== day) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
|
||||
return clarionDateToInt(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Clarion date integer to a formatted date string
|
||||
* @param days - Number of days since December 28, 1800 (Clarion date format)
|
||||
* @param format - Output format ('iso' for YYYY-MM-DD or 'us' for MM/DD/YYYY)
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
export function clarionIntToDateString(days: number, format: 'iso' | 'us' = 'iso'): string {
|
||||
const date = clarionIntToDate(days);
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
return format === 'iso'
|
||||
? `${year}-${month}-${day}`
|
||||
: `${month}/${day}/${year}`;
|
||||
}
|
175
src/strings/locale.ts
Normal file
175
src/strings/locale.ts
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Comprehensive localization utilities for handling text, numbers, dates, and currencies
|
||||
*/
|
||||
|
||||
// type LocaleConfig = {
|
||||
// dateFormat: string;
|
||||
// numberFormat: {
|
||||
// decimal: string;
|
||||
// thousands: string;
|
||||
// precision: number;
|
||||
// };
|
||||
// currency: {
|
||||
// symbol: string;
|
||||
// position: 'prefix' | 'suffix';
|
||||
// };
|
||||
// pluralRules?: Intl.PluralRules;
|
||||
// };
|
||||
|
||||
// const defaultConfig: LocaleConfig = {
|
||||
// dateFormat: 'YYYY-MM-DD',
|
||||
// numberFormat: {
|
||||
// decimal: '.',
|
||||
// thousands: ',',
|
||||
// precision: 2
|
||||
// },
|
||||
// currency: {
|
||||
// symbol: '$',
|
||||
// position: 'prefix'
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* Formats a number according to locale settings
|
||||
*/
|
||||
export const formatNumber = (
|
||||
value: number,
|
||||
locale: string,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
return new Intl.NumberFormat(locale, options).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats currency according to locale settings
|
||||
*/
|
||||
export const formatCurrency = (
|
||||
value: number,
|
||||
locale: string,
|
||||
currency: string
|
||||
): string => {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a date according to locale settings
|
||||
*/
|
||||
export const formatDate = (
|
||||
date: Date,
|
||||
locale: string,
|
||||
options?: Intl.DateTimeFormatOptions
|
||||
): string => {
|
||||
return new Intl.DateTimeFormat(locale, options).format(date);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats relative time (e.g., "2 days ago")
|
||||
*/
|
||||
export const formatRelativeTime = (
|
||||
value: number,
|
||||
unit: Intl.RelativeTimeFormatUnit,
|
||||
locale: string
|
||||
): string => {
|
||||
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
||||
return rtf.format(value, unit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles plural forms based on locale rules
|
||||
*/
|
||||
export const getPlural = (
|
||||
count: number,
|
||||
locale: string,
|
||||
forms: { [key: string]: string }
|
||||
): string => {
|
||||
const pluralRules = new Intl.PluralRules(locale);
|
||||
const rule = pluralRules.select(count);
|
||||
return forms[rule] || forms['other'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats lists according to locale conventions
|
||||
*/
|
||||
export const formatList = (
|
||||
items: string[],
|
||||
locale: string,
|
||||
type: 'conjunction' | 'disjunction' = 'conjunction'
|
||||
): string => {
|
||||
return new Intl.ListFormat(locale, { type }).format(items);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares strings according to locale rules
|
||||
*/
|
||||
export const compareStrings = (
|
||||
str1: string,
|
||||
str2: string,
|
||||
locale: string
|
||||
): number => {
|
||||
return new Intl.Collator(locale).compare(str1, str2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats percentages according to locale
|
||||
*/
|
||||
export const formatPercent = (
|
||||
value: number,
|
||||
locale: string,
|
||||
decimals: number = 0
|
||||
): string => {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats units according to locale
|
||||
*/
|
||||
export const formatUnit = (
|
||||
value: number,
|
||||
unit: string,
|
||||
locale: string
|
||||
): string => {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'unit',
|
||||
unit: unit
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts number words to digits based on locale
|
||||
*/
|
||||
export const parseNumberWords = (
|
||||
text: string,
|
||||
locale: string
|
||||
): number | null => {
|
||||
const numberWords: { [key: string]: number } = {
|
||||
zero: 0, one: 1, two: 2, three: 3, four: 4,
|
||||
five: 5, six: 6, seven: 7, eight: 8, nine: 9
|
||||
};
|
||||
|
||||
const localizedWords = Object.keys(numberWords).map(word =>
|
||||
new Intl.NumberFormat(locale).format(numberWords[word])
|
||||
);
|
||||
|
||||
const normalized = text.toLowerCase();
|
||||
for (let i = 0; i < localizedWords.length; i++) {
|
||||
if (normalized.includes(localizedWords[i].toLowerCase())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles bi-directional text
|
||||
*/
|
||||
export const handleBiDi = (text: string): string => {
|
||||
// Add Unicode control characters for bi-directional text
|
||||
return `\u202A${text}\u202C`;
|
||||
};
|
72
src/strings/replace.ts
Normal file
72
src/strings/replace.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// biome-rule-off
|
||||
|
||||
// biome-disable lint/style/noInferrableTypes
|
||||
/**
|
||||
* Replaces a specific occurrence of a substring in a string
|
||||
* @param {string} str - The input string
|
||||
* @param {string} search - The string to search for
|
||||
* @param {string} replacement - The string to replace with
|
||||
* @param {number} [occurrence=1] - Which occurrence to replace (1-based). Defaults to first occurrence
|
||||
* @returns {string} The string with the specified occurrence replaced
|
||||
*/
|
||||
export function replaceStr(
|
||||
str: string,
|
||||
search: string,
|
||||
replacement: string,
|
||||
// biome-ignore lint: Stupid biome Rule
|
||||
occurrence: number = 1
|
||||
): string {
|
||||
if (!str || !search || occurrence < 1) return str
|
||||
|
||||
let currentIndex = 0
|
||||
let currentOccurrence = 0
|
||||
|
||||
while (currentIndex < str.length) {
|
||||
const index = str.indexOf(search, currentIndex)
|
||||
if (index === -1) break
|
||||
|
||||
currentOccurrence++
|
||||
if (currentOccurrence === occurrence) {
|
||||
return (
|
||||
str.slice(0, index) + replacement + str.slice(index + search.length)
|
||||
)
|
||||
}
|
||||
|
||||
currentIndex = index + 1
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces multiple occurrences of a substring in a string
|
||||
* @param {string} str - The input string
|
||||
* @param {string} search - The string to search for
|
||||
* @param {string} replacement - The string to replace with
|
||||
* @param {number} [times=Infinity] - Maximum number of replacements to make. Defaults to all occurrences
|
||||
* @returns {string} The string with the specified number of occurrences replaced
|
||||
*/
|
||||
export function replaceStrAll(
|
||||
str: string,
|
||||
search: string,
|
||||
replacement: string,
|
||||
times: number = Number.POSITIVE_INFINITY
|
||||
): string {
|
||||
if (!str || !search || times < 1) return str
|
||||
|
||||
let result = str
|
||||
let currentIndex = 0
|
||||
let count = 0
|
||||
|
||||
while (currentIndex < result.length && count < times) {
|
||||
const index = result.indexOf(search, currentIndex)
|
||||
if (index === -1) break
|
||||
|
||||
result =
|
||||
result.slice(0, index) + replacement + result.slice(index + search.length)
|
||||
currentIndex = index + replacement.length
|
||||
count++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
53
src/strings/trim.ts
Normal file
53
src/strings/trim.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Trims specified characters from the left side of a string a specified number of times.
|
||||
* @param {string} str - The input string to trim
|
||||
* @param {string} [chars=' '] - The characters to trim (if multiple, any of them will be trimmed)
|
||||
* @param {number} [times=Infinity] - Number of times to trim. Defaults to all occurrences
|
||||
* @returns {string} The trimmed string
|
||||
*/
|
||||
export function trimLeft(
|
||||
str: string,
|
||||
// biome-ignore lint: Stupid biome Rule
|
||||
chars: string = ' ',
|
||||
times: number = Number.POSITIVE_INFINITY
|
||||
): string {
|
||||
if (!str) return str
|
||||
|
||||
let count = 0
|
||||
let startIdx = 0
|
||||
const charSet = new Set(chars)
|
||||
|
||||
while (startIdx < str.length && charSet.has(str[startIdx]) && count < times) {
|
||||
startIdx++
|
||||
count++
|
||||
}
|
||||
|
||||
return str.slice(startIdx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims specified characters from the right side of a string a specified number of times.
|
||||
* @param {string} str - The input string to trim
|
||||
* @param {string} [chars=' '] - The characters to trim (if multiple, any of them will be trimmed)
|
||||
* @param {number} [times=Infinity] - Number of times to trim. Defaults to all occurrences
|
||||
* @returns {string} The trimmed string
|
||||
*/
|
||||
export function trimRight(
|
||||
str: string,
|
||||
// biome-ignore lint: Stupid biome Rule
|
||||
chars: string = ' ',
|
||||
times: number = Number.POSITIVE_INFINITY
|
||||
): string {
|
||||
if (!str) return str
|
||||
|
||||
let count = 0
|
||||
let endIdx = str.length - 1
|
||||
const charSet = new Set(chars)
|
||||
|
||||
while (endIdx >= 0 && charSet.has(str[endIdx]) && count < times) {
|
||||
endIdx--
|
||||
count++
|
||||
}
|
||||
|
||||
return str.slice(0, endIdx + 1)
|
||||
}
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"target": "ES6",
|
||||
"declarationDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES6", "DOM","ESNext"],
|
||||
"emitDeclarationOnly": false
|
||||
}
|
||||
}
|
34
vite.config.js
Normal file
34
vite.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts'; // You'll need to install this
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es'],
|
||||
fileName: 'index'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'fs',
|
||||
'path',
|
||||
'url',
|
||||
'chalk',
|
||||
'semver',
|
||||
'yargs',
|
||||
'yargs/helpers'
|
||||
]
|
||||
},
|
||||
target: 'node16',
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
sourcemap: true
|
||||
}, plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
outDir: 'dist'
|
||||
})
|
||||
]
|
||||
});
|
Loading…
Reference in New Issue
Block a user