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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /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
+ };