Compare commits

..

11 Commits

Author SHA1 Message Date
95f8d5762c Fixed import error
All checks were successful
Build / Build NPM Project (push) Successful in 57s
Build / Tag Version (push) Successful in 13s
Build / Publish Documentation (push) Successful in 48s
2025-05-06 16:01:57 -04:00
3bc82fab45 - Fixed cache.addAll()
Some checks failed
Build / Build NPM Project (push) Failing after 34s
Build / Publish Documentation (push) Has been skipped
Build / Tag Version (push) Has been skipped
- Renamed jwtDecode to decodeJWT to match conventions
- Added testCondition to search
2025-05-06 15:59:08 -04:00
6b15848896 Fixed defaulting date in date formatter
All checks were successful
Build / Build NPM Project (push) Successful in 1m15s
Build / Tag Version (push) Successful in 16s
Build / Publish Documentation (push) Successful in 49s
2025-04-30 11:58:03 -04:00
9a0f32323e Fix HTTP empty responses
All checks were successful
Build / Build NPM Project (push) Successful in 37s
Build / Tag Version (push) Successful in 8s
Build / Publish Documentation (push) Successful in 34s
2025-04-13 10:16:50 -04:00
f952abc95a More zelous path fixing in the http client
All checks were successful
Build / Build NPM Project (push) Successful in 1m13s
Build / Tag Version (push) Successful in 14s
Build / Publish Documentation (push) Successful in 51s
2025-04-13 09:50:19 -04:00
21fc1378b8 Added more naming convention utilities
All checks were successful
Build / Build NPM Project (push) Successful in 42s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 38s
2025-04-08 19:02:42 -04:00
a03567eba3 toCSV comma fix
All checks were successful
Build / Build NPM Project (push) Successful in 1m15s
Build / Tag Version (push) Successful in 13s
Build / Publish Documentation (push) Successful in 50s
2025-04-06 21:37:01 -04:00
f9fc4fb7ff Better CSV handling
All checks were successful
Build / Build NPM Project (push) Successful in 38s
Build / Tag Version (push) Successful in 7s
Build / Publish Documentation (push) Successful in 35s
2025-03-24 23:01:57 -04:00
ff16f3bf9b Better CSV handling
All checks were successful
Build / Build NPM Project (push) Successful in 38s
Build / Tag Version (push) Successful in 8s
Build / Publish Documentation (push) Successful in 36s
2025-03-24 21:40:03 -04:00
e0085ecb6f Better CSV handling
All checks were successful
Build / Build NPM Project (push) Successful in 1m15s
Build / Tag Version (push) Successful in 16s
Build / Publish Documentation (push) Successful in 53s
2025-03-24 21:28:51 -04:00
1c2c18b65d Added camelCase function
All checks were successful
Build / Build NPM Project (push) Successful in 1m12s
Build / Tag Version (push) Successful in 12s
Build / Publish Documentation (push) Successful in 50s
2025-03-10 09:50:16 -04:00
10 changed files with 252 additions and 76 deletions

View File

@ -1,15 +1,104 @@
<!Doctype html> <!DOCTYPE html>
<html lang="en">
<html>
<head> <head>
<title>@ztimson/utils sandbox</title> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About Us | OurTrainingRoom</title>
<style>
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #fdfdfd;
color: #333;
line-height: 1.6;
}
header {
background: #004080;
color: #fff;
padding: 2rem 1rem;
text-align: center;
}
header h1 {
margin: 0;
font-size: 2.5rem;
}
main {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
section {
margin-bottom: 2rem;
}
h2 {
color: #004080;
margin-bottom: 0.5rem;
}
ul {
padding-left: 1.25rem;
}
footer {
text-align: center;
font-size: 0.9rem;
color: #777;
padding: 2rem 1rem;
background: #f1f1f1;
margin-top: 4rem;
}
</style>
</head> </head>
<body> <body>
<script type="module"> <header>
import {PathEvent} from './dist/index.mjs'; <h1>About Us</h1>
<p>Empowering Learning Through Innovation</p>
</header>
console.log(PathEvent.filter(['payments/ztimson:cr', 'logs/momentum:c', 'data/Testing:r'], 'data')); <main>
console.log(PathEvent.filter(['data/Submissions/Test:r'], 'data/Submissions/Test/test.html')); <section>
</script> <p>
E-learning has evolved significantly since its inception. Today, there's a shift towards
blended learning services, integrating online activities with practical, real-world applications.
</p>
</section>
<section>
<h2>What We Do</h2>
<p>At <strong>OurTrainingRoom.com</strong>, we specialize in content management and professional development training tailored for:</p>
<ul>
<li>School Boards</li>
<li>Municipalities</li>
<li>Hospitals</li>
<li>Large Corporations</li>
</ul>
</section>
<section>
<h2>Our Roots</h2>
<p>
Our parent company, <strong>The Auxilium Group</strong>, is a leader in online data management.
The formation of OurTrainingRoom.com was a natural progression to deliver state-of-the-art front-end e-learning programs.
</p>
</section>
<section>
<h2>Our Approach</h2>
<p>
Built on principles of quality and continuous improvement, our diverse delivery range continues to grow.
We set new trends by enhancing our existing products and attentively listening to our clients and their employees.
This unique approach has solidified our position in the industry, making a substantial impact for our clients.
</p>
</section>
<section>
<h2>Have a Question?</h2>
<p>
We value your inquiries and are here to assist you. Please reach out with any questions or feedback.
</p>
</section>
</main>
<footer>
&copy; 2025 OurTrainingRoom.com. All rights reserved.
</footer>
</body> </body>
</html> </html>

View File

@ -1,6 +1,6 @@
{ {
"name": "@ztimson/utils", "name": "@ztimson/utils",
"version": "0.23.14", "version": "0.24.0",
"description": "Utility library", "description": "Utility library",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",
@ -14,9 +14,9 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs", "import": "./dist/index.mjs",
"require": "./dist/index.cjs", "require": "./dist/index.cjs"
"types": "./dist/index.d.ts"
} }
}, },
"scripts": { "scripts": {
@ -34,7 +34,7 @@
"typedoc": "^0.26.7", "typedoc": "^0.26.7",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.12", "vite": "^5.0.12",
"vite-plugin-dts": "^3.7.2" "vite-plugin-dts": "^4.5.3"
}, },
"files": [ "files": [
"dist" "dist"

View File

@ -22,7 +22,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Create new cache * Create new cache
*
* @param {keyof T} key Default property to use as primary key * @param {keyof T} key Default property to use as primary key
* @param options * @param options
*/ */
@ -56,7 +55,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Get all cached items * Get all cached items
*
* @return {T[]} Array of items * @return {T[]} Array of items
*/ */
all(): T[] { all(): T[] {
@ -65,7 +63,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Add a new item to the cache. Like set, but finds key automatically * Add a new item to the cache. Like set, but finds key automatically
*
* @param {T} value Item to add to cache * @param {T} value Item to add to cache
* @param {number | undefined} ttl Override default expiry * @param {number | undefined} ttl Override default expiry
* @return {this} * @return {this}
@ -78,12 +75,12 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Add several rows to the cache * Add several rows to the cache
*
* @param {T[]} rows Several items that will be cached using the default key * @param {T[]} rows Several items that will be cached using the default key
* @param complete Mark cache as complete & reliable, defaults to true * @param complete Mark cache as complete & reliable, defaults to true
* @return {this} * @return {this}
*/ */
addAll(rows: T[], complete = true): this { addAll(rows: T[], complete = true): this {
this.clear();
rows.forEach(r => this.add(r)); rows.forEach(r => this.add(r));
this.complete = complete; this.complete = complete;
return this; return this;
@ -98,7 +95,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Delete an item from the cache * Delete an item from the cache
*
* @param {K} key Item's primary key * @param {K} key Item's primary key
*/ */
delete(key: K) { delete(key: K) {
@ -126,7 +122,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Get a list of cached keys * Get a list of cached keys
*
* @return {K[]} Array of keys * @return {K[]} Array of keys
*/ */
keys(): K[] { keys(): K[] {
@ -135,7 +130,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Get map of cached items * Get map of cached items
*
* @return {Record<K, T>} * @return {Record<K, T>}
*/ */
map(): Record<K, T> { map(): Record<K, T> {
@ -144,7 +138,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Add an item to the cache manually specifying the key * Add an item to the cache manually specifying the key
*
* @param {K} key Key item will be cached under * @param {K} key Key item will be cached under
* @param {T} value Item to cache * @param {T} value Item to cache
* @param {number | undefined} ttl Override default expiry in seconds * @param {number | undefined} ttl Override default expiry in seconds
@ -163,7 +156,6 @@ export class Cache<K extends string | number | symbol, T> {
/** /**
* Get all cached items * Get all cached items
*
* @return {T[]} Array of items * @return {T[]} Array of items
*/ */
values = this.all(); values = this.all();

View File

@ -1,32 +1,59 @@
import {makeArray} from './array.ts';
import {ASet} from './aset.ts'; import {ASet} from './aset.ts';
import {dotNotation, flattenObj, JSONSanitize} from './objects.ts'; import {dotNotation, flattenObj, JSONSanitize} from './objects.ts';
import {LETTER_LIST} from './string.ts'; import {LETTER_LIST} from './string.ts';
export function fromCsv<T = any>(csv: string, hasHeaders=true): T[] { /**
const row = csv.split('\n'); * Parse a CSV string into an array of objects
let headers: any = hasHeaders ? row.splice(0, 1)[0] : null; *
if(headers) headers = headers.match(/(?:[^,"']+|"[^"]*"|'[^']*')+/g); * @param csv String with CSV
return <T[]>row.map(r => { * @param hasHeaders First line of CSV contains headers
function parseLine(line: string): (string | null)[] { * @return {T[]} Array of parsed objects
const parts = line.split(','), columns: string[] = []; */
let quoted = false; export function fromCsv<T = any>(csv: string, hasHeaders = true): T[] {
for(const p of parts) { function parseLine(line: string): (string | null)[] {
if(quoted) columns[columns.length - 1] = columns.at(-1) + ',' + p; const columns: string[] = [];
else columns.push(p); let current = '', inQuotes = false;
if(/[^"]"$/g.test(p)) { for (let i = 0; i < line.length; i++) {
quoted = false; const char = line[i];
} else if(/^"[^"]/g.test(p)) { const nextChar = line[i + 1];
quoted = true; if (char === '"') {
} if (inQuotes && nextChar === '"') {
} current += '"'; // Handle escaped quotes
return columns; i++;
} else inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
columns.push(current.trim()); // Trim column values
current = '';
} else current += char;
} }
columns.push(current.trim()); // Trim last column value
return columns.map(col => col.replace(/^"|"$/g, '').replace(/""/g, '"'));
}
// Normalize line endings and split rows
const rows = [];
let currentRow = '', inQuotes = false;
for (const char of csv.replace(/\r\n/g, '\n')) { // Normalize \r\n to \n
if (char === '"') inQuotes = !inQuotes;
if (char === '\n' && !inQuotes) {
rows.push(currentRow.trim()); // Trim row
currentRow = '';
} else currentRow += char;
}
if (currentRow) rows.push(currentRow.trim()); // Trim last row
// Extract headers
let headers: any = hasHeaders ? rows.splice(0, 1)[0] : null;
if (headers) headers = headers.match(/(?:[^,"']+|"(?:[^"]|"")*"|'(?:[^']|'')*')+/g)?.map((h: any) => h.trim());
// Parse rows
return <T[]>rows.map(r => {
const props = parseLine(r); const props = parseLine(r);
const h = headers || (Array(props.length).fill(null).map((r, i) => { const h = headers || (Array(props.length).fill(null).map((_, i) => {
let letter = ''; let letter = '';
const first = i / 26; const first = i / 26;
if(first > 1) letter += LETTER_LIST[Math.floor(first - 1)]; if (first > 1) letter += LETTER_LIST[Math.floor(first - 1)];
letter += LETTER_LIST[i % 26]; letter += LETTER_LIST[i % 26];
return letter; return letter;
})); }));
@ -34,23 +61,28 @@ export function fromCsv<T = any>(csv: string, hasHeaders=true): T[] {
dotNotation(acc, h, props[i]); dotNotation(acc, h, props[i]);
return acc; return acc;
}, {}); }, {});
}) });
} }
/** /**
* Convert an object to a CSV string * Convert an array of objects to a CSV string
* *
* @param {any[]} target Array of objects to create CSV from * @param {any[]} target Array of objects to create CSV from
* @param {boolean} flatten Should nested object be flattened or treated as values * @param {boolean} flatten Should nested object be flattened or treated as values
* @return {string} CSV string * @return {string} CSV string
*/ */
export function toCsv(target: any[], flatten=true) { export function toCsv(target: any, flatten=true) {
const headers = new ASet(target.reduce((acc, row) => [...acc, ...Object.keys(flatten ? flattenObj(row) : row)], [])); const t = makeArray(target);
const headers = new ASet(t.reduce((acc, row) => [...acc, ...Object.keys(flatten ? flattenObj(row) : row)], []));
return [ return [
headers.join(','), headers.join(','),
...target.map(row => headers.map((h: string) => { ...t.map(row => headers.map((h: string) => {
const value = dotNotation<any>(row, h); const value = dotNotation<any>(row, h);
return (typeof value == 'object' && value != null) ? '"' + JSONSanitize(value).replaceAll('"', '""') + '"' : value; if(value == null) return '';
if(typeof value == 'object') return `"${JSONSanitize(value).replaceAll('"', '""')}"`;
if(typeof value == 'string' && /[\n",]/g.test(value)) return `"${value.replaceAll('"', '""')}"`;
return value;
}).join(',')) }).join(','))
].join('\n'); ].join('\n');
} }

View File

@ -30,7 +30,13 @@ class HttpResponse<T = any> extends Response {
url!: string; url!: string;
constructor(resp: Response, stream: ReadableStream) { constructor(resp: Response, stream: ReadableStream) {
super(stream, {headers: resp.headers, status: resp.status, statusText: resp.statusText}); const body = [204, 205, 304].includes(resp.status) ? null : stream;
super(body, {
headers: resp.headers,
status: resp.status,
statusText: resp.statusText,
});
this.ok = resp.ok; this.ok = resp.ok;
this.redirected = resp.redirected; this.redirected = resp.redirected;
this.type = resp.type; this.type = resp.type;
@ -70,8 +76,9 @@ export class Http {
request<T>(opts: HttpRequestOptions = {}): PromiseProgress<DecodedResponse<T>> { request<T>(opts: HttpRequestOptions = {}): PromiseProgress<DecodedResponse<T>> {
if(!this.url && !opts.url) throw new Error('URL needs to be set'); if(!this.url && !opts.url) throw new Error('URL needs to be set');
let url = (opts.url?.startsWith('http') ? opts.url : (this.url || '') + (opts.url || '')).replace(/([^:]\/)\/+/g, '$1'); let url = opts.url?.startsWith('http') ? opts.url : (this.url || '') + (opts.url || '');
if(opts.fragment) url.includes('#') ? url.replace(/#.*(\?|\n)/g, (match, arg1) => `#${opts.fragment}${arg1}`) : url += '#' + opts.fragment; url = url.replaceAll(/([^:]\/)\/+/g, '$1');
if(opts.fragment) url.includes('#') ? url.replace(/#.*(\?|\n)/g, (match, arg1) => `#${opts.fragment}${arg1}`) : `${url}#${opts.fragment}`;
if(opts.query) { if(opts.query) {
const q = Array.isArray(opts.query) ? opts.query : const q = Array.isArray(opts.query) ? opts.query :
Object.keys(opts.query).map(k => ({key: k, value: (<any>opts.query)[k]})) Object.keys(opts.query).map(k => ({key: k, value: (<any>opts.query)[k]}))

View File

@ -6,7 +6,7 @@ import {JSONAttemptParse} from './objects.ts';
* @param {string} token JWT to decode * @param {string} token JWT to decode
* @return {unknown} JWT payload * @return {unknown} JWT payload
*/ */
export function jwtDecode<T>(token: string): T { export function decodeJwt<T>(token: string): T {
const base64 = token.split('.')[1] const base64 = token.split('.')[1]
.replace(/-/g, '+').replace(/_/g, '/'); .replace(/-/g, '+').replace(/_/g, '/');
return <T>JSONAttemptParse(decodeURIComponent(atob(base64).split('').map(function(c) { return <T>JSONAttemptParse(decodeURIComponent(atob(base64).split('').map(function(c) {

View File

@ -109,7 +109,6 @@ export function encodeQuery(data: any): string {
).join('&'); ).join('&');
} }
/** /**
* Recursively flatten a nested object, while maintaining key structure * Recursively flatten a nested object, while maintaining key structure
* *

View File

@ -1,4 +1,4 @@
import {dotNotation, JSONAttemptParse} from './objects'; import {dotNotation, JSONAttemptParse} from './objects.ts';
export function search(rows: any[], search: string, regex?: boolean, transform: Function = (r: any) => r) { export function search(rows: any[], search: string, regex?: boolean, transform: Function = (r: any) => r) {
if(!rows) return []; if(!rows) return [];
@ -12,27 +12,42 @@ export function search(rows: any[], search: string, regex?: boolean, transform:
try { return RegExp(search, 'gm').test(v.toString()); } try { return RegExp(search, 'gm').test(v.toString()); }
catch { return false; } catch { return false; }
}).length }).length
} else {
return testCondition(search, r);
} }
// Make sure at least one OR passes });
const or = search.split('||').map(p => p.trim()).filter(p => !!p); }
return -1 != or.findIndex(p => {
// Make sure all ANDs pass export function testCondition(condition: string, row: any) {
const and = p.split('&&').map(p => p.trim()).filter(p => !!p); const evalBoolean = (a: any, op: string, b: any): boolean => {
return and.filter(p => { switch(op) {
// Boolean operator case '=':
const prop = /(\w+)\s*(==?|!=|>=|>|<=|<)\s*(\w+)/g.exec(p); case '==': return a == b;
if(prop) { case '!=': return a != b;
const a = JSON.stringify(JSONAttemptParse(dotNotation<any>(value, prop[1]))); case '>': return a > b;
const operator = prop[2] == '=' ? '==' : prop[2]; case '>=': return a >= b;
const b = JSON.stringify(JSONAttemptParse(prop[3])); case '<': return a < b;
return eval(`${a} ${operator} ${b}`); case '<=': return a <= b;
} default: return false;
// Case-sensitive }
const v = Object.values(value).join(''); }
if(/[A-Z]/g.test(search)) return v.includes(p);
// Case-insensitive const or = condition.split('||').map(p => p.trim()).filter(p => !!p);
return v.toLowerCase().includes(p); return -1 != or.findIndex(p => {
}).length == and.length; // Make sure all ANDs pass
}) const and = p.split('&&').map(p => p.trim()).filter(p => !!p);
return and.filter(p => {
// Boolean operator
const prop = /(\S+)\s*(==?|!=|>=|>|<=|<)\s*(\S+)/g.exec(p);
if(prop) {
const key = Object.keys(row).find(k => k.toLowerCase() == prop[1].toLowerCase());
return evalBoolean(dotNotation<any>(row, key || prop[1]), prop[2], JSONAttemptParse(prop[3]));
}
// Case-sensitive
const v = Object.values(row).map(v => typeof v == 'object' && v != null ? JSON.stringify(v) : v).join('');
if(/[A-Z]/g.test(condition)) return v.includes(p);
// Case-insensitive
return v.toLowerCase().includes(p);
}).length == and.length;
}); });
} }

View File

@ -18,6 +18,14 @@ export const SYMBOL_LIST = '~`!@#$%^&*()_-+={[}]|\\:;"\'<,>.?/';
*/ */
export const CHAR_LIST = LETTER_LIST + LETTER_LIST.toLowerCase() + NUMBER_LIST + SYMBOL_LIST; export const CHAR_LIST = LETTER_LIST + LETTER_LIST.toLowerCase() + NUMBER_LIST + SYMBOL_LIST;
/**
* Converts text to camelCase
*/
export function camelCase(str?: string) {
const text = pascalCase(str);
return text[0].toLowerCase() + text.slice(1);
}
/** /**
* Convert number of bytes into a human-readable size * Convert number of bytes into a human-readable size
* *
@ -64,6 +72,17 @@ export function insertAt(target: string, str: string, index: number): String {
return `${target.slice(0, index)}${str}${target.slice(index + 1)}`; return `${target.slice(0, index)}${str}${target.slice(index + 1)}`;
} }
/**
* Converts text to kebab-case
*/
export function kebabCase(str: string) {
if(!str) return '';
return str.replaceAll(/(^[^a-zA-Z]+|[^a-zA-Z0-9-_])/g, '')
.replaceAll(/([A-Z]|[0-9]+)/g, (...args) => `-${args[0].toLowerCase()}`)
.replaceAll(/([0-9])([a-z])/g, (...args) => `${args[1]}-${args[2]}`)
.replaceAll(/[^a-z0-9]+(\w?)/g, (...args) => `-${args[1] ?? ''}`).toLowerCase();
}
/** /**
* Add padding to string * Add padding to string
* *
@ -86,6 +105,18 @@ export function pad(text: any, length: number, char: string = ' ', start = true)
return text.toString().padEnd(length, char); return text.toString().padEnd(length, char);
} }
/**
* Convert text to PascalCase
* @param {string} str
* @return {string}
*/
export function pascalCase(str?: string) {
if(!str) return '';
const text = str.replaceAll(/(^[^a-zA-Z]+|[^a-zA-Z0-9-_])/g, '')
.replaceAll(/[^a-zA-Z0-9]+(\w?)/g, (...args) => args[1]?.toUpperCase() || '');
return text[0].toUpperCase() + text.slice(1);
}
/** /**
* Generate a random hexadecimal value * Generate a random hexadecimal value
* *
@ -149,6 +180,17 @@ export function randomStringBuilder(length: number, letters = false, numbers = f
}).join(''); }).join('');
} }
/**
* Converts text to snake_case
*/
export function snakeCase(str?: string) {
if(!str) return '';
return str.replaceAll(/(^[^a-zA-Z]+|[^a-zA-Z0-9-_])/g, '')
.replaceAll(/([A-Z]|[0-9]+)/g, (...args) => `_${args[0].toLowerCase()}`)
.replaceAll(/([0-9])([a-z])/g, (...args) => `${args[1]}_${args[2]}`)
.replaceAll(/[^a-z0-9]+(\w?)/g, (...args) => `_${args[1] ?? ''}`).toLowerCase();
}
/** /**
* Splice a string together (Similar to Array.splice) * Splice a string together (Similar to Array.splice)
* *

View File

@ -95,7 +95,7 @@ export function formatDate(format = 'YYYY-MM-DD H:mm', date: Date | number | str
return (offset > 0 ? '-' : '') + `${hours}:${minutes.toString().padStart(2, '0')}`; return (offset > 0 ? '-' : '') + `${hours}:${minutes.toString().padStart(2, '0')}`;
} }
if(typeof date == 'number' || typeof date == 'string') date = new Date(date); if(typeof date == 'number' || typeof date == 'string' || date == null) date = new Date(date);
// Handle timezones // Handle timezones
let t!: [string, number]; let t!: [string, number];