Setting the file. One moment. Help · Wix Replatform · wix/skills · Skills Docs(opens in a new tab)
vendor/commander/lib/help.js
JavaScript·731 lines·20 KB
10
*/
11
12// Although this is a class, methods are static in style to allow override using subclass or just functions.
13export class Help {
14 constructor() {
15 this.helpWidth = undefined;
16 this.minWidthToWrap = 40;
17 this.sortSubcommands = false;
18 this.sortOptions = false;
19 this.showGlobalOptions = false;
20 }
21
22 /**
23 * prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
24 * and just before calling `formatHelp()`.
25 *
26 * Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
27 *
28 * @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
29 */
30 prepareContext(contextOptions) {
31 this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
32 }
33
34 /**
35 * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
36 *
37 * @param {Command} cmd
38 * @returns {Command[]}
39 */
40
41 visibleCommands(cmd) {
42 const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);
43 const helpCommand = cmd._getHelpCommand();
44 if (helpCommand && !helpCommand._hidden) {
45 visibleCommands.push(helpCommand);
46 }
47 if (this.sortSubcommands) {
48 visibleCommands.sort((a, b) => {
49 // @ts-ignore: because overloaded return type
50 return a.name().localeCompare(b.name());
51 });
52 }
53 return visibleCommands;
54 }
55
56 /**
57 * Compare options for sort.
58 *
59 * @param {Option} a
60 * @param {Option} b
61 * @returns {number}
62 */
63 compareOptions(a, b) {
64 const getSortKey = (option) => {
65 // WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
66 return option.short
67 ? option.short.replace(/^-/, '')
68 : option.long.replace(/^--/, '');
69 };
70 return getSortKey(a).localeCompare(getSortKey(b));
71 }
72
73 /**
74 * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
75 *
76 * @param {Command} cmd
77 * @returns {Option[]}
78 */
79
80 visibleOptions(cmd) {
81 const visibleOptions = cmd.options.filter((option) => !option.hidden);
82 // Built-in help option.
83 const helpOption = cmd._getHelpOption();
84 if (helpOption && !helpOption.hidden) {
85 // Automatically hide conflicting flags. Bit dubious but a historical behaviour that is convenient for single-command programs.
86 const removeShort = helpOption.short && cmd._findOption(helpOption.short);
87 const removeLong = helpOption.long && cmd._findOption(helpOption.long);
88 if (!removeShort && !removeLong) {
89 visibleOptions.push(helpOption); // no changes needed
90 } else if (helpOption.long && !removeLong) {
91 visibleOptions.push(
92 cmd.createOption(helpOption.long, helpOption.description),
93 );
94 } else if (helpOption.short && !removeShort) {
95 visibleOptions.push(
96 cmd.createOption(helpOption.short, helpOption.description),
97 );
98 }
99 }
100 if (this.sortOptions) {
101 visibleOptions.sort(this.compareOptions);
102 }
103 return visibleOptions;
104 }
105
106 /**
107 * Get an array of the visible global options. (Not including help.)
108 *
109 * @param {Command} cmd
110 * @returns {Option[]}
111 */
112
113 visibleGlobalOptions(cmd) {
114 if (!this.showGlobalOptions) return [];
115
116 const globalOptions = [];
117 for (
118 let ancestorCmd = cmd.parent;
119 ancestorCmd;
120 ancestorCmd = ancestorCmd.parent
121 ) {
122 const visibleOptions = ancestorCmd.options.filter(
123 (option) => !option.hidden,
124 );
125 globalOptions.push(...visibleOptions);
126 }
127 if (this.sortOptions) {
128 globalOptions.sort(this.compareOptions);
129 }
130 return globalOptions;
131 }
132
133 /**
134 * Get an array of the arguments if any have a description.
135 *
136 * @param {Command} cmd
137 * @returns {Argument[]}
138 */
139
140 visibleArguments(cmd) {
141 // Side effect! Apply the legacy descriptions before the arguments are displayed.
142 if (cmd._argsDescription) {
143 cmd.registeredArguments.forEach((argument) => {
144 argument.description =
145 argument.description || cmd._argsDescription[argument.name()] || '';
146 });
147 }
148
149 // If there are any arguments with a description then return all the arguments.
150 if (cmd.registeredArguments.find((argument) => argument.description)) {
151 return cmd.registeredArguments;
152 }
153 return [];
154 }
155
156 /**
157 * Get the command term to show in the list of subcommands.
158 *
159 * @param {Command} cmd
160 * @returns {string}
161 */
162
163 subcommandTerm(cmd) {
164 // Legacy. Ignores custom usage string, and nested commands.
165 const args = cmd.registeredArguments
166 .map((arg) => humanReadableArgName(arg))
167 .join(' ');
168 return (
169 cmd._name +
170 (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
171 (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
172 (args ? ' ' + args : '')
173 );
174 }
175
176 /**
177 * Get the option term to show in the list of options.
178 *
179 * @param {Option} option
180 * @returns {string}
181 */
182
183 optionTerm(option) {
184 return option.flags;
185 }
186
187 /**
188 * Get the argument term to show in the list of arguments.
189 *
190 * @param {Argument} argument
191 * @returns {string}
192 */
193
194 argumentTerm(argument) {
195 return argument.name();
196 }
197
198 /**
199 * Get the longest command term length.
200 *
201 * @param {Command} cmd
202 * @param {Help} helper
203 * @returns {number}
204 */
205
206 longestSubcommandTermLength(cmd, helper) {
207 return helper.visibleCommands(cmd).reduce((max, command) => {
208 return Math.max(
209 max,
210 this.displayWidth(
211 helper.styleSubcommandTerm(helper.subcommandTerm(command)),
212 ),
213 );
214 }, 0);
215 }
216
217 /**
218 * Get the longest option term length.
219 *
220 * @param {Command} cmd
221 * @param {Help} helper
222 * @returns {number}
223 */
224
225 longestOptionTermLength(cmd, helper) {
226 return helper.visibleOptions(cmd).reduce((max, option) => {
227 return Math.max(
228 max,
229 this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
230 );
231 }, 0);
232 }
233
234 /**
235 * Get the longest global option term length.
236 *
237 * @param {Command} cmd
238 * @param {Help} helper
239 * @returns {number}
240 */
241
242 longestGlobalOptionTermLength(cmd, helper) {
243 return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
244 return Math.max(
245 max,
246 this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
247 );
248 }, 0);
249 }
250
251 /**
252 * Get the longest argument term length.
253 *
254 * @param {Command} cmd
255 * @param {Help} helper
256 * @returns {number}
257 */
258
259 longestArgumentTermLength(cmd, helper) {
260 return helper.visibleArguments(cmd).reduce((max, argument) => {
261 return Math.max(
262 max,
263 this.displayWidth(
264 helper.styleArgumentTerm(helper.argumentTerm(argument)),
265 ),
266 );
267 }, 0);
268 }
269
270 /**
271 * Get the command usage to be displayed at the top of the built-in help.
272 *
273 * @param {Command} cmd
274 * @returns {string}
275 */
276
277 commandUsage(cmd) {
278 // Usage
279 let cmdName = cmd._name;
280 if (cmd._aliases[0]) {
281 cmdName = cmdName + '|' + cmd._aliases[0];
282 }
283 let ancestorCmdNames = '';
284 for (
285 let ancestorCmd = cmd.parent;
286 ancestorCmd;
287 ancestorCmd = ancestorCmd.parent
288 ) {
289 ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;
290 }
291 return ancestorCmdNames + cmdName + ' ' + cmd.usage();
292 }
293
294 /**
295 * Get the description for the command.
296 *
297 * @param {Command} cmd
298 * @returns {string}
299 */
300
301 commandDescription(cmd) {
302 // @ts-ignore: because overloaded return type
303 return cmd.description();
304 }
305
306 /**
307 * Get the subcommand summary to show in the list of subcommands.
308 * (Fallback to description for backwards compatibility.)
309 *
310 * @param {Command} cmd
311 * @returns {string}
312 */
313
314 subcommandDescription(cmd) {
315 // @ts-ignore: because overloaded return type
316 return cmd.summary() || cmd.description();
317 }
318
319 /**
320 * Get the option description to show in the list of options.
321 *
322 * @param {Option} option
323 * @return {string}
324 */
325
326 optionDescription(option) {
327 const extraInfo = [];
328
329 if (option.argChoices) {
330 extraInfo.push(
331 // use stringify to match the display of the default value
332 `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
333 );
334 }
335 if (option.defaultValue !== undefined) {
336 // default for boolean and negated more for programmer than end user,
337 // but show true/false for boolean option as may be for hand-rolled env or config processing.
338 const showDefault =
339 option.required ||
340 option.optional ||
341 (option.isBoolean() && typeof option.defaultValue === 'boolean');
342 if (showDefault) {
343 extraInfo.push(
344 `default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
345 );
346 }
347 }
348 // preset for boolean and negated are more for programmer than end user
349 if (option.presetArg !== undefined && option.optional) {
350 extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`);
351 }
352 if (option.envVar !== undefined) {
353 extraInfo.push(`env: ${option.envVar}`);
354 }
355 if (extraInfo.length > 0) {
356 const extraDescription = `(${extraInfo.join(', ')})`;
357 if (option.description) {
358 return `${option.description} ${extraDescription}`;
359 }
360 return extraDescription;
361 }
362
363 return option.description;
364 }
365
366 /**
367 * Get the argument description to show in the list of arguments.
368 *
369 * @param {Argument} argument
370 * @return {string}
371 */
372
373 argumentDescription(argument) {
374 const extraInfo = [];
375 if (argument.argChoices) {
376 extraInfo.push(
377 // use stringify to match the display of the default value
378 `choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
379 );
380 }
381 if (argument.defaultValue !== undefined) {
382 extraInfo.push(
383 `default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,
384 );
385 }
386 if (extraInfo.length > 0) {
387 const extraDescription = `(${extraInfo.join(', ')})`;
388 if (argument.description) {
389 return `${argument.description} ${extraDescription}`;
390 }
391 return extraDescription;
392 }
393 return argument.description;
394 }
395
396 /**
397 * Format a list of items, given a heading and an array of formatted items.
398 *
399 * @param {string} heading
400 * @param {string[]} items
401 * @param {Help} helper
402 * @returns string[]
403 */
404 formatItemList(heading, items, helper) {
405 if (items.length === 0) return [];
406
407 return [helper.styleTitle(heading), ...items, ''];
408 }
409
410 /**
411 * Group items by their help group heading.
412 *
413 * @param {Command[] | Option[]} unsortedItems
414 * @param {Command[] | Option[]} visibleItems
415 * @param {Function} getGroup
416 * @returns {Map<string, Command[] | Option[]>}
417 */
418 groupItems(unsortedItems, visibleItems, getGroup) {
419 const result = new Map();
420 // Add groups in order of appearance in unsortedItems.
421 unsortedItems.forEach((item) => {
422 const group = getGroup(item);
423 if (!result.has(group)) result.set(group, []);
424 });
425 // Add items in order of appearance in visibleItems.
426 visibleItems.forEach((item) => {
427 const group = getGroup(item);
428 if (!result.has(group)) {
429 result.set(group, []);
430 }
431 result.get(group).push(item);
432 });
433 return result;
434 }
435
436 /**
437 * Generate the built-in help text.
438 *
439 * @param {Command} cmd
440 * @param {Help} helper
441 * @returns {string}
442 */
443
444 formatHelp(cmd, helper) {
445 const termWidth = helper.padWidth(cmd, helper);
446 const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
447
448 function callFormatItem(term, description) {
449 return helper.formatItem(term, termWidth, description, helper);
450 }
451
452 // Usage
453 let output = [
454 `${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
455 '',
456 ];
457
458 // Description
459 const commandDescription = helper.commandDescription(cmd);
460 if (commandDescription.length > 0) {
461 output = output.concat([
462 helper.boxWrap(
463 helper.styleCommandDescription(commandDescription),
464 helpWidth,
465 ),
466 '',
467 ]);
468 }
469
470 // Arguments
471 const argumentList = helper.visibleArguments(cmd).map((argument) => {
472 return callFormatItem(
473 helper.styleArgumentTerm(helper.argumentTerm(argument)),
474 helper.styleArgumentDescription(helper.argumentDescription(argument)),
475 );
476 });
477 output = output.concat(
478 this.formatItemList('Arguments:', argumentList, helper),
479 );
480
481 // Options
482 const optionGroups = this.groupItems(
483 cmd.options,
484 helper.visibleOptions(cmd),
485 (option) => option.helpGroupHeading ?? 'Options:',
486 );
487 optionGroups.forEach((options, group) => {
488 const optionList = options.map((option) => {
489 return callFormatItem(
490 helper.styleOptionTerm(helper.optionTerm(option)),
491 helper.styleOptionDescription(helper.optionDescription(option)),
492 );
493 });
494 output = output.concat(this.formatItemList(group, optionList, helper));
495 });
496
497 if (helper.showGlobalOptions) {
498 const globalOptionList = helper
499 .visibleGlobalOptions(cmd)
500 .map((option) => {
501 return callFormatItem(
502 helper.styleOptionTerm(helper.optionTerm(option)),
503 helper.styleOptionDescription(helper.optionDescription(option)),
504 );
505 });
506 output = output.concat(
507 this.formatItemList('Global Options:', globalOptionList, helper),
508 );
509 }
510
511 // Commands
512 const commandGroups = this.groupItems(
513 cmd.commands,
514 helper.visibleCommands(cmd),
515 (sub) => sub.helpGroup() || 'Commands:',
516 );
517 commandGroups.forEach((commands, group) => {
518 const commandList = commands.map((sub) => {
519 return callFormatItem(
520 helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
521 helper.styleSubcommandDescription(helper.subcommandDescription(sub)),
522 );
523 });
524 output = output.concat(this.formatItemList(group, commandList, helper));
525 });
526
527 return output.join('\n');
528 }
529
530 /**
531 * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
532 *
533 * @param {string} str
534 * @returns {number}
535 */
536 displayWidth(str) {
537 return stripVTControlCharacters(str).length;
538 }
539
540 /**
541 * Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
542 *
543 * @param {string} str
544 * @returns {string}
545 */
546 styleTitle(str) {
547 return str;
548 }
549
550 styleUsage(str) {
551 // Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
552 // command subcommand [options] [command] <foo> [bar]
553 return str
554 .split(' ')
555 .map((word) => {
556 if (word === '[options]') return this.styleOptionText(word);
557 if (word === '[command]') return this.styleSubcommandText(word);
558 if (word[0] === '[' || word[0] === '<')
559 return this.styleArgumentText(word);
560 return this.styleCommandText(word); // Restrict to initial words?
561 })
562 .join(' ');
563 }
564 styleCommandDescription(str) {
565 return this.styleDescriptionText(str);
566 }
567 styleOptionDescription(str) {
568 return this.styleDescriptionText(str);
569 }
570 styleSubcommandDescription(str) {
571 return this.styleDescriptionText(str);
572 }
573 styleArgumentDescription(str) {
574 return this.styleDescriptionText(str);
575 }
576 styleDescriptionText(str) {
577 return str;
578 }
579 styleOptionTerm(str) {
580 return this.styleOptionText(str);
581 }
582 styleSubcommandTerm(str) {
583 // This is very like usage with lots of parts! Assume default string which is formed like:
584 // subcommand [options] <foo> [bar]
585 return str
586 .split(' ')
587 .map((word) => {
588 if (word === '[options]') return this.styleOptionText(word);
589 if (word[0] === '[' || word[0] === '<')
590 return this.styleArgumentText(word);
591 return this.styleSubcommandText(word); // Restrict to initial words?
592 })
593 .join(' ');
594 }
595 styleArgumentTerm(str) {
596 return this.styleArgumentText(str);
597 }
598 styleOptionText(str) {
599 return str;
600 }
601 styleArgumentText(str) {
602 return str;
603 }
604 styleSubcommandText(str) {
605 return str;
606 }
607 styleCommandText(str) {
608 return str;
609 }
610
611 /**
612 * Calculate the pad width from the maximum term length.
613 *
614 * @param {Command} cmd
615 * @param {Help} helper
616 * @returns {number}
617 */
618
619 padWidth(cmd, helper) {
620 return Math.max(
621 helper.longestOptionTermLength(cmd, helper),
622 helper.longestGlobalOptionTermLength(cmd, helper),
623 helper.longestSubcommandTermLength(cmd, helper),
624 helper.longestArgumentTermLength(cmd, helper),
625 );
626 }
627
628 /**
629 * Detect manually wrapped and indented strings by checking for line break followed by whitespace.
630 *
631 * @param {string} str
632 * @returns {boolean}
633 */
634 preformatted(str) {
635 return /\n[^\S\r\n]/.test(str);
636 }
637
638 /**
639 * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
640 *
641 * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
642 * TTT DDD DDDD
643 * DD DDD
644 *
645 * @param {string} term
646 * @param {number} termWidth
647 * @param {string} description
648 * @param {Help} helper
649 * @returns {string}
650 */
651 formatItem(term, termWidth, description, helper) {
652 const itemIndent = 2;
653 const itemIndentStr = ' '.repeat(itemIndent);
654 if (!description) return itemIndentStr + term;
655
656 // Pad the term out to a consistent width, so descriptions are aligned.
657 const paddedTerm = term.padEnd(
658 termWidth + term.length - helper.displayWidth(term),
659 );
660
661 // Format the description.
662 const spacerWidth = 2; // between term and description
663 const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
664 const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
665 let formattedDescription;
666 if (
667 remainingWidth < this.minWidthToWrap ||
668 helper.preformatted(description)
669 ) {
670 formattedDescription = description;
671 } else {
672 const wrappedDescription = helper.boxWrap(description, remainingWidth);
673 formattedDescription = wrappedDescription.replace(
674 /\n/g,
675 '\n' + ' '.repeat(termWidth + spacerWidth),
676 );
677 }
678
679 // Construct and overall indent.
680 return (
681 itemIndentStr +
682 paddedTerm +
683 ' '.repeat(spacerWidth) +
684 formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
685 );
686 }
687
688 /**
689 * Wrap a string at whitespace, preserving existing line breaks.
690 * Wrapping is skipped if the width is less than `minWidthToWrap`.
691 *
692 * @param {string} str
693 * @param {number} width
694 * @returns {string}
695 */
696 boxWrap(str, width) {
697 if (width < this.minWidthToWrap) return str;
698
699 const rawLines = str.split(/\r\n|\n/);
700 // split up text by whitespace
701 const chunkPattern = /[\s]*[^\s]+/g;
702 const wrappedLines = [];
703 rawLines.forEach((line) => {
704 const chunks = line.match(chunkPattern);
705 if (chunks === null) {
706 wrappedLines.push('');
707 return;
708 }
709
710 let sumChunks = [chunks.shift()];
711 let sumWidth = this.displayWidth(sumChunks[0]);
712 chunks.forEach((chunk) => {
713 const visibleWidth = this.displayWidth(chunk);
714 // Accumulate chunks while they fit into width.
715 if (sumWidth + visibleWidth <= width) {
716 sumChunks.push(chunk);
717 sumWidth += visibleWidth;
718 return;
719 }
720 wrappedLines.push(sumChunks.join(''));
721
722 const nextChunk = chunk.trimStart(); // trim space at line break
723 sumChunks = [nextChunk];
724 sumWidth = this.displayWidth(nextChunk);
725 });
726 wrappedLines.push(sumChunks.join(''));
727 });
728
729 return wrappedLines.join('\n');
730 }
731}