novac 2.0.1 → 2.2.0
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.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ============================================================
|
|
4
|
+
// kitformat.js — A comprehensive formatting utility module
|
|
5
|
+
// module.exports = { kitdef: { ...all exports } }
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
// ────────────────────────────────────────────────────────────
|
|
9
|
+
// SECTION 1: CUSTOM FORMAT SPEC REGISTRY
|
|
10
|
+
// Register/unregister custom % specifiers globally
|
|
11
|
+
// ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const _customSpecs = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a custom printf-style format specifier.
|
|
17
|
+
* @param {string} char - Single character (e.g. 'q')
|
|
18
|
+
* @param {function(value, flags, width, precision): string} handler
|
|
19
|
+
*/
|
|
20
|
+
function registerSpec(char, handler) {
|
|
21
|
+
if (typeof char !== 'string' || char.length !== 1)
|
|
22
|
+
throw new TypeError('registerSpec: char must be a single character');
|
|
23
|
+
if (typeof handler !== 'function')
|
|
24
|
+
throw new TypeError('registerSpec: handler must be a function');
|
|
25
|
+
_customSpecs.set(char, handler);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Unregister a custom format specifier.
|
|
30
|
+
* @param {string} char
|
|
31
|
+
*/
|
|
32
|
+
function unregisterSpec(char) {
|
|
33
|
+
_customSpecs.delete(char);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ────────────────────────────────────────────────────────────
|
|
37
|
+
// SECTION 2: PRINTF-STYLE FORMATTING
|
|
38
|
+
// ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Internal: apply width/precision/flags padding to a formatted string chunk.
|
|
42
|
+
*/
|
|
43
|
+
function _applyWidth(str, flags, width, leftAlign) {
|
|
44
|
+
if (width !== null && str.length < width) {
|
|
45
|
+
const pad = flags.includes('0') && !leftAlign ? '0' : ' ';
|
|
46
|
+
if (leftAlign) {
|
|
47
|
+
str = str.padEnd(width, ' ');
|
|
48
|
+
} else {
|
|
49
|
+
if (pad === '0' && (str[0] === '-' || str[0] === '+' || str[0] === ' ')) {
|
|
50
|
+
str = str[0] + str.slice(1).padStart(width - 1, '0');
|
|
51
|
+
} else {
|
|
52
|
+
str = str.padStart(width, pad);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return str;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* printf(fmt, ...args) — C/C++ style printf formatting.
|
|
61
|
+
*
|
|
62
|
+
* Supported specifiers:
|
|
63
|
+
* %d / %i — signed decimal integer
|
|
64
|
+
* %u — unsigned decimal integer
|
|
65
|
+
* %f — fixed-point float
|
|
66
|
+
* %e / %E — scientific notation
|
|
67
|
+
* %g / %G — shorter of %f / %e
|
|
68
|
+
* %s — string
|
|
69
|
+
* %S — string UPPERCASE
|
|
70
|
+
* %c — character (number → char)
|
|
71
|
+
* %x / %X — hex (lower/upper)
|
|
72
|
+
* %o — octal
|
|
73
|
+
* %b — binary
|
|
74
|
+
* %p — "pointer" style (0x + hex)
|
|
75
|
+
* %j — JSON.stringify
|
|
76
|
+
* %% — literal %
|
|
77
|
+
*
|
|
78
|
+
* Flags: -, +, ' ' (space), 0, #
|
|
79
|
+
* Width & precision supported.
|
|
80
|
+
* Custom specs registered via registerSpec() also work here.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} fmt
|
|
83
|
+
* @param {...*} args
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function printf(fmt, ...args) {
|
|
87
|
+
let argIdx = 0;
|
|
88
|
+
// Regex: %[flags][width][.precision]specifier
|
|
89
|
+
return fmt.replace(/%(\d+\$)?([-+ #0]*)(\*|\d+)?(?:\.(\*|\d+))?([%diufFeEgGsSscxXobpj]|.)/g,
|
|
90
|
+
(match, argPos, flagsStr, widthStr, precStr, spec) => {
|
|
91
|
+
if (spec === '%') return '%';
|
|
92
|
+
|
|
93
|
+
const flags = flagsStr || '';
|
|
94
|
+
const leftAlign = flags.includes('-');
|
|
95
|
+
|
|
96
|
+
// Width
|
|
97
|
+
let width = null;
|
|
98
|
+
if (widthStr === '*') { width = Number(args[argIdx++]); }
|
|
99
|
+
else if (widthStr) { width = parseInt(widthStr, 10); }
|
|
100
|
+
|
|
101
|
+
// Precision
|
|
102
|
+
let prec = null;
|
|
103
|
+
if (precStr === '*') { prec = Number(args[argIdx++]); }
|
|
104
|
+
else if (precStr !== undefined && precStr !== null) { prec = parseInt(precStr, 10); }
|
|
105
|
+
|
|
106
|
+
// Positional arg index (%1$d style)
|
|
107
|
+
let val;
|
|
108
|
+
if (argPos) {
|
|
109
|
+
val = args[parseInt(argPos) - 1];
|
|
110
|
+
} else {
|
|
111
|
+
val = args[argIdx++];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let out = '';
|
|
115
|
+
|
|
116
|
+
// Custom spec?
|
|
117
|
+
if (_customSpecs.has(spec)) {
|
|
118
|
+
out = String(_customSpecs.get(spec)(val, flags, width, prec));
|
|
119
|
+
return _applyWidth(out, flags, width, leftAlign);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch (spec) {
|
|
123
|
+
case 'd': case 'i': {
|
|
124
|
+
let n = Math.trunc(Number(val));
|
|
125
|
+
const sign = n < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
|
|
126
|
+
out = Math.abs(n).toString(10);
|
|
127
|
+
if (prec !== null) out = out.padStart(prec, '0');
|
|
128
|
+
out = sign + out;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'u': {
|
|
132
|
+
let n = Math.trunc(Math.abs(Number(val)));
|
|
133
|
+
out = n.toString(10);
|
|
134
|
+
if (prec !== null) out = out.padStart(prec, '0');
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'f': {
|
|
138
|
+
const p = prec !== null ? prec : 6;
|
|
139
|
+
let n = Number(val);
|
|
140
|
+
const sign = n < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
|
|
141
|
+
out = sign + Math.abs(n).toFixed(p);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'e': case 'E': {
|
|
145
|
+
const p = prec !== null ? prec : 6;
|
|
146
|
+
let n = Number(val);
|
|
147
|
+
const sign = n < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
|
|
148
|
+
out = sign + Math.abs(n).toExponential(p);
|
|
149
|
+
if (spec === 'E') out = out.toUpperCase();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'g': case 'G': {
|
|
153
|
+
const p = prec !== null ? (prec || 1) : 6;
|
|
154
|
+
let n = Number(val);
|
|
155
|
+
const sign = n < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
|
|
156
|
+
const abs = Math.abs(n);
|
|
157
|
+
const exp = Math.floor(Math.log10(abs || 1));
|
|
158
|
+
out = sign + (exp < -4 || exp >= p ? abs.toExponential(p - 1) : abs.toPrecision(p));
|
|
159
|
+
// strip trailing zeros unless # flag
|
|
160
|
+
if (!flags.includes('#')) out = out.replace(/(\.\d*?)0+(e|$)/, '$1$2').replace(/\.$/, '');
|
|
161
|
+
if (spec === 'G') out = out.toUpperCase();
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 's': {
|
|
165
|
+
out = String(val == null ? '' : val);
|
|
166
|
+
if (prec !== null) out = out.slice(0, prec);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'S': {
|
|
170
|
+
out = String(val == null ? '' : val).toUpperCase();
|
|
171
|
+
if (prec !== null) out = out.slice(0, prec);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'c': {
|
|
175
|
+
out = typeof val === 'number' ? String.fromCharCode(val) : String(val)[0] || '';
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'x': case 'X': {
|
|
179
|
+
let n = Math.trunc(Math.abs(Number(val)));
|
|
180
|
+
out = n.toString(16);
|
|
181
|
+
if (prec !== null) out = out.padStart(prec, '0');
|
|
182
|
+
if (flags.includes('#')) out = '0x' + out;
|
|
183
|
+
if (spec === 'X') out = out.toUpperCase();
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'o': {
|
|
187
|
+
let n = Math.trunc(Math.abs(Number(val)));
|
|
188
|
+
out = n.toString(8);
|
|
189
|
+
if (prec !== null) out = out.padStart(prec, '0');
|
|
190
|
+
if (flags.includes('#') && out[0] !== '0') out = '0' + out;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'b': {
|
|
194
|
+
let n = Math.trunc(Math.abs(Number(val)));
|
|
195
|
+
out = n.toString(2);
|
|
196
|
+
if (prec !== null) out = out.padStart(prec, '0');
|
|
197
|
+
if (flags.includes('#')) out = '0b' + out;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'p': {
|
|
201
|
+
let n = Math.trunc(Math.abs(Number(val)));
|
|
202
|
+
out = '0x' + n.toString(16).toUpperCase().padStart(8, '0');
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'j': {
|
|
206
|
+
out = JSON.stringify(val);
|
|
207
|
+
if (prec !== null) out = out.slice(0, prec);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
out = match; // unknown spec → pass through
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return _applyWidth(out, flags, width, leftAlign);
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* sprintf alias — same as printf.
|
|
221
|
+
* @param {string} fmt
|
|
222
|
+
* @param {...*} args
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
const sprintf = printf;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* fmt`template ${val}` — tagged template literal using printf under the hood.
|
|
229
|
+
* Each interpolation is treated as %s by default, but you can prefix the
|
|
230
|
+
* placeholder with a format string: fmt`${'%05d' + n}` isn't ideal, so
|
|
231
|
+
* instead use: fmt(['%d', '%s'], n, s) syntax (see fmtArr).
|
|
232
|
+
*/
|
|
233
|
+
function fmt(strings, ...values) {
|
|
234
|
+
let result = '';
|
|
235
|
+
strings.forEach((str, i) => {
|
|
236
|
+
result += str;
|
|
237
|
+
if (i < values.length) result += String(values[i]);
|
|
238
|
+
});
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ────────────────────────────────────────────────────────────
|
|
243
|
+
// SECTION 3: STRING PADDING / ALIGNMENT / TRUNCATION
|
|
244
|
+
// ────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Pad a string on the left to a given width.
|
|
248
|
+
* @param {string} str
|
|
249
|
+
* @param {number} width
|
|
250
|
+
* @param {string} [char=' ']
|
|
251
|
+
* @returns {string}
|
|
252
|
+
*/
|
|
253
|
+
function padLeft(str, width, char = ' ') {
|
|
254
|
+
return String(str).padStart(width, char);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Pad a string on the right to a given width.
|
|
259
|
+
*/
|
|
260
|
+
function padRight(str, width, char = ' ') {
|
|
261
|
+
return String(str).padEnd(width, char);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Center a string within a given width.
|
|
266
|
+
* @param {string} str
|
|
267
|
+
* @param {number} width
|
|
268
|
+
* @param {string} [char=' ']
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
function padCenter(str, width, char = ' ') {
|
|
272
|
+
str = String(str);
|
|
273
|
+
if (str.length >= width) return str;
|
|
274
|
+
const total = width - str.length;
|
|
275
|
+
const left = Math.floor(total / 2);
|
|
276
|
+
const right = total - left;
|
|
277
|
+
return char.repeat(left) + str + char.repeat(right);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Truncate a string to maxLen, appending ellipsis if truncated.
|
|
282
|
+
* @param {string} str
|
|
283
|
+
* @param {number} maxLen
|
|
284
|
+
* @param {string} [ellipsis='…']
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
287
|
+
function truncate(str, maxLen, ellipsis = '…') {
|
|
288
|
+
str = String(str);
|
|
289
|
+
if (str.length <= maxLen) return str;
|
|
290
|
+
return str.slice(0, Math.max(0, maxLen - ellipsis.length)) + ellipsis;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Truncate from the left side.
|
|
295
|
+
*/
|
|
296
|
+
function truncateLeft(str, maxLen, ellipsis = '…') {
|
|
297
|
+
str = String(str);
|
|
298
|
+
if (str.length <= maxLen) return str;
|
|
299
|
+
return ellipsis + str.slice(str.length - maxLen + ellipsis.length);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Truncate in the middle.
|
|
304
|
+
*/
|
|
305
|
+
function truncateMid(str, maxLen, ellipsis = '…') {
|
|
306
|
+
str = String(str);
|
|
307
|
+
if (str.length <= maxLen) return str;
|
|
308
|
+
const half = Math.floor((maxLen - ellipsis.length) / 2);
|
|
309
|
+
return str.slice(0, half) + ellipsis + str.slice(str.length - (maxLen - ellipsis.length - half));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Wrap a string to a given line width, breaking at word boundaries.
|
|
314
|
+
* @param {string} str
|
|
315
|
+
* @param {number} width
|
|
316
|
+
* @param {string} [newline='\n']
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
function wordWrap(str, width, newline = '\n') {
|
|
320
|
+
const words = str.split(/\s+/);
|
|
321
|
+
const lines = [];
|
|
322
|
+
let current = '';
|
|
323
|
+
for (const word of words) {
|
|
324
|
+
if (!current) {
|
|
325
|
+
current = word;
|
|
326
|
+
} else if (current.length + 1 + word.length <= width) {
|
|
327
|
+
current += ' ' + word;
|
|
328
|
+
} else {
|
|
329
|
+
lines.push(current);
|
|
330
|
+
current = word;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (current) lines.push(current);
|
|
334
|
+
return lines.join(newline);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Repeat a string n times.
|
|
339
|
+
*/
|
|
340
|
+
function repeat(str, n) {
|
|
341
|
+
return String(str).repeat(n);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ────────────────────────────────────────────────────────────
|
|
345
|
+
// SECTION 4: NUMBER FORMATTING
|
|
346
|
+
// ────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Format a number with thousands separators.
|
|
350
|
+
* @param {number} n
|
|
351
|
+
* @param {string} [sep=',']
|
|
352
|
+
* @param {string} [dec='.']
|
|
353
|
+
* @returns {string}
|
|
354
|
+
*/
|
|
355
|
+
function thousands(n, sep = ',', dec = '.') {
|
|
356
|
+
const [intPart, fracPart] = String(Number(n)).split('.');
|
|
357
|
+
const formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
|
358
|
+
return fracPart !== undefined ? formatted + dec + fracPart : formatted;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Format a number as currency.
|
|
363
|
+
* @param {number} n
|
|
364
|
+
* @param {string} [symbol='$']
|
|
365
|
+
* @param {number} [decimals=2]
|
|
366
|
+
* @param {string} [thousandsSep=',']
|
|
367
|
+
* @param {string} [decSep='.']
|
|
368
|
+
* @returns {string}
|
|
369
|
+
*/
|
|
370
|
+
function currency(n, symbol = '$', decimals = 2, thousandsSep = ',', decSep = '.') {
|
|
371
|
+
const neg = n < 0;
|
|
372
|
+
const abs = Math.abs(Number(n)).toFixed(decimals);
|
|
373
|
+
const [intPart, fracPart] = abs.split('.');
|
|
374
|
+
const formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
|
|
375
|
+
const result = symbol + formatted + decSep + fracPart;
|
|
376
|
+
return neg ? '-' + result : result;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Format a number in scientific notation.
|
|
381
|
+
* @param {number} n
|
|
382
|
+
* @param {number} [precision=4]
|
|
383
|
+
* @returns {string}
|
|
384
|
+
*/
|
|
385
|
+
function scientific(n, precision = 4) {
|
|
386
|
+
return Number(n).toExponential(precision);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Format bytes into human-readable size.
|
|
391
|
+
* @param {number} bytes
|
|
392
|
+
* @param {number} [decimals=2]
|
|
393
|
+
* @param {boolean} [si=false] - true=1000-based, false=1024-based
|
|
394
|
+
* @returns {string}
|
|
395
|
+
*/
|
|
396
|
+
function fileSize(bytes, decimals = 2, si = false) {
|
|
397
|
+
const base = si ? 1000 : 1024;
|
|
398
|
+
const units = si
|
|
399
|
+
? ['B', 'kB', 'MB', 'GB', 'TB', 'PB']
|
|
400
|
+
: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
|
401
|
+
if (Math.abs(bytes) < base) return bytes + ' B';
|
|
402
|
+
let u = -1;
|
|
403
|
+
let n = bytes;
|
|
404
|
+
do { n /= base; u++; } while (Math.abs(n) >= base && u < units.length - 2);
|
|
405
|
+
return n.toFixed(decimals) + ' ' + units[u + 1];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Format a number as percentage.
|
|
410
|
+
* @param {number} n - value between 0 and 1 (or 0–100 if raw=true)
|
|
411
|
+
* @param {number} [decimals=1]
|
|
412
|
+
* @param {boolean} [raw=false]
|
|
413
|
+
* @returns {string}
|
|
414
|
+
*/
|
|
415
|
+
function percent(n, decimals = 1, raw = false) {
|
|
416
|
+
const val = raw ? Number(n) : Number(n) * 100;
|
|
417
|
+
return val.toFixed(decimals) + '%';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Format a number with a fixed number of decimal places.
|
|
422
|
+
* @param {number} n
|
|
423
|
+
* @param {number} [decimals=2]
|
|
424
|
+
* @returns {string}
|
|
425
|
+
*/
|
|
426
|
+
function fixed(n, decimals = 2) {
|
|
427
|
+
return Number(n).toFixed(decimals);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Format a number in compact notation (1K, 2.5M, etc).
|
|
432
|
+
* @param {number} n
|
|
433
|
+
* @param {number} [decimals=1]
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
function compact(n, decimals = 1) {
|
|
437
|
+
const abs = Math.abs(Number(n));
|
|
438
|
+
const sign = n < 0 ? '-' : '';
|
|
439
|
+
if (abs >= 1e12) return sign + (abs / 1e12).toFixed(decimals) + 'T';
|
|
440
|
+
if (abs >= 1e9) return sign + (abs / 1e9).toFixed(decimals) + 'B';
|
|
441
|
+
if (abs >= 1e6) return sign + (abs / 1e6).toFixed(decimals) + 'M';
|
|
442
|
+
if (abs >= 1e3) return sign + (abs / 1e3).toFixed(decimals) + 'K';
|
|
443
|
+
return sign + abs.toFixed(decimals);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Format as ordinal (1st, 2nd, 3rd, 4th…).
|
|
448
|
+
* @param {number} n
|
|
449
|
+
* @returns {string}
|
|
450
|
+
*/
|
|
451
|
+
function ordinal(n) {
|
|
452
|
+
n = Math.trunc(Number(n));
|
|
453
|
+
const abs = Math.abs(n);
|
|
454
|
+
const mod100 = abs % 100;
|
|
455
|
+
const mod10 = abs % 10;
|
|
456
|
+
if (mod100 >= 11 && mod100 <= 13) return n + 'th';
|
|
457
|
+
if (mod10 === 1) return n + 'st';
|
|
458
|
+
if (mod10 === 2) return n + 'nd';
|
|
459
|
+
if (mod10 === 3) return n + 'rd';
|
|
460
|
+
return n + 'th';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Format as Roman numerals (1–3999).
|
|
465
|
+
* @param {number} n
|
|
466
|
+
* @returns {string}
|
|
467
|
+
*/
|
|
468
|
+
function roman(n) {
|
|
469
|
+
n = Math.trunc(Number(n));
|
|
470
|
+
if (n < 1 || n > 3999) return String(n);
|
|
471
|
+
const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
|
|
472
|
+
const syms = ['M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'];
|
|
473
|
+
let out = '';
|
|
474
|
+
for (let i = 0; i < vals.length; i++) {
|
|
475
|
+
while (n >= vals[i]) { out += syms[i]; n -= vals[i]; }
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Format a number in a given radix with optional prefix.
|
|
482
|
+
* @param {number} n
|
|
483
|
+
* @param {number} radix
|
|
484
|
+
* @param {string} [prefix='']
|
|
485
|
+
* @param {number} [minWidth=0]
|
|
486
|
+
* @returns {string}
|
|
487
|
+
*/
|
|
488
|
+
function radixFmt(n, radix, prefix = '', minWidth = 0) {
|
|
489
|
+
const out = Math.trunc(Math.abs(Number(n))).toString(radix).toUpperCase();
|
|
490
|
+
return prefix + out.padStart(minWidth, '0');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ────────────────────────────────────────────────────────────
|
|
494
|
+
// SECTION 5: DATE / TIME FORMATTING
|
|
495
|
+
// ────────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Format a Date using a strftime-like format string.
|
|
499
|
+
*
|
|
500
|
+
* Tokens:
|
|
501
|
+
* %Y 4-digit year %y 2-digit year
|
|
502
|
+
* %m month 01-12 %n month 1-12 (no pad)
|
|
503
|
+
* %B full month name %b short month name
|
|
504
|
+
* %d day 01-31 %e day 1-31 (no pad)
|
|
505
|
+
* %A full weekday name %a short weekday name
|
|
506
|
+
* %H hour 00-23 %I hour 01-12
|
|
507
|
+
* %M minute 00-59 %S second 00-59
|
|
508
|
+
* %L milliseconds 000-999
|
|
509
|
+
* %p AM/PM %P am/pm
|
|
510
|
+
* %Z timezone offset (+HH:MM)
|
|
511
|
+
* %s Unix timestamp (seconds)
|
|
512
|
+
* %j day of year 001-366
|
|
513
|
+
* %W week of year 00-53
|
|
514
|
+
* %q quarter (1-4)
|
|
515
|
+
* %% literal %
|
|
516
|
+
*
|
|
517
|
+
* @param {Date|number|string} date
|
|
518
|
+
* @param {string} fmt
|
|
519
|
+
* @param {object} [options]
|
|
520
|
+
* @param {string} [options.locale='en'] - locale for month/day names
|
|
521
|
+
* @returns {string}
|
|
522
|
+
*/
|
|
523
|
+
function dateFormat(date, fmt, options = {}) {
|
|
524
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
525
|
+
const locale = options.locale || 'en';
|
|
526
|
+
|
|
527
|
+
const MONTHS_LONG = _monthNames(locale, 'long');
|
|
528
|
+
const MONTHS_SHORT = _monthNames(locale, 'short');
|
|
529
|
+
const DAYS_LONG = _dayNames(locale, 'long');
|
|
530
|
+
const DAYS_SHORT = _dayNames(locale, 'short');
|
|
531
|
+
|
|
532
|
+
const Y = d.getFullYear();
|
|
533
|
+
const M = d.getMonth(); // 0-based
|
|
534
|
+
const D = d.getDate();
|
|
535
|
+
const wd = d.getDay(); // 0=Sun
|
|
536
|
+
const H = d.getHours();
|
|
537
|
+
const min= d.getMinutes();
|
|
538
|
+
const sec= d.getSeconds();
|
|
539
|
+
const ms = d.getMilliseconds();
|
|
540
|
+
|
|
541
|
+
const h12 = H % 12 || 12;
|
|
542
|
+
const dayOfYear = Math.floor((d - new Date(Y, 0, 0)) / 86400000);
|
|
543
|
+
const weekOfYear= Math.floor(dayOfYear / 7);
|
|
544
|
+
const quarter = Math.floor(M / 3) + 1;
|
|
545
|
+
|
|
546
|
+
const offset = -d.getTimezoneOffset();
|
|
547
|
+
const offSign = offset >= 0 ? '+' : '-';
|
|
548
|
+
const offH = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
549
|
+
const offM = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
550
|
+
|
|
551
|
+
return fmt.replace(/%%|%[YymnBbdteAaHIMSLpPZsjWq]/g, (tok) => {
|
|
552
|
+
switch (tok) {
|
|
553
|
+
case '%%': return '%';
|
|
554
|
+
case '%Y': return String(Y);
|
|
555
|
+
case '%y': return String(Y).slice(-2);
|
|
556
|
+
case '%m': return String(M + 1).padStart(2, '0');
|
|
557
|
+
case '%n': return String(M + 1);
|
|
558
|
+
case '%B': return MONTHS_LONG[M];
|
|
559
|
+
case '%b': return MONTHS_SHORT[M];
|
|
560
|
+
case '%d': return String(D).padStart(2, '0');
|
|
561
|
+
case '%e': return String(D);
|
|
562
|
+
case '%A': return DAYS_LONG[wd];
|
|
563
|
+
case '%a': return DAYS_SHORT[wd];
|
|
564
|
+
case '%H': return String(H).padStart(2, '0');
|
|
565
|
+
case '%I': return String(h12).padStart(2, '0');
|
|
566
|
+
case '%M': return String(min).padStart(2, '0');
|
|
567
|
+
case '%S': return String(sec).padStart(2, '0');
|
|
568
|
+
case '%L': return String(ms).padStart(3, '0');
|
|
569
|
+
case '%p': return H < 12 ? 'AM' : 'PM';
|
|
570
|
+
case '%P': return H < 12 ? 'am' : 'pm';
|
|
571
|
+
case '%Z': return offSign + offH + ':' + offM;
|
|
572
|
+
case '%s': return String(Math.floor(d.getTime() / 1000));
|
|
573
|
+
case '%j': return String(dayOfYear).padStart(3, '0');
|
|
574
|
+
case '%W': return String(weekOfYear).padStart(2, '0');
|
|
575
|
+
case '%q': return String(quarter);
|
|
576
|
+
default: return tok;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Format a duration (milliseconds) into human-readable text.
|
|
583
|
+
* @param {number} ms - duration in milliseconds
|
|
584
|
+
* @param {object} [options]
|
|
585
|
+
* @param {'short'|'long'} [options.style='short']
|
|
586
|
+
* @param {boolean} [options.leading=false] - include zero units
|
|
587
|
+
* @returns {string}
|
|
588
|
+
*/
|
|
589
|
+
function duration(ms, options = {}) {
|
|
590
|
+
const style = options.style || 'short';
|
|
591
|
+
const leading = options.leading || false;
|
|
592
|
+
const neg = ms < 0;
|
|
593
|
+
let t = Math.abs(ms);
|
|
594
|
+
|
|
595
|
+
const parts = [
|
|
596
|
+
{ val: Math.floor(t / 86400000), short: 'd', long: ' day' },
|
|
597
|
+
{ val: Math.floor((t % 86400000) / 3600000), short: 'h', long: ' hour' },
|
|
598
|
+
{ val: Math.floor((t % 3600000) / 60000), short: 'm', long: ' minute' },
|
|
599
|
+
{ val: Math.floor((t % 60000) / 1000), short: 's', long: ' second' },
|
|
600
|
+
{ val: t % 1000, short: 'ms', long: ' millisecond' },
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
let out = parts
|
|
604
|
+
.filter(p => leading ? true : p.val > 0)
|
|
605
|
+
.map(p => {
|
|
606
|
+
const unit = style === 'long'
|
|
607
|
+
? p.long + (p.val !== 1 ? 's' : '')
|
|
608
|
+
: p.short;
|
|
609
|
+
return p.val + unit;
|
|
610
|
+
})
|
|
611
|
+
.join(style === 'long' ? ', ' : ' ');
|
|
612
|
+
|
|
613
|
+
if (!out) out = style === 'long' ? '0 milliseconds' : '0ms';
|
|
614
|
+
return neg ? '-' + out : out;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Relative time ("3 hours ago", "in 2 days").
|
|
619
|
+
* @param {Date|number|string} date
|
|
620
|
+
* @param {Date|number|string} [base=Date.now()]
|
|
621
|
+
* @returns {string}
|
|
622
|
+
*/
|
|
623
|
+
function relativeTime(date, base = Date.now()) {
|
|
624
|
+
const d = date instanceof Date ? date.getTime() : new Date(date).getTime();
|
|
625
|
+
const b = base instanceof Date ? base.getTime() : new Date(base).getTime();
|
|
626
|
+
const diff = d - b;
|
|
627
|
+
const abs = Math.abs(diff);
|
|
628
|
+
const past = diff < 0;
|
|
629
|
+
|
|
630
|
+
const units = [
|
|
631
|
+
{ limit: 60000, div: 1000, unit: 'second' },
|
|
632
|
+
{ limit: 3600000, div: 60000, unit: 'minute' },
|
|
633
|
+
{ limit: 86400000, div: 3600000, unit: 'hour' },
|
|
634
|
+
{ limit: 2592000000, div: 86400000,unit: 'day' },
|
|
635
|
+
{ limit: 31536000000, div: 2592000000, unit: 'month' },
|
|
636
|
+
{ limit: Infinity, div: 31536000000, unit: 'year' },
|
|
637
|
+
];
|
|
638
|
+
|
|
639
|
+
if (abs < 1000) return 'just now';
|
|
640
|
+
|
|
641
|
+
for (const { limit, div, unit } of units) {
|
|
642
|
+
if (abs < limit) {
|
|
643
|
+
const n = Math.round(abs / div);
|
|
644
|
+
const s = n !== 1 ? 's' : '';
|
|
645
|
+
return past ? `${n} ${unit}${s} ago` : `in ${n} ${unit}${s}`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function _monthNames(locale, type) {
|
|
651
|
+
try {
|
|
652
|
+
return Array.from({ length: 12 }, (_, i) =>
|
|
653
|
+
new Date(2000, i, 1).toLocaleString(locale, { month: type })
|
|
654
|
+
);
|
|
655
|
+
} catch { return ['January','February','March','April','May','June','July','August','September','October','November','December']; }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function _dayNames(locale, type) {
|
|
659
|
+
try {
|
|
660
|
+
return Array.from({ length: 7 }, (_, i) =>
|
|
661
|
+
new Date(2000, 0, i + 2).toLocaleString(locale, { weekday: type })
|
|
662
|
+
);
|
|
663
|
+
} catch { return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ────────────────────────────────────────────────────────────
|
|
667
|
+
// SECTION 6: ANSI / TERMINAL COLOR CODES
|
|
668
|
+
// ────────────────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
const ANSI = {
|
|
671
|
+
reset: '\x1b[0m',
|
|
672
|
+
bold: '\x1b[1m',
|
|
673
|
+
dim: '\x1b[2m',
|
|
674
|
+
italic: '\x1b[3m',
|
|
675
|
+
underline: '\x1b[4m',
|
|
676
|
+
blink: '\x1b[5m',
|
|
677
|
+
inverse: '\x1b[7m',
|
|
678
|
+
hidden: '\x1b[8m',
|
|
679
|
+
strike: '\x1b[9m',
|
|
680
|
+
|
|
681
|
+
black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m',
|
|
682
|
+
yellow: '\x1b[33m', blue: '\x1b[34m', magenta:'\x1b[35m',
|
|
683
|
+
cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
|
|
684
|
+
|
|
685
|
+
bgBlack: '\x1b[40m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m',
|
|
686
|
+
bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', bgMagenta:'\x1b[45m',
|
|
687
|
+
bgCyan: '\x1b[46m', bgWhite: '\x1b[47m', bgGray: '\x1b[100m',
|
|
688
|
+
|
|
689
|
+
brightBlack: '\x1b[90m', brightRed: '\x1b[91m', brightGreen: '\x1b[92m',
|
|
690
|
+
brightYellow: '\x1b[93m', brightBlue: '\x1b[94m', brightMagenta:'\x1b[95m',
|
|
691
|
+
brightCyan: '\x1b[96m', brightWhite: '\x1b[97m',
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Wrap a string with ANSI color/style codes.
|
|
696
|
+
* @param {string} str
|
|
697
|
+
* @param {...string} codes - keys from ANSI or raw escape codes
|
|
698
|
+
* @returns {string}
|
|
699
|
+
*/
|
|
700
|
+
function ansi(str, ...codes) {
|
|
701
|
+
const open = codes.map(c => ANSI[c] || c).join('');
|
|
702
|
+
return open + str + ANSI.reset;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Strip all ANSI escape codes from a string.
|
|
707
|
+
* @param {string} str
|
|
708
|
+
* @returns {string}
|
|
709
|
+
*/
|
|
710
|
+
function stripAnsi(str) {
|
|
711
|
+
return str.replace(/\x1b\[[0-9;]*[mGKHF]/g, '');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* 256-color foreground.
|
|
716
|
+
* @param {string} str
|
|
717
|
+
* @param {number} code - 0-255
|
|
718
|
+
* @returns {string}
|
|
719
|
+
*/
|
|
720
|
+
function color256(str, code) {
|
|
721
|
+
return `\x1b[38;5;${code}m${str}${ANSI.reset}`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* RGB foreground color (true color terminals).
|
|
726
|
+
* @param {string} str
|
|
727
|
+
* @param {number} r
|
|
728
|
+
* @param {number} g
|
|
729
|
+
* @param {number} b
|
|
730
|
+
* @returns {string}
|
|
731
|
+
*/
|
|
732
|
+
function colorRGB(str, r, g, b) {
|
|
733
|
+
return `\x1b[38;2;${r};${g};${b}m${str}${ANSI.reset}`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* RGB background color.
|
|
738
|
+
*/
|
|
739
|
+
function bgColorRGB(str, r, g, b) {
|
|
740
|
+
return `\x1b[48;2;${r};${g};${b}m${str}${ANSI.reset}`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Hex color string (#RRGGBB or #RGB) to RGB foreground.
|
|
745
|
+
*/
|
|
746
|
+
function colorHex(str, hex) {
|
|
747
|
+
hex = hex.replace('#', '');
|
|
748
|
+
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
|
749
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
750
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
751
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
752
|
+
return colorRGB(str, r, g, b);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ────────────────────────────────────────────────────────────
|
|
756
|
+
// SECTION 7: TABLE / ASCII GRID FORMATTING
|
|
757
|
+
// ────────────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Format data as an ASCII table.
|
|
761
|
+
*
|
|
762
|
+
* @param {Array<object|Array>} rows - array of objects or arrays
|
|
763
|
+
* @param {object} [options]
|
|
764
|
+
* @param {string[]} [options.headers] - column headers
|
|
765
|
+
* @param {'left'|'right'|'center'|Array} [options.align='left']
|
|
766
|
+
* @param {'simple'|'box'|'rounded'|'double'|'minimal'|'markdown'} [options.style='box']
|
|
767
|
+
* @param {number} [options.padding=1]
|
|
768
|
+
* @param {boolean} [options.borders=true]
|
|
769
|
+
* @param {number[]} [options.maxWidths] - max column widths (truncates)
|
|
770
|
+
* @returns {string}
|
|
771
|
+
*/
|
|
772
|
+
function table(rows, options = {}) {
|
|
773
|
+
const style = options.style || 'box';
|
|
774
|
+
const padding = options.padding !== undefined ? options.padding : 1;
|
|
775
|
+
const align = options.align || 'left';
|
|
776
|
+
const maxWids = options.maxWidths || [];
|
|
777
|
+
|
|
778
|
+
// Normalize rows to array of arrays
|
|
779
|
+
let headers = options.headers || null;
|
|
780
|
+
let data = rows.map(r => Array.isArray(r) ? r : Object.values(r));
|
|
781
|
+
if (!headers && rows.length && !Array.isArray(rows[0])) {
|
|
782
|
+
headers = Object.keys(rows[0]);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const allRows = headers ? [headers, ...data] : [...data];
|
|
786
|
+
const cols = Math.max(...allRows.map(r => r.length));
|
|
787
|
+
|
|
788
|
+
// Pad rows to equal columns
|
|
789
|
+
const normalized = allRows.map(r => {
|
|
790
|
+
const row = Array.from({ length: cols }, (_, i) => String(r[i] != null ? r[i] : ''));
|
|
791
|
+
return row;
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Compute column widths
|
|
795
|
+
let colWidths = Array.from({ length: cols }, (_, ci) =>
|
|
796
|
+
Math.max(...normalized.map(r => r[ci].length))
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
// Apply maxWidths truncation
|
|
800
|
+
colWidths = colWidths.map((w, i) =>
|
|
801
|
+
maxWids[i] ? Math.min(w, maxWids[i]) : w
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// Truncate cells to maxWidths
|
|
805
|
+
const cells = normalized.map(row =>
|
|
806
|
+
row.map((cell, ci) =>
|
|
807
|
+
maxWids[ci] && cell.length > maxWids[ci] ? truncate(cell, maxWids[ci]) : cell
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Alignment helper
|
|
812
|
+
function alignCell(str, width, colIdx) {
|
|
813
|
+
const a = Array.isArray(align) ? (align[colIdx] || 'left') : align;
|
|
814
|
+
if (a === 'right') return padLeft(str, width);
|
|
815
|
+
if (a === 'center') return padCenter(str, width);
|
|
816
|
+
return padRight(str, width);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const pad = ' '.repeat(padding);
|
|
820
|
+
|
|
821
|
+
// Border styles
|
|
822
|
+
const styles = {
|
|
823
|
+
box: { tl:'┌', tr:'┐', bl:'└', br:'┘', h:'─', v:'│', ml:'├', mr:'┤', mt:'┬', mb:'┴', mx:'┼', sep:true },
|
|
824
|
+
rounded: { tl:'╭', tr:'╮', bl:'╰', br:'╯', h:'─', v:'│', ml:'╞', mr:'╡', mt:'┬', mb:'┴', mx:'┼', sep:true },
|
|
825
|
+
double: { tl:'╔', tr:'╗', bl:'╚', br:'╝', h:'═', v:'║', ml:'╠', mr:'╣', mt:'╦', mb:'╩', mx:'╬', sep:true },
|
|
826
|
+
simple: { tl:'', tr:'', bl:'', br:'', h:'-', v:'|', ml:'', mr:'', mt:'', mb:'', mx:'+', sep:true },
|
|
827
|
+
minimal: { tl:'', tr:'', bl:'', br:'', h:' ', v:' ', ml:'', mr:'', mt:'', mb:'', mx:' ', sep:false },
|
|
828
|
+
markdown: { tl:'', tr:'', bl:'', br:'', h:'-', v:'|', ml:'', mr:'', mt:'', mb:'', mx:'|', sep:true },
|
|
829
|
+
};
|
|
830
|
+
const B = styles[style] || styles.box;
|
|
831
|
+
const isMarkdown = style === 'markdown';
|
|
832
|
+
|
|
833
|
+
function makeLine(left, mid, right, fill) {
|
|
834
|
+
const inner = colWidths.map(w => fill.repeat(w + padding * 2)).join(mid);
|
|
835
|
+
return left + inner + right;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const lines = [];
|
|
839
|
+
const topLine = makeLine(B.tl, B.mt, B.tr, B.h);
|
|
840
|
+
const sepLine = makeLine(B.ml, B.mx, B.mr, B.h);
|
|
841
|
+
const bottomLine = makeLine(B.bl, B.mb, B.br, B.h);
|
|
842
|
+
|
|
843
|
+
if (!isMarkdown && topLine.trim()) lines.push(topLine);
|
|
844
|
+
|
|
845
|
+
cells.forEach((row, ri) => {
|
|
846
|
+
const rowStr = B.v + row.map((cell, ci) =>
|
|
847
|
+
pad + alignCell(cell, colWidths[ci]) + pad
|
|
848
|
+
).join(B.v) + B.v;
|
|
849
|
+
lines.push(rowStr);
|
|
850
|
+
|
|
851
|
+
if (ri === 0 && headers) {
|
|
852
|
+
// separator after header
|
|
853
|
+
if (isMarkdown) {
|
|
854
|
+
const mdSep = '|' + colWidths.map(w => '-'.repeat(w + padding * 2)).join('|') + '|';
|
|
855
|
+
lines.push(mdSep);
|
|
856
|
+
} else if (B.sep && sepLine.trim()) {
|
|
857
|
+
lines.push(sepLine);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
if (!isMarkdown && bottomLine.trim()) lines.push(bottomLine);
|
|
863
|
+
|
|
864
|
+
return lines.join('\n');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Format a key-value list as an aligned two-column display.
|
|
869
|
+
* @param {object|Array<[string,any]>} obj
|
|
870
|
+
* @param {object} [options]
|
|
871
|
+
* @param {string} [options.separator=' ']
|
|
872
|
+
* @param {'left'|'right'} [options.keyAlign='right']
|
|
873
|
+
* @returns {string}
|
|
874
|
+
*/
|
|
875
|
+
function kvList(obj, options = {}) {
|
|
876
|
+
const sep = options.separator || ' ';
|
|
877
|
+
const keyAlign = options.keyAlign || 'right';
|
|
878
|
+
const pairs = Array.isArray(obj) ? obj : Object.entries(obj);
|
|
879
|
+
const maxKey = Math.max(...pairs.map(([k]) => String(k).length));
|
|
880
|
+
return pairs.map(([k, v]) => {
|
|
881
|
+
const key = keyAlign === 'right'
|
|
882
|
+
? padLeft(String(k), maxKey)
|
|
883
|
+
: padRight(String(k), maxKey);
|
|
884
|
+
return key + sep + String(v);
|
|
885
|
+
}).join('\n');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Render a simple horizontal progress bar.
|
|
890
|
+
* @param {number} value - current value
|
|
891
|
+
* @param {number} max - max value
|
|
892
|
+
* @param {number} [width=40]
|
|
893
|
+
* @param {object} [options]
|
|
894
|
+
* @param {string} [options.filled='█']
|
|
895
|
+
* @param {string} [options.empty='░']
|
|
896
|
+
* @param {boolean} [options.showPercent=true]
|
|
897
|
+
* @returns {string}
|
|
898
|
+
*/
|
|
899
|
+
function progressBar(value, max, width = 40, options = {}) {
|
|
900
|
+
const filled = options.filled || '█';
|
|
901
|
+
const empty = options.empty || '░';
|
|
902
|
+
const showPct = options.showPercent !== false;
|
|
903
|
+
const ratio = Math.max(0, Math.min(1, value / max));
|
|
904
|
+
const n = Math.round(ratio * width);
|
|
905
|
+
const bar = filled.repeat(n) + empty.repeat(width - n);
|
|
906
|
+
return showPct ? `[${bar}] ${percent(ratio)}` : `[${bar}]`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Render a tree structure from a nested object/array.
|
|
911
|
+
* @param {object|Array} node
|
|
912
|
+
* @param {string} [label='']
|
|
913
|
+
* @param {string} [prefix='']
|
|
914
|
+
* @returns {string}
|
|
915
|
+
*/
|
|
916
|
+
function treeView(node, label = '', prefix = '') {
|
|
917
|
+
const lines = [];
|
|
918
|
+
if (label) lines.push(prefix + label);
|
|
919
|
+
const entries = Array.isArray(node)
|
|
920
|
+
? node.map((v, i) => [String(i), v])
|
|
921
|
+
: Object.entries(node || {});
|
|
922
|
+
entries.forEach(([key, val], i) => {
|
|
923
|
+
const isLast = i === entries.length - 1;
|
|
924
|
+
const branch = isLast ? '└── ' : '├── ';
|
|
925
|
+
const childPfx = prefix + (isLast ? ' ' : '│ ');
|
|
926
|
+
if (val && typeof val === 'object') {
|
|
927
|
+
lines.push(prefix + branch + key + '/');
|
|
928
|
+
lines.push(treeView(val, '', childPfx));
|
|
929
|
+
} else {
|
|
930
|
+
lines.push(prefix + branch + key + ': ' + String(val));
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
return lines.filter(Boolean).join('\n');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ────────────────────────────────────────────────────────────
|
|
937
|
+
// SECTION 8: JSON / OBJECT PRETTY-PRINT
|
|
938
|
+
// ────────────────────────────────────────────────────────────
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Pretty-print a value as JSON.
|
|
942
|
+
* @param {*} val
|
|
943
|
+
* @param {object} [options]
|
|
944
|
+
* @param {number} [options.indent=2]
|
|
945
|
+
* @param {boolean} [options.color=false] - ANSI-color key types
|
|
946
|
+
* @param {number} [options.maxDepth] - truncate deeper objects
|
|
947
|
+
* @returns {string}
|
|
948
|
+
*/
|
|
949
|
+
function prettyJSON(val, options = {}) {
|
|
950
|
+
const indent = options.indent !== undefined ? options.indent : 2;
|
|
951
|
+
const colorize = options.color || false;
|
|
952
|
+
const maxDepth = options.maxDepth !== undefined ? options.maxDepth : Infinity;
|
|
953
|
+
|
|
954
|
+
function _serialize(v, depth) {
|
|
955
|
+
if (depth > maxDepth) return colorize ? ansi('"..."', 'gray') : '"..."';
|
|
956
|
+
if (v === null) return colorize ? ansi('null', 'magenta') : 'null';
|
|
957
|
+
if (v === undefined) return colorize ? ansi('undefined', 'gray') : 'undefined';
|
|
958
|
+
if (typeof v === 'boolean') return colorize ? ansi(String(v), 'yellow') : String(v);
|
|
959
|
+
if (typeof v === 'number') return colorize ? ansi(String(v), 'cyan') : String(v);
|
|
960
|
+
if (typeof v === 'string') {
|
|
961
|
+
const s = JSON.stringify(v);
|
|
962
|
+
return colorize ? ansi(s, 'green') : s;
|
|
963
|
+
}
|
|
964
|
+
if (Array.isArray(v)) {
|
|
965
|
+
if (v.length === 0) return '[]';
|
|
966
|
+
const pad = ' '.repeat((depth + 1) * indent);
|
|
967
|
+
const close = ' '.repeat(depth * indent);
|
|
968
|
+
const items = v.map(item => pad + _serialize(item, depth + 1));
|
|
969
|
+
return '[\n' + items.join(',\n') + '\n' + close + ']';
|
|
970
|
+
}
|
|
971
|
+
if (typeof v === 'object') {
|
|
972
|
+
const keys = Object.keys(v);
|
|
973
|
+
if (keys.length === 0) return '{}';
|
|
974
|
+
const pad = ' '.repeat((depth + 1) * indent);
|
|
975
|
+
const close = ' '.repeat(depth * indent);
|
|
976
|
+
const items = keys.map(k => {
|
|
977
|
+
const key = colorize ? ansi(JSON.stringify(k), 'blue') : JSON.stringify(k);
|
|
978
|
+
return pad + key + ': ' + _serialize(v[k], depth + 1);
|
|
979
|
+
});
|
|
980
|
+
return '{\n' + items.join(',\n') + '\n' + close + '}';
|
|
981
|
+
}
|
|
982
|
+
return String(v);
|
|
983
|
+
}
|
|
984
|
+
return _serialize(val, 0);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Diff two objects and return a formatted string of changes.
|
|
989
|
+
* @param {object} before
|
|
990
|
+
* @param {object} after
|
|
991
|
+
* @param {object} [options]
|
|
992
|
+
* @param {boolean} [options.color=false]
|
|
993
|
+
* @returns {string}
|
|
994
|
+
*/
|
|
995
|
+
function diffObjects(before, after, options = {}) {
|
|
996
|
+
const colorize = options.color || false;
|
|
997
|
+
const lines = [];
|
|
998
|
+
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
999
|
+
|
|
1000
|
+
for (const key of allKeys) {
|
|
1001
|
+
const bVal = JSON.stringify(before[key]);
|
|
1002
|
+
const aVal = JSON.stringify(after[key]);
|
|
1003
|
+
if (!(key in before)) {
|
|
1004
|
+
const line = `+ ${key}: ${aVal}`;
|
|
1005
|
+
lines.push(colorize ? ansi(line, 'green') : line);
|
|
1006
|
+
} else if (!(key in after)) {
|
|
1007
|
+
const line = `- ${key}: ${bVal}`;
|
|
1008
|
+
lines.push(colorize ? ansi(line, 'red') : line);
|
|
1009
|
+
} else if (bVal !== aVal) {
|
|
1010
|
+
const rm = `- ${key}: ${bVal}`;
|
|
1011
|
+
const add = `+ ${key}: ${aVal}`;
|
|
1012
|
+
lines.push(colorize ? ansi(rm, 'red') : rm);
|
|
1013
|
+
lines.push(colorize ? ansi(add, 'green') : add);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return lines.join('\n');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ────────────────────────────────────────────────────────────
|
|
1020
|
+
// SECTION 9: LOCALE / I18N SUPPORT
|
|
1021
|
+
// ────────────────────────────────────────────────────────────
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Format a number using the Intl API for locale-aware output.
|
|
1025
|
+
* @param {number} n
|
|
1026
|
+
* @param {string} [locale='en']
|
|
1027
|
+
* @param {object} [intlOptions] - passed to Intl.NumberFormat
|
|
1028
|
+
* @returns {string}
|
|
1029
|
+
*/
|
|
1030
|
+
function localeNumber(n, locale = 'en', intlOptions = {}) {
|
|
1031
|
+
return new Intl.NumberFormat(locale, intlOptions).format(n);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Format a date using the Intl API.
|
|
1036
|
+
* @param {Date|number|string} date
|
|
1037
|
+
* @param {string} [locale='en']
|
|
1038
|
+
* @param {object} [intlOptions] - passed to Intl.DateTimeFormat
|
|
1039
|
+
* @returns {string}
|
|
1040
|
+
*/
|
|
1041
|
+
function localeDate(date, locale = 'en', intlOptions = {}) {
|
|
1042
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
1043
|
+
return new Intl.DateTimeFormat(locale, intlOptions).format(d);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Format a number as currency using Intl.
|
|
1048
|
+
* @param {number} n
|
|
1049
|
+
* @param {string} [currencyCode='USD']
|
|
1050
|
+
* @param {string} [locale='en']
|
|
1051
|
+
* @returns {string}
|
|
1052
|
+
*/
|
|
1053
|
+
function localeCurrency(n, currencyCode = 'USD', locale = 'en') {
|
|
1054
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode }).format(n);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Get a list separator for a locale (e.g. English = ', ' / Arabic = '، ').
|
|
1059
|
+
* @param {string} [locale='en']
|
|
1060
|
+
* @returns {string}
|
|
1061
|
+
*/
|
|
1062
|
+
function localeSeparator(locale = 'en') {
|
|
1063
|
+
// Use Intl.ListFormat if available
|
|
1064
|
+
if (typeof Intl !== 'undefined' && Intl.ListFormat) {
|
|
1065
|
+
const lf = new Intl.ListFormat(locale, { type: 'conjunction' });
|
|
1066
|
+
// Extract the separator from a test format
|
|
1067
|
+
const parts = lf.formatToParts(['A', 'B', 'C']);
|
|
1068
|
+
const sep = parts.find(p => p.type === 'literal');
|
|
1069
|
+
return sep ? sep.value : ', ';
|
|
1070
|
+
}
|
|
1071
|
+
return ', ';
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Format a list of items into a natural language list.
|
|
1076
|
+
* @param {string[]} items
|
|
1077
|
+
* @param {'conjunction'|'disjunction'|'unit'} [type='conjunction']
|
|
1078
|
+
* @param {string} [locale='en']
|
|
1079
|
+
* @returns {string}
|
|
1080
|
+
*/
|
|
1081
|
+
function listFormat(items, type = 'conjunction', locale = 'en') {
|
|
1082
|
+
if (typeof Intl !== 'undefined' && Intl.ListFormat) {
|
|
1083
|
+
return new Intl.ListFormat(locale, { style: 'long', type }).format(items);
|
|
1084
|
+
}
|
|
1085
|
+
if (items.length === 0) return '';
|
|
1086
|
+
if (items.length === 1) return items[0];
|
|
1087
|
+
const conj = type === 'disjunction' ? 'or' : 'and';
|
|
1088
|
+
return items.slice(0, -1).join(', ') + ', ' + conj + ' ' + items[items.length - 1];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ────────────────────────────────────────────────────────────
|
|
1092
|
+
// SECTION 10: MISC UTILITIES
|
|
1093
|
+
// ────────────────────────────────────────────────────────────
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Convert camelCase / PascalCase to different cases.
|
|
1097
|
+
* @param {string} str
|
|
1098
|
+
* @param {'snake'|'kebab'|'title'|'upper'|'camel'|'pascal'|'dot'} to
|
|
1099
|
+
* @returns {string}
|
|
1100
|
+
*/
|
|
1101
|
+
function changeCase(str, to) {
|
|
1102
|
+
// Split into words
|
|
1103
|
+
const words = str
|
|
1104
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
1105
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
1106
|
+
.replace(/[-_.\s]+/g, ' ')
|
|
1107
|
+
.trim()
|
|
1108
|
+
.split(/\s+/)
|
|
1109
|
+
.map(w => w.toLowerCase());
|
|
1110
|
+
|
|
1111
|
+
switch (to) {
|
|
1112
|
+
case 'snake': return words.join('_');
|
|
1113
|
+
case 'kebab': return words.join('-');
|
|
1114
|
+
case 'dot': return words.join('.');
|
|
1115
|
+
case 'upper': return words.join('_').toUpperCase();
|
|
1116
|
+
case 'title': return words.map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
1117
|
+
case 'camel': return words[0] + words.slice(1).map(w => w[0].toUpperCase() + w.slice(1)).join('');
|
|
1118
|
+
case 'pascal': return words.map(w => w[0].toUpperCase() + w.slice(1)).join('');
|
|
1119
|
+
default: return str;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Pluralize a word based on count.
|
|
1125
|
+
* @param {number} count
|
|
1126
|
+
* @param {string} singular
|
|
1127
|
+
* @param {string} [plural] - defaults to singular + 's'
|
|
1128
|
+
* @param {boolean} [includeCount=true]
|
|
1129
|
+
* @returns {string}
|
|
1130
|
+
*/
|
|
1131
|
+
function pluralize(count, singular, plural, includeCount = true) {
|
|
1132
|
+
plural = plural || singular + 's';
|
|
1133
|
+
const word = count === 1 ? singular : plural;
|
|
1134
|
+
return includeCount ? `${count} ${word}` : word;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Slugify a string (URL-safe lowercase with hyphens).
|
|
1139
|
+
* @param {string} str
|
|
1140
|
+
* @returns {string}
|
|
1141
|
+
*/
|
|
1142
|
+
function slugify(str) {
|
|
1143
|
+
return str
|
|
1144
|
+
.toString()
|
|
1145
|
+
.normalize('NFD')
|
|
1146
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1147
|
+
.toLowerCase()
|
|
1148
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1149
|
+
.replace(/^-+|-+$/g, '');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Mask a string, showing only last N characters.
|
|
1154
|
+
* @param {string} str
|
|
1155
|
+
* @param {number} [show=4]
|
|
1156
|
+
* @param {string} [maskChar='*']
|
|
1157
|
+
* @returns {string}
|
|
1158
|
+
*/
|
|
1159
|
+
function mask(str, show = 4, maskChar = '*') {
|
|
1160
|
+
str = String(str);
|
|
1161
|
+
if (str.length <= show) return maskChar.repeat(str.length);
|
|
1162
|
+
return maskChar.repeat(str.length - show) + str.slice(-show);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Convert a number to its spelled-out English word.
|
|
1167
|
+
* Supports integers up to 999 quadrillion.
|
|
1168
|
+
* @param {number} n
|
|
1169
|
+
* @returns {string}
|
|
1170
|
+
*/
|
|
1171
|
+
function numberWords(n) {
|
|
1172
|
+
n = Math.trunc(Number(n));
|
|
1173
|
+
if (n === 0) return 'zero';
|
|
1174
|
+
const neg = n < 0;
|
|
1175
|
+
n = Math.abs(n);
|
|
1176
|
+
|
|
1177
|
+
const ones = ['','one','two','three','four','five','six','seven','eight','nine',
|
|
1178
|
+
'ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen',
|
|
1179
|
+
'seventeen','eighteen','nineteen'];
|
|
1180
|
+
const tens = ['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety'];
|
|
1181
|
+
|
|
1182
|
+
function _below1000(n) {
|
|
1183
|
+
if (n < 20) return ones[n];
|
|
1184
|
+
if (n < 100) return tens[Math.floor(n/10)] + (n%10 ? '-' + ones[n%10] : '');
|
|
1185
|
+
return ones[Math.floor(n/100)] + ' hundred' + (n%100 ? ' ' + _below1000(n%100) : '');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const chunks = [];
|
|
1189
|
+
const scales = ['', ' thousand', ' million', ' billion', ' trillion', ' quadrillion'];
|
|
1190
|
+
let idx = 0;
|
|
1191
|
+
while (n > 0) {
|
|
1192
|
+
const chunk = n % 1000;
|
|
1193
|
+
if (chunk) chunks.unshift(_below1000(chunk) + scales[idx]);
|
|
1194
|
+
n = Math.floor(n / 1000);
|
|
1195
|
+
idx++;
|
|
1196
|
+
}
|
|
1197
|
+
const result = chunks.join(', ');
|
|
1198
|
+
return (neg ? 'negative ' : '') + result;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Format a hex color string.
|
|
1203
|
+
* @param {number|string} r - red 0-255 or full '#RRGGBB'
|
|
1204
|
+
* @param {number} [g]
|
|
1205
|
+
* @param {number} [b]
|
|
1206
|
+
* @returns {string}
|
|
1207
|
+
*/
|
|
1208
|
+
function hexColor(r, g, b) {
|
|
1209
|
+
if (typeof r === 'string') return r.startsWith('#') ? r.toUpperCase() : '#' + r.toUpperCase();
|
|
1210
|
+
const toHex = n => Math.max(0, Math.min(255, Math.trunc(n))).toString(16).padStart(2, '0').toUpperCase();
|
|
1211
|
+
return '#' + toHex(r) + toHex(g) + toHex(b);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// ────────────────────────────────────────────────────────────
|
|
1215
|
+
// SECTION 11: CHAINABLE FORMATTER CLASS
|
|
1216
|
+
// ────────────────────────────────────────────────────────────
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* KitFormatter — chainable formatting class.
|
|
1220
|
+
*
|
|
1221
|
+
* Start with KitFormatter.of(value) and chain methods.
|
|
1222
|
+
* Call .toString() or .value to get the final string.
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* const s = KitFormatter.of(1234567.89)
|
|
1226
|
+
* .currency('$')
|
|
1227
|
+
* .ansiStyle('bold', 'green')
|
|
1228
|
+
* .toString();
|
|
1229
|
+
*/
|
|
1230
|
+
class KitFormatter {
|
|
1231
|
+
constructor(value) {
|
|
1232
|
+
this._val = String(value != null ? value : '');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Create a new KitFormatter from any value. */
|
|
1236
|
+
static of(value) {
|
|
1237
|
+
return new KitFormatter(value);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** Get the current formatted string. */
|
|
1241
|
+
get value() { return this._val; }
|
|
1242
|
+
toString() { return this._val; }
|
|
1243
|
+
valueOf() { return this._val; }
|
|
1244
|
+
|
|
1245
|
+
// ── String ops ──────────────────────────────────────
|
|
1246
|
+
|
|
1247
|
+
/** Pad left. */
|
|
1248
|
+
padLeft(width, char = ' ') {
|
|
1249
|
+
return new KitFormatter(padLeft(this._val, width, char));
|
|
1250
|
+
}
|
|
1251
|
+
/** Pad right. */
|
|
1252
|
+
padRight(width, char = ' ') {
|
|
1253
|
+
return new KitFormatter(padRight(this._val, width, char));
|
|
1254
|
+
}
|
|
1255
|
+
/** Center. */
|
|
1256
|
+
padCenter(width, char = ' ') {
|
|
1257
|
+
return new KitFormatter(padCenter(this._val, width, char));
|
|
1258
|
+
}
|
|
1259
|
+
/** Truncate right. */
|
|
1260
|
+
truncate(maxLen, ellipsis = '…') {
|
|
1261
|
+
return new KitFormatter(truncate(this._val, maxLen, ellipsis));
|
|
1262
|
+
}
|
|
1263
|
+
/** Truncate left. */
|
|
1264
|
+
truncateLeft(maxLen, ellipsis = '…') {
|
|
1265
|
+
return new KitFormatter(truncateLeft(this._val, maxLen, ellipsis));
|
|
1266
|
+
}
|
|
1267
|
+
/** Word wrap. */
|
|
1268
|
+
wordWrap(width) {
|
|
1269
|
+
return new KitFormatter(wordWrap(this._val, width));
|
|
1270
|
+
}
|
|
1271
|
+
/** To upper case. */
|
|
1272
|
+
upper() {
|
|
1273
|
+
return new KitFormatter(this._val.toUpperCase());
|
|
1274
|
+
}
|
|
1275
|
+
/** To lower case. */
|
|
1276
|
+
lower() {
|
|
1277
|
+
return new KitFormatter(this._val.toLowerCase());
|
|
1278
|
+
}
|
|
1279
|
+
/** Trim. */
|
|
1280
|
+
trim() {
|
|
1281
|
+
return new KitFormatter(this._val.trim());
|
|
1282
|
+
}
|
|
1283
|
+
/** Slugify. */
|
|
1284
|
+
slug() {
|
|
1285
|
+
return new KitFormatter(slugify(this._val));
|
|
1286
|
+
}
|
|
1287
|
+
/** Change case. */
|
|
1288
|
+
toCase(caseName) {
|
|
1289
|
+
return new KitFormatter(changeCase(this._val, caseName));
|
|
1290
|
+
}
|
|
1291
|
+
/** Strip ANSI codes. */
|
|
1292
|
+
stripAnsi() {
|
|
1293
|
+
return new KitFormatter(stripAnsi(this._val));
|
|
1294
|
+
}
|
|
1295
|
+
/** Repeat. */
|
|
1296
|
+
repeat(n) {
|
|
1297
|
+
return new KitFormatter(repeat(this._val, n));
|
|
1298
|
+
}
|
|
1299
|
+
/** Replace via regex or string. */
|
|
1300
|
+
replace(pattern, replacement) {
|
|
1301
|
+
return new KitFormatter(this._val.replace(pattern, replacement));
|
|
1302
|
+
}
|
|
1303
|
+
/** Prepend a string. */
|
|
1304
|
+
prepend(str) {
|
|
1305
|
+
return new KitFormatter(String(str) + this._val);
|
|
1306
|
+
}
|
|
1307
|
+
/** Append a string. */
|
|
1308
|
+
append(str) {
|
|
1309
|
+
return new KitFormatter(this._val + String(str));
|
|
1310
|
+
}
|
|
1311
|
+
/** Mask sensitive data. */
|
|
1312
|
+
mask(show = 4, char = '*') {
|
|
1313
|
+
return new KitFormatter(mask(this._val, show, char));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ── Number ops (parses _val as number) ──────────────
|
|
1317
|
+
|
|
1318
|
+
/** Format as thousands-separated. */
|
|
1319
|
+
thousands(sep = ',', dec = '.') {
|
|
1320
|
+
return new KitFormatter(thousands(this._val, sep, dec));
|
|
1321
|
+
}
|
|
1322
|
+
/** Format as currency. */
|
|
1323
|
+
currency(symbol = '$', decimals = 2) {
|
|
1324
|
+
return new KitFormatter(currency(Number(this._val), symbol, decimals));
|
|
1325
|
+
}
|
|
1326
|
+
/** Fixed decimals. */
|
|
1327
|
+
fixed(n = 2) {
|
|
1328
|
+
return new KitFormatter(fixed(this._val, n));
|
|
1329
|
+
}
|
|
1330
|
+
/** Compact notation. */
|
|
1331
|
+
compact(n = 1) {
|
|
1332
|
+
return new KitFormatter(compact(this._val, n));
|
|
1333
|
+
}
|
|
1334
|
+
/** Percent. */
|
|
1335
|
+
percent(decimals = 1, raw = false) {
|
|
1336
|
+
return new KitFormatter(percent(Number(this._val), decimals, raw));
|
|
1337
|
+
}
|
|
1338
|
+
/** File size. */
|
|
1339
|
+
fileSize(decimals = 2, si = false) {
|
|
1340
|
+
return new KitFormatter(fileSize(Number(this._val), decimals, si));
|
|
1341
|
+
}
|
|
1342
|
+
/** Ordinal. */
|
|
1343
|
+
ordinal() {
|
|
1344
|
+
return new KitFormatter(ordinal(Number(this._val)));
|
|
1345
|
+
}
|
|
1346
|
+
/** Roman numerals. */
|
|
1347
|
+
roman() {
|
|
1348
|
+
return new KitFormatter(roman(Number(this._val)));
|
|
1349
|
+
}
|
|
1350
|
+
/** Number in words. */
|
|
1351
|
+
words() {
|
|
1352
|
+
return new KitFormatter(numberWords(Number(this._val)));
|
|
1353
|
+
}
|
|
1354
|
+
/** printf-style format. */
|
|
1355
|
+
printf(fmt) {
|
|
1356
|
+
return new KitFormatter(printf(fmt, this._val));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ── ANSI / color ops ────────────────────────────────
|
|
1360
|
+
|
|
1361
|
+
/** Apply ANSI styles. */
|
|
1362
|
+
ansiStyle(...codes) {
|
|
1363
|
+
return new KitFormatter(ansi(this._val, ...codes));
|
|
1364
|
+
}
|
|
1365
|
+
/** 256-color. */
|
|
1366
|
+
color256(code) {
|
|
1367
|
+
return new KitFormatter(color256(this._val, code));
|
|
1368
|
+
}
|
|
1369
|
+
/** RGB color. */
|
|
1370
|
+
colorRGB(r, g, b) {
|
|
1371
|
+
return new KitFormatter(colorRGB(this._val, r, g, b));
|
|
1372
|
+
}
|
|
1373
|
+
/** Hex color. */
|
|
1374
|
+
colorHex(hex) {
|
|
1375
|
+
return new KitFormatter(colorHex(this._val, hex));
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// ── Date ops (parses _val as Date) ─────────────────
|
|
1379
|
+
|
|
1380
|
+
/** Format as date. */
|
|
1381
|
+
dateFormat(fmt, opts) {
|
|
1382
|
+
return new KitFormatter(dateFormat(this._val, fmt, opts));
|
|
1383
|
+
}
|
|
1384
|
+
/** Relative time. */
|
|
1385
|
+
relativeTime(base) {
|
|
1386
|
+
return new KitFormatter(relativeTime(this._val, base));
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// ── Misc ────────────────────────────────────────────
|
|
1390
|
+
|
|
1391
|
+
/** Pretty JSON. */
|
|
1392
|
+
prettyJSON(opts) {
|
|
1393
|
+
try {
|
|
1394
|
+
const parsed = JSON.parse(this._val);
|
|
1395
|
+
return new KitFormatter(prettyJSON(parsed, opts));
|
|
1396
|
+
} catch {
|
|
1397
|
+
return new KitFormatter(prettyJSON(this._val, opts));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
/** Apply an arbitrary transform function. */
|
|
1401
|
+
pipe(fn) {
|
|
1402
|
+
return new KitFormatter(fn(this._val));
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// ────────────────────────────────────────────────────────────
|
|
1407
|
+
// EXPORTS
|
|
1408
|
+
// ────────────────────────────────────────────────────────────
|
|
1409
|
+
|
|
1410
|
+
module.exports = {
|
|
1411
|
+
kitdef: {
|
|
1412
|
+
// Printf / sprintf
|
|
1413
|
+
printf,
|
|
1414
|
+
sprintf,
|
|
1415
|
+
fmt,
|
|
1416
|
+
|
|
1417
|
+
// Custom spec registry
|
|
1418
|
+
registerSpec,
|
|
1419
|
+
unregisterSpec,
|
|
1420
|
+
|
|
1421
|
+
// String formatting
|
|
1422
|
+
padLeft,
|
|
1423
|
+
padRight,
|
|
1424
|
+
padCenter,
|
|
1425
|
+
truncate,
|
|
1426
|
+
truncateLeft,
|
|
1427
|
+
truncateMid,
|
|
1428
|
+
wordWrap,
|
|
1429
|
+
repeat,
|
|
1430
|
+
|
|
1431
|
+
// Number formatting
|
|
1432
|
+
thousands,
|
|
1433
|
+
currency,
|
|
1434
|
+
scientific,
|
|
1435
|
+
fileSize,
|
|
1436
|
+
percent,
|
|
1437
|
+
fixed,
|
|
1438
|
+
compact,
|
|
1439
|
+
ordinal,
|
|
1440
|
+
roman,
|
|
1441
|
+
radixFmt,
|
|
1442
|
+
numberWords,
|
|
1443
|
+
hexColor,
|
|
1444
|
+
|
|
1445
|
+
// Date / time formatting
|
|
1446
|
+
dateFormat,
|
|
1447
|
+
duration,
|
|
1448
|
+
relativeTime,
|
|
1449
|
+
|
|
1450
|
+
// ANSI / color
|
|
1451
|
+
ANSI,
|
|
1452
|
+
ansi,
|
|
1453
|
+
stripAnsi,
|
|
1454
|
+
color256,
|
|
1455
|
+
colorRGB,
|
|
1456
|
+
bgColorRGB,
|
|
1457
|
+
colorHex,
|
|
1458
|
+
|
|
1459
|
+
// Table / grid
|
|
1460
|
+
table,
|
|
1461
|
+
kvList,
|
|
1462
|
+
progressBar,
|
|
1463
|
+
treeView,
|
|
1464
|
+
|
|
1465
|
+
// JSON / object
|
|
1466
|
+
prettyJSON,
|
|
1467
|
+
diffObjects,
|
|
1468
|
+
|
|
1469
|
+
// Locale / i18n
|
|
1470
|
+
localeNumber,
|
|
1471
|
+
localeDate,
|
|
1472
|
+
localeCurrency,
|
|
1473
|
+
localeSeparator,
|
|
1474
|
+
listFormat,
|
|
1475
|
+
|
|
1476
|
+
// Misc utilities
|
|
1477
|
+
changeCase,
|
|
1478
|
+
pluralize,
|
|
1479
|
+
slugify,
|
|
1480
|
+
mask,
|
|
1481
|
+
|
|
1482
|
+
// Chainable formatter class
|
|
1483
|
+
KitFormatter,
|
|
1484
|
+
}
|
|
1485
|
+
};
|