@endgame45/nodescript 1.0.0 → 1.1.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 (2) hide show
  1. package/package.json +9 -2
  2. package/src/nodescript.js +236 -33
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endgame45/nodescript",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "src/nodescript.js",
5
5
  "files": [
6
6
  "src"
@@ -22,5 +22,12 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "eslint": "^7.32.0"
25
- }
25
+ },
26
+ "url": {
27
+ "repository": "https://npmjs.com/package/@endgame45/nodescript",
28
+ "type": "git",
29
+ "url": "https://github.com/horizon-std/nodescript"
30
+ },
31
+ "bugs": "https://github.com/horizon-std/nodescript/issues",
32
+ "readme": ""
26
33
  }
package/src/nodescript.js CHANGED
@@ -5,25 +5,75 @@ const SUPPORTED_FONTS = new Set([
5
5
  'bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'
6
6
  ]);
7
7
 
8
+ function clamp(v, a = 0, b = 255) {
9
+ return Math.min(b, Math.max(a, Math.round(Number(v) || 0)));
10
+ }
11
+
12
+ function hslToRgb(h, s, l) {
13
+ // h: 0-360, s,l: 0-1
14
+ h = ((h % 360) + 360) % 360;
15
+ s = Math.max(0, Math.min(1, s));
16
+ l = Math.max(0, Math.min(1, l));
17
+ const c = (1 - Math.abs(2 * l - 1)) * s;
18
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
19
+ const m = l - c / 2;
20
+ let r1 = 0; let g1 = 0; let b1 = 0;
21
+ if (h < 60) { r1 = c; g1 = x; b1 = 0; }
22
+ else if (h < 120) { r1 = x; g1 = c; b1 = 0; }
23
+ else if (h < 180) { r1 = 0; g1 = c; b1 = x; }
24
+ else if (h < 240) { r1 = 0; g1 = x; b1 = c; }
25
+ else if (h < 300) { r1 = x; g1 = 0; b1 = c; }
26
+ else { r1 = c; g1 = 0; b1 = x; }
27
+ return [ clamp((r1 + m) * 255), clamp((g1 + m) * 255), clamp((b1 + m) * 255) ];
28
+ }
29
+
30
+ function blendOverBg(r, g, b, a, bgR = 0, bgG = 0, bgB = 0) {
31
+ // alpha in 0..1
32
+ const alpha = Math.max(0, Math.min(1, Number(a) || 0));
33
+ const rr = Math.round((1 - alpha) * bgR + alpha * r);
34
+ const gg = Math.round((1 - alpha) * bgG + alpha * g);
35
+ const bb = Math.round((1 - alpha) * bgB + alpha * b);
36
+ return [clamp(rr), clamp(gg), clamp(bb)];
37
+ }
38
+
8
39
  function parseColor(styler, color, isBg = false) {
9
40
  if (!color) return styler;
10
41
  const c = String(color).trim();
42
+ // hex
43
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) {
44
+ return isBg ? styler.bgHex(c) : styler.hex(c);
45
+ }
46
+ // rgb() or rgba()
47
+ const rgbMatch = c.match(/^rgba?\s*\(([^)]+)\)$/i);
48
+ if (rgbMatch) {
49
+ const parts = rgbMatch[1].split(',').map(s => s.trim());
50
+ const r = parseInt(parts[0], 10) || 0;
51
+ const g = parseInt(parts[1], 10) || 0;
52
+ const b = parseInt(parts[2], 10) || 0;
53
+ const a = parts[3] !== undefined ? parseFloat(parts[3]) : 1;
54
+ if (a >= 1) return isBg ? styler.bgRgb(r, g, b) : styler.rgb(r, g, b);
55
+ // blend alpha over background if provided via isBg===false and styler has bgColor; we don't know bg, assume black
56
+ const [rr, gg, bb] = blendOverBg(r, g, b, a, 0, 0, 0);
57
+ return isBg ? styler.bgRgb(rr, gg, bb) : styler.rgb(rr, gg, bb);
58
+ }
59
+ // hsl() or hsla()
60
+ const hslMatch = c.match(/^hsla?\s*\(([^)]+)\)$/i);
61
+ if (hslMatch) {
62
+ const parts = hslMatch[1].split(',').map(s => s.trim());
63
+ const h = parseFloat(parts[0]) || 0;
64
+ const s = (String(parts[1]).replace('%', '') || 0) / 100;
65
+ const l = (String(parts[2]).replace('%', '') || 0) / 100;
66
+ const a = parts[3] !== undefined ? parseFloat(parts[3]) : 1;
67
+ const [r, g, b] = hslToRgb(h, s, l);
68
+ if (a >= 1) return isBg ? styler.bgRgb(r, g, b) : styler.rgb(r, g, b);
69
+ const [rr, gg, bb] = blendOverBg(r, g, b, a, 0, 0, 0);
70
+ return isBg ? styler.bgRgb(rr, gg, bb) : styler.rgb(rr, gg, bb);
71
+ }
72
+ // fallback to CSS keyword (named color)
11
73
  try {
12
- if (/^#/.test(c)) {
13
- return isBg ? styler.bgHex(c) : styler.hex(c);
14
- }
15
- if (/^rgb\(/i.test(c)) {
16
- const nums = c
17
- .replace(/^[^(]*\(/, '')
18
- .replace(/\).*/, '')
19
- .split(',')
20
- .map(n => parseInt(n, 10) || 0);
21
- return isBg ? styler.bgRgb(nums[0], nums[1], nums[2]) : styler.rgb(nums[0], nums[1], nums[2]);
22
- }
23
- // fallback to CSS keyword (named color)
24
74
  return isBg ? styler.bgKeyword(c) : styler.keyword(c);
25
75
  } catch (e) {
26
- return styler; // if chalk call fails, return base styler
76
+ return styler;
27
77
  }
28
78
  }
29
79
 
@@ -225,8 +275,35 @@ const nsc = {
225
275
  textColor: null,
226
276
  bgColor: null,
227
277
  font: null,
278
+ namespace: null,
279
+ // logging level: error, warn, info, debug
280
+ level: 'info',
281
+ levels: { error: 0, warn: 1, info: 2, debug: 3 },
282
+ // auto color detection (respects NO_COLOR and TTY)
283
+ autoColor: true,
284
+ // timestamping options: { enabled: false, format: 'iso'|'local'|function }
285
+ timestamp: { enabled: false, format: 'iso' },
286
+
287
+ _colorEnabled() {
288
+ if (!this.autoColor) return false;
289
+ if (process && process.env && process.env.NO_COLOR) return false;
290
+ return Boolean(process && process.stdout && process.stdout.isTTY);
291
+ },
228
292
 
229
293
  _getStyler() {
294
+ if (!this._colorEnabled()) {
295
+ // minimal no-color styler that mimics chalk chaining API
296
+ const noColor = (s) => String(s);
297
+ const fonts = ['bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'];
298
+ for (const f of fonts) noColor[f] = noColor;
299
+ noColor.hex = () => noColor;
300
+ noColor.bgHex = () => noColor;
301
+ noColor.rgb = () => noColor;
302
+ noColor.bgRgb = () => noColor;
303
+ noColor.keyword = () => noColor;
304
+ noColor.bgKeyword = () => noColor;
305
+ return noColor;
306
+ }
230
307
  return buildStyler({
231
308
  textColor: this.textColor,
232
309
  bgColor: this.bgColor,
@@ -234,36 +311,81 @@ const nsc = {
234
311
  });
235
312
  },
236
313
 
237
- // nsc.log can be used in two ways:
238
- // 1) nsc.log('a', 'b', 123)
239
- // 2) nsc.log(() => { console.log('inside will be styled') }) <-- temporary override of console.log
240
- log(...args) {
241
- // callback-mode: override console.log while invoking the function
314
+ _formatPrefix(level) {
315
+ const parts = [];
316
+ if (this.timestamp && this.timestamp.enabled) {
317
+ const fmt = this.timestamp.format;
318
+ let ts = '';
319
+ if (typeof fmt === 'function') ts = String(fmt());
320
+ else if (fmt === 'local') ts = new Date().toLocaleString();
321
+ else ts = new Date().toISOString();
322
+ parts.push(ts);
323
+ }
324
+ if (this.namespace) parts.push(this.namespace);
325
+ if (level) parts.push(level.toUpperCase());
326
+ return parts.length ? '[' + parts.join('] [') + '] ' : '';
327
+ },
328
+
329
+ _shouldLog(level) {
330
+ const cur = this.levels[this.level] ?? 2;
331
+ const lvl = this.levels[level] ?? 2;
332
+ return lvl <= cur;
333
+ },
334
+
335
+ _printStyled(text, level) {
336
+ // choose effective color without mutating instance
337
+ let defaultColor;
338
+ switch (level) {
339
+ case 'error': defaultColor = 'red'; break;
340
+ case 'warn': defaultColor = 'yellow'; break;
341
+ case 'info': defaultColor = 'cyan'; break;
342
+ case 'debug': defaultColor = 'magenta'; break;
343
+ default: defaultColor = null; break;
344
+ }
345
+ let styler;
346
+ if (!this._colorEnabled()) {
347
+ const noColor = (s) => String(s);
348
+ const fonts = ['bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'];
349
+ for (const f of fonts) noColor[f] = noColor;
350
+ noColor.hex = () => noColor;
351
+ noColor.bgHex = () => noColor;
352
+ noColor.rgb = () => noColor;
353
+ noColor.bgRgb = () => noColor;
354
+ noColor.keyword = () => noColor;
355
+ noColor.bgKeyword = () => noColor;
356
+ styler = noColor;
357
+ } else {
358
+ styler = buildStyler({
359
+ textColor: this.textColor || defaultColor,
360
+ bgColor: this.bgColor,
361
+ font: this.font
362
+ });
363
+ }
364
+ const out = styler(text);
365
+ console.log(out);
366
+ },
367
+
368
+ // low-level logging method used by level helpers
369
+ _log(level, ...args) {
370
+ if (level && !this._shouldLog(level)) return;
371
+ // support callback-mode where a function temporarily overrides console.log
242
372
  if (args.length === 1 && typeof args[0] === 'function') {
243
373
  const fn = args[0];
244
374
  const originalLog = console.log;
375
+ const prefix = this._formatPrefix(level);
245
376
  const styler = this._getStyler();
246
377
 
247
378
  console.log = (...innerArgs) => {
248
379
  const out = util.format(...innerArgs);
249
- originalLog(styler(out));
380
+ originalLog(prefix + styler(out));
250
381
  };
251
382
 
252
383
  try {
253
384
  const result = fn();
254
- // support async callbacks (promises)
255
385
  if (result && typeof result.then === 'function') {
256
- return result
257
- .then(res => {
258
- console.log = originalLog;
259
- return res;
260
- })
261
- .catch(err => {
262
- console.log = originalLog;
263
- throw err;
264
- });
386
+ return result.then((res) => { console.log = originalLog; return res; })
387
+ .catch((err) => { console.log = originalLog; throw err; });
265
388
  }
266
- // sync
267
389
  console.log = originalLog;
268
390
  return result;
269
391
  } catch (err) {
@@ -272,10 +394,91 @@ const nsc = {
272
394
  }
273
395
  }
274
396
 
275
- // normal-mode: format args and print styled
397
+ const prefix = this._formatPrefix(level);
398
+ const message = util.format(...args);
399
+ // combine prefix and message
400
+ const full = prefix + message;
401
+ this._printStyled(full, level);
402
+ },
403
+
404
+ // public API: default log (info-level)
405
+ log(...args) { return this._log('info', ...args); },
406
+
407
+ info(...args) { return this._log('info', ...args); },
408
+ warn(...args) { return this._log('warn', ...args); },
409
+ error(...args) { return this._log('error', ...args); },
410
+ debug(...args) { return this._log('debug', ...args); },
411
+
412
+ // create a namespaced logger instance with optional defaults
413
+ create(namespace, opts = {}) {
414
+ const inst = Object.create(this);
415
+ inst.namespace = namespace || null;
416
+ if (opts.textColor) inst.textColor = opts.textColor;
417
+ if (opts.bgColor) inst.bgColor = opts.bgColor;
418
+ if (opts.font) inst.font = opts.font;
419
+ if (opts.level) inst.level = opts.level;
420
+ if (typeof opts.autoColor === 'boolean') inst.autoColor = opts.autoColor;
421
+ if (opts.timestamp) inst.timestamp = Object.assign({}, inst.timestamp, opts.timestamp);
422
+ // bind methods to instance for convenience
423
+ inst.log = inst.log.bind(inst);
424
+ inst.info = inst.info.bind(inst);
425
+ inst.warn = inst.warn.bind(inst);
426
+ inst.error = inst.error.bind(inst);
427
+ inst.debug = inst.debug.bind(inst);
428
+ inst.create = inst.create.bind(inst);
429
+ return inst;
430
+ },
431
+ // theme registry
432
+ _themes: new Map(),
433
+ registerTheme(name, opts) {
434
+ if (!name || typeof name !== 'string') throw new TypeError('theme name must be a string');
435
+ if (!opts || typeof opts !== 'object') throw new TypeError('theme opts must be an object');
436
+ // store a shallow copy
437
+ this._themes.set(name, Object.assign({}, opts));
438
+ return this;
439
+ },
440
+ getTheme(name) {
441
+ return this._themes.get(name) || null;
442
+ },
443
+ // apply theme to this instance (mutates)
444
+ applyTheme(name) {
445
+ const t = this.getTheme(name);
446
+ if (!t) throw new Error('unknown theme: ' + name);
447
+ if (t.textColor) this.textColor = t.textColor;
448
+ if (t.bgColor) this.bgColor = t.bgColor;
449
+ if (t.font) this.font = t.font;
450
+ if (t.level) this.level = t.level;
451
+ if (t.timestamp) this.timestamp = Object.assign({}, this.timestamp, t.timestamp);
452
+ return this;
453
+ },
454
+ // return a new instance pre-configured with theme
455
+ useTheme(name) {
456
+ const t = this.getTheme(name);
457
+ if (!t) throw new Error('unknown theme: ' + name);
458
+ return this.create(null, t);
459
+ },
460
+
461
+ // tagged template helper: returns styled string (does not log)
462
+ style(strings, ...values) {
463
+ // combine template parts, evaluating functions
464
+ const parts = [];
465
+ for (let i = 0; i < strings.length; i++) {
466
+ parts.push(strings[i]);
467
+ if (i < values.length) {
468
+ const v = values[i];
469
+ if (typeof v === 'function') parts.push(String(v()));
470
+ else parts.push(String(v));
471
+ }
472
+ }
473
+ const full = parts.join('');
276
474
  const styler = this._getStyler();
277
- const out = util.format(...args);
278
- console.log(styler(out));
475
+ return styler(full);
476
+ },
477
+
478
+ // convenience: styled log via template tag
479
+ tagLog(strings, ...values) {
480
+ const s = this.style(strings, ...values);
481
+ console.log(s);
279
482
  },
280
483
 
281
484
  /**