pixl-cli 1.1.0 → 1.1.2

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 (41) hide show
  1. package/README.md +101 -0
  2. package/cli.js +224 -16
  3. package/package.json +1 -1
  4. package/util.js +25 -1
  5. package/width.js +62 -1
  6. package/wrap.js +5 -24
  7. package/agents/string-width/.editorconfig +0 -12
  8. package/agents/string-width/.gitattributes +0 -1
  9. package/agents/string-width/.github/security.md +0 -3
  10. package/agents/string-width/.github/workflows/main.yml +0 -22
  11. package/agents/string-width/index.d.ts +0 -39
  12. package/agents/string-width/index.js +0 -207
  13. package/agents/string-width/index.test-d.ts +0 -7
  14. package/agents/string-width/license +0 -9
  15. package/agents/string-width/package.json +0 -65
  16. package/agents/string-width/readme.md +0 -66
  17. package/agents/string-width/test.js +0 -339
  18. package/agents/widest-line/.editorconfig +0 -12
  19. package/agents/widest-line/.gitattributes +0 -1
  20. package/agents/widest-line/.github/security.md +0 -3
  21. package/agents/widest-line/.github/workflows/main.yml +0 -21
  22. package/agents/widest-line/index.d.ts +0 -12
  23. package/agents/widest-line/index.js +0 -11
  24. package/agents/widest-line/license +0 -9
  25. package/agents/widest-line/package.json +0 -60
  26. package/agents/widest-line/readme.md +0 -26
  27. package/agents/widest-line/test.js +0 -8
  28. package/agents/word-wrap/.editorconfig +0 -13
  29. package/agents/word-wrap/.eslintrc.json +0 -122
  30. package/agents/word-wrap/.gitattributes +0 -10
  31. package/agents/word-wrap/.github/workflows/publish.yml +0 -19
  32. package/agents/word-wrap/.travis.yml +0 -13
  33. package/agents/word-wrap/.verb.md +0 -114
  34. package/agents/word-wrap/LICENSE +0 -21
  35. package/agents/word-wrap/README.md +0 -201
  36. package/agents/word-wrap/bower.json +0 -60
  37. package/agents/word-wrap/index.d.ts +0 -50
  38. package/agents/word-wrap/index.js +0 -61
  39. package/agents/word-wrap/package.json +0 -77
  40. package/agents/word-wrap/test.js +0 -69
  41. package/test.js +0 -20
package/README.md CHANGED
@@ -398,6 +398,105 @@ culpa qui officia deserunt mollit anim
398
398
  id est laborum.
399
399
  ```
400
400
 
401
+ ## Displaying Definition Lists
402
+
403
+ Call `cli.defList()` to render a two-column list of labels and values inside a box. Labels are bold by default, values are plain, and no vertical border is drawn between the columns. Colons are added to labels automatically when needed. Lines will automatically truncate with ellipsis as needed.
404
+
405
+ ```js
406
+ const cli = require('pixl-cli');
407
+
408
+ let event = {
409
+ id: "emm2wtlsxrtjz8v4",
410
+ title: "Simple Shell Test",
411
+ category: "general",
412
+ username: "admin"
413
+ };
414
+
415
+ cli.println( cli.defList([
416
+ [ "Event ID", event.id ],
417
+ [ "Title", event.title ],
418
+ [ "Category", event.category ],
419
+ [ "Author", event.username ]
420
+ ], {
421
+ labelStyles: ["yellow", "bold"],
422
+ textStyles: ["green"],
423
+ indent: 1
424
+ }) );
425
+ ```
426
+
427
+ This produces a definition list like this, with colors when the terminal supports them:
428
+
429
+ ```
430
+ ┌─────────────────────────────┐
431
+ │ Event ID: emm2wtlsxrtjz8v4 │
432
+ │ Title: Simple Shell Test │
433
+ │ Category: general │
434
+ │ Author: admin │
435
+ └─────────────────────────────┘
436
+ ```
437
+
438
+ The label and value columns are left-aligned. When the box would exceed the attached terminal width, long values are shortened with an ellipsis. The complete label column is preserved whenever possible. The `indent` is treated as a horizontal margin on both sides when calculating the available width.
439
+
440
+ You can customize the definition list with these options:
441
+
442
+ | Property Name | Description |
443
+ |---------------|-------------|
444
+ | `labelStyles` | An array of [chalk](https://www.npmjs.com/package/chalk) styles or functions for labels. Defaults to `["bold"]`. |
445
+ | `textStyles` | An array of styles or functions for values. Defaults to `[]`. |
446
+ | `borderStyles` | An array of styles or functions for the box border. Defaults to `["gray"]`. |
447
+ | `indent` | Horizontal margin in characters on both sides of the box. Defaults to `0`. |
448
+ | `gap` | Spaces between the label and value columns. Defaults to `1`. |
449
+ | `hspace` | Spaces between the content and each side of the box. Defaults to `1`. |
450
+ | `vspace` | Empty lines above and below the list inside the box. Defaults to `0`. |
451
+
452
+ ## Displaying Dashboard Grids
453
+
454
+ Call `cli.dashGrid()` to render a responsive grid of dashboard units. Each unit contains a centered value and label, and all units have the same dimensions regardless of their content. Pass an array of `[label, value]` pairs:
455
+
456
+ ```js
457
+ const cli = require('pixl-cli');
458
+
459
+ cli.println( cli.dashGrid([
460
+ [ "Conductors", 1 ],
461
+ [ "Servers", 2 ],
462
+ [ "Current Alerts", 0 ],
463
+ [ "Active Jobs", 0 ],
464
+ [ "Job Success Rate", "100%" ],
465
+ [ "Avg. Job Elapsed", "31 sec" ]
466
+ ], {
467
+ minCols: 3,
468
+ maxCols: 5,
469
+ gap: 1,
470
+ valueStyles: ["bold", "green"]
471
+ }) );
472
+ ```
473
+
474
+ At a width of 62 columns, the first row looks like this:
475
+
476
+ ```
477
+ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
478
+ │ 1 │ │ 2 │ │ 0 │
479
+ │ │ │ │ │ │
480
+ │ Conductors │ │ Servers │ │ Current Alerts │
481
+ └──────────────────┘ └──────────────────┘ └──────────────────┘
482
+ ```
483
+
484
+ The grid uses the current terminal width and flows additional units onto new rows. An incomplete final row ends after its last unit. Labels and values are limited to one line and automatically shortened with an ellipsis when necessary. ANSI styles and emoji are measured by their terminal display width.
485
+
486
+ You can customize the dashboard grid with these options:
487
+
488
+ | Property Name | Description |
489
+ |---------------|-------------|
490
+ | `width` | Overall layout width, including horizontal margins. Defaults to `cli.width()`, or `80` when no terminal width is available. |
491
+ | `unitWidth` | Target outer width used to select the responsive column count. Defaults to `20`. |
492
+ | `minCols` | Preferred minimum number of columns. Defaults to `3`. Extremely narrow terminals may use fewer to avoid overflow. |
493
+ | `maxCols` | Maximum number of columns. Defaults to `5`. |
494
+ | `gap` | Horizontal spaces between units and blank lines between grid rows. Defaults to `1`. |
495
+ | `indent` | Horizontal margin in characters on both sides of the grid. Defaults to `0`. |
496
+ | `valueStyles` | An array of [chalk](https://www.npmjs.com/package/chalk) styles or functions for values. Defaults to `["bold"]`. |
497
+ | `labelStyles` | An array of styles or functions for labels. Defaults to `["gray"]`. |
498
+ | `borderStyles` | An array of styles or functions for unit borders. Defaults to `["gray"]`. |
499
+
401
500
  ## Displaying Tables
402
501
 
403
502
  ![Table Example](https://pixlcore.com/software/pixl-cli/table.png)
@@ -687,6 +786,8 @@ The full list of methods and objects that are imported are:
687
786
  - `yesno()`
688
787
  - `table()`
689
788
  - `box()`
789
+ - `defList()`
790
+ - `dashGrid()`
690
791
  - `wrap()`
691
792
  - `center()`
692
793
  - `commify()`
package/cli.js CHANGED
@@ -197,6 +197,227 @@ var cli = module.exports = {
197
197
  return output.join("\n");
198
198
  },
199
199
 
200
+ defList: function(rows, args) {
201
+ // Render a two-column definition list inside a box. The labels share a
202
+ // common width, but there is deliberately no border between the columns.
203
+ var self = this;
204
+ if (!args) args = {};
205
+
206
+ // Empty arrays are valid style overrides, so test for the property rather
207
+ // than using || here. Labels are bold by default and values are plain.
208
+ var labelStyles = ('labelStyles' in args) ? args.labelStyles : ["bold"];
209
+ var textStyles = ('textStyles' in args) ? args.textStyles : [];
210
+ var borderStyles = ('borderStyles' in args) ? args.borderStyles :
211
+ (('styles' in args) ? args.styles : ["gray"]);
212
+ var hspace = ('hspace' in args) ? args.hspace : 1;
213
+ var vspace = args.vspace || 0;
214
+
215
+ // Gaps are whole terminal cells. Invalid and negative values become zero.
216
+ var gap = ('gap' in args) ? Math.floor(Number(args.gap)) : 1;
217
+ if (!isFinite(gap) || (gap < 0)) gap = 0;
218
+ var indent = args.indent || "";
219
+ if (typeof(indent) == 'number') indent = this.space(indent);
220
+
221
+ // Normalize and style all cells before measuring them. This allows ANSI
222
+ // color, custom style functions and the cli.emoji() cursor hack to coexist
223
+ // with the display-width calculations below.
224
+ var items = (rows || []).map( function(row) {
225
+ var label = '' + row[0];
226
+ var value = '' + row[1];
227
+
228
+ // Definition lists are one item per line. Flatten accidental newlines so
229
+ // a cell cannot escape its row and disturb the surrounding box.
230
+ label = label.replace(/\r?\n/g, ' ').replace(/\s+$/, '');
231
+ value = value.replace(/\r?\n/g, ' ');
232
+ if (!label.replace(ansiPattern, '').match(/:$/)) label += ':';
233
+
234
+ return {
235
+ label: self.applyStyles(label, labelStyles),
236
+ value: self.applyStyles(value, textStyles)
237
+ };
238
+ } );
239
+
240
+ var labelWidth = 0;
241
+ var valueWidth = 0;
242
+
243
+ items.forEach( function(item) {
244
+ labelWidth = Math.max( labelWidth, stringWidth(item.label) );
245
+ valueWidth = Math.max( valueWidth, stringWidth(item.value) );
246
+ } );
247
+
248
+ // The indent acts as a horizontal margin, so reserve it on both sides.
249
+ // Also reserve both box borders, both padding areas, and the requested gap
250
+ // separating the label and value columns.
251
+ var terminalWidth = this.width();
252
+ var contentWidth = labelWidth + gap + valueWidth;
253
+
254
+ if (terminalWidth) {
255
+ var availableWidth = terminalWidth - (stringWidth(indent) * 2);
256
+ var availableContentWidth = Math.max(0, availableWidth - 2 - (hspace * 2));
257
+ contentWidth = Math.min(contentWidth, availableContentWidth);
258
+ }
259
+
260
+ // Keep the complete label column whenever possible, and give all remaining
261
+ // room to values. On extremely narrow terminals labels are shortened too,
262
+ // while still reserving one cell for a value when any room remains.
263
+ if (labelWidth + gap + 1 > contentWidth) {
264
+ labelWidth = Math.max(0, contentWidth - gap - 1);
265
+ }
266
+ valueWidth = Math.max(0, contentWidth - labelWidth - (labelWidth ? gap : 0));
267
+
268
+ var truncate = function(text, width) {
269
+ // Width.truncate() preserves ANSI resets and complete emoji graphemes.
270
+ if (width < 1) return '';
271
+ return (stringWidth(text) > width) ? Width.truncate(text, width, '…') : text;
272
+ };
273
+
274
+ var lines = items.map( function(item) {
275
+ var label = truncate(item.label, labelWidth);
276
+ var value = truncate(item.value, valueWidth);
277
+ if (!labelWidth) return value;
278
+ return self.pad(label, labelWidth) + self.space(gap) + value;
279
+ } );
280
+
281
+ // Let box() handle the border, padding, vertical spacing and final row
282
+ // padding. Passing the normalized indent keeps numeric margins consistent.
283
+ return this.box( lines.join("\n"), {
284
+ styles: borderStyles,
285
+ hspace: hspace,
286
+ vspace: vspace,
287
+ indent: indent
288
+ } );
289
+ },
290
+
291
+ dashGrid: function(rows, args) {
292
+ // Render a responsive grid of equal-sized dashboard units. Each unit has a
293
+ // centered value, a centered label, two blank spacer rows and its own border.
294
+ var self = this;
295
+ if (!rows || !rows.length) return '';
296
+ if (!args) args = {};
297
+
298
+ // Normalize numeric layout options to non-negative whole terminal cells.
299
+ // Invalid values fall back to their documented defaults.
300
+ var wholeNumber = function(value, defaultValue, minimum) {
301
+ value = Math.floor(Number(value));
302
+ if (!isFinite(value)) value = defaultValue;
303
+ return Math.max(minimum, value);
304
+ };
305
+
306
+ var targetUnitWidth = wholeNumber(args.unitWidth, 20, 5);
307
+ var minCols = wholeNumber(args.minCols, 3, 1);
308
+ var maxCols = wholeNumber(args.maxCols, 5, 1);
309
+ var gap = wholeNumber(args.gap, 1, 0);
310
+ var indent = args.indent || '';
311
+ if (typeof(indent) == 'number') indent = this.space(indent);
312
+
313
+ // A maximum is a hard cap. If conflicting limits are supplied, lower the
314
+ // minimum to match rather than silently exceeding the requested maximum.
315
+ if (minCols > maxCols) minCols = maxCols;
316
+
317
+ // Empty style arrays are intentional overrides, so property checks are used
318
+ // instead of ||. Values are bold, labels and borders are gray by default.
319
+ var valueStyles = ('valueStyles' in args) ? args.valueStyles : ["bold"];
320
+ var labelStyles = ('labelStyles' in args) ? args.labelStyles : ["gray"];
321
+ var borderStyles = ('borderStyles' in args) ? args.borderStyles : ["gray"];
322
+
323
+ // An explicit width makes redirected output and tests deterministic. When
324
+ // attached to a terminal, cli.width() returns process.stdout.columns.
325
+ var layoutWidth = ('width' in args) ?
326
+ wholeNumber(args.width, 80, 0) : (this.width() || 80);
327
+ var availableWidth = Math.max(0, layoutWidth - (stringWidth(indent) * 2));
328
+ if (availableWidth < 2) return '';
329
+
330
+ // Start with the preferred responsive column count, bounded by the caller's
331
+ // limits and the number of units. unitWidth is a planning target; the actual
332
+ // width is calculated below after horizontal gaps have been reserved.
333
+ var numCols = Math.floor(availableWidth / targetUnitWidth);
334
+ numCols = Math.max(minCols, Math.min(maxCols, numCols));
335
+ numCols = Math.min(numCols, rows.length);
336
+
337
+ // Each useful unit needs five cells: two borders, two padding cells and one
338
+ // content cell. On a very narrow terminal, safety takes priority over the
339
+ // requested minimum column count so the grid never overflows.
340
+ var maxFittingCols = Math.floor((availableWidth + gap) / (5 + gap));
341
+ maxFittingCols = Math.max(1, maxFittingCols);
342
+ numCols = Math.max(1, Math.min(numCols, maxFittingCols));
343
+
344
+ // All units use the same exact outer width. Any indivisible remainder stays
345
+ // unused at the right edge, including on incomplete final rows.
346
+ var unitWidth = Math.floor(
347
+ (availableWidth - ((numCols - 1) * gap)) / numCols
348
+ );
349
+ if (unitWidth < 2) return '';
350
+
351
+ var innerWidth = unitWidth - 2;
352
+ var contentWidth = Math.max(0, innerWidth - 2);
353
+
354
+ var truncate = function(text) {
355
+ // Preserve ANSI styles, complete graphemes and cli.emoji() sequences.
356
+ if (contentWidth < 1) return '';
357
+ return (stringWidth(text) > contentWidth) ?
358
+ Width.truncate(text, contentWidth, '…') : text;
359
+ };
360
+
361
+ var centerCell = function(text) {
362
+ // Center by terminal display width rather than JavaScript string length.
363
+ var remain = Math.max(0, innerWidth - stringWidth(text));
364
+ var left = Math.floor(remain / 2);
365
+ return self.space(left) + text + self.space(remain - left);
366
+ };
367
+
368
+ var units = rows.map( function(row) {
369
+ var label = ('' + row[0]).replace(/\r?\n/g, ' ');
370
+ var value = ('' + row[1]).replace(/\r?\n/g, ' ');
371
+ return {
372
+ label: self.applyStyles(label, labelStyles),
373
+ value: self.applyStyles(value, valueStyles)
374
+ };
375
+ } );
376
+
377
+ var renderUnit = function(unit) {
378
+ // The interior layout is: blank, value, blank, label.
379
+ var top = self.applyStyles(
380
+ '┌' + self.repeat('─', innerWidth) + '┐', borderStyles
381
+ );
382
+ var bottom = self.applyStyles(
383
+ '└' + self.repeat('─', innerWidth) + '┘', borderStyles
384
+ );
385
+ var leftBorder = self.applyStyles('│', borderStyles);
386
+ var rightBorder = self.applyStyles('│', borderStyles);
387
+ var blank = leftBorder + self.space(innerWidth) + rightBorder;
388
+
389
+ return [
390
+ top,
391
+ // blank,
392
+ leftBorder + centerCell(truncate(unit.value)) + rightBorder,
393
+ blank,
394
+ leftBorder + centerCell(truncate(unit.label)) + rightBorder,
395
+ bottom
396
+ ];
397
+ };
398
+
399
+ var output = [];
400
+ for (var rowIdx = 0; rowIdx < units.length; rowIdx += numCols) {
401
+ var gridRow = units.slice(rowIdx, rowIdx + numCols).map(renderUnit);
402
+
403
+ // Join corresponding lines from each unit to form one complete grid row.
404
+ for (var lineIdx = 0; lineIdx < 6; lineIdx++) {
405
+ output.push(
406
+ indent + gridRow.map( function(unit) {
407
+ return unit[lineIdx];
408
+ } ).join(self.space(gap))
409
+ );
410
+ }
411
+
412
+ // The same gap controls vertical blank lines between rows of units.
413
+ if (rowIdx + numCols < units.length) {
414
+ for (var gapIdx = 0; gapIdx < gap; gapIdx++) output.push('');
415
+ }
416
+ }
417
+
418
+ return output.join("\n");
419
+ },
420
+
200
421
  applyStyles: function(text, styles) {
201
422
  // apply one or more chalk styles or functions to text string
202
423
  if (!styles) return text;
@@ -298,25 +519,12 @@ var cli = module.exports = {
298
519
  max_col_widths[longest_col_idx]--;
299
520
  }
300
521
 
301
- // finally prune affected columns, trying to preserve ANSI color inside column value
522
+ // finally prune affected columns by display width, preserving ANSI and emoji
302
523
  rows.forEach( function(cols, idx) {
303
524
  cols.forEach( function(col, idy) {
304
525
  col = '' + col;
305
526
  if (stringWidth(col) > max_col_widths[idy]) {
306
- var suffix = '';
307
- var prefix = '';
308
-
309
- while (col.match(/^(\u001b\[[^m]*?m)/)) {
310
- prefix += RegExp.$1;
311
- col = col.replace(/^(\u001b\[[^m]*?m)/, '');
312
- }
313
-
314
- while (col.match(/(\u001b\[[^m]*?m)$/)) {
315
- suffix = RegExp.$1 + suffix;
316
- col = col.replace(/(\u001b\[[^m]*?m)$/, '');
317
- }
318
-
319
- cols[idy] = prefix + col.substring(0, max_col_widths[idy] - 1) + '…' + suffix;
527
+ cols[idy] = Width.truncate(col, max_col_widths[idy], '');
320
528
  } // too wide
321
529
  });
322
530
  });
@@ -500,7 +708,7 @@ var cli = module.exports = {
500
708
  global.Tools = Tools;
501
709
 
502
710
  // bind wrap functions
503
- ["prompt", "yesno", "table", "box", "wrap", "center", "print", "println", "verbose", "verboseln", "warn", "warnln", "die", "dieln", "loadFile", "saveFile", "appendFile"].forEach( function(func) {
711
+ ["prompt", "yesno", "table", "box", "defList", "dashGrid", "wrap", "center", "print", "println", "verbose", "verboseln", "warn", "warnln", "die", "dieln", "loadFile", "saveFile", "appendFile"].forEach( function(func) {
504
712
  global[func] = self[func].bind(self);
505
713
  } );
506
714
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Tools for building command-line apps for Node.js.",
5
5
  "author": "Joseph Huckaby <jhuckaby@gmail.com>",
6
6
  "homepage": "https://github.com/jhuckaby/pixl-cli",
package/util.js CHANGED
@@ -9,7 +9,31 @@ var ansiPattern = new RegExp([
9
9
  // Split strings into user-perceived characters, including complete emoji sequences.
10
10
  var graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
11
11
 
12
+ function splitAnsiGraphemes(text) {
13
+ // Split a string into complete ANSI sequences and Unicode grapheme clusters.
14
+ // ANSI sequences are kept intact so callers can preserve them while editing text.
15
+ var units = [];
16
+ var pattern = new RegExp(ansiPattern.source, 'g');
17
+ var offset = 0;
18
+ var match = null;
19
+
20
+ var addText = function(value) {
21
+ for (var item of graphemeSegmenter.segment(value)) {
22
+ units.push({ text: item.segment, ansi: false });
23
+ }
24
+ };
25
+
26
+ while ((match = pattern.exec(text))) {
27
+ if (match.index > offset) addText( text.substring(offset, match.index) );
28
+ units.push({ text: match[0], ansi: true });
29
+ offset = pattern.lastIndex;
30
+ }
31
+ if (offset < text.length) addText( text.substring(offset) );
32
+ return units;
33
+ }
34
+
12
35
  module.exports = {
13
36
  ansiPattern: ansiPattern,
14
- graphemeSegmenter: graphemeSegmenter
37
+ graphemeSegmenter: graphemeSegmenter,
38
+ splitAnsiGraphemes: splitAnsiGraphemes
15
39
  };
package/width.js CHANGED
@@ -65,11 +65,72 @@ function widestLine(text) {
65
65
  return width;
66
66
  }
67
67
 
68
+ function truncate(text, width, suffix) {
69
+ // Truncate a string to an exact terminal display width. Preserve complete ANSI
70
+ // sequences and grapheme clusters, and retain trailing SGR codes to reset styles.
71
+ if (typeof(text) != 'string') text = '' + text;
72
+ if (suffix == null) suffix = '';
73
+ if (stringWidth(text) <= width) return text;
74
+
75
+ var suffixWidth = stringWidth(suffix);
76
+ var contentWidth = Math.max(0, width - suffixWidth);
77
+ var outputWidth = 0;
78
+ var output = '';
79
+ var units = Util.splitAnsiGraphemes(text);
80
+ var trailingAnsi = [];
81
+
82
+ // cli.emoji() surrounds one grapheme with cursor save, restore and movement
83
+ // sequences. Treat this entire construct as one display unit so truncation
84
+ // can never keep the save sequence while discarding its matching restore.
85
+ for (var idx = 0; idx <= units.length - 4; idx++) {
86
+ if (
87
+ units[idx].ansi && (units[idx].text === '\u001b7') &&
88
+ !units[idx + 1].ansi &&
89
+ units[idx + 2].ansi && (units[idx + 2].text === '\u001b8') &&
90
+ units[idx + 3].ansi && (units[idx + 3].text === '\u001b[2C')
91
+ ) {
92
+ var emojiUnit = units.slice(idx, idx + 4).map( function(unit) {
93
+ return unit.text;
94
+ } ).join('');
95
+ units.splice(idx, 4, {
96
+ text: emojiUnit,
97
+ ansi: false
98
+ });
99
+ }
100
+ }
101
+
102
+ for (var idx = units.length - 1; idx >= 0; idx--) {
103
+ if (!units[idx].ansi) break;
104
+ if (units[idx].text.match(/^(?:\u001B\[|\u009B)[^m]*m$/)) {
105
+ trailingAnsi.unshift(units[idx].text);
106
+ }
107
+ }
108
+
109
+ for (var idx = 0, len = units.length; idx < len; idx++) {
110
+ var unit = units[idx];
111
+ if (unit.ansi) {
112
+ output += unit.text;
113
+ continue;
114
+ }
115
+
116
+ var unitWidth = stringWidth(unit.text);
117
+ if ((outputWidth + unitWidth) > contentWidth) break;
118
+ output += unit.text;
119
+ outputWidth += unitWidth;
120
+ }
121
+
122
+ // A wide grapheme may not fit the final available cell. Pad that cell so the
123
+ // ellipsis still lands at the exact requested width and tables remain aligned.
124
+ if (outputWidth < contentWidth) output += ' '.repeat(contentWidth - outputWidth);
125
+ return output + suffix + trailingAnsi.join('');
126
+ }
127
+
68
128
  // Preserve the compatibility aliases provided by the old CommonJS dependencies.
69
129
  stringWidth.default = stringWidth;
70
130
  widestLine.default = widestLine;
71
131
 
72
132
  module.exports = {
73
133
  stringWidth: stringWidth,
74
- widestLine: widestLine
134
+ widestLine: widestLine,
135
+ truncate: truncate
75
136
  };
package/wrap.js CHANGED
@@ -2,34 +2,15 @@
2
2
 
3
3
  var Util = require('./util');
4
4
  var stringWidth = require('./width').stringWidth;
5
- var ansiPattern = Util.ansiPattern;
6
- var graphemeSegmenter = Util.graphemeSegmenter;
7
5
 
8
6
  function getWrapTokens(text) {
9
7
  // Split text into alternating words and breakable whitespace, while preserving
10
8
  // ANSI sequences as zero-width units in their original positions.
11
- var units = [];
12
- var pattern = new RegExp(ansiPattern.source, 'g');
13
- var offset = 0;
14
- var match = null;
15
-
16
- var addText = function(value) {
17
- for (var item of graphemeSegmenter.segment(value)) {
18
- units.push({
19
- text: item.segment,
20
- width: stringWidth(item.segment),
21
- breakable: !!item.segment.match(/^(?:\s|\u200B)+$/u),
22
- ansi: false
23
- });
24
- }
25
- };
26
-
27
- while ((match = pattern.exec(text))) {
28
- if (match.index > offset) addText( text.substring(offset, match.index) );
29
- units.push({ text: match[0], width: 0, breakable: false, ansi: true });
30
- offset = pattern.lastIndex;
31
- }
32
- if (offset < text.length) addText( text.substring(offset) );
9
+ var units = Util.splitAnsiGraphemes(text);
10
+ units.forEach( function(unit) {
11
+ unit.width = unit.ansi ? 0 : stringWidth(unit.text);
12
+ unit.breakable = !unit.ansi && !!unit.text.match(/^(?:\s|\u200B)+$/u);
13
+ } );
33
14
 
34
15
  var tokens = [];
35
16
  var token = null;
@@ -1,12 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- indent_style = tab
5
- end_of_line = lf
6
- charset = utf-8
7
- trim_trailing_whitespace = true
8
- insert_final_newline = true
9
-
10
- [*.yml]
11
- indent_style = space
12
- indent_size = 2
@@ -1 +0,0 @@
1
- * text=auto eol=lf
@@ -1,3 +0,0 @@
1
- # Security Policy
2
-
3
- To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
@@ -1,22 +0,0 @@
1
- name: CI
2
- on:
3
- - push
4
- - pull_request
5
- jobs:
6
- test:
7
- name: Node.js ${{ matrix.node-version }}
8
- runs-on: ubuntu-latest
9
- strategy:
10
- fail-fast: false
11
- matrix:
12
- node-version:
13
- - 24
14
- - 22
15
- - 20
16
- steps:
17
- - uses: actions/checkout@v4
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: ${{ matrix.node-version }}
21
- - run: npm install
22
- - run: npm test
@@ -1,39 +0,0 @@
1
- export type Options = {
2
- /**
3
- Count [ambiguous width characters](https://www.unicode.org/reports/tr11/#Ambiguous) as having narrow width (count of 1) instead of wide width (count of 2).
4
-
5
- @default true
6
-
7
- > Ambiguous characters behave like wide or narrow characters depending on the context (language tag, script identification, associated font, source of data, or explicit markup; all can provide the context). __If the context cannot be established reliably, they should be treated as narrow characters by default.__
8
- > - http://www.unicode.org/reports/tr11/
9
- */
10
- readonly ambiguousIsNarrow?: boolean;
11
-
12
- /**
13
- Whether [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) should be counted.
14
-
15
- @default false
16
- */
17
- readonly countAnsiEscapeCodes?: boolean;
18
- };
19
-
20
- /**
21
- Get the visual width of a string - the number of columns required to display it.
22
-
23
- Some Unicode characters are [fullwidth](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms) and use double the normal width. [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) are stripped and doesn't affect the width.
24
-
25
- @example
26
- ```
27
- import stringWidth from 'string-width';
28
-
29
- stringWidth('a');
30
- //=> 1
31
-
32
- stringWidth('古');
33
- //=> 2
34
-
35
- stringWidth('\u001B[1m古\u001B[22m');
36
- //=> 2
37
- ```
38
- */
39
- export default function stringWidth(string: string, options?: Options): number;