@seedcord/utils 0.6.1 → 0.7.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["path"],"sources":["../src/misc/assertNever.ts","../src/misc/directory.ts","../src/misc/fyShuffle.ts","../src/numbers/toEpochSeconds.ts","../src/numbers/currentTime.ts","../src/numbers/generateCode.ts","../src/numbers/ordinal.ts","../src/numbers/parseDuration.ts","../src/numbers/percentage.ts","../src/numbers/round.ts","../src/numbers/roundToDenomination.ts","../src/objects/filterCirculars.ts","../src/objects/hasKeys.ts","../src/objects/keepDefined.ts","../src/strings/capitalize.ts","../src/strings/longestStringLength.ts","../src/strings/generateAsciiTable.ts","../src/strings/prettify.ts","../src/strings/prettyDifference.ts","../src/index.ts"],"sourcesContent":["/**\n * Exhaustiveness guard for discriminated unions. Place in the `default` branch of a `switch` over a\n * union's discriminant: if a new variant is added without a matching case, the call fails to compile.\n * Throws at runtime if reached with a value the types said was impossible.\n */\nexport function assertNever(value: never): never {\n throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);\n}\n","import { readdir } from 'node:fs/promises';\nimport * as path from 'node:path';\n\nimport type { ILogger } from '@seedcord/types';\nimport type * as fs from 'node:fs';\n\n/**\n * Determines if a directory entry is a TypeScript or JavaScript file.\n *\n * @param entry - The directory entry to check.\n * @returns True if the entry is a file ending with .ts or .js.\n */\nexport function isTsOrJsFile(entry: fs.Dirent): boolean {\n return (\n entry.isFile() &&\n (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) &&\n !entry.name.endsWith('.d.ts') &&\n !entry.name.endsWith('.map')\n );\n}\n\n/**\n * Recursively traverses through a directory, importing all .ts and .js files and applying a callback to each import.\n *\n * @param dir - The directory path to traverse.\n * @param callback - A function that will be called for each imported module. It receives the full file path, the file's relative path, and the imported module as arguments.\n * @returns A Promise that resolves when the traversal is complete.\n */\nexport async function traverseDirectory(\n dir: string,\n callback: (fullPath: string, relativePath: string, imported: Record<string, unknown>) => Promise<void> | void,\n logger: ILogger\n): Promise<void> {\n let entries: fs.Dirent[];\n\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (err) {\n logger.error(`Failed to read directory ${dir}`, err);\n entries = [];\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(process.cwd(), fullPath);\n\n if (entry.isDirectory()) {\n await traverseDirectory(fullPath, callback, logger);\n } else if (isTsOrJsFile(entry)) {\n const imported = (await import(fullPath)) as Record<string, unknown>;\n await callback(fullPath, relativePath, imported);\n }\n }\n}\n\n/**\n * Options for formatting file paths.\n */\nexport interface FormatFileOptions {\n /**\n * Whether to return only the directory part of the path.\n *\n * @defaultValue `false`\n */\n onlyDir?: boolean;\n /**\n * A prefix to prepend to the formatted path.\n *\n * @defaultValue `'./'`\n */\n prefix?: string;\n}\n\n/**\n * Formats a file path relative to the current working directory.\n * @param filePath - The file path to format.\n * @param options - Formatting options.\n * @returns The formatted file path.\n */\nexport function formatFilePath(filePath: string, options: FormatFileOptions = {}): string {\n const { onlyDir = false, prefix = './' } = options;\n\n const resolved = onlyDir\n ? path.relative(process.cwd(), filePath.replace(/\\/[^/]*$/, ''))\n : path.relative(process.cwd(), filePath);\n return `${prefix}${resolved}`;\n}\n","/**\n * Shuffles an array using the Fisher-Yates algorithm.\n * This function creates a new array with the same elements in a random order,\n * without modifying the original array.\n *\n * @typeParam TArray - The type of elements in the array\n * @param items - The array to shuffle\n * @returns A new array with the same elements in a random order\n *\n * @example\n * ```typescript\n * const numbers = [1, 2, 3, 4, 5];\n * const shuffled = fyShuffle(numbers);\n * // shuffled might be [3, 1, 5, 2, 4]\n * // numbers is still [1, 2, 3, 4, 5]\n * ```\n */\nexport function fyShuffle<TArray>(items: TArray[]): TArray[] {\n const array = items.slice();\n for (let i = array.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n // @ts-expect-error - TypeScript doesn't recognize that TArray can be swapped\n [array[i], array[j]] = [array[j], array[i]];\n }\n return array;\n}\n","import type { EpochMs, EpochSec } from '@seedcord/types';\n\n/** Converts absolute epoch milliseconds to epoch seconds, the unit Discord `<t:...>` timestamp markup reads. */\nexport function toEpochSeconds(ms: EpochMs): EpochSec {\n return Math.round(ms / 1000) as EpochSec;\n}\n","import { toEpochSeconds } from './toEpochSeconds';\n\nimport type { EpochMs, EpochSec } from '@seedcord/types';\n\n/** Current time in epoch seconds. Shares the one ms→s rounding rule with {@link toEpochSeconds}. */\nexport function currentTime(): EpochSec {\n return toEpochSeconds(Date.now() as EpochMs);\n}\n","/**\n * Generates a random numeric code with the specified number of digits.\n *\n * @param digits - The number of digits for the generated code.\n * @returns A random numeric code with the specified number of digits.\n */\nexport function generateCode(digits: number): number {\n const min = Math.pow(10, digits - 1);\n const max = Math.pow(10, digits) - 1;\n return Math.floor(Math.random() * (max - min + 1) + min);\n}\n","/**\n * Returns the ordinal suffix for a given number.\n *\n * @param n - The number to get the ordinal for\n * @returns The number with its ordinal suffix\n *\n * @example\n * ordinal(1); // \"1st\"\n * ordinal(22); // \"22nd\"\n * ordinal(13); // \"13th\"\n */\nexport function ordinal(n: number): string {\n const s = ['th', 'st', 'nd', 'rd'];\n const v = n % 100;\n const index = (v - 20) % 10;\n const suffix = s[index] ?? s[v] ?? s[0];\n if (!suffix) return `${n}th`;\n\n return `${n}${suffix}`;\n}\n","const UNIT_MS = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000\n} as const;\n\ntype DurationUnit = keyof typeof UNIT_MS;\n\n/** A duration literal like `30m`, `24h`, or `500ms`, a number followed by a unit (`ms`, `s`, `m`, `h`, `d`). */\nexport type ValidDuration = `${number}${DurationUnit}`;\n\nconst DURATION_PATTERN = new RegExp(`^(\\\\d+)(${Object.keys(UNIT_MS).join('|')})$`);\n\n/**\n * Parses a short duration string like `24h`, `30m`, `90s`, `7d`, or `500ms` into milliseconds.\n *\n * The grammar is one or more digits followed by one lowercase unit (`ms`, `s`, `m`, `h`, `d`).\n * Anything else returns `null`, including a bare number, an unknown unit, an uppercase unit,\n * surrounding whitespace, a fractional value, and a result of zero. The return is a positive\n * number or `null`, never `0` or `NaN`, so a malformed input can never read as a valid duration.\n *\n * @param input - The duration string to parse.\n * @returns The duration in milliseconds, or `null` if `input` is not a well-formed positive duration.\n */\nexport function parseDuration(input: string): number | null {\n const match = DURATION_PATTERN.exec(input);\n if (!match) return null;\n\n const ms = Number(match[1]) * UNIT_MS[match[2] as DurationUnit];\n return ms > 0 ? ms : null;\n}\n","/**\n * Takes two numbers and returns the percentage of the first number in the second number with two decimal places.\n *\n * @param num1 - The first number.\n * @param num2 - The second number.\n *\n * @returns The percentage of the first number in the second number with two decimal places.\n */\nexport function percentage(num1: number, num2: number): number {\n return Number(((num1 / num2) * 100).toFixed(2));\n}\n","/**\n * Rounds a number to a specified number of decimal places.\n *\n * @param num - The number to be rounded.\n * @param precision - The number of decimal places to round to.\n * @returns The rounded number.\n */\nexport function round(num: number, precision: number): number {\n const factor = Math.pow(10, precision);\n return Math.round((num + Number.EPSILON) * factor) / factor;\n}\n","import type { TupleOf } from 'type-fest';\n\nexport interface RoundToDenomOptions {\n /**\n * Suffixes to use for each denomination level.\n *\n * @defaultValue `['K', 'M', 'B', 'T', 'Q']`\n */\n suffixes?: TupleOf<5, string>;\n /**\n * Number of decimal places to include in the rounded result.\n *\n * @defaultValue `1`\n */\n precision?: number;\n}\n\n/**\n * Rounds a number to a string representation with a denomination suffix.\n * @param num - The number to round.\n * @example\n * ```ts\n * roundToDenomination(1234); // \"1.2K\"\n * roundToDenomination(10000, { suffixes: ['k', 'm', 'b', 't', 'q'] }); // \"10k\"\n * roundToDenomination(12345678); // \"12.3M\"\n * ```\n * @returns The rounded number as a string with a denomination suffix.\n */\nexport function roundToDenomination(num: number, opts?: RoundToDenomOptions): string {\n const { suffixes = ['K', 'M', 'B', 'T', 'Q'], precision = 1 } = opts ?? {};\n\n if (num < 10000) {\n return num.toString();\n }\n\n let index = -1;\n let temp = num;\n\n while (temp >= 1000 && index < suffixes.length - 1) {\n temp /= 1000;\n index++;\n }\n\n let result;\n\n if (temp % 1 === 0) {\n result = temp.toString();\n } else {\n const adjustedTemp = Math.round(temp * Math.pow(10, precision + 1)) / Math.pow(10, precision + 1);\n result = adjustedTemp.toFixed(precision);\n }\n\n if (result.endsWith('.9')) {\n result = Math.ceil(Number(result)).toString();\n }\n\n if (result.endsWith('.0')) {\n result = result.substring(0, result.length - 2);\n }\n\n if (result === '1000') {\n index += 1;\n result = '1';\n }\n\n return result + (index >= 0 ? suffixes[index] : '');\n}\n","import type { ILogger } from '@seedcord/types';\nimport type { JsonPrimitive } from 'type-fest';\n\n/**\n * JSONify an arbitrary type while allowing any object position to be replaced\n * by a circular marker. Optional keys stay optional.\n */\nexport type JsonifyWithCirculars<BaseType, Marker extends string = '[Circular]'> = BaseType extends JsonPrimitive\n ? BaseType\n : BaseType extends bigint\n ? string\n : BaseType extends Date\n ? string\n : BaseType extends { toJSON(): infer J }\n ? unknown extends J\n ? JsonifyObject<BaseType, Marker>\n : JsonifyWithCirculars<J, Marker>\n : BaseType extends readonly (infer U)[]\n ? (JsonifyWithCirculars<U, Marker> | Marker)[]\n : BaseType extends Map<infer K, infer V>\n ? [JsonifyWithCirculars<K, Marker> | Marker, JsonifyWithCirculars<V, Marker> | Marker][]\n : BaseType extends Set<infer U2>\n ? (JsonifyWithCirculars<U2, Marker> | Marker)[]\n : BaseType extends (...args: unknown[]) => unknown\n ? never\n : BaseType extends object\n ? JsonifyObject<BaseType, Marker>\n : never;\n\n/**\n * Helper to JSONify object types with circular markers.\n *\n * @internal\n */\nexport type JsonifyObject<BaseType, Marker extends string> = {\n [K in keyof BaseType as K extends symbol\n ? never\n : BaseType[K] extends (...args: unknown[]) => unknown\n ? never\n : K]:\n | JsonifyWithCirculars<Exclude<BaseType[K], undefined>, Marker>\n | Extract<BaseType[K], undefined>\n | Marker;\n};\n\n/**\n * Returned by {@link filterCirculars} when a value cannot be made JSON-safe. The original value is\n * never returned on failure, because it would re-throw in the caller's own `JSON.stringify`.\n */\nexport interface UnserializableValue {\n '[unserializable]': string;\n}\n\n/**\n * Configuration for {@link filterCirculars}.\n */\nexport interface FilterCircularsOptions<Marker extends string = '[Circular]'> {\n /** Optional {@link ILogger} used to log stringify or parse errors. */\n logger?: ILogger;\n /**\n * Override the circular placeholder.\n *\n * @defaultValue `'[Circular]'`\n */\n marker?: Marker;\n /**\n * Processing mode. `json` uses stringify and parse (might end up using a `toJSON()` if found). `decycle` builds a safe clone first.\n *\n * @defaultValue `'decycle'`\n */\n mode?: 'json' | 'decycle';\n}\n\n/**\n * Creates a clean, JSON safe copy of a value and replaces circular references with a marker.\n *\n * In `json` mode it behaves like stringify then parse with a replacer that handles cycles and BigInt.\n * In `decycle` mode it first clones without using toJSON, then you can stringify the result later.\n *\n * @typeParam ObjType - Type of the input value.\n * @typeParam Marker - Marker string used for circular references.\n *\n * @param value - The value to clone safely.\n * @param options - Optional configuration.\n *\n * @returns A JSON safe structure with circular references replaced by the marker.\n *\n * @example\n * ```ts\n * interface Test {\n * name: string;\n * self?: Test;\n * }\n *\n * const obj: Test = { name: 'seedcord' };\n * obj.self = obj;\n *\n * const clean = filterCirculars(obj);\n * // ^? { name: string; self?: \"[Circular]\" | { ... } }\n * console.log(clean.self); // \"[Circular]\"\n * ```\n */\nexport function filterCirculars<ObjType, Marker extends string = '[Circular]'>(\n value: ObjType,\n options?: FilterCircularsOptions<Marker>\n): JsonifyWithCirculars<ObjType, Marker> | UnserializableValue {\n const logger = options?.logger;\n const marker = (options?.marker ?? '[Circular]') as Marker;\n const mode = options?.mode ?? 'decycle';\n\n if (mode === 'json') return json(value, marker, logger);\n\n try {\n return decycle(value, marker);\n } catch (error) {\n logger?.error('filterCirculars decycle error', error);\n return { '[unserializable]': 'decycle failed' };\n }\n}\n\n/**\n * Attempts to build a JSONified object using JSON.stringify and JSON.parse.\n *\n * @internal\n */\nfunction json<ObjType, Marker extends string>(\n value: ObjType,\n marker: Marker,\n logger?: ILogger\n): JsonifyWithCirculars<ObjType, Marker> | UnserializableValue {\n const seen = new WeakSet<object>();\n let json: string | undefined;\n\n try {\n json = JSON.stringify(value, (_k: string, v: unknown) => {\n if (typeof v === 'bigint') return v.toString();\n if (typeof v === 'object' && v !== null) {\n const obj = v;\n if (seen.has(obj)) return marker;\n seen.add(obj);\n }\n return v;\n });\n } catch (error) {\n logger?.error('filterCirculars stringify error', error);\n if (typeof value === 'object' && value !== null) {\n logger?.error('top level keys', Object.keys(value));\n }\n return { '[unserializable]': 'stringify failed' };\n }\n\n if (typeof json !== 'string') {\n return { '[unserializable]': 'stringify returned undefined' };\n }\n\n try {\n return JSON.parse(json) as JsonifyWithCirculars<ObjType, Marker>;\n } catch (error) {\n logger?.error('filterCirculars parse error', error);\n logger?.error('bad JSON', json);\n return { '[unserializable]': 'parse failed' };\n }\n}\n\n/**\n * Builds a JSON safe clone without calling toJSON on class instances.\n *\n * @internal\n */\nfunction decycle<ObjType, Marker extends string = '[Circular]'>(\n input: ObjType,\n marker: Marker,\n seen = new WeakSet<object>()\n): JsonifyWithCirculars<ObjType, Marker> {\n const recur = (val: unknown): unknown => {\n if (val === null) return null;\n const t = typeof val;\n\n if (t === 'bigint') return (val as bigint).toString();\n if (t !== 'object') return val;\n\n const obj = val as Record<string | number | symbol, unknown>;\n\n if (seen.has(obj)) return marker;\n seen.add(obj);\n\n if (obj instanceof Date) return obj.toISOString();\n if (obj instanceof RegExp) return obj.toString();\n\n if (Array.isArray(obj)) {\n return obj.map((item) => recur(item));\n }\n\n if (obj instanceof Map) {\n return Array.from(obj, ([k, v]) => [recur(k), recur(v)]);\n }\n\n if (obj instanceof Set) {\n return Array.from(obj, (v) => recur(v));\n }\n\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (typeof v === 'function') continue;\n out[k] = recur(v);\n }\n\n return out;\n };\n\n return recur(input) as JsonifyWithCirculars<ObjType, Marker>;\n}\n","import type { UnionToIntersection } from 'type-fest';\n\n/**\n * Extracts the type of a nested property, distributing over unions.\n *\n * @internal\n */\nexport type DeepGet<Obj, Key extends string> = Obj extends unknown\n ? Key extends `${infer K}.${infer Rest}`\n ? K extends keyof Obj\n ? DeepGet<NonNullable<Obj[K]>, Rest>\n : never\n : Key extends keyof Obj\n ? Obj[Key]\n : never\n : never;\n\n/**\n * Converts a dot-notation path string into a nested object type with a specific leaf value.\n *\n * @internal\n */\nexport type PathToObj<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}`\n ? { [K in Head]: PathToObj<Tail, Value> }\n : { [K in Path]: Value };\n\n/**\n * Checks for the presence of nested keys in an object that's possibly a distributed union, narrowing the object type accordingly.\n *\n * Checks if an object has the specified nested keys and that their values are not null or undefined. If they are not, the object type is narrowed to reflect the presence of these keys with their respective types from the original distributed union object.\n *\n * @param obj - The object to check.\n * @param keys - An array of dot-notation paths to check.\n * @returns True if all keys exist and are non-null/defined, narrowing the object type.\n *\n * @example\n * ```ts\n * interface Test {\n * a?: {\n * b?: {\n * c?: string;\n * };\n * } | null;\n * x?: number | null;\n * }\n *\n * const obj: Test = { a: { b: { c: 'hello' } }, x: 42 };\n *\n * if (hasKeys(obj, ['a.b.c', 'x'])) {\n * // Here, obj is narrowed to:\n * // {\n * // a: { b: { c: string } };\n * // x: number;\n * // }\n * console.log(obj.a.b.c.toUpperCase()); // Safe to access and use\n * console.log(obj.x.toFixed(2)); // Safe to access and use\n * }\n * ```\n */\nexport function hasKeys<Obj extends object, Keys extends string>(\n obj: Obj,\n keys: Keys[]\n): obj is Obj &\n UnionToIntersection<\n Keys extends unknown ? PathToObj<Keys, UnionToIntersection<NonNullable<DeepGet<Obj, Keys>>>> : never\n > {\n return keys.every((key) => {\n const parts = key.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (\n current === null ||\n current === undefined ||\n (typeof current !== 'object' && typeof current !== 'function')\n ) {\n return false;\n }\n if (!(part in current)) {\n return false;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current !== null && current !== undefined;\n });\n}\n","/**\n * Copies only the keys whose values are defined.\n *\n * @typeParam TObject - the original object type you're pulling from\n * @typeParam TKey - the keys to copy when defined\n * @param source - the object to read values from\n * @param keys - optional list of keys to include when present. {@default all keys}\n *\n * @example\n * ```ts\n * interface Config {\n * host?: string;\n * port?: number;\n * user?: string;\n * password?: string;\n * }\n *\n * const config: Config = {\n * host: 'localhost',\n * port: undefined,\n * user: 'admin',\n * password: undefined\n * };\n *\n * const definedConfig = keepDefined(config, 'host', 'port', 'user', 'password');\n * // Result: { host: 'localhost', user: 'admin' }\n * ```\n */\nexport function keepDefined<TObject extends object, TKey extends keyof TObject>(\n source: TObject,\n ...keys: readonly TKey[]\n): Partial<Pick<TObject, TKey extends never ? keyof TObject : TKey>> {\n const selectedKeys = keys.length > 0 ? keys : (Object.keys(source) as TKey[]);\n const result: Partial<TObject> = {};\n\n for (const key of selectedKeys) {\n const value = source[key];\n if (value !== undefined && value !== null) {\n result[key] = value;\n }\n }\n return result;\n}\n","/**\n * Returns the word with its first letter capitalized and the rest in lowercase.\n * @param word - The word to be formatted.\n * @returns The formatted word.\n */\nexport function capitalize(word: string): string {\n return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();\n}\n","/**\n * Function takes an array of strings or numbers and returns the number of characters in the longest string/number\n *\n * @param arr - The array of strings or numbers\n * @returns The length of the longest element when converted to string\n */\nexport function longestStringLength(arr: (string | number)[]): number {\n return Math.max(...arr.map((el) => el.toString().length));\n}\n","/**\n * Generates an ASCII table from the provided data.\n *\n * @param data - The data to be displayed in the table.\n * @returns The generated ASCII table as a string.\n */\nexport function generateAsciiTable(data: string[][]): string {\n if (data.length === 0) return '';\n\n const firstRow = data[0];\n if (!firstRow || firstRow.length === 0) return '';\n\n let table = '';\n const columnWidths: number[] = [];\n\n // Determine the maximum width for each column\n for (let i = 0; i < firstRow.length; i++) {\n let maxWidth = 0;\n for (const row of data) {\n const cell = row[i];\n if (cell !== undefined) maxWidth = Math.max(maxWidth, cell.length);\n }\n columnWidths.push(maxWidth);\n }\n\n // Function to create a horizontal line\n const createLine = (char: string, left: string, intersect: string, right: string): string => {\n let line = left;\n columnWidths.forEach((width, index) => {\n line += char.repeat(width + 2);\n if (index < columnWidths.length - 1) line += intersect;\n else line += right;\n });\n line += '\\n';\n return line;\n };\n\n // Top border\n table += createLine('═', '╔', '╦', '╗');\n\n data.forEach((row, rowIndex) => {\n // Row content\n table += '║';\n row.forEach((cell, columnIndex) => {\n const columnWidth = columnWidths[columnIndex];\n if (columnWidth !== undefined) table += ` ${cell.padEnd(columnWidth)} ║`;\n });\n table += '\\n';\n\n // Separator or bottom border\n if (rowIndex < data.length - 1) table += createLine('─', '╠', '╬', '╣');\n else table += createLine('═', '╚', '╩', '╝');\n });\n\n return table;\n}\n","import { capitalize } from './capitalize';\n\n/**\n * Options for the `prettify` function.\n */\nexport interface PrettifyOptions {\n capitalize?: boolean;\n}\n/**\n * Converts a string from any common naming convention to human-readable format.\n * Accepts camelCase, PascalCase, snake_case, and kebab-case input.\n *\n * @param key - The string to convert\n * @param opts - Optional configuration\n * @returns A space-separated, human-readable string\n *\n * @example\n * prettify(\"camelCaseString\") // \"camel Case String\"\n * prettify(\"PascalCaseString\") // \"Pascal Case String\"\n * prettify(\"snake_case_string\") // \"snake case string\"\n * prettify(\"kebab-case-string\") // \"kebab case string\"\n * prettify(\"mixedCase_string-name\") // \"mixed Case string name\"\n */\n\nexport function prettify(key: string, opts?: PrettifyOptions): string {\n const result = key\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase/PascalCase\n .replace(/[_-]/g, ' ') // snake_case and kebab-case\n .trim();\n\n if (opts?.capitalize) return capitalize(result);\n\n return result;\n}\n","/**\n * Calculates the difference between two numbers and formats it as a string with a '+' prefix for positive differences.\n *\n * @param numBefore - The initial number value\n * @param numAfter - The final number value\n * @returns A string representing the difference, with a '+' sign for positive differences\n *\n * @example\n * // Returns \"+5\"\n * prettyDifference(10, 15);\n *\n * @example\n * // Returns \"-3\"\n * prettyDifference(10, 7);\n */\nexport function prettyDifference(numBefore: number, numAfter: number): string {\n return (numAfter - numBefore > 0 ? `+${numAfter - numBefore}` : numAfter - numBefore).toString();\n}\n","export * from './misc';\nexport * from './numbers';\nexport * from './objects';\nexport * from './strings';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["path"],"sources":["../src/misc/assertNever.ts","../src/misc/directory.ts","../src/misc/fyShuffle.ts","../src/numbers/toEpochSeconds.ts","../src/numbers/currentTime.ts","../src/numbers/generateCode.ts","../src/numbers/ordinal.ts","../src/numbers/parseDuration.ts","../src/numbers/percentage.ts","../src/numbers/round.ts","../src/numbers/roundToDenomination.ts","../src/objects/filterCirculars.ts","../src/objects/hasKeys.ts","../src/objects/keepDefined.ts","../src/strings/capitalize.ts","../src/strings/longestStringLength.ts","../src/strings/renderTable/borders.ts","../src/strings/renderTable/displayWidth.ts","../src/strings/renderTable/cells.ts","../src/strings/renderTable/markdown.ts","../src/strings/renderTable/renderSingle.ts","../src/strings/renderTable/pagination.ts","../src/strings/renderTable/renderTable.ts","../src/strings/prettify.ts","../src/strings/prettyDifference.ts","../src/index.ts"],"sourcesContent":["/**\n * Exhaustiveness guard for discriminated unions. Place in the `default` branch of a `switch` over a\n * union's discriminant: if a new variant is added without a matching case, the call fails to compile.\n * Throws at runtime if reached with a value the types said was impossible.\n *\n * @example\n * ```ts\n * type Shape = { kind: 'circle' } | { kind: 'square' };\n *\n * function area(shape: Shape): number {\n * switch (shape.kind) {\n * case 'circle': return Math.PI;\n * case 'square': return 1;\n * default: return assertNever(shape); // compile error if a Shape variant is added\n * }\n * }\n * ```\n */\nexport function assertNever(value: never): never {\n throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);\n}\n","import { readdir } from 'node:fs/promises';\nimport * as path from 'node:path';\n\nimport type { ILogger } from '@seedcord/types';\nimport type * as fs from 'node:fs';\n\n/**\n * Determines if a directory entry is a TypeScript or JavaScript file.\n *\n * @param entry - The directory entry to check.\n * @returns True if the entry is a file ending with .ts or .js.\n */\nexport function isTsOrJsFile(entry: fs.Dirent): boolean {\n return (\n entry.isFile() &&\n (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) &&\n !entry.name.endsWith('.d.ts') &&\n !entry.name.endsWith('.map')\n );\n}\n\n/**\n * Recursively traverses through a directory, importing all .ts and .js files and applying a callback to each import.\n *\n * @param dir - The directory path to traverse.\n * @param callback - A function that will be called for each imported module. It receives the full file path, the file's relative path, and the imported module as arguments.\n * @returns A Promise that resolves when the traversal is complete.\n *\n * @example\n * ```ts\n * await traverseDirectory(\n * './commands',\n * (fullPath, relativePath, imported) => {\n * for (const exported of Object.values(imported)) register(exported);\n * },\n * logger\n * );\n * ```\n */\nexport async function traverseDirectory(\n dir: string,\n callback: (fullPath: string, relativePath: string, imported: Record<string, unknown>) => Promise<void> | void,\n logger: ILogger\n): Promise<void> {\n let entries: fs.Dirent[];\n\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (err) {\n logger.error(`Failed to read directory ${dir}`, err);\n entries = [];\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(process.cwd(), fullPath);\n\n if (entry.isDirectory()) {\n await traverseDirectory(fullPath, callback, logger);\n } else if (isTsOrJsFile(entry)) {\n const imported = (await import(fullPath)) as Record<string, unknown>;\n await callback(fullPath, relativePath, imported);\n }\n }\n}\n\n/**\n * Options for formatting file paths.\n */\nexport interface FormatFileOptions {\n /**\n * Whether to return only the directory part of the path.\n *\n * @defaultValue `false`\n */\n onlyDir?: boolean;\n /**\n * A prefix to prepend to the formatted path.\n *\n * @defaultValue `'./'`\n */\n prefix?: string;\n}\n\n/**\n * Formats a file path relative to the current working directory.\n * @param filePath - The file path to format.\n * @param options - Formatting options.\n * @returns The formatted file path.\n *\n * @example\n * ```ts\n * // cwd is /repo\n * formatFilePath('/repo/src/Bot.ts'); // './src/Bot.ts'\n * formatFilePath('/repo/src/Bot.ts', { onlyDir: true }); // './src'\n * formatFilePath('/repo/src/Bot.ts', { prefix: '' }); // 'src/Bot.ts'\n * ```\n */\nexport function formatFilePath(filePath: string, options: FormatFileOptions = {}): string {\n const { onlyDir = false, prefix = './' } = options;\n\n const resolved = onlyDir\n ? path.relative(process.cwd(), filePath.replace(/\\/[^/]*$/, ''))\n : path.relative(process.cwd(), filePath);\n return `${prefix}${resolved}`;\n}\n","/**\n * Shuffles an array using the Fisher-Yates algorithm.\n * This function creates a new array with the same elements in a random order,\n * without modifying the original array.\n *\n * @typeParam TArray - The type of elements in the array\n * @param items - The array to shuffle\n * @returns A new array with the same elements in a random order\n *\n * @example\n * ```typescript\n * const numbers = [1, 2, 3, 4, 5];\n * const shuffled = fyShuffle(numbers);\n * // shuffled might be [3, 1, 5, 2, 4]\n * // numbers is still [1, 2, 3, 4, 5]\n * ```\n */\nexport function fyShuffle<TArray>(items: TArray[]): TArray[] {\n const array = items.slice();\n for (let i = array.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n // @ts-expect-error - TypeScript doesn't recognize that TArray can be swapped\n [array[i], array[j]] = [array[j], array[i]];\n }\n return array;\n}\n","import type { EpochMs, EpochSec } from '@seedcord/types';\n\n/** Converts absolute epoch milliseconds to epoch seconds, the unit Discord `<t:...>` timestamp markup reads. */\nexport function toEpochSeconds(ms: EpochMs): EpochSec {\n return Math.round(ms / 1000) as EpochSec;\n}\n","import { toEpochSeconds } from './toEpochSeconds';\n\nimport type { EpochMs, EpochSec } from '@seedcord/types';\n\n/** Current time in epoch seconds. Shares the one ms→s rounding rule with {@link toEpochSeconds}. */\nexport function currentTime(): EpochSec {\n return toEpochSeconds(Date.now() as EpochMs);\n}\n","/**\n * Generates a random numeric code with the specified number of digits.\n *\n * @param digits - The number of digits for the generated code.\n * @returns A random numeric code with the specified number of digits.\n *\n * @example\n * ```ts\n * generateCode(6); // e.g. 482915\n * ```\n */\nexport function generateCode(digits: number): number {\n const min = Math.pow(10, digits - 1);\n const max = Math.pow(10, digits) - 1;\n return Math.floor(Math.random() * (max - min + 1) + min);\n}\n","/**\n * Returns the ordinal suffix for a given number.\n *\n * @param n - The number to get the ordinal for\n * @returns The number with its ordinal suffix\n *\n * @example\n * ordinal(1); // \"1st\"\n * ordinal(22); // \"22nd\"\n * ordinal(13); // \"13th\"\n */\nexport function ordinal(n: number): string {\n const s = ['th', 'st', 'nd', 'rd'];\n const v = n % 100;\n const index = (v - 20) % 10;\n const suffix = s[index] ?? s[v] ?? s[0];\n if (!suffix) return `${n}th`;\n\n return `${n}${suffix}`;\n}\n","const UNIT_MS = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000\n} as const;\n\ntype DurationUnit = keyof typeof UNIT_MS;\n\n/** A duration literal like `30m`, `24h`, or `500ms`, a number followed by a unit (`ms`, `s`, `m`, `h`, `d`). */\nexport type ValidDuration = `${number}${DurationUnit}`;\n\nconst DURATION_PATTERN = new RegExp(`^(\\\\d+)(${Object.keys(UNIT_MS).join('|')})$`);\n\n/**\n * Parses a short duration string like `24h`, `30m`, `90s`, `7d`, or `500ms` into milliseconds.\n *\n * The grammar is one or more digits followed by one lowercase unit (`ms`, `s`, `m`, `h`, `d`).\n * Anything else returns `null`, including a bare number, an unknown unit, an uppercase unit,\n * surrounding whitespace, a fractional value, and a result of zero. The return is a positive\n * number or `null`, never `0` or `NaN`, so a malformed input can never read as a valid duration.\n *\n * @param input - The duration string to parse.\n * @returns The duration in milliseconds, or `null` if `input` is not a well-formed positive duration.\n *\n * @example\n * ```ts\n * parseDuration('24h'); // 86400000\n * parseDuration('30m'); // 1800000\n * parseDuration('1.5h'); // null\n * parseDuration('foo'); // null\n * ```\n */\nexport function parseDuration(input: string): number | null {\n const match = DURATION_PATTERN.exec(input);\n if (!match) return null;\n\n const ms = Number(match[1]) * UNIT_MS[match[2] as DurationUnit];\n return ms > 0 ? ms : null;\n}\n","/**\n * Takes two numbers and returns the percentage of the first number in the second number with two decimal places.\n *\n * @param num1 - The first number.\n * @param num2 - The second number.\n *\n * @returns The percentage of the first number in the second number with two decimal places.\n *\n * @example\n * ```ts\n * percentage(25, 200); // 12.5\n * percentage(1, 3); // 33.33\n * ```\n */\nexport function percentage(num1: number, num2: number): number {\n return Number(((num1 / num2) * 100).toFixed(2));\n}\n","/**\n * Rounds a number to a specified number of decimal places.\n *\n * @param num - The number to be rounded.\n * @param precision - The number of decimal places to round to.\n * @returns The rounded number.\n *\n * @example\n * ```ts\n * round(3.14159, 2); // 3.14\n * round(2.5, 0); // 3\n * ```\n */\nexport function round(num: number, precision: number): number {\n const factor = Math.pow(10, precision);\n return Math.round((num + Number.EPSILON) * factor) / factor;\n}\n","import type { TupleOf } from 'type-fest';\n\nexport interface RoundToDenomOptions {\n /**\n * Suffixes to use for each denomination level.\n *\n * @defaultValue `['K', 'M', 'B', 'T', 'Q']`\n */\n suffixes?: TupleOf<5, string>;\n /**\n * Number of decimal places to include in the rounded result.\n *\n * @defaultValue `1`\n */\n precision?: number;\n}\n\n/**\n * Rounds a number to a string representation with a denomination suffix.\n * @param num - The number to round.\n * @example\n * ```ts\n * roundToDenomination(1234); // \"1.2K\"\n * roundToDenomination(10000, { suffixes: ['k', 'm', 'b', 't', 'q'] }); // \"10k\"\n * roundToDenomination(12345678); // \"12.3M\"\n * ```\n * @returns The rounded number as a string with a denomination suffix.\n */\nexport function roundToDenomination(num: number, opts?: RoundToDenomOptions): string {\n const { suffixes = ['K', 'M', 'B', 'T', 'Q'], precision = 1 } = opts ?? {};\n\n if (num < 10000) {\n return num.toString();\n }\n\n let index = -1;\n let temp = num;\n\n while (temp >= 1000 && index < suffixes.length - 1) {\n temp /= 1000;\n index++;\n }\n\n let result;\n\n if (temp % 1 === 0) {\n result = temp.toString();\n } else {\n const adjustedTemp = Math.round(temp * Math.pow(10, precision + 1)) / Math.pow(10, precision + 1);\n result = adjustedTemp.toFixed(precision);\n }\n\n if (result.endsWith('.9')) {\n result = Math.ceil(Number(result)).toString();\n }\n\n if (result.endsWith('.0')) {\n result = result.substring(0, result.length - 2);\n }\n\n if (result === '1000') {\n index += 1;\n result = '1';\n }\n\n return result + (index >= 0 ? suffixes[index] : '');\n}\n","import type { ILogger } from '@seedcord/types';\nimport type { JsonPrimitive } from 'type-fest';\n\n/**\n * JSONify an arbitrary type while allowing any object position to be replaced\n * by a circular marker. Optional keys stay optional.\n */\nexport type JsonifyWithCirculars<BaseType, Marker extends string = '[Circular]'> = BaseType extends JsonPrimitive\n ? BaseType\n : BaseType extends bigint\n ? string\n : BaseType extends Date\n ? string\n : BaseType extends { toJSON(): infer J }\n ? unknown extends J\n ? JsonifyObject<BaseType, Marker>\n : JsonifyWithCirculars<J, Marker>\n : BaseType extends readonly (infer U)[]\n ? (JsonifyWithCirculars<U, Marker> | Marker)[]\n : BaseType extends Map<infer K, infer V>\n ? [JsonifyWithCirculars<K, Marker> | Marker, JsonifyWithCirculars<V, Marker> | Marker][]\n : BaseType extends Set<infer U2>\n ? (JsonifyWithCirculars<U2, Marker> | Marker)[]\n : BaseType extends (...args: unknown[]) => unknown\n ? never\n : BaseType extends object\n ? JsonifyObject<BaseType, Marker>\n : never;\n\n/**\n * Helper to JSONify object types with circular markers.\n *\n * @internal\n */\nexport type JsonifyObject<BaseType, Marker extends string> = {\n [K in keyof BaseType as K extends symbol\n ? never\n : BaseType[K] extends (...args: unknown[]) => unknown\n ? never\n : K]:\n | JsonifyWithCirculars<Exclude<BaseType[K], undefined>, Marker>\n | Extract<BaseType[K], undefined>\n | Marker;\n};\n\n/**\n * Returned by {@link filterCirculars} when a value cannot be made JSON-safe. The original value is\n * never returned on failure, because it would re-throw in the caller's own `JSON.stringify`.\n */\nexport interface UnserializableValue {\n '[unserializable]': string;\n}\n\n/**\n * Configuration for {@link filterCirculars}.\n */\nexport interface FilterCircularsOptions<Marker extends string = '[Circular]'> {\n /** Optional {@link ILogger} used to log stringify or parse errors. */\n logger?: ILogger;\n /**\n * Override the circular placeholder.\n *\n * @defaultValue `'[Circular]'`\n */\n marker?: Marker;\n /**\n * Processing mode. `json` uses stringify and parse (might end up using a `toJSON()` if found). `decycle` builds a safe clone first.\n *\n * @defaultValue `'decycle'`\n */\n mode?: 'json' | 'decycle';\n}\n\n/**\n * Creates a clean, JSON safe copy of a value and replaces circular references with a marker.\n *\n * In `json` mode it behaves like stringify then parse with a replacer that handles cycles and BigInt.\n * In `decycle` mode it first clones without using toJSON, then you can stringify the result later.\n *\n * @typeParam ObjType - Type of the input value.\n * @typeParam Marker - Marker string used for circular references.\n *\n * @param value - The value to clone safely.\n * @param options - Optional configuration.\n *\n * @returns A JSON safe structure with circular references replaced by the marker.\n *\n * @example\n * ```ts\n * interface Test {\n * name: string;\n * self?: Test;\n * }\n *\n * const obj: Test = { name: 'seedcord' };\n * obj.self = obj;\n *\n * const clean = filterCirculars(obj);\n * // ^? { name: string; self?: \"[Circular]\" | { ... } }\n * console.log(clean.self); // \"[Circular]\"\n * ```\n */\nexport function filterCirculars<ObjType, Marker extends string = '[Circular]'>(\n value: ObjType,\n options?: FilterCircularsOptions<Marker>\n): JsonifyWithCirculars<ObjType, Marker> | UnserializableValue {\n const logger = options?.logger;\n const marker = (options?.marker ?? '[Circular]') as Marker;\n const mode = options?.mode ?? 'decycle';\n\n if (mode === 'json') return json(value, marker, logger);\n\n try {\n return decycle(value, marker);\n } catch (error) {\n logger?.error('filterCirculars decycle error', error);\n return { '[unserializable]': 'decycle failed' };\n }\n}\n\n/**\n * Attempts to build a JSONified object using JSON.stringify and JSON.parse.\n *\n * @internal\n */\nfunction json<ObjType, Marker extends string>(\n value: ObjType,\n marker: Marker,\n logger?: ILogger\n): JsonifyWithCirculars<ObjType, Marker> | UnserializableValue {\n const seen = new WeakSet<object>();\n let json: string | undefined;\n\n try {\n json = JSON.stringify(value, (_k: string, v: unknown) => {\n if (typeof v === 'bigint') return v.toString();\n if (typeof v === 'object' && v !== null) {\n const obj = v;\n if (seen.has(obj)) return marker;\n seen.add(obj);\n }\n return v;\n });\n } catch (error) {\n logger?.error('filterCirculars stringify error', error);\n if (typeof value === 'object' && value !== null) {\n logger?.error('top level keys', Object.keys(value));\n }\n return { '[unserializable]': 'stringify failed' };\n }\n\n if (typeof json !== 'string') {\n return { '[unserializable]': 'stringify returned undefined' };\n }\n\n try {\n return JSON.parse(json) as JsonifyWithCirculars<ObjType, Marker>;\n } catch (error) {\n logger?.error('filterCirculars parse error', error);\n logger?.error('bad JSON', json);\n return { '[unserializable]': 'parse failed' };\n }\n}\n\n/**\n * Builds a JSON safe clone without calling toJSON on class instances.\n *\n * @internal\n */\nfunction decycle<ObjType, Marker extends string = '[Circular]'>(\n input: ObjType,\n marker: Marker,\n seen = new WeakSet<object>()\n): JsonifyWithCirculars<ObjType, Marker> {\n const recur = (val: unknown): unknown => {\n if (val === null) return null;\n const t = typeof val;\n\n if (t === 'bigint') return (val as bigint).toString();\n if (t !== 'object') return val;\n\n const obj = val as Record<string | number | symbol, unknown>;\n\n if (seen.has(obj)) return marker;\n seen.add(obj);\n\n if (obj instanceof Date) return obj.toISOString();\n if (obj instanceof RegExp) return obj.toString();\n\n if (Array.isArray(obj)) {\n return obj.map((item) => recur(item));\n }\n\n if (obj instanceof Map) {\n return Array.from(obj, ([k, v]) => [recur(k), recur(v)]);\n }\n\n if (obj instanceof Set) {\n return Array.from(obj, (v) => recur(v));\n }\n\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (typeof v === 'function') continue;\n out[k] = recur(v);\n }\n\n return out;\n };\n\n return recur(input) as JsonifyWithCirculars<ObjType, Marker>;\n}\n","import type { UnionToIntersection } from 'type-fest';\n\n/**\n * Extracts the type of a nested property, distributing over unions.\n *\n * @internal\n */\nexport type DeepGet<Obj, Key extends string> = Obj extends unknown\n ? Key extends `${infer K}.${infer Rest}`\n ? K extends keyof Obj\n ? DeepGet<NonNullable<Obj[K]>, Rest>\n : never\n : Key extends keyof Obj\n ? Obj[Key]\n : never\n : never;\n\n/**\n * Converts a dot-notation path string into a nested object type with a specific leaf value.\n *\n * @internal\n */\nexport type PathToObj<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}`\n ? { [K in Head]: PathToObj<Tail, Value> }\n : { [K in Path]: Value };\n\n/**\n * Checks for the presence of nested keys in an object that's possibly a distributed union, narrowing the object type accordingly.\n *\n * Checks if an object has the specified nested keys and that their values are not null or undefined. If they are not, the object type is narrowed to reflect the presence of these keys with their respective types from the original distributed union object.\n *\n * @param obj - The object to check.\n * @param keys - An array of dot-notation paths to check.\n * @returns True if all keys exist and are non-null/defined, narrowing the object type.\n *\n * @example\n * ```ts\n * interface Test {\n * a?: {\n * b?: {\n * c?: string;\n * };\n * } | null;\n * x?: number | null;\n * }\n *\n * const obj: Test = { a: { b: { c: 'hello' } }, x: 42 };\n *\n * if (hasKeys(obj, ['a.b.c', 'x'])) {\n * // Here, obj is narrowed to:\n * // {\n * // a: { b: { c: string } };\n * // x: number;\n * // }\n * console.log(obj.a.b.c.toUpperCase()); // Safe to access and use\n * console.log(obj.x.toFixed(2)); // Safe to access and use\n * }\n * ```\n */\nexport function hasKeys<Obj extends object, Keys extends string>(\n obj: Obj,\n keys: Keys[]\n): obj is Obj &\n UnionToIntersection<\n Keys extends unknown ? PathToObj<Keys, UnionToIntersection<NonNullable<DeepGet<Obj, Keys>>>> : never\n > {\n return keys.every((key) => {\n const parts = key.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (\n current === null ||\n current === undefined ||\n (typeof current !== 'object' && typeof current !== 'function')\n ) {\n return false;\n }\n if (!(part in current)) {\n return false;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current !== null && current !== undefined;\n });\n}\n","/**\n * Copies only the keys whose values are defined.\n *\n * @typeParam TObject - the original object type you're pulling from\n * @typeParam TKey - the keys to copy when defined\n * @param source - the object to read values from\n * @param keys - optional list of keys to include when present. {@default all keys}\n *\n * @example\n * ```ts\n * interface Config {\n * host?: string;\n * port?: number;\n * user?: string;\n * password?: string;\n * }\n *\n * const config: Config = {\n * host: 'localhost',\n * port: undefined,\n * user: 'admin',\n * password: undefined\n * };\n *\n * const definedConfig = keepDefined(config, 'host', 'port', 'user', 'password');\n * // Result: { host: 'localhost', user: 'admin' }\n * ```\n */\nexport function keepDefined<TObject extends object, TKey extends keyof TObject>(\n source: TObject,\n ...keys: readonly TKey[]\n): Partial<Pick<TObject, TKey extends never ? keyof TObject : TKey>> {\n const selectedKeys = keys.length > 0 ? keys : (Object.keys(source) as TKey[]);\n const result: Partial<TObject> = {};\n\n for (const key of selectedKeys) {\n const value = source[key];\n if (value !== undefined && value !== null) {\n result[key] = value;\n }\n }\n return result;\n}\n","/**\n * Returns the word with its first letter capitalized and the rest in lowercase.\n * @param word - The word to be formatted.\n * @returns The formatted word.\n *\n * @example\n * ```ts\n * capitalize('hELLO'); // 'Hello'\n * ```\n */\nexport function capitalize(word: string): string {\n return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();\n}\n","/**\n * Function takes an array of strings or numbers and returns the number of characters in the longest string/number\n *\n * @param arr - The array of strings or numbers\n * @returns The length of the longest element when converted to string\n *\n * @example\n * ```ts\n * longestStringLength(['ab', 12345]); // 5\n * ```\n */\nexport function longestStringLength(arr: (string | number)[]): number {\n return Math.max(...arr.map((el) => el.toString().length));\n}\n","import type { BorderStyle } from './options';\n\nexport interface LinePart {\n left: string;\n mid: string;\n right: string;\n fill: string;\n}\n\nexport interface BorderChars {\n top: LinePart;\n bottom: LinePart;\n sep: LinePart;\n headerSep: LinePart;\n vertical: string;\n}\n\nconst DOUBLE: BorderChars = {\n top: { left: '╔', mid: '╦', right: '╗', fill: '═' },\n bottom: { left: '╚', mid: '╩', right: '╝', fill: '═' },\n sep: { left: '╟', mid: '╫', right: '╢', fill: '─' },\n headerSep: { left: '╠', mid: '╬', right: '╣', fill: '═' },\n vertical: '║'\n};\n\nconst ROUNDED: BorderChars = {\n top: { left: '╭', mid: '┬', right: '╮', fill: '─' },\n bottom: { left: '╰', mid: '┴', right: '╯', fill: '─' },\n sep: { left: '├', mid: '┼', right: '┤', fill: '─' },\n headerSep: { left: '├', mid: '┼', right: '┤', fill: '─' },\n vertical: '│'\n};\n\nconst ASCII: BorderChars = {\n top: { left: '+', mid: '+', right: '+', fill: '-' },\n bottom: { left: '+', mid: '+', right: '+', fill: '-' },\n sep: { left: '+', mid: '+', right: '+', fill: '-' },\n headerSep: { left: '+', mid: '+', right: '+', fill: '-' },\n vertical: '|'\n};\n\nexport const BORDERS: Record<Exclude<BorderStyle, 'markdown'>, BorderChars> = {\n double: DOUBLE,\n rounded: ROUNDED,\n ascii: ASCII\n};\n","const segmenter = new Intl.Segmenter();\n\n/* eslint-disable no-magic-numbers -- Unicode code-point range boundaries */\nconst ZERO_WIDTH_RANGES: readonly (readonly [number, number])[] = [\n [0x200b, 0x200b],\n [0x0300, 0x036f],\n [0x1ab0, 0x1aff],\n [0x1dc0, 0x1dff],\n [0x20d0, 0x20ff],\n [0xfe20, 0xfe2f]\n];\n\nconst WIDE_RANGES: readonly (readonly [number, number])[] = [\n [0x1100, 0x115f],\n [0x2e80, 0x303e],\n [0x3041, 0x33ff],\n [0x3400, 0x4dbf],\n [0x4e00, 0x9fff],\n [0xa000, 0xa4cf],\n [0xac00, 0xd7a3],\n [0xf900, 0xfaff],\n [0xfe30, 0xfe4f],\n [0xff00, 0xff60],\n [0xffe0, 0xffe6],\n [0x1f300, 0x1faff],\n [0x20000, 0x3fffd]\n];\n/* eslint-enable no-magic-numbers */\n\nfunction inRanges(cp: number, ranges: readonly (readonly [number, number])[]): boolean {\n return ranges.some(([lo, hi]) => cp >= lo && cp <= hi);\n}\n\nfunction segmentWidth(segment: string): number {\n const cp = segment.codePointAt(0) ?? 0;\n if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;\n // East Asian Wide and Fullwidth code points take two monospace columns\n return inRanges(cp, WIDE_RANGES) ? 2 : 1;\n}\n\n// .length counts UTF-16 units, so it miscounts emoji, astral, and CJK chars and the border drifts\nexport function displayWidth(text: string): number {\n let width = 0;\n for (const { segment } of segmenter.segment(text)) width += segmentWidth(segment);\n return width;\n}\n\nfunction segments(text: string): string[] {\n return Array.from(segmenter.segment(text), (entry) => entry.segment);\n}\n\nexport function takeWidth(text: string, maxColumns: number): string {\n let width = 0;\n let taken = '';\n for (const segment of segments(text)) {\n const next = width + segmentWidth(segment);\n if (next > maxColumns) break;\n width = next;\n taken += segment;\n }\n return taken;\n}\n\nfunction hardBreak(token: string, maxColumns: number): string[] {\n const pieces: string[] = [];\n let rest = token;\n while (displayWidth(rest) > maxColumns) {\n const head = takeWidth(rest, maxColumns);\n pieces.push(head);\n rest = rest.slice(head.length);\n }\n if (rest.length > 0) pieces.push(rest);\n return pieces;\n}\n\nexport function wrapText(text: string, maxColumns: number): string[] {\n const lines: string[] = [];\n let current = '';\n for (const token of text.split(' ')) {\n const candidate = current === '' ? token : `${current} ${token}`;\n if (displayWidth(candidate) <= maxColumns) {\n current = candidate;\n continue;\n }\n if (current !== '') lines.push(current);\n if (displayWidth(token) <= maxColumns) {\n current = token;\n continue;\n }\n const pieces = hardBreak(token, maxColumns);\n current = pieces.pop() ?? '';\n lines.push(...pieces);\n }\n if (current !== '' || lines.length === 0) lines.push(current);\n return lines;\n}\n","import { displayWidth, takeWidth } from './displayWidth';\n\nimport type { Alignment } from './options';\n\nconst ELLIPSIS = '…';\n\nexport function wrapFence(content: string): string {\n return `\\`\\`\\`\\n${content}\\`\\`\\``;\n}\n\nexport function truncate(content: string, maxWidth: number): string {\n if (displayWidth(content) <= maxWidth) return content;\n return takeWidth(content, maxWidth - displayWidth(ELLIPSIS)) + ELLIPSIS;\n}\n\nexport function isNumericColumn(grid: readonly (readonly string[])[], col: number, header: boolean): boolean {\n // the header label is rarely numeric, so judging the column by data rows only\n const dataRows = header ? grid.slice(1) : grid;\n const cells = dataRows.map((row) => row[col] ?? '').filter((cell) => cell !== '');\n return cells.length > 0 && cells.every((cell) => !Number.isNaN(Number(cell)));\n}\n\nexport function padCell(content: string, columnWidth: number, align: Alignment): string {\n const slack = columnWidth - displayWidth(content);\n if (align === 'right') return ' '.repeat(slack) + content;\n if (align === 'center') {\n const left = Math.floor(slack / 2);\n return ' '.repeat(left) + content + ' '.repeat(slack - left);\n }\n return content + ' '.repeat(slack);\n}\n","import { padCell } from './cells';\nimport { displayWidth } from './displayWidth';\n\nimport type { Alignment } from './options';\n\nexport function renderMarkdown(\n grid: readonly (readonly string[])[],\n columnCount: number,\n alignments: readonly Alignment[],\n pad: string\n): string {\n // a raw | or \\ in a cell would split or corrupt the GFM row, so escape before measuring widths\n const escapeCell = (value: string): string => value.replace(/\\\\/g, '\\\\\\\\').replace(/\\|/g, '\\\\|');\n const escaped = grid.map((row) => Array.from({ length: columnCount }, (_, col) => escapeCell(row[col] ?? '')));\n\n const columnWidths = Array.from({ length: columnCount }, (_, col) =>\n escaped.reduce((max, row) => Math.max(max, displayWidth(row[col] ?? '')), 0)\n );\n\n const cell = (content: string, col: number): string =>\n pad + padCell(content, columnWidths[col] ?? 0, alignments[col] ?? 'left') + pad;\n const row = (cells: readonly string[]): string =>\n `|${columnWidths.map((_, col) => cell(cells[col] ?? '', col)).join('|')}|`;\n\n const delimiterCell = (col: number): string => {\n const align = alignments[col] ?? 'left';\n if (align === 'center') return ' :---: ';\n if (align === 'right') return ' ---: ';\n return ' --- ';\n };\n\n const [head = [], ...body] = escaped;\n const delimiter = `|${columnWidths.map((_, col) => delimiterCell(col)).join('|')}|`;\n const lines = [row(head), delimiter, ...body.map(row)];\n return `${lines.join('\\n')}\\n`;\n}\n","import { BORDERS } from './borders';\nimport { isNumericColumn, padCell, truncate, wrapFence } from './cells';\nimport { displayWidth, wrapText } from './displayWidth';\nimport { renderMarkdown } from './markdown';\n\nimport type { LinePart } from './borders';\nimport type { TableOptions } from './options';\n\nexport function renderSingle(data: readonly (readonly string[])[], options?: TableOptions): string {\n if (data.length === 0) return '';\n\n const {\n align,\n border = 'rounded',\n header = true,\n padding = 1,\n emptyCell = '',\n numericAlign = false,\n maxWidth,\n overflow = 'wrap',\n fence\n } = options ?? {};\n\n const columnCount = data.reduce((max, row) => Math.max(max, row.length), 0);\n if (columnCount === 0) return '';\n\n // a raw newline in a cell would split the framed output across physical lines, so collapse it\n const grid = data.map((row) =>\n Array.from({ length: columnCount }, (_, i) => {\n const cell = (row[i] ?? '').replace(/\\r?\\n/g, ' ');\n const filled = cell === '' ? emptyCell : cell;\n if (maxWidth === undefined || overflow !== 'truncate') return filled;\n return truncate(filled, maxWidth);\n })\n );\n\n const alignments = Array.from({ length: columnCount }, (_, col) => {\n const explicit = typeof align === 'string' ? align : align?.[col];\n if (explicit) return explicit;\n if (numericAlign && isNumericColumn(grid, col, header)) return 'right';\n return 'left';\n });\n\n const pad = ' '.repeat(padding);\n\n if (border === 'markdown') {\n const md = renderMarkdown(grid, columnCount, alignments, pad);\n return fence ? wrapFence(md) : md;\n }\n\n const wrap = maxWidth !== undefined && overflow === 'wrap';\n const rows = grid.map((row) => row.map((cell) => (wrap ? wrapText(cell, maxWidth) : [cell])));\n\n const columnWidths = Array.from({ length: columnCount }, (_, col) =>\n rows.reduce(\n (max, row) =>\n Math.max(\n max,\n (row[col] ?? []).reduce((w, line) => Math.max(w, displayWidth(line)), 0)\n ),\n 0\n )\n );\n\n const chars = BORDERS[border];\n\n function drawLine(part: LinePart): string {\n const segments = columnWidths.map((width) => part.fill.repeat(width + padding * 2));\n return part.left + segments.join(part.mid) + part.right;\n }\n\n function renderRow(row: readonly (readonly string[])[]): string {\n const lineCount = row.reduce((max, cellLines) => Math.max(max, cellLines.length), 1);\n const physical = Array.from({ length: lineCount }, (_, line) =>\n columnWidths\n .map((width, col) => pad + padCell(row[col]?.[line] ?? '', width, alignments[col] ?? 'left') + pad)\n .join(chars.vertical)\n );\n return physical.map((line) => chars.vertical + line + chars.vertical).join('\\n');\n }\n\n const lines: string[] = [drawLine(chars.top)];\n rows.forEach((row, rowIndex) => {\n lines.push(renderRow(row));\n if (rowIndex >= rows.length - 1) return;\n lines.push(drawLine(header && rowIndex === 0 ? chars.headerSep : chars.sep));\n });\n lines.push(drawLine(chars.bottom));\n\n const body = `${lines.join('\\n')}\\n`;\n return fence ? wrapFence(body) : body;\n}\n","import { renderSingle } from './renderSingle';\n\nimport type { PagedTableOptions } from './options';\n\nconst DEFAULT_BUDGET = 2000;\n\nexport function paginate(data: readonly (readonly string[])[], options: PagedTableOptions): string[] {\n if (data.length === 0) return [];\n\n const { budget = DEFAULT_BUDGET, ...tableOptions } = options;\n const header = tableOptions.header === false ? undefined : data[0];\n const body = header ? data.slice(1) : data;\n\n const render = (rows: readonly (readonly string[])[]): string =>\n renderSingle(header ? [header, ...rows] : rows, tableOptions);\n\n if (body.length === 0) return [render([])];\n\n const pages: string[] = [];\n let current: (readonly string[])[] = [];\n\n for (const row of body) {\n // an empty page always takes the row, since a page smaller than one body row cannot exist\n if (current.length === 0 || render([...current, row]).length <= budget) {\n current.push(row);\n continue;\n }\n pages.push(render(current));\n current = [row];\n }\n\n pages.push(render(current));\n return pages;\n}\n","import { paginate } from './pagination';\nimport { renderSingle } from './renderSingle';\n\nimport type { PagedTableOptions, TableOptions } from './options';\n\n/**\n * Renders a grid of strings as a framed monospace table for Discord output.\n *\n * Column widths use display width, so emoji, astral, and CJK cells stay aligned. The first row is a\n * header by default with a separator beneath it. Ragged rows are padded to the widest row.\n *\n * Pass `budget` to get one string per page instead of a single string, splitting the body across a\n * character limit and re-emitting the header on each page.\n *\n * @param data - Rows of cells. The widest row sets the column count.\n * @param options - Rendering options, see {@link TableOptions} and {@link PagedTableOptions}.\n * @returns A single string by default, or `string[]` of pages when `budget` is set.\n *\n * @example\n * ```ts\n * renderTable([\n * ['Name', 'Age'],\n * ['Alice', '30'],\n * ['Bob', '25']\n * ]);\n * // ╭───────┬─────╮\n * // │ Name │ Age │\n * // ├───────┼─────┤\n * // │ Alice │ 30 │\n * // ├───────┼─────┤\n * // │ Bob │ 25 │\n * // ╰───────┴─────╯\n * ```\n *\n * @example\n * `budget` returns one page per chunk, each within the limit, with the header repeated.\n * ```ts\n * const pages = renderTable(\n * [['ID', 'Value'], ['0', 'xxxxx'], ['1', 'xxxxx'], ['2', 'xxxxx'], ['3', 'xxxxx']],\n * { budget: 120 }\n * );\n * // pages.length is 2, and pages[0] renders as\n * // ╭────┬───────╮\n * // │ ID │ Value │\n * // ├────┼───────┤\n * // │ 0 │ xxxxx │\n * // ├────┼───────┤\n * // │ 1 │ xxxxx │\n * // ╰────┴───────╯\n * ```\n */\nexport function renderTable(data: readonly (readonly string[])[], options: PagedTableOptions): string[];\nexport function renderTable(data: readonly (readonly string[])[], options?: TableOptions): string;\nexport function renderTable(\n data: readonly (readonly string[])[],\n options?: TableOptions | PagedTableOptions\n): string | string[] {\n if (options) validateOptions(options);\n if (options && 'budget' in options) return paginate(data, options);\n return renderSingle(data, options);\n}\n\n// maxWidth 0 loops forever in hardBreak, negative padding throws in String.repeat\nfunction validateOptions(options: TableOptions): void {\n const { maxWidth, padding } = options;\n if (maxWidth !== undefined && (!Number.isInteger(maxWidth) || maxWidth < 1)) {\n throw new RangeError(`renderTable maxWidth must be a positive integer, got ${maxWidth}`);\n }\n if (padding !== undefined && (!Number.isInteger(padding) || padding < 0)) {\n throw new RangeError(`renderTable padding must be a non-negative integer, got ${padding}`);\n }\n}\n","import { capitalize } from './capitalize';\n\n/**\n * Options for the `prettify` function.\n */\nexport interface PrettifyOptions {\n capitalize?: boolean;\n}\n/**\n * Converts a string from any common naming convention to human-readable format.\n * Accepts camelCase, PascalCase, snake_case, and kebab-case input.\n *\n * @param key - The string to convert\n * @param opts - Optional configuration\n * @returns A space-separated, human-readable string\n *\n * @example\n * prettify(\"camelCaseString\") // \"camel Case String\"\n * prettify(\"PascalCaseString\") // \"Pascal Case String\"\n * prettify(\"snake_case_string\") // \"snake case string\"\n * prettify(\"kebab-case-string\") // \"kebab case string\"\n * prettify(\"mixedCase_string-name\") // \"mixed Case string name\"\n */\n\nexport function prettify(key: string, opts?: PrettifyOptions): string {\n const result = key\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase/PascalCase\n .replace(/[_-]/g, ' ') // snake_case and kebab-case\n .trim();\n\n if (opts?.capitalize) return capitalize(result);\n\n return result;\n}\n","/**\n * Calculates the difference between two numbers and formats it as a string with a '+' prefix for positive differences.\n *\n * @param numBefore - The initial number value\n * @param numAfter - The final number value\n * @returns A string representing the difference, with a '+' sign for positive differences\n *\n * @example\n * // Returns \"+5\"\n * prettyDifference(10, 15);\n *\n * @example\n * // Returns \"-3\"\n * prettyDifference(10, 7);\n */\nexport function prettyDifference(numBefore: number, numAfter: number): string {\n return (numAfter - numBefore > 0 ? `+${numAfter - numBefore}` : numAfter - numBefore).toString();\n}\n","export * from './misc';\nexport * from './numbers';\nexport * from './objects';\nexport * from './strings';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -6,6 +6,19 @@ import * as fs from "node:fs";
6
6
  * Exhaustiveness guard for discriminated unions. Place in the `default` branch of a `switch` over a
7
7
  * union's discriminant: if a new variant is added without a matching case, the call fails to compile.
8
8
  * Throws at runtime if reached with a value the types said was impossible.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type Shape = { kind: 'circle' } | { kind: 'square' };
13
+ *
14
+ * function area(shape: Shape): number {
15
+ * switch (shape.kind) {
16
+ * case 'circle': return Math.PI;
17
+ * case 'square': return 1;
18
+ * default: return assertNever(shape); // compile error if a Shape variant is added
19
+ * }
20
+ * }
21
+ * ```
9
22
  */
10
23
  declare function assertNever(value: never): never;
11
24
  //#endregion
@@ -23,6 +36,17 @@ declare function isTsOrJsFile(entry: fs.Dirent): boolean;
23
36
  * @param dir - The directory path to traverse.
24
37
  * @param callback - A function that will be called for each imported module. It receives the full file path, the file's relative path, and the imported module as arguments.
25
38
  * @returns A Promise that resolves when the traversal is complete.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * await traverseDirectory(
43
+ * './commands',
44
+ * (fullPath, relativePath, imported) => {
45
+ * for (const exported of Object.values(imported)) register(exported);
46
+ * },
47
+ * logger
48
+ * );
49
+ * ```
26
50
  */
27
51
  declare function traverseDirectory(dir: string, callback: (fullPath: string, relativePath: string, imported: Record<string, unknown>) => Promise<void> | void, logger: ILogger): Promise<void>;
28
52
  /**
@@ -47,6 +71,14 @@ interface FormatFileOptions {
47
71
  * @param filePath - The file path to format.
48
72
  * @param options - Formatting options.
49
73
  * @returns The formatted file path.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // cwd is /repo
78
+ * formatFilePath('/repo/src/Bot.ts'); // './src/Bot.ts'
79
+ * formatFilePath('/repo/src/Bot.ts', { onlyDir: true }); // './src'
80
+ * formatFilePath('/repo/src/Bot.ts', { prefix: '' }); // 'src/Bot.ts'
81
+ * ```
50
82
  */
51
83
  declare function formatFilePath(filePath: string, options?: FormatFileOptions): string;
52
84
  //#endregion
@@ -80,6 +112,11 @@ declare function currentTime(): EpochSec;
80
112
  *
81
113
  * @param digits - The number of digits for the generated code.
82
114
  * @returns A random numeric code with the specified number of digits.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * generateCode(6); // e.g. 482915
119
+ * ```
83
120
  */
84
121
  declare function generateCode(digits: number): number;
85
122
  //#endregion
@@ -118,6 +155,14 @@ type ValidDuration = `${number}${DurationUnit}`;
118
155
  *
119
156
  * @param input - The duration string to parse.
120
157
  * @returns The duration in milliseconds, or `null` if `input` is not a well-formed positive duration.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * parseDuration('24h'); // 86400000
162
+ * parseDuration('30m'); // 1800000
163
+ * parseDuration('1.5h'); // null
164
+ * parseDuration('foo'); // null
165
+ * ```
121
166
  */
122
167
  declare function parseDuration(input: string): number | null;
123
168
  //#endregion
@@ -129,6 +174,12 @@ declare function parseDuration(input: string): number | null;
129
174
  * @param num2 - The second number.
130
175
  *
131
176
  * @returns The percentage of the first number in the second number with two decimal places.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * percentage(25, 200); // 12.5
181
+ * percentage(1, 3); // 33.33
182
+ * ```
132
183
  */
133
184
  declare function percentage(num1: number, num2: number): number;
134
185
  //#endregion
@@ -139,6 +190,12 @@ declare function percentage(num1: number, num2: number): number;
139
190
  * @param num - The number to be rounded.
140
191
  * @param precision - The number of decimal places to round to.
141
192
  * @returns The rounded number.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * round(3.14159, 2); // 3.14
197
+ * round(2.5, 0); // 3
198
+ * ```
142
199
  */
143
200
  declare function round(num: number, precision: number): number;
144
201
  //#endregion
@@ -762,6 +819,11 @@ declare function keepDefined<TObject extends object, TKey extends keyof TObject>
762
819
  * Returns the word with its first letter capitalized and the rest in lowercase.
763
820
  * @param word - The word to be formatted.
764
821
  * @returns The formatted word.
822
+ *
823
+ * @example
824
+ * ```ts
825
+ * capitalize('hELLO'); // 'Hello'
826
+ * ```
765
827
  */
766
828
  declare function capitalize(word: string): string;
767
829
  //#endregion
@@ -771,17 +833,123 @@ declare function capitalize(word: string): string;
771
833
  *
772
834
  * @param arr - The array of strings or numbers
773
835
  * @returns The length of the longest element when converted to string
836
+ *
837
+ * @example
838
+ * ```ts
839
+ * longestStringLength(['ab', 12345]); // 5
840
+ * ```
774
841
  */
775
842
  declare function longestStringLength(arr: (string | number)[]): number;
776
843
  //#endregion
777
- //#region src/strings/generateAsciiTable.d.ts
844
+ //#region src/strings/renderTable/options.d.ts
845
+ type Alignment = 'left' | 'center' | 'right';
846
+ type BorderStyle = 'double' | 'rounded' | 'ascii' | 'markdown';
847
+ type Overflow = 'wrap' | 'truncate';
848
+ interface TableOptions {
849
+ /**
850
+ * Treats the first row as a header and draws a separator beneath it (heavier for the double and ascii frames).
851
+ * @defaultValue true
852
+ */
853
+ header?: boolean;
854
+ /**
855
+ * Cell alignment. A scalar applies to every column. An array sets alignment per column and any column past the end of the array falls back to left.
856
+ * @defaultValue 'left'
857
+ */
858
+ align?: Alignment | readonly Alignment[];
859
+ /**
860
+ * Frame glyph set. `markdown` emits GFM with no outer frame.
861
+ * @defaultValue 'rounded'
862
+ */
863
+ border?: BorderStyle;
864
+ /**
865
+ * Spaces on each side of every cell.
866
+ * @defaultValue 1
867
+ */
868
+ padding?: number;
869
+ /**
870
+ * Fills a missing or empty cell so ragged rows stay aligned.
871
+ * @defaultValue ''
872
+ */
873
+ emptyCell?: string;
874
+ /**
875
+ * Right-aligns a column whose non-empty body cells are all numeric. The header label is not judged, so
876
+ * a numeric column under a text header still aligns. An explicit align for that column wins.
877
+ * @defaultValue false
878
+ */
879
+ numericAlign?: boolean;
880
+ /** Max content display width applied to every column. Wider cells are wrapped or truncated. */
881
+ maxWidth?: number;
882
+ /**
883
+ * How an over-wide cell is handled once maxWidth is set. `wrap` word-wraps onto multiple lines, `truncate` cuts with a trailing ellipsis. The markdown border applies `truncate` but not `wrap`.
884
+ * @defaultValue 'wrap'
885
+ */
886
+ overflow?: Overflow;
887
+ /**
888
+ * Wraps the output in a triple-backtick code block so Discord renders it monospace. The fence
889
+ * characters count against the page budget.
890
+ * @defaultValue false
891
+ */
892
+ fence?: boolean;
893
+ }
894
+ /** {@link TableOptions} with `budget` required. The overload signal that makes renderTable return `string[]` of pages. */
895
+ interface PagedTableOptions extends TableOptions {
896
+ /**
897
+ * Max characters per rendered page. The header is re-emitted on every page. Lower it to leave room
898
+ * for surrounding text. Defaults to Discord's 2000-char message limit.
899
+ * @defaultValue 2000
900
+ */
901
+ budget: number;
902
+ }
903
+ //#endregion
904
+ //#region src/strings/renderTable/renderTable.d.ts
778
905
  /**
779
- * Generates an ASCII table from the provided data.
906
+ * Renders a grid of strings as a framed monospace table for Discord output.
907
+ *
908
+ * Column widths use display width, so emoji, astral, and CJK cells stay aligned. The first row is a
909
+ * header by default with a separator beneath it. Ragged rows are padded to the widest row.
910
+ *
911
+ * Pass `budget` to get one string per page instead of a single string, splitting the body across a
912
+ * character limit and re-emitting the header on each page.
780
913
  *
781
- * @param data - The data to be displayed in the table.
782
- * @returns The generated ASCII table as a string.
914
+ * @param data - Rows of cells. The widest row sets the column count.
915
+ * @param options - Rendering options, see {@link TableOptions} and {@link PagedTableOptions}.
916
+ * @returns A single string by default, or `string[]` of pages when `budget` is set.
917
+ *
918
+ * @example
919
+ * ```ts
920
+ * renderTable([
921
+ * ['Name', 'Age'],
922
+ * ['Alice', '30'],
923
+ * ['Bob', '25']
924
+ * ]);
925
+ * // ╭───────┬─────╮
926
+ * // │ Name │ Age │
927
+ * // ├───────┼─────┤
928
+ * // │ Alice │ 30 │
929
+ * // ├───────┼─────┤
930
+ * // │ Bob │ 25 │
931
+ * // ╰───────┴─────╯
932
+ * ```
933
+ *
934
+ * @example
935
+ * `budget` returns one page per chunk, each within the limit, with the header repeated.
936
+ * ```ts
937
+ * const pages = renderTable(
938
+ * [['ID', 'Value'], ['0', 'xxxxx'], ['1', 'xxxxx'], ['2', 'xxxxx'], ['3', 'xxxxx']],
939
+ * { budget: 120 }
940
+ * );
941
+ * // pages.length is 2, and pages[0] renders as
942
+ * // ╭────┬───────╮
943
+ * // │ ID │ Value │
944
+ * // ├────┼───────┤
945
+ * // │ 0 │ xxxxx │
946
+ * // ├────┼───────┤
947
+ * // │ 1 │ xxxxx │
948
+ * // ╰────┴───────╯
949
+ * ```
783
950
  */
784
- declare function generateAsciiTable(data: string[][]): string;
951
+ declare function renderTable(data: readonly (readonly string[])[], options: PagedTableOptions): string[];
952
+ declare function renderTable(data: readonly (readonly string[])[], options?: TableOptions): string;
785
953
  //#endregion
786
954
  //#region src/strings/prettify.d.ts
787
955
  /**
@@ -829,5 +997,5 @@ declare function prettyDifference(numBefore: number, numAfter: number): string;
829
997
  /** Package version */
830
998
  declare const version: string;
831
999
  //#endregion
832
- export { DeepGet, FilterCircularsOptions, FormatFileOptions, JsonifyObject, JsonifyWithCirculars, PathToObj, PrettifyOptions, RoundToDenomOptions, UnserializableValue, ValidDuration, assertNever, capitalize, currentTime, filterCirculars, formatFilePath, fyShuffle, generateAsciiTable, generateCode, hasKeys, isTsOrJsFile, keepDefined, longestStringLength, ordinal, parseDuration, percentage, prettify, prettyDifference, round, roundToDenomination, toEpochSeconds, traverseDirectory, version };
1000
+ export { DeepGet, FilterCircularsOptions, FormatFileOptions, JsonifyObject, JsonifyWithCirculars, type PagedTableOptions, PathToObj, PrettifyOptions, RoundToDenomOptions, type TableOptions, UnserializableValue, ValidDuration, assertNever, capitalize, currentTime, filterCirculars, formatFilePath, fyShuffle, generateCode, hasKeys, isTsOrJsFile, keepDefined, longestStringLength, ordinal, parseDuration, percentage, prettify, prettyDifference, renderTable, round, roundToDenomination, toEpochSeconds, traverseDirectory, version };
833
1001
  //# sourceMappingURL=index.d.mts.map