commander 12.0.0 → 13.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/error.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * CommanderError class
3
- * @class
4
3
  */
5
4
  class CommanderError extends Error {
6
5
  /**
@@ -8,7 +7,6 @@ class CommanderError extends Error {
8
7
  * @param {number} exitCode suggested exit code which could be used with process.exit
9
8
  * @param {string} code an id string representing the error
10
9
  * @param {string} message human-readable description of the error
11
- * @constructor
12
10
  */
13
11
  constructor(exitCode, code, message) {
14
12
  super(message);
@@ -23,13 +21,11 @@ class CommanderError extends Error {
23
21
 
24
22
  /**
25
23
  * InvalidArgumentError class
26
- * @class
27
24
  */
28
25
  class InvalidArgumentError extends CommanderError {
29
26
  /**
30
27
  * Constructs the InvalidArgumentError class
31
28
  * @param {string} [message] explanation of why argument is invalid
32
- * @constructor
33
29
  */
34
30
  constructor(message) {
35
31
  super(1, 'commander.invalidArgument', message);
package/lib/help.js CHANGED
@@ -12,11 +12,24 @@ const { humanReadableArgName } = require('./argument.js');
12
12
  class Help {
13
13
  constructor() {
14
14
  this.helpWidth = undefined;
15
+ this.minWidthToWrap = 40;
15
16
  this.sortSubcommands = false;
16
17
  this.sortOptions = false;
17
18
  this.showGlobalOptions = false;
18
19
  }
19
20
 
21
+ /**
22
+ * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
23
+ * and just before calling `formatHelp()`.
24
+ *
25
+ * Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
26
+ *
27
+ * @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
28
+ */
29
+ prepareContext(contextOptions) {
30
+ this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
31
+ }
32
+
20
33
  /**
21
34
  * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
22
35
  *
@@ -25,14 +38,14 @@ class Help {
25
38
  */
26
39
 
27
40
  visibleCommands(cmd) {
28
- const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
41
+ const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);
29
42
  const helpCommand = cmd._getHelpCommand();
30
43
  if (helpCommand && !helpCommand._hidden) {
31
44
  visibleCommands.push(helpCommand);
32
45
  }
33
46
  if (this.sortSubcommands) {
34
47
  visibleCommands.sort((a, b) => {
35
- // @ts-ignore: overloaded return type
48
+ // @ts-ignore: because overloaded return type
36
49
  return a.name().localeCompare(b.name());
37
50
  });
38
51
  }
@@ -44,12 +57,14 @@ class Help {
44
57
  *
45
58
  * @param {Option} a
46
59
  * @param {Option} b
47
- * @returns number
60
+ * @returns {number}
48
61
  */
49
62
  compareOptions(a, b) {
50
63
  const getSortKey = (option) => {
51
64
  // WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
52
- return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
65
+ return option.short
66
+ ? option.short.replace(/^-/, '')
67
+ : option.long.replace(/^--/, '');
53
68
  };
54
69
  return getSortKey(a).localeCompare(getSortKey(b));
55
70
  }
@@ -72,9 +87,13 @@ class Help {
72
87
  if (!removeShort && !removeLong) {
73
88
  visibleOptions.push(helpOption); // no changes needed
74
89
  } else if (helpOption.long && !removeLong) {
75
- visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description));
90
+ visibleOptions.push(
91
+ cmd.createOption(helpOption.long, helpOption.description),
92
+ );
76
93
  } else if (helpOption.short && !removeShort) {
77
- visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description));
94
+ visibleOptions.push(
95
+ cmd.createOption(helpOption.short, helpOption.description),
96
+ );
78
97
  }
79
98
  }
80
99
  if (this.sortOptions) {
@@ -94,8 +113,14 @@ class Help {
94
113
  if (!this.showGlobalOptions) return [];
95
114
 
96
115
  const globalOptions = [];
97
- for (let ancestorCmd = cmd.parent; ancestorCmd; ancestorCmd = ancestorCmd.parent) {
98
- const visibleOptions = ancestorCmd.options.filter((option) => !option.hidden);
116
+ for (
117
+ let ancestorCmd = cmd.parent;
118
+ ancestorCmd;
119
+ ancestorCmd = ancestorCmd.parent
120
+ ) {
121
+ const visibleOptions = ancestorCmd.options.filter(
122
+ (option) => !option.hidden,
123
+ );
99
124
  globalOptions.push(...visibleOptions);
100
125
  }
101
126
  if (this.sortOptions) {
@@ -114,13 +139,14 @@ class Help {
114
139
  visibleArguments(cmd) {
115
140
  // Side effect! Apply the legacy descriptions before the arguments are displayed.
116
141
  if (cmd._argsDescription) {
117
- cmd.registeredArguments.forEach(argument => {
118
- argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
142
+ cmd.registeredArguments.forEach((argument) => {
143
+ argument.description =
144
+ argument.description || cmd._argsDescription[argument.name()] || '';
119
145
  });
120
146
  }
121
147
 
122
148
  // If there are any arguments with a description then return all the arguments.
123
- if (cmd.registeredArguments.find(argument => argument.description)) {
149
+ if (cmd.registeredArguments.find((argument) => argument.description)) {
124
150
  return cmd.registeredArguments;
125
151
  }
126
152
  return [];
@@ -135,11 +161,15 @@ class Help {
135
161
 
136
162
  subcommandTerm(cmd) {
137
163
  // Legacy. Ignores custom usage string, and nested commands.
138
- const args = cmd.registeredArguments.map(arg => humanReadableArgName(arg)).join(' ');
139
- return cmd._name +
164
+ const args = cmd.registeredArguments
165
+ .map((arg) => humanReadableArgName(arg))
166
+ .join(' ');
167
+ return (
168
+ cmd._name +
140
169
  (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
141
170
  (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
142
- (args ? ' ' + args : '');
171
+ (args ? ' ' + args : '')
172
+ );
143
173
  }
144
174
 
145
175
  /**
@@ -174,7 +204,12 @@ class Help {
174
204
 
175
205
  longestSubcommandTermLength(cmd, helper) {
176
206
  return helper.visibleCommands(cmd).reduce((max, command) => {
177
- return Math.max(max, helper.subcommandTerm(command).length);
207
+ return Math.max(
208
+ max,
209
+ this.displayWidth(
210
+ helper.styleSubcommandTerm(helper.subcommandTerm(command)),
211
+ ),
212
+ );
178
213
  }, 0);
179
214
  }
180
215
 
@@ -188,7 +223,10 @@ class Help {
188
223
 
189
224
  longestOptionTermLength(cmd, helper) {
190
225
  return helper.visibleOptions(cmd).reduce((max, option) => {
191
- return Math.max(max, helper.optionTerm(option).length);
226
+ return Math.max(
227
+ max,
228
+ this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
229
+ );
192
230
  }, 0);
193
231
  }
194
232
 
@@ -202,7 +240,10 @@ class Help {
202
240
 
203
241
  longestGlobalOptionTermLength(cmd, helper) {
204
242
  return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
205
- return Math.max(max, helper.optionTerm(option).length);
243
+ return Math.max(
244
+ max,
245
+ this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
246
+ );
206
247
  }, 0);
207
248
  }
208
249
 
@@ -216,7 +257,12 @@ class Help {
216
257
 
217
258
  longestArgumentTermLength(cmd, helper) {
218
259
  return helper.visibleArguments(cmd).reduce((max, argument) => {
219
- return Math.max(max, helper.argumentTerm(argument).length);
260
+ return Math.max(
261
+ max,
262
+ this.displayWidth(
263
+ helper.styleArgumentTerm(helper.argumentTerm(argument)),
264
+ ),
265
+ );
220
266
  }, 0);
221
267
  }
222
268
 
@@ -234,7 +280,11 @@ class Help {
234
280
  cmdName = cmdName + '|' + cmd._aliases[0];
235
281
  }
236
282
  let ancestorCmdNames = '';
237
- for (let ancestorCmd = cmd.parent; ancestorCmd; ancestorCmd = ancestorCmd.parent) {
283
+ for (
284
+ let ancestorCmd = cmd.parent;
285
+ ancestorCmd;
286
+ ancestorCmd = ancestorCmd.parent
287
+ ) {
238
288
  ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;
239
289
  }
240
290
  return ancestorCmdNames + cmdName + ' ' + cmd.usage();
@@ -248,7 +298,7 @@ class Help {
248
298
  */
249
299
 
250
300
  commandDescription(cmd) {
251
- // @ts-ignore: overloaded return type
301
+ // @ts-ignore: because overloaded return type
252
302
  return cmd.description();
253
303
  }
254
304
 
@@ -261,7 +311,7 @@ class Help {
261
311
  */
262
312
 
263
313
  subcommandDescription(cmd) {
264
- // @ts-ignore: overloaded return type
314
+ // @ts-ignore: because overloaded return type
265
315
  return cmd.summary() || cmd.description();
266
316
  }
267
317
 
@@ -278,15 +328,20 @@ class Help {
278
328
  if (option.argChoices) {
279
329
  extraInfo.push(
280
330
  // use stringify to match the display of the default value
281
- `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
331
+ `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
332
+ );
282
333
  }
283
334
  if (option.defaultValue !== undefined) {
284
335
  // default for boolean and negated more for programmer than end user,
285
336
  // but show true/false for boolean option as may be for hand-rolled env or config processing.
286
- const showDefault = option.required || option.optional ||
337
+ const showDefault =
338
+ option.required ||
339
+ option.optional ||
287
340
  (option.isBoolean() && typeof option.defaultValue === 'boolean');
288
341
  if (showDefault) {
289
- extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
342
+ extraInfo.push(
343
+ `default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
344
+ );
290
345
  }
291
346
  }
292
347
  // preset for boolean and negated are more for programmer than end user
@@ -315,17 +370,20 @@ class Help {
315
370
  if (argument.argChoices) {
316
371
  extraInfo.push(
317
372
  // use stringify to match the display of the default value
318
- `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
373
+ `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
374
+ );
319
375
  }
320
376
  if (argument.defaultValue !== undefined) {
321
- extraInfo.push(`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`);
377
+ extraInfo.push(
378
+ `default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,
379
+ );
322
380
  }
323
381
  if (extraInfo.length > 0) {
324
- const extraDescripton = `(${extraInfo.join(', ')})`;
382
+ const extraDescription = `(${extraInfo.join(', ')})`;
325
383
  if (argument.description) {
326
- return `${argument.description} ${extraDescripton}`;
384
+ return `${argument.description} ${extraDescription}`;
327
385
  }
328
- return extraDescripton;
386
+ return extraDescription;
329
387
  }
330
388
  return argument.description;
331
389
  }
@@ -340,65 +398,177 @@ class Help {
340
398
 
341
399
  formatHelp(cmd, helper) {
342
400
  const termWidth = helper.padWidth(cmd, helper);
343
- const helpWidth = helper.helpWidth || 80;
344
- const itemIndentWidth = 2;
345
- const itemSeparatorWidth = 2; // between term and description
346
- function formatItem(term, description) {
347
- if (description) {
348
- const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
349
- return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
350
- }
351
- return term;
352
- }
353
- function formatList(textArray) {
354
- return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
401
+ const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
402
+
403
+ function callFormatItem(term, description) {
404
+ return helper.formatItem(term, termWidth, description, helper);
355
405
  }
356
406
 
357
407
  // Usage
358
- let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
408
+ let output = [
409
+ `${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
410
+ '',
411
+ ];
359
412
 
360
413
  // Description
361
414
  const commandDescription = helper.commandDescription(cmd);
362
415
  if (commandDescription.length > 0) {
363
- output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
416
+ output = output.concat([
417
+ helper.boxWrap(
418
+ helper.styleCommandDescription(commandDescription),
419
+ helpWidth,
420
+ ),
421
+ '',
422
+ ]);
364
423
  }
365
424
 
366
425
  // Arguments
367
426
  const argumentList = helper.visibleArguments(cmd).map((argument) => {
368
- return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument));
427
+ return callFormatItem(
428
+ helper.styleArgumentTerm(helper.argumentTerm(argument)),
429
+ helper.styleArgumentDescription(helper.argumentDescription(argument)),
430
+ );
369
431
  });
370
432
  if (argumentList.length > 0) {
371
- output = output.concat(['Arguments:', formatList(argumentList), '']);
433
+ output = output.concat([
434
+ helper.styleTitle('Arguments:'),
435
+ ...argumentList,
436
+ '',
437
+ ]);
372
438
  }
373
439
 
374
440
  // Options
375
441
  const optionList = helper.visibleOptions(cmd).map((option) => {
376
- return formatItem(helper.optionTerm(option), helper.optionDescription(option));
442
+ return callFormatItem(
443
+ helper.styleOptionTerm(helper.optionTerm(option)),
444
+ helper.styleOptionDescription(helper.optionDescription(option)),
445
+ );
377
446
  });
378
447
  if (optionList.length > 0) {
379
- output = output.concat(['Options:', formatList(optionList), '']);
448
+ output = output.concat([
449
+ helper.styleTitle('Options:'),
450
+ ...optionList,
451
+ '',
452
+ ]);
380
453
  }
381
454
 
382
- if (this.showGlobalOptions) {
383
- const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
384
- return formatItem(helper.optionTerm(option), helper.optionDescription(option));
385
- });
455
+ if (helper.showGlobalOptions) {
456
+ const globalOptionList = helper
457
+ .visibleGlobalOptions(cmd)
458
+ .map((option) => {
459
+ return callFormatItem(
460
+ helper.styleOptionTerm(helper.optionTerm(option)),
461
+ helper.styleOptionDescription(helper.optionDescription(option)),
462
+ );
463
+ });
386
464
  if (globalOptionList.length > 0) {
387
- output = output.concat(['Global Options:', formatList(globalOptionList), '']);
465
+ output = output.concat([
466
+ helper.styleTitle('Global Options:'),
467
+ ...globalOptionList,
468
+ '',
469
+ ]);
388
470
  }
389
471
  }
390
472
 
391
473
  // Commands
392
474
  const commandList = helper.visibleCommands(cmd).map((cmd) => {
393
- return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
475
+ return callFormatItem(
476
+ helper.styleSubcommandTerm(helper.subcommandTerm(cmd)),
477
+ helper.styleSubcommandDescription(helper.subcommandDescription(cmd)),
478
+ );
394
479
  });
395
480
  if (commandList.length > 0) {
396
- output = output.concat(['Commands:', formatList(commandList), '']);
481
+ output = output.concat([
482
+ helper.styleTitle('Commands:'),
483
+ ...commandList,
484
+ '',
485
+ ]);
397
486
  }
398
487
 
399
488
  return output.join('\n');
400
489
  }
401
490
 
491
+ /**
492
+ * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
493
+ *
494
+ * @param {string} str
495
+ * @returns {number}
496
+ */
497
+ displayWidth(str) {
498
+ return stripColor(str).length;
499
+ }
500
+
501
+ /**
502
+ * Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
503
+ *
504
+ * @param {string} str
505
+ * @returns {string}
506
+ */
507
+ styleTitle(str) {
508
+ return str;
509
+ }
510
+
511
+ styleUsage(str) {
512
+ // Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
513
+ // command subcommand [options] [command] <foo> [bar]
514
+ return str
515
+ .split(' ')
516
+ .map((word) => {
517
+ if (word === '[options]') return this.styleOptionText(word);
518
+ if (word === '[command]') return this.styleSubcommandText(word);
519
+ if (word[0] === '[' || word[0] === '<')
520
+ return this.styleArgumentText(word);
521
+ return this.styleCommandText(word); // Restrict to initial words?
522
+ })
523
+ .join(' ');
524
+ }
525
+ styleCommandDescription(str) {
526
+ return this.styleDescriptionText(str);
527
+ }
528
+ styleOptionDescription(str) {
529
+ return this.styleDescriptionText(str);
530
+ }
531
+ styleSubcommandDescription(str) {
532
+ return this.styleDescriptionText(str);
533
+ }
534
+ styleArgumentDescription(str) {
535
+ return this.styleDescriptionText(str);
536
+ }
537
+ styleDescriptionText(str) {
538
+ return str;
539
+ }
540
+ styleOptionTerm(str) {
541
+ return this.styleOptionText(str);
542
+ }
543
+ styleSubcommandTerm(str) {
544
+ // This is very like usage with lots of parts! Assume default string which is formed like:
545
+ // subcommand [options] <foo> [bar]
546
+ return str
547
+ .split(' ')
548
+ .map((word) => {
549
+ if (word === '[options]') return this.styleOptionText(word);
550
+ if (word[0] === '[' || word[0] === '<')
551
+ return this.styleArgumentText(word);
552
+ return this.styleSubcommandText(word); // Restrict to initial words?
553
+ })
554
+ .join(' ');
555
+ }
556
+ styleArgumentTerm(str) {
557
+ return this.styleArgumentText(str);
558
+ }
559
+ styleOptionText(str) {
560
+ return str;
561
+ }
562
+ styleArgumentText(str) {
563
+ return str;
564
+ }
565
+ styleSubcommandText(str) {
566
+ return str;
567
+ }
568
+ styleCommandText(str) {
569
+ return str;
570
+ }
571
+
402
572
  /**
403
573
  * Calculate the pad width from the maximum term length.
404
574
  *
@@ -412,46 +582,128 @@ class Help {
412
582
  helper.longestOptionTermLength(cmd, helper),
413
583
  helper.longestGlobalOptionTermLength(cmd, helper),
414
584
  helper.longestSubcommandTermLength(cmd, helper),
415
- helper.longestArgumentTermLength(cmd, helper)
585
+ helper.longestArgumentTermLength(cmd, helper),
416
586
  );
417
587
  }
418
588
 
419
589
  /**
420
- * Wrap the given string to width characters per line, with lines after the first indented.
421
- * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.
590
+ * Detect manually wrapped and indented strings by checking for line break followed by whitespace.
422
591
  *
423
592
  * @param {string} str
424
- * @param {number} width
425
- * @param {number} indent
426
- * @param {number} [minColumnWidth=40]
427
- * @return {string}
593
+ * @returns {boolean}
594
+ */
595
+ preformatted(str) {
596
+ return /\n[^\S\r\n]/.test(str);
597
+ }
598
+
599
+ /**
600
+ * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
601
+ *
602
+ * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
603
+ * TTT DDD DDDD
604
+ * DD DDD
605
+ *
606
+ * @param {string} term
607
+ * @param {number} termWidth
608
+ * @param {string} description
609
+ * @param {Help} helper
610
+ * @returns {string}
611
+ */
612
+ formatItem(term, termWidth, description, helper) {
613
+ const itemIndent = 2;
614
+ const itemIndentStr = ' '.repeat(itemIndent);
615
+ if (!description) return itemIndentStr + term;
616
+
617
+ // Pad the term out to a consistent width, so descriptions are aligned.
618
+ const paddedTerm = term.padEnd(
619
+ termWidth + term.length - helper.displayWidth(term),
620
+ );
621
+
622
+ // Format the description.
623
+ const spacerWidth = 2; // between term and description
624
+ const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
625
+ const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
626
+ let formattedDescription;
627
+ if (
628
+ remainingWidth < this.minWidthToWrap ||
629
+ helper.preformatted(description)
630
+ ) {
631
+ formattedDescription = description;
632
+ } else {
633
+ const wrappedDescription = helper.boxWrap(description, remainingWidth);
634
+ formattedDescription = wrappedDescription.replace(
635
+ /\n/g,
636
+ '\n' + ' '.repeat(termWidth + spacerWidth),
637
+ );
638
+ }
639
+
640
+ // Construct and overall indent.
641
+ return (
642
+ itemIndentStr +
643
+ paddedTerm +
644
+ ' '.repeat(spacerWidth) +
645
+ formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
646
+ );
647
+ }
648
+
649
+ /**
650
+ * Wrap a string at whitespace, preserving existing line breaks.
651
+ * Wrapping is skipped if the width is less than `minWidthToWrap`.
428
652
  *
653
+ * @param {string} str
654
+ * @param {number} width
655
+ * @returns {string}
429
656
  */
657
+ boxWrap(str, width) {
658
+ if (width < this.minWidthToWrap) return str;
659
+
660
+ const rawLines = str.split(/\r\n|\n/);
661
+ // split up text by whitespace
662
+ const chunkPattern = /[\s]*[^\s]+/g;
663
+ const wrappedLines = [];
664
+ rawLines.forEach((line) => {
665
+ const chunks = line.match(chunkPattern);
666
+ if (chunks === null) {
667
+ wrappedLines.push('');
668
+ return;
669
+ }
430
670
 
431
- wrap(str, width, indent, minColumnWidth = 40) {
432
- // Full \s characters, minus the linefeeds.
433
- const indents = ' \\f\\t\\v\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff';
434
- // Detect manually wrapped and indented strings by searching for line break followed by spaces.
435
- const manualIndent = new RegExp(`[\\n][${indents}]+`);
436
- if (str.match(manualIndent)) return str;
437
- // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line).
438
- const columnWidth = width - indent;
439
- if (columnWidth < minColumnWidth) return str;
440
-
441
- const leadingStr = str.slice(0, indent);
442
- const columnText = str.slice(indent).replace('\r\n', '\n');
443
- const indentString = ' '.repeat(indent);
444
- const zeroWidthSpace = '\u200B';
445
- const breaks = `\\s${zeroWidthSpace}`;
446
- // Match line end (so empty lines don't collapse),
447
- // or as much text as will fit in column, or excess text up to first break.
448
- const regex = new RegExp(`\n|.{1,${columnWidth - 1}}([${breaks}]|$)|[^${breaks}]+?([${breaks}]|$)`, 'g');
449
- const lines = columnText.match(regex) || [];
450
- return leadingStr + lines.map((line, i) => {
451
- if (line === '\n') return ''; // preserve empty lines
452
- return ((i > 0) ? indentString : '') + line.trimEnd();
453
- }).join('\n');
671
+ let sumChunks = [chunks.shift()];
672
+ let sumWidth = this.displayWidth(sumChunks[0]);
673
+ chunks.forEach((chunk) => {
674
+ const visibleWidth = this.displayWidth(chunk);
675
+ // Accumulate chunks while they fit into width.
676
+ if (sumWidth + visibleWidth <= width) {
677
+ sumChunks.push(chunk);
678
+ sumWidth += visibleWidth;
679
+ return;
680
+ }
681
+ wrappedLines.push(sumChunks.join(''));
682
+
683
+ const nextChunk = chunk.trimStart(); // trim space at line break
684
+ sumChunks = [nextChunk];
685
+ sumWidth = this.displayWidth(nextChunk);
686
+ });
687
+ wrappedLines.push(sumChunks.join(''));
688
+ });
689
+
690
+ return wrappedLines.join('\n');
454
691
  }
455
692
  }
456
693
 
694
+ /**
695
+ * Strip style ANSI escape sequences from the string. In particular, SGR (Select Graphic Rendition) codes.
696
+ *
697
+ * @param {string} str
698
+ * @returns {string}
699
+ * @package
700
+ */
701
+
702
+ function stripColor(str) {
703
+ // eslint-disable-next-line no-control-regex
704
+ const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
705
+ return str.replace(sgrPattern, '');
706
+ }
707
+
457
708
  exports.Help = Help;
709
+ exports.stripColor = stripColor;